harnery 0.31.6 → 0.33.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 (264) hide show
  1. package/README.md +2 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commander.js +5 -1
  4. package/dist/commands/agents.d.ts +0 -9
  5. package/dist/commands/agents.d.ts.map +1 -1
  6. package/dist/commands/agents.js +280 -94
  7. package/dist/commands/browse-session.d.ts +21 -0
  8. package/dist/commands/browse-session.d.ts.map +1 -0
  9. package/dist/commands/browse-session.js +157 -0
  10. package/dist/commands/browse.d.ts.map +1 -1
  11. package/dist/commands/browse.js +205 -37
  12. package/dist/commands/checkpoint.js +1 -1
  13. package/dist/commands/claude-desktop.d.ts +2 -2
  14. package/dist/commands/claude-desktop.d.ts.map +1 -1
  15. package/dist/commands/claude-desktop.js +74 -3
  16. package/dist/commands/deinit.d.ts.map +1 -1
  17. package/dist/commands/deinit.js +4 -0
  18. package/dist/commands/docs.d.ts.map +1 -1
  19. package/dist/commands/docs.js +40 -0
  20. package/dist/commands/doctor.d.ts +4 -2
  21. package/dist/commands/doctor.d.ts.map +1 -1
  22. package/dist/commands/doctor.js +141 -19
  23. package/dist/commands/init.d.ts +2 -2
  24. package/dist/commands/init.d.ts.map +1 -1
  25. package/dist/commands/init.js +26 -5
  26. package/dist/commands/instructions.d.ts +4 -0
  27. package/dist/commands/instructions.d.ts.map +1 -0
  28. package/dist/commands/instructions.js +100 -0
  29. package/dist/commands/presence.d.ts.map +1 -1
  30. package/dist/commands/presence.js +4 -1
  31. package/dist/commands/tunnel.d.ts.map +1 -1
  32. package/dist/commands/tunnel.js +2 -3
  33. package/dist/core/agents/canonical-emit.d.ts +2 -3
  34. package/dist/core/agents/canonical-emit.d.ts.map +1 -1
  35. package/dist/core/agents/canonical-emit.js +6 -5
  36. package/dist/core/agents/cli.js +128 -89
  37. package/dist/core/agents/coord-client.d.ts +18 -3
  38. package/dist/core/agents/coord-client.d.ts.map +1 -1
  39. package/dist/core/agents/coord-client.js +5 -4
  40. package/dist/core/agents/finalization.d.ts +68 -0
  41. package/dist/core/agents/finalization.d.ts.map +1 -0
  42. package/dist/core/agents/finalization.js +443 -0
  43. package/dist/core/agents/git-hook.d.ts +51 -0
  44. package/dist/core/agents/git-hook.d.ts.map +1 -0
  45. package/dist/core/agents/git-hook.js +118 -0
  46. package/dist/core/agents/index.d.ts +1 -0
  47. package/dist/core/agents/index.d.ts.map +1 -1
  48. package/dist/core/agents/index.js +1 -0
  49. package/dist/core/agents/render/prompt-context.d.ts +12 -5
  50. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  51. package/dist/core/agents/render/prompt-context.js +70 -16
  52. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  53. package/dist/core/agents/render/session-context.js +23 -4
  54. package/dist/core/agents/rules/claim-conflict.js +3 -3
  55. package/dist/core/agents/rules/commit-conflict.d.ts +15 -6
  56. package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
  57. package/dist/core/agents/rules/commit-conflict.js +21 -5
  58. package/dist/core/agents/rules/stop-hook.d.ts +3 -0
  59. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  60. package/dist/core/agents/rules/stop-hook.js +59 -23
  61. package/dist/core/agents/session-events.d.ts +8 -16
  62. package/dist/core/agents/session-events.d.ts.map +1 -1
  63. package/dist/core/agents/session-events.js +12 -26
  64. package/dist/core/agents/state/heartbeat-projector.d.ts +14 -0
  65. package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
  66. package/dist/core/agents/state/heartbeat-projector.js +98 -1
  67. package/dist/core/agents/state/heartbeat-writer.d.ts +47 -5
  68. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  69. package/dist/core/agents/state/heartbeat-writer.js +86 -17
  70. package/dist/core/agents/state/names.d.ts +22 -1
  71. package/dist/core/agents/state/names.d.ts.map +1 -1
  72. package/dist/core/agents/state/names.js +40 -2
  73. package/dist/core/agents/state/session-state.d.ts +44 -0
  74. package/dist/core/agents/state/session-state.d.ts.map +1 -0
  75. package/dist/core/agents/state/session-state.js +81 -0
  76. package/dist/core/config.d.ts +30 -2
  77. package/dist/core/config.d.ts.map +1 -1
  78. package/dist/core/config.js +74 -11
  79. package/dist/core/governor/planning.d.ts.map +1 -1
  80. package/dist/core/governor/planning.js +12 -13
  81. package/dist/core/hooks/adapter/detect.d.ts +2 -4
  82. package/dist/core/hooks/adapter/detect.d.ts.map +1 -1
  83. package/dist/core/hooks/adapter/detect.js +4 -14
  84. package/dist/core/hooks/adapter/events.d.ts.map +1 -1
  85. package/dist/core/hooks/adapter/events.js +1 -0
  86. package/dist/core/hooks/adapter/output.d.ts +1 -1
  87. package/dist/core/hooks/adapter/output.d.ts.map +1 -1
  88. package/dist/core/hooks/adapter/output.js +5 -4
  89. package/dist/core/hooks/adapter/parse.d.ts +1 -1
  90. package/dist/core/hooks/adapter/parse.d.ts.map +1 -1
  91. package/dist/core/hooks/adapter/parse.js +2 -0
  92. package/dist/core/hooks/adapter/wiring.d.ts +23 -0
  93. package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
  94. package/dist/core/hooks/adapter/wiring.js +32 -0
  95. package/dist/core/hooks/cli.d.ts +1 -2
  96. package/dist/core/hooks/cli.d.ts.map +1 -1
  97. package/dist/core/hooks/cli.js +258 -32
  98. package/dist/core/hooks/codex-authorization.d.ts +30 -0
  99. package/dist/core/hooks/codex-authorization.d.ts.map +1 -0
  100. package/dist/core/hooks/codex-authorization.js +199 -0
  101. package/dist/core/hooks/codex-wsl-bridge.d.ts +46 -0
  102. package/dist/core/hooks/codex-wsl-bridge.d.ts.map +1 -0
  103. package/dist/core/hooks/codex-wsl-bridge.js +137 -0
  104. package/dist/core/hooks/effects/index.d.ts +1 -1
  105. package/dist/core/hooks/effects/index.js +3 -3
  106. package/dist/core/hooks/events/schema.d.ts +57 -1
  107. package/dist/core/hooks/events/schema.d.ts.map +1 -1
  108. package/dist/core/hooks/resolve/owner.d.ts +7 -1
  109. package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
  110. package/dist/core/hooks/resolve/owner.js +15 -0
  111. package/dist/core/hooks/resolve/transcript.d.ts +44 -0
  112. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  113. package/dist/core/hooks/resolve/transcript.js +176 -1
  114. package/dist/core/hooks/session-name-presence.d.ts +30 -0
  115. package/dist/core/hooks/session-name-presence.d.ts.map +1 -0
  116. package/dist/core/hooks/session-name-presence.js +43 -0
  117. package/dist/core/hooks/unsafe-cross-shell.d.ts +13 -0
  118. package/dist/core/hooks/unsafe-cross-shell.d.ts.map +1 -0
  119. package/dist/core/hooks/unsafe-cross-shell.js +151 -0
  120. package/dist/core/presence/blob.d.ts +4 -0
  121. package/dist/core/presence/blob.d.ts.map +1 -1
  122. package/dist/core/presence/blob.js +12 -0
  123. package/dist/core/presence/index.js +15 -1
  124. package/dist/core/work/state.d.ts +4 -5
  125. package/dist/core/work/state.d.ts.map +1 -1
  126. package/dist/core/work/state.js +5 -8
  127. package/dist/core/workflow/index.d.ts +1 -1
  128. package/dist/core/workflow/index.d.ts.map +1 -1
  129. package/dist/core/workflow/proof.d.ts +2 -2
  130. package/dist/core/workflow/proof.d.ts.map +1 -1
  131. package/dist/core/workflow/proof.js +2 -2
  132. package/dist/core/workflow/run-state.d.ts +2 -2
  133. package/dist/core/workflow/run-state.d.ts.map +1 -1
  134. package/dist/core/workflow/run-state.js +2 -2
  135. package/dist/core/workflow/types.d.ts +3 -3
  136. package/dist/core/workflow/types.d.ts.map +1 -1
  137. package/dist/core/workflow/workspaces/execution.d.ts +2 -2
  138. package/dist/core/workflow/workspaces/execution.d.ts.map +1 -1
  139. package/dist/core/workflow/workspaces/execution.js +5 -0
  140. package/dist/core/workflow/workspaces/index.d.ts +1 -1
  141. package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
  142. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  143. package/dist/core/workflow/workspaces/local-git.js +1 -2
  144. package/dist/core/workflow/workspaces/paths.d.ts +2 -2
  145. package/dist/core/workflow/workspaces/types.d.ts +0 -6
  146. package/dist/core/workflow/workspaces/types.d.ts.map +1 -1
  147. package/dist/core/workflow/workspaces/validate.d.ts +0 -2
  148. package/dist/core/workflow/workspaces/validate.d.ts.map +1 -1
  149. package/dist/core/workflow/workspaces/validate.js +0 -2
  150. package/dist/lib/browser/client.d.ts +118 -1
  151. package/dist/lib/browser/client.d.ts.map +1 -1
  152. package/dist/lib/browser/client.js +435 -6
  153. package/dist/lib/browser/geometry.d.ts.map +1 -1
  154. package/dist/lib/browser/geometry.js +180 -37
  155. package/dist/lib/browser/index.d.ts +5 -1
  156. package/dist/lib/browser/index.d.ts.map +1 -1
  157. package/dist/lib/browser/index.js +5 -1
  158. package/dist/lib/browser/netscape-cookies.d.ts +6 -0
  159. package/dist/lib/browser/netscape-cookies.d.ts.map +1 -0
  160. package/dist/lib/browser/netscape-cookies.js +31 -0
  161. package/dist/lib/browser/proxy.d.ts +24 -0
  162. package/dist/lib/browser/proxy.d.ts.map +1 -0
  163. package/dist/lib/browser/proxy.js +84 -0
  164. package/dist/lib/browser/runts.d.ts +6 -0
  165. package/dist/lib/browser/runts.d.ts.map +1 -1
  166. package/dist/lib/browser/runts.js +21 -4
  167. package/dist/lib/browser/session-control.d.ts +112 -0
  168. package/dist/lib/browser/session-control.d.ts.map +1 -0
  169. package/dist/lib/browser/session-control.js +670 -0
  170. package/dist/lib/claude-desktop.d.ts +47 -0
  171. package/dist/lib/claude-desktop.d.ts.map +1 -1
  172. package/dist/lib/claude-desktop.js +169 -1
  173. package/dist/lib/docs-links.d.ts +108 -0
  174. package/dist/lib/docs-links.d.ts.map +1 -0
  175. package/dist/lib/docs-links.js +555 -0
  176. package/dist/lib/exec.d.ts +1 -1
  177. package/dist/lib/exec.js +1 -1
  178. package/dist/lib/identities/assume.d.ts +4 -2
  179. package/dist/lib/identities/assume.d.ts.map +1 -1
  180. package/dist/lib/identities/assume.js +15 -2
  181. package/dist/lib/instructions/bundle.d.ts +33 -0
  182. package/dist/lib/instructions/bundle.d.ts.map +1 -0
  183. package/dist/lib/instructions/bundle.js +149 -0
  184. package/dist/lib/instructions/git-hooks.d.ts +75 -0
  185. package/dist/lib/instructions/git-hooks.d.ts.map +1 -0
  186. package/dist/lib/instructions/git-hooks.js +238 -0
  187. package/dist/lib/instructions/splice.d.ts +10 -4
  188. package/dist/lib/instructions/splice.d.ts.map +1 -1
  189. package/dist/lib/instructions/splice.js +19 -11
  190. package/package.json +2 -2
  191. package/schemas/config.schema.json +32 -0
  192. package/src/commander.ts +5 -1
  193. package/src/commands/agents.ts +341 -113
  194. package/src/commands/browse-session.ts +245 -0
  195. package/src/commands/browse.ts +287 -44
  196. package/src/commands/checkpoint.ts +1 -1
  197. package/src/commands/claude-desktop.ts +106 -5
  198. package/src/commands/deinit.ts +7 -1
  199. package/src/commands/docs.ts +56 -0
  200. package/src/commands/doctor.ts +155 -21
  201. package/src/commands/init.ts +28 -9
  202. package/src/commands/instructions.ts +129 -0
  203. package/src/commands/presence.ts +7 -1
  204. package/src/commands/tunnel.ts +2 -5
  205. package/src/core/agents/canonical-emit.ts +7 -6
  206. package/src/core/agents/cli.ts +143 -89
  207. package/src/core/agents/coord-client.ts +21 -5
  208. package/src/core/agents/finalization.ts +595 -0
  209. package/src/core/agents/git-hook.ts +126 -0
  210. package/src/core/agents/index.ts +1 -0
  211. package/src/core/agents/render/prompt-context.ts +96 -16
  212. package/src/core/agents/render/session-context.ts +29 -4
  213. package/src/core/agents/rules/claim-conflict.ts +3 -3
  214. package/src/core/agents/rules/commit-conflict.ts +35 -8
  215. package/src/core/agents/rules/stop-hook.ts +71 -23
  216. package/src/core/agents/session-events.ts +14 -41
  217. package/src/core/agents/state/heartbeat-projector.ts +108 -1
  218. package/src/core/agents/state/heartbeat-writer.ts +129 -16
  219. package/src/core/agents/state/names.ts +54 -2
  220. package/src/core/agents/state/session-state.ts +131 -0
  221. package/src/core/config.ts +92 -12
  222. package/src/core/governor/planning.ts +17 -12
  223. package/src/core/hooks/adapter/detect.ts +4 -13
  224. package/src/core/hooks/adapter/events.ts +1 -0
  225. package/src/core/hooks/adapter/output.ts +9 -4
  226. package/src/core/hooks/adapter/parse.ts +3 -0
  227. package/src/core/hooks/adapter/wiring.ts +43 -0
  228. package/src/core/hooks/cli.ts +296 -27
  229. package/src/core/hooks/codex-authorization.ts +253 -0
  230. package/src/core/hooks/codex-wsl-bridge.ts +188 -0
  231. package/src/core/hooks/effects/index.ts +5 -5
  232. package/src/core/hooks/events/schema.ts +71 -0
  233. package/src/core/hooks/resolve/owner.ts +22 -1
  234. package/src/core/hooks/resolve/transcript.ts +164 -1
  235. package/src/core/hooks/session-name-presence.ts +52 -0
  236. package/src/core/hooks/unsafe-cross-shell.ts +160 -0
  237. package/src/core/presence/blob.ts +18 -0
  238. package/src/core/presence/index.ts +18 -1
  239. package/src/core/work/state.ts +8 -12
  240. package/src/core/workflow/engine.ts +2 -2
  241. package/src/core/workflow/index.ts +0 -1
  242. package/src/core/workflow/proof.ts +4 -4
  243. package/src/core/workflow/run-state.ts +5 -5
  244. package/src/core/workflow/types.ts +3 -3
  245. package/src/core/workflow/workspaces/execution.ts +11 -4
  246. package/src/core/workflow/workspaces/index.ts +0 -3
  247. package/src/core/workflow/workspaces/local-git.ts +1 -2
  248. package/src/core/workflow/workspaces/paths.ts +2 -2
  249. package/src/core/workflow/workspaces/types.ts +0 -9
  250. package/src/core/workflow/workspaces/validate.ts +1 -5
  251. package/src/lib/browser/client.ts +528 -6
  252. package/src/lib/browser/geometry.ts +197 -37
  253. package/src/lib/browser/index.ts +39 -0
  254. package/src/lib/browser/netscape-cookies.ts +39 -0
  255. package/src/lib/browser/proxy.ts +105 -0
  256. package/src/lib/browser/runts.ts +27 -3
  257. package/src/lib/browser/session-control.ts +892 -0
  258. package/src/lib/claude-desktop.ts +232 -1
  259. package/src/lib/docs-links.ts +674 -0
  260. package/src/lib/exec.ts +1 -1
  261. package/src/lib/identities/assume.ts +22 -1
  262. package/src/lib/instructions/bundle.ts +199 -0
  263. package/src/lib/instructions/git-hooks.ts +259 -0
  264. package/src/lib/instructions/splice.ts +41 -11
