harnery 0.42.0 → 0.44.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 (343) hide show
  1. package/README.md +3 -3
  2. package/dist/commander.d.ts +16 -0
  3. package/dist/commander.d.ts.map +1 -1
  4. package/dist/commander.js +18 -0
  5. package/dist/commands/agents.d.ts.map +1 -1
  6. package/dist/commands/agents.js +95 -52
  7. package/dist/commands/browse.d.ts.map +1 -1
  8. package/dist/commands/browse.js +38 -11
  9. package/dist/commands/diagnostics.d.ts.map +1 -1
  10. package/dist/commands/diagnostics.js +10 -0
  11. package/dist/commands/doctor.d.ts +12 -0
  12. package/dist/commands/doctor.d.ts.map +1 -1
  13. package/dist/commands/doctor.js +22 -7
  14. package/dist/commands/governor.js +22 -10
  15. package/dist/commands/inbox.d.ts +1 -1
  16. package/dist/commands/inbox.d.ts.map +1 -1
  17. package/dist/commands/inbox.js +55 -2
  18. package/dist/commands/rm.d.ts.map +1 -1
  19. package/dist/commands/rm.js +28 -4
  20. package/dist/commands/servers.d.ts.map +1 -1
  21. package/dist/commands/servers.js +16 -1
  22. package/dist/commands/supervisor.d.ts.map +1 -1
  23. package/dist/commands/supervisor.js +7 -2
  24. package/dist/commands/toc.d.ts +14 -0
  25. package/dist/commands/toc.d.ts.map +1 -1
  26. package/dist/commands/toc.js +7 -4
  27. package/dist/commands/tunnel.d.ts +65 -0
  28. package/dist/commands/tunnel.d.ts.map +1 -1
  29. package/dist/commands/tunnel.js +290 -101
  30. package/dist/commands/web-fetch.d.ts +3 -2
  31. package/dist/commands/web-fetch.d.ts.map +1 -1
  32. package/dist/commands/web-fetch.js +25 -19
  33. package/dist/commands/web.d.ts +7 -1
  34. package/dist/commands/web.d.ts.map +1 -1
  35. package/dist/commands/web.js +9 -3
  36. package/dist/commands/workflow.js +24 -30
  37. package/dist/core/agents/live-authority-v3.d.ts +1 -0
  38. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  39. package/dist/core/agents/live-authority-v3.js +11 -0
  40. package/dist/core/agents/live-lifecycle-v3.d.ts +4 -2
  41. package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
  42. package/dist/core/agents/live-lifecycle-v3.js +28 -7
  43. package/dist/core/agents/mailbox.d.ts +9 -8
  44. package/dist/core/agents/mailbox.d.ts.map +1 -1
  45. package/dist/core/agents/mailbox.js +24 -17
  46. package/dist/core/agents/render/prompt-context.d.ts +2 -0
  47. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  48. package/dist/core/agents/render/prompt-context.js +19 -17
  49. package/dist/core/agents/render/session-context.d.ts +2 -8
  50. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  51. package/dist/core/agents/render/session-context.js +64 -43
  52. package/dist/core/agents/rules/stop-hook.js +3 -3
  53. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  54. package/dist/core/agents/state/heartbeat-writer.js +25 -14
  55. package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
  56. package/dist/core/agents/state/live-coordination-writer.js +5 -0
  57. package/dist/core/agents/state/names.d.ts +43 -3
  58. package/dist/core/agents/state/names.d.ts.map +1 -1
  59. package/dist/core/agents/state/names.js +87 -3
  60. package/dist/core/agents/state/pidmap.d.ts.map +1 -1
  61. package/dist/core/agents/state/pidmap.js +5 -0
  62. package/dist/core/agents/state/proc-start.d.ts +19 -0
  63. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  64. package/dist/core/agents/state/proc-start.js +41 -0
  65. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  66. package/dist/core/agents/state/stale-sweep.js +180 -70
  67. package/dist/core/agents/state/unchanged-file.d.ts +44 -0
  68. package/dist/core/agents/state/unchanged-file.d.ts.map +1 -0
  69. package/dist/core/agents/state/unchanged-file.js +390 -0
  70. package/dist/core/context/index.d.ts +22 -10
  71. package/dist/core/context/index.d.ts.map +1 -1
  72. package/dist/core/context/index.js +381 -119
  73. package/dist/core/context/observation.d.ts +26 -0
  74. package/dist/core/context/observation.d.ts.map +1 -0
  75. package/dist/core/context/observation.js +11 -0
  76. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  77. package/dist/core/diagnostics/bundle.js +25 -14
  78. package/dist/core/diagnostics/path-identity.d.ts +3 -0
  79. package/dist/core/diagnostics/path-identity.d.ts.map +1 -0
  80. package/dist/core/diagnostics/path-identity.js +12 -0
  81. package/dist/core/diagnostics/sanitize.d.ts.map +1 -1
  82. package/dist/core/diagnostics/sanitize.js +4 -3
  83. package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
  84. package/dist/core/events/v3/authority-outbox.js +29 -0
  85. package/dist/core/events/v3/generation-custody.d.ts +5 -0
  86. package/dist/core/events/v3/generation-custody.d.ts.map +1 -0
  87. package/dist/core/events/v3/generation-custody.js +50 -0
  88. package/dist/core/events/v3/producers/coordination-recorder.d.ts +1 -0
  89. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  90. package/dist/core/events/v3/producers/coordination-recorder.js +7 -0
  91. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  92. package/dist/core/events/v3/producers/recorder.js +77 -64
  93. package/dist/core/events/v3/support-storage/pack-reader.d.ts.map +1 -1
  94. package/dist/core/events/v3/support-storage/pack-reader.js +42 -16
  95. package/dist/core/governor/planning.d.ts.map +1 -1
  96. package/dist/core/governor/planning.js +14 -1
  97. package/dist/core/governor/service-read.d.ts +10 -2
  98. package/dist/core/governor/service-read.d.ts.map +1 -1
  99. package/dist/core/governor/service-read.js +105 -22
  100. package/dist/core/governor/service.d.ts +20 -2
  101. package/dist/core/governor/service.d.ts.map +1 -1
  102. package/dist/core/governor/service.js +72 -47
  103. package/dist/core/hooks/cli.js +120 -23
  104. package/dist/core/hooks/fork-lineage.d.ts +40 -0
  105. package/dist/core/hooks/fork-lineage.d.ts.map +1 -0
  106. package/dist/core/hooks/fork-lineage.js +62 -0
  107. package/dist/core/hooks/resolve/transcript.d.ts +16 -0
  108. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  109. package/dist/core/hooks/resolve/transcript.js +84 -0
  110. package/dist/core/hooks/shell-rm.d.ts +2 -0
  111. package/dist/core/hooks/shell-rm.d.ts.map +1 -0
  112. package/dist/core/hooks/shell-rm.js +105 -0
  113. package/dist/core/hooks/shell-tokens.d.ts +12 -0
  114. package/dist/core/hooks/shell-tokens.d.ts.map +1 -0
  115. package/dist/core/hooks/shell-tokens.js +97 -0
  116. package/dist/core/hooks/shell-waiter.d.ts.map +1 -1
  117. package/dist/core/hooks/shell-waiter.js +3 -93
  118. package/dist/core/inbox/contract.d.ts +2 -1
  119. package/dist/core/inbox/contract.d.ts.map +1 -1
  120. package/dist/core/inbox/index.d.ts +2 -0
  121. package/dist/core/inbox/index.d.ts.map +1 -1
  122. package/dist/core/inbox/index.js +2 -0
  123. package/dist/core/inbox/peer-messages.d.ts +37 -0
  124. package/dist/core/inbox/peer-messages.d.ts.map +1 -0
  125. package/dist/core/inbox/peer-messages.js +72 -0
  126. package/dist/core/inbox/service.d.ts +8 -1
  127. package/dist/core/inbox/service.d.ts.map +1 -1
  128. package/dist/core/inbox/service.js +67 -21
  129. package/dist/core/inbox/targets.d.ts +22 -0
  130. package/dist/core/inbox/targets.d.ts.map +1 -0
  131. package/dist/core/inbox/targets.js +108 -0
  132. package/dist/core/inbox/watcher.js +7 -3
  133. package/dist/core/journal/index.d.ts.map +1 -1
  134. package/dist/core/journal/index.js +17 -2
  135. package/dist/core/resources/contract.d.ts +3 -0
  136. package/dist/core/resources/contract.d.ts.map +1 -1
  137. package/dist/core/resources/service-status.d.ts +14 -1
  138. package/dist/core/resources/service-status.d.ts.map +1 -1
  139. package/dist/core/resources/service-status.js +23 -13
  140. package/dist/core/resources/service.d.ts +9 -1
  141. package/dist/core/resources/service.d.ts.map +1 -1
  142. package/dist/core/resources/service.js +47 -27
  143. package/dist/core/resources/status.d.ts +2 -5
  144. package/dist/core/resources/status.d.ts.map +1 -1
  145. package/dist/core/resources/status.js +36 -7
  146. package/dist/core/semantic/contract.d.ts +5 -5
  147. package/dist/core/servers/index.d.ts +24 -13
  148. package/dist/core/servers/index.d.ts.map +1 -1
  149. package/dist/core/servers/index.js +132 -92
  150. package/dist/core/servers/tunnels.d.ts +3 -5
  151. package/dist/core/servers/tunnels.d.ts.map +1 -1
  152. package/dist/core/servers/tunnels.js +3 -9
  153. package/dist/core/storage/process-log.d.ts +12 -0
  154. package/dist/core/storage/process-log.d.ts.map +1 -1
  155. package/dist/core/storage/process-log.js +10 -2
  156. package/dist/core/supervisor/contract.d.ts +12 -0
  157. package/dist/core/supervisor/contract.d.ts.map +1 -1
  158. package/dist/core/supervisor/explanations.d.ts.map +1 -1
  159. package/dist/core/supervisor/explanations.js +2 -1
  160. package/dist/core/supervisor/findings.d.ts.map +1 -1
  161. package/dist/core/supervisor/findings.js +54 -9
  162. package/dist/core/supervisor/service.d.ts +9 -1
  163. package/dist/core/supervisor/service.d.ts.map +1 -1
  164. package/dist/core/supervisor/service.js +59 -29
  165. package/dist/core/supervisor/services.d.ts.map +1 -1
  166. package/dist/core/supervisor/services.js +22 -1
  167. package/dist/core/supervisor/status.d.ts +17 -2
  168. package/dist/core/supervisor/status.d.ts.map +1 -1
  169. package/dist/core/supervisor/status.js +70 -12
  170. package/dist/core/supervisor/timeline.d.ts.map +1 -1
  171. package/dist/core/supervisor/timeline.js +2 -1
  172. package/dist/core/work/state.js +7 -7
  173. package/dist/core/workflow/engine.d.ts.map +1 -1
  174. package/dist/core/workflow/engine.js +105 -32
  175. package/dist/core/workflow/failure-evidence.d.ts +16 -0
  176. package/dist/core/workflow/failure-evidence.d.ts.map +1 -0
  177. package/dist/core/workflow/failure-evidence.js +114 -0
  178. package/dist/core/workflow/index.d.ts +1 -1
  179. package/dist/core/workflow/index.d.ts.map +1 -1
  180. package/dist/core/workflow/index.js +1 -1
  181. package/dist/core/workflow/live-session-v3.d.ts +6 -0
  182. package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
  183. package/dist/core/workflow/live-session-v3.js +126 -5
  184. package/dist/core/workflow/proof.d.ts +5 -5
  185. package/dist/core/workflow/proof.d.ts.map +1 -1
  186. package/dist/core/workflow/proof.js +115 -20
  187. package/dist/core/workflow/run-state.d.ts +3 -1
  188. package/dist/core/workflow/run-state.d.ts.map +1 -1
  189. package/dist/core/workflow/run-state.js +31 -27
  190. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  191. package/dist/core/workflow/spawn-claude.js +19 -0
  192. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  193. package/dist/core/workflow/spawn-codex.js +13 -0
  194. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  195. package/dist/core/workflow/spawn-cursor.js +19 -0
  196. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  197. package/dist/core/workflow/spawn-opencode.js +14 -0
  198. package/dist/core/workflow/types.d.ts +64 -17
  199. package/dist/core/workflow/types.d.ts.map +1 -1
  200. package/dist/core/workflow/workspaces/index.d.ts +0 -1
  201. package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
  202. package/dist/core/workflow/workspaces/index.js +0 -1
  203. package/dist/core/workflow/workspaces/leases.d.ts +3 -1
  204. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  205. package/dist/core/workflow/workspaces/leases.js +30 -16
  206. package/dist/core/workflow/workspaces/local-git.d.ts +6 -2
  207. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  208. package/dist/core/workflow/workspaces/local-git.js +270 -6
  209. package/dist/core/workflow/workspaces/ownership.d.ts +11 -0
  210. package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -0
  211. package/dist/core/workflow/workspaces/ownership.js +194 -0
  212. package/dist/core/workflow/workspaces/reclaim.d.ts +35 -0
  213. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  214. package/dist/core/workflow/workspaces/reclaim.js +14 -0
  215. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  216. package/dist/core/workflow/workspaces/recovery.js +21 -13
  217. package/dist/core/workflow/workspaces/state.d.ts +1 -0
  218. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  219. package/dist/core/workflow/workspaces/state.js +21 -1
  220. package/dist/lib/browser/windows-chrome.d.ts +100 -12
  221. package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
  222. package/dist/lib/browser/windows-chrome.js +262 -73
  223. package/dist/lib/docs-links.d.ts.map +1 -1
  224. package/dist/lib/docs-links.js +45 -2
  225. package/dist/lib/guarded-remove.d.ts +7 -0
  226. package/dist/lib/guarded-remove.d.ts.map +1 -1
  227. package/dist/lib/guarded-remove.js +106 -12
  228. package/dist/lib/identities/assume.d.ts.map +1 -1
  229. package/dist/lib/identities/assume.js +4 -0
  230. package/dist/lib/tunnel/allowlist.d.ts +44 -0
  231. package/dist/lib/tunnel/allowlist.d.ts.map +1 -0
  232. package/dist/lib/tunnel/allowlist.js +204 -0
  233. package/dist/lib/tunnel/current-address.d.ts +41 -0
  234. package/dist/lib/tunnel/current-address.d.ts.map +1 -0
  235. package/dist/lib/tunnel/current-address.js +123 -0
  236. package/dist/lib/tunnel/gate.js +13 -6
  237. package/dist/lib/tunnel/processes.d.ts +39 -0
  238. package/dist/lib/tunnel/processes.d.ts.map +1 -0
  239. package/dist/lib/tunnel/processes.js +121 -0
  240. package/dist/lib/tunnel/state.d.ts +28 -3
  241. package/dist/lib/tunnel/state.d.ts.map +1 -1
  242. package/dist/lib/tunnel/state.js +98 -27
  243. package/dist/lib/zip/index.d.ts +41 -0
  244. package/dist/lib/zip/index.d.ts.map +1 -0
  245. package/dist/lib/zip/index.js +398 -0
  246. package/package.json +8 -1
  247. package/src/commander.ts +32 -0
  248. package/src/commands/agents.ts +151 -74
  249. package/src/commands/browse.ts +50 -14
  250. package/src/commands/diagnostics.ts +10 -0
  251. package/src/commands/doctor.ts +26 -7
  252. package/src/commands/governor.ts +23 -10
  253. package/src/commands/inbox.ts +52 -2
  254. package/src/commands/rm.ts +43 -4
  255. package/src/commands/servers.ts +16 -4
  256. package/src/commands/supervisor.ts +7 -2
  257. package/src/commands/toc.ts +14 -6
  258. package/src/commands/tunnel.ts +394 -116
  259. package/src/commands/web-fetch.ts +30 -19
  260. package/src/commands/web.ts +9 -3
  261. package/src/commands/workflow.ts +30 -30
  262. package/src/core/agents/live-authority-v3.ts +17 -0
  263. package/src/core/agents/live-lifecycle-v3.ts +52 -14
  264. package/src/core/agents/mailbox.ts +26 -17
  265. package/src/core/agents/render/prompt-context.ts +35 -20
  266. package/src/core/agents/render/session-context.ts +92 -48
  267. package/src/core/agents/rules/stop-hook.ts +3 -3
  268. package/src/core/agents/state/heartbeat-writer.ts +29 -12
  269. package/src/core/agents/state/live-coordination-writer.ts +29 -0
  270. package/src/core/agents/state/names.ts +117 -6
  271. package/src/core/agents/state/pidmap.ts +13 -0
  272. package/src/core/agents/state/proc-start.ts +57 -0
  273. package/src/core/agents/state/stale-sweep.ts +247 -82
  274. package/src/core/agents/state/unchanged-file.ts +456 -0
  275. package/src/core/context/index.ts +446 -136
  276. package/src/core/context/observation.ts +29 -0
  277. package/src/core/diagnostics/bundle.ts +23 -18
  278. package/src/core/diagnostics/path-identity.ts +15 -0
  279. package/src/core/diagnostics/sanitize.ts +4 -3
  280. package/src/core/events/v3/authority-outbox.ts +56 -0
  281. package/src/core/events/v3/generation-custody.ts +59 -0
  282. package/src/core/events/v3/producers/coordination-recorder.ts +15 -0
  283. package/src/core/events/v3/producers/recorder.ts +94 -68
  284. package/src/core/events/v3/support-storage/pack-reader.ts +45 -19
  285. package/src/core/governor/planning.ts +20 -8
  286. package/src/core/governor/service-read.ts +128 -20
  287. package/src/core/governor/service.ts +119 -42
  288. package/src/core/hooks/cli.ts +139 -22
  289. package/src/core/hooks/fork-lineage.ts +82 -0
  290. package/src/core/hooks/resolve/transcript.ts +88 -0
  291. package/src/core/hooks/shell-rm.ts +103 -0
  292. package/src/core/hooks/shell-tokens.ts +96 -0
  293. package/src/core/hooks/shell-waiter.ts +4 -91
  294. package/src/core/inbox/contract.ts +2 -1
  295. package/src/core/inbox/index.ts +2 -0
  296. package/src/core/inbox/peer-messages.ts +94 -0
  297. package/src/core/inbox/service.ts +88 -22
  298. package/src/core/inbox/targets.ts +148 -0
  299. package/src/core/inbox/watcher.ts +8 -9
  300. package/src/core/journal/index.ts +19 -2
  301. package/src/core/resources/contract.ts +4 -0
  302. package/src/core/resources/service-status.ts +35 -12
  303. package/src/core/resources/service.ts +71 -24
  304. package/src/core/resources/status.ts +57 -8
  305. package/src/core/servers/index.ts +177 -91
  306. package/src/core/servers/tunnels.ts +3 -11
  307. package/src/core/storage/process-log.ts +17 -2
  308. package/src/core/supervisor/contract.ts +13 -0
  309. package/src/core/supervisor/explanations.ts +4 -1
  310. package/src/core/supervisor/findings.ts +68 -10
  311. package/src/core/supervisor/service.ts +88 -26
  312. package/src/core/supervisor/services.ts +26 -8
  313. package/src/core/supervisor/status.ts +89 -12
  314. package/src/core/supervisor/timeline.ts +2 -1
  315. package/src/core/work/state.ts +7 -7
  316. package/src/core/workflow/engine.ts +131 -49
  317. package/src/core/workflow/failure-evidence.ts +141 -0
  318. package/src/core/workflow/index.ts +0 -1
  319. package/src/core/workflow/live-session-v3.ts +146 -5
  320. package/src/core/workflow/proof.ts +134 -26
  321. package/src/core/workflow/run-state.ts +31 -29
  322. package/src/core/workflow/spawn-claude.ts +18 -0
  323. package/src/core/workflow/spawn-codex.ts +13 -0
  324. package/src/core/workflow/spawn-cursor.ts +18 -0
  325. package/src/core/workflow/spawn-opencode.ts +14 -0
  326. package/src/core/workflow/types.ts +51 -17
  327. package/src/core/workflow/workspaces/index.ts +0 -1
  328. package/src/core/workflow/workspaces/leases.ts +48 -17
  329. package/src/core/workflow/workspaces/local-git.ts +315 -5
  330. package/src/core/workflow/workspaces/ownership.ts +232 -0
  331. package/src/core/workflow/workspaces/reclaim.ts +58 -0
  332. package/src/core/workflow/workspaces/recovery.ts +25 -13
  333. package/src/core/workflow/workspaces/state.ts +20 -0
  334. package/src/lib/browser/windows-chrome.ts +346 -86
  335. package/src/lib/docs-links.ts +49 -2
  336. package/src/lib/guarded-remove.ts +132 -14
  337. package/src/lib/identities/assume.ts +4 -0
  338. package/src/lib/tunnel/allowlist.ts +208 -0
  339. package/src/lib/tunnel/current-address.ts +158 -0
  340. package/src/lib/tunnel/gate.ts +16 -8
  341. package/src/lib/tunnel/processes.ts +149 -0
  342. package/src/lib/tunnel/state.ts +123 -27
  343. package/src/lib/zip/index.ts +483 -0
