harnery 0.31.6 → 0.32.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 (218) hide show
  1. package/dist/commander.d.ts.map +1 -1
  2. package/dist/commander.js +2 -0
  3. package/dist/commands/agents.d.ts +0 -9
  4. package/dist/commands/agents.d.ts.map +1 -1
  5. package/dist/commands/agents.js +70 -95
  6. package/dist/commands/browse-session.d.ts +21 -0
  7. package/dist/commands/browse-session.d.ts.map +1 -0
  8. package/dist/commands/browse-session.js +157 -0
  9. package/dist/commands/browse.d.ts.map +1 -1
  10. package/dist/commands/browse.js +205 -37
  11. package/dist/commands/checkpoint.js +1 -1
  12. package/dist/commands/deinit.d.ts.map +1 -1
  13. package/dist/commands/deinit.js +4 -0
  14. package/dist/commands/docs.d.ts.map +1 -1
  15. package/dist/commands/docs.js +40 -0
  16. package/dist/commands/doctor.d.ts.map +1 -1
  17. package/dist/commands/doctor.js +100 -17
  18. package/dist/commands/init.d.ts +1 -2
  19. package/dist/commands/init.d.ts.map +1 -1
  20. package/dist/commands/init.js +16 -5
  21. package/dist/commands/tunnel.d.ts.map +1 -1
  22. package/dist/commands/tunnel.js +2 -3
  23. package/dist/core/agents/canonical-emit.d.ts +1 -2
  24. package/dist/core/agents/canonical-emit.d.ts.map +1 -1
  25. package/dist/core/agents/canonical-emit.js +1 -2
  26. package/dist/core/agents/cli.js +123 -88
  27. package/dist/core/agents/coord-client.d.ts +7 -3
  28. package/dist/core/agents/coord-client.d.ts.map +1 -1
  29. package/dist/core/agents/coord-client.js +5 -4
  30. package/dist/core/agents/finalization.d.ts +68 -0
  31. package/dist/core/agents/finalization.d.ts.map +1 -0
  32. package/dist/core/agents/finalization.js +443 -0
  33. package/dist/core/agents/git-hook.d.ts +51 -0
  34. package/dist/core/agents/git-hook.d.ts.map +1 -0
  35. package/dist/core/agents/git-hook.js +118 -0
  36. package/dist/core/agents/render/prompt-context.d.ts +6 -5
  37. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  38. package/dist/core/agents/render/prompt-context.js +26 -13
  39. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  40. package/dist/core/agents/render/session-context.js +15 -2
  41. package/dist/core/agents/rules/claim-conflict.js +3 -3
  42. package/dist/core/agents/rules/commit-conflict.d.ts +15 -6
  43. package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
  44. package/dist/core/agents/rules/commit-conflict.js +21 -5
  45. package/dist/core/agents/rules/stop-hook.d.ts +3 -0
  46. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  47. package/dist/core/agents/rules/stop-hook.js +59 -23
  48. package/dist/core/agents/session-events.d.ts +8 -16
  49. package/dist/core/agents/session-events.d.ts.map +1 -1
  50. package/dist/core/agents/session-events.js +12 -26
  51. package/dist/core/agents/state/heartbeat-projector.d.ts +3 -0
  52. package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
  53. package/dist/core/agents/state/heartbeat-projector.js +44 -1
  54. package/dist/core/agents/state/heartbeat-writer.d.ts +32 -5
  55. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  56. package/dist/core/agents/state/heartbeat-writer.js +54 -17
  57. package/dist/core/agents/state/names.d.ts +22 -1
  58. package/dist/core/agents/state/names.d.ts.map +1 -1
  59. package/dist/core/agents/state/names.js +40 -2
  60. package/dist/core/config.d.ts +30 -2
  61. package/dist/core/config.d.ts.map +1 -1
  62. package/dist/core/config.js +74 -11
  63. package/dist/core/governor/planning.d.ts.map +1 -1
  64. package/dist/core/governor/planning.js +12 -13
  65. package/dist/core/hooks/adapter/detect.d.ts +2 -4
  66. package/dist/core/hooks/adapter/detect.d.ts.map +1 -1
  67. package/dist/core/hooks/adapter/detect.js +4 -14
  68. package/dist/core/hooks/adapter/output.d.ts +1 -1
  69. package/dist/core/hooks/adapter/output.d.ts.map +1 -1
  70. package/dist/core/hooks/adapter/output.js +5 -4
  71. package/dist/core/hooks/adapter/wiring.d.ts +23 -0
  72. package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
  73. package/dist/core/hooks/adapter/wiring.js +32 -0
  74. package/dist/core/hooks/cli.d.ts +1 -2
  75. package/dist/core/hooks/cli.d.ts.map +1 -1
  76. package/dist/core/hooks/cli.js +169 -27
  77. package/dist/core/hooks/codex-wsl-bridge.d.ts +46 -0
  78. package/dist/core/hooks/codex-wsl-bridge.d.ts.map +1 -0
  79. package/dist/core/hooks/codex-wsl-bridge.js +137 -0
  80. package/dist/core/hooks/effects/index.d.ts +1 -1
  81. package/dist/core/hooks/effects/index.js +3 -3
  82. package/dist/core/hooks/events/schema.d.ts +27 -0
  83. package/dist/core/hooks/events/schema.d.ts.map +1 -1
  84. package/dist/core/hooks/resolve/owner.d.ts +7 -1
  85. package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
  86. package/dist/core/hooks/resolve/owner.js +15 -0
  87. package/dist/core/hooks/resolve/transcript.d.ts +44 -0
  88. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  89. package/dist/core/hooks/resolve/transcript.js +176 -1
  90. package/dist/core/hooks/session-name-presence.d.ts +30 -0
  91. package/dist/core/hooks/session-name-presence.d.ts.map +1 -0
  92. package/dist/core/hooks/session-name-presence.js +43 -0
  93. package/dist/core/hooks/unsafe-cross-shell.d.ts +13 -0
  94. package/dist/core/hooks/unsafe-cross-shell.d.ts.map +1 -0
  95. package/dist/core/hooks/unsafe-cross-shell.js +151 -0
  96. package/dist/core/work/state.d.ts +4 -5
  97. package/dist/core/work/state.d.ts.map +1 -1
  98. package/dist/core/work/state.js +5 -8
  99. package/dist/core/workflow/index.d.ts +1 -1
  100. package/dist/core/workflow/index.d.ts.map +1 -1
  101. package/dist/core/workflow/proof.d.ts +2 -2
  102. package/dist/core/workflow/proof.d.ts.map +1 -1
  103. package/dist/core/workflow/proof.js +2 -2
  104. package/dist/core/workflow/run-state.d.ts +2 -2
  105. package/dist/core/workflow/run-state.d.ts.map +1 -1
  106. package/dist/core/workflow/run-state.js +2 -2
  107. package/dist/core/workflow/types.d.ts +3 -3
  108. package/dist/core/workflow/types.d.ts.map +1 -1
  109. package/dist/core/workflow/workspaces/execution.d.ts +2 -2
  110. package/dist/core/workflow/workspaces/execution.d.ts.map +1 -1
  111. package/dist/core/workflow/workspaces/execution.js +5 -0
  112. package/dist/core/workflow/workspaces/index.d.ts +1 -1
  113. package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
  114. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  115. package/dist/core/workflow/workspaces/local-git.js +1 -2
  116. package/dist/core/workflow/workspaces/paths.d.ts +2 -2
  117. package/dist/core/workflow/workspaces/types.d.ts +0 -6
  118. package/dist/core/workflow/workspaces/types.d.ts.map +1 -1
  119. package/dist/core/workflow/workspaces/validate.d.ts +0 -2
  120. package/dist/core/workflow/workspaces/validate.d.ts.map +1 -1
  121. package/dist/core/workflow/workspaces/validate.js +0 -2
  122. package/dist/lib/browser/client.d.ts +118 -1
  123. package/dist/lib/browser/client.d.ts.map +1 -1
  124. package/dist/lib/browser/client.js +435 -6
  125. package/dist/lib/browser/geometry.d.ts.map +1 -1
  126. package/dist/lib/browser/geometry.js +180 -37
  127. package/dist/lib/browser/index.d.ts +5 -1
  128. package/dist/lib/browser/index.d.ts.map +1 -1
  129. package/dist/lib/browser/index.js +5 -1
  130. package/dist/lib/browser/netscape-cookies.d.ts +6 -0
  131. package/dist/lib/browser/netscape-cookies.d.ts.map +1 -0
  132. package/dist/lib/browser/netscape-cookies.js +31 -0
  133. package/dist/lib/browser/proxy.d.ts +24 -0
  134. package/dist/lib/browser/proxy.d.ts.map +1 -0
  135. package/dist/lib/browser/proxy.js +84 -0
  136. package/dist/lib/browser/runts.d.ts +6 -0
  137. package/dist/lib/browser/runts.d.ts.map +1 -1
  138. package/dist/lib/browser/runts.js +21 -4
  139. package/dist/lib/browser/session-control.d.ts +112 -0
  140. package/dist/lib/browser/session-control.d.ts.map +1 -0
  141. package/dist/lib/browser/session-control.js +670 -0
  142. package/dist/lib/docs-links.d.ts +108 -0
  143. package/dist/lib/docs-links.d.ts.map +1 -0
  144. package/dist/lib/docs-links.js +555 -0
  145. package/dist/lib/exec.d.ts +1 -1
  146. package/dist/lib/exec.js +1 -1
  147. package/dist/lib/identities/assume.d.ts +4 -2
  148. package/dist/lib/identities/assume.d.ts.map +1 -1
  149. package/dist/lib/identities/assume.js +15 -2
  150. package/dist/lib/instructions/git-hooks.d.ts +75 -0
  151. package/dist/lib/instructions/git-hooks.d.ts.map +1 -0
  152. package/dist/lib/instructions/git-hooks.js +238 -0
  153. package/dist/lib/instructions/splice.d.ts +10 -4
  154. package/dist/lib/instructions/splice.d.ts.map +1 -1
  155. package/dist/lib/instructions/splice.js +19 -11
  156. package/package.json +1 -1
  157. package/schemas/config.schema.json +32 -0
  158. package/src/commander.ts +2 -0
  159. package/src/commands/agents.ts +96 -115
  160. package/src/commands/browse-session.ts +245 -0
  161. package/src/commands/browse.ts +287 -44
  162. package/src/commands/checkpoint.ts +1 -1
  163. package/src/commands/deinit.ts +7 -1
  164. package/src/commands/docs.ts +56 -0
  165. package/src/commands/doctor.ts +100 -18
  166. package/src/commands/init.ts +17 -9
  167. package/src/commands/tunnel.ts +2 -5
  168. package/src/core/agents/canonical-emit.ts +1 -2
  169. package/src/core/agents/cli.ts +137 -88
  170. package/src/core/agents/coord-client.ts +10 -5
  171. package/src/core/agents/finalization.ts +595 -0
  172. package/src/core/agents/git-hook.ts +126 -0
  173. package/src/core/agents/render/prompt-context.ts +28 -13
  174. package/src/core/agents/render/session-context.ts +15 -2
  175. package/src/core/agents/rules/claim-conflict.ts +3 -3
  176. package/src/core/agents/rules/commit-conflict.ts +35 -8
  177. package/src/core/agents/rules/stop-hook.ts +71 -23
  178. package/src/core/agents/session-events.ts +14 -41
  179. package/src/core/agents/state/heartbeat-projector.ts +45 -1
  180. package/src/core/agents/state/heartbeat-writer.ts +78 -16
  181. package/src/core/agents/state/names.ts +54 -2
  182. package/src/core/config.ts +92 -12
  183. package/src/core/governor/planning.ts +17 -12
  184. package/src/core/hooks/adapter/detect.ts +4 -13
  185. package/src/core/hooks/adapter/output.ts +9 -4
  186. package/src/core/hooks/adapter/wiring.ts +43 -0
  187. package/src/core/hooks/cli.ts +201 -23
  188. package/src/core/hooks/codex-wsl-bridge.ts +188 -0
  189. package/src/core/hooks/effects/index.ts +5 -5
  190. package/src/core/hooks/events/schema.ts +27 -0
  191. package/src/core/hooks/resolve/owner.ts +22 -1
  192. package/src/core/hooks/resolve/transcript.ts +164 -1
  193. package/src/core/hooks/session-name-presence.ts +52 -0
  194. package/src/core/hooks/unsafe-cross-shell.ts +160 -0
  195. package/src/core/work/state.ts +8 -12
  196. package/src/core/workflow/engine.ts +2 -2
  197. package/src/core/workflow/index.ts +0 -1
  198. package/src/core/workflow/proof.ts +4 -4
  199. package/src/core/workflow/run-state.ts +5 -5
  200. package/src/core/workflow/types.ts +3 -3
  201. package/src/core/workflow/workspaces/execution.ts +11 -4
  202. package/src/core/workflow/workspaces/index.ts +0 -3
  203. package/src/core/workflow/workspaces/local-git.ts +1 -2
  204. package/src/core/workflow/workspaces/paths.ts +2 -2
  205. package/src/core/workflow/workspaces/types.ts +0 -9
  206. package/src/core/workflow/workspaces/validate.ts +1 -5
  207. package/src/lib/browser/client.ts +528 -6
  208. package/src/lib/browser/geometry.ts +197 -37
  209. package/src/lib/browser/index.ts +39 -0
  210. package/src/lib/browser/netscape-cookies.ts +39 -0
  211. package/src/lib/browser/proxy.ts +105 -0
  212. package/src/lib/browser/runts.ts +27 -3
  213. package/src/lib/browser/session-control.ts +892 -0
  214. package/src/lib/docs-links.ts +674 -0
  215. package/src/lib/exec.ts +1 -1
  216. package/src/lib/identities/assume.ts +22 -1
  217. package/src/lib/instructions/git-hooks.ts +259 -0
  218. 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.
