harnery 0.43.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 (278) hide show
  1. package/README.md +3 -3
  2. package/dist/commands/agents.d.ts.map +1 -1
  3. package/dist/commands/agents.js +41 -48
  4. package/dist/commands/browse.d.ts.map +1 -1
  5. package/dist/commands/browse.js +38 -11
  6. package/dist/commands/diagnostics.d.ts.map +1 -1
  7. package/dist/commands/diagnostics.js +10 -0
  8. package/dist/commands/doctor.d.ts +12 -0
  9. package/dist/commands/doctor.d.ts.map +1 -1
  10. package/dist/commands/doctor.js +22 -7
  11. package/dist/commands/governor.js +22 -10
  12. package/dist/commands/inbox.d.ts +1 -1
  13. package/dist/commands/inbox.d.ts.map +1 -1
  14. package/dist/commands/inbox.js +55 -2
  15. package/dist/commands/rm.d.ts.map +1 -1
  16. package/dist/commands/rm.js +28 -4
  17. package/dist/commands/servers.d.ts.map +1 -1
  18. package/dist/commands/servers.js +16 -1
  19. package/dist/commands/supervisor.d.ts.map +1 -1
  20. package/dist/commands/supervisor.js +7 -2
  21. package/dist/commands/toc.d.ts +14 -0
  22. package/dist/commands/toc.d.ts.map +1 -1
  23. package/dist/commands/toc.js +7 -4
  24. package/dist/commands/workflow.js +24 -30
  25. package/dist/core/agents/live-authority-v3.d.ts +1 -0
  26. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  27. package/dist/core/agents/live-authority-v3.js +11 -0
  28. package/dist/core/agents/live-lifecycle-v3.d.ts +4 -2
  29. package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
  30. package/dist/core/agents/live-lifecycle-v3.js +28 -7
  31. package/dist/core/agents/mailbox.d.ts +9 -8
  32. package/dist/core/agents/mailbox.d.ts.map +1 -1
  33. package/dist/core/agents/mailbox.js +24 -17
  34. package/dist/core/agents/render/prompt-context.d.ts +2 -0
  35. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  36. package/dist/core/agents/render/prompt-context.js +8 -12
  37. package/dist/core/agents/render/session-context.d.ts +2 -8
  38. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  39. package/dist/core/agents/render/session-context.js +7 -30
  40. package/dist/core/agents/rules/stop-hook.js +3 -3
  41. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  42. package/dist/core/agents/state/heartbeat-writer.js +25 -14
  43. package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
  44. package/dist/core/agents/state/live-coordination-writer.js +5 -0
  45. package/dist/core/agents/state/pidmap.d.ts.map +1 -1
  46. package/dist/core/agents/state/pidmap.js +5 -0
  47. package/dist/core/agents/state/proc-start.d.ts +19 -0
  48. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  49. package/dist/core/agents/state/proc-start.js +41 -0
  50. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  51. package/dist/core/agents/state/stale-sweep.js +180 -70
  52. package/dist/core/agents/state/unchanged-file.d.ts +44 -0
  53. package/dist/core/agents/state/unchanged-file.d.ts.map +1 -0
  54. package/dist/core/agents/state/unchanged-file.js +390 -0
  55. package/dist/core/context/index.d.ts +22 -10
  56. package/dist/core/context/index.d.ts.map +1 -1
  57. package/dist/core/context/index.js +381 -119
  58. package/dist/core/context/observation.d.ts +26 -0
  59. package/dist/core/context/observation.d.ts.map +1 -0
  60. package/dist/core/context/observation.js +11 -0
  61. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  62. package/dist/core/diagnostics/bundle.js +25 -14
  63. package/dist/core/diagnostics/path-identity.d.ts +3 -0
  64. package/dist/core/diagnostics/path-identity.d.ts.map +1 -0
  65. package/dist/core/diagnostics/path-identity.js +12 -0
  66. package/dist/core/diagnostics/sanitize.d.ts.map +1 -1
  67. package/dist/core/diagnostics/sanitize.js +4 -3
  68. package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
  69. package/dist/core/events/v3/authority-outbox.js +29 -0
  70. package/dist/core/events/v3/generation-custody.d.ts +5 -0
  71. package/dist/core/events/v3/generation-custody.d.ts.map +1 -0
  72. package/dist/core/events/v3/generation-custody.js +50 -0
  73. package/dist/core/events/v3/producers/coordination-recorder.d.ts +1 -0
  74. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  75. package/dist/core/events/v3/producers/coordination-recorder.js +7 -0
  76. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  77. package/dist/core/events/v3/producers/recorder.js +77 -64
  78. package/dist/core/governor/planning.d.ts.map +1 -1
  79. package/dist/core/governor/planning.js +14 -1
  80. package/dist/core/governor/service-read.d.ts +10 -2
  81. package/dist/core/governor/service-read.d.ts.map +1 -1
  82. package/dist/core/governor/service-read.js +105 -22
  83. package/dist/core/governor/service.d.ts +20 -2
  84. package/dist/core/governor/service.d.ts.map +1 -1
  85. package/dist/core/governor/service.js +72 -47
  86. package/dist/core/hooks/cli.js +33 -5
  87. package/dist/core/inbox/contract.d.ts +2 -1
  88. package/dist/core/inbox/contract.d.ts.map +1 -1
  89. package/dist/core/inbox/index.d.ts +2 -0
  90. package/dist/core/inbox/index.d.ts.map +1 -1
  91. package/dist/core/inbox/index.js +2 -0
  92. package/dist/core/inbox/peer-messages.d.ts +37 -0
  93. package/dist/core/inbox/peer-messages.d.ts.map +1 -0
  94. package/dist/core/inbox/peer-messages.js +72 -0
  95. package/dist/core/inbox/service.d.ts +8 -1
  96. package/dist/core/inbox/service.d.ts.map +1 -1
  97. package/dist/core/inbox/service.js +67 -21
  98. package/dist/core/inbox/targets.d.ts +22 -0
  99. package/dist/core/inbox/targets.d.ts.map +1 -0
  100. package/dist/core/inbox/targets.js +108 -0
  101. package/dist/core/inbox/watcher.js +7 -3
  102. package/dist/core/journal/index.d.ts.map +1 -1
  103. package/dist/core/journal/index.js +17 -2
  104. package/dist/core/resources/contract.d.ts +3 -0
  105. package/dist/core/resources/contract.d.ts.map +1 -1
  106. package/dist/core/resources/service-status.d.ts +14 -1
  107. package/dist/core/resources/service-status.d.ts.map +1 -1
  108. package/dist/core/resources/service-status.js +23 -13
  109. package/dist/core/resources/service.d.ts +9 -1
  110. package/dist/core/resources/service.d.ts.map +1 -1
  111. package/dist/core/resources/service.js +47 -27
  112. package/dist/core/resources/status.d.ts +2 -5
  113. package/dist/core/resources/status.d.ts.map +1 -1
  114. package/dist/core/resources/status.js +36 -7
  115. package/dist/core/servers/index.d.ts +24 -13
  116. package/dist/core/servers/index.d.ts.map +1 -1
  117. package/dist/core/servers/index.js +132 -92
  118. package/dist/core/servers/tunnels.d.ts +3 -5
  119. package/dist/core/servers/tunnels.d.ts.map +1 -1
  120. package/dist/core/servers/tunnels.js +3 -9
  121. package/dist/core/supervisor/contract.d.ts +12 -0
  122. package/dist/core/supervisor/contract.d.ts.map +1 -1
  123. package/dist/core/supervisor/explanations.d.ts.map +1 -1
  124. package/dist/core/supervisor/explanations.js +2 -1
  125. package/dist/core/supervisor/findings.d.ts.map +1 -1
  126. package/dist/core/supervisor/findings.js +54 -9
  127. package/dist/core/supervisor/service.d.ts +9 -1
  128. package/dist/core/supervisor/service.d.ts.map +1 -1
  129. package/dist/core/supervisor/service.js +59 -29
  130. package/dist/core/supervisor/services.d.ts.map +1 -1
  131. package/dist/core/supervisor/services.js +22 -1
  132. package/dist/core/supervisor/status.d.ts +17 -2
  133. package/dist/core/supervisor/status.d.ts.map +1 -1
  134. package/dist/core/supervisor/status.js +70 -12
  135. package/dist/core/supervisor/timeline.d.ts.map +1 -1
  136. package/dist/core/supervisor/timeline.js +2 -1
  137. package/dist/core/work/state.js +7 -7
  138. package/dist/core/workflow/engine.d.ts.map +1 -1
  139. package/dist/core/workflow/engine.js +105 -32
  140. package/dist/core/workflow/failure-evidence.d.ts +16 -0
  141. package/dist/core/workflow/failure-evidence.d.ts.map +1 -0
  142. package/dist/core/workflow/failure-evidence.js +114 -0
  143. package/dist/core/workflow/index.d.ts +1 -1
  144. package/dist/core/workflow/index.d.ts.map +1 -1
  145. package/dist/core/workflow/index.js +1 -1
  146. package/dist/core/workflow/live-session-v3.d.ts +6 -0
  147. package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
  148. package/dist/core/workflow/live-session-v3.js +126 -5
  149. package/dist/core/workflow/proof.d.ts +5 -5
  150. package/dist/core/workflow/proof.d.ts.map +1 -1
  151. package/dist/core/workflow/proof.js +115 -20
  152. package/dist/core/workflow/run-state.d.ts +3 -1
  153. package/dist/core/workflow/run-state.d.ts.map +1 -1
  154. package/dist/core/workflow/run-state.js +31 -27
  155. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  156. package/dist/core/workflow/spawn-claude.js +19 -0
  157. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  158. package/dist/core/workflow/spawn-codex.js +13 -0
  159. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  160. package/dist/core/workflow/spawn-cursor.js +19 -0
  161. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  162. package/dist/core/workflow/spawn-opencode.js +14 -0
  163. package/dist/core/workflow/types.d.ts +64 -17
  164. package/dist/core/workflow/types.d.ts.map +1 -1
  165. package/dist/core/workflow/workspaces/index.d.ts +0 -1
  166. package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
  167. package/dist/core/workflow/workspaces/index.js +0 -1
  168. package/dist/core/workflow/workspaces/leases.d.ts +3 -1
  169. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  170. package/dist/core/workflow/workspaces/leases.js +30 -16
  171. package/dist/core/workflow/workspaces/local-git.d.ts +6 -2
  172. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  173. package/dist/core/workflow/workspaces/local-git.js +270 -6
  174. package/dist/core/workflow/workspaces/ownership.d.ts +11 -0
  175. package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -0
  176. package/dist/core/workflow/workspaces/ownership.js +194 -0
  177. package/dist/core/workflow/workspaces/reclaim.d.ts +35 -0
  178. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  179. package/dist/core/workflow/workspaces/reclaim.js +14 -0
  180. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  181. package/dist/core/workflow/workspaces/recovery.js +21 -13
  182. package/dist/core/workflow/workspaces/state.d.ts +1 -0
  183. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  184. package/dist/core/workflow/workspaces/state.js +21 -1
  185. package/dist/lib/browser/windows-chrome.d.ts +100 -12
  186. package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
  187. package/dist/lib/browser/windows-chrome.js +262 -73
  188. package/dist/lib/docs-links.d.ts.map +1 -1
  189. package/dist/lib/docs-links.js +45 -2
  190. package/dist/lib/guarded-remove.d.ts +7 -0
  191. package/dist/lib/guarded-remove.d.ts.map +1 -1
  192. package/dist/lib/guarded-remove.js +106 -12
  193. package/dist/lib/identities/assume.d.ts.map +1 -1
  194. package/dist/lib/identities/assume.js +4 -0
  195. package/dist/lib/zip/index.d.ts +41 -0
  196. package/dist/lib/zip/index.d.ts.map +1 -0
  197. package/dist/lib/zip/index.js +398 -0
  198. package/package.json +7 -1
  199. package/src/commands/agents.ts +54 -63
  200. package/src/commands/browse.ts +50 -14
  201. package/src/commands/diagnostics.ts +10 -0
  202. package/src/commands/doctor.ts +26 -7
  203. package/src/commands/governor.ts +23 -10
  204. package/src/commands/inbox.ts +52 -2
  205. package/src/commands/rm.ts +43 -4
  206. package/src/commands/servers.ts +16 -4
  207. package/src/commands/supervisor.ts +7 -2
  208. package/src/commands/toc.ts +14 -6
  209. package/src/commands/workflow.ts +30 -30
  210. package/src/core/agents/live-authority-v3.ts +17 -0
  211. package/src/core/agents/live-lifecycle-v3.ts +52 -14
  212. package/src/core/agents/mailbox.ts +26 -17
  213. package/src/core/agents/render/prompt-context.ts +10 -12
  214. package/src/core/agents/render/session-context.ts +9 -32
  215. package/src/core/agents/rules/stop-hook.ts +3 -3
  216. package/src/core/agents/state/heartbeat-writer.ts +29 -12
  217. package/src/core/agents/state/live-coordination-writer.ts +29 -0
  218. package/src/core/agents/state/pidmap.ts +13 -0
  219. package/src/core/agents/state/proc-start.ts +57 -0
  220. package/src/core/agents/state/stale-sweep.ts +247 -82
  221. package/src/core/agents/state/unchanged-file.ts +456 -0
  222. package/src/core/context/index.ts +446 -136
  223. package/src/core/context/observation.ts +29 -0
  224. package/src/core/diagnostics/bundle.ts +23 -18
  225. package/src/core/diagnostics/path-identity.ts +15 -0
  226. package/src/core/diagnostics/sanitize.ts +4 -3
  227. package/src/core/events/v3/authority-outbox.ts +56 -0
  228. package/src/core/events/v3/generation-custody.ts +59 -0
  229. package/src/core/events/v3/producers/coordination-recorder.ts +15 -0
  230. package/src/core/events/v3/producers/recorder.ts +94 -68
  231. package/src/core/governor/planning.ts +20 -8
  232. package/src/core/governor/service-read.ts +128 -20
  233. package/src/core/governor/service.ts +119 -42
  234. package/src/core/hooks/cli.ts +33 -5
  235. package/src/core/inbox/contract.ts +2 -1
  236. package/src/core/inbox/index.ts +2 -0
  237. package/src/core/inbox/peer-messages.ts +94 -0
  238. package/src/core/inbox/service.ts +88 -22
  239. package/src/core/inbox/targets.ts +148 -0
  240. package/src/core/inbox/watcher.ts +8 -9
  241. package/src/core/journal/index.ts +19 -2
  242. package/src/core/resources/contract.ts +4 -0
  243. package/src/core/resources/service-status.ts +35 -12
  244. package/src/core/resources/service.ts +71 -24
  245. package/src/core/resources/status.ts +57 -8
  246. package/src/core/servers/index.ts +177 -91
  247. package/src/core/servers/tunnels.ts +3 -11
  248. package/src/core/supervisor/contract.ts +13 -0
  249. package/src/core/supervisor/explanations.ts +4 -1
  250. package/src/core/supervisor/findings.ts +68 -10
  251. package/src/core/supervisor/service.ts +88 -26
  252. package/src/core/supervisor/services.ts +26 -8
  253. package/src/core/supervisor/status.ts +89 -12
  254. package/src/core/supervisor/timeline.ts +2 -1
  255. package/src/core/work/state.ts +7 -7
  256. package/src/core/workflow/engine.ts +131 -49
  257. package/src/core/workflow/failure-evidence.ts +141 -0
  258. package/src/core/workflow/index.ts +0 -1
  259. package/src/core/workflow/live-session-v3.ts +146 -5
  260. package/src/core/workflow/proof.ts +134 -26
  261. package/src/core/workflow/run-state.ts +31 -29
  262. package/src/core/workflow/spawn-claude.ts +18 -0
  263. package/src/core/workflow/spawn-codex.ts +13 -0
  264. package/src/core/workflow/spawn-cursor.ts +18 -0
  265. package/src/core/workflow/spawn-opencode.ts +14 -0
  266. package/src/core/workflow/types.ts +51 -17
  267. package/src/core/workflow/workspaces/index.ts +0 -1
  268. package/src/core/workflow/workspaces/leases.ts +48 -17
  269. package/src/core/workflow/workspaces/local-git.ts +315 -5
  270. package/src/core/workflow/workspaces/ownership.ts +232 -0
  271. package/src/core/workflow/workspaces/reclaim.ts +58 -0
  272. package/src/core/workflow/workspaces/recovery.ts +25 -13
  273. package/src/core/workflow/workspaces/state.ts +20 -0
  274. package/src/lib/browser/windows-chrome.ts +346 -86
  275. package/src/lib/docs-links.ts +49 -2
  276. package/src/lib/guarded-remove.ts +132 -14
  277. package/src/lib/identities/assume.ts +4 -0
  278. package/src/lib/zip/index.ts +483 -0
