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,174 @@
1
+ import type { Command } from "commander";
2
+ import type { EmitContext, HarneryProgramContext } from "../commander.ts";
3
+ import { initDocsContext as initDocs, scanDocs } from "../lib/docs.ts";
4
+ import { initDocsContext as initDocsIndex, runIndex } from "../lib/docs-index.ts";
5
+ import { initDocsContext as initDocsLint, runLint } from "../lib/docs-lint.ts";
6
+ import {
7
+ countColdHandoffs,
8
+ initDocsContext as initDocsSweep,
9
+ runSweep,
10
+ } from "../lib/docs-sweep.ts";
11
+
12
+ function ensureContext(context: HarneryProgramContext | undefined): void {
13
+ if (!context?.repoRoot || !context?.submodules) {
14
+ throw new Error("docs commands require harnery to be configured with repoRoot + submodules");
15
+ }
16
+ const opts = { repoRoot: context.repoRoot, submodules: context.submodules };
17
+ initDocs(opts);
18
+ initDocsIndex(opts);
19
+ initDocsLint({ ...opts, extraExcludedPrefixes: context.extraDocsExcludedPrefixes });
20
+ initDocsSweep(opts);
21
+ }
22
+
23
+ let emit: EmitContext;
24
+
25
+ export function registerDocsCommand(
26
+ program: Command,
27
+ emitParam: EmitContext,
28
+ context?: HarneryProgramContext,
29
+ ): void {
30
+ emit = emitParam;
31
+ const docs = program
32
+ .command("docs")
33
+ .description("Documentation tooling: freshness report, lint, sweep, index")
34
+ // Options on the group itself back the default (no-subcommand) behavior.
35
+ // See handleDocs below.
36
+ .option("--stale <days>", "Only show files not committed in N+ days", Number.parseInt)
37
+ .option("--dir <name>", "Filter to a specific top-level directory")
38
+ .option("--no-submodules", "Exclude submodule files; only show parent repo docs")
39
+ .option("--commits <n>", "Number of recent commits to show per file", Number.parseInt, 1)
40
+ .option("--format <type>", "Output format: table, csv, json", "table")
41
+ .action(
42
+ async (opts: {
43
+ stale?: number;
44
+ dir?: string;
45
+ submodules?: boolean;
46
+ commits: number;
47
+ format: string;
48
+ }) => {
49
+ try {
50
+ ensureContext(context);
51
+ await handleDocs(opts);
52
+ } catch (err: unknown) {
53
+ const msg = err instanceof Error ? err.message : String(err);
54
+ emit.error({ code: "docs_error", message: msg });
55
+ }
56
+ },
57
+ );
58
+
59
+ docs
60
+ .command("lint")
61
+ .description(
62
+ "Verify every repo matches the documentation contract (directory layout + naming rules)",
63
+ )
64
+ .option("--fast", "Skip content-reading checks; filename/structure only (for pre-commit)")
65
+ .option("--repo <name>", "Limit to one submodule or '.' for parent")
66
+ .option("--format <type>", "Output format: human, json", "human")
67
+ .action(async (opts: { fast?: boolean; repo?: string; format: string }) => {
68
+ try {
69
+ ensureContext(context);
70
+ await handleLint(opts);
71
+ } catch (err: unknown) {
72
+ const msg = err instanceof Error ? err.message : String(err);
73
+ emit.error({ code: "docs_error", message: msg });
74
+ }
75
+ });
76
+
77
+ docs
78
+ .command("sweep")
79
+ .description(
80
+ "Surface stalled lifecycle states: stalled plans, cold issues, unverified runbooks",
81
+ )
82
+ .option("--repo <name>", "Limit to one submodule or '.' for parent")
83
+ .option("--format <type>", "Output format: human, json", "human")
84
+ .action(async (opts: { repo?: string; format: string }) => {
85
+ try {
86
+ ensureContext(context);
87
+ await handleSweep(opts);
88
+ } catch (err: unknown) {
89
+ const msg = err instanceof Error ? err.message : String(err);
90
+ emit.error({ code: "docs_error", message: msg });
91
+ }
92
+ });
93
+
94
+ docs
95
+ .command("index")
96
+ .description("Regenerate index READMEs for docs/audits/ and docs/issues/ directories")
97
+ .option("--dry-run", "Show what would change, don't write files")
98
+ .option("--repo <name>", "Limit to one submodule or '.' for parent")
99
+ .action(async (opts: { dryRun?: boolean; repo?: string }) => {
100
+ try {
101
+ ensureContext(context);
102
+ await handleIndex(opts);
103
+ } catch (err: unknown) {
104
+ const msg = err instanceof Error ? err.message : String(err);
105
+ emit.error({ code: "docs_error", message: msg });
106
+ }
107
+ });
108
+ }
109
+
110
+ // --- Default `harn docs` (freshness report) ---
111
+
112
+ async function handleDocs(opts: {
113
+ stale?: number;
114
+ dir?: string;
115
+ submodules?: boolean;
116
+ commits: number;
117
+ format: string;
118
+ }): Promise<void> {
119
+ if (opts.format === "json") emit.config({ format: "json" });
120
+ else if (opts.format === "csv") emit.config({ format: "csv" });
121
+
122
+ const files = await scanDocs({
123
+ commitCount: opts.commits,
124
+ dir: opts.dir,
125
+ noSubmodules: opts.submodules === false,
126
+ staleDays: opts.stale,
127
+ });
128
+ emit.rows(files as unknown as Record<string, unknown>[]);
129
+ }
130
+
131
+ // --- `harn docs lint` ---
132
+
133
+ async function handleLint(opts: { fast?: boolean; repo?: string; format: string }): Promise<void> {
134
+ const violations = await runLint({ fast: opts.fast, repo: opts.repo });
135
+ const errors = violations.filter((v) => v.severity === "error");
136
+ const warnings = violations.filter((v) => v.severity === "warning");
137
+ const cold = await countColdHandoffs();
138
+
139
+ emit.data({
140
+ fast: !!opts.fast,
141
+ repo: opts.repo ?? null,
142
+ error_count: errors.length,
143
+ warning_count: warnings.length,
144
+ cold_handoffs: cold,
145
+ violations,
146
+ });
147
+
148
+ if (errors.length > 0) emit.setExitCode(1);
149
+ }
150
+
151
+ // --- `harn docs sweep` ---
152
+
153
+ async function handleSweep(opts: { repo?: string; format: string }): Promise<void> {
154
+ if (opts.format === "json") emit.config({ format: "json" });
155
+ const items = await runSweep({ repo: opts.repo });
156
+ emit.data(items);
157
+ }
158
+
159
+ // --- `harn docs index` ---
160
+
161
+ async function handleIndex(opts: { dryRun?: boolean; repo?: string }): Promise<void> {
162
+ const results = await runIndex({ dryRun: opts.dryRun, repo: opts.repo });
163
+ emit.data({
164
+ dry_run: !!opts.dryRun,
165
+ repo: opts.repo ?? null,
166
+ counts: {
167
+ updated: results.filter((r) => r.status === "updated").length,
168
+ created: results.filter((r) => r.status === "created").length,
169
+ needs_markers: results.filter((r) => r.status === "needs-markers").length,
170
+ unchanged: results.filter((r) => r.status === "unchanged").length,
171
+ },
172
+ results,
173
+ });
174
+ }
@@ -0,0 +1,231 @@
1
+ /**
2
+ * `harn doctor`: runtime + dependency check.
3
+ *
4
+ * Walks through every dependency harnery touches and reports presence,
5
+ * version, and OS-specific install hints. Returns a checklist:
6
+ *
7
+ * ✓ ok: dep is present and recent enough
8
+ * ⚠ warn: optional dep missing (feature degrades)
9
+ * ✗ fail: required dep missing (commands will throw)
10
+ *
11
+ * Exits 0 unless a required dep is missing.
12
+ */
13
+
14
+ import { spawnSync } from "node:child_process";
15
+ import { existsSync } from "node:fs";
16
+ import os from "node:os";
17
+ import path from "node:path";
18
+ import type { Command } from "commander";
19
+ import type { EmitContext } from "../commander.ts";
20
+
21
+ type Severity = "ok" | "warn" | "fail";
22
+
23
+ interface Check {
24
+ name: string;
25
+ severity: Severity;
26
+ detail: string;
27
+ hint?: string;
28
+ }
29
+
30
+ interface CheckOpts {
31
+ json?: boolean;
32
+ }
33
+
34
+ export function registerDoctorCommand(program: Command, emit: EmitContext): void {
35
+ program
36
+ .command("doctor")
37
+ .description(
38
+ "Verify the runtime + optional deps harnery commands expect. Exits 0 " +
39
+ "unless a required dep (Node, git) is missing.",
40
+ )
41
+ .option("--json", "Machine-readable JSON output")
42
+ .action((opts: CheckOpts) => {
43
+ const checks = runChecks();
44
+ const requiredFailed = checks.some((c) => c.severity === "fail");
45
+
46
+ if (opts.json) {
47
+ emit.data({
48
+ checks,
49
+ summary: {
50
+ total: checks.length,
51
+ ok: checks.filter((c) => c.severity === "ok").length,
52
+ warn: checks.filter((c) => c.severity === "warn").length,
53
+ fail: checks.filter((c) => c.severity === "fail").length,
54
+ },
55
+ });
56
+ emit.setExitCode(requiredFailed ? 1 : 0);
57
+ return;
58
+ }
59
+
60
+ const symbols: Record<Severity, string> = { ok: "✓", warn: "⚠", fail: "✗" };
61
+ const widest = Math.max(...checks.map((c) => c.name.length));
62
+ const lines: string[] = [];
63
+ for (const c of checks) {
64
+ lines.push(`${symbols[c.severity]} ${c.name.padEnd(widest)} ${c.detail}`);
65
+ if (c.hint) lines.push(` ↳ ${c.hint}`);
66
+ }
67
+
68
+ const summary = `\n${checks.filter((c) => c.severity === "ok").length} ok, ${
69
+ checks.filter((c) => c.severity === "warn").length
70
+ } warn, ${checks.filter((c) => c.severity === "fail").length} fail`;
71
+ emit.text(`${lines.join("\n")}${summary}`);
72
+ emit.setExitCode(requiredFailed ? 1 : 0);
73
+ });
74
+ }
75
+
76
+ export function runChecks(): Check[] {
77
+ return [
78
+ checkNode(),
79
+ checkGit(),
80
+ checkBun(),
81
+ checkHarneryDir(),
82
+ checkRestic(),
83
+ checkRclone(),
84
+ checkPlaywright(),
85
+ checkPython(),
86
+ ];
87
+ }
88
+
89
+ function whichVersion(bin: string, args: string[] = ["--version"]): { ok: boolean; out: string } {
90
+ const r = spawnSync(bin, args, { encoding: "utf-8" });
91
+ if (r.status !== 0) return { ok: false, out: "" };
92
+ const out = (r.stdout || r.stderr).trim().split("\n")[0];
93
+ return { ok: true, out };
94
+ }
95
+
96
+ function checkNode(): Check {
97
+ const v = process.versions.node;
98
+ const major = Number.parseInt(v.split(".")[0], 10);
99
+ if (Number.isNaN(major) || major < 20) {
100
+ return {
101
+ name: "node",
102
+ severity: "fail",
103
+ detail: `${v} (need ≥ 20)`,
104
+ hint: "https://nodejs.org/en/download",
105
+ };
106
+ }
107
+ return { name: "node", severity: "ok", detail: v };
108
+ }
109
+
110
+ function checkGit(): Check {
111
+ const r = whichVersion("git");
112
+ if (!r.ok) {
113
+ return {
114
+ name: "git",
115
+ severity: "fail",
116
+ detail: "missing",
117
+ hint: macOrLinux("brew install git", "apt-get install -y git"),
118
+ };
119
+ }
120
+ return { name: "git", severity: "ok", detail: r.out.replace(/^git version\s*/, "") };
121
+ }
122
+
123
+ function checkBun(): Check {
124
+ const r = whichVersion("bun");
125
+ if (!r.ok) {
126
+ return {
127
+ name: "bun",
128
+ severity: "warn",
129
+ detail: "missing (Node-only mode: fine, just slower than bun-native)",
130
+ hint: "curl -fsSL https://bun.sh/install | bash",
131
+ };
132
+ }
133
+ return { name: "bun", severity: "ok", detail: r.out };
134
+ }
135
+
136
+ function checkRestic(): Check {
137
+ const r = whichVersion("restic", ["version"]);
138
+ if (!r.ok) {
139
+ return {
140
+ name: "restic",
141
+ severity: "warn",
142
+ detail: "missing (needed for `harn backup`)",
143
+ hint: macOrLinux("brew install restic", "apt-get install -y restic"),
144
+ };
145
+ }
146
+ return { name: "restic", severity: "ok", detail: r.out };
147
+ }
148
+
149
+ function checkRclone(): Check {
150
+ const r = whichVersion("rclone", ["version"]);
151
+ if (!r.ok) {
152
+ return {
153
+ name: "rclone",
154
+ severity: "warn",
155
+ detail: "missing (needed for `harn sync`)",
156
+ hint: "curl https://rclone.org/install.sh | sudo bash",
157
+ };
158
+ }
159
+ // first line is "rclone v1.XX.X"
160
+ return { name: "rclone", severity: "ok", detail: r.out };
161
+ }
162
+
163
+ function checkPlaywright(): Check {
164
+ // Check if playwright is importable + chromium installed.
165
+ try {
166
+ const moduleId = "playwright";
167
+ require.resolve(moduleId);
168
+ } catch {
169
+ return {
170
+ name: "playwright",
171
+ severity: "warn",
172
+ detail: "module missing (needed for `harn browse`)",
173
+ hint: "npm install -g playwright && npx playwright install chromium",
174
+ };
175
+ }
176
+ // Check chromium browser binary exists.
177
+ const home = os.homedir();
178
+ const candidates = [
179
+ path.join(home, ".cache", "ms-playwright"),
180
+ path.join(home, "Library", "Caches", "ms-playwright"),
181
+ ];
182
+ const found = candidates.find(existsSync);
183
+ if (!found) {
184
+ return {
185
+ name: "playwright",
186
+ severity: "warn",
187
+ detail: "module ok but no browsers installed",
188
+ hint: "npx playwright install chromium",
189
+ };
190
+ }
191
+ return { name: "playwright", severity: "ok", detail: `module + browsers at ${found}` };
192
+ }
193
+
194
+ function checkPython(): Check {
195
+ const r = whichVersion("python3");
196
+ if (!r.ok) {
197
+ return {
198
+ name: "python3",
199
+ severity: "warn",
200
+ detail: "missing (optional; some examples use python)",
201
+ };
202
+ }
203
+ return { name: "python3", severity: "ok", detail: r.out };
204
+ }
205
+
206
+ function checkHarneryDir(): Check {
207
+ // Walk up from cwd looking for .harnery/.
208
+ let dir = process.cwd();
209
+ for (let i = 0; i < 8; i++) {
210
+ if (existsSync(path.join(dir, ".harnery"))) {
211
+ return {
212
+ name: ".harnery/",
213
+ severity: "ok",
214
+ detail: path.join(dir, ".harnery"),
215
+ };
216
+ }
217
+ const parent = path.dirname(dir);
218
+ if (parent === dir) break;
219
+ dir = parent;
220
+ }
221
+ return {
222
+ name: ".harnery/",
223
+ severity: "warn",
224
+ detail: "no .harnery/ found above cwd",
225
+ hint: "create one with `mkdir -p .harnery/active` from your monorepo root",
226
+ };
227
+ }
228
+
229
+ function macOrLinux(mac: string, linux: string): string {
230
+ return os.platform() === "darwin" ? mac : linux;
231
+ }
@@ -0,0 +1,233 @@
1
+ import { existsSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import type { Command } from "commander";
4
+ import type { EmitContext } from "../commander.ts";
5
+
6
+ /**
7
+ * `harn edit-batch`: coordinated find/replace across N files in one call.
8
+ *
9
+ * Solves the recurring "rename oldName → newName everywhere" pattern that
10
+ * otherwise costs N sequential Edit tool calls. Literal-string by default;
11
+ * --regex flag treats the find string as a JS regex. --all flag replaces
12
+ * every occurrence per file (default: first match only, matching the Edit
13
+ * tool's default).
14
+ *
15
+ * Atomic per-file: writes to <path>.batch.tmp.<pid> then renames. If any
16
+ * file errors mid-batch, already-written files stay written: it's
17
+ * idempotent at the per-file level, not transactional across the set.
18
+ * Use --dry-run to preview before committing.
19
+ */
20
+ export function registerEditBatchCommand(program: Command, emit: EmitContext): void {
21
+ program
22
+ .command("edit-batch <old> <new> <files...>")
23
+ .description(
24
+ "Coordinated find/replace across N files (literal by default; --regex for pattern). " +
25
+ "Atomic per-file. Use --dry-run to preview.",
26
+ )
27
+ .option("--all", "Replace every occurrence in each file (default: first match only)")
28
+ .option("--regex", "Treat <old> as a JS regex (and <new> as the replacement template)")
29
+ .option(
30
+ "--regex-flags <flags>",
31
+ "Regex flags (e.g. 'i' for case-insensitive). Implies --regex.",
32
+ "",
33
+ )
34
+ .option("--dry-run", "Show what would change without writing")
35
+ .option("--require-match", "Fail (exit 1) if any file has zero matches")
36
+ .option("--json", "Structured JSON envelope")
37
+ .action(async (oldStr: string, newStr: string, files: string[], opts: EditBatchOpts) => {
38
+ try {
39
+ const result = await runEditBatch(oldStr, newStr, files, opts);
40
+ if (opts.json) {
41
+ emit.config({ format: "json" });
42
+ emit.data(result);
43
+ return;
44
+ }
45
+ emit.text(`${renderResult(result)}\n`);
46
+ if (opts.requireMatch && result.zero_match_files.length > 0) {
47
+ process.exit(1);
48
+ }
49
+ } catch (err) {
50
+ emit.error({ code: "edit_batch_failed", message: (err as Error).message });
51
+ process.exit(1);
52
+ }
53
+ });
54
+ }
55
+
56
+ interface EditBatchOpts {
57
+ all?: boolean;
58
+ regex?: boolean;
59
+ regexFlags?: string;
60
+ dryRun?: boolean;
61
+ requireMatch?: boolean;
62
+ json?: boolean;
63
+ }
64
+
65
+ interface FileResult {
66
+ path: string;
67
+ matched: number;
68
+ replaced: number;
69
+ bytes_before: number;
70
+ bytes_after: number;
71
+ written: boolean;
72
+ error: string | null;
73
+ }
74
+
75
+ interface EditBatchResult {
76
+ old: string;
77
+ new: string;
78
+ mode: "literal" | "regex";
79
+ all: boolean;
80
+ dry_run: boolean;
81
+ files: FileResult[];
82
+ total_matches: number;
83
+ total_replacements: number;
84
+ files_modified: number;
85
+ zero_match_files: string[];
86
+ }
87
+
88
+ async function runEditBatch(
89
+ oldStr: string,
90
+ newStr: string,
91
+ files: string[],
92
+ opts: EditBatchOpts,
93
+ ): Promise<EditBatchResult> {
94
+ if (files.length === 0) {
95
+ throw new Error("at least one file path required");
96
+ }
97
+ if (oldStr.length === 0) {
98
+ throw new Error("<old> string is empty");
99
+ }
100
+ const useRegex = !!opts.regex || (opts.regexFlags ?? "").length > 0;
101
+ let pattern: RegExp;
102
+ if (useRegex) {
103
+ let flags = opts.regexFlags ?? "";
104
+ if (opts.all && !flags.includes("g")) flags += "g";
105
+ try {
106
+ pattern = new RegExp(oldStr, flags);
107
+ } catch (err) {
108
+ throw new Error(`invalid regex: ${(err as Error).message}`);
109
+ }
110
+ } else {
111
+ // Literal-string mode: escape regex specials so the user can pass anything.
112
+ const escaped = oldStr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
113
+ pattern = new RegExp(escaped, opts.all ? "g" : "");
114
+ }
115
+
116
+ const fileResults: FileResult[] = [];
117
+ for (const filePath of files) {
118
+ const abs = resolve(filePath);
119
+ const r: FileResult = {
120
+ path: filePath,
121
+ matched: 0,
122
+ replaced: 0,
123
+ bytes_before: 0,
124
+ bytes_after: 0,
125
+ written: false,
126
+ error: null,
127
+ };
128
+ try {
129
+ if (!existsSync(abs)) {
130
+ r.error = "file not found";
131
+ fileResults.push(r);
132
+ continue;
133
+ }
134
+ const before = readFileSync(abs, "utf8");
135
+ r.bytes_before = Buffer.byteLength(before, "utf8");
136
+
137
+ // Count matches independently of replace (handles both modes).
138
+ const countPattern = new RegExp(
139
+ pattern.source,
140
+ pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`,
141
+ );
142
+ r.matched = (before.match(countPattern) || []).length;
143
+
144
+ if (r.matched === 0) {
145
+ fileResults.push(r);
146
+ continue;
147
+ }
148
+
149
+ const after = before.replace(pattern, newStr);
150
+ // The `g` flag (explicit --all OR passed via --regex-flags) is what actually
151
+ // controls replace-all behavior. Count what String.replace actually did,
152
+ // not what --all said.
153
+ const replacedAll = pattern.flags.includes("g");
154
+ r.replaced = replacedAll ? r.matched : Math.min(1, r.matched);
155
+ r.bytes_after = Buffer.byteLength(after, "utf8");
156
+
157
+ if (!opts.dryRun) {
158
+ const tmp = `${abs}.batch.tmp.${process.pid}`;
159
+ writeFileSync(tmp, after, "utf8");
160
+ const mode = statSync(abs).mode;
161
+ renameSync(tmp, abs);
162
+ // mode preservation: chmod after rename if needed.
163
+ // (omitted: writeFileSync defaults are fine; rename keeps inode anyway when same filesystem)
164
+ void mode;
165
+ r.written = true;
166
+ }
167
+ fileResults.push(r);
168
+ } catch (err) {
169
+ r.error = (err as Error).message;
170
+ fileResults.push(r);
171
+ }
172
+ }
173
+
174
+ const totalMatches = fileResults.reduce((acc, f) => acc + f.matched, 0);
175
+ const totalReplacements = fileResults.reduce((acc, f) => acc + f.replaced, 0);
176
+ const filesModified = fileResults.filter((f) => f.written).length;
177
+ const zeroMatch = fileResults.filter((f) => f.matched === 0 && !f.error).map((f) => f.path);
178
+
179
+ return {
180
+ old: oldStr,
181
+ new: newStr,
182
+ mode: useRegex ? "regex" : "literal",
183
+ all: !!opts.all,
184
+ dry_run: !!opts.dryRun,
185
+ files: fileResults,
186
+ total_matches: totalMatches,
187
+ total_replacements: totalReplacements,
188
+ files_modified: filesModified,
189
+ zero_match_files: zeroMatch,
190
+ };
191
+ }
192
+
193
+ function renderResult(r: EditBatchResult): string {
194
+ const lines: string[] = [];
195
+ const verb = r.dry_run ? "would replace" : "replaced";
196
+ lines.push(
197
+ `edit-batch · ${r.mode} · ${r.all ? "all" : "first"}-match · ${r.files.length} file(s)`,
198
+ );
199
+ lines.push(` '${truncate(r.old, 60)}' → '${truncate(r.new, 60)}'`);
200
+ lines.push("");
201
+ for (const f of r.files) {
202
+ if (f.error) {
203
+ lines.push(` ✗ ${f.path} (error: ${f.error})`);
204
+ continue;
205
+ }
206
+ if (f.matched === 0) {
207
+ lines.push(` · ${f.path} (no match)`);
208
+ continue;
209
+ }
210
+ const tag = r.dry_run ? "?" : f.written ? "✓" : "·";
211
+ const delta = f.bytes_after - f.bytes_before;
212
+ const deltaStr = delta === 0 ? "0 bytes" : delta > 0 ? `+${delta} bytes` : `${delta} bytes`;
213
+ lines.push(
214
+ ` ${tag} ${f.path} (${f.matched} match${f.matched === 1 ? "" : "es"}, ${verb} ${f.replaced}, ${deltaStr})`,
215
+ );
216
+ }
217
+ lines.push("");
218
+ lines.push(
219
+ `total: ${r.total_matches} match(es) across ${r.files.length} file(s); ` +
220
+ `${verb} ${r.total_replacements} in ${r.files_modified || (r.dry_run ? r.files.filter((f) => f.matched > 0).length : 0)} file(s)`,
221
+ );
222
+ if (r.zero_match_files.length > 0) {
223
+ lines.push(
224
+ `zero-match files: ${r.zero_match_files.length} (use --require-match to fail when this happens)`,
225
+ );
226
+ }
227
+ return lines.join("\n");
228
+ }
229
+
230
+ function truncate(s: string, n: number): string {
231
+ if (s.length <= n) return s;
232
+ return `${s.slice(0, n - 1)}…`;
233
+ }