harnery 0.0.1 → 0.2.0

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 +494 -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 +32 -0
  316. package/dist/lib/readability/client.d.ts.map +1 -0
  317. package/dist/lib/readability/client.js +119 -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 +564 -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 +156 -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,1247 @@
1
+ /**
2
+ * `agent-hook` CLI entry point. Phase 2: real canonical-event emission
3
+ * alongside the legacy stream.
4
+ *
5
+ * Flow:
6
+ * 1. Parse argv → event-name + harness.
7
+ * 2. Read stdin → harness payload (JSON or empty).
8
+ * 3. Find coord root (walk up for .harnery/).
9
+ * 4. Resolve instance_id (env → payload → pid-map walk).
10
+ * 5. Map event-name → canonical event_type.
11
+ * 6. Build event data from payload + resolvers (intent, transcript scan).
12
+ * 7. Append envelope to .harnery/events.ndjson via emit() under flock.
13
+ * 8. (Still also writes a debug breadcrumb to .harnery/debug/ for visibility.)
14
+ *
15
+ * Phase 2 ship criterion: confirms parser correctness across thousands of
16
+ * real events without affecting behavior. Always exits 0. Failures land in
17
+ * `.harnery/debug/agent-hook.errors.ndjson` for audit but never break the
18
+ * harness flow.
19
+ */
20
+
21
+ import { spawnSync } from "node:child_process";
22
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
23
+ import { dirname, join } from "node:path";
24
+ import { coordEnv } from "../../lib/env.ts";
25
+ import { replayCodexJsonl } from "../agents/codex-replay.ts";
26
+ import { consumeSince, writeCursor } from "../agents/events/consume.ts";
27
+ import { evaluateStopHook } from "../agents/rules/stop-hook.ts";
28
+ import { projectHeartbeats } from "../agents/state/heartbeat-projector.ts";
29
+ import { shellMutationPaths } from "../agents/state/shell-mutation.ts";
30
+ import {
31
+ captureImages,
32
+ detectPresence,
33
+ imageJanitor,
34
+ playSound,
35
+ resetSoundCounters,
36
+ runTurnSummary,
37
+ scratchArchive,
38
+ scratchJanitor,
39
+ scratchRecoveryCue,
40
+ soundForEvent,
41
+ syncClaudeSessions,
42
+ } from "./effects/index.ts";
43
+ import { emit } from "./events/emit.ts";
44
+ import type { Harness } from "./events/schema.ts";
45
+ import { detectHarness } from "./harness/detect.ts";
46
+ import {
47
+ extractBashCommand,
48
+ extractToolDescription,
49
+ type NormalizedEventType,
50
+ normalizeEventName,
51
+ type ParsedPayload,
52
+ parsePayload,
53
+ } from "./harness/parse.ts";
54
+ import { selectAnchorPid } from "./resolve/anchor.ts";
55
+ import { findCoordRoot } from "./resolve/coord-root.ts";
56
+ import { extractIntentComment, resolveIntent } from "./resolve/intent.ts";
57
+ import { resolveOwner } from "./resolve/owner.ts";
58
+ import { scanStatusBoxPresent, scanTranscriptModel } from "./resolve/transcript.ts";
59
+
60
+ interface Argv {
61
+ eventName: string | null;
62
+ extra: string[];
63
+ }
64
+
65
+ function parseArgv(argv: string[]): Argv {
66
+ const out: Argv = { eventName: null, extra: [] };
67
+ for (let i = 0; i < argv.length; i++) {
68
+ const arg = argv[i]!;
69
+ if (arg === "--harness") {
70
+ i++; // detectHarness will re-parse; just consume the value here.
71
+ continue;
72
+ }
73
+ if (arg.startsWith("--harness=")) continue;
74
+ if (!out.eventName && !arg.startsWith("--")) {
75
+ out.eventName = arg;
76
+ } else {
77
+ out.extra.push(arg);
78
+ }
79
+ }
80
+ return out;
81
+ }
82
+
83
+ async function readStdin(): Promise<string> {
84
+ if (process.stdin.isTTY) return "";
85
+ try {
86
+ const chunks: Uint8Array[] = [];
87
+ for await (const chunk of process.stdin) {
88
+ chunks.push(chunk as Uint8Array);
89
+ }
90
+ return Buffer.concat(chunks).toString("utf8");
91
+ } catch {
92
+ return "";
93
+ }
94
+ }
95
+
96
+ function appendDebug(coordRoot: string, entry: Record<string, unknown>): void {
97
+ const path = join(coordRoot, ".harnery", "debug", "agent-hook.ndjson");
98
+ try {
99
+ mkdirSync(dirname(path), { recursive: true });
100
+ appendFileSync(path, `${JSON.stringify(entry)}\n`, "utf8");
101
+ } catch {
102
+ /* swallow */
103
+ }
104
+ }
105
+
106
+ function logError(coordRoot: string | null, err: unknown, context: Record<string, unknown>): void {
107
+ if (!coordRoot) return;
108
+ const path = join(coordRoot, ".harnery", "debug", "agent-hook.errors.ndjson");
109
+ try {
110
+ mkdirSync(dirname(path), { recursive: true });
111
+ appendFileSync(
112
+ path,
113
+ `${JSON.stringify({
114
+ ts: new Date().toISOString(),
115
+ error: err instanceof Error ? `${err.name}: ${err.message}` : String(err),
116
+ stack: err instanceof Error ? err.stack : undefined,
117
+ ...context,
118
+ })}\n`,
119
+ "utf8",
120
+ );
121
+ } catch {
122
+ /* swallow */
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Spawn `agent-coord assign-name <owner> <kind>` to mint or recover the
128
+ * hurricane-style name for this owner. Returns null on any failure so
129
+ * session.start emission never breaks the harness flow.
130
+ *
131
+ * Lives at agent-hooks side (not agent-coord) to keep emitter/consumer
132
+ * separation: we spawn rather than import.
133
+ */
134
+ function assignNameViaAgentCoord(
135
+ coordRoot: string,
136
+ instanceId: string,
137
+ kind: "session" | "subagent" | "transient",
138
+ ): { name: string; kind: string } | null {
139
+ const binary = join(coordRoot, "harnery", "bin", "agent-coord");
140
+ if (!existsSync(binary)) return null;
141
+ try {
142
+ const result = spawnSync(binary, ["assign-name", instanceId, kind], {
143
+ encoding: "utf8",
144
+ timeout: 2000,
145
+ });
146
+ if (result.status !== 0 || !result.stdout) return null;
147
+ const parsed = JSON.parse(result.stdout.trim()) as {
148
+ name?: string;
149
+ kind?: string;
150
+ };
151
+ if (parsed.name && parsed.kind) {
152
+ return { name: parsed.name, kind: parsed.kind };
153
+ }
154
+ return null;
155
+ } catch {
156
+ return null;
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Direct (in-process) pidmap write, avoids the spawn overhead of going via
162
+ * the agent-coord CLI for every session.start / subagent.start. Pid-map
163
+ * rows are essential for `harn agents whoami` ppid resolution.
164
+ */
165
+ function writePidmapViaAgentCoord(
166
+ coordRoot: string,
167
+ pid: number,
168
+ instanceId: string,
169
+ platform: string,
170
+ ): void {
171
+ try {
172
+ // Inline write: same atomic temp+rename pattern as
173
+ // agent-coord/src/state/pidmap.ts but skips importing across module
174
+ // boundaries to keep agent-hooks's deps explicit.
175
+ const dir = join(coordRoot, ".harnery", "pid-map");
176
+ const path = join(dir, String(pid));
177
+ const row = `${instanceId}\t${platform}`;
178
+ if (existsSync(path)) {
179
+ try {
180
+ const current = require("node:fs").readFileSync(path, "utf8");
181
+ if (current === row) return;
182
+ } catch {
183
+ /* fall through */
184
+ }
185
+ }
186
+ mkdirSync(dir, { recursive: true });
187
+ const tmp = `${path}.tmp.${process.pid}`;
188
+ require("node:fs").writeFileSync(tmp, row, "utf8");
189
+ require("node:fs").renameSync(tmp, path);
190
+ } catch {
191
+ /* never break the harness flow */
192
+ }
193
+ }
194
+
195
+ function clampString(s: string, max: number): { value: string; truncated: boolean } {
196
+ if (s.length <= max) return { value: s, truncated: false };
197
+ return { value: s.slice(0, max), truncated: true };
198
+ }
199
+
200
+ function summarizeOutput(value: unknown, headTail = 500): { summary: string; truncated: boolean } {
201
+ const str = typeof value === "string" ? value : JSON.stringify(value ?? "");
202
+ if (str.length <= headTail * 2) return { summary: str, truncated: false };
203
+ return {
204
+ summary: `${str.slice(0, headTail)}\n…[truncated]…\n${str.slice(-headTail)}`,
205
+ truncated: true,
206
+ };
207
+ }
208
+
209
+ interface BuildContext {
210
+ coordRoot: string;
211
+ payload: ParsedPayload | null;
212
+ raw: string;
213
+ harness: Harness;
214
+ instanceId: string;
215
+ }
216
+
217
+ function buildEventData(
218
+ eventType: NormalizedEventType,
219
+ ctx: BuildContext,
220
+ ): Record<string, unknown> {
221
+ const p = ctx.payload;
222
+ switch (eventType) {
223
+ case "session.start": {
224
+ const harnessPlatform =
225
+ ctx.harness === "claude-code"
226
+ ? "claude_code"
227
+ : ctx.harness === "cursor"
228
+ ? "cursor"
229
+ : "codex";
230
+ // Assign (or recover) name + kind via agent-coord. Idempotent: resume
231
+ // returns the original name; new owner consumes a counter slot.
232
+ const assigned = assignNameViaAgentCoord(ctx.coordRoot, ctx.instanceId, "session");
233
+ // Write the harness pid-map row so `harn agents whoami` ppid-walks find
234
+ // this owner. Prefer the payload pid (the actual claude binary), then the
235
+ // anchor walk (the `node` ancestor for Cursor, which has no payload pid),
236
+ // then our own process.ppid. Without the anchor, Cursor anchored on the
237
+ // ephemeral hook bash parent, a PID that dies before the agent's next
238
+ // shell tool call, so the ppid walk found nothing (no_pidmap_entry).
239
+ const harnessPid = p?.pid ?? findHarnessAnchorPid(ctx.harness) ?? process.ppid;
240
+ if (harnessPid) {
241
+ writePidmapViaAgentCoord(ctx.coordRoot, harnessPid, ctx.instanceId, harnessPlatform);
242
+ }
243
+ return {
244
+ started_at: new Date().toISOString(),
245
+ cwd: p?.cwd ?? process.cwd(),
246
+ // Claude Code's SessionStart payload omits `model` (Codex + Cursor
247
+ // supply it). Fall back to the transcript, populated on `resume`, and
248
+ // backfilled later by `turn.stop` for a fresh `startup` session.
249
+ model: p?.model ?? scanTranscriptModel(p?.transcript_path),
250
+ pid: harnessPid,
251
+ source: p?.source,
252
+ platform: harnessPlatform,
253
+ name: assigned?.name,
254
+ kind: "session",
255
+ agent_id: ctx.instanceId,
256
+ };
257
+ }
258
+
259
+ case "session.end":
260
+ return {
261
+ ended_at: new Date().toISOString(),
262
+ clean_exit: p?.clean_exit ?? true,
263
+ };
264
+
265
+ case "user_prompt.submit": {
266
+ const prompt = p?.prompt ?? "";
267
+ const { value, truncated } = clampString(prompt, 4000);
268
+ return { prompt_text: value, ...(truncated ? { truncated: true } : {}) };
269
+ }
270
+
271
+ case "turn.stop": {
272
+ return {
273
+ // Backfill the model for harnesses that omit it at session.start
274
+ // (Claude Code). The transcript is populated with assistant turns by
275
+ // Stop-hook time, so this resolves even for fresh `startup` sessions.
276
+ model: p?.model ?? scanTranscriptModel(p?.transcript_path),
277
+ // Phase 2: tool_call_count + text_length aren't cheaply available
278
+ // from the Stop payload alone (they'd require a transcript scan that
279
+ // races with the JSONL flush). Emit `-1` / `0` sentinels and let
280
+ // Phase 5 (the verdict path) recompute these from the event stream
281
+ // itself rather than re-scanning the transcript.
282
+ tool_call_count: -1,
283
+ text_length: 0,
284
+ // Box present if the transcript scan finds it OR the final assistant
285
+ // message carries the `┌─ agent-` prefix. The latter covers codex's
286
+ // text-only stop (box in last_assistant_message, no transcript), which
287
+ // the verdict now sees because agent-hook emits this turn.stop itself
288
+ // (the previous path passed those via the no-history fail-open).
289
+ status_box_present:
290
+ scanStatusBoxPresent(p?.transcript_path) ||
291
+ ((p?.raw.last_assistant_message as string | undefined) ?? "").includes("┌─ agent-"),
292
+ stop_hook_active: p?.stop_hook_active,
293
+ };
294
+ }
295
+
296
+ case "subagent.start": {
297
+ const subagentCallId =
298
+ (p?.raw.subagent_id as string | undefined) ?? (p?.raw.agent_id as string | undefined);
299
+ // Subagents inherit parent's name via the resolve-name session_id path
300
+ // (agent-coord/state/names.ts → kind=transient). Use the call ID as the
301
+ // instance_id input; assignName falls through to transient.
302
+ const assigned = assignNameViaAgentCoord(ctx.coordRoot, ctx.instanceId, "subagent");
303
+ return {
304
+ agent_type:
305
+ (p?.raw.agent_type as string | undefined) ??
306
+ (p?.raw.subagent_type as string | undefined) ??
307
+ "unknown",
308
+ prompt_summary: p?.raw.prompt_summary as string | undefined,
309
+ name: assigned?.name,
310
+ kind: "subagent",
311
+ agent_id: ctx.instanceId,
312
+ subagent_call_id: subagentCallId,
313
+ parent_session_id: p?.parent_session_id,
314
+ };
315
+ }
316
+
317
+ case "subagent.stop": {
318
+ const status = p?.exit_status;
319
+ const normalized: "ok" | "error" | "interrupted" =
320
+ status === "error" || status === "interrupted" ? status : "ok";
321
+ return { exit_status: normalized, reason: p?.reason };
322
+ }
323
+
324
+ case "tool.pre_use": {
325
+ const toolName = p?.tool_name ?? "unknown";
326
+ const command = extractBashCommand(toolName, p?.tool_input);
327
+ const description = extractToolDescription(p?.tool_input);
328
+ const { intent, source } = resolveIntent({
329
+ coordRoot: ctx.coordRoot,
330
+ instanceId: ctx.instanceId,
331
+ commandIntentComment: extractIntentComment(command),
332
+ description,
333
+ });
334
+ const toolInputStr = JSON.stringify(p?.tool_input ?? null);
335
+ const clamped = clampString(toolInputStr, 8000);
336
+ return {
337
+ tool_name: toolName,
338
+ tool_input: clamped.value,
339
+ intent,
340
+ intent_source: source,
341
+ tool_use_id: p?.tool_use_id,
342
+ ...(clamped.truncated ? { truncated: true } : {}),
343
+ };
344
+ }
345
+
346
+ case "tool.post_use": {
347
+ const toolName = p?.tool_name ?? "unknown";
348
+ const summary = summarizeOutput(p?.tool_response);
349
+ return {
350
+ tool_name: toolName,
351
+ output_summary: summary.summary,
352
+ exit_status: "ok" as const,
353
+ duration_ms: 0, // Phase 3 pairs pre/post via tool_use_id
354
+ tool_use_id: p?.tool_use_id,
355
+ ...(summary.truncated ? { truncated: true } : {}),
356
+ };
357
+ }
358
+
359
+ case "tool.post_use_failure": {
360
+ const toolName = p?.tool_name ?? "unknown";
361
+ const summary = summarizeOutput(p?.tool_response);
362
+ return {
363
+ tool_name: toolName,
364
+ error: summary.summary,
365
+ duration_ms: 0,
366
+ tool_use_id: p?.tool_use_id,
367
+ ...(summary.truncated ? { truncated: true } : {}),
368
+ };
369
+ }
370
+ }
371
+ }
372
+
373
+ async function main(): Promise<number> {
374
+ const { eventName, extra } = parseArgv(process.argv.slice(2));
375
+ const harness = detectHarness(process.argv.slice(2));
376
+ const raw = await readStdin();
377
+
378
+ // Kill-switch-INDEPENDENT effects: notification sounds fire BEFORE the
379
+ // HARNERY_AGENT_COORD_OFF gate so audible feedback survives incident-triage
380
+ // bypass: sound playback happens before the kill-switch bailout.
381
+ // Claude-Code-only; stop-failure → error, sub-agent-start → subagent-start.
382
+ if (harness === "claude-code" && eventName) {
383
+ const s = soundForEvent(eventName);
384
+ if (s) {
385
+ const repoRoot = findCoordRoot(process.cwd());
386
+ if (repoRoot) {
387
+ let sid = "";
388
+ try {
389
+ const j = JSON.parse(raw) as { session_id?: string; conversation_id?: string };
390
+ sid = j.session_id ?? j.conversation_id ?? "";
391
+ } catch {
392
+ // non-JSON payload: play unkeyed (rate-limit just won't dedup)
393
+ }
394
+ playSound(repoRoot, s.sound, sid, s.maxPlays);
395
+ }
396
+ }
397
+ }
398
+
399
+ // Kill switch. Disables every effect of
400
+ // agent-hook + agent-coord: no event emit, no projection, no systemMessage,
401
+ // no G-guard verdict. Used for the cross-client `HARNERY_AGENT_COORD_OFF=1`
402
+ // bypass during incident triage.
403
+ if (coordEnv("AGENT_COORD_OFF") === "1") return 0;
404
+
405
+ const coordRoot = findCoordRoot(process.cwd());
406
+ if (!coordRoot) return 0;
407
+
408
+ // Always log a breadcrumb, useful when an event_type maps to null or owner
409
+ // resolution fails. Stays cheap (one append) and self-prunes via repo log
410
+ // rotation policy.
411
+ const debugBase = {
412
+ ts: new Date().toISOString(),
413
+ event_name: eventName,
414
+ harness,
415
+ extra_argv: extra,
416
+ payload_bytes: raw.length,
417
+ cwd: process.cwd(),
418
+ pid: process.pid,
419
+ ppid: process.ppid,
420
+ };
421
+
422
+ if (!eventName || !harness) {
423
+ appendDebug(coordRoot, { ...debugBase, skipped: "missing-event-or-harness" });
424
+ return 0;
425
+ }
426
+
427
+ const norm = normalizeEventName(eventName);
428
+ if (!norm) {
429
+ appendDebug(coordRoot, { ...debugBase, skipped: "non-canonical-event" });
430
+ return 0;
431
+ }
432
+
433
+ const payload = parsePayload(raw, harness);
434
+ const owner = resolveOwner({ payload: payload?.raw ?? null, coordRoot });
435
+ if (!owner) {
436
+ appendDebug(coordRoot, {
437
+ ...debugBase,
438
+ skipped: "no-owner-resolved",
439
+ event_type: norm.event_type,
440
+ });
441
+ return 0;
442
+ }
443
+
444
+ const sessionId = payload?.session_id ?? payload?.conversation_id ?? owner.instance_id;
445
+
446
+ const data = buildEventData(norm.event_type, {
447
+ coordRoot,
448
+ payload,
449
+ raw,
450
+ harness,
451
+ instanceId: owner.instance_id,
452
+ });
453
+
454
+ const envelope = emit(coordRoot, {
455
+ event_type: norm.event_type,
456
+ instance_id: owner.instance_id,
457
+ session_id: sessionId,
458
+ parent_session_id: payload?.parent_session_id,
459
+ turn_id: payload?.turn_id,
460
+ parent_turn_id: payload?.parent_turn_id,
461
+ harness,
462
+ data,
463
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
464
+ } as Parameters<typeof emit>[1]);
465
+
466
+ appendDebug(coordRoot, {
467
+ ...debugBase,
468
+ event_type: norm.event_type,
469
+ owner_source: owner.source,
470
+ event_id: envelope.event_id,
471
+ });
472
+
473
+ // Phase 8: SessionStart post-emit: project the event so the heartbeat
474
+ // lands synchronously, run stale-sweep, and emit the harness-shaped
475
+ // systemMessage JSON (peer table + wiring check + council invites).
476
+ // Harness-agnostic since v0.5.0; replaces the previous bash UX layer
477
+ // and the equivalent per-harness bash session_start handlers.
478
+ if (norm.event_type === "session.start") {
479
+ // Effect (claude-code): prune stale scratch archives + sweep orphans.
480
+ // The recovery-cue is merged into the
481
+ // session-start additionalContext inside emitSessionStartSystemMessage.
482
+ if (harness === "claude-code") scratchJanitor(coordRoot);
483
+ // Image-feed retention sweep (size + age cap on .harnery/images/). Harness-
484
+ // agnostic, cheap (one readdir), fail-soft. Paired with scratchJanitor as a
485
+ // session-start "tidy the coord layer" step.
486
+ try {
487
+ imageJanitor(coordRoot);
488
+ } catch (err) {
489
+ logError(coordRoot, err, { phase: "session-start-image-janitor" });
490
+ }
491
+ try {
492
+ await emitSessionStartSystemMessage(coordRoot, owner.instance_id, sessionId, data, harness);
493
+ } catch (err) {
494
+ logError(coordRoot, err, { phase: "session-start-systemMessage" });
495
+ }
496
+ }
497
+
498
+ // Phase 8: SessionEnd cleanup: delete heartbeat + pid-map rows. Harness-
499
+ // agnostic since v0.5.0.
500
+ if (norm.event_type === "session.end") {
501
+ try {
502
+ cleanupSessionEnd(coordRoot, owner.instance_id, (data.reason as string) ?? "unknown");
503
+ } catch (err) {
504
+ logError(coordRoot, err, { phase: "session-end-cleanup" });
505
+ }
506
+ // Effects (claude-code): archive the ending agent's scratchpad + force a
507
+ // session-telemetry sync (via HARNERY_CLAUDE_SESSIONS_FORCE=1).
508
+ if (harness === "claude-code") {
509
+ scratchArchive(coordRoot, owner.instance_id);
510
+ syncClaudeSessions(coordRoot, true);
511
+ }
512
+ }
513
+
514
+ // Phase 8: SubagentStart: sync-project to create the subagent heartbeat,
515
+ // log the lifecycle event, and emit a context message announcing the
516
+ // subagent (claude-code + cursor; codex doesn't fan out subagents today).
517
+ if (norm.event_type === "subagent.start") {
518
+ try {
519
+ const agentCoordBin = join(coordRoot, "harnery", "bin", "agent-coord");
520
+ if (existsSync(agentCoordBin)) {
521
+ spawnSync(agentCoordBin, ["project"], { encoding: "utf8", timeout: 3000 });
522
+ spawnSync(
523
+ agentCoordBin,
524
+ [
525
+ "log",
526
+ `SUBAGENT_START agent_type=${(data.agent_type as string) ?? "unknown"} agent_id=${owner.instance_id.slice(0, 8)} platform=${harnessPlatform(harness)}`,
527
+ "--instance",
528
+ owner.instance_id,
529
+ ],
530
+ { encoding: "utf8", timeout: 2000 },
531
+ );
532
+ }
533
+ emitSubagentStartContext(coordRoot, owner.instance_id, sessionId, data, harness);
534
+ } catch (err) {
535
+ logError(coordRoot, err, { phase: "subagent-start-project" });
536
+ }
537
+ }
538
+
539
+ // Phase 8: SubagentStop: delete subagent heartbeat + log.
540
+ if (norm.event_type === "subagent.stop") {
541
+ try {
542
+ cleanupSessionEnd(coordRoot, owner.instance_id, (data.reason as string) ?? "unknown");
543
+ const agentCoordBin = join(coordRoot, "harnery", "bin", "agent-coord");
544
+ if (existsSync(agentCoordBin)) {
545
+ spawnSync(
546
+ agentCoordBin,
547
+ [
548
+ "log",
549
+ `SUBAGENT_STOP agent_id=${owner.instance_id.slice(0, 8)} platform=${harnessPlatform(harness)}`,
550
+ "--instance",
551
+ owner.instance_id,
552
+ ],
553
+ { encoding: "utf8", timeout: 2000 },
554
+ );
555
+ }
556
+ } catch (err) {
557
+ logError(coordRoot, err, { phase: "subagent-stop-cleanup" });
558
+ }
559
+ }
560
+
561
+ // Phase 8: UserPromptSubmit: render dedup'd peer table + council pending
562
+ // and emit the harness-shaped systemMessage JSON. Harness-agnostic since v0.5.0.
563
+ if (norm.event_type === "user_prompt.submit") {
564
+ // Effects (claude-code): reset per-turn sound rate-limit counters + run
565
+ // presence detection on the prompt.
566
+ if (harness === "claude-code") {
567
+ resetSoundCounters(sessionId);
568
+ const prompt = (payload?.raw?.prompt as string | undefined) ?? "";
569
+ if (prompt) detectPresence(prompt);
570
+ }
571
+ try {
572
+ await emitUserPromptSubmitSystemMessage(coordRoot, owner.instance_id, sessionId, harness);
573
+ } catch (err) {
574
+ logError(coordRoot, err, { phase: "user-prompt-submit-systemMessage" });
575
+ }
576
+ }
577
+
578
+ // turn.stop: telemetry + turn-summary effects, then the stop verdict. The
579
+ // verdict + codex-replay previously lived in the per-harness shell adapters;
580
+ // agent-hook owns them now. Runs on the normal "stop" event only;
581
+ // "stop-failure" (API error) gets no gate, matching the previous
582
+ // stop vs stop-failure split.
583
+ if (norm.event_type === "turn.stop" && eventName === "stop") {
584
+ // Codex: replay the JSONL transcript → canonical events so the verdict has
585
+ // the status_checked / task_set / status_box_present evidence (codex
586
+ // doesn't emit those live; this re-emits turn.stop after agent-hook's own,
587
+ // so the verdict reads the replay's box signal as the latest).
588
+ if (harness === "codex" && payload?.transcript_path && existsSync(payload.transcript_path)) {
589
+ try {
590
+ replayCodexJsonl({
591
+ coordRoot,
592
+ jsonlPath: payload.transcript_path,
593
+ sessionId,
594
+ instanceId: owner.instance_id,
595
+ lastAssistantMessage: (payload.raw.last_assistant_message as string | undefined) ?? "",
596
+ });
597
+ } catch (err) {
598
+ logError(coordRoot, err, { phase: "codex-replay" });
599
+ }
600
+ }
601
+
602
+ // CC effects: rate-limited session-telemetry sync + turn-summary Haiku
603
+ // auto-summary.
604
+ if (harness === "claude-code") {
605
+ syncClaudeSessions(coordRoot, false);
606
+ runTurnSummary(coordRoot, owner.instance_id, sessionId, payload?.transcript_path);
607
+ }
608
+
609
+ // Master-state heartbeat projection. Drains events.ndjson since the last
610
+ // cursor → per-owner heartbeats. Was a SECOND binary (`agent-coord project`)
611
+ // pinned to Claude Code Stop only; folded in here so it (a) is one
612
+ // entry per event like everything else and (b) fires on EVERY harness's stop,
613
+ // not just CC. Runs unconditionally before the verdict's possible exit-2 return
614
+ // (the events are real regardless of whether the agent gets nagged), and after
615
+ // codex-replay above so codex's replayed events are included in the drain.
616
+ // Not an emitter (consumes + writes heartbeats), so no emitter/consumer conflict.
617
+ try {
618
+ const result = consumeSince(coordRoot);
619
+ projectHeartbeats(coordRoot, result.events);
620
+ if (result.lastEventId) writeCursor(coordRoot, result.lastEventId);
621
+ } catch (err) {
622
+ logError(coordRoot, err, { phase: "stop-projection" });
623
+ }
624
+
625
+ // Stop verdict (status-box + set-task gate). Direct in-process call: the
626
+ // rule lives in harnery. agent-hook already emitted this turn.stop (with
627
+ // status_box_present) above, so the evidence is in the stream.
628
+ const verdict = evaluateStopHook(coordRoot, {
629
+ rule: "stop-hook",
630
+ instance_id: owner.instance_id,
631
+ session_id: sessionId,
632
+ harness,
633
+ bypass: coordEnv("AGENT_COORD_BYPASS_STOP") === "1",
634
+ });
635
+ if (!verdict.allow) {
636
+ // Harness-aware enforcement channel: Claude Code / Codex honor exit-2 +
637
+ // stderr as a turn block; Cursor ignores exit codes (fail-open) and
638
+ // re-prompts only via a `followup_message` it auto-submits. emitStopBlock
639
+ // writes the right shape and returns the exit code to use.
640
+ const { emitStopBlock } = await import("./harness/output.ts");
641
+ return emitStopBlock(harness, verdict);
642
+ }
643
+ }
644
+
645
+ // Phase 7: PreToolUse: heartbeat + pid-map self-heal on every tool call.
646
+ // Harness-agnostic: both writes have the same shape regardless of who fired.
647
+ // Cursor/Codex bash dispatchers still fire their own G-guard logic, but the
648
+ // heals here keep the agent-coord layer's view of liveness fresh.
649
+ //
650
+ // The heartbeat + pid-map heals are paired by design; they were wired
651
+ // side-by-side in the previous pre-tool-use adapter. The Phase 4-6 refactor
652
+ // preserved the heartbeat half but dropped the pid-map half; the pid-map
653
+ // call was restored here afterward.
654
+ if (norm.event_type === "tool.pre_use") {
655
+ try {
656
+ healHeartbeatViaCli(coordRoot, owner.instance_id, sessionId, harness);
657
+ refreshPidmap(coordRoot, owner.instance_id, harness, payload?.pid);
658
+ } catch (err) {
659
+ logError(coordRoot, err, { phase: "pre-tool-use-heal" });
660
+ }
661
+
662
+ // Image feed: a Read on an image file is the "agent viewed this" signal.
663
+ // Capture the bytes (content-addressed, dedup'd) + emit image.captured.
664
+ try {
665
+ captureImages(coordRoot, {
666
+ eventType: "tool.pre_use",
667
+ data,
668
+ payload,
669
+ instanceId: owner.instance_id,
670
+ sessionId,
671
+ harness,
672
+ });
673
+ } catch (err) {
674
+ logError(coordRoot, err, { phase: "pre-tool-use-image-capture" });
675
+ }
676
+
677
+ // G-guard for ALL harnesses. Claude Code previously ran this via a
678
+ // pre-tool-use bash adapter (which called `agent-coord verdict --rule=claim`);
679
+ // that adapter is now deleted, so agent-hook owns the deny for every harness.
680
+ // emitDeny() inside emits the harness-shaped permission JSON (claude-code +
681
+ // codex use hookSpecificOutput.permissionDecision; cursor uses .permission).
682
+ // apply_patch (codex) parses paths from the patch body and runs verdict
683
+ // per-path; Edit/Write/NotebookEdit resolve a single target. Non-write tools
684
+ // (incl. Agent) yield no targets and pass through with no deny.
685
+ try {
686
+ await runPreToolUseGuard(coordRoot, owner.instance_id, sessionId, data, harness);
687
+ } catch (err) {
688
+ logError(coordRoot, err, { phase: "pre-tool-use-guard" });
689
+ }
690
+
691
+ // Shell-mutation warn (warn-only, never blocks). Was the cursor
692
+ // beforeShellExecution + codex preToolUse-Bash shell-mutation-claim-log in
693
+ // the per-harness shell adapters. Cursor sends the command at payload.command;
694
+ // codex Bash at tool_input.command. Emits a decision.warn per candidate-mutated
695
+ // path so a peer sees the write in events.ndjson. (CC never did this,
696
+ // preserved; it emits with its own hooks-side emitter per the
697
+ // independent-emitter rule.)
698
+ const shellCmd =
699
+ eventName === "before-shell-execution"
700
+ ? ((payload?.raw.command as string | undefined) ?? "")
701
+ : harness === "codex" && data.tool_name === "Bash"
702
+ ? (((payload?.raw.tool_input as Record<string, unknown> | undefined)?.command as
703
+ | string
704
+ | undefined) ?? "")
705
+ : "";
706
+ if (shellCmd) {
707
+ try {
708
+ const paths = shellMutationPaths(shellCmd, coordRoot);
709
+ const truncated = shellCmd.length > 80 ? shellCmd.slice(0, 80) : shellCmd;
710
+ const platform = harnessPlatform(harness);
711
+ for (const p of paths) {
712
+ emit(coordRoot, {
713
+ event_type: "decision.warn",
714
+ instance_id: owner.instance_id,
715
+ session_id: sessionId,
716
+ harness,
717
+ data: {
718
+ rule: "shell_mutation_candidate",
719
+ reason: `path=${p} cmd=${truncated} platform=${platform}`,
720
+ },
721
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
722
+ } as Parameters<typeof emit>[1]);
723
+ }
724
+ } catch (err) {
725
+ logError(coordRoot, err, { phase: "shell-mutation-warn" });
726
+ }
727
+ }
728
+ }
729
+
730
+ // Phase 7: PostToolUse: stamp last_tool + last_tool_target on heartbeat.
731
+ // Harness-agnostic for the same reason as tool.pre_use above.
732
+ if (norm.event_type === "tool.post_use") {
733
+ try {
734
+ stampToolActivity(coordRoot, owner.instance_id, data);
735
+ } catch (err) {
736
+ logError(coordRoot, err, { phase: "post-tool-use-stamp" });
737
+ }
738
+
739
+ // Image feed: a Bash command that wrote an image (harn browse, harn image,
740
+ // --diff, …) is the "agent produced this" signal. Scan the command + its
741
+ // output for freshly-written image paths and capture them.
742
+ try {
743
+ captureImages(coordRoot, {
744
+ eventType: "tool.post_use",
745
+ data,
746
+ payload,
747
+ instanceId: owner.instance_id,
748
+ sessionId,
749
+ harness,
750
+ });
751
+ } catch (err) {
752
+ logError(coordRoot, err, { phase: "post-tool-use-image-capture" });
753
+ }
754
+ }
755
+
756
+ // Phase 7: PostToolUseFailure: release claim on failed Edit (the file
757
+ // never landed; the claim is stale). Harness-agnostic.
758
+ if (norm.event_type === "tool.post_use_failure") {
759
+ try {
760
+ releaseClaimOnFailure(coordRoot, owner.instance_id, data, payload?.raw);
761
+ } catch (err) {
762
+ logError(coordRoot, err, { phase: "post-tool-use-failure-release" });
763
+ }
764
+ }
765
+
766
+ return 0;
767
+ }
768
+
769
+ async function runPreToolUseGuard(
770
+ coordRoot: string,
771
+ instanceId: string,
772
+ sessionId: string,
773
+ data: Record<string, unknown>,
774
+ harness: Harness,
775
+ ): Promise<void> {
776
+ const toolName = (data.tool_name as string | undefined) ?? "";
777
+ const targets = collectGuardTargets(toolName, data).map((p) => canonicalize(coordRoot, p));
778
+ if (targets.length === 0) return;
779
+
780
+ const agentCoordBin = join(coordRoot, "harnery", "bin", "agent-coord");
781
+ if (!existsSync(agentCoordBin)) return;
782
+
783
+ // For apply_patch (multi-file), collect siblings so the deny reason names
784
+ // them. For single-file tools the array has one entry.
785
+ for (const target of targets) {
786
+ const verdictReq = JSON.stringify({
787
+ rule: "claim",
788
+ instance_id: instanceId,
789
+ session_id: sessionId,
790
+ path: target,
791
+ });
792
+ const result = spawnSync(agentCoordBin, ["verdict"], {
793
+ input: verdictReq,
794
+ encoding: "utf8",
795
+ timeout: 3000,
796
+ });
797
+ if (result.status !== 0 || !result.stdout) continue;
798
+ let parsed: { allow?: boolean; reason?: string } = {};
799
+ try {
800
+ parsed = JSON.parse(result.stdout.trim());
801
+ } catch {
802
+ continue;
803
+ }
804
+ if (parsed.allow === false) {
805
+ let reason = parsed.reason ?? `Path ${target} is currently being edited by another agent.`;
806
+ if (targets.length > 1) {
807
+ const siblings = targets
808
+ .filter((p) => p !== target)
809
+ .slice(0, 3)
810
+ .join(", ");
811
+ if (siblings) {
812
+ reason += ` The patch also touched: ${siblings}: pick a different file or wait.`;
813
+ }
814
+ }
815
+ const { emitDeny } = await import("./harness/output.ts");
816
+ emitDeny(harness, reason);
817
+ return;
818
+ }
819
+ }
820
+ }
821
+
822
+ /** Canonicalize a path to monorepo-relative form. Absolute paths under
823
+ * coordRoot get the prefix stripped; relative paths pass through (assumed
824
+ * already canonical). */
825
+ function canonicalize(coordRoot: string, p: string): string {
826
+ if (!p) return p;
827
+ if (p.startsWith(`${coordRoot}/`)) return p.slice(coordRoot.length + 1);
828
+ if (p === coordRoot) return ".";
829
+ return p;
830
+ }
831
+
832
+ /** Pull the candidate path(s) out of a write-tool payload. Empty array when
833
+ * the tool isn't a write or no path could be derived. */
834
+ function collectGuardTargets(toolName: string, data: Record<string, unknown>): string[] {
835
+ const writeTools = new Set(["Edit", "Write", "NotebookEdit", "StrReplace"]);
836
+ if (writeTools.has(toolName)) {
837
+ const target = extractFilePathFromData(data);
838
+ return target ? [target] : [];
839
+ }
840
+ if (toolName === "apply_patch") {
841
+ return parseApplyPatchPaths(data);
842
+ }
843
+ return [];
844
+ }
845
+
846
+ function extractFilePathFromData(data: Record<string, unknown>): string | undefined {
847
+ const raw = data.tool_input;
848
+ if (typeof raw !== "string") return undefined;
849
+ try {
850
+ const parsed = JSON.parse(raw) as Record<string, unknown>;
851
+ return (
852
+ (parsed.file_path as string | undefined) ??
853
+ (parsed.path as string | undefined) ??
854
+ (parsed.notebook_path as string | undefined) ??
855
+ undefined
856
+ );
857
+ } catch {
858
+ return undefined;
859
+ }
860
+ }
861
+
862
+ /** Parse Codex's `apply_patch` body for `*** Add|Update|Delete File: <path>`
863
+ * directives. Extracts apply_patch target paths for Codex. */
864
+ function parseApplyPatchPaths(data: Record<string, unknown>): string[] {
865
+ const raw = data.tool_input;
866
+ if (typeof raw !== "string") return [];
867
+ let body = "";
868
+ try {
869
+ const parsed = JSON.parse(raw) as Record<string, unknown>;
870
+ body = (parsed.command as string | undefined) ?? "";
871
+ } catch {
872
+ return [];
873
+ }
874
+ if (!body) return [];
875
+ const out: string[] = [];
876
+ const re = /^\s*\*\*\* (Add|Update|Delete) File:\s*(.+)$/gm;
877
+ let m: RegExpExecArray | null = re.exec(body);
878
+ while (m !== null) {
879
+ out.push(m[2]!.trim());
880
+ m = re.exec(body);
881
+ }
882
+ return out;
883
+ }
884
+
885
+ function healHeartbeatViaCli(
886
+ coordRoot: string,
887
+ instanceId: string,
888
+ sessionId: string,
889
+ harness: string,
890
+ ): void {
891
+ const agentCoordBin = join(coordRoot, "harnery", "bin", "agent-coord");
892
+ if (!existsSync(agentCoordBin)) return;
893
+ // Pass the detected harness so a pruned Cursor/Codex heartbeat is recreated
894
+ // with the correct platform; without it, healHeartbeat defaults to
895
+ // claude_code and the dashboard mislabels the agent. See
896
+ // heartbeat-writer.healHeartbeat.
897
+ spawnSync(agentCoordBin, ["heal-heartbeat", instanceId, sessionId, `--harness=${harness}`], {
898
+ encoding: "utf8",
899
+ timeout: 2000,
900
+ });
901
+ }
902
+
903
+ /**
904
+ * Walk up the ppid chain on Linux/WSL looking for the harness anchor PID,
905
+ * the PID of the claude / cursor / codex binary. Finds the agent PID. Used by
906
+ * `tool.pre_use`'s pid-map self-heal so a re-parented harness binary (the
907
+ * VS Code 2.1.x sibling-claude spawn case) gets its pid-map row rewritten on
908
+ * the next tool call rather than going invisible until SessionStart fires
909
+ * again, which it may never do.
910
+ *
911
+ * Returns undefined on macOS (no /proc) or when no anchor is found; callers
912
+ * fall back to `process.ppid` (the bash wrapper's parent, which is usually
913
+ * the harness binary itself). `HARNERY_AGENT_COORD_TEST_ANCHOR_PID` overrides
914
+ * everything so the test sandbox can pin a deterministic PID.
915
+ */
916
+ function findHarnessAnchorPid(harness?: Harness): number | undefined {
917
+ const override = coordEnv("AGENT_COORD_TEST_ANCHOR_PID");
918
+ if (override) {
919
+ const n = Number(override);
920
+ if (Number.isFinite(n) && n > 0) return n;
921
+ }
922
+ // Build the ppid chain (nearest → root, up to 20 hops) from /proc, then hand
923
+ // it to the pure selector. Splitting the /proc walk (untestable off a live
924
+ // box) from the comm-matching (unit-tested against the real Phase 0 chains in
925
+ // resolve/anchor.ts) keeps the cursor `node`-fallback logic verifiable.
926
+ const chain: Array<{ pid: number; comm: string }> = [];
927
+ let pid = process.pid;
928
+ for (let hops = 0; hops < 20; hops++) {
929
+ let comm: string;
930
+ let status: string;
931
+ try {
932
+ comm = readFileSync(`/proc/${pid}/comm`, "utf8").trim();
933
+ status = readFileSync(`/proc/${pid}/status`, "utf8");
934
+ } catch {
935
+ break;
936
+ }
937
+ chain.push({ pid, comm });
938
+ const m = status.match(/^PPid:\s+(\d+)/m);
939
+ if (!m) break;
940
+ const ppid = Number(m[1]);
941
+ if (!Number.isFinite(ppid) || ppid === 0 || ppid === 1) break;
942
+ pid = ppid;
943
+ }
944
+ return selectAnchorPid(chain, harness);
945
+ }
946
+
947
+ /**
948
+ * Pid-map self-heal for `tool.pre_use`. Symmetric counterpart to
949
+ * `healHeartbeatViaCli`; the two were paired before the Phase 6 refactor split
950
+ * them apart, then restored together afterward.
951
+ *
952
+ * The pid argument prefers the payload's `pid` (CC populates it on
953
+ * SessionStart and may also send it on PreToolUse), then
954
+ * `findHarnessAnchorPid`, then `process.ppid`. Writes go through the same
955
+ * idempotent `writePidmapViaAgentCoord` helper that SessionStart uses: no
956
+ * disk I/O on no-op heals (when the row already points at us).
957
+ *
958
+ * Follow-up: emit `PIDMAP_HEAL` telemetry on actual writes to keep
959
+ * `harn agents heal-events` pidmap counts meaningful. The inline helper does
960
+ * not yet.
961
+ */
962
+ function refreshPidmap(
963
+ coordRoot: string,
964
+ instanceId: string,
965
+ harness: Harness,
966
+ payloadPid?: number,
967
+ ): void {
968
+ const pid = payloadPid ?? findHarnessAnchorPid(harness) ?? process.ppid;
969
+ if (!Number.isFinite(pid) || pid <= 0) return;
970
+ writePidmapViaAgentCoord(coordRoot, pid, instanceId, harnessPlatform(harness));
971
+ }
972
+
973
+ function stampToolActivity(
974
+ coordRoot: string,
975
+ instanceId: string,
976
+ data: Record<string, unknown>,
977
+ ): void {
978
+ const agentCoordBin = join(coordRoot, "harnery", "bin", "agent-coord");
979
+ if (!existsSync(agentCoordBin)) return;
980
+ const toolName = (data.tool_name as string | undefined) ?? "";
981
+ // Extract a 1-line target from the tool_input blob (file path / command head).
982
+ const toolInputRaw = data.tool_input;
983
+ let target = "";
984
+ if (typeof toolInputRaw === "string") {
985
+ try {
986
+ const parsed = JSON.parse(toolInputRaw) as Record<string, unknown>;
987
+ target =
988
+ (parsed.file_path as string | undefined) ??
989
+ (parsed.path as string | undefined) ??
990
+ (parsed.notebook_path as string | undefined) ??
991
+ (parsed.command as string | undefined) ??
992
+ (parsed.url as string | undefined) ??
993
+ (parsed.pattern as string | undefined) ??
994
+ "";
995
+ } catch {
996
+ /* skip */
997
+ }
998
+ }
999
+ if (target.length > 200) target = target.slice(0, 200);
1000
+ spawnSync(agentCoordBin, ["stamp-tool-activity", instanceId, toolName, target], {
1001
+ encoding: "utf8",
1002
+ timeout: 2000,
1003
+ });
1004
+ }
1005
+
1006
+ function releaseClaimOnFailure(
1007
+ coordRoot: string,
1008
+ instanceId: string,
1009
+ data: Record<string, unknown>,
1010
+ rawPayload: Record<string, unknown> | undefined,
1011
+ ): void {
1012
+ const toolName = (data.tool_name as string | undefined) ?? "";
1013
+ if (toolName !== "Edit" && toolName !== "Write" && toolName !== "NotebookEdit") return;
1014
+ // Path is in tool_input parsed from payload; try data first, fall back to raw.
1015
+ const toolInputRaw = data.tool_input;
1016
+ let filePath = "";
1017
+ if (typeof toolInputRaw === "string") {
1018
+ try {
1019
+ const parsed = JSON.parse(toolInputRaw) as Record<string, unknown>;
1020
+ filePath =
1021
+ (parsed.file_path as string | undefined) ??
1022
+ (parsed.path as string | undefined) ??
1023
+ (parsed.notebook_path as string | undefined) ??
1024
+ "";
1025
+ } catch {
1026
+ /* skip */
1027
+ }
1028
+ }
1029
+ if (!filePath && rawPayload) {
1030
+ const ti = rawPayload.tool_input as Record<string, unknown> | undefined;
1031
+ if (ti) {
1032
+ filePath =
1033
+ (ti.file_path as string | undefined) ??
1034
+ (ti.path as string | undefined) ??
1035
+ (ti.notebook_path as string | undefined) ??
1036
+ "";
1037
+ }
1038
+ }
1039
+ if (!filePath) return;
1040
+
1041
+ // Canonicalize path relative to coordRoot
1042
+ let canonical = filePath;
1043
+ if (filePath.startsWith("/")) {
1044
+ canonical = filePath.startsWith(`${coordRoot}/`)
1045
+ ? filePath.slice(coordRoot.length + 1)
1046
+ : filePath;
1047
+ }
1048
+
1049
+ const agentCoordBin = join(coordRoot, "harnery", "bin", "agent-coord");
1050
+ if (!existsSync(agentCoordBin)) return;
1051
+ spawnSync(agentCoordBin, ["release-claim", instanceId, canonical], {
1052
+ encoding: "utf8",
1053
+ timeout: 2000,
1054
+ });
1055
+ }
1056
+
1057
+ function cleanupSessionEnd(coordRoot: string, instanceId: string, reason: string): void {
1058
+ // Remove heartbeat from the canonical .harnery/active/ dir.
1059
+ const path = join(coordRoot, ".harnery", "active", `${instanceId}.json`);
1060
+ try {
1061
+ if (existsSync(path)) {
1062
+ require("node:fs").unlinkSync(path);
1063
+ }
1064
+ } catch {
1065
+ /* swallow */
1066
+ }
1067
+ // Sweep pid-map entries pointing to this instance
1068
+ const pidmapDir = join(coordRoot, ".harnery", "pid-map");
1069
+ if (existsSync(pidmapDir)) {
1070
+ try {
1071
+ const fs = require("node:fs") as typeof import("node:fs");
1072
+ for (const f of fs.readdirSync(pidmapDir)) {
1073
+ const rowPath = join(pidmapDir, f);
1074
+ try {
1075
+ const row = fs.readFileSync(rowPath, "utf8").trim();
1076
+ const ownerCol = row.split("\t")[0]?.trim() ?? "";
1077
+ if (ownerCol === instanceId) {
1078
+ fs.unlinkSync(rowPath);
1079
+ }
1080
+ } catch {
1081
+ /* swallow */
1082
+ }
1083
+ }
1084
+ } catch {
1085
+ /* swallow */
1086
+ }
1087
+ }
1088
+ // Activity log
1089
+ const agentCoordBin = join(coordRoot, "harnery", "bin", "agent-coord");
1090
+ if (existsSync(agentCoordBin)) {
1091
+ spawnSync(
1092
+ agentCoordBin,
1093
+ ["log", `SESSION_END reason=${reason}`, "--instance", instanceId],
1094
+ { encoding: "utf8", timeout: 2000 },
1095
+ );
1096
+ }
1097
+ }
1098
+
1099
+ async function emitUserPromptSubmitSystemMessage(
1100
+ coordRoot: string,
1101
+ instanceId: string,
1102
+ sessionId: string,
1103
+ harness: Harness,
1104
+ ): Promise<void> {
1105
+ const agentCoordBin = join(coordRoot, "harnery", "bin", "agent-coord");
1106
+ if (!existsSync(agentCoordBin)) return;
1107
+
1108
+ // Look up the agent's name from its heartbeat (for council pending rendering).
1109
+ let agentName = "";
1110
+ try {
1111
+ const fs = require("node:fs") as typeof import("node:fs");
1112
+ const hbPath = join(coordRoot, ".harnery", "active", `${instanceId}.json`);
1113
+ if (fs.existsSync(hbPath)) {
1114
+ const hb = JSON.parse(fs.readFileSync(hbPath, "utf8")) as { name?: string };
1115
+ agentName = hb.name ?? "";
1116
+ }
1117
+ } catch {
1118
+ /* fall through with empty name; peer table still renders */
1119
+ }
1120
+
1121
+ const args = ["prompt-context", "--instance", instanceId, "--session", sessionId];
1122
+ if (agentName) args.push("--name", agentName);
1123
+ // Cursor + Codex sessions get the set-task staleness nudge. CC enforces it
1124
+ // via the Stop-hook transcript scan; the nudge replaces that for harnesses
1125
+ // that don't reliably expose a transcript_path during stop.
1126
+ if (harness === "cursor" || harness === "codex") args.push("--task-nudge");
1127
+ const result = spawnSync(agentCoordBin, args, { encoding: "utf8", timeout: 3000 });
1128
+ if (result.status !== 0 || !result.stdout) return;
1129
+ const additionalContext = result.stdout.trim();
1130
+ if (!additionalContext) return;
1131
+
1132
+ const { emitContext } = await import("./harness/output.ts");
1133
+ emitContext(harness, "UserPromptSubmit", additionalContext);
1134
+ }
1135
+
1136
+ function emitSubagentStartContext(
1137
+ coordRoot: string,
1138
+ instanceId: string,
1139
+ sessionId: string,
1140
+ data: Record<string, unknown>,
1141
+ harness: Harness,
1142
+ ): void {
1143
+ // Look up the subagent's assigned name (just-written by agent-coord assignName
1144
+ // in session.start data) + the parent's short id for the "you are a subagent
1145
+ // of X" framing.
1146
+ const subagentName = (data.name as string | undefined) ?? "";
1147
+ if (!subagentName) return;
1148
+ const platformLabel = harnessPlatform(harness);
1149
+ const parentShort =
1150
+ sessionId && sessionId !== instanceId ? `agent-${sessionId.slice(0, 8)}` : "the parent session";
1151
+ const message = `You are agent-${subagentName} (${platformLabel} subagent). You're a subagent of ${parentShort}.`;
1152
+
1153
+ // Render peer table inline since the subagent might want to know who else
1154
+ // is around. Reuse prompt-context (which dedups against the per-owner hash);
1155
+ // first call will always emit.
1156
+ const agentCoordBin = join(coordRoot, "harnery", "bin", "agent-coord");
1157
+ let combined = message;
1158
+ if (existsSync(agentCoordBin)) {
1159
+ const result = spawnSync(
1160
+ agentCoordBin,
1161
+ ["prompt-context", "--instance", instanceId, "--session", sessionId, "--name", subagentName],
1162
+ { encoding: "utf8", timeout: 3000 },
1163
+ );
1164
+ const ctx = (result.stdout ?? "").trim();
1165
+ if (ctx) combined = `${message}\n\n${ctx}`;
1166
+ }
1167
+
1168
+ // Use SubagentStart event-name in CC's hookSpecificOutput shape; cursor's
1169
+ // flat `additional_context` works the same way.
1170
+ void import("./harness/output.ts").then(({ emitContext }) => {
1171
+ emitContext(harness, "SubagentStart", combined);
1172
+ });
1173
+ }
1174
+
1175
+ function harnessPlatform(harness: Harness): string {
1176
+ if (harness === "claude-code") return "claude_code";
1177
+ return harness;
1178
+ }
1179
+
1180
+ async function emitSessionStartSystemMessage(
1181
+ coordRoot: string,
1182
+ instanceId: string,
1183
+ sessionId: string,
1184
+ emittedData: Record<string, unknown>,
1185
+ harness: Harness,
1186
+ ): Promise<void> {
1187
+ const agentCoordBin = join(coordRoot, "harnery", "bin", "agent-coord");
1188
+ if (!existsSync(agentCoordBin)) return;
1189
+
1190
+ // Sync-project so the heartbeat exists for downstream readers (peer table,
1191
+ // wiring check, council invites).
1192
+ spawnSync(agentCoordBin, ["project"], { encoding: "utf8", timeout: 3000 });
1193
+ // Stale-sweep dead peers before rendering peer table.
1194
+ spawnSync(agentCoordBin, ["stale-sweep"], { encoding: "utf8", timeout: 3000 });
1195
+
1196
+ // SESSION_START activity log line, fired across all harnesses.
1197
+ const model = (emittedData.model as string | undefined) ?? "unknown";
1198
+ const source = (emittedData.source as string | undefined) ?? "startup";
1199
+ const platform = harnessPlatform(harness);
1200
+ spawnSync(
1201
+ agentCoordBin,
1202
+ [
1203
+ "log",
1204
+ `SESSION_START model=${model} source=${source} platform=${platform}`,
1205
+ "--instance",
1206
+ instanceId,
1207
+ ],
1208
+ { encoding: "utf8", timeout: 2000 },
1209
+ );
1210
+
1211
+ // Render the systemMessage via agent-coord.
1212
+ const agentName = (emittedData.name as string | undefined) ?? "";
1213
+ const args = ["session-context", "--instance", instanceId, "--session", sessionId];
1214
+ if (agentName) args.push("--name", agentName);
1215
+ // The "You are agent-X." prefix in session-context renders unqualified by
1216
+ // default (claude-code-style). For cursor/codex the bash dispatchers add
1217
+ // a "(Cursor)" / "(Codex)" suffix; pass it through as --platform-label.
1218
+ if (harness !== "claude-code") {
1219
+ args.push("--platform-label", platform === "cursor" ? "Cursor" : "Codex");
1220
+ }
1221
+ const result = spawnSync(agentCoordBin, args, { encoding: "utf8", timeout: 3000 });
1222
+ if (result.status !== 0 || !result.stdout) return;
1223
+ let additionalContext = result.stdout.trim();
1224
+ if (!additionalContext) return;
1225
+
1226
+ // Effect (claude-code): merge the scratch recovery cue into the session-start
1227
+ // context. Was a standalone additionalContext emission from the previous
1228
+ // scratch-on-start adapter; now that agent-hook is the single SessionStart
1229
+ // entry, it folds in here.
1230
+ if (harness === "claude-code") {
1231
+ const cue = scratchRecoveryCue(coordRoot);
1232
+ if (cue) additionalContext = `${additionalContext}\n\n${cue}`;
1233
+ }
1234
+
1235
+ const { emitContext } = await import("./harness/output.ts");
1236
+ emitContext(harness, "SessionStart", additionalContext);
1237
+ }
1238
+
1239
+ main()
1240
+ .then((code) => process.exit(code))
1241
+ .catch((err) => {
1242
+ logError(findCoordRoot(process.cwd()), err, {
1243
+ argv: process.argv.slice(2),
1244
+ pid: process.pid,
1245
+ });
1246
+ process.exit(0);
1247
+ });