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
@@ -7,13 +7,21 @@
7
7
  import { spawnSync } from "node:child_process";
8
8
  import { createHash, randomUUID } from "node:crypto";
9
9
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
10
- import { dirname, join } from "node:path";
10
+ import { dirname, isAbsolute, join } from "node:path";
11
11
  import { readLiveCoordinationRow } from "../agents/state/live-coordination-view.js";
12
+ import { resolveBinName } from "../config.js";
13
+ import { peerInbox } from "../inbox/peer-messages.js";
14
+ import { project } from "../inbox/service.js";
12
15
  import { stateFileMode } from "../storage/modes.js";
16
+ import { acquireNoClobberLease } from "../workflow/workspaces/leases.js";
17
+ import { observationsComparable } from "./observation.js";
18
+ export { observationsComparable } from "./observation.js";
13
19
  export const CONTEXT_SCHEMA_VERSION = 1;
14
20
  export const MAX_CAPSULE_BYTES = 32 * 1024;
15
21
  const MAX_SNAPSHOT_PATHS = 15;
16
22
  const MAX_SNAPSHOT_PATH_CHARS = 500;
23
+ const MAX_OBSERVATION_DETAIL_CHARS = 500;
24
+ const MAX_RECOVERY_INPUT_BODY_BYTES = 160;
17
25
  /** Parse the context-window shapes exposed by current adapter hook/status data. */
