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,64 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ import type { Command } from "commander";
3
+ import type { EmitContext } from "../commander.ts";
4
+ import { resolveBinName } from "../core/config.ts";
5
+ import { htmlToMarkdown } from "../lib/readability/index.ts";
6
+
7
+ // Module-scoped emit assigned by registerReadCommand. Same pattern as cookies.ts:
8
+ // the runRead helper closes over this so the .action callback stays concise.
9
+ let emit: EmitContext;
10
+
11
+ /**
12
+ * `harn read`: extract clean readable markdown from HTML.
13
+ */
14
+ export function registerReadCommand(program: Command, emitParam: EmitContext): void {
15
+ emit = emitParam;
16
+ program
17
+ .command("read [html-file]")
18
+ .description(
19
+ `Extract clean readable markdown from HTML. Reads from file or stdin (use '-'). Pair with \`${resolveBinName()} fetch\` or \`${resolveBinName()} browse\` for scrape-to-markdown.`,
20
+ )
21
+ .option("-o, --output <file>", "Write markdown to file instead of stdout")
22
+ .option("--url <url>", "Base URL: used to resolve relative links")
23
+ .option(
24
+ "--selector <css>",
25
+ "Use this CSS selector instead of Readability (fallback when extraction misses content)",
26
+ )
27
+ .option("--raw", "Output cleaned HTML instead of markdown (debugging)")
28
+ .option("--max-chars <n>", "Truncate output to N characters (0 = disable)", "100000")
29
+ .action((htmlFile: string | undefined, opts: ReadOpts) => {
30
+ try {
31
+ runRead(htmlFile, opts);
32
+ } catch (err: unknown) {
33
+ const msg = err instanceof Error ? err.message : String(err);
34
+ emit.error({ code: "read_error", message: msg });
35
+ process.exit(1);
36
+ }
37
+ });
38
+ }
39
+
40
+ interface ReadOpts {
41
+ output?: string;
42
+ url?: string;
43
+ selector?: string;
44
+ raw?: boolean;
45
+ maxChars: string;
46
+ }
47
+
48
+ function runRead(htmlFile: string | undefined, opts: ReadOpts): void {
49
+ const input =
50
+ htmlFile && htmlFile !== "-" ? readFileSync(htmlFile, "utf-8") : readFileSync(0, "utf-8");
51
+ const result = htmlToMarkdown(input, {
52
+ url: opts.url,
53
+ selector: opts.selector,
54
+ raw: opts.raw,
55
+ maxChars: Number.parseInt(opts.maxChars, 10),
56
+ });
57
+
58
+ if (opts.output) {
59
+ writeFileSync(opts.output, result.output);
60
+ emit.file(opts.output, { chars: result.output.length });
61
+ } else {
62
+ emit.text(result.output);
63
+ }
64
+ }
@@ -0,0 +1,445 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync, unlinkSync, watch } from "node:fs";
2
+ import { resolve as resolvePath } from "node:path";
3
+ import type { Command } from "commander";
4
+ import type { EmitContext } from "../commander.ts";
5
+ import { emitCanonical, normalizeHarness, readHeartbeat } from "../core/agents/index.ts";
6
+ import { resolveBinName } from "../core/config.ts";
7
+ import {
8
+ appendEntry,
9
+ archiveScratch,
10
+ currentOwnerOrThrow,
11
+ lintScratch,
12
+ listArchives,
13
+ loadScratch,
14
+ parseScratch,
15
+ pruneArchives,
16
+ resolveOwnerByName,
17
+ SCRATCH_CATEGORIES,
18
+ type ScratchCategory,
19
+ type ScratchDoc,
20
+ scratchDir,
21
+ scratchPath,
22
+ sweepOrphanScratchpads,
23
+ } from "../lib/scratch/index.ts";
24
+
25
+ /**
26
+ * `harn scratch`: per-agent markdown journal at `.harnery/scratch/<instance_id>.md`.
27
+ * Used both for self-notes (surviving compaction) and peer coordination (other
28
+ * agents pull-read it on demand). SessionEnd hook archives; SessionStart
29
+ * janitor surfaces the most-recent archive as a recovery cue.
30
+ */
31
+ let emit: EmitContext;
32
+
33
+ export function registerScratchCommand(program: Command, emitParam: EmitContext): void {
34
+ emit = emitParam;
35
+ const root = program
36
+ .command("scratch")
37
+ .description(
38
+ "Per-agent markdown journal: append-only timestamped entries. " +
39
+ "Survives in-session compaction, archived at session end, pruned after 7 days.",
40
+ );
41
+
42
+ // ── add ───────────────────────────────────────────────────────────────
43
+ root
44
+ .command("add <category> <text...>")
45
+ .description(`Append an entry to my scratchpad. Category: ${SCRATCH_CATEGORIES.join(" | ")}`)
46
+ .action((category: string, text: string[]) => {
47
+ const cat = validateCategory(category);
48
+ const body = text.join(" ");
49
+ if (!body.trim()) {
50
+ emit.error({ code: "empty_body", message: "scratch add: body is empty" });
51
+ process.exit(1);
52
+ }
53
+ try {
54
+ const owner = currentOwnerOrThrow();
55
+ const doc = appendEntry(owner, cat, body);
56
+ const hb = readHeartbeat(owner);
57
+ emitCanonical({
58
+ type: "state.scratch_append",
59
+ owner,
60
+ session: hb?.session_id ?? owner,
61
+ harness: normalizeHarness(hb?.platform),
62
+ data: {
63
+ category: cat,
64
+ body_summary: body.length > 1000 ? `${body.slice(0, 997)}...` : body,
65
+ },
66
+ });
67
+ emit.data({
68
+ instance_id: owner,
69
+ name: doc.header.name,
70
+ category: cat,
71
+ entries: doc.entries.length,
72
+ bytes: doc.bytes,
73
+ path: doc.path,
74
+ });
75
+ } catch (err) {
76
+ emit.error({ code: "add_failed", message: (err as Error).message });
77
+ process.exit(1);
78
+ }
79
+ });
80
+
81
+ // ── read ──────────────────────────────────────────────────────────────
82
+ root
83
+ .command("read")
84
+ .description("Render a scratchpad to stdout. No --name: my own. With --name: a peer's.")
85
+ .option("--name <name>", "Read the named peer's scratchpad (case-insensitive)")
86
+ .option("--owner <id>", "Read by instance_id directly")
87
+ .option("--archive <basename>", "Read an archive file (e.g. <owner>-<ts>.md)")
88
+ .option("--limit <n>", "Cap entries rendered (newest first)", "50")
89
+ .action((opts: { name?: string; owner?: string; archive?: string; limit: string }) => {
90
+ try {
91
+ runRead(opts);
92
+ } catch (err) {
93
+ emit.error({ code: "read_failed", message: (err as Error).message });
94
+ process.exit(1);
95
+ }
96
+ });
97
+
98
+ // ── list ──────────────────────────────────────────────────────────────
99
+ root
100
+ .command("list")
101
+ .description("Summarize all active scratchpads + archive count")
102
+ .option("--archives", "List archive files instead of active scratchpads")
103
+ .action((opts: { archives?: boolean }) => {
104
+ try {
105
+ if (opts.archives) {
106
+ const items = listArchives().map((a) => {
107
+ const doc = parseSafe(a.path);
108
+ const lastEntry = doc?.entries[0];
109
+ const ageMin = Math.floor((Date.now() - a.mtimeMs) / 60000);
110
+ return {
111
+ basename: a.basename,
112
+ name: doc?.header.name ?? "unknown",
113
+ entries: doc?.entries.length ?? 0,
114
+ bytes: a.bytes,
115
+ archived_min_ago: ageMin,
116
+ last_category: lastEntry?.category ?? null,
117
+ last_ts: lastEntry?.ts_display ?? null,
118
+ };
119
+ });
120
+ emit.data({ rows: items });
121
+ return;
122
+ }
123
+ const dir = scratchDir();
124
+ const rows: Array<{
125
+ instance_id: string;
126
+ name: string;
127
+ entries: number;
128
+ bytes: number;
129
+ last_category: string | null;
130
+ last_ts: string | null;
131
+ }> = [];
132
+ for (const f of readdirSync(dir)) {
133
+ if (!f.endsWith(".md")) continue;
134
+ const instanceId = f.replace(/\.md$/, "");
135
+ const doc = loadScratch(instanceId);
136
+ if (!doc) continue;
137
+ rows.push({
138
+ instance_id: instanceId,
139
+ name: doc.header.name,
140
+ entries: doc.entries.length,
141
+ bytes: doc.bytes,
142
+ last_category: doc.entries[0]?.category ?? null,
143
+ last_ts: doc.entries[0]?.ts_display ?? null,
144
+ });
145
+ }
146
+ rows.sort((a, b) => (b.last_ts ?? "").localeCompare(a.last_ts ?? ""));
147
+ emit.data({ rows });
148
+ } catch (err) {
149
+ emit.error({ code: "list_failed", message: (err as Error).message });
150
+ process.exit(1);
151
+ }
152
+ });
153
+
154
+ // ── tail ──────────────────────────────────────────────────────────────
155
+ root
156
+ .command("tail")
157
+ .description("Follow my scratchpad (or a peer's) for new entries.")
158
+ .option("--name <name>", "Tail the named peer's scratchpad")
159
+ .option("--owner <id>", "Tail by instance_id directly")
160
+ .action(async (opts: { name?: string; owner?: string }) => {
161
+ try {
162
+ const owner = resolveTargetOwner(opts);
163
+ const path = scratchPath(owner);
164
+ if (!existsSync(path)) {
165
+ emit.error({ code: "no_scratchpad", message: `no scratchpad at ${path} yet` });
166
+ process.exit(1);
167
+ }
168
+ process.stderr.write(`tailing ${path}\n`); // lint-ok-emission: tail banner, immediate stderr flush before streaming
169
+ let lastSize = statSync(path).size;
170
+ // Print initial render
171
+ process.stdout.write(`${renderScratch(loadScratch(owner)!, 10)}\n`); // lint-ok-emission: streaming tail body
172
+ const w = watch(path, { persistent: true }, () => {
173
+ try {
174
+ const curr = statSync(path).size;
175
+ if (curr === lastSize) return;
176
+ lastSize = curr;
177
+ const doc = loadScratch(owner);
178
+ if (!doc) return;
179
+ const newest = doc.entries[0];
180
+ if (!newest) return;
181
+ const line = `\n## ${newest.ts_display} · ${newest.category}\n${newest.body}\n`;
182
+ process.stdout.write(line); // lint-ok-emission: streaming tail, per-line stdout flush, no envelope wrap
183
+ } catch {
184
+ // ignore transient
185
+ }
186
+ });
187
+ await new Promise<void>((resolveP) => {
188
+ const stop = () => {
189
+ w.close();
190
+ resolveP();
191
+ };
192
+ process.on("SIGINT", stop);
193
+ process.on("SIGTERM", stop);
194
+ });
195
+ } catch (err) {
196
+ emit.error({ code: "tail_failed", message: (err as Error).message });
197
+ process.exit(1);
198
+ }
199
+ });
200
+
201
+ // ── clear ─────────────────────────────────────────────────────────────
202
+ root
203
+ .command("clear")
204
+ .description("Delete my scratchpad (rare; mainly for testing)")
205
+ .option("--yes", "Confirm deletion")
206
+ .action((opts: { yes?: boolean }) => {
207
+ if (!opts.yes) {
208
+ emit.error({ code: "needs_yes", message: "pass --yes to confirm" });
209
+ process.exit(1);
210
+ }
211
+ try {
212
+ const owner = currentOwnerOrThrow();
213
+ const path = scratchPath(owner);
214
+ if (existsSync(path)) {
215
+ unlinkSync(path);
216
+ emit.data({ cleared: true, path });
217
+ } else {
218
+ emit.data({ cleared: false, path, reason: "did not exist" });
219
+ }
220
+ } catch (err) {
221
+ emit.error({ code: "clear_failed", message: (err as Error).message });
222
+ process.exit(1);
223
+ }
224
+ });
225
+
226
+ // ── lint ──────────────────────────────────────────────────────────────
227
+ root
228
+ .command("lint")
229
+ .description("Validate scratchpad format + size")
230
+ .option("--all", "Lint every scratchpad in .harnery/scratch/")
231
+ .option("--owner <id>", "Lint a specific owner's scratchpad")
232
+ .action((opts: { all?: boolean; owner?: string }) => {
233
+ try {
234
+ runLint(opts);
235
+ } catch (err) {
236
+ emit.error({ code: "lint_failed", message: (err as Error).message });
237
+ process.exit(1);
238
+ }
239
+ });
240
+
241
+ // ── archive (manual / hook helper) ────────────────────────────────────
242
+ root
243
+ .command("archive")
244
+ .description("Archive my scratchpad now (idempotent; fired by SessionEnd hook)")
245
+ .option("--owner <id>", "Archive a specific owner's scratchpad")
246
+ .action((opts: { owner?: string }) => {
247
+ try {
248
+ const owner = opts.owner ?? currentOwnerOrThrow();
249
+ const dest = archiveScratch(owner);
250
+ emit.data({ instance_id: owner, archived: !!dest, path: dest });
251
+ } catch (err) {
252
+ emit.error({ code: "archive_failed", message: (err as Error).message });
253
+ process.exit(1);
254
+ }
255
+ });
256
+
257
+ // ── recovery-cue (SessionStart hook helper) ───────────────────────────
258
+ root
259
+ .command("recovery-cue")
260
+ .description(
261
+ "Emit a one-line recovery hint to stdout when a recent archive exists. " +
262
+ "Used by SessionStart hook to surface 'previous session was doing X'. " +
263
+ "Stays silent when no relevant archive (no noise on fresh sessions).",
264
+ )
265
+ .option("--max-age-hours <n>", "Only surface archives newer than this", "24")
266
+ .action((opts: { maxAgeHours: string }) => {
267
+ try {
268
+ const maxHours = Number.parseInt(opts.maxAgeHours, 10);
269
+ const archives = listArchives();
270
+ if (archives.length === 0) return;
271
+ const newest = archives[0];
272
+ const ageHours = (Date.now() - newest.mtimeMs) / 3600_000;
273
+ if (ageHours > maxHours) return;
274
+ const doc = parseSafe(newest.path);
275
+ if (!doc || doc.entries.length === 0) return;
276
+ const last = doc.entries[0];
277
+ const ageMin = Math.floor((Date.now() - newest.mtimeMs) / 60_000);
278
+ const ageStr = ageMin < 60 ? `${ageMin}m` : `${Math.floor(ageMin / 60)}h ${ageMin % 60}m`;
279
+ const bodyOneLine = last.body.replace(/\s+/g, " ").trim();
280
+ const bodyTrunc = bodyOneLine.length > 100 ? `${bodyOneLine.slice(0, 99)}…` : bodyOneLine;
281
+ const cue =
282
+ `Recent scratchpad archive (${ageStr} ago): agent-${doc.header.name}: ` +
283
+ `last entry [${last.category}] "${bodyTrunc}". ` +
284
+ `Read full: \`${resolveBinName()} scratch read --archive ${newest.basename}\`.`;
285
+ process.stdout.write(`${cue}\n`); // lint-ok-emission: bash hook reads stdout to compose SessionStart additionalContext
286
+ } catch (_err) {
287
+ // Recovery cue is best-effort; never fail the SessionStart hook.
288
+ }
289
+ });
290
+
291
+ // ── janitor (SessionStart hook) ───────────────────────────────────────
292
+ root
293
+ .command("janitor")
294
+ .description(
295
+ "Prune old archives + sweep orphan scratchpads (heartbeat gone). Fired by SessionStart hook.",
296
+ )
297
+ .option("--days <n>", "Archive retention in days", "7")
298
+ .option("--quiet", "Suppress stdout output")
299
+ .action((opts: { days: string; quiet?: boolean }) => {
300
+ try {
301
+ const days = Number.parseInt(opts.days, 10);
302
+ const swept = sweepOrphanScratchpads();
303
+ const pruned = pruneArchives(days);
304
+ if (!opts.quiet) {
305
+ emit.data({
306
+ archives_pruned: pruned,
307
+ orphans_swept: swept.length,
308
+ orphans: swept,
309
+ });
310
+ }
311
+ } catch (err) {
312
+ emit.error({ code: "janitor_failed", message: (err as Error).message });
313
+ process.exit(1);
314
+ }
315
+ });
316
+ }
317
+
318
+ // ─── runRead ──────────────────────────────────────────────────────────────
319
+
320
+ function runRead(opts: { name?: string; owner?: string; archive?: string; limit: string }): void {
321
+ const limit = Number.parseInt(opts.limit, 10);
322
+ if (opts.archive) {
323
+ const dir = scratchDir();
324
+ const path = resolvePath(dir, "archived", opts.archive);
325
+ if (!existsSync(path)) {
326
+ emit.error({ code: "no_archive", message: `archive not found: ${opts.archive}` });
327
+ process.exit(1);
328
+ }
329
+ const content = readFileSync(path, "utf8");
330
+ process.stdout.write(content); // lint-ok-emission: archive render, file content is already the rendered form
331
+ return;
332
+ }
333
+
334
+ const owner = resolveTargetOwner(opts);
335
+ const doc = loadScratch(owner);
336
+ if (!doc) {
337
+ emit.error({
338
+ code: "no_scratchpad",
339
+ message: `no scratchpad for owner=${owner.slice(0, 8)}… (file not created yet)`,
340
+ });
341
+ process.exit(1);
342
+ }
343
+
344
+ const hb = readHeartbeat(owner);
345
+ const staleBanner = renderStaleBannerIfNeeded(hb?.last_heartbeat);
346
+ process.stdout.write(`${staleBanner + renderScratch(doc, limit)}\n`); // lint-ok-emission: pretty render, multi-line markdown for direct TTY/pipe consumption
347
+ }
348
+
349
+ function runLint(opts: { all?: boolean; owner?: string }): void {
350
+ const dir = scratchDir();
351
+ const files: string[] = [];
352
+ if (opts.owner) {
353
+ files.push(scratchPath(opts.owner));
354
+ } else if (opts.all) {
355
+ for (const f of readdirSync(dir)) {
356
+ if (f.endsWith(".md")) files.push(resolvePath(dir, f));
357
+ }
358
+ } else {
359
+ const owner = currentOwnerOrThrow();
360
+ files.push(scratchPath(owner));
361
+ }
362
+ let totalIssues = 0;
363
+ const reports: Array<{ path: string; issues: { line: number; message: string }[] }> = [];
364
+ for (const path of files) {
365
+ if (!existsSync(path)) {
366
+ reports.push({ path, issues: [{ line: 0, message: "(file does not exist)" }] });
367
+ continue;
368
+ }
369
+ const content = readFileSync(path, "utf8");
370
+ const issues = lintScratch(content);
371
+ if (issues.length > 0) totalIssues += issues.length;
372
+ reports.push({ path, issues });
373
+ }
374
+ emit.data({ files: reports.length, total_issues: totalIssues, reports });
375
+ if (totalIssues > 0) process.exit(1);
376
+ }
377
+
378
+ // ─── Helpers ──────────────────────────────────────────────────────────────
379
+
380
+ function validateCategory(c: string): ScratchCategory {
381
+ if (!(SCRATCH_CATEGORIES as readonly string[]).includes(c)) {
382
+ emit.error({
383
+ code: "bad_category",
384
+ message: `category must be one of: ${SCRATCH_CATEGORIES.join(", ")}`,
385
+ });
386
+ process.exit(1);
387
+ }
388
+ return c as ScratchCategory;
389
+ }
390
+
391
+ function resolveTargetOwner(opts: { name?: string; owner?: string }): string {
392
+ if (opts.owner) return opts.owner;
393
+ if (opts.name) {
394
+ const id = resolveOwnerByName(opts.name);
395
+ if (!id) {
396
+ emit.error({
397
+ code: "no_match",
398
+ message: `no live agent named "${opts.name}". Try \`${resolveBinName()} agents list\`.`,
399
+ });
400
+ process.exit(1);
401
+ }
402
+ return id;
403
+ }
404
+ return currentOwnerOrThrow();
405
+ }
406
+
407
+ function renderScratch(doc: ScratchDoc, limit: number): string {
408
+ const lines: string[] = [];
409
+ lines.push(`# Scratchpad: agent-${doc.header.name}`);
410
+ if (doc.header.session_id) lines.push(`session_id: ${doc.header.session_id}`);
411
+ if (doc.header.machine) lines.push(`machine: ${doc.header.machine}`);
412
+ if (doc.header.started) lines.push(`started: ${doc.header.started}`);
413
+ if (doc.header.last_updated) lines.push(`last_updated: ${doc.header.last_updated}`);
414
+ lines.push("\n---\n");
415
+ for (const entry of doc.entries.slice(0, limit)) {
416
+ lines.push(`## ${entry.ts_display} · ${entry.category}`);
417
+ if (entry.body) lines.push(entry.body);
418
+ lines.push("");
419
+ }
420
+ if (doc.entries.length > limit) {
421
+ lines.push(`(+${doc.entries.length - limit} older entries; raise --limit to see them)`);
422
+ }
423
+ return lines.join("\n").trimEnd();
424
+ }
425
+
426
+ const FRESHNESS_SECS = 600;
427
+ function renderStaleBannerIfNeeded(lastHeartbeat: string | undefined): string {
428
+ if (!lastHeartbeat) return "";
429
+ const ts = Date.parse(lastHeartbeat);
430
+ if (!Number.isFinite(ts)) return "";
431
+ const ageSec = Math.floor((Date.now() - ts) / 1000);
432
+ if (ageSec < FRESHNESS_SECS) return "";
433
+ const m = Math.floor(ageSec / 60);
434
+ return `[STALE: heartbeat ${m}m old, agent may be dead]\n\n`;
435
+ }
436
+
437
+ function parseSafe(path: string): ScratchDoc | null {
438
+ try {
439
+ if (!existsSync(path)) return null;
440
+ const content = readFileSync(path, "utf8");
441
+ return parseScratch(path, content);
442
+ } catch {
443
+ return null;
444
+ }
445
+ }
@@ -0,0 +1,187 @@
1
+ import { spawn } from "node:child_process";
2
+ import type { Command } from "commander";
3
+ import type { EmitContext } from "../commander.ts";
4
+ import { resolveOwner, selfDisplayName } from "../core/agents/index.ts";
5
+ import {
6
+ clampField,
7
+ newCmdId,
8
+ readLastIntent,
9
+ writeSessionEvent,
10
+ } from "../core/agents/session-events.ts";
11
+ import { resolveBinName } from "../core/config.ts";
12
+
13
+ /** Strip the `agent-` prefix so structured event `agent` field matches heartbeat.name. */
14
+ function bareAgentName(): string {
15
+ const full = selfDisplayName();
16
+ return full.startsWith("agent-") ? full.slice(6) : full;
17
+ }
18
+
19
+ function selfOwnerId(): string | undefined {
20
+ return resolveOwner() ?? undefined;
21
+ }
22
+
23
+ /**
24
+ * `harn session`: run a command (or narrate) and emit canonical coordination
25
+ * events for follow-along.
26
+ *
27
+ * Follow-along happens through the canonical `.harnery/events.ndjson` stream +
28
+ * the `/live` web viewer (or `harn agents watch`): `harn session -- <cmd>` runs
29
+ * the command, forwards its output to the terminal verbatim, and emits canonical
30
+ * `command.start/output/end`; `harn session log` emits a canonical `narration`
31
+ * event. The file-management subcommands (`tail`/`clear`/`trim`/`path`) are
32
+ * retired no-op stubs; `trim` is a silent no-op so a SessionStart hook that
33
+ * calls it keeps working.
34
+ *
35
+ * Usage:
36
+ * harn session "<intent>" -- <cmd> [args...] run command, emit command.* events
37
+ * harn session log "<message>" emit a narration event
38
+ */
39
+ let emit: EmitContext;
40
+
41
+ export function registerSessionCommand(program: Command, emitParam: EmitContext): void {
42
+ emit = emitParam;
43
+ const session = program
44
+ .command("session")
45
+ .description("Run a command (or narrate) and emit canonical coordination events");
46
+
47
+ // Narration-only entry → canonical narration event.
48
+ session
49
+ .command("log <message...>")
50
+ .description("Emit a narration event (no command run)")
51
+ .action((messageParts: string[]) => {
52
+ const message = messageParts.join(" ");
53
+ writeSessionEvent("narration", bareAgentName(), {
54
+ instance_id: selfOwnerId(),
55
+ message: clampField(message),
56
+ });
57
+ emit.text(`⋯ ${message}\n`);
58
+ });
59
+
60
+ // Retired file-management subcommands, kept as graceful no-op stubs so
61
+ // existing callers (hooks, muscle memory) don't error. `trim` is the one a
62
+ // hook invokes, so it must exit 0 silently.
63
+ session
64
+ .command("trim")
65
+ .description("(retired): no-op kept for hook compatibility")
66
+ .option("-y, --yes", "(ignored)")
67
+ .option("--max-entries <n>", "(ignored)")
68
+ .option("--max-bytes <size>", "(ignored)")
69
+ .allowUnknownOption()
70
+ .allowExcessArguments()
71
+ .action(() => {
72
+ /* silent no-op, exit 0 */
73
+ });
74
+
75
+ for (const name of ["tail", "clear", "path"] as const) {
76
+ session
77
+ .command(name)
78
+ .description(`(retired): watch /live or '${resolveBinName()} agents watch'`)
79
+ .allowUnknownOption()
80
+ .action(() => {
81
+ emit.text(
82
+ `Retired. Live activity flows to .harnery/events.ndjson; watch the /live web viewer or run '${resolveBinName()} agents watch'.\n`,
83
+ );
84
+ });
85
+ }
86
+
87
+ // Default form: harn session "<intent>" -- <cmd> [args]
88
+ session
89
+ .argument("<intent>", "One-line description of what this action is for")
90
+ .argument("<cmd...>", "The command to run (prefix with -- if it has its own flags)")
91
+ .allowUnknownOption()
92
+ .action((intent: string, cmd: string[]) => {
93
+ runLogged(intent, cmd);
94
+ });
95
+ }
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // Core
99
+ // ---------------------------------------------------------------------------
100
+
101
+ /**
102
+ * Strip ONLY carriage-return redraw noise (progress bars like `docker pull`'s
103
+ * sticky line) from a chunk before splitting into event lines. Terminal
104
+ * forwarding keeps the raw bytes; this only cleans the canonical event copy.
105
+ */
106
+ function stripCRNoise(text: string): string {
107
+ return text.replace(/[^\n]*\r(?!\n)/g, "");
108
+ }
109
+
110
+ function runLogged(intent: string, cmdArg: string[]): void {
111
+ // Strip a leading `--` separator if commander left one in.
112
+ const cmd = cmdArg[0] === "--" ? cmdArg.slice(1) : cmdArg;
113
+ if (cmd.length === 0) {
114
+ emit.error({ code: "usage", message: "Usage: harn session <intent> -- <cmd> [args...]" });
115
+ process.exit(2);
116
+ }
117
+
118
+ const cmdLine = cmd.map(quoteArg).join(" ");
119
+ const agent = bareAgentName();
120
+ const instanceId = selfOwnerId();
121
+ const cmdId = newCmdId();
122
+
123
+ // Intent precedence: explicit arg wins; else the PreToolUse-stamped intent
124
+ // file; else the command line itself.
125
+ const resolvedIntent =
126
+ intent && intent.trim().length > 0 ? intent : (readLastIntent(instanceId) ?? cmdLine);
127
+ writeSessionEvent("command_start", agent, {
128
+ instance_id: instanceId,
129
+ cmd_id: cmdId,
130
+ intent: clampField(resolvedIntent),
131
+ cmd: clampField(cmdLine),
132
+ });
133
+
134
+ const start = Date.now();
135
+ const child = spawn(cmd[0]!, cmd.slice(1), {
136
+ stdio: ["inherit", "pipe", "pipe"],
137
+ env: process.env,
138
+ });
139
+
140
+ child.stdout?.on("data", (chunk: Buffer) => {
141
+ process.stdout.write(chunk); // lint-ok-emission: harn session forwards child stdout verbatim; this IS the command's primary purpose
142
+ for (const line of stripCRNoise(chunk.toString("utf8")).split("\n")) {
143
+ if (line.length === 0) continue;
144
+ writeSessionEvent("output", agent, {
145
+ instance_id: instanceId,
146
+ cmd_id: cmdId,
147
+ stream: "stdout",
148
+ line: clampField(line),
149
+ });
150
+ }
151
+ });
152
+ child.stderr?.on("data", (chunk: Buffer) => {
153
+ process.stderr.write(chunk); // lint-ok-emission: harn session forwards child stderr verbatim; this IS the command's primary purpose
154
+ for (const line of stripCRNoise(chunk.toString("utf8")).split("\n")) {
155
+ if (line.length === 0) continue;
156
+ writeSessionEvent("output", agent, {
157
+ instance_id: instanceId,
158
+ cmd_id: cmdId,
159
+ stream: "stderr",
160
+ line: clampField(line),
161
+ });
162
+ }
163
+ });
164
+
165
+ child.on("error", (err) => {
166
+ process.stderr.write(`✗ spawn failed: ${err.message}\n`);
167
+ process.exit(127);
168
+ });
169
+
170
+ child.on("close", (code, signal) => {
171
+ const durationMs = Date.now() - start;
172
+ writeSessionEvent("command_end", agent, {
173
+ instance_id: instanceId,
174
+ cmd_id: cmdId,
175
+ exit: code ?? null,
176
+ signal: signal ?? null,
177
+ duration_ms: durationMs,
178
+ });
179
+ process.exit(code ?? 1);
180
+ });
181
+ }
182
+
183
+ /** Shell-safe quote an argument for human-readable logging (not for eval). */
184
+ function quoteArg(arg: string): string {
185
+ if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(arg)) return arg;
186
+ return `'${arg.replace(/'/g, `'\\''`)}'`;
187
+ }