harnery 0.31.5 → 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 (222) 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 +11 -4
  28. package/dist/core/agents/coord-client.d.ts.map +1 -1
  29. package/dist/core/agents/coord-client.js +27 -9
  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/parse.d.ts +1 -1
  72. package/dist/core/hooks/adapter/parse.d.ts.map +1 -1
  73. package/dist/core/hooks/adapter/parse.js +22 -5
  74. package/dist/core/hooks/adapter/wiring.d.ts +23 -0
  75. package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
  76. package/dist/core/hooks/adapter/wiring.js +32 -0
  77. package/dist/core/hooks/cli.d.ts +1 -2
  78. package/dist/core/hooks/cli.d.ts.map +1 -1
  79. package/dist/core/hooks/cli.js +169 -27
  80. package/dist/core/hooks/codex-wsl-bridge.d.ts +46 -0
  81. package/dist/core/hooks/codex-wsl-bridge.d.ts.map +1 -0
  82. package/dist/core/hooks/codex-wsl-bridge.js +137 -0
  83. package/dist/core/hooks/effects/index.d.ts +1 -1
  84. package/dist/core/hooks/effects/index.js +3 -3
  85. package/dist/core/hooks/events/schema.d.ts +27 -0
  86. package/dist/core/hooks/events/schema.d.ts.map +1 -1
  87. package/dist/core/hooks/resolve/owner.d.ts +7 -1
  88. package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
  89. package/dist/core/hooks/resolve/owner.js +15 -0
  90. package/dist/core/hooks/resolve/transcript.d.ts +44 -0
  91. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  92. package/dist/core/hooks/resolve/transcript.js +176 -1
  93. package/dist/core/hooks/session-name-presence.d.ts +30 -0
  94. package/dist/core/hooks/session-name-presence.d.ts.map +1 -0
  95. package/dist/core/hooks/session-name-presence.js +43 -0
  96. package/dist/core/hooks/unsafe-cross-shell.d.ts +13 -0
  97. package/dist/core/hooks/unsafe-cross-shell.d.ts.map +1 -0
  98. package/dist/core/hooks/unsafe-cross-shell.js +151 -0
  99. package/dist/core/work/state.d.ts +4 -5
  100. package/dist/core/work/state.d.ts.map +1 -1
  101. package/dist/core/work/state.js +5 -8
  102. package/dist/core/workflow/index.d.ts +1 -1
  103. package/dist/core/workflow/index.d.ts.map +1 -1
  104. package/dist/core/workflow/proof.d.ts +2 -2
  105. package/dist/core/workflow/proof.d.ts.map +1 -1
  106. package/dist/core/workflow/proof.js +2 -2
  107. package/dist/core/workflow/run-state.d.ts +2 -2
  108. package/dist/core/workflow/run-state.d.ts.map +1 -1
  109. package/dist/core/workflow/run-state.js +2 -2
  110. package/dist/core/workflow/types.d.ts +3 -3
  111. package/dist/core/workflow/types.d.ts.map +1 -1
  112. package/dist/core/workflow/workspaces/execution.d.ts +2 -2
  113. package/dist/core/workflow/workspaces/execution.d.ts.map +1 -1
  114. package/dist/core/workflow/workspaces/execution.js +5 -0
  115. package/dist/core/workflow/workspaces/index.d.ts +1 -1
  116. package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
  117. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  118. package/dist/core/workflow/workspaces/local-git.js +1 -2
  119. package/dist/core/workflow/workspaces/paths.d.ts +2 -2
  120. package/dist/core/workflow/workspaces/types.d.ts +0 -6
  121. package/dist/core/workflow/workspaces/types.d.ts.map +1 -1
  122. package/dist/core/workflow/workspaces/validate.d.ts +0 -2
  123. package/dist/core/workflow/workspaces/validate.d.ts.map +1 -1
  124. package/dist/core/workflow/workspaces/validate.js +0 -2
  125. package/dist/lib/browser/client.d.ts +118 -1
  126. package/dist/lib/browser/client.d.ts.map +1 -1
  127. package/dist/lib/browser/client.js +435 -6
  128. package/dist/lib/browser/geometry.d.ts.map +1 -1
  129. package/dist/lib/browser/geometry.js +180 -37
  130. package/dist/lib/browser/index.d.ts +5 -1
  131. package/dist/lib/browser/index.d.ts.map +1 -1
  132. package/dist/lib/browser/index.js +5 -1
  133. package/dist/lib/browser/netscape-cookies.d.ts +6 -0
  134. package/dist/lib/browser/netscape-cookies.d.ts.map +1 -0
  135. package/dist/lib/browser/netscape-cookies.js +31 -0
  136. package/dist/lib/browser/proxy.d.ts +24 -0
  137. package/dist/lib/browser/proxy.d.ts.map +1 -0
  138. package/dist/lib/browser/proxy.js +84 -0
  139. package/dist/lib/browser/runts.d.ts +6 -0
  140. package/dist/lib/browser/runts.d.ts.map +1 -1
  141. package/dist/lib/browser/runts.js +21 -4
  142. package/dist/lib/browser/session-control.d.ts +112 -0
  143. package/dist/lib/browser/session-control.d.ts.map +1 -0
  144. package/dist/lib/browser/session-control.js +670 -0
  145. package/dist/lib/docs-links.d.ts +108 -0
  146. package/dist/lib/docs-links.d.ts.map +1 -0
  147. package/dist/lib/docs-links.js +555 -0
  148. package/dist/lib/exec.d.ts +1 -1
  149. package/dist/lib/exec.js +1 -1
  150. package/dist/lib/identities/assume.d.ts +4 -2
  151. package/dist/lib/identities/assume.d.ts.map +1 -1
  152. package/dist/lib/identities/assume.js +15 -2
  153. package/dist/lib/instructions/git-hooks.d.ts +75 -0
  154. package/dist/lib/instructions/git-hooks.d.ts.map +1 -0
  155. package/dist/lib/instructions/git-hooks.js +238 -0
  156. package/dist/lib/instructions/splice.d.ts +10 -4
  157. package/dist/lib/instructions/splice.d.ts.map +1 -1
  158. package/dist/lib/instructions/splice.js +19 -11
  159. package/package.json +1 -1
  160. package/schemas/config.schema.json +32 -0
  161. package/src/commander.ts +2 -0
  162. package/src/commands/agents.ts +96 -115
  163. package/src/commands/browse-session.ts +245 -0
  164. package/src/commands/browse.ts +287 -44
  165. package/src/commands/checkpoint.ts +1 -1
  166. package/src/commands/deinit.ts +7 -1
  167. package/src/commands/docs.ts +56 -0
  168. package/src/commands/doctor.ts +100 -18
  169. package/src/commands/init.ts +17 -9
  170. package/src/commands/tunnel.ts +2 -5
  171. package/src/core/agents/canonical-emit.ts +1 -2
  172. package/src/core/agents/cli.ts +137 -88
  173. package/src/core/agents/coord-client.ts +38 -9
  174. package/src/core/agents/finalization.ts +595 -0
  175. package/src/core/agents/git-hook.ts +126 -0
  176. package/src/core/agents/render/prompt-context.ts +28 -13
  177. package/src/core/agents/render/session-context.ts +15 -2
  178. package/src/core/agents/rules/claim-conflict.ts +3 -3
  179. package/src/core/agents/rules/commit-conflict.ts +35 -8
  180. package/src/core/agents/rules/stop-hook.ts +71 -23
  181. package/src/core/agents/session-events.ts +14 -41
  182. package/src/core/agents/state/heartbeat-projector.ts +45 -1
  183. package/src/core/agents/state/heartbeat-writer.ts +78 -16
  184. package/src/core/agents/state/names.ts +54 -2
  185. package/src/core/config.ts +92 -12
  186. package/src/core/governor/planning.ts +17 -12
  187. package/src/core/hooks/adapter/detect.ts +4 -13
  188. package/src/core/hooks/adapter/output.ts +9 -4
  189. package/src/core/hooks/adapter/parse.ts +24 -5
  190. package/src/core/hooks/adapter/wiring.ts +43 -0
  191. package/src/core/hooks/cli.ts +201 -23
  192. package/src/core/hooks/codex-wsl-bridge.ts +188 -0
  193. package/src/core/hooks/effects/index.ts +5 -5
  194. package/src/core/hooks/events/schema.ts +27 -0
  195. package/src/core/hooks/resolve/owner.ts +22 -1
  196. package/src/core/hooks/resolve/transcript.ts +164 -1
  197. package/src/core/hooks/session-name-presence.ts +52 -0
  198. package/src/core/hooks/unsafe-cross-shell.ts +160 -0
  199. package/src/core/work/state.ts +8 -12
  200. package/src/core/workflow/engine.ts +2 -2
  201. package/src/core/workflow/index.ts +0 -1
  202. package/src/core/workflow/proof.ts +4 -4
  203. package/src/core/workflow/run-state.ts +5 -5
  204. package/src/core/workflow/types.ts +3 -3
  205. package/src/core/workflow/workspaces/execution.ts +11 -4
  206. package/src/core/workflow/workspaces/index.ts +0 -3
  207. package/src/core/workflow/workspaces/local-git.ts +1 -2
  208. package/src/core/workflow/workspaces/paths.ts +2 -2
  209. package/src/core/workflow/workspaces/types.ts +0 -9
  210. package/src/core/workflow/workspaces/validate.ts +1 -5
  211. package/src/lib/browser/client.ts +528 -6
  212. package/src/lib/browser/geometry.ts +197 -37
  213. package/src/lib/browser/index.ts +39 -0
  214. package/src/lib/browser/netscape-cookies.ts +39 -0
  215. package/src/lib/browser/proxy.ts +105 -0
  216. package/src/lib/browser/runts.ts +27 -3
  217. package/src/lib/browser/session-control.ts +892 -0
  218. package/src/lib/docs-links.ts +674 -0
  219. package/src/lib/exec.ts +1 -1
  220. package/src/lib/identities/assume.ts +22 -1
  221. package/src/lib/instructions/git-hooks.ts +259 -0
  222. package/src/lib/instructions/splice.ts +41 -11
