@phyzess/pappra 0.1.0-beta.13 → 0.1.0-beta.18

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 (357) hide show
  1. package/README.md +134 -15
  2. package/assets/integrations/skills/pappra/SKILL.md +28 -7
  3. package/assets/integrations/skills/pappra/references/collaboration.md +633 -0
  4. package/assets/integrations/skills/pappra/references/environment.md +360 -0
  5. package/assets/integrations/skills/pappra/references/hosts.md +20 -0
  6. package/assets/integrations/skills/pappra/references/operations.md +36 -5
  7. package/dist/adapters/pi/recovery-extension.d.ts +65 -0
  8. package/dist/adapters/pi/recovery-extension.js +238 -0
  9. package/dist/adapters/pi/session-transition-extension.d.ts +30 -0
  10. package/dist/adapters/pi/session-transition-extension.js +265 -0
  11. package/dist/app/automatic-handoff-events.d.ts +26 -0
  12. package/dist/app/automatic-handoff-events.js +134 -0
  13. package/dist/app/bounded-file.d.ts +2 -0
  14. package/dist/app/bounded-file.js +2 -0
  15. package/dist/app/capability-evidence/claude.d.ts +37 -0
  16. package/dist/app/capability-evidence/claude.js +206 -0
  17. package/dist/app/capability-evidence/codex.d.ts +35 -0
  18. package/dist/app/capability-evidence/codex.js +193 -0
  19. package/dist/app/capability-evidence/index.d.ts +33 -0
  20. package/dist/app/capability-evidence/index.js +117 -0
  21. package/dist/app/capability-evidence/pi.d.ts +73 -0
  22. package/dist/app/capability-evidence/pi.js +669 -0
  23. package/dist/app/capability-evidence/shared.d.ts +69 -0
  24. package/dist/app/capability-evidence/shared.js +195 -0
  25. package/dist/app/collab-auto-handoff.d.ts +55 -0
  26. package/dist/app/collab-auto-handoff.js +384 -0
  27. package/dist/app/collab-bindings.d.ts +57 -0
  28. package/dist/app/collab-bindings.js +1006 -0
  29. package/dist/app/collab-bootstrap-transport.d.ts +163 -0
  30. package/dist/app/collab-bootstrap-transport.js +769 -0
  31. package/dist/app/collab-bootstrap.d.ts +137 -0
  32. package/dist/app/collab-bootstrap.js +3455 -0
  33. package/dist/app/collab-handoff.d.ts +12 -0
  34. package/dist/app/collab-handoff.js +511 -0
  35. package/dist/app/collab-messages.d.ts +2 -0
  36. package/dist/app/collab-messages.js +2 -0
  37. package/dist/app/collab-notify.d.ts +156 -0
  38. package/dist/app/collab-notify.js +1208 -0
  39. package/dist/app/collab-preflight.d.ts +34 -0
  40. package/dist/app/collab-preflight.js +96 -0
  41. package/dist/app/collab-recovery-executor.d.ts +161 -0
  42. package/dist/app/collab-recovery-executor.js +1153 -0
  43. package/dist/app/collab-recovery-projection.d.ts +103 -0
  44. package/dist/app/collab-recovery-projection.js +659 -0
  45. package/dist/app/collab-recovery.d.ts +25 -0
  46. package/dist/app/collab-recovery.js +429 -0
  47. package/dist/app/collab-resources.d.ts +16 -0
  48. package/dist/app/collab-resources.js +278 -0
  49. package/dist/app/collab-role-events.d.ts +22 -0
  50. package/dist/app/collab-role-events.js +329 -0
  51. package/dist/app/collab-role-orchestration.d.ts +53 -0
  52. package/dist/app/collab-role-orchestration.js +601 -0
  53. package/dist/app/collab-role-plan.d.ts +81 -0
  54. package/dist/app/collab-role-plan.js +504 -0
  55. package/dist/app/collab-role-repair.d.ts +109 -0
  56. package/dist/app/collab-role-repair.js +821 -0
  57. package/dist/app/collab-rollover.d.ts +263 -0
  58. package/dist/app/collab-rollover.js +1112 -0
  59. package/dist/app/collab-route.d.ts +64 -0
  60. package/dist/app/collab-route.js +240 -0
  61. package/dist/app/collab-session-transition.d.ts +221 -0
  62. package/dist/app/collab-session-transition.js +3334 -0
  63. package/dist/app/collab-state.d.ts +7 -0
  64. package/dist/app/collab-state.js +162 -0
  65. package/dist/app/collab-task-store.d.ts +22 -0
  66. package/dist/app/collab-task-store.js +204 -0
  67. package/dist/app/collab-work-execution.d.ts +127 -0
  68. package/dist/app/collab-work-execution.js +1027 -0
  69. package/dist/app/collab-work-plan.d.ts +87 -0
  70. package/dist/app/collab-work-plan.js +437 -0
  71. package/dist/app/collaboration.d.ts +8 -2
  72. package/dist/app/collaboration.js +101 -12
  73. package/dist/app/doctor.js +108 -5
  74. package/dist/app/host-mcp.js +34 -8
  75. package/dist/app/host-probe.js +4 -4
  76. package/dist/app/init.d.ts +58 -4
  77. package/dist/app/init.js +768 -155
  78. package/dist/app/managed-pi-bootstrap-admission.d.ts +14 -0
  79. package/dist/app/managed-pi-bootstrap-admission.js +104 -0
  80. package/dist/app/managed-pi-bootstrap-refresh.d.ts +10 -0
  81. package/dist/app/managed-pi-bootstrap-refresh.js +78 -0
  82. package/dist/app/managed-pi-delivery.d.ts +8 -0
  83. package/dist/app/managed-pi-delivery.js +110 -0
  84. package/dist/app/managed-pi-handoff-receiver.d.ts +15 -0
  85. package/dist/app/managed-pi-handoff-receiver.js +263 -0
  86. package/dist/app/managed-pi-handoff-reference.d.ts +3 -0
  87. package/dist/app/managed-pi-handoff-reference.js +60 -0
  88. package/dist/app/managed-pi-herdr.d.ts +21 -0
  89. package/dist/app/managed-pi-herdr.js +185 -0
  90. package/dist/app/managed-pi-initial-admission.d.ts +28 -0
  91. package/dist/app/managed-pi-initial-admission.js +416 -0
  92. package/dist/app/managed-pi-initial-launch.d.ts +5 -0
  93. package/dist/app/managed-pi-initial-launch.js +61 -0
  94. package/dist/app/managed-pi-native-observation.d.ts +36 -0
  95. package/dist/app/managed-pi-native-observation.js +191 -0
  96. package/dist/app/managed-pi-readiness.d.ts +29 -0
  97. package/dist/app/managed-pi-readiness.js +288 -0
  98. package/dist/app/managed-pi-repair-stop.d.ts +21 -0
  99. package/dist/app/managed-pi-repair-stop.js +371 -0
  100. package/dist/app/managed-pi-resume-admission.d.ts +32 -0
  101. package/dist/app/managed-pi-resume-admission.js +618 -0
  102. package/dist/app/managed-pi-retirement.d.ts +28 -0
  103. package/dist/app/managed-pi-retirement.js +112 -0
  104. package/dist/app/managed-pi-sdk-control.d.ts +7 -0
  105. package/dist/app/managed-pi-sdk-control.js +169 -0
  106. package/dist/app/managed-pi-sdk-runtime.d.ts +3 -0
  107. package/dist/app/managed-pi-sdk-runtime.js +483 -0
  108. package/dist/app/managed-pi-source-admission.d.ts +14 -0
  109. package/dist/app/managed-pi-source-admission.js +323 -0
  110. package/dist/app/managed-pi-transition-host.d.ts +36 -0
  111. package/dist/app/managed-pi-transition-host.js +292 -0
  112. package/dist/app/managed-pi.d.ts +54 -0
  113. package/dist/app/managed-pi.js +248 -0
  114. package/dist/app/shell-readiness.d.ts +66 -0
  115. package/dist/app/shell-readiness.js +368 -0
  116. package/dist/app/task-pin.d.ts +2 -23
  117. package/dist/app/task-pin.js +2 -113
  118. package/dist/cli/commands/collaboration-binding.d.ts +13 -0
  119. package/dist/cli/commands/collaboration-binding.js +118 -0
  120. package/dist/cli/commands/collaboration-bootstrap.d.ts +10 -0
  121. package/dist/cli/commands/collaboration-bootstrap.js +84 -0
  122. package/dist/cli/commands/collaboration-capabilities.d.ts +13 -0
  123. package/dist/cli/commands/collaboration-capabilities.js +127 -0
  124. package/dist/cli/commands/collaboration-handoff.d.ts +13 -0
  125. package/dist/cli/commands/collaboration-handoff.js +102 -0
  126. package/dist/cli/commands/collaboration-message.d.ts +17 -0
  127. package/dist/cli/commands/collaboration-message.js +177 -0
  128. package/dist/cli/commands/collaboration-plan.d.ts +8 -0
  129. package/dist/cli/commands/collaboration-plan.js +108 -0
  130. package/dist/cli/commands/collaboration-preflight.d.ts +10 -0
  131. package/dist/cli/commands/collaboration-preflight.js +167 -0
  132. package/dist/cli/commands/collaboration-recovery-run.d.ts +46 -0
  133. package/dist/cli/commands/collaboration-recovery-run.js +224 -0
  134. package/dist/cli/commands/collaboration-recovery.d.ts +11 -0
  135. package/dist/cli/commands/collaboration-recovery.js +104 -0
  136. package/dist/cli/commands/collaboration-resources.d.ts +11 -0
  137. package/dist/cli/commands/collaboration-resources.js +96 -0
  138. package/dist/cli/commands/collaboration-role-plan.d.ts +9 -0
  139. package/dist/cli/commands/collaboration-role-plan.js +69 -0
  140. package/dist/cli/commands/collaboration-role-run.d.ts +11 -0
  141. package/dist/cli/commands/collaboration-role-run.js +128 -0
  142. package/dist/cli/commands/collaboration-rollover.d.ts +10 -0
  143. package/dist/cli/commands/collaboration-rollover.js +103 -0
  144. package/dist/cli/commands/collaboration-state.d.ts +10 -0
  145. package/dist/cli/commands/collaboration-state.js +96 -0
  146. package/dist/cli/commands/collaboration-transition.d.ts +10 -0
  147. package/dist/cli/commands/collaboration-transition.js +75 -0
  148. package/dist/cli/commands/collaboration-work-run.d.ts +34 -0
  149. package/dist/cli/commands/collaboration-work-run.js +178 -0
  150. package/dist/cli/commands/collaboration.d.ts +4 -0
  151. package/dist/cli/commands/collaboration.js +39 -4
  152. package/dist/cli/commands/continuity.js +2 -6
  153. package/dist/cli/commands/docs.d.ts +2 -0
  154. package/dist/cli/commands/docs.js +89 -15
  155. package/dist/cli/commands/host.d.ts +7 -1
  156. package/dist/cli/commands/host.js +122 -7
  157. package/dist/cli/commands/init.d.ts +20 -3
  158. package/dist/cli/commands/init.js +897 -232
  159. package/dist/cli/commands/mcp.d.ts +1 -0
  160. package/dist/cli/commands/mcp.js +11 -5
  161. package/dist/cli/commands/repair.d.ts +12 -0
  162. package/dist/cli/commands/repair.js +41 -0
  163. package/dist/cli/commands/runtime.d.ts +1 -1
  164. package/dist/cli/commands/runtime.js +139 -38
  165. package/dist/cli/commands/session.d.ts +7 -0
  166. package/dist/cli/commands/session.js +82 -0
  167. package/dist/cli/commands/task.js +19 -5
  168. package/dist/cli/run.js +866 -24
  169. package/dist/code/identity.d.ts +7 -0
  170. package/dist/code/identity.js +7 -3
  171. package/dist/code/registration.js +7 -0
  172. package/dist/code/service.d.ts +9 -0
  173. package/dist/code/service.js +16 -3
  174. package/dist/code/text.js +19 -7
  175. package/dist/collab/admission.d.ts +91 -66
  176. package/dist/collab/admission.js +21 -6
  177. package/dist/collab/canonical.d.ts +1 -1
  178. package/dist/collab/canonical.js +44 -3
  179. package/dist/collab/envelope.d.ts +2 -2
  180. package/dist/collab/envelope.js +12 -12
  181. package/dist/collab/first-work.d.ts +3 -0
  182. package/dist/collab/first-work.js +15 -0
  183. package/dist/collab/handoff.d.ts +16 -0
  184. package/dist/collab/handoff.js +39 -5
  185. package/dist/collab/index.d.ts +97 -1
  186. package/dist/collab/index.js +9 -1
  187. package/dist/collab/messages.js +126 -3
  188. package/dist/collab/persisted-messages.d.ts +123 -0
  189. package/dist/collab/persisted-messages.js +496 -0
  190. package/dist/collab/preflight.d.ts +1 -1
  191. package/dist/collab/resources.d.ts +13 -3
  192. package/dist/collab/resources.js +37 -6
  193. package/dist/collab/rollover.d.ts +311 -0
  194. package/dist/collab/rollover.js +1024 -0
  195. package/dist/collab/store.d.ts +1 -0
  196. package/dist/collab/store.js +1 -4
  197. package/dist/collab/types.d.ts +2 -354
  198. package/dist/collab/validation.d.ts +2 -10
  199. package/dist/collab/validation.js +2 -303
  200. package/dist/collab/work-plan.d.ts +105 -0
  201. package/dist/collab/work-plan.js +843 -0
  202. package/dist/continuity/context.d.ts +11 -2
  203. package/dist/continuity/context.js +100 -13
  204. package/dist/continuity/handoff-bundle.d.ts +296 -0
  205. package/dist/continuity/handoff-bundle.js +477 -0
  206. package/dist/continuity/index.d.ts +2 -0
  207. package/dist/continuity/index.js +1 -0
  208. package/dist/continuity/paths.js +10 -2
  209. package/dist/continuity/service.js +91 -15
  210. package/dist/continuity/task-pin.d.ts +24 -0
  211. package/dist/continuity/task-pin.js +590 -0
  212. package/dist/doctor.d.ts +1 -0
  213. package/dist/doctor.js +6 -0
  214. package/dist/experimental/index.d.ts +13 -0
  215. package/dist/experimental/index.js +25 -0
  216. package/dist/experimental/pi.d.ts +4 -0
  217. package/dist/experimental/pi.js +12 -0
  218. package/dist/hosts/claude/native.js +1 -1
  219. package/dist/hosts/codex/native.js +1 -1
  220. package/dist/hosts/codex/protocol.js +1 -1
  221. package/dist/hosts/integration/bootstrap-config.d.ts +58 -0
  222. package/dist/hosts/integration/bootstrap-config.js +254 -0
  223. package/dist/hosts/integration/bundle-export.d.ts +44 -0
  224. package/dist/hosts/integration/bundle-export.js +366 -3
  225. package/dist/hosts/integration/bundle.js +3 -0
  226. package/dist/hosts/integration/hook.js +36 -9
  227. package/dist/hosts/integration/pappra-bootstrap-extension.mjs +80 -0
  228. package/dist/hosts/integration/pappra-extension-runtime.mjs +59 -24631
  229. package/dist/hosts/integration/pappra-hook.mjs +61 -24749
  230. package/dist/hosts/integration/pappra-recovery-extension.mjs +259 -0
  231. package/dist/hosts/integration/pappra-session-transition-extension.mjs +276 -0
  232. package/dist/hosts/integration/pi-bootstrap-extension.d.ts +36 -0
  233. package/dist/hosts/integration/pi-bootstrap-extension.js +101 -0
  234. package/dist/hosts/integration/pi-bootstrap-guard.d.ts +125 -0
  235. package/dist/hosts/integration/pi-bootstrap-guard.js +438 -0
  236. package/dist/hosts/integration/pi-bootstrap-runtime.d.ts +63 -0
  237. package/dist/hosts/integration/pi-bootstrap-runtime.js +223 -0
  238. package/dist/hosts/integration/pi-native-events.d.ts +112 -0
  239. package/dist/hosts/integration/pi-native-events.js +348 -0
  240. package/dist/hosts/integration/pi-rollover-extension.d.ts +166 -0
  241. package/dist/hosts/integration/pi-rollover-extension.js +431 -0
  242. package/dist/hosts/integration/recovery-config.d.ts +48 -0
  243. package/dist/hosts/integration/recovery-config.js +117 -0
  244. package/dist/hosts/integration/service.d.ts +6 -1
  245. package/dist/hosts/integration/service.js +485 -35
  246. package/dist/hosts/integration/support.d.ts +7 -0
  247. package/dist/hosts/integration/support.js +9 -1
  248. package/dist/hosts/integration/templates.d.ts +3 -0
  249. package/dist/hosts/integration/templates.js +26 -3
  250. package/dist/hosts/pi/managed-host-identity.d.ts +2 -0
  251. package/dist/hosts/pi/managed-host-identity.js +40 -0
  252. package/dist/hosts/pi/managed-provider-diagnostics.d.ts +6 -0
  253. package/dist/hosts/pi/managed-provider-diagnostics.js +184 -0
  254. package/dist/hosts/pi/managed-sdk-session.d.ts +335 -0
  255. package/dist/hosts/pi/managed-sdk-session.js +1639 -0
  256. package/dist/hosts/pi/managed-session-extension.d.ts +20 -0
  257. package/dist/hosts/pi/managed-session-extension.js +84 -0
  258. package/dist/hosts/pi/managed-session.d.ts +177 -0
  259. package/dist/hosts/pi/managed-session.js +1557 -0
  260. package/dist/hosts/pi/protocol.js +1 -1
  261. package/dist/hosts/pi/query-result.js +1 -1
  262. package/dist/hosts/pi/resume-capability.d.ts +41 -0
  263. package/dist/hosts/pi/resume-capability.js +33 -0
  264. package/dist/hosts/pi/rpc.d.ts +182 -134
  265. package/dist/hosts/pi/rpc.js +230 -37
  266. package/dist/hosts/pilot.d.ts +97 -74
  267. package/dist/hosts/pilot.js +5 -1
  268. package/dist/hosts/version.d.ts +3 -0
  269. package/dist/hosts/version.js +18 -0
  270. package/dist/index.d.ts +0 -1
  271. package/dist/index.js +0 -1
  272. package/dist/infra/bounded-file.d.ts +19 -0
  273. package/dist/infra/bounded-file.js +66 -0
  274. package/dist/infra/fs.js +10 -2
  275. package/dist/infra/git.d.ts +29 -0
  276. package/dist/infra/git.js +45 -9
  277. package/dist/infra/json-property.d.ts +2 -0
  278. package/dist/infra/json-property.js +69 -0
  279. package/dist/infra/json.d.ts +4 -0
  280. package/dist/infra/json.js +24 -0
  281. package/dist/infra/owned-process-group.js +1 -1
  282. package/dist/infra/private-lock.d.ts +16 -0
  283. package/dist/infra/private-lock.js +148 -0
  284. package/dist/infra/private-state.d.ts +7 -0
  285. package/dist/infra/private-state.js +146 -0
  286. package/dist/infra/process-identity.d.ts +12 -0
  287. package/dist/infra/process-identity.js +142 -0
  288. package/dist/infra/validation.d.ts +5 -0
  289. package/dist/infra/validation.js +15 -0
  290. package/dist/init.d.ts +1 -0
  291. package/dist/init.js +6 -0
  292. package/dist/install/apply.d.ts +9 -1
  293. package/dist/install/apply.js +176 -12
  294. package/dist/install/dependencies.d.ts +38 -0
  295. package/dist/install/dependencies.js +282 -0
  296. package/dist/install/ownership.js +30 -1
  297. package/dist/install/references.d.ts +31 -0
  298. package/dist/install/references.js +208 -0
  299. package/dist/install/repair.d.ts +37 -0
  300. package/dist/install/repair.js +798 -0
  301. package/dist/install/reset-local.d.ts +16 -0
  302. package/dist/install/reset-local.js +304 -0
  303. package/dist/install/rollback.d.ts +3 -0
  304. package/dist/install/rollback.js +9 -3
  305. package/dist/install/runtime.d.ts +10 -6
  306. package/dist/install/runtime.js +299 -59
  307. package/dist/install/source-fingerprint.d.ts +2 -0
  308. package/dist/install/source-fingerprint.js +103 -0
  309. package/dist/knowledge/catalog.d.ts +1 -0
  310. package/dist/knowledge/catalog.js +143 -10
  311. package/dist/knowledge/journal.js +17 -7
  312. package/dist/knowledge/paths.js +2 -1
  313. package/dist/knowledge/refs.js +4 -0
  314. package/dist/knowledge/register-batch.d.ts +4 -0
  315. package/dist/knowledge/register-batch.js +225 -0
  316. package/dist/knowledge/relocate.d.ts +5 -0
  317. package/dist/knowledge/relocate.js +92 -0
  318. package/dist/knowledge/scan.js +103 -47
  319. package/dist/knowledge/service.d.ts +3 -0
  320. package/dist/knowledge/service.js +33 -1
  321. package/dist/knowledge/write.d.ts +4 -1
  322. package/dist/knowledge/write.js +1 -1
  323. package/dist/mcp/server.d.ts +4 -2
  324. package/dist/mcp/server.js +87 -136
  325. package/dist/schemas/collaboration-state-validation.d.ts +6 -0
  326. package/dist/schemas/collaboration-state-validation.js +342 -0
  327. package/dist/schemas/collaboration-state.d.ts +436 -0
  328. package/dist/schemas/collaboration-state.js +1 -0
  329. package/dist/schemas/collaboration.d.ts +335 -0
  330. package/dist/schemas/collaboration.js +272 -0
  331. package/dist/schemas/continuity.d.ts +8 -0
  332. package/dist/schemas/continuity.js +13 -3
  333. package/dist/schemas/doctor.d.ts +1 -0
  334. package/dist/schemas/doctor.js +1 -0
  335. package/dist/schemas/document.d.ts +61 -0
  336. package/dist/schemas/document.js +9 -0
  337. package/dist/schemas/errors.d.ts +1 -0
  338. package/dist/schemas/errors.js +1 -0
  339. package/dist/schemas/host.d.ts +24 -0
  340. package/dist/schemas/host.js +19 -0
  341. package/dist/schemas/install.d.ts +8 -2
  342. package/dist/schemas/install.js +11 -1
  343. package/dist/schemas/managed-pi-execution.d.ts +16 -0
  344. package/dist/schemas/managed-pi-execution.js +2 -0
  345. package/dist/schemas/outcome.d.ts +9 -5
  346. package/dist/workspace/config-file.js +1 -3
  347. package/dist/workspace/documents.js +12 -2
  348. package/dist/workspace/repositories.d.ts +7 -0
  349. package/dist/workspace/repositories.js +14 -5
  350. package/package.json +35 -35
  351. package/skills/pappra/SKILL.md +28 -7
  352. package/skills/pappra/references/collaboration.md +633 -0
  353. package/skills/pappra/references/environment.md +360 -0
  354. package/skills/pappra/references/hosts.md +20 -0
  355. package/skills/pappra/references/operations.md +36 -5
  356. package/dist/app/collab-from.d.ts +0 -35
  357. package/dist/app/collab-from.js +0 -84
