harnery 0.0.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (445) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +84 -2
  3. package/bin/agent-coord +42 -0
  4. package/bin/agent-hook +44 -0
  5. package/bin/harn +40 -0
  6. package/dist/cli.d.ts +9 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +18 -0
  9. package/dist/commander.d.ts +128 -0
  10. package/dist/commander.d.ts.map +1 -0
  11. package/dist/commander.js +126 -0
  12. package/dist/commands/agents.d.ts +18 -0
  13. package/dist/commands/agents.d.ts.map +1 -0
  14. package/dist/commands/agents.js +3946 -0
  15. package/dist/commands/backup.d.ts +22 -0
  16. package/dist/commands/backup.d.ts.map +1 -0
  17. package/dist/commands/backup.js +262 -0
  18. package/dist/commands/browse-ai.d.ts +4 -0
  19. package/dist/commands/browse-ai.d.ts.map +1 -0
  20. package/dist/commands/browse-ai.js +156 -0
  21. package/dist/commands/browse.d.ts +4 -0
  22. package/dist/commands/browse.d.ts.map +1 -0
  23. package/dist/commands/browse.js +590 -0
  24. package/dist/commands/callers.d.ts +4 -0
  25. package/dist/commands/callers.d.ts.map +1 -0
  26. package/dist/commands/callers.js +276 -0
  27. package/dist/commands/completion.d.ts +17 -0
  28. package/dist/commands/completion.d.ts.map +1 -0
  29. package/dist/commands/completion.js +158 -0
  30. package/dist/commands/config-get.d.ts +4 -0
  31. package/dist/commands/config-get.d.ts.map +1 -0
  32. package/dist/commands/config-get.js +131 -0
  33. package/dist/commands/context.d.ts +11 -0
  34. package/dist/commands/context.d.ts.map +1 -0
  35. package/dist/commands/context.js +185 -0
  36. package/dist/commands/cookies.d.ts +4 -0
  37. package/dist/commands/cookies.d.ts.map +1 -0
  38. package/dist/commands/cookies.js +140 -0
  39. package/dist/commands/docs.d.ts +4 -0
  40. package/dist/commands/docs.d.ts.map +1 -0
  41. package/dist/commands/docs.js +137 -0
  42. package/dist/commands/doctor.d.ts +25 -0
  43. package/dist/commands/doctor.d.ts.map +1 -0
  44. package/dist/commands/doctor.js +200 -0
  45. package/dist/commands/edit-batch.d.ts +18 -0
  46. package/dist/commands/edit-batch.d.ts.map +1 -0
  47. package/dist/commands/edit-batch.js +172 -0
  48. package/dist/commands/eml.d.ts +4 -0
  49. package/dist/commands/eml.d.ts.map +1 -0
  50. package/dist/commands/eml.js +428 -0
  51. package/dist/commands/env.d.ts +4 -0
  52. package/dist/commands/env.d.ts.map +1 -0
  53. package/dist/commands/env.js +201 -0
  54. package/dist/commands/fetch.d.ts +4 -0
  55. package/dist/commands/fetch.d.ts.map +1 -0
  56. package/dist/commands/fetch.js +99 -0
  57. package/dist/commands/file-history.d.ts +4 -0
  58. package/dist/commands/file-history.d.ts.map +1 -0
  59. package/dist/commands/file-history.js +152 -0
  60. package/dist/commands/grep.d.ts +4 -0
  61. package/dist/commands/grep.d.ts.map +1 -0
  62. package/dist/commands/grep.js +317 -0
  63. package/dist/commands/init.d.ts +82 -0
  64. package/dist/commands/init.d.ts.map +1 -0
  65. package/dist/commands/init.js +288 -0
  66. package/dist/commands/outline.d.ts +4 -0
  67. package/dist/commands/outline.d.ts.map +1 -0
  68. package/dist/commands/outline.js +509 -0
  69. package/dist/commands/presence.d.ts +12 -0
  70. package/dist/commands/presence.d.ts.map +1 -0
  71. package/dist/commands/presence.js +123 -0
  72. package/dist/commands/read.d.ts +7 -0
  73. package/dist/commands/read.d.ts.map +1 -0
  74. package/dist/commands/read.js +46 -0
  75. package/dist/commands/scratch.d.ts +4 -0
  76. package/dist/commands/scratch.d.ts.map +1 -0
  77. package/dist/commands/scratch.js +426 -0
  78. package/dist/commands/session.d.ts +4 -0
  79. package/dist/commands/session.d.ts.map +1 -0
  80. package/dist/commands/session.js +162 -0
  81. package/dist/commands/sync.d.ts +24 -0
  82. package/dist/commands/sync.d.ts.map +1 -0
  83. package/dist/commands/sync.js +275 -0
  84. package/dist/commands/toc.d.ts +5 -0
  85. package/dist/commands/toc.d.ts.map +1 -0
  86. package/dist/commands/toc.js +153 -0
  87. package/dist/commands/tokens.d.ts +4 -0
  88. package/dist/commands/tokens.d.ts.map +1 -0
  89. package/dist/commands/tokens.js +48 -0
  90. package/dist/commands/tunnel.d.ts +4 -0
  91. package/dist/commands/tunnel.d.ts.map +1 -0
  92. package/dist/commands/tunnel.js +513 -0
  93. package/dist/commands/uninstall.d.ts +22 -0
  94. package/dist/commands/uninstall.d.ts.map +1 -0
  95. package/dist/commands/uninstall.js +126 -0
  96. package/dist/commands/web.d.ts +4 -0
  97. package/dist/commands/web.d.ts.map +1 -0
  98. package/dist/commands/web.js +165 -0
  99. package/dist/core/agents/canonical-emit.d.ts +27 -0
  100. package/dist/core/agents/canonical-emit.d.ts.map +1 -0
  101. package/dist/core/agents/canonical-emit.js +72 -0
  102. package/dist/core/agents/cli-emit.d.ts +27 -0
  103. package/dist/core/agents/cli-emit.d.ts.map +1 -0
  104. package/dist/core/agents/cli-emit.js +57 -0
  105. package/dist/core/agents/cli.d.ts +10 -0
  106. package/dist/core/agents/cli.d.ts.map +1 -0
  107. package/dist/core/agents/cli.js +757 -0
  108. package/dist/core/agents/codex-replay.d.ts +29 -0
  109. package/dist/core/agents/codex-replay.d.ts.map +1 -0
  110. package/dist/core/agents/codex-replay.js +138 -0
  111. package/dist/core/agents/coord-client.d.ts +98 -0
  112. package/dist/core/agents/coord-client.d.ts.map +1 -0
  113. package/dist/core/agents/coord-client.js +212 -0
  114. package/dist/core/agents/events/consume.d.ts +59 -0
  115. package/dist/core/agents/events/consume.d.ts.map +1 -0
  116. package/dist/core/agents/events/consume.js +147 -0
  117. package/dist/core/agents/events/emit.d.ts +42 -0
  118. package/dist/core/agents/events/emit.d.ts.map +1 -0
  119. package/dist/core/agents/events/emit.js +70 -0
  120. package/dist/core/agents/events/ulid.d.ts +11 -0
  121. package/dist/core/agents/events/ulid.d.ts.map +1 -0
  122. package/dist/core/agents/events/ulid.js +47 -0
  123. package/dist/core/agents/index.d.ts +14 -0
  124. package/dist/core/agents/index.d.ts.map +1 -0
  125. package/dist/core/agents/index.js +13 -0
  126. package/dist/core/agents/paths.d.ts +6 -0
  127. package/dist/core/agents/paths.d.ts.map +1 -0
  128. package/dist/core/agents/paths.js +17 -0
  129. package/dist/core/agents/render/prompt-context.d.ts +43 -0
  130. package/dist/core/agents/render/prompt-context.d.ts.map +1 -0
  131. package/dist/core/agents/render/prompt-context.js +335 -0
  132. package/dist/core/agents/render/session-context.d.ts +39 -0
  133. package/dist/core/agents/render/session-context.d.ts.map +1 -0
  134. package/dist/core/agents/render/session-context.js +283 -0
  135. package/dist/core/agents/rules/claim-conflict.d.ts +35 -0
  136. package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -0
  137. package/dist/core/agents/rules/claim-conflict.js +244 -0
  138. package/dist/core/agents/rules/commit-conflict.d.ts +59 -0
  139. package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -0
  140. package/dist/core/agents/rules/commit-conflict.js +244 -0
  141. package/dist/core/agents/rules/stop-hook.d.ts +44 -0
  142. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -0
  143. package/dist/core/agents/rules/stop-hook.js +161 -0
  144. package/dist/core/agents/session-events.d.ts +41 -0
  145. package/dist/core/agents/session-events.d.ts.map +1 -0
  146. package/dist/core/agents/session-events.js +205 -0
  147. package/dist/core/agents/state/activity-log.d.ts +18 -0
  148. package/dist/core/agents/state/activity-log.d.ts.map +1 -0
  149. package/dist/core/agents/state/activity-log.js +34 -0
  150. package/dist/core/agents/state/council.d.ts +39 -0
  151. package/dist/core/agents/state/council.d.ts.map +1 -0
  152. package/dist/core/agents/state/council.js +216 -0
  153. package/dist/core/agents/state/heartbeat-projector.d.ts +59 -0
  154. package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -0
  155. package/dist/core/agents/state/heartbeat-projector.js +436 -0
  156. package/dist/core/agents/state/heartbeat-writer.d.ts +64 -0
  157. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -0
  158. package/dist/core/agents/state/heartbeat-writer.js +271 -0
  159. package/dist/core/agents/state/names.d.ts +35 -0
  160. package/dist/core/agents/state/names.d.ts.map +1 -0
  161. package/dist/core/agents/state/names.js +376 -0
  162. package/dist/core/agents/state/pidmap.d.ts +11 -0
  163. package/dist/core/agents/state/pidmap.d.ts.map +1 -0
  164. package/dist/core/agents/state/pidmap.js +32 -0
  165. package/dist/core/agents/state/scratch.d.ts +27 -0
  166. package/dist/core/agents/state/scratch.d.ts.map +1 -0
  167. package/dist/core/agents/state/scratch.js +90 -0
  168. package/dist/core/agents/state/shell-mutation.d.ts +17 -0
  169. package/dist/core/agents/state/shell-mutation.d.ts.map +1 -0
  170. package/dist/core/agents/state/shell-mutation.js +41 -0
  171. package/dist/core/agents/state/stale-sweep.d.ts +16 -0
  172. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -0
  173. package/dist/core/agents/state/stale-sweep.js +166 -0
  174. package/dist/core/config.d.ts +29 -0
  175. package/dist/core/config.d.ts.map +1 -0
  176. package/dist/core/config.js +108 -0
  177. package/dist/core/hooks/cli.d.ts +21 -0
  178. package/dist/core/hooks/cli.d.ts.map +1 -0
  179. package/dist/core/hooks/cli.js +1123 -0
  180. package/dist/core/hooks/effects/image-capture.d.ts +43 -0
  181. package/dist/core/hooks/effects/image-capture.d.ts.map +1 -0
  182. package/dist/core/hooks/effects/image-capture.js +288 -0
  183. package/dist/core/hooks/effects/index.d.ts +64 -0
  184. package/dist/core/hooks/effects/index.d.ts.map +1 -0
  185. package/dist/core/hooks/effects/index.js +197 -0
  186. package/dist/core/hooks/events/emit.d.ts +31 -0
  187. package/dist/core/hooks/events/emit.d.ts.map +1 -0
  188. package/dist/core/hooks/events/emit.js +89 -0
  189. package/dist/core/hooks/events/schema.d.ts +235 -0
  190. package/dist/core/hooks/events/schema.d.ts.map +1 -0
  191. package/dist/core/hooks/events/schema.js +12 -0
  192. package/dist/core/hooks/events/ulid.d.ts +10 -0
  193. package/dist/core/hooks/events/ulid.d.ts.map +1 -0
  194. package/dist/core/hooks/events/ulid.js +47 -0
  195. package/dist/core/hooks/harness/detect.d.ts +9 -0
  196. package/dist/core/hooks/harness/detect.d.ts.map +1 -0
  197. package/dist/core/hooks/harness/detect.js +29 -0
  198. package/dist/core/hooks/harness/events.d.ts +45 -0
  199. package/dist/core/hooks/harness/events.d.ts.map +1 -0
  200. package/dist/core/hooks/harness/events.js +71 -0
  201. package/dist/core/hooks/harness/output.d.ts +46 -0
  202. package/dist/core/hooks/harness/output.d.ts.map +1 -0
  203. package/dist/core/hooks/harness/output.js +87 -0
  204. package/dist/core/hooks/harness/parse.d.ts +67 -0
  205. package/dist/core/hooks/harness/parse.d.ts.map +1 -0
  206. package/dist/core/hooks/harness/parse.js +132 -0
  207. package/dist/core/hooks/index.d.ts +8 -0
  208. package/dist/core/hooks/index.d.ts.map +1 -0
  209. package/dist/core/hooks/index.js +7 -0
  210. package/dist/core/hooks/resolve/anchor.d.ts +37 -0
  211. package/dist/core/hooks/resolve/anchor.d.ts.map +1 -0
  212. package/dist/core/hooks/resolve/anchor.js +48 -0
  213. package/dist/core/hooks/resolve/coord-root.d.ts +6 -0
  214. package/dist/core/hooks/resolve/coord-root.d.ts.map +1 -0
  215. package/dist/core/hooks/resolve/coord-root.js +27 -0
  216. package/dist/core/hooks/resolve/intent.d.ts +33 -0
  217. package/dist/core/hooks/resolve/intent.d.ts.map +1 -0
  218. package/dist/core/hooks/resolve/intent.js +79 -0
  219. package/dist/core/hooks/resolve/owner.d.ts +42 -0
  220. package/dist/core/hooks/resolve/owner.d.ts.map +1 -0
  221. package/dist/core/hooks/resolve/owner.js +140 -0
  222. package/dist/core/hooks/resolve/transcript.d.ts +26 -0
  223. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -0
  224. package/dist/core/hooks/resolve/transcript.js +73 -0
  225. package/dist/index.d.ts +15 -0
  226. package/dist/index.d.ts.map +1 -0
  227. package/dist/index.js +13 -0
  228. package/dist/lib/agent-browser/client.d.ts +99 -0
  229. package/dist/lib/agent-browser/client.d.ts.map +1 -0
  230. package/dist/lib/agent-browser/client.js +177 -0
  231. package/dist/lib/agent-browser/index.d.ts +2 -0
  232. package/dist/lib/agent-browser/index.d.ts.map +1 -0
  233. package/dist/lib/agent-browser/index.js +1 -0
  234. package/dist/lib/browser/client.d.ts +193 -0
  235. package/dist/lib/browser/client.d.ts.map +1 -0
  236. package/dist/lib/browser/client.js +325 -0
  237. package/dist/lib/browser/dev-overlay.d.ts +23 -0
  238. package/dist/lib/browser/dev-overlay.d.ts.map +1 -0
  239. package/dist/lib/browser/dev-overlay.js +153 -0
  240. package/dist/lib/browser/index.d.ts +5 -0
  241. package/dist/lib/browser/index.d.ts.map +1 -0
  242. package/dist/lib/browser/index.js +2 -0
  243. package/dist/lib/browser/layout.d.ts +79 -0
  244. package/dist/lib/browser/layout.d.ts.map +1 -0
  245. package/dist/lib/browser/layout.js +220 -0
  246. package/dist/lib/browser/visibility.d.ts +86 -0
  247. package/dist/lib/browser/visibility.d.ts.map +1 -0
  248. package/dist/lib/browser/visibility.js +333 -0
  249. package/dist/lib/browser/visual-diff.d.ts +38 -0
  250. package/dist/lib/browser/visual-diff.d.ts.map +1 -0
  251. package/dist/lib/browser/visual-diff.js +107 -0
  252. package/dist/lib/completion/bash.d.ts +25 -0
  253. package/dist/lib/completion/bash.d.ts.map +1 -0
  254. package/dist/lib/completion/bash.js +284 -0
  255. package/dist/lib/completion/fish.d.ts +16 -0
  256. package/dist/lib/completion/fish.d.ts.map +1 -0
  257. package/dist/lib/completion/fish.js +118 -0
  258. package/dist/lib/completion/index.d.ts +5 -0
  259. package/dist/lib/completion/index.d.ts.map +1 -0
  260. package/dist/lib/completion/index.js +4 -0
  261. package/dist/lib/completion/walk.d.ts +68 -0
  262. package/dist/lib/completion/walk.d.ts.map +1 -0
  263. package/dist/lib/completion/walk.js +102 -0
  264. package/dist/lib/completion/zsh.d.ts +13 -0
  265. package/dist/lib/completion/zsh.d.ts.map +1 -0
  266. package/dist/lib/completion/zsh.js +249 -0
  267. package/dist/lib/context/index.d.ts +107 -0
  268. package/dist/lib/context/index.d.ts.map +1 -0
  269. package/dist/lib/context/index.js +275 -0
  270. package/dist/lib/cookies/client.d.ts +131 -0
  271. package/dist/lib/cookies/client.d.ts.map +1 -0
  272. package/dist/lib/cookies/client.js +239 -0
  273. package/dist/lib/cookies/index.d.ts +2 -0
  274. package/dist/lib/cookies/index.d.ts.map +1 -0
  275. package/dist/lib/cookies/index.js +1 -0
  276. package/dist/lib/council/index.d.ts +266 -0
  277. package/dist/lib/council/index.d.ts.map +1 -0
  278. package/dist/lib/council/index.js +674 -0
  279. package/dist/lib/docs-index.d.ts +28 -0
  280. package/dist/lib/docs-index.d.ts.map +1 -0
  281. package/dist/lib/docs-index.js +169 -0
  282. package/dist/lib/docs-lint.d.ts +26 -0
  283. package/dist/lib/docs-lint.d.ts.map +1 -0
  284. package/dist/lib/docs-lint.js +378 -0
  285. package/dist/lib/docs-sweep.d.ts +34 -0
  286. package/dist/lib/docs-sweep.d.ts.map +1 -0
  287. package/dist/lib/docs-sweep.js +304 -0
  288. package/dist/lib/docs.d.ts +27 -0
  289. package/dist/lib/docs.d.ts.map +1 -0
  290. package/dist/lib/docs.js +142 -0
  291. package/dist/lib/env.d.ts +11 -0
  292. package/dist/lib/env.d.ts.map +1 -0
  293. package/dist/lib/env.js +12 -0
  294. package/dist/lib/exec.d.ts +32 -0
  295. package/dist/lib/exec.d.ts.map +1 -0
  296. package/dist/lib/exec.js +54 -0
  297. package/dist/lib/format.d.ts +29 -0
  298. package/dist/lib/format.d.ts.map +1 -0
  299. package/dist/lib/format.js +139 -0
  300. package/dist/lib/http/client.d.ts +56 -0
  301. package/dist/lib/http/client.d.ts.map +1 -0
  302. package/dist/lib/http/client.js +160 -0
  303. package/dist/lib/http/index.d.ts +2 -0
  304. package/dist/lib/http/index.d.ts.map +1 -0
  305. package/dist/lib/http/index.js +1 -0
  306. package/dist/lib/identities/index.d.ts +77 -0
  307. package/dist/lib/identities/index.d.ts.map +1 -0
  308. package/dist/lib/identities/index.js +190 -0
  309. package/dist/lib/machine.d.ts +19 -0
  310. package/dist/lib/machine.d.ts.map +1 -0
  311. package/dist/lib/machine.js +61 -0
  312. package/dist/lib/presence.d.ts +48 -0
  313. package/dist/lib/presence.d.ts.map +1 -0
  314. package/dist/lib/presence.js +123 -0
  315. package/dist/lib/readability/client.d.ts +39 -0
  316. package/dist/lib/readability/client.d.ts.map +1 -0
  317. package/dist/lib/readability/client.js +121 -0
  318. package/dist/lib/readability/index.d.ts +2 -0
  319. package/dist/lib/readability/index.d.ts.map +1 -0
  320. package/dist/lib/readability/index.js +1 -0
  321. package/dist/lib/scratch/index.d.ts +74 -0
  322. package/dist/lib/scratch/index.d.ts.map +1 -0
  323. package/dist/lib/scratch/index.js +393 -0
  324. package/dist/lib/tunnel/gate.d.ts +12 -0
  325. package/dist/lib/tunnel/gate.d.ts.map +1 -0
  326. package/dist/lib/tunnel/gate.js +101 -0
  327. package/dist/lib/tunnel/state.d.ts +34 -0
  328. package/dist/lib/tunnel/state.d.ts.map +1 -0
  329. package/dist/lib/tunnel/state.js +132 -0
  330. package/package.json +160 -8
  331. package/schemas/.gitkeep +0 -0
  332. package/schemas/config.schema.json +109 -0
  333. package/src/cli.ts +22 -0
  334. package/src/commander.ts +242 -0
  335. package/src/commands/.gitkeep +0 -0
  336. package/src/commands/agents.ts +4567 -0
  337. package/src/commands/backup.ts +305 -0
  338. package/src/commands/browse-ai.ts +198 -0
  339. package/src/commands/browse.ts +849 -0
  340. package/src/commands/callers.ts +363 -0
  341. package/src/commands/completion.ts +193 -0
  342. package/src/commands/config-get.ts +161 -0
  343. package/src/commands/context.ts +209 -0
  344. package/src/commands/cookies.ts +198 -0
  345. package/src/commands/docs.ts +174 -0
  346. package/src/commands/doctor.ts +231 -0
  347. package/src/commands/edit-batch.ts +233 -0
  348. package/src/commands/eml.ts +519 -0
  349. package/src/commands/env.ts +254 -0
  350. package/src/commands/fetch.ts +136 -0
  351. package/src/commands/file-history.ts +202 -0
  352. package/src/commands/grep.ts +371 -0
  353. package/src/commands/init.ts +335 -0
  354. package/src/commands/outline.ts +583 -0
  355. package/src/commands/presence.ts +152 -0
  356. package/src/commands/read.ts +64 -0
  357. package/src/commands/scratch.ts +445 -0
  358. package/src/commands/session.ts +187 -0
  359. package/src/commands/sync.ts +306 -0
  360. package/src/commands/toc.ts +218 -0
  361. package/src/commands/tokens.ts +79 -0
  362. package/src/commands/tunnel.ts +633 -0
  363. package/src/commands/uninstall.ts +144 -0
  364. package/src/commands/web.ts +193 -0
  365. package/src/core/agents/canonical-emit.ts +77 -0
  366. package/src/core/agents/cli-emit.ts +64 -0
  367. package/src/core/agents/cli.ts +838 -0
  368. package/src/core/agents/codex-replay.ts +163 -0
  369. package/src/core/agents/coord-client.ts +249 -0
  370. package/src/core/agents/events/consume.ts +196 -0
  371. package/src/core/agents/events/emit.ts +108 -0
  372. package/src/core/agents/events/ulid.ts +51 -0
  373. package/src/core/agents/index.ts +14 -0
  374. package/src/core/agents/paths.ts +16 -0
  375. package/src/core/agents/render/prompt-context.ts +401 -0
  376. package/src/core/agents/render/session-context.ts +341 -0
  377. package/src/core/agents/rules/claim-conflict.ts +282 -0
  378. package/src/core/agents/rules/commit-conflict.ts +303 -0
  379. package/src/core/agents/rules/stop-hook.ts +229 -0
  380. package/src/core/agents/session-events.ts +228 -0
  381. package/src/core/agents/state/activity-log.ts +33 -0
  382. package/src/core/agents/state/council.ts +265 -0
  383. package/src/core/agents/state/heartbeat-projector.ts +488 -0
  384. package/src/core/agents/state/heartbeat-writer.ts +333 -0
  385. package/src/core/agents/state/names.ts +399 -0
  386. package/src/core/agents/state/pidmap.ts +38 -0
  387. package/src/core/agents/state/scratch.ts +121 -0
  388. package/src/core/agents/state/shell-mutation.ts +44 -0
  389. package/src/core/agents/state/stale-sweep.ts +190 -0
  390. package/src/core/config.ts +111 -0
  391. package/src/core/hooks/cli.ts +1247 -0
  392. package/src/core/hooks/effects/image-capture.ts +330 -0
  393. package/src/core/hooks/effects/index.ts +210 -0
  394. package/src/core/hooks/events/emit.ts +120 -0
  395. package/src/core/hooks/events/schema.ts +430 -0
  396. package/src/core/hooks/events/ulid.ts +51 -0
  397. package/src/core/hooks/harness/detect.ts +30 -0
  398. package/src/core/hooks/harness/events.ts +102 -0
  399. package/src/core/hooks/harness/output.ts +100 -0
  400. package/src/core/hooks/harness/parse.ts +180 -0
  401. package/src/core/hooks/index.ts +16 -0
  402. package/src/core/hooks/resolve/anchor.ts +51 -0
  403. package/src/core/hooks/resolve/coord-root.ts +25 -0
  404. package/src/core/hooks/resolve/intent.ts +89 -0
  405. package/src/core/hooks/resolve/owner.ts +140 -0
  406. package/src/core/hooks/resolve/transcript.ts +72 -0
  407. package/src/hooks/.gitkeep +0 -0
  408. package/src/index.ts +15 -0
  409. package/src/lib/agent-browser/client.ts +239 -0
  410. package/src/lib/agent-browser/index.ts +1 -0
  411. package/src/lib/browser/client.ts +449 -0
  412. package/src/lib/browser/dev-overlay.ts +207 -0
  413. package/src/lib/browser/index.ts +24 -0
  414. package/src/lib/browser/layout.ts +288 -0
  415. package/src/lib/browser/visibility.ts +419 -0
  416. package/src/lib/browser/visual-diff.ts +150 -0
  417. package/src/lib/completion/bash.ts +291 -0
  418. package/src/lib/completion/fish.ts +134 -0
  419. package/src/lib/completion/index.ts +10 -0
  420. package/src/lib/completion/walk.ts +184 -0
  421. package/src/lib/completion/zsh.ts +262 -0
  422. package/src/lib/context/index.ts +386 -0
  423. package/src/lib/cookies/client.ts +301 -0
  424. package/src/lib/cookies/index.ts +13 -0
  425. package/src/lib/council/index.ts +803 -0
  426. package/src/lib/docs-index.ts +216 -0
  427. package/src/lib/docs-lint.ts +413 -0
  428. package/src/lib/docs-sweep.ts +348 -0
  429. package/src/lib/docs.ts +199 -0
  430. package/src/lib/env.ts +12 -0
  431. package/src/lib/exec.ts +74 -0
  432. package/src/lib/format.ts +147 -0
  433. package/src/lib/http/client.ts +211 -0
  434. package/src/lib/http/index.ts +1 -0
  435. package/src/lib/identities/index.ts +210 -0
  436. package/src/lib/machine.ts +61 -0
  437. package/src/lib/presence.ts +154 -0
  438. package/src/lib/readability/client.ts +169 -0
  439. package/src/lib/readability/index.ts +5 -0
  440. package/src/lib/readability/turndown-plugin-gfm.d.ts +10 -0
  441. package/src/lib/scratch/index.ts +470 -0
  442. package/src/lib/tunnel/gate.ts +113 -0
  443. package/src/lib/tunnel/state.ts +167 -0
  444. package/src/web/.gitkeep +0 -0
  445. package/index.js +0 -1
