harnery 0.36.0 → 0.37.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 (311) hide show
  1. package/dist/commander.js +4 -0
  2. package/dist/commands/agents.d.ts.map +1 -1
  3. package/dist/commands/agents.js +87 -46
  4. package/dist/commands/artifacts.d.ts.map +1 -1
  5. package/dist/commands/artifacts.js +26 -1
  6. package/dist/commands/browse.d.ts.map +1 -1
  7. package/dist/commands/browse.js +11 -0
  8. package/dist/commands/diagnostics.d.ts +4 -0
  9. package/dist/commands/diagnostics.d.ts.map +1 -0
  10. package/dist/commands/diagnostics.js +229 -0
  11. package/dist/commands/files.d.ts +12 -0
  12. package/dist/commands/files.d.ts.map +1 -1
  13. package/dist/commands/files.js +54 -1
  14. package/dist/commands/governor.d.ts.map +1 -1
  15. package/dist/commands/governor.js +4 -0
  16. package/dist/commands/ledger-v3.d.ts.map +1 -1
  17. package/dist/commands/ledger-v3.js +71 -2
  18. package/dist/commands/qa-run.d.ts +4 -0
  19. package/dist/commands/qa-run.d.ts.map +1 -0
  20. package/dist/commands/qa-run.js +141 -0
  21. package/dist/commands/resources.d.ts +4 -0
  22. package/dist/commands/resources.d.ts.map +1 -0
  23. package/dist/commands/resources.js +29 -0
  24. package/dist/commands/supervisor.d.ts +4 -0
  25. package/dist/commands/supervisor.d.ts.map +1 -0
  26. package/dist/commands/supervisor.js +101 -0
  27. package/dist/commands/web.d.ts.map +1 -1
  28. package/dist/commands/web.js +17 -0
  29. package/dist/commands/workflow.d.ts.map +1 -1
  30. package/dist/commands/workflow.js +4 -0
  31. package/dist/core/agents/cli.js +7 -3
  32. package/dist/core/agents/health.d.ts +17 -0
  33. package/dist/core/agents/health.d.ts.map +1 -0
  34. package/dist/core/agents/health.js +109 -0
  35. package/dist/core/agents/mailbox.d.ts +92 -0
  36. package/dist/core/agents/mailbox.d.ts.map +1 -0
  37. package/dist/core/agents/mailbox.js +325 -0
  38. package/dist/core/agents/reconcile-coordination-v3.d.ts +44 -0
  39. package/dist/core/agents/reconcile-coordination-v3.d.ts.map +1 -0
  40. package/dist/core/agents/reconcile-coordination-v3.js +58 -0
  41. package/dist/core/agents/render/prompt-context.d.ts +5 -0
  42. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  43. package/dist/core/agents/render/prompt-context.js +34 -13
  44. package/dist/core/agents/render/session-context.d.ts +8 -0
  45. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  46. package/dist/core/agents/render/session-context.js +34 -3
  47. package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
  48. package/dist/core/agents/rules/commit-conflict.js +50 -1
  49. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  50. package/dist/core/agents/rules/stop-hook.js +6 -10
  51. package/dist/core/agents/session-finalizer-v3.js +4 -1
  52. package/dist/core/artifacts/index.d.ts +26 -1
  53. package/dist/core/artifacts/index.d.ts.map +1 -1
  54. package/dist/core/artifacts/index.js +146 -6
  55. package/dist/core/config.d.ts +12 -0
  56. package/dist/core/config.d.ts.map +1 -1
  57. package/dist/core/config.js +43 -0
  58. package/dist/core/diagnostics/advice.d.ts +9 -0
  59. package/dist/core/diagnostics/advice.d.ts.map +1 -0
  60. package/dist/core/diagnostics/advice.js +112 -0
  61. package/dist/core/diagnostics/bundle.d.ts +35 -0
  62. package/dist/core/diagnostics/bundle.d.ts.map +1 -0
  63. package/dist/core/diagnostics/bundle.js +874 -0
  64. package/dist/core/diagnostics/comparison.d.ts +5 -0
  65. package/dist/core/diagnostics/comparison.d.ts.map +1 -0
  66. package/dist/core/diagnostics/comparison.js +444 -0
  67. package/dist/core/diagnostics/contract.d.ts +245 -0
  68. package/dist/core/diagnostics/contract.d.ts.map +1 -0
  69. package/dist/core/diagnostics/contract.js +18 -0
  70. package/dist/core/diagnostics/identity.d.ts +3 -0
  71. package/dist/core/diagnostics/identity.d.ts.map +1 -0
  72. package/dist/core/diagnostics/identity.js +10 -0
  73. package/dist/core/diagnostics/index.d.ts +8 -0
  74. package/dist/core/diagnostics/index.d.ts.map +1 -0
  75. package/dist/core/diagnostics/index.js +7 -0
  76. package/dist/core/diagnostics/replay.d.ts +8 -0
  77. package/dist/core/diagnostics/replay.d.ts.map +1 -0
  78. package/dist/core/diagnostics/replay.js +213 -0
  79. package/dist/core/diagnostics/sanitize.d.ts +9 -0
  80. package/dist/core/diagnostics/sanitize.d.ts.map +1 -0
  81. package/dist/core/diagnostics/sanitize.js +84 -0
  82. package/dist/core/events/legacy-storage/compression.d.ts +13 -0
  83. package/dist/core/events/legacy-storage/compression.d.ts.map +1 -0
  84. package/dist/core/events/legacy-storage/compression.js +107 -0
  85. package/dist/core/events/legacy-storage/index.d.ts +1 -0
  86. package/dist/core/events/legacy-storage/index.d.ts.map +1 -1
  87. package/dist/core/events/legacy-storage/index.js +1 -0
  88. package/dist/core/events/v3/archive-retention.d.ts +33 -0
  89. package/dist/core/events/v3/archive-retention.d.ts.map +1 -0
  90. package/dist/core/events/v3/archive-retention.js +195 -0
  91. package/dist/core/events/v3/capabilities.d.ts +1 -1
  92. package/dist/core/events/v3/capabilities.d.ts.map +1 -1
  93. package/dist/core/events/v3/capabilities.js +1 -0
  94. package/dist/core/events/v3/index.d.ts +1 -0
  95. package/dist/core/events/v3/index.d.ts.map +1 -1
  96. package/dist/core/events/v3/index.js +1 -0
  97. package/dist/core/events/v3/live-routing.d.ts.map +1 -1
  98. package/dist/core/events/v3/live-routing.js +1 -0
  99. package/dist/core/events/v3/producers/hook-base.d.ts +1 -1
  100. package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
  101. package/dist/core/events/v3/producers/hook-base.js +13 -2
  102. package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
  103. package/dist/core/events/v3/producers/intake.js +1 -0
  104. package/dist/core/events/v3/producers/recorder.d.ts +16 -0
  105. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  106. package/dist/core/events/v3/producers/recorder.js +88 -1
  107. package/dist/core/hooks/adapter/output.d.ts +8 -0
  108. package/dist/core/hooks/adapter/output.d.ts.map +1 -1
  109. package/dist/core/hooks/adapter/output.js +11 -1
  110. package/dist/core/hooks/cli.js +183 -192
  111. package/dist/core/hooks/health.d.ts +59 -0
  112. package/dist/core/hooks/health.d.ts.map +1 -0
  113. package/dist/core/hooks/health.js +84 -0
  114. package/dist/core/resources/contract.d.ts +90 -0
  115. package/dist/core/resources/contract.d.ts.map +1 -0
  116. package/dist/core/resources/contract.js +2 -0
  117. package/dist/core/resources/index.d.ts +6 -0
  118. package/dist/core/resources/index.d.ts.map +1 -0
  119. package/dist/core/resources/index.js +5 -0
  120. package/dist/core/resources/sampler.d.ts +27 -0
  121. package/dist/core/resources/sampler.d.ts.map +1 -0
  122. package/dist/core/resources/sampler.js +357 -0
  123. package/dist/core/resources/service-status.d.ts +3 -0
  124. package/dist/core/resources/service-status.d.ts.map +1 -0
  125. package/dist/core/resources/service-status.js +48 -0
  126. package/dist/core/resources/service.d.ts +26 -0
  127. package/dist/core/resources/service.d.ts.map +1 -0
  128. package/dist/core/resources/service.js +292 -0
  129. package/dist/core/resources/storage.d.ts +13 -0
  130. package/dist/core/resources/storage.d.ts.map +1 -0
  131. package/dist/core/resources/storage.js +30 -0
  132. package/dist/core/storage/atomic-json.d.ts +3 -0
  133. package/dist/core/storage/atomic-json.d.ts.map +1 -0
  134. package/dist/core/storage/atomic-json.js +15 -0
  135. package/dist/core/storage/builtins.d.ts.map +1 -1
  136. package/dist/core/storage/builtins.js +45 -6
  137. package/dist/core/storage/logger.d.ts +2 -0
  138. package/dist/core/storage/logger.d.ts.map +1 -1
  139. package/dist/core/storage/logger.js +2 -0
  140. package/dist/core/storage/query.d.ts +14 -0
  141. package/dist/core/storage/query.d.ts.map +1 -1
  142. package/dist/core/storage/query.js +51 -0
  143. package/dist/core/supervisor/activity.d.ts +4 -0
  144. package/dist/core/supervisor/activity.d.ts.map +1 -0
  145. package/dist/core/supervisor/activity.js +141 -0
  146. package/dist/core/supervisor/contract.d.ts +268 -0
  147. package/dist/core/supervisor/contract.d.ts.map +1 -0
  148. package/dist/core/supervisor/contract.js +35 -0
  149. package/dist/core/supervisor/explanations.d.ts +4 -0
  150. package/dist/core/supervisor/explanations.d.ts.map +1 -0
  151. package/dist/core/supervisor/explanations.js +71 -0
  152. package/dist/core/supervisor/findings.d.ts +20 -0
  153. package/dist/core/supervisor/findings.d.ts.map +1 -0
  154. package/dist/core/supervisor/findings.js +399 -0
  155. package/dist/core/supervisor/history.d.ts +9 -0
  156. package/dist/core/supervisor/history.d.ts.map +1 -0
  157. package/dist/core/supervisor/history.js +46 -0
  158. package/dist/core/supervisor/hook-health-alerts.d.ts +14 -0
  159. package/dist/core/supervisor/hook-health-alerts.d.ts.map +1 -0
  160. package/dist/core/supervisor/hook-health-alerts.js +70 -0
  161. package/dist/core/supervisor/hook-health-storage.d.ts +5 -0
  162. package/dist/core/supervisor/hook-health-storage.d.ts.map +1 -0
  163. package/dist/core/supervisor/hook-health-storage.js +24 -0
  164. package/dist/core/supervisor/hook-health.d.ts +72 -0
  165. package/dist/core/supervisor/hook-health.d.ts.map +1 -0
  166. package/dist/core/supervisor/hook-health.js +203 -0
  167. package/dist/core/supervisor/hooks.d.ts +10 -0
  168. package/dist/core/supervisor/hooks.d.ts.map +1 -0
  169. package/dist/core/supervisor/hooks.js +28 -0
  170. package/dist/core/supervisor/index.d.ts +15 -0
  171. package/dist/core/supervisor/index.d.ts.map +1 -0
  172. package/dist/core/supervisor/index.js +14 -0
  173. package/dist/core/supervisor/log-feed.d.ts +9 -0
  174. package/dist/core/supervisor/log-feed.d.ts.map +1 -0
  175. package/dist/core/supervisor/log-feed.js +103 -0
  176. package/dist/core/supervisor/service.d.ts +29 -0
  177. package/dist/core/supervisor/service.d.ts.map +1 -0
  178. package/dist/core/supervisor/service.js +450 -0
  179. package/dist/core/supervisor/services.d.ts +11 -0
  180. package/dist/core/supervisor/services.d.ts.map +1 -0
  181. package/dist/core/supervisor/services.js +155 -0
  182. package/dist/core/supervisor/status.d.ts +3 -0
  183. package/dist/core/supervisor/status.d.ts.map +1 -0
  184. package/dist/core/supervisor/status.js +48 -0
  185. package/dist/core/supervisor/storage.d.ts +30 -0
  186. package/dist/core/supervisor/storage.d.ts.map +1 -0
  187. package/dist/core/supervisor/storage.js +73 -0
  188. package/dist/core/supervisor/timeline.d.ts +3 -0
  189. package/dist/core/supervisor/timeline.d.ts.map +1 -0
  190. package/dist/core/supervisor/timeline.js +87 -0
  191. package/dist/core/workflow/admission.d.ts +23 -0
  192. package/dist/core/workflow/admission.d.ts.map +1 -0
  193. package/dist/core/workflow/admission.js +136 -0
  194. package/dist/core/workflow/engine.d.ts.map +1 -1
  195. package/dist/core/workflow/engine.js +77 -2
  196. package/dist/core/workflow/index.d.ts +3 -2
  197. package/dist/core/workflow/index.d.ts.map +1 -1
  198. package/dist/core/workflow/index.js +2 -1
  199. package/dist/core/workflow/proof.d.ts +2 -1
  200. package/dist/core/workflow/proof.d.ts.map +1 -1
  201. package/dist/core/workflow/proof.js +39 -1
  202. package/dist/core/workflow/run-state.d.ts +3 -2
  203. package/dist/core/workflow/run-state.d.ts.map +1 -1
  204. package/dist/core/workflow/run-state.js +8 -1
  205. package/dist/core/workflow/types.d.ts +33 -1
  206. package/dist/core/workflow/types.d.ts.map +1 -1
  207. package/dist/core/workflow/types.js +2 -1
  208. package/dist/lib/browser/client.d.ts +5 -0
  209. package/dist/lib/browser/client.d.ts.map +1 -1
  210. package/dist/lib/browser/client.js +1 -0
  211. package/dist/lib/browser/index.d.ts +2 -0
  212. package/dist/lib/browser/index.d.ts.map +1 -1
  213. package/dist/lib/browser/index.js +2 -0
  214. package/dist/lib/browser/qa-run-contracts.d.ts +173 -0
  215. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -0
  216. package/dist/lib/browser/qa-run-contracts.js +226 -0
  217. package/dist/lib/browser/qa-run.d.ts +51 -0
  218. package/dist/lib/browser/qa-run.d.ts.map +1 -0
  219. package/dist/lib/browser/qa-run.js +684 -0
  220. package/dist/lib/coord-root-id.d.ts +9 -0
  221. package/dist/lib/coord-root-id.d.ts.map +1 -0
  222. package/dist/lib/coord-root-id.js +17 -0
  223. package/dist/lib/instructions/apply.d.ts +3 -1
  224. package/dist/lib/instructions/apply.d.ts.map +1 -1
  225. package/dist/lib/instructions/apply.js +74 -3
  226. package/dist/lib/instructions/templates.d.ts.map +1 -1
  227. package/dist/lib/instructions/templates.js +6 -0
  228. package/package.json +12 -1
  229. package/src/commander.ts +26 -0
  230. package/src/commands/agents.ts +102 -49
  231. package/src/commands/artifacts.ts +40 -2
  232. package/src/commands/browse.ts +15 -0
  233. package/src/commands/diagnostics.ts +271 -0
  234. package/src/commands/files.ts +102 -2
  235. package/src/commands/governor.ts +8 -0
  236. package/src/commands/ledger-v3.ts +82 -0
  237. package/src/commands/qa-run.ts +184 -0
  238. package/src/commands/resources.ts +38 -0
  239. package/src/commands/supervisor.ts +141 -0
  240. package/src/commands/web.ts +19 -0
  241. package/src/commands/workflow.ts +8 -0
  242. package/src/core/agents/cli.ts +7 -3
  243. package/src/core/agents/health.ts +142 -0
  244. package/src/core/agents/mailbox.ts +373 -0
  245. package/src/core/agents/reconcile-coordination-v3.ts +77 -0
  246. package/src/core/agents/render/prompt-context.ts +42 -12
  247. package/src/core/agents/render/session-context.ts +37 -3
  248. package/src/core/agents/rules/commit-conflict.ts +58 -1
  249. package/src/core/agents/rules/stop-hook.ts +6 -12
  250. package/src/core/agents/session-finalizer-v3.ts +4 -1
  251. package/src/core/artifacts/index.ts +196 -10
  252. package/src/core/config.ts +111 -2
  253. package/src/core/diagnostics/advice.ts +147 -0
  254. package/src/core/diagnostics/bundle.ts +1142 -0
  255. package/src/core/diagnostics/comparison.ts +565 -0
  256. package/src/core/diagnostics/contract.ts +311 -0
  257. package/src/core/diagnostics/identity.ts +11 -0
  258. package/src/core/diagnostics/index.ts +7 -0
  259. package/src/core/diagnostics/replay.ts +314 -0
  260. package/src/core/diagnostics/sanitize.ts +97 -0
  261. package/src/core/events/legacy-storage/compression.ts +130 -0
  262. package/src/core/events/legacy-storage/index.ts +1 -0
  263. package/src/core/events/v3/archive-retention.ts +251 -0
  264. package/src/core/events/v3/capabilities.ts +2 -0
  265. package/src/core/events/v3/index.ts +9 -0
  266. package/src/core/events/v3/live-routing.ts +1 -0
  267. package/src/core/events/v3/producers/hook-base.ts +14 -2
  268. package/src/core/events/v3/producers/intake.ts +1 -0
  269. package/src/core/events/v3/producers/recorder.ts +110 -1
  270. package/src/core/hooks/adapter/output.ts +12 -1
  271. package/src/core/hooks/cli.ts +192 -215
  272. package/src/core/hooks/health.ts +146 -0
  273. package/src/core/resources/contract.ts +98 -0
  274. package/src/core/resources/index.ts +5 -0
  275. package/src/core/resources/sampler.ts +458 -0
  276. package/src/core/resources/service-status.ts +62 -0
  277. package/src/core/resources/service.ts +337 -0
  278. package/src/core/resources/storage.ts +44 -0
  279. package/src/core/storage/atomic-json.ts +15 -0
  280. package/src/core/storage/builtins.ts +62 -6
  281. package/src/core/storage/logger.ts +2 -0
  282. package/src/core/storage/query.ts +58 -0
  283. package/src/core/supervisor/activity.ts +193 -0
  284. package/src/core/supervisor/contract.ts +312 -0
  285. package/src/core/supervisor/explanations.ts +104 -0
  286. package/src/core/supervisor/findings.ts +788 -0
  287. package/src/core/supervisor/history.ts +61 -0
  288. package/src/core/supervisor/hook-health-alerts.ts +108 -0
  289. package/src/core/supervisor/hook-health-storage.ts +25 -0
  290. package/src/core/supervisor/hook-health.ts +301 -0
  291. package/src/core/supervisor/hooks.ts +35 -0
  292. package/src/core/supervisor/index.ts +14 -0
  293. package/src/core/supervisor/log-feed.ts +131 -0
  294. package/src/core/supervisor/service.ts +511 -0
  295. package/src/core/supervisor/services.ts +203 -0
  296. package/src/core/supervisor/status.ts +59 -0
  297. package/src/core/supervisor/storage.ts +126 -0
  298. package/src/core/supervisor/timeline.ts +133 -0
  299. package/src/core/workflow/admission.ts +215 -0
  300. package/src/core/workflow/engine.ts +93 -1
  301. package/src/core/workflow/index.ts +10 -0
  302. package/src/core/workflow/proof.ts +54 -1
  303. package/src/core/workflow/run-state.ts +13 -1
  304. package/src/core/workflow/types.ts +39 -1
  305. package/src/lib/browser/client.ts +6 -0
  306. package/src/lib/browser/index.ts +29 -0
  307. package/src/lib/browser/qa-run-contracts.ts +380 -0
  308. package/src/lib/browser/qa-run.ts +784 -0
  309. package/src/lib/coord-root-id.ts +23 -0
  310. package/src/lib/instructions/apply.ts +81 -2
  311. package/src/lib/instructions/templates.ts +6 -0
