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
@@ -21,12 +21,13 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
21
21
  import { dirname, join } from "node:path";
22
22
  import { coordEnv } from "../../lib/env.js";
23
23
  import { buildInstructionBundle } from "../../lib/instructions/bundle.js";
24
+ import { adapterFromPlatform as sharedAdapterFromPlatform } from "../adapter.js";
24
25
  import { resolveCoordRoot } from "../agents/coord-client.js";
25
26
  import { classifyWriteClaimFinalization, formatWriteClaimFinalizationDenial, } from "../agents/finalization.js";
26
27
  import { recordLiveClaimChangeV3, restoreLiveCoordinationStateAfterEpochV3, } from "../agents/live-authority-v3.js";
27
28
  import { evaluateStopHook, STOP_REMEDIATION_MARKER } from "../agents/rules/stop-hook.js";
28
- import { assistantTextStartsWithSessionNameBlock, isSessionNameRemediationCommand, matchSessionNameDisplay, sessionNameDisplayAcceptedNames, sessionNameDisplayInstruction, sessionNameDisplayPending, sessionNameDisplayRecoveryInstruction, toolResponseMintedSessionName, } from "../agents/session-name-display.js";
29
- import { readHeartbeat, setAssignedNameCache, stampSessionNameRequested, stampSessionNameSeen, } from "../agents/state/heartbeat-writer.js";
29
+ import { assistantTextStartsWithSessionNameBlock, isSessionNameRemediationCommand, matchSessionNameDisplay, sessionNameDisplayAcceptedNames, sessionNameDisplayInstruction, sessionNameDisplayPending, sessionNameDisplayRecoveryInstruction, sessionNameDisplayReminder, sessionNameDisplayReminderDue, toolResponseMintedSessionName, } from "../agents/session-name-display.js";
30
+ import { readHeartbeat, setAssignedNameCache, stampSessionNameReminded, stampSessionNameRequested, stampSessionNameSeen, } from "../agents/state/heartbeat-writer.js";
30
31
  import { readLiveCoordinationRow } from "../agents/state/live-coordination-view.js";
31
32
  import { ensureLiveCoordinationHeartbeat } from "../agents/state/live-coordination-writer.js";
32
33
  import { assignName } from "../agents/state/names.js";
@@ -39,11 +40,10 @@ import { captureSpanClockV3 } from "../events/v3/span-state.js";
39
40
  import { ensureRelayDaemon, fetchPresence, publishPresence } from "../presence/index.js";
40
41
  import { closeProcessLoggers, legacyLogFields, processLogger } from "../storage/logger.js";
41
42
  import { stableScopeId } from "../workflow/scope-id.js";
43
+ import { adapterBehavior } from "./adapter/behaviors/index.js";
42
44
  import { detectAdapter, shouldSkipHookAdapter } from "./adapter/detect.js";
43
45
  import { extractBashCommand, extractToolDescription, normalizeEventName, parsePayload, } from "./adapter/parse.js";
44
- import { discoverCodexSessionTranscript } from "./adapter/runtime-telemetry.js";
45
46
  import { scheduleBackupSnapshot } from "./backup-schedule.js";
46
- import { codexWslFileLinkTelemetry, inspectCodexWslBridge, isWslUncPath, renderCodexWslFileLinkContext, } from "./codex-wsl-bridge.js";
47
47
  import { captureImages, detectPresence, imageJanitor, journalArchive, journalJanitor, journalRecoveryCue, playSound, recordImageArtifactsV3, resetSoundCounters, runSessionSyncExtension, soundForEvent, } from "./effects/index.js";
48
48
  import { toolInputHash, toolTargetHash } from "./events/input-hash.js";
49
49
  import { canonicalize } from "./guard-path.js";
