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