harnery 0.40.0 → 0.42.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 (595) hide show
  1. package/README.md +1 -1
  2. package/dist/commander.js +4 -0
  3. package/dist/commands/agents.d.ts.map +1 -1
  4. package/dist/commands/agents.js +34 -20
  5. package/dist/commands/artifacts.d.ts.map +1 -1
  6. package/dist/commands/artifacts.js +113 -12
  7. package/dist/commands/browse-ai.d.ts.map +1 -1
  8. package/dist/commands/browse-ai.js +4 -0
  9. package/dist/commands/browse.d.ts +164 -0
  10. package/dist/commands/browse.d.ts.map +1 -1
  11. package/dist/commands/browse.js +388 -45
  12. package/dist/commands/checkpoint.js +8 -12
  13. package/dist/commands/claude-desktop.d.ts.map +1 -1
  14. package/dist/commands/claude-desktop.js +52 -0
  15. package/dist/commands/deinit.d.ts +1 -1
  16. package/dist/commands/deinit.d.ts.map +1 -1
  17. package/dist/commands/deinit.js +8 -4
  18. package/dist/commands/disk.d.ts +6 -0
  19. package/dist/commands/disk.d.ts.map +1 -0
  20. package/dist/commands/disk.js +80 -0
  21. package/dist/commands/docs.d.ts.map +1 -1
  22. package/dist/commands/docs.js +2 -0
  23. package/dist/commands/doctor.d.ts +8 -0
  24. package/dist/commands/doctor.d.ts.map +1 -1
  25. package/dist/commands/doctor.js +58 -3
  26. package/dist/commands/fetch.d.ts.map +1 -1
  27. package/dist/commands/fetch.js +10 -0
  28. package/dist/commands/files.d.ts +16 -0
  29. package/dist/commands/files.d.ts.map +1 -1
  30. package/dist/commands/files.js +54 -2
  31. package/dist/commands/init.d.ts +21 -5
  32. package/dist/commands/init.d.ts.map +1 -1
  33. package/dist/commands/init.js +271 -109
  34. package/dist/commands/instructions.js +5 -4
  35. package/dist/commands/ledger-v3.d.ts.map +1 -1
  36. package/dist/commands/ledger-v3.js +2 -1
  37. package/dist/commands/rm.d.ts +4 -0
  38. package/dist/commands/rm.d.ts.map +1 -0
  39. package/dist/commands/rm.js +87 -0
  40. package/dist/commands/servers.d.ts +7 -0
  41. package/dist/commands/servers.d.ts.map +1 -0
  42. package/dist/commands/servers.js +394 -0
  43. package/dist/commands/tunnel.d.ts.map +1 -1
  44. package/dist/commands/tunnel.js +82 -2
  45. package/dist/commands/webview.d.ts +4 -0
  46. package/dist/commands/webview.d.ts.map +1 -0
  47. package/dist/commands/webview.js +94 -0
  48. package/dist/core/adapter.d.ts +50 -1
  49. package/dist/core/adapter.d.ts.map +1 -1
  50. package/dist/core/adapter.js +59 -1
  51. package/dist/core/adapters/attestation.d.ts.map +1 -1
  52. package/dist/core/adapters/attestation.js +3 -2
  53. package/dist/core/adapters/profiles.d.ts +29 -3
  54. package/dist/core/adapters/profiles.d.ts.map +1 -1
  55. package/dist/core/adapters/profiles.js +53 -1
  56. package/dist/core/adapters/registry.d.ts.map +1 -1
  57. package/dist/core/adapters/registry.js +33 -0
  58. package/dist/core/adapters/types.d.ts +9 -0
  59. package/dist/core/adapters/types.d.ts.map +1 -1
  60. package/dist/core/agents/canonical-emit.d.ts +1 -1
  61. package/dist/core/agents/canonical-emit.d.ts.map +1 -1
  62. package/dist/core/agents/canonical-emit.js +2 -7
  63. package/dist/core/agents/cli.js +25 -16
  64. package/dist/core/agents/coord-client.d.ts.map +1 -1
  65. package/dist/core/agents/coord-client.js +7 -2
  66. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  67. package/dist/core/agents/render/session-context.js +2 -0
  68. package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
  69. package/dist/core/agents/rules/claim-conflict.js +3 -9
  70. package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
  71. package/dist/core/agents/rules/commit-conflict.js +2 -8
  72. package/dist/core/agents/rules/stop-hook.d.ts +2 -0
  73. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  74. package/dist/core/agents/rules/stop-hook.js +1 -1
  75. package/dist/core/agents/session-events.d.ts +6 -2
  76. package/dist/core/agents/session-events.d.ts.map +1 -1
  77. package/dist/core/agents/session-events.js +105 -26
  78. package/dist/core/agents/session-finalization-state-v3.d.ts.map +1 -1
  79. package/dist/core/agents/session-finalization-state-v3.js +2 -1
  80. package/dist/core/agents/session-finalizer-v3.d.ts.map +1 -1
  81. package/dist/core/agents/session-finalizer-v3.js +5 -4
  82. package/dist/core/agents/session-name-display.d.ts +28 -0
  83. package/dist/core/agents/session-name-display.d.ts.map +1 -1
  84. package/dist/core/agents/session-name-display.js +43 -0
  85. package/dist/core/agents/state/heartbeat-reader.d.ts +7 -0
  86. package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
  87. package/dist/core/agents/state/heartbeat-writer.d.ts +10 -0
  88. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  89. package/dist/core/agents/state/heartbeat-writer.js +21 -2
  90. package/dist/core/agents/state/live-coordination-view.d.ts +20 -0
  91. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  92. package/dist/core/agents/state/live-coordination-view.js +61 -0
  93. package/dist/core/agents/state/live-coordination-writer.js +3 -0
  94. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  95. package/dist/core/agents/state/stale-sweep.js +2 -10
  96. package/dist/core/agents/turn-changes.d.ts +17 -0
  97. package/dist/core/agents/turn-changes.d.ts.map +1 -0
  98. package/dist/core/agents/turn-changes.js +76 -0
  99. package/dist/core/artifacts/delivery-card.d.ts.map +1 -1
  100. package/dist/core/artifacts/delivery-card.js +11 -3
  101. package/dist/core/artifacts/index.d.ts +140 -4
  102. package/dist/core/artifacts/index.d.ts.map +1 -1
  103. package/dist/core/artifacts/index.js +526 -52
  104. package/dist/core/config.d.ts +73 -11
  105. package/dist/core/config.d.ts.map +1 -1
  106. package/dist/core/config.js +142 -3
  107. package/dist/core/context/index.d.ts.map +1 -1
  108. package/dist/core/context/index.js +2 -1
  109. package/dist/core/conversations/archive.d.ts.map +1 -1
  110. package/dist/core/conversations/archive.js +4 -3
  111. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  112. package/dist/core/diagnostics/bundle.js +5 -4
  113. package/dist/core/events/legacy-storage/canary.d.ts.map +1 -1
  114. package/dist/core/events/legacy-storage/canary.js +5 -4
  115. package/dist/core/events/legacy-storage/compression.d.ts.map +1 -1
  116. package/dist/core/events/legacy-storage/compression.js +2 -1
  117. package/dist/core/events/v3/adapter-id.d.ts +10 -2
  118. package/dist/core/events/v3/adapter-id.d.ts.map +1 -1
  119. package/dist/core/events/v3/adapter-id.js +16 -0
  120. package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
  121. package/dist/core/events/v3/authority-outbox.js +2 -1
  122. package/dist/core/events/v3/bootstrap.d.ts.map +1 -1
  123. package/dist/core/events/v3/bootstrap.js +5 -4
  124. package/dist/core/events/v3/capabilities.d.ts.map +1 -1
  125. package/dist/core/events/v3/capabilities.js +17 -0
  126. package/dist/core/events/v3/contract.d.ts +5 -0
  127. package/dist/core/events/v3/contract.d.ts.map +1 -1
  128. package/dist/core/events/v3/contract.js +7 -1
  129. package/dist/core/events/v3/control-witness.d.ts.map +1 -1
  130. package/dist/core/events/v3/control-witness.js +3 -2
  131. package/dist/core/events/v3/coordination-transaction-recovery.d.ts.map +1 -1
  132. package/dist/core/events/v3/coordination-transaction-recovery.js +4 -3
  133. package/dist/core/events/v3/fingerprint-keys.d.ts.map +1 -1
  134. package/dist/core/events/v3/fingerprint-keys.js +7 -6
  135. package/dist/core/events/v3/generated.d.ts +1 -1
  136. package/dist/core/events/v3/generated.js +1 -1
  137. package/dist/core/events/v3/live-feed.d.ts.map +1 -1
  138. package/dist/core/events/v3/live-feed.js +5 -4
  139. package/dist/core/events/v3/producers/command-recorder.d.ts.map +1 -1
  140. package/dist/core/events/v3/producers/command-recorder.js +8 -7
  141. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  142. package/dist/core/events/v3/producers/coordination-recorder.js +8 -7
  143. package/dist/core/events/v3/producers/diagnostic-summaries.d.ts.map +1 -1
  144. package/dist/core/events/v3/producers/diagnostic-summaries.js +5 -4
  145. package/dist/core/events/v3/producers/hook-base.d.ts +1 -0
  146. package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
  147. package/dist/core/events/v3/producers/hook-base.js +10 -4
  148. package/dist/core/events/v3/producers/hook.d.ts +2 -0
  149. package/dist/core/events/v3/producers/hook.d.ts.map +1 -1
  150. package/dist/core/events/v3/producers/hook.js +16 -1
  151. package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
  152. package/dist/core/events/v3/producers/intake.js +16 -6
  153. package/dist/core/events/v3/producers/recorder.d.ts +2 -4
  154. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  155. package/dist/core/events/v3/producers/recorder.js +125 -101
  156. package/dist/core/events/v3/projection.js +9 -1
  157. package/dist/core/events/v3/recovery.d.ts.map +1 -1
  158. package/dist/core/events/v3/recovery.js +4 -3
  159. package/dist/core/events/v3/support-storage/inventory.d.ts.map +1 -1
  160. package/dist/core/events/v3/support-storage/inventory.js +2 -1
  161. package/dist/core/events/v3/support-storage/maintenance.d.ts.map +1 -1
  162. package/dist/core/events/v3/support-storage/maintenance.js +4 -3
  163. package/dist/core/events/v3/support-storage/pack-reader.d.ts.map +1 -1
  164. package/dist/core/events/v3/support-storage/pack-reader.js +4 -3
  165. package/dist/core/events/v3/support-storage/pack-writer.d.ts.map +1 -1
  166. package/dist/core/events/v3/support-storage/pack-writer.js +6 -5
  167. package/dist/core/events/v3/tool-line-changes.d.ts +8 -0
  168. package/dist/core/events/v3/tool-line-changes.d.ts.map +1 -0
  169. package/dist/core/events/v3/tool-line-changes.js +135 -0
  170. package/dist/core/events/v3/writer.d.ts.map +1 -1
  171. package/dist/core/events/v3/writer.js +8 -7
  172. package/dist/core/governor/planning.d.ts.map +1 -1
  173. package/dist/core/governor/planning.js +6 -5
  174. package/dist/core/governor/service.d.ts.map +1 -1
  175. package/dist/core/governor/service.js +12 -11
  176. package/dist/core/governor/state.d.ts.map +1 -1
  177. package/dist/core/governor/state.js +7 -6
  178. package/dist/core/guard/evaluator.js +1 -0
  179. package/dist/core/guard/storage.d.ts.map +1 -1
  180. package/dist/core/guard/storage.js +3 -2
  181. package/dist/core/hooks/adapter/behaviors/claude-code.d.ts +4 -0
  182. package/dist/core/hooks/adapter/behaviors/claude-code.d.ts.map +1 -0
  183. package/dist/core/hooks/adapter/behaviors/claude-code.js +31 -0
  184. package/dist/core/hooks/adapter/behaviors/codex.d.ts +15 -0
  185. package/dist/core/hooks/adapter/behaviors/codex.d.ts.map +1 -0
  186. package/dist/core/hooks/adapter/behaviors/codex.js +82 -0
  187. package/dist/core/hooks/adapter/behaviors/cursor.d.ts +7 -0
  188. package/dist/core/hooks/adapter/behaviors/cursor.d.ts.map +1 -0
  189. package/dist/core/hooks/adapter/behaviors/cursor.js +35 -0
  190. package/dist/core/hooks/adapter/behaviors/index.d.ts +11 -0
  191. package/dist/core/hooks/adapter/behaviors/index.d.ts.map +1 -0
  192. package/dist/core/hooks/adapter/behaviors/index.js +20 -0
  193. package/dist/core/hooks/adapter/behaviors/openclaw.d.ts +7 -0
  194. package/dist/core/hooks/adapter/behaviors/openclaw.d.ts.map +1 -0
  195. package/dist/core/hooks/adapter/behaviors/openclaw.js +34 -0
  196. package/dist/core/hooks/adapter/behaviors/opencode.d.ts +7 -0
  197. package/dist/core/hooks/adapter/behaviors/opencode.d.ts.map +1 -0
  198. package/dist/core/hooks/adapter/behaviors/opencode.js +34 -0
  199. package/dist/core/hooks/adapter/behaviors/types.d.ts +89 -0
  200. package/dist/core/hooks/adapter/behaviors/types.d.ts.map +1 -0
  201. package/dist/core/hooks/adapter/behaviors/types.js +1 -0
  202. package/dist/core/hooks/adapter/detect.d.ts +1 -1
  203. package/dist/core/hooks/adapter/detect.d.ts.map +1 -1
  204. package/dist/core/hooks/adapter/detect.js +2 -3
  205. package/dist/core/hooks/adapter/events.d.ts +46 -1
  206. package/dist/core/hooks/adapter/events.d.ts.map +1 -1
  207. package/dist/core/hooks/adapter/events.js +36 -0
  208. package/dist/core/hooks/adapter/opencode-plugin.d.ts +60 -0
  209. package/dist/core/hooks/adapter/opencode-plugin.d.ts.map +1 -0
  210. package/dist/core/hooks/adapter/opencode-plugin.js +129 -0
  211. package/dist/core/hooks/adapter/output.d.ts +7 -0
  212. package/dist/core/hooks/adapter/output.d.ts.map +1 -1
  213. package/dist/core/hooks/adapter/output.js +8 -1
  214. package/dist/core/hooks/adapter/runtime-telemetry.js +3 -1
  215. package/dist/core/hooks/adapter/wiring.d.ts +52 -8
  216. package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
  217. package/dist/core/hooks/adapter/wiring.js +133 -12
  218. package/dist/core/hooks/cli.js +676 -531
  219. package/dist/core/hooks/effects/image-capture.d.ts.map +1 -1
  220. package/dist/core/hooks/effects/image-capture.js +2 -1
  221. package/dist/core/hooks/prompt-context/contract.d.ts.map +1 -1
  222. package/dist/core/hooks/prompt-context/contract.js +1 -0
  223. package/dist/core/hooks/resolve/transcript.d.ts +1 -1
  224. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  225. package/dist/core/hooks/resolve/transcript.js +35 -4
  226. package/dist/core/hooks/run-markers.d.ts +37 -0
  227. package/dist/core/hooks/run-markers.d.ts.map +1 -0
  228. package/dist/core/hooks/run-markers.js +109 -0
  229. package/dist/core/hooks/session-name-presence.d.ts +3 -1
  230. package/dist/core/hooks/session-name-presence.d.ts.map +1 -1
  231. package/dist/core/hooks/session-name-presence.js +7 -2
  232. package/dist/core/hooks/shell-waiter.d.ts +2 -0
  233. package/dist/core/hooks/shell-waiter.d.ts.map +1 -0
  234. package/dist/core/hooks/shell-waiter.js +173 -0
  235. package/dist/core/resources/service.d.ts.map +1 -1
  236. package/dist/core/resources/service.js +5 -4
  237. package/dist/core/semantic/review.d.ts.map +1 -1
  238. package/dist/core/semantic/review.js +5 -4
  239. package/dist/core/semantic/service.d.ts.map +1 -1
  240. package/dist/core/semantic/service.js +15 -14
  241. package/dist/core/semantic/storage.d.ts.map +1 -1
  242. package/dist/core/semantic/storage.js +5 -4
  243. package/dist/core/servers/index.d.ts +211 -0
  244. package/dist/core/servers/index.d.ts.map +1 -0
  245. package/dist/core/servers/index.js +560 -0
  246. package/dist/core/servers/net.d.ts +58 -0
  247. package/dist/core/servers/net.d.ts.map +1 -0
  248. package/dist/core/servers/net.js +270 -0
  249. package/dist/core/servers/tunnels.d.ts +14 -0
  250. package/dist/core/servers/tunnels.d.ts.map +1 -0
  251. package/dist/core/servers/tunnels.js +33 -0
  252. package/dist/core/storage/atomic-json.d.ts.map +1 -1
  253. package/dist/core/storage/atomic-json.js +7 -3
  254. package/dist/core/storage/builtins.js +1 -0
  255. package/dist/core/storage/durable-history.d.ts.map +1 -1
  256. package/dist/core/storage/durable-history.js +6 -5
  257. package/dist/core/storage/maintenance-providers.d.ts.map +1 -1
  258. package/dist/core/storage/maintenance-providers.js +2 -1
  259. package/dist/core/storage/maintenance.d.ts.map +1 -1
  260. package/dist/core/storage/maintenance.js +7 -6
  261. package/dist/core/storage/metrics.d.ts.map +1 -1
  262. package/dist/core/storage/metrics.js +5 -1
  263. package/dist/core/storage/modes.d.ts +17 -0
  264. package/dist/core/storage/modes.d.ts.map +1 -0
  265. package/dist/core/storage/modes.js +47 -0
  266. package/dist/core/storage/process-log.d.ts.map +1 -1
  267. package/dist/core/storage/process-log.js +3 -2
  268. package/dist/core/storage/receipts.d.ts.map +1 -1
  269. package/dist/core/storage/receipts.js +4 -3
  270. package/dist/core/storage/segments.d.ts.map +1 -1
  271. package/dist/core/storage/segments.js +7 -6
  272. package/dist/core/supervisor/hook-health.d.ts +2 -1
  273. package/dist/core/supervisor/hook-health.d.ts.map +1 -1
  274. package/dist/core/supervisor/hook-health.js +4 -3
  275. package/dist/core/supervisor/service.d.ts.map +1 -1
  276. package/dist/core/supervisor/service.js +7 -6
  277. package/dist/core/work/state.d.ts.map +1 -1
  278. package/dist/core/work/state.js +8 -7
  279. package/dist/core/workflow/adapters.d.ts +7 -0
  280. package/dist/core/workflow/adapters.d.ts.map +1 -1
  281. package/dist/core/workflow/adapters.js +9 -0
  282. package/dist/core/workflow/approvals.d.ts.map +1 -1
  283. package/dist/core/workflow/approvals.js +6 -5
  284. package/dist/core/workflow/billing.d.ts.map +1 -1
  285. package/dist/core/workflow/billing.js +33 -4
  286. package/dist/core/workflow/durable-record.d.ts.map +1 -1
  287. package/dist/core/workflow/durable-record.js +4 -3
  288. package/dist/core/workflow/engine.d.ts.map +1 -1
  289. package/dist/core/workflow/engine.js +30 -1
  290. package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
  291. package/dist/core/workflow/live-session-v3.js +2 -3
  292. package/dist/core/workflow/run-state.d.ts.map +1 -1
  293. package/dist/core/workflow/run-state.js +3 -2
  294. package/dist/core/workflow/sandbox-projection.d.ts +13 -2
  295. package/dist/core/workflow/sandbox-projection.d.ts.map +1 -1
  296. package/dist/core/workflow/sandbox-projection.js +23 -0
  297. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  298. package/dist/core/workflow/spawn-claude.js +6 -1
  299. package/dist/core/workflow/spawn-codex.d.ts +2 -0
  300. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  301. package/dist/core/workflow/spawn-codex.js +20 -1
  302. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  303. package/dist/core/workflow/spawn-cursor.js +6 -1
  304. package/dist/core/workflow/spawn-opencode.d.ts +36 -0
  305. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -0
  306. package/dist/core/workflow/spawn-opencode.js +135 -0
  307. package/dist/core/workflow/transcript.d.ts.map +1 -1
  308. package/dist/core/workflow/transcript.js +3 -2
  309. package/dist/core/workflow/types.d.ts +47 -2
  310. package/dist/core/workflow/types.d.ts.map +1 -1
  311. package/dist/core/workflow/worker-access.d.ts +92 -0
  312. package/dist/core/workflow/worker-access.d.ts.map +1 -0
  313. package/dist/core/workflow/worker-access.js +450 -0
  314. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  315. package/dist/core/workflow/workspaces/leases.js +5 -4
  316. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  317. package/dist/core/workflow/workspaces/local-git.js +3 -2
  318. package/dist/core/workflow/workspaces/paths.d.ts.map +1 -1
  319. package/dist/core/workflow/workspaces/paths.js +2 -1
  320. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  321. package/dist/core/workflow/workspaces/state.js +10 -9
  322. package/dist/lazy-commands.js +8 -1
  323. package/dist/lib/agent-browser/client.d.ts +9 -0
  324. package/dist/lib/agent-browser/client.d.ts.map +1 -1
  325. package/dist/lib/agent-browser/client.js +13 -0
  326. package/dist/lib/browser/batch-steps.d.ts +18 -0
  327. package/dist/lib/browser/batch-steps.d.ts.map +1 -0
  328. package/dist/lib/browser/batch-steps.js +82 -0
  329. package/dist/lib/browser/client.d.ts +35 -1
  330. package/dist/lib/browser/client.d.ts.map +1 -1
  331. package/dist/lib/browser/client.js +117 -8
  332. package/dist/lib/browser/collect/collector.d.ts +99 -0
  333. package/dist/lib/browser/collect/collector.d.ts.map +1 -0
  334. package/dist/lib/browser/collect/collector.js +202 -0
  335. package/dist/lib/browser/collect/fields.d.ts +31 -0
  336. package/dist/lib/browser/collect/fields.d.ts.map +1 -0
  337. package/dist/lib/browser/collect/fields.js +75 -0
  338. package/dist/lib/browser/collect/index.d.ts +6 -0
  339. package/dist/lib/browser/collect/index.d.ts.map +1 -0
  340. package/dist/lib/browser/collect/index.js +5 -0
  341. package/dist/lib/browser/collect/page-driver.d.ts +14 -0
  342. package/dist/lib/browser/collect/page-driver.d.ts.map +1 -0
  343. package/dist/lib/browser/collect/page-driver.js +306 -0
  344. package/dist/lib/browser/collect/presets.d.ts +52 -0
  345. package/dist/lib/browser/collect/presets.d.ts.map +1 -0
  346. package/dist/lib/browser/collect/presets.js +176 -0
  347. package/dist/lib/browser/collect/scroll-plan.d.ts +85 -0
  348. package/dist/lib/browser/collect/scroll-plan.d.ts.map +1 -0
  349. package/dist/lib/browser/collect/scroll-plan.js +118 -0
  350. package/dist/lib/browser/identity.d.ts +25 -0
  351. package/dist/lib/browser/identity.d.ts.map +1 -0
  352. package/dist/lib/browser/identity.js +66 -0
  353. package/dist/lib/browser/index.d.ts +3 -2
  354. package/dist/lib/browser/index.d.ts.map +1 -1
  355. package/dist/lib/browser/index.js +2 -1
  356. package/dist/lib/browser/launch-args.d.ts +26 -0
  357. package/dist/lib/browser/launch-args.d.ts.map +1 -1
  358. package/dist/lib/browser/launch-args.js +41 -1
  359. package/dist/lib/browser/page-review-judge.d.ts +2 -0
  360. package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
  361. package/dist/lib/browser/page-review-judge.js +13 -0
  362. package/dist/lib/browser/qa-run-contracts.d.ts +10 -0
  363. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
  364. package/dist/lib/browser/qa-run.d.ts.map +1 -1
  365. package/dist/lib/browser/qa-run.js +1 -0
  366. package/dist/lib/browser/user-agent.d.ts +53 -0
  367. package/dist/lib/browser/user-agent.d.ts.map +1 -0
  368. package/dist/lib/browser/user-agent.js +79 -0
  369. package/dist/lib/browser/windows-chrome.d.ts +59 -0
  370. package/dist/lib/browser/windows-chrome.d.ts.map +1 -0
  371. package/dist/lib/browser/windows-chrome.js +179 -0
  372. package/dist/lib/claude-desktop-share.d.ts +136 -0
  373. package/dist/lib/claude-desktop-share.d.ts.map +1 -0
  374. package/dist/lib/claude-desktop-share.js +394 -0
  375. package/dist/lib/disk-usage.d.ts +50 -0
  376. package/dist/lib/disk-usage.d.ts.map +1 -0
  377. package/dist/lib/disk-usage.js +303 -0
  378. package/dist/lib/docs-lint.d.ts +6 -0
  379. package/dist/lib/docs-lint.d.ts.map +1 -1
  380. package/dist/lib/docs-lint.js +8 -3
  381. package/dist/lib/docs-metadata-managed.d.ts +1 -0
  382. package/dist/lib/docs-metadata-managed.d.ts.map +1 -1
  383. package/dist/lib/docs-metadata-managed.js +17 -1
  384. package/dist/lib/guarded-remove.d.ts +34 -0
  385. package/dist/lib/guarded-remove.d.ts.map +1 -0
  386. package/dist/lib/guarded-remove.js +310 -0
  387. package/dist/lib/identities/assume.d.ts.map +1 -1
  388. package/dist/lib/identities/assume.js +4 -9
  389. package/dist/lib/instructions/apply.d.ts.map +1 -1
  390. package/dist/lib/instructions/apply.js +2 -0
  391. package/dist/lib/instructions/templates.js +8 -8
  392. package/dist/lib/pace/index.d.ts +2 -0
  393. package/dist/lib/pace/index.d.ts.map +1 -0
  394. package/dist/lib/pace/index.js +1 -0
  395. package/dist/lib/pace/pace.d.ts +95 -0
  396. package/dist/lib/pace/pace.d.ts.map +1 -0
  397. package/dist/lib/pace/pace.js +313 -0
  398. package/dist/lib/tunnel/error-page.d.ts +1 -1
  399. package/dist/lib/tunnel/error-page.d.ts.map +1 -1
  400. package/dist/lib/tunnel/error-page.js +19 -8
  401. package/dist/lib/tunnel/forward-headers.d.ts +11 -0
  402. package/dist/lib/tunnel/forward-headers.d.ts.map +1 -0
  403. package/dist/lib/tunnel/forward-headers.js +17 -0
  404. package/dist/lib/tunnel/gate.js +34 -10
  405. package/dist/lib/tunnel/path-scope.d.ts +22 -0
  406. package/dist/lib/tunnel/path-scope.d.ts.map +1 -0
  407. package/dist/lib/tunnel/path-scope.js +62 -0
  408. package/dist/lib/tunnel/state.d.ts +5 -0
  409. package/dist/lib/tunnel/state.d.ts.map +1 -1
  410. package/dist/lib/tunnel/state.js +17 -3
  411. package/dist/lib/webview/index.d.ts +2 -0
  412. package/dist/lib/webview/index.d.ts.map +1 -0
  413. package/dist/lib/webview/index.js +1 -0
  414. package/dist/lib/webview/webview.d.ts +116 -0
  415. package/dist/lib/webview/webview.d.ts.map +1 -0
  416. package/dist/lib/webview/webview.js +289 -0
  417. package/opencode-plugin/package.json +19 -0
  418. package/opencode-plugin/src/index.ts +791 -0
  419. package/package.json +16 -4
  420. package/schemas/config.schema.json +104 -0
  421. package/schemas/event-v3.schema.json +164 -0
  422. package/src/commander.ts +26 -0
  423. package/src/commands/agents.ts +47 -18
  424. package/src/commands/artifacts.ts +171 -19
  425. package/src/commands/browse-ai.ts +8 -0
  426. package/src/commands/browse.ts +570 -48
  427. package/src/commands/checkpoint.ts +7 -9
  428. package/src/commands/claude-desktop.ts +61 -0
  429. package/src/commands/deinit.ts +9 -4
  430. package/src/commands/disk.ts +114 -0
  431. package/src/commands/docs.ts +2 -0
  432. package/src/commands/doctor.ts +63 -3
  433. package/src/commands/fetch.ts +14 -0
  434. package/src/commands/files.ts +93 -2
  435. package/src/commands/init.ts +334 -122
  436. package/src/commands/instructions.ts +8 -4
  437. package/src/commands/ledger-v3.ts +2 -1
  438. package/src/commands/rm.ts +115 -0
  439. package/src/commands/servers.ts +506 -0
  440. package/src/commands/tunnel.ts +102 -2
  441. package/src/commands/webview.ts +159 -0
  442. package/src/core/adapter.ts +102 -1
  443. package/src/core/adapters/attestation.ts +3 -2
  444. package/src/core/adapters/profiles.ts +63 -1
  445. package/src/core/adapters/registry.ts +37 -0
  446. package/src/core/adapters/types.ts +10 -0
  447. package/src/core/agents/canonical-emit.ts +2 -5
  448. package/src/core/agents/cli.ts +26 -17
  449. package/src/core/agents/coord-client.ts +9 -3
  450. package/src/core/agents/render/session-context.ts +1 -0
  451. package/src/core/agents/rules/claim-conflict.ts +3 -8
  452. package/src/core/agents/rules/commit-conflict.ts +2 -7
  453. package/src/core/agents/rules/stop-hook.ts +3 -1
  454. package/src/core/agents/session-events.ts +120 -26
  455. package/src/core/agents/session-finalization-state-v3.ts +2 -1
  456. package/src/core/agents/session-finalizer-v3.ts +5 -4
  457. package/src/core/agents/session-name-display.ts +48 -0
  458. package/src/core/agents/state/heartbeat-reader.ts +7 -0
  459. package/src/core/agents/state/heartbeat-writer.ts +24 -2
  460. package/src/core/agents/state/live-coordination-view.ts +74 -0
  461. package/src/core/agents/state/live-coordination-writer.ts +3 -0
  462. package/src/core/agents/state/stale-sweep.ts +2 -9
  463. package/src/core/agents/turn-changes.ts +97 -0
  464. package/src/core/artifacts/delivery-card.ts +12 -3
  465. package/src/core/artifacts/index.ts +701 -47
  466. package/src/core/config.ts +206 -4
  467. package/src/core/context/index.ts +2 -1
  468. package/src/core/conversations/archive.ts +4 -3
  469. package/src/core/diagnostics/bundle.ts +5 -3
  470. package/src/core/events/legacy-storage/canary.ts +5 -4
  471. package/src/core/events/legacy-storage/compression.ts +2 -1
  472. package/src/core/events/v3/adapter-id.ts +29 -1
  473. package/src/core/events/v3/authority-outbox.ts +2 -1
  474. package/src/core/events/v3/bootstrap.ts +5 -4
  475. package/src/core/events/v3/capabilities.ts +17 -0
  476. package/src/core/events/v3/contract.ts +9 -1
  477. package/src/core/events/v3/control-witness.ts +3 -2
  478. package/src/core/events/v3/coordination-transaction-recovery.ts +4 -3
  479. package/src/core/events/v3/fingerprint-keys.ts +7 -6
  480. package/src/core/events/v3/generated.ts +1 -1
  481. package/src/core/events/v3/live-feed.ts +5 -4
  482. package/src/core/events/v3/producers/command-recorder.ts +8 -6
  483. package/src/core/events/v3/producers/coordination-recorder.ts +8 -7
  484. package/src/core/events/v3/producers/diagnostic-summaries.ts +5 -4
  485. package/src/core/events/v3/producers/hook-base.ts +11 -4
  486. package/src/core/events/v3/producers/hook.ts +19 -1
  487. package/src/core/events/v3/producers/intake.ts +18 -6
  488. package/src/core/events/v3/producers/recorder.ts +139 -114
  489. package/src/core/events/v3/projection.ts +11 -1
  490. package/src/core/events/v3/recovery.ts +4 -3
  491. package/src/core/events/v3/support-storage/inventory.ts +2 -1
  492. package/src/core/events/v3/support-storage/maintenance.ts +8 -3
  493. package/src/core/events/v3/support-storage/pack-reader.ts +4 -3
  494. package/src/core/events/v3/support-storage/pack-writer.ts +6 -5
  495. package/src/core/events/v3/tool-line-changes.ts +139 -0
  496. package/src/core/events/v3/writer.ts +8 -7
  497. package/src/core/governor/planning.ts +6 -5
  498. package/src/core/governor/service.ts +12 -11
  499. package/src/core/governor/state.ts +7 -6
  500. package/src/core/guard/evaluator.ts +1 -0
  501. package/src/core/guard/storage.ts +3 -2
  502. package/src/core/hooks/adapter/behaviors/claude-code.ts +33 -0
  503. package/src/core/hooks/adapter/behaviors/codex.ts +96 -0
  504. package/src/core/hooks/adapter/behaviors/cursor.ts +37 -0
  505. package/src/core/hooks/adapter/behaviors/index.ts +31 -0
  506. package/src/core/hooks/adapter/behaviors/openclaw.ts +36 -0
  507. package/src/core/hooks/adapter/behaviors/opencode.ts +36 -0
  508. package/src/core/hooks/adapter/behaviors/types.ts +110 -0
  509. package/src/core/hooks/adapter/detect.ts +2 -3
  510. package/src/core/hooks/adapter/events.ts +72 -1
  511. package/src/core/hooks/adapter/opencode-plugin.ts +177 -0
  512. package/src/core/hooks/adapter/output.ts +8 -1
  513. package/src/core/hooks/adapter/runtime-telemetry.ts +3 -1
  514. package/src/core/hooks/adapter/wiring.ts +198 -16
  515. package/src/core/hooks/cli.ts +776 -539
  516. package/src/core/hooks/effects/image-capture.ts +2 -1
  517. package/src/core/hooks/prompt-context/contract.ts +1 -0
  518. package/src/core/hooks/resolve/transcript.ts +42 -5
  519. package/src/core/hooks/run-markers.ts +132 -0
  520. package/src/core/hooks/session-name-presence.ts +10 -3
  521. package/src/core/hooks/shell-waiter.ts +166 -0
  522. package/src/core/resources/service.ts +5 -4
  523. package/src/core/semantic/review.ts +5 -4
  524. package/src/core/semantic/service.ts +15 -14
  525. package/src/core/semantic/storage.ts +5 -4
  526. package/src/core/servers/index.ts +712 -0
  527. package/src/core/servers/net.ts +283 -0
  528. package/src/core/servers/tunnels.ts +39 -0
  529. package/src/core/storage/atomic-json.ts +7 -3
  530. package/src/core/storage/builtins.ts +1 -0
  531. package/src/core/storage/durable-history.ts +6 -5
  532. package/src/core/storage/maintenance-providers.ts +2 -1
  533. package/src/core/storage/maintenance.ts +7 -6
  534. package/src/core/storage/metrics.ts +5 -1
  535. package/src/core/storage/modes.ts +53 -0
  536. package/src/core/storage/process-log.ts +3 -2
  537. package/src/core/storage/receipts.ts +4 -3
  538. package/src/core/storage/segments.ts +7 -6
  539. package/src/core/supervisor/hook-health.ts +4 -4
  540. package/src/core/supervisor/service.ts +7 -6
  541. package/src/core/work/state.ts +8 -7
  542. package/src/core/workflow/adapters.ts +15 -0
  543. package/src/core/workflow/approvals.ts +6 -5
  544. package/src/core/workflow/billing.ts +34 -4
  545. package/src/core/workflow/durable-record.ts +4 -3
  546. package/src/core/workflow/engine.ts +38 -1
  547. package/src/core/workflow/live-session-v3.ts +2 -3
  548. package/src/core/workflow/run-state.ts +3 -2
  549. package/src/core/workflow/sandbox-projection.ts +38 -2
  550. package/src/core/workflow/spawn-claude.ts +5 -1
  551. package/src/core/workflow/spawn-codex.ts +18 -1
  552. package/src/core/workflow/spawn-cursor.ts +5 -1
  553. package/src/core/workflow/spawn-opencode.ts +153 -0
  554. package/src/core/workflow/transcript.ts +3 -2
  555. package/src/core/workflow/types.ts +62 -2
  556. package/src/core/workflow/worker-access.ts +525 -0
  557. package/src/core/workflow/workspaces/leases.ts +5 -4
  558. package/src/core/workflow/workspaces/local-git.ts +3 -2
  559. package/src/core/workflow/workspaces/paths.ts +2 -1
  560. package/src/core/workflow/workspaces/state.ts +10 -9
  561. package/src/lazy-commands.ts +11 -1
  562. package/src/lib/agent-browser/client.ts +18 -0
  563. package/src/lib/browser/batch-steps.ts +79 -0
  564. package/src/lib/browser/client.ts +147 -9
  565. package/src/lib/browser/collect/collector.ts +306 -0
  566. package/src/lib/browser/collect/fields.ts +87 -0
  567. package/src/lib/browser/collect/index.ts +47 -0
  568. package/src/lib/browser/collect/page-driver.ts +327 -0
  569. package/src/lib/browser/collect/presets.ts +237 -0
  570. package/src/lib/browser/collect/scroll-plan.ts +179 -0
  571. package/src/lib/browser/identity.ts +64 -0
  572. package/src/lib/browser/index.ts +16 -1
  573. package/src/lib/browser/launch-args.ts +53 -1
  574. package/src/lib/browser/page-review-judge.ts +16 -0
  575. package/src/lib/browser/qa-run-contracts.ts +11 -0
  576. package/src/lib/browser/qa-run.ts +1 -0
  577. package/src/lib/browser/user-agent.ts +130 -0
  578. package/src/lib/browser/windows-chrome.ts +227 -0
  579. package/src/lib/claude-desktop-share.ts +511 -0
  580. package/src/lib/disk-usage.ts +347 -0
  581. package/src/lib/docs-lint.ts +8 -3
  582. package/src/lib/docs-metadata-managed.ts +21 -0
  583. package/src/lib/guarded-remove.ts +376 -0
  584. package/src/lib/identities/assume.ts +7 -7
  585. package/src/lib/instructions/apply.ts +2 -0
  586. package/src/lib/instructions/templates.ts +8 -8
  587. package/src/lib/pace/index.ts +14 -0
  588. package/src/lib/pace/pace.ts +370 -0
  589. package/src/lib/tunnel/error-page.ts +25 -9
  590. package/src/lib/tunnel/forward-headers.ts +16 -0
  591. package/src/lib/tunnel/gate.ts +39 -10
  592. package/src/lib/tunnel/path-scope.ts +73 -0
  593. package/src/lib/tunnel/state.ts +20 -3
  594. package/src/lib/webview/index.ts +13 -0
  595. package/src/lib/webview/webview.ts +457 -0