@@ -47,7 +47,9 @@ const MAX_RECORD_BYTES = 256 * 1024;
47
47
  const MAX_EVENTS_BYTES = 512 * 1024;
48
48
  const MAX_REASON = 2_000;
49
49
  const PROPOSAL_ROOT_RULE =
50
- "proposal.root names one newly proposed work key using a root-capable template; it does not preserve or repeat active_root, which is context only";
50
+ "proposal.root is inferred by Harnery as the single final outcome that no other proposed item depends on; it must use a root-capable template and depend transitively on every other proposed item; it never preserves or repeats active_root, which is context only";
51
+ const PLANNER_ROOT_RULE =
52
+ "Dependencies point from later work to earlier prerequisites. Do not return proposal.root. Harnery infers it as the single final outcome that no other proposed item depends on; make that outcome use a root-capable template and depend transitively on every other proposed item.";
51
53
 
52
54
  export interface RunGovernorPlannerInput {
53
55
  coordRoot: string;
@@ -729,7 +731,7 @@ function plannerScript(
729
731
  : "Return decision=apply with a replacement immutable root graph, or decision=attention when human judgment is required.",
730
732
  "Never claim mission completion unless the frozen mission acceptance is met. Never request a workflow outside the frozen template catalog.",
731
733
  "Dependencies may name an active work ID or an earlier key in your proposed work array.",
732
- `Every proposed item must be reachable from root. ${PROPOSAL_ROOT_RULE}.`,
734
+ PLANNER_ROOT_RULE,
733
735
  "Work keys are lowercase identifiers no longer than 32 characters. Keep titles within 200 characters, objectives within 4000 characters, and each acceptance criterion within 500 characters.",
734
736
  `Plan id: ${planId}`,
735
737
  `Planning trigger: ${trigger}`,
@@ -1331,12 +1333,6 @@ function planProposalSchema(
1331
1333
  const applyProperties: Record<string, StageSchema> = {
1332
1334
  decision: { type: "string", enum: ["apply"] },
1333
1335
  rationale,
1334
- root: {
1335
- type: "string",
1336
- minLength: 1,
1337
- maxLength: 32,
1338
- pattern: "^[a-z][a-z0-9-]{0,31}$",
1339
- },
1340
1336
  work: {
1341
1337
  type: "array",
1342
1338
  minItems: 1,
@@ -1344,7 +1340,7 @@ function planProposalSchema(
1344
1340
  items: workItem,
1345
1341
  },
1346
1342
  };
1347
- const applyRequired = ["decision", "rationale", "root", "work"];
1343
+ const applyRequired = ["decision", "rationale", "work"];
1348
1344
  if (record.intent.mission) {
1349
1345
  applyProperties.milestone = milestone;
1350
1346
  applyRequired.push("milestone");
@@ -1390,10 +1386,10 @@ function normalizeProposal(
1390
1386
  const proposedAt = preservedProposedAt ?? new Date().toISOString();
1391
1387
  const decision = enumValue(value.decision, ["apply", "complete", "attention"], "plan decision");
1392
1388
  const rationale = bounded(value.rationale, "plan rationale", MAX_REASON);
1393
- const root = typeof value.root === "string" ? value.root.trim() : "";
1389
+ const terminalRoot = typeof value.root === "string" ? value.root.trim() : "";
1394
1390
  if (!Array.isArray(value.work)) throw new Error("plan work must be an array");
1395
1391
  if (decision === "attention" || decision === "complete") {
1396
- if (root || value.work.length > 0 || value.milestone !== undefined) {
1392
+ if (terminalRoot || value.work.length > 0 || value.milestone !== undefined) {
1397
1393
  throw new Error(`${decision} plan must not contain work or milestone data`);
1398
1394
  }
1399
1395
  if (decision === "complete" && (!record.intent.mission || trigger !== "milestone")) {
@@ -1473,7 +1469,16 @@ function normalizeProposal(
1473
1469
  template,
1474
1470
  };
1475
1471
  });
1476
- if (!seen.has(root)) throw new Error(`plan root ${root || "(empty)"} is not a proposed key`);
1472
+ const dependedOn = new Set(
1473
+ work.flatMap((item) => item.dependencies.filter((dependency) => seen.has(dependency))),
1474
+ );
1475
+ const terminalOutcomes = work.filter((item) => !dependedOn.has(item.key));
1476
+ if (terminalOutcomes.length !== 1) {
1477
+ throw new Error(
1478
+ `plan must have exactly one final outcome; found ${terminalOutcomes.map((item) => item.key).join(", ")}`,
1479
+ );
1480
+ }
1481
+ const root = terminalOutcomes[0]!.key;
1477
1482
  const rootSpec = work.find((item) => item.key === root);
1478
1483
  if (!rootSpec || !policy.templates[rootSpec.template]?.root) {
1479
1484
  throw new Error(`plan root ${root} must use a root-capable template`);
@@ -4,32 +4,23 @@ import type { Adapter } from "../events/schema.ts";
4
4
  /**
5
5
  * Resolve the adapter firing the hook. The dispatcher binary is always
6
6
  * invoked with `--adapter <name>` per the current wiring in each adapter's
7
- * settings file. `--harness` remains an accepted alias because settings files
8
- * written by older releases survive a package upgrade until the consumer
9
- * re-runs `harn init`. Returns null when neither flag is present; caller falls
10
- * through to the env-based fallback or skips emission.
7
+ * settings file. Returns null when the flag is absent; caller falls through to
8
+ * the environment fallback or skips emission.
11
9
  */
12
10
  export function detectAdapter(argv: readonly string[]): Adapter | null {
13
11
  for (let i = 0; i < argv.length; i++) {
14
12
  const a = argv[i]!;
15
- if (a === "--adapter" || a === "--harness") {
13
+ if (a === "--adapter") {
16
14
  return validate(argv[i + 1]);
17
15
  }
18
16
  if (a.startsWith("--adapter=")) {
19
17
  return validate(a.slice("--adapter=".length));
20
18
  }
21
- if (a.startsWith("--harness=")) {
22
- return validate(a.slice("--harness=".length));
23
- }
24
19
  }
25
- return validate(coordEnv("AGENT_COORD_ADAPTER") ?? coordEnv("AGENT_COORD_HARNESS"));
20
+ return validate(coordEnv("AGENT_COORD_ADAPTER"));
26
21
  }
27
22
 
28
23
  function validate(v: string | undefined): Adapter | null {
29
24
  if (v === "claude-code" || v === "cursor" || v === "codex") return v;
30
- // Legacy: some adapters use `claude_code` (underscore). Map
31
- // it through so the env-based fallback works for callers that haven't
32
- // updated yet.
33
- if (v === "claude_code") return "claude-code";
34
25
  return null;
35
26
  }
@@ -19,7 +19,7 @@
19
19
  */
20
20
 
21
21
  import { STOP_REMEDIATION_MARKER } from "../../agents/rules/stop-hook.ts";
22
- import { resolveBinName } from "../../config.ts";
22
+ import { endOfTurnStatusCommand, resolveBinName } from "../../config.ts";
23
23
  import type { Adapter } from "../events/schema.ts";
24
24
 
25
25
  export type SystemEvent = "SessionStart" | "UserPromptSubmit" | "SubagentStart";
@@ -60,7 +60,11 @@ export function emitDeny(adapter: Adapter, reason: string): void {
60
60
  * We exit 0 so Cursor treats the run as a success and honors the output.
61
61
  * (Confirmed against cursor.com/docs/agent/hooks.)
62
62
  */
63
- export function emitStopBlock(adapter: Adapter, verdict: { reason?: string; rule: string }): 0 | 2 {
63
+ export function emitStopBlock(
64
+ adapter: Adapter,
65
+ verdict: { reason?: string; rule: string },
66
+ coordRoot?: string,
67
+ ): 0 | 2 {
64
68
  if (adapter === "codex") return 0;
65
69
 
66
70
  const reason = verdict.reason ?? "End-of-turn coordination ritual incomplete.";
@@ -70,11 +74,12 @@ export function emitStopBlock(adapter: Adapter, verdict: { reason?: string; rule
70
74
  // see in chat that the message came from Harnery rather than from them.
71
75
  // Both commands are named because one message that repairs the whole ritual
72
76
  // ends the chain in a single followup; the failing rule is context.
73
- const bin = resolveBinName();
77
+ const bin = resolveBinName(coordRoot);
78
+ const statusCommand = endOfTurnStatusCommand(coordRoot);
74
79
  const message = [
75
80
  `${STOP_REMEDIATION_MARKER} rule=${verdict.rule}]`,
76
81
  reason,
77
- `Repair the ritual in this turn: run \`${bin} agents set-task "<short focus>"\` and then \`${bin} agents status\` as your last tool call.`,
82
+ `Repair the ritual in this turn: run \`${bin} agents set-task "<short focus>"\` and then \`${statusCommand}\` as your last tool call.`,
78
83
  ].join("\n");
79
84
  process.stdout.write(`${JSON.stringify({ followup_message: message })}\n`);
80
85
  return 0;
@@ -43,7 +43,7 @@ export interface ParsedPayload {
43
43
  * Parse the raw stdin payload string for any adapter. Returns null when JSON
44
44
  * parse fails (Cursor occasionally fires hooks with no payload).
45
45
  */
46
- export function parsePayload(raw: string, _adapter: Adapter): ParsedPayload | null {
46
+ export function parsePayload(raw: string, adapter: Adapter): ParsedPayload | null {
47
47
  if (!raw || raw.trim().length === 0) return null;
48
48
  let json: Record<string, unknown>;
49
49
  try {
@@ -52,13 +52,26 @@ export function parsePayload(raw: string, _adapter: Adapter): ParsedPayload | nu
52
52
  return null;
53
53
  }
54
54
 
55
+ const sessionId = normalizeSessionId(adapter, pickStr(json, "session_id"));
56
+ const conversationId = normalizeSessionId(adapter, pickStr(json, "conversation_id"));
57
+ const parentSessionId = normalizeSessionId(adapter, pickStr(json, "parent_session_id"));
58
+ const normalizedRaw =
59
+ adapter === "cursor"
60
+ ? {
61
+ ...json,
62
+ ...(sessionId ? { session_id: sessionId } : {}),
63
+ ...(conversationId ? { conversation_id: conversationId } : {}),
64
+ ...(parentSessionId ? { parent_session_id: parentSessionId } : {}),
65
+ }
66
+ : json;
67
+
55
68
  return {
56
69
  hook_event_name: pickStr(json, "hook_event_name"),
57
- session_id: pickStr(json, "session_id"),
70
+ session_id: sessionId,
58
71
  agent_id: pickStr(json, "agent_id"),
59
72
  subagent_id: pickStr(json, "subagent_id"),
60
- conversation_id: pickStr(json, "conversation_id"),
61
- parent_session_id: pickStr(json, "parent_session_id"),
73
+ conversation_id: conversationId,
74
+ parent_session_id: parentSessionId,
62
75
  turn_id: pickStr(json, "turn_id"),
63
76
  parent_turn_id: pickStr(json, "parent_turn_id"),
64
77
  transcript_path: pickStr(json, "transcript_path"),
@@ -75,10 +88,16 @@ export function parsePayload(raw: string, _adapter: Adapter): ParsedPayload | nu
75
88
  clean_exit: pickBool(json, "clean_exit"),
76
89
  exit_status: pickStr(json, "exit_status"),
77
90
  reason: pickStr(json, "reason"),
78
- raw: json,
91
+ raw: normalizedRaw,
79
92
  };
80
93
  }
81
94
 
95
+ /** Cursor Glass prefixes conversation ids with `bc-`; coordination uses the bare id everywhere. */
96
+ function normalizeSessionId(adapter: Adapter, value: string | undefined): string | undefined {
97
+ if (adapter !== "cursor" || !value?.startsWith("bc-") || value.length <= 3) return value;
98
+ return value.slice(3);
99
+ }
100
+
82
101
  /**
83
102
  * Pull the bash command string out of a Bash/Shell tool_input. Returns
84
103
  * undefined for non-shell tools.
@@ -200,6 +200,49 @@ export function loadAdapterWiring(projectRoot: string): AdapterWiringStatus[] {
200
200
  return out;
201
201
  }
202
202
 
203
+ export interface AdapterWiringSummary {
204
+ /** Adapters carrying at least one harnery hook. */
205
+ wired: AdapterId[];
206
+ /**
207
+ * Adapters carrying none: no settings file at all, or a settings file with
208
+ * none of ours in it. An unparseable file lands in neither list, since
209
+ * `loadAdapterWiring` already reports that as drift.
210
+ */
211
+ unwired: AdapterId[];
212
+ }
213
+
214
+ /**
215
+ * Classify every known adapter as wired or unwired, without judging whether an
216
+ * unwired one is a problem.
217
+ *
218
+ * `loadAdapterWiring` stays deliberately silent about an adapter with zero
219
+ * harnery hooks, because a bare settings file is not an opt-out signal and
220
+ * nagging about it would false-warn every session. That silence is right in
221
+ * isolation and wrong once you also know the adapter's CLI is installed: then
222
+ * an agent can start a session here and register nothing. Splitting the fact
223
+ * from the judgement lets a caller holding both (doctor) tell "not used here"
224
+ * from "never wired" without changing what counts as drift.
225
+ */
226
+ export function summarizeAdapterWiring(projectRoot: string): AdapterWiringSummary {
227
+ const wired: AdapterId[] = [];
228
+ const unwired: AdapterId[] = [];
229
+ for (const [id, spec] of Object.entries(ADAPTER_SPECS) as [AdapterId, AdapterSpec][]) {
230
+ const settingsPath = resolve(projectRoot, spec.settingsFile);
231
+ if (!existsSync(settingsPath)) {
232
+ unwired.push(id);
233
+ continue;
234
+ }
235
+ let settings: SettingsFile;
236
+ try {
237
+ settings = JSON.parse(readFileSync(settingsPath, "utf8")) as SettingsFile;
238
+ } catch {
239
+ continue; // unparseable → already surfaced by loadAdapterWiring
240
+ }
241
+ (diffWiring(settings, spec).present.length > 0 ? wired : unwired).push(id);
242
+ }
243
+ return { wired, unwired };
244
+ }
245
+
203
246
  /**
204
247
  * Resolve the harnery package version for context in nudges/checks. Walks up
205
248
  * 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.
@@ -25,10 +24,17 @@ import { coordEnv } from "../../lib/env.ts";
25
24
  import { replayCodexJsonl } from "../agents/codex-replay.ts";
26
25
  import { coordBinPath } from "../agents/coord-bin.ts";
27
26
  import { consumeSince, writeCursor } from "../agents/events/consume.ts";
27
+ import {
28
+ type ClaimFinalizationDecision,
29
+ type ClaimFinalizationDescriptor,
30
+ classifyWriteClaimFinalization,
31
+ formatWriteClaimFinalizationDenial,
32
+ } from "../agents/finalization.ts";
28
33
  import { evaluateStopHook } from "../agents/rules/stop-hook.ts";
29
34
  import { projectHeartbeats } from "../agents/state/heartbeat-projector.ts";
30
35
  import { writePidmapRow } from "../agents/state/pidmap.ts";
31
36
  import { shellMutationPaths } from "../agents/state/shell-mutation.ts";
37
+ import { agentsRequireGitFinalization, resolveBinName } from "../config.ts";
32
38
  import {
33
39
  checkpointContext,
34
40
  completeContextRecovery,
@@ -49,6 +55,12 @@ import {
49
55
  type ParsedPayload,
50
56
  parsePayload,
51
57
  } from "./adapter/parse.ts";
58
+ import {
59
+ codexWslFileLinkTelemetry,
60
+ inspectCodexWslBridge,
61
+ isWslUncPath,
62
+ renderCodexWslFileLinkContext,
63
+ } from "./codex-wsl-bridge.ts";
52
64
  import {
53
65
  captureImages,
54
66
  detectPresence,
@@ -69,7 +81,14 @@ import { adapterPidFromEnv, parsePsChainLine, selectAnchorPid } from "./resolve/
69
81
  import { findCoordRoot } from "./resolve/coord-root.ts";
70
82
  import { extractIntentComment, resolveIntent } from "./resolve/intent.ts";
71
83
  import { resolveOwner } from "./resolve/owner.ts";
72
- import { scanStatusBoxPresent, scanTranscriptModel } from "./resolve/transcript.ts";
84
+ import {
85
+ detectForkParent,
86
+ scanAssistantTextIncludes,
87
+ scanStatusBoxPresent,
88
+ scanTranscriptModel,
89
+ } from "./resolve/transcript.ts";
90
+ import { sessionNamePresence } from "./session-name-presence.ts";
91
+ import { unsafeCrossShellReason } from "./unsafe-cross-shell.ts";
73
92
 
74
93
  interface Argv {
75
94
  eventName: string | null;
@@ -80,11 +99,11 @@ function parseArgv(argv: string[]): Argv {
80
99
  const out: Argv = { eventName: null, extra: [] };
81
100
  for (let i = 0; i < argv.length; i++) {
82
101
  const arg = argv[i]!;
83
- if (arg === "--adapter" || arg === "--harness") {
102
+ if (arg === "--adapter") {
84
103
  i++; // detectAdapter will re-parse; just consume the value here.
85
104
  continue;
86
105
  }
87
- if (arg.startsWith("--adapter=") || arg.startsWith("--harness=")) continue;
106
+ if (arg.startsWith("--adapter=")) continue;
88
107
  if (!out.eventName && !arg.startsWith("--")) {
89
108
  out.eventName = arg;
90
109
  } else {
@@ -160,11 +179,14 @@ function assignNameViaAgentCoord(
160
179
  coordRoot: string,
161
180
  instanceId: string,
162
181
  kind: "session" | "subagent" | "transient",
182
+ forkedFrom?: string,
163
183
  ): { name: string; kind: string } | null {
164
184
  const binary = coordBinPath("agent-coord", coordRoot) ?? "";
165
185
  if (!existsSync(binary)) return null;
166
186
  try {
167
- const result = spawnSync(binary, ["assign-name", instanceId, kind], {
187
+ const args = ["assign-name", instanceId, kind];
188
+ if (forkedFrom) args.push("--forked-from", forkedFrom);
189
+ const result = spawnSync(binary, args, {
168
190
  encoding: "utf8",
169
191
  timeout: 2000,
170
192
  env: childEnv(coordRoot),
@@ -238,13 +260,26 @@ function buildEventData(
238
260
  case "session.start": {
239
261
  const adapterPlatform =
240
262
  ctx.adapter === "claude-code"
241
- ? "claude_code"
263
+ ? "claude-code"
242
264
  : ctx.adapter === "cursor"
243
265
  ? "cursor"
244
266
  : "codex";
267
+ // Recorded fork lineage is NOT detected here. On claude-code a fork
268
+ // never fires its own session.start — SessionStart fires under the
269
+ // PARENT's session id (source=resume) before the fork id is minted
270
+ // (verified 2026-08-05) — so detection at this point can only mislabel
271
+ // the resumed parent. The fork's new instance is caught by the
272
+ // tool.pre_use heal path instead. The forkedFrom plumbing below stays
273
+ // for adapters that DO report a parent at session start.
274
+ const forkedFrom: string | undefined = undefined;
245
275
  // Assign (or recover) name + kind via agent-coord. Idempotent: resume
246
276
  // returns the original name; new owner consumes a counter slot.
247
- const assigned = assignNameViaAgentCoord(ctx.coordRoot, ctx.instanceId, "session");
277
+ const assigned = assignNameViaAgentCoord(
278
+ ctx.coordRoot,
279
+ ctx.instanceId,
280
+ "session",
281
+ forkedFrom,
282
+ );
248
283
  // Write the adapter pid-map row so `harn agents whoami` ppid-walks find
249
284
  // this owner. Prefer the payload pid (the actual claude binary), then the
250
285
  // anchor walk (the `node` ancestor for Cursor, which has no payload pid),
@@ -268,6 +303,10 @@ function buildEventData(
268
303
  name: assigned?.name,
269
304
  kind: "session",
270
305
  agent_id: ctx.instanceId,
306
+ // Recorded fork lineage rides the canonical event too, so full replay
307
+ // and derived readers converge on the same parent linkage that
308
+ // .name-history carries (the ADR 0017 dual-write pattern).
309
+ ...(forkedFrom ? { forked_from: forkedFrom } : {}),
271
310
  // Workflow-child linkage: `workflow run` children carry the run id in
272
311
  // env (spawn adapters set it via buildChildEnv), so their sessions and
273
312
  // heartbeats join back to the run transcript + /workflows web view.
@@ -298,6 +337,11 @@ function buildEventData(
298
337
  }
299
338
 
300
339
  case "turn.stop": {
340
+ const lastAssistantMessage = (p?.raw.last_assistant_message as string | undefined) ?? "";
341
+ const fileLinkTelemetry =
342
+ ctx.adapter === "codex"
343
+ ? codexWslFileLinkTelemetry(ctx.coordRoot, p?.cwd, lastAssistantMessage)
344
+ : null;
301
345
  return {
302
346
  // Backfill the model for adapters that omit it at session.start
303
347
  // (Claude Code). The transcript is populated with assistant turns by
@@ -316,8 +360,27 @@ function buildEventData(
316
360
  // the verdict now sees because agent-hook emits this turn.stop itself
317
361
  // (the previous path passed those via the no-history fail-open).
318
362
  status_box_present:
319
- scanStatusBoxPresent(p?.transcript_path) ||
320
- ((p?.raw.last_assistant_message as string | undefined) ?? "").includes("┌─ agent-"),
363
+ scanStatusBoxPresent(p?.transcript_path) || lastAssistantMessage.includes("┌─ agent-"),
364
+ // Shadow measurement for the rule-2/3 detector: the loose scan above
365
+ // matches the box prefix ANYWHERE in the transcript tail, including
366
+ // the tool_result row the status command itself writes, so it can
367
+ // report a paste that never happened. The strict variant counts
368
+ // assistant text only (same window, same scanner as the session-name
369
+ // detection). Telemetry-only: the Stop verdict still reads
370
+ // status_box_present. Once the divergence rate between the two fields
371
+ // is known, the gate either flips to strict or the rule gets
372
+ // reconsidered; either way this field decides it with data.
373
+ status_box_present_strict:
374
+ scanAssistantTextIncludes(p?.transcript_path, "┌─ agent-") ||
375
+ lastAssistantMessage.includes("┌─ agent-"),
376
+ // Whether the session's suggested name is satisfied as of this turn,
377
+ // and which name that refers to. Reported on every stop while a name
378
+ // exists (the stop-hook.session_name verdict reads it per turn); the
379
+ // transcript scan itself stops once the name has been sighted.
380
+ ...sessionNamePresence(ctx.coordRoot, ctx.instanceId, lastAssistantMessage, (name) =>
381
+ scanAssistantTextIncludes(p?.transcript_path, name),
382
+ ),
383
+ ...(fileLinkTelemetry ?? {}),
321
384
  stop_hook_active: p?.stop_hook_active,
322
385
  };
323
386
  }
@@ -805,6 +868,7 @@ async function main(): Promise<number> {
805
868
  owner.instance_id,
806
869
  sessionId,
807
870
  adapter,
871
+ payload?.cwd,
808
872
  recovery?.briefing ?? "",
809
873
  );
810
874
  if (injected && recovery) {
@@ -897,7 +961,7 @@ async function main(): Promise<number> {
897
961
  // re-prompts only via a `followup_message` it auto-submits. emitStopBlock
898
962
  // writes the right shape and returns the exit code to use.
899
963
  const { emitStopBlock } = await import("./adapter/output.ts");
900
- return emitStopBlock(adapter, verdict);
964
+ return emitStopBlock(adapter, verdict, coordRoot);
901
965
  }
902
966
  }
903
967
 
@@ -912,7 +976,21 @@ async function main(): Promise<number> {
912
976
  // call was restored here afterward.
913
977
  if (norm.event_type === "tool.pre_use") {
914
978
  try {
915
- healHeartbeatViaCli(coordRoot, owner.instance_id, sessionId, adapter);
979
+ // Recorded fork lineage, heal-path flavor. A forked CC conversation
980
+ // never fires its own session.start (SessionStart fires under the
981
+ // PARENT's session id with source=resume, before the fork id is
982
+ // minted; verified 2026-08-05), so the fork's new instance first
983
+ // materializes right here. Gate detection on "no heartbeat yet" so the
984
+ // transcript scan runs once per instance lifetime, not per tool call.
985
+ let forkedFrom: string | undefined;
986
+ if (
987
+ adapter === "claude-code" &&
988
+ payload?.transcript_path &&
989
+ !existsSync(join(coordRoot, ".harnery", "active", `${owner.instance_id}.json`))
990
+ ) {
991
+ forkedFrom = detectForkParent(payload.transcript_path, owner.instance_id);
992
+ }
993
+ healHeartbeatViaCli(coordRoot, owner.instance_id, sessionId, adapter, forkedFrom);
916
994
  refreshPidmap(coordRoot, owner.instance_id, adapter, payload?.pid);
917
995
  } catch (err) {
918
996
  logError(coordRoot, err, { phase: "pre-tool-use-heal" });
@@ -933,6 +1011,29 @@ async function main(): Promise<number> {
933
1011
  logError(coordRoot, err, { phase: "pre-tool-use-image-capture" });
934
1012
  }
935
1013
 
1014
+ // Windows-native Codex + WSL UNC only: block the one cross-shell shape
1015
+ // proven to corrupt argument boundaries. Normal WSL argv calls, literal
1016
+ // bash -s scripts, native Linux/macOS sessions, and every other adapter
1017
+ // pass through. The host instructions name its concrete safe bridge.
1018
+ const unsafeShellReason = unsafeCrossShellReason({
1019
+ adapter,
1020
+ cwd: payload?.cwd,
1021
+ toolName: payload?.tool_name,
1022
+ toolInput: payload?.tool_input,
1023
+ });
1024
+ if (unsafeShellReason) {
1025
+ emit(coordRoot, {
1026
+ event_type: "decision.block",
1027
+ instance_id: owner.instance_id,
1028
+ session_id: sessionId,
1029
+ adapter,
1030
+ data: { rule: "unsafe_cross_shell", reason: unsafeShellReason },
1031
+ });
1032
+ const { emitDeny } = await import("./adapter/output.ts");
1033
+ emitDeny(adapter, unsafeShellReason);
1034
+ return 0;
1035
+ }
1036
+
936
1037
  // G-guard for ALL adapters. Claude Code previously ran this via a
937
1038
  // pre-tool-use bash adapter (which called `agent-coord verdict --rule=claim`);
938
1039
  // that adapter is now deleted, so agent-hook owns the deny for every adapter.
@@ -1033,13 +1134,44 @@ async function runPreToolUseGuard(
1033
1134
  adapter: Adapter,
1034
1135
  ): Promise<void> {
1035
1136
  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);
1137
+ const rawTargets = collectGuardTargets(toolName, data);
1138
+ if (rawTargets.length === 0) return;
1139
+
1140
+ let targets: Array<{ path: string; finalization?: ClaimFinalizationDescriptor }>;
1141
+ if (agentsRequireGitFinalization(coordRoot)) {
1142
+ const decisions = rawTargets.map((path) => classifyWriteClaimFinalization(coordRoot, path));
1143
+ const denied = decisions.find((decision) => !decision.allow);
1144
+ if (denied && !denied.allow) {
1145
+ const { emitDeny } = await import("./adapter/output.ts");
1146
+ emitDeny(adapter, formatWriteClaimFinalizationDenial(denied, resolveBinName(coordRoot)));
1147
+ return;
1148
+ }
1149
+ const allowed = decisions.filter(
1150
+ (decision): decision is Extract<ClaimFinalizationDecision, { allow: true }> => decision.allow,
1151
+ );
1152
+ targets = allowed.map((decision) => ({
1153
+ path: decision.path,
1154
+ finalization: decision.descriptor,
1155
+ }));
1156
+ } else {
1157
+ targets = rawTargets
1158
+ .map((path) => canonicalize(coordRoot, path))
1159
+ .filter((path): path is string => path !== null)
1160
+ .map((path) => ({ path }));
1161
+ }
1039
1162
  if (targets.length === 0) return;
1040
1163
 
1041
1164
  const agentCoordBin = coordBinPath("agent-coord", coordRoot) ?? "";
1042
- if (!existsSync(agentCoordBin)) return;
1165
+ if (!existsSync(agentCoordBin)) {
1166
+ if (agentsRequireGitFinalization(coordRoot)) {
1167
+ const { emitDeny } = await import("./adapter/output.ts");
1168
+ emitDeny(
1169
+ adapter,
1170
+ "Harnery denied the write before mutation because the claim coordinator is unavailable; repair the project's Harnery installation and retry.",
1171
+ );
1172
+ }
1173
+ return;
1174
+ }
1043
1175
 
1044
1176
  // For apply_patch (multi-file), collect siblings so the deny reason names
1045
1177
  // them. For single-file tools the array has one entry.
@@ -1048,7 +1180,7 @@ async function runPreToolUseGuard(
1048
1180
  rule: "claim",
1049
1181
  instance_id: instanceId,
1050
1182
  session_id: sessionId,
1051
- path: target,
1183
+ path: target.path,
1052
1184
  });
1053
1185
  const result = spawnSync(agentCoordBin, ["verdict"], {
1054
1186
  input: verdictReq,
@@ -1064,10 +1196,12 @@ async function runPreToolUseGuard(
1064
1196
  continue;
1065
1197
  }
1066
1198
  if (parsed.allow === false) {
1067
- let reason = parsed.reason ?? `Path ${target} is currently being edited by another agent.`;
1199
+ let reason =
1200
+ parsed.reason ?? `Path ${target.path} is currently being edited by another agent.`;
1068
1201
  if (targets.length > 1) {
1069
1202
  const siblings = targets
1070
- .filter((p) => p !== target)
1203
+ .filter((candidate) => candidate !== target)
1204
+ .map((candidate) => candidate.path)
1071
1205
  .slice(0, 3)
1072
1206
  .join(", ");
1073
1207
  if (siblings) {
@@ -1078,6 +1212,24 @@ async function runPreToolUseGuard(
1078
1212
  emitDeny(adapter, reason);
1079
1213
  return;
1080
1214
  }
1215
+
1216
+ // The verdict mutates the live heartbeat immediately, but that file is a
1217
+ // projection and can be rebuilt at any later hook. Persist the acquisition
1218
+ // in the canonical stream too. This matters most for Codex apply_patch:
1219
+ // one tool call can claim several files, and tool.pre_use's clamped payload
1220
+ // is not a durable ownership representation for those targets.
1221
+ emit(coordRoot, {
1222
+ event_type: "claim.acquire",
1223
+ instance_id: instanceId,
1224
+ session_id: sessionId,
1225
+ adapter,
1226
+ source: "agent-hooks",
1227
+ data: {
1228
+ path: target.path,
1229
+ mode: "write",
1230
+ ...(target.finalization ? { finalization: target.finalization } : {}),
1231
+ },
1232
+ });
1081
1233
  }
1082
1234
  }
1083
1235
 
@@ -1142,14 +1294,18 @@ function healHeartbeatViaCli(
1142
1294
  instanceId: string,
1143
1295
  sessionId: string,
1144
1296
  adapter: string,
1297
+ forkedFrom?: string,
1145
1298
  ): void {
1146
1299
  const agentCoordBin = coordBinPath("agent-coord", coordRoot) ?? "";
1147
1300
  if (!existsSync(agentCoordBin)) return;
1148
1301
  // Pass the detected adapter so a pruned Cursor/Codex heartbeat is recreated
1149
1302
  // 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}`], {
1303
+ // claude-code and the dashboard mislabels the agent. See
1304
+ // heartbeat-writer.healHeartbeat. --forked-from carries detected fork
1305
+ // lineage for an instance that never session.started (the CC fork flow).
1306
+ const args = ["heal-heartbeat", instanceId, sessionId, `--adapter=${adapter}`];
1307
+ if (forkedFrom) args.push(`--forked-from=${forkedFrom}`);
1308
+ spawnSync(agentCoordBin, args, {
1153
1309
  encoding: "utf8",
1154
1310
  timeout: 2000,
1155
1311
  env: childEnv(coordRoot),
@@ -1384,6 +1540,7 @@ async function emitUserPromptSubmitSystemMessage(
1384
1540
  instanceId: string,
1385
1541
  sessionId: string,
1386
1542
  adapter: Adapter,
1543
+ workspaceCwd?: string,
1387
1544
  recoveryBriefing = "",
1388
1545
  ): Promise<boolean> {
1389
1546
  const agentCoordBin = coordBinPath("agent-coord", coordRoot) ?? "";
@@ -1416,6 +1573,12 @@ async function emitUserPromptSubmitSystemMessage(
1416
1573
  const result = spawnSync(agentCoordBin, args, { encoding: "utf8", timeout: 3000 });
1417
1574
  if (result.status === 0 && result.stdout) additionalContext = result.stdout.trim();
1418
1575
  }
1576
+ if (adapter === "codex") {
1577
+ const fileLinkContext = renderCodexWslFileLinkContext(coordRoot, workspaceCwd);
1578
+ if (fileLinkContext) {
1579
+ additionalContext = [additionalContext, fileLinkContext].filter(Boolean).join("\n\n");
1580
+ }
1581
+ }
1419
1582
  if (recoveryBriefing) {
1420
1583
  additionalContext = [additionalContext, recoveryBriefing].filter(Boolean).join("\n\n");
1421
1584
  }
@@ -1466,7 +1629,7 @@ function emitSubagentStartContext(
1466
1629
  }
1467
1630
 
1468
1631
  function adapterPlatform(adapter: Adapter): string {
1469
- if (adapter === "claude-code") return "claude_code";
1632
+ if (adapter === "claude-code") return "claude-code";
1470
1633
  return adapter;
1471
1634
  }
1472
1635
 
@@ -1527,6 +1690,21 @@ async function emitSessionStartSystemMessage(
1527
1690
  if (recoveryBriefing) {
1528
1691
  additionalContext = [additionalContext, recoveryBriefing].filter(Boolean).join("\n\n");
1529
1692
  }
1693
+ if (adapter === "codex" && isWslUncPath(emittedData.cwd)) {
1694
+ const fileLinkContext = renderCodexWslFileLinkContext(coordRoot, emittedData.cwd);
1695
+ if (fileLinkContext) {
1696
+ additionalContext = [additionalContext, fileLinkContext].filter(Boolean).join("\n\n");
1697
+ }
1698
+ const bridge = inspectCodexWslBridge(process.env, { expected: true });
1699
+ if (bridge && !bridge.ok) {
1700
+ additionalContext = [
1701
+ additionalContext,
1702
+ `Harnery hybrid warning: ${bridge.detail}. Run \`${resolveBinName(coordRoot)} doctor\` for the repair hint.`,
1703
+ ]
1704
+ .filter(Boolean)
1705
+ .join("\n\n");
1706
+ }
1707
+ }
1530
1708
  if (!additionalContext) return false;
1531
1709
 
1532
1710
  const { emitContext } = await import("./adapter/output.ts");