@@ -84,6 +84,29 @@ export interface AdapterWiringStatus {
84
84
  * with no settings file at all, or an unparseable one, is skipped.
85
85
  */
86
86
  export declare function loadAdapterWiring(projectRoot: string): AdapterWiringStatus[];
87
+ export interface AdapterWiringSummary {
88
+ /** Adapters carrying at least one harnery hook. */
89
+ wired: AdapterId[];
90
+ /**
91
+ * Adapters carrying none: no settings file at all, or a settings file with
92
+ * none of ours in it. An unparseable file lands in neither list, since
93
+ * `loadAdapterWiring` already reports that as drift.
94
+ */
95
+ unwired: AdapterId[];
96
+ }
97
+ /**
98
+ * Classify every known adapter as wired or unwired, without judging whether an
99
+ * unwired one is a problem.
100
+ *
101
+ * `loadAdapterWiring` stays deliberately silent about an adapter with zero
102
+ * harnery hooks, because a bare settings file is not an opt-out signal and
103
+ * nagging about it would false-warn every session. That silence is right in
104
+ * isolation and wrong once you also know the adapter's CLI is installed: then
105
+ * an agent can start a session here and register nothing. Splitting the fact
106
+ * from the judgement lets a caller holding both (doctor) tell "not used here"
107
+ * from "never wired" without changing what counts as drift.
108
+ */
109
+ export declare function summarizeAdapterWiring(projectRoot: string): AdapterWiringSummary;
87
110
  /**
88
111
  * Resolve the harnery package version for context in nudges/checks. Walks up
89
112
  * from this module to the package root (works under Bun from `src/` and Node
@@ -1 +1 @@
1
- {"version":3,"file":"wiring.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/adapter/wiring.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,EAEL,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,SAAS,EACf,MAAM,aAAa,CAAC;AAErB,mEAAmE;AACnE,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC5C;AACD,0CAA0C;AAC1C,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AACD,MAAM,MAAM,SAAS,GAAG,eAAe,GAAG,eAAe,CAAC;AAE1D,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;IACpC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACtB;AAED,iDAAiD;AACjD,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,CAE3E;AAED,0EAA0E;AAC1E,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,EAAE,CAMxD;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAEnF;AAQD,MAAM,WAAW,UAAU;IACzB,kDAAkD;IAClD,OAAO,EAAE,SAAS,EAAE,CAAC;IACrB,iCAAiC;IACjC,OAAO,EAAE,SAAS,EAAE,CAAC;IACrB;;;;OAIG;IACH,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,4EAA4E;IAC5E,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,yEAAyE;IACzE,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,YAAY,EAAE,IAAI,EAAE,WAAW,GAAG,UAAU,CA2ChF;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,SAAS,CAAC;IACnB,wDAAwD;IACxD,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,SAAS,EAAE,CAAC;IACrB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,mBAAmB,EAAE,CAwC5E;AAED;;;;GAIG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAkBvC"}
1
+ {"version":3,"file":"wiring.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/adapter/wiring.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,EAEL,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,SAAS,EACf,MAAM,aAAa,CAAC;AAErB,mEAAmE;AACnE,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC5C;AACD,0CAA0C;AAC1C,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AACD,MAAM,MAAM,SAAS,GAAG,eAAe,GAAG,eAAe,CAAC;AAE1D,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;IACpC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACtB;AAED,iDAAiD;AACjD,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,CAE3E;AAED,0EAA0E;AAC1E,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,EAAE,CAMxD;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAEnF;AAQD,MAAM,WAAW,UAAU;IACzB,kDAAkD;IAClD,OAAO,EAAE,SAAS,EAAE,CAAC;IACrB,iCAAiC;IACjC,OAAO,EAAE,SAAS,EAAE,CAAC;IACrB;;;;OAIG;IACH,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,4EAA4E;IAC5E,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,yEAAyE;IACzE,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,YAAY,EAAE,IAAI,EAAE,WAAW,GAAG,UAAU,CA2ChF;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,SAAS,CAAC;IACnB,wDAAwD;IACxD,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,SAAS,EAAE,CAAC;IACrB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,mBAAmB,EAAE,CAwC5E;AAED,MAAM,WAAW,oBAAoB;IACnC,mDAAmD;IACnD,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB;;;;OAIG;IACH,OAAO,EAAE,SAAS,EAAE,CAAC;CACtB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,oBAAoB,CAkBhF;AAED;;;;GAIG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAkBvC"}
@@ -136,6 +136,38 @@ export function loadAdapterWiring(projectRoot) {
136
136
  }
137
137
  return out;
138
138
  }
139
+ /**
140
+ * Classify every known adapter as wired or unwired, without judging whether an
141
+ * unwired one is a problem.
142
+ *
143
+ * `loadAdapterWiring` stays deliberately silent about an adapter with zero
144
+ * harnery hooks, because a bare settings file is not an opt-out signal and
145
+ * nagging about it would false-warn every session. That silence is right in
146
+ * isolation and wrong once you also know the adapter's CLI is installed: then
147
+ * an agent can start a session here and register nothing. Splitting the fact
148
+ * from the judgement lets a caller holding both (doctor) tell "not used here"
149
+ * from "never wired" without changing what counts as drift.
150
+ */
151
+ export function summarizeAdapterWiring(projectRoot) {
152
+ const wired = [];
153
+ const unwired = [];
154
+ for (const [id, spec] of Object.entries(ADAPTER_SPECS)) {
155
+ const settingsPath = resolve(projectRoot, spec.settingsFile);
156
+ if (!existsSync(settingsPath)) {
157
+ unwired.push(id);
158
+ continue;
159
+ }
160
+ let settings;
161
+ try {
162
+ settings = JSON.parse(readFileSync(settingsPath, "utf8"));
163
+ }
164
+ catch {
165
+ continue; // unparseable → already surfaced by loadAdapterWiring
166
+ }
167
+ (diffWiring(settings, spec).present.length > 0 ? wired : unwired).push(id);
168
+ }
169
+ return { wired, unwired };
170
+ }
139
171
  /**
140
172
  * Resolve the harnery package version for context in nudges/checks. Walks up
141
173
  * from this module to the package root (works under Bun from `src/` and Node
@@ -1,6 +1,5 @@
1
1
  /**
2
- * `agent-hook` CLI entry point. Phase 2: real canonical-event emission
3
- * alongside the legacy stream.
2
+ * `agent-hook` CLI entry point for canonical event emission.
4
3
  *
5
4
  * Flow:
6
5
  * 1. Parse argv → event-name + adapter.
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG"}
@@ -1,6 +1,5 @@
1
1
  /**
2
- * `agent-hook` CLI entry point. Phase 2: real canonical-event emission
3
- * alongside the legacy stream.
2
+ * `agent-hook` CLI entry point for canonical event emission.
4
3
  *
5
4
  * Flow:
6
5
  * 1. Parse argv → event-name + adapter.
@@ -21,17 +20,22 @@ import { spawnSync } from "node:child_process";
21
20
  import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
22
21
  import { dirname, join } from "node:path";
23
22
  import { coordEnv } from "../../lib/env.js";
23
+ import { buildInstructionBundle } from "../../lib/instructions/bundle.js";
24
24
  import { replayCodexJsonl } from "../agents/codex-replay.js";
25
25
  import { coordBinPath } from "../agents/coord-bin.js";
26
26
  import { consumeSince, writeCursor } from "../agents/events/consume.js";
27
+ import { classifyWriteClaimFinalization, formatWriteClaimFinalizationDenial, } from "../agents/finalization.js";
27
28
  import { evaluateStopHook } from "../agents/rules/stop-hook.js";
28
29
  import { projectHeartbeats } from "../agents/state/heartbeat-projector.js";
30
+ import { stampSessionStateEvent } from "../agents/state/heartbeat-writer.js";
29
31
  import { writePidmapRow } from "../agents/state/pidmap.js";
30
32
  import { shellMutationPaths } from "../agents/state/shell-mutation.js";
33
+ import { agentsRequireGitFinalization, resolveBinName } from "../config.js";
31
34
  import { checkpointContext, completeContextRecovery, extractContextSample, markContextCompactionCompleted, prepareContextRecovery, readContextState, recordContextSample, } from "../context/index.js";
32
35
  import { ensureRelayDaemon, fetchPresence, publishPresence } from "../presence/index.js";
33
36
  import { detectAdapter } from "./adapter/detect.js";
34
37
  import { extractBashCommand, extractToolDescription, normalizeEventName, parsePayload, } from "./adapter/parse.js";
38
+ import { codexWslFileLinkTelemetry, inspectCodexWslBridge, isWslUncPath, renderCodexWslFileLinkContext, } from "./codex-wsl-bridge.js";
35
39
  import { captureImages, detectPresence, imageJanitor, journalArchive, journalJanitor, journalRecoveryCue, playSound, resetSoundCounters, runSessionSyncExtension, runTurnSummary, soundForEvent, } from "./effects/index.js";
36
40
  import { emit } from "./events/emit.js";
37
41
  import { canonicalize } from "./guard-path.js";
@@ -39,16 +43,18 @@ import { adapterPidFromEnv, parsePsChainLine, selectAnchorPid } from "./resolve/
39
43
  import { findCoordRoot } from "./resolve/coord-root.js";
40
44
  import { extractIntentComment, resolveIntent } from "./resolve/intent.js";
41
45
  import { resolveOwner } from "./resolve/owner.js";
42
- import { scanStatusBoxPresent, scanTranscriptModel } from "./resolve/transcript.js";
46
+ import { detectForkParent, scanAssistantTextIncludes, scanStatusBoxPresent, scanTranscriptModel, } from "./resolve/transcript.js";
47
+ import { sessionNamePresence } from "./session-name-presence.js";
48
+ import { unsafeCrossShellReason } from "./unsafe-cross-shell.js";
43
49
  function parseArgv(argv) {
44
50
  const out = { eventName: null, extra: [] };
45
51
  for (let i = 0; i < argv.length; i++) {
46
52
  const arg = argv[i];
47
- if (arg === "--adapter" || arg === "--harness") {
53
+ if (arg === "--adapter") {
48
54
  i++; // detectAdapter will re-parse; just consume the value here.
49
55
  continue;
50
56
  }
51
- if (arg.startsWith("--adapter=") || arg.startsWith("--harness="))
57
+ if (arg.startsWith("--adapter="))
52
58
  continue;
53
59
  if (!out.eventName && !arg.startsWith("--")) {
54
60
  out.eventName = arg;
@@ -118,12 +124,15 @@ function logError(coordRoot, err, context) {
118
124
  * Lives at agent-hooks side (not agent-coord) to keep emitter/consumer
119
125
  * separation: we spawn rather than import.
120
126
  */
121
- function assignNameViaAgentCoord(coordRoot, instanceId, kind) {
127
+ function assignNameViaAgentCoord(coordRoot, instanceId, kind, forkedFrom) {
122
128
  const binary = coordBinPath("agent-coord", coordRoot) ?? "";
123
129
  if (!existsSync(binary))
124
130
  return null;
125
131
  try {
126
- const result = spawnSync(binary, ["assign-name", instanceId, kind], {
132
+ const args = ["assign-name", instanceId, kind];
133
+ if (forkedFrom)
134
+ args.push("--forked-from", forkedFrom);
135
+ const result = spawnSync(binary, args, {
127
136
  encoding: "utf8",
128
137
  timeout: 2000,
129
138
  env: childEnv(coordRoot),
@@ -178,13 +187,21 @@ function buildEventData(eventType, ctx) {
178
187
  switch (eventType) {
179
188
  case "session.start": {
180
189
  const adapterPlatform = ctx.adapter === "claude-code"
181
- ? "claude_code"
190
+ ? "claude-code"
182
191
  : ctx.adapter === "cursor"
183
192
  ? "cursor"
184
193
  : "codex";
194
+ // Recorded fork lineage is NOT detected here. On claude-code a fork
195
+ // never fires its own session.start — SessionStart fires under the
196
+ // PARENT's session id (source=resume) before the fork id is minted
197
+ // (verified 2026-08-05) — so detection at this point can only mislabel
198
+ // the resumed parent. The fork's new instance is caught by the
199
+ // tool.pre_use heal path instead. The forkedFrom plumbing below stays
200
+ // for adapters that DO report a parent at session start.
201
+ const forkedFrom = undefined;
185
202
  // Assign (or recover) name + kind via agent-coord. Idempotent: resume
186
203
  // returns the original name; new owner consumes a counter slot.
187
- const assigned = assignNameViaAgentCoord(ctx.coordRoot, ctx.instanceId, "session");
204
+ const assigned = assignNameViaAgentCoord(ctx.coordRoot, ctx.instanceId, "session", forkedFrom);
188
205
  // Write the adapter pid-map row so `harn agents whoami` ppid-walks find
189
206
  // this owner. Prefer the payload pid (the actual claude binary), then the
190
207
  // anchor walk (the `node` ancestor for Cursor, which has no payload pid),
@@ -195,6 +212,7 @@ function buildEventData(eventType, ctx) {
195
212
  if (adapterPid) {
196
213
  writePidmapViaAgentCoord(ctx.coordRoot, adapterPid, ctx.instanceId, adapterPlatform);
197
214
  }
215
+ const bundle = safeInstructionBundle(ctx.coordRoot, p?.cwd ?? process.cwd(), ctx.adapter);
198
216
  return {
199
217
  started_at: new Date().toISOString(),
200
218
  cwd: p?.cwd ?? process.cwd(),
@@ -208,6 +226,18 @@ function buildEventData(eventType, ctx) {
208
226
  name: assigned?.name,
209
227
  kind: "session",
210
228
  agent_id: ctx.instanceId,
229
+ ...(bundle
230
+ ? {
231
+ instruction_bundle_id: bundle.instruction_bundle_id,
232
+ instruction_source_id: bundle.canonical_source_id,
233
+ instruction_profile_root: bundle.profile_root,
234
+ instruction_component_count: bundle.components.length,
235
+ }
236
+ : {}),
237
+ // Recorded fork lineage rides the canonical event too, so full replay
238
+ // and derived readers converge on the same parent linkage that
239
+ // .name-history carries (the ADR 0017 dual-write pattern).
240
+ ...(forkedFrom ? { forked_from: forkedFrom } : {}),
211
241
  // Workflow-child linkage: `workflow run` children carry the run id in
212
242
  // env (spawn adapters set it via buildChildEnv), so their sessions and
213
243
  // heartbeats join back to the run transcript + /workflows web view.
@@ -235,6 +265,10 @@ function buildEventData(eventType, ctx) {
235
265
  return { prompt_text: value, ...(truncated ? { truncated: true } : {}) };
236
266
  }
237
267
  case "turn.stop": {
268
+ const lastAssistantMessage = p?.raw.last_assistant_message ?? "";
269
+ const fileLinkTelemetry = ctx.adapter === "codex"
270
+ ? codexWslFileLinkTelemetry(ctx.coordRoot, p?.cwd, lastAssistantMessage)
271
+ : null;
238
272
  return {
239
273
  // Backfill the model for adapters that omit it at session.start
240
274
  // (Claude Code). The transcript is populated with assistant turns by
@@ -252,8 +286,24 @@ function buildEventData(eventType, ctx) {
252
286
  // text-only stop (box in last_assistant_message, no transcript), which
253
287
  // the verdict now sees because agent-hook emits this turn.stop itself
254
288
  // (the previous path passed those via the no-history fail-open).
255
- status_box_present: scanStatusBoxPresent(p?.transcript_path) ||
256
- (p?.raw.last_assistant_message ?? "").includes("┌─ agent-"),
289
+ status_box_present: scanStatusBoxPresent(p?.transcript_path) || lastAssistantMessage.includes("┌─ agent-"),
290
+ // Shadow measurement for the rule-2/3 detector: the loose scan above
291
+ // matches the box prefix ANYWHERE in the transcript tail, including
292
+ // the tool_result row the status command itself writes, so it can
293
+ // report a paste that never happened. The strict variant counts
294
+ // assistant text only (same window, same scanner as the session-name
295
+ // detection). Telemetry-only: the Stop verdict still reads
296
+ // status_box_present. Once the divergence rate between the two fields
297
+ // is known, the gate either flips to strict or the rule gets
298
+ // reconsidered; either way this field decides it with data.
299
+ status_box_present_strict: scanAssistantTextIncludes(p?.transcript_path, "┌─ agent-") ||
300
+ lastAssistantMessage.includes("┌─ agent-"),
301
+ // Whether the session's suggested name is satisfied as of this turn,
302
+ // and which name that refers to. Reported on every stop while a name
303
+ // exists (the stop-hook.session_name verdict reads it per turn); the
304
+ // transcript scan itself stops once the name has been sighted.
305
+ ...sessionNamePresence(ctx.coordRoot, ctx.instanceId, lastAssistantMessage, (name) => scanAssistantTextIncludes(p?.transcript_path, name)),
306
+ ...(fileLinkTelemetry ?? {}),
257
307
  stop_hook_active: p?.stop_hook_active,
258
308
  };
259
309
  }
@@ -301,6 +351,15 @@ function buildEventData(eventType, ctx) {
301
351
  ...(clamped.truncated ? { truncated: true } : {}),
302
352
  };
303
353
  }
354
+ case "interaction.input_requested": {
355
+ const description = extractToolDescription(p?.tool_input);
356
+ const reason = description ? clampString(description, 500).value : undefined;
357
+ return {
358
+ request_kind: "permission",
359
+ tool_name: p?.tool_name,
360
+ ...(reason ? { reason } : {}),
361
+ };
362
+ }
304
363
  case "tool.post_use": {
305
364
  const toolName = p?.tool_name ?? "unknown";
306
365
  const summary = summarizeOutput(p?.tool_response);
@@ -347,6 +406,15 @@ function buildEventData(eventType, ctx) {
347
406
  }
348
407
  }
349
408
  }
409
+ function safeInstructionBundle(coordRoot, cwd, adapter) {
410
+ try {
411
+ return buildInstructionBundle({ coordRoot, cwd, adapter });
412
+ }
413
+ catch (err) {
414
+ logError(coordRoot, err, { phase: "instruction-bundle-identity" });
415
+ return null;
416
+ }
417
+ }
350
418
  function objectRecord(value) {
351
419
  return typeof value === "object" && value !== null && !Array.isArray(value)
352
420
  ? value
@@ -449,6 +517,19 @@ async function main() {
449
517
  owner_source: owner.source,
450
518
  event_id: envelope.event_id,
451
519
  });
520
+ // Activity is only useful while it is current. Stamp prompt and permission
521
+ // evidence directly from this envelope rather than draining the global
522
+ // projector: a drain here could apply a tool event before its authorization
523
+ // verdict. Tool progress is stamped later, after the guard allows it.
524
+ if (norm.event_type === "user_prompt.submit" ||
525
+ norm.event_type === "interaction.input_requested") {
526
+ try {
527
+ stampSessionStateEvent(coordRoot, owner.instance_id, envelope);
528
+ }
529
+ catch (err) {
530
+ logError(coordRoot, err, { phase: "activity-projection" });
531
+ }
532
+ }
452
533
  // Context telemetry is opportunistic and truthful: only persist a sample
453
534
  // when the adapter payload actually exposes usage/window data. Identical
454
535
  // measurements are de-duplicated before they reach the canonical stream.
@@ -694,7 +775,7 @@ async function main() {
694
775
  }
695
776
  }
696
777
  try {
697
- const injected = await emitUserPromptSubmitSystemMessage(coordRoot, owner.instance_id, sessionId, adapter, recovery?.briefing ?? "");
778
+ const injected = await emitUserPromptSubmitSystemMessage(coordRoot, owner.instance_id, sessionId, adapter, payload?.cwd, recovery?.briefing ?? "");
698
779
  if (injected && recovery) {
699
780
  completeRecoveryInjection(coordRoot, owner.instance_id, sessionId, adapter, recovery, "UserPromptSubmit");
700
781
  }
@@ -772,13 +853,30 @@ async function main() {
772
853
  bypass: coordEnv("AGENT_COORD_BYPASS_STOP") === "1",
773
854
  workflow_child: coordEnv("WORKFLOW_CHILD") === "1",
774
855
  });
856
+ const enforcementMode = stopEnforcementMode(verdict.rule);
857
+ emit(coordRoot, {
858
+ event_type: "stop.verdict",
859
+ instance_id: owner.instance_id,
860
+ session_id: sessionId,
861
+ turn_id: payload?.turn_id,
862
+ parent_turn_id: payload?.parent_turn_id,
863
+ adapter,
864
+ data: {
865
+ allow: verdict.allow,
866
+ rule: verdict.rule,
867
+ ...(verdict.reason ? { reason: verdict.reason } : {}),
868
+ enforcement_mode: enforcementMode,
869
+ eligible: enforcementMode === "enforced",
870
+ nag_delivered: enforcementMode === "enforced" && !verdict.allow,
871
+ },
872
+ });
775
873
  if (!verdict.allow) {
776
874
  // Adapter-aware enforcement channel: Claude Code / Codex honor exit-2 +
777
875
  // stderr as a turn block; Cursor ignores exit codes (fail-open) and
778
876
  // re-prompts only via a `followup_message` it auto-submits. emitStopBlock
779
877
  // writes the right shape and returns the exit code to use.
780
878
  const { emitStopBlock } = await import("./adapter/output.js");
781
- return emitStopBlock(adapter, verdict);
879
+ return emitStopBlock(adapter, verdict, coordRoot);
782
880
  }
783
881
  }
784
882
  // Phase 7: PreToolUse: heartbeat + pid-map self-heal on every tool call.
@@ -792,7 +890,19 @@ async function main() {
792
890
  // call was restored here afterward.
793
891
  if (norm.event_type === "tool.pre_use") {
794
892
  try {
795
- healHeartbeatViaCli(coordRoot, owner.instance_id, sessionId, adapter);
893
+ // Recorded fork lineage, heal-path flavor. A forked CC conversation
894
+ // never fires its own session.start (SessionStart fires under the
895
+ // PARENT's session id with source=resume, before the fork id is
896
+ // minted; verified 2026-08-05), so the fork's new instance first
897
+ // materializes right here. Gate detection on "no heartbeat yet" so the
898
+ // transcript scan runs once per instance lifetime, not per tool call.
899
+ let forkedFrom;
900
+ if (adapter === "claude-code" &&
901
+ payload?.transcript_path &&
902
+ !existsSync(join(coordRoot, ".harnery", "active", `${owner.instance_id}.json`))) {
903
+ forkedFrom = detectForkParent(payload.transcript_path, owner.instance_id);
904
+ }
905
+ healHeartbeatViaCli(coordRoot, owner.instance_id, sessionId, adapter, forkedFrom);
796
906
  refreshPidmap(coordRoot, owner.instance_id, adapter, payload?.pid);
797
907
  }
798
908
  catch (err) {
@@ -813,6 +923,28 @@ async function main() {
813
923
  catch (err) {
814
924
  logError(coordRoot, err, { phase: "pre-tool-use-image-capture" });
815
925
  }
926
+ // Windows-native Codex + WSL UNC only: block the one cross-shell shape
927
+ // proven to corrupt argument boundaries. Normal WSL argv calls, literal
928
+ // bash -s scripts, native Linux/macOS sessions, and every other adapter
929
+ // pass through. The host instructions name its concrete safe bridge.
930
+ const unsafeShellReason = unsafeCrossShellReason({
931
+ adapter,
932
+ cwd: payload?.cwd,
933
+ toolName: payload?.tool_name,
934
+ toolInput: payload?.tool_input,
935
+ });
936
+ if (unsafeShellReason) {
937
+ emit(coordRoot, {
938
+ event_type: "decision.block",
939
+ instance_id: owner.instance_id,
940
+ session_id: sessionId,
941
+ adapter,
942
+ data: { rule: "unsafe_cross_shell", reason: unsafeShellReason },
943
+ });
944
+ const { emitDeny } = await import("./adapter/output.js");
945
+ emitDeny(adapter, unsafeShellReason);
946
+ return 0;
947
+ }
816
948
  // G-guard for ALL adapters. Claude Code previously ran this via a
817
949
  // pre-tool-use bash adapter (which called `agent-coord verdict --rule=claim`);
818
950
  // that adapter is now deleted, so agent-hook owns the deny for every adapter.
@@ -821,12 +953,21 @@ async function main() {
821
953
  // apply_patch (codex) parses paths from the patch body and runs verdict
822
954
  // per-path; Edit/Write/NotebookEdit resolve a single target. Non-write tools
823
955
  // (incl. Agent) yield no targets and pass through with no deny.
956
+ let guardAllowed = true;
824
957
  try {
825
- await runPreToolUseGuard(coordRoot, owner.instance_id, sessionId, data, adapter);
958
+ guardAllowed = await runPreToolUseGuard(coordRoot, owner.instance_id, sessionId, data, adapter);
826
959
  }
827
960
  catch (err) {
828
961
  logError(coordRoot, err, { phase: "pre-tool-use-guard" });
829
962
  }
963
+ if (guardAllowed) {
964
+ try {
965
+ stampSessionStateEvent(coordRoot, owner.instance_id, envelope);
966
+ }
967
+ catch (err) {
968
+ logError(coordRoot, err, { phase: "activity-projection" });
969
+ }
970
+ }
830
971
  // Shell-mutation warn (warn-only, never blocks). Was the cursor
831
972
  // beforeShellExecution + codex preToolUse-Bash shell-mutation-claim-log in
832
973
  // the per-adapter shell adapters. Cursor sends the command at payload.command;
@@ -901,16 +1042,52 @@ async function main() {
901
1042
  }
902
1043
  return 0;
903
1044
  }
1045
+ function stopEnforcementMode(rule) {
1046
+ if (rule === "stop-hook.codex_observe_only")
1047
+ return "observe_only";
1048
+ if (rule === "stop-hook.fail_open" || rule === "stop-hook.no_history")
1049
+ return "fail_open";
1050
+ if (rule === "stop-hook.bypass" || rule === "stop-hook.workflow_child")
1051
+ return "exempt";
1052
+ return "enforced";
1053
+ }
904
1054
  async function runPreToolUseGuard(coordRoot, instanceId, sessionId, data, adapter) {
905
1055
  const toolName = data.tool_name ?? "";
906
- const targets = collectGuardTargets(toolName, data)
907
- .map((p) => canonicalize(coordRoot, p))
908
- .filter((p) => p !== null);
1056
+ const rawTargets = collectGuardTargets(toolName, data);
1057
+ if (rawTargets.length === 0)
1058
+ return true;
1059
+ let targets;
1060
+ if (agentsRequireGitFinalization(coordRoot)) {
1061
+ const decisions = rawTargets.map((path) => classifyWriteClaimFinalization(coordRoot, path));
1062
+ const denied = decisions.find((decision) => !decision.allow);
1063
+ if (denied && !denied.allow) {
1064
+ const { emitDeny } = await import("./adapter/output.js");
1065
+ emitDeny(adapter, formatWriteClaimFinalizationDenial(denied, resolveBinName(coordRoot)));
1066
+ return false;
1067
+ }
1068
+ const allowed = decisions.filter((decision) => decision.allow);
1069
+ targets = allowed.map((decision) => ({
1070
+ path: decision.path,
1071
+ finalization: decision.descriptor,
1072
+ }));
1073
+ }
1074
+ else {
1075
+ targets = rawTargets
1076
+ .map((path) => canonicalize(coordRoot, path))
1077
+ .filter((path) => path !== null)
1078
+ .map((path) => ({ path }));
1079
+ }
909
1080
  if (targets.length === 0)
910
- return;
1081
+ return true;
911
1082
  const agentCoordBin = coordBinPath("agent-coord", coordRoot) ?? "";
912
- if (!existsSync(agentCoordBin))
913
- return;
1083
+ if (!existsSync(agentCoordBin)) {
1084
+ if (agentsRequireGitFinalization(coordRoot)) {
1085
+ const { emitDeny } = await import("./adapter/output.js");
1086
+ emitDeny(adapter, "Harnery denied the write before mutation because the claim coordinator is unavailable; repair the project's Harnery installation and retry.");
1087
+ return false;
1088
+ }
1089
+ return true;
1090
+ }
914
1091
  // For apply_patch (multi-file), collect siblings so the deny reason names
915
1092
  // them. For single-file tools the array has one entry.
916
1093
  for (const target of targets) {
@@ -918,7 +1095,7 @@ async function runPreToolUseGuard(coordRoot, instanceId, sessionId, data, adapte
918
1095
  rule: "claim",
919
1096
  instance_id: instanceId,
920
1097
  session_id: sessionId,
921
- path: target,
1098
+ path: target.path,
922
1099
  });
923
1100
  const result = spawnSync(agentCoordBin, ["verdict"], {
924
1101
  input: verdictReq,
@@ -936,10 +1113,11 @@ async function runPreToolUseGuard(coordRoot, instanceId, sessionId, data, adapte
936
1113
  continue;
937
1114
  }
938
1115
  if (parsed.allow === false) {
939
- let reason = parsed.reason ?? `Path ${target} is currently being edited by another agent.`;
1116
+ let reason = parsed.reason ?? `Path ${target.path} is currently being edited by another agent.`;
940
1117
  if (targets.length > 1) {
941
1118
  const siblings = targets
942
- .filter((p) => p !== target)
1119
+ .filter((candidate) => candidate !== target)
1120
+ .map((candidate) => candidate.path)
943
1121
  .slice(0, 3)
944
1122
  .join(", ");
945
1123
  if (siblings) {
@@ -948,9 +1126,27 @@ async function runPreToolUseGuard(coordRoot, instanceId, sessionId, data, adapte
948
1126
  }
949
1127
  const { emitDeny } = await import("./adapter/output.js");
950
1128
  emitDeny(adapter, reason);
951
- return;
952
- }
1129
+ return false;
1130
+ }
1131
+ // The verdict mutates the live heartbeat immediately, but that file is a
1132
+ // projection and can be rebuilt at any later hook. Persist the acquisition
1133
+ // in the canonical stream too. This matters most for Codex apply_patch:
1134
+ // one tool call can claim several files, and tool.pre_use's clamped payload
1135
+ // is not a durable ownership representation for those targets.
1136
+ emit(coordRoot, {
1137
+ event_type: "claim.acquire",
1138
+ instance_id: instanceId,
1139
+ session_id: sessionId,
1140
+ adapter,
1141
+ source: "agent-hooks",
1142
+ data: {
1143
+ path: target.path,
1144
+ mode: "write",
1145
+ ...(target.finalization ? { finalization: target.finalization } : {}),
1146
+ },
1147
+ });
953
1148
  }
1149
+ return true;
954
1150
  }
955
1151
  /** Canonicalize a path to monorepo-relative form. Absolute paths under
956
1152
  * coordRoot get the prefix stripped; relative paths pass through (assumed
@@ -1008,15 +1204,19 @@ function parseApplyPatchPaths(data) {
1008
1204
  }
1009
1205
  return out;
1010
1206
  }
1011
- function healHeartbeatViaCli(coordRoot, instanceId, sessionId, adapter) {
1207
+ function healHeartbeatViaCli(coordRoot, instanceId, sessionId, adapter, forkedFrom) {
1012
1208
  const agentCoordBin = coordBinPath("agent-coord", coordRoot) ?? "";
1013
1209
  if (!existsSync(agentCoordBin))
1014
1210
  return;
1015
1211
  // Pass the detected adapter so a pruned Cursor/Codex heartbeat is recreated
1016
1212
  // with the correct platform; without it, healHeartbeat defaults to
1017
- // claude_code and the dashboard mislabels the agent. See
1018
- // heartbeat-writer.healHeartbeat.
1019
- spawnSync(agentCoordBin, ["heal-heartbeat", instanceId, sessionId, `--adapter=${adapter}`], {
1213
+ // claude-code and the dashboard mislabels the agent. See
1214
+ // heartbeat-writer.healHeartbeat. --forked-from carries detected fork
1215
+ // lineage for an instance that never session.started (the CC fork flow).
1216
+ const args = ["heal-heartbeat", instanceId, sessionId, `--adapter=${adapter}`];
1217
+ if (forkedFrom)
1218
+ args.push(`--forked-from=${forkedFrom}`);
1219
+ spawnSync(agentCoordBin, args, {
1020
1220
  encoding: "utf8",
1021
1221
  timeout: 2000,
1022
1222
  env: childEnv(coordRoot),
@@ -1238,7 +1438,7 @@ function cleanupSessionEnd(coordRoot, instanceId, reason) {
1238
1438
  spawnSync(agentCoordBin, ["log", `SESSION_END reason=${reason}`, "--instance", instanceId], { encoding: "utf8", timeout: 2000 });
1239
1439
  }
1240
1440
  }
1241
- async function emitUserPromptSubmitSystemMessage(coordRoot, instanceId, sessionId, adapter, recoveryBriefing = "") {
1441
+ async function emitUserPromptSubmitSystemMessage(coordRoot, instanceId, sessionId, adapter, workspaceCwd, recoveryBriefing = "") {
1242
1442
  const agentCoordBin = coordBinPath("agent-coord", coordRoot) ?? "";
1243
1443
  let additionalContext = "";
1244
1444
  if (existsSync(agentCoordBin)) {
@@ -1263,15 +1463,26 @@ async function emitUserPromptSubmitSystemMessage(coordRoot, instanceId, sessionI
1263
1463
  // staleness nudges; Claude Code enforces those via its Stop transcript.
1264
1464
  // Codex gets a fresh status-footer reminder on every prompt. Its Stop hook
1265
1465
  // stays observe-only, so missing the footer never replaces the answer.
1466
+ // Claude Code + Cursor (the Stop-ENFORCING adapters) get a fresh per-turn
1467
+ // ritual reminder: satisfying the contract up front is far cheaper than
1468
+ // the bounce-and-retry the Stop hook otherwise forces.
1266
1469
  args.push("--session-name-nudge");
1267
1470
  if (adapter === "cursor" || adapter === "codex")
1268
1471
  args.push("--task-nudge");
1269
1472
  if (adapter === "codex")
1270
1473
  args.push("--status-footer-nudge");
1474
+ if (adapter === "claude-code" || adapter === "cursor")
1475
+ args.push("--turn-ritual-nudge", adapter);
1271
1476
  const result = spawnSync(agentCoordBin, args, { encoding: "utf8", timeout: 3000 });
1272
1477
  if (result.status === 0 && result.stdout)
1273
1478
  additionalContext = result.stdout.trim();
1274
1479
  }
1480
+ if (adapter === "codex") {
1481
+ const fileLinkContext = renderCodexWslFileLinkContext(coordRoot, workspaceCwd);
1482
+ if (fileLinkContext) {
1483
+ additionalContext = [additionalContext, fileLinkContext].filter(Boolean).join("\n\n");
1484
+ }
1485
+ }
1275
1486
  if (recoveryBriefing) {
1276
1487
  additionalContext = [additionalContext, recoveryBriefing].filter(Boolean).join("\n\n");
1277
1488
  }
@@ -1310,7 +1521,7 @@ function emitSubagentStartContext(coordRoot, instanceId, sessionId, data, adapte
1310
1521
  }
1311
1522
  function adapterPlatform(adapter) {
1312
1523
  if (adapter === "claude-code")
1313
- return "claude_code";
1524
+ return "claude-code";
1314
1525
  return adapter;
1315
1526
  }
1316
1527
  async function emitSessionStartSystemMessage(coordRoot, instanceId, sessionId, emittedData, adapter, recoveryBriefing = "") {
@@ -1359,6 +1570,21 @@ async function emitSessionStartSystemMessage(coordRoot, instanceId, sessionId, e
1359
1570
  if (recoveryBriefing) {
1360
1571
  additionalContext = [additionalContext, recoveryBriefing].filter(Boolean).join("\n\n");
1361
1572
  }
1573
+ if (adapter === "codex" && isWslUncPath(emittedData.cwd)) {
1574
+ const fileLinkContext = renderCodexWslFileLinkContext(coordRoot, emittedData.cwd);
1575
+ if (fileLinkContext) {
1576
+ additionalContext = [additionalContext, fileLinkContext].filter(Boolean).join("\n\n");
1577
+ }
1578
+ const bridge = inspectCodexWslBridge(process.env, { expected: true });
1579
+ if (bridge && !bridge.ok) {
1580
+ additionalContext = [
1581
+ additionalContext,
1582
+ `Harnery hybrid warning: ${bridge.detail}. Run \`${resolveBinName(coordRoot)} doctor\` for the repair hint.`,
1583
+ ]
1584
+ .filter(Boolean)
1585
+ .join("\n\n");
1586
+ }
1587
+ }
1362
1588
  if (!additionalContext)
1363
1589
  return false;
1364
1590
  const { emitContext } = await import("./adapter/output.js");
@@ -0,0 +1,30 @@
1
+ export type CodexHookTrustStatus = "managed" | "trusted" | "untrusted" | "modified";
2
+ export type CodexHookAuthorizationStatus = "runnable" | "review_required" | "disabled" | "not_wired" | "error";
3
+ export interface CodexHookAuthorizationResult {
4
+ status: CodexHookAuthorizationStatus;
5
+ detail: string;
6
+ hookCount: number;
7
+ disabledCount: number;
8
+ trustCounts: Partial<Record<CodexHookTrustStatus, number>>;
9
+ }
10
+ export interface CodexHookAuthorizationProbeOptions {
11
+ cwd: string;
12
+ codexBin?: string;
13
+ appServerArgs?: string[];
14
+ env?: NodeJS.ProcessEnv;
15
+ timeoutMs?: number;
16
+ }
17
+ /**
18
+ * Convert Codex app-server's `hooks/list` response into the one authorization
19
+ * state doctor needs. This is intentionally read-only: hashes are observed but
20
+ * never persisted, and only commands containing Harnery's `agent-hook` binary
21
+ * are included in the result.
22
+ */
23
+ export declare function assessCodexHookList(response: unknown, cwd: string): CodexHookAuthorizationResult;
24
+ /**
25
+ * Ask the active Codex CLI for hook authorization through its app-server
26
+ * protocol. The child is closed as soon as `hooks/list` responds. Harnery never
27
+ * calls the trust-write method and never reads Codex's private config format.
28
+ */
29
+ export declare function probeCodexHookAuthorization(options: CodexHookAuthorizationProbeOptions): Promise<CodexHookAuthorizationResult>;
30
+ //# sourceMappingURL=codex-authorization.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex-authorization.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/codex-authorization.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,oBAAoB,GAAG,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,UAAU,CAAC;AAEpF,MAAM,MAAM,4BAA4B,GACpC,UAAU,GACV,iBAAiB,GACjB,UAAU,GACV,WAAW,GACX,OAAO,CAAC;AAEZ,MAAM,WAAW,4BAA4B;IAC3C,MAAM,EAAE,4BAA4B,CAAC;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,OAAO,CAAC,MAAM,CAAC,oBAAoB,EAAE,MAAM,CAAC,CAAC,CAAC;CAC5D;AAED,MAAM,WAAW,kCAAkC;IACjD,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA0BD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,4BAA4B,CAqGhG;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,kCAAkC,GAC1C,OAAO,CAAC,4BAA4B,CAAC,CAoFvC"}