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,803 @@
1
+ /**
2
+ * Council manifest helpers: file-based multi-agent coordination primitives.
3
+ *
4
+ * Lives under .harnery/councils/ alongside heartbeats + scratchpads. Council
5
+ * lifecycle commands serialize manifest mutations through a shared flock;
6
+ * round contribution files are per-member and don't need shared locking.
7
+ */
8
+
9
+ import { createHash, randomBytes } from "node:crypto";
10
+ import {
11
+ existsSync,
12
+ mkdirSync,
13
+ readdirSync,
14
+ readFileSync,
15
+ renameSync,
16
+ rmSync,
17
+ writeFileSync,
18
+ } from "node:fs";
19
+ import { basename, resolve } from "node:path";
20
+
21
+ import { monorepoRoot } from "../../core/agents/index.ts";
22
+ import { resolveBinName } from "../../core/config.ts";
23
+ import { ensureIdentity, lookupById as lookupIdentityById } from "../identities/index.js";
24
+
25
+ export const COUNCIL_SCHEMA_VERSION = 2 as const;
26
+
27
+ export type CouncilStatus = "active" | "closed" | "archived";
28
+ export type CouncilRoundStatus = "open" | "collected";
29
+ export type CouncilRoundVisibility = "next_round" | "live";
30
+
31
+ export interface CouncilManifest {
32
+ schema_version: typeof COUNCIL_SCHEMA_VERSION;
33
+ council_id: string;
34
+ created_at: string;
35
+ /** Display name of the convener (denormalized, for human scan).
36
+ * Canonical FK is `created_by_id`. */
37
+ created_by: string;
38
+ /** Durable persona UUID of the convener (registry key). Authoritative. */
39
+ created_by_id: string;
40
+ /**
41
+ * Optional ongoing process-tender. Distinct from `created_by`, which is the
42
+ * one-time act of creation; the steward is whoever drafts + maintains the
43
+ * per-round prompts that route operator → contributor each round. Defaults
44
+ * to `created_by` when omitted (set at create-time via --steward, or
45
+ * retrofitted via direct manifest edit). Read by `agents council prompt`
46
+ * to enforce write authority.
47
+ *
48
+ * Display name (denormalized). Canonical FK is `steward_id`.
49
+ */
50
+ steward?: string;
51
+ /** Durable persona UUID of the steward. */
52
+ steward_id?: string;
53
+ objective: string;
54
+ target_doc: string | null;
55
+ /** Member display names (denormalized, parallel to `member_ids`). */
56
+ members: string[];
57
+ /** Canonical FK array of durable persona UUIDs of every member, parallel
58
+ * to `members[]` (same length, same order). Used by contributors-in-round
59
+ * lookups and contribution filenames (`round-N/<member_id>.md`). */
60
+ member_ids: string[];
61
+ current_round: number;
62
+ round_status: CouncilRoundStatus;
63
+ status: CouncilStatus;
64
+ auto_advance: boolean;
65
+ round_visibility: CouncilRoundVisibility;
66
+ closed_at?: string;
67
+ archived_at?: string;
68
+ }
69
+
70
+ /**
71
+ * Resolve the effective steward: explicit `steward` field if set, otherwise
72
+ * fall back to `created_by`. Always returns a normalized `agent-Foo` name.
73
+ */
74
+ export function effectiveSteward(manifest: CouncilManifest): string {
75
+ return normalizeAgentName(manifest.steward || manifest.created_by);
76
+ }
77
+
78
+ /** Resolve `.harnery/councils/` (creates the dir lazily on first write). */
79
+ export function councilsDir(): string | null {
80
+ const root = monorepoRoot();
81
+ if (!root) return null;
82
+ return resolve(root, ".harnery", "councils");
83
+ }
84
+
85
+ /** Resolve `.harnery/councils/archive/`. */
86
+ export function councilsArchiveDir(): string | null {
87
+ const cd = councilsDir();
88
+ if (!cd) return null;
89
+ return resolve(cd, "archive");
90
+ }
91
+
92
+ /** Manifest file path: `.harnery/councils/<id>.json`. */
93
+ export function manifestPath(councilId: string): string | null {
94
+ const cd = councilsDir();
95
+ if (!cd) return null;
96
+ return resolve(cd, `${councilId}.json`);
97
+ }
98
+
99
+ /** Council body dir: `.harnery/councils/<id>/` (holds invite.md + round-N/...). */
100
+ export function councilBodyDir(councilId: string): string | null {
101
+ const cd = councilsDir();
102
+ if (!cd) return null;
103
+ return resolve(cd, councilId);
104
+ }
105
+
106
+ /** Normalize an `agent-Foo`/`Foo` reference to canonical `agent-Foo` form. */
107
+ export function normalizeAgentName(raw: string): string {
108
+ const trimmed = raw.trim();
109
+ if (!trimmed) return "";
110
+ return trimmed.startsWith("agent-") ? trimmed : `agent-${trimmed}`;
111
+ }
112
+
113
+ /** Strip the `agent-` prefix for output to lookups that expect bare name. */
114
+ export function bareAgentName(raw: string): string {
115
+ return raw.startsWith("agent-") ? raw.slice("agent-".length) : raw;
116
+ }
117
+
118
+ /**
119
+ * Derive a kebab-case slug from objective text. Keeps the first 5 words after
120
+ * lowercasing and stripping non-alphanumeric chars.
121
+ */
122
+ export function deriveSlug(objective: string): string {
123
+ const cleaned = objective
124
+ .toLowerCase()
125
+ .replace(/[^a-z0-9\s-]+/g, " ")
126
+ .split(/\s+/)
127
+ .filter(Boolean)
128
+ .slice(0, 5)
129
+ .join("-")
130
+ .replace(/-+/g, "-")
131
+ .replace(/^-|-$/g, "");
132
+ return cleaned || "council";
133
+ }
134
+
135
+ /**
136
+ * Build a council_id from objective + today's UTC date.
137
+ *
138
+ * Format: `<slug>-<YYYY-MM-DD>-<4hex>`. The 4-hex suffix is sourced from a
139
+ * crypto-strong random byte (not from a hash of the objective) to avoid
140
+ * collisions when two councils share the same slug + date.
141
+ */
142
+ export function buildCouncilId(objective: string, now: Date = new Date()): string {
143
+ const slug = deriveSlug(objective);
144
+ const date = now.toISOString().slice(0, 10);
145
+ const hash = randomBytes(2).toString("hex");
146
+ return `${slug}-${date}-${hash}`;
147
+ }
148
+
149
+ /** Hash an objective deterministically, used by tests for stable IDs. */
150
+ export function deterministicCouncilId(objective: string, now: Date = new Date()): string {
151
+ const slug = deriveSlug(objective);
152
+ const date = now.toISOString().slice(0, 10);
153
+ const hash = createHash("sha256").update(`${objective}|${date}`).digest("hex").slice(0, 4);
154
+ return `${slug}-${date}-${hash}`;
155
+ }
156
+
157
+ /** Atomically write a manifest (write tmp → rename). */
158
+ export function writeManifest(manifest: CouncilManifest): void {
159
+ const mp = manifestPath(manifest.council_id);
160
+ if (!mp) throw new Error("not in an agent session; no monorepo root");
161
+ const cd = councilsDir();
162
+ if (cd && !existsSync(cd)) mkdirSync(cd, { recursive: true });
163
+ const tmp = `${mp}.tmp.${process.pid}`;
164
+ writeFileSync(tmp, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
165
+ renameSync(tmp, mp);
166
+ }
167
+
168
+ /** Read a manifest by id. Returns null when the file is missing. */
169
+ export function readManifest(councilId: string): CouncilManifest | null {
170
+ const mp = manifestPath(councilId);
171
+ if (!mp || !existsSync(mp)) return null;
172
+ try {
173
+ const parsed = JSON.parse(readFileSync(mp, "utf8")) as CouncilManifest;
174
+ if (parsed.schema_version !== COUNCIL_SCHEMA_VERSION) {
175
+ throw new Error(
176
+ `council ${councilId}: unsupported schema_version=${parsed.schema_version} (expected ${COUNCIL_SCHEMA_VERSION})`,
177
+ );
178
+ }
179
+ return parsed;
180
+ } catch (err) {
181
+ throw new Error(
182
+ `failed to read council manifest ${councilId}: ${err instanceof Error ? err.message : String(err)}`,
183
+ );
184
+ }
185
+ }
186
+
187
+ /** Read an archived manifest by id from `.harnery/councils/archive/<id>.json`.
188
+ * Symmetric to readManifest() but scoped to the archive dir, used by
189
+ * `agents council unarchive` to load an archived council that is
190
+ * (by definition) not in the active councils dir. */
191
+ export function readArchivedManifest(councilId: string): CouncilManifest | null {
192
+ const archive = councilsArchiveDir();
193
+ if (!archive) return null;
194
+ const mp = resolve(archive, `${councilId}.json`);
195
+ if (!existsSync(mp)) return null;
196
+ try {
197
+ const parsed = JSON.parse(readFileSync(mp, "utf8")) as CouncilManifest;
198
+ if (parsed.schema_version !== COUNCIL_SCHEMA_VERSION) {
199
+ throw new Error(
200
+ `council ${councilId}: unsupported schema_version=${parsed.schema_version} (expected ${COUNCIL_SCHEMA_VERSION})`,
201
+ );
202
+ }
203
+ return parsed;
204
+ } catch (err) {
205
+ throw new Error(
206
+ `failed to read archived council manifest ${councilId}: ${err instanceof Error ? err.message : String(err)}`,
207
+ );
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Reassign the steward on an active or closed council. Atomic via
213
+ * writeManifest's tmp+rename. Refuses to mutate archived councils
214
+ * (those are read-only by convention). Pass `null` to clear the field
215
+ * and revert to the default (the convener, via effectiveSteward).
216
+ */
217
+ export function setCouncilSteward(councilId: string, steward: string | null): CouncilManifest {
218
+ const manifest = readManifest(councilId);
219
+ if (!manifest) {
220
+ throw new Error(`no council matching '${councilId}' in .harnery/councils/`);
221
+ }
222
+ if (manifest.status === "archived") {
223
+ throw new Error(
224
+ `council ${manifest.council_id} is archived (read-only); cannot reassign steward`,
225
+ );
226
+ }
227
+ let next: CouncilManifest;
228
+ if (steward === null) {
229
+ const { steward: _ds, steward_id: _di, ...rest } = manifest;
230
+ void _ds;
231
+ void _di;
232
+ next = rest as CouncilManifest;
233
+ } else {
234
+ const identity = ensureIdentity(steward);
235
+ next = { ...manifest, steward, steward_id: identity.agent_id };
236
+ }
237
+ writeManifest(next);
238
+ return next;
239
+ }
240
+
241
+ export interface KnownAgent {
242
+ /** `agent-<Name>` canonical handle. */
243
+ name: string;
244
+ /** `active` = currently has a heartbeat in `.harnery/active/`. `stale` =
245
+ * recently ended (scratchpad archived within the lookback window). */
246
+ state: "active" | "stale";
247
+ /** ISO timestamp of the most-recent signal. */
248
+ last_seen: string;
249
+ }
250
+
251
+ /** Default lookback for "recently stale" agents, kept in sync with the
252
+ * next-app's same-named constant. */
253
+ const KNOWN_AGENT_STALE_WINDOW_MS = 30 * 24 * 60 * 60 * 1000;
254
+
255
+ /**
256
+ * Active heartbeats + recently-archived scratchpads, deduped by name.
257
+ * Used by `agents council set-steward` to refuse arbitrary names;
258
+ * pass `--allow-unknown` to bypass when bootstrapping a new agent.
259
+ */
260
+ export function listKnownAgents(): KnownAgent[] {
261
+ const root = monorepoRoot();
262
+ if (!root) return [];
263
+ const activeDir = resolve(root, ".harnery", "active");
264
+ const archiveDir = resolve(root, ".harnery", "scratch", "archived");
265
+ const byName = new Map<string, KnownAgent>();
266
+
267
+ if (existsSync(activeDir)) {
268
+ for (const f of readdirSync(activeDir)) {
269
+ if (!f.endsWith(".json")) continue;
270
+ try {
271
+ const hb = JSON.parse(readFileSync(resolve(activeDir, f), "utf8")) as {
272
+ name?: string;
273
+ last_heartbeat?: string;
274
+ };
275
+ if (!hb.name) continue;
276
+ const name = hb.name.startsWith("agent-") ? hb.name : `agent-${hb.name}`;
277
+ const last_seen = hb.last_heartbeat ?? new Date().toISOString();
278
+ const existing = byName.get(name);
279
+ if (existing?.state !== "active") {
280
+ byName.set(name, { name, state: "active", last_seen });
281
+ }
282
+ } catch {
283
+ /* skip unreadable */
284
+ }
285
+ }
286
+ }
287
+
288
+ const cutoff = Date.now() - KNOWN_AGENT_STALE_WINDOW_MS;
289
+ if (existsSync(archiveDir)) {
290
+ const fileTimestampRe = /-(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z\.md$/;
291
+ for (const f of readdirSync(archiveDir)) {
292
+ const m = f.match(fileTimestampRe);
293
+ if (!m) continue;
294
+ const iso = `${m[1]}T${m[2]}:${m[3]}:${m[4]}.${m[5]}Z`;
295
+ const ts = Date.parse(iso);
296
+ if (Number.isNaN(ts) || ts < cutoff) continue;
297
+ try {
298
+ const head = readFileSync(resolve(archiveDir, f), "utf8").slice(0, 200);
299
+ const nameMatch = head.match(/^#\s+Scratchpad:\s+(agent-[A-Za-z][A-Za-z0-9_-]*)/m);
300
+ if (!nameMatch) continue;
301
+ const name = nameMatch[1]!;
302
+ const existing = byName.get(name);
303
+ if (existing) {
304
+ if (existing.state === "stale" && iso > existing.last_seen) {
305
+ existing.last_seen = iso;
306
+ }
307
+ } else {
308
+ byName.set(name, { name, state: "stale", last_seen: iso });
309
+ }
310
+ } catch {
311
+ /* skip */
312
+ }
313
+ }
314
+ }
315
+
316
+ return Array.from(byName.values()).sort((a, b) => {
317
+ if (a.state !== b.state) return a.state === "active" ? -1 : 1;
318
+ return b.last_seen.localeCompare(a.last_seen);
319
+ });
320
+ }
321
+
322
+ /** List all council manifests in the active dir (skips archive/). */
323
+ export function listManifests(): CouncilManifest[] {
324
+ const cd = councilsDir();
325
+ if (!cd || !existsSync(cd)) return [];
326
+ const out: CouncilManifest[] = [];
327
+ for (const f of readdirSync(cd)) {
328
+ if (!f.endsWith(".json") || f === "archive") continue;
329
+ const id = f.slice(0, -5);
330
+ try {
331
+ const m = readManifest(id);
332
+ if (m) out.push(m);
333
+ } catch {
334
+ /* skip malformed manifests; surfaced separately if needed */
335
+ }
336
+ }
337
+ return out;
338
+ }
339
+
340
+ /**
341
+ * Move a council's manifest + body dir into the archive subdir. Idempotent:
342
+ * archiving an already-archived council is a no-op (the source paths won't
343
+ * exist). Used by `council archive` and by `council close --archive`.
344
+ */
345
+ export function moveToArchive(councilId: string): void {
346
+ const cd = councilsDir();
347
+ const archive = councilsArchiveDir();
348
+ if (!cd || !archive) {
349
+ throw new Error("not in an agent session; no monorepo root");
350
+ }
351
+ if (!existsSync(archive)) mkdirSync(archive, { recursive: true });
352
+
353
+ const srcManifest = resolve(cd, `${councilId}.json`);
354
+ const dstManifest = resolve(archive, `${councilId}.json`);
355
+ if (existsSync(srcManifest)) {
356
+ renameSync(srcManifest, dstManifest);
357
+ }
358
+
359
+ const srcDir = resolve(cd, councilId);
360
+ const dstDir = resolve(archive, councilId);
361
+ if (existsSync(srcDir)) {
362
+ if (existsSync(dstDir)) {
363
+ // already archived, keep the original archive, drop the duplicate
364
+ rmSync(srcDir, { recursive: true, force: true });
365
+ } else {
366
+ renameSync(srcDir, dstDir);
367
+ }
368
+ }
369
+ }
370
+
371
+ /**
372
+ * Reverse of moveToArchive: move a council's manifest + body dir back from
373
+ * archive/ to the active councils dir. Idempotent: a missing archive path is
374
+ * a no-op; an existing active path is left untouched and the archived copy
375
+ * is dropped (mirrors moveToArchive's clobber-avoidance rule). Used by
376
+ * `agents council unarchive` for testing the archive flow and as an undo
377
+ * escape hatch.
378
+ */
379
+ export function moveFromArchive(councilId: string): void {
380
+ const cd = councilsDir();
381
+ const archive = councilsArchiveDir();
382
+ if (!cd || !archive) {
383
+ throw new Error("not in an agent session; no monorepo root");
384
+ }
385
+
386
+ const srcManifest = resolve(archive, `${councilId}.json`);
387
+ const dstManifest = resolve(cd, `${councilId}.json`);
388
+ if (existsSync(srcManifest)) {
389
+ if (existsSync(dstManifest)) {
390
+ // already-active manifest wins; drop the archived copy
391
+ rmSync(srcManifest, { force: true });
392
+ } else {
393
+ renameSync(srcManifest, dstManifest);
394
+ }
395
+ }
396
+
397
+ const srcDir = resolve(archive, councilId);
398
+ const dstDir = resolve(cd, councilId);
399
+ if (existsSync(srcDir)) {
400
+ if (existsSync(dstDir)) {
401
+ rmSync(srcDir, { recursive: true, force: true });
402
+ } else {
403
+ renameSync(srcDir, dstDir);
404
+ }
405
+ }
406
+ }
407
+
408
+ /**
409
+ * Permanently remove an archived council: manifest + body dir under
410
+ * .harnery/councils/archive/<id>. Refuses to touch a council that's still
411
+ * in the active dir (caller must archive first; the trash-can pattern).
412
+ * Idempotent: missing paths are a no-op. Returns true when something was
413
+ * actually deleted, false when both targets were already absent.
414
+ *
415
+ * NB: does NOT touch the council's target_doc (separate authored artifact),
416
+ * close_handoff_path (separate authored artifact), or the canonical event
417
+ * stream (immutable activity log). The delete is scoped to the manifest +
418
+ * per-round member contributions.
419
+ */
420
+ export function deleteArchivedCouncil(councilId: string): boolean {
421
+ const cd = councilsDir();
422
+ const archive = councilsArchiveDir();
423
+ if (!cd || !archive) {
424
+ throw new Error("not in an agent session; no monorepo root");
425
+ }
426
+ const activeManifest = resolve(cd, `${councilId}.json`);
427
+ if (existsSync(activeManifest)) {
428
+ throw new Error(`council ${councilId} is not archived; archive it before delete`);
429
+ }
430
+ const archivedManifest = resolve(archive, `${councilId}.json`);
431
+ const archivedBody = resolve(archive, councilId);
432
+ let removed = false;
433
+ if (existsSync(archivedManifest)) {
434
+ rmSync(archivedManifest, { force: true });
435
+ removed = true;
436
+ }
437
+ if (existsSync(archivedBody)) {
438
+ rmSync(archivedBody, { recursive: true, force: true });
439
+ removed = true;
440
+ }
441
+ return removed;
442
+ }
443
+
444
+ /** Resolve a member name (or partial id) to a council manifest from the active dir. */
445
+ export function findManifestByPartialId(partial: string): CouncilManifest | null {
446
+ const cd = councilsDir();
447
+ if (!cd || !existsSync(cd)) return null;
448
+ for (const f of readdirSync(cd)) {
449
+ if (!f.endsWith(".json")) continue;
450
+ const id = f.slice(0, -5);
451
+ if (id === partial || id.includes(partial) || basename(f, ".json").startsWith(partial)) {
452
+ try {
453
+ return readManifest(id);
454
+ } catch {
455
+ return null;
456
+ }
457
+ }
458
+ }
459
+ return null;
460
+ }
461
+
462
+ /** Build the invite.md body that ships alongside the manifest. */
463
+ export function buildInviteMarkdown(manifest: CouncilManifest): string {
464
+ const lines: string[] = [];
465
+ lines.push(`# Council invitation: ${manifest.council_id}`);
466
+ lines.push("");
467
+ lines.push(`**Convened by:** ${manifest.created_by}`);
468
+ lines.push(`**Created:** ${manifest.created_at}`);
469
+ lines.push(`**Members:** ${manifest.members.join(", ")}`);
470
+ if (manifest.target_doc) {
471
+ lines.push(`**Target doc:** \`${manifest.target_doc}\``);
472
+ }
473
+ lines.push(
474
+ `**Auto-advance:** ${manifest.auto_advance ? "yes" : "no (convener advances each round manually)"}`,
475
+ );
476
+ lines.push(
477
+ `**Round visibility:** ${manifest.round_visibility} (peer contributions surface at round N+1 by default)`,
478
+ );
479
+ lines.push("");
480
+ lines.push("## Objective");
481
+ lines.push("");
482
+ lines.push(manifest.objective);
483
+ lines.push("");
484
+ lines.push("## How to participate");
485
+ lines.push("");
486
+ const bin = resolveBinName();
487
+ lines.push(
488
+ `1. Read the objective + target doc (if any).\n2. Run \`${bin} agents council show ${manifest.council_id}\` for full state.\n3. Contribute your round-${manifest.current_round} take with:\n\n ${bin} agents council contribute ${manifest.council_id} \\\n --message "<your take>"\n # or --file /path/to/written/feedback.md\n\n4. After all members contribute, ${manifest.created_by} (or auto-advance) opens round ${manifest.current_round + 1}.`,
489
+ );
490
+ lines.push("");
491
+ return lines.join("\n");
492
+ }
493
+
494
+ /**
495
+ * Path to the contribution file for a member in a specific round.
496
+ * Filename uses the agent's durable persona uuid (`<agent_id>.md`) so
497
+ * a future rename doesn't break the link between manifest and on-disk
498
+ * contribution. Resolves the name through the identity registry; mints a
499
+ * new identity when the member name isn't registered yet (rare post-
500
+ * migration; only happens for a brand-new persona that's never run).
501
+ */
502
+ export function contributionPath(
503
+ councilId: string,
504
+ round: number,
505
+ memberName: string,
506
+ ): string | null {
507
+ const body = councilBodyDir(councilId);
508
+ if (!body) return null;
509
+ const id = ensureIdentity(memberName).agent_id;
510
+ return resolve(body, `round-${round}`, `${id}.md`);
511
+ }
512
+
513
+ /** Path to a round's directory: `.harnery/councils/<id>/round-<N>/`. */
514
+ export function roundDir(councilId: string, round: number): string | null {
515
+ const body = councilBodyDir(councilId);
516
+ if (!body) return null;
517
+ return resolve(body, `round-${round}`);
518
+ }
519
+
520
+ /**
521
+ * Read the set of agent-Names that have contributed to a given round.
522
+ * Returns display names ("agent-Maya"), not raw uuids; filenames on disk
523
+ * are now `<agent_id>.md`, so we resolve each one through the registry
524
+ * before returning. An identity that's been pruned (or never registered)
525
+ * surfaces as `agent-<8-char-prefix>` so the value remains scannable.
526
+ *
527
+ * For UUID-keyed callers, use `contributorIdsInRound`.
528
+ * Empty array when the round directory doesn't exist yet.
529
+ */
530
+ export function contributorsInRound(councilId: string, round: number): string[] {
531
+ return contributorIdsInRound(councilId, round)
532
+ .map((id) => {
533
+ const identity = lookupIdentityById(id);
534
+ return identity ? `agent-${identity.name}` : `agent-${id.slice(0, 8)}`;
535
+ })
536
+ .sort();
537
+ }
538
+
539
+ /** Like contributorsInRound but returns the raw agent_id uuids: the
540
+ * filenames on disk without the .md extension. */
541
+ export function contributorIdsInRound(councilId: string, round: number): string[] {
542
+ const rd = roundDir(councilId, round);
543
+ if (!rd || !existsSync(rd)) return [];
544
+ return readdirSync(rd)
545
+ .filter((f) => f.endsWith(".md"))
546
+ .map((f) => f.slice(0, -3));
547
+ }
548
+
549
+ /**
550
+ * Return the IDs of active councils where this agent is a member AND has not
551
+ * yet contributed to the current open round. Used by `agents status` to
552
+ * surface a `council N pending` line in the status box, and by SessionStart
553
+ * adapters to inject system reminders about pending invites.
554
+ */
555
+ export function pendingCouncilsForMember(memberName: string): string[] {
556
+ const normalized = normalizeAgentName(memberName);
557
+ if (!normalized) return [];
558
+ const out: string[] = [];
559
+ for (const m of listManifests()) {
560
+ if (m.status !== "active") continue;
561
+ if (!m.members.includes(normalized)) continue;
562
+ if (m.round_status === "collected") continue;
563
+ const contributors = contributorsInRound(m.council_id, m.current_round);
564
+ if (contributors.includes(normalized)) continue;
565
+ out.push(m.council_id);
566
+ }
567
+ return out;
568
+ }
569
+
570
+ /** Write a contribution file (atomic). Creates the round directory lazily. */
571
+ export function writeContribution(
572
+ councilId: string,
573
+ round: number,
574
+ memberName: string,
575
+ body: string,
576
+ ): string {
577
+ const filePath = contributionPath(councilId, round, memberName);
578
+ if (!filePath) throw new Error("not in an agent session; no monorepo root");
579
+ const rd = roundDir(councilId, round);
580
+ if (rd && !existsSync(rd)) mkdirSync(rd, { recursive: true });
581
+ const tmp = `${filePath}.tmp.${process.pid}`;
582
+ writeFileSync(tmp, body, "utf8");
583
+ renameSync(tmp, filePath);
584
+ return filePath;
585
+ }
586
+
587
+ /**
588
+ * Path to the round-N prompts directory: `.harnery/councils/<id>/round-N/prompts/`.
589
+ * Sibling to the contribution files. Holds one `<member>.md` per non-self
590
+ * council member, drafted by the steward, read by the operator (copy-paste
591
+ * into each agent harness) and the web UI (per-member panel).
592
+ */
593
+ export function promptsDir(councilId: string, round: number): string | null {
594
+ const rd = roundDir(councilId, round);
595
+ if (!rd) return null;
596
+ return resolve(rd, "prompts");
597
+ }
598
+
599
+ /** Path to a single member's prompt file in a given round. Like
600
+ * contributionPath, filename uses the agent's durable persona uuid. */
601
+ export function promptPath(councilId: string, round: number, memberName: string): string | null {
602
+ const pd = promptsDir(councilId, round);
603
+ if (!pd) return null;
604
+ const id = ensureIdentity(memberName).agent_id;
605
+ return resolve(pd, `${id}.md`);
606
+ }
607
+
608
+ /**
609
+ * Build the routing header prepended to every steward-drafted prompt. The
610
+ * contributor skill scans inbound messages for this comment block; if the
611
+ * `member:` line does not match the receiving agent's whoami, the agent
612
+ * refuses to contribute (catches operator misrouting). HTML-comment so it
613
+ * renders invisibly in markdown previews.
614
+ */
615
+ export function buildRouteHeader(councilId: string, round: number, memberName: string): string {
616
+ const m = normalizeAgentName(memberName);
617
+ return [
618
+ "<!-- council-route",
619
+ `council-id: ${councilId}`,
620
+ `council-round: ${round}`,
621
+ `member: ${m}`,
622
+ "-->",
623
+ "",
624
+ ].join("\n");
625
+ }
626
+
627
+ /** Strip a leading route header from a prompt body, if present. */
628
+ export function stripRouteHeader(body: string): string {
629
+ return body.replace(/^<!--\s*council-route[\s\S]*?-->\n?/, "");
630
+ }
631
+
632
+ /** Sentinel marking the start of the auto-appended submit footer. */
633
+ const SUBMIT_FOOTER_MARKER = "<!-- council-submit-footer -->";
634
+
635
+ /**
636
+ * Build the submit footer appended to every steward-drafted prompt. This is the
637
+ * load-bearing instruction that a contribution composed in chat is NOT recorded;
638
+ * the agent must run the command below. It rides on the prompt (the one
639
+ * artifact the operator always pastes) so it reaches every harness regardless of
640
+ * whether the convene-time invitation was delivered or a `/council` skill is
641
+ * available. Without it, agents (esp. non-Claude harnesses with no skill) write
642
+ * their take as a reply and end the turn, leaving the council showing them as
643
+ * still-pending. Visible markdown (not an HTML comment) so the agent reads it.
644
+ */
645
+ export function buildSubmitFooter(councilId: string): string {
646
+ const bin = resolveBinName();
647
+ return [
648
+ SUBMIT_FOOTER_MARKER,
649
+ "---",
650
+ "**⚠ To record your contribution you MUST run the command below; a reply in chat is NOT counted:**",
651
+ "",
652
+ "```bash",
653
+ `${bin} agents council contribute ${councilId} --message "<your take, end with the status tag>"`,
654
+ `# longer write-up? ${bin} agents council contribute ${councilId} --file <path>`,
655
+ "```",
656
+ "",
657
+ '_(Or invoke the `council` skill in your harness: `/council contribute` in Claude Code, `$council` / "use the council skill" in Codex/Cursor, for the same flow with routing guards.)_',
658
+ ].join("\n");
659
+ }
660
+
661
+ /** Strip an appended submit footer from a prompt body, if present. */
662
+ export function stripSubmitFooter(body: string): string {
663
+ return body.replace(new RegExp(`\\n*${SUBMIT_FOOTER_MARKER}[\\s\\S]*$`), "");
664
+ }
665
+
666
+ /** Parse a route header from a string (the inbound user message). Returns
667
+ * null when the comment is absent or malformed. Used by the /council
668
+ * contribute skill to detect operator misrouting before contributing. */
669
+ export function parseRouteHeader(text: string): {
670
+ council_id: string;
671
+ council_round: number;
672
+ member: string;
673
+ } | null {
674
+ const m = text.match(/<!--\s*council-route\s*([\s\S]*?)-->/);
675
+ if (!m) return null;
676
+ const lines = m[1].split("\n");
677
+ const get = (key: string): string | null => {
678
+ for (const line of lines) {
679
+ const mm = line.match(new RegExp(`^\\s*${key}:\\s*(.+)$`));
680
+ if (mm) return mm[1].trim();
681
+ }
682
+ return null;
683
+ };
684
+ const councilId = get("council-id");
685
+ const roundStr = get("council-round");
686
+ const member = get("member");
687
+ if (!councilId || !roundStr || !member) return null;
688
+ const round = Number.parseInt(roundStr, 10);
689
+ if (!Number.isFinite(round)) return null;
690
+ return { council_id: councilId, council_round: round, member };
691
+ }
692
+
693
+ /** Write a prompt file (atomic). Creates the prompts dir lazily. The body
694
+ * is automatically prepended with a route header (see `buildRouteHeader`) so
695
+ * the contributor skill can verify the operator routed the prompt to the
696
+ * right agent. */
697
+ export function writePrompt(
698
+ councilId: string,
699
+ round: number,
700
+ memberName: string,
701
+ body: string,
702
+ ): string {
703
+ const filePath = promptPath(councilId, round, memberName);
704
+ if (!filePath) throw new Error("not in an agent session; no monorepo root");
705
+ const pd = promptsDir(councilId, round);
706
+ if (pd && !existsSync(pd)) mkdirSync(pd, { recursive: true });
707
+ // Strip any existing route header + submit footer from `body` first so
708
+ // re-writes don't stack them when a steward updates a prompt.
709
+ const header = buildRouteHeader(councilId, round, memberName);
710
+ const footer = buildSubmitFooter(councilId);
711
+ const cleaned = stripSubmitFooter(stripRouteHeader(body)).trimEnd();
712
+ const tmp = `${filePath}.tmp.${process.pid}`;
713
+ writeFileSync(tmp, `${header}${cleaned}\n\n${footer}\n`, "utf8");
714
+ renameSync(tmp, filePath);
715
+ return filePath;
716
+ }
717
+
718
+ /**
719
+ * Read a member's prompt for a given round. Returns null when the file
720
+ * doesn't exist (steward hasn't drafted one yet). Includes a `completed`
721
+ * boolean so the UI can mark the prompt deactivated once the contribution
722
+ * has landed.
723
+ */
724
+ export function readPrompt(
725
+ councilId: string,
726
+ round: number,
727
+ memberName: string,
728
+ ): { body: string; completed: boolean } | null {
729
+ const filePath = promptPath(councilId, round, memberName);
730
+ if (!filePath || !existsSync(filePath)) return null;
731
+ const body = readFileSync(filePath, "utf8");
732
+ const contributors = contributorsInRound(councilId, round);
733
+ const completed = contributors.includes(normalizeAgentName(memberName));
734
+ return { body, completed };
735
+ }
736
+
737
+ /**
738
+ * Visual/behavioral state of a per-member routing prompt within a round:
739
+ *
740
+ * - `contributed`: the member already submitted; the prompt is preserved for
741
+ * audit but no longer actionable. UIs render it dimmed + struck-through.
742
+ * - `active`: the first not-yet-contributed prompt in `manifest.members`
743
+ * order. This is the one the operator should route next. UIs highlight it.
744
+ * - `queued`: drafted but waiting for an earlier member to contribute first.
745
+ * UIs render it dimmed with the Copy button disabled so the operator can't
746
+ * route it out of order.
747
+ */
748
+ export type CouncilPromptState = "contributed" | "active" | "queued";
749
+
750
+ /**
751
+ * Read every member's prompt for a round, in `manifest.members` order
752
+ * (which is the agreed round-robin sequence; alphabetical is wrong because
753
+ * stewards typically build councils with a deliberate first-to-last order).
754
+ *
755
+ * Each entry carries `order` (1-indexed position within manifest.members,
756
+ * skipping members whose prompts don't exist) + `state` (contributed /
757
+ * active / queued) so the UI can render the three-state pattern without
758
+ * duplicating the active-determination logic.
759
+ */
760
+ export function readRoundPrompts(
761
+ manifest: CouncilManifest,
762
+ round: number,
763
+ ): Array<{
764
+ member: string;
765
+ body: string;
766
+ completed: boolean;
767
+ order: number;
768
+ state: CouncilPromptState;
769
+ }> {
770
+ const contributors = contributorsInRound(manifest.council_id, round);
771
+
772
+ // First pass: collect drafted prompts in manifest order.
773
+ type Row = {
774
+ member: string;
775
+ body: string;
776
+ completed: boolean;
777
+ order: number;
778
+ state: CouncilPromptState;
779
+ };
780
+ const out: Row[] = [];
781
+ for (const member of manifest.members) {
782
+ const filePath = promptPath(manifest.council_id, round, member);
783
+ if (!filePath || !existsSync(filePath)) continue;
784
+ const body = readFileSync(filePath, "utf8");
785
+ const completed = contributors.includes(normalizeAgentName(member));
786
+ out.push({
787
+ member,
788
+ body,
789
+ completed,
790
+ order: out.length + 1,
791
+ state: completed ? "contributed" : "queued", // placeholder; promoted below
792
+ });
793
+ }
794
+
795
+ // Second pass: promote the first not-contributed entry to `active`.
796
+ for (const row of out) {
797
+ if (row.state !== "contributed") {
798
+ row.state = "active";
799
+ break;
800
+ }
801
+ }
802
+ return out;
803
+ }