@@ -21,12 +21,13 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
21
21
  import { dirname, join } from "node:path";
22
22
  import { coordEnv } from "../../lib/env.js";
23
23
  import { buildInstructionBundle } from "../../lib/instructions/bundle.js";
24
+ import { adapterFromPlatform as sharedAdapterFromPlatform } from "../adapter.js";
24
25
  import { resolveCoordRoot } from "../agents/coord-client.js";
25
26
  import { classifyWriteClaimFinalization, formatWriteClaimFinalizationDenial, } from "../agents/finalization.js";
26
27
  import { recordLiveClaimChangeV3, restoreLiveCoordinationStateAfterEpochV3, } from "../agents/live-authority-v3.js";
27
28
  import { evaluateStopHook, STOP_REMEDIATION_MARKER } from "../agents/rules/stop-hook.js";
28
- import { assistantTextStartsWithSessionNameBlock, isSessionNameRemediationCommand, matchSessionNameDisplay, sessionNameDisplayAcceptedNames, sessionNameDisplayInstruction, sessionNameDisplayPending, sessionNameDisplayRecoveryInstruction, toolResponseMintedSessionName, } from "../agents/session-name-display.js";
29
- import { readHeartbeat, setAssignedNameCache, stampSessionNameRequested, stampSessionNameSeen, } from "../agents/state/heartbeat-writer.js";
29
+ import { assistantTextStartsWithSessionNameBlock, isSessionNameRemediationCommand, matchSessionNameDisplay, sessionNameDisplayAcceptedNames, sessionNameDisplayInstruction, sessionNameDisplayPending, sessionNameDisplayRecoveryInstruction, sessionNameDisplayReminder, sessionNameDisplayReminderDue, toolResponseMintedSessionName, } from "../agents/session-name-display.js";
30
+ import { readHeartbeat, setAssignedNameCache, stampSessionNameReminded, stampSessionNameRequested, stampSessionNameSeen, } from "../agents/state/heartbeat-writer.js";
30
31
  import { readLiveCoordinationRow } from "../agents/state/live-coordination-view.js";
