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
@@ -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.
@@ -22,13 +21,22 @@ import { spawnSync } from "node:child_process";
22
21
  import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
23
22
  import { dirname, join } from "node:path";
24
23
  import { coordEnv } from "../../lib/env.ts";
24
+ import { buildInstructionBundle } from "../../lib/instructions/bundle.ts";
25
25
  import { replayCodexJsonl } from "../agents/codex-replay.ts";
26
26
  import { coordBinPath } from "../agents/coord-bin.ts";
27
27
  import { consumeSince, writeCursor } from "../agents/events/consume.ts";
28
+ import {
29
+ type ClaimFinalizationDecision,
30
+ type ClaimFinalizationDescriptor,
31
+ classifyWriteClaimFinalization,
32
+ formatWriteClaimFinalizationDenial,
33
+ } from "../agents/finalization.ts";
28
34
  import { evaluateStopHook } from "../agents/rules/stop-hook.ts";
29
35
  import { projectHeartbeats } from "../agents/state/heartbeat-projector.ts";
36
+ import { stampSessionStateEvent } from "../agents/state/heartbeat-writer.ts";
30
37
  import { writePidmapRow } from "../agents/state/pidmap.ts";
31
38
  import { shellMutationPaths } from "../agents/state/shell-mutation.ts";
