harnery 0.0.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (445) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +84 -2
  3. package/bin/agent-coord +42 -0
  4. package/bin/agent-hook +44 -0
  5. package/bin/harn +40 -0
  6. package/dist/cli.d.ts +9 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +18 -0
  9. package/dist/commander.d.ts +128 -0
  10. package/dist/commander.d.ts.map +1 -0
  11. package/dist/commander.js +126 -0
  12. package/dist/commands/agents.d.ts +18 -0
  13. package/dist/commands/agents.d.ts.map +1 -0
  14. package/dist/commands/agents.js +3946 -0
  15. package/dist/commands/backup.d.ts +22 -0
  16. package/dist/commands/backup.d.ts.map +1 -0
  17. package/dist/commands/backup.js +262 -0
  18. package/dist/commands/browse-ai.d.ts +4 -0
  19. package/dist/commands/browse-ai.d.ts.map +1 -0
  20. package/dist/commands/browse-ai.js +156 -0
  21. package/dist/commands/browse.d.ts +4 -0
  22. package/dist/commands/browse.d.ts.map +1 -0
  23. package/dist/commands/browse.js +590 -0
  24. package/dist/commands/callers.d.ts +4 -0
  25. package/dist/commands/callers.d.ts.map +1 -0
  26. package/dist/commands/callers.js +276 -0
  27. package/dist/commands/completion.d.ts +17 -0
  28. package/dist/commands/completion.d.ts.map +1 -0
  29. package/dist/commands/completion.js +158 -0
  30. package/dist/commands/config-get.d.ts +4 -0
  31. package/dist/commands/config-get.d.ts.map +1 -0
  32. package/dist/commands/config-get.js +131 -0
  33. package/dist/commands/context.d.ts +11 -0
  34. package/dist/commands/context.d.ts.map +1 -0
  35. package/dist/commands/context.js +185 -0
  36. package/dist/commands/cookies.d.ts +4 -0
  37. package/dist/commands/cookies.d.ts.map +1 -0
  38. package/dist/commands/cookies.js +140 -0
  39. package/dist/commands/docs.d.ts +4 -0
  40. package/dist/commands/docs.d.ts.map +1 -0
  41. package/dist/commands/docs.js +137 -0
  42. package/dist/commands/doctor.d.ts +25 -0
  43. package/dist/commands/doctor.d.ts.map +1 -0
  44. package/dist/commands/doctor.js +200 -0
  45. package/dist/commands/edit-batch.d.ts +18 -0
  46. package/dist/commands/edit-batch.d.ts.map +1 -0
  47. package/dist/commands/edit-batch.js +172 -0
  48. package/dist/commands/eml.d.ts +4 -0
  49. package/dist/commands/eml.d.ts.map +1 -0
  50. package/dist/commands/eml.js +428 -0
  51. package/dist/commands/env.d.ts +4 -0
  52. package/dist/commands/env.d.ts.map +1 -0
  53. package/dist/commands/env.js +201 -0
  54. package/dist/commands/fetch.d.ts +4 -0
  55. package/dist/commands/fetch.d.ts.map +1 -0
  56. package/dist/commands/fetch.js +99 -0
  57. package/dist/commands/file-history.d.ts +4 -0
  58. package/dist/commands/file-history.d.ts.map +1 -0
  59. package/dist/commands/file-history.js +152 -0
  60. package/dist/commands/grep.d.ts +4 -0
  61. package/dist/commands/grep.d.ts.map +1 -0
  62. package/dist/commands/grep.js +317 -0
  63. package/dist/commands/init.d.ts +82 -0
  64. package/dist/commands/init.d.ts.map +1 -0
  65. package/dist/commands/init.js +288 -0
  66. package/dist/commands/outline.d.ts +4 -0
  67. package/dist/commands/outline.d.ts.map +1 -0
  68. package/dist/commands/outline.js +509 -0
  69. package/dist/commands/presence.d.ts +12 -0
  70. package/dist/commands/presence.d.ts.map +1 -0
  71. package/dist/commands/presence.js +123 -0
  72. package/dist/commands/read.d.ts +7 -0
  73. package/dist/commands/read.d.ts.map +1 -0
  74. package/dist/commands/read.js +46 -0
  75. package/dist/commands/scratch.d.ts +4 -0
  76. package/dist/commands/scratch.d.ts.map +1 -0
  77. package/dist/commands/scratch.js +426 -0
  78. package/dist/commands/session.d.ts +4 -0
  79. package/dist/commands/session.d.ts.map +1 -0
  80. package/dist/commands/session.js +162 -0
  81. package/dist/commands/sync.d.ts +24 -0
  82. package/dist/commands/sync.d.ts.map +1 -0
  83. package/dist/commands/sync.js +275 -0
  84. package/dist/commands/toc.d.ts +5 -0
  85. package/dist/commands/toc.d.ts.map +1 -0
  86. package/dist/commands/toc.js +153 -0
  87. package/dist/commands/tokens.d.ts +4 -0
  88. package/dist/commands/tokens.d.ts.map +1 -0
  89. package/dist/commands/tokens.js +48 -0
  90. package/dist/commands/tunnel.d.ts +4 -0
  91. package/dist/commands/tunnel.d.ts.map +1 -0
  92. package/dist/commands/tunnel.js +513 -0
  93. package/dist/commands/uninstall.d.ts +22 -0
  94. package/dist/commands/uninstall.d.ts.map +1 -0
  95. package/dist/commands/uninstall.js +126 -0
  96. package/dist/commands/web.d.ts +4 -0
  97. package/dist/commands/web.d.ts.map +1 -0
  98. package/dist/commands/web.js +165 -0
  99. package/dist/core/agents/canonical-emit.d.ts +27 -0
  100. package/dist/core/agents/canonical-emit.d.ts.map +1 -0
  101. package/dist/core/agents/canonical-emit.js +72 -0
  102. package/dist/core/agents/cli-emit.d.ts +27 -0
  103. package/dist/core/agents/cli-emit.d.ts.map +1 -0
  104. package/dist/core/agents/cli-emit.js +57 -0
  105. package/dist/core/agents/cli.d.ts +10 -0
  106. package/dist/core/agents/cli.d.ts.map +1 -0
  107. package/dist/core/agents/cli.js +757 -0
  108. package/dist/core/agents/codex-replay.d.ts +29 -0
  109. package/dist/core/agents/codex-replay.d.ts.map +1 -0
  110. package/dist/core/agents/codex-replay.js +138 -0
  111. package/dist/core/agents/coord-client.d.ts +98 -0
  112. package/dist/core/agents/coord-client.d.ts.map +1 -0
  113. package/dist/core/agents/coord-client.js +212 -0
  114. package/dist/core/agents/events/consume.d.ts +59 -0
  115. package/dist/core/agents/events/consume.d.ts.map +1 -0
  116. package/dist/core/agents/events/consume.js +147 -0
  117. package/dist/core/agents/events/emit.d.ts +42 -0
  118. package/dist/core/agents/events/emit.d.ts.map +1 -0
  119. package/dist/core/agents/events/emit.js +70 -0
  120. package/dist/core/agents/events/ulid.d.ts +11 -0
  121. package/dist/core/agents/events/ulid.d.ts.map +1 -0
  122. package/dist/core/agents/events/ulid.js +47 -0
  123. package/dist/core/agents/index.d.ts +14 -0
  124. package/dist/core/agents/index.d.ts.map +1 -0
  125. package/dist/core/agents/index.js +13 -0
  126. package/dist/core/agents/paths.d.ts +6 -0
  127. package/dist/core/agents/paths.d.ts.map +1 -0
  128. package/dist/core/agents/paths.js +17 -0
  129. package/dist/core/agents/render/prompt-context.d.ts +43 -0
  130. package/dist/core/agents/render/prompt-context.d.ts.map +1 -0
  131. package/dist/core/agents/render/prompt-context.js +335 -0
  132. package/dist/core/agents/render/session-context.d.ts +39 -0
  133. package/dist/core/agents/render/session-context.d.ts.map +1 -0
  134. package/dist/core/agents/render/session-context.js +283 -0
  135. package/dist/core/agents/rules/claim-conflict.d.ts +35 -0
  136. package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -0
  137. package/dist/core/agents/rules/claim-conflict.js +244 -0
  138. package/dist/core/agents/rules/commit-conflict.d.ts +59 -0
  139. package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -0
  140. package/dist/core/agents/rules/commit-conflict.js +244 -0
  141. package/dist/core/agents/rules/stop-hook.d.ts +44 -0
  142. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -0
  143. package/dist/core/agents/rules/stop-hook.js +161 -0
  144. package/dist/core/agents/session-events.d.ts +41 -0
  145. package/dist/core/agents/session-events.d.ts.map +1 -0
  146. package/dist/core/agents/session-events.js +205 -0
  147. package/dist/core/agents/state/activity-log.d.ts +18 -0
  148. package/dist/core/agents/state/activity-log.d.ts.map +1 -0
  149. package/dist/core/agents/state/activity-log.js +34 -0
  150. package/dist/core/agents/state/council.d.ts +39 -0
  151. package/dist/core/agents/state/council.d.ts.map +1 -0
  152. package/dist/core/agents/state/council.js +216 -0
  153. package/dist/core/agents/state/heartbeat-projector.d.ts +59 -0
  154. package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -0
  155. package/dist/core/agents/state/heartbeat-projector.js +436 -0
  156. package/dist/core/agents/state/heartbeat-writer.d.ts +64 -0
  157. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -0
  158. package/dist/core/agents/state/heartbeat-writer.js +271 -0
  159. package/dist/core/agents/state/names.d.ts +35 -0
  160. package/dist/core/agents/state/names.d.ts.map +1 -0
  161. package/dist/core/agents/state/names.js +376 -0
  162. package/dist/core/agents/state/pidmap.d.ts +11 -0
  163. package/dist/core/agents/state/pidmap.d.ts.map +1 -0
  164. package/dist/core/agents/state/pidmap.js +32 -0
  165. package/dist/core/agents/state/scratch.d.ts +27 -0
  166. package/dist/core/agents/state/scratch.d.ts.map +1 -0
  167. package/dist/core/agents/state/scratch.js +90 -0
  168. package/dist/core/agents/state/shell-mutation.d.ts +17 -0
  169. package/dist/core/agents/state/shell-mutation.d.ts.map +1 -0
  170. package/dist/core/agents/state/shell-mutation.js +41 -0
  171. package/dist/core/agents/state/stale-sweep.d.ts +16 -0
  172. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -0
  173. package/dist/core/agents/state/stale-sweep.js +166 -0
  174. package/dist/core/config.d.ts +29 -0
  175. package/dist/core/config.d.ts.map +1 -0
  176. package/dist/core/config.js +108 -0
  177. package/dist/core/hooks/cli.d.ts +21 -0
  178. package/dist/core/hooks/cli.d.ts.map +1 -0
  179. package/dist/core/hooks/cli.js +1123 -0
  180. package/dist/core/hooks/effects/image-capture.d.ts +43 -0
  181. package/dist/core/hooks/effects/image-capture.d.ts.map +1 -0
  182. package/dist/core/hooks/effects/image-capture.js +288 -0
  183. package/dist/core/hooks/effects/index.d.ts +64 -0
  184. package/dist/core/hooks/effects/index.d.ts.map +1 -0
  185. package/dist/core/hooks/effects/index.js +197 -0
  186. package/dist/core/hooks/events/emit.d.ts +31 -0
  187. package/dist/core/hooks/events/emit.d.ts.map +1 -0
  188. package/dist/core/hooks/events/emit.js +89 -0
  189. package/dist/core/hooks/events/schema.d.ts +235 -0
  190. package/dist/core/hooks/events/schema.d.ts.map +1 -0
  191. package/dist/core/hooks/events/schema.js +12 -0
  192. package/dist/core/hooks/events/ulid.d.ts +10 -0
  193. package/dist/core/hooks/events/ulid.d.ts.map +1 -0
  194. package/dist/core/hooks/events/ulid.js +47 -0
  195. package/dist/core/hooks/harness/detect.d.ts +9 -0
  196. package/dist/core/hooks/harness/detect.d.ts.map +1 -0
  197. package/dist/core/hooks/harness/detect.js +29 -0
  198. package/dist/core/hooks/harness/events.d.ts +45 -0
  199. package/dist/core/hooks/harness/events.d.ts.map +1 -0
  200. package/dist/core/hooks/harness/events.js +71 -0
  201. package/dist/core/hooks/harness/output.d.ts +46 -0
  202. package/dist/core/hooks/harness/output.d.ts.map +1 -0
  203. package/dist/core/hooks/harness/output.js +87 -0
  204. package/dist/core/hooks/harness/parse.d.ts +67 -0
  205. package/dist/core/hooks/harness/parse.d.ts.map +1 -0
  206. package/dist/core/hooks/harness/parse.js +132 -0
  207. package/dist/core/hooks/index.d.ts +8 -0
  208. package/dist/core/hooks/index.d.ts.map +1 -0
  209. package/dist/core/hooks/index.js +7 -0
  210. package/dist/core/hooks/resolve/anchor.d.ts +37 -0
  211. package/dist/core/hooks/resolve/anchor.d.ts.map +1 -0
  212. package/dist/core/hooks/resolve/anchor.js +48 -0
  213. package/dist/core/hooks/resolve/coord-root.d.ts +6 -0
  214. package/dist/core/hooks/resolve/coord-root.d.ts.map +1 -0
  215. package/dist/core/hooks/resolve/coord-root.js +27 -0
  216. package/dist/core/hooks/resolve/intent.d.ts +33 -0
  217. package/dist/core/hooks/resolve/intent.d.ts.map +1 -0
  218. package/dist/core/hooks/resolve/intent.js +79 -0
  219. package/dist/core/hooks/resolve/owner.d.ts +42 -0
  220. package/dist/core/hooks/resolve/owner.d.ts.map +1 -0
  221. package/dist/core/hooks/resolve/owner.js +140 -0
  222. package/dist/core/hooks/resolve/transcript.d.ts +26 -0
  223. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -0
  224. package/dist/core/hooks/resolve/transcript.js +73 -0
  225. package/dist/index.d.ts +15 -0
  226. package/dist/index.d.ts.map +1 -0
  227. package/dist/index.js +13 -0
  228. package/dist/lib/agent-browser/client.d.ts +99 -0
  229. package/dist/lib/agent-browser/client.d.ts.map +1 -0
  230. package/dist/lib/agent-browser/client.js +177 -0
  231. package/dist/lib/agent-browser/index.d.ts +2 -0
  232. package/dist/lib/agent-browser/index.d.ts.map +1 -0
  233. package/dist/lib/agent-browser/index.js +1 -0
  234. package/dist/lib/browser/client.d.ts +193 -0
  235. package/dist/lib/browser/client.d.ts.map +1 -0
  236. package/dist/lib/browser/client.js +325 -0
  237. package/dist/lib/browser/dev-overlay.d.ts +23 -0
  238. package/dist/lib/browser/dev-overlay.d.ts.map +1 -0
  239. package/dist/lib/browser/dev-overlay.js +153 -0
  240. package/dist/lib/browser/index.d.ts +5 -0
  241. package/dist/lib/browser/index.d.ts.map +1 -0
  242. package/dist/lib/browser/index.js +2 -0
  243. package/dist/lib/browser/layout.d.ts +79 -0
  244. package/dist/lib/browser/layout.d.ts.map +1 -0
  245. package/dist/lib/browser/layout.js +220 -0
  246. package/dist/lib/browser/visibility.d.ts +86 -0
  247. package/dist/lib/browser/visibility.d.ts.map +1 -0
  248. package/dist/lib/browser/visibility.js +333 -0
  249. package/dist/lib/browser/visual-diff.d.ts +38 -0
  250. package/dist/lib/browser/visual-diff.d.ts.map +1 -0
  251. package/dist/lib/browser/visual-diff.js +107 -0
  252. package/dist/lib/completion/bash.d.ts +25 -0
  253. package/dist/lib/completion/bash.d.ts.map +1 -0
  254. package/dist/lib/completion/bash.js +284 -0
  255. package/dist/lib/completion/fish.d.ts +16 -0
  256. package/dist/lib/completion/fish.d.ts.map +1 -0
  257. package/dist/lib/completion/fish.js +118 -0
  258. package/dist/lib/completion/index.d.ts +5 -0
  259. package/dist/lib/completion/index.d.ts.map +1 -0
  260. package/dist/lib/completion/index.js +4 -0
  261. package/dist/lib/completion/walk.d.ts +68 -0
  262. package/dist/lib/completion/walk.d.ts.map +1 -0
  263. package/dist/lib/completion/walk.js +102 -0
  264. package/dist/lib/completion/zsh.d.ts +13 -0
  265. package/dist/lib/completion/zsh.d.ts.map +1 -0
  266. package/dist/lib/completion/zsh.js +249 -0
  267. package/dist/lib/context/index.d.ts +107 -0
  268. package/dist/lib/context/index.d.ts.map +1 -0
  269. package/dist/lib/context/index.js +275 -0
  270. package/dist/lib/cookies/client.d.ts +131 -0
  271. package/dist/lib/cookies/client.d.ts.map +1 -0
  272. package/dist/lib/cookies/client.js +239 -0
  273. package/dist/lib/cookies/index.d.ts +2 -0
  274. package/dist/lib/cookies/index.d.ts.map +1 -0
  275. package/dist/lib/cookies/index.js +1 -0
  276. package/dist/lib/council/index.d.ts +266 -0
  277. package/dist/lib/council/index.d.ts.map +1 -0
  278. package/dist/lib/council/index.js +674 -0
  279. package/dist/lib/docs-index.d.ts +28 -0
  280. package/dist/lib/docs-index.d.ts.map +1 -0
  281. package/dist/lib/docs-index.js +169 -0
  282. package/dist/lib/docs-lint.d.ts +26 -0
  283. package/dist/lib/docs-lint.d.ts.map +1 -0
  284. package/dist/lib/docs-lint.js +378 -0
  285. package/dist/lib/docs-sweep.d.ts +34 -0
  286. package/dist/lib/docs-sweep.d.ts.map +1 -0
  287. package/dist/lib/docs-sweep.js +304 -0
  288. package/dist/lib/docs.d.ts +27 -0
  289. package/dist/lib/docs.d.ts.map +1 -0
  290. package/dist/lib/docs.js +142 -0
  291. package/dist/lib/env.d.ts +11 -0
  292. package/dist/lib/env.d.ts.map +1 -0
  293. package/dist/lib/env.js +12 -0
  294. package/dist/lib/exec.d.ts +32 -0
  295. package/dist/lib/exec.d.ts.map +1 -0
  296. package/dist/lib/exec.js +54 -0
  297. package/dist/lib/format.d.ts +29 -0
  298. package/dist/lib/format.d.ts.map +1 -0
  299. package/dist/lib/format.js +139 -0
  300. package/dist/lib/http/client.d.ts +56 -0
  301. package/dist/lib/http/client.d.ts.map +1 -0
  302. package/dist/lib/http/client.js +160 -0
  303. package/dist/lib/http/index.d.ts +2 -0
  304. package/dist/lib/http/index.d.ts.map +1 -0
  305. package/dist/lib/http/index.js +1 -0
  306. package/dist/lib/identities/index.d.ts +77 -0
  307. package/dist/lib/identities/index.d.ts.map +1 -0
  308. package/dist/lib/identities/index.js +190 -0
  309. package/dist/lib/machine.d.ts +19 -0
  310. package/dist/lib/machine.d.ts.map +1 -0
  311. package/dist/lib/machine.js +61 -0
  312. package/dist/lib/presence.d.ts +48 -0
  313. package/dist/lib/presence.d.ts.map +1 -0
  314. package/dist/lib/presence.js +123 -0
  315. package/dist/lib/readability/client.d.ts +39 -0
  316. package/dist/lib/readability/client.d.ts.map +1 -0
  317. package/dist/lib/readability/client.js +121 -0
  318. package/dist/lib/readability/index.d.ts +2 -0
  319. package/dist/lib/readability/index.d.ts.map +1 -0
  320. package/dist/lib/readability/index.js +1 -0
  321. package/dist/lib/scratch/index.d.ts +74 -0
  322. package/dist/lib/scratch/index.d.ts.map +1 -0
  323. package/dist/lib/scratch/index.js +393 -0
  324. package/dist/lib/tunnel/gate.d.ts +12 -0
  325. package/dist/lib/tunnel/gate.d.ts.map +1 -0
  326. package/dist/lib/tunnel/gate.js +101 -0
  327. package/dist/lib/tunnel/state.d.ts +34 -0
  328. package/dist/lib/tunnel/state.d.ts.map +1 -0
  329. package/dist/lib/tunnel/state.js +132 -0
  330. package/package.json +160 -8
  331. package/schemas/.gitkeep +0 -0
  332. package/schemas/config.schema.json +109 -0
  333. package/src/cli.ts +22 -0
  334. package/src/commander.ts +242 -0
  335. package/src/commands/.gitkeep +0 -0
  336. package/src/commands/agents.ts +4567 -0
  337. package/src/commands/backup.ts +305 -0
  338. package/src/commands/browse-ai.ts +198 -0
  339. package/src/commands/browse.ts +849 -0
  340. package/src/commands/callers.ts +363 -0
  341. package/src/commands/completion.ts +193 -0
  342. package/src/commands/config-get.ts +161 -0
  343. package/src/commands/context.ts +209 -0
  344. package/src/commands/cookies.ts +198 -0
  345. package/src/commands/docs.ts +174 -0
  346. package/src/commands/doctor.ts +231 -0
  347. package/src/commands/edit-batch.ts +233 -0
  348. package/src/commands/eml.ts +519 -0
  349. package/src/commands/env.ts +254 -0
  350. package/src/commands/fetch.ts +136 -0
  351. package/src/commands/file-history.ts +202 -0
  352. package/src/commands/grep.ts +371 -0
  353. package/src/commands/init.ts +335 -0
  354. package/src/commands/outline.ts +583 -0
  355. package/src/commands/presence.ts +152 -0
  356. package/src/commands/read.ts +64 -0
  357. package/src/commands/scratch.ts +445 -0
  358. package/src/commands/session.ts +187 -0
  359. package/src/commands/sync.ts +306 -0
  360. package/src/commands/toc.ts +218 -0
  361. package/src/commands/tokens.ts +79 -0
  362. package/src/commands/tunnel.ts +633 -0
  363. package/src/commands/uninstall.ts +144 -0
  364. package/src/commands/web.ts +193 -0
  365. package/src/core/agents/canonical-emit.ts +77 -0
  366. package/src/core/agents/cli-emit.ts +64 -0
  367. package/src/core/agents/cli.ts +838 -0
  368. package/src/core/agents/codex-replay.ts +163 -0
  369. package/src/core/agents/coord-client.ts +249 -0
  370. package/src/core/agents/events/consume.ts +196 -0
  371. package/src/core/agents/events/emit.ts +108 -0
  372. package/src/core/agents/events/ulid.ts +51 -0
  373. package/src/core/agents/index.ts +14 -0
  374. package/src/core/agents/paths.ts +16 -0
  375. package/src/core/agents/render/prompt-context.ts +401 -0
  376. package/src/core/agents/render/session-context.ts +341 -0
  377. package/src/core/agents/rules/claim-conflict.ts +282 -0
  378. package/src/core/agents/rules/commit-conflict.ts +303 -0
  379. package/src/core/agents/rules/stop-hook.ts +229 -0
  380. package/src/core/agents/session-events.ts +228 -0
  381. package/src/core/agents/state/activity-log.ts +33 -0
  382. package/src/core/agents/state/council.ts +265 -0
  383. package/src/core/agents/state/heartbeat-projector.ts +488 -0
  384. package/src/core/agents/state/heartbeat-writer.ts +333 -0
  385. package/src/core/agents/state/names.ts +399 -0
  386. package/src/core/agents/state/pidmap.ts +38 -0
  387. package/src/core/agents/state/scratch.ts +121 -0
  388. package/src/core/agents/state/shell-mutation.ts +44 -0
  389. package/src/core/agents/state/stale-sweep.ts +190 -0
  390. package/src/core/config.ts +111 -0
  391. package/src/core/hooks/cli.ts +1247 -0
  392. package/src/core/hooks/effects/image-capture.ts +330 -0
  393. package/src/core/hooks/effects/index.ts +210 -0
  394. package/src/core/hooks/events/emit.ts +120 -0
  395. package/src/core/hooks/events/schema.ts +430 -0
  396. package/src/core/hooks/events/ulid.ts +51 -0
  397. package/src/core/hooks/harness/detect.ts +30 -0
  398. package/src/core/hooks/harness/events.ts +102 -0
  399. package/src/core/hooks/harness/output.ts +100 -0
  400. package/src/core/hooks/harness/parse.ts +180 -0
  401. package/src/core/hooks/index.ts +16 -0
  402. package/src/core/hooks/resolve/anchor.ts +51 -0
  403. package/src/core/hooks/resolve/coord-root.ts +25 -0
  404. package/src/core/hooks/resolve/intent.ts +89 -0
  405. package/src/core/hooks/resolve/owner.ts +140 -0
  406. package/src/core/hooks/resolve/transcript.ts +72 -0
  407. package/src/hooks/.gitkeep +0 -0
  408. package/src/index.ts +15 -0
  409. package/src/lib/agent-browser/client.ts +239 -0
  410. package/src/lib/agent-browser/index.ts +1 -0
  411. package/src/lib/browser/client.ts +449 -0
  412. package/src/lib/browser/dev-overlay.ts +207 -0
  413. package/src/lib/browser/index.ts +24 -0
  414. package/src/lib/browser/layout.ts +288 -0
  415. package/src/lib/browser/visibility.ts +419 -0
  416. package/src/lib/browser/visual-diff.ts +150 -0
  417. package/src/lib/completion/bash.ts +291 -0
  418. package/src/lib/completion/fish.ts +134 -0
  419. package/src/lib/completion/index.ts +10 -0
  420. package/src/lib/completion/walk.ts +184 -0
  421. package/src/lib/completion/zsh.ts +262 -0
  422. package/src/lib/context/index.ts +386 -0
  423. package/src/lib/cookies/client.ts +301 -0
  424. package/src/lib/cookies/index.ts +13 -0
  425. package/src/lib/council/index.ts +803 -0
  426. package/src/lib/docs-index.ts +216 -0
  427. package/src/lib/docs-lint.ts +413 -0
  428. package/src/lib/docs-sweep.ts +348 -0
  429. package/src/lib/docs.ts +199 -0
  430. package/src/lib/env.ts +12 -0
  431. package/src/lib/exec.ts +74 -0
  432. package/src/lib/format.ts +147 -0
  433. package/src/lib/http/client.ts +211 -0
  434. package/src/lib/http/index.ts +1 -0
  435. package/src/lib/identities/index.ts +210 -0
  436. package/src/lib/machine.ts +61 -0
  437. package/src/lib/presence.ts +154 -0
  438. package/src/lib/readability/client.ts +169 -0
  439. package/src/lib/readability/index.ts +5 -0
  440. package/src/lib/readability/turndown-plugin-gfm.d.ts +10 -0
  441. package/src/lib/scratch/index.ts +470 -0
  442. package/src/lib/tunnel/gate.ts +113 -0
  443. package/src/lib/tunnel/state.ts +167 -0
  444. package/src/web/.gitkeep +0 -0
  445. package/index.js +0 -1
