harnery 0.40.0 → 0.41.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 (441) hide show
  1. package/README.md +1 -1
  2. package/dist/commander.js +1 -0
  3. package/dist/commands/agents.d.ts.map +1 -1
  4. package/dist/commands/agents.js +25 -19
  5. package/dist/commands/browse-ai.d.ts.map +1 -1
  6. package/dist/commands/browse-ai.js +4 -0
  7. package/dist/commands/browse.d.ts.map +1 -1
  8. package/dist/commands/browse.js +141 -10
  9. package/dist/commands/checkpoint.js +8 -12
  10. package/dist/commands/deinit.d.ts +1 -1
  11. package/dist/commands/deinit.d.ts.map +1 -1
  12. package/dist/commands/deinit.js +8 -4
  13. package/dist/commands/docs.d.ts.map +1 -1
  14. package/dist/commands/docs.js +2 -0
  15. package/dist/commands/doctor.js +10 -2
  16. package/dist/commands/fetch.d.ts.map +1 -1
  17. package/dist/commands/fetch.js +10 -0
  18. package/dist/commands/init.d.ts +19 -3
  19. package/dist/commands/init.d.ts.map +1 -1
  20. package/dist/commands/init.js +232 -104
  21. package/dist/commands/instructions.js +5 -4
  22. package/dist/commands/ledger-v3.d.ts.map +1 -1
  23. package/dist/commands/ledger-v3.js +2 -1
  24. package/dist/commands/webview.d.ts +4 -0
  25. package/dist/commands/webview.d.ts.map +1 -0
  26. package/dist/commands/webview.js +94 -0
  27. package/dist/core/adapter.d.ts +50 -1
  28. package/dist/core/adapter.d.ts.map +1 -1
  29. package/dist/core/adapter.js +59 -1
  30. package/dist/core/adapters/attestation.d.ts.map +1 -1
  31. package/dist/core/adapters/attestation.js +3 -2
  32. package/dist/core/adapters/profiles.d.ts +20 -3
  33. package/dist/core/adapters/profiles.d.ts.map +1 -1
  34. package/dist/core/adapters/profiles.js +32 -1
  35. package/dist/core/adapters/registry.d.ts.map +1 -1
  36. package/dist/core/adapters/registry.js +33 -0
  37. package/dist/core/agents/canonical-emit.d.ts +1 -1
  38. package/dist/core/agents/canonical-emit.d.ts.map +1 -1
  39. package/dist/core/agents/canonical-emit.js +2 -7
  40. package/dist/core/agents/cli.js +18 -16
  41. package/dist/core/agents/coord-client.d.ts.map +1 -1
  42. package/dist/core/agents/coord-client.js +7 -2
  43. package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
  44. package/dist/core/agents/rules/claim-conflict.js +3 -9
  45. package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
  46. package/dist/core/agents/rules/commit-conflict.js +2 -8
  47. package/dist/core/agents/rules/stop-hook.d.ts +2 -0
  48. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  49. package/dist/core/agents/rules/stop-hook.js +1 -1
  50. package/dist/core/agents/session-events.d.ts +6 -2
  51. package/dist/core/agents/session-events.d.ts.map +1 -1
  52. package/dist/core/agents/session-events.js +105 -26
  53. package/dist/core/agents/session-finalization-state-v3.d.ts.map +1 -1
  54. package/dist/core/agents/session-finalization-state-v3.js +2 -1
  55. package/dist/core/agents/session-finalizer-v3.d.ts.map +1 -1
  56. package/dist/core/agents/session-finalizer-v3.js +5 -4
  57. package/dist/core/agents/session-name-display.d.ts +28 -0
  58. package/dist/core/agents/session-name-display.d.ts.map +1 -1
  59. package/dist/core/agents/session-name-display.js +43 -0
  60. package/dist/core/agents/state/heartbeat-reader.d.ts +7 -0
  61. package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
  62. package/dist/core/agents/state/heartbeat-writer.d.ts +10 -0
  63. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  64. package/dist/core/agents/state/heartbeat-writer.js +21 -2
  65. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  66. package/dist/core/agents/state/live-coordination-view.js +1 -0
  67. package/dist/core/agents/state/live-coordination-writer.js +3 -0
  68. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  69. package/dist/core/agents/state/stale-sweep.js +2 -10
  70. package/dist/core/agents/turn-changes.d.ts +17 -0
  71. package/dist/core/agents/turn-changes.d.ts.map +1 -0
  72. package/dist/core/agents/turn-changes.js +76 -0
  73. package/dist/core/artifacts/delivery-card.d.ts.map +1 -1
  74. package/dist/core/artifacts/delivery-card.js +2 -1
  75. package/dist/core/artifacts/index.d.ts.map +1 -1
  76. package/dist/core/artifacts/index.js +4 -3
  77. package/dist/core/config.d.ts +5 -0
  78. package/dist/core/config.d.ts.map +1 -1
  79. package/dist/core/config.js +14 -0
  80. package/dist/core/context/index.d.ts.map +1 -1
  81. package/dist/core/context/index.js +2 -1
  82. package/dist/core/conversations/archive.d.ts.map +1 -1
  83. package/dist/core/conversations/archive.js +4 -3
  84. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  85. package/dist/core/diagnostics/bundle.js +3 -2
  86. package/dist/core/events/legacy-storage/canary.d.ts.map +1 -1
  87. package/dist/core/events/legacy-storage/canary.js +5 -4
  88. package/dist/core/events/legacy-storage/compression.d.ts.map +1 -1
  89. package/dist/core/events/legacy-storage/compression.js +2 -1
  90. package/dist/core/events/v3/adapter-id.d.ts +10 -2
  91. package/dist/core/events/v3/adapter-id.d.ts.map +1 -1
  92. package/dist/core/events/v3/adapter-id.js +16 -0
  93. package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
  94. package/dist/core/events/v3/authority-outbox.js +2 -1
  95. package/dist/core/events/v3/bootstrap.d.ts.map +1 -1
  96. package/dist/core/events/v3/bootstrap.js +5 -4
  97. package/dist/core/events/v3/capabilities.d.ts.map +1 -1
  98. package/dist/core/events/v3/capabilities.js +17 -0
  99. package/dist/core/events/v3/contract.d.ts +5 -0
  100. package/dist/core/events/v3/contract.d.ts.map +1 -1
  101. package/dist/core/events/v3/contract.js +7 -1
  102. package/dist/core/events/v3/control-witness.d.ts.map +1 -1
  103. package/dist/core/events/v3/control-witness.js +3 -2
  104. package/dist/core/events/v3/coordination-transaction-recovery.d.ts.map +1 -1
  105. package/dist/core/events/v3/coordination-transaction-recovery.js +4 -3
  106. package/dist/core/events/v3/fingerprint-keys.d.ts.map +1 -1
  107. package/dist/core/events/v3/fingerprint-keys.js +7 -6
  108. package/dist/core/events/v3/generated.d.ts +1 -1
  109. package/dist/core/events/v3/generated.js +1 -1
  110. package/dist/core/events/v3/live-feed.d.ts.map +1 -1
  111. package/dist/core/events/v3/live-feed.js +5 -4
  112. package/dist/core/events/v3/producers/command-recorder.d.ts.map +1 -1
  113. package/dist/core/events/v3/producers/command-recorder.js +8 -7
  114. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  115. package/dist/core/events/v3/producers/coordination-recorder.js +8 -7
  116. package/dist/core/events/v3/producers/diagnostic-summaries.d.ts.map +1 -1
  117. package/dist/core/events/v3/producers/diagnostic-summaries.js +5 -4
  118. package/dist/core/events/v3/producers/hook-base.d.ts +1 -0
  119. package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
  120. package/dist/core/events/v3/producers/hook-base.js +10 -4
  121. package/dist/core/events/v3/producers/hook.d.ts +2 -0
  122. package/dist/core/events/v3/producers/hook.d.ts.map +1 -1
  123. package/dist/core/events/v3/producers/hook.js +16 -1
  124. package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
  125. package/dist/core/events/v3/producers/intake.js +16 -6
  126. package/dist/core/events/v3/producers/recorder.d.ts +2 -4
  127. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  128. package/dist/core/events/v3/producers/recorder.js +109 -101
  129. package/dist/core/events/v3/recovery.d.ts.map +1 -1
  130. package/dist/core/events/v3/recovery.js +4 -3
  131. package/dist/core/events/v3/support-storage/inventory.d.ts.map +1 -1
  132. package/dist/core/events/v3/support-storage/inventory.js +2 -1
  133. package/dist/core/events/v3/support-storage/maintenance.d.ts.map +1 -1
  134. package/dist/core/events/v3/support-storage/maintenance.js +4 -3
  135. package/dist/core/events/v3/support-storage/pack-reader.d.ts.map +1 -1
  136. package/dist/core/events/v3/support-storage/pack-reader.js +4 -3
  137. package/dist/core/events/v3/support-storage/pack-writer.d.ts.map +1 -1
  138. package/dist/core/events/v3/support-storage/pack-writer.js +6 -5
  139. package/dist/core/events/v3/tool-line-changes.d.ts +8 -0
  140. package/dist/core/events/v3/tool-line-changes.d.ts.map +1 -0
  141. package/dist/core/events/v3/tool-line-changes.js +135 -0
  142. package/dist/core/events/v3/writer.d.ts.map +1 -1
  143. package/dist/core/events/v3/writer.js +8 -7
  144. package/dist/core/governor/planning.d.ts.map +1 -1
  145. package/dist/core/governor/planning.js +6 -5
  146. package/dist/core/governor/service.d.ts.map +1 -1
  147. package/dist/core/governor/service.js +12 -11
  148. package/dist/core/governor/state.d.ts.map +1 -1
  149. package/dist/core/governor/state.js +7 -6
  150. package/dist/core/guard/evaluator.js +1 -0
  151. package/dist/core/guard/storage.d.ts.map +1 -1
  152. package/dist/core/guard/storage.js +3 -2
  153. package/dist/core/hooks/adapter/behaviors/claude-code.d.ts +4 -0
  154. package/dist/core/hooks/adapter/behaviors/claude-code.d.ts.map +1 -0
  155. package/dist/core/hooks/adapter/behaviors/claude-code.js +31 -0
  156. package/dist/core/hooks/adapter/behaviors/codex.d.ts +15 -0
  157. package/dist/core/hooks/adapter/behaviors/codex.d.ts.map +1 -0
  158. package/dist/core/hooks/adapter/behaviors/codex.js +82 -0
  159. package/dist/core/hooks/adapter/behaviors/cursor.d.ts +7 -0
  160. package/dist/core/hooks/adapter/behaviors/cursor.d.ts.map +1 -0
  161. package/dist/core/hooks/adapter/behaviors/cursor.js +35 -0
  162. package/dist/core/hooks/adapter/behaviors/index.d.ts +11 -0
  163. package/dist/core/hooks/adapter/behaviors/index.d.ts.map +1 -0
  164. package/dist/core/hooks/adapter/behaviors/index.js +20 -0
  165. package/dist/core/hooks/adapter/behaviors/openclaw.d.ts +7 -0
  166. package/dist/core/hooks/adapter/behaviors/openclaw.d.ts.map +1 -0
  167. package/dist/core/hooks/adapter/behaviors/openclaw.js +34 -0
  168. package/dist/core/hooks/adapter/behaviors/opencode.d.ts +7 -0
  169. package/dist/core/hooks/adapter/behaviors/opencode.d.ts.map +1 -0
  170. package/dist/core/hooks/adapter/behaviors/opencode.js +34 -0
  171. package/dist/core/hooks/adapter/behaviors/types.d.ts +89 -0
  172. package/dist/core/hooks/adapter/behaviors/types.d.ts.map +1 -0
  173. package/dist/core/hooks/adapter/behaviors/types.js +1 -0
  174. package/dist/core/hooks/adapter/detect.d.ts +1 -1
  175. package/dist/core/hooks/adapter/detect.d.ts.map +1 -1
  176. package/dist/core/hooks/adapter/detect.js +2 -3
  177. package/dist/core/hooks/adapter/events.d.ts +29 -1
  178. package/dist/core/hooks/adapter/events.d.ts.map +1 -1
  179. package/dist/core/hooks/adapter/events.js +31 -0
  180. package/dist/core/hooks/adapter/opencode-plugin.d.ts +60 -0
  181. package/dist/core/hooks/adapter/opencode-plugin.d.ts.map +1 -0
  182. package/dist/core/hooks/adapter/opencode-plugin.js +129 -0
  183. package/dist/core/hooks/adapter/output.d.ts +7 -0
  184. package/dist/core/hooks/adapter/output.d.ts.map +1 -1
  185. package/dist/core/hooks/adapter/output.js +8 -1
  186. package/dist/core/hooks/adapter/wiring.d.ts +15 -1
  187. package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
  188. package/dist/core/hooks/adapter/wiring.js +74 -11
  189. package/dist/core/hooks/cli.js +635 -529
  190. package/dist/core/hooks/effects/image-capture.d.ts.map +1 -1
  191. package/dist/core/hooks/effects/image-capture.js +2 -1
  192. package/dist/core/hooks/prompt-context/contract.d.ts.map +1 -1
  193. package/dist/core/hooks/prompt-context/contract.js +1 -0
  194. package/dist/core/hooks/resolve/transcript.d.ts +1 -1
  195. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  196. package/dist/core/hooks/resolve/transcript.js +35 -4
  197. package/dist/core/hooks/session-name-presence.d.ts +3 -1
  198. package/dist/core/hooks/session-name-presence.d.ts.map +1 -1
  199. package/dist/core/hooks/session-name-presence.js +7 -2
  200. package/dist/core/hooks/shell-waiter.d.ts +2 -0
  201. package/dist/core/hooks/shell-waiter.d.ts.map +1 -0
  202. package/dist/core/hooks/shell-waiter.js +173 -0
  203. package/dist/core/resources/service.d.ts.map +1 -1
  204. package/dist/core/resources/service.js +5 -4
  205. package/dist/core/semantic/review.d.ts.map +1 -1
  206. package/dist/core/semantic/review.js +5 -4
  207. package/dist/core/semantic/service.d.ts.map +1 -1
  208. package/dist/core/semantic/service.js +15 -14
  209. package/dist/core/semantic/storage.d.ts.map +1 -1
  210. package/dist/core/semantic/storage.js +5 -4
  211. package/dist/core/storage/atomic-json.d.ts.map +1 -1
  212. package/dist/core/storage/atomic-json.js +7 -3
  213. package/dist/core/storage/durable-history.d.ts.map +1 -1
  214. package/dist/core/storage/durable-history.js +6 -5
  215. package/dist/core/storage/maintenance-providers.d.ts.map +1 -1
  216. package/dist/core/storage/maintenance-providers.js +2 -1
  217. package/dist/core/storage/maintenance.d.ts.map +1 -1
  218. package/dist/core/storage/maintenance.js +7 -6
  219. package/dist/core/storage/metrics.d.ts.map +1 -1
  220. package/dist/core/storage/metrics.js +5 -1
  221. package/dist/core/storage/modes.d.ts +17 -0
  222. package/dist/core/storage/modes.d.ts.map +1 -0
  223. package/dist/core/storage/modes.js +47 -0
  224. package/dist/core/storage/process-log.d.ts.map +1 -1
  225. package/dist/core/storage/process-log.js +3 -2
  226. package/dist/core/storage/receipts.d.ts.map +1 -1
  227. package/dist/core/storage/receipts.js +4 -3
  228. package/dist/core/storage/segments.d.ts.map +1 -1
  229. package/dist/core/storage/segments.js +7 -6
  230. package/dist/core/supervisor/hook-health.d.ts +2 -1
  231. package/dist/core/supervisor/hook-health.d.ts.map +1 -1
  232. package/dist/core/supervisor/hook-health.js +4 -3
  233. package/dist/core/supervisor/service.d.ts.map +1 -1
  234. package/dist/core/supervisor/service.js +7 -6
  235. package/dist/core/work/state.d.ts.map +1 -1
  236. package/dist/core/work/state.js +8 -7
  237. package/dist/core/workflow/adapters.d.ts +7 -0
  238. package/dist/core/workflow/adapters.d.ts.map +1 -1
  239. package/dist/core/workflow/adapters.js +9 -0
  240. package/dist/core/workflow/approvals.d.ts.map +1 -1
  241. package/dist/core/workflow/approvals.js +6 -5
  242. package/dist/core/workflow/billing.d.ts.map +1 -1
  243. package/dist/core/workflow/billing.js +33 -4
  244. package/dist/core/workflow/durable-record.d.ts.map +1 -1
  245. package/dist/core/workflow/durable-record.js +4 -3
  246. package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
  247. package/dist/core/workflow/live-session-v3.js +2 -3
  248. package/dist/core/workflow/run-state.d.ts.map +1 -1
  249. package/dist/core/workflow/run-state.js +3 -2
  250. package/dist/core/workflow/spawn-opencode.d.ts +36 -0
  251. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -0
  252. package/dist/core/workflow/spawn-opencode.js +132 -0
  253. package/dist/core/workflow/transcript.d.ts.map +1 -1
  254. package/dist/core/workflow/transcript.js +3 -2
  255. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  256. package/dist/core/workflow/workspaces/leases.js +5 -4
  257. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  258. package/dist/core/workflow/workspaces/local-git.js +3 -2
  259. package/dist/core/workflow/workspaces/paths.d.ts.map +1 -1
  260. package/dist/core/workflow/workspaces/paths.js +2 -1
  261. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  262. package/dist/core/workflow/workspaces/state.js +10 -9
  263. package/dist/lazy-commands.js +8 -1
  264. package/dist/lib/agent-browser/client.d.ts +9 -0
  265. package/dist/lib/agent-browser/client.d.ts.map +1 -1
  266. package/dist/lib/agent-browser/client.js +13 -0
  267. package/dist/lib/browser/client.d.ts +35 -1
  268. package/dist/lib/browser/client.d.ts.map +1 -1
  269. package/dist/lib/browser/client.js +117 -8
  270. package/dist/lib/browser/identity.d.ts +25 -0
  271. package/dist/lib/browser/identity.d.ts.map +1 -0
  272. package/dist/lib/browser/identity.js +66 -0
  273. package/dist/lib/browser/index.d.ts +2 -1
  274. package/dist/lib/browser/index.d.ts.map +1 -1
  275. package/dist/lib/browser/index.js +2 -1
  276. package/dist/lib/browser/launch-args.d.ts +26 -0
  277. package/dist/lib/browser/launch-args.d.ts.map +1 -1
  278. package/dist/lib/browser/launch-args.js +41 -1
  279. package/dist/lib/browser/user-agent.d.ts +53 -0
  280. package/dist/lib/browser/user-agent.d.ts.map +1 -0
  281. package/dist/lib/browser/user-agent.js +79 -0
  282. package/dist/lib/docs-metadata-managed.d.ts +1 -0
  283. package/dist/lib/docs-metadata-managed.d.ts.map +1 -1
  284. package/dist/lib/docs-metadata-managed.js +17 -1
  285. package/dist/lib/identities/assume.d.ts.map +1 -1
  286. package/dist/lib/identities/assume.js +4 -9
  287. package/dist/lib/instructions/apply.d.ts.map +1 -1
  288. package/dist/lib/instructions/apply.js +2 -0
  289. package/dist/lib/pace/index.d.ts +2 -0
  290. package/dist/lib/pace/index.d.ts.map +1 -0
  291. package/dist/lib/pace/index.js +1 -0
  292. package/dist/lib/pace/pace.d.ts +95 -0
  293. package/dist/lib/pace/pace.d.ts.map +1 -0
  294. package/dist/lib/pace/pace.js +313 -0
  295. package/dist/lib/tunnel/forward-headers.d.ts +11 -0
  296. package/dist/lib/tunnel/forward-headers.d.ts.map +1 -0
  297. package/dist/lib/tunnel/forward-headers.js +17 -0
  298. package/dist/lib/tunnel/gate.js +15 -10
  299. package/dist/lib/webview/index.d.ts +2 -0
  300. package/dist/lib/webview/index.d.ts.map +1 -0
  301. package/dist/lib/webview/index.js +1 -0
  302. package/dist/lib/webview/webview.d.ts +116 -0
  303. package/dist/lib/webview/webview.d.ts.map +1 -0
  304. package/dist/lib/webview/webview.js +289 -0
  305. package/opencode-plugin/package.json +19 -0
  306. package/opencode-plugin/src/index.ts +791 -0
  307. package/package.json +11 -4
  308. package/schemas/config.schema.json +13 -0
  309. package/schemas/event-v3.schema.json +164 -0
  310. package/src/commander.ts +7 -0
  311. package/src/commands/agents.ts +33 -18
  312. package/src/commands/browse-ai.ts +8 -0
  313. package/src/commands/browse.ts +189 -10
  314. package/src/commands/checkpoint.ts +7 -9
  315. package/src/commands/deinit.ts +9 -4
  316. package/src/commands/docs.ts +2 -0
  317. package/src/commands/doctor.ts +11 -2
  318. package/src/commands/fetch.ts +14 -0
  319. package/src/commands/init.ts +281 -119
  320. package/src/commands/instructions.ts +8 -4
  321. package/src/commands/ledger-v3.ts +2 -1
  322. package/src/commands/webview.ts +159 -0
  323. package/src/core/adapter.ts +102 -1
  324. package/src/core/adapters/attestation.ts +3 -2
  325. package/src/core/adapters/profiles.ts +42 -1
  326. package/src/core/adapters/registry.ts +37 -0
  327. package/src/core/agents/canonical-emit.ts +2 -5
  328. package/src/core/agents/cli.ts +18 -17
  329. package/src/core/agents/coord-client.ts +9 -3
  330. package/src/core/agents/rules/claim-conflict.ts +3 -8
  331. package/src/core/agents/rules/commit-conflict.ts +2 -7
  332. package/src/core/agents/rules/stop-hook.ts +3 -1
  333. package/src/core/agents/session-events.ts +120 -26
  334. package/src/core/agents/session-finalization-state-v3.ts +2 -1
  335. package/src/core/agents/session-finalizer-v3.ts +5 -4
  336. package/src/core/agents/session-name-display.ts +48 -0
  337. package/src/core/agents/state/heartbeat-reader.ts +7 -0
  338. package/src/core/agents/state/heartbeat-writer.ts +24 -2
  339. package/src/core/agents/state/live-coordination-view.ts +1 -0
  340. package/src/core/agents/state/live-coordination-writer.ts +3 -0
  341. package/src/core/agents/state/stale-sweep.ts +2 -9
  342. package/src/core/agents/turn-changes.ts +97 -0
  343. package/src/core/artifacts/delivery-card.ts +2 -1
  344. package/src/core/artifacts/index.ts +4 -3
  345. package/src/core/config.ts +23 -0
  346. package/src/core/context/index.ts +2 -1
  347. package/src/core/conversations/archive.ts +4 -3
  348. package/src/core/diagnostics/bundle.ts +3 -2
  349. package/src/core/events/legacy-storage/canary.ts +5 -4
  350. package/src/core/events/legacy-storage/compression.ts +2 -1
  351. package/src/core/events/v3/adapter-id.ts +29 -1
  352. package/src/core/events/v3/authority-outbox.ts +2 -1
  353. package/src/core/events/v3/bootstrap.ts +5 -4
  354. package/src/core/events/v3/capabilities.ts +17 -0
  355. package/src/core/events/v3/contract.ts +9 -1
  356. package/src/core/events/v3/control-witness.ts +3 -2
  357. package/src/core/events/v3/coordination-transaction-recovery.ts +4 -3
  358. package/src/core/events/v3/fingerprint-keys.ts +7 -6
  359. package/src/core/events/v3/generated.ts +1 -1
  360. package/src/core/events/v3/live-feed.ts +5 -4
  361. package/src/core/events/v3/producers/command-recorder.ts +8 -6
  362. package/src/core/events/v3/producers/coordination-recorder.ts +8 -7
  363. package/src/core/events/v3/producers/diagnostic-summaries.ts +5 -4
  364. package/src/core/events/v3/producers/hook-base.ts +11 -4
  365. package/src/core/events/v3/producers/hook.ts +19 -1
  366. package/src/core/events/v3/producers/intake.ts +18 -6
  367. package/src/core/events/v3/producers/recorder.ts +123 -114
  368. package/src/core/events/v3/recovery.ts +4 -3
  369. package/src/core/events/v3/support-storage/inventory.ts +2 -1
  370. package/src/core/events/v3/support-storage/maintenance.ts +8 -3
  371. package/src/core/events/v3/support-storage/pack-reader.ts +4 -3
  372. package/src/core/events/v3/support-storage/pack-writer.ts +6 -5
  373. package/src/core/events/v3/tool-line-changes.ts +139 -0
  374. package/src/core/events/v3/writer.ts +8 -7
  375. package/src/core/governor/planning.ts +6 -5
  376. package/src/core/governor/service.ts +12 -11
  377. package/src/core/governor/state.ts +7 -6
  378. package/src/core/guard/evaluator.ts +1 -0
  379. package/src/core/guard/storage.ts +3 -2
  380. package/src/core/hooks/adapter/behaviors/claude-code.ts +33 -0
  381. package/src/core/hooks/adapter/behaviors/codex.ts +96 -0
  382. package/src/core/hooks/adapter/behaviors/cursor.ts +37 -0
  383. package/src/core/hooks/adapter/behaviors/index.ts +31 -0
  384. package/src/core/hooks/adapter/behaviors/openclaw.ts +36 -0
  385. package/src/core/hooks/adapter/behaviors/opencode.ts +36 -0
  386. package/src/core/hooks/adapter/behaviors/types.ts +110 -0
  387. package/src/core/hooks/adapter/detect.ts +2 -3
  388. package/src/core/hooks/adapter/events.ts +53 -1
  389. package/src/core/hooks/adapter/opencode-plugin.ts +177 -0
  390. package/src/core/hooks/adapter/output.ts +8 -1
  391. package/src/core/hooks/adapter/wiring.ts +85 -11
  392. package/src/core/hooks/cli.ts +733 -537
  393. package/src/core/hooks/effects/image-capture.ts +2 -1
  394. package/src/core/hooks/prompt-context/contract.ts +1 -0
  395. package/src/core/hooks/resolve/transcript.ts +42 -5
  396. package/src/core/hooks/session-name-presence.ts +10 -3
  397. package/src/core/hooks/shell-waiter.ts +166 -0
  398. package/src/core/resources/service.ts +5 -4
  399. package/src/core/semantic/review.ts +5 -4
  400. package/src/core/semantic/service.ts +15 -14
  401. package/src/core/semantic/storage.ts +5 -4
  402. package/src/core/storage/atomic-json.ts +7 -3
  403. package/src/core/storage/durable-history.ts +6 -5
  404. package/src/core/storage/maintenance-providers.ts +2 -1
  405. package/src/core/storage/maintenance.ts +7 -6
  406. package/src/core/storage/metrics.ts +5 -1
  407. package/src/core/storage/modes.ts +53 -0
  408. package/src/core/storage/process-log.ts +3 -2
  409. package/src/core/storage/receipts.ts +4 -3
  410. package/src/core/storage/segments.ts +7 -6
  411. package/src/core/supervisor/hook-health.ts +4 -4
  412. package/src/core/supervisor/service.ts +7 -6
  413. package/src/core/work/state.ts +8 -7
  414. package/src/core/workflow/adapters.ts +15 -0
  415. package/src/core/workflow/approvals.ts +6 -5
  416. package/src/core/workflow/billing.ts +34 -4
  417. package/src/core/workflow/durable-record.ts +4 -3
  418. package/src/core/workflow/live-session-v3.ts +2 -3
  419. package/src/core/workflow/run-state.ts +3 -2
  420. package/src/core/workflow/spawn-opencode.ts +150 -0
  421. package/src/core/workflow/transcript.ts +3 -2
  422. package/src/core/workflow/workspaces/leases.ts +5 -4
  423. package/src/core/workflow/workspaces/local-git.ts +3 -2
  424. package/src/core/workflow/workspaces/paths.ts +2 -1
  425. package/src/core/workflow/workspaces/state.ts +10 -9
  426. package/src/lazy-commands.ts +11 -1
  427. package/src/lib/agent-browser/client.ts +18 -0
  428. package/src/lib/browser/client.ts +147 -9
  429. package/src/lib/browser/identity.ts +64 -0
  430. package/src/lib/browser/index.ts +15 -1
  431. package/src/lib/browser/launch-args.ts +53 -1
  432. package/src/lib/browser/user-agent.ts +130 -0
  433. package/src/lib/docs-metadata-managed.ts +21 -0
  434. package/src/lib/identities/assume.ts +7 -7
  435. package/src/lib/instructions/apply.ts +2 -0
  436. package/src/lib/pace/index.ts +14 -0
  437. package/src/lib/pace/pace.ts +370 -0
  438. package/src/lib/tunnel/forward-headers.ts +16 -0
  439. package/src/lib/tunnel/gate.ts +18 -10
  440. package/src/lib/webview/index.ts +13 -0
  441. package/src/lib/webview/webview.ts +457 -0
