@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,843 @@
1
+ import { object as isRecord } from '../infra/validation.js';
2
+ import path from 'node:path';
3
+ import { pathRelation } from './canonical.js';
4
+ import { contentDigest, validateEnvelope } from './envelope.js';
5
+ /**
6
+ * Bounded, read-only role and dependency planning over the authoritative task record.
7
+ *
8
+ * This module never writes a record, never claims a resource, never dispatches and never
9
+ * refreshes evidence. It reads the caller's `TaskRecord` plus the full registered message
10
+ * envelopes the caller already holds, and reports what the current record says about each
11
+ * planned node. Every dependency is gated on an explicit coordinator acceptance whose
12
+ * meaning is re-derived here from the registered envelope, the record entry and the digest
13
+ * of the accepted report; a finished report alone never unlocks dependent work. The result
14
+ * is advisory and must be re-evaluated before a later explicit claim or dispatch: role
15
+ * labels select responsibilities, never authority.
16
+ */
17
+ export const WORK_PLAN_SCHEMA = 'pappra/collab-work-plan/1';
18
+ export const WORK_PLAN_EVALUATION_SCHEMA = 'pappra/collab-work-plan-evaluation/1';
19
+ const MAX_NODES = 64;
20
+ const MAX_DEPENDENCIES = 16;
21
+ const MAX_RESOURCES = 16;
22
+ const MAX_STRING = 512;
23
+ const MAX_EVIDENCE = 256;
24
+ /** Documented bound per record array; beyond it planning refuses rather than truncating. */
25
+ const MAX_RECORD_ENTRIES = 512;
26
+ /** Blockers that never stop the request itself: they say "re-evaluate before acting". */
27
+ const IGNORED_UNCERTAINTY = new Set(['registered_not_started', 'late_result']);
28
+ const isString = (value) => typeof value === 'string' &&
29
+ value.length > 0 &&
30
+ value.length <= MAX_STRING &&
31
+ !value.includes('\u0000');
32
+ const isIdentity = (value) => isRecord(value) &&
33
+ isString(value['agent_id']) &&
34
+ isString(value['session_id']);
35
+ const isOptionalString = (value) => value === undefined || value === null || typeof value === 'string';
36
+ const isOptionalInteger = (value) => value === undefined || value === null || Number.isSafeInteger(value);
37
+ /** The fields each record entry must expose before evaluation dereferences it. */
38
+ const RECORD_ENTRIES = [
39
+ [
40
+ 'attempts',
41
+ (value) => isRecord(value) &&
42
+ typeof value['subtask_id'] === 'string' &&
43
+ Number.isSafeInteger(value['attempt']) &&
44
+ typeof value['state'] === 'string' &&
45
+ isIdentity(value['assignee']) &&
46
+ isOptionalString(value['input_revision']) &&
47
+ isOptionalInteger(value['owner_epoch']),
48
+ ],
49
+ [
50
+ 'messages',
51
+ (value) => isRecord(value) &&
52
+ typeof value['message_id'] === 'string' &&
53
+ typeof value['type'] === 'string' &&
54
+ typeof value['state'] === 'string' &&
55
+ typeof value['content_digest'] === 'string' &&
56
+ typeof value['at'] === 'string' &&
57
+ isIdentity(value['from']) &&
58
+ isOptionalString(value['reply_to']) &&
59
+ isOptionalString(value['subtask_id']) &&
60
+ isOptionalInteger(value['attempt']),
61
+ ],
62
+ [
63
+ 'results',
64
+ (value) => isRecord(value) &&
65
+ typeof value['subtask_id'] === 'string' &&
66
+ Number.isSafeInteger(value['attempt']) &&
67
+ typeof value['classification'] === 'string' &&
68
+ typeof value['status'] === 'string' &&
69
+ typeof value['message_id'] === 'string',
70
+ ],
71
+ [
72
+ 'claims',
73
+ (value) => isRecord(value) &&
74
+ typeof value['claim_id'] === 'string' &&
75
+ typeof value['access'] === 'string' &&
76
+ isOptionalString(value['path']) &&
77
+ isOptionalString(value['released_at']) &&
78
+ (value['holder'] === undefined ||
79
+ value['holder'] === null ||
80
+ isIdentity(value['holder'])),
81
+ ],
82
+ [
83
+ 'uncertainty',
84
+ (value) => isRecord(value) &&
85
+ typeof value['uncertainty_id'] === 'string' &&
86
+ typeof value['kind'] === 'string' &&
87
+ isOptionalString(value['resolved_at']) &&
88
+ isOptionalString(value['subtask_id']) &&
89
+ isOptionalInteger(value['attempt']),
90
+ ],
91
+ ];
92
+ function diagnostic(diagnostics, code, detail, nodeId = null) {
93
+ diagnostics.push({ code, node_id: nodeId, detail });
94
+ }
95
+ /** Plan paths are compared lexically only; `..` and symlink aliases are refused, not guessed. */
96
+ function normalizePlanPath(value) {
97
+ if (!path.isAbsolute(value))
98
+ return null;
99
+ const strip = (candidate) => candidate.length > 1 ? candidate.replace(/\/+$/, '') : candidate;
100
+ const normalized = path.normalize(value);
101
+ if (strip(normalized) !== strip(value))
102
+ return null;
103
+ return strip(value);
104
+ }
105
+ function readResources(value, nodeId, diagnostics) {
106
+ if (!Array.isArray(value) || value.length > MAX_RESOURCES) {
107
+ diagnostic(diagnostics, 'plan_too_large', `resources must be an array of at most ${MAX_RESOURCES}`, nodeId);
108
+ return null;
109
+ }
110
+ const resources = [];
111
+ for (const entry of value) {
112
+ if (!isRecord(entry) || !isString(entry['path'])) {
113
+ diagnostic(diagnostics, 'resource_invalid', 'resource needs a bounded absolute path', nodeId);
114
+ return null;
115
+ }
116
+ if (normalizePlanPath(entry['path']) === null) {
117
+ diagnostic(diagnostics, 'resource_invalid', 'resource path must be absolute and already normalized', nodeId);
118
+ return null;
119
+ }
120
+ if (entry['access'] !== 'read' && entry['access'] !== 'write') {
121
+ diagnostic(diagnostics, 'resource_invalid', 'resource access must be read or write', nodeId);
122
+ return null;
123
+ }
124
+ resources.push({ path: entry['path'], access: entry['access'] });
125
+ }
126
+ return resources;
127
+ }
128
+ function readNode(value, index, diagnostics) {
129
+ if (!isRecord(value)) {
130
+ diagnostic(diagnostics, 'node_invalid', `node ${index} is not an object`);
131
+ return null;
132
+ }
133
+ const nodeId = value['node_id'];
134
+ if (!isString(nodeId)) {
135
+ diagnostic(diagnostics, 'node_invalid', `node ${index} needs a bounded node_id`);
136
+ return null;
137
+ }
138
+ if (value['requires_acceptance'] !== undefined) {
139
+ // Dependency acceptance is coordinator-only: a plan cannot declare itself exempt.
140
+ diagnostic(diagnostics, 'plan_invalid', 'requires_acceptance is not part of the plan format; coordinator acceptance always gates dependents', nodeId);
141
+ return null;
142
+ }
143
+ const role = value['role'];
144
+ const assignmentId = value['assignment_id'];
145
+ const inputRevision = value['input_revision'];
146
+ const attempt = value['attempt'];
147
+ const assignee = value['assignee'];
148
+ const dependsOn = value['depends_on'];
149
+ if (!isString(role) || !isString(assignmentId) || !isString(inputRevision)) {
150
+ diagnostic(diagnostics, 'node_invalid', 'role, assignment_id and input_revision are required', nodeId);
151
+ return null;
152
+ }
153
+ if (typeof attempt !== 'number' ||
154
+ !Number.isSafeInteger(attempt) ||
155
+ attempt < 1 ||
156
+ attempt > 1000) {
157
+ diagnostic(diagnostics, 'node_invalid', 'attempt must be a positive bounded integer', nodeId);
158
+ return null;
159
+ }
160
+ if (!isIdentity(assignee)) {
161
+ diagnostic(diagnostics, 'node_invalid', 'assignee needs agent_id and session_id', nodeId);
162
+ return null;
163
+ }
164
+ if (!Array.isArray(dependsOn) || dependsOn.length > MAX_DEPENDENCIES) {
165
+ diagnostic(diagnostics, 'plan_too_large', `depends_on must be an array of at most ${MAX_DEPENDENCIES}`, nodeId);
166
+ return null;
167
+ }
168
+ const dependencies = [];
169
+ for (const dependency of dependsOn) {
170
+ if (!isString(dependency)) {
171
+ diagnostic(diagnostics, 'dependency_invalid', 'dependency ids must be bounded strings', nodeId);
172
+ return null;
173
+ }
174
+ dependencies.push(dependency);
175
+ }
176
+ const resources = readResources(value['resources'], nodeId, diagnostics);
177
+ if (resources === null)
178
+ return null;
179
+ return {
180
+ node_id: nodeId,
181
+ role,
182
+ assignment_id: assignmentId,
183
+ attempt,
184
+ input_revision: inputRevision,
185
+ assignee: {
186
+ agent_id: assignee['agent_id'],
187
+ session_id: assignee['session_id'],
188
+ },
189
+ depends_on: dependencies,
190
+ resources,
191
+ };
192
+ }
193
+ /** Validates shape, bounds and the dependency graph before any evaluation. */
194
+ export function validateWorkPlan(input) {
195
+ const diagnostics = [];
196
+ if (!isRecord(input)) {
197
+ diagnostic(diagnostics, 'plan_invalid', 'plan must be an object');
198
+ return { ok: false, diagnostics };
199
+ }
200
+ if (input['schema'] !== WORK_PLAN_SCHEMA) {
201
+ diagnostic(diagnostics, 'plan_invalid', `schema must be ${WORK_PLAN_SCHEMA}`);
202
+ return { ok: false, diagnostics };
203
+ }
204
+ const planId = input['plan_id'];
205
+ const taskId = input['task_id'];
206
+ const ownerEpoch = input['owner_epoch'];
207
+ if (!isString(planId) || !isString(taskId)) {
208
+ diagnostic(diagnostics, 'plan_invalid', 'plan_id and task_id are required');
209
+ return { ok: false, diagnostics };
210
+ }
211
+ if (typeof ownerEpoch !== 'number' ||
212
+ !Number.isSafeInteger(ownerEpoch) ||
213
+ ownerEpoch < 1) {
214
+ diagnostic(diagnostics, 'plan_invalid', 'owner_epoch must be a positive integer');
215
+ return { ok: false, diagnostics };
216
+ }
217
+ const rawNodes = input['nodes'];
218
+ if (!Array.isArray(rawNodes) || rawNodes.length === 0) {
219
+ diagnostic(diagnostics, 'plan_invalid', 'nodes must be a non-empty array');
220
+ return { ok: false, diagnostics };
221
+ }
222
+ if (rawNodes.length > MAX_NODES) {
223
+ diagnostic(diagnostics, 'plan_too_large', `at most ${MAX_NODES} nodes are supported`);
224
+ return { ok: false, diagnostics };
225
+ }
226
+ const nodes = [];
227
+ const seen = new Set();
228
+ for (const [index, raw] of rawNodes.entries()) {
229
+ const node = readNode(raw, index, diagnostics);
230
+ if (node === null)
231
+ continue;
232
+ if (seen.has(node.node_id)) {
233
+ diagnostic(diagnostics, 'duplicate_node_id', `node id is repeated: ${node.node_id}`, node.node_id);
234
+ continue;
235
+ }
236
+ seen.add(node.node_id);
237
+ nodes.push(node);
238
+ }
239
+ if (diagnostics.length > 0)
240
+ return { ok: false, diagnostics };
241
+ for (const node of nodes) {
242
+ const unique = new Set(node.depends_on);
243
+ if (unique.size !== node.depends_on.length) {
244
+ diagnostic(diagnostics, 'duplicate_dependency', 'a dependency is listed twice', node.node_id);
245
+ continue;
246
+ }
247
+ for (const dependency of node.depends_on) {
248
+ if (dependency === node.node_id) {
249
+ diagnostic(diagnostics, 'self_dependency', 'a node cannot depend on itself', node.node_id);
250
+ continue;
251
+ }
252
+ if (!seen.has(dependency)) {
253
+ diagnostic(diagnostics, 'unknown_dependency', `dependency is not a plan node: ${dependency}`, node.node_id);
254
+ }
255
+ }
256
+ }
257
+ if (diagnostics.length > 0)
258
+ return { ok: false, diagnostics };
259
+ const state = new Map();
260
+ const byId = new Map(nodes.map((node) => [node.node_id, node]));
261
+ const visit = (nodeId, trail) => {
262
+ const current = state.get(nodeId);
263
+ if (current === 'done')
264
+ return true;
265
+ if (current === 'visiting') {
266
+ diagnostic(diagnostics, 'dependency_cycle', `cycle: ${[...trail, nodeId].join(' -> ')}`, nodeId);
267
+ return false;
268
+ }
269
+ state.set(nodeId, 'visiting');
270
+ const node = byId.get(nodeId);
271
+ for (const dependency of node?.depends_on ?? []) {
272
+ if (!visit(dependency, [...trail, nodeId]))
273
+ return false;
274
+ }
275
+ state.set(nodeId, 'done');
276
+ return true;
277
+ };
278
+ for (const node of nodes) {
279
+ if (!visit(node.node_id, []))
280
+ break;
281
+ }
282
+ if (diagnostics.length > 0)
283
+ return { ok: false, diagnostics };
284
+ return {
285
+ ok: true,
286
+ plan: {
287
+ schema: WORK_PLAN_SCHEMA,
288
+ plan_id: planId,
289
+ task_id: taskId,
290
+ owner_epoch: ownerEpoch,
291
+ nodes,
292
+ },
293
+ };
294
+ }
295
+ const RECOVERY = new Map([
296
+ [
297
+ 'owner_epoch_mismatch',
298
+ 'Re-read the task record and rebuild the plan against the current owner epoch.',
299
+ ],
300
+ [
301
+ 'dispatch_frozen',
302
+ 'A handoff froze dispatch; finish or release it before planning further dispatch.',
303
+ ],
304
+ [
305
+ 'assignment_stale',
306
+ 'Re-issue or re-pin the assignment attempt; planning never rewrites an existing attempt.',
307
+ ],
308
+ [
309
+ 'assignee_mismatch',
310
+ 'Plan the node for the assignee recorded on the current attempt, or re-issue the assignment.',
311
+ ],
312
+ [
313
+ 'input_unpinned',
314
+ 'Pin an input revision on the attempt before planning work against it.',
315
+ ],
316
+ [
317
+ 'input_changed',
318
+ 'Repin the assignment input revision before dispatching this node.',
319
+ ],
320
+ [
321
+ 'own_failed',
322
+ 'The recorded attempt already failed or was cancelled; decide explicitly whether a new attempt is needed.',
323
+ ],
324
+ ['own_blocked', 'Resolve the recorded block before this node can proceed.'],
325
+ [
326
+ 'own_uncertain',
327
+ 'Confirm what the native execution actually did before planning further work.',
328
+ ],
329
+ [
330
+ 'uncertainty_open',
331
+ 'Reconcile the open uncertainty on this attempt; planning does not resolve it.',
332
+ ],
333
+ [
334
+ 'predecessor_incomplete',
335
+ 'Wait for the predecessor attempt to finish; readiness is advisory and must be re-evaluated.',
336
+ ],
337
+ [
338
+ 'predecessor_failed',
339
+ 'Resolve the failed predecessor explicitly; dependent work is not silently redispatched.',
340
+ ],
341
+ [
342
+ 'predecessor_blocked',
343
+ 'Resolve the predecessor blocker; this node cannot advance past an unusable predecessor.',
344
+ ],
345
+ [
346
+ 'acceptance_missing',
347
+ 'Obtain an explicit coordinator acceptance correlated to the predecessor report.',
348
+ ],
349
+ [
350
+ 'acceptance_untrusted',
351
+ 'Supply the registered acceptance envelope unchanged; an unregistered or edited one proves nothing.',
352
+ ],
353
+ [
354
+ 'acceptance_stale',
355
+ 'Re-accept the predecessor against the pinned input revision and owner epoch currently in force.',
356
+ ],
357
+ [
358
+ 'acceptance_mismatch',
359
+ 'Accept the exact predecessor assignment attempt and registered report digest.',
360
+ ],
361
+ [
362
+ 'acceptance_rejected',
363
+ 'Revise the rejected work under a new attempt before dependents advance.',
364
+ ],
365
+ [
366
+ 'acceptance_self_review',
367
+ 'Have the coordinator, not the writer, accept the report.',
368
+ ],
369
+ [
370
+ 'acceptance_not_coordinator',
371
+ 'Obtain acceptance from the current coordinator identity.',
372
+ ],
373
+ [
374
+ 'resource_overlap',
375
+ 'Serialize the concurrently overlapping write scope or narrow it; planning cannot fence.',
376
+ ],
377
+ [
378
+ 'resource_claim_conflict',
379
+ 'Wait for or explicitly transfer the active claim before dispatch; planning cannot fence.',
380
+ ],
381
+ ]);
382
+ function overlap(left, right) {
383
+ return pathRelation(left, right) !== 'disjoint';
384
+ }
385
+ function claimOverlaps(claim, resource, access) {
386
+ if (typeof claim.path !== 'string')
387
+ return false;
388
+ if (claim.access === 'read' && access === 'read')
389
+ return false;
390
+ return overlap(claim.path, resource.path);
391
+ }
392
+ /**
393
+ * Evaluates a validated plan against the authoritative record and the supplied envelopes.
394
+ *
395
+ * Returns a refusal with diagnostics when the plan, the record or the supplied evidence is
396
+ * malformed or oversized; readiness is never reported from facts that could not be read.
397
+ */
398
+ export function evaluateWorkPlan(evidence) {
399
+ const validated = validateWorkPlan(evidence.plan);
400
+ if (!validated.ok)
401
+ return { ok: false, diagnostics: validated.diagnostics };
402
+ const plan = validated.plan;
403
+ const diagnostics = [];
404
+ const record = evidence.record;
405
+ if (!isRecord(record)) {
406
+ diagnostic(diagnostics, 'record_invalid', 'record must be an object');
407
+ return { ok: false, diagnostics };
408
+ }
409
+ for (const [key, isValid] of RECORD_ENTRIES) {
410
+ const entries = record[key];
411
+ if (!Array.isArray(entries)) {
412
+ diagnostic(diagnostics, 'record_invalid', `record.${key} must be an array`);
413
+ continue;
414
+ }
415
+ if (entries.length > MAX_RECORD_ENTRIES) {
416
+ diagnostic(diagnostics, 'record_too_large', `record.${key} has ${entries.length} entries; planning reads at most ${MAX_RECORD_ENTRIES}`);
417
+ continue;
418
+ }
419
+ const malformed = entries.findIndex((entry) => !isValid(entry));
420
+ if (malformed >= 0) {
421
+ diagnostic(diagnostics, 'record_invalid', `record.${key}[${malformed}] does not have the expected shape`);
422
+ }
423
+ }
424
+ if (record.coordinator !== null && record.coordinator !== undefined) {
425
+ const coordinator = record.coordinator;
426
+ if (!isRecord(coordinator) ||
427
+ !isIdentity(coordinator) ||
428
+ !Number.isSafeInteger(coordinator['owner_epoch'])) {
429
+ diagnostic(diagnostics, 'record_invalid', 'record.coordinator must carry an identity and an owner epoch');
430
+ }
431
+ }
432
+ if (record.dispatch_frozen !== undefined &&
433
+ typeof record.dispatch_frozen !== 'boolean') {
434
+ diagnostic(diagnostics, 'record_invalid', 'record.dispatch_frozen must be a boolean when present');
435
+ }
436
+ if (diagnostics.length > 0)
437
+ return { ok: false, diagnostics };
438
+ if (record.task_id !== plan.task_id) {
439
+ diagnostic(diagnostics, 'task_identity_mismatch', `record is ${record.task_id}, plan is ${plan.task_id}`);
440
+ return { ok: false, diagnostics };
441
+ }
442
+ const supplied = [];
443
+ const rawMessages = evidence.messages ?? [];
444
+ if (!Array.isArray(rawMessages)) {
445
+ diagnostic(diagnostics, 'evidence_invalid', 'messages must be an array');
446
+ return { ok: false, diagnostics };
447
+ }
448
+ if (rawMessages.length > MAX_EVIDENCE) {
449
+ diagnostic(diagnostics, 'evidence_too_large', `at most ${MAX_EVIDENCE} message envelopes may be supplied; narrow the evidence instead of truncating`);
450
+ return { ok: false, diagnostics };
451
+ }
452
+ for (const raw of rawMessages) {
453
+ const label = isRecord(raw) && typeof raw['message_id'] === 'string'
454
+ ? raw['message_id']
455
+ : null;
456
+ let envelope;
457
+ try {
458
+ envelope = validateEnvelope(raw);
459
+ }
460
+ catch {
461
+ diagnostic(diagnostics, 'message_invalid', `supplied envelope ${label ?? '<unknown>'} is not a valid message`);
462
+ continue;
463
+ }
464
+ if (envelope.task_id !== plan.task_id) {
465
+ diagnostic(diagnostics, 'message_task_mismatch', `supplied envelope ${envelope.message_id} belongs to ${envelope.task_id}`);
466
+ continue;
467
+ }
468
+ supplied.push(envelope);
469
+ }
470
+ const envelopes = new Map();
471
+ for (const envelope of supplied) {
472
+ const existing = envelopes.get(envelope.message_id);
473
+ if (existing !== undefined) {
474
+ if (contentDigest(existing) !== contentDigest(envelope)) {
475
+ diagnostic(diagnostics, 'message_duplicate_conflict', `two different envelopes share message id ${envelope.message_id}`);
476
+ continue;
477
+ }
478
+ continue;
479
+ }
480
+ envelopes.set(envelope.message_id, envelope);
481
+ }
482
+ if (diagnostics.length > 0)
483
+ return { ok: false, diagnostics };
484
+ const limits = new Set([
485
+ 'advisory_read_only_plan',
486
+ 'no_resource_fencing',
487
+ 'no_dispatch_or_launch',
488
+ 're_evaluate_before_claim',
489
+ 'path_aliases_not_verified',
490
+ ]);
491
+ const attempts = record.attempts ?? [];
492
+ const results = record.results ?? [];
493
+ const claims = (record.claims ?? []).filter((claim) => !claim.released_at);
494
+ const appliedMessages = record.messages ?? [];
495
+ const appliedById = new Map(appliedMessages.map((entry) => [entry.message_id, entry]));
496
+ // Application order is the record's own append order; sender timestamps never order
497
+ // authority, so a backdated or future-dated decision cannot change the outcome.
498
+ const registrationIndex = new Map();
499
+ appliedMessages.forEach((entry, index) => {
500
+ if (!registrationIndex.has(entry.message_id))
501
+ registrationIndex.set(entry.message_id, index);
502
+ });
503
+ const attemptsBySubtask = new Map();
504
+ const latestAttempt = new Map();
505
+ for (const attempt of attempts) {
506
+ const list = attemptsBySubtask.get(attempt.subtask_id) ?? [];
507
+ list.push(attempt);
508
+ attemptsBySubtask.set(attempt.subtask_id, list);
509
+ const seen = latestAttempt.get(attempt.subtask_id) ?? 0;
510
+ latestAttempt.set(attempt.subtask_id, Math.max(seen, attempt.attempt));
511
+ }
512
+ const admittedByAttempt = new Map();
513
+ for (const result of results) {
514
+ if (result.classification !== 'applied')
515
+ continue;
516
+ const key = `${result.subtask_id}#${result.attempt}`;
517
+ const list = admittedByAttempt.get(key) ?? [];
518
+ list.push(result);
519
+ admittedByAttempt.set(key, list);
520
+ }
521
+ const coordinator = record.coordinator ?? null;
522
+ const epochCurrent = coordinator !== null && coordinator.owner_epoch === plan.owner_epoch;
523
+ const byId = new Map(plan.nodes.map((node) => [node.node_id, node]));
524
+ const memo = new Map();
525
+ const acceptanceMemo = new Map();
526
+ const factsFor = (node) => {
527
+ const attempt = (attemptsBySubtask.get(node.assignment_id) ?? []).find((candidate) => candidate.attempt === node.attempt) ?? null;
528
+ const list = admittedByAttempt.get(`${node.assignment_id}#${node.attempt}`) ?? [];
529
+ return {
530
+ attempt,
531
+ latestAttempt: latestAttempt.get(node.assignment_id) ?? null,
532
+ admitted: list.length > 0 ? list[list.length - 1] : null,
533
+ };
534
+ };
535
+ /** True when the node's recorded attempt already ended and no longer writes. */
536
+ const executionEndedFor = (node) => {
537
+ const state = factsFor(node).attempt?.state ?? null;
538
+ return state === 'completed' || state === 'failed' || state === 'cancelled';
539
+ };
540
+ /** True when `nodeId` transitively depends on `targetId`. */
541
+ const dependsOn = (nodeId, targetId) => {
542
+ const seen = new Set();
543
+ const walk = (current) => {
544
+ const node = byId.get(current);
545
+ if (node === undefined)
546
+ return false;
547
+ for (const dependency of node.depends_on) {
548
+ if (dependency === targetId)
549
+ return true;
550
+ if (seen.has(dependency))
551
+ continue;
552
+ seen.add(dependency);
553
+ if (walk(dependency))
554
+ return true;
555
+ }
556
+ return false;
557
+ };
558
+ return walk(nodeId);
559
+ };
560
+ const acceptanceOf = (node) => {
561
+ const cached = acceptanceMemo.get(node.node_id);
562
+ if (cached !== undefined)
563
+ return cached;
564
+ const settle = (outcome) => {
565
+ acceptanceMemo.set(node.node_id, outcome);
566
+ return outcome;
567
+ };
568
+ const denied = (code, detail) => settle({ accepted: false, blocker: { code, detail } });
569
+ const facts = factsFor(node);
570
+ const admitted = facts.admitted;
571
+ if (admitted === null) {
572
+ return denied('predecessor_incomplete', 'no current applied result is recorded for this node');
573
+ }
574
+ const reportEntry = appliedById.get(admitted.message_id);
575
+ if (reportEntry === undefined) {
576
+ return denied('acceptance_untrusted', `report ${admitted.message_id} is not registered in the task record`);
577
+ }
578
+ // The authoritative decision set comes from the record, never from the supplied list
579
+ // alone: an omitted later rejection must not disappear.
580
+ const decisions = appliedMessages.filter((entry) => entry.type === 'ANSWER' &&
581
+ entry.state === 'applied' &&
582
+ // A decision addressed to this report belongs to the authoritative set even when it
583
+ // names a different attempt; the correlation checks below then refuse it.
584
+ entry.reply_to === admitted.message_id);
585
+ if (decisions.length === 0) {
586
+ const suppliedTargetsReport = [...envelopes.values()].some((envelope) => envelope.type === 'ANSWER' &&
587
+ envelope.reply_to === admitted.message_id);
588
+ return suppliedTargetsReport
589
+ ? denied('acceptance_untrusted', 'the supplied decision is not registered in the task record')
590
+ : denied('acceptance_missing', 'no registered coordinator decision targets this report');
591
+ }
592
+ const resolved = [];
593
+ for (const entry of decisions) {
594
+ const envelope = envelopes.get(entry.message_id);
595
+ if (envelope === undefined) {
596
+ limits.add('acceptance_envelope_missing');
597
+ return denied('acceptance_untrusted', `registered decision ${entry.message_id} was not supplied, so the decision set is incomplete`);
598
+ }
599
+ if (entry.type !== envelope.type ||
600
+ entry.content_digest !== contentDigest(envelope) ||
601
+ entry.from.agent_id !== envelope.from.agent_id ||
602
+ entry.from.session_id !== envelope.from.session_id) {
603
+ return denied('acceptance_untrusted', `registered decision ${entry.message_id} does not match its stored digest`);
604
+ }
605
+ const acceptance = envelope.body?.['acceptance'];
606
+ if (!isRecord(acceptance)) {
607
+ return denied('acceptance_mismatch', `registered decision ${entry.message_id} carries no acceptance payload`);
608
+ }
609
+ resolved.push({ entry, envelope, acceptance });
610
+ }
611
+ // Authoritative application order, never sender timestamps or message-id ordering.
612
+ resolved.sort((left, right) => (registrationIndex.get(left.entry.message_id) ?? 0) -
613
+ (registrationIndex.get(right.entry.message_id) ?? 0));
614
+ const witness = resolved[resolved.length - 1];
615
+ if (witness.acceptance['plan_id'] !== plan.plan_id ||
616
+ witness.acceptance['node_id'] !== node.node_id) {
617
+ return denied('acceptance_mismatch', 'the latest decision targets a different plan or node');
618
+ }
619
+ const assignmentId = witness.acceptance['assignment_id'];
620
+ const attemptNumber = witness.acceptance['attempt'];
621
+ const inputRevision = witness.acceptance['input_revision'];
622
+ const reportDigest = witness.acceptance['report_digest'];
623
+ if (assignmentId !== node.assignment_id ||
624
+ attemptNumber !== node.attempt ||
625
+ reportDigest !== reportEntry.content_digest) {
626
+ return denied('acceptance_mismatch', 'acceptance does not target this assignment attempt and registered report digest');
627
+ }
628
+ if (inputRevision !== node.input_revision) {
629
+ return denied('acceptance_stale', 'acceptance targets a different input revision');
630
+ }
631
+ const envelope = witness.envelope;
632
+ if (envelope.subtask_id !== node.assignment_id ||
633
+ envelope.attempt !== node.attempt ||
634
+ envelope.reply_to !== admitted.message_id) {
635
+ return denied('acceptance_mismatch', 'acceptance envelope does not reply to this report for this attempt');
636
+ }
637
+ if (envelope.owner_epoch !== plan.owner_epoch) {
638
+ return denied('acceptance_stale', 'acceptance was issued under a different owner epoch');
639
+ }
640
+ if (envelope.to.agent_id !== node.assignee.agent_id ||
641
+ envelope.to.session_id !== node.assignee.session_id) {
642
+ return denied('acceptance_mismatch', 'acceptance is not addressed to the predecessor assignee');
643
+ }
644
+ if (envelope.from.agent_id === node.assignee.agent_id &&
645
+ envelope.from.session_id === node.assignee.session_id) {
646
+ return denied('acceptance_self_review', 'a writer cannot accept its own report');
647
+ }
648
+ if (coordinator === null) {
649
+ return denied('owner_epoch_mismatch', 'the record has no coordinator, so no acceptance can be current');
650
+ }
651
+ if (envelope.from.agent_id !== coordinator.agent_id ||
652
+ envelope.from.session_id !== coordinator.session_id) {
653
+ return denied('acceptance_not_coordinator', 'acceptance must come from the current coordinator identity');
654
+ }
655
+ const decision = witness.acceptance['decision'];
656
+ if (decision !== 'accepted') {
657
+ return denied(decision === 'rejected' ? 'acceptance_rejected' : 'acceptance_mismatch', `acceptance decision must be accepted, found ${String(decision)}`);
658
+ }
659
+ return settle({ accepted: true, blocker: null });
660
+ };
661
+ const evaluateNode = (nodeId) => {
662
+ const cached = memo.get(nodeId);
663
+ if (cached !== undefined)
664
+ return cached;
665
+ const node = byId.get(nodeId);
666
+ if (node === undefined) {
667
+ const missing = {
668
+ node_id: nodeId,
669
+ role: 'unknown',
670
+ status: 'blocked',
671
+ blockers: [
672
+ {
673
+ code: 'predecessor_incomplete',
674
+ detail: 'node is not part of the plan',
675
+ },
676
+ ],
677
+ recovery: RECOVERY.get('predecessor_incomplete') ?? null,
678
+ };
679
+ memo.set(nodeId, missing);
680
+ return missing;
681
+ }
682
+ const blockers = [];
683
+ const add = (code, detail) => {
684
+ if (blockers.some((blocker) => blocker.code === code))
685
+ return;
686
+ blockers.push({ code, detail });
687
+ };
688
+ if (!epochCurrent) {
689
+ add('owner_epoch_mismatch', `plan owner epoch ${plan.owner_epoch} is not the current epoch ${coordinator?.owner_epoch ?? 'none'}`);
690
+ }
691
+ if (record.dispatch_frozen === true) {
692
+ add('dispatch_frozen', 'the record is dispatch-frozen; authority is not currently dispatchable');
693
+ }
694
+ const facts = factsFor(node);
695
+ if (facts.latestAttempt !== null && facts.latestAttempt !== node.attempt) {
696
+ add('assignment_stale', `planned attempt ${node.attempt} is not the latest attempt ${facts.latestAttempt}`);
697
+ }
698
+ const attempt = facts.attempt;
699
+ if (attempt !== null) {
700
+ if (attempt.assignee.agent_id !== node.assignee.agent_id ||
701
+ attempt.assignee.session_id !== node.assignee.session_id) {
702
+ add('assignee_mismatch', 'recorded assignee differs from the planned assignee');
703
+ }
704
+ if (attempt.input_revision === null) {
705
+ add('input_unpinned', 'the recorded attempt has no pinned input revision');
706
+ }
707
+ else if (attempt.input_revision !== node.input_revision) {
708
+ add('input_changed', `recorded input revision ${attempt.input_revision} differs from the plan`);
709
+ }
710
+ if (attempt.owner_epoch !== null &&
711
+ attempt.owner_epoch !== plan.owner_epoch) {
712
+ add('owner_epoch_mismatch', `attempt was issued under owner epoch ${attempt.owner_epoch}`);
713
+ }
714
+ for (const entry of record.uncertainty ?? []) {
715
+ if (entry.resolved_at)
716
+ continue;
717
+ if (entry.subtask_id !== node.assignment_id)
718
+ continue;
719
+ if (entry.attempt !== null && entry.attempt !== node.attempt)
720
+ continue;
721
+ if (IGNORED_UNCERTAINTY.has(entry.kind))
722
+ continue;
723
+ add('uncertainty_open', `unresolved ${entry.kind} uncertainty ${entry.uncertainty_id}`);
724
+ break;
725
+ }
726
+ }
727
+ const ownState = attempt?.state ?? null;
728
+ if (ownState === 'failed' || ownState === 'cancelled') {
729
+ add('own_failed', `recorded attempt is ${ownState}`);
730
+ }
731
+ else if (ownState === 'needs_input') {
732
+ add('own_blocked', 'recorded attempt is waiting for input');
733
+ }
734
+ else if (ownState === 'unknown') {
735
+ add('own_uncertain', 'native execution state of the attempt is unknown');
736
+ }
737
+ for (const dependencyId of node.depends_on) {
738
+ const dependency = evaluateNode(dependencyId);
739
+ if (dependency.status === 'ready' || dependency.status === 'running') {
740
+ add('predecessor_incomplete', `predecessor ${dependencyId} has no current applied result yet`);
741
+ continue;
742
+ }
743
+ if (dependency.status === 'blocked') {
744
+ const failed = dependency.blockers.some((blocker) => blocker.code === 'own_failed');
745
+ add(failed ? 'predecessor_failed' : 'predecessor_blocked', `predecessor ${dependencyId} is blocked by ${dependency.blockers[0]?.code ?? 'unknown'}`);
746
+ continue;
747
+ }
748
+ if (dependency.status === 'awaiting_acceptance') {
749
+ const dependencyNode = byId.get(dependencyId);
750
+ const outcome = dependencyNode === undefined
751
+ ? {
752
+ accepted: false,
753
+ blocker: {
754
+ code: 'predecessor_incomplete',
755
+ detail: `predecessor ${dependencyId} is not part of the plan`,
756
+ },
757
+ }
758
+ : acceptanceOf(dependencyNode);
759
+ if (outcome.blocker !== null) {
760
+ add(outcome.blocker.code, outcome.blocker.detail);
761
+ }
762
+ }
763
+ }
764
+ // Resource scope describes current and future execution. A node whose attempt already
765
+ // ended (completed, failed or cancelled) no longer proposes to write, so a successor's
766
+ // claim on the same path cannot retroactively block it; planning never moves a claim.
767
+ const executionEnded = ownState === 'completed' ||
768
+ ownState === 'failed' ||
769
+ ownState === 'cancelled';
770
+ if (!executionEnded) {
771
+ for (const resource of node.resources) {
772
+ const conflicting = claims.find((claim) => claimOverlaps(claim, resource, resource.access) &&
773
+ !(claim.holder?.agent_id === node.assignee.agent_id &&
774
+ claim.holder?.session_id === node.assignee.session_id));
775
+ if (conflicting !== undefined) {
776
+ add('resource_claim_conflict', `active claim ${conflicting.claim_id} on ${resource.path} is held by another holder`);
777
+ }
778
+ for (const other of plan.nodes) {
779
+ if (other.node_id === node.node_id)
780
+ continue;
781
+ if (dependsOn(node.node_id, other.node_id) ||
782
+ dependsOn(other.node_id, node.node_id))
783
+ continue;
784
+ const otherEnded = other.node_id !== node.node_id && executionEndedFor(other);
785
+ if (otherEnded)
786
+ continue;
787
+ const overlapping = other.resources.some((candidate) => (candidate.access === 'write' || resource.access === 'write') &&
788
+ overlap(candidate.path, resource.path));
789
+ if (overlapping) {
790
+ add('resource_overlap', `scope overlaps concurrently planned node ${other.node_id}`);
791
+ }
792
+ }
793
+ }
794
+ }
795
+ const acceptance = ownState === 'completed' ? acceptanceOf(node) : null;
796
+ let status;
797
+ if (blockers.length > 0)
798
+ status = 'blocked';
799
+ else if (ownState === 'completed') {
800
+ status =
801
+ acceptance?.accepted === true ? 'accepted' : 'awaiting_acceptance';
802
+ }
803
+ else if (ownState === null)
804
+ status = 'ready';
805
+ else if (ownState === 'assigned' ||
806
+ ownState === 'accepted' ||
807
+ ownState === 'running')
808
+ status = 'running';
809
+ else
810
+ status = 'blocked';
811
+ const firstBlocker = blockers[0];
812
+ const evaluation = {
813
+ node_id: node.node_id,
814
+ role: node.role,
815
+ status,
816
+ blockers,
817
+ acceptance_blocker: acceptance?.blocker ?? null,
818
+ recovery: firstBlocker !== undefined
819
+ ? (RECOVERY.get(firstBlocker.code) ?? null)
820
+ : acceptance !== null && acceptance.blocker !== null
821
+ ? (RECOVERY.get(acceptance.blocker.code) ?? null)
822
+ : null,
823
+ };
824
+ memo.set(node.node_id, evaluation);
825
+ return evaluation;
826
+ };
827
+ const nodes = plan.nodes.map((node) => evaluateNode(node.node_id));
828
+ const ids = (status) => nodes.filter((node) => node.status === status).map((node) => node.node_id);
829
+ return {
830
+ schema: WORK_PLAN_EVALUATION_SCHEMA,
831
+ ok: true,
832
+ plan_id: plan.plan_id,
833
+ task_id: plan.task_id,
834
+ owner_epoch: plan.owner_epoch,
835
+ nodes,
836
+ ready_nodes: ids('ready'),
837
+ running_nodes: ids('running'),
838
+ awaiting_acceptance_nodes: ids('awaiting_acceptance'),
839
+ accepted_nodes: ids('accepted'),
840
+ blocked_nodes: ids('blocked'),
841
+ limits: [...limits],
842
+ };
843
+ }