31
32
  import { ensureLiveCoordinationHeartbeat } from "../agents/state/live-coordination-writer.js";
32
33
  import { assignName } from "../agents/state/names.js";
@@ -39,11 +40,12 @@ import { captureSpanClockV3 } from "../events/v3/span-state.js";
39
40
  import { ensureRelayDaemon, fetchPresence, publishPresence } from "../presence/index.js";
40
41
  import { closeProcessLoggers, legacyLogFields, processLogger } from "../storage/logger.js";
41
42
  import { stableScopeId } from "../workflow/scope-id.js";
43
+ import { adapterBehavior } from "./adapter/behaviors/index.js";
42
44
  import { detectAdapter, shouldSkipHookAdapter } from "./adapter/detect.js";
45
+ import { ADAPTER_SPECS } from "./adapter/events.js";
43
46
  import { extractBashCommand, extractToolDescription, normalizeEventName, parsePayload, } from "./adapter/parse.js";
44
- import { discoverCodexSessionTranscript } from "./adapter/runtime-telemetry.js";
47
+ import { hookTimeoutFor } from "./adapter/wiring.js";
45
48
  import { scheduleBackupSnapshot } from "./backup-schedule.js";
46
- import { codexWslFileLinkTelemetry, inspectCodexWslBridge, isWslUncPath, renderCodexWslFileLinkContext, } from "./codex-wsl-bridge.js";
47
49
  import { captureImages, detectPresence, imageJanitor, journalArchive, journalJanitor, journalRecoveryCue, playSound, recordImageArtifactsV3, resetSoundCounters, runSessionSyncExtension, soundForEvent, } from "./effects/index.js";
48
50
  import { toolInputHash, toolTargetHash } from "./events/input-hash.js";
49
51
  import { canonicalize } from "./guard-path.js";