@@ -25,6 +25,7 @@ import { appendFileSync, existsSync } from "node:fs";
25
25
  import { basename, dirname, join } from "node:path";
26
26
  import { coordFreshnessSeconds } from "../../config.ts";
27
27
  import { resolveOwner } from "../../hooks/resolve/owner.ts";
28
+ import { recordLiveClaimChangeV3 } from "../live-authority-v3.ts";
28
29
  import { readLiveCoordinationRows } from "../state/live-coordination-view.ts";
29
30
  import { instanceHasLivePid } from "../state/pidmap.ts";
30
31
 
@@ -34,6 +35,7 @@ interface PeerHeartbeat {
34
35
  name?: string;
35
36
  files_touched?: string[];
36
37
  last_heartbeat?: string;
38
+ platform?: string;
37
39
  }
38
40
 
39
41
  interface Conflict {
@@ -190,6 +192,15 @@ export function evaluateCommit(coordRoot: string, req: CommitVerdictRequest): Co
190
192
  };
191
193
  }
192
194
 
195
+ // A blocked commit proves this session has work in every staged path, even
196
+ // when the edit hook never acquired a claim (for example, a shell edit made
197
+ // just before another session claimed the same file). Keep those paths on
198
+ // the blocked session's durable claim set. The original holder's later
199
+ // commit will then see the overlap instead of silently sweeping this
200
+ // session's working-tree hunks. A successful later commit releases clean
201
+ // paths through the existing post-commit hook.
202
+ retainBlockedStagedPaths(coordRoot, me, mySession, req.staged_paths, peers);
203
+
193
204
  return {
194
205
  allow: false,
195
206
  exit_code: 1,
@@ -204,6 +215,41 @@ export function evaluateCommit(coordRoot: string, req: CommitVerdictRequest): Co
204
215
  };