@@ -51,9 +51,9 @@ import { beginHookHealth, observeHookDebug, observeHookError, writeHookHealthCom
51
51
  import { adapterPidFromEnv, parsePsChainLine, selectAnchorPid } from "./resolve/anchor.js";
52
52
  import { extractIntentComment, resolveIntent } from "./resolve/intent.js";
53
53
  import { resolveOwner } from "./resolve/owner.js";
54
- import { inspectSessionNameDisplayImmediately, scanAssistantStatusBoxPresent, scanSessionNameDisplayedImmediately, scanStatusBoxPresent, scanTranscriptRuntime, } from "./resolve/transcript.js";
54
+ import { inspectSessionNameDisplayImmediately, scanAssistantStatusBoxPresent, scanStatusBoxPresent, scanTranscriptRuntime, } from "./resolve/transcript.js";
55
55
  import { sessionNamePresence } from "./session-name-presence.js";
56
- import { unsafeCrossShellReason } from "./unsafe-cross-shell.js";
56
+ import { shellWaiterReason } from "./shell-waiter.js";
57
57
  let hookHealthState;
58
58
  function parseArgv(argv) {
59
59
  const out = { eventName: null, extra: [] };
@@ -223,11 +223,9 @@ function buildEventData(eventType, ctx) {
223
223
  const p = ctx.payload;
224
224
  switch (eventType) {
225
225
  case "session.started": {
226
- const adapterPlatform = ctx.adapter === "claude-code"
227
- ? "claude-code"
228
- : ctx.adapter === "cursor"
229
- ? "cursor"
230
- : "codex";
226
+ // The platform label is the adapter id itself; enumerating and defaulting
227
+ // to codex silently mislabels any adapter not in the list.
228
+ const adapterPlatform = ctx.adapter;
231
229
  // Recorded fork lineage is NOT detected here. On claude-code a fork
232
230
  // never fires its own session.started — SessionStart fires under the
233
231
  // PARENT's session id (source=resume) before the fork id is minted
@@ -310,9 +308,7 @@ function buildEventData(eventType, ctx) {
310
308
  }
311
309
  case "turn.completed": {
312
310
  const lastAssistantMessage = p?.raw.last_assistant_message ?? "";
313
- const fileLinkTelemetry = ctx.adapter === "codex"
314
- ? codexWslFileLinkTelemetry(ctx.coordRoot, p?.cwd, lastAssistantMessage)
315
- : null;
311
+ const fileLinkTelemetry = adapterBehavior(ctx.adapter).fileLinkTelemetry?.(ctx.coordRoot, p?.cwd, lastAssistantMessage) ?? null;
316
312
  return {
317
313
  // Backfill the model for adapters that omit it at session.started
318
314
  // (Claude Code). The transcript is populated with assistant turns by
@@ -343,13 +339,12 @@ function buildEventData(eventType, ctx) {
343
339
  // and which name that refers to. Reported on every stop while a name
344
340
  // exists (the stop-hook.session_name verdict reads it per turn); the
345
341
  // transcript scan itself stops once the name has been sighted.
346
- ...sessionNamePresence(ctx.coordRoot, ctx.instanceId, (name) => scanSessionNameDisplayedImmediately(
347
- // Codex stops carry no transcript_path; discover the rollout by
348
- // session id so the naming ritual can verify and stamp there too.
342
+ ...sessionNamePresence(ctx.coordRoot, ctx.instanceId, (name) => inspectSessionNameDisplayImmediately(
343
+ // Some adapters omit transcript_path on stop; the behavior may
344
+ // discover the transcript by session id so the naming ritual can
345
+ // verify and stamp there too.
349
346
  p?.transcript_path ??
350
- (ctx.adapter === "codex"
351
- ? discoverCodexSessionTranscript(p?.session_id ?? ctx.instanceId)
352
- : undefined), name, assistantTextStartsWithSessionNameBlock)),
347
+ adapterBehavior(ctx.adapter).discoverTranscript?.(p?.session_id ?? ctx.instanceId), name, assistantTextStartsWithSessionNameBlock)),
353
348
  ...(fileLinkTelemetry ?? {}),
354
349
  stop_hook_active: p?.stop_hook_active,
355
350
  };
@@ -479,6 +474,7 @@ async function main() {
479
474
  const hookClock = captureSpanClockV3();
480
475
  const { eventName, extra } = parseArgv(process.argv.slice(2));
481
476
  const adapter = detectAdapter(process.argv.slice(2));
477
+ const behavior = adapter ? adapterBehavior(adapter) : null;
482
478
  const raw = await readStdin();
483
479
  hookHealthState = beginHookHealth({
484
480
  started_at_ms: hookStartedAt,
@@ -516,8 +512,8 @@ async function main() {
516
512
  // Kill-switch-INDEPENDENT effects: notification sounds fire BEFORE the
517
513
  // HARNERY_AGENT_COORD_OFF gate so audible feedback survives incident-triage
518
514
  // bypass: sound playback happens before the kill-switch bailout.
519
- // Claude-Code-only; stop-failure → error, sub-agent-start → subagent-start.
520
- if (adapter === "claude-code" && eventName) {
515
+ // Behavior-gated; stop-failure → error, sub-agent-start → subagent-start.
516
+ if (behavior?.soundEffects && eventName) {
521
517
  const s = soundForEvent(eventName);
522
518
  if (s) {
523
519
  const repoRoot = resolveCoordRoot(process.cwd());
@@ -568,7 +564,7 @@ async function main() {
568
564
  pid: process.pid,
569
565
  ppid: process.ppid,
570
566
  };
571
- if (!eventName || !adapter) {
567
+ if (!eventName || !adapter || !behavior) {
572
568
  appendDebug(coordRoot, { ...debugBase, skipped: "missing-event-or-adapter" });
573
569
  return 0;
574
570
  }
@@ -579,90 +575,17 @@ async function main() {
579
575
  // assistant text. Preserve only privacy-safe ritual booleans in the V3
580
576
  // producer's open-turn state; Stop consumes them into the authoritative
581
577
  // turn.completed event. The response body never enters V3 or heartbeat.
582
- if (adapter === "cursor" && eventName === "after-agent-response") {
583
- const payload = parsePayload(raw, adapter);
584
- const owner = resolveOwner({ payload: payload?.raw ?? null, coordRoot });
585
- if (!owner) {
586
- appendDebug(coordRoot, { ...debugBase, skipped: "no-owner-resolved" });
587
- return 0;
588
- }
589
- const row = readLiveCoordinationRow(coordRoot, owner.instance_id);
590
- const name = sessionNameDisplayPending(row);
591
- const text = typeof payload?.raw.text === "string" ? payload.raw.text : "";
592
- const sighting = matchSessionNameDisplay(row, text);
593
- if (sighting) {
594
- stampSessionNameSeen(coordRoot, owner.instance_id, sighting.pending);
595
- appendDebug(coordRoot, {
596
- ...debugBase,
597
- effect: "session-name-display-stamped",
598
- session_name_pending: sighting.pending,
599
- ...(sighting.displayed === sighting.pending
600
- ? {}
601
- : { session_name_displayed: sighting.displayed, session_name_drift: true }),
602
- });
603
- }
604
- else if (!name) {
605
- // Nothing was owed. Distinct from a miss: conflating the two is what made
606
- // a latched Cursor session undiagnosable from this log.
607
- appendDebug(coordRoot, { ...debugBase, skipped: "no-pending-session-name" });
608
- }
609
- else {
610
- appendDebug(coordRoot, {
611
- ...debugBase,
612
- skipped: "session-name-block-absent",
613
- session_name_pending: name,
614
- reply_bytes: text.length,
615
- reply_leads_with_fence: /^\s*`{3,}/.test(text),
616
- });
617
- }
618
- if (ledgerRoute.state === "blocked") {
619
- appendDebug(coordRoot, {
620
- ...debugBase,
621
- skipped: "v3-control-blocked",
622
- reason: ledgerRoute.reason,
623
- owner_source: owner.source,
624
- });
625
- return 0;
626
- }
627
- const statusBoxPresentStrict = scanAssistantStatusBoxPresent(undefined, text);
628
- // The hook parser needs the completed reply long enough to scan it, but
629
- // durable hook intake must never retain that body. Keep every other native
630
- // field so turn correlation remains available to the producer.
631
- const { text: _completedReply, ...privacySafeRaw } = payload?.raw ?? {};
632
- const privacySafePayload = payload ? { ...payload, raw: privacySafeRaw } : null;
633
- const v3Result = recordLiveHookSignalV3({
578
+ if (behavior && eventName && eventName === behavior.completedResponseEvent) {
579
+ return handleCompletedResponse({
634
580
  coordRoot,
635
- route: ledgerRoute,
636
- eventName,
637
- payload: privacySafePayload,
638
581
  adapter,
639
- instanceId: owner.instance_id,
640
- hook_name: eventName,
641
- hook_duration_ms: Math.max(0, Math.floor(performance.now() - hookStartedAt)),
642
- monotonic_ns: hookClock.monotonic_ns,
643
- turn_ritual: {
644
- status_box_present: statusBoxPresentStrict,
645
- status_box_present_strict: statusBoxPresentStrict,
646
- session_name_required: name !== undefined,
647
- session_name_present: name ? assistantTextStartsWithSessionNameBlock(text, name) : false,
648
- },
649
- });
650
- observeHookDebug(hookHealthState, { event_v3_state: v3Result.state });
651
- appendDebug(coordRoot, {
652
- ...debugBase,
653
- effect: "cursor-response-ritual-observed",
654
- owner_source: owner.source,
655
- event_v3_state: v3Result.state,
656
- ...(v3Result.state === "observed"
657
- ? {
658
- generation_id: v3Result.generation_id,
659
- turn_id: v3Result.turn_id,
660
- response_observed_at: v3Result.observed_at,
661
- status_box_present_strict: statusBoxPresentStrict,
662
- }
663
- : {}),
582
+ eventName,
583
+ raw,
584
+ debugBase,
585
+ ledgerRoute,
586
+ hookStartedAt,
587
+ hookClock,
664
588
  });
665
- return 0;
666
589
  }
667
590
  const norm = normalizeEventName(eventName);
668
591
  if (!norm) {
@@ -670,6 +593,7 @@ async function main() {
670
593
  return 0;
671
594
  }
672
595
  const payload = parsePayload(raw, adapter);
596
+ const bridge = behavior.bridgeFor(payload?.cwd);
673
597
  const owner = resolveOwner({
674
598
  payload: payload?.raw ?? null,
675
599
  coordRoot,
@@ -711,8 +635,14 @@ async function main() {
711
635
  status_box_present_strict: data.status_box_present_strict === true,
712
636
  session_name_required: typeof data.session_name_present_for === "string",
713
637
  session_name_present: data.session_name_present === true,
638
+ ...(typeof data.session_name_unavailable_reason === "string"
639
+ ? { session_name_unavailable_reason: data.session_name_unavailable_reason }
640
+ : {}),
714
641
  }
715
642
  : undefined;
643
+ if (typeof data.session_name_unavailable_reason === "string") {
644
+ reportSessionNameObservationUnavailable(coordRoot, owner.instance_id, adapter, eventName, data.session_name_unavailable_reason);
645
+ }
716
646
  const stopRemediation = norm.event_type === "turn.started" &&
717
647
  typeof data.prompt_text === "string" &&
718
648
  data.prompt_text.includes(STOP_REMEDIATION_MARKER);
@@ -726,6 +656,86 @@ async function main() {
726
656
  });
727
657
  return 0;
728
658
  }
659
+ const run = {
660
+ coordRoot,
661
+ adapter,
662
+ behavior,
663
+ eventName,
664
+ norm,
665
+ payload,
666
+ raw,
667
+ owner,
668
+ sessionId,
669
+ data,
670
+ debugBase,
671
+ ledgerRoute,
672
+ hookStartedAt,
673
+ hookClock,
674
+ priorCoordination,
675
+ topLevelSession,
676
+ stopRemediation,
677
+ bridge,
678
+ turnRitual,
679
+ };
680
+ recordSignal(run);
681
+ recordContextTelemetry(run);
682
+ // Phase 8: SessionStart post-emit: project the event so the heartbeat
683
+ // lands synchronously, run stale-sweep, and emit the adapter-shaped
684
+ // systemMessage JSON (peer table + wiring check + council invites).
685
+ // Adapter-agnostic since v0.5.0; replaces the previous bash UX layer
686
+ // and the equivalent per-adapter bash session_start handlers.
687
+ if (norm.event_type === "session.started")
688
+ await handleSessionStarted(run);
689
+ // Phase 8: SessionEnd cleanup: delete heartbeat + pid-map rows. Adapter-
690
+ // agnostic since v0.5.0.
691
+ if (norm.event_type === "session.ended")
692
+ handleSessionEnded(run);
693
+ // Phase 8: SubagentStart: the child generation was opened above from the
694
+ // native delegation identity. Refresh its disposable projection, log the
695
+ // lifecycle event, and emit a context message announcing the subagent.
696
+ if (norm.event_type === "agent.started")
697
+ await handleAgentStarted(run);
698
+ // Phase 8: SubagentStop: delete subagent heartbeat + log.
699
+ if (norm.event_type === "agent.completed")
700
+ await handleAgentCompleted(run);
701
+ // Phase 8: UserPromptSubmit: render dedup'd peer table + council pending
702
+ // and emit the adapter-shaped systemMessage JSON. Adapter-agnostic since v0.5.0.
703
+ if (norm.event_type === "turn.started")
704
+ await handleTurnStarted(run);
705
+ // turn.completed: telemetry, then the stop verdict. The
706
+ // verdict + codex-replay previously lived in the per-adapter shell adapters;
707
+ // agent-hook owns them now. Runs on the normal "stop" event only;
708
+ // "stop-failure" (API error) gets no gate, matching the previous
709
+ // stop vs stop-failure split.
710
+ if (norm.event_type === "turn.completed" && eventName === "stop") {
711
+ const exit = await handleStop(run);
712
+ if (exit !== undefined)
713
+ return exit;
714
+ }
715
+ // Phase 7: PreToolUse: heartbeat + pid-map self-heal on every tool call.
716
+ // Adapter-agnostic: both writes have the same shape regardless of who fired.
717
+ // Cursor/Codex bash dispatchers still fire their own G-guard logic, but the
718
+ // heals here keep the agent-coord layer's view of liveness fresh.
719
+ //
720
+ // The heartbeat + pid-map heals are paired by design; they were wired
721
+ // side-by-side in the previous pre-tool-use adapter. The Phase 4-6 refactor
722
+ // preserved the heartbeat half but dropped the pid-map half; the pid-map
723
+ // call was restored here afterward.
724
+ if (norm.event_type === "tool.requested") {
725
+ const exit = await handleToolRequested(run);
726
+ if (exit !== undefined)
727
+ return exit;
728
+ }
729
+ if (norm.event_type === "tool.completed" && eventName === "post-tool-use")
730
+ await handlePostToolUse(run);
731
+ // Phase 7: PostToolUseFailure: release claim on failed Edit (the file
732
+ // never landed; the claim is stale). Adapter-agnostic.
733
+ if (norm.event_type === "tool.completed" && eventName === "post-tool-use-failure")
734
+ handlePostToolUseFailure(run);
735
+ return 0;
736
+ }
737
+ function recordSignal(run) {
738
+ const { coordRoot, adapter, eventName, norm, payload, owner, sessionId, data, debugBase, ledgerRoute, hookStartedAt, hookClock, priorCoordination, stopRemediation, bridge, turnRitual, } = run;
729
739
  let capturedImages = [];
730
740
  if (norm.event_type === "tool.requested" ||
731
741
  (norm.event_type === "tool.completed" && eventName === "post-tool-use")) {
@@ -753,7 +763,7 @@ async function main() {
753
763
  : {}),
754
764
  }
755
765
  : {}),
756
- ...(adapter === "codex" && isWslUncPath(payload?.cwd) ? { bridge: "codex-wsl" } : {}),
766
+ ...(bridge ? { bridge } : {}),
757
767
  hook_name: eventName,
758
768
  hook_duration_ms: Math.max(0, Math.floor(performance.now() - hookStartedAt)),
759
769
  monotonic_ns: hookClock.monotonic_ns,
@@ -806,9 +816,7 @@ async function main() {
806
816
  parentEvent: v3Result.event,
807
817
  payload,
808
818
  adapter,
809
- ...(adapter === "codex" && isWslUncPath(payload.cwd)
810
- ? { bridge: "codex-wsl" }
811
- : {}),
819
+ ...(bridge ? { bridge } : {}),
812
820
  monotonic_ns: hookClock.monotonic_ns,
813
821
  });
814
822
  const nativeChild = payload.subagent_id ?? payload.agent_id;
@@ -851,6 +859,12 @@ async function main() {
851
859
  ...(v3Result ? { event_v3_state: v3Result.state } : {}),
852
860
  ...(v3Result && "reason" in v3Result ? { event_v3_reason: v3Result.reason } : {}),
853
861
  });
862
+ run.v3Result = v3Result;
863
+ run.v3EventId = v3EventId;
864
+ run.recordedGenerationId = recordedGenerationId;
865
+ }
866
+ function recordContextTelemetry(run) {
867
+ const { coordRoot, adapter, norm, payload, owner, sessionId } = run;
854
868
  // Context telemetry is opportunistic and truthful: only persist a sample
855
869
  // when the adapter payload actually exposes usage/window data. Identical
856
870
  // measurements are de-duplicated before they reach the canonical stream.
@@ -902,413 +916,515 @@ async function main() {
902
916
  logError(coordRoot, err, { phase: "context-compaction-completed" });
903
917
  }
904
918
  }
905
- // Phase 8: SessionStart post-emit: project the event so the heartbeat
906
- // lands synchronously, run stale-sweep, and emit the adapter-shaped
907
- // systemMessage JSON (peer table + wiring check + council invites).
908
- // Adapter-agnostic since v0.5.0; replaces the previous bash UX layer
909
- // and the equivalent per-adapter bash session_start handlers.
910
- if (norm.event_type === "session.started") {
911
- // A configured backup runs out of process and is freshness-gated per host.
912
- // Startup never waits for restic or a remote provider; the child owns no
913
- // hook stdio, so the harness is not held open until it finishes. A failed
914
- // previous run surfaces as one line in this session's context.
915
- let backupCue = "";
916
- try {
917
- backupCue = scheduleBackupSnapshot(coordRoot).cue ?? "";
918
- }
919
- catch (err) {
920
- logError(coordRoot, err, { phase: "backup-schedule" });
921
- }
922
- // Effect (claude-code): prune stale journal archives + sweep orphans.
923
- // The recovery-cue is merged into the
924
- // session-start additionalContext inside emitSessionStartSystemMessage.
925
- if (adapter === "claude-code")
926
- journalJanitor(coordRoot);
927
- imageJanitor(coordRoot);
928
- // Session start is the only caller of the artifact janitor and the storage
929
- // maintenance pass, so both load here instead of on every tool call.
930
- const { autoCleanArtifacts } = await import("../artifacts/index.js");
931
- const { autoCleanEventV3Archives } = await import("../events/v3/archive-retention.js");
932
- const { createAutomaticMaintenanceComposition, runAutomaticMaintenancePass } = await import("../storage/maintenance-providers.js");
933
- // Effect: throttled daily sweep of expired artifact workspaces (guarded,
934
- // managed-expired only; see autoCleanArtifacts). Best-effort like every
935
- // janitor here: a failure logs and never blocks session start.
936
- try {
937
- autoCleanArtifacts(coordRoot);
938
- }
939
- catch (err) {
940
- logError(coordRoot, err, { phase: "artifact-auto-clean" });
941
- }
942
- // Effect: apply the same daily throttle to closed V3 epochs. The newest
943
- // recovery epochs remain protected by the archive retention policy.
944
- try {
945
- autoCleanEventV3Archives(coordRoot);
946
- }
947
- catch (err) {
948
- logError(coordRoot, err, { phase: "event-v3-archive-auto-clean" });
949
- }
950
- // Plan one bounded, claim-first global slice from cached pressure only.
951
- // Existing owner janitors above remain the only active executors until
952
- // each provider receives a separate production activation.
953
- try {
954
- await runAutomaticMaintenancePass(createAutomaticMaintenanceComposition(coordRoot, {
955
- journal: () => {
956
- if (adapter === "claude-code")
957
- journalJanitor(coordRoot);
958
- },
959
- images: () => imageJanitor(coordRoot),
960
- artifacts: () => autoCleanArtifacts(coordRoot),
961
- }));
962
- }
963
- catch (err) {
964
- logError(coordRoot, err, { phase: "storage-maintenance-auto" });
965
- }
966
- let recovery = null;
967
- if (payload?.source === "compact") {
968
- try {
969
- markContextCompactionCompleted(coordRoot, {
970
- sessionId,
971
- instanceId: owner.instance_id,
972
- });
973
- recovery = prepareContextRecovery(coordRoot, {
974
- instanceId: owner.instance_id,
975
- sessionId,
976
- cwd: payload?.cwd ?? process.cwd(),
977
- });
978
- }
979
- catch (err) {
980
- logError(coordRoot, err, { phase: "context-recovery-session-start" });
981
- }
982
- }
983
- try {
984
- const injected = await emitSessionStartSystemMessage(coordRoot, owner.instance_id, sessionId, data, adapter, recovery?.briefing ?? "", backupCue);
985
- if (injected && recovery) {
986
- completeRecoveryInjection(coordRoot, owner.instance_id, sessionId, recovery);
987
- }
988
- }
989
- catch (err) {
990
- logError(coordRoot, err, { phase: "session-start-systemMessage" });
991
- }
992
- // Cross-machine presence is derived from the authority-safe V3 projection,
993
- // not the disposable heartbeat cache. Announce this session, pull peers,
994
- // and keep the optional live relay connected.
995
- try {
996
- publishPresence(coordRoot);
997
- fetchPresence(coordRoot);
998
- ensureRelayDaemon(coordRoot);
999
- }
1000
- catch (err) {
1001
- logError(coordRoot, err, { phase: "session-start-presence" });
1002
- }
919
+ }
920
+ async function handleSessionStarted(run) {
921
+ const { coordRoot, adapter, behavior, payload, owner, sessionId, data } = run;
922
+ // A configured backup runs out of process and is freshness-gated per host.
923
+ // Startup never waits for restic or a remote provider; the child owns no
924
+ // hook stdio, so the harness is not held open until it finishes. A failed
925
+ // previous run surfaces as one line in this session's context.
926
+ let backupCue = "";
927
+ try {
928
+ backupCue = scheduleBackupSnapshot(coordRoot).cue ?? "";
1003
929
  }
1004
- // Phase 8: SessionEnd cleanup: delete heartbeat + pid-map rows. Adapter-
1005
- // agnostic since v0.5.0.
1006
- if (norm.event_type === "session.ended") {
1007
- try {
1008
- cleanupSessionEnd(coordRoot, owner.instance_id);
1009
- }
1010
- catch (err) {
1011
- logError(coordRoot, err, { phase: "session-end-cleanup" });
1012
- }
1013
- // Effects (claude-code): archive the ending agent's journal + force a
1014
- // session-telemetry sync (via HARNERY_CLAUDE_SESSIONS_FORCE=1).
1015
- if (adapter === "claude-code") {
1016
- journalArchive(coordRoot, owner.instance_id);
1017
- runSessionSyncExtension(coordRoot, true);
1018
- }
1019
- // Publish the post-cleanup V3 projection so remote machines see this
1020
- // session disappear without waiting for the stale window.
1021
- try {
1022
- publishPresence(coordRoot);
1023
- }
1024
- catch (err) {
1025
- logError(coordRoot, err, { phase: "session-end-presence" });
1026
- }
930
+ catch (err) {
931
+ logError(coordRoot, err, { phase: "backup-schedule" });
932
+ }
933
+ // Effect (journal-maintaining adapters): prune stale journal archives +
934
+ // sweep orphans. The recovery-cue is merged into the session-start
935
+ // additionalContext inside emitSessionStartSystemMessage.
936
+ if (behavior.journalMaintenance)
937
+ journalJanitor(coordRoot);
938
+ imageJanitor(coordRoot);
939
+ // Session start is the only caller of the artifact janitor and the storage
940
+ // maintenance pass, so both load here instead of on every tool call.
941
+ const { autoCleanArtifacts } = await import("../artifacts/index.js");
942
+ const { autoCleanEventV3Archives } = await import("../events/v3/archive-retention.js");
943
+ const { createAutomaticMaintenanceComposition, runAutomaticMaintenancePass } = await import("../storage/maintenance-providers.js");
944
+ // Effect: throttled daily sweep of expired artifact workspaces (guarded,
945
+ // managed-expired only; see autoCleanArtifacts). Best-effort like every
946
+ // janitor here: a failure logs and never blocks session start.
947
+ try {
948
+ autoCleanArtifacts(coordRoot);
1027
949
  }
1028
- // Phase 8: SubagentStart: the child generation was opened above from the
1029
- // native delegation identity. Refresh its disposable projection, log the
1030
- // lifecycle event, and emit a context message announcing the subagent.
1031
- if (norm.event_type === "agent.started") {
950
+ catch (err) {
951
+ logError(coordRoot, err, { phase: "artifact-auto-clean" });
952
+ }
953
+ // Effect: apply the same daily throttle to closed V3 epochs. The newest
954
+ // recovery epochs remain protected by the archive retention policy.
955
+ try {
956
+ autoCleanEventV3Archives(coordRoot);
957
+ }
958
+ catch (err) {
959
+ logError(coordRoot, err, { phase: "event-v3-archive-auto-clean" });
960
+ }
961
+ // Plan one bounded, claim-first global slice from cached pressure only.
962
+ // Existing owner janitors above remain the only active executors until
963
+ // each provider receives a separate production activation.
964
+ try {
965
+ await runAutomaticMaintenancePass(createAutomaticMaintenanceComposition(coordRoot, {
966
+ journal: () => {
967
+ if (behavior.journalMaintenance)
968
+ journalJanitor(coordRoot);
969
+ },
970
+ images: () => imageJanitor(coordRoot),
971
+ artifacts: () => autoCleanArtifacts(coordRoot),
972
+ }));
973
+ }
974
+ catch (err) {
975
+ logError(coordRoot, err, { phase: "storage-maintenance-auto" });
976
+ }
977
+ let recovery = null;
978
+ if (payload?.source === "compact") {
1032
979
  try {
1033
- const childInstanceId = data.agent_id ?? owner.instance_id;
1034
- await emitSubagentStartContext(coordRoot, childInstanceId, sessionId, data, adapter);
980
+ markContextCompactionCompleted(coordRoot, {
981
+ sessionId,
982
+ instanceId: owner.instance_id,
983
+ });
984
+ recovery = prepareContextRecovery(coordRoot, {
985
+ instanceId: owner.instance_id,
986
+ sessionId,
987
+ cwd: payload?.cwd ?? process.cwd(),
988
+ });
1035
989
  }
1036
990
  catch (err) {
1037
- logError(coordRoot, err, { phase: "subagent-start-context" });
991
+ logError(coordRoot, err, { phase: "context-recovery-session-start" });
1038
992
  }
1039
993
  }
1040
- // Phase 8: SubagentStop: delete subagent heartbeat + log.
1041
- if (norm.event_type === "agent.completed") {
1042
- try {
1043
- const childInstanceId = payload?.subagent_id ?? payload?.agent_id ?? owner.instance_id;
1044
- cleanupSessionEnd(coordRoot, childInstanceId);
1045
- const { reconcileSessionFinalizationV3 } = await import("../agents/session-finalizer-v3.js");
1046
- reconcileSessionFinalizationV3(coordRoot, { archive_observations: [] });
1047
- }
1048
- catch (err) {
1049
- logError(coordRoot, err, { phase: "subagent-stop-cleanup" });
994
+ try {
995
+ const injected = await emitSessionStartSystemMessage(coordRoot, owner.instance_id, sessionId, data, adapter, recovery?.briefing ?? "", backupCue);
996
+ if (injected && recovery) {
997
+ completeRecoveryInjection(coordRoot, owner.instance_id, sessionId, recovery);
1050
998
  }
1051
999
  }
1052
- // Phase 8: UserPromptSubmit: render dedup'd peer table + council pending
1053
- // and emit the adapter-shaped systemMessage JSON. Adapter-agnostic since v0.5.0.
1054
- if (norm.event_type === "turn.started") {
1055
- // Effects (claude-code): reset per-turn sound rate-limit counters + run
1056
- // presence detection on the prompt.
1057
- if (adapter === "claude-code") {
1058
- resetSoundCounters(sessionId);
1059
- const prompt = payload?.raw?.prompt ?? "";
1060
- if (prompt)
1061
- detectPresence(prompt);
1062
- }
1063
- // Presence: throttled pull of peer machines' presence refs (default 60s
1064
- // interval; the systemMessage below reads whatever is locally known).
1000
+ catch (err) {
1001
+ logError(coordRoot, err, { phase: "session-start-systemMessage" });
1002
+ }
1003
+ // Cross-machine presence is derived from the authority-safe V3 projection,
1004
+ // not the disposable heartbeat cache. Announce this session, pull peers,
1005
+ // and keep the optional live relay connected.
1006
+ try {
1007
+ publishPresence(coordRoot);
1008
+ fetchPresence(coordRoot);
1009
+ ensureRelayDaemon(coordRoot);
1010
+ }
1011
+ catch (err) {
1012
+ logError(coordRoot, err, { phase: "session-start-presence" });
1013
+ }
1014
+ }
1015
+ function handleSessionEnded(run) {
1016
+ const { coordRoot, behavior, owner } = run;
1017
+ try {
1018
+ cleanupSessionEnd(coordRoot, owner.instance_id);
1019
+ }
1020
+ catch (err) {
1021
+ logError(coordRoot, err, { phase: "session-end-cleanup" });
1022
+ }
1023
+ // Effects: archive the ending agent's journal + force a session-telemetry
1024
+ // sync (via HARNERY_CLAUDE_SESSIONS_FORCE=1), each gated by the behavior.
1025
+ if (behavior.journalMaintenance)
1026
+ journalArchive(coordRoot, owner.instance_id);
1027
+ if (behavior.sessionTelemetrySync)
1028
+ runSessionSyncExtension(coordRoot, true);
1029
+ // Publish the post-cleanup V3 projection so remote machines see this
1030
+ // session disappear without waiting for the stale window.
1031
+ try {
1032
+ publishPresence(coordRoot);
1033
+ }
1034
+ catch (err) {
1035
+ logError(coordRoot, err, { phase: "session-end-presence" });
1036
+ }
1037
+ }
1038
+ async function handleAgentStarted(run) {
1039
+ const { coordRoot, adapter, owner, sessionId, data } = run;
1040
+ try {
1041
+ const childInstanceId = data.agent_id ?? owner.instance_id;
1042
+ await emitSubagentStartContext(coordRoot, childInstanceId, sessionId, data, adapter);
1043
+ }
1044
+ catch (err) {
1045
+ logError(coordRoot, err, { phase: "subagent-start-context" });
1046
+ }
1047
+ }
1048
+ async function handleAgentCompleted(run) {
1049
+ const { coordRoot, payload, owner } = run;
1050
+ try {
1051
+ const childInstanceId = payload?.subagent_id ?? payload?.agent_id ?? owner.instance_id;
1052
+ cleanupSessionEnd(coordRoot, childInstanceId);
1053
+ const { reconcileSessionFinalizationV3 } = await import("../agents/session-finalizer-v3.js");
1054
+ reconcileSessionFinalizationV3(coordRoot, { archive_observations: [] });
1055
+ }
1056
+ catch (err) {
1057
+ logError(coordRoot, err, { phase: "subagent-stop-cleanup" });
1058
+ }
1059
+ }
1060
+ async function handleTurnStarted(run) {
1061
+ const { coordRoot, adapter, behavior, payload, owner, sessionId, debugBase, topLevelSession, stopRemediation, v3EventId, } = run;
1062
+ // Effects: reset per-turn sound rate-limit counters + run presence
1063
+ // detection on the prompt, each gated by the behavior.
1064
+ if (behavior.soundEffects)
1065
+ resetSoundCounters(sessionId);
1066
+ if (behavior.promptPresenceDetection) {
1067
+ const prompt = payload?.raw?.prompt ?? "";
1068
+ if (prompt)
1069
+ detectPresence(prompt);
1070
+ }
1071
+ // Presence: throttled pull of peer machines' presence refs (default 60s
1072
+ // interval; the systemMessage below reads whatever is locally known).
1073
+ try {
1074
+ fetchPresence(coordRoot);
1075
+ }
1076
+ catch (err) {
1077
+ logError(coordRoot, err, { phase: "user-prompt-submit-presence" });
1078
+ }
1079
+ let recovery = null;
1080
+ const continuityState = readContextState(coordRoot, sessionId);
1081
+ if (continuityState?.phase === "checkpointed" &&
1082
+ continuityState.compaction_completed_at !== undefined) {
1065
1083
  try {
1066
- fetchPresence(coordRoot);
1084
+ recovery = prepareContextRecovery(coordRoot, {
1085
+ instanceId: owner.instance_id,
1086
+ sessionId,
1087
+ cwd: payload?.cwd ?? process.cwd(),
1088
+ });
1067
1089
  }
1068
1090
  catch (err) {
1069
- logError(coordRoot, err, { phase: "user-prompt-submit-presence" });
1070
- }
1071
- let recovery = null;
1072
- const continuityState = readContextState(coordRoot, sessionId);
1073
- if (continuityState?.phase === "checkpointed" &&
1074
- continuityState.compaction_completed_at !== undefined) {
1075
- try {
1076
- recovery = prepareContextRecovery(coordRoot, {
1077
- instanceId: owner.instance_id,
1078
- sessionId,
1079
- cwd: payload?.cwd ?? process.cwd(),
1080
- });
1081
- }
1082
- catch (err) {
1083
- logError(coordRoot, err, { phase: "context-recovery-user-prompt" });
1084
- }
1085
- }
1086
- let hostPromptContext = "";
1087
- const hostPromptEnabled = hostPromptContextConfig(coordRoot)?.enabled === true;
1088
- if (hostPromptEnabled && topLevelSession && !stopRemediation) {
1089
- try {
1090
- const { runPromptContext } = await import("./prompt-context/runner.js");
1091
- const result = await runPromptContext({
1092
- coordRoot,
1093
- adapter,
1094
- sessionId,
1095
- turnId: payload?.turn_id ?? v3EventId ?? sessionId,
1096
- cwd: payload?.cwd ?? process.cwd(),
1097
- prompt: payload?.prompt ?? "",
1098
- });
1099
- appendDebug(coordRoot, {
1100
- ...debugBase,
1101
- effect: "prompt-context",
1102
- ...result.audit,
1103
- });
1104
- hostPromptContext = result.context ?? "";
1105
- }
1106
- catch (err) {
1107
- logError(coordRoot, err, { phase: "prompt-context-user-prompt" });
1108
- }
1091
+ logError(coordRoot, err, { phase: "context-recovery-user-prompt" });
1109
1092
  }
1093
+ }
1094
+ let hostPromptContext = "";
1095
+ const hostPromptEnabled = hostPromptContextConfig(coordRoot)?.enabled === true;
1096
+ if (hostPromptEnabled && topLevelSession && !stopRemediation) {
1110
1097
  try {
1111
- const injected = await emitUserPromptSubmitSystemMessage(coordRoot, owner.instance_id, sessionId, adapter, payload?.cwd, recovery?.briefing ?? "", hostPromptContext);
1112
- if (injected && recovery) {
1113
- completeRecoveryInjection(coordRoot, owner.instance_id, sessionId, recovery);
1114
- }
1098
+ const { runPromptContext } = await import("./prompt-context/runner.js");
1099
+ const result = await runPromptContext({
1100
+ coordRoot,
1101
+ adapter,
1102
+ sessionId,
1103
+ turnId: payload?.turn_id ?? v3EventId ?? sessionId,
1104
+ cwd: payload?.cwd ?? process.cwd(),
1105
+ prompt: payload?.prompt ?? "",
1106
+ });
1107
+ appendDebug(coordRoot, {
1108
+ ...debugBase,
1109
+ effect: "prompt-context",
1110
+ ...result.audit,
1111
+ });
1112
+ hostPromptContext = result.context ?? "";
1115
1113
  }
1116
1114
  catch (err) {
1117
- logError(coordRoot, err, { phase: "user-prompt-submit-systemMessage" });
1115
+ logError(coordRoot, err, { phase: "prompt-context-user-prompt" });
1118
1116
  }
1119
1117
  }
1120
- // turn.completed: telemetry, then the stop verdict. The
1121
- // verdict + codex-replay previously lived in the per-adapter shell adapters;
1122
- // agent-hook owns them now. Runs on the normal "stop" event only;
1123
- // "stop-failure" (API error) gets no gate, matching the previous
1124
- // stop vs stop-failure split.
1125
- if (norm.event_type === "turn.completed" && eventName === "stop") {
1126
- // Claude Code session telemetry sync remains an independent side effect.
1127
- if (adapter === "claude-code") {
1128
- runSessionSyncExtension(coordRoot, false);
1129
- }
1130
- // Publish after the canonical turn event has landed so the blob carries
1131
- // current task, activity, and claim state. Re-ensure the relay here because
1132
- // its daemon intentionally exits when a machine has no live sessions.
1133
- try {
1134
- publishPresence(coordRoot);
1135
- ensureRelayDaemon(coordRoot);
1118
+ try {
1119
+ const injected = await emitUserPromptSubmitSystemMessage(coordRoot, owner.instance_id, sessionId, adapter, payload?.cwd, recovery?.briefing ?? "", hostPromptContext);
1120
+ if (injected && recovery) {
1121
+ completeRecoveryInjection(coordRoot, owner.instance_id, sessionId, recovery);
1136
1122
  }
1137
- catch (err) {
1138
- logError(coordRoot, err, { phase: "stop-presence" });
1139
- }
1140
- // Stop verdict (V3 ritual + task/status gate). Direct in-process call: the
1141
- // rule lives in Harnery. agent-hook already emitted this turn.completed
1142
- // with privacy-safe ritual observations, so the evidence is in the stream.
1143
- const verdict = evaluateStopHook(coordRoot, {
1144
- rule: "stop-hook",
1145
- instance_id: owner.instance_id,
1146
- session_id: sessionId,
1147
- adapter,
1148
- stop_hook_active: payload?.stop_hook_active === true,
1149
- status_box_present_strict: data.status_box_present_strict === true,
1150
- bypass: coordEnv("AGENT_COORD_BYPASS_STOP") === "1",
1151
- workflow_child: coordEnv("WORKFLOW_CHILD") === "1",
1152
- });
1153
- if (!verdict.allow) {
1154
- // Backstop for sessions whose evidence can never land (e.g. a headless
1155
- // child whose status command resolves to a different owner): after
1156
- // `cap` consecutive blocked stops in one cycle, allow the stop instead
1157
- // of bouncing the model forever. Cycle boundaries come from the
1158
- // adapter's continuation flag, so adapters without one are unaffected.
1159
- const { recordRemediationBlock } = await import("./remediation-cap.js");
1160
- const remediation = recordRemediationBlock(sessionId, payload?.stop_hook_active === true);
1161
- if (remediation.exceeded) {
1162
- appendDebug(coordRoot, {
1163
- ...debugBase,
1164
- skipped: "stop-remediation-cap-exhausted",
1165
- blocked_rule: verdict.rule,
1166
- blocked_count: remediation.count,
1167
- session_id: sessionId,
1168
- remediation_cycle_anchor: verdict.remediation_cycle_anchor,
1169
- });
1170
- }
1171
- else {
1172
- // Adapter-aware enforcement channel: Claude Code honors exit-2 + stderr
1173
- // as a turn block; Cursor ignores exit codes and re-prompts only via a
1174
- // `followup_message` it auto-submits.
1175
- const { emitStopOutcome } = await import("./adapter/output.js");
1176
- return emitStopOutcome(adapter, { verdict }, coordRoot);
1177
- }
1123
+ }
1124
+ catch (err) {
1125
+ logError(coordRoot, err, { phase: "user-prompt-submit-systemMessage" });
1126
+ }
1127
+ }
1128
+ async function handleStop(run) {
1129
+ const { coordRoot, adapter, behavior, payload, owner, sessionId, data, debugBase } = run;
1130
+ // Session telemetry sync remains an independent side effect.
1131
+ if (behavior.sessionTelemetrySync)
1132
+ runSessionSyncExtension(coordRoot, false);
1133
+ // Publish after the canonical turn event has landed so the blob carries
1134
+ // current task, activity, and claim state. Re-ensure the relay here because
1135
+ // its daemon intentionally exits when a machine has no live sessions.
1136
+ try {
1137
+ publishPresence(coordRoot);
1138
+ ensureRelayDaemon(coordRoot);
1139
+ }
1140
+ catch (err) {
1141
+ logError(coordRoot, err, { phase: "stop-presence" });
1142
+ }
1143
+ // Stop verdict (V3 ritual + task/status gate). Direct in-process call: the
1144
+ // rule lives in Harnery. agent-hook already emitted this turn.completed
1145
+ // with privacy-safe ritual observations, so the evidence is in the stream.
1146
+ const verdict = evaluateStopHook(coordRoot, {
1147
+ rule: "stop-hook",
1148
+ instance_id: owner.instance_id,
1149
+ session_id: sessionId,
1150
+ adapter,
1151
+ stop_hook_active: payload?.stop_hook_active === true,
1152
+ status_box_present_strict: data.status_box_present_strict === true,
1153
+ session_name_observation_unavailable: typeof data.session_name_unavailable_reason === "string",
1154
+ bypass: coordEnv("AGENT_COORD_BYPASS_STOP") === "1",
1155
+ workflow_child: coordEnv("WORKFLOW_CHILD") === "1",
1156
+ });
1157
+ if (!verdict.allow) {
1158
+ // Backstop for sessions whose evidence can never land (e.g. a headless
1159
+ // child whose status command resolves to a different owner): after
1160
+ // `cap` consecutive blocked stops in one cycle, allow the stop instead
1161
+ // of bouncing the model forever. Cycle boundaries come from the
1162
+ // adapter's continuation flag, so adapters without one are unaffected.
1163
+ const { recordRemediationBlock } = await import("./remediation-cap.js");
1164
+ const remediation = recordRemediationBlock(sessionId, payload?.stop_hook_active === true);
1165
+ if (remediation.exceeded) {
1166
+ appendDebug(coordRoot, {
1167
+ ...debugBase,
1168
+ skipped: "stop-remediation-cap-exhausted",
1169
+ blocked_rule: verdict.rule,
1170
+ blocked_count: remediation.count,
1171
+ session_id: sessionId,
1172
+ remediation_cycle_anchor: verdict.remediation_cycle_anchor,
1173
+ });
1178
1174
  }
1179
1175
  else {
1180
- const { clearRemediationCount } = await import("./remediation-cap.js");
1181
- clearRemediationCount(sessionId);
1176
+ // Adapter-aware enforcement channel: Claude Code honors exit-2 + stderr
1177
+ // as a turn block; Cursor ignores exit codes and re-prompts only via a
1178
+ // `followup_message` it auto-submits.
1179
+ const { emitStopOutcome } = await import("./adapter/output.js");
1180
+ return emitStopOutcome(adapter, { verdict }, coordRoot);
1182
1181
  }
1183
- // An explicit end requested from inside this turn cannot be authoritative
1184
- // until the adapter has committed turn.completed above. Reconcile only
1185
- // when such a request exists; any later real work cancels it in the
1186
- // finalizer instead of being terminated underneath the agent.
1187
- try {
1188
- const { hasPendingExplicitSessionEndV3, reconcileSessionFinalizationV3 } = await import("../agents/session-finalizer-v3.js");
1189
- if (hasPendingExplicitSessionEndV3(coordRoot)) {
1190
- reconcileSessionFinalizationV3(coordRoot);
1191
- }
1192
- }
1193
- catch (err) {
1194
- logError(coordRoot, err, { phase: "stop-explicit-session-finalization" });
1195
- }
1196
- if (adapter === "codex")
1197
- scheduleCodexRuntimeContextRetry(coordRoot, sessionId);
1198
1182
  }
1199
- // Phase 7: PreToolUse: heartbeat + pid-map self-heal on every tool call.
1200
- // Adapter-agnostic: both writes have the same shape regardless of who fired.
1201
- // Cursor/Codex bash dispatchers still fire their own G-guard logic, but the
1202
- // heals here keep the agent-coord layer's view of liveness fresh.
1203
- //
1204
- // The heartbeat + pid-map heals are paired by design; they were wired
1205
- // side-by-side in the previous pre-tool-use adapter. The Phase 4-6 refactor
1206
- // preserved the heartbeat half but dropped the pid-map half; the pid-map
1207
- // call was restored here afterward.
1208
- if (norm.event_type === "tool.requested") {
1209
- try {
1210
- // Recorded fork lineage, heal-path flavor. A forked CC conversation
1211
- // never fires its own session.started (SessionStart fires under the
1212
- // PARENT's session id with source=resume, before the fork id is
1213
- // minted; verified 2026-08-05), so the fork's new instance first
1214
- // materializes right here. Gate detection on "no heartbeat yet" so the
1215
- // transcript scan runs once per instance lifetime, not per tool call.
1216
- refreshPidmap(coordRoot, owner.instance_id, adapter, payload?.pid);
1217
- }
1218
- catch (err) {
1219
- logError(coordRoot, err, { phase: "pre-tool-use-heal" });
1220
- }
1221
- // The suggested name is a pending display latch. The set-task call itself
1222
- // runs before a name exists; every later tool waits until the exact block
1223
- // is the first assistant text after the mint result. Cursor supplies
1224
- // agent_message directly; Claude Code and Codex are resolved from their
1225
- // JSONL transcripts. Cursor's current narration can provide positive
1226
- // evidence, but cannot disprove an earlier response; afterAgentResponse
1227
- // stamps that durable sighting. Later commentary must not erase an
1228
- // already-correct display, and unavailable evidence must not deadlock
1229
- // every tool.
1230
- try {
1231
- const displayAllowed = await enforcePendingSessionNameDisplay(coordRoot, owner.instance_id, adapter, payload, generationBoundHeartbeat(readHeartbeat(coordRoot, owner.instance_id), recordedGenerationId));
1232
- if (!displayAllowed)
1233
- return 0;
1234
- }
1235
- catch (err) {
1236
- logError(coordRoot, err, { phase: "pre-tool-use-session-name" });
1237
- }
1238
- // Windows-native Codex + WSL UNC only: block the one cross-shell shape
1239
- // proven to corrupt argument boundaries. Normal WSL argv calls, literal
1240
- // bash -s scripts, native Linux/macOS sessions, and every other adapter
1241
- // pass through. The host instructions name its concrete safe bridge.
1242
- const unsafeShellReason = unsafeCrossShellReason({
1243
- adapter,
1244
- cwd: payload?.cwd,
1245
- toolName: payload?.tool_name,
1246
- toolInput: payload?.tool_input,
1247
- });
1248
- if (unsafeShellReason) {
1249
- const { emitDeny } = await import("./adapter/output.js");
1250
- emitDeny(adapter, unsafeShellReason);
1251
- return 0;
1252
- }
1253
- // G-guard for ALL adapters. Claude Code previously ran this via a
1254
- // pre-tool-use bash adapter (which called `agent-coord verdict --rule=claim`);
1255
- // that adapter is now deleted, so agent-hook owns the deny for every adapter.
1256
- // emitDeny() inside emits the adapter-shaped permission JSON (claude-code +
1257
- // codex use hookSpecificOutput.permissionDecision; cursor uses .permission).
1258
- // apply_patch (codex) parses paths from the patch body and runs verdict
1259
- // per-path; Edit/Write/NotebookEdit resolve a single target. Non-write tools
1260
- // (incl. Agent) yield no targets and pass through with no deny.
1261
- let guardAllowed = true;
1262
- try {
1263
- guardAllowed = await runPreToolUseGuard(coordRoot, owner.instance_id, sessionId, data, adapter);
1264
- }
1265
- catch (err) {
1266
- logError(coordRoot, err, { phase: "pre-tool-use-guard" });
1183
+ else {
1184
+ const { clearRemediationCount } = await import("./remediation-cap.js");
1185
+ clearRemediationCount(sessionId);
1186
+ }
1187
+ // An explicit end requested from inside this turn cannot be authoritative
1188
+ // until the adapter has committed turn.completed above. Reconcile only
1189
+ // when such a request exists; any later real work cancels it in the
1190
+ // finalizer instead of being terminated underneath the agent.
1191
+ try {
1192
+ const { hasPendingExplicitSessionEndV3, reconcileSessionFinalizationV3 } = await import("../agents/session-finalizer-v3.js");
1193
+ if (hasPendingExplicitSessionEndV3(coordRoot)) {
1194
+ reconcileSessionFinalizationV3(coordRoot);
1267
1195
  }
1268
- if (!guardAllowed)
1196
+ }
1197
+ catch (err) {
1198
+ logError(coordRoot, err, { phase: "stop-explicit-session-finalization" });
1199
+ }
1200
+ if (behavior.runtimeContextRetry)
1201
+ scheduleRuntimeContextRetry(coordRoot, adapter, sessionId);
1202
+ return undefined;
1203
+ }
1204
+ async function handleToolRequested(run) {
1205
+ const { coordRoot, adapter, behavior, payload, owner, sessionId, data, recordedGenerationId } = run;
1206
+ try {
1207
+ // Recorded fork lineage, heal-path flavor. A forked CC conversation
1208
+ // never fires its own session.started (SessionStart fires under the
1209
+ // PARENT's session id with source=resume, before the fork id is
1210
+ // minted; verified 2026-08-05), so the fork's new instance first
1211
+ // materializes right here. Gate detection on "no heartbeat yet" so the
1212
+ // transcript scan runs once per instance lifetime, not per tool call.
1213
+ refreshPidmap(coordRoot, owner.instance_id, adapter, payload?.pid);
1214
+ }
1215
+ catch (err) {
1216
+ logError(coordRoot, err, { phase: "pre-tool-use-heal" });
1217
+ }
1218
+ // The suggested name is a pending display latch. The set-task call itself
1219
+ // runs before a name exists; every later tool waits until the exact block
1220
+ // is the first assistant text after the mint result. Cursor supplies
1221
+ // agent_message directly; Claude Code and Codex are resolved from their
1222
+ // JSONL transcripts. Cursor's current narration can provide positive
1223
+ // evidence, but cannot disprove an earlier response; afterAgentResponse
1224
+ // stamps that durable sighting. Later commentary must not erase an
1225
+ // already-correct display, and unavailable evidence must not deadlock
1226
+ // every tool.
1227
+ try {
1228
+ const displayAllowed = await enforcePendingSessionNameDisplay(coordRoot, owner.instance_id, adapter, payload, generationBoundHeartbeat(readHeartbeat(coordRoot, owner.instance_id), recordedGenerationId));
1229
+ if (!displayAllowed)
1269
1230
  return 0;
1270
1231
  }
1271
- if (norm.event_type === "tool.completed" && eventName === "post-tool-use") {
1272
- // Inject only at the successful tool boundary that actually minted or
1273
- // retried a name: the first non-empty set-task, a pending-name set-task
1274
- // retry, or the transition to lifecycle done. The
1275
- // coordination row intentionally remains pending until transcript evidence
1276
- // catches up; reading that latch alone here would re-inject after every
1277
- // later tool and make the agent print the same block repeatedly.
1278
- try {
1279
- const name = sessionNameDisplayPending(readLiveCoordinationRow(coordRoot, owner.instance_id));
1280
- if (name && toolResponseMintedSessionName(payload?.tool_response, name)) {
1281
- // Record the title before asking for it. The suggestion can still
1282
- // change afterwards (an assigned-name rewrite, a lifecycle re-mint, a
1283
- // rebuilt cache), and the agent must not be stranded for displaying
1284
- // exactly what it was handed.
1285
- stampSessionNameRequested(coordRoot, owner.instance_id, name);
1232
+ catch (err) {
1233
+ logError(coordRoot, err, { phase: "pre-tool-use-session-name" });
1234
+ }
1235
+ // Windows-native Codex + WSL UNC only: block the one cross-shell shape
1236
+ // proven to corrupt argument boundaries. Normal WSL argv calls, literal
1237
+ // bash -s scripts, native Linux/macOS sessions, and every other adapter
1238
+ // pass through. The host instructions name its concrete safe bridge.
1239
+ const unsafeShellReason = behavior.unsafeCrossShellReason?.({
1240
+ cwd: payload?.cwd,
1241
+ toolName: payload?.tool_name,
1242
+ toolInput: payload?.tool_input,
1243
+ }) ?? null;
1244
+ if (unsafeShellReason) {
1245
+ const { emitDeny } = await import("./adapter/output.js");
1246
+ emitDeny(adapter, unsafeShellReason);
1247
+ return 0;
1248
+ }
1249
+ const waiterReason = shellWaiterReason(payload?.tool_name, payload?.tool_input);
1250
+ if (waiterReason) {
1251
+ const { emitDeny } = await import("./adapter/output.js");
1252
+ emitDeny(adapter, waiterReason);
1253
+ return 0;
1254
+ }
1255
+ // G-guard for ALL adapters. Claude Code previously ran this via a
1256
+ // pre-tool-use bash adapter (which called `agent-coord verdict --rule=claim`);
1257
+ // that adapter is now deleted, so agent-hook owns the deny for every adapter.
1258
+ // emitDeny() inside emits the adapter-shaped permission JSON (claude-code +
1259
+ // codex use hookSpecificOutput.permissionDecision; cursor uses .permission).
1260
+ // apply_patch (codex) parses paths from the patch body and runs verdict
1261
+ // per-path; Edit/Write/NotebookEdit resolve a single target. Non-write tools
1262
+ // (incl. Agent) yield no targets and pass through with no deny.
1263
+ let guardAllowed = true;
1264
+ try {
1265
+ guardAllowed = await runPreToolUseGuard(coordRoot, owner.instance_id, sessionId, data, adapter);
1266
+ }
1267
+ catch (err) {
1268
+ logError(coordRoot, err, { phase: "pre-tool-use-guard" });
1269
+ }
1270
+ if (!guardAllowed)
1271
+ return 0;
1272
+ return undefined;
1273
+ }
1274
+ async function handlePostToolUse(run) {
1275
+ const { coordRoot, adapter, eventName, payload, owner } = run;
1276
+ // Inject the full instruction only at the successful tool boundary that
1277
+ // actually minted or retried a name: the first non-empty set-task, a
1278
+ // pending-name set-task retry, or the transition to lifecycle done. The
1279
+ // coordination row intentionally remains pending until transcript evidence
1280
+ // catches up, so the latch alone cannot drive injection: that would print
1281
+ // the same block after every later tool.
1282
+ //
1283
+ // Between those two extremes sits a bounded reminder. Since ADR 0182 the
1284
+ // PreToolUse gate allows a tool when transcript evidence is unavailable
1285
+ // and says so only on stderr, which the model never reads. A model that
1286
+ // skipped the block after the mint was therefore never asked again
1287
+ // (ADR 0183). While the requested title is still pending, ordinary tool
1288
+ // results carry an idempotent reminder up to a fixed count; coordination
1289
+ // chores (set-task, status, suggest-name) are skipped because a mint or
1290
+ // retry response already carries the full instruction and an end-turn
1291
+ // status is the wrong place to ask.
1292
+ try {
1293
+ const row = readLiveCoordinationRow(coordRoot, owner.instance_id);
1294
+ const name = sessionNameDisplayPending(row);
1295
+ if (name && toolResponseMintedSessionName(payload?.tool_response, name)) {
1296
+ // Record the title before asking for it. The suggestion can still
1297
+ // change afterwards (an assigned-name rewrite, a lifecycle re-mint, a
1298
+ // rebuilt cache), and the agent must not be stranded for displaying
1299
+ // exactly what it was handed.
1300
+ stampSessionNameRequested(coordRoot, owner.instance_id, name);
1301
+ const { emitContext } = await import("./adapter/output.js");
1302
+ emitContext(adapter, "PostToolUse", sessionNameDisplayInstruction(name));
1303
+ }
1304
+ else if (name) {
1305
+ const due = sessionNameDisplayReminderDue(row);
1306
+ const command = extractBashCommand(payload?.tool_name, payload?.tool_input);
1307
+ if (due && !isSessionNameRemediationCommand(command, resolveBinName(coordRoot))) {
1308
+ stampSessionNameReminded(coordRoot, owner.instance_id, due);
1309
+ appendDebug(coordRoot, {
1310
+ ts: new Date().toISOString(),
1311
+ event_name: eventName,
1312
+ adapter,
1313
+ instance_id: owner.instance_id,
1314
+ effect: "session-name-display-reminded",
1315
+ session_name_pending: due,
1316
+ reminders: (row?.session_name_display_reminders ?? 0) + 1,
1317
+ });
1286
1318
  const { emitContext } = await import("./adapter/output.js");
1287
- emitContext(adapter, "PostToolUse", sessionNameDisplayInstruction(name));
1319
+ emitContext(adapter, "PostToolUse", sessionNameDisplayReminder(due));
1288
1320
  }
1289
1321
  }
1290
- catch (err) {
1291
- logError(coordRoot, err, { phase: "post-tool-use-session-name" });
1292
- }
1293
1322
  }
1294
- // Phase 7: PostToolUseFailure: release claim on failed Edit (the file
1295
- // never landed; the claim is stale). Adapter-agnostic.
1296
- if (norm.event_type === "tool.completed" && eventName === "post-tool-use-failure") {
1297
- try {
1298
- releaseClaimOnFailure(coordRoot, owner.instance_id, data, payload?.raw);
1299
- }
1300
- catch (err) {
1301
- logError(coordRoot, err, { phase: "post-tool-use-failure-release" });
1302
- }
1323
+ catch (err) {
1324
+ logError(coordRoot, err, { phase: "post-tool-use-session-name" });
1325
+ }
1326
+ }
1327
+ function handlePostToolUseFailure(run) {
1328
+ const { coordRoot, payload, owner, data } = run;
1329
+ try {
1330
+ releaseClaimOnFailure(coordRoot, owner.instance_id, data, payload?.raw);
1331
+ }
1332
+ catch (err) {
1333
+ logError(coordRoot, err, { phase: "post-tool-use-failure-release" });
1334
+ }
1335
+ }
1336
+ async function handleCompletedResponse(run) {
1337
+ const { coordRoot, adapter, eventName, raw, debugBase, ledgerRoute, hookStartedAt, hookClock } = run;
1338
+ const payload = parsePayload(raw, adapter);
1339
+ const owner = resolveOwner({ payload: payload?.raw ?? null, coordRoot });
1340
+ if (!owner) {
1341
+ appendDebug(coordRoot, { ...debugBase, skipped: "no-owner-resolved" });
1342
+ return 0;
1303
1343
  }
1344
+ const row = readLiveCoordinationRow(coordRoot, owner.instance_id);
1345
+ const name = sessionNameDisplayPending(row);
1346
+ const text = typeof payload?.raw.text === "string" ? payload.raw.text : "";
1347
+ const sighting = matchSessionNameDisplay(row, text);
1348
+ if (sighting) {
1349
+ stampSessionNameSeen(coordRoot, owner.instance_id, sighting.pending);
1350
+ appendDebug(coordRoot, {
1351
+ ...debugBase,
1352
+ effect: "session-name-display-stamped",
1353
+ session_name_pending: sighting.pending,
1354
+ ...(sighting.displayed === sighting.pending
1355
+ ? {}
1356
+ : { session_name_displayed: sighting.displayed, session_name_drift: true }),
1357
+ });
1358
+ }
1359
+ else if (!name) {
1360
+ // Nothing was owed. Distinct from a miss: conflating the two is what made
1361
+ // a latched Cursor session undiagnosable from this log.
1362
+ appendDebug(coordRoot, { ...debugBase, skipped: "no-pending-session-name" });
1363
+ }
1364
+ else {
1365
+ appendDebug(coordRoot, {
1366
+ ...debugBase,
1367
+ skipped: "session-name-block-absent",
1368
+ session_name_pending: name,
1369
+ reply_bytes: text.length,
1370
+ reply_leads_with_fence: /^\s*`{3,}/.test(text),
1371
+ });
1372
+ }
1373
+ if (ledgerRoute.state === "blocked") {
1374
+ appendDebug(coordRoot, {
1375
+ ...debugBase,
1376
+ skipped: "v3-control-blocked",
1377
+ reason: ledgerRoute.reason,
1378
+ owner_source: owner.source,
1379
+ });
1380
+ return 0;
1381
+ }
1382
+ const statusBoxPresentStrict = scanAssistantStatusBoxPresent(undefined, text);
1383
+ // The hook parser needs the completed reply long enough to scan it, but
1384
+ // durable hook intake must never retain that body. Keep every other native
1385
+ // field so turn correlation remains available to the producer.
1386
+ const { text: _completedReply, ...privacySafeRaw } = payload?.raw ?? {};
1387
+ const privacySafePayload = payload ? { ...payload, raw: privacySafeRaw } : null;
1388
+ const v3Result = recordLiveHookSignalV3({
1389
+ coordRoot,
1390
+ route: ledgerRoute,
1391
+ eventName,
1392
+ payload: privacySafePayload,
1393
+ adapter,
1394
+ instanceId: owner.instance_id,
1395
+ hook_name: eventName,
1396
+ hook_duration_ms: Math.max(0, Math.floor(performance.now() - hookStartedAt)),
1397
+ monotonic_ns: hookClock.monotonic_ns,
1398
+ turn_ritual: {
1399
+ status_box_present: statusBoxPresentStrict,
1400
+ status_box_present_strict: statusBoxPresentStrict,
1401
+ session_name_required: name !== undefined,
1402
+ session_name_present: name ? assistantTextStartsWithSessionNameBlock(text, name) : false,
1403
+ },
1404
+ });
1405
+ observeHookDebug(hookHealthState, { event_v3_state: v3Result.state });
1406
+ appendDebug(coordRoot, {
1407
+ ...debugBase,
1408
+ effect: "cursor-response-ritual-observed",
1409
+ owner_source: owner.source,
1410
+ event_v3_state: v3Result.state,
1411
+ status_box_present_strict: statusBoxPresentStrict,
1412
+ ...(v3Result.state === "observed"
1413
+ ? {
1414
+ generation_id: v3Result.generation_id,
1415
+ turn_id: v3Result.turn_id,
1416
+ response_observed_at: v3Result.observed_at,
1417
+ }
1418
+ : {}),
1419
+ });
1304
1420
  return 0;
1305
1421
  }
1306
- function scheduleCodexRuntimeContextRetry(coordRoot, nativeSessionId) {
1422
+ function scheduleRuntimeContextRetry(coordRoot, adapter, nativeSessionId) {
1307
1423
  const modulePath = process.argv[1];
1308
1424
  if (!modulePath)
1309
1425
  return;
1310
1426
  try {
1311
- const child = spawn(process.execPath, [modulePath, "runtime-context-retry", "--adapter", "codex", "--session-id", nativeSessionId], {
1427
+ const child = spawn(process.execPath, [modulePath, "runtime-context-retry", "--adapter", adapter, "--session-id", nativeSessionId], {
1312
1428
  cwd: coordRoot,
1313
1429
  detached: true,
1314
1430
  stdio: "ignore",
@@ -1322,7 +1438,7 @@ function scheduleCodexRuntimeContextRetry(coordRoot, nativeSessionId) {
1322
1438
  }
1323
1439
  async function runRuntimeContextRetryWorker(coordRoot, adapter, extra) {
1324
1440
  const nativeSessionId = extraArg(extra, "--session-id");
1325
- if (adapter !== "codex" || !nativeSessionId)
1441
+ if (!adapter || !adapterBehavior(adapter).runtimeContextRetry || !nativeSessionId)
1326
1442
  return 0;
1327
1443
  for (const [index, delayMs] of [400, 1_000].entries()) {
1328
1444
  await new Promise((resolve) => setTimeout(resolve, delayMs));
@@ -1340,6 +1456,18 @@ async function runRuntimeContextRetryWorker(coordRoot, adapter, extra) {
1340
1456
  }
1341
1457
  return 0;
1342
1458
  }
1459
+ function reportSessionNameObservationUnavailable(coordRoot, instanceId, adapter, eventName, reason) {
1460
+ appendDebug(coordRoot, {
1461
+ ts: new Date().toISOString(),
1462
+ event_name: eventName,
1463
+ adapter,
1464
+ instance_id: instanceId,
1465
+ effect: "session-name-observation-unavailable",
1466
+ reason,
1467
+ action: "fail-open",
1468
+ });
1469
+ process.stderr.write(`Harnery: session-name observation unavailable (${reason}); allowing this ${eventName} without display enforcement. The name remains pending.\n`);
1470
+ }
1343
1471
  async function enforcePendingSessionNameDisplay(coordRoot, instanceId, adapter, payload, coordination) {
1344
1472
  const name = sessionNameDisplayPending(coordination);
1345
1473
  if (!name)
@@ -1353,11 +1481,7 @@ async function enforcePendingSessionNameDisplay(coordRoot, instanceId, adapter,
1353
1481
  return true;
1354
1482
  }
1355
1483
  if (inspection.state === "unavailable") {
1356
- // PostToolUse already delivered the exact block for this mint. An
1357
- // unavailable transcript cannot become evidence, and repeating a generic
1358
- // reminder on every tool creates an unbounded Codex loop. Keep the latch
1359
- // honestly pending and fail open without another instruction. A later
1360
- // readable transcript can still verify it.
1484
+ reportSessionNameObservationUnavailable(coordRoot, instanceId, adapter, "pre-tool-use", inspection.reason);
1361
1485
  return true;
1362
1486
  }
1363
1487
  // Record the refusal. Without this the gate was the one enforcement path
@@ -1384,19 +1508,18 @@ async function enforcePendingSessionNameDisplay(coordRoot, instanceId, adapter,
1384
1508
  * other opening text for every accepted title.
1385
1509
  */
1386
1510
  function inspectDisplayEvidence(instanceId, adapter, payload, coordination) {
1387
- if (adapter === "cursor" && payload?.agent_message !== undefined) {
1388
- return matchSessionNameDisplay(coordination, payload.agent_message)
1511
+ const behavior = adapterBehavior(adapter);
1512
+ const inlineText = behavior.inlineAssistantText?.(payload);
1513
+ if (inlineText !== undefined) {
1514
+ return matchSessionNameDisplay(coordination, inlineText)
1389
1515
  ? { state: "present" }
1390
1516
  : { state: "unavailable", reason: "transcript_not_ready" };
1391
1517
  }
1392
- // Codex hook payloads omit transcript_path on every event, which left this
1393
- // inspection permanently unavailable and the latch never stamped. Discover
1394
- // the rollout by session id; the scan only runs while a name is pending, so
1395
- // the latch closes after one success.
1396
- const transcriptPath = payload?.transcript_path ??
1397
- (adapter === "codex"
1398
- ? discoverCodexSessionTranscript(payload?.session_id ?? instanceId)
1399
- : undefined);
1518
+ // Some adapters omit transcript_path on every event, which left this
1519
+ // inspection permanently unavailable and the latch never stamped. The
1520
+ // behavior may discover the transcript by session id; the scan only runs
1521
+ // while a name is pending, so the latch closes after one success.
1522
+ const transcriptPath = payload?.transcript_path ?? behavior.discoverTranscript?.(payload?.session_id ?? instanceId);
1400
1523
  let verdict = { state: "absent" };
1401
1524
  for (const candidate of sessionNameDisplayAcceptedNames(coordination)) {
1402
1525
  const inspection = inspectSessionNameDisplayImmediately(transcriptPath, candidate, assistantTextStartsWithSessionNameBlock);
@@ -1671,7 +1794,7 @@ function releaseClaimOnFailure(coordRoot, instanceId, data, rawPayload) {
1671
1794
  coordRoot,
1672
1795
  owner: instanceId,
1673
1796
  nativeSessionId: before?.session_id ?? instanceId,
1674
- adapter: adapterFromPlatform(before?.platform),
1797
+ adapter: sharedAdapterFromPlatform(before?.platform, { context: "hook-claim-release" }),
1675
1798
  operation: "released",
1676
1799
  path: canonical,
1677
1800
  access: "write",
@@ -1707,22 +1830,18 @@ function cleanupSessionEnd(coordRoot, instanceId) {
1707
1830
  }
1708
1831
  }
1709
1832
  async function emitUserPromptSubmitSystemMessage(coordRoot, instanceId, sessionId, adapter, workspaceCwd, recoveryBriefing = "", hostPromptContext = "") {
1833
+ const behavior = adapterBehavior(adapter);
1710
1834
  const { renderPromptContext } = await import("../agents/render/prompt-context.js");
1711
1835
  let additionalContext = renderPromptContext({
1712
1836
  coordRoot,
1713
1837
  instanceId,
1714
1838
  sessionId,
1715
1839
  sessionNameNudge: true,
1716
- taskNudge: adapter === "cursor" || adapter === "codex",
1717
- hostPromptReminder: adapter === "codex" || adapter === "cursor",
1718
- statusFooterNudge: adapter === "codex",
1719
- turnRitualNudge: adapter === "claude-code" ? adapter : undefined,
1840
+ ...behavior.promptContextNudges,
1720
1841
  }).trim();
1721
- if (adapter === "codex") {
1722
- const fileLinkContext = renderCodexWslFileLinkContext(coordRoot, workspaceCwd);
1723
- if (fileLinkContext) {
1724
- additionalContext = [additionalContext, fileLinkContext].filter(Boolean).join("\n\n");
1725
- }
1842
+ const fileLinkContext = behavior.fileLinkContext?.(coordRoot, workspaceCwd) ?? "";
1843
+ if (fileLinkContext) {
1844
+ additionalContext = [additionalContext, fileLinkContext].filter(Boolean).join("\n\n");
1726
1845
  }
1727
1846
  if (recoveryBriefing) {
1728
1847
  additionalContext = [additionalContext, recoveryBriefing].filter(Boolean).join("\n\n");
@@ -1764,18 +1883,10 @@ async function emitSubagentStartContext(coordRoot, instanceId, sessionId, data,
1764
1883
  emitContext(adapter, "SubagentStart", combined);
1765
1884
  }
1766
1885
  function adapterPlatform(adapter) {
1767
- if (adapter === "claude-code")
1768
- return "claude-code";
1769
1886
  return adapter;
1770
1887
  }
1771
- function adapterFromPlatform(platform) {
1772
- if (platform === "cursor")
1773
- return "cursor";
1774
- if (platform === "codex")
1775
- return "codex";
1776
- return "claude-code";
1777
- }
1778
1888
  async function emitSessionStartSystemMessage(coordRoot, instanceId, sessionId, emittedData, adapter, recoveryBriefing = "", backupCue = "") {
1889
+ const behavior = adapterBehavior(adapter);
1779
1890
  const workflowChild = coordEnv("WORKFLOW_CHILD") === "1";
1780
1891
  let additionalContext = "";
1781
1892
  // Opportunistic reconciliation makes normal session starts a failsafe for
@@ -1795,14 +1906,14 @@ async function emitSessionStartSystemMessage(coordRoot, instanceId, sessionId, e
1795
1906
  instanceId,
1796
1907
  sessionId,
1797
1908
  agentName: agentName || undefined,
1798
- platformLabel: adapter === "claude-code" ? undefined : adapter === "cursor" ? "Cursor" : "Codex",
1909
+ platformLabel: behavior.sessionStartPlatformLabel,
1799
1910
  }).trim();
1800
1911
  }
1801
- // Effect (claude-code): merge the journal recovery cue into the session-start
1802
- // context. Was a standalone additionalContext emission from the previous
1803
- // journal-on-start adapter; now that agent-hook is the single SessionStart
1804
- // entry, it folds in here.
1805
- if (adapter === "claude-code" && !workflowChild) {
1912
+ // Effect (journal-maintaining adapters): merge the journal recovery cue into
1913
+ // the session-start context. Was a standalone additionalContext emission
1914
+ // from the previous journal-on-start adapter; now that agent-hook is the
1915
+ // single SessionStart entry, it folds in here.
1916
+ if (behavior.journalMaintenance && !workflowChild) {
1806
1917
  const cue = journalRecoveryCue(coordRoot);
1807
1918
  if (cue)
1808
1919
  additionalContext = [additionalContext, cue].filter(Boolean).join("\n\n");
@@ -1813,19 +1924,14 @@ async function emitSessionStartSystemMessage(coordRoot, instanceId, sessionId, e
1813
1924
  if (backupCue && !workflowChild) {
1814
1925
  additionalContext = [additionalContext, backupCue].filter(Boolean).join("\n\n");
1815
1926
  }
1816
- if (adapter === "codex" && !workflowChild && isWslUncPath(emittedData.cwd)) {
1817
- const fileLinkContext = renderCodexWslFileLinkContext(coordRoot, emittedData.cwd);
1927
+ if (!workflowChild && behavior.bridgeFor(emittedData.cwd)) {
1928
+ const fileLinkContext = behavior.fileLinkContext?.(coordRoot, emittedData.cwd) ?? "";
1818
1929
  if (fileLinkContext) {
1819
1930
  additionalContext = [additionalContext, fileLinkContext].filter(Boolean).join("\n\n");
1820
1931
  }
1821
- const bridge = inspectCodexWslBridge(process.env, { expected: true });
1822
- if (bridge && !bridge.ok) {
1823
- additionalContext = [
1824
- additionalContext,
1825
- `Harnery hybrid warning: ${bridge.detail}. Run \`${resolveBinName(coordRoot)} doctor\` for the repair hint.`,
1826
- ]
1827
- .filter(Boolean)
1828
- .join("\n\n");
1932
+ const warning = behavior.bridgeWarning?.(coordRoot, emittedData.cwd);
1933
+ if (warning) {
1934
+ additionalContext = [additionalContext, warning].filter(Boolean).join("\n\n");
1829
1935
  }
1830
1936
  }
1831
1937
  const { emitContext } = await import("./adapter/output.js");