@@ -0,0 +1,1027 @@
1
+ import { createHash } from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { buildMessage, contentDigest, messageFile, } from '../collab/envelope.js';
5
+ import { pathRelation } from '../collab/canonical.js';
6
+ import { submitMessage } from '../collab/messages.js';
7
+ import { readDeliveryRecord, readRegisteredMessageSafe, } from './collab-messages.js';
8
+ import { assertTaskStoreCurrent, openTaskStore, } from './collab-task-store.js';
9
+ import { readBoundedFile } from './bounded-file.js';
10
+ import { runCollaborationPreflight } from './collab-preflight.js';
11
+ import { inspectWorkPlan } from './collab-work-plan.js';
12
+ import { createHerdrTransport, notifyRegisteredMessage, } from './collab-notify.js';
13
+ import { collabBindingSchema } from '../schemas/collaboration.js';
14
+ import { isAssignment, isCard, } from '../collab/contracts.js';
15
+ import { validateWorkPlan, } from '../collab/work-plan.js';
16
+ export const COLLAB_WORK_EXECUTION_SCHEMA = 'pappra/collab-work-execution/1';
17
+ const MAX_FILE_BYTES = 1024 * 1024;
18
+ const MAX_CONCURRENCY = 8;
19
+ const MAX_NODES = 64;
20
+ const LIMITS = Object.freeze([
21
+ 'visible-bound-participants-only',
22
+ 'role-labels-do-not-grant-authority',
23
+ 'coordinator-acceptance-required-for-dependencies',
24
+ 'durable-intent-before-notification',
25
+ 'unknown-delivery-requires-reconciliation',
26
+ 'no-automatic-merge-or-approval',
27
+ 'no-native-fencing-or-session-launch',
28
+ ]);
29
+ function sha256(value) {
30
+ return `sha256:${createHash('sha256').update(value).digest('hex')}`;
31
+ }
32
+ function isObject(value) {
33
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
34
+ }
35
+ function parseJson(text) {
36
+ return JSON.parse(text);
37
+ }
38
+ function readJsonFile(file) {
39
+ const read = readBoundedFile(file, MAX_FILE_BYTES);
40
+ if (!read.ok)
41
+ return { ok: false, reason: `file_${read.code}` };
42
+ try {
43
+ return { ok: true, value: parseJson(read.text), digest: read.digest };
44
+ }
45
+ catch {
46
+ return { ok: false, reason: 'file_invalid_json' };
47
+ }
48
+ }
49
+ function sameIdentity(left, right) {
50
+ return (left.agent_id === right.agent_id &&
51
+ left.session_id === right.session_id &&
52
+ left.harness === right.harness);
53
+ }
54
+ function samePerson(left, right) {
55
+ return (left.agent_id === right.agent_id && left.session_id === right.session_id);
56
+ }
57
+ function sameResources(left, right) {
58
+ const key = (resource) => `${resource.access}\0${resource.path}`;
59
+ const a = left.map(key).sort();
60
+ const b = right.map(key).sort();
61
+ return a.length === b.length && a.every((value, index) => value === b[index]);
62
+ }
63
+ function nodeResult(node, status, blockers, extras = {}) {
64
+ return {
65
+ node_id: node.node_id,
66
+ role: node.role,
67
+ assignment_id: node.assignment_id,
68
+ attempt: node.attempt,
69
+ input_revision: node.input_revision,
70
+ work_plan_status: 'unknown',
71
+ execution_status: status,
72
+ blockers: [...new Set(blockers)].sort(),
73
+ next_action: null,
74
+ assignment: { ref: null, digest: null },
75
+ capabilities: { ref: null, digest: null },
76
+ intent_message_id: null,
77
+ delivery: null,
78
+ acceptance: 'not-applicable',
79
+ ...extras,
80
+ };
81
+ }
82
+ function staticBindingCheck(file, taskRoot, taskId, caller, callerPane, assignment) {
83
+ const loaded = readJsonFile(file);
84
+ if (!loaded.ok)
85
+ return { ok: false, reason: `binding_${loaded.reason}` };
86
+ const binding = collabBindingSchema.safeParse(loaded.value);
87
+ if (!binding.success)
88
+ return { ok: false, reason: 'binding_invalid' };
89
+ const ids = binding.data.participants.map((item) => item.agent_id);
90
+ const herdrNames = binding.data.participants.map((item) => item.herdr_agent);
91
+ if (new Set(ids).size !== ids.length ||
92
+ new Set(herdrNames).size !== herdrNames.length)
93
+ return { ok: false, reason: 'binding_ambiguous' };
94
+ let canonicalRoot;
95
+ try {
96
+ canonicalRoot = fs.realpathSync(taskRoot);
97
+ }
98
+ catch {
99
+ return { ok: false, reason: 'task_root_unavailable' };
100
+ }
101
+ if (binding.data.task_id !== taskId ||
102
+ path.resolve(binding.data.task_root) !== canonicalRoot)
103
+ return { ok: false, reason: 'binding_task_mismatch' };
104
+ const sender = binding.data.participants.find((item) => item.agent_id === caller.agent_id);
105
+ const recipient = binding.data.participants.find((item) => item.agent_id === assignment.assignee.agent_id);
106
+ if (!sender || !recipient)
107
+ return { ok: false, reason: 'binding_participant_missing' };
108
+ if (sender.harness !== caller.harness ||
109
+ sender.cwd !== caller.cwd ||
110
+ sender.pane_id !== callerPane ||
111
+ (sender.native_session_id !== null &&
112
+ sender.native_session_id !== caller.session_id))
113
+ return { ok: false, reason: 'binding_sender_mismatch' };
114
+ if (recipient.harness !== assignment.assignee.harness ||
115
+ recipient.cwd !== assignment.assignee.cwd ||
116
+ (recipient.native_session_id !== null &&
117
+ recipient.native_session_id !== assignment.assignee.session_id))
118
+ return { ok: false, reason: 'binding_recipient_mismatch' };
119
+ return { ok: true, digest: loaded.digest };
120
+ }
121
+ function currentAttempt(record, node) {
122
+ return (record.attempts.find((item) => item.subtask_id === node.assignment_id && item.attempt === node.attempt) ?? null);
123
+ }
124
+ function activeClaimFor(record, node, resourcePath) {
125
+ return (record.claims.find((claim) => claim.released_at === null &&
126
+ claim.path === resourcePath &&
127
+ claim.access === 'write' &&
128
+ claim.subtask_id === node.assignment_id &&
129
+ claim.attempt === node.attempt &&
130
+ claim.holder.agent_id === node.assignee.agent_id &&
131
+ claim.holder.session_id === node.assignee.session_id &&
132
+ Number.isSafeInteger(claim.resource_epoch) &&
133
+ claim.resource_epoch > 0) ?? null);
134
+ }
135
+ function expectedCause(record, node, attempt, assignmentDigest, caller) {
136
+ return {
137
+ task_id: record.task_id,
138
+ assignment_id: node.assignment_id,
139
+ attempt: node.attempt,
140
+ input_revision: node.input_revision,
141
+ owner_epoch: record.coordinator?.owner_epoch ?? null,
142
+ assignment_digest: assignmentDigest,
143
+ assignee: {
144
+ agent_id: node.assignee.agent_id,
145
+ session_id: node.assignee.session_id,
146
+ },
147
+ coordinator: {
148
+ agent_id: caller.agent_id,
149
+ session_id: caller.session_id,
150
+ harness: caller.harness,
151
+ },
152
+ assignment_message_id: attempt.assignment_message_id,
153
+ };
154
+ }
155
+ function createIntent(cause, record, attempt, assignment) {
156
+ const causeDigest = sha256(JSON.stringify(cause));
157
+ const messageId = `msg-work-intent-${causeDigest.slice('sha256:'.length, 48)}`;
158
+ return buildMessage({
159
+ message_id: messageId,
160
+ type: 'STATUS_QUERY',
161
+ task_id: record.task_id,
162
+ from: {
163
+ agent_id: assignment.coordinator.agent_id,
164
+ session_id: assignment.coordinator.session_id,
165
+ harness: assignment.coordinator.harness ?? null,
166
+ },
167
+ to: {
168
+ agent_id: assignment.assignee.agent_id,
169
+ session_id: assignment.assignee.session_id,
170
+ harness: assignment.assignee.harness,
171
+ },
172
+ reply_to: attempt.assignment_message_id,
173
+ subtask_id: attempt.subtask_id,
174
+ attempt: attempt.attempt,
175
+ owner_epoch: assignment.coordinator.owner_epoch,
176
+ body: { work_execution_intent: { cause_id: causeDigest, ...cause } },
177
+ });
178
+ }
179
+ function reportPointers(record, plan, messages) {
180
+ const pointers = [];
181
+ for (const node of plan.nodes) {
182
+ const results = record.results.filter((result) => result.classification === 'applied' &&
183
+ result.subtask_id === node.assignment_id &&
184
+ result.attempt === node.attempt);
185
+ const result = results[results.length - 1];
186
+ if (!result)
187
+ continue;
188
+ const report = messages.get(result.message_id);
189
+ if (!report)
190
+ continue;
191
+ const outputs = (Array.isArray(result.outputs) ? result.outputs : [])
192
+ .flatMap((output) => {
193
+ if (!isObject(output) ||
194
+ typeof output.ref !== 'string' ||
195
+ typeof output.digest !== 'string')
196
+ return [];
197
+ return [{ ref: output.ref, digest: output.digest }];
198
+ })
199
+ .sort((left, right) => left.ref.localeCompare(right.ref) ||
200
+ left.digest.localeCompare(right.digest));
201
+ pointers.push({
202
+ node_id: node.node_id,
203
+ assignment_id: node.assignment_id,
204
+ attempt: node.attempt,
205
+ status: result.status,
206
+ message_id: result.message_id,
207
+ digest: report.content_digest ?? '',
208
+ input_revision: result.input_revision,
209
+ evidence_ref: result.evidence_ref,
210
+ outputs,
211
+ });
212
+ }
213
+ return pointers.sort((left, right) => left.node_id.localeCompare(right.node_id) ||
214
+ left.message_id.localeCompare(right.message_id));
215
+ }
216
+ function makeReport(input) {
217
+ const nodes = [...input.nodes].sort((left, right) => left.node_id.localeCompare(right.node_id));
218
+ const accepted = [...input.accepted].sort();
219
+ const awaiting = [...input.awaiting].sort();
220
+ const rejected = [...input.rejected].sort();
221
+ const uncertain = nodes.some((node) => node.execution_status === 'reconciliation-required');
222
+ const changed = nodes.some((node) => node.execution_status === 'dispatched');
223
+ const blocked = nodes.some((node) => node.execution_status === 'blocked');
224
+ const status = input.status ??
225
+ (uncertain
226
+ ? 'needs-reconciliation'
227
+ : changed
228
+ ? 'applied'
229
+ : input.apply && blocked
230
+ ? 'blocked'
231
+ : 'planned');
232
+ const diagnostics = [...(input.diagnostics ?? [])].sort();
233
+ return {
234
+ schema: COLLAB_WORK_EXECUTION_SCHEMA,
235
+ ok: status === 'planned' || status === 'applied',
236
+ status,
237
+ diagnostics,
238
+ apply: input.apply,
239
+ task_root: input.taskRoot,
240
+ task_id: input.taskId,
241
+ plan: {
242
+ ref: input.planFile,
243
+ digest: input.planDigest,
244
+ plan_id: input.planId,
245
+ owner_epoch: input.ownerEpoch,
246
+ },
247
+ concurrency: {
248
+ limit: input.concurrency,
249
+ selected: nodes.filter((node) => node.execution_status === 'planned' ||
250
+ node.execution_status === 'dispatched' ||
251
+ node.execution_status === 'reconciliation-required').length,
252
+ actual_overlap: input.actualOverlap ?? false,
253
+ },
254
+ nodes,
255
+ reports: [...input.reports].sort((left, right) => left.node_id.localeCompare(right.node_id) ||
256
+ left.message_id.localeCompare(right.message_id)),
257
+ acceptance: {
258
+ accepted_nodes: accepted,
259
+ awaiting_acceptance_nodes: awaiting,
260
+ rejected_nodes: rejected,
261
+ },
262
+ limits: [...LIMITS],
263
+ };
264
+ }
265
+ function failureReport(options, reason, taskId = null) {
266
+ return makeReport({
267
+ apply: options.apply,
268
+ taskRoot: options.request.task_root,
269
+ taskId,
270
+ planFile: options.request.plan_file,
271
+ planDigest: null,
272
+ planId: null,
273
+ ownerEpoch: null,
274
+ concurrency: options.request.max_concurrency ?? 2,
275
+ nodes: [],
276
+ reports: [],
277
+ accepted: [],
278
+ awaiting: [],
279
+ rejected: [],
280
+ status: 'invalid',
281
+ diagnostics: [reason],
282
+ });
283
+ }
284
+ async function loadContext(request) {
285
+ const planRead = readJsonFile(request.plan_file);
286
+ if (!planRead.ok)
287
+ return { ok: false, reason: `plan_${planRead.reason}` };
288
+ const validated = validateWorkPlan(planRead.value);
289
+ if (!validated.ok)
290
+ return { ok: false, reason: 'plan_invalid' };
291
+ const opened = await openTaskStore(request.task_root);
292
+ if (!opened.ok)
293
+ return { ok: false, reason: opened.code };
294
+ if (opened.value.record.task_id !== validated.plan.task_id)
295
+ return { ok: false, reason: 'task_identity_mismatch' };
296
+ const report = await inspectWorkPlan({
297
+ taskRoot: request.task_root,
298
+ planFile: request.plan_file,
299
+ });
300
+ if (report.status !== 'evaluated' || report.core === null || report.stale)
301
+ return { ok: false, reason: 'work_plan_not_current' };
302
+ const assignmentMessages = new Map();
303
+ for (const attempt of opened.value.record.attempts) {
304
+ if (!validated.plan.nodes.some((node) => node.assignment_id === attempt.subtask_id &&
305
+ node.attempt === attempt.attempt))
306
+ continue;
307
+ if (!attempt.assignment_message_id)
308
+ continue;
309
+ const message = await readRegisteredMessageSafe(opened.value, attempt.assignment_message_id);
310
+ if (message.ok)
311
+ assignmentMessages.set(attempt.assignment_message_id, message.value.message);
312
+ }
313
+ for (const result of opened.value.record.results) {
314
+ if (!validated.plan.nodes.some((node) => node.assignment_id === result.subtask_id &&
315
+ node.attempt === result.attempt))
316
+ continue;
317
+ const message = await readRegisteredMessageSafe(opened.value, result.message_id);
318
+ if (message.ok)
319
+ assignmentMessages.set(result.message_id, message.value.message);
320
+ }
321
+ return {
322
+ ok: true,
323
+ value: {
324
+ plan: validated.plan,
325
+ planDigest: planRead.digest,
326
+ report,
327
+ record: opened.value.record,
328
+ opened: opened.value,
329
+ assignmentMessages,
330
+ },
331
+ };
332
+ }
333
+ function eligibility(context, node, request, options = {}) {
334
+ const workNode = context.report.nodes.find((entry) => entry.node_id === node.node_id);
335
+ const workPlanStatus = workNode?.status ?? 'blocked';
336
+ const coreNode = context.report.core?.nodes.find((entry) => entry.node_id === node.node_id);
337
+ const attempt = currentAttempt(context.record, node);
338
+ const acceptance = workPlanStatus === 'accepted'
339
+ ? 'accepted'
340
+ : workPlanStatus === 'awaiting_acceptance'
341
+ ? 'awaiting_acceptance'
342
+ : workNode?.blockers.some((item) => item.code === 'acceptance_rejected')
343
+ ? 'rejected'
344
+ : 'not-applicable';
345
+ const common = {
346
+ work_plan_status: workPlanStatus,
347
+ acceptance,
348
+ next_action: workNode?.next_action ?? null,
349
+ };
350
+ if (workPlanStatus === 'accepted' ||
351
+ workPlanStatus === 'awaiting_acceptance') {
352
+ return {
353
+ ok: false,
354
+ blockers: [],
355
+ result: nodeResult(node, workPlanStatus === 'accepted' ? 'accepted' : 'awaiting_acceptance', [], common),
356
+ };
357
+ }
358
+ if (!coreNode || coreNode.status === 'blocked' || !workNode || !attempt) {
359
+ const blockers = workNode?.blockers.map((item) => item.code) ?? [
360
+ 'current_assignment_missing',
361
+ ];
362
+ return {
363
+ ok: false,
364
+ blockers,
365
+ result: nodeResult(node, 'blocked', blockers, common),
366
+ };
367
+ }
368
+ const blockers = [];
369
+ const coordinator = context.record.coordinator;
370
+ if (!coordinator ||
371
+ !sameIdentity(coordinator, request.caller) ||
372
+ coordinator.owner_epoch !== context.plan.owner_epoch)
373
+ blockers.push('coordinator_identity_or_epoch_mismatch');
374
+ if (attempt.state !== 'assigned' || coreNode.status !== 'running')
375
+ blockers.push('attempt_not_assigned_and_pending');
376
+ if (attempt.assignee.agent_id !== node.assignee.agent_id ||
377
+ attempt.assignee.session_id !== node.assignee.session_id)
378
+ blockers.push('assigned_participant_mismatch');
379
+ if (attempt.input_revision !== node.input_revision)
380
+ blockers.push('input_revision_mismatch');
381
+ if (attempt.owner_epoch !== context.plan.owner_epoch)
382
+ blockers.push('attempt_epoch_mismatch');
383
+ if (attempt.native.session_id !== null ||
384
+ attempt.native.run_id !== null ||
385
+ attempt.native.mission_id !== null ||
386
+ attempt.native.evidence_ref !== null)
387
+ blockers.push('native_execution_already_observed');
388
+ if (context.record.results.some((result) => result.classification === 'applied' &&
389
+ result.subtask_id === node.assignment_id &&
390
+ result.attempt === node.attempt))
391
+ blockers.push('result_already_recorded');
392
+ const assignmentMessage = context.assignmentMessages.get(attempt.assignment_message_id);
393
+ if (!assignmentMessage || assignmentMessage.type !== 'ASSIGN')
394
+ blockers.push('registered_assignment_missing');
395
+ if (assignmentMessage &&
396
+ (!context.record.coordinator ||
397
+ !sameIdentity(assignmentMessage.from, context.record.coordinator) ||
398
+ assignmentMessage.body.assignment_ref !== attempt.assignment_ref ||
399
+ assignmentMessage.body.input_revision !== node.input_revision))
400
+ blockers.push('assignment_message_pins_mismatch');
401
+ const assignmentDelivery = assignmentMessage
402
+ ? readDeliveryRecord(request.task_root, assignmentMessage.message_id)
403
+ : null;
404
+ if (assignmentDelivery !== null &&
405
+ !(assignmentMessage?.message_id ===
406
+ options.allowSubmittingDeliveryMessageId &&
407
+ assignmentDelivery.status === 'submitting'))
408
+ blockers.push('assignment_delivery_already_recorded');
409
+ if (!attempt.assignment_ref ||
410
+ !path.isAbsolute(attempt.assignment_ref) ||
411
+ !attempt.assignment_digest)
412
+ blockers.push('assignment_pin_missing');
413
+ const assignmentRead = attempt.assignment_ref
414
+ ? readJsonFile(attempt.assignment_ref)
415
+ : { ok: false, reason: 'assignment_missing' };
416
+ const assignmentDigest = assignmentRead.ok ? assignmentRead.digest : null;
417
+ if (!assignmentRead.ok)
418
+ blockers.push(assignmentRead.reason);
419
+ const assignment = assignmentRead.ok && isAssignment(assignmentRead.value)
420
+ ? assignmentRead.value
421
+ : null;
422
+ if (!assignment)
423
+ blockers.push('assignment_contract_invalid');
424
+ if (assignment &&
425
+ assignmentRead.ok &&
426
+ assignmentRead.digest !== attempt.assignment_digest)
427
+ blockers.push('assignment_digest_mismatch');
428
+ if (assignment &&
429
+ assignmentMessage &&
430
+ assignmentMessage.body.assignment_digest !== attempt.assignment_digest)
431
+ blockers.push('assignment_message_digest_mismatch');
432
+ if (assignment &&
433
+ (assignment.task_id !== context.record.task_id ||
434
+ assignment.assignment_id !== node.assignment_id ||
435
+ assignment.attempt !== node.attempt ||
436
+ assignment.coordinator.owner_epoch !== context.plan.owner_epoch ||
437
+ !samePerson(assignment.assignee, node.assignee)))
438
+ blockers.push('assignment_identity_mismatch');
439
+ if (assignment &&
440
+ (assignment.coordinator.agent_id !== request.caller.agent_id ||
441
+ assignment.coordinator.session_id !== request.caller.session_id))
442
+ blockers.push('assignment_coordinator_mismatch');
443
+ if (assignment && !sameResources(assignment.resources, node.resources))
444
+ blockers.push('assignment_resource_mismatch');
445
+ if (assignment)
446
+ for (const resource of assignment.resources) {
447
+ if (resource.access !== 'write')
448
+ continue;
449
+ const claim = activeClaimFor(context.record, node, resource.path);
450
+ if (!claim)
451
+ blockers.push(`resource_claim_missing:${resource.path}`);
452
+ else if (resource.resource_epoch !== claim.resource_epoch)
453
+ blockers.push(`resource_claim_epoch_mismatch:${resource.path}`);
454
+ }
455
+ const cardInput = request.capability_cards.find((item) => item.assignment_id === node.assignment_id);
456
+ if (!cardInput || !path.isAbsolute(cardInput.capabilities_file))
457
+ blockers.push('capability_card_missing');
458
+ const cardRead = cardInput
459
+ ? readJsonFile(cardInput.capabilities_file)
460
+ : { ok: false, reason: 'capability_card_missing' };
461
+ if (!cardRead.ok)
462
+ blockers.push(cardRead.reason);
463
+ const card = cardRead.ok && isCard(cardRead.value) ? cardRead.value : null;
464
+ if (!card)
465
+ blockers.push('capability_card_invalid');
466
+ const capabilitiesDigest = cardRead.ok ? cardRead.digest : null;
467
+ if (assignment && cardInput && cardRead.ok) {
468
+ const preflight = runCollaborationPreflight({
469
+ assignmentFile: attempt.assignment_ref,
470
+ capabilitiesFile: cardInput.capabilities_file,
471
+ });
472
+ if (preflight.status !== 'ready')
473
+ blockers.push(...preflight.blockers.map((item) => `preflight:${item.code}`));
474
+ }
475
+ let bindingDigest = null;
476
+ if (assignment) {
477
+ const binding = staticBindingCheck(request.binding_file, request.task_root, context.record.task_id, request.caller, request.caller_pane, assignment);
478
+ if (!binding.ok)
479
+ blockers.push(binding.reason);
480
+ else
481
+ bindingDigest = binding.digest;
482
+ }
483
+ const cause = assignment && assignmentDigest
484
+ ? expectedCause(context.record, node, attempt, assignmentDigest, request.caller)
485
+ : null;
486
+ const intent = cause && assignment
487
+ ? createIntent(cause, context.record, attempt, assignment)
488
+ : null;
489
+ if (intent &&
490
+ options.allowIntentMessageId !== intent.message_id &&
491
+ (context.record.messages.some((item) => item.message_id === intent.message_id) ||
492
+ fs.existsSync(messageFile(request.task_root, intent.message_id))))
493
+ blockers.push('dispatch_intent_exists_reconcile');
494
+ const result = nodeResult(node, blockers.length
495
+ ? blockers.includes('dispatch_intent_exists_reconcile')
496
+ ? 'reconciliation-required'
497
+ : 'blocked'
498
+ : 'planned', blockers, {
499
+ ...common,
500
+ assignment: {
501
+ ref: attempt.assignment_ref,
502
+ digest: attempt.assignment_digest,
503
+ },
504
+ capabilities: {
505
+ ref: cardInput?.capabilities_file ?? null,
506
+ digest: capabilitiesDigest,
507
+ },
508
+ intent_message_id: intent?.message_id ?? null,
509
+ });
510
+ if (blockers.length ||
511
+ !assignment ||
512
+ !assignmentRead.ok ||
513
+ !card ||
514
+ !cardInput ||
515
+ !intent ||
516
+ !assignmentMessage)
517
+ return { ok: false, blockers: result.blockers, result };
518
+ return {
519
+ ok: true,
520
+ value: {
521
+ node,
522
+ planDigest: context.planDigest,
523
+ attempt,
524
+ assignment,
525
+ assignmentFile: attempt.assignment_ref,
526
+ assignmentDigest: assignmentRead.digest,
527
+ assignmentMessage,
528
+ capabilitiesFile: cardInput.capabilities_file,
529
+ capabilitiesDigest: capabilitiesDigest,
530
+ card,
531
+ bindingDigest: bindingDigest,
532
+ intentMessageId: intent.message_id,
533
+ intent,
534
+ resources: node.resources,
535
+ },
536
+ };
537
+ }
538
+ async function finalDispatchAuthority(request, candidate) {
539
+ const latest = await loadContext(request);
540
+ if (!latest.ok)
541
+ return { ok: false, reason: latest.reason };
542
+ if (latest.value.planDigest !== candidate.planDigest)
543
+ return { ok: false, reason: 'plan_digest_changed' };
544
+ const node = latest.value.plan.nodes.find((item) => item.node_id === candidate.node.node_id);
545
+ if (!node)
546
+ return { ok: false, reason: 'plan_node_missing' };
547
+ const checked = eligibility(latest.value, node, request, {
548
+ allowIntentMessageId: candidate.intentMessageId,
549
+ allowSubmittingDeliveryMessageId: candidate.assignmentMessage.message_id,
550
+ });
551
+ if (!checked.ok)
552
+ return {
553
+ ok: false,
554
+ reason: checked.blockers.join(',') || 'assignment_not_current',
555
+ };
556
+ if (checked.value.assignmentDigest !== candidate.assignmentDigest ||
557
+ checked.value.capabilitiesDigest !== candidate.capabilitiesDigest ||
558
+ checked.value.bindingDigest !== candidate.bindingDigest ||
559
+ checked.value.intentMessageId !== candidate.intentMessageId ||
560
+ checked.value.assignmentMessage.message_id !==
561
+ candidate.assignmentMessage.message_id)
562
+ return { ok: false, reason: 'pinned_dispatch_inputs_changed' };
563
+ const intent = await readRegisteredMessageSafe(latest.value.opened, candidate.intentMessageId);
564
+ if (!intent.ok ||
565
+ contentDigest(intent.value.message) !== contentDigest(candidate.intent))
566
+ return { ok: false, reason: 'dispatch_intent_untrusted' };
567
+ const intentEntry = latest.value.record.messages.find((item) => item.message_id === candidate.intentMessageId);
568
+ if (!intentEntry ||
569
+ intentEntry.type !== 'STATUS_QUERY' ||
570
+ intentEntry.content_digest !== candidate.intent.content_digest ||
571
+ !sameIdentity(intentEntry.from, request.caller))
572
+ return { ok: false, reason: 'dispatch_intent_not_current' };
573
+ const delivery = readDeliveryRecord(request.task_root, candidate.assignmentMessage.message_id);
574
+ if (delivery?.status !== 'submitting')
575
+ return { ok: false, reason: 'notification_reservation_not_current' };
576
+ const changed = await assertTaskStoreCurrent(latest.value.opened);
577
+ if (changed)
578
+ return { ok: false, reason: changed.code };
579
+ const current = await openTaskStore(request.task_root);
580
+ if (!current.ok)
581
+ return { ok: false, reason: current.code };
582
+ if (current.value.record.state_revision !== latest.value.record.state_revision)
583
+ return { ok: false, reason: 'task_revision_changed_before_prompt' };
584
+ return { ok: true, reason: '' };
585
+ }
586
+ function acceptanceSets(context) {
587
+ const accepted = context.report.core?.accepted_nodes ?? [];
588
+ const awaiting = context.report.core?.awaiting_acceptance_nodes ?? [];
589
+ const rejected = context.report.nodes
590
+ .filter((node) => node.blockers.some((item) => item.code === 'acceptance_rejected'))
591
+ .map((node) => node.node_id);
592
+ return {
593
+ accepted: [...accepted].sort(),
594
+ awaiting: [...awaiting].sort(),
595
+ rejected: [...rejected].sort(),
596
+ };
597
+ }
598
+ function overlap(left, right) {
599
+ return left.resources.some((a) => right.resources.some((b) => (a.access === 'write' || b.access === 'write') &&
600
+ pathRelation(a.path, b.path) !== 'disjoint'));
601
+ }
602
+ function asObservation(value) {
603
+ if (!value.ok || !value.value)
604
+ return {
605
+ ok: false,
606
+ status: value.code ?? 'notify_refused',
607
+ detail: value.message ?? 'Notification preflight refused.',
608
+ };
609
+ const result = value.value;
610
+ return {
611
+ ok: result.status === 'planned' || result.status === 'submitted',
612
+ status: result.status,
613
+ detail: result.detail,
614
+ delivery_path: result.deliveryPath,
615
+ message_digest: result.messageDigest,
616
+ target_pane: result.target?.pane_id ?? null,
617
+ };
618
+ }
619
+ export function createHerdrWorkExecutionAdapter(options = {}) {
620
+ return {
621
+ preflight: async (request) => asObservation(await notifyRegisteredMessage({
622
+ taskRoot: request.taskRoot,
623
+ messageId: request.messageId,
624
+ bindingFile: request.bindingFile,
625
+ apply: false,
626
+ retry: false,
627
+ callerPane: request.callerPane,
628
+ transport: options.transport ??
629
+ createHerdrTransport({ taskRoot: request.taskRoot }),
630
+ })),
631
+ dispatch: async (request) => {
632
+ const transport = options.transport ??
633
+ createHerdrTransport({ taskRoot: request.taskRoot });
634
+ const guardedTransport = {
635
+ id: `${transport.id}:work-execution-guard`,
636
+ verifyCaller: (callerOptions) => transport.verifyCaller(callerOptions),
637
+ list: () => transport.list(),
638
+ prompt: async (agent, promptText, registered) => {
639
+ if (!request.finalAuthorityCheck)
640
+ return {
641
+ status: 'blocked',
642
+ detail: 'Final work authority callback is missing; no prompt was sent.',
643
+ };
644
+ let authority;
645
+ try {
646
+ authority = await request.finalAuthorityCheck();
647
+ }
648
+ catch (error) {
649
+ return {
650
+ status: 'blocked',
651
+ detail: `Final work authority check failed: ${error instanceof Error ? error.message : String(error)}`,
652
+ };
653
+ }
654
+ if (!authority.ok)
655
+ return {
656
+ status: 'blocked',
657
+ detail: `Final work authority check refused the prompt: ${authority.reason}`,
658
+ };
659
+ return transport.prompt(agent, promptText, registered);
660
+ },
661
+ };
662
+ return asObservation(await notifyRegisteredMessage({
663
+ taskRoot: request.taskRoot,
664
+ messageId: request.messageId,
665
+ bindingFile: request.bindingFile,
666
+ apply: true,
667
+ retry: false,
668
+ callerPane: request.callerPane,
669
+ transport: guardedTransport,
670
+ }));
671
+ },
672
+ };
673
+ }
674
+ function observationResult(node, observation, intentMessageId, assignment) {
675
+ const status = observation.status === 'submitted'
676
+ ? 'dispatched'
677
+ : observation.status === 'planned'
678
+ ? 'planned'
679
+ : observation.status === 'in_flight_unknown' ||
680
+ observation.status === 'unknown' ||
681
+ observation.status === 'already_recorded'
682
+ ? 'reconciliation-required'
683
+ : 'blocked';
684
+ return nodeResult(node, status, status === 'dispatched' || status === 'planned' ? [] : [observation.status], {
685
+ work_plan_status: 'running',
686
+ assignment: {
687
+ ref: assignment.assignmentFile,
688
+ digest: assignment.assignmentDigest,
689
+ },
690
+ capabilities: {
691
+ ref: assignment.capabilitiesFile,
692
+ digest: assignment.capabilitiesDigest,
693
+ },
694
+ intent_message_id: intentMessageId,
695
+ delivery: {
696
+ status: observation.status,
697
+ message_digest: observation.message_digest ?? null,
698
+ delivery_path: observation.delivery_path ?? null,
699
+ target_pane: observation.target_pane ?? null,
700
+ },
701
+ });
702
+ }
703
+ /**
704
+ * Applies one bounded batch of already assigned work. Plan mode is read-only. Apply
705
+ * persists a deterministic STATUS_QUERY intent with the existing message transaction before
706
+ * one visible notification attempt; duplicate intents are reconciliation-only.
707
+ */
708
+ export async function runWorkExecution(options, deps = {}) {
709
+ const { request } = options;
710
+ const concurrency = request.max_concurrency ?? 2;
711
+ if (!path.isAbsolute(request.task_root) ||
712
+ !path.isAbsolute(request.plan_file) ||
713
+ !path.isAbsolute(request.binding_file) ||
714
+ !request.caller_pane ||
715
+ !/^[A-Za-z0-9]+:p[A-Za-z0-9]+$/.test(request.caller_pane) ||
716
+ !request.caller.agent_id ||
717
+ !request.caller.session_id ||
718
+ !request.caller.harness ||
719
+ !path.isAbsolute(request.caller.cwd) ||
720
+ !Array.isArray(request.capability_cards) ||
721
+ request.capability_cards.length > MAX_NODES ||
722
+ request.capability_cards.some((item) => !item.assignment_id || !path.isAbsolute(item.capabilities_file)) ||
723
+ new Set(request.capability_cards.map((item) => item.assignment_id)).size !==
724
+ request.capability_cards.length ||
725
+ !Number.isSafeInteger(concurrency) ||
726
+ concurrency < 1 ||
727
+ concurrency > MAX_CONCURRENCY)
728
+ return failureReport(options, 'invalid_request');
729
+ const initial = await loadContext(request);
730
+ if (!initial.ok)
731
+ return failureReport(options, initial.reason);
732
+ const context = initial.value;
733
+ const initialAcceptance = acceptanceSets(context);
734
+ const initialNodes = context.plan.nodes.map((node) => {
735
+ const check = eligibility(context, node, request);
736
+ return check.ok
737
+ ? nodeResult(node, 'planned', [], {
738
+ work_plan_status: context.report.nodes.find((item) => item.node_id === node.node_id)
739
+ ?.status ?? 'unknown',
740
+ assignment: {
741
+ ref: check.value.assignmentFile,
742
+ digest: check.value.assignmentDigest,
743
+ },
744
+ capabilities: {
745
+ ref: check.value.capabilitiesFile,
746
+ digest: check.value.capabilitiesDigest,
747
+ },
748
+ intent_message_id: check.value.intentMessageId,
749
+ })
750
+ : check.result;
751
+ });
752
+ const candidates = context.plan.nodes
753
+ .flatMap((node) => {
754
+ const check = eligibility(context, node, request);
755
+ return check.ok ? [check.value] : [];
756
+ })
757
+ .sort((left, right) => left.node.node_id.localeCompare(right.node.node_id));
758
+ const planReport = (nodes, status, actualOverlap = false) => makeReport({
759
+ apply: options.apply,
760
+ taskRoot: request.task_root,
761
+ taskId: context.record.task_id,
762
+ planFile: request.plan_file,
763
+ planDigest: context.planDigest,
764
+ planId: context.plan.plan_id,
765
+ ownerEpoch: context.plan.owner_epoch,
766
+ concurrency,
767
+ nodes,
768
+ reports: reportPointers(context.record, context.plan, context.assignmentMessages),
769
+ accepted: initialAcceptance.accepted,
770
+ awaiting: initialAcceptance.awaiting,
771
+ rejected: initialAcceptance.rejected,
772
+ ...(status ? { status } : {}),
773
+ actualOverlap,
774
+ });
775
+ if (!options.apply)
776
+ return planReport(initialNodes);
777
+ if (candidates.length === 0)
778
+ return planReport(initialNodes, initialNodes.some((node) => node.execution_status === 'reconciliation-required')
779
+ ? 'needs-reconciliation'
780
+ : initialNodes.some((node) => node.execution_status === 'blocked')
781
+ ? 'blocked'
782
+ : 'planned');
783
+ const adapter = deps.adapter ?? createHerdrWorkExecutionAdapter();
784
+ const prepared = await Promise.all(candidates.map(async (candidate) => {
785
+ const observation = await adapter.preflight({
786
+ taskRoot: request.task_root,
787
+ messageId: candidate.assignmentMessage.message_id,
788
+ bindingFile: request.binding_file,
789
+ callerPane: request.caller_pane,
790
+ });
791
+ return { candidate, observation };
792
+ }));
793
+ const results = new Map(initialNodes.map((result) => [result.node_id, result]));
794
+ const readyToPersist = [];
795
+ for (const { candidate, observation } of prepared) {
796
+ if (observation.ok && observation.status === 'planned')
797
+ readyToPersist.push(candidate);
798
+ else
799
+ results.set(candidate.node.node_id, nodeResult(candidate.node, 'blocked', [`participant_preflight:${observation.status}`], {
800
+ work_plan_status: context.report.nodes.find((item) => item.node_id === candidate.node.node_id)?.status ?? 'unknown',
801
+ assignment: {
802
+ ref: candidate.assignmentFile,
803
+ digest: candidate.assignmentDigest,
804
+ },
805
+ capabilities: {
806
+ ref: candidate.capabilitiesFile,
807
+ digest: candidate.capabilitiesDigest,
808
+ },
809
+ }));
810
+ }
811
+ const dispatchable = [];
812
+ for (const preparedCandidate of readyToPersist) {
813
+ const latest = await loadContext(request);
814
+ if (!latest.ok) {
815
+ results.set(preparedCandidate.node.node_id, nodeResult(preparedCandidate.node, 'blocked', [
816
+ `revalidation:${latest.reason}`,
817
+ ]));
818
+ continue;
819
+ }
820
+ const freshNode = latest.value.plan.nodes.find((node) => node.node_id === preparedCandidate.node.node_id);
821
+ if (!freshNode) {
822
+ results.set(preparedCandidate.node.node_id, nodeResult(preparedCandidate.node, 'blocked', ['plan_node_changed']));
823
+ continue;
824
+ }
825
+ const checked = eligibility(latest.value, freshNode, request);
826
+ if (!checked.ok) {
827
+ results.set(freshNode.node_id, checked.result);
828
+ continue;
829
+ }
830
+ const participant = await adapter.preflight({
831
+ taskRoot: request.task_root,
832
+ messageId: checked.value.assignmentMessage.message_id,
833
+ bindingFile: request.binding_file,
834
+ callerPane: request.caller_pane,
835
+ });
836
+ if (!participant.ok || participant.status !== 'planned') {
837
+ results.set(freshNode.node_id, nodeResult(freshNode, 'blocked', [
838
+ `participant_revalidation:${participant.status}`,
839
+ ]));
840
+ continue;
841
+ }
842
+ const afterObservation = await loadContext(request);
843
+ if (!afterObservation.ok) {
844
+ results.set(freshNode.node_id, nodeResult(freshNode, 'blocked', [
845
+ `post_preflight_revalidation:${afterObservation.reason}`,
846
+ ]));
847
+ continue;
848
+ }
849
+ const afterNode = afterObservation.value.plan.nodes.find((node) => node.node_id === freshNode.node_id);
850
+ if (!afterNode) {
851
+ results.set(freshNode.node_id, nodeResult(freshNode, 'blocked', ['plan_node_changed_after_preflight']));
852
+ continue;
853
+ }
854
+ const afterChecked = eligibility(afterObservation.value, afterNode, request);
855
+ if (!afterChecked.ok) {
856
+ results.set(afterNode.node_id, afterChecked.result);
857
+ continue;
858
+ }
859
+ const current = await openTaskStore(request.task_root);
860
+ if (!current.ok) {
861
+ results.set(freshNode.node_id, nodeResult(freshNode, 'blocked', [`task_revalidation:${current.code}`]));
862
+ continue;
863
+ }
864
+ const changed = await assertTaskStoreCurrent(current.value);
865
+ if (changed) {
866
+ results.set(freshNode.node_id, nodeResult(freshNode, 'blocked', [`task_revalidation:${changed.code}`]));
867
+ continue;
868
+ }
869
+ if (current.value.record.state_revision !==
870
+ afterObservation.value.record.state_revision) {
871
+ results.set(freshNode.node_id, nodeResult(freshNode, 'blocked', ['task_revision_changed']));
872
+ continue;
873
+ }
874
+ const prior = current.value.record.messages.some((item) => item.message_id === afterChecked.value.intentMessageId) ||
875
+ fs.existsSync(messageFile(request.task_root, afterChecked.value.intentMessageId));
876
+ if (prior) {
877
+ results.set(freshNode.node_id, nodeResult(freshNode, 'reconciliation-required', ['dispatch_intent_exists_reconcile'], {
878
+ work_plan_status: afterObservation.value.report.nodes.find((item) => item.node_id === freshNode.node_id)?.status ?? 'unknown',
879
+ assignment: {
880
+ ref: afterChecked.value.assignmentFile,
881
+ digest: afterChecked.value.assignmentDigest,
882
+ },
883
+ capabilities: {
884
+ ref: afterChecked.value.capabilitiesFile,
885
+ digest: afterChecked.value.capabilitiesDigest,
886
+ },
887
+ intent_message_id: afterChecked.value.intentMessageId,
888
+ }));
889
+ continue;
890
+ }
891
+ const persisted = submitMessage(request.task_root, afterChecked.value.intent, {
892
+ expectRevision: current.value.record.state_revision,
893
+ });
894
+ if (!persisted.ok ||
895
+ persisted.registration.status !== 'registered' ||
896
+ persisted.duplicate === true) {
897
+ results.set(freshNode.node_id, nodeResult(freshNode, 'reconciliation-required', [
898
+ persisted.ok
899
+ ? 'dispatch_intent_duplicate'
900
+ : `intent_persist:${persisted.reason}`,
901
+ ], {
902
+ work_plan_status: afterObservation.value.report.nodes.find((item) => item.node_id === freshNode.node_id)?.status ?? 'unknown',
903
+ assignment: {
904
+ ref: afterChecked.value.assignmentFile,
905
+ digest: afterChecked.value.assignmentDigest,
906
+ },
907
+ capabilities: {
908
+ ref: afterChecked.value.capabilitiesFile,
909
+ digest: afterChecked.value.capabilitiesDigest,
910
+ },
911
+ intent_message_id: afterChecked.value.intentMessageId,
912
+ }));
913
+ continue;
914
+ }
915
+ dispatchable.push(afterChecked.value);
916
+ }
917
+ let actualOverlap = false;
918
+ for (let offset = 0; offset < dispatchable.length; offset += concurrency) {
919
+ const batch = dispatchable.slice(offset, offset + concurrency);
920
+ if (batch.length > 1 &&
921
+ batch.some((item, index) => batch.slice(index + 1).some((other) => overlap(item, other)))) {
922
+ for (const candidate of batch)
923
+ results.set(candidate.node.node_id, nodeResult(candidate.node, 'blocked', ['batch_resource_overlap']));
924
+ continue;
925
+ }
926
+ let active = 0;
927
+ let peak = 0;
928
+ const outcomes = await Promise.all(batch.map(async (candidate) => {
929
+ let dispatchStarted = false;
930
+ try {
931
+ const latest = await loadContext(request);
932
+ if (!latest.ok)
933
+ return {
934
+ candidate,
935
+ observation: {
936
+ ok: false,
937
+ status: `revalidation:${latest.reason}`,
938
+ detail: null,
939
+ },
940
+ };
941
+ const liveNode = latest.value.plan.nodes.find((node) => node.node_id === candidate.node.node_id);
942
+ if (!liveNode)
943
+ return {
944
+ candidate,
945
+ observation: {
946
+ ok: false,
947
+ status: 'late_block:plan_node_missing',
948
+ detail: null,
949
+ },
950
+ };
951
+ const live = eligibility(latest.value, liveNode, request, {
952
+ allowIntentMessageId: candidate.intentMessageId,
953
+ });
954
+ if (!live.ok)
955
+ return {
956
+ candidate,
957
+ observation: {
958
+ ok: false,
959
+ status: `late_block:${live.blockers[0] ?? 'unknown'}`,
960
+ detail: null,
961
+ },
962
+ };
963
+ active += 1;
964
+ peak = Math.max(peak, active);
965
+ dispatchStarted = true;
966
+ const observation = await adapter.dispatch({
967
+ taskRoot: request.task_root,
968
+ messageId: candidate.assignmentMessage.message_id,
969
+ bindingFile: request.binding_file,
970
+ callerPane: request.caller_pane,
971
+ finalAuthorityCheck: () => finalDispatchAuthority(request, candidate),
972
+ });
973
+ return { candidate, observation };
974
+ }
975
+ catch (error) {
976
+ return {
977
+ candidate,
978
+ observation: {
979
+ ok: false,
980
+ status: 'unknown',
981
+ detail: error instanceof Error ? error.message : String(error),
982
+ },
983
+ };
984
+ }
985
+ finally {
986
+ if (dispatchStarted)
987
+ active -= 1;
988
+ }
989
+ }));
990
+ actualOverlap ||= peak > 1;
991
+ for (const { candidate, observation } of outcomes)
992
+ results.set(candidate.node.node_id, observationResult(candidate.node, observation, candidate.intentMessageId, candidate));
993
+ }
994
+ const finalContext = await loadContext(request);
995
+ const final = finalContext.ok ? finalContext.value : context;
996
+ const finalAcceptance = acceptanceSets(final);
997
+ const finalMessages = new Map(final.assignmentMessages);
998
+ if (finalContext.ok) {
999
+ for (const node of final.plan.nodes) {
1000
+ const attempt = currentAttempt(final.record, node);
1001
+ if (!attempt)
1002
+ continue;
1003
+ for (const result of final.record.results.filter((entry) => entry.subtask_id === node.assignment_id &&
1004
+ entry.attempt === node.attempt)) {
1005
+ const message = await readRegisteredMessageSafe(final.opened, result.message_id);
1006
+ if (message.ok)
1007
+ finalMessages.set(result.message_id, message.value.message);
1008
+ }
1009
+ }
1010
+ }
1011
+ return makeReport({
1012
+ apply: true,
1013
+ taskRoot: request.task_root,
1014
+ taskId: final.record.task_id,
1015
+ planFile: request.plan_file,
1016
+ planDigest: final.planDigest,
1017
+ planId: final.plan.plan_id,
1018
+ ownerEpoch: final.plan.owner_epoch,
1019
+ concurrency,
1020
+ nodes: [...results.values()],
1021
+ reports: reportPointers(final.record, final.plan, finalMessages),
1022
+ accepted: finalAcceptance.accepted,
1023
+ awaiting: finalAcceptance.awaiting,
1024
+ rejected: finalAcceptance.rejected,
1025
+ actualOverlap,
1026
+ });
1027
+ }