harnery 0.43.0 → 0.45.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 (323) hide show
  1. package/README.md +3 -3
  2. package/dist/commander.js +1 -0
  3. package/dist/commands/agents.d.ts.map +1 -1
  4. package/dist/commands/agents.js +41 -48
  5. package/dist/commands/browse.d.ts.map +1 -1
  6. package/dist/commands/browse.js +38 -11
  7. package/dist/commands/diagnostics.d.ts.map +1 -1
  8. package/dist/commands/diagnostics.js +10 -0
  9. package/dist/commands/doctor.d.ts +12 -0
  10. package/dist/commands/doctor.d.ts.map +1 -1
  11. package/dist/commands/doctor.js +22 -7
  12. package/dist/commands/files.d.ts +2 -2
  13. package/dist/commands/files.d.ts.map +1 -1
  14. package/dist/commands/files.js +7 -4
  15. package/dist/commands/governor.js +22 -10
  16. package/dist/commands/inbox.d.ts +1 -1
  17. package/dist/commands/inbox.d.ts.map +1 -1
  18. package/dist/commands/inbox.js +55 -2
  19. package/dist/commands/rm.d.ts.map +1 -1
  20. package/dist/commands/rm.js +28 -4
  21. package/dist/commands/servers.d.ts.map +1 -1
  22. package/dist/commands/servers.js +26 -17
  23. package/dist/commands/supervisor.d.ts.map +1 -1
  24. package/dist/commands/supervisor.js +7 -2
  25. package/dist/commands/toc.d.ts +14 -0
  26. package/dist/commands/toc.d.ts.map +1 -1
  27. package/dist/commands/toc.js +7 -4
  28. package/dist/commands/tunnel.d.ts +15 -0
  29. package/dist/commands/tunnel.d.ts.map +1 -1
  30. package/dist/commands/tunnel.js +453 -153
  31. package/dist/commands/turn-rollback.d.ts +5 -0
  32. package/dist/commands/turn-rollback.d.ts.map +1 -0
  33. package/dist/commands/turn-rollback.js +76 -0
  34. package/dist/commands/workflow.d.ts.map +1 -1
  35. package/dist/commands/workflow.js +68 -30
  36. package/dist/core/agents/coord-client.d.ts.map +1 -1
  37. package/dist/core/agents/coord-client.js +18 -0
  38. package/dist/core/agents/live-authority-v3.d.ts +1 -0
  39. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  40. package/dist/core/agents/live-authority-v3.js +11 -0
  41. package/dist/core/agents/live-lifecycle-v3.d.ts +4 -2
  42. package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
  43. package/dist/core/agents/live-lifecycle-v3.js +28 -7
  44. package/dist/core/agents/mailbox.d.ts +9 -8
  45. package/dist/core/agents/mailbox.d.ts.map +1 -1
  46. package/dist/core/agents/mailbox.js +24 -17
  47. package/dist/core/agents/render/prompt-context.d.ts +2 -0
  48. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  49. package/dist/core/agents/render/prompt-context.js +8 -12
  50. package/dist/core/agents/render/session-context.d.ts +2 -8
  51. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  52. package/dist/core/agents/render/session-context.js +7 -30
  53. package/dist/core/agents/rules/stop-hook.js +3 -3
  54. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  55. package/dist/core/agents/state/heartbeat-writer.js +25 -14
  56. package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
  57. package/dist/core/agents/state/live-coordination-writer.js +5 -0
  58. package/dist/core/agents/state/pidmap.d.ts.map +1 -1
  59. package/dist/core/agents/state/pidmap.js +5 -0
  60. package/dist/core/agents/state/proc-start.d.ts +19 -1
  61. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  62. package/dist/core/agents/state/proc-start.js +46 -1
  63. package/dist/core/agents/state/process-handle.d.ts +26 -0
  64. package/dist/core/agents/state/process-handle.d.ts.map +1 -0
  65. package/dist/core/agents/state/process-handle.js +121 -0
  66. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  67. package/dist/core/agents/state/stale-sweep.js +180 -70
  68. package/dist/core/agents/state/unchanged-file.d.ts +44 -0
  69. package/dist/core/agents/state/unchanged-file.d.ts.map +1 -0
  70. package/dist/core/agents/state/unchanged-file.js +390 -0
  71. package/dist/core/artifacts/delivery-card.d.ts.map +1 -1
  72. package/dist/core/artifacts/delivery-card.js +2 -1
  73. package/dist/core/context/index.d.ts +22 -10
  74. package/dist/core/context/index.d.ts.map +1 -1
  75. package/dist/core/context/index.js +381 -119
  76. package/dist/core/context/observation.d.ts +26 -0
  77. package/dist/core/context/observation.d.ts.map +1 -0
  78. package/dist/core/context/observation.js +11 -0
  79. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  80. package/dist/core/diagnostics/bundle.js +25 -14
  81. package/dist/core/diagnostics/path-identity.d.ts +3 -0
  82. package/dist/core/diagnostics/path-identity.d.ts.map +1 -0
  83. package/dist/core/diagnostics/path-identity.js +12 -0
  84. package/dist/core/diagnostics/sanitize.d.ts.map +1 -1
  85. package/dist/core/diagnostics/sanitize.js +4 -3
  86. package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
  87. package/dist/core/events/v3/authority-outbox.js +29 -0
  88. package/dist/core/events/v3/generation-custody.d.ts +5 -0
  89. package/dist/core/events/v3/generation-custody.d.ts.map +1 -0
  90. package/dist/core/events/v3/generation-custody.js +50 -0
  91. package/dist/core/events/v3/producers/coordination-recorder.d.ts +1 -0
  92. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  93. package/dist/core/events/v3/producers/coordination-recorder.js +7 -0
  94. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  95. package/dist/core/events/v3/producers/recorder.js +77 -64
  96. package/dist/core/governor/planning.d.ts.map +1 -1
  97. package/dist/core/governor/planning.js +14 -1
  98. package/dist/core/governor/service-read.d.ts +10 -2
  99. package/dist/core/governor/service-read.d.ts.map +1 -1
  100. package/dist/core/governor/service-read.js +105 -22
  101. package/dist/core/governor/service.d.ts +20 -2
  102. package/dist/core/governor/service.d.ts.map +1 -1
  103. package/dist/core/governor/service.js +72 -47
  104. package/dist/core/hooks/cli.js +33 -5
  105. package/dist/core/inbox/contract.d.ts +2 -1
  106. package/dist/core/inbox/contract.d.ts.map +1 -1
  107. package/dist/core/inbox/index.d.ts +2 -0
  108. package/dist/core/inbox/index.d.ts.map +1 -1
  109. package/dist/core/inbox/index.js +2 -0
  110. package/dist/core/inbox/peer-messages.d.ts +37 -0
  111. package/dist/core/inbox/peer-messages.d.ts.map +1 -0
  112. package/dist/core/inbox/peer-messages.js +72 -0
  113. package/dist/core/inbox/service.d.ts +8 -1
  114. package/dist/core/inbox/service.d.ts.map +1 -1
  115. package/dist/core/inbox/service.js +67 -21
  116. package/dist/core/inbox/targets.d.ts +22 -0
  117. package/dist/core/inbox/targets.d.ts.map +1 -0
  118. package/dist/core/inbox/targets.js +108 -0
  119. package/dist/core/inbox/watcher.js +7 -3
  120. package/dist/core/journal/index.d.ts.map +1 -1
  121. package/dist/core/journal/index.js +17 -2
  122. package/dist/core/resources/contract.d.ts +3 -0
  123. package/dist/core/resources/contract.d.ts.map +1 -1
  124. package/dist/core/resources/service-status.d.ts +14 -1
  125. package/dist/core/resources/service-status.d.ts.map +1 -1
  126. package/dist/core/resources/service-status.js +23 -13
  127. package/dist/core/resources/service.d.ts +7 -13
  128. package/dist/core/resources/service.d.ts.map +1 -1
  129. package/dist/core/resources/service.js +34 -91
  130. package/dist/core/resources/status.d.ts +2 -5
  131. package/dist/core/resources/status.d.ts.map +1 -1
  132. package/dist/core/resources/status.js +36 -7
  133. package/dist/core/semantic/service.d.ts.map +1 -1
  134. package/dist/core/semantic/service.js +2 -9
  135. package/dist/core/servers/index.d.ts +34 -22
  136. package/dist/core/servers/index.d.ts.map +1 -1
  137. package/dist/core/servers/index.js +297 -144
  138. package/dist/core/servers/tunnels.d.ts +7 -7
  139. package/dist/core/servers/tunnels.d.ts.map +1 -1
  140. package/dist/core/servers/tunnels.js +47 -10
  141. package/dist/core/supervisor/contract.d.ts +12 -0
  142. package/dist/core/supervisor/contract.d.ts.map +1 -1
  143. package/dist/core/supervisor/explanations.d.ts.map +1 -1
  144. package/dist/core/supervisor/explanations.js +2 -1
  145. package/dist/core/supervisor/findings.d.ts.map +1 -1
  146. package/dist/core/supervisor/findings.js +54 -9
  147. package/dist/core/supervisor/service.d.ts +7 -1
  148. package/dist/core/supervisor/service.d.ts.map +1 -1
  149. package/dist/core/supervisor/service.js +59 -29
  150. package/dist/core/supervisor/services.d.ts.map +1 -1
  151. package/dist/core/supervisor/services.js +22 -1
  152. package/dist/core/supervisor/status.d.ts +17 -2
  153. package/dist/core/supervisor/status.d.ts.map +1 -1
  154. package/dist/core/supervisor/status.js +70 -12
  155. package/dist/core/supervisor/timeline.d.ts.map +1 -1
  156. package/dist/core/supervisor/timeline.js +2 -1
  157. package/dist/core/turn-rollback/content.d.ts +53 -0
  158. package/dist/core/turn-rollback/content.d.ts.map +1 -0
  159. package/dist/core/turn-rollback/content.js +396 -0
  160. package/dist/core/turn-rollback/index.d.ts +72 -0
  161. package/dist/core/turn-rollback/index.d.ts.map +1 -0
  162. package/dist/core/turn-rollback/index.js +443 -0
  163. package/dist/core/work/state.js +7 -7
  164. package/dist/core/workflow/engine.d.ts.map +1 -1
  165. package/dist/core/workflow/engine.js +105 -32
  166. package/dist/core/workflow/failure-evidence.d.ts +16 -0
  167. package/dist/core/workflow/failure-evidence.d.ts.map +1 -0
  168. package/dist/core/workflow/failure-evidence.js +114 -0
  169. package/dist/core/workflow/index.d.ts +5 -3
  170. package/dist/core/workflow/index.d.ts.map +1 -1
  171. package/dist/core/workflow/index.js +3 -2
  172. package/dist/core/workflow/live-session-v3.d.ts +6 -0
  173. package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
  174. package/dist/core/workflow/live-session-v3.js +126 -5
  175. package/dist/core/workflow/proof.d.ts +5 -5
  176. package/dist/core/workflow/proof.d.ts.map +1 -1
  177. package/dist/core/workflow/proof.js +115 -20
  178. package/dist/core/workflow/recovery-audit.d.ts +30 -0
  179. package/dist/core/workflow/recovery-audit.d.ts.map +1 -0
  180. package/dist/core/workflow/recovery-audit.js +116 -0
  181. package/dist/core/workflow/run-state.d.ts +20 -1
  182. package/dist/core/workflow/run-state.d.ts.map +1 -1
  183. package/dist/core/workflow/run-state.js +141 -32
  184. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  185. package/dist/core/workflow/spawn-claude.js +19 -0
  186. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  187. package/dist/core/workflow/spawn-codex.js +13 -0
  188. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  189. package/dist/core/workflow/spawn-cursor.js +19 -0
  190. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  191. package/dist/core/workflow/spawn-opencode.js +14 -0
  192. package/dist/core/workflow/types.d.ts +64 -17
  193. package/dist/core/workflow/types.d.ts.map +1 -1
  194. package/dist/core/workflow/workspaces/index.d.ts +0 -1
  195. package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
  196. package/dist/core/workflow/workspaces/index.js +0 -1
  197. package/dist/core/workflow/workspaces/leases.d.ts +3 -1
  198. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  199. package/dist/core/workflow/workspaces/leases.js +30 -16
  200. package/dist/core/workflow/workspaces/local-git.d.ts +6 -2
  201. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  202. package/dist/core/workflow/workspaces/local-git.js +328 -8
  203. package/dist/core/workflow/workspaces/ownership.d.ts +11 -0
  204. package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -0
  205. package/dist/core/workflow/workspaces/ownership.js +194 -0
  206. package/dist/core/workflow/workspaces/reclaim.d.ts +40 -0
  207. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  208. package/dist/core/workflow/workspaces/reclaim.js +14 -0
  209. package/dist/core/workflow/workspaces/recovery.d.ts +2 -0
  210. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  211. package/dist/core/workflow/workspaces/recovery.js +42 -10
  212. package/dist/core/workflow/workspaces/state.d.ts +1 -0
  213. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  214. package/dist/core/workflow/workspaces/state.js +21 -1
  215. package/dist/lib/browser/windows-chrome.d.ts +100 -12
  216. package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
  217. package/dist/lib/browser/windows-chrome.js +262 -73
  218. package/dist/lib/docs-links.d.ts.map +1 -1
  219. package/dist/lib/docs-links.js +45 -2
  220. package/dist/lib/guarded-remove.d.ts +7 -0
  221. package/dist/lib/guarded-remove.d.ts.map +1 -1
  222. package/dist/lib/guarded-remove.js +106 -12
  223. package/dist/lib/identities/assume.d.ts.map +1 -1
  224. package/dist/lib/identities/assume.js +4 -0
  225. package/dist/lib/tunnel/state.d.ts +13 -6
  226. package/dist/lib/tunnel/state.d.ts.map +1 -1
  227. package/dist/lib/tunnel/state.js +58 -12
  228. package/dist/lib/zip/index.d.ts +41 -0
  229. package/dist/lib/zip/index.d.ts.map +1 -0
  230. package/dist/lib/zip/index.js +398 -0
  231. package/package.json +12 -1
  232. package/src/commander.ts +10 -0
  233. package/src/commands/agents.ts +54 -63
  234. package/src/commands/browse.ts +50 -14
  235. package/src/commands/diagnostics.ts +10 -0
  236. package/src/commands/doctor.ts +26 -7
  237. package/src/commands/files.ts +13 -5
  238. package/src/commands/governor.ts +23 -10
  239. package/src/commands/inbox.ts +52 -2
  240. package/src/commands/rm.ts +43 -4
  241. package/src/commands/servers.ts +27 -26
  242. package/src/commands/supervisor.ts +7 -2
  243. package/src/commands/toc.ts +14 -6
  244. package/src/commands/tunnel.ts +533 -166
  245. package/src/commands/turn-rollback.ts +111 -0
  246. package/src/commands/workflow.ts +89 -30
  247. package/src/core/agents/coord-client.ts +20 -0
  248. package/src/core/agents/live-authority-v3.ts +17 -0
  249. package/src/core/agents/live-lifecycle-v3.ts +52 -14
  250. package/src/core/agents/mailbox.ts +26 -17
  251. package/src/core/agents/render/prompt-context.ts +10 -12
  252. package/src/core/agents/render/session-context.ts +9 -32
  253. package/src/core/agents/rules/stop-hook.ts +3 -3
  254. package/src/core/agents/state/heartbeat-writer.ts +29 -12
  255. package/src/core/agents/state/live-coordination-writer.ts +29 -0
  256. package/src/core/agents/state/pidmap.ts +13 -0
  257. package/src/core/agents/state/proc-start.ts +62 -2
  258. package/src/core/agents/state/process-handle.ts +165 -0
  259. package/src/core/agents/state/stale-sweep.ts +247 -82
  260. package/src/core/agents/state/unchanged-file.ts +456 -0
  261. package/src/core/artifacts/delivery-card.ts +7 -1
  262. package/src/core/context/index.ts +446 -136
  263. package/src/core/context/observation.ts +29 -0
  264. package/src/core/diagnostics/bundle.ts +23 -18
  265. package/src/core/diagnostics/path-identity.ts +15 -0
  266. package/src/core/diagnostics/sanitize.ts +4 -3
  267. package/src/core/events/v3/authority-outbox.ts +56 -0
  268. package/src/core/events/v3/generation-custody.ts +59 -0
  269. package/src/core/events/v3/producers/coordination-recorder.ts +15 -0
  270. package/src/core/events/v3/producers/recorder.ts +94 -68
  271. package/src/core/governor/planning.ts +20 -8
  272. package/src/core/governor/service-read.ts +128 -20
  273. package/src/core/governor/service.ts +119 -42
  274. package/src/core/hooks/cli.ts +33 -5
  275. package/src/core/inbox/contract.ts +2 -1
  276. package/src/core/inbox/index.ts +2 -0
  277. package/src/core/inbox/peer-messages.ts +94 -0
  278. package/src/core/inbox/service.ts +88 -22
  279. package/src/core/inbox/targets.ts +148 -0
  280. package/src/core/inbox/watcher.ts +8 -9
  281. package/src/core/journal/index.ts +19 -2
  282. package/src/core/resources/contract.ts +4 -0
  283. package/src/core/resources/service-status.ts +35 -12
  284. package/src/core/resources/service.ts +57 -95
  285. package/src/core/resources/status.ts +57 -8
  286. package/src/core/semantic/service.ts +2 -8
  287. package/src/core/servers/index.ts +376 -141
  288. package/src/core/servers/tunnels.ts +57 -13
  289. package/src/core/supervisor/contract.ts +13 -0
  290. package/src/core/supervisor/explanations.ts +4 -1
  291. package/src/core/supervisor/findings.ts +68 -10
  292. package/src/core/supervisor/service.ts +87 -26
  293. package/src/core/supervisor/services.ts +26 -8
  294. package/src/core/supervisor/status.ts +89 -12
  295. package/src/core/supervisor/timeline.ts +2 -1
  296. package/src/core/turn-rollback/content.ts +499 -0
  297. package/src/core/turn-rollback/index.ts +580 -0
  298. package/src/core/work/state.ts +7 -7
  299. package/src/core/workflow/engine.ts +131 -49
  300. package/src/core/workflow/failure-evidence.ts +141 -0
  301. package/src/core/workflow/index.ts +12 -2
  302. package/src/core/workflow/live-session-v3.ts +146 -5
  303. package/src/core/workflow/proof.ts +134 -26
  304. package/src/core/workflow/recovery-audit.ts +181 -0
  305. package/src/core/workflow/run-state.ts +165 -34
  306. package/src/core/workflow/spawn-claude.ts +18 -0
  307. package/src/core/workflow/spawn-codex.ts +13 -0
  308. package/src/core/workflow/spawn-cursor.ts +18 -0
  309. package/src/core/workflow/spawn-opencode.ts +14 -0
  310. package/src/core/workflow/types.ts +51 -17
  311. package/src/core/workflow/workspaces/index.ts +0 -1
  312. package/src/core/workflow/workspaces/leases.ts +48 -17
  313. package/src/core/workflow/workspaces/local-git.ts +388 -7
  314. package/src/core/workflow/workspaces/ownership.ts +232 -0
  315. package/src/core/workflow/workspaces/reclaim.ts +62 -0
  316. package/src/core/workflow/workspaces/recovery.ts +45 -10
  317. package/src/core/workflow/workspaces/state.ts +20 -0
  318. package/src/lib/browser/windows-chrome.ts +346 -86
  319. package/src/lib/docs-links.ts +49 -2
  320. package/src/lib/guarded-remove.ts +132 -14
  321. package/src/lib/identities/assume.ts +4 -0
  322. package/src/lib/tunnel/state.ts +70 -15
  323. package/src/lib/zip/index.ts +483 -0
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { existsSync, readFileSync, statSync } from "node:fs";
3
- import { join } from "node:path";
4
- import type { RepoSnapshot } from "../context/index.ts";
3
+ import { isAbsolute, join } from "node:path";
4
+ import { observationsComparable, type RepoSnapshot } from "../context/observation.ts";
5
5
  import { DIAGNOSTIC_ADVICE_SCHEMA_VERSION } from "../diagnostics/contract.ts";