@@ -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,
@@ -115,6 +121,7 @@ import {
115
121
  soundForEvent,
116
122
  } from "./effects/index.ts";
117
123
  import { toolInputHash, toolTargetHash } from "./events/input-hash.ts";
124
+ import { detectClaudeCodeFork } from "./fork-lineage.ts";
118
125
  import { canonicalize } from "./guard-path.ts";
119
126
  import {
120
127
  beginHookHealth,
@@ -135,6 +142,7 @@ import {
135
142
  } from "./resolve/transcript.ts";
136
143
  import { clearHookRunMarker, writeHookRunMarker } from "./run-markers.ts";
137
144
  import { sessionNamePresence } from "./session-name-presence.ts";
145
+ import { shellRmReason } from "./shell-rm.ts";
138
146
  import { shellWaiterReason } from "./shell-waiter.ts";
139
147
 
140
148
  interface Argv {
@@ -250,12 +258,14 @@ function assignNameInProcess(
250
258
  coordRoot: string,
251
259
  instanceId: string,
252
260
  kind: "session" | "subagent" | "transient",
253
- forkedFrom?: string,
261
+ fork?: { forkedFrom?: string; fork?: boolean; forkChecked?: boolean },
254
262
  ): { name: string; kind: string } | null {
255
263
  try {
256
264
  const name = assignName(coordRoot, instanceId, kind, {
257
265
  freshnessSecs: coordFreshnessSeconds(coordRoot),
258
- ...(forkedFrom ? { forkedFrom } : {}),
266
+ ...(fork?.forkedFrom ? { forkedFrom: fork.forkedFrom } : {}),
267
+ ...(fork?.fork ? { fork: true } : {}),
268
+ ...(fork?.forkChecked ? { forkChecked: true } : {}),
259
269
  });
260
270
  return { name, kind };
261
271
  } catch {
@@ -263,6 +273,66 @@ function assignNameInProcess(
263
273
  }
264
274
  }
265
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
+
266
336
  /** Mid-flight sessions can emit tools before SessionStart assigns a pool name.
267
337
  * Stamp the disposable cache so Codec does not render the 8-character id. */
268
338
  function ensureSessionDisplayName(
@@ -270,13 +340,14 @@ function ensureSessionDisplayName(
270
340
  instanceId: string,
271
341
  sessionId: string,
272
342
  adapter: Adapter,
343
+ payload: ParsedPayload | null,
273
344
  ): void {
274
345
  if (coordEnv("WORKFLOW_CHILD") === "1") return;
275
346
  const row =
276
347
  readLiveCoordinationRow(coordRoot, instanceId) ?? readHeartbeat(coordRoot, instanceId);
277
348
  if (row?.kind === "subagent" || row?.kind === "transient" || row?.workflow_run_id) return;
278
349
  if (row?.name?.trim()) return;
279
- const assigned = assignNameInProcess(coordRoot, instanceId, "session");
350
+ const assigned = assignSessionNameInProcess(coordRoot, instanceId, payload, adapter);
280
351
  if (!assigned?.name) return;
281
352
  if (!readHeartbeat(coordRoot, instanceId)) {
282
353
  try {
@@ -345,22 +416,22 @@ function buildEventData(
345
416
  // The platform label is the adapter id itself; enumerating and defaulting
346
417
  // to codex silently mislabels any adapter not in the list.
347
418
  const adapterPlatform = ctx.adapter;
348
- // Recorded fork lineage is NOT detected here. On claude-code a fork
349
- // never fires its own session.started — SessionStart fires under the
350
- // PARENT's session id (source=resume) before the fork id is minted
351
- // (verified 2026-08-05) — so detection at this point can only mislabel
352
- // the resumed parent. The fork's new instance is caught by the
353
- // tool.requested heal path instead. The forkedFrom plumbing below stays
354
- // for adapters that DO report a parent at session start.
355
- const forkedFrom: string | undefined = undefined;
356
419
  // Assign (or recover) name + kind in-process. Idempotent: resume
357
- // returns the original name; new owner consumes a counter slot.
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.
358
426
  // Workflow children stay unnamed, as on the tool path: a pool name
359
427
  // would make them the addressee of mail held for that name.
360
428
  const assigned =
361
429
  coordEnv("WORKFLOW_CHILD") === "1"
362
430
  ? undefined
363
- : assignNameInProcess(ctx.coordRoot, ctx.instanceId, "session", forkedFrom);
431
+ : assignSessionNameInProcess(ctx.coordRoot, ctx.instanceId, p, ctx.adapter);
432
+ const forkedFrom = assigned
433
+ ? readForkParent(ctx.coordRoot, ctx.instanceId)?.instance_id
434
+ : undefined;
364
435
  // Write the adapter pid-map row so `harn agents whoami` ppid-walks find
365
436
  // this owner. Prefer the payload pid (the actual claude binary), then the
366
437
  // anchor walk (the `node` ancestor for Cursor, which has no payload pid),
@@ -429,7 +500,7 @@ function buildEventData(
429
500
  coordEnv("WORKFLOW_CHILD") !== "1" &&
430
501
  !readLiveCoordinationRow(ctx.coordRoot, ctx.instanceId)?.name
431
502
  ) {
432
- assignNameInProcess(ctx.coordRoot, ctx.instanceId, "session");
503
+ assignSessionNameInProcess(ctx.coordRoot, ctx.instanceId, p, ctx.adapter);
433
504
  }
434
505
  const prompt = p?.prompt ?? "";
435
506
  const { value, truncated } = clampString(prompt, 4000);
@@ -851,7 +922,7 @@ async function main(): Promise<number> {
851
922
  !priorCoordination?.workflow_run_id &&
852
923
  !priorCoordination?.name?.trim()
853
924
  ) {
854
- assignNameInProcess(coordRoot, owner.instance_id, "session");
925
+ assignSessionNameInProcess(coordRoot, owner.instance_id, payload, adapter);
855
926
  }
856
927
 
857
928
  const data = buildEventData(norm.event_type, {
@@ -1063,11 +1134,18 @@ function recordSignal(run: HookRun): void {
1063
1134
  }
1064
1135
  if (norm.event_type !== "session.ended") {
1065
1136
  try {
1066
- ensureSessionDisplayName(coordRoot, owner.instance_id, sessionId, adapter);
1137
+ ensureSessionDisplayName(coordRoot, owner.instance_id, sessionId, adapter, payload);
1067
1138
  } catch (error) {
1068
1139
  logError(coordRoot, error, { phase: "session-display-name" });
1069
1140
  }
1070
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
+ }
1071
1149
  const v3EventId =
1072
1150
  v3Result && "event" in v3Result
1073
1151
  ? v3Result.event.event_id
@@ -1559,6 +1637,17 @@ async function handleToolRequested(run: HookRun): Promise<number | undefined> {
1559
1637
  return 0;
1560
1638
  }
1561
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
+
1562
1651
  // G-guard for ALL adapters. Claude Code previously ran this via a
1563
1652
  // pre-tool-use bash adapter (which called `agent-coord verdict --rule=claim`);
1564
1653
  // that adapter is now deleted, so agent-hook owns the deny for every adapter.
@@ -2209,12 +2298,16 @@ async function emitUserPromptSubmitSystemMessage(
2209
2298
  if (coordEnv("WORKFLOW_CHILD") === "1") return false;
2210
2299
  const behavior = adapterBehavior(adapter);
2211
2300
  const { renderPromptContext } = await import("../agents/render/prompt-context.ts");
2301
+ let peerMessageIds: readonly string[] = [];
2212
2302
  let additionalContext = renderPromptContext({
2213
2303
  coordRoot,
2214
2304
  instanceId,
2215
2305
  sessionId,
2216
2306
  sessionNameNudge: true,
2217
2307
  ...behavior.promptContextNudges,
2308
+ onInboxPrepared: (ids) => {
2309
+ peerMessageIds = ids;
2310
+ },
2218
2311
  }).trim();
2219
2312
  const fileLinkContext = behavior.fileLinkContext?.(coordRoot, workspaceCwd) ?? "";
2220
2313
  if (fileLinkContext) {
@@ -2229,7 +2322,12 @@ async function emitUserPromptSubmitSystemMessage(
2229
2322
  if (!additionalContext) return false;
2230
2323
 
2231
2324
  const { emitContext } = await import("./adapter/output.ts");
2232
- return emitContext(adapter, "UserPromptSubmit", additionalContext);
2325
+ const emitted = emitContext(adapter, "UserPromptSubmit", additionalContext);
2326
+ if (emitted) {
2327
+ const { acknowledgeEmittedPeerInbox } = await import("../inbox/peer-messages.ts");
2328
+ await acknowledgeEmittedPeerInbox(coordRoot, instanceId, peerMessageIds);
2329
+ }
2330
+ return emitted;
2233
2331
  }
2234
2332
 
2235
2333
  async function emitSubagentStartContext(
@@ -2253,19 +2351,26 @@ async function emitSubagentStartContext(
2253
2351
  // is around. Reuse prompt-context (which dedups against the per-owner hash);
2254
2352
  // first call will always emit.
2255
2353
  let combined = message;
2354
+ let peerMessageIds: readonly string[] = [];
2256
2355
  const { renderPromptContext } = await import("../agents/render/prompt-context.ts");
2257
2356
  const context = renderPromptContext({
2258
2357
  coordRoot,
2259
2358
  instanceId,
2260
2359
  sessionId,
2261
2360
  agentName: subagentName,
2361
+ onInboxPrepared: (ids) => {
2362
+ peerMessageIds = ids;
2363
+ },
2262
2364
  }).trim();
2263
2365
  if (context) combined = `${message}\n\n${context}`;
2264
2366
 
2265
2367
  // Use SubagentStart event-name in CC's hookSpecificOutput shape; cursor's
2266
2368
  // flat `additional_context` works the same way.
2267
2369
  const { emitContext } = await import("./adapter/output.ts");
2268
- emitContext(adapter, "SubagentStart", combined);
2370
+ if (emitContext(adapter, "SubagentStart", combined)) {
2371
+ const { acknowledgeEmittedPeerInbox } = await import("../inbox/peer-messages.ts");
2372
+ await acknowledgeEmittedPeerInbox(coordRoot, instanceId, peerMessageIds);
2373
+ }
2269
2374
  }
2270
2375
 
2271
2376
  function adapterPlatform(adapter: Adapter): string {
@@ -2284,6 +2389,7 @@ async function emitSessionStartSystemMessage(
2284
2389
  const behavior = adapterBehavior(adapter);
2285
2390
  const workflowChild = coordEnv("WORKFLOW_CHILD") === "1";
2286
2391
  let additionalContext = "";
2392
+ let peerMessageIds: readonly string[] = [];
2287
2393
  // Opportunistic reconciliation makes normal session starts a failsafe for
2288
2394
  // archive, idle, cascade, and host lifecycle observations. It also runs the
2289
2395
  // stale cache sweep through the shared ADR 0077 composition.
@@ -2303,6 +2409,9 @@ async function emitSessionStartSystemMessage(
2303
2409
  sessionId,
2304
2410
  agentName: agentName || undefined,
2305
2411
  platformLabel: behavior.sessionStartPlatformLabel,
2412
+ onInboxPrepared: (ids) => {
2413
+ peerMessageIds = ids;
2414
+ },
2306
2415
  }).trim();
2307
2416
  }
2308
2417
 
@@ -2331,7 +2440,12 @@ async function emitSessionStartSystemMessage(
2331
2440
  }
2332
2441
  }
2333
2442
  const { emitContext } = await import("./adapter/output.ts");
2334
- return emitContext(adapter, "SessionStart", additionalContext);
2443
+ const emitted = emitContext(adapter, "SessionStart", additionalContext);
2444
+ if (emitted) {
2445
+ const { acknowledgeEmittedPeerInbox } = await import("../inbox/peer-messages.ts");
2446
+ await acknowledgeEmittedPeerInbox(coordRoot, instanceId, peerMessageIds);
2447
+ }
2448
+ return emitted;
2335
2449
  }
2336
2450
 
2337
2451
  function completeRecoveryInjection(
@@ -2340,8 +2454,11 @@ function completeRecoveryInjection(
2340
2454
  sessionId: string,
2341
2455
  recovery: PreparedContextRecovery,
2342
2456
  ): void {
2343
- completeContextRecovery(coordRoot, { sessionId, instanceId });
2344
- void recovery;
2457
+ completeContextRecovery(coordRoot, {
2458
+ sessionId,
2459
+ instanceId,
2460
+ checkpoint: recovery.checkpoint,
2461
+ });
2345
2462
  }
2346
2463
 
2347
2464
  main()
@@ -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,103 @@
1
+ import { basename, shellCommandFromTool, tokenize } from "./shell-tokens.ts";
2
+
3
+ /**
4
+ * Deny a shell `rm` and name the guarded replacement. The instructions have
5
+ * said "use <bin> rm instead of shell rm" since the command shipped, and
6
+ * agents still reached for bare rm several times a day, often on unquoted
7
+ * variable paths that Claude Code then stops on for a human prompt. A
8
+ * repeated, machine-detectable failure belongs in a gate, not more prose.
9
+ *
10
+ * This is a guardrail against the reflex, not a security boundary: it reads
11
+ * command positions only (so `git rm`, `docker rm`, and quoted examples pass),
12
+ * plus the `xargs rm`, `find -exec rm`, and `bash -c '...rm...'` shapes.
13
+ * HARNERY_ALLOW_RM=1 on the command is the deliberate override.
14
+ */
15
+
16
+ const OVERRIDE = "HARNERY_ALLOW_RM=1";
17
+ const SEPARATORS = new Set([";", "|", "||", "&", "&&", "(", ")", "{", "}"]);
18
+ const KEYWORDS = new Set(["do", "if", "then", "elif", "else", "!", "while", "until", "time"]);
19
+ const WRAPPERS = new Set(["command", "exec", "builtin", "nohup", "sudo", "doas", "env", "nice"]);
20
+ // xargs options that consume the following token as their value.
21
+ const XARGS_VALUE_FLAGS = new Set(["-n", "-I", "-L", "-P", "-d", "-E", "-s", "-a"]);
22
+
23
+ function isRm(value: string, quoted: boolean): boolean {
24
+ return !quoted && basename(value) === "rm";
25
+ }
26
+
27
+ function invokesRm(source: string, depth = 0): boolean {
28
+ if (depth > 8) return false;
29
+ const tokens = tokenize(source);
30
+ if (tokens.some((t) => !t.quoted && t.value === OVERRIDE)) return false;
31
+ let start = true;
32
+ let afterWrapper = false;
33
+ for (let i = 0; i < tokens.length; i++) {
34
+ const token = tokens[i]!;
35
+ if (token.operator) {
36
+ if (SEPARATORS.has(token.value)) {
37
+ start = true;
38
+ afterWrapper = false;
39
+ }
40
+ continue;
41
+ }
42
+ if (!start) continue;
43
+ if (!token.quoted) {
44
+ if (KEYWORDS.has(token.value)) continue;
45
+ if (/^[A-Za-z_][A-Za-z_0-9]*=/.test(token.value)) continue;
46
+ if (afterWrapper && token.value.startsWith("-")) continue;
47
+ }
48
+ const name = basename(token.value);
49
+ if (WRAPPERS.has(name)) {
50
+ afterWrapper = true;
51
+ continue;
52
+ }
53
+ start = false;
54
+ afterWrapper = false;
55
+ if (isRm(token.value, token.quoted)) return true;
56
+ if (name === "xargs") {
57
+ for (let j = i + 1; j < tokens.length && !tokens[j]!.operator; j++) {
58
+ const arg = tokens[j]!;
59
+ if (arg.value.startsWith("-")) {
60
+ if (XARGS_VALUE_FLAGS.has(arg.value)) j++;
61
+ continue;
62
+ }
63
+ if (isRm(arg.value, arg.quoted)) return true;
64
+ break;
65
+ }
66
+ }
67
+ if (name === "find") {
68
+ for (let j = i + 1; j < tokens.length && !tokens[j]!.operator; j++) {
69
+ if (!/^-(?:exec|execdir|ok|okdir)$/.test(tokens[j]!.value)) continue;
70
+ const next = tokens[j + 1];
71
+ if (next && !next.operator && isRm(next.value, next.quoted)) return true;
72
+ }
73
+ }
74
+ if (["bash", "sh", "dash", "zsh", "ksh"].includes(name)) {
75
+ for (let j = i + 1; j < tokens.length && !tokens[j]!.operator; j++) {
76
+ if (/^-[a-z]*c[a-z]*$/.test(tokens[j]!.value)) {
77
+ const script = tokens[j + 1];
78
+ if (script && !script.operator && invokesRm(script.value, depth + 1)) return true;
79
+ break;
80
+ }
81
+ }
82
+ }
83
+ }
84
+ return false;
85
+ }
86
+
87
+ export function shellRmReason(
88
+ toolName: unknown,
89
+ toolInput: unknown,
90
+ binName: string,
91
+ ): string | null {
92
+ const command = shellCommandFromTool(toolName, toolInput);
93
+ if (command === null || !invokesRm(command)) return null;
94
+ return (
95
+ `Shell rm blocked: it skips the resolved-path, Git-ownership, and peer-claim checks, and an ` +
96
+ `unquoted variable path can expand to the wrong target. Use the guarded paths instead: ` +
97
+ `\`${binName} rm --root <directory> <paths...>\` previews removal of untracked files (add ` +
98
+ `--yes to delete, -r for a directory); \`git rm\` removes tracked source; ` +
99
+ `\`${binName} artifacts discard <id>\` retires managed evidence. To replace a file, overwrite ` +
100
+ `it in place or \`mv\` the new file over the old one, so nothing needs deleting. If none of ` +
101
+ `these fit, prefix the command with ${OVERRIDE}.`
102
+ );
103
+ }