@@ -1,10 +1,12 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { existsSync, readFileSync, statSync } from "node:fs";
3
- import { join } from "node:path";
3
+ import { isAbsolute, join } from "node:path";
4
+ import { observationsComparable } from "../context/observation.js";
4
5
  import { DIAGNOSTIC_ADVICE_SCHEMA_VERSION } from "../diagnostics/contract.js";
5
6
  import { policyDigest } from "../policy/index.js";
6
7
  import { isCanonicalWorkflowAttemptContext } from "./attempt-context.js";
7
8
  import { stableDigest, writeImmutableJson } from "./durable-record.js";
9
+ import { failureEvidenceAllowsUncharged, isSpawnFailureEvidence } from "./failure-evidence.js";
8
10
  import { readWorkflowRunManifest } from "./run-state.js";
9
11
  import { EVIDENCE_KINDS, WORKFLOW_DIAGNOSTIC_ADMISSION_SCHEMA_VERSION, WORKFLOW_PROOF_SCHEMA_VERSION, } from "./types.js";
10
12
  import { isCanonicalWorkflowWorkContext } from "./work-context.js";
@@ -125,15 +127,18 @@ export function digestResult(value, kind) {
125
127
  * attempt was informative about the work — charge it. This also keeps a
126
128
  * resumed run whose earlier segment did real work from being written off by
127
129
  * a later environment failure.
128
- * 3. Otherwise, among the failed agents, environment wins over upstream: a
129
- * missing binary means nothing ran at all, and it is the operator-chosen
130
- * hard stop.
130
+ * 3. Every participant must have failed with positive no-effect evidence,
131
+ * observed absent output and no refusal. A class alone is insufficient.
132
+ * Among those eligible failures, environment is the hard stop.
131
133
  * 4. Anything else is undefined ⇒ a charged work failure, exactly as today.
132
134
  */
133
135
  export function deriveRunFailureClass(status, agents) {
134
136
  if (status !== "failed")
135
137
  return undefined;
136
- if (agents.some((agent) => agent.status === "succeeded" || agent.status === "cached")) {
138
+ if (agents.length === 0 ||
139
+ agents.some((agent) => agent.status !== "failed" ||
140
+ (agent.class !== "environment" && agent.class !== "upstream") ||
141
+ !failureEvidenceAllowsUncharged(agent.failure_evidence))) {
137
142
  return undefined;
138
143
  }
139
144
  const failed = agents.filter((agent) => agent.status === "failed");
@@ -262,6 +267,10 @@ export function readWorkflowProof(coordRoot, runId) {
262
267
  proof.run.class !== "environment" &&
263
268
  proof.run.class !== "upstream" &&
264
269
  proof.run.class !== "decision") ||
270
+ !Array.isArray(proof.agents) ||
271
+ proof.agents.some((agent) => !agent ||
272
+ typeof agent !== "object" ||
273
+ (agent.failure_evidence !== undefined && !isSpawnFailureEvidence(agent.failure_evidence))) ||
265
274
  (proof.run.work_context !== undefined &&
266
275
  (!proof.run.work_item_id ||
267
276
  proof.run.work_context.id !== proof.run.work_item_id ||
@@ -275,6 +284,20 @@ export function readWorkflowProof(coordRoot, runId) {
275
284
  !isWorkspaceExecutionEvidence(proof.execution, runId, proof.run.status))) {
276
285
  throw new Error(`workflow proof at ${path} has an unsupported or mismatched schema`);
277
286
  }
287
+ if (repoComparisonProblem(proof.repository.before, proof.repository.after) !== undefined ||
288
+ proof.repository.before.dirty_paths_truncated ||
289
+ proof.repository.after.dirty_paths_truncated ||
290
+ !proof.repository.before.branch?.trim() ||
291
+ !proof.repository.after.branch?.trim() ||
292
+ !proof.repository.before.head?.trim() ||
293
+ !proof.repository.after.head?.trim()) {
294
+ proof.repository = buildRepoEvidence(proof.repository.before, proof.repository.after);
295
+ proof.unknowns = proof.unknowns.filter((item) => item.code !== "repository_drift_incomplete");
296
+ proof.unknowns.push({
297
+ code: "repository_drift_incomplete",
298
+ message: proof.repository.drift.note,
299
+ });
300
+ }
278
301
  const manifestPath = join(coordRoot, ".harnery", "workflows", runId, "run.json");
279
302
  if (proof.execution === undefined && !existsSync(manifestPath))
280
303
  return proof;
@@ -369,11 +392,27 @@ export function renderWorkflowProof(proof) {
369
392
  `${observation.advice.assessment.recommended_action}; action none`
370
393
  : "admission: shadow not needed; no real child dispatch");
371
394
  }
372
- const repo = proof.repository;
395
+ const comparable = repoComparisonProblem(proof.repository.before, proof.repository.after) === undefined;
396
+ const repo = comparable &&
397
+ !proof.repository.before.dirty_paths_truncated &&
398
+ !proof.repository.after.dirty_paths_truncated &&
399
+ proof.repository.before.branch?.trim() &&
400
+ proof.repository.after.branch?.trim() &&
401
+ proof.repository.before.head?.trim() &&
402
+ proof.repository.after.head?.trim()
403
+ ? proof.repository
404
+ : buildRepoEvidence(proof.repository.before, proof.repository.after);
373
405
  const drift = repo.drift;
374
- lines.push(`repository: branch ${repo.before.branch ?? "unknown"} -> ${repo.after.branch ?? "unknown"}; ` +
375
- `HEAD ${short(repo.before.head)} -> ${short(repo.after.head)}; ` +
376
- `${drift.dirty_paths_added.length} dirty added, ${drift.dirty_paths_cleared.length} cleared`);
406
+ if (!comparable) {
407
+ lines.push(`repository: drift unknown. ${drift.note}`);
408
+ }
409
+ else {
410
+ const paths = drift.dirty_paths_added === null || drift.dirty_paths_cleared === null
411
+ ? "dirty-path drift unknown"
412
+ : `${drift.dirty_paths_added.length} dirty added, ${drift.dirty_paths_cleared.length} cleared`;
413
+ lines.push(`repository: branch ${repo.before.branch ?? "unknown"} -> ${repo.after.branch ?? "unknown"}; ` +
414
+ `HEAD ${short(repo.before.head)} -> ${short(repo.after.head)}; ${paths}`);
415
+ }
377
416
  if (proof.unknowns.length > 0) {
378
417
  lines.push(`unknowns: ${proof.unknowns.length}`);
379
418
  for (const unknown of proof.unknowns)
@@ -385,37 +424,93 @@ export function renderWorkflowProof(proof) {
385
424
  function buildRepoEvidence(beforeRaw, afterRaw) {
386
425
  const before = normalizeRepoSnapshot(beforeRaw);
387
426
  const after = normalizeRepoSnapshot(afterRaw);
427
+ const problem = repoComparisonProblem(before, after);
428
+ if (problem !== undefined) {
429
+ return {
430
+ source: "engine",
431
+ before,
432
+ after,
433
+ drift: {
434
+ branch_changed: null,
435
+ head_changed: null,
436
+ dirty_paths_added: null,
437
+ dirty_paths_cleared: null,
438
+ dirty_paths_retained: null,
439
+ incomplete: true,
440
+ note: `Repository drift could not be verified: ${problem}. Re-check before editing.`,
441
+ },
442
+ };
443
+ }
444
+ const pathsComparable = !before.dirty_paths_truncated && !after.dirty_paths_truncated;
445
+ const branchKnown = Boolean(before.branch && after.branch);
446
+ const headKnown = Boolean(before.head && after.head);
388
447
  const beforeDirty = new Set(before.dirty_paths);
389
448
  const afterDirty = new Set(after.dirty_paths);
390
449
  const retained = after.dirty_paths.filter((path) => beforeDirty.has(path));
391
- const incomplete = Boolean(before.dirty_paths_truncated || after.dirty_paths_truncated || retained.length > 0);
450
+ const notes = [
451
+ ...(!pathsComparable
452
+ ? ["Dirty-path drift is unknown because a snapshot list was truncated."]
453
+ : []),
454
+ ...(!branchKnown ? ["Branch drift is unknown because a snapshot has no branch."] : []),
455
+ ...(!headKnown ? ["HEAD drift is unknown because a snapshot has no HEAD."] : []),
456
+ ...(pathsComparable && retained.length > 0
457
+ ? ["Snapshots cannot prove whether retained dirty paths changed during the run."]
458
+ : []),
459
+ ];
460
+ const incomplete = notes.length > 0;
392
461
  return {
393
462
  source: "engine",
394
463
  before,
395
464
  after,
396
465
  drift: {
397
- branch_changed: before.branch !== after.branch,
398
- head_changed: before.head !== after.head,
399
- dirty_paths_added: after.dirty_paths.filter((path) => !beforeDirty.has(path)),
400
- dirty_paths_cleared: before.dirty_paths.filter((path) => !afterDirty.has(path)),
401
- dirty_paths_retained: retained,
466
+ branch_changed: branchKnown ? before.branch !== after.branch : null,
467
+ head_changed: headKnown ? before.head !== after.head : null,
468
+ dirty_paths_added: pathsComparable
469
+ ? after.dirty_paths.filter((path) => !beforeDirty.has(path))
470
+ : null,
471
+ dirty_paths_cleared: pathsComparable
472
+ ? before.dirty_paths.filter((path) => !afterDirty.has(path))
473
+ : null,
474
+ dirty_paths_retained: pathsComparable ? retained : null,
402
475
  incomplete,
403
- note: incomplete
404
- ? "Snapshots cannot prove whether retained dirty paths changed during the run, and truncated lists may omit paths."
405
- : undefined,
476
+ note: incomplete ? notes.join(" ") : undefined,
406
477
  },
407
478
  };
408
479
  }
409
480
  function normalizeRepoSnapshot(snapshot) {
410
481
  return {
411
482
  cwd: snapshot.cwd,
483
+ observation: snapshot.observation ?? "unavailable",
484
+ observation_detail: snapshot.observation
485
+ ? clippedOptional(snapshot.observation_detail, 500)
486
+ : "No repository observation was recorded.",
412
487
  root: snapshot.root,
413
- branch: snapshot.branch,
414
- head: snapshot.head,
488
+ branch: snapshot.branch?.trim() || undefined,
489
+ head: snapshot.head?.trim() || undefined,
415
490
  dirty_paths: snapshot.dirty_paths,
416
491
  dirty_paths_truncated: snapshot.dirty_paths_truncated,
417
492
  };
418
493
  }
494
+ function snapshotFailure(label, snapshot) {
495
+ return `${label} snapshot is ${snapshot.observation}${snapshot.observation_detail ? ` (${snapshot.observation_detail})` : ""}`;
496
+ }
497
+ function repoComparisonProblem(before, after) {
498
+ if (!observationsComparable(before, after)) {
499
+ return [
500
+ ...(before.observation !== "ok" ? [snapshotFailure("before", before)] : []),
501
+ ...(after.observation !== "ok" ? [snapshotFailure("after", after)] : []),
502
+ ].join("; ");
503
+ }
504
+ const unknownRoots = [
505
+ ...(!before.root || !isAbsolute(before.root) ? ["before"] : []),
506
+ ...(!after.root || !isAbsolute(after.root) ? ["after"] : []),
507
+ ];
508
+ if (unknownRoots.length > 0)
509
+ return `${unknownRoots.join(" and ")} snapshot repository root is unknown`;
510
+ if (before.root !== after.root)
511
+ return "snapshots name different repository roots";
512
+ return undefined;
513
+ }
419
514
  function buildAdapterCoverage(agents, claims, attestations) {
420
515
  return [...new Set(agents.map((agent) => agent.adapter))].map((adapter) => {
421
516
  const adapterAgents = agents.filter((agent) => agent.adapter === adapter);
@@ -47,6 +47,8 @@ export declare function assertWorkflowRunResumable(coordRoot: string, runId: str
47
47
  };
48
48
  export declare function assertWorkflowScriptUnchanged(manifest: WorkflowRunManifest): void;
49
49
  export declare function workflowRunManifestPath(coordRoot: string, runId: string): string;
50
- /** Acquire an exclusive, crash-recoverable lease for one resume segment. */
50
+ /** Acquire an exclusive lease shared by resume and reclaim. Existing locks
51
+ * always refuse: a stale observation cannot authorize unlinking a new owner.
52
+ * Crash recovery requires an audited ownership check outside this operation. */
51
53
  export declare function acquireWorkflowResumeLease(coordRoot: string, runId: string): () => void;
52
54
  //# sourceMappingURL=run-state.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"run-state.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/run-state.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EACL,KAAK,gBAAgB,EAErB,KAAK,eAAe,EACpB,KAAK,mBAAmB,EAEzB,MAAM,oBAAoB,CAAC;AAM5B,OAAO,KAAK,EACV,sBAAsB,EACtB,iCAAiC,EACjC,yBAAyB,EACzB,mBAAmB,EACpB,MAAM,YAAY,CAAC;AAGpB,OAAO,KAAK,EAEV,gBAAgB,EAChB,uCAAuC,EACxC,MAAM,uBAAuB,CAAC;AAO/B,eAAO,MAAM,oCAAoC,EAAG,CAAU,CAAC;AAK/D,MAAM,WAAW,mBAAmB;IAClC,cAAc,EAAE,OAAO,oCAAoC,CAAC;IAC5D,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,mBAAmB,CAAC;IACnC,eAAe,CAAC,EAAE,sBAAsB,CAAC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,iBAAiB,EAAE,YAAY,CAAC;IAChC,SAAS,EAAE;QACT,GAAG,EAAE,MAAM,CAAC;QACZ,eAAe,EAAE,MAAM,CAAC;QACxB,UAAU,EAAE,MAAM,CAAC;QACnB,WAAW,EAAE,MAAM,CAAC;QACpB,oBAAoB,CAAC,EAAE,iCAAiC,CAAC;QACzD,iBAAiB,EAAE,OAAO,CAAC;QAC3B,iBAAiB,EAAE,OAAO,CAAC;QAC3B,aAAa,EAAE,MAAM,GAAG,MAAM,CAAC;QAC/B,kBAAkB,EAAE,MAAM,CAAC;QAC3B,SAAS,EAAE,eAAe,CAAC;QAC3B,cAAc,EAAE,mBAAmB,CAAC;QACpC,MAAM,CAAC,EAAE,gBAAgB,CAAC;QAC1B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAC;QACxD,iBAAiB,CAAC,EAAE,gBAAgB,CAAC;QACrC,kBAAkB,CAAC,EAAE,uCAAuC,CAAC;KAC9D,CAAC;CACH;AAED,MAAM,WAAW,6BAA6B;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,mBAAmB,CAAC;CAC/B;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,6BAA6B,GAAG,MAAM,CAgBrF;AAED,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,mBAAmB,CAyC7F;AAED,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GACZ;IAAE,QAAQ,EAAE,mBAAmB,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CA4BvD;AAED,wBAAgB,6BAA6B,CAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI,CASjF;AAED,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAGhF;AAED,4EAA4E;AAC5E,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,IAAI,CA0CvF"}
1
+ {"version":3,"file":"run-state.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/run-state.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EACL,KAAK,gBAAgB,EAErB,KAAK,eAAe,EACpB,KAAK,mBAAmB,EAEzB,MAAM,oBAAoB,CAAC;AAM5B,OAAO,KAAK,EACV,sBAAsB,EACtB,iCAAiC,EACjC,yBAAyB,EACzB,mBAAmB,EACpB,MAAM,YAAY,CAAC;AAGpB,OAAO,KAAK,EAEV,gBAAgB,EAChB,uCAAuC,EACxC,MAAM,uBAAuB,CAAC;AAO/B,eAAO,MAAM,oCAAoC,EAAG,CAAU,CAAC;AAI/D,MAAM,WAAW,mBAAmB;IAClC,cAAc,EAAE,OAAO,oCAAoC,CAAC;IAC5D,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,mBAAmB,CAAC;IACnC,eAAe,CAAC,EAAE,sBAAsB,CAAC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,iBAAiB,EAAE,YAAY,CAAC;IAChC,SAAS,EAAE;QACT,GAAG,EAAE,MAAM,CAAC;QACZ,eAAe,EAAE,MAAM,CAAC;QACxB,UAAU,EAAE,MAAM,CAAC;QACnB,WAAW,EAAE,MAAM,CAAC;QACpB,oBAAoB,CAAC,EAAE,iCAAiC,CAAC;QACzD,iBAAiB,EAAE,OAAO,CAAC;QAC3B,iBAAiB,EAAE,OAAO,CAAC;QAC3B,aAAa,EAAE,MAAM,GAAG,MAAM,CAAC;QAC/B,kBAAkB,EAAE,MAAM,CAAC;QAC3B,SAAS,EAAE,eAAe,CAAC;QAC3B,cAAc,EAAE,mBAAmB,CAAC;QACpC,MAAM,CAAC,EAAE,gBAAgB,CAAC;QAC1B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAC;QACxD,iBAAiB,CAAC,EAAE,gBAAgB,CAAC;QACrC,kBAAkB,CAAC,EAAE,uCAAuC,CAAC;KAC9D,CAAC;CACH;AAED,MAAM,WAAW,6BAA6B;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,mBAAmB,CAAC;CAC/B;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,6BAA6B,GAAG,MAAM,CAgBrF;AAED,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,mBAAmB,CAgD7F;AAED,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GACZ;IAAE,QAAQ,EAAE,mBAAmB,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CA4BvD;AAED,wBAAgB,6BAA6B,CAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI,CASjF;AAED,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAGhF;AAED;;gFAEgF;AAChF,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,IAAI,CA2CvF"}
@@ -1,4 +1,4 @@
1
- import { createHash } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
2
  import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
3
3
  import { hostname } from "node:os";
4
4
  import { dirname, isAbsolute, join, resolve } from "node:path";
@@ -13,7 +13,6 @@ import { isCanonicalWorkflowWorkContext } from "./work-context.js";
13
13
  import { assertWorkspaceManifestAuthority, isWorkspaceBinding, isWorkspaceCompatibilityExecutionEvidence, } from "./workspaces/validate.js";
14
14
  export const WORKFLOW_RUN_MANIFEST_SCHEMA_VERSION = 2;
15
15
  const MANIFEST_LIMIT_BYTES = 256 * 1024;
16
- const FOREIGN_LEASE_STALE_MS = 24 * 60 * 60 * 1_000;
17
16
  export function workflowScriptDigest(scriptPath) {
18
17
  return createHash("sha256").update(readFileSync(scriptPath)).digest("hex");
19
18
  }
@@ -70,6 +69,13 @@ export function readWorkflowRunManifest(coordRoot, runId) {
70
69
  !validExecution(manifest.execution)) {
71
70
  throw new Error(`workflow run manifest at ${path} has an unsupported or mismatched schema`);
72
71
  }
72
+ if (manifest.repository_before.observation === undefined) {
73
+ manifest.repository_before = {
74
+ ...manifest.repository_before,
75
+ observation: "unavailable",
76
+ observation_detail: "No repository observation was recorded.",
77
+ };
78
+ }
73
79
  assertWorkspaceManifestAuthority(manifest, readManifestWorkspaceRequest(coordRoot, runId));
74
80
  return manifest;
75
81
  }
@@ -121,17 +127,25 @@ export function workflowRunManifestPath(coordRoot, runId) {
121
127
  assertWorkflowRunId(runId);
122
128
  return join(coordRoot, ".harnery", "workflows", runId, "run.json");
123
129
  }
124
- /** Acquire an exclusive, crash-recoverable lease for one resume segment. */
130
+ /** Acquire an exclusive lease shared by resume and reclaim. Existing locks
131
+ * always refuse: a stale observation cannot authorize unlinking a new owner.
132
+ * Crash recovery requires an audited ownership check outside this operation. */
125
133
  export function acquireWorkflowResumeLease(coordRoot, runId) {
126
134
  assertWorkflowRunId(runId);
127
135
  const path = join(coordRoot, ".harnery", "workflows", runId, "resume.lock");
128
- const owner = { pid: process.pid, host: hostname(), created_at: new Date().toISOString() };
136
+ const owner = {
137
+ owner_id: randomUUID(),
138
+ pid: process.pid,
139
+ host: hostname(),
140
+ created_at: new Date().toISOString(),
141
+ };
142
+ const ownerBytes = `${JSON.stringify(owner)}\n`;
129
143
  mkdirSync(dirname(path), { recursive: true });
130
144
  const acquire = () => {
131
145
  try {
132
146
  const fd = openSync(path, "wx", stateFileMode());
133
147
  try {
134
- writeFileSync(fd, `${JSON.stringify(owner)}\n`, "utf8");
148
+ writeFileSync(fd, ownerBytes, "utf8");
135
149
  }
136
150
  finally {
137
151
  closeSync(fd);
@@ -147,21 +161,18 @@ export function acquireWorkflowResumeLease(coordRoot, runId) {
147
161
  };
148
162
  if (!acquire()) {
149
163
  const existing = readResumeLease(path);
150
- if (existing && resumeLeaseIsLive(existing)) {
151
- throw new Error(`workflow run ${runId} is already being resumed by pid ${existing.pid} on ${existing.host}`);
152
- }
153
- unlinkSync(path);
154
- if (!acquire())
155
- throw new Error(`workflow run ${runId} resume lease raced with another process`);
164
+ const holder = existing
165
+ ? ` by pid ${existing.pid} on ${existing.host}; its liveness has not been verified`
166
+ : "; its owner could not be verified";
167
+ throw new Error(`workflow run ${runId} resume lease is already held${holder}`);
156
168
  }
157
169
  return () => {
158
170
  try {
159
- const existing = readResumeLease(path);
160
- if (existing?.pid === owner.pid && existing.host === owner.host)
171
+ if (readFileSync(path, "utf8") === ownerBytes)
161
172
  unlinkSync(path);
162
173
  }
163
174
  catch {
164
- // Best effort. A stale lease is recoverable on the next resume attempt.
175
+ // Leave an unreadable or replaced lock untouched.
165
176
  }
166
177
  };
167
178
  }
@@ -180,19 +191,6 @@ function readResumeLease(path) {
180
191
  return null;
181
192
  }
182
193
  }
183
- function resumeLeaseIsLive(lease) {
184
- if (lease.host !== hostname()) {
185
- const age = Date.now() - Date.parse(lease.created_at);
186
- return Number.isFinite(age) && age < FOREIGN_LEASE_STALE_MS;
187
- }
188
- try {
189
- process.kill(lease.pid, 0);
190
- return true;
191
- }
192
- catch (error) {
193
- return error.code === "EPERM";
194
- }
195
- }
196
194
  function validExecution(value) {
197
195
  if (!value || typeof value !== "object")
198
196
  return false;
@@ -259,6 +257,12 @@ function validFrozenPolicy(policy, cwd) {
259
257
  function validRepoSnapshot(value) {
260
258
  return (Boolean(value) &&
261
259
  typeof value === "object" &&
260
+ (value.observation === undefined ||
261
+ value.observation === "ok" ||
262
+ value.observation === "partial" ||
263
+ value.observation === "unavailable") &&
264
+ (value.observation_detail === undefined ||
265
+ (typeof value.observation_detail === "string" && value.observation_detail.length <= 500)) &&
262
266
  isAbsolute(value.cwd) &&
263
267
  (value.root === undefined || isAbsolute(value.root)) &&
264
268
  (value.branch === undefined || typeof value.branch === "string") &&
@@ -1 +1 @@
1
- {"version":3,"file":"spawn-claude.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/spawn-claude.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAKhF,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAYrE,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,YAAY,GAAG,iBAAiB,CA0B1E;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,gBAAgB,GAAG,WAAW,CA2ExE;AAED,eAAO,MAAM,iBAAiB,EAAE,OAiB/B,CAAC"}
1
+ {"version":3,"file":"spawn-claude.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/spawn-claude.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAMhF,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAYrE,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,YAAY,GAAG,iBAAiB,CA0B1E;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,gBAAgB,GAAG,WAAW,CAexE;AA+ED,eAAO,MAAM,iBAAiB,EAAE,OAiB/B,CAAC"}
@@ -21,6 +21,7 @@ import { exec } from "../../lib/exec.js";
21
21
  import { builtinAdapterProfile, validateAdapterEffort } from "../adapters/profiles.js";
22
22
  import { notFoundError } from "./adapters.js";
23
23
  import { buildChildEnv } from "./child-env.js";
24
+ import { observeSpawnFailure } from "./failure-evidence.js";
24
25
  import { resolveFullAccessArgv, resolveSandboxProjection } from "./sandbox-projection.js";
25
26
  import { isUpstreamFailureText, vendorFailureText } from "./spawn-failure.js";
26
27
  export function buildClaudeInvocation(req) {
@@ -50,6 +51,24 @@ export function buildClaudeInvocation(req) {
50
51
  return { argv };
51
52
  }
52
53
  export function normalizeClaudeResult(raw) {
54
+ const result = normalizeClaudeTransportResult(raw);
55
+ if (result.ok)
56
+ return result;
57
+ let text = raw.stdout.trim();
58
+ let source = "stdout";
59
+ try {
60
+ const envelope = JSON.parse(raw.stdout);
61
+ if (typeof envelope.result === "string" && envelope.result.length > 0) {
62
+ text = envelope.result;
63
+ source = "vendor_envelope";
64
+ }
65
+ }
66
+ catch {
67
+ // Retain unstructured output without treating it as native authority.
68
+ }
69
+ return { ...result, text, failureEvidence: observeSpawnFailure(raw, text, source) };
70
+ }
71
+ function normalizeClaudeTransportResult(raw) {
53
72
  if (raw.timedOut) {
54
73
  return {
55
74
  ok: false,
@@ -1 +1 @@
1
- {"version":3,"file":"spawn-codex.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/spawn-codex.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAQH,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAKhF,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAErE,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,YAAY,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,iBAAiB,CA6C9F;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,gBAAgB,GAAG,WAAW,CAwCvE;AAED,eAAO,MAAM,YAAY,EAAE,OA8B1B,CAAC"}
1
+ {"version":3,"file":"spawn-codex.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/spawn-codex.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAQH,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAMhF,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAErE,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,YAAY,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,iBAAiB,CA6C9F;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,gBAAgB,GAAG,WAAW,CAUvE;AA4CD,eAAO,MAAM,YAAY,EAAE,OA8B1B,CAAC"}
@@ -24,6 +24,7 @@ import { exec } from "../../lib/exec.js";
24
24
  import { builtinAdapterProfile, validateAdapterEffort } from "../adapters/profiles.js";
25
25
  import { notFoundError } from "./adapters.js";
26
26
  import { buildChildEnv } from "./child-env.js";
27
+ import { observeSpawnFailure } from "./failure-evidence.js";
27
28
  import { resolveFullAccessArgv, resolveSandboxProjection } from "./sandbox-projection.js";
28
29
  import { isUpstreamFailureText, vendorFailureText } from "./spawn-failure.js";
29
30
  export function buildCodexInvocation(req, resultFile) {
@@ -71,6 +72,18 @@ export function buildCodexInvocation(req, resultFile) {
71
72
  return { argv, resultFile };
72
73
  }
73
74
  export function normalizeCodexResult(raw) {
75
+ const result = normalizeCodexTransportResult(raw);
76
+ if (result.ok)
77
+ return result;
78
+ const finalText = raw.resultFileText?.trim();
79
+ const text = finalText || raw.stdout.trim();
80
+ return {
81
+ ...result,
82
+ text,
83
+ failureEvidence: observeSpawnFailure(raw, text, finalText ? "final_message" : "stdout"),
84
+ };
85
+ }
86
+ function normalizeCodexTransportResult(raw) {
74
87
  if (raw.timedOut) {
75
88
  return {
76
89
  ok: false,
@@ -1 +1 @@
1
- {"version":3,"file":"spawn-cursor.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/spawn-cursor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAKhF,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AASrE,gEAAgE;AAChE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;CAClB,CAWA;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,YAAY,GAAG,iBAAiB,CAsB1E;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,gBAAgB,GAAG,WAAW,CAqDxE;AAED,eAAO,MAAM,aAAa,EAAE,OAkB3B,CAAC"}
1
+ {"version":3,"file":"spawn-cursor.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/spawn-cursor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAMhF,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AASrE,gEAAgE;AAChE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;CAClB,CAWA;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,YAAY,GAAG,iBAAiB,CAsB1E;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,gBAAgB,GAAG,WAAW,CAexE;AAyDD,eAAO,MAAM,aAAa,EAAE,OAkB3B,CAAC"}
@@ -22,6 +22,7 @@ import { exec } from "../../lib/exec.js";
22
22
  import { builtinAdapterProfile, validateAdapterEffort } from "../adapters/profiles.js";
23
23
  import { notFoundError } from "./adapters.js";
24
24
  import { buildChildEnv } from "./child-env.js";
25
+ import { observeSpawnFailure } from "./failure-evidence.js";
25
26
  import { resolveFullAccessArgv, resolveSandboxProjection } from "./sandbox-projection.js";
26
27
  import { isUpstreamFailureText, vendorFailureText } from "./spawn-failure.js";
27
28
  /** Exported for unit tests (no live binary to test against). */
@@ -60,6 +61,24 @@ export function buildCursorInvocation(req) {
60
61
  return { argv };
61
62
  }
62
63
  export function normalizeCursorResult(raw) {
64
+ const result = normalizeCursorTransportResult(raw);
65
+ if (result.ok)
66
+ return result;
67
+ let text = raw.stdout;
68
+ let source = "stdout";
69
+ try {
70
+ const envelope = JSON.parse(raw.stdout);
71
+ if (typeof envelope.result === "string" && envelope.result.length > 0) {
72
+ text = envelope.result;
73
+ source = "vendor_envelope";
74
+ }
75
+ }
76
+ catch {
77
+ // Raw output remains evidence, not an inferred refusal or effect result.
78
+ }
79
+ return { ...result, text, failureEvidence: observeSpawnFailure(raw, text, source) };
80
+ }
81
+ function normalizeCursorTransportResult(raw) {
63
82
  if (raw.timedOut) {
64
83
  return {
65
84
  ok: false,
@@ -1 +1 @@
1
- {"version":3,"file":"spawn-opencode.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/spawn-opencode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAKhF,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAQrE;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG;IACnD,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAoBA;AAED,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,YAAY,GAAG,iBAAiB,CAoB5E;AAED,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,gBAAgB,GAAG,WAAW,CA2C1E;AAED,eAAO,MAAM,eAAe,EAAE,OAkB7B,CAAC"}
1
+ {"version":3,"file":"spawn-opencode.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/spawn-opencode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAMhF,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAQrE;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG;IACnD,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAoBA;AAED,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,YAAY,GAAG,iBAAiB,CAoB5E;AAED,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,gBAAgB,GAAG,WAAW,CAW1E;AA+CD,eAAO,MAAM,eAAe,EAAE,OAkB7B,CAAC"}
@@ -20,6 +20,7 @@ import { exec } from "../../lib/exec.js";
20
20
  import { builtinAdapterProfile, validateAdapterEffort } from "../adapters/profiles.js";
21
21
  import { notFoundError } from "./adapters.js";
22
22
  import { buildChildEnv } from "./child-env.js";
23
+ import { observeSpawnFailure } from "./failure-evidence.js";
23
24
  import { resolveFullAccessArgv, resolveSandboxProjection } from "./sandbox-projection.js";
24
25
  import { isUpstreamFailureText, vendorFailureText } from "./spawn-failure.js";
25
26
  /**
@@ -72,6 +73,19 @@ export function buildOpenCodeInvocation(req) {
72
73
  return { argv };
73
74
  }
74
75
  export function normalizeOpenCodeResult(raw) {
76
+ const result = normalizeOpenCodeTransportResult(raw);
77
+ if (result.ok)
78
+ return result;
79
+ const parsed = parseOpenCodeStream(raw.stdout);
80
+ const text = parsed.text || raw.stdout.trim();
81
+ return {
82
+ ...result,
83
+ text,
84
+ sessionId: parsed.sessionId,
85
+ failureEvidence: observeSpawnFailure(raw, text, parsed.text ? "vendor_envelope" : "stdout"),
86
+ };
87
+ }
88
+ function normalizeOpenCodeTransportResult(raw) {
75
89
  if (raw.timedOut) {
76
90
  return {
77
91
  ok: false,
@@ -103,17 +103,21 @@ export interface WorkflowAgentProof {
103
103
  session_id?: string;
104
104
  result?: ResultDigest;
105
105
  error?: string;
106
- /** Set on a failed agent whose spawn was classified uninformative about the
107
- * work (ADR 0046): environment (binary absent) or upstream (vendor refused).
108
- * Absent ⇒ a work failure. Recorded even when a script's parallel() swallows
109
- * the rejection, so the run-level class can still be derived from proof. */
106
+ /** Failed-spawn transport class. It is retained even when a script swallows
107
+ * the rejection; charging additionally requires validated failure evidence. */
110
108
  class?: SpawnFailureClass;
109
+ /** Failure observations carried from the adapter. Missing observations are
110
+ * unknown and cannot authorize an uncharged attempt. */
111
+ failure_evidence?: SpawnFailureEvidence;
111
112
  /** Which vendor sandbox mode this agent launched with and why (ADR 0192).
112
113
  * Absent on a cached agent, which launched nothing. */
113
114
  access?: WorkerAccessDecision;
114
115
  }
115
116
  export interface WorkflowRepoSnapshot {
116
117
  cwd: string;
118
+ observation: "ok" | "unavailable" | "partial";
119
+ /** Bounded explanation of an unavailable or incomplete observation. */
120
+ observation_detail?: string;
117
121
  root?: string;
118
122
  branch?: string;
119
123
  head?: string;
@@ -125,12 +129,13 @@ export interface WorkflowRepoEvidence {
125
129
  before: WorkflowRepoSnapshot;
126
130
  after: WorkflowRepoSnapshot;
127
131
  drift: {
128
- branch_changed: boolean;
129
- head_changed: boolean;
130
- dirty_paths_added: string[];
131
- dirty_paths_cleared: string[];
132
- dirty_paths_retained: string[];
133
- /** True when snapshots cannot prove whether every retained dirty path changed. */
132
+ /** Null means the snapshots do not establish this comparison. */
133
+ branch_changed: boolean | null;
134
+ head_changed: boolean | null;
135
+ dirty_paths_added: string[] | null;
136
+ dirty_paths_cleared: string[] | null;
137
+ dirty_paths_retained: string[] | null;
138
+ /** True when an observation, comparison, or retained-path change is unknown. */
134
139
  incomplete: boolean;
135
140
  note?: string;
136
141
  };
@@ -305,15 +310,16 @@ export interface WorkflowSpecialistProfile {
305
310
  maxTurns?: number;
306
311
  }
307
312
  /**
308
- * Why a failed run was uninformative about the work (ADR 0046). Absent means
309
- * the attempt produced information about the work (or succeeded), which is the
310
- * default and is charged against the attempt budget exactly as before.
313
+ * The positively identified transport cause of a failed spawn (ADR 0046).
314
+ * This class alone proves neither absence of effects nor retry eligibility.
311
315
  *
312
316
  * - `environment`: the run never started — a precondition was missing (the
313
- * vendor binary was absent). Uncharged AND not retried: retrying an unchanged
317
+ * vendor binary was absent). With positive no-effect/output evidence it is
318
+ * uncharged and not retried: retrying an unchanged
314
319
  * environment cannot help, so the work item stops and names the precondition.
315
320
  * - `upstream`: the vendor was reached and refused (5xx, 429, circuit open).
316
- * Uncharged, but retry stays available (bounded by max_uncharged_attempts).
321
+ * Retry/free-attempt eligibility also requires positive no-effect evidence,
322
+ * observed absent output, and no refusal. Unsupported evidence stays unknown.
317
323
  */
318
324
  export type SpawnFailureClass = "environment" | "upstream";
319
325
  /**
@@ -333,6 +339,43 @@ export type SpawnFailureClass = "environment" | "upstream";
333
339
  * waiting for is a person who was never told.
334
340
  */
335
341
  export type RunFailureClass = SpawnFailureClass | "decision";
342
+ /** Positive claims name the observation that supports them. A transport error
343
+ * or an empty reply is never evidence that no effects occurred. */
344
+ export type SpawnEffectEvidence = {
345
+ state: "unknown";
346
+ } | {
347
+ state: "none";
348
+ source: "spawn_errno";
349
+ reference: "ENOENT";
350
+ } | {
351
+ state: "none" | "observed";
352
+ source: "adapter_evidence";
353
+ reference: string;
354
+ };
355
+ export type SpawnRefusalEvidence = {
356
+ state: "unknown";
357
+ } | {
358
+ state: "refused";
359
+ source: "vendor_envelope";
360
+ reference: string;
361
+ };
362
+ export type SpawnOutputSource = "final_message" | "vendor_envelope" | "stdout";
363
+ export type SpawnPartialOutputEvidence = {
364
+ state: "unknown";
365
+ } | {
366
+ state: "absent";
367
+ source: SpawnOutputSource;
368
+ } | {
369
+ state: "present";
370
+ source: SpawnOutputSource;
371
+ bytes: number;
372
+ sha256: string;
373
+ };
374
+ export interface SpawnFailureEvidence {
375
+ effect: SpawnEffectEvidence;
376
+ refusal: SpawnRefusalEvidence;
377
+ partial_output: SpawnPartialOutputEvidence;
378
+ }
336
379
  /** What a spawn adapter returns for one subagent run. */
337
380
  export interface SpawnResult {
338
381
  ok: boolean;
@@ -344,9 +387,13 @@ export interface SpawnResult {
344
387
  durationMs: number;
345
388
  /** Populated when ok=false. */
346
389
  error?: string;
347
- /** Set when ok=false and the failure was positively identified as
348
- * uninformative about the work. Absent ⇒ a work failure (charged). */
390
+ /** Transport classification when ok=false; never sufficient by itself to
391
+ * grant free work or automatic redispatch. */
349
392
  class?: SpawnFailureClass;
393
+ /** Transport classification alone grants neither a retry nor free work.
394
+ * Absent evidence is explicitly unknown. Positive adapter_evidence claims
395
+ * require a documented producer observation, never an HTTP/text heuristic. */
396
+ failureEvidence?: SpawnFailureEvidence;
350
397
  }
351
398
  export interface SpawnRequest {
352
399
  prompt: string;