@@ -51,10 +53,12 @@ import { beginHookHealth, observeHookDebug, observeHookError, writeHookHealthCom
51
53
  import { adapterPidFromEnv, parsePsChainLine, selectAnchorPid } from "./resolve/anchor.js";
52
54
  import { extractIntentComment, resolveIntent } from "./resolve/intent.js";
53
55
  import { resolveOwner } from "./resolve/owner.js";
54
- import { inspectSessionNameDisplayImmediately, scanAssistantStatusBoxPresent, scanSessionNameDisplayedImmediately, scanStatusBoxPresent, scanTranscriptRuntime, } from "./resolve/transcript.js";
56
+ import { inspectSessionNameDisplayImmediately, scanAssistantStatusBoxPresent, scanStatusBoxPresent, scanTranscriptRuntime, } from "./resolve/transcript.js";
57
+ import { clearHookRunMarker, writeHookRunMarker } from "./run-markers.js";
55
58
  import { sessionNamePresence } from "./session-name-presence.js";
56
- import { unsafeCrossShellReason } from "./unsafe-cross-shell.js";
59
+ import { shellWaiterReason } from "./shell-waiter.js";
57
60
  let hookHealthState;
61
+ let hookRunMarkerPath = null;
58
62
  function parseArgv(argv) {
59
63
  const out = { eventName: null, extra: [] };
60
64
  for (let i = 0; i < argv.length; i++) {
@@ -223,11 +227,9 @@ function buildEventData(eventType, ctx) {
223
227
  const p = ctx.payload;
224
228
  switch (eventType) {
225
229
  case "session.started": {
226
- const adapterPlatform = ctx.adapter === "claude-code"
227
- ? "claude-code"
228
- : ctx.adapter === "cursor"
229
- ? "cursor"
230
- : "codex";
230
+ // The platform label is the adapter id itself; enumerating and defaulting
231
+ // to codex silently mislabels any adapter not in the list.
232
+ const adapterPlatform = ctx.adapter;
231
233
  // Recorded fork lineage is NOT detected here. On claude-code a fork
232
234
  // never fires its own session.started — SessionStart fires under the
233
235
  // PARENT's session id (source=resume) before the fork id is minted
@@ -238,7 +240,11 @@ function buildEventData(eventType, ctx) {
238
240
  const forkedFrom = undefined;
239
241
  // Assign (or recover) name + kind in-process. Idempotent: resume
240
242
  // returns the original name; new owner consumes a counter slot.
241
- const assigned = assignNameInProcess(ctx.coordRoot, ctx.instanceId, "session", forkedFrom);
243
+ // Workflow children stay unnamed, as on the tool path: a pool name
244
+ // would make them the addressee of mail held for that name.
245
+ const assigned = coordEnv("WORKFLOW_CHILD") === "1"
246
+ ? undefined
247
+ : assignNameInProcess(ctx.coordRoot, ctx.instanceId, "session", forkedFrom);
242
248
  // Write the adapter pid-map row so `harn agents whoami` ppid-walks find
243
249
  // this owner. Prefer the payload pid (the actual claude binary), then the
244
250
  // anchor walk (the `node` ancestor for Cursor, which has no payload pid),
@@ -301,7 +307,8 @@ function buildEventData(eventType, ctx) {
301
307
  // SessionStart. Mint durable display identity here so a later
302
308
  // SessionStart remains idempotent and Codec is not stuck on the
303
309
  // 8-character native id.
304
- if (!readLiveCoordinationRow(ctx.coordRoot, ctx.instanceId)?.name) {
310
+ if (coordEnv("WORKFLOW_CHILD") !== "1" &&
311
+ !readLiveCoordinationRow(ctx.coordRoot, ctx.instanceId)?.name) {
305
312
  assignNameInProcess(ctx.coordRoot, ctx.instanceId, "session");
306
313
  }
307
314
  const prompt = p?.prompt ?? "";
@@ -310,9 +317,7 @@ function buildEventData(eventType, ctx) {
310
317
  }
311
318
  case "turn.completed": {
312
319
  const lastAssistantMessage = p?.raw.last_assistant_message ?? "";
313
- const fileLinkTelemetry = ctx.adapter === "codex"
314
- ? codexWslFileLinkTelemetry(ctx.coordRoot, p?.cwd, lastAssistantMessage)
315
- : null;
320
+ const fileLinkTelemetry = adapterBehavior(ctx.adapter).fileLinkTelemetry?.(ctx.coordRoot, p?.cwd, lastAssistantMessage) ?? null;
316
321
  return {
317
322
  // Backfill the model for adapters that omit it at session.started
318
323
  // (Claude Code). The transcript is populated with assistant turns by
@@ -343,13 +348,12 @@ function buildEventData(eventType, ctx) {
343
348
  // and which name that refers to. Reported on every stop while a name
344
349
  // exists (the stop-hook.session_name verdict reads it per turn); the
345
350
  // transcript scan itself stops once the name has been sighted.
346
- ...sessionNamePresence(ctx.coordRoot, ctx.instanceId, (name) => scanSessionNameDisplayedImmediately(
347
- // Codex stops carry no transcript_path; discover the rollout by
348
- // session id so the naming ritual can verify and stamp there too.
351
+ ...sessionNamePresence(ctx.coordRoot, ctx.instanceId, (name) => inspectSessionNameDisplayImmediately(
352
+ // Some adapters omit transcript_path on stop; the behavior may
353
+ // discover the transcript by session id so the naming ritual can
354
+ // verify and stamp there too.
349
355
  p?.transcript_path ??
350
- (ctx.adapter === "codex"
351
- ? discoverCodexSessionTranscript(p?.session_id ?? ctx.instanceId)
352
- : undefined), name, assistantTextStartsWithSessionNameBlock)),
356
+ adapterBehavior(ctx.adapter).discoverTranscript?.(p?.session_id ?? ctx.instanceId), name, assistantTextStartsWithSessionNameBlock)),
353
357
  ...(fileLinkTelemetry ?? {}),
354
358
  stop_hook_active: p?.stop_hook_active,
355
359
  };
@@ -479,6 +483,24 @@ async function main() {
479
483
  const hookClock = captureSpanClockV3();
480
484
  const { eventName, extra } = parseArgv(process.argv.slice(2));
481
485
  const adapter = detectAdapter(process.argv.slice(2));
486
+ const behavior = adapter ? adapterBehavior(adapter) : null;
487
+ // Adapters that kill a hook at its timeout get a start marker, so a killed
488
+ // run leaves evidence for doctor (ADR 0197). Written before stdin is read.
489
+ const markerSpec = adapter ? ADAPTER_SPECS[adapter] : undefined;
490
+ const markerTimeout = markerSpec && eventName && markerSpec.events.some((e) => e.subcommand === eventName)
491
+ ? hookTimeoutFor(markerSpec, eventName)
492
+ : undefined;
493
+ if (markerTimeout !== undefined && adapter && eventName && coordEnv("AGENT_COORD_OFF") !== "1") {
494
+ const markerRoot = resolveCoordRoot(process.cwd());
495
+ if (markerRoot) {
496
+ hookRunMarkerPath = writeHookRunMarker(markerRoot, {
497
+ event: eventName,
498
+ adapter,
499
+ pid: process.pid,
500
+ timeout_sec: markerTimeout,
501
+ });
502
+ }
503
+ }
482
504
  const raw = await readStdin();
483
505
  hookHealthState = beginHookHealth({
484
506
  started_at_ms: hookStartedAt,
@@ -516,8 +538,8 @@ async function main() {
516
538
  // Kill-switch-INDEPENDENT effects: notification sounds fire BEFORE the
517
539
  // HARNERY_AGENT_COORD_OFF gate so audible feedback survives incident-triage
518
540
  // bypass: sound playback happens before the kill-switch bailout.
519
- // Claude-Code-only; stop-failure → error, sub-agent-start → subagent-start.
520
- if (adapter === "claude-code" && eventName) {
541
+ // Behavior-gated; stop-failure → error, sub-agent-start → subagent-start.
542
+ if (behavior?.soundEffects && eventName) {
521
543
  const s = soundForEvent(eventName);
522
544
  if (s) {
523
545
  const repoRoot = resolveCoordRoot(process.cwd());
@@ -568,7 +590,7 @@ async function main() {
568
590
  pid: process.pid,
569
591
  ppid: process.ppid,
570
592
  };
571
- if (!eventName || !adapter) {
593
+ if (!eventName || !adapter || !behavior) {
572
594
  appendDebug(coordRoot, { ...debugBase, skipped: "missing-event-or-adapter" });
573
595
  return 0;
574
596
  }
@@ -579,90 +601,17 @@ async function main() {
579
601
  // assistant text. Preserve only privacy-safe ritual booleans in the V3
580
602
  // producer's open-turn state; Stop consumes them into the authoritative
581
603
  // turn.completed event. The response body never enters V3 or heartbeat.
582
- if (adapter === "cursor" && eventName === "after-agent-response") {
583
- const payload = parsePayload(raw, adapter);
584
- const owner = resolveOwner({ payload: payload?.raw ?? null, coordRoot });
585
- if (!owner) {
586
- appendDebug(coordRoot, { ...debugBase, skipped: "no-owner-resolved" });
587
- return 0;
588
- }
589
- const row = readLiveCoordinationRow(coordRoot, owner.instance_id);
590
- const name = sessionNameDisplayPending(row);
591
- const text = typeof payload?.raw.text === "string" ? payload.raw.text : "";
592
- const sighting = matchSessionNameDisplay(row, text);
593
- if (sighting) {
594
- stampSessionNameSeen(coordRoot, owner.instance_id, sighting.pending);
595
- appendDebug(coordRoot, {
596
- ...debugBase,
597
- effect: "session-name-display-stamped",
598
- session_name_pending: sighting.pending,
599
- ...(sighting.displayed === sighting.pending
600
- ? {}
601
- : { session_name_displayed: sighting.displayed, session_name_drift: true }),
602
- });
603
- }
604
- else if (!name) {
605
- // Nothing was owed. Distinct from a miss: conflating the two is what made
606
- // a latched Cursor session undiagnosable from this log.
607
- appendDebug(coordRoot, { ...debugBase, skipped: "no-pending-session-name" });
608
- }
609
- else {
610
- appendDebug(coordRoot, {
611
- ...debugBase,
612
- skipped: "session-name-block-absent",
613
- session_name_pending: name,
614
- reply_bytes: text.length,
615
- reply_leads_with_fence: /^\s*`{3,}/.test(text),
616
- });
617
- }
618
- if (ledgerRoute.state === "blocked") {
619
- appendDebug(coordRoot, {
620
- ...debugBase,
621
- skipped: "v3-control-blocked",
622
- reason: ledgerRoute.reason,
623
- owner_source: owner.source,
624
- });
625
- return 0;
626
- }
627
- const statusBoxPresentStrict = scanAssistantStatusBoxPresent(undefined, text);
628
- // The hook parser needs the completed reply long enough to scan it, but
629
- // durable hook intake must never retain that body. Keep every other native
630
- // field so turn correlation remains available to the producer.
631
- const { text: _completedReply, ...privacySafeRaw } = payload?.raw ?? {};
632
- const privacySafePayload = payload ? { ...payload, raw: privacySafeRaw } : null;
633
- const v3Result = recordLiveHookSignalV3({
604
+ if (behavior && eventName && eventName === behavior.completedResponseEvent) {
605
+ return handleCompletedResponse({
634
606
  coordRoot,
635
- route: ledgerRoute,
636
- eventName,
637
- payload: privacySafePayload,
638
607
  adapter,
639
- instanceId: owner.instance_id,
640
- hook_name: eventName,
641
- hook_duration_ms: Math.max(0, Math.floor(performance.now() - hookStartedAt)),
642
- monotonic_ns: hookClock.monotonic_ns,
643
- turn_ritual: {
644
- status_box_present: statusBoxPresentStrict,
645
- status_box_present_strict: statusBoxPresentStrict,
646
- session_name_required: name !== undefined,
647
- session_name_present: name ? assistantTextStartsWithSessionNameBlock(text, name) : false,
648
- },
649
- });
650
- observeHookDebug(hookHealthState, { event_v3_state: v3Result.state });
651
- appendDebug(coordRoot, {
652
- ...debugBase,
653
- effect: "cursor-response-ritual-observed",
654
- owner_source: owner.source,
655
- event_v3_state: v3Result.state,
656
- ...(v3Result.state === "observed"
657
- ? {
658
- generation_id: v3Result.generation_id,
659
- turn_id: v3Result.turn_id,
660
- response_observed_at: v3Result.observed_at,
661
- status_box_present_strict: statusBoxPresentStrict,
662
- }
663
- : {}),
608
+ eventName,
609
+ raw,
610
+ debugBase,
611
+ ledgerRoute,
612
+ hookStartedAt,
613
+ hookClock,
664
614
  });
665
- return 0;
666
615
  }
667
616
  const norm = normalizeEventName(eventName);
668
617
  if (!norm) {
@@ -670,6 +619,7 @@ async function main() {
670
619
  return 0;
671
620
  }
672
621
  const payload = parsePayload(raw, adapter);
622
+ const bridge = behavior.bridgeFor(payload?.cwd);
673
623
  const owner = resolveOwner({
674
624
  payload: payload?.raw ?? null,
675
625
  coordRoot,
@@ -711,8 +661,14 @@ async function main() {
711
661
  status_box_present_strict: data.status_box_present_strict === true,
712
662
  session_name_required: typeof data.session_name_present_for === "string",
713
663
  session_name_present: data.session_name_present === true,
664
+ ...(typeof data.session_name_unavailable_reason === "string"
665
+ ? { session_name_unavailable_reason: data.session_name_unavailable_reason }
666
+ : {}),
714
667
  }
715
668
  : undefined;
669
+ if (typeof data.session_name_unavailable_reason === "string") {
670
+ reportSessionNameObservationUnavailable(coordRoot, owner.instance_id, adapter, eventName, data.session_name_unavailable_reason);
671
+ }
716
672
  const stopRemediation = norm.event_type === "turn.started" &&
717
673
  typeof data.prompt_text === "string" &&
718
674
  data.prompt_text.includes(STOP_REMEDIATION_MARKER);
@@ -726,6 +682,86 @@ async function main() {
726
682
  });
727
683
  return 0;
728
684
  }
685
+ const run = {
686
+ coordRoot,
687
+ adapter,
688
+ behavior,
689
+ eventName,
690
+ norm,
691
+ payload,
692
+ raw,
693
+ owner,
694
+ sessionId,
695
+ data,
696
+ debugBase,
697
+ ledgerRoute,
698
+ hookStartedAt,
699
+ hookClock,
700
+ priorCoordination,
701
+ topLevelSession,
702
+ stopRemediation,
703
+ bridge,
704
+ turnRitual,
705
+ };
706
+ recordSignal(run);
707
+ recordContextTelemetry(run);
708
+ // Phase 8: SessionStart post-emit: project the event so the heartbeat
709
+ // lands synchronously, run stale-sweep, and emit the adapter-shaped
710
+ // systemMessage JSON (peer table + wiring check + council invites).
711
+ // Adapter-agnostic since v0.5.0; replaces the previous bash UX layer
712
+ // and the equivalent per-adapter bash session_start handlers.
713
+ if (norm.event_type === "session.started")
714
+ await handleSessionStarted(run);
715
+ // Phase 8: SessionEnd cleanup: delete heartbeat + pid-map rows. Adapter-
716
+ // agnostic since v0.5.0.
717
+ if (norm.event_type === "session.ended")
718
+ handleSessionEnded(run);
719
+ // Phase 8: SubagentStart: the child generation was opened above from the
720
+ // native delegation identity. Refresh its disposable projection, log the
721
+ // lifecycle event, and emit a context message announcing the subagent.
722
+ if (norm.event_type === "agent.started")
723
+ await handleAgentStarted(run);
724
+ // Phase 8: SubagentStop: delete subagent heartbeat + log.
725
+ if (norm.event_type === "agent.completed")
726
+ await handleAgentCompleted(run);
727
+ // Phase 8: UserPromptSubmit: render dedup'd peer table + council pending
728
+ // and emit the adapter-shaped systemMessage JSON. Adapter-agnostic since v0.5.0.
729
+ if (norm.event_type === "turn.started")
730
+ await handleTurnStarted(run);
731
+ // turn.completed: telemetry, then the stop verdict. The
732
+ // verdict + codex-replay previously lived in the per-adapter shell adapters;
733
+ // agent-hook owns them now. Runs on the normal "stop" event only;
734
+ // "stop-failure" (API error) gets no gate, matching the previous
735
+ // stop vs stop-failure split.
736
+ if (norm.event_type === "turn.completed" && eventName === "stop") {
737
+ const exit = await handleStop(run);
738
+ if (exit !== undefined)
739
+ return exit;
740
+ }
741
+ // Phase 7: PreToolUse: heartbeat + pid-map self-heal on every tool call.
742
+ // Adapter-agnostic: both writes have the same shape regardless of who fired.
743
+ // Cursor/Codex bash dispatchers still fire their own G-guard logic, but the
744
+ // heals here keep the agent-coord layer's view of liveness fresh.
745
+ //
746
+ // The heartbeat + pid-map heals are paired by design; they were wired
747
+ // side-by-side in the previous pre-tool-use adapter. The Phase 4-6 refactor
748
+ // preserved the heartbeat half but dropped the pid-map half; the pid-map
749
+ // call was restored here afterward.
750
+ if (norm.event_type === "tool.requested") {
751
+ const exit = await handleToolRequested(run);
752
+ if (exit !== undefined)
753
+ return exit;
754
+ }
755
+ if (norm.event_type === "tool.completed" && eventName === "post-tool-use")
756
+ await handlePostToolUse(run);
757
+ // Phase 7: PostToolUseFailure: release claim on failed Edit (the file
758
+ // never landed; the claim is stale). Adapter-agnostic.
759
+ if (norm.event_type === "tool.completed" && eventName === "post-tool-use-failure")
760
+ handlePostToolUseFailure(run);
761
+ return 0;
762
+ }
763
+ function recordSignal(run) {
764
+ const { coordRoot, adapter, eventName, norm, payload, owner, sessionId, data, debugBase, ledgerRoute, hookStartedAt, hookClock, priorCoordination, stopRemediation, bridge, turnRitual, } = run;
729
765
  let capturedImages = [];
730
766
  if (norm.event_type === "tool.requested" ||
731
767
  (norm.event_type === "tool.completed" && eventName === "post-tool-use")) {
@@ -753,7 +789,7 @@ async function main() {
753
789
  : {}),
754
790
  }
755
791
  : {}),
756
- ...(adapter === "codex" && isWslUncPath(payload?.cwd) ? { bridge: "codex-wsl" } : {}),
792
+ ...(bridge ? { bridge } : {}),
757
793
  hook_name: eventName,
758
794
  hook_duration_ms: Math.max(0, Math.floor(performance.now() - hookStartedAt)),
759
795
  monotonic_ns: hookClock.monotonic_ns,
@@ -806,9 +842,7 @@ async function main() {
806
842
  parentEvent: v3Result.event,
807
843
  payload,
808
844
  adapter,
809
- ...(adapter === "codex" && isWslUncPath(payload.cwd)
810
- ? { bridge: "codex-wsl" }
811
- : {}),
845
+ ...(bridge ? { bridge } : {}),
812
846
  monotonic_ns: hookClock.monotonic_ns,
813
847
  });
814
848
  const nativeChild = payload.subagent_id ?? payload.agent_id;
@@ -851,6 +885,12 @@ async function main() {
851
885
  ...(v3Result ? { event_v3_state: v3Result.state } : {}),
852
886
  ...(v3Result && "reason" in v3Result ? { event_v3_reason: v3Result.reason } : {}),
853
887
  });
888
+ run.v3Result = v3Result;
889
+ run.v3EventId = v3EventId;
890
+ run.recordedGenerationId = recordedGenerationId;
891
+ }
892
+ function recordContextTelemetry(run) {
893
+ const { coordRoot, adapter, norm, payload, owner, sessionId } = run;
854
894
  // Context telemetry is opportunistic and truthful: only persist a sample
855
895
  // when the adapter payload actually exposes usage/window data. Identical
856
896
  // measurements are de-duplicated before they reach the canonical stream.
@@ -902,413 +942,519 @@ async function main() {
902
942
  logError(coordRoot, err, { phase: "context-compaction-completed" });
903
943
  }
904
944
  }
905
- // Phase 8: SessionStart post-emit: project the event so the heartbeat
906
- // lands synchronously, run stale-sweep, and emit the adapter-shaped
907
- // systemMessage JSON (peer table + wiring check + council invites).
908
- // Adapter-agnostic since v0.5.0; replaces the previous bash UX layer
909
- // and the equivalent per-adapter bash session_start handlers.
910
- if (norm.event_type === "session.started") {
911
- // A configured backup runs out of process and is freshness-gated per host.
912
- // Startup never waits for restic or a remote provider; the child owns no
913
- // hook stdio, so the harness is not held open until it finishes. A failed
914
- // previous run surfaces as one line in this session's context.
915
- let backupCue = "";
916
- try {
917
- backupCue = scheduleBackupSnapshot(coordRoot).cue ?? "";
918
- }
919
- catch (err) {
920
- logError(coordRoot, err, { phase: "backup-schedule" });
921
- }
922
- // Effect (claude-code): prune stale journal archives + sweep orphans.
923
- // The recovery-cue is merged into the
924
- // session-start additionalContext inside emitSessionStartSystemMessage.
925
- if (adapter === "claude-code")
926
- journalJanitor(coordRoot);
927
- imageJanitor(coordRoot);
928
- // Session start is the only caller of the artifact janitor and the storage
929
- // maintenance pass, so both load here instead of on every tool call.
930
- const { autoCleanArtifacts } = await import("../artifacts/index.js");
931
- const { autoCleanEventV3Archives } = await import("../events/v3/archive-retention.js");
932
- const { createAutomaticMaintenanceComposition, runAutomaticMaintenancePass } = await import("../storage/maintenance-providers.js");
933
- // Effect: throttled daily sweep of expired artifact workspaces (guarded,
934
- // managed-expired only; see autoCleanArtifacts). Best-effort like every
935
- // janitor here: a failure logs and never blocks session start.
936
- try {
937
- autoCleanArtifacts(coordRoot);
938
- }
939
- catch (err) {
940
- logError(coordRoot, err, { phase: "artifact-auto-clean" });
941
- }
942
- // Effect: apply the same daily throttle to closed V3 epochs. The newest
943
- // recovery epochs remain protected by the archive retention policy.
944
- try {
945
- autoCleanEventV3Archives(coordRoot);
946
- }
947
- catch (err) {
948
- logError(coordRoot, err, { phase: "event-v3-archive-auto-clean" });
949
- }
950
- // Plan one bounded, claim-first global slice from cached pressure only.
951
- // Existing owner janitors above remain the only active executors until
952
- // each provider receives a separate production activation.
953
- try {
954
- await runAutomaticMaintenancePass(createAutomaticMaintenanceComposition(coordRoot, {
955
- journal: () => {
956
- if (adapter === "claude-code")
957
- journalJanitor(coordRoot);
958
- },
959
- images: () => imageJanitor(coordRoot),
960
- artifacts: () => autoCleanArtifacts(coordRoot),
961
- }));
962
- }
963
- catch (err) {
964
- logError(coordRoot, err, { phase: "storage-maintenance-auto" });
965
- }
966
- let recovery = null;
967
- if (payload?.source === "compact") {
968
- try {
969
- markContextCompactionCompleted(coordRoot, {
970
- sessionId,
971
- instanceId: owner.instance_id,
972
- });
973
- recovery = prepareContextRecovery(coordRoot, {
974
- instanceId: owner.instance_id,
975
- sessionId,
976
- cwd: payload?.cwd ?? process.cwd(),
977
- });
978
- }
979
- catch (err) {
980
- logError(coordRoot, err, { phase: "context-recovery-session-start" });
981
- }
982
- }
983
- try {
984
- const injected = await emitSessionStartSystemMessage(coordRoot, owner.instance_id, sessionId, data, adapter, recovery?.briefing ?? "", backupCue);
985
- if (injected && recovery) {
986
- completeRecoveryInjection(coordRoot, owner.instance_id, sessionId, recovery);
987
- }
988
- }
989
- catch (err) {
990
- logError(coordRoot, err, { phase: "session-start-systemMessage" });
991
- }
992
- // Cross-machine presence is derived from the authority-safe V3 projection,
993
- // not the disposable heartbeat cache. Announce this session, pull peers,
994
- // and keep the optional live relay connected.
995
- try {
996
- publishPresence(coordRoot);
997
- fetchPresence(coordRoot);
998
- ensureRelayDaemon(coordRoot);
999
- }
1000
- catch (err) {
1001
- logError(coordRoot, err, { phase: "session-start-presence" });
1002
- }
945
+ }
946
+ async function handleSessionStarted(run) {
947
+ const { coordRoot, adapter, behavior, payload, owner, sessionId, data } = run;
948
+ // A configured backup runs out of process and is freshness-gated per host.
949
+ // Startup never waits for restic or a remote provider; the child owns no
950
+ // hook stdio, so the harness is not held open until it finishes. A failed
951
+ // previous run surfaces as one line in this session's context.
952
+ let backupCue = "";
953
+ try {
954
+ backupCue = scheduleBackupSnapshot(coordRoot).cue ?? "";
1003
955
  }
1004
- // Phase 8: SessionEnd cleanup: delete heartbeat + pid-map rows. Adapter-
1005
- // agnostic since v0.5.0.
1006
- if (norm.event_type === "session.ended") {
1007
- try {
1008
- cleanupSessionEnd(coordRoot, owner.instance_id);
1009
- }
1010
- catch (err) {
1011
- logError(coordRoot, err, { phase: "session-end-cleanup" });
1012
- }
1013
- // Effects (claude-code): archive the ending agent's journal + force a
1014
- // session-telemetry sync (via HARNERY_CLAUDE_SESSIONS_FORCE=1).
1015
- if (adapter === "claude-code") {
1016
- journalArchive(coordRoot, owner.instance_id);
1017
- runSessionSyncExtension(coordRoot, true);
1018
- }
1019
- // Publish the post-cleanup V3 projection so remote machines see this
1020
- // session disappear without waiting for the stale window.
1021
- try {
1022
- publishPresence(coordRoot);
1023
- }
1024
- catch (err) {
1025
- logError(coordRoot, err, { phase: "session-end-presence" });
1026
- }
956
+ catch (err) {
957
+ logError(coordRoot, err, { phase: "backup-schedule" });
958
+ }
959
+ // Effect (journal-maintaining adapters): prune stale journal archives +
960
+ // sweep orphans. The recovery-cue is merged into the session-start
961
+ // additionalContext inside emitSessionStartSystemMessage.
962
+ if (behavior.journalMaintenance)
963
+ journalJanitor(coordRoot);
964
+ imageJanitor(coordRoot);
965
+ // Session start is the only caller of the artifact janitor and the storage
966
+ // maintenance pass, so both load here instead of on every tool call.
967
+ const { autoCleanArtifacts } = await import("../artifacts/index.js");
968
+ const { autoCleanEventV3Archives } = await import("../events/v3/archive-retention.js");
969
+ const { createAutomaticMaintenanceComposition, runAutomaticMaintenancePass } = await import("../storage/maintenance-providers.js");
970
+ // Effect: throttled hourly artifact sweep (see autoCleanArtifacts). It
971
+ // deletes expired workspaces, plus unheld workspaces over the per-bundle
972
+ // ceiling or repository budget once they have been idle past
973
+ // artifacts.idle_grace_hours. Each deletion is rechecked first and recorded
974
+ // in .harnery/artifact-deletions.jsonl. Best-effort like every janitor
975
+ // here: a failure logs and never blocks session start.
976
+ try {
977
+ autoCleanArtifacts(coordRoot);
1027
978
  }
1028
- // Phase 8: SubagentStart: the child generation was opened above from the
1029
- // native delegation identity. Refresh its disposable projection, log the
1030
- // lifecycle event, and emit a context message announcing the subagent.
1031
- if (norm.event_type === "agent.started") {
979
+ catch (err) {
980
+ logError(coordRoot, err, { phase: "artifact-auto-clean" });
981
+ }
982
+ // Effect: apply the same daily throttle to closed V3 epochs. The newest
983
+ // recovery epochs remain protected by the archive retention policy.
984
+ try {
985
+ autoCleanEventV3Archives(coordRoot);
986
+ }
987
+ catch (err) {
988
+ logError(coordRoot, err, { phase: "event-v3-archive-auto-clean" });
989
+ }
990
+ // Plan one bounded, claim-first global slice from cached pressure only.
991
+ // Existing owner janitors above remain the only active executors until
992
+ // each provider receives a separate production activation.
993
+ try {
994
+ await runAutomaticMaintenancePass(createAutomaticMaintenanceComposition(coordRoot, {
995
+ journal: () => {
996
+ if (behavior.journalMaintenance)
997
+ journalJanitor(coordRoot);
998
+ },
999
+ images: () => imageJanitor(coordRoot),
1000
+ artifacts: () => autoCleanArtifacts(coordRoot),
1001
+ }));
1002
+ }
1003
+ catch (err) {
1004
+ logError(coordRoot, err, { phase: "storage-maintenance-auto" });
1005
+ }
1006
+ let recovery = null;
1007
+ if (payload?.source === "compact") {
1032
1008
  try {
1033
- const childInstanceId = data.agent_id ?? owner.instance_id;
1034
- await emitSubagentStartContext(coordRoot, childInstanceId, sessionId, data, adapter);
1009
+ markContextCompactionCompleted(coordRoot, {
1010
+ sessionId,
1011
+ instanceId: owner.instance_id,
1012
+ });
1013
+ recovery = prepareContextRecovery(coordRoot, {
1014
+ instanceId: owner.instance_id,
1015
+ sessionId,
1016
+ cwd: payload?.cwd ?? process.cwd(),
1017
+ });
1035
1018
  }
1036
1019
  catch (err) {
1037
- logError(coordRoot, err, { phase: "subagent-start-context" });
1020
+ logError(coordRoot, err, { phase: "context-recovery-session-start" });
1038
1021
  }
1039
1022
  }
1040
- // Phase 8: SubagentStop: delete subagent heartbeat + log.
1041
- if (norm.event_type === "agent.completed") {
1042
- try {
1043
- const childInstanceId = payload?.subagent_id ?? payload?.agent_id ?? owner.instance_id;
1044
- cleanupSessionEnd(coordRoot, childInstanceId);
1045
- const { reconcileSessionFinalizationV3 } = await import("../agents/session-finalizer-v3.js");
1046
- reconcileSessionFinalizationV3(coordRoot, { archive_observations: [] });
1047
- }
1048
- catch (err) {
1049
- logError(coordRoot, err, { phase: "subagent-stop-cleanup" });
1023
+ try {
1024
+ const injected = await emitSessionStartSystemMessage(coordRoot, owner.instance_id, sessionId, data, adapter, recovery?.briefing ?? "", backupCue);
1025
+ if (injected && recovery) {
1026
+ completeRecoveryInjection(coordRoot, owner.instance_id, sessionId, recovery);
1050
1027
  }
1051
1028
  }
1052
- // Phase 8: UserPromptSubmit: render dedup'd peer table + council pending
1053
- // and emit the adapter-shaped systemMessage JSON. Adapter-agnostic since v0.5.0.
1054
- if (norm.event_type === "turn.started") {
1055
- // Effects (claude-code): reset per-turn sound rate-limit counters + run
1056
- // presence detection on the prompt.
1057
- if (adapter === "claude-code") {
1058
- resetSoundCounters(sessionId);
1059
- const prompt = payload?.raw?.prompt ?? "";
1060
- if (prompt)
1061
- detectPresence(prompt);
1062
- }
1063
- // Presence: throttled pull of peer machines' presence refs (default 60s
1064
- // interval; the systemMessage below reads whatever is locally known).
1029
+ catch (err) {
1030
+ logError(coordRoot, err, { phase: "session-start-systemMessage" });
1031
+ }
1032
+ // Cross-machine presence is derived from the authority-safe V3 projection,
1033
+ // not the disposable heartbeat cache. Announce this session, pull peers,
1034
+ // and keep the optional live relay connected.
1035
+ try {
1036
+ publishPresence(coordRoot);
1037
+ fetchPresence(coordRoot);
1038
+ ensureRelayDaemon(coordRoot);
1039
+ }
1040
+ catch (err) {
1041
+ logError(coordRoot, err, { phase: "session-start-presence" });
1042
+ }
1043
+ }
1044
+ function handleSessionEnded(run) {
1045
+ const { coordRoot, behavior, owner } = run;
1046
+ try {
1047
+ cleanupSessionEnd(coordRoot, owner.instance_id);
1048
+ }
1049
+ catch (err) {
1050
+ logError(coordRoot, err, { phase: "session-end-cleanup" });
1051
+ }
1052
+ // Effects: archive the ending agent's journal + force a session-telemetry
1053
+ // sync (via HARNERY_CLAUDE_SESSIONS_FORCE=1), each gated by the behavior.
1054
+ if (behavior.journalMaintenance)
1055
+ journalArchive(coordRoot, owner.instance_id);
1056
+ if (behavior.sessionTelemetrySync)
1057
+ runSessionSyncExtension(coordRoot, true);
1058
+ // Publish the post-cleanup V3 projection so remote machines see this
1059
+ // session disappear without waiting for the stale window.
1060
+ try {
1061
+ publishPresence(coordRoot);
1062
+ }
1063
+ catch (err) {
1064
+ logError(coordRoot, err, { phase: "session-end-presence" });
1065
+ }
1066
+ }
1067
+ async function handleAgentStarted(run) {
1068
+ const { coordRoot, adapter, owner, sessionId, data } = run;
1069
+ try {
1070
+ const childInstanceId = data.agent_id ?? owner.instance_id;
1071
+ await emitSubagentStartContext(coordRoot, childInstanceId, sessionId, data, adapter);
1072
+ }
1073
+ catch (err) {
1074
+ logError(coordRoot, err, { phase: "subagent-start-context" });
1075
+ }
1076
+ }
1077
+ async function handleAgentCompleted(run) {
1078
+ const { coordRoot, payload, owner } = run;
1079
+ try {
1080
+ const childInstanceId = payload?.subagent_id ?? payload?.agent_id ?? owner.instance_id;
1081
+ cleanupSessionEnd(coordRoot, childInstanceId);
1082
+ const { reconcileSessionFinalizationV3 } = await import("../agents/session-finalizer-v3.js");
1083
+ reconcileSessionFinalizationV3(coordRoot, { archive_observations: [] });
1084
+ }
1085
+ catch (err) {
1086
+ logError(coordRoot, err, { phase: "subagent-stop-cleanup" });
1087
+ }
1088
+ }
1089
+ async function handleTurnStarted(run) {
1090
+ const { coordRoot, adapter, behavior, payload, owner, sessionId, debugBase, topLevelSession, stopRemediation, v3EventId, } = run;
1091
+ // Effects: reset per-turn sound rate-limit counters + run presence
1092
+ // detection on the prompt, each gated by the behavior.
1093
+ if (behavior.soundEffects)
1094
+ resetSoundCounters(sessionId);
1095
+ if (behavior.promptPresenceDetection) {
1096
+ const prompt = payload?.raw?.prompt ?? "";
1097
+ if (prompt)
1098
+ detectPresence(prompt);
1099
+ }
1100
+ // Presence: throttled pull of peer machines' presence refs (default 60s
1101
+ // interval; the systemMessage below reads whatever is locally known).
1102
+ try {
1103
+ fetchPresence(coordRoot);
1104
+ }
1105
+ catch (err) {
1106
+ logError(coordRoot, err, { phase: "user-prompt-submit-presence" });
1107
+ }
1108
+ let recovery = null;
1109
+ const continuityState = readContextState(coordRoot, sessionId);
1110
+ if (continuityState?.phase === "checkpointed" &&
1111
+ continuityState.compaction_completed_at !== undefined) {
1065
1112
  try {
1066
- fetchPresence(coordRoot);
1113
+ recovery = prepareContextRecovery(coordRoot, {
1114
+ instanceId: owner.instance_id,
1115
+ sessionId,
1116
+ cwd: payload?.cwd ?? process.cwd(),
1117
+ });
1067
1118
  }
1068
1119
  catch (err) {
1069
- logError(coordRoot, err, { phase: "user-prompt-submit-presence" });
1070
- }
1071
- let recovery = null;
1072
- const continuityState = readContextState(coordRoot, sessionId);
1073
- if (continuityState?.phase === "checkpointed" &&
1074
- continuityState.compaction_completed_at !== undefined) {
1075
- try {
1076
- recovery = prepareContextRecovery(coordRoot, {
1077
- instanceId: owner.instance_id,
1078
- sessionId,
1079
- cwd: payload?.cwd ?? process.cwd(),
1080
- });
1081
- }
1082
- catch (err) {
1083
- logError(coordRoot, err, { phase: "context-recovery-user-prompt" });
1084
- }
1085
- }
1086
- let hostPromptContext = "";
1087
- const hostPromptEnabled = hostPromptContextConfig(coordRoot)?.enabled === true;
1088
- if (hostPromptEnabled && topLevelSession && !stopRemediation) {
1089
- try {
1090
- const { runPromptContext } = await import("./prompt-context/runner.js");
1091
- const result = await runPromptContext({
1092
- coordRoot,
1093
- adapter,
1094
- sessionId,
1095
- turnId: payload?.turn_id ?? v3EventId ?? sessionId,
1096
- cwd: payload?.cwd ?? process.cwd(),
1097
- prompt: payload?.prompt ?? "",
1098
- });
1099
- appendDebug(coordRoot, {
1100
- ...debugBase,
1101
- effect: "prompt-context",
1102
- ...result.audit,
1103
- });
1104
- hostPromptContext = result.context ?? "";
1105
- }
1106
- catch (err) {
1107
- logError(coordRoot, err, { phase: "prompt-context-user-prompt" });
1108
- }
1120
+ logError(coordRoot, err, { phase: "context-recovery-user-prompt" });
1109
1121
  }
1122
+ }
1123
+ let hostPromptContext = "";
1124
+ const hostPromptEnabled = hostPromptContextConfig(coordRoot)?.enabled === true;
1125
+ if (hostPromptEnabled && topLevelSession && !stopRemediation) {
1110
1126
  try {
1111
- const injected = await emitUserPromptSubmitSystemMessage(coordRoot, owner.instance_id, sessionId, adapter, payload?.cwd, recovery?.briefing ?? "", hostPromptContext);
1112
- if (injected && recovery) {
1113
- completeRecoveryInjection(coordRoot, owner.instance_id, sessionId, recovery);
1114
- }
1127
+ const { runPromptContext } = await import("./prompt-context/runner.js");
1128
+ const result = await runPromptContext({
1129
+ coordRoot,
1130
+ adapter,
1131
+ sessionId,
1132
+ turnId: payload?.turn_id ?? v3EventId ?? sessionId,
1133
+ cwd: payload?.cwd ?? process.cwd(),
1134
+ prompt: payload?.prompt ?? "",
1135
+ });
1136
+ appendDebug(coordRoot, {
1137
+ ...debugBase,
1138
+ effect: "prompt-context",
1139
+ ...result.audit,
1140
+ });
1141
+ hostPromptContext = result.context ?? "";
1115
1142
  }
1116
1143
  catch (err) {
1117
- logError(coordRoot, err, { phase: "user-prompt-submit-systemMessage" });
1144
+ logError(coordRoot, err, { phase: "prompt-context-user-prompt" });
1118
1145
  }
1119
1146
  }
1120
- // turn.completed: telemetry, then the stop verdict. The
1121
- // verdict + codex-replay previously lived in the per-adapter shell adapters;
1122
- // agent-hook owns them now. Runs on the normal "stop" event only;
1123
- // "stop-failure" (API error) gets no gate, matching the previous
1124
- // stop vs stop-failure split.
1125
- if (norm.event_type === "turn.completed" && eventName === "stop") {
1126
- // Claude Code session telemetry sync remains an independent side effect.
1127
- if (adapter === "claude-code") {
1128
- runSessionSyncExtension(coordRoot, false);
1129
- }
1130
- // Publish after the canonical turn event has landed so the blob carries
1131
- // current task, activity, and claim state. Re-ensure the relay here because
1132
- // its daemon intentionally exits when a machine has no live sessions.
1133
- try {
1134
- publishPresence(coordRoot);
1135
- ensureRelayDaemon(coordRoot);
1147
+ try {
1148
+ const injected = await emitUserPromptSubmitSystemMessage(coordRoot, owner.instance_id, sessionId, adapter, payload?.cwd, recovery?.briefing ?? "", hostPromptContext);
1149
+ if (injected && recovery) {
1150
+ completeRecoveryInjection(coordRoot, owner.instance_id, sessionId, recovery);
1136
1151
  }
1137
- catch (err) {
1138
- logError(coordRoot, err, { phase: "stop-presence" });
1139
- }
1140
- // Stop verdict (V3 ritual + task/status gate). Direct in-process call: the
1141
- // rule lives in Harnery. agent-hook already emitted this turn.completed
1142
- // with privacy-safe ritual observations, so the evidence is in the stream.
1143
- const verdict = evaluateStopHook(coordRoot, {
1144
- rule: "stop-hook",
1145
- instance_id: owner.instance_id,
1146
- session_id: sessionId,
1147
- adapter,
1148
- stop_hook_active: payload?.stop_hook_active === true,
1149
- status_box_present_strict: data.status_box_present_strict === true,
1150
- bypass: coordEnv("AGENT_COORD_BYPASS_STOP") === "1",
1151
- workflow_child: coordEnv("WORKFLOW_CHILD") === "1",
1152
- });
1153
- if (!verdict.allow) {
1154
- // Backstop for sessions whose evidence can never land (e.g. a headless
1155
- // child whose status command resolves to a different owner): after
1156
- // `cap` consecutive blocked stops in one cycle, allow the stop instead
1157
- // of bouncing the model forever. Cycle boundaries come from the
1158
- // adapter's continuation flag, so adapters without one are unaffected.
1159
- const { recordRemediationBlock } = await import("./remediation-cap.js");
1160
- const remediation = recordRemediationBlock(sessionId, payload?.stop_hook_active === true);
1161
- if (remediation.exceeded) {
1162
- appendDebug(coordRoot, {
1163
- ...debugBase,
1164
- skipped: "stop-remediation-cap-exhausted",
1165
- blocked_rule: verdict.rule,
1166
- blocked_count: remediation.count,
1167
- session_id: sessionId,
1168
- remediation_cycle_anchor: verdict.remediation_cycle_anchor,
1169
- });
1170
- }
1171
- else {
1172
- // Adapter-aware enforcement channel: Claude Code honors exit-2 + stderr
1173
- // as a turn block; Cursor ignores exit codes and re-prompts only via a
1174
- // `followup_message` it auto-submits.
1175
- const { emitStopOutcome } = await import("./adapter/output.js");
1176
- return emitStopOutcome(adapter, { verdict }, coordRoot);
1177
- }
1152
+ }
1153
+ catch (err) {
1154
+ logError(coordRoot, err, { phase: "user-prompt-submit-systemMessage" });
1155
+ }
1156
+ }
1157
+ async function handleStop(run) {
1158
+ const { coordRoot, adapter, behavior, payload, owner, sessionId, data, debugBase } = run;
1159
+ // Session telemetry sync remains an independent side effect.
1160
+ if (behavior.sessionTelemetrySync)
1161
+ runSessionSyncExtension(coordRoot, false);
1162
+ // Publish after the canonical turn event has landed so the blob carries
1163
+ // current task, activity, and claim state. Re-ensure the relay here because
1164
+ // its daemon intentionally exits when a machine has no live sessions.
1165
+ try {
1166
+ publishPresence(coordRoot);
1167
+ ensureRelayDaemon(coordRoot);
1168
+ }
1169
+ catch (err) {
1170
+ logError(coordRoot, err, { phase: "stop-presence" });
1171
+ }
1172
+ // Stop verdict (V3 ritual + task/status gate). Direct in-process call: the
1173
+ // rule lives in Harnery. agent-hook already emitted this turn.completed
1174
+ // with privacy-safe ritual observations, so the evidence is in the stream.
1175
+ const verdict = evaluateStopHook(coordRoot, {
1176
+ rule: "stop-hook",
1177
+ instance_id: owner.instance_id,
1178
+ session_id: sessionId,
1179
+ adapter,
1180
+ stop_hook_active: payload?.stop_hook_active === true,
1181
+ status_box_present_strict: data.status_box_present_strict === true,
1182
+ session_name_observation_unavailable: typeof data.session_name_unavailable_reason === "string",
1183
+ bypass: coordEnv("AGENT_COORD_BYPASS_STOP") === "1",
1184
+ workflow_child: coordEnv("WORKFLOW_CHILD") === "1",
1185
+ });
1186
+ if (!verdict.allow) {
1187
+ // Backstop for sessions whose evidence can never land (e.g. a headless
1188
+ // child whose status command resolves to a different owner): after
1189
+ // `cap` consecutive blocked stops in one cycle, allow the stop instead
1190
+ // of bouncing the model forever. Cycle boundaries come from the
1191
+ // adapter's continuation flag, so adapters without one are unaffected.
1192
+ const { recordRemediationBlock } = await import("./remediation-cap.js");
1193
+ const remediation = recordRemediationBlock(sessionId, payload?.stop_hook_active === true);
1194
+ if (remediation.exceeded) {
1195
+ appendDebug(coordRoot, {
1196
+ ...debugBase,
1197
+ skipped: "stop-remediation-cap-exhausted",
1198
+ blocked_rule: verdict.rule,
1199
+ blocked_count: remediation.count,
1200
+ session_id: sessionId,
1201
+ remediation_cycle_anchor: verdict.remediation_cycle_anchor,
1202
+ });
1178
1203
  }
1179
1204
  else {
1180
- const { clearRemediationCount } = await import("./remediation-cap.js");
1181
- clearRemediationCount(sessionId);
1182
- }
1183
- // An explicit end requested from inside this turn cannot be authoritative
1184
- // until the adapter has committed turn.completed above. Reconcile only
1185
- // when such a request exists; any later real work cancels it in the
1186
- // finalizer instead of being terminated underneath the agent.
1187
- try {
1188
- const { hasPendingExplicitSessionEndV3, reconcileSessionFinalizationV3 } = await import("../agents/session-finalizer-v3.js");
1189
- if (hasPendingExplicitSessionEndV3(coordRoot)) {
1190
- reconcileSessionFinalizationV3(coordRoot);
1191
- }
1192
- }
1193
- catch (err) {
1194
- logError(coordRoot, err, { phase: "stop-explicit-session-finalization" });
1205
+ // Adapter-aware enforcement channel: Claude Code honors exit-2 + stderr
1206
+ // as a turn block; Cursor ignores exit codes and re-prompts only via a
1207
+ // `followup_message` it auto-submits.
1208
+ const { emitStopOutcome } = await import("./adapter/output.js");
1209
+ return emitStopOutcome(adapter, { verdict }, coordRoot);
1195
1210
  }
1196
- if (adapter === "codex")
1197
- scheduleCodexRuntimeContextRetry(coordRoot, sessionId);
1198
1211
  }
1199
- // Phase 7: PreToolUse: heartbeat + pid-map self-heal on every tool call.
1200
- // Adapter-agnostic: both writes have the same shape regardless of who fired.
1201
- // Cursor/Codex bash dispatchers still fire their own G-guard logic, but the
1202
- // heals here keep the agent-coord layer's view of liveness fresh.
1203
- //
1204
- // The heartbeat + pid-map heals are paired by design; they were wired
1205
- // side-by-side in the previous pre-tool-use adapter. The Phase 4-6 refactor
1206
- // preserved the heartbeat half but dropped the pid-map half; the pid-map
1207
- // call was restored here afterward.
1208
- if (norm.event_type === "tool.requested") {
1209
- try {
1210
- // Recorded fork lineage, heal-path flavor. A forked CC conversation
1211
- // never fires its own session.started (SessionStart fires under the
1212
- // PARENT's session id with source=resume, before the fork id is
1213
- // minted; verified 2026-08-05), so the fork's new instance first
1214
- // materializes right here. Gate detection on "no heartbeat yet" so the
1215
- // transcript scan runs once per instance lifetime, not per tool call.
1216
- refreshPidmap(coordRoot, owner.instance_id, adapter, payload?.pid);
1217
- }
1218
- catch (err) {
1219
- logError(coordRoot, err, { phase: "pre-tool-use-heal" });
1220
- }
1221
- // The suggested name is a pending display latch. The set-task call itself
1222
- // runs before a name exists; every later tool waits until the exact block
1223
- // is the first assistant text after the mint result. Cursor supplies
1224
- // agent_message directly; Claude Code and Codex are resolved from their
1225
- // JSONL transcripts. Cursor's current narration can provide positive
1226
- // evidence, but cannot disprove an earlier response; afterAgentResponse
1227
- // stamps that durable sighting. Later commentary must not erase an
1228
- // already-correct display, and unavailable evidence must not deadlock
1229
- // every tool.
1230
- try {
1231
- const displayAllowed = await enforcePendingSessionNameDisplay(coordRoot, owner.instance_id, adapter, payload, generationBoundHeartbeat(readHeartbeat(coordRoot, owner.instance_id), recordedGenerationId));
1232
- if (!displayAllowed)
1233
- return 0;
1234
- }
1235
- catch (err) {
1236
- logError(coordRoot, err, { phase: "pre-tool-use-session-name" });
1237
- }
1238
- // Windows-native Codex + WSL UNC only: block the one cross-shell shape
1239
- // proven to corrupt argument boundaries. Normal WSL argv calls, literal
1240
- // bash -s scripts, native Linux/macOS sessions, and every other adapter
1241
- // pass through. The host instructions name its concrete safe bridge.
1242
- const unsafeShellReason = unsafeCrossShellReason({
1243
- adapter,
1244
- cwd: payload?.cwd,
1245
- toolName: payload?.tool_name,
1246
- toolInput: payload?.tool_input,
1247
- });
1248
- if (unsafeShellReason) {
1249
- const { emitDeny } = await import("./adapter/output.js");
1250
- emitDeny(adapter, unsafeShellReason);
1251
- return 0;
1252
- }
1253
- // G-guard for ALL adapters. Claude Code previously ran this via a
1254
- // pre-tool-use bash adapter (which called `agent-coord verdict --rule=claim`);
1255
- // that adapter is now deleted, so agent-hook owns the deny for every adapter.
1256
- // emitDeny() inside emits the adapter-shaped permission JSON (claude-code +
1257
- // codex use hookSpecificOutput.permissionDecision; cursor uses .permission).
1258
- // apply_patch (codex) parses paths from the patch body and runs verdict
1259
- // per-path; Edit/Write/NotebookEdit resolve a single target. Non-write tools
1260
- // (incl. Agent) yield no targets and pass through with no deny.
1261
- let guardAllowed = true;
1262
- try {
1263
- guardAllowed = await runPreToolUseGuard(coordRoot, owner.instance_id, sessionId, data, adapter);
1264
- }
1265
- catch (err) {
1266
- logError(coordRoot, err, { phase: "pre-tool-use-guard" });
1212
+ else {
1213
+ const { clearRemediationCount } = await import("./remediation-cap.js");
1214
+ clearRemediationCount(sessionId);
1215
+ }
1216
+ // An explicit end requested from inside this turn cannot be authoritative
1217
+ // until the adapter has committed turn.completed above. Reconcile only
1218
+ // when such a request exists; any later real work cancels it in the
1219
+ // finalizer instead of being terminated underneath the agent.
1220
+ try {
1221
+ const { hasPendingExplicitSessionEndV3, reconcileSessionFinalizationV3 } = await import("../agents/session-finalizer-v3.js");
1222
+ if (hasPendingExplicitSessionEndV3(coordRoot)) {
1223
+ reconcileSessionFinalizationV3(coordRoot);
1267
1224
  }
1268
- if (!guardAllowed)
1225
+ }
1226
+ catch (err) {
1227
+ logError(coordRoot, err, { phase: "stop-explicit-session-finalization" });
1228
+ }
1229
+ if (behavior.runtimeContextRetry)
1230
+ scheduleRuntimeContextRetry(coordRoot, adapter, sessionId);
1231
+ return undefined;
1232
+ }
1233
+ async function handleToolRequested(run) {
1234
+ const { coordRoot, adapter, behavior, payload, owner, sessionId, data, recordedGenerationId } = run;
1235
+ try {
1236
+ // Recorded fork lineage, heal-path flavor. A forked CC conversation
1237
+ // never fires its own session.started (SessionStart fires under the
1238
+ // PARENT's session id with source=resume, before the fork id is
1239
+ // minted; verified 2026-08-05), so the fork's new instance first
1240
+ // materializes right here. Gate detection on "no heartbeat yet" so the
1241
+ // transcript scan runs once per instance lifetime, not per tool call.
1242
+ refreshPidmap(coordRoot, owner.instance_id, adapter, payload?.pid);
1243
+ }
1244
+ catch (err) {
1245
+ logError(coordRoot, err, { phase: "pre-tool-use-heal" });
1246
+ }
1247
+ // The suggested name is a pending display latch. The set-task call itself
1248
+ // runs before a name exists; every later tool waits until the exact block
1249
+ // is the first assistant text after the mint result. Cursor supplies
1250
+ // agent_message directly; Claude Code and Codex are resolved from their
1251
+ // JSONL transcripts. Cursor's current narration can provide positive
1252
+ // evidence, but cannot disprove an earlier response; afterAgentResponse
1253
+ // stamps that durable sighting. Later commentary must not erase an
1254
+ // already-correct display, and unavailable evidence must not deadlock
1255
+ // every tool.
1256
+ try {
1257
+ const displayAllowed = await enforcePendingSessionNameDisplay(coordRoot, owner.instance_id, adapter, payload, generationBoundHeartbeat(readHeartbeat(coordRoot, owner.instance_id), recordedGenerationId));
1258
+ if (!displayAllowed)
1269
1259
  return 0;
1270
1260
  }
1271
- if (norm.event_type === "tool.completed" && eventName === "post-tool-use") {
1272
- // Inject only at the successful tool boundary that actually minted or
1273
- // retried a name: the first non-empty set-task, a pending-name set-task
1274
- // retry, or the transition to lifecycle done. The
1275
- // coordination row intentionally remains pending until transcript evidence
1276
- // catches up; reading that latch alone here would re-inject after every
1277
- // later tool and make the agent print the same block repeatedly.
1278
- try {
1279
- const name = sessionNameDisplayPending(readLiveCoordinationRow(coordRoot, owner.instance_id));
1280
- if (name && toolResponseMintedSessionName(payload?.tool_response, name)) {
1281
- // Record the title before asking for it. The suggestion can still
1282
- // change afterwards (an assigned-name rewrite, a lifecycle re-mint, a
1283
- // rebuilt cache), and the agent must not be stranded for displaying
1284
- // exactly what it was handed.
1285
- stampSessionNameRequested(coordRoot, owner.instance_id, name);
1261
+ catch (err) {
1262
+ logError(coordRoot, err, { phase: "pre-tool-use-session-name" });
1263
+ }
1264
+ // Windows-native Codex + WSL UNC only: block the one cross-shell shape
1265
+ // proven to corrupt argument boundaries. Normal WSL argv calls, literal
1266
+ // bash -s scripts, native Linux/macOS sessions, and every other adapter
1267
+ // pass through. The host instructions name its concrete safe bridge.
1268
+ const unsafeShellReason = behavior.unsafeCrossShellReason?.({
1269
+ cwd: payload?.cwd,
1270
+ toolName: payload?.tool_name,
1271
+ toolInput: payload?.tool_input,
1272
+ }) ?? null;
1273
+ if (unsafeShellReason) {
1274
+ const { emitDeny } = await import("./adapter/output.js");
1275
+ emitDeny(adapter, unsafeShellReason);
1276
+ return 0;
1277
+ }
1278
+ const waiterReason = shellWaiterReason(payload?.tool_name, payload?.tool_input);
1279
+ if (waiterReason) {
1280
+ const { emitDeny } = await import("./adapter/output.js");
1281
+ emitDeny(adapter, waiterReason);
1282
+ return 0;
1283
+ }
1284
+ // G-guard for ALL adapters. Claude Code previously ran this via a
1285
+ // pre-tool-use bash adapter (which called `agent-coord verdict --rule=claim`);
1286
+ // that adapter is now deleted, so agent-hook owns the deny for every adapter.
1287
+ // emitDeny() inside emits the adapter-shaped permission JSON (claude-code +
1288
+ // codex use hookSpecificOutput.permissionDecision; cursor uses .permission).
1289
+ // apply_patch (codex) parses paths from the patch body and runs verdict
1290
+ // per-path; Edit/Write/NotebookEdit resolve a single target. Non-write tools
1291
+ // (incl. Agent) yield no targets and pass through with no deny.
1292
+ let guardAllowed = true;
1293
+ try {
1294
+ guardAllowed = await runPreToolUseGuard(coordRoot, owner.instance_id, sessionId, data, adapter);
1295
+ }
1296
+ catch (err) {
1297
+ logError(coordRoot, err, { phase: "pre-tool-use-guard" });
1298
+ }
1299
+ if (!guardAllowed)
1300
+ return 0;
1301
+ return undefined;
1302
+ }
1303
+ async function handlePostToolUse(run) {
1304
+ const { coordRoot, adapter, eventName, payload, owner } = run;
1305
+ // Inject the full instruction only at the successful tool boundary that
1306
+ // actually minted or retried a name: the first non-empty set-task, a
1307
+ // pending-name set-task retry, or the transition to lifecycle done. The
1308
+ // coordination row intentionally remains pending until transcript evidence
1309
+ // catches up, so the latch alone cannot drive injection: that would print
1310
+ // the same block after every later tool.
1311
+ //
1312
+ // Between those two extremes sits a bounded reminder. Since ADR 0182 the
1313
+ // PreToolUse gate allows a tool when transcript evidence is unavailable
1314
+ // and says so only on stderr, which the model never reads. A model that
1315
+ // skipped the block after the mint was therefore never asked again
1316
+ // (ADR 0183). While the requested title is still pending, ordinary tool
1317
+ // results carry an idempotent reminder up to a fixed count; coordination
1318
+ // chores (set-task, status, suggest-name) are skipped because a mint or
1319
+ // retry response already carries the full instruction and an end-turn
1320
+ // status is the wrong place to ask.
1321
+ try {
1322
+ const row = readLiveCoordinationRow(coordRoot, owner.instance_id);
1323
+ // A workflow child has no user-visible title to set.
1324
+ const name = coordEnv("WORKFLOW_CHILD") === "1" ? undefined : sessionNameDisplayPending(row);
1325
+ if (name && toolResponseMintedSessionName(payload?.tool_response, name)) {
1326
+ // Record the title before asking for it. The suggestion can still
1327
+ // change afterwards (an assigned-name rewrite, a lifecycle re-mint, a
1328
+ // rebuilt cache), and the agent must not be stranded for displaying
1329
+ // exactly what it was handed.
1330
+ stampSessionNameRequested(coordRoot, owner.instance_id, name);
1331
+ const { emitContext } = await import("./adapter/output.js");
1332
+ emitContext(adapter, "PostToolUse", sessionNameDisplayInstruction(name));
1333
+ }
1334
+ else if (name) {
1335
+ const due = sessionNameDisplayReminderDue(row);
1336
+ const command = extractBashCommand(payload?.tool_name, payload?.tool_input);
1337
+ if (due && !isSessionNameRemediationCommand(command, resolveBinName(coordRoot))) {
1338
+ stampSessionNameReminded(coordRoot, owner.instance_id, due);
1339
+ appendDebug(coordRoot, {
1340
+ ts: new Date().toISOString(),
1341
+ event_name: eventName,
1342
+ adapter,
1343
+ instance_id: owner.instance_id,
1344
+ effect: "session-name-display-reminded",
1345
+ session_name_pending: due,
1346
+ reminders: (row?.session_name_display_reminders ?? 0) + 1,
1347
+ });
1286
1348
  const { emitContext } = await import("./adapter/output.js");
1287
- emitContext(adapter, "PostToolUse", sessionNameDisplayInstruction(name));
1349
+ emitContext(adapter, "PostToolUse", sessionNameDisplayReminder(due));
1288
1350
  }
1289
1351
  }
1290
- catch (err) {
1291
- logError(coordRoot, err, { phase: "post-tool-use-session-name" });
1292
- }
1293
1352
  }
1294
- // Phase 7: PostToolUseFailure: release claim on failed Edit (the file
1295
- // never landed; the claim is stale). Adapter-agnostic.
1296
- if (norm.event_type === "tool.completed" && eventName === "post-tool-use-failure") {
1297
- try {
1298
- releaseClaimOnFailure(coordRoot, owner.instance_id, data, payload?.raw);
1299
- }
1300
- catch (err) {
1301
- logError(coordRoot, err, { phase: "post-tool-use-failure-release" });
1302
- }
1353
+ catch (err) {
1354
+ logError(coordRoot, err, { phase: "post-tool-use-session-name" });
1355
+ }
1356
+ }
1357
+ function handlePostToolUseFailure(run) {
1358
+ const { coordRoot, payload, owner, data } = run;
1359
+ try {
1360
+ releaseClaimOnFailure(coordRoot, owner.instance_id, data, payload?.raw);
1361
+ }
1362
+ catch (err) {
1363
+ logError(coordRoot, err, { phase: "post-tool-use-failure-release" });
1364
+ }
1365
+ }
1366
+ async function handleCompletedResponse(run) {
1367
+ const { coordRoot, adapter, eventName, raw, debugBase, ledgerRoute, hookStartedAt, hookClock } = run;
1368
+ const payload = parsePayload(raw, adapter);
1369
+ const owner = resolveOwner({ payload: payload?.raw ?? null, coordRoot });
1370
+ if (!owner) {
1371
+ appendDebug(coordRoot, { ...debugBase, skipped: "no-owner-resolved" });
1372
+ return 0;
1373
+ }
1374
+ const row = readLiveCoordinationRow(coordRoot, owner.instance_id);
1375
+ const name = sessionNameDisplayPending(row);
1376
+ const text = typeof payload?.raw.text === "string" ? payload.raw.text : "";
1377
+ const sighting = matchSessionNameDisplay(row, text);
1378
+ if (sighting) {
1379
+ stampSessionNameSeen(coordRoot, owner.instance_id, sighting.pending);
1380
+ appendDebug(coordRoot, {
1381
+ ...debugBase,
1382
+ effect: "session-name-display-stamped",
1383
+ session_name_pending: sighting.pending,
1384
+ ...(sighting.displayed === sighting.pending
1385
+ ? {}
1386
+ : { session_name_displayed: sighting.displayed, session_name_drift: true }),
1387
+ });
1388
+ }
1389
+ else if (!name) {
1390
+ // Nothing was owed. Distinct from a miss: conflating the two is what made
1391
+ // a latched Cursor session undiagnosable from this log.
1392
+ appendDebug(coordRoot, { ...debugBase, skipped: "no-pending-session-name" });
1393
+ }
1394
+ else {
1395
+ appendDebug(coordRoot, {
1396
+ ...debugBase,
1397
+ skipped: "session-name-block-absent",
1398
+ session_name_pending: name,
1399
+ reply_bytes: text.length,
1400
+ reply_leads_with_fence: /^\s*`{3,}/.test(text),
1401
+ });
1402
+ }
1403
+ if (ledgerRoute.state === "blocked") {
1404
+ appendDebug(coordRoot, {
1405
+ ...debugBase,
1406
+ skipped: "v3-control-blocked",
1407
+ reason: ledgerRoute.reason,
1408
+ owner_source: owner.source,
1409
+ });
1410
+ return 0;
1303
1411
  }
1412
+ const statusBoxPresentStrict = scanAssistantStatusBoxPresent(undefined, text);
1413
+ // The hook parser needs the completed reply long enough to scan it, but
1414
+ // durable hook intake must never retain that body. Keep every other native
1415
+ // field so turn correlation remains available to the producer.
1416
+ const { text: _completedReply, ...privacySafeRaw } = payload?.raw ?? {};
1417
+ const privacySafePayload = payload ? { ...payload, raw: privacySafeRaw } : null;
1418
+ const v3Result = recordLiveHookSignalV3({
1419
+ coordRoot,
1420
+ route: ledgerRoute,
1421
+ eventName,
1422
+ payload: privacySafePayload,
1423
+ adapter,
1424
+ instanceId: owner.instance_id,
1425
+ hook_name: eventName,
1426
+ hook_duration_ms: Math.max(0, Math.floor(performance.now() - hookStartedAt)),
1427
+ monotonic_ns: hookClock.monotonic_ns,
1428
+ turn_ritual: {
1429
+ status_box_present: statusBoxPresentStrict,
1430
+ status_box_present_strict: statusBoxPresentStrict,
1431
+ session_name_required: name !== undefined,
1432
+ session_name_present: name ? assistantTextStartsWithSessionNameBlock(text, name) : false,
1433
+ },
1434
+ });
1435
+ observeHookDebug(hookHealthState, { event_v3_state: v3Result.state });
1436
+ appendDebug(coordRoot, {
1437
+ ...debugBase,
1438
+ effect: "cursor-response-ritual-observed",
1439
+ owner_source: owner.source,
1440
+ event_v3_state: v3Result.state,
1441
+ status_box_present_strict: statusBoxPresentStrict,
1442
+ ...(v3Result.state === "observed"
1443
+ ? {
1444
+ generation_id: v3Result.generation_id,
1445
+ turn_id: v3Result.turn_id,
1446
+ response_observed_at: v3Result.observed_at,
1447
+ }
1448
+ : {}),
1449
+ });
1304
1450
  return 0;
1305
1451
  }
1306
- function scheduleCodexRuntimeContextRetry(coordRoot, nativeSessionId) {
1452
+ function scheduleRuntimeContextRetry(coordRoot, adapter, nativeSessionId) {
1307
1453
  const modulePath = process.argv[1];
1308
1454
  if (!modulePath)
1309
1455
  return;
1310
1456
  try {
1311
- const child = spawn(process.execPath, [modulePath, "runtime-context-retry", "--adapter", "codex", "--session-id", nativeSessionId], {
1457
+ const child = spawn(process.execPath, [modulePath, "runtime-context-retry", "--adapter", adapter, "--session-id", nativeSessionId], {
1312
1458
  cwd: coordRoot,
1313
1459
  detached: true,
1314
1460
  stdio: "ignore",
@@ -1322,7 +1468,7 @@ function scheduleCodexRuntimeContextRetry(coordRoot, nativeSessionId) {
1322
1468
  }
1323
1469
  async function runRuntimeContextRetryWorker(coordRoot, adapter, extra) {
1324
1470
  const nativeSessionId = extraArg(extra, "--session-id");
1325
- if (adapter !== "codex" || !nativeSessionId)
1471
+ if (!adapter || !adapterBehavior(adapter).runtimeContextRetry || !nativeSessionId)
1326
1472
  return 0;
1327
1473
  for (const [index, delayMs] of [400, 1_000].entries()) {
1328
1474
  await new Promise((resolve) => setTimeout(resolve, delayMs));
@@ -1340,7 +1486,21 @@ async function runRuntimeContextRetryWorker(coordRoot, adapter, extra) {
1340
1486
  }
1341
1487
  return 0;
1342
1488
  }
1489
+ function reportSessionNameObservationUnavailable(coordRoot, instanceId, adapter, eventName, reason) {
1490
+ appendDebug(coordRoot, {
1491
+ ts: new Date().toISOString(),
1492
+ event_name: eventName,
1493
+ adapter,
1494
+ instance_id: instanceId,
1495
+ effect: "session-name-observation-unavailable",
1496
+ reason,
1497
+ action: "fail-open",
1498
+ });
1499
+ process.stderr.write(`Harnery: session-name observation unavailable (${reason}); allowing this ${eventName} without display enforcement. The name remains pending.\n`);
1500
+ }
1343
1501
  async function enforcePendingSessionNameDisplay(coordRoot, instanceId, adapter, payload, coordination) {
1502
+ if (coordEnv("WORKFLOW_CHILD") === "1")
1503
+ return true;
1344
1504
  const name = sessionNameDisplayPending(coordination);
1345
1505
  if (!name)
1346
1506
  return true;
@@ -1353,11 +1513,7 @@ async function enforcePendingSessionNameDisplay(coordRoot, instanceId, adapter,
1353
1513
  return true;
1354
1514
  }
1355
1515
  if (inspection.state === "unavailable") {
1356
- // PostToolUse already delivered the exact block for this mint. An
1357
- // unavailable transcript cannot become evidence, and repeating a generic
1358
- // reminder on every tool creates an unbounded Codex loop. Keep the latch
1359
- // honestly pending and fail open without another instruction. A later
1360
- // readable transcript can still verify it.
1516
+ reportSessionNameObservationUnavailable(coordRoot, instanceId, adapter, "pre-tool-use", inspection.reason);
1361
1517
  return true;
1362
1518
  }
1363
1519
  // Record the refusal. Without this the gate was the one enforcement path
@@ -1384,19 +1540,18 @@ async function enforcePendingSessionNameDisplay(coordRoot, instanceId, adapter,
1384
1540
  * other opening text for every accepted title.
1385
1541
  */
1386
1542
  function inspectDisplayEvidence(instanceId, adapter, payload, coordination) {
1387
- if (adapter === "cursor" && payload?.agent_message !== undefined) {
1388
- return matchSessionNameDisplay(coordination, payload.agent_message)
1543
+ const behavior = adapterBehavior(adapter);
1544
+ const inlineText = behavior.inlineAssistantText?.(payload);
1545
+ if (inlineText !== undefined) {
1546
+ return matchSessionNameDisplay(coordination, inlineText)
1389
1547
  ? { state: "present" }
1390
1548
  : { state: "unavailable", reason: "transcript_not_ready" };
1391
1549
  }
1392
- // Codex hook payloads omit transcript_path on every event, which left this
1393
- // inspection permanently unavailable and the latch never stamped. Discover
1394
- // the rollout by session id; the scan only runs while a name is pending, so
1395
- // the latch closes after one success.
1396
- const transcriptPath = payload?.transcript_path ??
1397
- (adapter === "codex"
1398
- ? discoverCodexSessionTranscript(payload?.session_id ?? instanceId)
1399
- : undefined);
1550
+ // Some adapters omit transcript_path on every event, which left this
1551
+ // inspection permanently unavailable and the latch never stamped. The
1552
+ // behavior may discover the transcript by session id; the scan only runs
1553
+ // while a name is pending, so the latch closes after one success.
1554
+ const transcriptPath = payload?.transcript_path ?? behavior.discoverTranscript?.(payload?.session_id ?? instanceId);
1400
1555
  let verdict = { state: "absent" };
1401
1556
  for (const candidate of sessionNameDisplayAcceptedNames(coordination)) {
1402
1557
  const inspection = inspectSessionNameDisplayImmediately(transcriptPath, candidate, assistantTextStartsWithSessionNameBlock);
@@ -1671,7 +1826,7 @@ function releaseClaimOnFailure(coordRoot, instanceId, data, rawPayload) {
1671
1826
  coordRoot,
1672
1827
  owner: instanceId,
1673
1828
  nativeSessionId: before?.session_id ?? instanceId,
1674
- adapter: adapterFromPlatform(before?.platform),
1829
+ adapter: sharedAdapterFromPlatform(before?.platform, { context: "hook-claim-release" }),
1675
1830
  operation: "released",
1676
1831
  path: canonical,
1677
1832
  access: "write",
@@ -1707,22 +1862,23 @@ function cleanupSessionEnd(coordRoot, instanceId) {
1707
1862
  }
1708
1863
  }
1709
1864
  async function emitUserPromptSubmitSystemMessage(coordRoot, instanceId, sessionId, adapter, workspaceCwd, recoveryBriefing = "", hostPromptContext = "") {
1865
+ // Workflow children get no prompt-time coordination context, matching
1866
+ // SessionStart. The turn ritual is not enforced for them, the naming nudge
1867
+ // would register them as named peers, and rendering drains peer mail.
1868
+ if (coordEnv("WORKFLOW_CHILD") === "1")
1869
+ return false;
1870
+ const behavior = adapterBehavior(adapter);
1710
1871
  const { renderPromptContext } = await import("../agents/render/prompt-context.js");
1711
1872
  let additionalContext = renderPromptContext({
1712
1873
  coordRoot,
1713
1874
  instanceId,
1714
1875
  sessionId,
1715
1876
  sessionNameNudge: true,
1716
- taskNudge: adapter === "cursor" || adapter === "codex",
1717
- hostPromptReminder: adapter === "codex" || adapter === "cursor",
1718
- statusFooterNudge: adapter === "codex",
1719
- turnRitualNudge: adapter === "claude-code" ? adapter : undefined,
1877
+ ...behavior.promptContextNudges,
1720
1878
  }).trim();
1721
- if (adapter === "codex") {
1722
- const fileLinkContext = renderCodexWslFileLinkContext(coordRoot, workspaceCwd);
1723
- if (fileLinkContext) {
1724
- additionalContext = [additionalContext, fileLinkContext].filter(Boolean).join("\n\n");
1725
- }
1879
+ const fileLinkContext = behavior.fileLinkContext?.(coordRoot, workspaceCwd) ?? "";
1880
+ if (fileLinkContext) {
1881
+ additionalContext = [additionalContext, fileLinkContext].filter(Boolean).join("\n\n");
1726
1882
  }
1727
1883
  if (recoveryBriefing) {
1728
1884
  additionalContext = [additionalContext, recoveryBriefing].filter(Boolean).join("\n\n");
@@ -1764,18 +1920,10 @@ async function emitSubagentStartContext(coordRoot, instanceId, sessionId, data,
1764
1920
  emitContext(adapter, "SubagentStart", combined);
1765
1921
  }
1766
1922
  function adapterPlatform(adapter) {
1767
- if (adapter === "claude-code")
1768
- return "claude-code";
1769
1923
  return adapter;
1770
1924
  }
1771
- function adapterFromPlatform(platform) {
1772
- if (platform === "cursor")
1773
- return "cursor";
1774
- if (platform === "codex")
1775
- return "codex";
1776
- return "claude-code";
1777
- }
1778
1925
  async function emitSessionStartSystemMessage(coordRoot, instanceId, sessionId, emittedData, adapter, recoveryBriefing = "", backupCue = "") {
1926
+ const behavior = adapterBehavior(adapter);
1779
1927
  const workflowChild = coordEnv("WORKFLOW_CHILD") === "1";
1780
1928
  let additionalContext = "";
1781
1929
  // Opportunistic reconciliation makes normal session starts a failsafe for
@@ -1795,14 +1943,14 @@ async function emitSessionStartSystemMessage(coordRoot, instanceId, sessionId, e
1795
1943
  instanceId,
1796
1944
  sessionId,
1797
1945
  agentName: agentName || undefined,
1798
- platformLabel: adapter === "claude-code" ? undefined : adapter === "cursor" ? "Cursor" : "Codex",
1946
+ platformLabel: behavior.sessionStartPlatformLabel,
1799
1947
  }).trim();
1800
1948
  }
1801
- // Effect (claude-code): merge the journal recovery cue into the session-start
1802
- // context. Was a standalone additionalContext emission from the previous
1803
- // journal-on-start adapter; now that agent-hook is the single SessionStart
1804
- // entry, it folds in here.
1805
- if (adapter === "claude-code" && !workflowChild) {
1949
+ // Effect (journal-maintaining adapters): merge the journal recovery cue into
1950
+ // the session-start context. Was a standalone additionalContext emission
1951
+ // from the previous journal-on-start adapter; now that agent-hook is the
1952
+ // single SessionStart entry, it folds in here.
1953
+ if (behavior.journalMaintenance && !workflowChild) {
1806
1954
  const cue = journalRecoveryCue(coordRoot);
1807
1955
  if (cue)
1808
1956
  additionalContext = [additionalContext, cue].filter(Boolean).join("\n\n");
@@ -1813,19 +1961,14 @@ async function emitSessionStartSystemMessage(coordRoot, instanceId, sessionId, e
1813
1961
  if (backupCue && !workflowChild) {
1814
1962
  additionalContext = [additionalContext, backupCue].filter(Boolean).join("\n\n");
1815
1963
  }
1816
- if (adapter === "codex" && !workflowChild && isWslUncPath(emittedData.cwd)) {
1817
- const fileLinkContext = renderCodexWslFileLinkContext(coordRoot, emittedData.cwd);
1964
+ if (!workflowChild && behavior.bridgeFor(emittedData.cwd)) {
1965
+ const fileLinkContext = behavior.fileLinkContext?.(coordRoot, emittedData.cwd) ?? "";
1818
1966
  if (fileLinkContext) {
1819
1967
  additionalContext = [additionalContext, fileLinkContext].filter(Boolean).join("\n\n");
1820
1968
  }
1821
- const bridge = inspectCodexWslBridge(process.env, { expected: true });
1822
- if (bridge && !bridge.ok) {
1823
- additionalContext = [
1824
- additionalContext,
1825
- `Harnery hybrid warning: ${bridge.detail}. Run \`${resolveBinName(coordRoot)} doctor\` for the repair hint.`,
1826
- ]
1827
- .filter(Boolean)
1828
- .join("\n\n");
1969
+ const warning = behavior.bridgeWarning?.(coordRoot, emittedData.cwd);
1970
+ if (warning) {
1971
+ additionalContext = [additionalContext, warning].filter(Boolean).join("\n\n");
1829
1972
  }
1830
1973
  }
1831
1974
  const { emitContext } = await import("./adapter/output.js");
@@ -1837,6 +1980,7 @@ function completeRecoveryInjection(coordRoot, instanceId, sessionId, recovery) {
1837
1980
  }
1838
1981
  main()
1839
1982
  .then(async (code) => {
1983
+ clearHookRunMarker(hookRunMarkerPath);
1840
1984
  writeHookHealthCompletion(hookHealthState, {
1841
1985
  finished_at_ms: performance.now(),
1842
1986
  finished_rss_bytes: process.memoryUsage().rss,
@@ -1851,6 +1995,7 @@ main()
1851
1995
  pid: process.pid,
1852
1996
  phase: "top-level",
1853
1997
  });
1998
+ clearHookRunMarker(hookRunMarkerPath);
1854
1999
  writeHookHealthCompletion(hookHealthState, {
1855
2000
  finished_at_ms: performance.now(),
1856
2001
  finished_rss_bytes: process.memoryUsage().rss,