@@ -0,0 +1,341 @@
1
+ /**
2
+ * SessionStart UX renderer: combines the peer table, the wiring check, and
3
+ * the pending-council formatter so agent-hook session.start can emit the
4
+ * combined `systemMessage` JSON directly.
5
+ *
6
+ * Outputs a Claude Code SessionStart hookSpecificOutput.additionalContext
7
+ * string.
8
+ */
9
+
10
+ import { spawnSync } from "node:child_process";
11
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { resolveBinName } from "../../config.ts";
14
+
15
+ interface HeartbeatRow {
16
+ instance_id?: string;
17
+ name?: string;
18
+ kind?: string;
19
+ session_id?: string;
20
+ started_at?: string;
21
+ last_heartbeat?: string;
22
+ last_tool?: string;
23
+ last_tool_target?: string;
24
+ files_touched?: string[];
25
+ platform?: string;
26
+ task?: string;
27
+ turn_summary?: string;
28
+ }
29
+
30
+ export interface RenderOpts {
31
+ coordRoot: string;
32
+ instanceId: string;
33
+ sessionId: string;
34
+ agentName?: string;
35
+ /** Harness label rendered in parens after the self-name (e.g. "Cursor", "Codex").
36
+ * Claude Code omits it. */
37
+ platformLabel?: string;
38
+ }
39
+
40
+ /**
41
+ * Build the combined SessionStart systemMessage. Returns the additionalContext
42
+ * string (or "" if there's nothing to say).
43
+ */
44
+ export function renderSessionContext(opts: RenderOpts): string {
45
+ const { coordRoot, instanceId, sessionId, agentName, platformLabel } = opts;
46
+ const messages: string[] = [];
47
+
48
+ // 1. Self-name line + peer table (folded if peers present)
49
+ const peers = readActivePeers(coordRoot, instanceId);
50
+ const peerTable = formatPeerTable(peers, sessionId);
51
+ if (agentName) {
52
+ const suffix = platformLabel ? ` (${platformLabel})` : "";
53
+ const selfLine = `You are agent-${agentName}${suffix}.`;
54
+ messages.push(peerTable ? `${selfLine}\n\n${peerTable}` : selfLine);
55
+ } else if (peerTable) {
56
+ messages.push(peerTable);
57
+ }
58
+
59
+ // 2. Linked-worktree detection
60
+ if (isLinkedWorktree(coordRoot)) {
61
+ messages.push(
62
+ `Running inside worktree ${process.cwd()}. The coord layer is scoped to this worktree only; use \`${resolveBinName(coordRoot)} worktree diff\` to check for conflicts against sibling worktrees.`,
63
+ );
64
+ }
65
+
66
+ // 3. Council invites
67
+ if (agentName) {
68
+ const councilMsg = formatPendingCouncils(coordRoot, agentName);
69
+ if (councilMsg) messages.push(councilMsg);
70
+ }
71
+
72
+ // 4. Wiring check
73
+ const wiringIssues = checkWiring(coordRoot);
74
+ if (wiringIssues.length > 0) {
75
+ const wiringSummary = `Coordination hooks are NOT wired: the E-guard will not block conflicting commits, and post-commit claim pruning will not run. Run \`scripts/setup-hooks.sh\` to fix. Detected:\n${wiringIssues.map((i) => ` - ${i}`).join("\n")}`;
76
+ messages.push(wiringSummary);
77
+ }
78
+
79
+ return messages.join("\n\n");
80
+ }
81
+
82
+ /** Read all peer heartbeats from .harnery/active/, excluding self. */
83
+ function readActivePeers(coordRoot: string, selfInstanceId: string): HeartbeatRow[] {
84
+ const out: HeartbeatRow[] = [];
85
+ const dir = join(coordRoot, ".harnery", "active");
86
+ if (!existsSync(dir)) return out;
87
+ for (const f of readdirSync(dir)) {
88
+ if (!f.endsWith(".json")) continue;
89
+ try {
90
+ const hb = JSON.parse(readFileSync(join(dir, f), "utf8")) as HeartbeatRow;
91
+ if (!hb.instance_id || hb.instance_id === selfInstanceId) continue;
92
+ out.push(hb);
93
+ } catch {
94
+ /* skip */
95
+ }
96
+ }
97
+ return out;
98
+ }
99
+
100
+ /**
101
+ * Renders the peer table as
102
+ * two subsections: "Other agent groups active" (blocking) and "Your group"
103
+ * (subagents/siblings, no mutual block). Folds transient subagents' files
104
+ * into their parent session.
105
+ */
106
+ function formatPeerTable(peers: HeartbeatRow[], mySessionId: string): string {
107
+ if (peers.length === 0) return "";
108
+ const nowSec = Math.floor(Date.now() / 1000);
109
+
110
+ // Fold transient peers' files into their session_id parent.
111
+ const fold: Record<string, string[]> = {};
112
+ for (const p of peers) {
113
+ const kind = p.kind ?? "unknown";
114
+ if (kind === "transient" && p.session_id) {
115
+ fold[p.session_id] = (fold[p.session_id] ?? []).concat(p.files_touched ?? []);
116
+ }
117
+ }
118
+
119
+ // Build rows with display_files (own files + folded transient files).
120
+ type RowExt = HeartbeatRow & { display_files: string[] };
121
+ const rows: RowExt[] = peers
122
+ .filter((p) => (p.kind ?? "unknown") !== "transient")
123
+ .map((p) => {
124
+ const folded = fold[p.instance_id ?? ""] ?? [];
125
+ const display = Array.from(new Set([...(p.files_touched ?? []), ...folded])).sort();
126
+ return { ...p, display_files: display };
127
+ });
128
+
129
+ const blocking = rows.filter((p) => p.session_id !== mySessionId).sort(byStartedAt);
130
+ const group = rows.filter((p) => p.session_id === mySessionId).sort(byStartedAt);
131
+
132
+ const sections: string[] = [];
133
+ const blockingSection = renderSubtable(
134
+ blocking,
135
+ "Other agent groups active (their files block you):",
136
+ nowSec,
137
+ );
138
+ if (blockingSection) sections.push(blockingSection);
139
+ const groupSection = renderSubtable(
140
+ group,
141
+ "Your group (subagents / parent / siblings; no mutual block):",
142
+ nowSec,
143
+ );
144
+ if (groupSection) sections.push(groupSection);
145
+
146
+ return sections.join("\n\n");
147
+ }
148
+
149
+ function byStartedAt(a: HeartbeatRow, b: HeartbeatRow): number {
150
+ return (a.started_at ?? "").localeCompare(b.started_at ?? "");
151
+ }
152
+
153
+ function renderSubtable(
154
+ rows: Array<HeartbeatRow & { display_files: string[] }>,
155
+ header: string,
156
+ nowSec: number,
157
+ ): string {
158
+ if (rows.length === 0) return "";
159
+ const first = rows.slice(0, 10).map((r) => formatRow(r, nowSec));
160
+ const overflow = rows.length > 10 ? `\n +${rows.length - 10} more` : "";
161
+ return `${header}\n${first.join("\n")}${overflow}`;
162
+ }
163
+
164
+ function formatRow(r: HeartbeatRow & { display_files: string[] }, nowSec: number): string {
165
+ const taskPart = r.task ? ` "${r.task.slice(0, 60)}"` : "";
166
+ const ageFrom = fmtAge(nowSec - parseIsoSec(r.started_at));
167
+ const filesPart = fmtFiles(r.display_files);
168
+ const lastActivity = fmtLastActivity(r, nowSec);
169
+ const turnSummary = r.turn_summary ? `\n last turn: ${r.turn_summary.slice(0, 80)}` : "";
170
+ return ` - agent-${r.name ?? "unknown"}${taskPart} (${ageFrom}, ${filesPart}${lastActivity})${turnSummary}`;
171
+ }
172
+
173
+ function fmtFiles(files: string[]): string {
174
+ if (files.length === 0) return "nothing yet";
175
+ if (files.length <= 3) return `holds: ${files.join(", ")}`;
176
+ return `holds: ${files.slice(0, 3).join(", ")}, +${files.length - 3} more`;
177
+ }
178
+
179
+ function fmtLastActivity(r: HeartbeatRow, nowSec: number): string {
180
+ if (!r.last_tool) return "";
181
+ const lastTs = parseIsoSec(r.last_heartbeat ?? r.started_at);
182
+ const ageSec = nowSec - lastTs;
183
+ const tail = r.last_tool_target ? ` ${r.last_tool_target.slice(0, 60)}` : "";
184
+ return `, last: ${r.last_tool}${tail} ${fmtAge(ageSec)}`;
185
+ }
186
+
187
+ function parseIsoSec(iso: string | undefined): number {
188
+ if (!iso) return 0;
189
+ const ms = Date.parse(iso);
190
+ return Number.isFinite(ms) ? Math.floor(ms / 1000) : 0;
191
+ }
192
+
193
+ function fmtAge(secs: number): string {
194
+ if (secs < 60) return `${Math.floor(secs)}s ago`;
195
+ if (secs < 3600) return `${Math.floor(secs / 60)}m ago`;
196
+ if (secs < 86400) return `${Math.floor(secs / 3600)}h ago`;
197
+ return `${Math.floor(secs / 86400)}d ago`;
198
+ }
199
+
200
+ /**
201
+ * Returns a list of wiring issues (empty when everything's wired). Checks
202
+ * parent core.hooksPath + one representative submodule.
203
+ */
204
+ export function checkWiring(coordRoot: string): string[] {
205
+ const expected = join(coordRoot, "scripts", "hooks");
206
+ const issues: string[] = [];
207
+
208
+ // Parent repo
209
+ const parentHp = gitConfig(coordRoot, "core.hooksPath");
210
+ const parentResolved = resolveHooksPath(coordRoot, parentHp);
211
+ if (parentResolved !== expected) {
212
+ issues.push(
213
+ `parent core.hooksPath=${parentHp || "<unset>"} (resolves to ${parentResolved}, expected ${expected})`,
214
+ );
215
+ }
216
+
217
+ // One representative submodule
218
+ const gitmodules = join(coordRoot, ".gitmodules");
219
+ if (existsSync(gitmodules)) {
220
+ const sampleSub = extractFirstSubmodule(gitmodules);
221
+ if (sampleSub) {
222
+ const subPath = join(coordRoot, sampleSub);
223
+ const subGitDir = join(subPath, ".git");
224
+ if (existsSync(subGitDir)) {
225
+ const subHp = gitConfig(subPath, "core.hooksPath");
226
+ const subResolved = resolveSubmoduleHooksPath(coordRoot, sampleSub, subHp);
227
+ if (subResolved !== expected) {
228
+ issues.push(
229
+ `submodule ${sampleSub} core.hooksPath=${subHp || "<unset>"} (resolves to ${subResolved}; other submodules likely affected too)`,
230
+ );
231
+ }
232
+ }
233
+ }
234
+ }
235
+
236
+ return issues;
237
+ }
238
+
239
+ function gitConfig(cwd: string, key: string): string {
240
+ const result = spawnSync("git", ["-C", cwd, "config", "--get", key], { encoding: "utf8" });
241
+ if (result.status !== 0) return "";
242
+ return result.stdout.trim();
243
+ }
244
+
245
+ function resolveHooksPath(root: string, hp: string): string {
246
+ if (!hp) return join(root, ".git", "hooks");
247
+ if (hp.startsWith("/")) return hp.replace(/\/$/, "");
248
+ return join(root, hp).replace(/\/$/, "");
249
+ }
250
+
251
+ function resolveSubmoduleHooksPath(root: string, sub: string, hp: string): string {
252
+ if (!hp) return join(root, sub, ".git", "hooks");
253
+ if (hp.startsWith("/")) return hp.replace(/\/$/, "");
254
+ return join(root, sub, hp).replace(/\/$/, "");
255
+ }
256
+
257
+ function extractFirstSubmodule(gitmodulesPath: string): string | null {
258
+ try {
259
+ const content = readFileSync(gitmodulesPath, "utf8");
260
+ const match = content.match(/^\s*path\s*=\s*(.+)$/m);
261
+ return match ? match[1]!.trim() : null;
262
+ } catch {
263
+ return null;
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Detect linked-worktree environment (`git worktree` created from the
269
+ * superproject) via a `git rev-parse --git-dir` vs `--git-common-dir` check.
270
+ */
271
+ function isLinkedWorktree(coordRoot: string): boolean {
272
+ const dir = spawnSync("git", ["-C", coordRoot, "rev-parse", "--git-dir"], { encoding: "utf8" });
273
+ const common = spawnSync("git", ["-C", coordRoot, "rev-parse", "--git-common-dir"], {
274
+ encoding: "utf8",
275
+ });
276
+ if (dir.status !== 0 || common.status !== 0) return false;
277
+ const d = dir.stdout.trim();
278
+ const c = common.stdout.trim();
279
+ return d !== "" && c !== "" && d !== c;
280
+ }
281
+
282
+ /**
283
+ * Returns the formatted council invite reminder
284
+ * (or "" when no councils await input).
285
+ */
286
+ export function formatPendingCouncils(coordRoot: string, agentName: string): string {
287
+ const councilsDir = join(coordRoot, ".harnery", "councils");
288
+ if (!existsSync(councilsDir)) return "";
289
+ const canonicalName = agentName.startsWith("agent-") ? agentName : `agent-${agentName}`;
290
+ const pending: string[] = [];
291
+ try {
292
+ for (const f of readdirSync(councilsDir)) {
293
+ if (!f.endsWith(".json")) continue;
294
+ const manifestPath = join(councilsDir, f);
295
+ try {
296
+ const m = JSON.parse(readFileSync(manifestPath, "utf8")) as {
297
+ council_id?: string;
298
+ status?: string;
299
+ round_status?: string;
300
+ current_round?: number;
301
+ members?: string[];
302
+ };
303
+ if (m.status !== "active" || m.round_status !== "open") continue;
304
+ if (!m.members?.includes(canonicalName)) continue;
305
+ const round = m.current_round ?? 1;
306
+ const contributionPath = join(
307
+ councilsDir,
308
+ m.council_id ?? "",
309
+ `round-${round}`,
310
+ `${canonicalName}.md`,
311
+ );
312
+ if (existsSync(contributionPath)) continue; // already contributed
313
+ if (m.council_id) pending.push(m.council_id);
314
+ } catch {
315
+ /* skip */
316
+ }
317
+ }
318
+ } catch {
319
+ /* skip */
320
+ }
321
+
322
+ if (pending.length === 0) return "";
323
+ const bin = resolveBinName(coordRoot);
324
+ const firstThree = pending.slice(0, 3);
325
+ const tail = pending.length > 3 ? `, +${pending.length - 3} more` : "";
326
+ const list = firstThree.join(", ") + tail;
327
+ if (pending.length === 1) {
328
+ return (
329
+ `Council waiting on your input: \`${pending[0]}\`. ` +
330
+ `Run \`${bin} agents council show ${pending[0]}\` for the brief and ` +
331
+ `\`${bin} agents council contribute ${pending[0]} --message "<your take>"\` to weigh in.`
332
+ );
333
+ }
334
+ return `Councils waiting on your input (${pending.length} open): ${list}. Run \`${bin} agents council list --mine\` to see all of them, then \`${bin} agents council show <id>\` for any brief.`;
335
+ }
336
+
337
+ /**
338
+ * Suppress unused import warning when statSync isn't used in this file
339
+ * (kept exported for future renderers, e.g. heartbeat-freshness coloring).
340
+ */
341
+ export const _ensureStatSyncImported = statSync;
@@ -0,0 +1,282 @@
1
+ /**
2
+ * File-claim verdict.
3
+ *
4
+ * Phase 5 cutover: PreToolUse routes Edit/Write/NotebookEdit through here.
5
+ *
6
+ * Two checks:
7
+ * 1. Conflict: is the path already claimed by a fresh peer? Block with
8
+ * the peer's name in the reason.
9
+ * 2. Ordering: if we already hold a claim on path A and want path
10
+ * B, B must sort > A lexicographically. Otherwise emit
11
+ * claim.conflict (ordering_violation) and block.
12
+ *
13
+ * Reads from `.harnery/active/<id>.json`, the single canonical heartbeat
14
+ * location after Phase 8 collapsed the v1/v2 dual-write.
15
+ */
16
+
17
+ import { spawnSync } from "node:child_process";
18
+ import { existsSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
19
+ import { join } from "node:path";
20
+
21
+ const FRESHNESS_SECS = 600;
22
+
23
+ export type VerdictResult = {
24
+ allow: boolean;
25
+ exit_code: 0 | 2;
26
+ rule: string;
27
+ reason?: string;
28
+ };
29
+
30
+ interface PeerView {
31
+ instance_id: string;
32
+ name?: string;
33
+ files_touched: string[];
34
+ last_heartbeat: string;
35
+ session_id: string;
36
+ /** parent_instance_id from heartbeat, present for subagent rows. Used by
37
+ * the group-ownership check (sibling subagents + parent share a group
38
+ * and don't block each other's claims).
39
+ */
40
+ parent_instance_id?: string;
41
+ }
42
+
43
+ export interface ClaimRequest {
44
+ rule: "claim";
45
+ instance_id: string;
46
+ session_id?: string;
47
+ path: string; // canonical monorepo-relative path
48
+ mode?: "read" | "write"; // default "write"
49
+ }
50
+
51
+ /**
52
+ * Evaluate a file-claim. Returns deny when another fresh peer claims the
53
+ * path, OR when this owner already holds a lower-sorted claim and the new
54
+ * one would create a circular wait.
55
+ */
56
+ export function evaluateClaim(coordRoot: string, req: ClaimRequest): VerdictResult {
57
+ const peers = readPeers(coordRoot);
58
+ const myPeer = peers.find((p) => p.instance_id === req.instance_id);
59
+
60
+ // Group-ownership exclusion ("Option B" semantics): the parent
61
+ // session and all subagents under it share a group; claims within the
62
+ // group don't block each other. Compute the group root for the calling
63
+ // instance_id, then exclude any peer in the same group from the conflict
64
+ // scan. session_id from the payload is the parent_owner for subagents.
65
+ const groupRoot = computeGroupRoot(peers, req.instance_id, req.session_id);
66
+ const inMyGroup = (p: PeerView): boolean =>
67
+ p.instance_id === req.instance_id ||
68
+ p.instance_id === groupRoot ||
69
+ p.parent_instance_id === groupRoot ||
70
+ (p.parent_instance_id !== undefined && p.parent_instance_id === req.instance_id);
71
+
72
+ const otherPeers = peers.filter((p) => !inMyGroup(p));
73
+
74
+ // Conflict scan: any other fresh peer claiming this exact path?
75
+ const conflict = otherPeers.find(
76
+ (p) => isFresh(p.last_heartbeat) && p.files_touched.includes(req.path),
77
+ );
78
+ if (conflict) {
79
+ // Self-heal probe: if the file is committed-clean (exists in the repo AND has no uncommitted
80
+ // changes), the peer's claim is stale (worker crashed without releasing) →
81
+ // prune the claim from their heartbeat and allow the edit. Skips when the
82
+ // file doesn't exist (intent-to-create claim); that surface IS load-bearing.
83
+ if (isFileCommittedClean(coordRoot, req.path)) {
84
+ pruneClaimFromPeer(coordRoot, conflict.instance_id, req.path);
85
+ // Fall through to ordering check + allow.
86
+ } else {
87
+ return {
88
+ allow: false,
89
+ exit_code: 2,
90
+ rule: "claim.conflict",
91
+ reason: `File ${req.path} is currently being edited by agent-${conflict.name ?? conflict.instance_id.slice(0, 8)}. Wait for them to finish or pick a different file. Set HARNERY_AGENT_COORD_OFF=1 to bypass (not recommended).`,
92
+ };
93
+ }
94
+ }
95
+
96
+ // Ordering check: if we hold any claim with path < req.path, fine.
97
+ // Otherwise the new claim would create a backward-edge in the dependency
98
+ // graph and risk deadlock. Only applies when there are OTHER fresh peers:
99
+ // single-agent flow can't deadlock with itself, and the rule otherwise
100
+ // forces release-and-reacquire cycles on every reverse-order edit pair.
101
+ const hasFreshPeers = otherPeers.some(
102
+ (p) => isFresh(p.last_heartbeat) && p.files_touched.length > 0,
103
+ );
104
+ if (hasFreshPeers && myPeer && myPeer.files_touched.length > 0) {
105
+ const highest = [...myPeer.files_touched].sort().at(-1)!;
106
+ if (req.path < highest) {
107
+ return {
108
+ allow: false,
109
+ exit_code: 2,
110
+ rule: "claim.ordering_violation",
111
+ reason: `Cannot acquire ${req.path} while holding ${highest} (claim ordering rule: paths must be acquired in sorted order). Release the higher claim first.`,
112
+ };
113
+ }
114
+ }
115
+
116
+ // Acquire the claim: atomic check-and-set. Adds req.path to my
117
+ // files_touched if not already present.
118
+ if (req.mode !== "read") {
119
+ addClaimToOwner(coordRoot, req.instance_id, req.path);
120
+ }
121
+
122
+ return { allow: true, exit_code: 0, rule: "claim.pass" };
123
+ }
124
+
125
+ /**
126
+ * Add `relPath` to the owner's heartbeat `files_touched` array (idempotent;
127
+ * no-op if already present). Atomic temp + rename. If the owner has no
128
+ * heartbeat yet (subagent that hasn't been initialized), creates a minimal one.
129
+ */
130
+ function addClaimToOwner(coordRoot: string, instanceId: string, relPath: string): void {
131
+ const activeDir = join(coordRoot, ".harnery", "active");
132
+ if (!existsSync(activeDir)) return;
133
+ const path = join(activeDir, `${instanceId}.json`);
134
+ if (!existsSync(path)) {
135
+ const now = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
136
+ const minimal = {
137
+ schema_version: 1,
138
+ instance_id: instanceId,
139
+ session_id: instanceId,
140
+ files_touched: [relPath],
141
+ started_at: now,
142
+ last_heartbeat: now,
143
+ };
144
+ try {
145
+ const tmp = `${path}.tmp.${process.pid}`;
146
+ writeFileSync(tmp, JSON.stringify(minimal, null, 2), "utf8");
147
+ renameSync(tmp, path);
148
+ } catch {
149
+ /* silent */
150
+ }
151
+ return;
152
+ }
153
+ try {
154
+ const body = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
155
+ const files = (body.files_touched as string[] | undefined) ?? [];
156
+ if (files.includes(relPath)) return;
157
+ body.files_touched = [...files, relPath];
158
+ const tmp = `${path}.tmp.${process.pid}`;
159
+ writeFileSync(tmp, JSON.stringify(body, null, 2), "utf8");
160
+ renameSync(tmp, path);
161
+ } catch {
162
+ /* silent */
163
+ }
164
+ }
165
+
166
+ function readPeers(coordRoot: string): PeerView[] {
167
+ const out: PeerView[] = [];
168
+ const path = join(coordRoot, ".harnery", "active");
169
+ if (!existsSync(path)) return out;
170
+ for (const f of readdirSync(path)) {
171
+ if (!f.endsWith(".json")) continue;
172
+ try {
173
+ const hb = JSON.parse(readFileSync(join(path, f), "utf8")) as {
174
+ instance_id?: string;
175
+ name?: string;
176
+ session_id?: string;
177
+ files_touched?: string[];
178
+ last_heartbeat?: string;
179
+ parent_instance_id?: string;
180
+ parent_session_id?: string;
181
+ };
182
+ if (!hb.instance_id) continue;
183
+ // Derive parent_instance_id: explicit field first, then infer from
184
+ // session_id-differs-from-instance_id (the subagent shape:
185
+ // instance_id is the agent_id, session_id is the parent session).
186
+ const inferredParent =
187
+ hb.parent_instance_id ??
188
+ hb.parent_session_id ??
189
+ (hb.session_id && hb.session_id !== hb.instance_id ? hb.session_id : undefined);
190
+ out.push({
191
+ instance_id: hb.instance_id,
192
+ name: hb.name,
193
+ session_id: hb.session_id ?? hb.instance_id,
194
+ files_touched: hb.files_touched ?? [],
195
+ last_heartbeat: hb.last_heartbeat ?? "",
196
+ parent_instance_id: inferredParent,
197
+ });
198
+ } catch {
199
+ /* skip */
200
+ }
201
+ }
202
+ return out;
203
+ }
204
+
205
+ /**
206
+ * Compute the group root for `instanceId`. For a parent session it's instanceId
207
+ * itself. For a subagent it's the parent_instance_id from the heartbeat
208
+ * (stamped on subagent heartbeats). When session_id is
209
+ * supplied and differs from instanceId (Claude Code's `session_id != agent_id`
210
+ * shape for subagents), that's the group root too.
211
+ */
212
+ function computeGroupRoot(peers: PeerView[], instanceId: string, sessionId?: string): string {
213
+ // If we have a peer entry, its parent_instance_id is authoritative.
214
+ const myPeer = peers.find((p) => p.instance_id === instanceId);
215
+ if (myPeer?.parent_instance_id) return myPeer.parent_instance_id;
216
+ // Fallback to session_id (parent's instance_id for subagents).
217
+ if (sessionId && sessionId !== instanceId) return sessionId;
218
+ // I am the group root.
219
+ return instanceId;
220
+ }
221
+
222
+ function isFresh(lastHeartbeat: string): boolean {
223
+ if (!lastHeartbeat) return false;
224
+ const ts = Date.parse(lastHeartbeat);
225
+ if (!Number.isFinite(ts)) return false;
226
+ return (Date.now() - ts) / 1000 <= FRESHNESS_SECS;
227
+ }
228
+
229
+ /**
230
+ * Check if a monorepo-relative path is committed-clean: file exists in the
231
+ * repo, is tracked, AND `git diff HEAD -- path` shows no uncommitted
232
+ * modifications. Untracked files are NOT committed-clean: `git diff HEAD`
233
+ * exits 0 on an untracked path (because git ignores it), so we have to
234
+ * positively confirm tracking via `git ls-files` before trusting the diff.
235
+ *
236
+ * Returns false when:
237
+ * - file doesn't exist (intent-to-create, preserve the claim)
238
+ * - file is untracked (peer wrote it, hasn't staged yet, preserve the claim)
239
+ * - any git op fails (treat as dirty, fail-safe)
240
+ * - diff shows non-empty output (genuinely dirty)
241
+ */
242
+ function isFileCommittedClean(coordRoot: string, relPath: string): boolean {
243
+ const abs = join(coordRoot, relPath);
244
+ if (!existsSync(abs)) return false;
245
+ try {
246
+ const tracked = spawnSync("git", ["ls-files", "--error-unmatch", "--", relPath], {
247
+ cwd: coordRoot,
248
+ encoding: "utf8",
249
+ timeout: 2000,
250
+ });
251
+ if (tracked.status !== 0) return false;
252
+ const result = spawnSync("git", ["diff", "--quiet", "HEAD", "--", relPath], {
253
+ cwd: coordRoot,
254
+ encoding: "utf8",
255
+ timeout: 2000,
256
+ });
257
+ return result.status === 0;
258
+ } catch {
259
+ return false;
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Remove a stale claim from a peer's heartbeat. Atomic temp + rename. Silent
265
+ * on failure.
266
+ */
267
+ function pruneClaimFromPeer(coordRoot: string, instanceId: string, relPath: string): void {
268
+ const path = join(coordRoot, ".harnery", "active", `${instanceId}.json`);
269
+ if (!existsSync(path)) return;
270
+ try {
271
+ const body = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
272
+ const files = (body.files_touched as string[] | undefined) ?? [];
273
+ const next = files.filter((p) => p !== relPath);
274
+ if (next.length === files.length) return;
275
+ body.files_touched = next;
276
+ const tmp = `${path}.tmp.${process.pid}`;
277
+ writeFileSync(tmp, JSON.stringify(body, null, 2), "utf8");
278
+ renameSync(tmp, path);
279
+ } catch {
280
+ /* silent */
281
+ }
282
+ }