@@ -24,14 +23,17 @@ import { coordEnv } from "../../lib/env.js";
24
23
  import { replayCodexJsonl } from "../agents/codex-replay.js";
25
24
  import { coordBinPath } from "../agents/coord-bin.js";
26
25
  import { consumeSince, writeCursor } from "../agents/events/consume.js";
26
+ import { classifyWriteClaimFinalization, formatWriteClaimFinalizationDenial, } from "../agents/finalization.js";
27
27
  import { evaluateStopHook } from "../agents/rules/stop-hook.js";
28
28
  import { projectHeartbeats } from "../agents/state/heartbeat-projector.js";
29
29
  import { writePidmapRow } from "../agents/state/pidmap.js";
30
30
  import { shellMutationPaths } from "../agents/state/shell-mutation.js";
31
+ import { agentsRequireGitFinalization, resolveBinName } from "../config.js";
31
32
  import { checkpointContext, completeContextRecovery, extractContextSample, markContextCompactionCompleted, prepareContextRecovery, readContextState, recordContextSample, } from "../context/index.js";
32
33
  import { ensureRelayDaemon, fetchPresence, publishPresence } from "../presence/index.js";
33
34
  import { detectAdapter } from "./adapter/detect.js";
34
35
  import { extractBashCommand, extractToolDescription, normalizeEventName, parsePayload, } from "./adapter/parse.js";
