harnery 0.41.0 → 0.43.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 (269) hide show
  1. package/dist/commander.d.ts +16 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commander.js +21 -0
  4. package/dist/commands/agents.d.ts.map +1 -1
  5. package/dist/commands/agents.js +63 -5
  6. package/dist/commands/artifacts.d.ts.map +1 -1
  7. package/dist/commands/artifacts.js +113 -12
  8. package/dist/commands/browse.d.ts +164 -0
  9. package/dist/commands/browse.d.ts.map +1 -1
  10. package/dist/commands/browse.js +263 -51
  11. package/dist/commands/claude-desktop.d.ts.map +1 -1
  12. package/dist/commands/claude-desktop.js +52 -0
  13. package/dist/commands/disk.d.ts +6 -0
  14. package/dist/commands/disk.d.ts.map +1 -0
  15. package/dist/commands/disk.js +80 -0
  16. package/dist/commands/doctor.d.ts +8 -0
  17. package/dist/commands/doctor.d.ts.map +1 -1
  18. package/dist/commands/doctor.js +48 -1
  19. package/dist/commands/files.d.ts +16 -0
  20. package/dist/commands/files.d.ts.map +1 -1
  21. package/dist/commands/files.js +54 -2
  22. package/dist/commands/init.d.ts +2 -2
  23. package/dist/commands/init.d.ts.map +1 -1
  24. package/dist/commands/init.js +43 -9
  25. package/dist/commands/rm.d.ts +4 -0
  26. package/dist/commands/rm.d.ts.map +1 -0
  27. package/dist/commands/rm.js +87 -0
  28. package/dist/commands/servers.d.ts +7 -0
  29. package/dist/commands/servers.d.ts.map +1 -0
  30. package/dist/commands/servers.js +394 -0
  31. package/dist/commands/tunnel.d.ts +65 -0
  32. package/dist/commands/tunnel.d.ts.map +1 -1
  33. package/dist/commands/tunnel.js +372 -103
  34. package/dist/commands/web-fetch.d.ts +3 -2
  35. package/dist/commands/web-fetch.d.ts.map +1 -1
  36. package/dist/commands/web-fetch.js +25 -19
  37. package/dist/commands/web.d.ts +7 -1
  38. package/dist/commands/web.d.ts.map +1 -1
  39. package/dist/commands/web.js +9 -3
  40. package/dist/core/adapters/profiles.d.ts +9 -0
  41. package/dist/core/adapters/profiles.d.ts.map +1 -1
  42. package/dist/core/adapters/profiles.js +22 -1
  43. package/dist/core/adapters/types.d.ts +9 -0
  44. package/dist/core/adapters/types.d.ts.map +1 -1
  45. package/dist/core/agents/cli.js +13 -6
  46. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  47. package/dist/core/agents/render/prompt-context.js +11 -5
  48. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  49. package/dist/core/agents/render/session-context.js +59 -13
  50. package/dist/core/agents/state/live-coordination-view.d.ts +20 -0
  51. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  52. package/dist/core/agents/state/live-coordination-view.js +60 -0
  53. package/dist/core/agents/state/names.d.ts +43 -3
  54. package/dist/core/agents/state/names.d.ts.map +1 -1
  55. package/dist/core/agents/state/names.js +87 -3
  56. package/dist/core/artifacts/delivery-card.js +9 -2
  57. package/dist/core/artifacts/index.d.ts +140 -4
  58. package/dist/core/artifacts/index.d.ts.map +1 -1
  59. package/dist/core/artifacts/index.js +522 -49
  60. package/dist/core/config.d.ts +68 -11
  61. package/dist/core/config.d.ts.map +1 -1
  62. package/dist/core/config.js +128 -3
  63. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  64. package/dist/core/diagnostics/bundle.js +2 -2
  65. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  66. package/dist/core/events/v3/producers/recorder.js +16 -0
  67. package/dist/core/events/v3/projection.js +9 -1
  68. package/dist/core/events/v3/support-storage/pack-reader.d.ts.map +1 -1
  69. package/dist/core/events/v3/support-storage/pack-reader.js +42 -16
  70. package/dist/core/hooks/adapter/events.d.ts +17 -0
  71. package/dist/core/hooks/adapter/events.d.ts.map +1 -1
  72. package/dist/core/hooks/adapter/events.js +5 -0
  73. package/dist/core/hooks/adapter/runtime-telemetry.js +3 -1
  74. package/dist/core/hooks/adapter/wiring.d.ts +37 -7
  75. package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
  76. package/dist/core/hooks/adapter/wiring.js +62 -4
  77. package/dist/core/hooks/cli.js +131 -23
  78. package/dist/core/hooks/fork-lineage.d.ts +40 -0
  79. package/dist/core/hooks/fork-lineage.d.ts.map +1 -0
  80. package/dist/core/hooks/fork-lineage.js +62 -0
  81. package/dist/core/hooks/resolve/transcript.d.ts +16 -0
  82. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  83. package/dist/core/hooks/resolve/transcript.js +84 -0
  84. package/dist/core/hooks/run-markers.d.ts +37 -0
  85. package/dist/core/hooks/run-markers.d.ts.map +1 -0
  86. package/dist/core/hooks/run-markers.js +109 -0
  87. package/dist/core/hooks/shell-rm.d.ts +2 -0
  88. package/dist/core/hooks/shell-rm.d.ts.map +1 -0
  89. package/dist/core/hooks/shell-rm.js +105 -0
  90. package/dist/core/hooks/shell-tokens.d.ts +12 -0
  91. package/dist/core/hooks/shell-tokens.d.ts.map +1 -0
  92. package/dist/core/hooks/shell-tokens.js +97 -0
  93. package/dist/core/hooks/shell-waiter.d.ts.map +1 -1
  94. package/dist/core/hooks/shell-waiter.js +3 -93
  95. package/dist/core/semantic/contract.d.ts +5 -5
  96. package/dist/core/servers/index.d.ts +211 -0
  97. package/dist/core/servers/index.d.ts.map +1 -0
  98. package/dist/core/servers/index.js +560 -0
  99. package/dist/core/servers/net.d.ts +58 -0
  100. package/dist/core/servers/net.d.ts.map +1 -0
  101. package/dist/core/servers/net.js +270 -0
  102. package/dist/core/servers/tunnels.d.ts +14 -0
  103. package/dist/core/servers/tunnels.d.ts.map +1 -0
  104. package/dist/core/servers/tunnels.js +33 -0
  105. package/dist/core/storage/builtins.js +1 -0
  106. package/dist/core/storage/process-log.d.ts +12 -0
  107. package/dist/core/storage/process-log.d.ts.map +1 -1
  108. package/dist/core/storage/process-log.js +10 -2
  109. package/dist/core/workflow/engine.d.ts.map +1 -1
  110. package/dist/core/workflow/engine.js +30 -1
  111. package/dist/core/workflow/sandbox-projection.d.ts +13 -2
  112. package/dist/core/workflow/sandbox-projection.d.ts.map +1 -1
  113. package/dist/core/workflow/sandbox-projection.js +23 -0
  114. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  115. package/dist/core/workflow/spawn-claude.js +6 -1
  116. package/dist/core/workflow/spawn-codex.d.ts +2 -0
  117. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  118. package/dist/core/workflow/spawn-codex.js +20 -1
  119. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  120. package/dist/core/workflow/spawn-cursor.js +6 -1
  121. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  122. package/dist/core/workflow/spawn-opencode.js +4 -1
  123. package/dist/core/workflow/types.d.ts +47 -2
  124. package/dist/core/workflow/types.d.ts.map +1 -1
  125. package/dist/core/workflow/worker-access.d.ts +92 -0
  126. package/dist/core/workflow/worker-access.d.ts.map +1 -0
  127. package/dist/core/workflow/worker-access.js +450 -0
  128. package/dist/lib/browser/batch-steps.d.ts +18 -0
  129. package/dist/lib/browser/batch-steps.d.ts.map +1 -0
  130. package/dist/lib/browser/batch-steps.js +82 -0
  131. package/dist/lib/browser/collect/collector.d.ts +99 -0
  132. package/dist/lib/browser/collect/collector.d.ts.map +1 -0
  133. package/dist/lib/browser/collect/collector.js +202 -0
  134. package/dist/lib/browser/collect/fields.d.ts +31 -0
  135. package/dist/lib/browser/collect/fields.d.ts.map +1 -0
  136. package/dist/lib/browser/collect/fields.js +75 -0
  137. package/dist/lib/browser/collect/index.d.ts +6 -0
  138. package/dist/lib/browser/collect/index.d.ts.map +1 -0
  139. package/dist/lib/browser/collect/index.js +5 -0
  140. package/dist/lib/browser/collect/page-driver.d.ts +14 -0
  141. package/dist/lib/browser/collect/page-driver.d.ts.map +1 -0
  142. package/dist/lib/browser/collect/page-driver.js +306 -0
  143. package/dist/lib/browser/collect/presets.d.ts +52 -0
  144. package/dist/lib/browser/collect/presets.d.ts.map +1 -0
  145. package/dist/lib/browser/collect/presets.js +176 -0
  146. package/dist/lib/browser/collect/scroll-plan.d.ts +85 -0
  147. package/dist/lib/browser/collect/scroll-plan.d.ts.map +1 -0
  148. package/dist/lib/browser/collect/scroll-plan.js +118 -0
  149. package/dist/lib/browser/index.d.ts +1 -1
  150. package/dist/lib/browser/index.d.ts.map +1 -1
  151. package/dist/lib/browser/page-review-judge.d.ts +2 -0
  152. package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
  153. package/dist/lib/browser/page-review-judge.js +13 -0
  154. package/dist/lib/browser/qa-run-contracts.d.ts +10 -0
  155. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
  156. package/dist/lib/browser/qa-run.d.ts.map +1 -1
  157. package/dist/lib/browser/qa-run.js +1 -0
  158. package/dist/lib/browser/windows-chrome.d.ts +59 -0
  159. package/dist/lib/browser/windows-chrome.d.ts.map +1 -0
  160. package/dist/lib/browser/windows-chrome.js +179 -0
  161. package/dist/lib/claude-desktop-share.d.ts +136 -0
  162. package/dist/lib/claude-desktop-share.d.ts.map +1 -0
  163. package/dist/lib/claude-desktop-share.js +394 -0
  164. package/dist/lib/disk-usage.d.ts +50 -0
  165. package/dist/lib/disk-usage.d.ts.map +1 -0
  166. package/dist/lib/disk-usage.js +303 -0
  167. package/dist/lib/docs-lint.d.ts +6 -0
  168. package/dist/lib/docs-lint.d.ts.map +1 -1
  169. package/dist/lib/docs-lint.js +8 -3
  170. package/dist/lib/guarded-remove.d.ts +34 -0
  171. package/dist/lib/guarded-remove.d.ts.map +1 -0
  172. package/dist/lib/guarded-remove.js +310 -0
  173. package/dist/lib/instructions/templates.js +8 -8
  174. package/dist/lib/tunnel/allowlist.d.ts +44 -0
  175. package/dist/lib/tunnel/allowlist.d.ts.map +1 -0
  176. package/dist/lib/tunnel/allowlist.js +204 -0
  177. package/dist/lib/tunnel/current-address.d.ts +41 -0
  178. package/dist/lib/tunnel/current-address.d.ts.map +1 -0
  179. package/dist/lib/tunnel/current-address.js +123 -0
  180. package/dist/lib/tunnel/error-page.d.ts +1 -1
  181. package/dist/lib/tunnel/error-page.d.ts.map +1 -1
  182. package/dist/lib/tunnel/error-page.js +19 -8
  183. package/dist/lib/tunnel/gate.js +33 -7
  184. package/dist/lib/tunnel/path-scope.d.ts +22 -0
  185. package/dist/lib/tunnel/path-scope.d.ts.map +1 -0
  186. package/dist/lib/tunnel/path-scope.js +62 -0
  187. package/dist/lib/tunnel/processes.d.ts +39 -0
  188. package/dist/lib/tunnel/processes.d.ts.map +1 -0
  189. package/dist/lib/tunnel/processes.js +121 -0
  190. package/dist/lib/tunnel/state.d.ts +33 -3
  191. package/dist/lib/tunnel/state.d.ts.map +1 -1
  192. package/dist/lib/tunnel/state.js +115 -30
  193. package/package.json +7 -1
  194. package/schemas/config.schema.json +91 -0
  195. package/src/commander.ts +51 -0
  196. package/src/commands/agents.ts +111 -11
  197. package/src/commands/artifacts.ts +171 -19
  198. package/src/commands/browse.ts +400 -57
  199. package/src/commands/claude-desktop.ts +61 -0
  200. package/src/commands/disk.ts +114 -0
  201. package/src/commands/doctor.ts +52 -1
  202. package/src/commands/files.ts +93 -2
  203. package/src/commands/init.ts +55 -5
  204. package/src/commands/rm.ts +115 -0
  205. package/src/commands/servers.ts +506 -0
  206. package/src/commands/tunnel.ts +494 -116
  207. package/src/commands/web-fetch.ts +30 -19
  208. package/src/commands/web.ts +9 -3
  209. package/src/core/adapters/profiles.ts +22 -1
  210. package/src/core/adapters/types.ts +10 -0
  211. package/src/core/agents/cli.ts +14 -6
  212. package/src/core/agents/render/prompt-context.ts +25 -8
  213. package/src/core/agents/render/session-context.ts +84 -16
  214. package/src/core/agents/state/live-coordination-view.ts +73 -0
  215. package/src/core/agents/state/names.ts +117 -6
  216. package/src/core/artifacts/delivery-card.ts +10 -2
  217. package/src/core/artifacts/index.ts +697 -44
  218. package/src/core/config.ts +183 -4
  219. package/src/core/diagnostics/bundle.ts +2 -1
  220. package/src/core/events/v3/producers/recorder.ts +16 -0
  221. package/src/core/events/v3/projection.ts +11 -1
  222. package/src/core/events/v3/support-storage/pack-reader.ts +45 -19
  223. package/src/core/hooks/adapter/events.ts +19 -0
  224. package/src/core/hooks/adapter/runtime-telemetry.ts +3 -1
  225. package/src/core/hooks/adapter/wiring.ts +116 -8
  226. package/src/core/hooks/cli.ts +152 -22
  227. package/src/core/hooks/fork-lineage.ts +82 -0
  228. package/src/core/hooks/resolve/transcript.ts +88 -0
  229. package/src/core/hooks/run-markers.ts +132 -0
  230. package/src/core/hooks/shell-rm.ts +103 -0
  231. package/src/core/hooks/shell-tokens.ts +96 -0
  232. package/src/core/hooks/shell-waiter.ts +4 -91
  233. package/src/core/servers/index.ts +712 -0
  234. package/src/core/servers/net.ts +283 -0
  235. package/src/core/servers/tunnels.ts +39 -0
  236. package/src/core/storage/builtins.ts +1 -0
  237. package/src/core/storage/process-log.ts +17 -2
  238. package/src/core/workflow/engine.ts +38 -1
  239. package/src/core/workflow/sandbox-projection.ts +38 -2
  240. package/src/core/workflow/spawn-claude.ts +5 -1
  241. package/src/core/workflow/spawn-codex.ts +18 -1
  242. package/src/core/workflow/spawn-cursor.ts +5 -1
  243. package/src/core/workflow/spawn-opencode.ts +4 -1
  244. package/src/core/workflow/types.ts +62 -2
  245. package/src/core/workflow/worker-access.ts +525 -0
  246. package/src/lib/browser/batch-steps.ts +79 -0
  247. package/src/lib/browser/collect/collector.ts +306 -0
  248. package/src/lib/browser/collect/fields.ts +87 -0
  249. package/src/lib/browser/collect/index.ts +47 -0
  250. package/src/lib/browser/collect/page-driver.ts +327 -0
  251. package/src/lib/browser/collect/presets.ts +237 -0
  252. package/src/lib/browser/collect/scroll-plan.ts +179 -0
  253. package/src/lib/browser/index.ts +1 -0
  254. package/src/lib/browser/page-review-judge.ts +16 -0
  255. package/src/lib/browser/qa-run-contracts.ts +11 -0
  256. package/src/lib/browser/qa-run.ts +1 -0
  257. package/src/lib/browser/windows-chrome.ts +227 -0
  258. package/src/lib/claude-desktop-share.ts +511 -0
  259. package/src/lib/disk-usage.ts +347 -0
  260. package/src/lib/docs-lint.ts +8 -3
  261. package/src/lib/guarded-remove.ts +376 -0
  262. package/src/lib/instructions/templates.ts +8 -8
  263. package/src/lib/tunnel/allowlist.ts +208 -0
  264. package/src/lib/tunnel/current-address.ts +158 -0
  265. package/src/lib/tunnel/error-page.ts +25 -9
  266. package/src/lib/tunnel/gate.ts +38 -9
  267. package/src/lib/tunnel/path-scope.ts +73 -0
  268. package/src/lib/tunnel/processes.ts +149 -0
  269. package/src/lib/tunnel/state.ts +143 -30