6
6
  import type {
7
7
  NormalizedPolicy,
@@ -12,6 +12,7 @@ import type {
12
12
  import { policyDigest } from "../policy/index.ts";
13
13
  import { isCanonicalWorkflowAttemptContext } from "./attempt-context.ts";
14
14
  import { stableDigest, writeImmutableJson } from "./durable-record.ts";
15
+ import { failureEvidenceAllowsUncharged, isSpawnFailureEvidence } from "./failure-evidence.ts";
15
16
  import { readWorkflowRunManifest } from "./run-state.ts";
16
17
  import type {
17
18
  AcceptanceCriterion,
@@ -242,17 +243,25 @@ export function digestResult(value: unknown, kind?: "text" | "json"): ResultDige
242
243
  * attempt was informative about the work — charge it. This also keeps a
243
244
  * resumed run whose earlier segment did real work from being written off by
244
245
  * a later environment failure.
245
- * 3. Otherwise, among the failed agents, environment wins over upstream: a
246
- * missing binary means nothing ran at all, and it is the operator-chosen
247
- * hard stop.
246
+ * 3. Every participant must have failed with positive no-effect evidence,
247
+ * observed absent output and no refusal. A class alone is insufficient.
248
+ * Among those eligible failures, environment is the hard stop.
248
249
  * 4. Anything else is undefined ⇒ a charged work failure, exactly as today.
249
250
  */
250
251
  export function deriveRunFailureClass(
251
252
  status: "succeeded" | "failed",
252
- agents: readonly Pick<WorkflowAgentProof, "status" | "class">[],
253
+ agents: readonly Pick<WorkflowAgentProof, "status" | "class" | "failure_evidence">[],
253
254
  ): SpawnFailureClass | undefined {
254
255
  if (status !== "failed") return undefined;
255
- if (agents.some((agent) => agent.status === "succeeded" || agent.status === "cached")) {
256
+ if (
257
+ agents.length === 0 ||
258
+ agents.some(
259
+ (agent) =>
260
+ agent.status !== "failed" ||
261
+ (agent.class !== "environment" && agent.class !== "upstream") ||
262
+ !failureEvidenceAllowsUncharged(agent.failure_evidence),
263
+ )
264
+ ) {
256
265
  return undefined;
257
266
  }
258
267
  const failed = agents.filter((agent) => agent.status === "failed");
@@ -385,6 +394,13 @@ export function readWorkflowProof(coordRoot: string, runId: string): WorkflowPro
385
394
  proof.run.class !== "environment" &&
386
395
  proof.run.class !== "upstream" &&
387
396
  proof.run.class !== "decision") ||
397
+ !Array.isArray(proof.agents) ||
398
+ proof.agents.some(
399
+ (agent) =>
400
+ !agent ||
401
+ typeof agent !== "object" ||
402
+ (agent.failure_evidence !== undefined && !isSpawnFailureEvidence(agent.failure_evidence)),
403
+ ) ||
388
404
  (proof.run.work_context !== undefined &&
389
405
  (!proof.run.work_item_id ||
390
406
  proof.run.work_context.id !== proof.run.work_item_id ||
@@ -399,6 +415,22 @@ export function readWorkflowProof(coordRoot: string, runId: string): WorkflowPro
399
415
  ) {
400
416
  throw new Error(`workflow proof at ${path} has an unsupported or mismatched schema`);
401
417
  }
418
+ if (
419
+ repoComparisonProblem(proof.repository.before, proof.repository.after) !== undefined ||
420
+ proof.repository.before.dirty_paths_truncated ||
421
+ proof.repository.after.dirty_paths_truncated ||
422
+ !proof.repository.before.branch?.trim() ||
423
+ !proof.repository.after.branch?.trim() ||
424
+ !proof.repository.before.head?.trim() ||
425
+ !proof.repository.after.head?.trim()
426
+ ) {
427
+ proof.repository = buildRepoEvidence(proof.repository.before, proof.repository.after);
428
+ proof.unknowns = proof.unknowns.filter((item) => item.code !== "repository_drift_incomplete");
429
+ proof.unknowns.push({
430
+ code: "repository_drift_incomplete",
431
+ message: proof.repository.drift.note!,
432
+ });
433
+ }
402
434
  const manifestPath = join(coordRoot, ".harnery", "workflows", runId, "run.json");
403
435
  if (proof.execution === undefined && !existsSync(manifestPath)) return proof;
404
436
  const manifest = readWorkflowRunManifest(coordRoot, runId);
@@ -512,13 +544,31 @@ export function renderWorkflowProof(proof: WorkflowProof): string {
512
544
  : "admission: shadow not needed; no real child dispatch",
513
545
  );
514
546
  }
515
- const repo = proof.repository;
547
+ const comparable =
548
+ repoComparisonProblem(proof.repository.before, proof.repository.after) === undefined;
549
+ const repo =
550
+ comparable &&
551
+ !proof.repository.before.dirty_paths_truncated &&
552
+ !proof.repository.after.dirty_paths_truncated &&
553
+ proof.repository.before.branch?.trim() &&
554
+ proof.repository.after.branch?.trim() &&
555
+ proof.repository.before.head?.trim() &&
556
+ proof.repository.after.head?.trim()
557
+ ? proof.repository
558
+ : buildRepoEvidence(proof.repository.before, proof.repository.after);
516
559
  const drift = repo.drift;
517
- lines.push(
518
- `repository: branch ${repo.before.branch ?? "unknown"} -> ${repo.after.branch ?? "unknown"}; ` +
519
- `HEAD ${short(repo.before.head)} -> ${short(repo.after.head)}; ` +
520
- `${drift.dirty_paths_added.length} dirty added, ${drift.dirty_paths_cleared.length} cleared`,
521
- );
560
+ if (!comparable) {
561
+ lines.push(`repository: drift unknown. ${drift.note}`);
562
+ } else {
563
+ const paths =
564
+ drift.dirty_paths_added === null || drift.dirty_paths_cleared === null
565
+ ? "dirty-path drift unknown"
566
+ : `${drift.dirty_paths_added.length} dirty added, ${drift.dirty_paths_cleared.length} cleared`;
567
+ lines.push(
568
+ `repository: branch ${repo.before.branch ?? "unknown"} -> ${repo.after.branch ?? "unknown"}; ` +
569
+ `HEAD ${short(repo.before.head)} -> ${short(repo.after.head)}; ${paths}`,
570
+ );
571
+ }
522
572
  if (proof.unknowns.length > 0) {
523
573
  lines.push(`unknowns: ${proof.unknowns.length}`);
524
574
  for (const unknown of proof.unknowns) lines.push(` - ${unknown.message}`);
@@ -530,26 +580,56 @@ export function renderWorkflowProof(proof: WorkflowProof): string {
530
580
  function buildRepoEvidence(beforeRaw: RepoSnapshot, afterRaw: RepoSnapshot): WorkflowRepoEvidence {
531
581
  const before = normalizeRepoSnapshot(beforeRaw);
532
582
  const after = normalizeRepoSnapshot(afterRaw);
583
+ const problem = repoComparisonProblem(before, after);
584
+ if (problem !== undefined) {
585
+ return {
586
+ source: "engine",
587
+ before,
588
+ after,
589
+ drift: {
590
+ branch_changed: null,
591
+ head_changed: null,
592
+ dirty_paths_added: null,
593
+ dirty_paths_cleared: null,
594
+ dirty_paths_retained: null,
595
+ incomplete: true,
596
+ note: `Repository drift could not be verified: ${problem}. Re-check before editing.`,
597
+ },
598
+ };
599
+ }
600
+ const pathsComparable = !before.dirty_paths_truncated && !after.dirty_paths_truncated;
601
+ const branchKnown = Boolean(before.branch && after.branch);
602
+ const headKnown = Boolean(before.head && after.head);
533
603
  const beforeDirty = new Set(before.dirty_paths);
534
604
  const afterDirty = new Set(after.dirty_paths);
535
605
  const retained = after.dirty_paths.filter((path) => beforeDirty.has(path));
536
- const incomplete = Boolean(
537
- before.dirty_paths_truncated || after.dirty_paths_truncated || retained.length > 0,
538
- );
606
+ const notes = [
607
+ ...(!pathsComparable
608
+ ? ["Dirty-path drift is unknown because a snapshot list was truncated."]
609
+ : []),
610
+ ...(!branchKnown ? ["Branch drift is unknown because a snapshot has no branch."] : []),
611
+ ...(!headKnown ? ["HEAD drift is unknown because a snapshot has no HEAD."] : []),
612
+ ...(pathsComparable && retained.length > 0
613
+ ? ["Snapshots cannot prove whether retained dirty paths changed during the run."]
614
+ : []),
615
+ ];
616
+ const incomplete = notes.length > 0;
539
617
  return {
540
618
  source: "engine",
541
619
  before,
542
620
  after,
543
621
  drift: {
544
- branch_changed: before.branch !== after.branch,
545
- head_changed: before.head !== after.head,
546
- dirty_paths_added: after.dirty_paths.filter((path) => !beforeDirty.has(path)),
547
- dirty_paths_cleared: before.dirty_paths.filter((path) => !afterDirty.has(path)),
548
- dirty_paths_retained: retained,
622
+ branch_changed: branchKnown ? before.branch !== after.branch : null,
623
+ head_changed: headKnown ? before.head !== after.head : null,
624
+ dirty_paths_added: pathsComparable
625
+ ? after.dirty_paths.filter((path) => !beforeDirty.has(path))
626
+ : null,
627
+ dirty_paths_cleared: pathsComparable
628
+ ? before.dirty_paths.filter((path) => !afterDirty.has(path))
629
+ : null,
630
+ dirty_paths_retained: pathsComparable ? retained : null,
549
631
  incomplete,
550
- note: incomplete
551
- ? "Snapshots cannot prove whether retained dirty paths changed during the run, and truncated lists may omit paths."
552
- : undefined,
632
+ note: incomplete ? notes.join(" ") : undefined,
553
633
  },
554
634
  };
555
635
  }
@@ -557,14 +637,42 @@ function buildRepoEvidence(beforeRaw: RepoSnapshot, afterRaw: RepoSnapshot): Wor
557
637
  function normalizeRepoSnapshot(snapshot: RepoSnapshot): WorkflowRepoSnapshot {
558
638
  return {
559
639
  cwd: snapshot.cwd,
640
+ observation: snapshot.observation ?? "unavailable",
641
+ observation_detail: snapshot.observation
642
+ ? clippedOptional(snapshot.observation_detail, 500)
643
+ : "No repository observation was recorded.",
560
644
  root: snapshot.root,
561
- branch: snapshot.branch,
562
- head: snapshot.head,
645
+ branch: snapshot.branch?.trim() || undefined,
646
+ head: snapshot.head?.trim() || undefined,
563
647
  dirty_paths: snapshot.dirty_paths,
564
648
  dirty_paths_truncated: snapshot.dirty_paths_truncated,
565
649
  };
566
650
  }
567
651
 
652
+ function snapshotFailure(label: string, snapshot: WorkflowRepoSnapshot): string {
653
+ return `${label} snapshot is ${snapshot.observation}${snapshot.observation_detail ? ` (${snapshot.observation_detail})` : ""}`;
654
+ }
655
+
656
+ function repoComparisonProblem(
657
+ before: WorkflowRepoSnapshot,
658
+ after: WorkflowRepoSnapshot,
659
+ ): string | undefined {
660
+ if (!observationsComparable(before, after)) {
661
+ return [
662
+ ...(before.observation !== "ok" ? [snapshotFailure("before", before)] : []),
663
+ ...(after.observation !== "ok" ? [snapshotFailure("after", after)] : []),
664
+ ].join("; ");
665
+ }
666
+ const unknownRoots = [
667
+ ...(!before.root || !isAbsolute(before.root) ? ["before"] : []),
668
+ ...(!after.root || !isAbsolute(after.root) ? ["after"] : []),
669
+ ];
670
+ if (unknownRoots.length > 0)
671
+ return `${unknownRoots.join(" and ")} snapshot repository root is unknown`;
672
+ if (before.root !== after.root) return "snapshots name different repository roots";
673
+ return undefined;
674
+ }
675
+
568
676
  function buildAdapterCoverage(
569
677
  agents: WorkflowAgentProof[],
570
678
  claims: Readonly<Record<string, AdapterEvidenceCapability | undefined>> | undefined,
@@ -0,0 +1,181 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync, lstatSync, mkdirSync, readdirSync } from "node:fs";
3
+ import { hostname } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { stateDirMode } from "../storage/modes.ts";
6
+ import { assertWorkflowRunId } from "./approvals.ts";
7
+ import {
8
+ fsyncParentDirectory,
9
+ readJsonRecord,
10
+ stableDigest,
11
+ writeImmutableJson,
12
+ } from "./durable-record.ts";
13
+
14
+ export interface WorkflowRecoveryAuthorization {
15
+ actor: string;
16
+ reason: string;
17
+ }
18
+
19
+ export interface WorkflowRecoveryIntent extends WorkflowRecoveryAuthorization {
20
+ schema_version: 1;
21
+ operation_id: string;
22
+ run_id: string;
23
+ action: "reclaim_unknown_liveness" | "unlock_resume";
24
+ target_sha256: string;
25
+ target: Record<string, unknown>;
26
+ recorded_at: string;
27
+ pid: number;
28
+ host: string;
29
+ outcome: "unknown";
30
+ }
31
+
32
+ export interface WorkflowRecoveryRecord {
33
+ intent: WorkflowRecoveryIntent;
34
+ outcome: "unknown" | "completed";
35
+ }
36
+
37
+ export function validateWorkflowRecoveryAuthorization(
38
+ authorization: WorkflowRecoveryAuthorization,
39
+ ): void {
40
+ for (const [field, limit] of [
41
+ ["actor", 200],
42
+ ["reason", 2_000],
43
+ ] as const) {
44
+ const value = authorization[field];
45
+ if (
46
+ typeof value !== "string" ||
47
+ !value.trim() ||
48
+ value.length > limit ||
49
+ Array.from(value).some(
50
+ (character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127,
51
+ )
52
+ ) {
53
+ throw new Error(
54
+ `workflow recovery requires a nonempty ${field} of at most ${limit} characters without control characters`,
55
+ );
56
+ }
57
+ }
58
+ }
59
+
60
+ /** Canonical recovery ledger: immutable intent is durable before effects.
61
+ * Completion cites that exact intent; absence always means unknown. */
62
+ export function beginWorkflowRecovery(
63
+ coordRoot: string,
64
+ input: Omit<
65
+ WorkflowRecoveryIntent,
66
+ "schema_version" | "operation_id" | "recorded_at" | "pid" | "host" | "outcome"
67
+ >,
68
+ ): { intent: WorkflowRecoveryIntent; assertCurrent(): void; complete(): void } {
69
+ assertWorkflowRunId(input.run_id);
70
+ validateWorkflowRecoveryAuthorization(input);
71
+ if (!/^[a-f0-9]{64}$/.test(input.target_sha256))
72
+ throw new Error("workflow recovery requires an exact preview digest");
73
+ const intent: WorkflowRecoveryIntent = {
74
+ ...input,
75
+ schema_version: 1,
76
+ operation_id: `recovery-${randomUUID()}`,
77
+ recorded_at: new Date().toISOString(),
78
+ pid: process.pid,
79
+ host: hostname(),
80
+ outcome: "unknown",
81
+ };
82
+ const directory = join(
83
+ coordRoot,
84
+ ".harnery",
85
+ "workflows",
86
+ input.run_id,
87
+ "recovery",
88
+ intent.operation_id,
89
+ );
90
+ // Sync each newly published directory name too. Syncing only the intent's
91
+ // immediate parent cannot make a newly created ancestor durable.
92
+ const runDirectory = join(coordRoot, ".harnery", "workflows", input.run_id);
93
+ ensureDurableDirectory(runDirectory);
94
+ const ledgerDirectory = join(runDirectory, "recovery");
95
+ ensureDurableDirectory(ledgerDirectory);
96
+ fsyncParentDirectory(ledgerDirectory);
97
+ mkdirSync(directory, { mode: stateDirMode() });
98
+ fsyncParentDirectory(directory);
99
+ const intentPath = join(directory, "intent.json");
100
+ writeImmutableJson(intentPath, intent);
101
+ const assertCurrent = () => {
102
+ if (stableDigest(readJsonRecord(intentPath, "recovery intent")) !== stableDigest(intent))
103
+ throw new Error("workflow recovery audit authority changed");
104
+ };
105
+ return {
106
+ intent,
107
+ assertCurrent,
108
+ complete: () => {
109
+ assertCurrent();
110
+ writeImmutableJson(join(directory, "completion.json"), {
111
+ schema_version: 1,
112
+ operation_id: intent.operation_id,
113
+ intent_sha256: stableDigest(intent),
114
+ outcome: "completed",
115
+ recorded_at: new Date().toISOString(),
116
+ });
117
+ },
118
+ };
119
+ }
120
+
121
+ function ensureDurableDirectory(path: string): void {
122
+ if (existsSync(path)) {
123
+ if (!lstatSync(path).isDirectory())
124
+ throw new Error("workflow recovery ledger is not a directory");
125
+ return;
126
+ }
127
+ ensureDurableDirectory(dirname(path));
128
+ try {
129
+ mkdirSync(path, { mode: stateDirMode() });
130
+ } catch (error) {
131
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST" || !lstatSync(path).isDirectory())
132
+ throw error;
133
+ }
134
+ fsyncParentDirectory(path);
135
+ }
136
+
137
+ export function readWorkflowRecoveryLedger(
138
+ coordRoot: string,
139
+ runId: string,
140
+ ): WorkflowRecoveryRecord[] {
141
+ assertWorkflowRunId(runId);
142
+ const directory = join(coordRoot, ".harnery", "workflows", runId, "recovery");
143
+ if (!existsSync(directory)) return [];
144
+ return readdirSync(directory)
145
+ .sort()
146
+ .map((entry) => {
147
+ if (!/^recovery-[a-f0-9-]{36}$/.test(entry))
148
+ throw new Error("invalid workflow recovery ledger entry");
149
+ const intent = readJsonRecord<WorkflowRecoveryIntent>(
150
+ join(directory, entry, "intent.json"),
151
+ "recovery intent",
152
+ );
153
+ if (
154
+ intent.schema_version !== 1 ||
155
+ intent.operation_id !== entry ||
156
+ intent.run_id !== runId ||
157
+ intent.outcome !== "unknown" ||
158
+ !["reclaim_unknown_liveness", "unlock_resume"].includes(intent.action) ||
159
+ !/^[a-f0-9]{64}$/.test(intent.target_sha256)
160
+ ) {
161
+ throw new Error("invalid workflow recovery intent authority");
162
+ }
163
+ validateWorkflowRecoveryAuthorization(intent);
164
+ const completionPath = join(directory, entry, "completion.json");
165
+ if (!existsSync(completionPath)) return { intent, outcome: "unknown" };
166
+ const completion = readJsonRecord<{
167
+ schema_version: number;
168
+ operation_id: string;
169
+ intent_sha256: string;
170
+ outcome: string;
171
+ }>(completionPath, "recovery completion");
172
+ if (
173
+ completion.schema_version !== 1 ||
174
+ completion.operation_id !== entry ||
175
+ completion.intent_sha256 !== stableDigest(intent) ||
176
+ completion.outcome !== "completed"
177
+ )
178
+ throw new Error("invalid workflow recovery completion authority");
179
+ return { intent, outcome: "completed" };
180
+ });
181
+ }