39
+ import { agentsRequireGitFinalization, resolveBinName } from "../config.ts";
32
40
  import {
33
41
  checkpointContext,
34
42
  completeContextRecovery,
@@ -49,6 +57,12 @@ import {
49
57
  type ParsedPayload,
50
58
  parsePayload,
51
59
  } from "./adapter/parse.ts";
60
+ import {
61
+ codexWslFileLinkTelemetry,
62
+ inspectCodexWslBridge,
63
+ isWslUncPath,
64
+ renderCodexWslFileLinkContext,
65
+ } from "./codex-wsl-bridge.ts";
52
66
  import {
53
67
  captureImages,
54
68
  detectPresence,
@@ -69,7 +83,14 @@ import { adapterPidFromEnv, parsePsChainLine, selectAnchorPid } from "./resolve/
69
83
  import { findCoordRoot } from "./resolve/coord-root.ts";
70
84
  import { extractIntentComment, resolveIntent } from "./resolve/intent.ts";
71
85
  import { resolveOwner } from "./resolve/owner.ts";
72
- import { scanStatusBoxPresent, scanTranscriptModel } from "./resolve/transcript.ts";
86
+ import {
87
+ detectForkParent,
88
+ scanAssistantTextIncludes,
89
+ scanStatusBoxPresent,
90
+ scanTranscriptModel,
91
+ } from "./resolve/transcript.ts";
92
+ import { sessionNamePresence } from "./session-name-presence.ts";
93
+ import { unsafeCrossShellReason } from "./unsafe-cross-shell.ts";
73
94
 
74
95
  interface Argv {
75
96
  eventName: string | null;
@@ -80,11 +101,11 @@ function parseArgv(argv: string[]): Argv {
80
101
  const out: Argv = { eventName: null, extra: [] };
81
102
  for (let i = 0; i < argv.length; i++) {
82
103
  const arg = argv[i]!;
83
- if (arg === "--adapter" || arg === "--harness") {
104
+ if (arg === "--adapter") {
84
105
  i++; // detectAdapter will re-parse; just consume the value here.
85
106
  continue;
86
107
  }
87
- if (arg.startsWith("--adapter=") || arg.startsWith("--harness=")) continue;
108
+ if (arg.startsWith("--adapter=")) continue;
88
109
  if (!out.eventName && !arg.startsWith("--")) {
89
110
  out.eventName = arg;
90
111
  } else {
@@ -160,11 +181,14 @@ function assignNameViaAgentCoord(
160
181
  coordRoot: string,
161
182
  instanceId: string,
162
183
  kind: "session" | "subagent" | "transient",
184
+ forkedFrom?: string,
163
185
  ): { name: string; kind: string } | null {
164
186
  const binary = coordBinPath("agent-coord", coordRoot) ?? "";
165
187
  if (!existsSync(binary)) return null;
166
188
  try {
167
- const result = spawnSync(binary, ["assign-name", instanceId, kind], {
189
+ const args = ["assign-name", instanceId, kind];
190
+ if (forkedFrom) args.push("--forked-from", forkedFrom);
191
+ const result = spawnSync(binary, args, {
168
192
  encoding: "utf8",
169
193
  timeout: 2000,
170
194
  env: childEnv(coordRoot),
@@ -238,13 +262,26 @@ function buildEventData(
238
262
  case "session.start": {
239
263
  const adapterPlatform =
240
264
  ctx.adapter === "claude-code"
241
- ? "claude_code"
265
+ ? "claude-code"
242
266
  : ctx.adapter === "cursor"
243
267
  ? "cursor"
244
268
  : "codex";
269
+ // Recorded fork lineage is NOT detected here. On claude-code a fork
270
+ // never fires its own session.start — SessionStart fires under the
271
+ // PARENT's session id (source=resume) before the fork id is minted
272
+ // (verified 2026-08-05) — so detection at this point can only mislabel
273
+ // the resumed parent. The fork's new instance is caught by the
274
+ // tool.pre_use heal path instead. The forkedFrom plumbing below stays
275
+ // for adapters that DO report a parent at session start.
276
+ const forkedFrom: string | undefined = undefined;
245
277
  // Assign (or recover) name + kind via agent-coord. Idempotent: resume
246
278
  // returns the original name; new owner consumes a counter slot.
247
- const assigned = assignNameViaAgentCoord(ctx.coordRoot, ctx.instanceId, "session");
279
+ const assigned = assignNameViaAgentCoord(
280
+ ctx.coordRoot,
281
+ ctx.instanceId,
282
+ "session",
283
+ forkedFrom,
284
+ );
248
285
  // Write the adapter pid-map row so `harn agents whoami` ppid-walks find
249
286
  // this owner. Prefer the payload pid (the actual claude binary), then the
250
287
  // anchor walk (the `node` ancestor for Cursor, which has no payload pid),
@@ -255,6 +292,7 @@ function buildEventData(
255
292
  if (adapterPid) {
256
293
  writePidmapViaAgentCoord(ctx.coordRoot, adapterPid, ctx.instanceId, adapterPlatform);
257
294
  }
295
+ const bundle = safeInstructionBundle(ctx.coordRoot, p?.cwd ?? process.cwd(), ctx.adapter);
258
296
  return {
259
297
  started_at: new Date().toISOString(),
260
298
  cwd: p?.cwd ?? process.cwd(),
@@ -268,6 +306,18 @@ function buildEventData(
268
306
  name: assigned?.name,
269
307
  kind: "session",
270
308
  agent_id: ctx.instanceId,
309
+ ...(bundle
310
+ ? {
311
+ instruction_bundle_id: bundle.instruction_bundle_id,
312
+ instruction_source_id: bundle.canonical_source_id,
313
+ instruction_profile_root: bundle.profile_root,
314
+ instruction_component_count: bundle.components.length,
315
+ }
316
+ : {}),
317
+ // Recorded fork lineage rides the canonical event too, so full replay
318
+ // and derived readers converge on the same parent linkage that
319
+ // .name-history carries (the ADR 0017 dual-write pattern).
320
+ ...(forkedFrom ? { forked_from: forkedFrom } : {}),
271
321
  // Workflow-child linkage: `workflow run` children carry the run id in
272
322
  // env (spawn adapters set it via buildChildEnv), so their sessions and
273
323
  // heartbeats join back to the run transcript + /workflows web view.
@@ -298,6 +348,11 @@ function buildEventData(
298
348
  }
299
349
 
300
350
  case "turn.stop": {
351
+ const lastAssistantMessage = (p?.raw.last_assistant_message as string | undefined) ?? "";
352
+ const fileLinkTelemetry =
353
+ ctx.adapter === "codex"
354
+ ? codexWslFileLinkTelemetry(ctx.coordRoot, p?.cwd, lastAssistantMessage)
355
+ : null;
301
356
  return {
302
357
  // Backfill the model for adapters that omit it at session.start
303
358
  // (Claude Code). The transcript is populated with assistant turns by
@@ -316,8 +371,27 @@ function buildEventData(
316
371
  // the verdict now sees because agent-hook emits this turn.stop itself
317
372
  // (the previous path passed those via the no-history fail-open).
318
373
  status_box_present:
319
- scanStatusBoxPresent(p?.transcript_path) ||
320
- ((p?.raw.last_assistant_message as string | undefined) ?? "").includes("┌─ agent-"),
374
+ scanStatusBoxPresent(p?.transcript_path) || lastAssistantMessage.includes("┌─ agent-"),
375
+ // Shadow measurement for the rule-2/3 detector: the loose scan above
376
+ // matches the box prefix ANYWHERE in the transcript tail, including
377
+ // the tool_result row the status command itself writes, so it can
378
+ // report a paste that never happened. The strict variant counts
379
+ // assistant text only (same window, same scanner as the session-name
380
+ // detection). Telemetry-only: the Stop verdict still reads
381
+ // status_box_present. Once the divergence rate between the two fields
382
+ // is known, the gate either flips to strict or the rule gets
383
+ // reconsidered; either way this field decides it with data.
384
+ status_box_present_strict:
385
+ scanAssistantTextIncludes(p?.transcript_path, "┌─ agent-") ||
386
+ lastAssistantMessage.includes("┌─ agent-"),
387
+ // Whether the session's suggested name is satisfied as of this turn,
388
+ // and which name that refers to. Reported on every stop while a name
389
+ // exists (the stop-hook.session_name verdict reads it per turn); the
390
+ // transcript scan itself stops once the name has been sighted.
391
+ ...sessionNamePresence(ctx.coordRoot, ctx.instanceId, lastAssistantMessage, (name) =>
392
+ scanAssistantTextIncludes(p?.transcript_path, name),
393
+ ),
394
+ ...(fileLinkTelemetry ?? {}),
321
395
  stop_hook_active: p?.stop_hook_active,
322
396
  };
323
397
  }
@@ -372,6 +446,16 @@ function buildEventData(
372
446
  };
373
447
  }
374
448
 
449
+ case "interaction.input_requested": {
450
+ const description = extractToolDescription(p?.tool_input);
451
+ const reason = description ? clampString(description, 500).value : undefined;
452
+ return {
453
+ request_kind: "permission",
454
+ tool_name: p?.tool_name,
455
+ ...(reason ? { reason } : {}),
456
+ };
457
+ }
458
+
375
459
  case "tool.post_use": {
376
460
  const toolName = p?.tool_name ?? "unknown";
377
461
  const summary = summarizeOutput(p?.tool_response);
@@ -425,6 +509,15 @@ function buildEventData(
425
509
  }
426
510
  }
427
511
 
512
+ function safeInstructionBundle(coordRoot: string, cwd: string, adapter: Adapter) {
513
+ try {
514
+ return buildInstructionBundle({ coordRoot, cwd, adapter });
515
+ } catch (err) {
516
+ logError(coordRoot, err, { phase: "instruction-bundle-identity" });
517
+ return null;
518
+ }
519
+ }
520
+
428
521
  function objectRecord(value: unknown): Record<string, unknown> | undefined {
429
522
  return typeof value === "object" && value !== null && !Array.isArray(value)
430
523
  ? (value as Record<string, unknown>)
@@ -539,6 +632,21 @@ async function main(): Promise<number> {
539
632
  event_id: envelope.event_id,
540
633
  });
541
634
 
635
+ // Activity is only useful while it is current. Stamp prompt and permission
636
+ // evidence directly from this envelope rather than draining the global
637
+ // projector: a drain here could apply a tool event before its authorization
638
+ // verdict. Tool progress is stamped later, after the guard allows it.
639
+ if (
640
+ norm.event_type === "user_prompt.submit" ||
641
+ norm.event_type === "interaction.input_requested"
642
+ ) {
643
+ try {
644
+ stampSessionStateEvent(coordRoot, owner.instance_id, envelope);
645
+ } catch (err) {
646
+ logError(coordRoot, err, { phase: "activity-projection" });
647
+ }
648
+ }
649
+
542
650
  // Context telemetry is opportunistic and truthful: only persist a sample
543
651
  // when the adapter payload actually exposes usage/window data. Identical
544
652
  // measurements are de-duplicated before they reach the canonical stream.
@@ -805,6 +913,7 @@ async function main(): Promise<number> {
805
913
  owner.instance_id,
806
914
  sessionId,
807
915
  adapter,
916
+ payload?.cwd,
808
917
  recovery?.briefing ?? "",
809
918
  );
810
919
  if (injected && recovery) {
@@ -891,13 +1000,30 @@ async function main(): Promise<number> {
891
1000
  bypass: coordEnv("AGENT_COORD_BYPASS_STOP") === "1",
892
1001
  workflow_child: coordEnv("WORKFLOW_CHILD") === "1",
893
1002
  });
1003
+ const enforcementMode = stopEnforcementMode(verdict.rule);
1004
+ emit(coordRoot, {
1005
+ event_type: "stop.verdict",
1006
+ instance_id: owner.instance_id,
1007
+ session_id: sessionId,
1008
+ turn_id: payload?.turn_id,
1009
+ parent_turn_id: payload?.parent_turn_id,
1010
+ adapter,
1011
+ data: {
1012
+ allow: verdict.allow,
1013
+ rule: verdict.rule,
1014
+ ...(verdict.reason ? { reason: verdict.reason } : {}),
1015
+ enforcement_mode: enforcementMode,
1016
+ eligible: enforcementMode === "enforced",
1017
+ nag_delivered: enforcementMode === "enforced" && !verdict.allow,
1018
+ },
1019
+ });
894
1020
  if (!verdict.allow) {
895
1021
  // Adapter-aware enforcement channel: Claude Code / Codex honor exit-2 +
896
1022
  // stderr as a turn block; Cursor ignores exit codes (fail-open) and
897
1023
  // re-prompts only via a `followup_message` it auto-submits. emitStopBlock
898
1024
  // writes the right shape and returns the exit code to use.
899
1025
  const { emitStopBlock } = await import("./adapter/output.ts");
900
- return emitStopBlock(adapter, verdict);
1026
+ return emitStopBlock(adapter, verdict, coordRoot);
901
1027
  }
902
1028
  }
903
1029
 
@@ -912,7 +1038,21 @@ async function main(): Promise<number> {
912
1038
  // call was restored here afterward.
913
1039
  if (norm.event_type === "tool.pre_use") {
914
1040
  try {
915
- healHeartbeatViaCli(coordRoot, owner.instance_id, sessionId, adapter);
1041
+ // Recorded fork lineage, heal-path flavor. A forked CC conversation
1042
+ // never fires its own session.start (SessionStart fires under the
1043
+ // PARENT's session id with source=resume, before the fork id is
1044
+ // minted; verified 2026-08-05), so the fork's new instance first
1045
+ // materializes right here. Gate detection on "no heartbeat yet" so the
1046
+ // transcript scan runs once per instance lifetime, not per tool call.
1047
+ let forkedFrom: string | undefined;
1048
+ if (
1049
+ adapter === "claude-code" &&
1050
+ payload?.transcript_path &&
1051
+ !existsSync(join(coordRoot, ".harnery", "active", `${owner.instance_id}.json`))
1052
+ ) {
1053
+ forkedFrom = detectForkParent(payload.transcript_path, owner.instance_id);
1054
+ }
1055
+ healHeartbeatViaCli(coordRoot, owner.instance_id, sessionId, adapter, forkedFrom);
916
1056
  refreshPidmap(coordRoot, owner.instance_id, adapter, payload?.pid);
917
1057
  } catch (err) {
918
1058
  logError(coordRoot, err, { phase: "pre-tool-use-heal" });
@@ -933,6 +1073,29 @@ async function main(): Promise<number> {
933
1073
  logError(coordRoot, err, { phase: "pre-tool-use-image-capture" });
934
1074
  }
935
1075
 
1076
+ // Windows-native Codex + WSL UNC only: block the one cross-shell shape
1077
+ // proven to corrupt argument boundaries. Normal WSL argv calls, literal
1078
+ // bash -s scripts, native Linux/macOS sessions, and every other adapter
1079
+ // pass through. The host instructions name its concrete safe bridge.
1080
+ const unsafeShellReason = unsafeCrossShellReason({
1081
+ adapter,
1082
+ cwd: payload?.cwd,
1083
+ toolName: payload?.tool_name,
1084
+ toolInput: payload?.tool_input,
1085
+ });
1086
+ if (unsafeShellReason) {
1087
+ emit(coordRoot, {
1088
+ event_type: "decision.block",
1089
+ instance_id: owner.instance_id,
1090
+ session_id: sessionId,
1091
+ adapter,
1092
+ data: { rule: "unsafe_cross_shell", reason: unsafeShellReason },
1093
+ });
1094
+ const { emitDeny } = await import("./adapter/output.ts");
1095
+ emitDeny(adapter, unsafeShellReason);
1096
+ return 0;
1097
+ }
1098
+
936
1099
  // G-guard for ALL adapters. Claude Code previously ran this via a
937
1100
  // pre-tool-use bash adapter (which called `agent-coord verdict --rule=claim`);
938
1101
  // that adapter is now deleted, so agent-hook owns the deny for every adapter.
@@ -941,12 +1104,27 @@ async function main(): Promise<number> {
941
1104
  // apply_patch (codex) parses paths from the patch body and runs verdict
942
1105
  // per-path; Edit/Write/NotebookEdit resolve a single target. Non-write tools
943
1106
  // (incl. Agent) yield no targets and pass through with no deny.
1107
+ let guardAllowed = true;
944
1108
  try {
945
- await runPreToolUseGuard(coordRoot, owner.instance_id, sessionId, data, adapter);
1109
+ guardAllowed = await runPreToolUseGuard(
1110
+ coordRoot,
1111
+ owner.instance_id,
1112
+ sessionId,
1113
+ data,
1114
+ adapter,
1115
+ );
946
1116
  } catch (err) {
947
1117
  logError(coordRoot, err, { phase: "pre-tool-use-guard" });
948
1118
  }
949
1119
 
1120
+ if (guardAllowed) {
1121
+ try {
1122
+ stampSessionStateEvent(coordRoot, owner.instance_id, envelope);
1123
+ } catch (err) {
1124
+ logError(coordRoot, err, { phase: "activity-projection" });
1125
+ }
1126
+ }
1127
+
950
1128
  // Shell-mutation warn (warn-only, never blocks). Was the cursor
951
1129
  // beforeShellExecution + codex preToolUse-Bash shell-mutation-claim-log in
952
1130
  // the per-adapter shell adapters. Cursor sends the command at payload.command;
@@ -1025,21 +1203,60 @@ async function main(): Promise<number> {
1025
1203
  return 0;
1026
1204
  }
1027
1205
 
1206
+ function stopEnforcementMode(rule: string): "enforced" | "observe_only" | "exempt" | "fail_open" {
1207
+ if (rule === "stop-hook.codex_observe_only") return "observe_only";
1208
+ if (rule === "stop-hook.fail_open" || rule === "stop-hook.no_history") return "fail_open";
1209
+ if (rule === "stop-hook.bypass" || rule === "stop-hook.workflow_child") return "exempt";
1210
+ return "enforced";
1211
+ }
1212
+
1028
1213
  async function runPreToolUseGuard(
1029
1214
  coordRoot: string,
1030
1215
  instanceId: string,
1031
1216
  sessionId: string,
1032
1217
  data: Record<string, unknown>,
1033
1218
  adapter: Adapter,
1034
- ): Promise<void> {
1219
+ ): Promise<boolean> {
1035
1220
  const toolName = (data.tool_name as string | undefined) ?? "";
1036
- const targets = collectGuardTargets(toolName, data)
1037
- .map((p) => canonicalize(coordRoot, p))
1038
- .filter((p): p is string => p !== null);
1039
- if (targets.length === 0) return;
1221
+ const rawTargets = collectGuardTargets(toolName, data);
1222
+ if (rawTargets.length === 0) return true;
1223
+
1224
+ let targets: Array<{ path: string; finalization?: ClaimFinalizationDescriptor }>;
1225
+ if (agentsRequireGitFinalization(coordRoot)) {
1226
+ const decisions = rawTargets.map((path) => classifyWriteClaimFinalization(coordRoot, path));
1227
+ const denied = decisions.find((decision) => !decision.allow);
1228
+ if (denied && !denied.allow) {
1229
+ const { emitDeny } = await import("./adapter/output.ts");
1230
+ emitDeny(adapter, formatWriteClaimFinalizationDenial(denied, resolveBinName(coordRoot)));
1231
+ return false;
1232
+ }
1233
+ const allowed = decisions.filter(
1234
+ (decision): decision is Extract<ClaimFinalizationDecision, { allow: true }> => decision.allow,
1235
+ );
1236
+ targets = allowed.map((decision) => ({
1237
+ path: decision.path,
1238
+ finalization: decision.descriptor,
1239
+ }));
1240
+ } else {
1241
+ targets = rawTargets
1242
+ .map((path) => canonicalize(coordRoot, path))
1243
+ .filter((path): path is string => path !== null)
1244
+ .map((path) => ({ path }));
1245
+ }
1246
+ if (targets.length === 0) return true;
1040
1247
 
1041
1248
  const agentCoordBin = coordBinPath("agent-coord", coordRoot) ?? "";
1042
- if (!existsSync(agentCoordBin)) return;
1249
+ if (!existsSync(agentCoordBin)) {
1250
+ if (agentsRequireGitFinalization(coordRoot)) {
1251
+ const { emitDeny } = await import("./adapter/output.ts");
1252
+ emitDeny(
1253
+ adapter,
1254
+ "Harnery denied the write before mutation because the claim coordinator is unavailable; repair the project's Harnery installation and retry.",
1255
+ );
1256
+ return false;
1257
+ }
1258
+ return true;
1259
+ }
1043
1260
 
1044
1261
  // For apply_patch (multi-file), collect siblings so the deny reason names
1045
1262
  // them. For single-file tools the array has one entry.
@@ -1048,7 +1265,7 @@ async function runPreToolUseGuard(
1048
1265
  rule: "claim",
1049
1266
  instance_id: instanceId,
1050
1267
  session_id: sessionId,
1051
- path: target,
1268
+ path: target.path,
1052
1269
  });
1053
1270
  const result = spawnSync(agentCoordBin, ["verdict"], {
1054
1271
  input: verdictReq,
@@ -1064,10 +1281,12 @@ async function runPreToolUseGuard(
1064
1281
  continue;
1065
1282
  }
1066
1283
  if (parsed.allow === false) {
1067
- let reason = parsed.reason ?? `Path ${target} is currently being edited by another agent.`;
1284
+ let reason =
1285
+ parsed.reason ?? `Path ${target.path} is currently being edited by another agent.`;
1068
1286
  if (targets.length > 1) {
1069
1287
  const siblings = targets
1070
- .filter((p) => p !== target)
1288
+ .filter((candidate) => candidate !== target)
1289
+ .map((candidate) => candidate.path)
1071
1290
  .slice(0, 3)
1072
1291
  .join(", ");
1073
1292
  if (siblings) {
@@ -1076,9 +1295,28 @@ async function runPreToolUseGuard(
1076
1295
  }
1077
1296
  const { emitDeny } = await import("./adapter/output.ts");
1078
1297
  emitDeny(adapter, reason);
1079
- return;
1298
+ return false;
1080
1299
  }
1300
+
1301
+ // The verdict mutates the live heartbeat immediately, but that file is a
1302
+ // projection and can be rebuilt at any later hook. Persist the acquisition
1303
+ // in the canonical stream too. This matters most for Codex apply_patch:
1304
+ // one tool call can claim several files, and tool.pre_use's clamped payload
1305
+ // is not a durable ownership representation for those targets.
1306
+ emit(coordRoot, {
1307
+ event_type: "claim.acquire",
1308
+ instance_id: instanceId,
1309
+ session_id: sessionId,
1310
+ adapter,
1311
+ source: "agent-hooks",
1312
+ data: {
1313
+ path: target.path,
1314
+ mode: "write",
1315
+ ...(target.finalization ? { finalization: target.finalization } : {}),
1316
+ },
1317
+ });
1081
1318
  }
1319
+ return true;
1082
1320
  }
1083
1321
 
1084
1322
  /** Canonicalize a path to monorepo-relative form. Absolute paths under
@@ -1142,14 +1380,18 @@ function healHeartbeatViaCli(
1142
1380
  instanceId: string,
1143
1381
  sessionId: string,
1144
1382
  adapter: string,
1383
+ forkedFrom?: string,
1145
1384
  ): void {
1146
1385
  const agentCoordBin = coordBinPath("agent-coord", coordRoot) ?? "";
1147
1386
  if (!existsSync(agentCoordBin)) return;
1148
1387
  // Pass the detected adapter so a pruned Cursor/Codex heartbeat is recreated
1149
1388
  // with the correct platform; without it, healHeartbeat defaults to
1150
- // claude_code and the dashboard mislabels the agent. See
1151
- // heartbeat-writer.healHeartbeat.
1152
- spawnSync(agentCoordBin, ["heal-heartbeat", instanceId, sessionId, `--adapter=${adapter}`], {
1389
+ // claude-code and the dashboard mislabels the agent. See
1390
+ // heartbeat-writer.healHeartbeat. --forked-from carries detected fork
1391
+ // lineage for an instance that never session.started (the CC fork flow).
1392
+ const args = ["heal-heartbeat", instanceId, sessionId, `--adapter=${adapter}`];
1393
+ if (forkedFrom) args.push(`--forked-from=${forkedFrom}`);
1394
+ spawnSync(agentCoordBin, args, {
1153
1395
  encoding: "utf8",
1154
1396
  timeout: 2000,
1155
1397
  env: childEnv(coordRoot),
@@ -1384,6 +1626,7 @@ async function emitUserPromptSubmitSystemMessage(
1384
1626
  instanceId: string,
1385
1627
  sessionId: string,
1386
1628
  adapter: Adapter,
1629
+ workspaceCwd?: string,
1387
1630
  recoveryBriefing = "",
1388
1631
  ): Promise<boolean> {
1389
1632
  const agentCoordBin = coordBinPath("agent-coord", coordRoot) ?? "";
@@ -1410,12 +1653,23 @@ async function emitUserPromptSubmitSystemMessage(
1410
1653
  // staleness nudges; Claude Code enforces those via its Stop transcript.
1411
1654
  // Codex gets a fresh status-footer reminder on every prompt. Its Stop hook
1412
1655
  // stays observe-only, so missing the footer never replaces the answer.
1656
+ // Claude Code + Cursor (the Stop-ENFORCING adapters) get a fresh per-turn
1657
+ // ritual reminder: satisfying the contract up front is far cheaper than
1658
+ // the bounce-and-retry the Stop hook otherwise forces.
1413
1659
  args.push("--session-name-nudge");
1414
1660
  if (adapter === "cursor" || adapter === "codex") args.push("--task-nudge");
1415
1661
  if (adapter === "codex") args.push("--status-footer-nudge");
1662
+ if (adapter === "claude-code" || adapter === "cursor")
1663
+ args.push("--turn-ritual-nudge", adapter);
1416
1664
  const result = spawnSync(agentCoordBin, args, { encoding: "utf8", timeout: 3000 });
1417
1665
  if (result.status === 0 && result.stdout) additionalContext = result.stdout.trim();
1418
1666
  }
1667
+ if (adapter === "codex") {
1668
+ const fileLinkContext = renderCodexWslFileLinkContext(coordRoot, workspaceCwd);
1669
+ if (fileLinkContext) {
1670
+ additionalContext = [additionalContext, fileLinkContext].filter(Boolean).join("\n\n");
1671
+ }
1672
+ }
1419
1673
  if (recoveryBriefing) {
1420
1674
  additionalContext = [additionalContext, recoveryBriefing].filter(Boolean).join("\n\n");
1421
1675
  }
@@ -1466,7 +1720,7 @@ function emitSubagentStartContext(
1466
1720
  }
1467
1721
 
1468
1722
  function adapterPlatform(adapter: Adapter): string {
1469
- if (adapter === "claude-code") return "claude_code";
1723
+ if (adapter === "claude-code") return "claude-code";
1470
1724
  return adapter;
1471
1725
  }
1472
1726
 
@@ -1527,6 +1781,21 @@ async function emitSessionStartSystemMessage(
1527
1781
  if (recoveryBriefing) {
1528
1782
  additionalContext = [additionalContext, recoveryBriefing].filter(Boolean).join("\n\n");
1529
1783
  }
1784
+ if (adapter === "codex" && isWslUncPath(emittedData.cwd)) {
1785
+ const fileLinkContext = renderCodexWslFileLinkContext(coordRoot, emittedData.cwd);
1786
+ if (fileLinkContext) {
1787
+ additionalContext = [additionalContext, fileLinkContext].filter(Boolean).join("\n\n");
1788
+ }
1789
+ const bridge = inspectCodexWslBridge(process.env, { expected: true });
1790
+ if (bridge && !bridge.ok) {
1791
+ additionalContext = [
1792
+ additionalContext,
1793
+ `Harnery hybrid warning: ${bridge.detail}. Run \`${resolveBinName(coordRoot)} doctor\` for the repair hint.`,
1794
+ ]
1795
+ .filter(Boolean)
1796
+ .join("\n\n");
1797
+ }
1798
+ }
1530
1799
  if (!additionalContext) return false;
1531
1800
 
1532
1801
  const { emitContext } = await import("./adapter/output.ts");