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,108 @@
1
+ /**
2
+ * Canonical-event writer for agent-coord. Mirrors the agent-hooks emitter:
3
+ * both modules write the same envelope shape to the same
4
+ * `.harnery/events.ndjson` file but stay independent (shared code limited to
5
+ * event/schema types).
6
+ *
7
+ * Phase 4: agent-coord uses this from CLI handlers (state.task_set,
8
+ * state.status_checked, state.scratch_append, council.*, presence.*).
9
+ */
10
+
11
+ import { appendFileSync, closeSync, mkdirSync, openSync } from "node:fs";
12
+ import { dirname, join } from "node:path";
13
+ import { ulid } from "./ulid.ts";
14
+
15
+ const SCHEMA_VERSION = 1 as const;
16
+ const STREAM_FILE = ".harnery/events.ndjson";
17
+ const LOCK_FILE = ".harnery/events.ndjson.lock";
18
+ const MAX_LINE_BYTES = 64 * 1024;
19
+
20
+ export type Harness = "claude-code" | "cursor" | "codex";
21
+ export type Source = "agent-hooks" | "agent-coord" | "user" | "system";
22
+
23
+ export interface Envelope {
24
+ schema_version: typeof SCHEMA_VERSION;
25
+ event_id: string;
26
+ event_type: string;
27
+ ts: string;
28
+ instance_id: string;
29
+ session_id: string;
30
+ parent_session_id?: string;
31
+ turn_id?: string;
32
+ parent_turn_id?: string;
33
+ harness: Harness;
34
+ source: Source;
35
+ data: Record<string, unknown>;
36
+ }
37
+
38
+ export interface EmitInput {
39
+ event_type: string;
40
+ instance_id: string;
41
+ session_id: string;
42
+ harness: Harness;
43
+ source?: Source;
44
+ parent_session_id?: string;
45
+ turn_id?: string;
46
+ parent_turn_id?: string;
47
+ ts?: string;
48
+ data: Record<string, unknown>;
49
+ }
50
+
51
+ export function buildEnvelope(input: EmitInput): Envelope {
52
+ return {
53
+ schema_version: SCHEMA_VERSION,
54
+ event_id: ulid(),
55
+ event_type: input.event_type,
56
+ ts: input.ts ?? new Date().toISOString(),
57
+ instance_id: input.instance_id,
58
+ session_id: input.session_id,
59
+ parent_session_id: input.parent_session_id,
60
+ turn_id: input.turn_id,
61
+ parent_turn_id: input.parent_turn_id,
62
+ harness: input.harness,
63
+ source: input.source ?? "agent-coord",
64
+ data: input.data,
65
+ };
66
+ }
67
+
68
+ export function emit(coordRoot: string, input: EmitInput): Envelope {
69
+ const envelope = buildEnvelope(input);
70
+ const streamPath = join(coordRoot, STREAM_FILE);
71
+ const lockPath = join(coordRoot, LOCK_FILE);
72
+
73
+ ensureDir(dirname(streamPath));
74
+ ensureFile(lockPath);
75
+
76
+ let line = `${JSON.stringify(envelope)}\n`;
77
+ if (Buffer.byteLength(line, "utf8") > MAX_LINE_BYTES) {
78
+ line = `${JSON.stringify({
79
+ ...envelope,
80
+ data: { __over_size_limit: true, original_bytes: Buffer.byteLength(line, "utf8") },
81
+ })}\n`;
82
+ }
83
+
84
+ const lockFd = openSync(lockPath, "r+");
85
+ try {
86
+ appendFileSync(streamPath, line, { encoding: "utf8", flag: "a" });
87
+ } finally {
88
+ closeSync(lockFd);
89
+ }
90
+
91
+ return envelope;
92
+ }
93
+
94
+ function ensureDir(path: string): void {
95
+ try {
96
+ mkdirSync(path, { recursive: true });
97
+ } catch {
98
+ /* swallow */
99
+ }
100
+ }
101
+
102
+ function ensureFile(path: string): void {
103
+ try {
104
+ closeSync(openSync(path, "a"));
105
+ } catch {
106
+ /* swallow */
107
+ }
108
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Tiny ULID generator. Crockford-base32, 10-char timestamp (48-bit ms epoch)
3
+ * + 16-char random (80-bit). 26 chars total, monotonically-sortable.
4
+ *
5
+ * Intentionally duplicated from the agent-hooks ULID generator: agent-hooks
6
+ * and agent-coord stay strictly-separated packages, sharing only event/schema
7
+ * types. The writer side reimplements per-module so neither depends on the
8
+ * other's runtime code.
9
+ */
10
+
11
+ const ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
12
+
13
+ export function ulid(): string {
14
+ const ts = encodeTime(Date.now());
15
+ const rnd = encodeRandom();
16
+ return ts + rnd;
17
+ }
18
+
19
+ function encodeTime(ms: number): string {
20
+ let out = "";
21
+ let n = BigInt(ms);
22
+ const base = BigInt(32);
23
+ for (let i = 0; i < 10; i++) {
24
+ const mod = Number(n % base);
25
+ out = ALPHABET[mod] + out;
26
+ n = n / base;
27
+ }
28
+ return out;
29
+ }
30
+
31
+ function encodeRandom(): string {
32
+ const bytes = new Uint8Array(10);
33
+ crypto.getRandomValues(bytes);
34
+ let out = "";
35
+ let buf = 0n;
36
+ let bits = 0;
37
+ for (const b of bytes) {
38
+ buf = (buf << 8n) | BigInt(b);
39
+ bits += 8;
40
+ while (bits >= 5) {
41
+ bits -= 5;
42
+ const idx = Number((buf >> BigInt(bits)) & 0x1fn);
43
+ out += ALPHABET[idx];
44
+ }
45
+ }
46
+ if (bits > 0) {
47
+ const idx = Number((buf << BigInt(5 - bits)) & 0x1fn);
48
+ out += ALPHABET[idx];
49
+ }
50
+ return out.slice(0, 16);
51
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * agent-coord library public exports.
3
+ *
4
+ * `coord-client`: coordination helpers
5
+ * (Heartbeat reader, owner-resolver via ppid-walk, monorepo-root finder).
6
+ * Imported by every TS caller that needs to read coord state.
7
+ *
8
+ * `canonical-emit`: fire-and-forget client for the canonical event stream;
9
+ * spawns `bin/agent-coord emit-event` and never blocks the caller.
10
+ */
11
+
12
+ export * from "./canonical-emit.js";
13
+ export * from "./coord-client.js";
14
+ export * from "./session-events.js";
@@ -0,0 +1,16 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+
4
+ /**
5
+ * Walk up from `start` looking for a directory containing `.harnery/`, so
6
+ * adapter + legacy code agree on the same monorepo root.
7
+ */
8
+ export function findCoordRoot(start: string = process.cwd()): string | null {
9
+ let dir = resolve(start);
10
+ while (true) {
11
+ if (existsSync(join(dir, ".harnery"))) return dir;
12
+ const parent = dirname(dir);
13
+ if (parent === dir) return null;
14
+ dir = parent;
15
+ }
16
+ }
@@ -0,0 +1,401 @@
1
+ /**
2
+ * UserPromptSubmit UX renderer. Combines the peer-refresh dedup, the
3
+ * council-pending hash-dedup, and the Cursor set-task staleness nudge.
4
+ * agent-hook's user_prompt.submit post-emit handler calls this
5
+ * and forwards the result as the harness-shaped additionalContext payload.
6
+ *
7
+ * Three hash-dedup'd subsections combined into one additionalContext payload:
8
+ * 1. Peer table: semantically-relevant peer fields hashed; only re-emits
9
+ * when peers change (name + instance_id + session_id + kind + started_at
10
+ * + sorted(files_touched) + platform, sorted by instance_id).
11
+ * 2. Council pending: pending open-council IDs hashed; re-emits when the
12
+ * ID set changes.
13
+ * 3. Task staleness nudge (cursor only, since CC enforces via the Stop hook
14
+ * transcript scan). Fires when `task` is null or `task_updated_at` is
15
+ * older than HARNERY_TASK_STALE_SECONDS (default 1800 = 30 min). Hash-deduped
16
+ * against the previous nudge state.
17
+ *
18
+ * Hash files live at:
19
+ * .harnery/.last-peer-hash.<instance_id>
20
+ * .harnery/.last-council-hash.<instance_id>
21
+ * .harnery/.last-task-nudge-hash.<instance_id>
22
+ *
23
+ * First call always emits (hash files don't exist).
24
+ */
25
+
26
+ import { createHash } from "node:crypto";
27
+ import {
28
+ existsSync,
29
+ mkdirSync,
30
+ readdirSync,
31
+ readFileSync,
32
+ renameSync,
33
+ rmSync,
34
+ writeFileSync,
35
+ } from "node:fs";
36
+ import { join } from "node:path";
37
+
38
+ import { coordEnv } from "../../../lib/env.ts";
39
+ import { formatPendingCouncils } from "./session-context.ts";
40
+
41
+ interface HeartbeatRow {
42
+ instance_id?: string;
43
+ name?: string;
44
+ kind?: string;
45
+ session_id?: string;
46
+ started_at?: string;
47
+ last_heartbeat?: string;
48
+ last_tool?: string;
49
+ last_tool_target?: string;
50
+ files_touched?: string[];
51
+ platform?: string;
52
+ task?: string;
53
+ turn_summary?: string;
54
+ }
55
+
56
+ export interface PromptContextOpts {
57
+ coordRoot: string;
58
+ instanceId: string;
59
+ sessionId: string;
60
+ agentName?: string;
61
+ /** When true, run the task-staleness nudge check (cursor only; CC has the
62
+ * Stop-hook transcript-scan enforcement). */
63
+ taskNudge?: boolean;
64
+ }
65
+
66
+ /**
67
+ * Build the combined UserPromptSubmit additionalContext string. Returns "" when
68
+ * nothing has changed since the last call (so the caller can skip the JSON emit).
69
+ *
70
+ * Side effects: updates `.harnery/.last-peer-hash.<id>` and `.harnery/.last-council-hash.<id>`
71
+ * when their respective sections re-emit (or removes the council hash when no
72
+ * councils are pending, matching the bash behavior).
73
+ */
74
+ export function renderPromptContext(opts: PromptContextOpts): string {
75
+ const { coordRoot, instanceId, sessionId, agentName, taskNudge } = opts;
76
+ const sections: string[] = [];
77
+
78
+ // 1. Peer table with hash dedup.
79
+ const peerTable = computePeerTableIfChanged(coordRoot, instanceId, sessionId);
80
+ if (peerTable) sections.push(peerTable);
81
+
82
+ // 2. Council pending with hash dedup.
83
+ if (agentName) {
84
+ const councilMsg = computeCouncilPendingIfChanged(coordRoot, instanceId, agentName);
85
+ if (councilMsg) sections.push(councilMsg);
86
+ }
87
+
88
+ // 3. Task staleness nudge (cursor only).
89
+ if (taskNudge) {
90
+ const nudgeMsg = computeTaskNudgeIfChanged(coordRoot, instanceId);
91
+ if (nudgeMsg) sections.push(nudgeMsg);
92
+ }
93
+
94
+ return sections.join("\n\n");
95
+ }
96
+
97
+ /** The set-task staleness nudge for Cursor.
98
+ * Emits a one-line reminder when `task` is null or `task_updated_at` is older
99
+ * than HARNERY_TASK_STALE_SECONDS (default 1800). Hash-deduped against previous
100
+ * nudge state. */
101
+ function computeTaskNudgeIfChanged(coordRoot: string, selfInstanceId: string): string {
102
+ const hbPath = join(coordRoot, ".harnery", "active", `${selfInstanceId}.json`);
103
+ if (!existsSync(hbPath)) return "";
104
+ let hb: { task?: string; task_updated_at?: string };
105
+ try {
106
+ hb = JSON.parse(readFileSync(hbPath, "utf8"));
107
+ } catch {
108
+ return "";
109
+ }
110
+
111
+ const threshold = Number.parseInt(coordEnv("TASK_STALE_SECONDS") ?? "1800", 10);
112
+ const taskValue = hb.task ?? "";
113
+ let needsNudge = false;
114
+ let message = "";
115
+
116
+ if (!taskValue) {
117
+ needsNudge = true;
118
+ message =
119
+ "Heads up: your `task` field is unset. Run `agents set-task \"<short focus>\"` so peers + the coord dashboard can see what you're working on. (Cursor sessions can't enforce this from the Stop hook the way Claude Code does, so this is a one-time soft reminder per staleness state.)";
120
+ } else if (hb.task_updated_at) {
121
+ const updatedSec = Math.floor(Date.parse(hb.task_updated_at) / 1000);
122
+ const nowSec = Math.floor(Date.now() / 1000);
123
+ if (Number.isFinite(updatedSec) && updatedSec > 0) {
124
+ const ageSec = nowSec - updatedSec;
125
+ if (ageSec > threshold) {
126
+ needsNudge = true;
127
+ message = `Heads up: your \`task\` field hasn't changed in ${ageSec}s (threshold ${threshold}s). If you've moved on from "${taskValue.slice(0, 60)}", update via \`agents set-task "<new focus>"\`. Pass an empty string to clear.`;
128
+ }
129
+ }
130
+ }
131
+
132
+ const hashFile = join(coordRoot, ".harnery", `.last-task-nudge-hash.${selfInstanceId}`);
133
+ if (!needsNudge) {
134
+ try {
135
+ if (existsSync(hashFile)) rmSync(hashFile, { force: true });
136
+ } catch {
137
+ /* swallow */
138
+ }
139
+ return "";
140
+ }
141
+
142
+ // Dedup on a state-hash (task value + threshold) so same-state turns don't re-nudge.
143
+ const state = `${taskValue}|stale=1|threshold=${threshold}`;
144
+ const newHash = sha256Hex16(state);
145
+ const oldHash = safeRead(hashFile);
146
+ if (oldHash && oldHash === newHash) return "";
147
+ writeHashFile(hashFile, newHash);
148
+ return message;
149
+ }
150
+
151
+ function computePeerTableIfChanged(
152
+ coordRoot: string,
153
+ selfInstanceId: string,
154
+ selfSessionIdFallback: string,
155
+ ): string {
156
+ const activeDir = join(coordRoot, ".harnery", "active");
157
+ if (!existsSync(activeDir)) return "";
158
+
159
+ // Read self heartbeat for session_id (group key); fall back to the caller's hint.
160
+ let mySessionId = selfSessionIdFallback;
161
+ const selfHb = readHeartbeat(join(activeDir, `${selfInstanceId}.json`));
162
+ if (selfHb?.session_id) mySessionId = selfHb.session_id;
163
+ if (!mySessionId) return "";
164
+
165
+ // Collect peer heartbeats.
166
+ const peers: HeartbeatRow[] = [];
167
+ for (const f of readdirSync(activeDir)) {
168
+ if (!f.endsWith(".json")) continue;
169
+ const hb = readHeartbeat(join(activeDir, f));
170
+ if (!hb?.instance_id || hb.instance_id === selfInstanceId) continue;
171
+ peers.push(hb);
172
+ }
173
+ if (peers.length === 0) return "";
174
+
175
+ // Build hash basis: sorted-by-instance_id projection of semantically-relevant fields.
176
+ const basis = peers
177
+ .map((p) => ({
178
+ name: p.name ?? null,
179
+ instance_id: p.instance_id ?? null,
180
+ session_id: p.session_id ?? null,
181
+ kind: p.kind ?? null,
182
+ started_at: p.started_at ?? null,
183
+ files_touched: Array.from(p.files_touched ?? []).sort(),
184
+ platform: p.platform ?? null,
185
+ }))
186
+ .sort((a, b) => (a.instance_id ?? "").localeCompare(b.instance_id ?? ""));
187
+ const newHash = sha256Hex16(JSON.stringify(basis));
188
+
189
+ const hashFile = join(coordRoot, ".harnery", `.last-peer-hash.${selfInstanceId}`);
190
+ const oldHash = safeRead(hashFile);
191
+ if (oldHash && oldHash === newHash) return "";
192
+
193
+ // Render peer table via the same formatter used at SessionStart.
194
+ const table = formatPeerTable(peers, mySessionId);
195
+ if (!table) return "";
196
+
197
+ // Persist hash atomically (temp + rename, same convention as other coord writes).
198
+ writeHashFile(hashFile, newHash);
199
+ return table;
200
+ }
201
+
202
+ function computeCouncilPendingIfChanged(
203
+ coordRoot: string,
204
+ selfInstanceId: string,
205
+ agentName: string,
206
+ ): string {
207
+ const councilsDir = join(coordRoot, ".harnery", "councils");
208
+ if (!existsSync(councilsDir)) return "";
209
+ const canonicalName = agentName.startsWith("agent-") ? agentName : `agent-${agentName}`;
210
+
211
+ // Collect pending council IDs (open councils where I'm a member and haven't contributed).
212
+ const pendingIds: string[] = [];
213
+ try {
214
+ for (const f of readdirSync(councilsDir)) {
215
+ if (!f.endsWith(".json")) continue;
216
+ try {
217
+ const m = JSON.parse(readFileSync(join(councilsDir, f), "utf8")) as {
218
+ council_id?: string;
219
+ status?: string;
220
+ round_status?: string;
221
+ current_round?: number;
222
+ members?: string[];
223
+ };
224
+ if (m.status !== "active" || m.round_status !== "open") continue;
225
+ if (!m.council_id || !m.members?.includes(canonicalName)) continue;
226
+ const round = m.current_round ?? 1;
227
+ const contributionPath = join(
228
+ councilsDir,
229
+ m.council_id,
230
+ `round-${round}`,
231
+ `${canonicalName}.md`,
232
+ );
233
+ if (existsSync(contributionPath)) continue;
234
+ pendingIds.push(m.council_id);
235
+ } catch {
236
+ /* skip */
237
+ }
238
+ }
239
+ } catch {
240
+ return "";
241
+ }
242
+ pendingIds.sort();
243
+
244
+ const hashFile = join(coordRoot, ".harnery", `.last-council-hash.${selfInstanceId}`);
245
+ const newHash = sha256Hex16(pendingIds.join("\n"));
246
+ const oldHash = safeRead(hashFile);
247
+
248
+ // Clear hash file when no councils pending, matching bash behavior.
249
+ if (pendingIds.length === 0) {
250
+ try {
251
+ if (existsSync(hashFile)) rmSync(hashFile, { force: true });
252
+ } catch {
253
+ /* swallow */
254
+ }
255
+ return "";
256
+ }
257
+
258
+ // Always update hash file when pending councils exist (eager-rewrite, matches bash).
259
+ writeHashFile(hashFile, newHash);
260
+ if (oldHash && oldHash === newHash) return "";
261
+
262
+ return formatPendingCouncils(coordRoot, agentName);
263
+ }
264
+
265
+ /* ---------- helpers ---------- */
266
+
267
+ function readHeartbeat(path: string): HeartbeatRow | null {
268
+ try {
269
+ return JSON.parse(readFileSync(path, "utf8")) as HeartbeatRow;
270
+ } catch {
271
+ return null;
272
+ }
273
+ }
274
+
275
+ function safeRead(path: string): string {
276
+ try {
277
+ return readFileSync(path, "utf8").trim();
278
+ } catch {
279
+ return "";
280
+ }
281
+ }
282
+
283
+ function sha256Hex16(input: string): string {
284
+ return createHash("sha256").update(input).digest("hex").slice(0, 16);
285
+ }
286
+
287
+ function writeHashFile(path: string, value: string): void {
288
+ try {
289
+ mkdirSync(join(path, ".."), { recursive: true });
290
+ const tmp = `${path}.${process.pid}.tmp`;
291
+ writeFileSync(tmp, value, "utf8");
292
+ renameSync(tmp, path);
293
+ } catch {
294
+ /* swallow */
295
+ }
296
+ }
297
+
298
+ /* The peer-table formatter is a near-duplicate of session-context.ts's, but
299
+ * intentionally co-located here so the prompt-context renderer is self-contained
300
+ * (no cross-file imports of formatting internals). If both renderers diverge,
301
+ * pull the shared bits into a small util module. */
302
+
303
+ function formatPeerTable(peers: HeartbeatRow[], mySessionId: string): string {
304
+ if (peers.length === 0) return "";
305
+ const nowSec = Math.floor(Date.now() / 1000);
306
+
307
+ const fold: Record<string, string[]> = {};
308
+ for (const p of peers) {
309
+ const kind = p.kind ?? "unknown";
310
+ if (kind === "transient" && p.session_id) {
311
+ fold[p.session_id] = (fold[p.session_id] ?? []).concat(p.files_touched ?? []);
312
+ }
313
+ }
314
+
315
+ type RowExt = HeartbeatRow & { display_files: string[] };
316
+ const rows: RowExt[] = peers
317
+ .filter((p) => (p.kind ?? "unknown") !== "transient")
318
+ .map((p) => {
319
+ const folded = fold[p.instance_id ?? ""] ?? [];
320
+ const display = Array.from(new Set([...(p.files_touched ?? []), ...folded])).sort();
321
+ return { ...p, display_files: display };
322
+ });
323
+
324
+ const blocking = rows.filter((p) => p.session_id !== mySessionId).sort(byStartedAt);
325
+ const group = rows.filter((p) => p.session_id === mySessionId).sort(byStartedAt);
326
+
327
+ const out: string[] = [];
328
+ const blk = renderSubtable(
329
+ blocking,
330
+ "Other agent groups active (their files block you):",
331
+ nowSec,
332
+ );
333
+ if (blk) out.push(blk);
334
+ const grp = renderSubtable(
335
+ group,
336
+ "Your group (subagents / parent / siblings; no mutual block):",
337
+ nowSec,
338
+ );
339
+ if (grp) out.push(grp);
340
+ return out.join("\n\n");
341
+ }
342
+
343
+ function byStartedAt(a: HeartbeatRow, b: HeartbeatRow): number {
344
+ return (a.started_at ?? "").localeCompare(b.started_at ?? "");
345
+ }
346
+
347
+ function renderSubtable(
348
+ rows: Array<HeartbeatRow & { display_files: string[] }>,
349
+ header: string,
350
+ nowSec: number,
351
+ ): string {
352
+ if (rows.length === 0) return "";
353
+ const first = rows.slice(0, 10).map((r) => formatRow(r, nowSec));
354
+ const overflow = rows.length > 10 ? `\n +${rows.length - 10} more` : "";
355
+ return `${header}\n${first.join("\n")}${overflow}`;
356
+ }
357
+
358
+ function formatRow(r: HeartbeatRow & { display_files: string[] }, nowSec: number): string {
359
+ const taskPart = r.task ? ` "${r.task.slice(0, 60)}"` : "";
360
+ // Fall back started_at → last_heartbeat; if neither is a valid timestamp,
361
+ // show "age unknown" rather than the epoch-derived "20608d ago" ghost.
362
+ const startedSec = parseIsoSec(r.started_at) ?? parseIsoSec(r.last_heartbeat);
363
+ const ageFrom = startedSec === null ? "age unknown" : fmtAge(nowSec - startedSec);
364
+ const filesPart = fmtFiles(r.display_files);
365
+ const lastActivity = fmtLastActivity(r, nowSec);
366
+ const turnSummary = r.turn_summary ? `\n last turn: ${r.turn_summary.slice(0, 80)}` : "";
367
+ // Prefer a short instance_id over a bare "unknown" so an incomplete row is
368
+ // still identifiable.
369
+ const label = r.name ?? (r.instance_id ? r.instance_id.slice(0, 8) : "unknown");
370
+ return ` - agent-${label}${taskPart} (${ageFrom}, ${filesPart}${lastActivity})${turnSummary}`;
371
+ }
372
+
373
+ function fmtFiles(files: string[]): string {
374
+ if (files.length === 0) return "nothing yet";
375
+ if (files.length <= 3) return `holds: ${files.join(", ")}`;
376
+ return `holds: ${files.slice(0, 3).join(", ")}, +${files.length - 3} more`;
377
+ }
378
+
379
+ function fmtLastActivity(r: HeartbeatRow, nowSec: number): string {
380
+ if (!r.last_tool) return "";
381
+ const lastTs = parseIsoSec(r.last_heartbeat) ?? parseIsoSec(r.started_at);
382
+ const tail = r.last_tool_target ? ` ${r.last_tool_target.slice(0, 60)}` : "";
383
+ // No valid timestamp → render the tool without an absurd age.
384
+ if (lastTs === null) return `, last: ${r.last_tool}${tail}`;
385
+ return `, last: ${r.last_tool}${tail} ${fmtAge(nowSec - lastTs)}`;
386
+ }
387
+
388
+ /** Epoch-seconds for an ISO string, or null when missing/unparseable (so callers
389
+ * can render "age unknown" instead of an epoch-derived absurd age). */
390
+ function parseIsoSec(iso: string | undefined): number | null {
391
+ if (!iso) return null;
392
+ const ms = Date.parse(iso);
393
+ return Number.isFinite(ms) ? Math.floor(ms / 1000) : null;
394
+ }
395
+
396
+ function fmtAge(secs: number): string {
397
+ if (secs < 60) return `${Math.floor(secs)}s ago`;
398
+ if (secs < 3600) return `${Math.floor(secs / 60)}m ago`;
399
+ if (secs < 86400) return `${Math.floor(secs / 3600)}h ago`;
400
+ return `${Math.floor(secs / 86400)}d ago`;
401
+ }