36
+ import { codexWslFileLinkTelemetry, inspectCodexWslBridge, isWslUncPath, renderCodexWslFileLinkContext, } from "./codex-wsl-bridge.js";
35
37
  import { captureImages, detectPresence, imageJanitor, journalArchive, journalJanitor, journalRecoveryCue, playSound, resetSoundCounters, runSessionSyncExtension, runTurnSummary, soundForEvent, } from "./effects/index.js";
36
38
  import { emit } from "./events/emit.js";
37
39
  import { canonicalize } from "./guard-path.js";
@@ -39,16 +41,18 @@ import { adapterPidFromEnv, parsePsChainLine, selectAnchorPid } from "./resolve/
39
41
  import { findCoordRoot } from "./resolve/coord-root.js";
40
42
  import { extractIntentComment, resolveIntent } from "./resolve/intent.js";
41
43
  import { resolveOwner } from "./resolve/owner.js";
42
- import { scanStatusBoxPresent, scanTranscriptModel } from "./resolve/transcript.js";
44
+ import { detectForkParent, scanAssistantTextIncludes, scanStatusBoxPresent, scanTranscriptModel, } from "./resolve/transcript.js";
45
+ import { sessionNamePresence } from "./session-name-presence.js";
46
+ import { unsafeCrossShellReason } from "./unsafe-cross-shell.js";
43
47
  function parseArgv(argv) {
44
48
  const out = { eventName: null, extra: [] };
45
49
  for (let i = 0; i < argv.length; i++) {
46
50
  const arg = argv[i];
47
- if (arg === "--adapter" || arg === "--harness") {
51
+ if (arg === "--adapter") {
48
52
  i++; // detectAdapter will re-parse; just consume the value here.
49
53
  continue;
50
54
  }
51
- if (arg.startsWith("--adapter=") || arg.startsWith("--harness="))
55
+ if (arg.startsWith("--adapter="))
52
56
  continue;
53
57
  if (!out.eventName && !arg.startsWith("--")) {
54
58
  out.eventName = arg;
@@ -118,12 +122,15 @@ function logError(coordRoot, err, context) {
118
122
  * Lives at agent-hooks side (not agent-coord) to keep emitter/consumer
119
123
  * separation: we spawn rather than import.
120
124
  */
121
- function assignNameViaAgentCoord(coordRoot, instanceId, kind) {
125
+ function assignNameViaAgentCoord(coordRoot, instanceId, kind, forkedFrom) {
122
126
  const binary = coordBinPath("agent-coord", coordRoot) ?? "";
123
127
  if (!existsSync(binary))
124
128
  return null;
125
129
  try {
126
- const result = spawnSync(binary, ["assign-name", instanceId, kind], {
130
+ const args = ["assign-name", instanceId, kind];
131
+ if (forkedFrom)
132
+ args.push("--forked-from", forkedFrom);
133
+ const result = spawnSync(binary, args, {
127
134
  encoding: "utf8",
128
135
  timeout: 2000,
129
136
  env: childEnv(coordRoot),
@@ -178,13 +185,21 @@ function buildEventData(eventType, ctx) {
178
185
  switch (eventType) {
179
186
  case "session.start": {
180
187
  const adapterPlatform = ctx.adapter === "claude-code"
181
- ? "claude_code"
188
+ ? "claude-code"
182
189
  : ctx.adapter === "cursor"
183
190
  ? "cursor"
184
191
  : "codex";
192
+ // Recorded fork lineage is NOT detected here. On claude-code a fork
193
+ // never fires its own session.start — SessionStart fires under the
194
+ // PARENT's session id (source=resume) before the fork id is minted
195
+ // (verified 2026-08-05) — so detection at this point can only mislabel
196
+ // the resumed parent. The fork's new instance is caught by the
197
+ // tool.pre_use heal path instead. The forkedFrom plumbing below stays
198
+ // for adapters that DO report a parent at session start.
199
+ const forkedFrom = undefined;
185
200
  // Assign (or recover) name + kind via agent-coord. Idempotent: resume
186
201
  // returns the original name; new owner consumes a counter slot.
187
- const assigned = assignNameViaAgentCoord(ctx.coordRoot, ctx.instanceId, "session");
202
+ const assigned = assignNameViaAgentCoord(ctx.coordRoot, ctx.instanceId, "session", forkedFrom);
188
203
  // Write the adapter pid-map row so `harn agents whoami` ppid-walks find
189
204
  // this owner. Prefer the payload pid (the actual claude binary), then the
190
205
  // anchor walk (the `node` ancestor for Cursor, which has no payload pid),
@@ -208,6 +223,10 @@ function buildEventData(eventType, ctx) {
208
223
  name: assigned?.name,
209
224
  kind: "session",
210
225
  agent_id: ctx.instanceId,
226
+ // Recorded fork lineage rides the canonical event too, so full replay
227
+ // and derived readers converge on the same parent linkage that
228
+ // .name-history carries (the ADR 0017 dual-write pattern).
229
+ ...(forkedFrom ? { forked_from: forkedFrom } : {}),
211
230
  // Workflow-child linkage: `workflow run` children carry the run id in
212
231
  // env (spawn adapters set it via buildChildEnv), so their sessions and
213
232
  // heartbeats join back to the run transcript + /workflows web view.
@@ -235,6 +254,10 @@ function buildEventData(eventType, ctx) {
235
254
  return { prompt_text: value, ...(truncated ? { truncated: true } : {}) };
236
255
  }
237
256
  case "turn.stop": {
257
+ const lastAssistantMessage = p?.raw.last_assistant_message ?? "";
258
+ const fileLinkTelemetry = ctx.adapter === "codex"
259
+ ? codexWslFileLinkTelemetry(ctx.coordRoot, p?.cwd, lastAssistantMessage)
260
+ : null;
238
261
  return {
239
262
  // Backfill the model for adapters that omit it at session.start
240
263
  // (Claude Code). The transcript is populated with assistant turns by
@@ -252,8 +275,24 @@ function buildEventData(eventType, ctx) {
252
275
  // text-only stop (box in last_assistant_message, no transcript), which
253
276
  // the verdict now sees because agent-hook emits this turn.stop itself
254
277
  // (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-"),
278
+ status_box_present: scanStatusBoxPresent(p?.transcript_path) || lastAssistantMessage.includes("┌─ agent-"),
279
+ // Shadow measurement for the rule-2/3 detector: the loose scan above
280
+ // matches the box prefix ANYWHERE in the transcript tail, including
281
+ // the tool_result row the status command itself writes, so it can
282
+ // report a paste that never happened. The strict variant counts
283
+ // assistant text only (same window, same scanner as the session-name
284
+ // detection). Telemetry-only: the Stop verdict still reads
285
+ // status_box_present. Once the divergence rate between the two fields
286
+ // is known, the gate either flips to strict or the rule gets
287
+ // reconsidered; either way this field decides it with data.
288
+ status_box_present_strict: scanAssistantTextIncludes(p?.transcript_path, "┌─ agent-") ||
289
+ lastAssistantMessage.includes("┌─ agent-"),
290
+ // Whether the session's suggested name is satisfied as of this turn,
291
+ // and which name that refers to. Reported on every stop while a name
292
+ // exists (the stop-hook.session_name verdict reads it per turn); the
293
+ // transcript scan itself stops once the name has been sighted.
294
+ ...sessionNamePresence(ctx.coordRoot, ctx.instanceId, lastAssistantMessage, (name) => scanAssistantTextIncludes(p?.transcript_path, name)),
295
+ ...(fileLinkTelemetry ?? {}),
257
296
  stop_hook_active: p?.stop_hook_active,
258
297
  };
259
298
  }
@@ -694,7 +733,7 @@ async function main() {
694
733
  }
695
734
  }
696
735
  try {
697
- const injected = await emitUserPromptSubmitSystemMessage(coordRoot, owner.instance_id, sessionId, adapter, recovery?.briefing ?? "");
736
+ const injected = await emitUserPromptSubmitSystemMessage(coordRoot, owner.instance_id, sessionId, adapter, payload?.cwd, recovery?.briefing ?? "");
698
737
  if (injected && recovery) {
699
738
  completeRecoveryInjection(coordRoot, owner.instance_id, sessionId, adapter, recovery, "UserPromptSubmit");
700
739
  }
@@ -778,7 +817,7 @@ async function main() {
778
817
  // re-prompts only via a `followup_message` it auto-submits. emitStopBlock
779
818
  // writes the right shape and returns the exit code to use.
780
819
  const { emitStopBlock } = await import("./adapter/output.js");
781
- return emitStopBlock(adapter, verdict);
820
+ return emitStopBlock(adapter, verdict, coordRoot);
782
821
  }
783
822
  }
784
823
  // Phase 7: PreToolUse: heartbeat + pid-map self-heal on every tool call.
@@ -792,7 +831,19 @@ async function main() {
792
831
  // call was restored here afterward.
793
832
  if (norm.event_type === "tool.pre_use") {
794
833
  try {
795
- healHeartbeatViaCli(coordRoot, owner.instance_id, sessionId, adapter);
834
+ // Recorded fork lineage, heal-path flavor. A forked CC conversation
835
+ // never fires its own session.start (SessionStart fires under the
836
+ // PARENT's session id with source=resume, before the fork id is
837
+ // minted; verified 2026-08-05), so the fork's new instance first
838
+ // materializes right here. Gate detection on "no heartbeat yet" so the
839
+ // transcript scan runs once per instance lifetime, not per tool call.
840
+ let forkedFrom;
841
+ if (adapter === "claude-code" &&
842
+ payload?.transcript_path &&
843
+ !existsSync(join(coordRoot, ".harnery", "active", `${owner.instance_id}.json`))) {
844
+ forkedFrom = detectForkParent(payload.transcript_path, owner.instance_id);
845
+ }
846
+ healHeartbeatViaCli(coordRoot, owner.instance_id, sessionId, adapter, forkedFrom);
796
847
  refreshPidmap(coordRoot, owner.instance_id, adapter, payload?.pid);
797
848
  }
798
849
  catch (err) {
@@ -813,6 +864,28 @@ async function main() {
813
864
  catch (err) {
814
865
  logError(coordRoot, err, { phase: "pre-tool-use-image-capture" });
815
866
  }
867
+ // Windows-native Codex + WSL UNC only: block the one cross-shell shape
868
+ // proven to corrupt argument boundaries. Normal WSL argv calls, literal
869
+ // bash -s scripts, native Linux/macOS sessions, and every other adapter
870
+ // pass through. The host instructions name its concrete safe bridge.
871
+ const unsafeShellReason = unsafeCrossShellReason({
872
+ adapter,
873
+ cwd: payload?.cwd,
874
+ toolName: payload?.tool_name,
875
+ toolInput: payload?.tool_input,
876
+ });
877
+ if (unsafeShellReason) {
878
+ emit(coordRoot, {
879
+ event_type: "decision.block",
880
+ instance_id: owner.instance_id,
881
+ session_id: sessionId,
882
+ adapter,
883
+ data: { rule: "unsafe_cross_shell", reason: unsafeShellReason },
884
+ });
885
+ const { emitDeny } = await import("./adapter/output.js");
886
+ emitDeny(adapter, unsafeShellReason);
887
+ return 0;
888
+ }
816
889
  // G-guard for ALL adapters. Claude Code previously ran this via a
817
890
  // pre-tool-use bash adapter (which called `agent-coord verdict --rule=claim`);
818
891
  // that adapter is now deleted, so agent-hook owns the deny for every adapter.
@@ -903,14 +976,40 @@ async function main() {
903
976
  }
904
977
  async function runPreToolUseGuard(coordRoot, instanceId, sessionId, data, adapter) {
905
978
  const toolName = data.tool_name ?? "";
906
- const targets = collectGuardTargets(toolName, data)
907
- .map((p) => canonicalize(coordRoot, p))
908
- .filter((p) => p !== null);
979
+ const rawTargets = collectGuardTargets(toolName, data);
980
+ if (rawTargets.length === 0)
981
+ return;
982
+ let targets;
983
+ if (agentsRequireGitFinalization(coordRoot)) {
984
+ const decisions = rawTargets.map((path) => classifyWriteClaimFinalization(coordRoot, path));
985
+ const denied = decisions.find((decision) => !decision.allow);
986
+ if (denied && !denied.allow) {
987
+ const { emitDeny } = await import("./adapter/output.js");
988
+ emitDeny(adapter, formatWriteClaimFinalizationDenial(denied, resolveBinName(coordRoot)));
989
+ return;
990
+ }
991
+ const allowed = decisions.filter((decision) => decision.allow);
992
+ targets = allowed.map((decision) => ({
993
+ path: decision.path,
994
+ finalization: decision.descriptor,
995
+ }));
996
+ }
997
+ else {
998
+ targets = rawTargets
999
+ .map((path) => canonicalize(coordRoot, path))
1000
+ .filter((path) => path !== null)
1001
+ .map((path) => ({ path }));
1002
+ }
909
1003
  if (targets.length === 0)
910
1004
  return;
911
1005
  const agentCoordBin = coordBinPath("agent-coord", coordRoot) ?? "";
912
- if (!existsSync(agentCoordBin))
1006
+ if (!existsSync(agentCoordBin)) {
1007
+ if (agentsRequireGitFinalization(coordRoot)) {
1008
+ const { emitDeny } = await import("./adapter/output.js");
1009
+ emitDeny(adapter, "Harnery denied the write before mutation because the claim coordinator is unavailable; repair the project's Harnery installation and retry.");
1010
+ }
913
1011
  return;
1012
+ }
914
1013
  // For apply_patch (multi-file), collect siblings so the deny reason names
915
1014
  // them. For single-file tools the array has one entry.
916
1015
  for (const target of targets) {
@@ -918,7 +1017,7 @@ async function runPreToolUseGuard(coordRoot, instanceId, sessionId, data, adapte
918
1017
  rule: "claim",
919
1018
  instance_id: instanceId,
920
1019
  session_id: sessionId,
921
- path: target,
1020
+ path: target.path,
922
1021
  });
923
1022
  const result = spawnSync(agentCoordBin, ["verdict"], {
924
1023
  input: verdictReq,
@@ -936,10 +1035,11 @@ async function runPreToolUseGuard(coordRoot, instanceId, sessionId, data, adapte
936
1035
  continue;
937
1036
  }
938
1037
  if (parsed.allow === false) {
939
- let reason = parsed.reason ?? `Path ${target} is currently being edited by another agent.`;
1038
+ let reason = parsed.reason ?? `Path ${target.path} is currently being edited by another agent.`;
940
1039
  if (targets.length > 1) {
941
1040
  const siblings = targets
942
- .filter((p) => p !== target)
1041
+ .filter((candidate) => candidate !== target)
1042
+ .map((candidate) => candidate.path)
943
1043
  .slice(0, 3)
944
1044
  .join(", ");
945
1045
  if (siblings) {
@@ -950,6 +1050,23 @@ async function runPreToolUseGuard(coordRoot, instanceId, sessionId, data, adapte
950
1050
  emitDeny(adapter, reason);
951
1051
  return;
952
1052
  }
1053
+ // The verdict mutates the live heartbeat immediately, but that file is a
1054
+ // projection and can be rebuilt at any later hook. Persist the acquisition
1055
+ // in the canonical stream too. This matters most for Codex apply_patch:
1056
+ // one tool call can claim several files, and tool.pre_use's clamped payload
1057
+ // is not a durable ownership representation for those targets.
1058
+ emit(coordRoot, {
1059
+ event_type: "claim.acquire",
1060
+ instance_id: instanceId,
1061
+ session_id: sessionId,
1062
+ adapter,
1063
+ source: "agent-hooks",
1064
+ data: {
1065
+ path: target.path,
1066
+ mode: "write",
1067
+ ...(target.finalization ? { finalization: target.finalization } : {}),
1068
+ },
1069
+ });
953
1070
  }
954
1071
  }
955
1072
  /** Canonicalize a path to monorepo-relative form. Absolute paths under
@@ -1008,15 +1125,19 @@ function parseApplyPatchPaths(data) {
1008
1125
  }
1009
1126
  return out;
1010
1127
  }
1011
- function healHeartbeatViaCli(coordRoot, instanceId, sessionId, adapter) {
1128
+ function healHeartbeatViaCli(coordRoot, instanceId, sessionId, adapter, forkedFrom) {
1012
1129
  const agentCoordBin = coordBinPath("agent-coord", coordRoot) ?? "";
1013
1130
  if (!existsSync(agentCoordBin))
1014
1131
  return;
1015
1132
  // Pass the detected adapter so a pruned Cursor/Codex heartbeat is recreated
1016
1133
  // 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}`], {
1134
+ // claude-code and the dashboard mislabels the agent. See
1135
+ // heartbeat-writer.healHeartbeat. --forked-from carries detected fork
1136
+ // lineage for an instance that never session.started (the CC fork flow).
1137
+ const args = ["heal-heartbeat", instanceId, sessionId, `--adapter=${adapter}`];
1138
+ if (forkedFrom)
1139
+ args.push(`--forked-from=${forkedFrom}`);
1140
+ spawnSync(agentCoordBin, args, {
1020
1141
  encoding: "utf8",
1021
1142
  timeout: 2000,
1022
1143
  env: childEnv(coordRoot),
@@ -1238,7 +1359,7 @@ function cleanupSessionEnd(coordRoot, instanceId, reason) {
1238
1359
  spawnSync(agentCoordBin, ["log", `SESSION_END reason=${reason}`, "--instance", instanceId], { encoding: "utf8", timeout: 2000 });
1239
1360
  }
1240
1361
  }
1241
- async function emitUserPromptSubmitSystemMessage(coordRoot, instanceId, sessionId, adapter, recoveryBriefing = "") {
1362
+ async function emitUserPromptSubmitSystemMessage(coordRoot, instanceId, sessionId, adapter, workspaceCwd, recoveryBriefing = "") {
1242
1363
  const agentCoordBin = coordBinPath("agent-coord", coordRoot) ?? "";
1243
1364
  let additionalContext = "";
1244
1365
  if (existsSync(agentCoordBin)) {
@@ -1272,6 +1393,12 @@ async function emitUserPromptSubmitSystemMessage(coordRoot, instanceId, sessionI
1272
1393
  if (result.status === 0 && result.stdout)
1273
1394
  additionalContext = result.stdout.trim();
1274
1395
  }
1396
+ if (adapter === "codex") {
1397
+ const fileLinkContext = renderCodexWslFileLinkContext(coordRoot, workspaceCwd);
1398
+ if (fileLinkContext) {
1399
+ additionalContext = [additionalContext, fileLinkContext].filter(Boolean).join("\n\n");
1400
+ }
1401
+ }
1275
1402
  if (recoveryBriefing) {
1276
1403
  additionalContext = [additionalContext, recoveryBriefing].filter(Boolean).join("\n\n");
1277
1404
  }
@@ -1310,7 +1437,7 @@ function emitSubagentStartContext(coordRoot, instanceId, sessionId, data, adapte
1310
1437
  }
1311
1438
  function adapterPlatform(adapter) {
1312
1439
  if (adapter === "claude-code")
1313
- return "claude_code";
1440
+ return "claude-code";
1314
1441
  return adapter;
1315
1442
  }
1316
1443
  async function emitSessionStartSystemMessage(coordRoot, instanceId, sessionId, emittedData, adapter, recoveryBriefing = "") {
@@ -1359,6 +1486,21 @@ async function emitSessionStartSystemMessage(coordRoot, instanceId, sessionId, e
1359
1486
  if (recoveryBriefing) {
1360
1487
  additionalContext = [additionalContext, recoveryBriefing].filter(Boolean).join("\n\n");
1361
1488
  }
1489
+ if (adapter === "codex" && isWslUncPath(emittedData.cwd)) {
1490
+ const fileLinkContext = renderCodexWslFileLinkContext(coordRoot, emittedData.cwd);
1491
+ if (fileLinkContext) {
1492
+ additionalContext = [additionalContext, fileLinkContext].filter(Boolean).join("\n\n");
1493
+ }
1494
+ const bridge = inspectCodexWslBridge(process.env, { expected: true });
1495
+ if (bridge && !bridge.ok) {
1496
+ additionalContext = [
1497
+ additionalContext,
1498
+ `Harnery hybrid warning: ${bridge.detail}. Run \`${resolveBinName(coordRoot)} doctor\` for the repair hint.`,
1499
+ ]
1500
+ .filter(Boolean)
1501
+ .join("\n\n");
1502
+ }
1503
+ }
1362
1504
  if (!additionalContext)
1363
1505
  return false;
1364
1506
  const { emitContext } = await import("./adapter/output.js");
@@ -0,0 +1,46 @@
1
+ export interface CodexWslBridgeStatus {
2
+ ok: boolean;
3
+ detail: string;
4
+ threadIdPresent: boolean;
5
+ wslenvForwardsThreadId: boolean;
6
+ }
7
+ export interface CodexWslBridgeOptions {
8
+ /** The caller already knows this is a Codex adapter process. */
9
+ expected?: boolean;
10
+ }
11
+ export interface CodexWslWorkspaceLinkMapping {
12
+ /** Absolute path seen by Linux-side tools. */
13
+ linuxRoot: string;
14
+ /** Forward-slash UNC path that Codex Desktop can use as a Markdown destination. */
15
+ hostRoot: string;
16
+ distro: string;
17
+ }
18
+ export interface CodexWslFileLinkTelemetry {
19
+ /** Markdown destinations that still use the Linux root in a Windows-hosted WSL task. */
20
+ wsl_linux_file_link_count: number;
21
+ /** Bounded examples for local diagnosis; omitted when the count is zero. */
22
+ wsl_linux_file_link_examples?: string[];
23
+ }
24
+ /** Whether an adapter-reported workspace is a Linux path exposed through WSL UNC. */
25
+ export declare function isWslUncPath(value: unknown): boolean;
26
+ /**
27
+ * Derive the Linux-to-host path pair from the actual adapter workspace. The
28
+ * roots must overlap so an unrelated WSL UNC path cannot authorize a mapping
29
+ * for some other checkout.
30
+ */
31
+ export declare function codexWslWorkspaceLinkMapping(coordRootRaw: string, workspaceCwd: unknown): CodexWslWorkspaceLinkMapping | null;
32
+ /** Fresh adapter context for the model. Shell paths stay Linux-native; only
33
+ * Markdown destinations use the host-visible root. */
34
+ export declare function renderCodexWslFileLinkContext(coordRoot: string, workspaceCwd: unknown): string;
35
+ /**
36
+ * Count Markdown links that point at the Linux workspace root even though the
37
+ * adapter workspace is Windows-visible UNC. This is telemetry only: the Stop
38
+ * hook must not replace or retry an otherwise complete Codex reply.
39
+ */
40
+ export declare function codexWslFileLinkTelemetry(coordRoot: string, workspaceCwd: unknown, assistantMessage: string): CodexWslFileLinkTelemetry | null;
41
+ /**
42
+ * Inspect the environment contract used when Windows-native Codex launches a
43
+ * Harnery hook or shell inside WSL. Returns null outside that hybrid mode.
44
+ */
45
+ export declare function inspectCodexWslBridge(env?: NodeJS.ProcessEnv, options?: CodexWslBridgeOptions): CodexWslBridgeStatus | null;
46
+ //# sourceMappingURL=codex-wsl-bridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex-wsl-bridge.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/codex-wsl-bridge.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,OAAO,CAAC;IACzB,sBAAsB,EAAE,OAAO,CAAC;CACjC;AAED,MAAM,WAAW,qBAAqB;IACpC,gEAAgE;IAChE,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,4BAA4B;IAC3C,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,mFAAmF;IACnF,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,yBAAyB;IACxC,wFAAwF;IACxF,yBAAyB,EAAE,MAAM,CAAC;IAClC,4EAA4E;IAC5E,4BAA4B,CAAC,EAAE,MAAM,EAAE,CAAC;CACzC;AAED,qFAAqF;AACrF,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAMpD;AA2BD;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC1C,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,OAAO,GACpB,4BAA4B,GAAG,IAAI,CAarC;AAED;sDACsD;AACtD,wBAAgB,6BAA6B,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,GAAG,MAAM,CAS9F;AAmCD;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,OAAO,EACrB,gBAAgB,EAAE,MAAM,GACvB,yBAAyB,GAAG,IAAI,CAYlC;AAOD;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,OAAO,GAAE,qBAA0B,GAClC,oBAAoB,GAAG,IAAI,CAsB7B"}
@@ -0,0 +1,137 @@
1
+ /** Whether an adapter-reported workspace is a Linux path exposed through WSL UNC. */
2
+ export function isWslUncPath(value) {
3
+ if (typeof value !== "string")
4
+ return false;
5
+ return (/^\\\\(?:wsl\.localhost|wsl\$)\\/i.test(value) ||
6
+ /^\\\\\?\\unc\\(?:wsl\.localhost|wsl\$)\\/i.test(value));
7
+ }
8
+ function parseWslUncPath(value) {
9
+ if (typeof value !== "string")
10
+ return null;
11
+ const ordinary = value.replace(/^\\\\\?\\UNC\\/i, "\\\\");
12
+ const match = ordinary.match(/^\\\\(wsl\.localhost|wsl\$)\\([^\\]+)(?:\\(.*))?$/i);
13
+ if (!match)
14
+ return null;
15
+ const host = match[1];
16
+ const distro = match[2];
17
+ const tail = (match[3] ?? "").split("\\").filter(Boolean).join("/");
18
+ return { host, distro, linuxPath: tail ? `/${tail}` : "/" };
19
+ }
20
+ function trimTrailingSlash(path) {
21
+ return path === "/" ? path : path.replace(/\/+$/, "");
22
+ }
23
+ function isWithinOrEqual(path, root) {
24
+ return path === root || path.startsWith(`${root}/`);
25
+ }
26
+ /**
27
+ * Derive the Linux-to-host path pair from the actual adapter workspace. The
28
+ * roots must overlap so an unrelated WSL UNC path cannot authorize a mapping
29
+ * for some other checkout.
30
+ */
31
+ export function codexWslWorkspaceLinkMapping(coordRootRaw, workspaceCwd) {
32
+ const parsed = parseWslUncPath(workspaceCwd);
33
+ const linuxRoot = trimTrailingSlash(coordRootRaw.replace(/\\/g, "/"));
34
+ if (!parsed || !linuxRoot.startsWith("/"))
35
+ return null;
36
+ const linuxCwd = trimTrailingSlash(parsed.linuxPath);
37
+ if (!isWithinOrEqual(linuxCwd, linuxRoot) && !isWithinOrEqual(linuxRoot, linuxCwd)) {
38
+ return null;
39
+ }
40
+ return {
41
+ linuxRoot,
42
+ hostRoot: `//${parsed.host}/${parsed.distro}${linuxRoot}`,
43
+ distro: parsed.distro,
44
+ };
45
+ }
46
+ /** Fresh adapter context for the model. Shell paths stay Linux-native; only
47
+ * Markdown destinations use the host-visible root. */
48
+ export function renderCodexWslFileLinkContext(coordRoot, workspaceCwd) {
49
+ const mapping = codexWslWorkspaceLinkMapping(coordRoot, workspaceCwd);
50
+ if (!mapping)
51
+ return "";
52
+ return ("Codex WSL file links: this task is hosted by Windows while tools run inside WSL. " +
53
+ `For clickable local-file Markdown links, replace the Linux workspace root \`${mapping.linuxRoot}\` with the host-visible root \`${mapping.hostRoot}\`. ` +
54
+ `Example: \`[label](<${mapping.hostRoot}/path/to/file>)\`. ` +
55
+ "Keep Linux paths unchanged in shell commands and tool inputs.");
56
+ }
57
+ function withoutMarkdownCode(text) {
58
+ const output = [];
59
+ let fence = null;
60
+ for (const line of text.split(/\r?\n/)) {
61
+ const candidate = line.match(/^\s{0,3}(`{3,}|~{3,})/);
62
+ if (fence) {
63
+ if (candidate && candidate[1][0] === fence.marker && candidate[1].length >= fence.length) {
64
+ fence = null;
65
+ }
66
+ output.push("");
67
+ continue;
68
+ }
69
+ if (candidate) {
70
+ fence = { marker: candidate[1][0], length: candidate[1].length };
71
+ output.push("");
72
+ continue;
73
+ }
74
+ output.push(line.replace(/(`+)([^`\n]*?)\1/g, ""));
75
+ }
76
+ return output.join("\n");
77
+ }
78
+ function markdownDestinations(text) {
79
+ const out = [];
80
+ const visible = withoutMarkdownCode(text);
81
+ const re = /!?\[[^\]\n]*\]\(\s*(?:<([^>\n]+)>|([^\s)\n]+))/g;
82
+ for (const match of visible.matchAll(re)) {
83
+ const target = match[1] ?? match[2];
84
+ if (target)
85
+ out.push(target);
86
+ }
87
+ return out;
88
+ }
89
+ /**
90
+ * Count Markdown links that point at the Linux workspace root even though the
91
+ * adapter workspace is Windows-visible UNC. This is telemetry only: the Stop
92
+ * hook must not replace or retry an otherwise complete Codex reply.
93
+ */
94
+ export function codexWslFileLinkTelemetry(coordRoot, workspaceCwd, assistantMessage) {
95
+ const mapping = codexWslWorkspaceLinkMapping(coordRoot, workspaceCwd);
96
+ if (!mapping)
97
+ return null;
98
+ const mismatches = markdownDestinations(assistantMessage).filter((target) => isWithinOrEqual(target, mapping.linuxRoot));
99
+ return {
100
+ wsl_linux_file_link_count: mismatches.length,
101
+ ...(mismatches.length > 0
102
+ ? { wsl_linux_file_link_examples: mismatches.slice(0, 3).map((path) => path.slice(0, 240)) }
103
+ : {}),
104
+ };
105
+ }
106
+ function wslenvForwards(wslenv, variable) {
107
+ if (!wslenv)
108
+ return false;
109
+ return wslenv.split(":").some((entry) => entry.split("/", 1)[0] === variable);
110
+ }
111
+ /**
112
+ * Inspect the environment contract used when Windows-native Codex launches a
113
+ * Harnery hook or shell inside WSL. Returns null outside that hybrid mode.
114
+ */
115
+ export function inspectCodexWslBridge(env = process.env, options = {}) {
116
+ const inWsl = Boolean(env.WSL_DISTRO_NAME?.trim() || env.WSL_INTEROP?.trim());
117
+ if (!inWsl)
118
+ return null;
119
+ const threadIdPresent = Boolean(env.CODEX_THREAD_ID?.trim());
120
+ const wslenvForwardsThreadId = wslenvForwards(env.WSLENV, "CODEX_THREAD_ID");
121
+ if (!options.expected && !threadIdPresent && !wslenvForwardsThreadId)
122
+ return null;
123
+ const problems = [];
124
+ if (!threadIdPresent)
125
+ problems.push("CODEX_THREAD_ID did not reach WSL");
126
+ if (!wslenvForwardsThreadId)
127
+ problems.push("WSLENV does not forward CODEX_THREAD_ID");
128
+ const distro = env.WSL_DISTRO_NAME?.trim();
129
+ return {
130
+ ok: problems.length === 0,
131
+ detail: problems.length === 0
132
+ ? `thread identity forwarded through WSLENV${distro ? ` (${distro})` : ""}`
133
+ : problems.join("; "),
134
+ threadIdPresent,
135
+ wslenvForwardsThreadId,
136
+ };
137
+ }
@@ -40,7 +40,7 @@ export declare function journalArchive(repoRoot: string, owner: string): void;
40
40
  * Fire the optional host session-sync extension on turn stop / session end.
41
41
  * harnery core has no session-telemetry sink of its own; a host that wants one
42
42
  * drops an executable at
43
- * `scripts/hooks/adapter/claude_code/extensions/session-sync.sh` under the coord
43
+ * `scripts/hooks/harness/claude-code/extensions/session-sync.sh` under the coord
44
44
  * root, and core runs it detached + unref'd so a slow sink never blocks the
45
45
  * hook. `force` arrives as argv $1 ("1" on session end, "0" on turn stop) so the
46
46
  * host can rate-limit the stop path and force-flush on end. No-op when the
@@ -113,7 +113,7 @@ export function journalArchive(repoRoot, owner) {
113
113
  * Fire the optional host session-sync extension on turn stop / session end.
114
114
  * harnery core has no session-telemetry sink of its own; a host that wants one
115
115
  * drops an executable at
116
- * `scripts/hooks/adapter/claude_code/extensions/session-sync.sh` under the coord
116
+ * `scripts/hooks/harness/claude-code/extensions/session-sync.sh` under the coord
117
117
  * root, and core runs it detached + unref'd so a slow sink never blocks the
118
118
  * hook. `force` arrives as argv $1 ("1" on session end, "0" on turn stop) so the
119
119
  * host can rate-limit the stop path and force-flush on end. No-op when the
@@ -123,7 +123,7 @@ export function journalArchive(repoRoot, owner) {
123
123
  */
124
124
  export function runSessionSyncExtension(repoRoot, force) {
125
125
  try {
126
- const script = join(repoRoot, "scripts", "hooks", "adapter", "claude_code", "extensions", "session-sync.sh");
126
+ const script = join(repoRoot, "scripts", "hooks", "harness", "claude-code", "extensions", "session-sync.sh");
127
127
  if (!existsSync(script))
128
128
  return;
129
129
  const child = spawn("bash", [script, force ? "1" : "0"], {
@@ -147,7 +147,7 @@ export function runTurnSummary(repoRoot, owner, sessionId, transcriptPath) {
147
147
  try {
148
148
  if (!transcriptPath || !existsSync(transcriptPath))
149
149
  return;
150
- const script = join(repoRoot, "scripts", "hooks", "adapter", "claude_code", "extensions", "turn-summary.sh");
150
+ const script = join(repoRoot, "scripts", "hooks", "harness", "claude-code", "extensions", "turn-summary.sh");
151
151
  if (!existsSync(script))
152
152
  return;
153
153
  const child = spawn("bash", [script, owner, sessionId, transcriptPath], {
@@ -69,6 +69,24 @@ export type TurnStop = EventEnvelope<"turn.stop", {
69
69
  tool_call_count: number;
70
70
  text_length: number;
71
71
  status_box_present: boolean;
72
+ /** Strict variant of status_box_present: assistant text blocks only, so a
73
+ * tool_result carrying the box (the status command's own output) does not
74
+ * count. Shadow telemetry while the rule-2/3 detector decision is open;
75
+ * the Stop verdict does not read it yet. */
76
+ status_box_present_strict?: boolean;
77
+ /** Whether the session's suggested name is satisfied as of this turn:
78
+ * either this reply showed it, or an earlier reply did. Present whenever a
79
+ * suggested name exists, since the Stop verdict reads it per turn. */
80
+ session_name_present?: boolean;
81
+ /** Which suggested name `session_name_present` refers to. A projector
82
+ * rebuild attributes the sighting to this name instead of to whichever
83
+ * name is current during the replay. */
84
+ session_name_present_for?: string;
85
+ /** Shadow telemetry for Windows-hosted Codex workspaces: Markdown link
86
+ * destinations that still use the Linux coordination root. */
87
+ wsl_linux_file_link_count?: number;
88
+ /** Up to three bounded examples for diagnosing the mismatches locally. */
89
+ wsl_linux_file_link_examples?: string[];
72
90
  }>;
73
91
  export type CommandStart = EventEnvelope<"command.start", {
74
92
  cmd_id: string;
@@ -164,6 +182,10 @@ export type ContextRecoveryInjected = EventEnvelope<"context.recovery.injected",
164
182
  export type ClaimAcquire = EventEnvelope<"claim.acquire", {
165
183
  path: string;
166
184
  mode: "read" | "write";
185
+ finalization?: {
186
+ disposition: "git" | "output";
187
+ root: string;
188
+ };
167
189
  }>;
168
190
  export type ClaimRelease = EventEnvelope<"claim.release", {
169
191
  path: string;
@@ -177,6 +199,11 @@ export type ClaimConflict = EventEnvelope<"claim.conflict", {
177
199
  export type StateTaskSet = EventEnvelope<"state.task_set", {
178
200
  task: string;
179
201
  cleared: boolean;
202
+ /** True exactly when this call produced the session name (first non-empty
203
+ * declaration of a human-facing session); never true with a null name. */
204
+ first_of_session?: boolean;
205
+ /** The name produced by this call, for the Stop-hook naming rule. */
206
+ suggested_session_name?: string | null;
180
207
  }>;
181
208
  export type StateStatusChecked = EventEnvelope<"state.status_checked", {
182
209
  format: "box" | "json" | "table";