@@ -4,7 +4,7 @@
4
4
  * `writeSessionEvent` records command spans in the canonical V3 ledger.
5
5
  */
6
6
 
7
- import { randomBytes } from "node:crypto";
7
+ import { createHash, type Hash, randomBytes } from "node:crypto";
8
8
  import { existsSync, readFileSync } from "node:fs";
9
9
  import { resolve } from "node:path";
10
10
  // Kept dependency-light: vendored verbatim into a downstream consumer, so no coordEnv import.
@@ -64,18 +64,52 @@ export function readLastIntent(instanceId?: string): string | null {
64
64
  }
65
65
  }
66
66
 
67
- const outputSequence = new Map<string, number>();
67
+ type OutputStream = "stdout" | "stderr" | "combined";
68
+
69
+ interface OutputSummary {
70
+ stream: OutputStream;
71
+ bytes: number;
72
+ lines: number;
73
+ chunks: number;
74
+ hash: Hash;
75
+ }
76
+
77
+ interface PendingCommand {
78
+ coordRoot: string;
79
+ fields: Record<string, unknown>;
80
+ streams: Map<OutputStream, OutputSummary>;
81
+ }
82
+
83
+ const pendingCommands = new Map<string, PendingCommand>();
84
+ let exitFlushInstalled = false;
85
+
86
+ function commandKey(fields: Record<string, unknown>): string | undefined {
87
+ return typeof fields.instance_id === "string" && typeof fields.cmd_id === "string"
88
+ ? JSON.stringify([fields.instance_id, fields.cmd_id])
89
+ : undefined;
90
+ }
91
+
92
+ function flushCommandOutput(command: PendingCommand): void {
93
+ for (const summary of command.streams.values()) {
94
+ recordCommandObservation("command.output_observed", command.fields, command.coordRoot, summary);
95
+ }
96
+ command.streams.clear();
97
+ }
68
98
 