18
26
  export function extractContextSample(payload, options) {
19
27
  const contextValue = payload.context_window;
@@ -62,19 +70,26 @@ export function extractContextSample(payload, options) {
62
70
  }
63
71
  /** Persist a latest context sample. Identical measurements are a no-op. */
64
72
  export function recordContextSample(coordRoot, instanceId, sample) {
65
- const current = readContextState(coordRoot, sample.session_id);
66
- const state = current ?? newState({ sessionId: sample.session_id, instanceId, now: sample.observed_at });
67
- const changed = !sameMeasurement(state.latest_context, sample);
68
- if (!changed)
69
- return { state, changed: false };
70
- const next = {
71
- ...state,
72
- instance_id: instanceId,
73
- latest_context: sample,
74
- updated_at: sample.observed_at,
75
- };
76
- writeState(coordRoot, next);
77
- return { state: next, changed: true };
73
+ return withContextMutation(coordRoot, sample.session_id, () => {
74
+ const current = readContextState(coordRoot, sample.session_id);
75
+ const state = current?.instance_id === instanceId
76
+ ? current
77
+ : {
78
+ ...newState({ sessionId: sample.session_id, instanceId, now: sample.observed_at }),
79
+ generation: current?.generation ?? 0,
80
+ };
81
+ const changed = !sameMeasurement(state.latest_context, sample);
82
+ if (!changed)
83
+ return { state, changed: false };
84
+ const next = {
85
+ ...state,
86
+ instance_id: instanceId,
87
+ latest_context: sample,
88
+ updated_at: sample.observed_at,
89
+ };
90
+ writeState(coordRoot, next);
91
+ return { state: next, changed: true };
92
+ });
78
93
  }
79
94
  /**
80
95
  * Snapshot external work state. Repeated PreCompact events reuse the existing
@@ -82,109 +97,213 @@ export function recordContextSample(coordRoot, instanceId, sample) {
82
97
  * create a new generation.
83
98
  */
84
99
  export function checkpointContext(coordRoot, input) {
85
- const now = new Date().toISOString();
86
- const current = readContextState(coordRoot, input.sessionId) ??
87
- newState({ sessionId: input.sessionId, instanceId: input.instanceId, now });
88
- if (input.reason === "pre_compact" && current.phase === "checkpointed") {
89
- const existing = readLatestCapsule(coordRoot, input.sessionId);
90
- if (existing && current.latest_capsule) {
91
- return {
92
- capsule: existing,
93
- path: join(coordRoot, ".harnery", current.latest_capsule),
94
- state: current,
95
- reused: true,
100
+ return withContextMutation(coordRoot, input.sessionId, () => {
101
+ const now = new Date().toISOString();
102
+ const previous = readContextState(coordRoot, input.sessionId);
103
+ const current = previous?.instance_id === input.instanceId
104
+ ? previous
105
+ : {
106
+ ...newState({ sessionId: input.sessionId, instanceId: input.instanceId, now }),
107
+ generation: previous?.generation ?? 0,
96
108
  };
109
+ if (input.reason === "pre_compact" && current.phase === "checkpointed") {
110
+ const existing = readLatestCapsule(coordRoot, input.sessionId);
111
+ if (existing && capsuleMatchesState(current, existing, input) && current.latest_capsule) {
112
+ return {
113
+ capsule: existing,
114
+ path: join(coordRoot, ".harnery", current.latest_capsule),
115
+ state: current,
116
+ reused: true,
117
+ };
118
+ }
97
119
  }
98
- }
99
- const heartbeat = readHeartbeat(coordRoot, input.instanceId);
100
- const generation = current.generation + 1;
101
- const capsule = {
102
- schema_version: CONTEXT_SCHEMA_VERSION,
103
- capsule_id: randomUUID(),
104
- generation,
105
- created_at: now,
106
- reason: input.reason,
107
- session: {
108
- session_id: input.sessionId,
120
+ const heartbeat = readHeartbeat(coordRoot, input.instanceId);
121
+ const generation = current.generation + 1;
122
+ const capsule = {
123
+ schema_version: CONTEXT_SCHEMA_VERSION,
124
+ capsule_id: randomUUID(),
125
+ generation,
126
+ created_at: now,
127
+ reason: input.reason,
128
+ session: {
129
+ session_id: input.sessionId,
130
+ instance_id: input.instanceId,
131
+ adapter: input.adapter,
132
+ model: input.model ?? current.latest_context?.model ?? heartbeat?.model,
133
+ },
134
+ context: current.latest_context,
135
+ work: buildWorkSnapshot(heartbeat, input.continuationNote),
136
+ repo: snapshotRepo(input.cwd),
137
+ };
138
+ const relativePath = capsuleRelativePath(input.sessionId, generation);
139
+ const absolutePath = join(coordRoot, ".harnery", relativePath);
140
+ writeBoundedJson(absolutePath, capsule, MAX_CAPSULE_BYTES);
141
+ const next = {
142
+ ...current,
109
143
  instance_id: input.instanceId,
110
- adapter: input.adapter,
111
- model: input.model ?? current.latest_context?.model ?? heartbeat?.model,
112
- },
113
- context: current.latest_context,
114
- work: buildWorkSnapshot(heartbeat, input.continuationNote),
115
- repo: snapshotRepo(input.cwd),
116
- };
117
- const relativePath = capsuleRelativePath(input.sessionId, generation);
118
- const absolutePath = join(coordRoot, ".harnery", relativePath);
119
- writeBoundedJson(absolutePath, capsule, MAX_CAPSULE_BYTES);
120
- const next = {
121
- ...current,
122
- instance_id: input.instanceId,
123
- phase: "checkpointed",
124
- generation,
125
- latest_capsule: relativePath,
126
- updated_at: now,
127
- recovered_at: undefined,
128
- compaction_completed_at: undefined,
129
- degraded_reason: undefined,
130
- };
131
- writeState(coordRoot, next);
132
- return { capsule, path: absolutePath, state: next, reused: false };
144
+ phase: "checkpointed",
145
+ generation,
146
+ latest_capsule: relativePath,
147
+ updated_at: now,
148
+ recovered_at: undefined,
149
+ compaction_completed_at: undefined,
150
+ degraded_reason: undefined,
151
+ };
152
+ writeState(coordRoot, next);
153
+ return { capsule, path: absolutePath, state: next, reused: false };
154
+ });
133
155
  }
134
156
  /** Prepare a briefing without claiming that a adapter has accepted it yet. */
135
157
  export function prepareContextRecovery(coordRoot, input) {
136
- const now = new Date().toISOString();
137
- const current = readContextState(coordRoot, input.sessionId) ??
138
- newState({ sessionId: input.sessionId, instanceId: input.instanceId, now });
139
- const capsule = readLatestCapsule(coordRoot, input.sessionId);
140
- if (!capsule) {
141
- const degraded = {
142
- ...current,
143
- instance_id: input.instanceId,
144
- phase: "degraded",
145
- updated_at: now,
146
- degraded_reason: "post-compaction signal arrived without a continuity checkpoint",
158
+ return withContextMutation(coordRoot, input.sessionId, () => {
159
+ const now = new Date().toISOString();
160
+ const current = readContextState(coordRoot, input.sessionId) ??
161
+ newState({ sessionId: input.sessionId, instanceId: input.instanceId, now });
162
+ // A reused provider ID does not transfer the prior task's context.
163
+ if (current.instance_id !== input.instanceId)
164
+ return null;
165
+ const capsule = readLatestCapsule(coordRoot, input.sessionId);
166
+ if (!capsule || !capsuleMatchesState(current, capsule, input)) {
167
+ const degraded = {
168
+ ...current,
169
+ instance_id: input.instanceId,
170
+ phase: "degraded",
171
+ updated_at: now,
172
+ degraded_reason: capsule
173
+ ? "continuity checkpoint does not match the requesting instance and state"
174
+ : "post-compaction signal arrived without a continuity checkpoint",
175
+ };
176
+ writeState(coordRoot, degraded);
177
+ return null;
178
+ }
179
+ const liveRepo = snapshotRepo(input.cwd);
180
+ const briefing = [
181
+ renderRecoveryBriefing(capsule, liveRepo),
182
+ renderRecoveryInput(coordRoot, input.instanceId),
183
+ ]
184
+ .filter(Boolean)
185
+ .join("\n\n");
186
+ return {
187
+ state: current,
188
+ capsule,
189
+ briefing,
190
+ checkpoint: Object.freeze({
191
+ session_id: input.sessionId,
192
+ instance_id: input.instanceId,
193
+ generation: capsule.generation,
194
+ capsule_id: capsule.capsule_id,
195
+ capsule_sha256: capsuleDigest(capsule),
196
+ }),
147
197
  };
148
- writeState(coordRoot, degraded);
149
- return null;
198
+ });
199
+ }
200
+ /** Recovery lists input without surfacing it again or changing its receipts. */
201
+ function renderRecoveryInput(coordRoot, instanceId) {
202
+ const readCommand = `${resolveBinName(coordRoot)} agents inbox list ${instanceId}`;
203
+ try {
204
+ const records = peerInbox(coordRoot).records(instanceId);
205
+ if (records.some((record) => (record.kind !== "message" && record.kind !== "surfaced") ||
206
+ typeof record.message_id !== "string" ||
207
+ Buffer.byteLength(record.message_id) > 128)) {
208
+ throw new Error("inbox input cannot be listed safely");
209
+ }
210
+ const { surfaced } = project(records);
211
+ const messages = records.filter((record) => record.kind === "message" && record.recipient_instance_id === instanceId);
212
+ const pending = messages.filter((message) => !surfaced.has(message.message_id));
213
+ const recent = messages.filter((message) => surfaced.has(message.message_id)).slice(-10);
214
+ if (pending.length === 0 && recent.length === 0)
215
+ return "";
216
+ const lines = [
217
+ "Coordination input after compaction (surfaced does not mean read):",
218
+ ...pending
219
+ .slice(0, 25)
220
+ .map((message) => inputLine(message.message_id, "pending", message.body)),
221
+ ...recent.map((message) => inputLine(message.message_id, "previously surfaced", message.body)),
222
+ ];
223
+ if (pending.length > 25)
224
+ lines.push(`${pending.length - 25} more pending messages are not listed.`);
225
+ lines.push(`Recovery did not acknowledge these messages. Read pending input with \`${readCommand}\`.`);
226
+ return lines.join("\n");
227
+ }
228
+ catch {
229
+ return `Coordination input could not be read. Check \`${readCommand}\` before continuing.`;
230
+ }
231
+ }
232
+ function inputLine(messageId, status, body) {
233
+ const text = body.replace(/\s+/g, " ").trim();
234
+ let excerpt = "";
235
+ let bytes = 0;
236
+ for (const character of text) {
237
+ const size = Buffer.byteLength(character);
238
+ if (bytes + size > MAX_RECOVERY_INPUT_BODY_BYTES)
239
+ break;
240
+ excerpt += character;
241
+ bytes += size;
150
242
  }
151
- const liveRepo = snapshotRepo(input.cwd);
152
- const briefing = renderRecoveryBriefing(capsule, liveRepo);
153
- return { state: current, capsule, briefing };
243
+ return `- ${messageId} (${status}): ${excerpt}${excerpt.length < text.length ? " [excerpt]" : ""}`;
154
244
  }
155
- /** Advance continuity only after the adapter context channel accepted the briefing. */
245
+ /**
246
+ * Advance continuity only after the adapter accepted the prepared briefing.
247
+ * Missing, altered, foreign or superseded checkpoints refuse without writing.
248
+ * Repeating the same accepted checkpoint is an exact no-op.
249
+ */
156
250
  export function completeContextRecovery(coordRoot, input) {
157
- const now = new Date().toISOString();
158
- const current = readContextState(coordRoot, input.sessionId) ??
159
- newState({ sessionId: input.sessionId, instanceId: input.instanceId, now });
160
- const recovered = {
161
- ...current,
162
- instance_id: input.instanceId,
163
- phase: "recovered",
164
- updated_at: now,
165
- recovered_at: now,
166
- degraded_reason: undefined,
167
- };
168
- writeState(coordRoot, recovered);
169
- return recovered;
251
+ return withContextMutation(coordRoot, input.sessionId, () => {
252
+ const current = readContextState(coordRoot, input.sessionId);
253
+ const checkpoint = input.checkpoint;
254
+ if (!current ||
255
+ !checkpoint ||
256
+ checkpoint.session_id !== input.sessionId ||
257
+ checkpoint.instance_id !== input.instanceId ||
258
+ !Number.isSafeInteger(checkpoint.generation) ||
259
+ checkpoint.generation < 1 ||
260
+ typeof checkpoint.capsule_id !== "string" ||
261
+ !checkpoint.capsule_id ||
262
+ !/^[a-f0-9]{64}$/.test(checkpoint.capsule_sha256) ||
263
+ (current.phase !== "checkpointed" && current.phase !== "recovered"))
264
+ return null;
265
+ const capsule = readLatestCapsule(coordRoot, input.sessionId);
266
+ if (!capsule ||
267
+ !capsuleMatchesState(current, capsule, input) ||
268
+ checkpoint.generation !== capsule.generation ||
269
+ checkpoint.capsule_id !== capsule.capsule_id ||
270
+ checkpoint.capsule_sha256 !== capsuleDigest(capsule))
271
+ return null;
272
+ if (current.phase === "recovered")
273
+ return current;
274
+ const now = new Date().toISOString();
275
+ const recovered = {
276
+ ...current,
277
+ phase: "recovered",
278
+ updated_at: now,
279
+ recovered_at: now,
280
+ degraded_reason: undefined,
281
+ };
282
+ writeState(coordRoot, recovered);
283
+ return recovered;
284
+ });
170
285
  }
171
286
  /** Record that the adapter, rather than Harnery, reported compaction complete. */
172
287
  export function markContextCompactionCompleted(coordRoot, input) {
173
- const now = input.observedAt ?? new Date().toISOString();
174
- const current = readContextState(coordRoot, input.sessionId) ??
175
- newState({ sessionId: input.sessionId, instanceId: input.instanceId, now });
176
- const next = {
177
- ...current,
178
- instance_id: input.instanceId,
179
- phase: current.latest_capsule ? current.phase : "degraded",
180
- compaction_completed_at: now,
181
- updated_at: now,
182
- degraded_reason: current.latest_capsule
183
- ? current.degraded_reason
184
- : "post-compaction signal arrived without a continuity checkpoint",
185
- };
186
- writeState(coordRoot, next);
187
- return next;
288
+ return withContextMutation(coordRoot, input.sessionId, () => {
289
+ const now = input.observedAt ?? new Date().toISOString();
290
+ const current = readContextState(coordRoot, input.sessionId) ??
291
+ newState({ sessionId: input.sessionId, instanceId: input.instanceId, now });
292
+ if (current.instance_id !== input.instanceId)
293
+ return current;
294
+ const next = {
295
+ ...current,
296
+ instance_id: input.instanceId,
297
+ phase: current.latest_capsule ? current.phase : "degraded",
298
+ compaction_completed_at: now,
299
+ updated_at: now,
300
+ degraded_reason: current.latest_capsule
301
+ ? current.degraded_reason
302
+ : "post-compaction signal arrived without a continuity checkpoint",
303
+ };
304
+ writeState(coordRoot, next);
305
+ return next;
306
+ });
188
307
  }
189
308
  /** Convenience API for callers whose injection is synchronous and guaranteed. */
190
309
  export function recoverContext(coordRoot, input) {
@@ -193,7 +312,19 @@ export function recoverContext(coordRoot, input) {
193
312
  const state = readContextState(coordRoot, input.sessionId);
194
313
  return { state, capsule: null, briefing: "", recovered: false };
195
314
  }
196
- const recovered = completeContextRecovery(coordRoot, input);
315
+ const recovered = completeContextRecovery(coordRoot, {
316
+ sessionId: input.sessionId,
317
+ instanceId: input.instanceId,
318
+ checkpoint: prepared.checkpoint,
319
+ });
320
+ if (!recovered) {
321
+ return {
322
+ state: readContextState(coordRoot, input.sessionId) ?? prepared.state,
323
+ capsule: null,
324
+ briefing: "",
325
+ recovered: false,
326
+ };
327
+ }
197
328
  return {
198
329
  state: recovered,
199
330
  capsule: prepared.capsule,
@@ -206,9 +337,25 @@ export function readContextState(coordRoot, sessionId) {
206
337
  }
207
338
  export function readLatestCapsule(coordRoot, sessionId) {
208
339
  const state = readContextState(coordRoot, sessionId);
209
- if (!state?.latest_capsule)
340
+ if (typeof state?.latest_capsule !== "string" || !state.latest_capsule)
341
+ return null;
342
+ const capsule = readJson(join(coordRoot, ".harnery", state.latest_capsule));
343
+ if (!capsule ||
344
+ !record(capsule.repo) ||
345
+ !record(capsule.work) ||
346
+ typeof capsule.repo.cwd !== "string" ||
347
+ !Array.isArray(capsule.repo.dirty_paths) ||
348
+ !capsule.repo.dirty_paths.every((path) => typeof path === "string") ||
349
+ !Array.isArray(capsule.work.files_held) ||
350
+ !capsule.work.files_held.every((path) => typeof path === "string")) {
210
351
  return null;
211
- return readJson(join(coordRoot, ".harnery", state.latest_capsule));
352
+ }
353
+ capsule.repo.observation = repoObservation(capsule.repo.observation);
354
+ capsule.repo.observation_detail = clampString(string(capsule.repo.observation_detail), MAX_OBSERVATION_DETAIL_CHARS);
355
+ capsule.repo.branch = string(capsule.repo.branch)?.trim() || undefined;
356
+ capsule.repo.head = string(capsule.repo.head)?.trim() || undefined;
357
+ capsule.repo.root = string(capsule.repo.root)?.trim() || undefined;
358
+ return capsule;
212
359
  }
213
360
  export function renderRecoveryBriefing(capsule, liveRepo) {
214
361
  const lines = [
@@ -222,6 +369,38 @@ export function renderRecoveryBriefing(capsule, liveRepo) {
222
369
  if (capsule.work.files_held.length > 0) {
223
370
  lines.push(`Files held: ${capsule.work.files_held.join(", ")}`);
224
371
  }
372
+ const unknown = [];
373
+ if (!observationsComparable(capsule.repo, liveRepo)) {
374
+ for (const [label, repo] of [
375
+ ["checkpoint", capsule.repo],
376
+ ["current repository", liveRepo],
377
+ ]) {
378
+ if (repo.observation !== "ok") {
379
+ const detail = clampString(string(repo.observation_detail), MAX_OBSERVATION_DETAIL_CHARS);
380
+ unknown.push(`${label}: ${repoObservation(repo.observation)}${detail ? ` (${detail})` : ""}`);
381
+ }
382
+ }
383
+ }
384
+ if (capsule.repo.dirty_paths_truncated || liveRepo.dirty_paths_truncated) {
385
+ unknown.push("dirty path list is truncated");
386
+ }
387
+ if (!capsule.repo.root ||
388
+ !liveRepo.root ||
389
+ !isAbsolute(capsule.repo.root) ||
390
+ !isAbsolute(liveRepo.root)) {
391
+ unknown.push("repository root is unknown");
392
+ }
393
+ else if (capsule.repo.root !== liveRepo.root) {
394
+ unknown.push("repository root differs from the checkpoint");
395
+ }
396
+ if (!capsule.repo.branch?.trim() || !liveRepo.branch?.trim())
397
+ unknown.push("branch is unknown");
398
+ if (!capsule.repo.head?.trim() || !liveRepo.head?.trim())
399
+ unknown.push("HEAD is unknown");
400
+ if (unknown.length > 0) {
401
+ lines.push(`Repository could not be verified against the checkpoint: ${unknown.join("; ")}. Re-check before editing.`);
402
+ return lines.join("\n");
403
+ }
225
404
  const drift = [];
226
405
  if (capsule.repo.branch && liveRepo.branch && capsule.repo.branch !== liveRepo.branch) {
227
406
  drift.push(`branch changed ${capsule.repo.branch} -> ${liveRepo.branch}`);
@@ -244,24 +423,58 @@ export function renderRecoveryBriefing(capsule, liveRepo) {
244
423
  }
245
424
  export function snapshotRepo(cwd) {
246
425
  const root = git(cwd, ["rev-parse", "--show-toplevel"]);
247
- if (!root)
248
- return { cwd, dirty_paths: [] };
426
+ if (!root.ok || !root.out.trim()) {
427
+ return {
428
+ cwd: clampString(cwd, 2_000) ?? cwd,
429
+ observation: "unavailable",
430
+ observation_detail: `top-level: ${root.ok ? "empty result" : root.reason}`,
431
+ dirty_paths: [],
432
+ };
433
+ }
249
434
  const status = git(cwd, ["status", "--porcelain=v1", "--untracked-files=normal"]);
250
- const dirty = status
251
- ? status
435
+ const branch = git(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
436
+ const head = git(cwd, ["rev-parse", "HEAD"]);
437
+ const reads = [
438
+ ["status", status],
439
+ ["branch", branch],
440
+ ["HEAD", head],
441
+ ];
442
+ const details = reads.flatMap(([label, read]) => !read.ok
443
+ ? [`${label}: ${read.reason}`]
444
+ : label !== "status" && !read.out.trim()
445
+ ? [`${label}: empty result`]
446
+ : []);
447
+ const dirty = status.ok
448
+ ? status.out
252
449
  .split("\n")
253
450
  .map((line) => line.trimEnd())
254
451
  .filter(Boolean)
255
- .map((line) => clampString(line.slice(3).trim(), MAX_SNAPSHOT_PATH_CHARS) ?? "")
452
+ .map((line) => line.slice(3).trim())
256
453
  .filter(Boolean)
257
454
  : [];
455
+ const truncated = dirty.length > MAX_SNAPSHOT_PATHS ||
456
+ dirty.some((path) => path.length > MAX_SNAPSHOT_PATH_CHARS);
457
+ if (truncated)
458
+ details.push("dirty path list is truncated");
258
459
  return {
259
460
  cwd: clampString(cwd, 2_000) ?? cwd,
260
- root: clampString(root, 2_000),
261
- branch: git(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]),
262
- head: git(cwd, ["rev-parse", "HEAD"]),
263
- dirty_paths: dirty.slice(0, MAX_SNAPSHOT_PATHS),
264
- ...(dirty.length > MAX_SNAPSHOT_PATHS ? { dirty_paths_truncated: true } : {}),
461
+ observation: reads.some(([, read]) => !read.ok && read.reason === "timed_out")
462
+ ? "unavailable"
463
+ : details.length > 0
464
+ ? "partial"
465
+ : "ok",
466
+ ...(details.length > 0
467
+ ? {
468
+ observation_detail: clampString(details.join("; "), MAX_OBSERVATION_DETAIL_CHARS),
469
+ }
470
+ : {}),
471
+ root: clampString(root.out.trim(), 2_000),
472
+ branch: branch.ok ? branch.out.trim() || undefined : undefined,
473
+ head: head.ok ? head.out.trim() || undefined : undefined,
474
+ dirty_paths: dirty
475
+ .slice(0, MAX_SNAPSHOT_PATHS)
476
+ .map((path) => path.slice(0, MAX_SNAPSHOT_PATH_CHARS)),
477
+ ...(truncated ? { dirty_paths_truncated: true } : {}),
265
478
  };
266
479
  }
267
480
  function buildWorkSnapshot(heartbeat, continuationNote) {
@@ -297,6 +510,39 @@ function newState(input) {
297
510
  updated_at: input.now,
298
511
  };
299
512
  }
513
+ function capsuleMatchesState(state, capsule, input) {
514
+ return (state.session_id === input.sessionId &&
515
+ state.instance_id === input.instanceId &&
516
+ capsule.session?.session_id === input.sessionId &&
517
+ capsule.session?.instance_id === input.instanceId &&
518
+ capsule.schema_version === CONTEXT_SCHEMA_VERSION &&
519
+ Number.isSafeInteger(capsule.generation) &&
520
+ capsule.generation > 0 &&
521
+ capsule.generation === state.generation &&
522
+ typeof capsule.capsule_id === "string" &&
523
+ capsule.capsule_id.length > 0 &&
524
+ state.latest_capsule === capsuleRelativePath(input.sessionId, capsule.generation));
525
+ }
526
+ function capsuleDigest(capsule) {
527
+ return createHash("sha256").update(JSON.stringify(capsule)).digest("hex");
528
+ }
529
+ /** Every state writer shares the same crash-recoverable session reservation. */
530
+ function withContextMutation(coordRoot, sessionId, action) {
531
+ const authority = createHash("sha256").update(sessionId).digest("hex");
532
+ const lease = acquireNoClobberLease({
533
+ path: join(contextDir(coordRoot, sessionId), "mutation.lease"),
534
+ scope: "context-continuity",
535
+ authoritySha256: authority,
536
+ staleAfterMs: 30_000,
537
+ validateStaleOwner: (owner) => owner.authority_sha256 === authority,
538
+ });
539
+ try {
540
+ return action();
541
+ }
542
+ finally {
543
+ lease.release();
544
+ }
545
+ }
300
546
  function writeState(coordRoot, state) {
301
547
  writeBoundedJson(statePath(coordRoot, state.session_id), state, MAX_CAPSULE_BYTES);
302
548
  }
@@ -336,12 +582,28 @@ function readJson(path) {
336
582
  function git(cwd, args) {
337
583
  try {
338
584
  const result = spawnSync("git", args, { cwd, encoding: "utf8", timeout: 2_000 });
339
- return result.status === 0 ? result.stdout.trim() || undefined : undefined;
585
+ if (result.error) {
586
+ return {
587
+ ok: false,
588
+ reason: result.error.code === "ETIMEDOUT"
589
+ ? "timed_out"
590
+ : "spawn_error",
591
+ };
592
+ }
593
+ if (result.status === 0)
594
+ return { ok: true, out: result.stdout.trimEnd() };
595
+ return {
596
+ ok: false,
597
+ reason: /not a git repository/i.test(result.stderr) ? "not_repository" : "failed",
598
+ };
340
599
  }
341
600
  catch {
342
- return undefined;
601
+ return { ok: false, reason: "spawn_error" };
343
602
  }
344
603
  }
604
+ function repoObservation(value) {
605
+ return value === "ok" || value === "partial" ? value : "unavailable";
606
+ }
345
607
  function sameMeasurement(a, b) {
346
608
  if (!a)
347
609
  return false;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Repository observation contract shared by context recovery and workflow proof.
3
+ *
4
+ * This module has no runtime imports so read-only consumers (the Codec
5
+ * dashboard, proof readers) can use it without reaching the Git-running
6
+ * snapshot code in ./index.ts.
7
+ */
8
+ export type RepoObservation = "ok" | "unavailable" | "partial";
9
+ export interface RepoSnapshot {
10
+ cwd: string;
11
+ observation: RepoObservation;
12
+ /** Bounded explanation of an unavailable or incomplete observation. */
13
+ observation_detail?: string;
14
+ root?: string;
15
+ branch?: string;
16
+ head?: string;
17
+ dirty_paths: string[];
18
+ dirty_paths_truncated?: boolean;
19
+ }
20
+ /** Only explicitly successful observations can establish repository drift. */
21
+ export declare function observationsComparable(a: {
22
+ observation?: string;
23
+ }, b: {
24
+ observation?: string;
25
+ }): boolean;
26
+ //# sourceMappingURL=observation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observation.d.ts","sourceRoot":"","sources":["../../../src/core/context/observation.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,aAAa,GAAG,SAAS,CAAC;AAE/D,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,eAAe,CAAC;IAC7B,uEAAuE;IACvE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED,8EAA8E;AAC9E,wBAAgB,sBAAsB,CACpC,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,EAC3B,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1B,OAAO,CAET"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Repository observation contract shared by context recovery and workflow proof.
3
+ *
4
+ * This module has no runtime imports so read-only consumers (the Codec
5
+ * dashboard, proof readers) can use it without reaching the Git-running
6
+ * snapshot code in ./index.ts.
7
+ */
8
+ /** Only explicitly successful observations can establish repository drift. */
9
+ export function observationsComparable(a, b) {
10
+ return a.observation === "ok" && b.observation === "ok";
11
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../../../src/core/diagnostics/bundle.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,aAAa,EACb,sBAAsB,EAEvB,MAAM,uBAAuB,CAAC;AA0B/B,OAAO,EASL,KAAK,wBAAwB,EAG7B,KAAK,sBAAsB,EAE3B,KAAK,iBAAiB,EAItB,KAAK,yBAAyB,EAC/B,MAAM,eAAe,CAAC;AA2BvB,MAAM,WAAW,4BAA4B;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,GAAG,CAAC,EAAE,IAAI,CAAC;IACX,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,wBAAwB,CAAC;IACnC,OAAO,EAAE,iBAAiB,CAAC;CAC5B;AAED,MAAM,WAAW,uBAAuB;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,cAAc,EAAE,sBAAsB,CAAC,gBAAgB,CAAC,CAAC;IACzD,KAAK,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,4BAA4B;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,cAAc,EAAE,sBAAsB,CAAC,gBAAgB,CAAC,CAAC;IACzD,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,MAAM,EACnB,KAAK,GAAE,4BAAiC,GACvC,wBAAwB,CAwG1B;AAkCD,wBAAgB,wBAAwB,CACtC,WAAW,EAAE,MAAM,EACnB,GAAG,EAAE,MAAM,GACV,yBAAyB,CAsB3B;AAED,gFAAgF;AAChF,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,yBAAyB,CAK3B;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,yBAAyB,CAE7F;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,uBAAuB,EAAE,CA0BjF;AAED;;;;GAIG;AACH,wBAAgB,8BAA8B,CAAC,QAAQ,EAAE,MAAM,GAAG,4BAA4B,EAAE,CA4C/F;AAqCD,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,EACX,GAAG,OAAa,GACf,sBAAsB,CAgBxB"}
1
+ {"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../../../src/core/diagnostics/bundle.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,aAAa,EACb,sBAAsB,EAEvB,MAAM,uBAAuB,CAAC;AA0B/B,OAAO,EASL,KAAK,wBAAwB,EAG7B,KAAK,sBAAsB,EAE3B,KAAK,iBAAiB,EAItB,KAAK,yBAAyB,EAC/B,MAAM,eAAe,CAAC;AA2BvB,MAAM,WAAW,4BAA4B;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,GAAG,CAAC,EAAE,IAAI,CAAC;IACX,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,wBAAwB,CAAC;IACnC,OAAO,EAAE,iBAAiB,CAAC;CAC5B;AAED,MAAM,WAAW,uBAAuB;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,cAAc,EAAE,sBAAsB,CAAC,gBAAgB,CAAC,CAAC;IACzD,KAAK,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,4BAA4B;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,cAAc,EAAE,sBAAsB,CAAC,gBAAgB,CAAC,CAAC;IACzD,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,MAAM,EACnB,KAAK,GAAE,4BAAiC,GACvC,wBAAwB,CAwH1B;AAkCD,wBAAgB,wBAAwB,CACtC,WAAW,EAAE,MAAM,EACnB,GAAG,EAAE,MAAM,GACV,yBAAyB,CAsB3B;AAED,gFAAgF;AAChF,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,yBAAyB,CAK3B;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,yBAAyB,CAE7F;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,uBAAuB,EAAE,CA0BjF;AAED;;;;GAIG;AACH,wBAAgB,8BAA8B,CAAC,QAAQ,EAAE,MAAM,GAAG,4BAA4B,EAAE,CA4C/F;AAqCD,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,EACX,GAAG,OAAa,GACf,sBAAsB,CAgBxB"}