@@ -57,7 +57,13 @@ import {
57
57
  } from "../agents/state/heartbeat-writer.ts";
58
58
  import { readLiveCoordinationRow } from "../agents/state/live-coordination-view.ts";
59
59
  import { ensureLiveCoordinationHeartbeat } from "../agents/state/live-coordination-writer.ts";
60
- import { assignName } from "../agents/state/names.ts";
60
+ import {
61
+ assignName,
62
+ readForkParent,
63
+ readForkState,
64
+ recordForkDetection,
65
+ resolveName,
66
+ } from "../agents/state/names.ts";
61
67
  import { writePidmapRow } from "../agents/state/pidmap.ts";
62
68
  import {
63
69
  agentsRequireGitFinalization,
@@ -90,6 +96,7 @@ import { closeProcessLoggers, legacyLogFields, processLogger } from "../storage/
90
96
  import { stableScopeId } from "../workflow/scope-id.ts";
91
97
  import { type AdapterBehavior, adapterBehavior } from "./adapter/behaviors/index.ts";
92
98
  import { detectAdapter, shouldSkipHookAdapter } from "./adapter/detect.ts";
99
+ import { ADAPTER_SPECS } from "./adapter/events.ts";
93
100
  import {
94
101
  extractBashCommand,
95
102
  extractToolDescription,
@@ -98,6 +105,7 @@ import {
98
105
  type ParsedPayload,
99
106
  parsePayload,
100
107
  } from "./adapter/parse.ts";
108
+ import { hookTimeoutFor } from "./adapter/wiring.ts";
101
109
  import { scheduleBackupSnapshot } from "./backup-schedule.ts";
102
110
  import {
103
111
  captureImages,
@@ -113,6 +121,7 @@ import {
113
121
  soundForEvent,
114
122
  } from "./effects/index.ts";
115
123
  import { toolInputHash, toolTargetHash } from "./events/input-hash.ts";
124
+ import { detectClaudeCodeFork } from "./fork-lineage.ts";
116
125
  import { canonicalize } from "./guard-path.ts";
117
126
  import {
118
127
  beginHookHealth,
@@ -131,7 +140,9 @@ import {
131
140
  scanStatusBoxPresent,
132
141
  scanTranscriptRuntime,
133
142
  } from "./resolve/transcript.ts";
143
+ import { clearHookRunMarker, writeHookRunMarker } from "./run-markers.ts";
134
144
  import { sessionNamePresence } from "./session-name-presence.ts";
145
+ import { shellRmReason } from "./shell-rm.ts";
135
146
  import { shellWaiterReason } from "./shell-waiter.ts";
136
147
 
137
148
  interface Argv {
@@ -140,6 +151,7 @@ interface Argv {
140
151
  }
141
152
 
142
153
  let hookHealthState: HookHealthState | undefined;
154
+ let hookRunMarkerPath: string | null = null;
143
155
 
144
156
  function parseArgv(argv: string[]): Argv {
145
157
  const out: Argv = { eventName: null, extra: [] };
@@ -246,12 +258,14 @@ function assignNameInProcess(
246
258
  coordRoot: string,
247
259
  instanceId: string,
248
260
  kind: "session" | "subagent" | "transient",
249
- forkedFrom?: string,
261
+ fork?: { forkedFrom?: string; fork?: boolean; forkChecked?: boolean },
250
262
  ): { name: string; kind: string } | null {
251
263
  try {
252
264
  const name = assignName(coordRoot, instanceId, kind, {
253
265
  freshnessSecs: coordFreshnessSeconds(coordRoot),
254
- ...(forkedFrom ? { forkedFrom } : {}),
266
+ ...(fork?.forkedFrom ? { forkedFrom: fork.forkedFrom } : {}),
267
+ ...(fork?.fork ? { fork: true } : {}),
268
+ ...(fork?.forkChecked ? { forkChecked: true } : {}),
255
269
  });
256
270
  return { name, kind };
257
271
  } catch {
@@ -259,6 +273,66 @@ function assignNameInProcess(
259
273
  }
260
274
  }
261
275
 
276
+ /**
277
+ * Name a main session, recording fork lineage when the instance is new. Every
278
+ * main-session naming path goes through here: whichever hook first sees a new
279
+ * instance writes its history row, and lineage can only ride that first row
280
+ * (later detection appends a `fork.detected` row instead).
281
+ */
282
+ function assignSessionNameInProcess(
283
+ coordRoot: string,
284
+ instanceId: string,
285
+ payload: ParsedPayload | null,
286
+ adapter: Adapter,
287
+ ): { name: string; kind: string } | null {
288
+ let fork: { forkedFrom?: string; fork?: boolean; forkChecked?: boolean } | undefined;
289
+ try {
290
+ if (
291
+ adapter === "claude-code" &&
292
+ payload?.session_id === instanceId &&
293
+ !resolveName(coordRoot, instanceId)
294
+ ) {
295
+ const detected = detectClaudeCodeFork({
296
+ coordRoot,
297
+ transcriptPath: payload.transcript_path,
298
+ sessionId: instanceId,
299
+ source: payload.source,
300
+ });
301
+ fork = {
302
+ ...(detected.forkedFrom ? { forkedFrom: detected.forkedFrom } : {}),
303
+ fork: detected.fork,
304
+ forkChecked: detected.checked,
305
+ };
306
+ }
307
+ } catch {
308
+ fork = undefined;
309
+ }
310
+ return assignNameInProcess(coordRoot, instanceId, "session", fork);
311
+ }
312
+
313
+ /**
314
+ * Finish fork detection that session start could not: a CLI fork starts before
315
+ * its transcript exists, and a mid-flight instance may never see SessionStart.
316
+ * Runs once per instance; the appended `fork_checked` row stops later calls.
317
+ */
318
+ function healForkLineage(
319
+ coordRoot: string,
320
+ instanceId: string,
321
+ payload: ParsedPayload | null,
322
+ ): void {
323
+ if (!payload?.transcript_path || payload.session_id !== instanceId) return;
324
+ const state = readForkState(coordRoot, instanceId);
325
+ if (!state || state.checked) return;
326
+ const detected = detectClaudeCodeFork({
327
+ coordRoot,
328
+ transcriptPath: payload.transcript_path,
329
+ sessionId: instanceId,
330
+ knownFork: state.fork,
331
+ });
332
+ if (!detected.checked) return;
333
+ recordForkDetection(coordRoot, instanceId, detected);
334
+ }
335
+
262
336
  /** Mid-flight sessions can emit tools before SessionStart assigns a pool name.
263
337
  * Stamp the disposable cache so Codec does not render the 8-character id. */
264
338
  function ensureSessionDisplayName(
@@ -266,13 +340,14 @@ function ensureSessionDisplayName(
266
340
  instanceId: string,
267
341
  sessionId: string,
268
342
  adapter: Adapter,
343
+ payload: ParsedPayload | null,
269
344
  ): void {
270
345
  if (coordEnv("WORKFLOW_CHILD") === "1") return;
271
346
  const row =
272
347
  readLiveCoordinationRow(coordRoot, instanceId) ?? readHeartbeat(coordRoot, instanceId);
273
348
  if (row?.kind === "subagent" || row?.kind === "transient" || row?.workflow_run_id) return;
274
349
  if (row?.name?.trim()) return;
275
- const assigned = assignNameInProcess(coordRoot, instanceId, "session");
350
+ const assigned = assignSessionNameInProcess(coordRoot, instanceId, payload, adapter);
276
351
  if (!assigned?.name) return;
277
352
  if (!readHeartbeat(coordRoot, instanceId)) {
278
353
  try {
@@ -341,17 +416,22 @@ function buildEventData(
341
416
  // The platform label is the adapter id itself; enumerating and defaulting
342
417
  // to codex silently mislabels any adapter not in the list.
343
418
  const adapterPlatform = ctx.adapter;
344
- // Recorded fork lineage is NOT detected here. On claude-code a fork
345
- // never fires its own session.started — SessionStart fires under the
346
- // PARENT's session id (source=resume) before the fork id is minted
347
- // (verified 2026-08-05) — so detection at this point can only mislabel
348
- // the resumed parent. The fork's new instance is caught by the
349
- // tool.requested heal path instead. The forkedFrom plumbing below stays
350
- // for adapters that DO report a parent at session start.
351
- const forkedFrom: string | undefined = undefined;
352
419
  // Assign (or recover) name + kind in-process. Idempotent: resume
353
- // returns the original name; new owner consumes a counter slot.
354
- const assigned = assignNameInProcess(ctx.coordRoot, ctx.instanceId, "session", forkedFrom);
420
+ // returns the original name; new owner consumes a counter slot and,
421
+ // on claude-code, runs fork detection (fork-lineage.ts). Since Claude
422
+ // Code 2.1.283 both fork flows fire SessionStart under the FORK's own
423
+ // id (verified 2026-10-07), superseding the 2026-08-05 finding that a
424
+ // fork never started on its own id. A resumed parent keeps its existing
425
+ // history row, so it is never relabeled here.
426
+ // Workflow children stay unnamed, as on the tool path: a pool name
427
+ // would make them the addressee of mail held for that name.
428
+ const assigned =
429
+ coordEnv("WORKFLOW_CHILD") === "1"
430
+ ? undefined
431
+ : assignSessionNameInProcess(ctx.coordRoot, ctx.instanceId, p, ctx.adapter);
432
+ const forkedFrom = assigned
433
+ ? readForkParent(ctx.coordRoot, ctx.instanceId)?.instance_id
434
+ : undefined;
355
435
  // Write the adapter pid-map row so `harn agents whoami` ppid-walks find
356
436
  // this owner. Prefer the payload pid (the actual claude binary), then the
357
437
  // anchor walk (the `node` ancestor for Cursor, which has no payload pid),
@@ -416,8 +496,11 @@ function buildEventData(
416
496
  // SessionStart. Mint durable display identity here so a later
417
497
  // SessionStart remains idempotent and Codec is not stuck on the
418
498
  // 8-character native id.
419
- if (!readLiveCoordinationRow(ctx.coordRoot, ctx.instanceId)?.name) {
420
- assignNameInProcess(ctx.coordRoot, ctx.instanceId, "session");
499
+ if (
500
+ coordEnv("WORKFLOW_CHILD") !== "1" &&
501
+ !readLiveCoordinationRow(ctx.coordRoot, ctx.instanceId)?.name
502
+ ) {
503
+ assignSessionNameInProcess(ctx.coordRoot, ctx.instanceId, p, ctx.adapter);
421
504
  }
422
505
  const prompt = p?.prompt ?? "";
423
506
  const { value, truncated } = clampString(prompt, 4000);
@@ -663,6 +746,24 @@ async function main(): Promise<number> {
663
746
  const { eventName, extra } = parseArgv(process.argv.slice(2));
664
747
  const adapter = detectAdapter(process.argv.slice(2));
665
748
  const behavior = adapter ? adapterBehavior(adapter) : null;
749
+ // Adapters that kill a hook at its timeout get a start marker, so a killed
750
+ // run leaves evidence for doctor (ADR 0197). Written before stdin is read.
751
+ const markerSpec = adapter ? ADAPTER_SPECS[adapter] : undefined;
752
+ const markerTimeout =
753
+ markerSpec && eventName && markerSpec.events.some((e) => e.subcommand === eventName)
754
+ ? hookTimeoutFor(markerSpec, eventName)
755
+ : undefined;
756
+ if (markerTimeout !== undefined && adapter && eventName && coordEnv("AGENT_COORD_OFF") !== "1") {
757
+ const markerRoot = resolveCoordRoot(process.cwd());
758
+ if (markerRoot) {
759
+ hookRunMarkerPath = writeHookRunMarker(markerRoot, {
760
+ event: eventName,
761
+ adapter,
762
+ pid: process.pid,
763
+ timeout_sec: markerTimeout,
764
+ });
765
+ }
766
+ }
666
767
  const raw = await readStdin();
667
768
  hookHealthState = beginHookHealth({
668
769
  started_at_ms: hookStartedAt,
@@ -821,7 +922,7 @@ async function main(): Promise<number> {
821
922
  !priorCoordination?.workflow_run_id &&
822
923
  !priorCoordination?.name?.trim()
823
924
  ) {
824
- assignNameInProcess(coordRoot, owner.instance_id, "session");
925
+ assignSessionNameInProcess(coordRoot, owner.instance_id, payload, adapter);
825
926
  }
826
927
 
827
928
  const data = buildEventData(norm.event_type, {
@@ -1033,11 +1134,18 @@ function recordSignal(run: HookRun): void {
1033
1134
  }
1034
1135
  if (norm.event_type !== "session.ended") {
1035
1136
  try {
1036
- ensureSessionDisplayName(coordRoot, owner.instance_id, sessionId, adapter);
1137
+ ensureSessionDisplayName(coordRoot, owner.instance_id, sessionId, adapter, payload);
1037
1138
  } catch (error) {
1038
1139
  logError(coordRoot, error, { phase: "session-display-name" });
1039
1140
  }
1040
1141
  }
1142
+ if (norm.event_type === "tool.requested" && adapter === "claude-code" && run.topLevelSession) {
1143
+ try {
1144
+ healForkLineage(coordRoot, owner.instance_id, payload);
1145
+ } catch (error) {
1146
+ logError(coordRoot, error, { phase: "fork-lineage-heal" });
1147
+ }
1148
+ }
1041
1149
  const v3EventId =
1042
1150
  v3Result && "event" in v3Result
1043
1151
  ? v3Result.event.event_id
@@ -1189,9 +1297,12 @@ async function handleSessionStarted(run: HookRun): Promise<void> {
1189
1297
  const { createAutomaticMaintenanceComposition, runAutomaticMaintenancePass } = await import(
1190
1298
  "../storage/maintenance-providers.ts"
1191
1299
  );
1192
- // Effect: throttled daily sweep of expired artifact workspaces (guarded,
1193
- // managed-expired only; see autoCleanArtifacts). Best-effort like every
1194
- // janitor here: a failure logs and never blocks session start.
1300
+ // Effect: throttled hourly artifact sweep (see autoCleanArtifacts). It
1301
+ // deletes expired workspaces, plus unheld workspaces over the per-bundle
1302
+ // ceiling or repository budget once they have been idle past
1303
+ // artifacts.idle_grace_hours. Each deletion is rechecked first and recorded
1304
+ // in .harnery/artifact-deletions.jsonl. Best-effort like every janitor
1305
+ // here: a failure logs and never blocks session start.
1195
1306
  try {
1196
1307
  autoCleanArtifacts(coordRoot);
1197
1308
  } catch (err) {
@@ -1526,6 +1637,17 @@ async function handleToolRequested(run: HookRun): Promise<number | undefined> {
1526
1637
  return 0;
1527
1638
  }
1528
1639
 
1640
+ const rmReason = shellRmReason(
1641
+ payload?.tool_name,
1642
+ payload?.tool_input,
1643
+ resolveBinName(coordRoot),
1644
+ );
1645
+ if (rmReason) {
1646
+ const { emitDeny } = await import("./adapter/output.ts");
1647
+ emitDeny(adapter, rmReason);
1648
+ return 0;
1649
+ }
1650
+
1529
1651
  // G-guard for ALL adapters. Claude Code previously ran this via a
1530
1652
  // pre-tool-use bash adapter (which called `agent-coord verdict --rule=claim`);
1531
1653
  // that adapter is now deleted, so agent-hook owns the deny for every adapter.
@@ -1564,7 +1686,8 @@ async function handlePostToolUse(run: HookRun): Promise<void> {
1564
1686
  // status is the wrong place to ask.
1565
1687
  try {
1566
1688
  const row = readLiveCoordinationRow(coordRoot, owner.instance_id);
1567
- const name = sessionNameDisplayPending(row);
1689
+ // A workflow child has no user-visible title to set.
1690
+ const name = coordEnv("WORKFLOW_CHILD") === "1" ? undefined : sessionNameDisplayPending(row);
1568
1691
  if (name && toolResponseMintedSessionName(payload?.tool_response, name)) {
1569
1692
  // Record the title before asking for it. The suggestion can still
1570
1693
  // change afterwards (an assigned-name rewrite, a lifecycle re-mint, a
@@ -1765,6 +1888,7 @@ async function enforcePendingSessionNameDisplay(
1765
1888
  payload: ParsedPayload | null,
1766
1889
  coordination: Heartbeat | null,
1767
1890
  ): Promise<boolean> {
1891
+ if (coordEnv("WORKFLOW_CHILD") === "1") return true;
1768
1892
  const name = sessionNameDisplayPending(coordination);
1769
1893
  if (!name) return true;
1770
1894
 
@@ -2168,6 +2292,10 @@ async function emitUserPromptSubmitSystemMessage(
2168
2292
  recoveryBriefing = "",
2169
2293
  hostPromptContext = "",
2170
2294
  ): Promise<boolean> {
2295
+ // Workflow children get no prompt-time coordination context, matching
2296
+ // SessionStart. The turn ritual is not enforced for them, the naming nudge
2297
+ // would register them as named peers, and rendering drains peer mail.
2298
+ if (coordEnv("WORKFLOW_CHILD") === "1") return false;
2171
2299
  const behavior = adapterBehavior(adapter);
2172
2300
  const { renderPromptContext } = await import("../agents/render/prompt-context.ts");
2173
2301
  let additionalContext = renderPromptContext({
@@ -2307,6 +2435,7 @@ function completeRecoveryInjection(
2307
2435
 
2308
2436
  main()
2309
2437
  .then(async (code) => {
2438
+ clearHookRunMarker(hookRunMarkerPath);
2310
2439
  writeHookHealthCompletion(hookHealthState, {
2311
2440
  finished_at_ms: performance.now(),
2312
2441
  finished_rss_bytes: process.memoryUsage().rss,
@@ -2321,6 +2450,7 @@ main()
2321
2450
  pid: process.pid,
2322
2451
  phase: "top-level",
2323
2452
  });
2453
+ clearHookRunMarker(hookRunMarkerPath);
2324
2454
  writeHookHealthCompletion(hookHealthState, {
2325
2455
  finished_at_ms: performance.now(),
2326
2456
  finished_rss_bytes: process.memoryUsage().rss,
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Claude Code fork detection: decide whether a new main-session instance is a
3
+ * branch of another session, and which one.
4
+ *
5
+ * Two shapes exist as of Claude Code 2.1.283 (verified 2026-10-07):
6
+ *
7
+ * - CLI `--resume <id> --fork-session` fires SessionStart under the FORK's id
8
+ * with `source: "fork"`, before its transcript exists, and names no parent.
9
+ * The transcript later holds the parent's message uuids with every row's
10
+ * `sessionId` rewritten, so the parent is found by uuid containment
11
+ * (`detectForkParent`) once the transcript is on disk.
12
+ * - The desktop app's "Fork from here" copies the parent's rows into the
13
+ * fork's transcript with their original `sessionId`, so inherited rows
14
+ * name the parent directly (`detectInheritedSessionParent`).
15
+ *
16
+ * Inherited rows alone do not prove a fork: a continuation of a finished
17
+ * session can carry them too. The parent's liveness separates the two. A
18
+ * parent with an unended generation is still running (or resumable) and owns
19
+ * its name, task, and claims, so the new instance is a fork. A finished
20
+ * parent makes it a continuation, which keeps the generic notice.
21
+ *
22
+ * Everything here fails open: an unreadable transcript or an unknown layout
23
+ * yields "not a fork", never an error.
24
+ */
25
+
26
+ import { existsSync } from "node:fs";
27
+ import { readLiveCoordinationRow } from "../agents/state/live-coordination-view.ts";
28
+ import { detectForkParent, detectInheritedSessionParent } from "./resolve/transcript.ts";
29
+
30
+ export interface ForkDetection {
31
+ fork: boolean;
32
+ forkedFrom?: string;
33
+ /** Detection had the evidence it needs; the tool-path heal can skip it. */
34
+ checked: boolean;
35
+ }
36
+
37
+ /** Sources after which a brand-new instance cannot hold inherited history. */
38
+ const FRESH_SOURCES = new Set(["startup", "clear"]);
39
+
40
+ export function detectClaudeCodeFork(input: {
41
+ coordRoot: string;
42
+ transcriptPath: string | undefined;
43
+ sessionId: string;
44
+ source?: string;
45
+ /** The instance was already recorded as a fork without a parent. */
46
+ knownFork?: boolean;
47
+ isLive?: (instanceId: string) => boolean;
48
+ }): ForkDetection {
49
+ try {
50
+ const declared = input.source === "fork" || input.knownFork === true;
51
+ const transcriptReady = !!input.transcriptPath && existsSync(input.transcriptPath);
52
+ const isLive =
53
+ input.isLive ?? ((id: string) => readLiveCoordinationRow(input.coordRoot, id) !== null);
54
+
55
+ const inherited = transcriptReady
56
+ ? detectInheritedSessionParent(input.transcriptPath, input.sessionId)
57
+ : undefined;
58
+ if (inherited && inherited !== input.sessionId) {
59
+ if (declared || isLive(inherited)) {
60
+ return { fork: true, forkedFrom: inherited, checked: true };
61
+ }
62
+ return { fork: false, checked: true };
63
+ }
64
+
65
+ if (declared) {
66
+ if (!transcriptReady) return { fork: true, checked: false };
67
+ const parent = detectForkParent(input.transcriptPath, input.sessionId);
68
+ return {
69
+ fork: true,
70
+ ...(parent && parent !== input.sessionId ? { forkedFrom: parent } : {}),
71
+ checked: true,
72
+ };
73
+ }
74
+
75
+ return {
76
+ fork: false,
77
+ checked: transcriptReady || FRESH_SOURCES.has(input.source ?? ""),
78
+ };
79
+ } catch {
80
+ return { fork: false, checked: true };
81
+ }
82
+ }
@@ -356,6 +356,94 @@ const FORK_SCAN_MAX_BYTES = 128 * 1024 * 1024;
356
356
  /** How many message uuids to sample across the fork's copied prefix. */
357
357
  const FORK_SAMPLE_SIZE = 24;
358
358
 
359
+ /** Tail window scanned for inherited rows; a fork's own rows are few when the
360
+ * scan runs (session start or the first tool call), so the copied history ends
361
+ * well inside it. */
362
+ const INHERITED_TAIL_BYTES = 4 * 1024 * 1024;
363
+ const INHERITED_HEAD_BYTES = 64 * 1024;
364
+ const SESSION_ID_FIELD_RE =
365
+ /"sessionId":"([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})"/gi;
366
+
367
+ /**
368
+ * Detect inherited history from row session ids: the session whose rows were
369
+ * copied into this transcript, or undefined when every row is this session's.
370
+ *
371
+ * Empirical basis (verified 2026-10-07, Claude Code 2.1.283): the desktop
372
+ * app's "Fork from here" copies the parent's rows into the fork's transcript
373
+ * WITHOUT rewriting their `sessionId`, so the fork's file opens with thousands
374
+ * of rows stamped with the parent's id. The CLI `--fork-session` path rewrites
375
+ * the ids instead and is covered by {@link detectForkParent}.
376
+ *
377
+ * When the history is itself several generations deep, the LAST foreign row is
378
+ * the nearest ancestor, so the tail is scanned first. Whether the foreign
379
+ * session is a fork parent or a dead predecessor (a continuation) is the
380
+ * caller's call. Fail-soft: never throws, undefined on any read problem.
381
+ */
382
+ export function detectInheritedSessionParent(
383
+ transcriptPath: string | undefined,
384
+ sessionId: string,
385
+ ): string | undefined {
386
+ try {
387
+ const readablePath = resolveTranscriptPath(transcriptPath);
388
+ if (!readablePath || !sessionId) return undefined;
389
+ const size = statSync(readablePath).size;
390
+ const fromTail = lastForeignSessionId(
391
+ readRange(readablePath, Math.max(0, size - INHERITED_TAIL_BYTES), size),
392
+ sessionId,
393
+ );
394
+ if (fromTail || size <= INHERITED_TAIL_BYTES) return fromTail;
395
+ return lastForeignSessionId(
396
+ readRange(readablePath, 0, Math.min(size, INHERITED_HEAD_BYTES)),
397
+ sessionId,
398
+ );
399
+ } catch {
400
+ return undefined;
401
+ }
402
+ }
403
+
404
+ function lastForeignSessionId(text: string | undefined, sessionId: string): string | undefined {
405
+ if (!text) return undefined;
406
+ const own = sessionId.toLowerCase();
407
+ const lines = text.split("\n");
408
+ for (let i = lines.length - 1; i >= 0; i--) {
409
+ const line = lines[i]!;
410
+ if (!line.includes('"sessionId":"')) continue;
411
+ let foreign = false;
412
+ for (const m of line.matchAll(SESSION_ID_FIELD_RE)) {
413
+ if (m[1]!.toLowerCase() !== own) foreign = true;
414
+ }
415
+ if (!foreign) continue;
416
+ // Confirm on the parsed row: a nested object can carry another session's
417
+ // id, but only the row's own top-level field says whose row it is. A
418
+ // partial line at a window edge fails to parse and is skipped.
419
+ try {
420
+ const row = JSON.parse(line) as { sessionId?: unknown };
421
+ if (typeof row.sessionId === "string" && row.sessionId.toLowerCase() !== own) {
422
+ return row.sessionId;
423
+ }
424
+ } catch {
425
+ /* torn line */
426
+ }
427
+ }
428
+ return undefined;
429
+ }
430
+
431
+ function readRange(path: string, start: number, end: number): string | undefined {
432
+ try {
433
+ const fs = require("node:fs") as typeof import("node:fs");
434
+ const fd = fs.openSync(path, "r");
435
+ try {
436
+ const buf = Buffer.alloc(Math.max(0, end - start));
437
+ fs.readSync(fd, buf, 0, buf.length, start);
438
+ return buf.toString("utf8");
439
+ } finally {
440
+ fs.closeSync(fd);
441
+ }
442
+ } catch {
443
+ return undefined;
444
+ }
445
+ }
446
+
359
447
  /**
360
448
  * Detect the parent session of a forked CC conversation from its transcript.
361
449
  *
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Start markers for hook runs on adapters that kill a hook at its `timeout`
3
+ * (ADR 0197). A Codex hook that times out has its process tree killed and the
4
+ * tool proceeds without that hook's checks, and the killed process never
5
+ * reaches the completion receipt in `health.ts`. A small marker written at
6
+ * start and removed at completion leaves evidence of that case for doctor.
7
+ *
8
+ * Observer-only: every write, read, and removal swallows its own errors so a
9
+ * marker can never change hook behavior.
10
+ */
11
+
12
+ import { mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+
15
+ /** Markers older than this are pruned when a new one is written. */
16
+ const MARKER_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
17
+ /** A run is unfinished once its marker outlives the timeout by this much. */
18
+ const UNFINISHED_GRACE_MS = 30_000;
19
+
20
+ export interface HookRunMarker {
21
+ event: string;
22
+ adapter: string;
23
+ pid: number;
24
+ started_at: string;
25
+ timeout_sec: number;
26
+ }
27
+
28
+ export function hookRunMarkerDir(coordRoot: string): string {
29
+ return join(coordRoot, ".harnery", "active", "hook-runs");
30
+ }
31
+
32
+ /** Write this run's marker; returns its path, or null when it could not be written. */
33
+ export function writeHookRunMarker(
34
+ coordRoot: string,
35
+ marker: Omit<HookRunMarker, "started_at">,
36
+ now = new Date(),
37
+ ): string | null {
38
+ try {
39
+ const dir = hookRunMarkerDir(coordRoot);
40
+ mkdirSync(dir, { recursive: true });
41
+ pruneHookRunMarkers(dir, now.getTime());
42
+ const path = join(dir, `${marker.pid}-${safeName(marker.event)}.json`);
43
+ const body: HookRunMarker = { ...marker, started_at: now.toISOString() };
44
+ writeFileSync(path, `${JSON.stringify(body)}\n`);
45
+ return path;
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ export function clearHookRunMarker(path: string | null): void {
52
+ if (!path) return;
53
+ try {
54
+ rmSync(path, { force: true });
55
+ } catch {
56
+ // Observer-only.
57
+ }
58
+ }
59
+
60
+ export interface UnfinishedHookRuns {
61
+ count: number;
62
+ /** Count per event name, most frequent first. */
63
+ events: Array<{ event: string; count: number }>;
64
+ latest: string | null;
65
+ }
66
+
67
+ /**
68
+ * Markers whose run outlived its timeout plus a grace period and whose
69
+ * process is gone, within the window. A live process past its timeout is
70
+ * still running and is not counted.
71
+ */
72
+ export function findUnfinishedHookRuns(
73
+ coordRoot: string,
74
+ nowMs = Date.now(),
75
+ windowMs = 24 * 60 * 60 * 1000,
76
+ isAlive: (pid: number) => boolean = processAlive,
77
+ ): UnfinishedHookRuns {
78
+ const counts = new Map<string, number>();
79
+ let latest: string | null = null;
80
+ let count = 0;
81
+ let names: string[] = [];
82
+ try {
83
+ names = readdirSync(hookRunMarkerDir(coordRoot)).filter((n) => n.endsWith(".json"));
84
+ } catch {
85
+ return { count: 0, events: [], latest: null };
86
+ }
87
+ for (const name of names) {
88
+ let marker: HookRunMarker;
89
+ try {
90
+ marker = JSON.parse(
91
+ readFileSync(join(hookRunMarkerDir(coordRoot), name), "utf8"),
92
+ ) as HookRunMarker;
93
+ } catch {
94
+ continue;
95
+ }
96
+ const started = Date.parse(marker.started_at);
97
+ if (!Number.isFinite(started) || nowMs - started > windowMs) continue;
98
+ if (nowMs - started < marker.timeout_sec * 1000 + UNFINISHED_GRACE_MS) continue;
99
+ if (isAlive(marker.pid)) continue;
100
+ count++;
101
+ counts.set(marker.event, (counts.get(marker.event) ?? 0) + 1);
102
+ if (!latest || marker.started_at > latest) latest = marker.started_at;
103
+ }
104
+ const events = [...counts.entries()]
105
+ .map(([event, n]) => ({ event, count: n }))
106
+ .sort((a, b) => b.count - a.count || a.event.localeCompare(b.event));
107
+ return { count, events, latest };
108
+ }
109
+
110
+ function pruneHookRunMarkers(dir: string, nowMs: number): void {
111
+ for (const name of readdirSync(dir)) {
112
+ const path = join(dir, name);
113
+ try {
114
+ if (nowMs - statSync(path).mtimeMs > MARKER_RETENTION_MS) rmSync(path, { force: true });
115
+ } catch {
116
+ // Observer-only.
117
+ }
118
+ }
119
+ }
120
+
121
+ function processAlive(pid: number): boolean {
122
+ try {
123
+ process.kill(pid, 0);
124
+ return true;
125
+ } catch (err) {
126
+ return (err as NodeJS.ErrnoException).code === "EPERM";
127
+ }
128
+ }
129
+
130
+ function safeName(value: string): string {
131
+ return value.replace(/[^a-zA-Z0-9._-]+/g, "-").slice(0, 64) || "unknown";
132
+ }