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
@@ -22,7 +22,7 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
22
22
  import { dirname, join } from "node:path";
23
23
  import { coordEnv } from "../../lib/env.ts";
24
24
  import { buildInstructionBundle } from "../../lib/instructions/bundle.ts";
25
- import type { Adapter } from "../adapter.ts";
25
+ import { type Adapter, adapterFromPlatform as sharedAdapterFromPlatform } from "../adapter.ts";
26
26
  import { resolveCoordRoot } from "../agents/coord-client.ts";
27
27
  import {
28
28
  type ClaimFinalizationDecision,
@@ -43,12 +43,15 @@ import {
43
43
  sessionNameDisplayInstruction,
44
44
  sessionNameDisplayPending,
45
45
  sessionNameDisplayRecoveryInstruction,
46
+ sessionNameDisplayReminder,
47
+ sessionNameDisplayReminderDue,
46
48
  toolResponseMintedSessionName,
47
49
  } from "../agents/session-name-display.ts";
48
50
  import type { Heartbeat } from "../agents/state/heartbeat-reader.ts";
49
51
  import {
50
52
  readHeartbeat,
51
53
  setAssignedNameCache,
54
+ stampSessionNameReminded,
52
55
  stampSessionNameRequested,
53
56
  stampSessionNameSeen,
54
57
  } from "../agents/state/heartbeat-writer.ts";
@@ -80,10 +83,12 @@ import {
80
83
  recordLiveHookSignalV3,
81
84
  resolveLiveEventLedgerRouteV3,
82
85
  } from "../events/v3/live-routing.ts";
86
+ import type { TurnRitualEvidenceV3 } from "../events/v3/producers/hook-base.ts";
83
87
  import { captureSpanClockV3 } from "../events/v3/span-state.ts";
84
88
  import { ensureRelayDaemon, fetchPresence, publishPresence } from "../presence/index.ts";
85
89
  import { closeProcessLoggers, legacyLogFields, processLogger } from "../storage/logger.ts";
86
90
  import { stableScopeId } from "../workflow/scope-id.ts";
91
+ import { type AdapterBehavior, adapterBehavior } from "./adapter/behaviors/index.ts";
87
92
  import { detectAdapter, shouldSkipHookAdapter } from "./adapter/detect.ts";
88
93
  import {
89
94
  extractBashCommand,
@@ -93,14 +98,7 @@ import {
93
98
  type ParsedPayload,
94
99
  parsePayload,
95
100
  } from "./adapter/parse.ts";
96
- import { discoverCodexSessionTranscript } from "./adapter/runtime-telemetry.ts";
97
101
  import { scheduleBackupSnapshot } from "./backup-schedule.ts";
98
- import {
99
- codexWslFileLinkTelemetry,
100
- inspectCodexWslBridge,
101
- isWslUncPath,
102
- renderCodexWslFileLinkContext,
103
- } from "./codex-wsl-bridge.ts";
104
102
  import {
105
103
  captureImages,
106
104
  detectPresence,
@@ -130,12 +128,11 @@ import type { SessionNameDisplayInspection } from "./resolve/transcript.ts";
130
128
  import {
131
129
  inspectSessionNameDisplayImmediately,
132
130
  scanAssistantStatusBoxPresent,
133
- scanSessionNameDisplayedImmediately,
134
131
  scanStatusBoxPresent,
135
132
  scanTranscriptRuntime,
136
133
  } from "./resolve/transcript.ts";
137
134
  import { sessionNamePresence } from "./session-name-presence.ts";
138
- import { unsafeCrossShellReason } from "./unsafe-cross-shell.ts";
135
+ import { shellWaiterReason } from "./shell-waiter.ts";
139
136
 
140
137
  interface Argv {
141
138
  eventName: string | null;
@@ -341,12 +338,9 @@ function buildEventData(
341
338
  const p = ctx.payload;
342
339
  switch (eventType) {
343
340
  case "session.started": {
344
- const adapterPlatform =
345
- ctx.adapter === "claude-code"
346
- ? "claude-code"
347
- : ctx.adapter === "cursor"
348
- ? "cursor"
349
- : "codex";
341
+ // The platform label is the adapter id itself; enumerating and defaulting
342
+ // to codex silently mislabels any adapter not in the list.
343
+ const adapterPlatform = ctx.adapter;
350
344
  // Recorded fork lineage is NOT detected here. On claude-code a fork
351
345
  // never fires its own session.started — SessionStart fires under the
352
346
  // PARENT's session id (source=resume) before the fork id is minted
@@ -433,9 +427,11 @@ function buildEventData(
433
427
  case "turn.completed": {
434
428
  const lastAssistantMessage = (p?.raw.last_assistant_message as string | undefined) ?? "";
435
429
  const fileLinkTelemetry =
436
- ctx.adapter === "codex"
437
- ? codexWslFileLinkTelemetry(ctx.coordRoot, p?.cwd, lastAssistantMessage)
438
- : null;
430
+ adapterBehavior(ctx.adapter).fileLinkTelemetry?.(
431
+ ctx.coordRoot,
432
+ p?.cwd,
433
+ lastAssistantMessage,
434
+ ) ?? null;
439
435
  return {
440
436
  // Backfill the model for adapters that omit it at session.started
441
437
  // (Claude Code). The transcript is populated with assistant turns by
@@ -471,13 +467,12 @@ function buildEventData(
471
467
  // exists (the stop-hook.session_name verdict reads it per turn); the
472
468
  // transcript scan itself stops once the name has been sighted.
473
469
  ...sessionNamePresence(ctx.coordRoot, ctx.instanceId, (name) =>
474
- scanSessionNameDisplayedImmediately(
475
- // Codex stops carry no transcript_path; discover the rollout by
476
- // session id so the naming ritual can verify and stamp there too.
470
+ inspectSessionNameDisplayImmediately(
471
+ // Some adapters omit transcript_path on stop; the behavior may
472
+ // discover the transcript by session id so the naming ritual can
473
+ // verify and stamp there too.
477
474
  p?.transcript_path ??
478
- (ctx.adapter === "codex"
479
- ? discoverCodexSessionTranscript(p?.session_id ?? ctx.instanceId)
480
- : undefined),
475
+ adapterBehavior(ctx.adapter).discoverTranscript?.(p?.session_id ?? ctx.instanceId),
481
476
  name,
482
477
  assistantTextStartsWithSessionNameBlock,
483
478
  ),
@@ -622,12 +617,52 @@ function numberField(value: unknown): number | undefined {
622
617
  return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
623
618
  }
624
619
 
620
+ /** Everything one hook invocation resolved before its event handlers run. */
621
+ interface HookRun {
622
+ coordRoot: string;
623
+ adapter: Adapter;
624
+ behavior: AdapterBehavior;
625
+ eventName: string;
626
+ norm: NonNullable<ReturnType<typeof normalizeEventName>>;
627
+ payload: ParsedPayload | null;
628
+ raw: string;
629
+ owner: NonNullable<ReturnType<typeof resolveOwner>>;
630
+ sessionId: string;
631
+ data: Record<string, unknown>;
632
+ debugBase: Record<string, unknown>;
633
+ ledgerRoute: Exclude<ReturnType<typeof resolveLiveEventLedgerRouteV3>, { state: "blocked" }>;
634
+ hookStartedAt: number;
635
+ hookClock: ReturnType<typeof captureSpanClockV3>;
636
+ priorCoordination: Heartbeat | null;
637
+ topLevelSession: boolean;
638
+ stopRemediation: boolean;
639
+ bridge: ReturnType<AdapterBehavior["bridgeFor"]>;
640
+ turnRitual: TurnRitualEvidenceV3 | undefined;
641
+ /** Set by recordSignal once the canonical V3 event has landed. */
642
+ v3Result?: ReturnType<typeof recordLiveHookSignalV3>;
643
+ v3EventId?: string;
644
+ recordedGenerationId?: `gen_${string}`;
645
+ }
646
+
647
+ /** The completed-reply ritual path runs before event normalization and owner resolution. */
648
+ interface CompletedResponseRun {
649
+ coordRoot: string;
650
+ adapter: Adapter;
651
+ eventName: string;
652
+ raw: string;
653
+ debugBase: Record<string, unknown>;
654
+ ledgerRoute: ReturnType<typeof resolveLiveEventLedgerRouteV3>;
655
+ hookStartedAt: number;
656
+ hookClock: ReturnType<typeof captureSpanClockV3>;
657
+ }
658
+
625
659
  async function main(): Promise<number> {
626
660
  const hookStartedAt = performance.now();
627
661
  const hookStartedRss = process.memoryUsage().rss;
628
662
  const hookClock = captureSpanClockV3();
629
663
  const { eventName, extra } = parseArgv(process.argv.slice(2));
630
664
  const adapter = detectAdapter(process.argv.slice(2));
665
+ const behavior = adapter ? adapterBehavior(adapter) : null;
631
666
  const raw = await readStdin();
632
667
  hookHealthState = beginHookHealth({
633
668
  started_at_ms: hookStartedAt,
@@ -667,8 +702,8 @@ async function main(): Promise<number> {
667
702
  // Kill-switch-INDEPENDENT effects: notification sounds fire BEFORE the
668
703
  // HARNERY_AGENT_COORD_OFF gate so audible feedback survives incident-triage
669
704
  // bypass: sound playback happens before the kill-switch bailout.
670
- // Claude-Code-only; stop-failure → error, sub-agent-start → subagent-start.
671
- if (adapter === "claude-code" && eventName) {
705
+ // Behavior-gated; stop-failure → error, sub-agent-start → subagent-start.
706
+ if (behavior?.soundEffects && eventName) {
672
707
  const s = soundForEvent(eventName);
673
708
  if (s) {
674
709
  const repoRoot = resolveCoordRoot(process.cwd());
@@ -724,7 +759,7 @@ async function main(): Promise<number> {
724
759
  ppid: process.ppid,
725
760
  };
726
761
 
727
- if (!eventName || !adapter) {
762
+ if (!eventName || !adapter || !behavior) {
728
763
  appendDebug(coordRoot, { ...debugBase, skipped: "missing-event-or-adapter" });
729
764
  return 0;
730
765
  }
@@ -736,88 +771,17 @@ async function main(): Promise<number> {
736
771
  // assistant text. Preserve only privacy-safe ritual booleans in the V3
737
772
  // producer's open-turn state; Stop consumes them into the authoritative
738
773
  // turn.completed event. The response body never enters V3 or heartbeat.
739
- if (adapter === "cursor" && eventName === "after-agent-response") {
740
- const payload = parsePayload(raw, adapter);
741
- const owner = resolveOwner({ payload: payload?.raw ?? null, coordRoot });
742
- if (!owner) {
743
- appendDebug(coordRoot, { ...debugBase, skipped: "no-owner-resolved" });
744
- return 0;
745
- }
746
- const row = readLiveCoordinationRow(coordRoot, owner.instance_id);
747
- const name = sessionNameDisplayPending(row);
748
- const text = typeof payload?.raw.text === "string" ? payload.raw.text : "";
749
- const sighting = matchSessionNameDisplay(row, text);
750
- if (sighting) {
751
- stampSessionNameSeen(coordRoot, owner.instance_id, sighting.pending);
752
- appendDebug(coordRoot, {
753
- ...debugBase,
754
- effect: "session-name-display-stamped",
755
- session_name_pending: sighting.pending,
756
- ...(sighting.displayed === sighting.pending
757
- ? {}
758
- : { session_name_displayed: sighting.displayed, session_name_drift: true }),
759
- });
760
- } else if (!name) {
761
- // Nothing was owed. Distinct from a miss: conflating the two is what made
762
- // a latched Cursor session undiagnosable from this log.
763
- appendDebug(coordRoot, { ...debugBase, skipped: "no-pending-session-name" });
764
- } else {
765
- appendDebug(coordRoot, {
766
- ...debugBase,
767
- skipped: "session-name-block-absent",
768
- session_name_pending: name,
769
- reply_bytes: text.length,
770
- reply_leads_with_fence: /^\s*`{3,}/.test(text),
771
- });
772
- }
773
- if (ledgerRoute.state === "blocked") {
774
- appendDebug(coordRoot, {
775
- ...debugBase,
776
- skipped: "v3-control-blocked",
777
- reason: ledgerRoute.reason,
778
- owner_source: owner.source,
779
- });
780
- return 0;
781
- }
782
- const statusBoxPresentStrict = scanAssistantStatusBoxPresent(undefined, text);
783
- // The hook parser needs the completed reply long enough to scan it, but
784
- // durable hook intake must never retain that body. Keep every other native
785
- // field so turn correlation remains available to the producer.
786
- const { text: _completedReply, ...privacySafeRaw } = payload?.raw ?? {};
787
- const privacySafePayload = payload ? { ...payload, raw: privacySafeRaw } : null;
788
- const v3Result = recordLiveHookSignalV3({
774
+ if (behavior && eventName && eventName === behavior.completedResponseEvent) {
775
+ return handleCompletedResponse({
789
776
  coordRoot,
790
- route: ledgerRoute,
791
- eventName,
792
- payload: privacySafePayload,
793
777
  adapter,
794
- instanceId: owner.instance_id,
795
- hook_name: eventName,
796
- hook_duration_ms: Math.max(0, Math.floor(performance.now() - hookStartedAt)),
797
- monotonic_ns: hookClock.monotonic_ns,
798
- turn_ritual: {
799
- status_box_present: statusBoxPresentStrict,
800
- status_box_present_strict: statusBoxPresentStrict,
801
- session_name_required: name !== undefined,
802
- session_name_present: name ? assistantTextStartsWithSessionNameBlock(text, name) : false,
803
- },
804
- });
805
- observeHookDebug(hookHealthState, { event_v3_state: v3Result.state });
806
- appendDebug(coordRoot, {
807
- ...debugBase,
808
- effect: "cursor-response-ritual-observed",
809
- owner_source: owner.source,
810
- event_v3_state: v3Result.state,
811
- ...(v3Result.state === "observed"
812
- ? {
813
- generation_id: v3Result.generation_id,
814
- turn_id: v3Result.turn_id,
815
- response_observed_at: v3Result.observed_at,
816
- status_box_present_strict: statusBoxPresentStrict,
817
- }
818
- : {}),
778
+ eventName,
779
+ raw,
780
+ debugBase,
781
+ ledgerRoute,
782
+ hookStartedAt,
783
+ hookClock,
819
784
  });
820
- return 0;
821
785
  }
822
786
 
823
787
  const norm = normalizeEventName(eventName);
@@ -827,6 +791,7 @@ async function main(): Promise<number> {
827
791
  }
828
792
 
829
793
  const payload = parsePayload(raw, adapter);
794
+ const bridge = behavior.bridgeFor(payload?.cwd);
830
795
  const owner = resolveOwner({
831
796
  payload: payload?.raw ?? null,
832
797
  coordRoot,
@@ -874,8 +839,20 @@ async function main(): Promise<number> {
874
839
  status_box_present_strict: data.status_box_present_strict === true,
875
840
  session_name_required: typeof data.session_name_present_for === "string",
876
841
  session_name_present: data.session_name_present === true,
842
+ ...(typeof data.session_name_unavailable_reason === "string"
843
+ ? { session_name_unavailable_reason: data.session_name_unavailable_reason }
844
+ : {}),
877
845
  }
878
846
  : undefined;
847
+ if (typeof data.session_name_unavailable_reason === "string") {
848
+ reportSessionNameObservationUnavailable(
849
+ coordRoot,
850
+ owner.instance_id,
851
+ adapter,
852
+ eventName,
853
+ data.session_name_unavailable_reason,
854
+ );
855
+ }
879
856
  const stopRemediation =
880
857
  norm.event_type === "turn.started" &&
881
858
  typeof data.prompt_text === "string" &&
@@ -892,6 +869,107 @@ async function main(): Promise<number> {
892
869
  return 0;
893
870
  }
894
871
 
872
+ const run: HookRun = {
873
+ coordRoot,
874
+ adapter,
875
+ behavior,
876
+ eventName,
877
+ norm,
878
+ payload,
879
+ raw,
880
+ owner,
881
+ sessionId,
882
+ data,
883
+ debugBase,
884
+ ledgerRoute,
885
+ hookStartedAt,
886
+ hookClock,
887
+ priorCoordination,
888
+ topLevelSession,
889
+ stopRemediation,
890
+ bridge,
891
+ turnRitual,
892
+ };
893
+ recordSignal(run);
894
+ recordContextTelemetry(run);
895
+
896
+ // Phase 8: SessionStart post-emit: project the event so the heartbeat
897
+ // lands synchronously, run stale-sweep, and emit the adapter-shaped
898
+ // systemMessage JSON (peer table + wiring check + council invites).
899
+ // Adapter-agnostic since v0.5.0; replaces the previous bash UX layer
900
+ // and the equivalent per-adapter bash session_start handlers.
901
+ if (norm.event_type === "session.started") await handleSessionStarted(run);
902
+
903
+ // Phase 8: SessionEnd cleanup: delete heartbeat + pid-map rows. Adapter-
904
+ // agnostic since v0.5.0.
905
+ if (norm.event_type === "session.ended") handleSessionEnded(run);
906
+
907
+ // Phase 8: SubagentStart: the child generation was opened above from the
908
+ // native delegation identity. Refresh its disposable projection, log the
909
+ // lifecycle event, and emit a context message announcing the subagent.
910
+ if (norm.event_type === "agent.started") await handleAgentStarted(run);
911
+
912
+ // Phase 8: SubagentStop: delete subagent heartbeat + log.
913
+ if (norm.event_type === "agent.completed") await handleAgentCompleted(run);
914
+
915
+ // Phase 8: UserPromptSubmit: render dedup'd peer table + council pending
916
+ // and emit the adapter-shaped systemMessage JSON. Adapter-agnostic since v0.5.0.
917
+ if (norm.event_type === "turn.started") await handleTurnStarted(run);
918
+
919
+ // turn.completed: telemetry, then the stop verdict. The
920
+ // verdict + codex-replay previously lived in the per-adapter shell adapters;
921
+ // agent-hook owns them now. Runs on the normal "stop" event only;
922
+ // "stop-failure" (API error) gets no gate, matching the previous
923
+ // stop vs stop-failure split.
924
+ if (norm.event_type === "turn.completed" && eventName === "stop") {
925
+ const exit = await handleStop(run);
926
+ if (exit !== undefined) return exit;
927
+ }
928
+
929
+ // Phase 7: PreToolUse: heartbeat + pid-map self-heal on every tool call.
930
+ // Adapter-agnostic: both writes have the same shape regardless of who fired.
931
+ // Cursor/Codex bash dispatchers still fire their own G-guard logic, but the
932
+ // heals here keep the agent-coord layer's view of liveness fresh.
933
+ //
934
+ // The heartbeat + pid-map heals are paired by design; they were wired
935
+ // side-by-side in the previous pre-tool-use adapter. The Phase 4-6 refactor
936
+ // preserved the heartbeat half but dropped the pid-map half; the pid-map
937
+ // call was restored here afterward.
938
+ if (norm.event_type === "tool.requested") {
939
+ const exit = await handleToolRequested(run);
940
+ if (exit !== undefined) return exit;
941
+ }
942
+
943
+ if (norm.event_type === "tool.completed" && eventName === "post-tool-use")
944
+ await handlePostToolUse(run);
945
+
946
+ // Phase 7: PostToolUseFailure: release claim on failed Edit (the file
947
+ // never landed; the claim is stale). Adapter-agnostic.
948
+ if (norm.event_type === "tool.completed" && eventName === "post-tool-use-failure")
949
+ handlePostToolUseFailure(run);
950
+
951
+ return 0;
952
+ }
953
+
954
+ function recordSignal(run: HookRun): void {
955
+ const {
956
+ coordRoot,
957
+ adapter,
958
+ eventName,
959
+ norm,
960
+ payload,
961
+ owner,
962
+ sessionId,
963
+ data,
964
+ debugBase,
965
+ ledgerRoute,
966
+ hookStartedAt,
967
+ hookClock,
968
+ priorCoordination,
969
+ stopRemediation,
970
+ bridge,
971
+ turnRitual,
972
+ } = run;
895
973
  let capturedImages: ReturnType<typeof captureImages> = [];
896
974
  if (
897
975
  norm.event_type === "tool.requested" ||
@@ -924,7 +1002,7 @@ async function main(): Promise<number> {
924
1002
  : {}),
925
1003
  }
926
1004
  : {}),
927
- ...(adapter === "codex" && isWslUncPath(payload?.cwd) ? { bridge: "codex-wsl" as const } : {}),
1005
+ ...(bridge ? { bridge } : {}),
928
1006
  hook_name: eventName,
929
1007
  hook_duration_ms: Math.max(0, Math.floor(performance.now() - hookStartedAt)),
930
1008
  monotonic_ns: hookClock.monotonic_ns,
@@ -980,9 +1058,7 @@ async function main(): Promise<number> {
980
1058
  parentEvent: v3Result.event,
981
1059
  payload,
982
1060
  adapter,
983
- ...(adapter === "codex" && isWslUncPath(payload.cwd)
984
- ? { bridge: "codex-wsl" as const }
985
- : {}),
1061
+ ...(bridge ? { bridge } : {}),
986
1062
  monotonic_ns: hookClock.monotonic_ns,
987
1063
  });
988
1064
  const nativeChild = payload.subagent_id ?? payload.agent_id;
@@ -1030,7 +1106,13 @@ async function main(): Promise<number> {
1030
1106
  ...(v3Result ? { event_v3_state: v3Result.state } : {}),
1031
1107
  ...(v3Result && "reason" in v3Result ? { event_v3_reason: v3Result.reason } : {}),
1032
1108
  });
1109
+ run.v3Result = v3Result;
1110
+ run.v3EventId = v3EventId;
1111
+ run.recordedGenerationId = recordedGenerationId;
1112
+ }
1033
1113
 
1114
+ function recordContextTelemetry(run: HookRun): void {
1115
+ const { coordRoot, adapter, norm, payload, owner, sessionId } = run;
1034
1116
  // Context telemetry is opportunistic and truthful: only persist a sample
1035
1117
  // when the adapter payload actually exposes usage/window data. Identical
1036
1118
  // measurements are de-duplicated before they reach the canonical stream.
@@ -1081,437 +1163,545 @@ async function main(): Promise<number> {
1081
1163
  logError(coordRoot, err, { phase: "context-compaction-completed" });
1082
1164
  }
1083
1165
  }
1166
+ }
1084
1167
 
1085
- // Phase 8: SessionStart post-emit: project the event so the heartbeat
1086
- // lands synchronously, run stale-sweep, and emit the adapter-shaped
1087
- // systemMessage JSON (peer table + wiring check + council invites).
1088
- // Adapter-agnostic since v0.5.0; replaces the previous bash UX layer
1089
- // and the equivalent per-adapter bash session_start handlers.
1090
- if (norm.event_type === "session.started") {
1091
- // A configured backup runs out of process and is freshness-gated per host.
1092
- // Startup never waits for restic or a remote provider; the child owns no
1093
- // hook stdio, so the harness is not held open until it finishes. A failed
1094
- // previous run surfaces as one line in this session's context.
1095
- let backupCue = "";
1096
- try {
1097
- backupCue = scheduleBackupSnapshot(coordRoot).cue ?? "";
1098
- } catch (err) {
1099
- logError(coordRoot, err, { phase: "backup-schedule" });
1100
- }
1101
- // Effect (claude-code): prune stale journal archives + sweep orphans.
1102
- // The recovery-cue is merged into the
1103
- // session-start additionalContext inside emitSessionStartSystemMessage.
1104
- if (adapter === "claude-code") journalJanitor(coordRoot);
1105
- imageJanitor(coordRoot);
1106
- // Session start is the only caller of the artifact janitor and the storage
1107
- // maintenance pass, so both load here instead of on every tool call.
1108
- const { autoCleanArtifacts } = await import("../artifacts/index.ts");
1109
- const { autoCleanEventV3Archives } = await import("../events/v3/archive-retention.ts");
1110
- const { createAutomaticMaintenanceComposition, runAutomaticMaintenancePass } = await import(
1111
- "../storage/maintenance-providers.ts"
1168
+ async function handleSessionStarted(run: HookRun): Promise<void> {
1169
+ const { coordRoot, adapter, behavior, payload, owner, sessionId, data } = run;
1170
+ // A configured backup runs out of process and is freshness-gated per host.
1171
+ // Startup never waits for restic or a remote provider; the child owns no
1172
+ // hook stdio, so the harness is not held open until it finishes. A failed
1173
+ // previous run surfaces as one line in this session's context.
1174
+ let backupCue = "";
1175
+ try {
1176
+ backupCue = scheduleBackupSnapshot(coordRoot).cue ?? "";
1177
+ } catch (err) {
1178
+ logError(coordRoot, err, { phase: "backup-schedule" });
1179
+ }
1180
+ // Effect (journal-maintaining adapters): prune stale journal archives +
1181
+ // sweep orphans. The recovery-cue is merged into the session-start
1182
+ // additionalContext inside emitSessionStartSystemMessage.
1183
+ if (behavior.journalMaintenance) journalJanitor(coordRoot);
1184
+ imageJanitor(coordRoot);
1185
+ // Session start is the only caller of the artifact janitor and the storage
1186
+ // maintenance pass, so both load here instead of on every tool call.
1187
+ const { autoCleanArtifacts } = await import("../artifacts/index.ts");
1188
+ const { autoCleanEventV3Archives } = await import("../events/v3/archive-retention.ts");
1189
+ const { createAutomaticMaintenanceComposition, runAutomaticMaintenancePass } = await import(
1190
+ "../storage/maintenance-providers.ts"
1191
+ );
1192
+ // Effect: throttled daily sweep of expired artifact workspaces (guarded,
1193
+ // managed-expired only; see autoCleanArtifacts). Best-effort like every
1194
+ // janitor here: a failure logs and never blocks session start.
1195
+ try {
1196
+ autoCleanArtifacts(coordRoot);
1197
+ } catch (err) {
1198
+ logError(coordRoot, err, { phase: "artifact-auto-clean" });
1199
+ }
1200
+ // Effect: apply the same daily throttle to closed V3 epochs. The newest
1201
+ // recovery epochs remain protected by the archive retention policy.
1202
+ try {
1203
+ autoCleanEventV3Archives(coordRoot);
1204
+ } catch (err) {
1205
+ logError(coordRoot, err, { phase: "event-v3-archive-auto-clean" });
1206
+ }
1207
+ // Plan one bounded, claim-first global slice from cached pressure only.
1208
+ // Existing owner janitors above remain the only active executors until
1209
+ // each provider receives a separate production activation.
1210
+ try {
1211
+ await runAutomaticMaintenancePass(
1212
+ createAutomaticMaintenanceComposition(coordRoot, {
1213
+ journal: () => {
1214
+ if (behavior.journalMaintenance) journalJanitor(coordRoot);
1215
+ },
1216
+ images: () => imageJanitor(coordRoot),
1217
+ artifacts: () => autoCleanArtifacts(coordRoot),
1218
+ }),
1112
1219
  );
1113
- // Effect: throttled daily sweep of expired artifact workspaces (guarded,
1114
- // managed-expired only; see autoCleanArtifacts). Best-effort like every
1115
- // janitor here: a failure logs and never blocks session start.
1116
- try {
1117
- autoCleanArtifacts(coordRoot);
1118
- } catch (err) {
1119
- logError(coordRoot, err, { phase: "artifact-auto-clean" });
1120
- }
1121
- // Effect: apply the same daily throttle to closed V3 epochs. The newest
1122
- // recovery epochs remain protected by the archive retention policy.
1123
- try {
1124
- autoCleanEventV3Archives(coordRoot);
1125
- } catch (err) {
1126
- logError(coordRoot, err, { phase: "event-v3-archive-auto-clean" });
1127
- }
1128
- // Plan one bounded, claim-first global slice from cached pressure only.
1129
- // Existing owner janitors above remain the only active executors until
1130
- // each provider receives a separate production activation.
1131
- try {
1132
- await runAutomaticMaintenancePass(
1133
- createAutomaticMaintenanceComposition(coordRoot, {
1134
- journal: () => {
1135
- if (adapter === "claude-code") journalJanitor(coordRoot);
1136
- },
1137
- images: () => imageJanitor(coordRoot),
1138
- artifacts: () => autoCleanArtifacts(coordRoot),
1139
- }),
1140
- );
1141
- } catch (err) {
1142
- logError(coordRoot, err, { phase: "storage-maintenance-auto" });
1143
- }
1144
- let recovery: PreparedContextRecovery | null = null;
1145
- if (payload?.source === "compact") {
1146
- try {
1147
- markContextCompactionCompleted(coordRoot, {
1148
- sessionId,
1149
- instanceId: owner.instance_id,
1150
- });
1151
- recovery = prepareContextRecovery(coordRoot, {
1152
- instanceId: owner.instance_id,
1153
- sessionId,
1154
- cwd: payload?.cwd ?? process.cwd(),
1155
- });
1156
- } catch (err) {
1157
- logError(coordRoot, err, { phase: "context-recovery-session-start" });
1158
- }
1159
- }
1220
+ } catch (err) {
1221
+ logError(coordRoot, err, { phase: "storage-maintenance-auto" });
1222
+ }
1223
+ let recovery: PreparedContextRecovery | null = null;
1224
+ if (payload?.source === "compact") {
1160
1225
  try {
1161
- const injected = await emitSessionStartSystemMessage(
1162
- coordRoot,
1163
- owner.instance_id,
1226
+ markContextCompactionCompleted(coordRoot, {
1164
1227
  sessionId,
1165
- data,
1166
- adapter,
1167
- recovery?.briefing ?? "",
1168
- backupCue,
1169
- );
1170
- if (injected && recovery) {
1171
- completeRecoveryInjection(coordRoot, owner.instance_id, sessionId, recovery);
1172
- }
1228
+ instanceId: owner.instance_id,
1229
+ });
1230
+ recovery = prepareContextRecovery(coordRoot, {
1231
+ instanceId: owner.instance_id,
1232
+ sessionId,
1233
+ cwd: payload?.cwd ?? process.cwd(),
1234
+ });
1173
1235
  } catch (err) {
1174
- logError(coordRoot, err, { phase: "session-start-systemMessage" });
1236
+ logError(coordRoot, err, { phase: "context-recovery-session-start" });
1175
1237
  }
1176
- // Cross-machine presence is derived from the authority-safe V3 projection,
1177
- // not the disposable heartbeat cache. Announce this session, pull peers,
1178
- // and keep the optional live relay connected.
1179
- try {
1180
- publishPresence(coordRoot);
1181
- fetchPresence(coordRoot);
1182
- ensureRelayDaemon(coordRoot);
1183
- } catch (err) {
1184
- logError(coordRoot, err, { phase: "session-start-presence" });
1238
+ }
1239
+ try {
1240
+ const injected = await emitSessionStartSystemMessage(
1241
+ coordRoot,
1242
+ owner.instance_id,
1243
+ sessionId,
1244
+ data,
1245
+ adapter,
1246
+ recovery?.briefing ?? "",
1247
+ backupCue,
1248
+ );
1249
+ if (injected && recovery) {
1250
+ completeRecoveryInjection(coordRoot, owner.instance_id, sessionId, recovery);
1185
1251
  }
1252
+ } catch (err) {
1253
+ logError(coordRoot, err, { phase: "session-start-systemMessage" });
1254
+ }
1255
+ // Cross-machine presence is derived from the authority-safe V3 projection,
1256
+ // not the disposable heartbeat cache. Announce this session, pull peers,
1257
+ // and keep the optional live relay connected.
1258
+ try {
1259
+ publishPresence(coordRoot);
1260
+ fetchPresence(coordRoot);
1261
+ ensureRelayDaemon(coordRoot);
1262
+ } catch (err) {
1263
+ logError(coordRoot, err, { phase: "session-start-presence" });
1186
1264
  }
1265
+ }
1187
1266
 
1188
- // Phase 8: SessionEnd cleanup: delete heartbeat + pid-map rows. Adapter-
1189
- // agnostic since v0.5.0.
1190
- if (norm.event_type === "session.ended") {
1191
- try {
1192
- cleanupSessionEnd(coordRoot, owner.instance_id);
1193
- } catch (err) {
1194
- logError(coordRoot, err, { phase: "session-end-cleanup" });
1195
- }
1196
- // Effects (claude-code): archive the ending agent's journal + force a
1197
- // session-telemetry sync (via HARNERY_CLAUDE_SESSIONS_FORCE=1).
1198
- if (adapter === "claude-code") {
1199
- journalArchive(coordRoot, owner.instance_id);
1200
- runSessionSyncExtension(coordRoot, true);
1201
- }
1202
- // Publish the post-cleanup V3 projection so remote machines see this
1203
- // session disappear without waiting for the stale window.
1204
- try {
1205
- publishPresence(coordRoot);
1206
- } catch (err) {
1207
- logError(coordRoot, err, { phase: "session-end-presence" });
1208
- }
1267
+ function handleSessionEnded(run: HookRun): void {
1268
+ const { coordRoot, behavior, owner } = run;
1269
+ try {
1270
+ cleanupSessionEnd(coordRoot, owner.instance_id);
1271
+ } catch (err) {
1272
+ logError(coordRoot, err, { phase: "session-end-cleanup" });
1273
+ }
1274
+ // Effects: archive the ending agent's journal + force a session-telemetry
1275
+ // sync (via HARNERY_CLAUDE_SESSIONS_FORCE=1), each gated by the behavior.
1276
+ if (behavior.journalMaintenance) journalArchive(coordRoot, owner.instance_id);
1277
+ if (behavior.sessionTelemetrySync) runSessionSyncExtension(coordRoot, true);
1278
+ // Publish the post-cleanup V3 projection so remote machines see this
1279
+ // session disappear without waiting for the stale window.
1280
+ try {
1281
+ publishPresence(coordRoot);
1282
+ } catch (err) {
1283
+ logError(coordRoot, err, { phase: "session-end-presence" });
1209
1284
  }
1285
+ }
1210
1286
 
1211
- // Phase 8: SubagentStart: the child generation was opened above from the
1212
- // native delegation identity. Refresh its disposable projection, log the
1213
- // lifecycle event, and emit a context message announcing the subagent.
1214
- if (norm.event_type === "agent.started") {
1215
- try {
1216
- const childInstanceId = (data.agent_id as string | undefined) ?? owner.instance_id;
1217
- await emitSubagentStartContext(coordRoot, childInstanceId, sessionId, data, adapter);
1218
- } catch (err) {
1219
- logError(coordRoot, err, { phase: "subagent-start-context" });
1220
- }
1287
+ async function handleAgentStarted(run: HookRun): Promise<void> {
1288
+ const { coordRoot, adapter, owner, sessionId, data } = run;
1289
+ try {
1290
+ const childInstanceId = (data.agent_id as string | undefined) ?? owner.instance_id;
1291
+ await emitSubagentStartContext(coordRoot, childInstanceId, sessionId, data, adapter);
1292
+ } catch (err) {
1293
+ logError(coordRoot, err, { phase: "subagent-start-context" });
1221
1294
  }
1295
+ }
1222
1296
 
1223
- // Phase 8: SubagentStop: delete subagent heartbeat + log.
1224
- if (norm.event_type === "agent.completed") {
1225
- try {
1226
- const childInstanceId = payload?.subagent_id ?? payload?.agent_id ?? owner.instance_id;
1227
- cleanupSessionEnd(coordRoot, childInstanceId);
1228
- const { reconcileSessionFinalizationV3 } = await import("../agents/session-finalizer-v3.ts");
1229
- reconcileSessionFinalizationV3(coordRoot, { archive_observations: [] });
1230
- } catch (err) {
1231
- logError(coordRoot, err, { phase: "subagent-stop-cleanup" });
1232
- }
1297
+ async function handleAgentCompleted(run: HookRun): Promise<void> {
1298
+ const { coordRoot, payload, owner } = run;
1299
+ try {
1300
+ const childInstanceId = payload?.subagent_id ?? payload?.agent_id ?? owner.instance_id;
1301
+ cleanupSessionEnd(coordRoot, childInstanceId);
1302
+ const { reconcileSessionFinalizationV3 } = await import("../agents/session-finalizer-v3.ts");
1303
+ reconcileSessionFinalizationV3(coordRoot, { archive_observations: [] });
1304
+ } catch (err) {
1305
+ logError(coordRoot, err, { phase: "subagent-stop-cleanup" });
1233
1306
  }
1307
+ }
1234
1308
 
1235
- // Phase 8: UserPromptSubmit: render dedup'd peer table + council pending
1236
- // and emit the adapter-shaped systemMessage JSON. Adapter-agnostic since v0.5.0.
1237
- if (norm.event_type === "turn.started") {
1238
- // Effects (claude-code): reset per-turn sound rate-limit counters + run
1239
- // presence detection on the prompt.
1240
- if (adapter === "claude-code") {
1241
- resetSoundCounters(sessionId);
1242
- const prompt = (payload?.raw?.prompt as string | undefined) ?? "";
1243
- if (prompt) detectPresence(prompt);
1244
- }
1245
- // Presence: throttled pull of peer machines' presence refs (default 60s
1246
- // interval; the systemMessage below reads whatever is locally known).
1309
+ async function handleTurnStarted(run: HookRun): Promise<void> {
1310
+ const {
1311
+ coordRoot,
1312
+ adapter,
1313
+ behavior,
1314
+ payload,
1315
+ owner,
1316
+ sessionId,
1317
+ debugBase,
1318
+ topLevelSession,
1319
+ stopRemediation,
1320
+ v3EventId,
1321
+ } = run;
1322
+ // Effects: reset per-turn sound rate-limit counters + run presence
1323
+ // detection on the prompt, each gated by the behavior.
1324
+ if (behavior.soundEffects) resetSoundCounters(sessionId);
1325
+ if (behavior.promptPresenceDetection) {
1326
+ const prompt = (payload?.raw?.prompt as string | undefined) ?? "";
1327
+ if (prompt) detectPresence(prompt);
1328
+ }
1329
+ // Presence: throttled pull of peer machines' presence refs (default 60s
1330
+ // interval; the systemMessage below reads whatever is locally known).
1331
+ try {
1332
+ fetchPresence(coordRoot);
1333
+ } catch (err) {
1334
+ logError(coordRoot, err, { phase: "user-prompt-submit-presence" });
1335
+ }
1336
+ let recovery: PreparedContextRecovery | null = null;
1337
+ const continuityState = readContextState(coordRoot, sessionId);
1338
+ if (
1339
+ continuityState?.phase === "checkpointed" &&
1340
+ continuityState.compaction_completed_at !== undefined
1341
+ ) {
1247
1342
  try {
1248
- fetchPresence(coordRoot);
1343
+ recovery = prepareContextRecovery(coordRoot, {
1344
+ instanceId: owner.instance_id,
1345
+ sessionId,
1346
+ cwd: payload?.cwd ?? process.cwd(),
1347
+ });
1249
1348
  } catch (err) {
1250
- logError(coordRoot, err, { phase: "user-prompt-submit-presence" });
1251
- }
1252
- let recovery: PreparedContextRecovery | null = null;
1253
- const continuityState = readContextState(coordRoot, sessionId);
1254
- if (
1255
- continuityState?.phase === "checkpointed" &&
1256
- continuityState.compaction_completed_at !== undefined
1257
- ) {
1258
- try {
1259
- recovery = prepareContextRecovery(coordRoot, {
1260
- instanceId: owner.instance_id,
1261
- sessionId,
1262
- cwd: payload?.cwd ?? process.cwd(),
1263
- });
1264
- } catch (err) {
1265
- logError(coordRoot, err, { phase: "context-recovery-user-prompt" });
1266
- }
1267
- }
1268
- let hostPromptContext = "";
1269
- const hostPromptEnabled = hostPromptContextConfig(coordRoot)?.enabled === true;
1270
- if (hostPromptEnabled && topLevelSession && !stopRemediation) {
1271
- try {
1272
- const { runPromptContext } = await import("./prompt-context/runner.ts");
1273
- const result = await runPromptContext({
1274
- coordRoot,
1275
- adapter,
1276
- sessionId,
1277
- turnId: payload?.turn_id ?? v3EventId ?? sessionId,
1278
- cwd: payload?.cwd ?? process.cwd(),
1279
- prompt: payload?.prompt ?? "",
1280
- });
1281
- appendDebug(coordRoot, {
1282
- ...debugBase,
1283
- effect: "prompt-context",
1284
- ...result.audit,
1285
- });
1286
- hostPromptContext = result.context ?? "";
1287
- } catch (err) {
1288
- logError(coordRoot, err, { phase: "prompt-context-user-prompt" });
1289
- }
1349
+ logError(coordRoot, err, { phase: "context-recovery-user-prompt" });
1290
1350
  }
1351
+ }
1352
+ let hostPromptContext = "";
1353
+ const hostPromptEnabled = hostPromptContextConfig(coordRoot)?.enabled === true;
1354
+ if (hostPromptEnabled && topLevelSession && !stopRemediation) {
1291
1355
  try {
1292
- const injected = await emitUserPromptSubmitSystemMessage(
1356
+ const { runPromptContext } = await import("./prompt-context/runner.ts");
1357
+ const result = await runPromptContext({
1293
1358
  coordRoot,
1294
- owner.instance_id,
1295
- sessionId,
1296
1359
  adapter,
1297
- payload?.cwd,
1298
- recovery?.briefing ?? "",
1299
- hostPromptContext,
1300
- );
1301
- if (injected && recovery) {
1302
- completeRecoveryInjection(coordRoot, owner.instance_id, sessionId, recovery);
1303
- }
1360
+ sessionId,
1361
+ turnId: payload?.turn_id ?? v3EventId ?? sessionId,
1362
+ cwd: payload?.cwd ?? process.cwd(),
1363
+ prompt: payload?.prompt ?? "",
1364
+ });
1365
+ appendDebug(coordRoot, {
1366
+ ...debugBase,
1367
+ effect: "prompt-context",
1368
+ ...result.audit,
1369
+ });
1370
+ hostPromptContext = result.context ?? "";
1304
1371
  } catch (err) {
1305
- logError(coordRoot, err, { phase: "user-prompt-submit-systemMessage" });
1372
+ logError(coordRoot, err, { phase: "prompt-context-user-prompt" });
1306
1373
  }
1307
1374
  }
1308
-
1309
- // turn.completed: telemetry, then the stop verdict. The
1310
- // verdict + codex-replay previously lived in the per-adapter shell adapters;
1311
- // agent-hook owns them now. Runs on the normal "stop" event only;
1312
- // "stop-failure" (API error) gets no gate, matching the previous
1313
- // stop vs stop-failure split.
1314
- if (norm.event_type === "turn.completed" && eventName === "stop") {
1315
- // Claude Code session telemetry sync remains an independent side effect.
1316
- if (adapter === "claude-code") {
1317
- runSessionSyncExtension(coordRoot, false);
1318
- }
1319
-
1320
- // Publish after the canonical turn event has landed so the blob carries
1321
- // current task, activity, and claim state. Re-ensure the relay here because
1322
- // its daemon intentionally exits when a machine has no live sessions.
1323
- try {
1324
- publishPresence(coordRoot);
1325
- ensureRelayDaemon(coordRoot);
1326
- } catch (err) {
1327
- logError(coordRoot, err, { phase: "stop-presence" });
1328
- }
1329
-
1330
- // Stop verdict (V3 ritual + task/status gate). Direct in-process call: the
1331
- // rule lives in Harnery. agent-hook already emitted this turn.completed
1332
- // with privacy-safe ritual observations, so the evidence is in the stream.
1333
- const verdict = evaluateStopHook(coordRoot, {
1334
- rule: "stop-hook",
1335
- instance_id: owner.instance_id,
1336
- session_id: sessionId,
1375
+ try {
1376
+ const injected = await emitUserPromptSubmitSystemMessage(
1377
+ coordRoot,
1378
+ owner.instance_id,
1379
+ sessionId,
1337
1380
  adapter,
1338
- stop_hook_active: payload?.stop_hook_active === true,
1339
- status_box_present_strict: data.status_box_present_strict === true,
1340
- bypass: coordEnv("AGENT_COORD_BYPASS_STOP") === "1",
1341
- workflow_child: coordEnv("WORKFLOW_CHILD") === "1",
1342
- });
1343
- if (!verdict.allow) {
1344
- // Backstop for sessions whose evidence can never land (e.g. a headless
1345
- // child whose status command resolves to a different owner): after
1346
- // `cap` consecutive blocked stops in one cycle, allow the stop instead
1347
- // of bouncing the model forever. Cycle boundaries come from the
1348
- // adapter's continuation flag, so adapters without one are unaffected.
1349
- const { recordRemediationBlock } = await import("./remediation-cap.ts");
1350
- const remediation = recordRemediationBlock(sessionId, payload?.stop_hook_active === true);
1351
- if (remediation.exceeded) {
1352
- appendDebug(coordRoot, {
1353
- ...debugBase,
1354
- skipped: "stop-remediation-cap-exhausted",
1355
- blocked_rule: verdict.rule,
1356
- blocked_count: remediation.count,
1357
- session_id: sessionId,
1358
- remediation_cycle_anchor: verdict.remediation_cycle_anchor,
1359
- });
1360
- } else {
1361
- // Adapter-aware enforcement channel: Claude Code honors exit-2 + stderr
1362
- // as a turn block; Cursor ignores exit codes and re-prompts only via a
1363
- // `followup_message` it auto-submits.
1364
- const { emitStopOutcome } = await import("./adapter/output.ts");
1365
- return emitStopOutcome(adapter, { verdict }, coordRoot);
1366
- }
1367
- } else {
1368
- const { clearRemediationCount } = await import("./remediation-cap.ts");
1369
- clearRemediationCount(sessionId);
1381
+ payload?.cwd,
1382
+ recovery?.briefing ?? "",
1383
+ hostPromptContext,
1384
+ );
1385
+ if (injected && recovery) {
1386
+ completeRecoveryInjection(coordRoot, owner.instance_id, sessionId, recovery);
1370
1387
  }
1388
+ } catch (err) {
1389
+ logError(coordRoot, err, { phase: "user-prompt-submit-systemMessage" });
1390
+ }
1391
+ }
1371
1392
 
1372
- // An explicit end requested from inside this turn cannot be authoritative
1373
- // until the adapter has committed turn.completed above. Reconcile only
1374
- // when such a request exists; any later real work cancels it in the
1375
- // finalizer instead of being terminated underneath the agent.
1376
- try {
1377
- const { hasPendingExplicitSessionEndV3, reconcileSessionFinalizationV3 } = await import(
1378
- "../agents/session-finalizer-v3.ts"
1379
- );
1380
- if (hasPendingExplicitSessionEndV3(coordRoot)) {
1381
- reconcileSessionFinalizationV3(coordRoot);
1382
- }
1383
- } catch (err) {
1384
- logError(coordRoot, err, { phase: "stop-explicit-session-finalization" });
1385
- }
1386
- if (adapter === "codex") scheduleCodexRuntimeContextRetry(coordRoot, sessionId);
1393
+ async function handleStop(run: HookRun): Promise<number | undefined> {
1394
+ const { coordRoot, adapter, behavior, payload, owner, sessionId, data, debugBase } = run;
1395
+ // Session telemetry sync remains an independent side effect.
1396
+ if (behavior.sessionTelemetrySync) runSessionSyncExtension(coordRoot, false);
1397
+
1398
+ // Publish after the canonical turn event has landed so the blob carries
1399
+ // current task, activity, and claim state. Re-ensure the relay here because
1400
+ // its daemon intentionally exits when a machine has no live sessions.
1401
+ try {
1402
+ publishPresence(coordRoot);
1403
+ ensureRelayDaemon(coordRoot);
1404
+ } catch (err) {
1405
+ logError(coordRoot, err, { phase: "stop-presence" });
1387
1406
  }
1388
1407
 
1389
- // Phase 7: PreToolUse: heartbeat + pid-map self-heal on every tool call.
1390
- // Adapter-agnostic: both writes have the same shape regardless of who fired.
1391
- // Cursor/Codex bash dispatchers still fire their own G-guard logic, but the
1392
- // heals here keep the agent-coord layer's view of liveness fresh.
1393
- //
1394
- // The heartbeat + pid-map heals are paired by design; they were wired
1395
- // side-by-side in the previous pre-tool-use adapter. The Phase 4-6 refactor
1396
- // preserved the heartbeat half but dropped the pid-map half; the pid-map
1397
- // call was restored here afterward.
1398
- if (norm.event_type === "tool.requested") {
1399
- try {
1400
- // Recorded fork lineage, heal-path flavor. A forked CC conversation
1401
- // never fires its own session.started (SessionStart fires under the
1402
- // PARENT's session id with source=resume, before the fork id is
1403
- // minted; verified 2026-08-05), so the fork's new instance first
1404
- // materializes right here. Gate detection on "no heartbeat yet" so the
1405
- // transcript scan runs once per instance lifetime, not per tool call.
1406
- refreshPidmap(coordRoot, owner.instance_id, adapter, payload?.pid);
1407
- } catch (err) {
1408
- logError(coordRoot, err, { phase: "pre-tool-use-heal" });
1408
+ // Stop verdict (V3 ritual + task/status gate). Direct in-process call: the
1409
+ // rule lives in Harnery. agent-hook already emitted this turn.completed
1410
+ // with privacy-safe ritual observations, so the evidence is in the stream.
1411
+ const verdict = evaluateStopHook(coordRoot, {
1412
+ rule: "stop-hook",
1413
+ instance_id: owner.instance_id,
1414
+ session_id: sessionId,
1415
+ adapter,
1416
+ stop_hook_active: payload?.stop_hook_active === true,
1417
+ status_box_present_strict: data.status_box_present_strict === true,
1418
+ session_name_observation_unavailable: typeof data.session_name_unavailable_reason === "string",
1419
+ bypass: coordEnv("AGENT_COORD_BYPASS_STOP") === "1",
1420
+ workflow_child: coordEnv("WORKFLOW_CHILD") === "1",
1421
+ });
1422
+ if (!verdict.allow) {
1423
+ // Backstop for sessions whose evidence can never land (e.g. a headless
1424
+ // child whose status command resolves to a different owner): after
1425
+ // `cap` consecutive blocked stops in one cycle, allow the stop instead
1426
+ // of bouncing the model forever. Cycle boundaries come from the
1427
+ // adapter's continuation flag, so adapters without one are unaffected.
1428
+ const { recordRemediationBlock } = await import("./remediation-cap.ts");
1429
+ const remediation = recordRemediationBlock(sessionId, payload?.stop_hook_active === true);
1430
+ if (remediation.exceeded) {
1431
+ appendDebug(coordRoot, {
1432
+ ...debugBase,
1433
+ skipped: "stop-remediation-cap-exhausted",
1434
+ blocked_rule: verdict.rule,
1435
+ blocked_count: remediation.count,
1436
+ session_id: sessionId,
1437
+ remediation_cycle_anchor: verdict.remediation_cycle_anchor,
1438
+ });
1439
+ } else {
1440
+ // Adapter-aware enforcement channel: Claude Code honors exit-2 + stderr
1441
+ // as a turn block; Cursor ignores exit codes and re-prompts only via a
1442
+ // `followup_message` it auto-submits.
1443
+ const { emitStopOutcome } = await import("./adapter/output.ts");
1444
+ return emitStopOutcome(adapter, { verdict }, coordRoot);
1409
1445
  }
1446
+ } else {
1447
+ const { clearRemediationCount } = await import("./remediation-cap.ts");
1448
+ clearRemediationCount(sessionId);
1449
+ }
1410
1450
 
1411
- // The suggested name is a pending display latch. The set-task call itself
1412
- // runs before a name exists; every later tool waits until the exact block
1413
- // is the first assistant text after the mint result. Cursor supplies
1414
- // agent_message directly; Claude Code and Codex are resolved from their
1415
- // JSONL transcripts. Cursor's current narration can provide positive
1416
- // evidence, but cannot disprove an earlier response; afterAgentResponse
1417
- // stamps that durable sighting. Later commentary must not erase an
1418
- // already-correct display, and unavailable evidence must not deadlock
1419
- // every tool.
1420
- try {
1421
- const displayAllowed = await enforcePendingSessionNameDisplay(
1422
- coordRoot,
1423
- owner.instance_id,
1424
- adapter,
1425
- payload,
1426
- generationBoundHeartbeat(readHeartbeat(coordRoot, owner.instance_id), recordedGenerationId),
1427
- );
1428
- if (!displayAllowed) return 0;
1429
- } catch (err) {
1430
- logError(coordRoot, err, { phase: "pre-tool-use-session-name" });
1451
+ // An explicit end requested from inside this turn cannot be authoritative
1452
+ // until the adapter has committed turn.completed above. Reconcile only
1453
+ // when such a request exists; any later real work cancels it in the
1454
+ // finalizer instead of being terminated underneath the agent.
1455
+ try {
1456
+ const { hasPendingExplicitSessionEndV3, reconcileSessionFinalizationV3 } = await import(
1457
+ "../agents/session-finalizer-v3.ts"
1458
+ );
1459
+ if (hasPendingExplicitSessionEndV3(coordRoot)) {
1460
+ reconcileSessionFinalizationV3(coordRoot);
1431
1461
  }
1462
+ } catch (err) {
1463
+ logError(coordRoot, err, { phase: "stop-explicit-session-finalization" });
1464
+ }
1465
+ if (behavior.runtimeContextRetry) scheduleRuntimeContextRetry(coordRoot, adapter, sessionId);
1466
+ return undefined;
1467
+ }
1432
1468
 
1433
- // Windows-native Codex + WSL UNC only: block the one cross-shell shape
1434
- // proven to corrupt argument boundaries. Normal WSL argv calls, literal
1435
- // bash -s scripts, native Linux/macOS sessions, and every other adapter
1436
- // pass through. The host instructions name its concrete safe bridge.
1437
- const unsafeShellReason = unsafeCrossShellReason({
1469
+ async function handleToolRequested(run: HookRun): Promise<number | undefined> {
1470
+ const { coordRoot, adapter, behavior, payload, owner, sessionId, data, recordedGenerationId } =
1471
+ run;
1472
+ try {
1473
+ // Recorded fork lineage, heal-path flavor. A forked CC conversation
1474
+ // never fires its own session.started (SessionStart fires under the
1475
+ // PARENT's session id with source=resume, before the fork id is
1476
+ // minted; verified 2026-08-05), so the fork's new instance first
1477
+ // materializes right here. Gate detection on "no heartbeat yet" so the
1478
+ // transcript scan runs once per instance lifetime, not per tool call.
1479
+ refreshPidmap(coordRoot, owner.instance_id, adapter, payload?.pid);
1480
+ } catch (err) {
1481
+ logError(coordRoot, err, { phase: "pre-tool-use-heal" });
1482
+ }
1483
+
1484
+ // The suggested name is a pending display latch. The set-task call itself
1485
+ // runs before a name exists; every later tool waits until the exact block
1486
+ // is the first assistant text after the mint result. Cursor supplies
1487
+ // agent_message directly; Claude Code and Codex are resolved from their
1488
+ // JSONL transcripts. Cursor's current narration can provide positive
1489
+ // evidence, but cannot disprove an earlier response; afterAgentResponse
1490
+ // stamps that durable sighting. Later commentary must not erase an
1491
+ // already-correct display, and unavailable evidence must not deadlock
1492
+ // every tool.
1493
+ try {
1494
+ const displayAllowed = await enforcePendingSessionNameDisplay(
1495
+ coordRoot,
1496
+ owner.instance_id,
1438
1497
  adapter,
1498
+ payload,
1499
+ generationBoundHeartbeat(readHeartbeat(coordRoot, owner.instance_id), recordedGenerationId),
1500
+ );
1501
+ if (!displayAllowed) return 0;
1502
+ } catch (err) {
1503
+ logError(coordRoot, err, { phase: "pre-tool-use-session-name" });
1504
+ }
1505
+
1506
+ // Windows-native Codex + WSL UNC only: block the one cross-shell shape
1507
+ // proven to corrupt argument boundaries. Normal WSL argv calls, literal
1508
+ // bash -s scripts, native Linux/macOS sessions, and every other adapter
1509
+ // pass through. The host instructions name its concrete safe bridge.
1510
+ const unsafeShellReason =
1511
+ behavior.unsafeCrossShellReason?.({
1439
1512
  cwd: payload?.cwd,
1440
1513
  toolName: payload?.tool_name,
1441
1514
  toolInput: payload?.tool_input,
1442
- });
1443
- if (unsafeShellReason) {
1444
- const { emitDeny } = await import("./adapter/output.ts");
1445
- emitDeny(adapter, unsafeShellReason);
1446
- return 0;
1447
- }
1515
+ }) ?? null;
1516
+ if (unsafeShellReason) {
1517
+ const { emitDeny } = await import("./adapter/output.ts");
1518
+ emitDeny(adapter, unsafeShellReason);
1519
+ return 0;
1520
+ }
1448
1521
 
1449
- // G-guard for ALL adapters. Claude Code previously ran this via a
1450
- // pre-tool-use bash adapter (which called `agent-coord verdict --rule=claim`);
1451
- // that adapter is now deleted, so agent-hook owns the deny for every adapter.
1452
- // emitDeny() inside emits the adapter-shaped permission JSON (claude-code +
1453
- // codex use hookSpecificOutput.permissionDecision; cursor uses .permission).
1454
- // apply_patch (codex) parses paths from the patch body and runs verdict
1455
- // per-path; Edit/Write/NotebookEdit resolve a single target. Non-write tools
1456
- // (incl. Agent) yield no targets and pass through with no deny.
1457
- let guardAllowed = true;
1458
- try {
1459
- guardAllowed = await runPreToolUseGuard(
1460
- coordRoot,
1461
- owner.instance_id,
1462
- sessionId,
1463
- data,
1464
- adapter,
1465
- );
1466
- } catch (err) {
1467
- logError(coordRoot, err, { phase: "pre-tool-use-guard" });
1468
- }
1469
- if (!guardAllowed) return 0;
1522
+ const waiterReason = shellWaiterReason(payload?.tool_name, payload?.tool_input);
1523
+ if (waiterReason) {
1524
+ const { emitDeny } = await import("./adapter/output.ts");
1525
+ emitDeny(adapter, waiterReason);
1526
+ return 0;
1470
1527
  }
1471
1528
 
1472
- if (norm.event_type === "tool.completed" && eventName === "post-tool-use") {
1473
- // Inject only at the successful tool boundary that actually minted or
1474
- // retried a name: the first non-empty set-task, a pending-name set-task
1475
- // retry, or the transition to lifecycle done. The
1476
- // coordination row intentionally remains pending until transcript evidence
1477
- // catches up; reading that latch alone here would re-inject after every
1478
- // later tool and make the agent print the same block repeatedly.
1479
- try {
1480
- const name = sessionNameDisplayPending(readLiveCoordinationRow(coordRoot, owner.instance_id));
1481
- if (name && toolResponseMintedSessionName(payload?.tool_response, name)) {
1482
- // Record the title before asking for it. The suggestion can still
1483
- // change afterwards (an assigned-name rewrite, a lifecycle re-mint, a
1484
- // rebuilt cache), and the agent must not be stranded for displaying
1485
- // exactly what it was handed.
1486
- stampSessionNameRequested(coordRoot, owner.instance_id, name);
1529
+ // G-guard for ALL adapters. Claude Code previously ran this via a
1530
+ // pre-tool-use bash adapter (which called `agent-coord verdict --rule=claim`);
1531
+ // that adapter is now deleted, so agent-hook owns the deny for every adapter.
1532
+ // emitDeny() inside emits the adapter-shaped permission JSON (claude-code +
1533
+ // codex use hookSpecificOutput.permissionDecision; cursor uses .permission).
1534
+ // apply_patch (codex) parses paths from the patch body and runs verdict
1535
+ // per-path; Edit/Write/NotebookEdit resolve a single target. Non-write tools
1536
+ // (incl. Agent) yield no targets and pass through with no deny.
1537
+ let guardAllowed = true;
1538
+ try {
1539
+ guardAllowed = await runPreToolUseGuard(coordRoot, owner.instance_id, sessionId, data, adapter);
1540
+ } catch (err) {
1541
+ logError(coordRoot, err, { phase: "pre-tool-use-guard" });
1542
+ }
1543
+ if (!guardAllowed) return 0;
1544
+ return undefined;
1545
+ }
1546
+
1547
+ async function handlePostToolUse(run: HookRun): Promise<void> {
1548
+ const { coordRoot, adapter, eventName, payload, owner } = run;
1549
+ // Inject the full instruction only at the successful tool boundary that
1550
+ // actually minted or retried a name: the first non-empty set-task, a
1551
+ // pending-name set-task retry, or the transition to lifecycle done. The
1552
+ // coordination row intentionally remains pending until transcript evidence
1553
+ // catches up, so the latch alone cannot drive injection: that would print
1554
+ // the same block after every later tool.
1555
+ //
1556
+ // Between those two extremes sits a bounded reminder. Since ADR 0182 the
1557
+ // PreToolUse gate allows a tool when transcript evidence is unavailable
1558
+ // and says so only on stderr, which the model never reads. A model that
1559
+ // skipped the block after the mint was therefore never asked again
1560
+ // (ADR 0183). While the requested title is still pending, ordinary tool
1561
+ // results carry an idempotent reminder up to a fixed count; coordination
1562
+ // chores (set-task, status, suggest-name) are skipped because a mint or
1563
+ // retry response already carries the full instruction and an end-turn
1564
+ // status is the wrong place to ask.
1565
+ try {
1566
+ const row = readLiveCoordinationRow(coordRoot, owner.instance_id);
1567
+ const name = sessionNameDisplayPending(row);
1568
+ if (name && toolResponseMintedSessionName(payload?.tool_response, name)) {
1569
+ // Record the title before asking for it. The suggestion can still
1570
+ // change afterwards (an assigned-name rewrite, a lifecycle re-mint, a
1571
+ // rebuilt cache), and the agent must not be stranded for displaying
1572
+ // exactly what it was handed.
1573
+ stampSessionNameRequested(coordRoot, owner.instance_id, name);
1574
+ const { emitContext } = await import("./adapter/output.ts");
1575
+ emitContext(adapter, "PostToolUse", sessionNameDisplayInstruction(name));
1576
+ } else if (name) {
1577
+ const due = sessionNameDisplayReminderDue(row);
1578
+ const command = extractBashCommand(payload?.tool_name, payload?.tool_input);
1579
+ if (due && !isSessionNameRemediationCommand(command, resolveBinName(coordRoot))) {
1580
+ stampSessionNameReminded(coordRoot, owner.instance_id, due);
1581
+ appendDebug(coordRoot, {
1582
+ ts: new Date().toISOString(),
1583
+ event_name: eventName,
1584
+ adapter,
1585
+ instance_id: owner.instance_id,
1586
+ effect: "session-name-display-reminded",
1587
+ session_name_pending: due,
1588
+ reminders: (row?.session_name_display_reminders ?? 0) + 1,
1589
+ });
1487
1590
  const { emitContext } = await import("./adapter/output.ts");
1488
- emitContext(adapter, "PostToolUse", sessionNameDisplayInstruction(name));
1591
+ emitContext(adapter, "PostToolUse", sessionNameDisplayReminder(due));
1489
1592
  }
1490
- } catch (err) {
1491
- logError(coordRoot, err, { phase: "post-tool-use-session-name" });
1492
1593
  }
1594
+ } catch (err) {
1595
+ logError(coordRoot, err, { phase: "post-tool-use-session-name" });
1493
1596
  }
1597
+ }
1494
1598
 
1495
- // Phase 7: PostToolUseFailure: release claim on failed Edit (the file
1496
- // never landed; the claim is stale). Adapter-agnostic.
1497
- if (norm.event_type === "tool.completed" && eventName === "post-tool-use-failure") {
1498
- try {
1499
- releaseClaimOnFailure(coordRoot, owner.instance_id, data, payload?.raw);
1500
- } catch (err) {
1501
- logError(coordRoot, err, { phase: "post-tool-use-failure-release" });
1502
- }
1599
+ function handlePostToolUseFailure(run: HookRun): void {
1600
+ const { coordRoot, payload, owner, data } = run;
1601
+ try {
1602
+ releaseClaimOnFailure(coordRoot, owner.instance_id, data, payload?.raw);
1603
+ } catch (err) {
1604
+ logError(coordRoot, err, { phase: "post-tool-use-failure-release" });
1503
1605
  }
1606
+ }
1504
1607
 
1608
+ async function handleCompletedResponse(run: CompletedResponseRun): Promise<number> {
1609
+ const { coordRoot, adapter, eventName, raw, debugBase, ledgerRoute, hookStartedAt, hookClock } =
1610
+ run;
1611
+ const payload = parsePayload(raw, adapter);
1612
+ const owner = resolveOwner({ payload: payload?.raw ?? null, coordRoot });
1613
+ if (!owner) {
1614
+ appendDebug(coordRoot, { ...debugBase, skipped: "no-owner-resolved" });
1615
+ return 0;
1616
+ }
1617
+ const row = readLiveCoordinationRow(coordRoot, owner.instance_id);
1618
+ const name = sessionNameDisplayPending(row);
1619
+ const text = typeof payload?.raw.text === "string" ? payload.raw.text : "";
1620
+ const sighting = matchSessionNameDisplay(row, text);
1621
+ if (sighting) {
1622
+ stampSessionNameSeen(coordRoot, owner.instance_id, sighting.pending);
1623
+ appendDebug(coordRoot, {
1624
+ ...debugBase,
1625
+ effect: "session-name-display-stamped",
1626
+ session_name_pending: sighting.pending,
1627
+ ...(sighting.displayed === sighting.pending
1628
+ ? {}
1629
+ : { session_name_displayed: sighting.displayed, session_name_drift: true }),
1630
+ });
1631
+ } else if (!name) {
1632
+ // Nothing was owed. Distinct from a miss: conflating the two is what made
1633
+ // a latched Cursor session undiagnosable from this log.
1634
+ appendDebug(coordRoot, { ...debugBase, skipped: "no-pending-session-name" });
1635
+ } else {
1636
+ appendDebug(coordRoot, {
1637
+ ...debugBase,
1638
+ skipped: "session-name-block-absent",
1639
+ session_name_pending: name,
1640
+ reply_bytes: text.length,
1641
+ reply_leads_with_fence: /^\s*`{3,}/.test(text),
1642
+ });
1643
+ }
1644
+ if (ledgerRoute.state === "blocked") {
1645
+ appendDebug(coordRoot, {
1646
+ ...debugBase,
1647
+ skipped: "v3-control-blocked",
1648
+ reason: ledgerRoute.reason,
1649
+ owner_source: owner.source,
1650
+ });
1651
+ return 0;
1652
+ }
1653
+ const statusBoxPresentStrict = scanAssistantStatusBoxPresent(undefined, text);
1654
+ // The hook parser needs the completed reply long enough to scan it, but
1655
+ // durable hook intake must never retain that body. Keep every other native
1656
+ // field so turn correlation remains available to the producer.
1657
+ const { text: _completedReply, ...privacySafeRaw } = payload?.raw ?? {};
1658
+ const privacySafePayload = payload ? { ...payload, raw: privacySafeRaw } : null;
1659
+ const v3Result = recordLiveHookSignalV3({
1660
+ coordRoot,
1661
+ route: ledgerRoute,
1662
+ eventName,
1663
+ payload: privacySafePayload,
1664
+ adapter,
1665
+ instanceId: owner.instance_id,
1666
+ hook_name: eventName,
1667
+ hook_duration_ms: Math.max(0, Math.floor(performance.now() - hookStartedAt)),
1668
+ monotonic_ns: hookClock.monotonic_ns,
1669
+ turn_ritual: {
1670
+ status_box_present: statusBoxPresentStrict,
1671
+ status_box_present_strict: statusBoxPresentStrict,
1672
+ session_name_required: name !== undefined,
1673
+ session_name_present: name ? assistantTextStartsWithSessionNameBlock(text, name) : false,
1674
+ },
1675
+ });
1676
+ observeHookDebug(hookHealthState, { event_v3_state: v3Result.state });
1677
+ appendDebug(coordRoot, {
1678
+ ...debugBase,
1679
+ effect: "cursor-response-ritual-observed",
1680
+ owner_source: owner.source,
1681
+ event_v3_state: v3Result.state,
1682
+ status_box_present_strict: statusBoxPresentStrict,
1683
+ ...(v3Result.state === "observed"
1684
+ ? {
1685
+ generation_id: v3Result.generation_id,
1686
+ turn_id: v3Result.turn_id,
1687
+ response_observed_at: v3Result.observed_at,
1688
+ }
1689
+ : {}),
1690
+ });
1505
1691
  return 0;
1506
1692
  }
1507
1693
 
1508
- function scheduleCodexRuntimeContextRetry(coordRoot: string, nativeSessionId: string): void {
1694
+ function scheduleRuntimeContextRetry(
1695
+ coordRoot: string,
1696
+ adapter: Adapter,
1697
+ nativeSessionId: string,
1698
+ ): void {
1509
1699
  const modulePath = process.argv[1];
1510
1700
  if (!modulePath) return;
1511
1701
  try {
1512
1702
  const child = spawn(
1513
1703
  process.execPath,
1514
- [modulePath, "runtime-context-retry", "--adapter", "codex", "--session-id", nativeSessionId],
1704
+ [modulePath, "runtime-context-retry", "--adapter", adapter, "--session-id", nativeSessionId],
1515
1705
  {
1516
1706
  cwd: coordRoot,
1517
1707
  detached: true,
@@ -1531,7 +1721,7 @@ async function runRuntimeContextRetryWorker(
1531
1721
  extra: string[],
1532
1722
  ): Promise<number> {
1533
1723
  const nativeSessionId = extraArg(extra, "--session-id");
1534
- if (adapter !== "codex" || !nativeSessionId) return 0;
1724
+ if (!adapter || !adapterBehavior(adapter).runtimeContextRetry || !nativeSessionId) return 0;
1535
1725
  for (const [index, delayMs] of [400, 1_000].entries()) {
1536
1726
  await new Promise((resolve) => setTimeout(resolve, delayMs));
1537
1727
  const route = resolveLiveEventLedgerRouteV3(coordRoot);
@@ -1547,6 +1737,27 @@ async function runRuntimeContextRetryWorker(
1547
1737
  return 0;
1548
1738
  }
1549
1739
 
1740
+ function reportSessionNameObservationUnavailable(
1741
+ coordRoot: string,
1742
+ instanceId: string,
1743
+ adapter: Adapter,
1744
+ eventName: string,
1745
+ reason: string,
1746
+ ): void {
1747
+ appendDebug(coordRoot, {
1748
+ ts: new Date().toISOString(),
1749
+ event_name: eventName,
1750
+ adapter,
1751
+ instance_id: instanceId,
1752
+ effect: "session-name-observation-unavailable",
1753
+ reason,
1754
+ action: "fail-open",
1755
+ });
1756
+ process.stderr.write(
1757
+ `Harnery: session-name observation unavailable (${reason}); allowing this ${eventName} without display enforcement. The name remains pending.\n`,
1758
+ );
1759
+ }
1760
+
1550
1761
  async function enforcePendingSessionNameDisplay(
1551
1762
  coordRoot: string,
1552
1763
  instanceId: string,
@@ -1567,11 +1778,13 @@ async function enforcePendingSessionNameDisplay(
1567
1778
  }
1568
1779
 
1569
1780
  if (inspection.state === "unavailable") {
1570
- // PostToolUse already delivered the exact block for this mint. An
1571
- // unavailable transcript cannot become evidence, and repeating a generic
1572
- // reminder on every tool creates an unbounded Codex loop. Keep the latch
1573
- // honestly pending and fail open without another instruction. A later
1574
- // readable transcript can still verify it.
1781
+ reportSessionNameObservationUnavailable(
1782
+ coordRoot,
1783
+ instanceId,
1784
+ adapter,
1785
+ "pre-tool-use",
1786
+ inspection.reason,
1787
+ );
1575
1788
  return true;
1576
1789
  }
1577
1790
 
@@ -1605,21 +1818,20 @@ function inspectDisplayEvidence(
1605
1818
  payload: ParsedPayload | null,
1606
1819
  coordination: Heartbeat | null,
1607
1820
  ): SessionNameDisplayInspection {
1608
- if (adapter === "cursor" && payload?.agent_message !== undefined) {
1609
- return matchSessionNameDisplay(coordination, payload.agent_message)
1821
+ const behavior = adapterBehavior(adapter);
1822
+ const inlineText = behavior.inlineAssistantText?.(payload);
1823
+ if (inlineText !== undefined) {
1824
+ return matchSessionNameDisplay(coordination, inlineText)
1610
1825
  ? { state: "present" }
1611
1826
  : { state: "unavailable", reason: "transcript_not_ready" };
1612
1827
  }
1613
1828
 
1614
- // Codex hook payloads omit transcript_path on every event, which left this
1615
- // inspection permanently unavailable and the latch never stamped. Discover
1616
- // the rollout by session id; the scan only runs while a name is pending, so
1617
- // the latch closes after one success.
1829
+ // Some adapters omit transcript_path on every event, which left this
1830
+ // inspection permanently unavailable and the latch never stamped. The
1831
+ // behavior may discover the transcript by session id; the scan only runs
1832
+ // while a name is pending, so the latch closes after one success.
1618
1833
  const transcriptPath =
1619
- payload?.transcript_path ??
1620
- (adapter === "codex"
1621
- ? discoverCodexSessionTranscript(payload?.session_id ?? instanceId)
1622
- : undefined);
1834
+ payload?.transcript_path ?? behavior.discoverTranscript?.(payload?.session_id ?? instanceId);
1623
1835
 
1624
1836
  let verdict: SessionNameDisplayInspection = { state: "absent" };
1625
1837
  for (const candidate of sessionNameDisplayAcceptedNames(coordination)) {
@@ -1913,7 +2125,7 @@ function releaseClaimOnFailure(
1913
2125
  coordRoot,
1914
2126
  owner: instanceId,
1915
2127
  nativeSessionId: before?.session_id ?? instanceId,
1916
- adapter: adapterFromPlatform(before?.platform),
2128
+ adapter: sharedAdapterFromPlatform(before?.platform, { context: "hook-claim-release" }),
1917
2129
  operation: "released",
1918
2130
  path: canonical,
1919
2131
  access: "write",
@@ -1956,22 +2168,18 @@ async function emitUserPromptSubmitSystemMessage(
1956
2168
  recoveryBriefing = "",
1957
2169
  hostPromptContext = "",
1958
2170
  ): Promise<boolean> {
2171
+ const behavior = adapterBehavior(adapter);
1959
2172
  const { renderPromptContext } = await import("../agents/render/prompt-context.ts");
1960
2173
  let additionalContext = renderPromptContext({
1961
2174
  coordRoot,
1962
2175
  instanceId,
1963
2176
  sessionId,
1964
2177
  sessionNameNudge: true,
1965
- taskNudge: adapter === "cursor" || adapter === "codex",
1966
- hostPromptReminder: adapter === "codex" || adapter === "cursor",
1967
- statusFooterNudge: adapter === "codex",
1968
- turnRitualNudge: adapter === "claude-code" ? adapter : undefined,
2178
+ ...behavior.promptContextNudges,
1969
2179
  }).trim();
1970
- if (adapter === "codex") {
1971
- const fileLinkContext = renderCodexWslFileLinkContext(coordRoot, workspaceCwd);
1972
- if (fileLinkContext) {
1973
- additionalContext = [additionalContext, fileLinkContext].filter(Boolean).join("\n\n");
1974
- }
2180
+ const fileLinkContext = behavior.fileLinkContext?.(coordRoot, workspaceCwd) ?? "";
2181
+ if (fileLinkContext) {
2182
+ additionalContext = [additionalContext, fileLinkContext].filter(Boolean).join("\n\n");
1975
2183
  }
1976
2184
  if (recoveryBriefing) {
1977
2185
  additionalContext = [additionalContext, recoveryBriefing].filter(Boolean).join("\n\n");
@@ -2022,16 +2230,9 @@ async function emitSubagentStartContext(
2022
2230
  }
2023
2231
 
2024
2232
  function adapterPlatform(adapter: Adapter): string {
2025
- if (adapter === "claude-code") return "claude-code";
2026
2233
  return adapter;
2027
2234
  }
2028
2235
 
2029
- function adapterFromPlatform(platform: unknown): Adapter {
2030
- if (platform === "cursor") return "cursor";
2031
- if (platform === "codex") return "codex";
2032
- return "claude-code";
2033
- }
2034
-
2035
2236
  async function emitSessionStartSystemMessage(
2036
2237
  coordRoot: string,
2037
2238
  instanceId: string,
@@ -2041,6 +2242,7 @@ async function emitSessionStartSystemMessage(
2041
2242
  recoveryBriefing = "",
2042
2243
  backupCue = "",
2043
2244
  ): Promise<boolean> {
2245
+ const behavior = adapterBehavior(adapter);
2044
2246
  const workflowChild = coordEnv("WORKFLOW_CHILD") === "1";
2045
2247
  let additionalContext = "";
2046
2248
  // Opportunistic reconciliation makes normal session starts a failsafe for
@@ -2061,16 +2263,15 @@ async function emitSessionStartSystemMessage(
2061
2263
  instanceId,
2062
2264
  sessionId,
2063
2265
  agentName: agentName || undefined,
2064
- platformLabel:
2065
- adapter === "claude-code" ? undefined : adapter === "cursor" ? "Cursor" : "Codex",
2266
+ platformLabel: behavior.sessionStartPlatformLabel,
2066
2267
  }).trim();
2067
2268
  }
2068
2269
 
2069
- // Effect (claude-code): merge the journal recovery cue into the session-start
2070
- // context. Was a standalone additionalContext emission from the previous
2071
- // journal-on-start adapter; now that agent-hook is the single SessionStart
2072
- // entry, it folds in here.
2073
- if (adapter === "claude-code" && !workflowChild) {
2270
+ // Effect (journal-maintaining adapters): merge the journal recovery cue into
2271
+ // the session-start context. Was a standalone additionalContext emission
2272
+ // from the previous journal-on-start adapter; now that agent-hook is the
2273
+ // single SessionStart entry, it folds in here.
2274
+ if (behavior.journalMaintenance && !workflowChild) {
2074
2275
  const cue = journalRecoveryCue(coordRoot);
2075
2276
  if (cue) additionalContext = [additionalContext, cue].filter(Boolean).join("\n\n");
2076
2277
  }
@@ -2080,19 +2281,14 @@ async function emitSessionStartSystemMessage(
2080
2281
  if (backupCue && !workflowChild) {
2081
2282
  additionalContext = [additionalContext, backupCue].filter(Boolean).join("\n\n");
2082
2283
  }
2083
- if (adapter === "codex" && !workflowChild && isWslUncPath(emittedData.cwd)) {
2084
- const fileLinkContext = renderCodexWslFileLinkContext(coordRoot, emittedData.cwd);
2284
+ if (!workflowChild && behavior.bridgeFor(emittedData.cwd)) {
2285
+ const fileLinkContext = behavior.fileLinkContext?.(coordRoot, emittedData.cwd) ?? "";
2085
2286
  if (fileLinkContext) {
2086
2287
  additionalContext = [additionalContext, fileLinkContext].filter(Boolean).join("\n\n");
2087
2288
  }
2088
- const bridge = inspectCodexWslBridge(process.env, { expected: true });
2089
- if (bridge && !bridge.ok) {
2090
- additionalContext = [
2091
- additionalContext,
2092
- `Harnery hybrid warning: ${bridge.detail}. Run \`${resolveBinName(coordRoot)} doctor\` for the repair hint.`,
2093
- ]
2094
- .filter(Boolean)
2095
- .join("\n\n");
2289
+ const warning = behavior.bridgeWarning?.(coordRoot, emittedData.cwd);
2290
+ if (warning) {
2291
+ additionalContext = [additionalContext, warning].filter(Boolean).join("\n\n");
2096
2292
  }
2097
2293
  }
2098
2294
  const { emitContext } = await import("./adapter/output.ts");