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
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The permission modes for everything Harnery writes as project state under `.harnery/`.
3
+ *
4
+ * `private` (the default) is owner-only: 0700 directories and 0600 files, exactly as before this
5
+ * module existed. `group` is for a project several Unix users share through one group, such as a
6
+ * workspace that several people's agents work in at once: 2770 (setgid) and 0660, with nothing ever granted
7
+ * to other users. Every state write takes its mode from here, and every integrity check that
8
+ * rejects a loosely permissioned file asks `stateModeTooOpen`, so the two cannot disagree.
9
+ *
10
+ * Personal state outside the project (browser cookies, sessions, pacing ledgers in the user's home)
11
+ * is not project state and stays owner-only regardless.
12
+ */
13
+ import { storageSharing } from "../config.ts";
14
+
15
+ let resolved: "private" | "group" | undefined;
16
+
17
+ /** The sharing mode for this process, resolved once. Tests reset it with `resetStorageSharing`. */
18
+ export function stateSharing(): "private" | "group" {
19
+ if (resolved === undefined) {
20
+ resolved = storageSharing();
21
+ // A umask that strips group bits would quietly turn every group file back into an owner-only
22
+ // one, and the next user's write would fail. Keep the process's other-bits mask as it was.
23
+ if (resolved === "group" && typeof process.umask === "function")
24
+ process.umask(process.umask() & ~0o070);
25
+ }
26
+ return resolved;
27
+ }
28
+
29
+ export function resetStorageSharing(): void {
30
+ resolved = undefined;
31
+ }
32
+
33
+ /**
34
+ * Mode for a directory of project state. Group mode includes setgid: Harnery chmods directories
35
+ * after creating them, and a plain 0770 would clear the bit, so files made inside would take the
36
+ * writer's own group instead of the project's, and other users could not read them.
37
+ */
38
+ export function stateDirMode(): number {
39
+ return stateSharing() === "group" ? 0o2770 : 0o700;
40
+ }
41
+
42
+ /** Mode for a file of project state. */
43
+ export function stateFileMode(): number {
44
+ return stateSharing() === "group" ? 0o660 : 0o600;
45
+ }
46
+
47
+ /**
48
+ * True when a state file or directory is readable or writable by someone it should not be: anyone
49
+ * but the owner in private mode, anyone outside the owner and group in group mode.
50
+ */
51
+ export function stateModeTooOpen(mode: number): boolean {
52
+ return (mode & (stateSharing() === "group" ? 0o007 : 0o077)) !== 0;
53
+ }
@@ -12,6 +12,7 @@ import {
12
12
  import { basename, dirname, join, resolve } from "node:path";
13
13
  import { fileURLToPath } from "node:url";
14
14
  import { createStorageCatalog } from "./catalog.ts";
15
+ import { stateDirMode, stateFileMode } from "./modes.ts";
15
16
  import { familyLogDirectory } from "./segments.ts";
16
17
 
17
18
  export interface RotatingTextSinkOptions {
@@ -79,7 +80,7 @@ export class RotatingTextSink {
79
80
  append(text: string): void {
80
81
  if (this.#closed) throw new Error("text sink is closed");
81
82
  const bytes = Buffer.from(text, "utf8");
82
- mkdirSync(dirname(this.#options.path), { recursive: true, mode: 0o700 });
83
+ mkdirSync(dirname(this.#options.path), { recursive: true, mode: stateDirMode() });
83
84
  let offset = 0;
84
85
  while (offset < bytes.byteLength) {
85
86
  let current = existsSync(this.#options.path) ? statSync(this.#options.path).size : 0;
@@ -88,7 +89,7 @@ export class RotatingTextSink {
88
89
  current = 0;
89
90
  }
90
91
  const length = Math.min(bytes.byteLength - offset, this.#options.max_bytes - current);
91
- const fd = openSync(this.#options.path, "a", 0o600);
92
+ const fd = openSync(this.#options.path, "a", stateFileMode());
92
93
  try {
93
94
  writeSync(fd, bytes.subarray(offset, offset + length));
94
95
  if (this.#options.durable) fdatasyncSync(fd);
@@ -18,6 +18,7 @@ import {
18
18
  HarneryMaintenanceError,
19
19
  type HarneryMaintenanceReceipt,
20
20
  } from "./maintenance.ts";
21
+ import { stateDirMode, stateFileMode } from "./modes.ts";
21
22
 
22
23
  export const HARNERY_RECEIPT_SEGMENT_MANIFEST_SCHEMA =
23
24
  "harnery.storage-maintenance-receipt-segment/v1" as const;
@@ -85,7 +86,7 @@ export function consolidateMutationReceipts(
85
86
  );
86
87
  }
87
88
  const segments = join(root, "segments");
88
- mkdirSync(segments, { recursive: true, mode: 0o700 });
89
+ mkdirSync(segments, { recursive: true, mode: stateDirMode() });
89
90
  const payloadPath = join(segments, `${segmentId}.jsonl`);
90
91
  const manifestPath = join(segments, `${segmentId}.manifest.json`);
91
92
  writeExclusiveDurable(payloadPath, payload);
@@ -144,9 +145,9 @@ function readReceipt(path: string): {
144
145
  }
145
146
 
146
147
  function writeExclusiveDurable(path: string, bytes: string): void {
147
- mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
148
+ mkdirSync(dirname(path), { recursive: true, mode: stateDirMode() });
148
149
  const temporary = `${path}.tmp-${process.pid}-${randomUUID()}`;
149
- const fd = openSync(temporary, "wx", 0o600);
150
+ const fd = openSync(temporary, "wx", stateFileMode());
150
151
  try {
151
152
  writeFileSync(fd, bytes, "utf8");
152
153
  fsyncSync(fd);
@@ -23,6 +23,7 @@ import type { HarneryRegisteredStorageFamily } from "./contract.ts";
23
23
  import { parseLogRecord } from "./jsonl.ts";
24
24
  import type { HarneryLogMetricsDelta } from "./metrics.ts";
25
25
  import { mergeMetricsSidecar } from "./metrics.ts";
26
+ import { stateDirMode, stateFileMode } from "./modes.ts";
26
27
 
27
28
  export interface HarneryLogSegmentV1 {
28
29
  sequence: number;
@@ -165,7 +166,7 @@ export class FileSegmentSink {
165
166
  const fd = openSync(
166
167
  active,
167
168
  fsConstants.O_RDWR | fsConstants.O_APPEND | fsConstants.O_CREAT | noFollowFlag(),
168
- 0o600,
169
+ stateFileMode(),
169
170
  );
170
171
  let bytes = 0;
171
172
  try {
@@ -337,7 +338,7 @@ export async function withFamilyLease<T>(
337
338
  let owner: LeaseOwner | undefined;
338
339
  while (true) {
339
340
  try {
340
- mkdirSync(lease, { mode: 0o700 });
341
+ mkdirSync(lease, { mode: stateDirMode() });
341
342
  const leaseIdentity = captureDirectoryIdentity(lease);
342
343
  owner = {
343
344
  owner_id: randomUUID(),
@@ -397,7 +398,7 @@ function sealActive(active: string, options: ResolvedSegmentOptions, now: Date):
397
398
  const sequence = manifest.next_sequence;
398
399
  const name = `${now.toISOString().slice(0, 10).replaceAll("-", "")}-${String(sequence).padStart(8, "0")}.jsonl.gz`;
399
400
  const target = join(directory, "segments", name);
400
- mkdirSync(dirname(target), { recursive: true, mode: 0o700 });
401
+ mkdirSync(dirname(target), { recursive: true, mode: stateDirMode() });
401
402
  const temporary = `${target}.tmp-${process.pid}-${randomUUID()}`;
402
403
  const compressed = gzipSync(content);
403
404
  writeNewFile(temporary, compressed);
@@ -425,7 +426,7 @@ function sealActive(active: string, options: ResolvedSegmentOptions, now: Date):
425
426
  ],
426
427
  };
427
428
  const sealedManifest = join(directory, "manifests", `${String(sequence).padStart(8, "0")}.json`);
428
- mkdirSync(dirname(sealedManifest), { recursive: true, mode: 0o700 });
429
+ mkdirSync(dirname(sealedManifest), { recursive: true, mode: stateDirMode() });
429
430
  writeNewFile(sealedManifest, Buffer.from(`${JSON.stringify(next)}\n`));
430
431
  atomicJson(join(directory, "manifest.json"), next);
431
432
  truncateRegularFile(active);
@@ -539,7 +540,7 @@ interface DirectoryIdentity {
539
540
  }
540
541
 
541
542
  function ensurePrivateDirectory(path: string): DirectoryIdentity {
542
- mkdirSync(path, { recursive: true, mode: 0o700 });
543
+ mkdirSync(path, { recursive: true, mode: stateDirMode() });
543
544
  return captureDirectoryIdentity(path);
544
545
  }
545
546
 
@@ -632,7 +633,7 @@ function writeNewFile(path: string, content: Buffer): void {
632
633
  const fd = openSync(
633
634
  path,
634
635
  fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | noFollowFlag(),
635
- 0o600,
636
+ stateFileMode(),
636
637
  );
637
638
  try {
638
639
  assertOpenFileIdentity(path, fd, parent);
@@ -1,3 +1,4 @@
1
+ import { type EventAdapterIdV3, normalizeEventAdapterIdV3 } from "../events/v3/adapter-id.ts";
1
2
  import {
2
3
  HOOK_HEALTH_EVENT,
3
4
  HOOK_HEALTH_RECEIPT_VERSION,
@@ -18,7 +19,7 @@ export interface CompletedHookHealth {
18
19
  id: string;
19
20
  observed_at: string;
20
21
  hook_name: string;
21
- adapter: "claude-code" | "codex" | "cursor" | "unknown";
22
+ adapter: EventAdapterIdV3 | "unknown";
22
23
  outcome: HookHealthOutcome;
23
24
  duration_ms: number;
24
25
  rss_start_bytes: number;
@@ -267,9 +268,8 @@ function token(value: unknown): string | undefined {
267
268
  }
268
269
 
269
270
  function adapterValue(value: unknown): CompletedHookHealth["adapter"] | undefined {
270
- return value === "claude-code" || value === "codex" || value === "cursor" || value === "unknown"
271
- ? value
272
- : undefined;
271
+ if (value === "unknown") return value;
272
+ return normalizeEventAdapterIdV3(value) ?? undefined;
273
273
  }
274
274
 
275
275
  function outcomeValue(value: unknown): HookHealthOutcome | undefined {
@@ -28,6 +28,7 @@ import { readResourceServiceStatus } from "../resources/service-status.ts";
28
28
  import { resourcePaths } from "../resources/storage.ts";
29
29
  import { writePrivateJsonAtomic } from "../storage/atomic-json.ts";
30
30
  import { closeProcessLoggers, legacyLogFields, processLogger } from "../storage/logger.ts";
31
+ import { stateDirMode, stateFileMode } from "../storage/modes.ts";
31
32
  import { collectSupervisorActivitySnapshot } from "./activity.ts";
32
33
  import {
33
34
  SUPERVISOR_PRESSURE_SCHEMA_VERSION,
@@ -103,7 +104,7 @@ export async function spawnSupervisor(
103
104
  const intervalMs = normalizeInterval(options.intervalMs);
104
105
  const idleExitMs = normalizeIdleExit(options.idleExitMs);
105
106
  const paths = supervisorPaths(coordRoot);
106
- mkdirSync(paths.root, { recursive: true, mode: 0o700 });
107
+ mkdirSync(paths.root, { recursive: true, mode: stateDirMode() });
107
108
  rmSync(paths.stop, { force: true });
108
109
  const cliPath = fileURLToPath(
109
110
  new URL(import.meta.url.endsWith(".ts") ? "../../cli.ts" : "../../cli.js", import.meta.url),
@@ -170,7 +171,7 @@ export function requestSupervisorStop(coordRootRaw: string): SupervisorStatus {
170
171
  const coordRoot = resolve(coordRootRaw);
171
172
  const paths = supervisorPaths(coordRoot);
172
173
  const status = readSupervisorStatus(coordRoot);
173
- mkdirSync(paths.root, { recursive: true, mode: 0o700 });
174
+ mkdirSync(paths.root, { recursive: true, mode: stateDirMode() });
174
175
  writePrivateJsonAtomic(paths.stop, {
175
176
  requested_at: new Date().toISOString(),
176
177
  requested_by_pid: process.pid,
@@ -352,8 +353,8 @@ export async function runSupervisor(
352
353
  });
353
354
  }
354
355
  pressureHysteresis = assessment.hysteresis;
355
- mkdirSync(paths.timelines, { recursive: true, mode: 0o700 });
356
- mkdirSync(paths.explanations, { recursive: true, mode: 0o700 });
356
+ mkdirSync(paths.timelines, { recursive: true, mode: stateDirMode() });
357
+ mkdirSync(paths.explanations, { recursive: true, mode: stateDirMode() });
357
358
  const projectionUpdates = [
358
359
  ...findings.active.filter((finding) => !priorActiveFindingIds.has(finding.id)),
359
360
  ...findings.transitions.filter(
@@ -495,7 +496,7 @@ function observeLedgerControlState(
495
496
 
496
497
  function acquireLease(coordRoot: string, now: Date): SupervisorLease {
497
498
  const paths = supervisorPaths(coordRoot);
498
- mkdirSync(paths.root, { recursive: true, mode: 0o700 });
499
+ mkdirSync(paths.root, { recursive: true, mode: stateDirMode() });
499
500
  for (let attempt = 0; attempt < 2; attempt++) {
500
501
  const startToken = processStartToken(process.pid);
501
502
  const lease: SupervisorLease = {
@@ -506,7 +507,7 @@ function acquireLease(coordRoot: string, now: Date): SupervisorLease {
506
507
  created_at: now.toISOString(),
507
508
  };
508
509
  try {
509
- const fd = openSync(paths.lease, "wx", 0o600);
510
+ const fd = openSync(paths.lease, "wx", stateFileMode());
510
511
  try {
511
512
  writeFileSync(fd, `${JSON.stringify(lease)}\n`, "utf8");
512
513
  } finally {
@@ -17,6 +17,7 @@ import {
17
17
  appendSegmentedJsonlFile,
18
18
  readSegmentedJsonlFileSync,
19
19
  } from "../storage/durable-history.ts";
20
+ import { stateDirMode, stateFileMode } from "../storage/modes.ts";
20
21
  import { readWorkflowApproval } from "../workflow/approvals.ts";
21
22
  import { stableDigest } from "../workflow/durable-record.ts";
22
23
  import { readWorkflowProof } from "../workflow/proof.ts";
@@ -260,9 +261,9 @@ export function createWorkItem(input: CreateWorkItemInput): WorkRecord {
260
261
  created_at: new Date().toISOString(),
261
262
  };
262
263
  const dir = workDir(coordRoot, id);
263
- mkdirSync(dirname(dir), { recursive: true, mode: 0o700 });
264
- mkdirSync(dir, { mode: 0o700 });
265
- chmodSync(dir, 0o700);
264
+ mkdirSync(dirname(dir), { recursive: true, mode: stateDirMode() });
265
+ mkdirSync(dir, { mode: stateDirMode() });
266
+ chmodSync(dir, stateDirMode());
266
267
  writePrivateJson(join(dir, "intent.json"), intent, MAX_INTENT_BYTES);
267
268
  const release = acquireWorkLease(coordRoot, id);
268
269
  try {
@@ -1250,13 +1251,13 @@ function acquireWorkFileLease(
1250
1251
  };
1251
1252
  const acquire = (): boolean => {
1252
1253
  try {
1253
- const fd = openSync(path, "wx", 0o600);
1254
+ const fd = openSync(path, "wx", stateFileMode());
1254
1255
  try {
1255
1256
  writeFileSync(fd, `${JSON.stringify(owner)}\n`, "utf8");
1256
1257
  } finally {
1257
1258
  closeSync(fd);
1258
1259
  }
1259
- chmodSync(path, 0o600);
1260
+ chmodSync(path, stateFileMode());
1260
1261
  return true;
1261
1262
  } catch (error) {
1262
1263
  if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
@@ -1337,8 +1338,8 @@ function writePrivateJson(path: string, value: unknown, maxBytes: number): void
1337
1338
  const body = `${JSON.stringify(value, null, 2)}\n`;
1338
1339
  if (Buffer.byteLength(body) > maxBytes)
1339
1340
  throw new Error(`private record exceeds ${maxBytes} bytes`);
1340
- writeFileSync(path, body, { encoding: "utf8", flag: "wx", mode: 0o600 });
1341
- chmodSync(path, 0o600);
1341
+ writeFileSync(path, body, { encoding: "utf8", flag: "wx", mode: stateFileMode() });
1342
+ chmodSync(path, stateFileMode());
1342
1343
  }
1343
1344
 
1344
1345
  function readBoundedLines(
@@ -7,6 +7,8 @@
7
7
  * but they do drift — keep this module the single place they live.
8
8
  */
9
9
 
10
+ import type { Adapter } from "../adapter.ts";
11
+ import { probeBinaryVersion } from "../adapters/bench.ts";
10
12
  import { BUILTIN_ADAPTER_PROFILES } from "../adapters/profiles.ts";
11
13
  import type { AdapterName } from "./types.ts";
12
14
 
@@ -24,6 +26,19 @@ export const ADAPTER_LOGIN_HINTS: Record<string, string> = Object.fromEntries(
24
26
  Object.values(BUILTIN_ADAPTER_PROFILES).map((profile) => [profile.id, profile.loginHint]),
25
27
  );
26
28
 
29
+ /**
30
+ * Adapter ids whose CLI answers a version probe on this machine's PATH, in
31
+ * `BUILTIN_ADAPTER_PROFILES` order. The probe is injectable so callers can keep
32
+ * tests deterministic instead of keying on the host's toolchain.
33
+ */
34
+ export function detectInstalledAdapters(
35
+ probe: (binary: string) => unknown = probeBinaryVersion,
36
+ ): Adapter[] {
37
+ return (Object.keys(ADAPTER_BINARIES) as Adapter[]).filter(
38
+ (id) => probe(ADAPTER_BINARIES[id]) !== null,
39
+ );
40
+ }
41
+
27
42
  /** One-line "it's missing, here's the fix" string for spawn adapters. */
28
43
  export function notFoundError(adapter: AdapterName): string {
29
44
  const binary = ADAPTER_BINARIES[adapter] ?? adapter;
@@ -12,6 +12,7 @@ import {
12
12
  } from "node:fs";
13
13
  import { dirname, join } from "node:path";
14
14
  import type { PolicyEvaluation, PolicyRequestSummary, PolicyVerdict } from "../policy/index.ts";
15
+ import { stateDirMode, stateFileMode } from "../storage/modes.ts";
15
16
  import { appendWorkflowTranscriptEvent } from "./transcript.ts";
16
17
 
17
18
  export const WORKFLOW_APPROVAL_SCHEMA_VERSION = 1 as const;
@@ -108,8 +109,8 @@ export function createWorkflowApproval(input: CreateWorkflowApprovalInput): {
108
109
  request_sha256: requestSha256,
109
110
  };
110
111
  const dir = approvalDir(input.coordRoot, id);
111
- mkdirSync(dir, { recursive: true, mode: 0o700 });
112
- chmodSync(dir, 0o700);
112
+ mkdirSync(dir, { recursive: true, mode: stateDirMode() });
113
+ chmodSync(dir, stateDirMode());
113
114
  const created = writeExclusiveJson(join(dir, "request.json"), record);
114
115
  const approval = readWorkflowApproval(input.coordRoot, id);
115
116
  if (
@@ -249,17 +250,17 @@ function writeExclusiveJson(path: string, value: unknown): boolean {
249
250
  if (Buffer.byteLength(body) > RECORD_LIMIT_BYTES) {
250
251
  throw new Error(`approval record exceeds ${RECORD_LIMIT_BYTES} bytes`);
251
252
  }
252
- mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
253
+ mkdirSync(dirname(path), { recursive: true, mode: stateDirMode() });
253
254
  const temporary = `${path}.tmp-${process.pid}-${randomUUID()}`;
254
255
  try {
255
- writeFileSync(temporary, body, { encoding: "utf8", flag: "wx", mode: 0o600 });
256
+ writeFileSync(temporary, body, { encoding: "utf8", flag: "wx", mode: stateFileMode() });
256
257
  try {
257
258
  linkSync(temporary, path);
258
259
  } catch (error) {
259
260
  if ((error as NodeJS.ErrnoException).code === "EEXIST") return false;
260
261
  throw error;
261
262
  }
262
- chmodSync(path, 0o600);
263
+ chmodSync(path, stateFileMode());
263
264
  return true;
264
265
  } finally {
265
266
  if (existsSync(temporary)) unlinkSync(temporary);
@@ -85,10 +85,7 @@ export function probeBilling(adapter: AdapterName, io: ProbeIo = {}): BillingPro
85
85
  break;
86
86
  }
87
87
  case "cursor":
88
- // cursor-agent's stored-login location is not yet verified against a
89
- // live install (adapter itself is pending verification); never claim
90
- // presence or absence we can't prove.
91
- login = "unknown";
88
+ login = probeCursorLogin(env, home);
92
89
  break;
93
90
  default:
94
91
  // External adapters own their authentication. The generic engine must
@@ -126,6 +123,39 @@ function probeClaudeLogin(home: string): LoginState {
126
123
  return existsSync(dir) ? "unknown" : "absent";
127
124
  }
128
125
 
126
+ /**
127
+ * cursor-agent: the stored login is `auth.json` holding `accessToken` and
128
+ * `refreshToken`. The path follows the CLI's own resolution, read from the
129
+ * installed 2026.09.18 build: `$CURSOR_CONFIG_DIR` when set, else
130
+ * `%APPDATA%\\cursor` on Windows, `~/.cursor` on macOS, and
131
+ * `$XDG_CONFIG_HOME/cursor` (default `~/.config/cursor`) elsewhere.
132
+ * `cursor-agent status --format json` reports the same two tokens as
133
+ * `hasAccessToken` / `hasRefreshToken`.
134
+ */
135
+ function probeCursorLogin(env: NodeJS.ProcessEnv, home: string): LoginState {
136
+ const authPath = join(cursorConfigDir(env, home), "auth.json");
137
+ if (!existsSync(authPath)) return "absent";
138
+ try {
139
+ const parsed = JSON.parse(readFileSync(authPath, "utf8")) as {
140
+ accessToken?: string;
141
+ refreshToken?: string;
142
+ };
143
+ return parsed.accessToken?.trim() || parsed.refreshToken?.trim() ? "present" : "absent";
144
+ } catch {
145
+ return "unknown";
146
+ }
147
+ }
148
+
149
+ function cursorConfigDir(env: NodeJS.ProcessEnv, home: string): string {
150
+ const override = env.CURSOR_CONFIG_DIR?.trim();
151
+ if (override) return override;
152
+ const platform = env.HARNERY_BILLING_PLATFORM?.trim() || process.platform;
153
+ if (platform === "win32")
154
+ return join(env.APPDATA?.trim() || join(home, "AppData", "Roaming"), "cursor");
155
+ if (platform === "darwin") return join(home, ".cursor");
156
+ return join(env.XDG_CONFIG_HOME?.trim() || join(home, ".config"), "cursor");
157
+ }
158
+
129
159
  /** codex: auth lives at $CODEX_HOME/auth.json (default ~/.codex). A `tokens`
130
160
  * object means a ChatGPT (subscription) login; an OPENAI_API_KEY field means
131
161
  * a stored API key (`codex login --api-key`). */
@@ -13,6 +13,7 @@ import {
13
13
  writeFileSync,
14
14
  } from "node:fs";
15
15
  import { dirname } from "node:path";
16
+ import { stateDirMode, stateFileMode } from "../storage/modes.ts";
16
17
 
17
18
  const JSON_LIMIT = 512 * 1024;
18
19
 
@@ -33,18 +34,18 @@ export function writeImmutableJson(path: string, value: unknown): boolean {
33
34
  if (Buffer.byteLength(body) > JSON_LIMIT) {
34
35
  throw new Error(`immutable record exceeds ${JSON_LIMIT} bytes`);
35
36
  }
36
- mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
37
+ mkdirSync(dirname(path), { recursive: true, mode: stateDirMode() });
37
38
  const temporary = `${path}.tmp-${process.pid}-${randomUUID()}`;
38
39
  let fd: number | undefined;
39
40
  try {
40
- fd = openSync(temporary, "wx", 0o600);
41
+ fd = openSync(temporary, "wx", stateFileMode());
41
42
  writeFileSync(fd, body, "utf8");
42
43
  fsyncSync(fd);
43
44
  closeSync(fd);
44
45
  fd = undefined;
45
46
  try {
46
47
  linkSync(temporary, path);
47
- chmodSync(path, 0o600);
48
+ chmodSync(path, stateFileMode());
48
49
  fsyncParentDirectory(path);
49
50
  return true;
50
51
  } catch (error) {
@@ -1,4 +1,4 @@
1
- import type { Adapter } from "../adapter.ts";
1
+ import { type Adapter, adapterFromPlatform } from "../adapter.ts";
2
2
  import { recordLiveTaskChangeV3 } from "../agents/live-authority-v3.ts";
3
3
  import { endSessionExplicitV3 } from "../agents/session-finalizer-v3.ts";
4
4
  import {
@@ -115,8 +115,7 @@ function requireV3Route(coordRoot: string) {
115
115
  }
116
116
 
117
117
  function workflowAdapter(adapter: string | undefined): Adapter {
118
- if (adapter === "codex" || adapter === "cursor" || adapter === "claude-code") return adapter;
119
- return "claude-code";
118
+ return adapterFromPlatform(adapter, { context: "workflow-child" });
120
119
  }
121
120
 
122
121
  export { stableScopeId } from "./scope-id.ts";
@@ -20,6 +20,7 @@ import {
20
20
  type PolicyNetworkAccess,
21
21
  policyDigest,
22
22
  } from "../policy/index.ts";
23
+ import { stateFileMode } from "../storage/modes.ts";
23
24
  import { assertWorkflowRunId, readWorkflowApproval } from "./approvals.ts";
24
25
  import { isCanonicalWorkflowAttemptContext } from "./attempt-context.ts";
25
26
  import { readJsonRecord, writeImmutableJson } from "./durable-record.ts";
@@ -205,13 +206,13 @@ export function acquireWorkflowResumeLease(coordRoot: string, runId: string): ()
205
206
 
206
207
  const acquire = (): boolean => {
207
208
  try {
208
- const fd = openSync(path, "wx", 0o600);
209
+ const fd = openSync(path, "wx", stateFileMode());
209
210
  try {
210
211
  writeFileSync(fd, `${JSON.stringify(owner)}\n`, "utf8");
211
212
  } finally {
212
213
  closeSync(fd);
213
214
  }
214
- chmodSync(path, 0o600);
215
+ chmodSync(path, stateFileMode());
215
216
  return true;
216
217
  } catch (error) {
217
218
  if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
@@ -0,0 +1,150 @@
1
+ /**
2
+ * opencode spawn adapter: runs one subagent as a headless `opencode run`
3
+ * subprocess with `--format json`.
4
+ *
5
+ * Contract notes (verified 2026-09-20 against opencode v2.0.11: a
6
+ * `--format json` run streams NDJSON parts and a `PONG` probe round-trips):
7
+ * - `opencode run --format json --model <provider/model#variant> "<prompt>"`
8
+ * prints one JSON object per line (NDJSON). Each line carries `sessionID`;
9
+ * the assistant answer is the concatenation of `type:"text"` parts.
10
+ * - `--title` names the session; the final text is assembled from the stream
11
+ * rather than a single result envelope (there is no `type:"result"` line).
12
+ * - `run` connects to the shared background service by default; a workflow
13
+ * child inherits whatever service is discovered, matching interactive runs.
14
+ * - No per-run cost surface in the stream (cost lives in `session export`) →
15
+ * left undefined. No max-turns equivalent → `maxTurns` accepted and ignored.
16
+ * - OpenCode selects reasoning through the model id variant, so there is no
17
+ * effort flag; `effortValues` is empty and a supplied effort is rejected.
18
+ */
19
+
20
+ import { exec } from "../../lib/exec.ts";
21
+ import { builtinAdapterProfile, validateAdapterEffort } from "../adapters/profiles.ts";
22
+ import type { AdapterInvocation, AdapterRawResult } from "../adapters/types.ts";
23
+ import { notFoundError } from "./adapters.ts";
24
+ import { buildChildEnv } from "./child-env.ts";
25
+ import { resolveSandboxProjection } from "./sandbox-projection.ts";
26
+ import { isUpstreamFailureText, vendorFailureText } from "./spawn-failure.ts";
27
+ import type { Spawner, SpawnRequest, SpawnResult } from "./types.ts";
28
+
29
+ interface OpenCodeStreamPart {
30
+ type?: string;
31
+ sessionID?: string;
32
+ part?: { type?: string; text?: string };
33
+ }
34
+
35
+ /**
36
+ * Parse the NDJSON `opencode run --format json` stream into the final answer
37
+ * text and the session id. Exported for unit tests (no live binary needed).
38
+ *
39
+ * Tolerant by contract: non-JSON lines (a stray log line) are skipped, and the
40
+ * text is the ordered concatenation of every `text` part so a multi-part answer
41
+ * is preserved.
42
+ */
43
+ export function parseOpenCodeStream(stdout: string): {
44
+ text: string;
45
+ sessionId?: string;
46
+ } {
47
+ let sessionId: string | undefined;
48
+ const chunks: string[] = [];
49
+ for (const line of stdout.split("\n")) {
50
+ const trimmed = line.trim();
51
+ if (!trimmed) continue;
52
+ let parsed: OpenCodeStreamPart;
53
+ try {
54
+ parsed = JSON.parse(trimmed) as OpenCodeStreamPart;
55
+ } catch {
56
+ continue;
57
+ }
58
+ if (!sessionId && typeof parsed.sessionID === "string" && parsed.sessionID.length > 0) {
59
+ sessionId = parsed.sessionID;
60
+ }
61
+ if (parsed.type === "text" && typeof parsed.part?.text === "string") {
62
+ chunks.push(parsed.part.text);
63
+ }
64
+ }
65
+ return { text: chunks.join("").trim(), sessionId };
66
+ }
67
+
68
+ export function buildOpenCodeInvocation(req: SpawnRequest): AdapterInvocation {
69
+ validateAdapterEffort("opencode", req.effort);
70
+ if (req.filesystemPolicy) {
71
+ // Declared unrepresentable: refuse rather than drop it silently (ADR 0039).
72
+ resolveSandboxProjection(
73
+ "opencode",
74
+ builtinAdapterProfile("opencode")?.sandboxProjection,
75
+ req.filesystemPolicy,
76
+ );
77
+ }
78
+ // `--auto` auto-approves permissions that are not explicitly denied, which a
79
+ // headless workflow child needs because it has no interactive approval
80
+ // channel; it does not map host policy into child tools (unsupported).
81
+ const argv = ["opencode", "run", "--format", "json", "--auto"];
82
+ if (req.model) argv.push("--model", req.model);
83
+ argv.push(req.prompt);
84
+ return { argv };
85
+ }
86
+
87
+ export function normalizeOpenCodeResult(raw: AdapterRawResult): SpawnResult {
88
+ if (raw.timedOut) {
89
+ return {
90
+ ok: false,
91
+ text: "",
92
+ durationMs: raw.durationMs,
93
+ error: `opencode timed out after ${raw.durationMs}ms and was killed`,
94
+ };
95
+ }
96
+ // Structural environment signal: the binary was never there (spawned directly,
97
+ // so a missing binary surfaces as ENOENT). Uncharged and not retried.
98
+ if (raw.spawnErrno === "ENOENT") {
99
+ return {
100
+ ok: false,
101
+ text: "",
102
+ durationMs: raw.durationMs,
103
+ error: notFoundError("opencode"),
104
+ class: "environment",
105
+ };
106
+ }
107
+ if (raw.exitCode === 127) {
108
+ // A bare 127 with no errno is a shell/vendor 127, indistinguishable from a
109
+ // legitimate one — charged as work rather than classed environment.
110
+ return { ok: false, text: "", durationMs: raw.durationMs, error: notFoundError("opencode") };
111
+ }
112
+ if (raw.exitCode !== 0) {
113
+ const failureText = vendorFailureText(raw);
114
+ return {
115
+ ok: false,
116
+ text: "",
117
+ durationMs: raw.durationMs,
118
+ error: `opencode exited ${raw.exitCode}: ${failureText}`,
119
+ ...(isUpstreamFailureText(failureText) ? { class: "upstream" as const } : {}),
120
+ };
121
+ }
122
+
123
+ const parsed = parseOpenCodeStream(raw.stdout);
124
+ return {
125
+ ok: true,
126
+ text: parsed.text,
127
+ sessionId: parsed.sessionId,
128
+ durationMs: raw.durationMs,
129
+ };
130
+ }
131
+
132
+ export const openCodeSpawner: Spawner = async (req: SpawnRequest): Promise<SpawnResult> => {
133
+ const t0 = Date.now();
134
+ let invocation: AdapterInvocation;
135
+ try {
136
+ invocation = buildOpenCodeInvocation(req);
137
+ } catch (error) {
138
+ return { ok: false, text: "", durationMs: 0, error: (error as Error).message };
139
+ }
140
+
141
+ const r = await exec(invocation.argv, {
142
+ cwd: req.cwd,
143
+ env: buildChildEnv(req.runId, {
144
+ subscriptionOnly: req.subscriptionOnly,
145
+ agentId: req.agentId,
146
+ }),
147
+ timeout: req.timeoutMs,
148
+ });
149
+ return normalizeOpenCodeResult({ ...r, durationMs: Date.now() - t0 });
150
+ };
@@ -1,5 +1,6 @@
1
1
  import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
+ import { stateDirMode, stateFileMode } from "../storage/modes.ts";
3
4
  import { fsyncParentDirectory, stableDigest } from "./durable-record.ts";
4
5
 
5
6
  // Limit for the JSON record body. The trailing newline delimiter is outside the
@@ -47,9 +48,9 @@ export function appendWorkflowTranscriptEvent(
47
48
  data,
48
49
  );
49
50
  const line = `${JSON.stringify(record)}\n`;
50
- mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
51
+ mkdirSync(dirname(path), { recursive: true, mode: stateDirMode() });
51
52
  const existed = existsSync(path);
52
- const fd = openSync(path, "a", 0o600);
53
+ const fd = openSync(path, "a", stateFileMode());
53
54
  try {
54
55
  writeFileSync(fd, line, "utf8");
55
56
  fsyncSync(fd);