69
99
  /** Emit a command/narration event to the canonical stream. Swallows every
70
100
  * error and skips when identity can't be resolved: telemetry must never break
71
101
  * or slow down a command. */
72
- function recordCommandObservation(type: SessionEventType, fields: Record<string, unknown>): void {
102
+ function recordCommandObservation(
103
+ type: SessionEventType,
104
+ fields: Record<string, unknown>,
105
+ coordRoot: string,
106
+ summary?: OutputSummary,
107
+ ): boolean {
73
108
  const instanceId = typeof fields.instance_id === "string" ? fields.instance_id : undefined;
74
- if (!instanceId) return;
109
+ if (!instanceId) return false;
75
110
  try {
76
- const coordRoot = coordinationRootPath();
77
111
  const route = resolveLiveEventLedgerRouteV3(coordRoot);
78
- if (route.state === "blocked") return;
112
+ if (route.state === "blocked") return false;
79
113
  const liveInstanceId = liveInstanceIdV3(instanceId);
80
114
  const hook = readHookProducerStateByInstanceV3(coordRoot, liveInstanceId);
81
115
  if (!hook) {
@@ -84,10 +118,10 @@ function recordCommandObservation(type: SessionEventType, fields: Record<string,
84
118
  instance_id: instanceId,
85
119
  reason: "hook_generation_not_found",
86
120
  });
87
- return;
121
+ return false;
88
122
  }
89
- const command = commandSignalAndObservation(type, fields);
90
- if (!command) return;
123
+ const command = commandSignalAndObservation(type, fields, summary);
124
+ if (!command) return false;
91
125
  const result = recordCommandSignalV3({
92
126
  coordRoot,
93
127
  mode: route.mode,
@@ -105,7 +139,7 @@ function recordCommandObservation(type: SessionEventType, fields: Record<string,
105
139
  const expectedLifecycleReopenGap =
106
140
  result.reason === "turn_not_started" &&
107
141
  hook.session_start_derivation === "approved_lifecycle_reopen";
108
- if (expectedLifecycleReopenGap) return;
142
+ if (expectedLifecycleReopenGap) return false;
109
143
  writeProducerDiagnosticV3(coordRoot, "command_emit_unjoinable", {
110
144
  type,
111
145
  instance_id: instanceId,
@@ -120,10 +154,10 @@ function recordCommandObservation(type: SessionEventType, fields: Record<string,
120
154
  result_state: result.state,
121
155
  });
122
156
  }
157
+ return result.state === "recorded" || result.state === "already_recorded";
123
158
  } catch (error) {
124
159
  // Telemetry must never break the command, but the loss is preserved.
125
160
  try {
126
- const coordRoot = coordinationRootPath();
127
161
  writeProducerDiagnosticV3(coordRoot, "command_emit_failed", {
128
162
  type,
129
163
  instance_id: instanceId,
@@ -132,17 +166,26 @@ function recordCommandObservation(type: SessionEventType, fields: Record<string,
132
166
  } catch {
133
167
  /* diagnostics are best-effort */
134
168
  }
169
+ return false;
135
170
  }
136
171
  }
137
172
 
173
+ /** Count the non-empty lines in one output chunk; a single event may carry many. */
174
+ export function countOutputLines(text: string): number {
175
+ if (!text) return 0;
176
+ let count = 0;
177
+ for (const line of text.split("\n")) if (line.length > 0) count += 1;
178
+ return count;
179
+ }
180
+
138
181
  function commandSignalAndObservation(
139
182
  type: SessionEventType,
140
183
  fields: Record<string, unknown>,
184
+ summary?: OutputSummary,
141
185
  ): { signal: CommandSignalV3; observation: CommandObservationV3 } | undefined {
142
186
  const commandId = typeof fields.cmd_id === "string" ? fields.cmd_id : undefined;
143
187
  if (!commandId) return undefined;
144
188
  if (type === "command.started") {
145
- outputSequence.set(commandId, 0);
146
189
  const command = typeof fields.cmd === "string" ? fields.cmd : "";
147
190
  const executable = command.trim().split(/\s+/, 1)[0] || "unknown";
148
191
  return {
@@ -161,24 +204,25 @@ function commandSignalAndObservation(
161
204
  };
162
205
  }
163
206
  if (type === "command.output_observed") {
164
- const sequence = (outputSequence.get(commandId) ?? 0) + 1;
165
- outputSequence.set(commandId, sequence);
166
- const line = typeof fields.line === "string" ? fields.line : "";
167
- const stream =
168
- fields.stream === "stdout" || fields.stream === "stderr" ? fields.stream : "combined";
207
+ if (!summary) return undefined;
169
208
  return {
170
209
  signal: "command.output_observed",
171
210
  observation: {
172
211
  native_command_id: commandId,
173
- native_observation_id: `${commandId}:output:${sequence}`,
174
- stream,
175
- output: line,
176
- output_bytes: Buffer.byteLength(line, "utf8"),
177
- output_lines: line ? 1 : 0,
212
+ native_observation_id: `${commandId}:output-summary:${summary.stream}`,
213
+ stream: summary.stream,
214
+ // The normalizer HMACs this bounded descriptor with the generation key.
215
+ // Neither raw output nor its unkeyed digest is written to the ledger.
216
+ output: {
217
+ format: "harnery-output-chunks-v1",
218
+ chunks: summary.chunks,
219
+ sha256: summary.hash.digest("hex"),
220
+ },
221
+ output_bytes: summary.bytes,
222
+ output_lines: summary.lines,
178
223
  },
179
224
  };
180
225
  }
181
- outputSequence.delete(commandId);
182
226
  const exitCode =
183
227
  typeof fields.exit === "number" && Number.isSafeInteger(fields.exit) ? fields.exit : undefined;
184
228
  return {
@@ -197,14 +241,64 @@ function commandSignalAndObservation(
197
241
  }
198
242
 
199
243
  /**
200
- * Record a command event in V3. Best-effort, never throws into the caller;
201
- * telemetry must not break or slow a command.
244
+ * Start/completion use the durable V3 recorder. Output only updates bounded
245
+ * per-stream counters and hashes; summaries flush before completion or at
246
+ * process exit. A forced kill can lose unflushed summaries (ADR 0188).
247
+ * Best-effort: telemetry never throws into the caller.
202
248
  */
203
249
  export function writeSessionEvent(
204
250
  type: SessionEventType,
205
251
  fields: Record<string, unknown> = {},
206
252
  ): void {
207
- recordCommandObservation(type, fields);
253
+ try {
254
+ const key = commandKey(fields);
255
+ if (!key) return;
256
+ if (type === "command.output_observed") {
257
+ const command = pendingCommands.get(key);
258
+ if (!command) return;
259
+ const stream =
260
+ fields.stream === "stdout" || fields.stream === "stderr" ? fields.stream : "combined";
261
+ let summary = command.streams.get(stream);
262
+ if (!summary) {
263
+ summary = { stream, bytes: 0, lines: 0, chunks: 0, hash: createHash("sha256") };
264
+ command.streams.set(stream, summary);
265
+ }
266
+ const line = typeof fields.line === "string" ? fields.line : "";
267
+ const normalized = line.normalize("NFC");
268
+ // Length framing preserves chunk boundaries, including empty chunks.
269
+ summary.hash.update(`${Buffer.byteLength(normalized, "utf8")}:`).update(normalized);
270
+ summary.bytes += Buffer.byteLength(line, "utf8");
271
+ summary.lines += countOutputLines(line);
272
+ summary.chunks += 1;
273
+ return;
274
+ }
275
+ const command = pendingCommands.get(key);
276
+ const coordRoot = command?.coordRoot ?? coordinationRootPath();
277
+ if (type === "command.completed") {
278
+ pendingCommands.delete(key);
279
+ if (command) flushCommandOutput(command);
280
+ recordCommandObservation(type, fields, coordRoot);
281
+ } else if (recordCommandObservation(type, fields, coordRoot) && !command) {
282
+ pendingCommands.set(key, {
283
+ coordRoot,
284
+ fields: {
285
+ instance_id: fields.instance_id,
286
+ cmd_id: fields.cmd_id,
287
+ bridge: fields.bridge,
288
+ },
289
+ streams: new Map(),
290
+ });
291
+ if (!exitFlushInstalled) {
292
+ exitFlushInstalled = true;
293
+ process.once("exit", () => {
294
+ for (const pending of pendingCommands.values()) flushCommandOutput(pending);
295
+ pendingCommands.clear();
296
+ });
297
+ }
298
+ }
299
+ } catch {
300
+ // Root resolution and in-memory bookkeeping must not break the command.
301
+ }
208
302
  }
209
303
 
210
304
  /** Trim long values to keep individual events small. */
@@ -2,6 +2,7 @@ import { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
  import type { ApprovedSessionEndReasonV3 } from "../events/v3/producers/recorder.ts";
4
4
  import { EVENT_V3_LEDGER_RELATIVE_ROOT } from "../events/v3/reader.ts";
5
+ import { stateModeTooOpen } from "../storage/modes.ts";
5
6
 
6
7
  export const SESSION_FINALIZATION_REQUEST_FORMAT_V3 =
7
8
  "harnery-v3-session-finalization-request" as const;
@@ -72,7 +73,7 @@ export function listSessionFinalizationRequestsV3(
72
73
 
73
74
  export function readSessionFinalizationRequestV3(filePath: string): SessionFinalizationRequestV3 {
74
75
  const metadata = lstatSync(filePath);
75
- if (!metadata.isFile() || metadata.isSymbolicLink() || (metadata.mode & 0o077) !== 0) {
76
+ if (!metadata.isFile() || metadata.isSymbolicLink() || stateModeTooOpen(metadata.mode)) {
76
77
  throw new Error("V3 session finalization request is unsafe");
77
78
  }
78
79
  const request = JSON.parse(readFileSync(filePath, "utf8")) as SessionFinalizationRequestV3;
@@ -32,6 +32,7 @@ import {
32
32
  import { readLedgerV3 } from "../events/v3/reader.ts";
33
33
  import { assertEventV3 } from "../events/v3/validate.ts";
34
34
  import { EVENT_V3_LEDGER_RELATIVE_ROOT, writeEventV3 } from "../events/v3/writer.ts";
35
+ import { stateDirMode, stateFileMode } from "../storage/modes.ts";
35
36
  import { fsyncParentDirectory } from "../workflow/durable-record.ts";
36
37
  import { acquireNoClobberLease } from "../workflow/workspaces/leases.ts";
37
38
  import { readCodexArchiveObservationsV3 } from "./codex-archive-v3.ts";
@@ -901,20 +902,20 @@ function writeRequest(
901
902
  create = false,
902
903
  ): void {
903
904
  const directory = sessionFinalizationRequestDirectoryV3(coordRoot);
904
- mkdirSync(directory, { recursive: true, mode: 0o700 });
905
- chmodSync(directory, 0o700);
905
+ mkdirSync(directory, { recursive: true, mode: stateDirMode() });
906
+ chmodSync(directory, stateDirMode());
906
907
  const path = sessionFinalizationRequestPathV3(coordRoot, request.request_id);
907
908
  if (create && existsSync(path)) throw new Error("V3 finalization request already exists");
908
909
  const temporary = `${path}.tmp-${process.pid}-${randomUUID()}`;
909
910
  let fd: number | undefined;
910
911
  try {
911
- fd = openSync(temporary, "wx", 0o600);
912
+ fd = openSync(temporary, "wx", stateFileMode());
912
913
  writeFileSync(fd, `${JSON.stringify(request)}\n`, "utf8");
913
914
  fsyncSync(fd);
914
915
  closeSync(fd);
915
916
  fd = undefined;
916
917
  renameSync(temporary, path);
917
- chmodSync(path, 0o600);
918
+ chmodSync(path, stateFileMode());
918
919
  fsyncParentDirectory(path);
919
920
  } finally {
920
921
  if (fd !== undefined) closeSync(fd);
@@ -9,8 +9,17 @@ export interface SessionNameDisplayState {
9
9
  suggested_session_name?: string;
10
10
  session_name_seen_for?: string;
11
11
  session_name_display_requested_for?: string;
12
+ session_name_display_reminders?: number;
12
13
  }
13
14
 
15
+ /**
16
+ * How many times PostToolUse may re-attach the display instruction after the
17
+ * mint-time one, per requested title. Two covers the observed miss (the model
18
+ * narrates, calls a tool, and forgets) without turning an unreadable
19
+ * transcript into a reminder on every tool for the rest of the session.
20
+ */
21
+ export const SESSION_NAME_DISPLAY_REMINDER_LIMIT = 2;
22
+
14
23
  export const SESSION_NAME_DISPLAY_NOTE =
15
24
  "Next, before any prose or another tool call, send suggested_session_name verbatim by itself in a fenced code block.";
16
25
 
@@ -76,6 +85,45 @@ export function sessionNameDisplayInstruction(name: string): string {
76
85
  ].join("\n");
77
86
  }
78
87
 
88
+ /**
89
+ * Whether an ordinary tool result should carry a bounded reminder.
90
+ *
91
+ * Only after the mint-time instruction has gone out (`requested_for` is set
92
+ * for the pending title), and only while the counter is under the cap. The
93
+ * transcript is deliberately not consulted: PreToolUse already stamped a
94
+ * readable sighting, and an unreadable one is exactly the case the reminder
95
+ * exists for.
96
+ */
97
+ export function sessionNameDisplayReminderDue(
98
+ row: SessionNameDisplayState | null | undefined,
99
+ ): string | null {
100
+ const pending = sessionNameDisplayPending(row);
101
+ if (!pending || row?.session_name_display_requested_for !== pending) return null;
102
+ return (row.session_name_display_reminders ?? 0) < SESSION_NAME_DISPLAY_REMINDER_LIMIT
103
+ ? pending
104
+ : null;
105
+ }
106
+
107
+ /**
108
+ * The follow-up nudge for a title still pending after an ordinary tool.
109
+ *
110
+ * Worded so the model can answer it from its own memory: the desktop transcript
111
+ * sometimes drops a text row that the operator did see, so Harnery cannot tell
112
+ * a skipped block from an unrecorded one. A model that already showed the block
113
+ * is told to do nothing, which is what keeps the reminder from producing
114
+ * duplicate blocks in the chat.
115
+ */
116
+ export function sessionNameDisplayReminder(name: string): string {
117
+ return [
118
+ "Session name still pending.",
119
+ "If you have not already shown it this turn, send this exact fenced block as your next assistant text, before any prose or another tool call:",
120
+ "",
121
+ sessionNameDisplayBlock(name),
122
+ "",
123
+ "If you already showed that block, do nothing; Harnery could not read the transcript row and is asking once more.",
124
+ ].join("\n");
125
+ }
126
+
79
127
  export function sessionNameDisplayRecoveryInstruction(name: string, binName: string): string {
80
128
  return [
81
129
  sessionNameDisplayInstruction(name),
@@ -40,6 +40,13 @@ export interface Heartbeat {
40
40
  * counts so that drift cannot strand the latch.
41
41
  */
42
42
  session_name_display_requested_for?: string;
43
+ /**
44
+ * How many bounded PostToolUse reminders have gone out for
45
+ * `session_name_display_requested_for`. Reset whenever a different title is
46
+ * requested. Caps the nudge so an unreadable transcript never turns into an
47
+ * endless stream of "show the block" context.
48
+ */
49
+ session_name_display_reminders?: number;
43
50
  last_status_at?: string;
44
51
  current_turn_id?: string;
45
52
  parent_instance_id?: string;
@@ -194,12 +194,34 @@ export function stampSessionNameRequested(
194
194
  coordRoot: string,
195
195
  instanceId: string,
196
196
  name: string,
197
+ ): Heartbeat | null {
198
+ if (!name) return null;
199
+ return mutate(coordRoot, instanceId, (hb) => {
200
+ if (hb.session_name_display_requested_for === name) return hb;
201
+ const { session_name_display_reminders: _reset, ...rest } = hb;
202
+ return { ...rest, session_name_display_requested_for: name };
203
+ });
204
+ }
205
+
206
+ /**
207
+ * Count one bounded PostToolUse reminder for the title currently requested.
208
+ *
209
+ * The mint-time instruction is the only thing that used to make the block
210
+ * appear; a model that went straight to another tool was never asked again,
211
+ * because the fail-open gate writes its reason to stderr, which the model
212
+ * cannot read. The counter lets the hook re-attach the instruction a fixed
213
+ * number of times and then stop, whatever the transcript can or cannot prove.
214
+ */
215
+ export function stampSessionNameReminded(
216
+ coordRoot: string,
217
+ instanceId: string,
218
+ name: string,
197
219
  ): Heartbeat | null {
198
220
  if (!name) return null;
199
221
  return mutate(coordRoot, instanceId, (hb) =>
200
222
  hb.session_name_display_requested_for === name
201
- ? hb
202
- : { ...hb, session_name_display_requested_for: name },
223
+ ? { ...hb, session_name_display_reminders: (hb.session_name_display_reminders ?? 0) + 1 }
224
+ : hb,
203
225
  );
204
226
  }
205
227
 
@@ -198,6 +198,7 @@ export function projectHeartbeatV3(
198
198
  session_name_seen_at: cache?.session_name_seen_at,
199
199
  session_name_seen_for: cache?.session_name_seen_for,
200
200
  session_name_display_requested_for: cache?.session_name_display_requested_for,
201
+ session_name_display_reminders: cache?.session_name_display_reminders,
201
202
  workflow_run_id: generation.run_id,
202
203
  workflow_agent_id: generation.workflow_agent_id,
203
204
  parent_instance_id: parent ? nativeInstanceIdV3(parent.instance_id) : undefined,
@@ -112,6 +112,9 @@ function materializeLiveCoordinationHeartbeat(
112
112
  session_name_display_requested_for: current.session_name_display_requested_for,
113
113
  }
114
114
  : {}),
115
+ ...(typeof current.session_name_display_reminders === "number"
116
+ ? { session_name_display_reminders: current.session_name_display_reminders }
117
+ : {}),
115
118
  }
116
119
  : {};
117
120
  const materialized: V3HeartbeatMaterialization = {
@@ -13,18 +13,11 @@
13
13
  import { existsSync, readdirSync, readFileSync, statSync, unlinkSync } from "node:fs";
14
14
  import { join } from "node:path";
15
15
  import { coordFreshnessSeconds } from "../../config.ts";
16
- import type { EventAdapterIdV3 } from "../../events/v3/adapter-id.ts";
16
+ import { type EventAdapterIdV3, eventAdapterIdV3FromPlatform } from "../../events/v3/adapter-id.ts";
17
17
  import { writeProducerDiagnosticV3 } from "../../events/v3/producers/intake.ts";
18
18
  import { recordLiveSweepObservationV3 } from "../live-lifecycle-v3.ts";
19
19
 
20
20
  /** platform → adapter, for the swept-event envelope (mirrors heartbeat-writer's adapterOf). */
21
- function adapterFromPlatform(platform: unknown): EventAdapterIdV3 {
22
- if (platform === "cursor") return "cursor";
23
- if (platform === "codex") return "codex";
24
- if (platform === "openclaw") return "openclaw";
25
- return "claude-code";
26
- }
27
-
28
21
  /**
29
22
  * Record the sweep before deleting disposable cache state.
30
23
  *
@@ -150,7 +143,7 @@ export function staleSweep(coordRoot: string): {
150
143
  }
151
144
 
152
145
  const instanceId = parsed.instance_id ?? idFromFile;
153
- const adapter = adapterFromPlatform(parsed.platform);
146
+ const adapter = eventAdapterIdV3FromPlatform(parsed.platform, { context: "stale-sweep" });
154
147
  const sessionId = parsed.session_id ?? instanceId;
155
148
  const ts = parsed.last_heartbeat
156
149
  ? Math.floor(Date.parse(parsed.last_heartbeat) / 1000)
@@ -0,0 +1,97 @@
1
+ import type { EventV3 } from "../events/v3/contract.ts";
2
+ import { liveInstanceIdV3 } from "../events/v3/live-routing.ts";
3
+ import { readLedgerV3 } from "../events/v3/reader.ts";
4
+ import type { LineChanges } from "../events/v3/tool-line-changes.ts";
5
+
6
+ export interface TurnChanges extends LineChanges {
7
+ measured_tools: number;
8
+ unmeasured_tools: number;
9
+ }
10
+
11
+ /** Sum edit activity, not a net workspace diff. Commits cannot erase it. */
12
+ export function resolveTurnChanges(
13
+ events: readonly EventV3[],
14
+ instanceId: string,
15
+ now = Date.now(),
16
+ ): TurnChanges | null {
17
+ const owner = liveInstanceIdV3(instanceId);
18
+ const own = events.filter(
19
+ (event) => event.scope.instance_id === owner && Date.parse(event.time.observed_at) <= now,
20
+ );
21
+ const starts = own.filter((event) => event.event_type === "turn.started");
22
+ const newest = starts.at(-1);
23
+ if (!newest || !("generation_id" in newest.scope)) return null;
24
+ const generation = newest.scope.generation_id;
25
+ let index = starts.length - 1;
26
+ let restarts = 0;
27
+ while (
28
+ index > 0 &&
29
+ restarts < 8 &&
30
+ starts[index]?.event_type === "turn.started" &&
31
+ starts[index]?.payload.stop_remediation === true
32
+ ) {
33
+ const previous = starts[index - 1]!;
34
+ if (!("generation_id" in previous.scope) || previous.scope.generation_id !== generation) break;
35
+ index--;
36
+ restarts++;
37
+ }
38
+ const selected = starts.slice(index);
39
+ const ids = new Set(
40
+ selected.map((event) => ("turn_id" in event.scope ? event.scope.turn_id : undefined)),
41
+ );
42
+ const from = Date.parse(selected[0]!.time.observed_at);
43
+ const tools = own.filter(
44
+ (event) =>
45
+ "generation_id" in event.scope &&
46
+ event.scope.generation_id === generation &&
47
+ "turn_id" in event.scope &&
48
+ ids.has(event.scope.turn_id) &&
49
+ Date.parse(event.time.observed_at) >= from,
50
+ );
51
+ const result: TurnChanges = { added: 0, removed: 0, measured_tools: 0, unmeasured_tools: 0 };
52
+ const completed = new Set<string>();
53
+ for (const event of tools) {
54
+ if (event.event_type !== "tool.completed") continue;
55
+ const span = event.payload.span.span_id;
56
+ if (completed.has(span)) continue;
57
+ completed.add(span);
58
+ const changes = event.payload.line_changes;
59
+ if (event.payload.outcome === "succeeded" && changes?.state === "observed") {
60
+ result.added += changes.value.added;
61
+ result.removed += changes.value.removed;
62
+ result.measured_tools++;
63
+ } else result.unmeasured_tools++;
64
+ }
65
+ for (const event of tools) {
66
+ const span = (event.links as { span_id?: string }).span_id;
67
+ if (event.event_type === "tool.requested" && span && !completed.has(span)) {
68
+ completed.add(span);
69
+ result.unmeasured_tools++;
70
+ }
71
+ }
72
+ // No delivered tool evidence cannot prove no file changes occurred.
73
+ return result.measured_tools + result.unmeasured_tools > 0 ? result : null;
74
+ }
75
+
76
+ export function formatTurnChanges(changes: TurnChanges | null): string {
77
+ if (!changes || changes.measured_tools === 0) return "unavailable (no measured edits)";
78
+ return `+${changes.added} / -${changes.removed} lines${changes.unmeasured_tools > 0 ? " (partial; unmeasured tools)" : " (recorded edits)"}`;
79
+ }
80
+
81
+ export function turnChangesStatusRow(target: { coordRoot: string; instanceId: string }): {
82
+ value: string;
83
+ changes: TurnChanges | null;
84
+ } {
85
+ let changes: TurnChanges | null = null;
86
+ try {
87
+ const ledger = readLedgerV3(target.coordRoot);
88
+ if (ledger.complete)
89
+ changes = resolveTurnChanges(
90
+ ledger.events.map((item) => item.event),
91
+ target.instanceId,
92
+ );
93
+ } catch {
94
+ /* Status remains available when evidence is missing. */
95
+ }
96
+ return { value: formatTurnChanges(changes), changes };
97
+ }
@@ -14,6 +14,7 @@ import { isAbsolute, relative, resolve, sep } from "node:path";
14
14
  import { encodeLinkSafeComponent } from "../../lib/local-file-url.ts";
15
15
  import { findLiveTunnelForOrigin } from "../../lib/tunnel/state.ts";
16
16
  import { resolveWebPort } from "../config.ts";
17
+ import { stateFileMode } from "../storage/modes.ts";
17
18
  import { ARTIFACT_MANIFEST } from "./constants.ts";
18
19
  import { resolveArtifactRef, showArtifact } from "./index.ts";
19
20
 
@@ -71,7 +72,7 @@ export function writeArtifactDeliveryManifest(
71
72
  try {
72
73
  writeFileSync(tmp, `${JSON.stringify(manifest, null, 2)}\n`, {
73
74
  encoding: "utf8",
74
- mode: 0o600,
75
+ mode: stateFileMode(),
75
76
  flag: "wx",
76
77
  });
77
78
  renameSync(tmp, target);
@@ -33,6 +33,7 @@ import {
33
33
  coordFreshnessSeconds,
34
34
  resolveBinName,
35
35
  } from "../config.ts";
36
+ import { stateFileMode } from "../storage/modes.ts";
36
37
  import {
37
38
  type ArtifactActivity,
38
39
  artifactRootEntries,
@@ -1068,7 +1069,7 @@ function atomicWriteManifest(path: string, manifest: ArtifactManifestV2, now = n
1068
1069
  try {
1069
1070
  writeFileSync(tmp, `${JSON.stringify(manifest, null, 2)}\n`, {
1070
1071
  encoding: "utf8",
1071
- mode: 0o600,
1072
+ mode: stateFileMode(),
1072
1073
  flag: "wx",
1073
1074
  });
1074
1075
  renameSync(tmp, target);
@@ -1143,7 +1144,7 @@ export function migrateArtifacts(
1143
1144
  };
1144
1145
  mkdirSync(dirname(preimagePath), { recursive: true });
1145
1146
  try {
1146
- writeFileSync(preimagePath, preimage, { flag: "wx", mode: 0o600 });
1147
+ writeFileSync(preimagePath, preimage, { flag: "wx", mode: stateFileMode() });
1147
1148
  } catch (error) {
1148
1149
  if (
1149
1150
  (error as NodeJS.ErrnoException).code !== "EEXIST" ||
@@ -1287,7 +1288,7 @@ export function repairArtifactActivity(
1287
1288
  null,
1288
1289
  2,
1289
1290
  )}\n`,
1290
- { flag: "wx", mode: 0o600 },
1291
+ { flag: "wx", mode: stateFileMode() },
1291
1292
  );
1292
1293
  const current = lstatSync(path);
1293
1294
  if (
@@ -152,6 +152,16 @@ interface HarneryConfig {
152
152
  archive_keep_min?: number;
153
153
  archive_auto_clean?: boolean;
154
154
  };
155
+ /**
156
+ * Who can use this project's `.harnery/` state. `private` (the default) keeps every state
157
+ * directory 0700 and every state file 0600, owned by whoever wrote it. `group` makes them 0770 and
158
+ * 0660 so several Unix users who share the project directory's group can coordinate in one
159
+ * project. It never grants access to other users. The project directory must be setgid to that
160
+ * group so new state inherits it. Read via `storageSharing()`.
161
+ */
162
+ storage?: {
163
+ sharing?: "private" | "group";
164
+ };
155
165
  /**
156
166
  * Managed working-artifact defaults. `default_retention_days` is the
157
167
  * create-time TTL when the caller does not pass `artifacts create --days`.
@@ -591,6 +601,19 @@ export function workflowSubscriptionOnly(coordRoot?: string | null): boolean {
591
601
  return readConfig(root).workflow?.subscriptionOnly === true;
592
602
  }
593
603
 
604
+ /**
605
+ * How this project's `.harnery/` state is shared: `private` or `group`. `HARNERY_STORAGE_SHARING`
606
+ * overrides per process; an unrecognized value falls back to `private`, the safer reading.
607
+ */
608
+ export function storageSharing(coordRoot?: string | null): "private" | "group" {
609
+ const env = coordEnv("STORAGE_SHARING");
610
+ if (env === "group" || env === "private") return env;
611
+ const root = coordRoot ?? findCoordRoot();
612
+ if (!root) return "private";
613
+ // Project file only: a user-global "group" would loosen every project that user touches.
614
+ return readProjectConfig(root).storage?.sharing === "group" ? "group" : "private";
615
+ }
616
+
594
617
  /**
595
618
  * Whether cross-machine presence (ADR 0016) is enabled for this repo.
596
619
  * Default ON — the transport itself additionally gates on an origin remote
@@ -11,6 +11,7 @@ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "
11
11
  import { dirname, join } from "node:path";
12
12
  import type { Adapter } from "../adapter.ts";
13
13
  import { readLiveCoordinationRow } from "../agents/state/live-coordination-view.ts";
14
+ import { stateFileMode } from "../storage/modes.ts";
14
15
 
15
16
  export const CONTEXT_SCHEMA_VERSION = 1 as const;
16
17
  export const MAX_CAPSULE_BYTES = 32 * 1024;
@@ -499,7 +500,7 @@ function writeBoundedJson(path: string, value: unknown, maxBytes: number): void
499
500
  }
500
501
  mkdirSync(dirname(path), { recursive: true });
501
502
  const temp = `${path}.tmp.${process.pid}.${randomUUID()}`;
502
- writeFileSync(temp, text, { encoding: "utf8", mode: 0o600 });
503
+ writeFileSync(temp, text, { encoding: "utf8", mode: stateFileMode() });
503
504
  renameSync(temp, path);
504
505
  }
505
506