@@ -0,0 +1,348 @@
1
+ import { existsSync as __existsSyncForDocs } from "node:fs";
2
+ import { resolve as __resolveForDocs } from "node:path";
3
+ import { sh } from "./exec.ts";
4
+
5
+ // Module-level docs context, initialized by initDocsContext() before any
6
+ // other function in this file is called. The repo root + submodule list
7
+ // are passed via the context provided to registerDocsCommand.
8
+ let REPO_ROOT = "";
9
+ let SUBMODULES: readonly string[] = [];
10
+
11
+ export function initDocsContext(opts: { repoRoot: string; submodules: readonly string[] }): void {
12
+ REPO_ROOT = opts.repoRoot;
13
+ SUBMODULES = opts.submodules;
14
+ }
15
+
16
+ function submodulePath(name: string): string {
17
+ return __resolveForDocs(REPO_ROOT, name);
18
+ }
19
+
20
+ function isSubmoduleInitialized(name: string): boolean {
21
+ return __existsSyncForDocs(__resolveForDocs(REPO_ROOT, name, ".git"));
22
+ }
23
+
24
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
25
+ import { join, relative } from "node:path";
26
+
27
+ /**
28
+ * Surfaces stalled lifecycle states across the monorepo: plans in-progress
29
+ * too long, open issues gone cold, runbooks that haven't been verified, etc.
30
+ *
31
+ * This is the opposite of `docs --stale`, which flags file-level freshness.
32
+ * Sweep only flags items where the lifecycle state (status header + mtime)
33
+ * suggests attention is needed.
34
+ *
35
+ * Audit files and issue files under `docs/audits/` are explicitly **not**
36
+ * flagged for age; they're immutable records by design.
37
+ */
38
+
39
+ export interface SweepOpts {
40
+ repo?: string;
41
+ }
42
+
43
+ export interface SweepItem {
44
+ kind: SweepKind;
45
+ repo: string;
46
+ path: string; // relative to monorepo root
47
+ ageDays: number;
48
+ message: string;
49
+ }
50
+
51
+ export type SweepKind =
52
+ | "stalled-plan" // status: in-progress, untouched >90d
53
+ | "unarchived-shipped" // status: shipped, in plans/ (not archive/) >180d
54
+ | "open-issue-cold" // status: open, untouched >30d
55
+ | "cold-handoff" // status: open, untouched >30d (docs/handoffs/)
56
+ | "runbook-unverified" // docs/runbook.md untouched >180d
57
+ | "topic-doc-stale" // docs/<topic>/... untouched >365d
58
+ | "decisions-dormant"; // docs/decisions.md untouched >180d (active repos)
59
+
60
+ const STALLED_PLAN_DAYS = 90;
61
+ const UNARCHIVED_SHIPPED_DAYS = 180;
62
+ const OPEN_ISSUE_DAYS = 30;
63
+ const COLD_HANDOFF_DAYS = 30;
64
+ const RUNBOOK_DAYS = 180;
65
+ const TOPIC_DOC_DAYS = 365;
66
+ const DECISIONS_DORMANT_DAYS = 180;
67
+
68
+ /** Days since last git commit touching a file */
69
+ async function lastCommitAgeDays(cwd: string, file: string): Promise<number | null> {
70
+ const result = await sh(`git log -1 --format=%aI -- "${file}"`, { cwd });
71
+ if (result.exitCode !== 0 || !result.stdout.trim()) return null;
72
+ const ms = Date.now() - new Date(result.stdout.trim()).getTime();
73
+ return Math.floor(ms / (1000 * 60 * 60 * 24));
74
+ }
75
+
76
+ /** Days since ANY commit in the repo (measures repo activity) */
77
+ async function lastRepoCommitAgeDays(cwd: string): Promise<number | null> {
78
+ const result = await sh("git log -1 --format=%aI", { cwd });
79
+ if (result.exitCode !== 0 || !result.stdout.trim()) return null;
80
+ const ms = Date.now() - new Date(result.stdout.trim()).getTime();
81
+ return Math.floor(ms / (1000 * 60 * 60 * 24));
82
+ }
83
+
84
+ function readStatus(filePath: string): string | null {
85
+ try {
86
+ const content = readFileSync(filePath, "utf8");
87
+ const head = content.split("\n").slice(0, 20).join("\n");
88
+ const m = head.match(/\*\*Status:\*\*\s*([a-zA-Z][a-zA-Z-]*)/);
89
+ return m ? m[1]!.toLowerCase() : null;
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
94
+
95
+ async function sweepPlans(repoName: string, repoPath: string, items: SweepItem[]): Promise<void> {
96
+ const plansDir = join(repoPath, "docs", "plans");
97
+ if (!existsSync(plansDir)) return;
98
+
99
+ const walk = (dir: string): string[] => {
100
+ const out: string[] = [];
101
+ for (const entry of readdirSync(dir)) {
102
+ const full = join(dir, entry);
103
+ let st: ReturnType<typeof statSync> | undefined;
104
+ try {
105
+ st = statSync(full);
106
+ } catch {
107
+ continue;
108
+ }
109
+ if (st.isDirectory()) {
110
+ out.push(...walk(full));
111
+ } else if (entry.endsWith(".md") && entry !== "README.md") {
112
+ out.push(full);
113
+ }
114
+ }
115
+ return out;
116
+ };
117
+
118
+ const files = walk(plansDir);
119
+ for (const full of files) {
120
+ const rel = relative(repoPath, full);
121
+ const displayPath = join(repoName === "(root)" ? "" : repoName, rel);
122
+ const isArchived = rel.includes("/archive/");
123
+ const status = readStatus(full);
124
+ const age = await lastCommitAgeDays(repoPath, rel);
125
+ if (age == null) continue;
126
+
127
+ if (!isArchived && status === "in-progress" && age > STALLED_PLAN_DAYS) {
128
+ items.push({
129
+ kind: "stalled-plan",
130
+ repo: repoName,
131
+ path: displayPath,
132
+ ageDays: age,
133
+ message: `plan is in-progress, last touched ${age}d ago; confirm still active or bump status`,
134
+ });
135
+ }
136
+ if (!isArchived && status === "shipped" && age > UNARCHIVED_SHIPPED_DAYS) {
137
+ items.push({
138
+ kind: "unarchived-shipped",
139
+ repo: repoName,
140
+ path: displayPath,
141
+ ageDays: age,
142
+ message: `plan is shipped but still in docs/plans/ after ${age}d; move to docs/plans/archive/`,
143
+ });
144
+ }
145
+ }
146
+ }
147
+
148
+ async function sweepIssues(repoName: string, repoPath: string, items: SweepItem[]): Promise<void> {
149
+ const issuesDir = join(repoPath, "docs", "issues");
150
+ if (!existsSync(issuesDir)) return;
151
+
152
+ for (const entry of readdirSync(issuesDir)) {
153
+ if (!entry.endsWith(".md") || entry === "README.md") continue;
154
+ const full = join(issuesDir, entry);
155
+ const rel = join("docs", "issues", entry);
156
+ const displayPath = join(repoName === "(root)" ? "" : repoName, rel);
157
+ const status = readStatus(full);
158
+ if (status !== "open") continue;
159
+ const age = await lastCommitAgeDays(repoPath, rel);
160
+ if (age == null || age <= OPEN_ISSUE_DAYS) continue;
161
+ items.push({
162
+ kind: "open-issue-cold",
163
+ repo: repoName,
164
+ path: displayPath,
165
+ ageDays: age,
166
+ message: `issue still marked open after ${age}d; resolve, mark wontfix, or triage`,
167
+ });
168
+ }
169
+ }
170
+
171
+ async function sweepHandoffs(
172
+ repoName: string,
173
+ repoPath: string,
174
+ items: SweepItem[],
175
+ ): Promise<void> {
176
+ const handoffsDir = join(repoPath, "docs", "handoffs");
177
+ if (!existsSync(handoffsDir)) return;
178
+
179
+ const walk = (dir: string): string[] => {
180
+ const out: string[] = [];
181
+ for (const entry of readdirSync(dir)) {
182
+ const full = join(dir, entry);
183
+ let st: ReturnType<typeof statSync> | undefined;
184
+ try {
185
+ st = statSync(full);
186
+ } catch {
187
+ continue;
188
+ }
189
+ if (st.isDirectory()) {
190
+ out.push(...walk(full));
191
+ } else if (entry.endsWith(".md")) {
192
+ out.push(full);
193
+ }
194
+ }
195
+ return out;
196
+ };
197
+
198
+ for (const full of walk(handoffsDir)) {
199
+ const rel = relative(repoPath, full);
200
+ const status = readStatus(full);
201
+ if (status !== "open") continue;
202
+ const age = await lastCommitAgeDays(repoPath, rel);
203
+ if (age == null || age <= COLD_HANDOFF_DAYS) continue;
204
+ const displayPath = join(repoName === "(root)" ? "" : repoName, rel);
205
+ items.push({
206
+ kind: "cold-handoff",
207
+ repo: repoName,
208
+ path: displayPath,
209
+ ageDays: age,
210
+ message: `handoff still marked open after ${age}d; resolve, abandon, or note current progress`,
211
+ });
212
+ }
213
+ }
214
+
215
+ async function sweepRunbook(repoName: string, repoPath: string, items: SweepItem[]): Promise<void> {
216
+ const runbook = join(repoPath, "docs", "runbook.md");
217
+ if (!existsSync(runbook)) return;
218
+ const age = await lastCommitAgeDays(repoPath, "docs/runbook.md");
219
+ if (age == null || age <= RUNBOOK_DAYS) return;
220
+ const displayPath = join(repoName === "(root)" ? "" : repoName, "docs/runbook.md");
221
+ items.push({
222
+ kind: "runbook-unverified",
223
+ repo: repoName,
224
+ path: displayPath,
225
+ ageDays: age,
226
+ message: `runbook hasn't been edited in ${age}d; re-verify procedures`,
227
+ });
228
+ }
229
+
230
+ async function sweepTopicDocs(
231
+ repoName: string,
232
+ repoPath: string,
233
+ items: SweepItem[],
234
+ ): Promise<void> {
235
+ const docsDir = join(repoPath, "docs");
236
+ if (!existsSync(docsDir)) return;
237
+
238
+ // Skip known date-stamped or lifecycle-managed dirs
239
+ const skipDirs = new Set([
240
+ "audits",
241
+ "issues",
242
+ "plans",
243
+ "changelogs",
244
+ "emails", // parent-specific
245
+ ]);
246
+
247
+ for (const entry of readdirSync(docsDir)) {
248
+ if (skipDirs.has(entry)) continue;
249
+ const full = join(docsDir, entry);
250
+ let st: ReturnType<typeof statSync> | undefined;
251
+ try {
252
+ st = statSync(full);
253
+ } catch {
254
+ continue;
255
+ }
256
+ if (!st.isDirectory()) continue;
257
+
258
+ // For each topic dir, find .md files and check age
259
+ const walk = (dir: string): string[] => {
260
+ const out: string[] = [];
261
+ for (const f of readdirSync(dir)) {
262
+ const fp = join(dir, f);
263
+ let s: ReturnType<typeof statSync> | undefined;
264
+ try {
265
+ s = statSync(fp);
266
+ } catch {
267
+ continue;
268
+ }
269
+ if (s.isDirectory()) out.push(...walk(fp));
270
+ else if (f.endsWith(".md")) out.push(fp);
271
+ }
272
+ return out;
273
+ };
274
+
275
+ for (const full2 of walk(full)) {
276
+ const rel = relative(repoPath, full2);
277
+ const age = await lastCommitAgeDays(repoPath, rel);
278
+ if (age == null || age <= TOPIC_DOC_DAYS) continue;
279
+ const displayPath = join(repoName === "(root)" ? "" : repoName, rel);
280
+ items.push({
281
+ kind: "topic-doc-stale",
282
+ repo: repoName,
283
+ path: displayPath,
284
+ ageDays: age,
285
+ message: `topic doc untouched for ${age}d; confirm still accurate or archive`,
286
+ });
287
+ }
288
+ }
289
+ }
290
+
291
+ async function sweepDecisions(
292
+ repoName: string,
293
+ repoPath: string,
294
+ items: SweepItem[],
295
+ ): Promise<void> {
296
+ const decisions = join(repoPath, "docs", "decisions.md");
297
+ if (!existsSync(decisions)) return;
298
+ const age = await lastCommitAgeDays(repoPath, "docs/decisions.md");
299
+ const repoAge = await lastRepoCommitAgeDays(repoPath);
300
+ if (age == null || repoAge == null) return;
301
+ // Only flag if the repo is active (recent commits) but decisions haven't moved
302
+ if (age <= DECISIONS_DORMANT_DAYS) return;
303
+ if (repoAge > 30) return; // repo itself is dormant, nothing to decide
304
+ const displayPath = join(repoName === "(root)" ? "" : repoName, "docs/decisions.md");
305
+ items.push({
306
+ kind: "decisions-dormant",
307
+ repo: repoName,
308
+ path: displayPath,
309
+ ageDays: age,
310
+ message: `repo is active but decisions.md hasn't been appended to in ${age}d; any architectural choices undocumented?`,
311
+ });
312
+ }
313
+
314
+ export async function runSweep(opts: SweepOpts): Promise<SweepItem[]> {
315
+ const targets: { name: string; path: string }[] = [{ name: "(root)", path: REPO_ROOT }];
316
+ for (const name of SUBMODULES) {
317
+ if (!isSubmoduleInitialized(name)) continue;
318
+ targets.push({ name, path: submodulePath(name) });
319
+ }
320
+
321
+ const filter = opts.repo === "." ? "(root)" : opts.repo;
322
+ const filtered = filter ? targets.filter((t) => t.name === filter) : targets;
323
+
324
+ const items: SweepItem[] = [];
325
+ for (const { name, path } of filtered) {
326
+ await sweepPlans(name, path, items);
327
+ await sweepIssues(name, path, items);
328
+ await sweepHandoffs(name, path, items);
329
+ await sweepRunbook(name, path, items);
330
+ await sweepTopicDocs(name, path, items);
331
+ await sweepDecisions(name, path, items);
332
+ }
333
+
334
+ // Sort by severity proxy: oldest first within kind
335
+ items.sort((a, b) => b.ageDays - a.ageDays);
336
+ return items;
337
+ }
338
+
339
+ /**
340
+ * Cheap parent-repo-only count of `cold-handoff` items, used by `docs lint`
341
+ * to print a one-line nudge without running the full sweep across every
342
+ * submodule (handoffs live under the parent's docs/handoffs/ by convention).
343
+ */
344
+ export async function countColdHandoffs(): Promise<number> {
345
+ const items: SweepItem[] = [];
346
+ await sweepHandoffs("(root)", REPO_ROOT, items);
347
+ return items.length;
348
+ }
@@ -0,0 +1,199 @@
1
+ import { existsSync as __existsSyncForDocs } from "node:fs";
2
+ import { resolve as __resolveForDocs } from "node:path";
3
+ import { sh } from "./exec.ts";
4
+
5
+ // Module-level docs context, initialized by initDocsContext() before any
6
+ // other function in this file is called. The repo root + submodule list
7
+ // are passed via the context provided to registerDocsCommand.
8
+ let REPO_ROOT = "";
9
+ let SUBMODULES: readonly string[] = [];
10
+
11
+ export function initDocsContext(opts: { repoRoot: string; submodules: readonly string[] }): void {
12
+ REPO_ROOT = opts.repoRoot;
13
+ SUBMODULES = opts.submodules;
14
+ }
15
+
16
+ function submodulePath(name: string): string {
17
+ return __resolveForDocs(REPO_ROOT, name);
18
+ }
19
+
20
+ function isSubmoduleInitialized(name: string): boolean {
21
+ return __existsSyncForDocs(__resolveForDocs(REPO_ROOT, name, ".git"));
22
+ }
23
+
24
+ export interface DocCommit {
25
+ date: string; // ISO date string
26
+ message: string; // First line of commit message
27
+ }
28
+
29
+ export interface DocFileInfo {
30
+ path: string; // Relative to repo root (e.g., "docs/foo.md" or "subdir/README.md")
31
+ dir: string; // Top-level directory (e.g., "docs", "subdir", "(root)")
32
+ commits: DocCommit[];
33
+ lastCommitDate: string | null; // ISO date or null if untracked
34
+ lastCommitAgeDays: number | null; // Days since last commit, or null if untracked
35
+ untracked: boolean;
36
+ }
37
+
38
+ /** Directories/patterns to skip even within git-tracked files */
39
+ const SKIP_PATTERNS = [
40
+ /\/\.cursor\//,
41
+ /\/\.claude\//,
42
+ /\/\.pytest_cache\//,
43
+ /\/AGENTS\.md$/, // Auto-generated
44
+ ];
45
+
46
+ /** Top-level directories to ignore by default (path prefix match) */
47
+ const IGNORE_DIRS = ["docs/vendors"];
48
+
49
+ /**
50
+ * Scan for .md files across the parent repo and all submodules.
51
+ * Uses `git ls-files` to only find tracked files (skips .venv, node_modules, etc.).
52
+ */
53
+ export async function scanDocs(opts: {
54
+ commitCount?: number;
55
+ dir?: string;
56
+ noSubmodules?: boolean;
57
+ staleDays?: number;
58
+ }): Promise<DocFileInfo[]> {
59
+ const commitCount = opts.commitCount ?? 1;
60
+ const now = Date.now();
61
+
62
+ // Collect tracked .md files from parent repo
63
+ const parentFiles = await getTrackedMdFiles(REPO_ROOT, "");
64
+
65
+ // Collect tracked .md files from each initialized submodule (unless --no-submodules)
66
+ let submoduleFiles: string[] = [];
67
+ if (!opts.noSubmodules) {
68
+ const submoduleResults = await Promise.all(
69
+ SUBMODULES.filter(isSubmoduleInitialized).map(async (name) => {
70
+ const cwd = submodulePath(name);
71
+ return getTrackedMdFiles(cwd, name);
72
+ }),
73
+ );
74
+ submoduleFiles = submoduleResults.flat();
75
+ }
76
+
77
+ let allFiles = [...parentFiles, ...submoduleFiles];
78
+
79
+ // Filter out skipped patterns and ignored directories
80
+ allFiles = allFiles.filter(
81
+ (f) =>
82
+ !SKIP_PATTERNS.some((p) => p.test(`/${f}`)) &&
83
+ !IGNORE_DIRS.some((d) => f.startsWith(`${d}/`)),
84
+ );
85
+
86
+ // Filter by directory if specified
87
+ if (opts.dir) {
88
+ // Treat "." as an alias for "(root)", the natural way to say "root-level files"
89
+ const dirFilter = opts.dir === "." ? "(root)" : opts.dir.toLowerCase();
90
+ allFiles = allFiles.filter((f) => {
91
+ const topDir = getTopDir(f).toLowerCase();
92
+ // Exact match first, fall back to substring
93
+ return topDir === dirFilter || topDir.includes(dirFilter);
94
+ });
95
+ }
96
+
97
+ // Fetch git history for all files, batched in parallel groups of 20
98
+ const results: DocFileInfo[] = [];
99
+ const batchSize = 20;
100
+
101
+ for (let i = 0; i < allFiles.length; i += batchSize) {
102
+ const batch = allFiles.slice(i, i + batchSize);
103
+ const batchResults = await Promise.all(
104
+ batch.map((filePath) => getFileInfo(filePath, commitCount, now)),
105
+ );
106
+ results.push(...batchResults);
107
+ }
108
+
109
+ // Filter by stale days if specified
110
+ if (opts.staleDays) {
111
+ const minAge = opts.staleDays;
112
+ return results.filter(
113
+ (r) => r.untracked || (r.lastCommitAgeDays !== null && r.lastCommitAgeDays >= minAge),
114
+ );
115
+ }
116
+
117
+ return results;
118
+ }
119
+
120
+ /** Get top-level directory for grouping */
121
+ function getTopDir(filePath: string): string {
122
+ const parts = filePath.split("/");
123
+ if (parts.length === 1) return "(root)";
124
+ return parts[0]!;
125
+ }
126
+
127
+ /** Get tracked .md files from a git repo */
128
+ async function getTrackedMdFiles(cwd: string, prefix: string): Promise<string[]> {
129
+ // Use shell to avoid pathspec vs glob ambiguity; ** ensures recursive matching
130
+ const result = await sh('git ls-files --cached "**/*.md" "*.md"', { cwd });
131
+ if (result.exitCode !== 0 || !result.stdout) return [];
132
+
133
+ return result.stdout
134
+ .split("\n")
135
+ .filter((f) => f.endsWith(".md"))
136
+ .map((f) => (prefix ? `${prefix}/${f}` : f));
137
+ }
138
+
139
+ /** Get file info with git history */
140
+ async function getFileInfo(
141
+ filePath: string,
142
+ commitCount: number,
143
+ now: number,
144
+ ): Promise<DocFileInfo> {
145
+ const dir = getTopDir(filePath);
146
+
147
+ // Determine which repo to query and the relative path within it
148
+ const { cwd, relativePath } = resolveFilePath(filePath);
149
+
150
+ const logResult = await sh(`git log -${commitCount} --format="%aI%x09%s" -- "${relativePath}"`, {
151
+ cwd,
152
+ });
153
+
154
+ if (logResult.exitCode !== 0 || !logResult.stdout.trim()) {
155
+ return {
156
+ path: filePath,
157
+ dir,
158
+ commits: [],
159
+ lastCommitDate: null,
160
+ lastCommitAgeDays: null,
161
+ untracked: true,
162
+ };
163
+ }
164
+
165
+ const commits: DocCommit[] = logResult.stdout
166
+ .trim()
167
+ .split("\n")
168
+ .map((line) => {
169
+ const [date, ...msgParts] = line.split("\t");
170
+ return { date: date!, message: msgParts.join("\t") };
171
+ });
172
+
173
+ const lastDate = commits[0]?.date ?? null;
174
+ const ageDays = lastDate
175
+ ? Math.floor((now - new Date(lastDate).getTime()) / (1000 * 60 * 60 * 24))
176
+ : null;
177
+
178
+ return {
179
+ path: filePath,
180
+ dir,
181
+ commits,
182
+ lastCommitDate: lastDate,
183
+ lastCommitAgeDays: ageDays,
184
+ untracked: false,
185
+ };
186
+ }
187
+
188
+ /** Resolve a monorepo-relative path to the correct git repo cwd + relative path */
189
+ function resolveFilePath(filePath: string): { cwd: string; relativePath: string } {
190
+ for (const name of SUBMODULES) {
191
+ if (filePath.startsWith(`${name}/`)) {
192
+ return {
193
+ cwd: submodulePath(name),
194
+ relativePath: filePath.slice(name.length + 1),
195
+ };
196
+ }
197
+ }
198
+ return { cwd: REPO_ROOT, relativePath: filePath };
199
+ }
package/src/lib/env.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Read a harnery environment variable by its suffix (the part after the
3
+ * `HARNERY_` prefix). Centralizes the env-var namespace so the prefix lives in
4
+ * exactly one place.
5
+ *
6
+ * Examples:
7
+ * coordEnv("AGENT_COORD_OWNER") → process.env.HARNERY_AGENT_COORD_OWNER
8
+ * coordEnv("COORD_ROOT_OVERRIDE") → process.env.HARNERY_COORD_ROOT_OVERRIDE
9
+ */
10
+ export function coordEnv(suffix: string): string | undefined {
11
+ return process.env[`HARNERY_${suffix}`];
12
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Generic process-spawn helpers shared by harnery commands. Callers pass
3
+ * `cwd` explicitly or fall back to `process.cwd()`.
4
+ *
5
+ * Built on `node:child_process` so it runs identically under Bun and Node.
6
+ * harnery's published package targets Node ≥ 20, and Bun implements the same
7
+ * module, so there's one implementation, not a runtime fork.
8
+ */
9
+
10
+ import { spawn } from "node:child_process";
11
+
12
+ export interface ExecResult {
13
+ stdout: string;
14
+ stderr: string;
15
+ exitCode: number;
16
+ }
17
+
18
+ export interface ExecOpts {
19
+ cwd?: string;
20
+ timeout?: number;
21
+ env?: Record<string, string>;
22
+ trim?: boolean;
23
+ }
24
+
25
+ /**
26
+ * Run a shell command and return stdout/stderr/exitCode.
27
+ *
28
+ * `trim` defaults to true (back-compat with every existing caller). Pass
29
+ * `trim: false` when the consumer depends on leading whitespace. `git status
30
+ * --porcelain` is the canonical case: the first line of ` M PATH` output gets
31
+ * its leading space stripped by .trim(), shifting the X/Y status columns and
32
+ * breaking any positional parser. Trailing newline still gets dropped.
33
+ */
34
+ export function exec(cmd: string[], opts: ExecOpts = {}): Promise<ExecResult> {
35
+ const [file, ...args] = cmd;
36
+ return new Promise<ExecResult>((resolveResult) => {
37
+ const proc = spawn(file, args, {
38
+ cwd: opts.cwd ?? process.cwd(),
39
+ env: opts.env ?? { ...process.env },
40
+ stdio: ["ignore", "pipe", "pipe"],
41
+ });
42
+
43
+ const out: Buffer[] = [];
44
+ const err: Buffer[] = [];
45
+ proc.stdout?.on("data", (d: Buffer) => out.push(d));
46
+ proc.stderr?.on("data", (d: Buffer) => err.push(d));
47
+
48
+ const timeout = opts.timeout ?? 30_000;
49
+ const timer = setTimeout(() => proc.kill(), timeout);
50
+
51
+ const finish = (exitCode: number, errOverride?: string): void => {
52
+ clearTimeout(timer);
53
+ const stdout = Buffer.concat(out).toString("utf-8");
54
+ const stderr = errOverride ?? Buffer.concat(err).toString("utf-8");
55
+ const shouldTrim = opts.trim !== false;
56
+ resolveResult({
57
+ stdout: shouldTrim ? stdout.trim() : stdout.replace(/\n$/, ""),
58
+ stderr: shouldTrim ? stderr.trim() : stderr.replace(/\n$/, ""),
59
+ exitCode,
60
+ });
61
+ };
62
+
63
+ // ENOENT (binary not found) and similar spawn failures surface here rather
64
+ // than throwing: resolve with 127 + the message so callers' exitCode
65
+ // branches handle it instead of crashing on an unhandled rejection.
66
+ proc.on("error", (e: Error) => finish(127, e.message));
67
+ proc.on("close", (code) => finish(code ?? 1));
68
+ });
69
+ }
70
+
71
+ /** Run a shell command via /bin/sh -c (for pipes, redirects, etc.) */
72
+ export async function sh(command: string, opts: Omit<ExecOpts, "trim"> = {}): Promise<ExecResult> {
73
+ return exec(["sh", "-c", command], opts);
74
+ }