205
216
  }
206
217
 
218
+ function retainBlockedStagedPaths(
219
+ coordRoot: string,
220
+ instanceId: string,
221
+ sessionId: string,
222
+ stagedPaths: readonly string[],
223
+ peers: readonly PeerHeartbeat[],
224
+ ): void {
225
+ if (instanceId === "__unattributed__") return;
226
+ const self = peers.find((peer) => peer.instance_id === instanceId);
227
+ if (!self) return;
228
+
229
+ for (const path of stagedPaths) {
230
+ try {
231
+ recordLiveClaimChangeV3({
232
+ coordRoot,
233
+ owner: instanceId,
234
+ nativeSessionId: self.session_id ?? sessionId,
235
+ adapter: adapterFromPlatform(self.platform),
236
+ operation: "acquired",
237
+ path,
238
+ access: "write",
239
+ });
240
+ } catch {
241
+ // The commit is already blocked. Coordination remains fail-open for
242
+ // authority-write failures so a damaged cache cannot brick Git.
243
+ }
244
+ }
245
+ }
246
+
247
+ function adapterFromPlatform(platform: unknown): "claude-code" | "cursor" | "codex" {
248
+ if (platform === "cursor") return "cursor";
249
+ if (platform === "codex") return "codex";
250
+ return "claude-code";
251
+ }
252
+
207
253
  function findOverlap(
208
254
  staged: string,
209
255
  files: readonly string[],
@@ -269,6 +315,18 @@ function isHolderSelfAttributed(
269
315
  stagedSet: Set<string>,
270
316
  peers: readonly PeerHeartbeat[],
271
317
  ): boolean {
318
+ const holder = peers.find((p) => p.instance_id === holderId);
319
+
320
+ // Windows-hosted Codex sessions cross a process boundary into WSL, so they
321
+ // deliberately have no usable PID anchor. Their canonical V3 session row is
322
+ // the foreign-identity evidence instead. Treating that row as a transient
323
+ // self identity would suppress the symmetric claim retained after a blocked
324
+ // commit and reopen the working-tree sweep this guard is meant to stop.
325
+ if (holder?.platform === "codex" && holder.session_id) {
326
+ debugGate(coordRoot, { holder: holderId, gate: "B", reason: "canonical_codex_session" });
327
+ return false;
328
+ }
329
+
272
330
  // Gate B (cheaper): live foreign pid-map entry blocks self-attribution.
273
331
  if (holderHasLiveForeignPid(coordRoot, holderId)) {
274
332
  debugGate(coordRoot, { holder: holderId, gate: "B", reason: "live_pid_anchor" });
@@ -276,7 +334,6 @@ function isHolderSelfAttributed(
276
334
  }
277
335
 
278
336
  // Gate A: every held path is either in the staged set or already clean in HEAD.
279
- const holder = peers.find((p) => p.instance_id === holderId);
280
337
  if (!holder) {
281
338
  debugGate(coordRoot, { holder: holderId, gate: "A", reason: "holder_not_found" });
282
339
  return false;
@@ -154,22 +154,16 @@ export function evaluateStopHookV3Events(
154
154
  .map((event) => observedBoolean(event.payload.ritual?.status_box_present_strict))
155
155
  .filter((value): value is boolean => value !== undefined);
156
156
  const currentStrictBox = claudeContinuation ? req.status_box_present_strict : undefined;
157
- if (req.adapter !== "cursor" && latestBox === undefined && currentStrictBox === undefined) {
157
+ if (latestBox === undefined && currentStrictBox === undefined) {
158
158
  return evidenceUnavailable(
159
159
  "turn.completed has no observed V3 assistant-text status-box result; Stop remains fail-open",
160
160
  );
161
161
  }
162
162
 
163
- const ackPresent =
164
- req.adapter === "cursor"
165
- ? requiredStatusChecked
166
- : claudeContinuation
167
- ? strictBoxObservations.includes(true) || currentStrictBox === true
168
- : latestBox === true;
169
- const ackBlock =
170
- req.adapter === "cursor"
171
- ? () => rule13Block(coordRoot, req.session_id)
172
- : () => rule23Block(coordRoot, req.session_id);
163
+ const ackPresent = claudeContinuation
164
+ ? strictBoxObservations.includes(true) || currentStrictBox === true
165
+ : latestBox === true;
166
+ const ackBlock = () => rule23Block(coordRoot, req.session_id);
173
167
 
174
168
  // Pure prose turns keep the human-visible acknowledgement but do not require
175
169
  // task or final-status mutations. This preserves the pre-V3 policy exactly.
@@ -354,7 +348,7 @@ function sessionIdSuffix(sessionId?: string): string {
354
348
  * The verdict keeps the first failure's rule id, so the `blocked_rule`
355
349
  * telemetry and the adapter's `rule=` stderr contract are unchanged; the
356
350
  * reason enumerates the rest so a single continuation can repair all of them.
357
- * Cursor maps the acknowledgement onto rule 1/3, so identical rules dedupe.
351
+ * Duplicate rules are folded so one continuation repairs every missing signal.
358
352
  */
359
353
  function mergeStopBlocks(blocks: VerdictResult[]): VerdictResult | undefined {
360
354
  let primary: VerdictResult | undefined;
@@ -584,7 +584,10 @@ function observeCascades(
584
584
  }
585
585
  if (
586
586
  event.event_type === "lifecycle.sweep_observed" &&
587
- event.payload.observation === "stale_sweep"
587
+ // stale-sweep.ts emits the observation as `stale_heartbeat`; the
588
+ // finalization request it produces is the one named `stale_sweep`.
589
+ // Matching the request name here made this branch unreachable.
590
+ event.payload.observation === "stale_heartbeat"
588
591
  ) {
589
592
  const record = records.find(
590
593
  ({ state }) => !state.terminal && state.instance_id === event.payload.subject_instance_id,
@@ -27,6 +27,8 @@ import {
27
27
  artifactAutoCleanEnabled,
28
28
  artifactAutoCleanIntervalHours,
29
29
  artifactDefaultRetentionDays,
30
+ artifactMaxBytes,
31
+ artifactMaxUnitBytes,
30
32
  } from "../config.ts";
31
33
  import { ARTIFACT_MANIFEST, ARTIFACT_SCHEMA_VERSION, ARTIFACTS_DIR } from "./constants.ts";
32
34
 
@@ -52,12 +54,15 @@ export interface ArtifactManifestV1 {
52
54
  };
53
55
  released_at?: string;
54
56
  released_by?: ArtifactActor;
57
+ oversize_acknowledged?: boolean;
55
58
  }
56
59
 
57
60
  export type ArtifactClassification =
58
61
  | "managed-active"
59
62
  | "managed-current"
60
63
  | "managed-expired"
64
+ | "managed-oversize"
65
+ | "managed-over-budget"
61
66
  | "managed-tracked"
62
67
  | "invalid-manifest"
63
68
  | "unmanaged"
@@ -78,6 +83,7 @@ export interface ArtifactInventoryEntry {
78
83
  last_modified_at: string | null;
79
84
  expires_at: string | null;
80
85
  owner_instance_id: string | null;
86
+ oversize_acknowledged: boolean;
81
87
  }
82
88
 
83
89
  export interface ArtifactCreateInput {
@@ -87,6 +93,7 @@ export interface ArtifactCreateInput {
87
93
  actor?: ArtifactActor;
88
94
  now?: Date;
89
95
  id?: string;
96
+ big?: boolean;
90
97
  }
91
98
 
92
99
  export interface ArtifactMutationInput {
@@ -94,6 +101,15 @@ export interface ArtifactMutationInput {
94
101
  now?: Date;
95
102
  }
96
103
 
104
+ export interface ArtifactAdoptionResult {
105
+ candidates: Array<{ path: string; name: string; bytes: number; kind: "file" | "directory" }>;
106
+ candidate_bytes: number;
107
+ requires_big: boolean;
108
+ adopted_artifact_id: string | null;
109
+ adopted_path: string | null;
110
+ adopted_directories: number;
111
+ }
112
+
97
113
  interface ParsedManifest {
98
114
  ok: true;
99
115
  manifest: ArtifactManifestV1;
@@ -145,6 +161,7 @@ export function createArtifact(
145
161
  retention: {
146
162
  expires_at: addDays(now, retentionDays).toISOString(),
147
163
  },
164
+ ...(input.big ? { oversize_acknowledged: true } : {}),
148
165
  };
149
166
  atomicWriteManifest(path, manifest);
150
167
  return { path, manifest };
@@ -177,7 +194,7 @@ export function inventoryArtifacts(
177
194
  for (const name of names) {
178
195
  rows.push(classifyArtifactPath(repoRoot, join(root, name), now, freshnessSeconds));
179
196
  }
180
- return rows;
197
+ return applyArtifactBudgets(repoRoot, rows);
181
198
  }
182
199
 
183
200
  export function showArtifact(
@@ -186,12 +203,8 @@ export function showArtifact(
186
203
  opts: { now?: Date; freshnessSeconds?: number } = {},
187
204
  ): { entry: ArtifactInventoryEntry; manifest: ArtifactManifestV1 } {
188
205
  const path = resolveArtifactRef(repoRoot, ref);
189
- const entry = classifyArtifactPath(
190
- repoRoot,
191
- path,
192
- opts.now ?? new Date(),
193
- opts.freshnessSeconds ?? 600,
194
- );
206
+ const entry = inventoryArtifacts(repoRoot, opts).find((row) => row.path === path);
207
+ if (!entry) throw new Error(`artifact "${ref}" was not found`);
195
208
  const parsed = readManifest(path);
196
209
  if (!parsed.ok) throw new Error(parsed.reason);
197
210
  return { entry, manifest: parsed.manifest };
@@ -243,6 +256,108 @@ export function releaseArtifact(
243
256
  return manifest;
244
257
  }
245
258
 
259
+ /** Adopt untracked loose files and legacy directories without changing directory paths. */
260
+ export function adoptUnmanagedArtifactFiles(
261
+ repoRoot: string,
262
+ input: {
263
+ yes?: boolean;
264
+ big?: boolean;
265
+ purpose: string;
266
+ retentionDays: number;
267
+ actor?: ArtifactActor;
268
+ now?: Date;
269
+ },
270
+ ): ArtifactAdoptionResult {
271
+ const now = input.now ?? new Date();
272
+ const candidates: ArtifactAdoptionResult["candidates"] = [];
273
+ for (const row of inventoryArtifacts(repoRoot, { now })) {
274
+ if (row.classification !== "unmanaged") continue;
275
+ try {
276
+ const stat = lstatSync(row.path);
277
+ if (stat.isSymbolicLink() || containsTrackedPath(repoRoot, row.path)) continue;
278
+ if (stat.isFile()) {
279
+ candidates.push({ path: row.path, name: row.name, bytes: stat.size, kind: "file" });
280
+ continue;
281
+ }
282
+ if (stat.isDirectory() && !existsSync(join(row.path, ARTIFACT_MANIFEST))) {
283
+ const bytes = safeTreeSize(row.path);
284
+ if (bytes !== null && normalizeSlug(row.name)) {
285
+ candidates.push({ path: row.path, name: row.name, bytes, kind: "directory" });
286
+ }
287
+ }
288
+ } catch {
289
+ // A racing or unreadable entry stays unmanaged.
290
+ }
291
+ }
292
+ const candidateBytes = candidates.reduce((sum, row) => sum + row.bytes, 0);
293
+ const fileBytes = candidates.reduce((sum, row) => sum + (row.kind === "file" ? row.bytes : 0), 0);
294
+ const maxUnitBytes = artifactMaxUnitBytes(repoRoot);
295
+ const requiresBig =
296
+ fileBytes > maxUnitBytes ||
297
+ candidates.some((row) => row.kind === "directory" && row.bytes > maxUnitBytes);
298
+ const preview: ArtifactAdoptionResult = {
299
+ candidates,
300
+ candidate_bytes: candidateBytes,
301
+ requires_big: requiresBig,
302
+ adopted_artifact_id: null,
303
+ adopted_path: null,
304
+ adopted_directories: 0,
305
+ };
306
+ if (!input.yes || candidates.length === 0) return preview;
307
+ if (requiresBig && !input.big) {
308
+ throw new Error("unmanaged adoption exceeds the per-bundle ceiling; repeat with --big");
309
+ }
310
+
311
+ // Revalidate every exact source before creating a destination. A changed,
312
+ // tracked, linked, or non-regular entry aborts the whole adoption.
313
+ for (const candidate of candidates) {
314
+ const stat = lstatSync(candidate.path);
315
+ const bytes = stat.isDirectory() ? safeTreeSize(candidate.path) : stat.size;
316
+ if (
317
+ stat.isSymbolicLink() ||
318
+ bytes !== candidate.bytes ||
319
+ containsTrackedPath(repoRoot, candidate.path)
320
+ ) {
321
+ throw new Error(`unmanaged entry changed before adoption: ${candidate.name}`);
322
+ }
323
+ if (candidate.kind === "file" ? !stat.isFile() : !stat.isDirectory()) {
324
+ throw new Error(`unmanaged entry changed before adoption: ${candidate.name}`);
325
+ }
326
+ }
327
+ const files = candidates.filter((candidate) => candidate.kind === "file");
328
+ const created = files.length
329
+ ? createArtifact(repoRoot, {
330
+ slug: "adopted-unmanaged",
331
+ purpose: input.purpose,
332
+ retentionDays: input.retentionDays,
333
+ actor: input.actor,
334
+ now,
335
+ big: input.big,
336
+ })
337
+ : null;
338
+ for (const candidate of files) renameSync(candidate.path, join(created!.path, candidate.name));
339
+ const retentionDays = positiveDays(input.retentionDays);
340
+ const directories = candidates.filter((candidate) => candidate.kind === "directory");
341
+ for (const candidate of directories) {
342
+ atomicWriteManifest(candidate.path, {
343
+ schema_version: ARTIFACT_SCHEMA_VERSION,
344
+ artifact_id: randomUUID(),
345
+ slug: normalizeSlug(candidate.name),
346
+ purpose: `${input.purpose}: ${candidate.name}`,
347
+ created_at: now.toISOString(),
348
+ created_by: input.actor,
349
+ retention: { expires_at: addDays(now, retentionDays).toISOString() },
350
+ ...(input.big ? { oversize_acknowledged: true } : {}),
351
+ });
352
+ }
353
+ return {
354
+ ...preview,
355
+ adopted_artifact_id: created?.manifest.artifact_id ?? null,
356
+ adopted_path: created?.path ?? null,
357
+ adopted_directories: directories.length,
358
+ };
359
+ }
360
+
246
361
  export function cleanArtifacts(
247
362
  repoRoot: string,
248
363
  opts: { yes?: boolean; now?: Date; freshnessSeconds?: number } = {},
@@ -253,11 +368,24 @@ export function cleanArtifacts(
253
368
  if (!opts.yes) return rows;
254
369
 
255
370
  return rows.map((entry) => {
256
- if (entry.classification !== "managed-expired") return entry;
371
+ if (entry.action !== "would-delete") return entry;
257
372
  // Reclassify immediately before removal. A renewal, release-state change,
258
373
  // heartbeat, symlink swap, or tracked file added since inventory must win.
259
- const current = classifyArtifactPath(repoRoot, entry.path, now, freshnessSeconds);
260
- if (current.classification !== "managed-expired") return current;
374
+ const current = inventoryArtifacts(repoRoot, { now, freshnessSeconds }).find(
375
+ (row) => row.path === entry.path,
376
+ );
377
+ if (!current) {
378
+ return { ...entry, classification: "unknown", reason: "entry disappeared", action: "keep" };
379
+ }
380
+ if (
381
+ current.action !== "would-delete" ||
382
+ current.artifact_id !== entry.artifact_id ||
383
+ current.bytes !== entry.bytes ||
384
+ current.last_modified_at !== entry.last_modified_at ||
385
+ current.expires_at !== entry.expires_at
386
+ ) {
387
+ return current;
388
+ }
261
389
  try {
262
390
  const top = lstatSync(current.path);
263
391
  if (!top.isDirectory() || top.isSymbolicLink()) {
@@ -404,6 +532,7 @@ function classifyArtifactPath(
404
532
  last_modified_at: new Date(effectiveLastModifiedMs).toISOString(),
405
533
  expires_at: effectiveExpiresAt,
406
534
  owner_instance_id: manifest.created_by?.instance_id ?? null,
535
+ oversize_acknowledged: manifest.oversize_acknowledged === true,
407
536
  });
408
537
 
409
538
  if (base.bytes === null || lastModifiedMs === null) {
@@ -450,6 +579,59 @@ function classifyArtifactPath(
450
579
  };
451
580
  }
452
581
 
582
+ function applyArtifactBudgets(
583
+ repoRoot: string,
584
+ inputRows: ArtifactInventoryEntry[],
585
+ ): ArtifactInventoryEntry[] {
586
+ const maxUnitBytes = artifactMaxUnitBytes(repoRoot);
587
+ const maxBytes = artifactMaxBytes(repoRoot);
588
+ const rows = inputRows.map((row) => ({ ...row }));
589
+
590
+ for (const row of rows) {
591
+ if (
592
+ row.classification === "managed-current" &&
593
+ row.bytes !== null &&
594
+ row.bytes > maxUnitBytes &&
595
+ !row.oversize_acknowledged
596
+ ) {
597
+ row.classification = "managed-oversize";
598
+ row.reason = `bundle uses ${row.bytes} bytes, above the ${maxUnitBytes}-byte ceiling without --big`;
599
+ row.action = "would-delete";
600
+ }
601
+ }
602
+
603
+ const managedBytes = rows.reduce(
604
+ (sum, row) => sum + (row.artifact_id && row.bytes !== null ? row.bytes : 0),
605
+ 0,
606
+ );
607
+ let retainedBytes =
608
+ managedBytes -
609
+ rows.reduce(
610
+ (sum, row) => sum + (row.action === "would-delete" && row.bytes !== null ? row.bytes : 0),
611
+ 0,
612
+ );
613
+ if (retainedBytes <= maxBytes) return rows;
614
+
615
+ const candidates = rows
616
+ .filter(
617
+ (row) =>
618
+ row.classification === "managed-current" && row.action === "keep" && row.bytes !== null,
619
+ )
620
+ .sort((left, right) =>
621
+ `${left.expires_at ?? ""}\0${left.created_at ?? ""}\0${left.name}`.localeCompare(
622
+ `${right.expires_at ?? ""}\0${right.created_at ?? ""}\0${right.name}`,
623
+ ),
624
+ );
625
+ for (const row of candidates) {
626
+ if (retainedBytes <= maxBytes) break;
627
+ row.classification = "managed-over-budget";
628
+ row.reason = `repository artifact budget is ${maxBytes} bytes; earliest-expiring inactive bundles are removed first`;
629
+ row.action = "would-delete";
630
+ retainedBytes -= row.bytes ?? 0;
631
+ }
632
+ return rows;
633
+ }
634
+
453
635
  function readManifest(path: string): ParsedManifest | ManifestError {
454
636
  const manifestPath = join(path, ARTIFACT_MANIFEST);
455
637
  if (!existsSync(manifestPath)) {
@@ -495,6 +677,9 @@ function readManifest(path: string): ParsedManifest | ManifestError {
495
677
  if (m.released_by !== undefined && !validActor(m.released_by)) {
496
678
  return { ok: false, reason: "invalid released_by" };
497
679
  }
680
+ if (m.oversize_acknowledged !== undefined && typeof m.oversize_acknowledged !== "boolean") {
681
+ return { ok: false, reason: "invalid oversize_acknowledged" };
682
+ }
498
683
  return { ok: true, manifest: m as ArtifactManifestV1 };
499
684
  }
500
685
 
@@ -604,6 +789,7 @@ function rowFor(
604
789
  last_modified_at: null,
605
790
  expires_at: null,
606
791
  owner_instance_id: null,
792
+ oversize_acknowledged: false,
607
793
  };
608
794
  }
609
795
 
@@ -137,12 +137,23 @@ interface HarneryConfig {
137
137
  * (default 32 MiB; `0` disables rotation). Read via
138
138
  * `eventLedgerRotateActiveBytes()`.
139
139
  */
140
- events?: { rotate_active_bytes?: number };
140
+ events?: {
141
+ rotate_active_bytes?: number;
142
+ archive_max_bytes?: number;
143
+ archive_max_age_days?: number;
144
+ archive_keep_min?: number;
145
+ archive_auto_clean?: boolean;
146
+ };
141
147
  /**
142
148
  * Managed working-artifact defaults. `default_retention_days` is the
143
149
  * create-time TTL when the caller does not pass `artifacts create --days`.
144
150
  */
145
- artifacts?: { default_retention_days?: number; auto_clean?: boolean };
151
+ artifacts?: {
152
+ default_retention_days?: number;
153
+ auto_clean?: boolean;
154
+ max_bytes?: number;
155
+ max_unit_bytes?: number;
156
+ };
146
157
  /**
147
158
  * `harn backup` (restic) defaults: `repo` path/URL, `password_file`, and the
148
159
  * `keep_daily`/`keep_weekly`/`keep_monthly` prune policy. Read via `backupConfig()`.
@@ -622,6 +633,51 @@ export function eventLedgerRotateActiveBytes(coordRoot?: string | null): number
622
633
  return resolveEventLedgerRotateActiveBytesV3(root);
623
634
  }
624
635
 
636
+ const DEFAULT_EVENT_ARCHIVE_MAX_BYTES = 1024 * 1024 * 1024;
637
+ const DEFAULT_EVENT_ARCHIVE_MAX_AGE_DAYS = 7;
638
+ const DEFAULT_EVENT_ARCHIVE_KEEP_MIN = 2;
639
+
640
+ export interface EventLedgerArchivePolicy {
641
+ maxBytes: number;
642
+ maxAgeDays: number;
643
+ keepMin: number;
644
+ autoClean: boolean;
645
+ }
646
+
647
+ /** Bounded retention for closed V3 epochs under `ledgers/v3-archives`. */
648
+ export function eventLedgerArchivePolicy(coordRoot?: string | null): EventLedgerArchivePolicy {
649
+ const root = coordRoot ?? findCoordRoot();
650
+ const configured = root ? readConfig(root).events : undefined;
651
+ return {
652
+ maxBytes: integerSetting(
653
+ coordEnv("EVENT_V3_ARCHIVE_MAX_BYTES"),
654
+ configured?.archive_max_bytes,
655
+ 64 * 1024 * 1024,
656
+ 1024 * 1024 * 1024 * 1024,
657
+ DEFAULT_EVENT_ARCHIVE_MAX_BYTES,
658
+ ),
659
+ maxAgeDays: integerSetting(
660
+ coordEnv("EVENT_V3_ARCHIVE_MAX_AGE_DAYS"),
661
+ configured?.archive_max_age_days,
662
+ 1,
663
+ 3650,
664
+ DEFAULT_EVENT_ARCHIVE_MAX_AGE_DAYS,
665
+ ),
666
+ keepMin: integerSetting(
667
+ coordEnv("EVENT_V3_ARCHIVE_KEEP_MIN"),
668
+ configured?.archive_keep_min,
669
+ 1,
670
+ 1000,
671
+ DEFAULT_EVENT_ARCHIVE_KEEP_MIN,
672
+ ),
673
+ autoClean: booleanSetting(
674
+ coordEnv("EVENT_V3_ARCHIVE_AUTO_CLEAN"),
675
+ configured?.archive_auto_clean,
676
+ true,
677
+ ),
678
+ };
679
+ }
680
+
625
681
  function parseWebPort(value: unknown, source: string): number {
626
682
  const port = typeof value === "number" ? value : Number(value);
627
683
  if (!Number.isInteger(port) || port < 1024 || port > 65535) {
@@ -703,6 +759,33 @@ export function artifactDefaultRetentionDays(coordRoot?: string | null): number
703
759
  : 3;
704
760
  }
705
761
 
762
+ const DEFAULT_ARTIFACT_MAX_BYTES = 20 * 1024 * 1024 * 1024;
763
+ const DEFAULT_ARTIFACT_MAX_UNIT_BYTES = 1024 * 1024 * 1024;
764
+
765
+ /** Soft repository budget for managed working artifacts. */
766
+ export function artifactMaxBytes(coordRoot?: string | null): number {
767
+ const root = coordRoot ?? findCoordRoot();
768
+ return integerSetting(
769
+ coordEnv("ARTIFACT_MAX_BYTES"),
770
+ root ? readConfig(root).artifacts?.max_bytes : undefined,
771
+ 64 * 1024 * 1024,
772
+ 1024 * 1024 * 1024 * 1024,
773
+ DEFAULT_ARTIFACT_MAX_BYTES,
774
+ );
775
+ }
776
+
777
+ /** Size at which one bundle requires an explicit `artifacts create --big` acknowledgement. */
778
+ export function artifactMaxUnitBytes(coordRoot?: string | null): number {
779
+ const root = coordRoot ?? findCoordRoot();
780
+ return integerSetting(
781
+ coordEnv("ARTIFACT_MAX_UNIT_BYTES"),
782
+ root ? readConfig(root).artifacts?.max_unit_bytes : undefined,
783
+ 16 * 1024 * 1024,
784
+ 1024 * 1024 * 1024 * 1024,
785
+ DEFAULT_ARTIFACT_MAX_UNIT_BYTES,
786
+ );
787
+ }
788
+
706
789
  /**
707
790
  * Whether the daily SessionStart sweep of expired artifact workspaces runs.
708
791
  * Precedence: `HARNERY_ARTIFACT_AUTO_CLEAN` (0/false disables) ->
@@ -733,6 +816,32 @@ export function artifactAutoCleanIntervalHours(): number {
733
816
  return 24;
734
817
  }
735
818
 
819
+ function integerSetting(
820
+ envValue: string | undefined,
821
+ configured: unknown,
822
+ minimum: number,
823
+ maximum: number,
824
+ fallback: number,
825
+ ): number {
826
+ for (const value of [envValue, configured]) {
827
+ const parsed = typeof value === "number" ? value : Number(value);
828
+ if (Number.isSafeInteger(parsed) && parsed >= minimum && parsed <= maximum) return parsed;
829
+ }
830
+ return fallback;
831
+ }
832
+
833
+ function booleanSetting(
834
+ envValue: string | undefined,
835
+ configured: unknown,
836
+ fallback: boolean,
837
+ ): boolean {
838
+ if (envValue !== undefined) {
839
+ if (envValue === "1" || envValue.toLowerCase() === "true") return true;
840
+ if (envValue === "0" || envValue.toLowerCase() === "false") return false;
841
+ }
842
+ return typeof configured === "boolean" ? configured : fallback;
843
+ }
844
+
736
845
  /** Resolved `harn backup` defaults (restic repo/password + prune policy). */
737
846
  export interface BackupConfig {
738
847
  repo: string;