@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,821 @@
1
+ import { object as isRecord } from '../infra/validation.js';
2
+ import { createHash } from 'node:crypto';
3
+ import { canonicalJsonDigest } from '../infra/json.js';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { assertTaskStoreCurrent, openTaskStore } from './collab-task-store.js';
7
+ import { readBoundedFile } from './bounded-file.js';
8
+ import { buildMessage, canonicalJson } from '../collab/envelope.js';
9
+ import { CLOSED_STATES, resolveUncertainty, transitionAttempt, } from '../collab/lifecycle.js';
10
+ import { readRecord, transact } from '../collab/store.js';
11
+ import { submitMessage } from '../collab/messages.js';
12
+ import { isAssignment } from '../collab/contracts.js';
13
+ import { expandRolePlan, } from './collab-role-plan.js';
14
+ import { validateWorkPlan, } from '../collab/work-plan.js';
15
+ import { claimResources, expandResources, releaseClaim, } from '../collab/resources.js';
16
+ const REPAIR_JOURNAL_SCHEMA = 'pappra/collab-role-repair-journal/2';
17
+ const STOP_RECEIPT_SCHEMA = 'pappra/collab-native-stop-receipt/1';
18
+ const MAX_REPAIR_JOURNAL_BYTES = 1024 * 1024;
19
+ const MAX_REPAIRS = 64;
20
+ const MAX_STOP_RECEIPT_AGE_MS = 30_000;
21
+ function stableDigest(value) {
22
+ return canonicalJsonDigest(value);
23
+ }
24
+ function sameJson(left, right) {
25
+ return canonicalJson(left) === canonicalJson(right);
26
+ }
27
+ function validateJournal(value) {
28
+ if (!isRecord(value) ||
29
+ value['schema'] !== REPAIR_JOURNAL_SCHEMA ||
30
+ !Array.isArray(value['intents']) ||
31
+ value['intents'].length > MAX_REPAIRS)
32
+ return null;
33
+ for (const item of value['intents']) {
34
+ if (!isRecord(item) ||
35
+ typeof item['authorization_digest'] !== 'string' ||
36
+ typeof item['event_id'] !== 'string' ||
37
+ !isRecord(item['source']) ||
38
+ !isRecord(item['source_plan']) ||
39
+ !isRecord(item['next_role_plan']) ||
40
+ !isRecord(item['next_execution']) ||
41
+ !Array.isArray(item['release_claim_ids']) ||
42
+ !isRecord(item['next_plan']) ||
43
+ !isRecord(item['assign_message']) ||
44
+ typeof item['close_source'] !== 'boolean' ||
45
+ !(item['stop_receipt'] === null || isRecord(item['stop_receipt'])) ||
46
+ ![
47
+ 'prepared',
48
+ 'source-closed',
49
+ 'source-claims-released',
50
+ 'assignment-applied',
51
+ 'next-claims-acquired',
52
+ 'complete',
53
+ ].includes(String(item['state'])))
54
+ return null;
55
+ }
56
+ return value;
57
+ }
58
+ function readJournal(file) {
59
+ if (!fs.existsSync(file))
60
+ return { schema: REPAIR_JOURNAL_SCHEMA, intents: [] };
61
+ const read = readBoundedFile(file, MAX_REPAIR_JOURNAL_BYTES);
62
+ if (!read.ok)
63
+ return undefined;
64
+ try {
65
+ return validateJournal(JSON.parse(read.text)) ?? undefined;
66
+ }
67
+ catch {
68
+ return undefined;
69
+ }
70
+ }
71
+ function writeAtomic(file, content) {
72
+ const temporary = `${file}.${process.pid}.${createHash('sha256').update(`${Date.now()}-${Math.random()}`).digest('hex').slice(0, 16)}.tmp`;
73
+ let descriptor = null;
74
+ try {
75
+ descriptor = fs.openSync(temporary, 'wx', 0o600);
76
+ fs.writeFileSync(descriptor, content, 'utf8');
77
+ fs.fsyncSync(descriptor);
78
+ fs.closeSync(descriptor);
79
+ descriptor = null;
80
+ fs.renameSync(temporary, file);
81
+ const parent = fs.openSync(path.dirname(file), 'r');
82
+ try {
83
+ fs.fsyncSync(parent);
84
+ }
85
+ finally {
86
+ fs.closeSync(parent);
87
+ }
88
+ }
89
+ finally {
90
+ if (descriptor !== null)
91
+ fs.closeSync(descriptor);
92
+ try {
93
+ fs.unlinkSync(temporary);
94
+ }
95
+ catch {
96
+ // Rename already consumed it, or cleanup can be retried later.
97
+ }
98
+ }
99
+ }
100
+ function persistJournal(file, journal) {
101
+ writeAtomic(file, `${JSON.stringify(journal)}\n`);
102
+ }
103
+ function requireTaskLocalFile(taskRoot, file) {
104
+ if (!path.isAbsolute(file))
105
+ return false;
106
+ const absolute = path.resolve(file);
107
+ const relative = path.relative(path.resolve(taskRoot), absolute);
108
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative))
109
+ return false;
110
+ try {
111
+ const parent = fs.lstatSync(path.dirname(absolute));
112
+ const target = fs.existsSync(absolute) ? fs.lstatSync(absolute) : null;
113
+ return (parent.isDirectory() &&
114
+ !parent.isSymbolicLink() &&
115
+ (target === null || (target.isFile() && !target.isSymbolicLink())));
116
+ }
117
+ catch {
118
+ return false;
119
+ }
120
+ }
121
+ function sameIdentity(left, right) {
122
+ return (left?.agent_id === right.agent_id &&
123
+ left.session_id === right.session_id &&
124
+ (right.harness === undefined || (left.harness ?? null) === right.harness) &&
125
+ (right.cwd === undefined || (left.cwd ?? null) === right.cwd));
126
+ }
127
+ function validAuthorization(authorization, sourceNode, coordinator, maxAttempts, resources) {
128
+ const { source, next, approval } = authorization;
129
+ return (coordinator !== null &&
130
+ authorization.coordinator.agent_id === coordinator.agent_id &&
131
+ authorization.coordinator.session_id === coordinator.session_id &&
132
+ authorization.coordinator.owner_epoch === coordinator.owner_epoch &&
133
+ source.node_id === sourceNode.node_id &&
134
+ source.assignment_id === sourceNode.assignment_id &&
135
+ source.attempt === sourceNode.attempt &&
136
+ source.input_revision === sourceNode.input_revision &&
137
+ sameJson(source.resources, sourceNode.resources) &&
138
+ source.attempt < maxAttempts &&
139
+ next.assignment_id === source.assignment_id &&
140
+ typeof next.assignment_ref === 'string' &&
141
+ path.isAbsolute(next.assignment_ref) &&
142
+ typeof next.assignment_digest === 'string' &&
143
+ /^sha256:[0-9a-f]{64}$/.test(next.assignment_digest) &&
144
+ next.attempt === source.attempt + 1 &&
145
+ next.attempt <= maxAttempts &&
146
+ next.input_revision.length > 0 &&
147
+ next.participant_id.length > 0 &&
148
+ next.assignee.agent_id.length > 0 &&
149
+ next.assignee.session_id.length > 0 &&
150
+ sameJson(next.resources, resources) &&
151
+ approval.approved_by === coordinator.agent_id &&
152
+ approval.reason.trim().length > 0 &&
153
+ approval.authorization_id.length > 0 &&
154
+ approval.owner_epoch === coordinator.owner_epoch &&
155
+ approval.source_attempt === source.attempt &&
156
+ approval.source_input_revision === source.input_revision &&
157
+ approval.next_assignment_id === next.assignment_id &&
158
+ approval.next_attempt === next.attempt &&
159
+ approval.next_input_revision === next.input_revision &&
160
+ approval.resources_digest === stableDigest(next.resources));
161
+ }
162
+ function validStopReceipt(receipt, taskId, coordinator, source, assignee, now) {
163
+ if (!receipt ||
164
+ receipt.schema !== STOP_RECEIPT_SCHEMA ||
165
+ receipt.observation !== 'confirmed')
166
+ return false;
167
+ const observed = Date.parse(receipt.observed_at);
168
+ return (receipt.task_id === taskId &&
169
+ receipt.subtask_id === source.assignment_id &&
170
+ receipt.attempt === source.attempt &&
171
+ receipt.owner_epoch === coordinator.owner_epoch &&
172
+ sameIdentity(receipt.assignee, assignee) &&
173
+ receipt.evidence_ref.trim().length > 0 &&
174
+ Number.isFinite(observed) &&
175
+ observed <= now + 5_000 &&
176
+ now - observed <= MAX_STOP_RECEIPT_AGE_MS);
177
+ }
178
+ function currentAuthorization(policy, node) {
179
+ return (policy.authorizations.find((authorization) => authorization.source.node_id === node.node_id &&
180
+ authorization.source.assignment_id === node.assignment_id &&
181
+ authorization.source.attempt === node.attempt) ?? null);
182
+ }
183
+ function nextRolePlan(rolePlan, authorization) {
184
+ const sourceAssignment = rolePlan.assignments.find((entry) => entry.node_id === authorization.source.node_id);
185
+ if (!sourceAssignment)
186
+ return null;
187
+ const participant = rolePlan.participants.find((entry) => entry.participant_id === authorization.next.participant_id);
188
+ if (participant &&
189
+ !sameIdentity(participant.assignee, authorization.next.assignee) &&
190
+ (participant.participant_id !== sourceAssignment.participant_id ||
191
+ rolePlan.assignments.some((entry) => entry.node_id !== sourceAssignment.node_id &&
192
+ entry.participant_id === participant.participant_id)))
193
+ return null;
194
+ const assignments = rolePlan.assignments.map((assignment) => assignment.node_id === authorization.source.node_id
195
+ ? {
196
+ ...assignment,
197
+ assignment_id: authorization.next.assignment_id,
198
+ attempt: authorization.next.attempt,
199
+ input_revision: authorization.next.input_revision,
200
+ participant_id: authorization.next.participant_id,
201
+ resources: authorization.next.resources,
202
+ }
203
+ : assignment);
204
+ const usedParticipants = new Set(assignments.map((entry) => entry.participant_id));
205
+ const participants = [
206
+ ...rolePlan.participants.filter((entry) => entry.participant_id !== authorization.next.participant_id),
207
+ {
208
+ participant_id: authorization.next.participant_id,
209
+ role: sourceAssignment.role,
210
+ assignee: authorization.next.assignee,
211
+ },
212
+ ].filter((entry) => usedParticipants.has(entry.participant_id));
213
+ return { ...rolePlan, assignments, participants };
214
+ }
215
+ function nextExecution(current, authorization, sourceNode) {
216
+ const configured = authorization.next_execution;
217
+ const unchangedPins = authorization.next.input_revision === authorization.source.input_revision &&
218
+ sameJson(authorization.next.resources, authorization.source.resources) &&
219
+ authorization.next.assignee.agent_id === sourceNode.assignee.agent_id &&
220
+ authorization.next.assignee.session_id === sourceNode.assignee.session_id;
221
+ if (!configured)
222
+ return unchangedPins ? current : null;
223
+ if (configured.task_root !== current.task_root ||
224
+ configured.plan_file !== current.plan_file ||
225
+ configured.binding_file !== current.binding_file ||
226
+ configured.caller.agent_id !== current.caller.agent_id ||
227
+ configured.caller.session_id !== current.caller.session_id ||
228
+ configured.caller_pane !== current.caller_pane ||
229
+ configured.capability_cards.some((card) => !path.isAbsolute(card.capabilities_file) || !card.assignment_id))
230
+ return null;
231
+ const cards = configured.capability_cards.filter((card) => card.assignment_id === authorization.next.assignment_id);
232
+ return cards.length === 1 ? configured : null;
233
+ }
234
+ function protocolMessageIds(authorizationDigest) {
235
+ const prefix = `role-repair-${authorizationDigest.slice(0, 32)}`;
236
+ return { assign: `${prefix}-assign` };
237
+ }
238
+ /** A trusted host observation is coordinator evidence, never a forged worker report. */
239
+ function closeSourceAfterVerifiedStop(taskRoot, authorization, assignee, receipt, clock) {
240
+ const closed = transact(taskRoot, {
241
+ actor: authorization.coordinator.agent_id,
242
+ purpose: 'role-repair-verified-stop',
243
+ clock,
244
+ apply(record) {
245
+ const coordinator = record.coordinator;
246
+ const source = record.attempts.find((entry) => entry.subtask_id === authorization.source.assignment_id &&
247
+ entry.attempt === authorization.source.attempt);
248
+ if (!coordinator ||
249
+ !sameIdentity(coordinator, authorization.coordinator) ||
250
+ coordinator.owner_epoch !== authorization.coordinator.owner_epoch ||
251
+ !source ||
252
+ source.owner_epoch !== authorization.coordinator.owner_epoch ||
253
+ source.input_revision !== authorization.source.input_revision ||
254
+ !sameIdentity(source.assignee, assignee) ||
255
+ !validStopReceipt(receipt, record.task_id, authorization.coordinator, authorization.source, source.assignee, clock))
256
+ return { ok: false, reason: 'verified_stop_pins_stale' };
257
+ if (source.state !== 'cancelled') {
258
+ const transition = transitionAttempt(record, {
259
+ subtaskId: source.subtask_id,
260
+ attempt: source.attempt,
261
+ to: 'cancelled',
262
+ actor: coordinator.agent_id,
263
+ reason: `authorized role repair ${authorization.approval.authorization_id}`,
264
+ evidence: receipt,
265
+ clock,
266
+ });
267
+ if (!transition.ok)
268
+ return transition;
269
+ }
270
+ for (const claim of record.claims) {
271
+ if (!claim.released_at &&
272
+ sameIdentity(claim.holder, source.assignee) &&
273
+ claim.subtask_id === source.subtask_id &&
274
+ claim.attempt === source.attempt)
275
+ claim.cancel_pending = false;
276
+ }
277
+ for (const entry of record.uncertainty) {
278
+ if (!entry.resolved_at &&
279
+ entry.kind === 'cancel_unconfirmed' &&
280
+ entry.subtask_id === source.subtask_id &&
281
+ entry.attempt === source.attempt)
282
+ resolveUncertainty(record, {
283
+ uncertaintyId: entry.uncertainty_id,
284
+ resolution: 'trusted host confirmed the source stopped for role repair',
285
+ resolutionEvidence: receipt,
286
+ actor: coordinator.agent_id,
287
+ clock,
288
+ });
289
+ }
290
+ return {
291
+ record,
292
+ op: 'role-repair-verified-stop',
293
+ summary: {
294
+ subtask_id: source.subtask_id,
295
+ attempt: source.attempt,
296
+ authorization_id: authorization.approval.authorization_id,
297
+ },
298
+ result: null,
299
+ };
300
+ },
301
+ });
302
+ return closed.ok;
303
+ }
304
+ function writeUpdatedPlan(planFile, sourcePlan, nextPlan) {
305
+ try {
306
+ const existing = fs.lstatSync(planFile);
307
+ if (!existing.isFile() || existing.isSymbolicLink())
308
+ return false;
309
+ const current = readBoundedFile(planFile, 512 * 1024);
310
+ if (!current.ok)
311
+ return false;
312
+ const parsed = JSON.parse(current.text);
313
+ if (sameJson(parsed, nextPlan))
314
+ return true;
315
+ if (!sameJson(parsed, sourcePlan))
316
+ return false;
317
+ writeAtomic(planFile, `${JSON.stringify(nextPlan)}\n`);
318
+ return true;
319
+ }
320
+ catch {
321
+ return false;
322
+ }
323
+ }
324
+ function submitIdempotently(taskRoot, message) {
325
+ try {
326
+ const result = submitMessage(taskRoot, message);
327
+ return result.ok;
328
+ }
329
+ catch {
330
+ return false;
331
+ }
332
+ }
333
+ function matchingClaimKeys(taskRoot, resources, holder, subtaskId, attempt) {
334
+ const record = readRecord(taskRoot);
335
+ if (!record)
336
+ return null;
337
+ const expected = new Set();
338
+ for (const access of ['read', 'write']) {
339
+ const group = resources.filter((resource) => resource.access === access);
340
+ if (group.length === 0)
341
+ continue;
342
+ const expanded = expandResources(group.map((resource) => ({ path: resource.path })), { includeGit: access === 'write' });
343
+ for (const item of expanded)
344
+ expected.add(`${access}:${item.key}`);
345
+ }
346
+ const existing = (record.claims ?? [])
347
+ .filter((claim) => claim.access === resources[0]?.access &&
348
+ !claim.released_at &&
349
+ claim.subtask_id === subtaskId &&
350
+ claim.attempt === attempt &&
351
+ claim.holder?.agent_id === holder.agent_id &&
352
+ claim.holder?.session_id === holder.session_id)
353
+ .map((claim) => `${claim.access}:${claim.key}`);
354
+ return { expected: [...expected], existing };
355
+ }
356
+ function releaseSourceClaims(taskRoot, ids, holder) {
357
+ for (const claimId of ids) {
358
+ const record = readRecord(taskRoot);
359
+ const claim = record?.claims?.find((entry) => entry.claim_id === claimId);
360
+ if (!claim || claim.released_at)
361
+ continue;
362
+ const released = releaseClaim(taskRoot, {
363
+ claimId,
364
+ holder,
365
+ reason: 'authorized role repair closed the source attempt',
366
+ });
367
+ if (!released.ok)
368
+ return false;
369
+ }
370
+ return true;
371
+ }
372
+ function acquireNextClaims(taskRoot, authorization) {
373
+ for (const access of ['read', 'write']) {
374
+ const resources = authorization.next.resources.filter((resource) => resource.access === access);
375
+ if (resources.length === 0)
376
+ continue;
377
+ const observed = matchingClaimKeys(taskRoot, resources, authorization.next.assignee, authorization.next.assignment_id, authorization.next.attempt);
378
+ if (!observed)
379
+ return false;
380
+ const expected = observed.expected;
381
+ if (observed.existing.length === expected.length &&
382
+ expected.every((key) => observed.existing.includes(key)))
383
+ continue;
384
+ if (observed.existing.length > 0)
385
+ return false;
386
+ const claimed = claimResources(taskRoot, {
387
+ resources: resources.map((resource) => ({ path: resource.path })),
388
+ access,
389
+ holder: authorization.next.assignee,
390
+ subtaskId: authorization.next.assignment_id,
391
+ attempt: authorization.next.attempt,
392
+ note: `authorized role repair ${authorization.approval.authorization_id}`,
393
+ });
394
+ if (!claimed.ok)
395
+ return false;
396
+ }
397
+ return true;
398
+ }
399
+ function outcome(status, diagnostic, rolePlan = null, expansion = null, execution = null) {
400
+ return { status, diagnostic, role_plan: rolePlan, expansion, execution };
401
+ }
402
+ /** Replace only the exact rejected node selected by the bounded authorization policy. */
403
+ export async function repairRejectedRoleNode(request, deps = {}) {
404
+ const { role_plan: rolePlan, policy } = request;
405
+ if (!policy ||
406
+ !Array.isArray(policy.authorizations) ||
407
+ policy.authorizations.length === 0 ||
408
+ policy.authorizations.length > MAX_REPAIRS ||
409
+ !Number.isSafeInteger(request.max_attempts) ||
410
+ request.reserved_actions < 1 ||
411
+ request.reserved_actions > request.max_actions)
412
+ return outcome('repair-required', 'repair_authority_missing_or_unbounded');
413
+ if (policy.authorizations.length > Math.max(0, request.max_attempts - 1))
414
+ return outcome('repair-exhausted', 'repair_policy_exceeds_attempt_bound');
415
+ if (!requireTaskLocalFile(request.task_root, request.journal_file) ||
416
+ !requireTaskLocalFile(request.task_root, request.plan_file))
417
+ return outcome('reconciliation-required', 'repair_paths_not_task_local_regular_files');
418
+ const expanded = expandRolePlan(rolePlan);
419
+ if (!expanded.ok)
420
+ return outcome('repair-required', 'repair_role_plan_invalid');
421
+ const rejected = new Set(request.rejected_node_ids);
422
+ const sourceNode = expanded.plan.nodes.find((node) => rejected.has(node.node_id));
423
+ if (!sourceNode)
424
+ return outcome('repair-required', 'repair_source_not_rejected');
425
+ const authorization = currentAuthorization(policy, sourceNode);
426
+ if (!authorization)
427
+ return outcome(sourceNode.attempt >= request.max_attempts
428
+ ? 'repair-exhausted'
429
+ : 'repair-required', sourceNode.attempt >= request.max_attempts
430
+ ? 'rejected_attempt_limit_reached'
431
+ : 'repair_authorization_missing_for_exact_attempt');
432
+ if (sourceNode.attempt >= request.max_attempts)
433
+ return outcome('repair-exhausted', 'rejected_attempt_limit_reached');
434
+ const updatedRolePlan = nextRolePlan(rolePlan, authorization);
435
+ if (!updatedRolePlan)
436
+ return outcome('repair-required', 'repair_next_participant_pin_mismatch');
437
+ const updatedExpansion = expandRolePlan(updatedRolePlan);
438
+ if (!updatedExpansion.ok)
439
+ return outcome('repair-required', 'repair_next_role_plan_refused');
440
+ const updatedExecution = nextExecution(request.execution, authorization, sourceNode);
441
+ if (!updatedExecution)
442
+ return outcome('repair-required', 'next_execution_pins_missing_or_incompatible');
443
+ const nextFile = readBoundedFile(authorization.next.assignment_ref, 1024 * 1024);
444
+ let nextAssignment;
445
+ try {
446
+ nextAssignment = nextFile.ok ? JSON.parse(nextFile.text) : null;
447
+ }
448
+ catch {
449
+ nextAssignment = null;
450
+ }
451
+ if (!nextFile.ok ||
452
+ nextFile.digest !== authorization.next.assignment_digest ||
453
+ !isAssignment(nextAssignment) ||
454
+ nextAssignment.task_id !== rolePlan.task_id ||
455
+ nextAssignment.assignment_id !== authorization.next.assignment_id ||
456
+ nextAssignment.attempt !== authorization.next.attempt ||
457
+ !sameIdentity(nextAssignment.assignee, authorization.next.assignee) ||
458
+ nextAssignment.coordinator.agent_id !==
459
+ authorization.coordinator.agent_id ||
460
+ nextAssignment.coordinator.session_id !==
461
+ authorization.coordinator.session_id ||
462
+ nextAssignment.coordinator.owner_epoch !==
463
+ authorization.coordinator.owner_epoch ||
464
+ !sameJson(nextAssignment.resources.map((resource) => ({
465
+ path: resource.path,
466
+ access: resource.access,
467
+ })), authorization.next.resources))
468
+ return outcome('repair-required', 'next_assignment_artifact_missing_or_mismatched');
469
+ const opened = await openTaskStore(request.task_root);
470
+ if (!opened.ok)
471
+ return outcome('reconciliation-required', 'repair_task_record_unavailable');
472
+ const current = await assertTaskStoreCurrent(opened.value);
473
+ if (current !== null)
474
+ return outcome('reconciliation-required', 'repair_task_record_changed');
475
+ const record = opened.value.record;
476
+ const coordinator = record.coordinator;
477
+ if (!validAuthorization(authorization, sourceNode, coordinator, request.max_attempts, authorization.next.resources) ||
478
+ record.task_id !== rolePlan.task_id ||
479
+ rolePlan.owner_epoch !== coordinator?.owner_epoch ||
480
+ coordinator?.agent_id !== authorization.coordinator.agent_id ||
481
+ coordinator?.session_id !== authorization.coordinator.session_id)
482
+ return outcome('repair-required', 'repair_approval_or_coordinator_pins_stale');
483
+ const sourceAttempt = (record.attempts ?? []).find((attempt) => attempt.subtask_id === authorization.source.assignment_id &&
484
+ attempt.attempt === authorization.source.attempt);
485
+ if (!sourceAttempt ||
486
+ sourceAttempt.input_revision !== authorization.source.input_revision ||
487
+ sourceAttempt.owner_epoch !== authorization.coordinator.owner_epoch ||
488
+ !sameIdentity(sourceAttempt.assignee, sourceNode.assignee))
489
+ return outcome('repair-required', 'repair_source_attempt_pins_stale');
490
+ const authDigest = stableDigest(authorization);
491
+ const journalFile = `${request.journal_file}.repair.json`;
492
+ if (!requireTaskLocalFile(request.task_root, journalFile))
493
+ return outcome('reconciliation-required', 'repair_journal_path_invalid');
494
+ const journal = readJournal(journalFile);
495
+ if (!journal)
496
+ return outcome('reconciliation-required', 'repair_journal_invalid_or_unreadable');
497
+ let intent = journal.intents.find((entry) => entry.authorization_digest === authDigest);
498
+ if (intent?.state === 'complete') {
499
+ const recovered = await recoverRoleRepairPlan(rolePlan, request.execution, policy, request.journal_file, request.task_root, request.plan_file, request.max_attempts);
500
+ return sameJson(recovered.role_plan, updatedRolePlan) &&
501
+ sameJson(recovered.execution, updatedExecution)
502
+ ? outcome('repaired', 'repair_already_reconciled', intent.next_role_plan, updatedExpansion, intent.next_execution)
503
+ : outcome('reconciliation-required', 'repair_journal_plan_invalid');
504
+ }
505
+ if (intent) {
506
+ if (intent.source.attempt !== authorization.source.attempt ||
507
+ !sameJson(intent.source, authorization.source) ||
508
+ !sameJson(intent.source_plan, expanded.plan) ||
509
+ !sameJson(intent.next_plan, updatedExpansion.plan) ||
510
+ !sameJson(intent.next_role_plan, updatedRolePlan) ||
511
+ !sameJson(intent.next_execution, updatedExecution))
512
+ return outcome('reconciliation-required', 'repair_intent_conflicts_with_current_authority');
513
+ }
514
+ else {
515
+ if (journal.intents.length >= MAX_REPAIRS)
516
+ return outcome('repair-exhausted', 'repair_journal_limit_reached');
517
+ const alreadyClosed = CLOSED_STATES.includes(sourceAttempt.state);
518
+ // A terminal task state does not fence the old native session's input.
519
+ const stopReceipt = (await deps.confirm_stopped?.({
520
+ task_id: record.task_id,
521
+ coordinator: authorization.coordinator,
522
+ source: authorization.source,
523
+ assignee: sourceAttempt.assignee,
524
+ })) ?? null;
525
+ if (!validStopReceipt(stopReceipt, record.task_id, authorization.coordinator, authorization.source, sourceAttempt.assignee, (deps.now ?? Date.now)()))
526
+ return outcome('repair-required', 'live_supported_stop_receipt_missing_or_stale');
527
+ if (await assertTaskStoreCurrent(opened.value))
528
+ return outcome('reconciliation-required', 'repair_authority_changed_during_native_stop');
529
+ // Normal commits replace task.json. Revalidate the authority that justified
530
+ // the stop instead of using its inode as an accidental state-change fence.
531
+ const afterStop = readRecord(request.task_root);
532
+ const currentSource = afterStop?.attempts.find((attempt) => attempt.subtask_id === authorization.source.assignment_id &&
533
+ attempt.attempt === authorization.source.attempt);
534
+ if (!afterStop ||
535
+ !validAuthorization(authorization, sourceNode, afterStop.coordinator, request.max_attempts, authorization.next.resources) ||
536
+ canonicalJson(currentSource) !== canonicalJson(sourceAttempt))
537
+ return outcome('reconciliation-required', 'repair_authority_changed_during_native_stop');
538
+ const activeClaims = (afterStop.claims ?? []).filter((claim) => !claim.released_at);
539
+ const sameHolder = (claim) => claim.holder?.agent_id === sourceAttempt.assignee.agent_id &&
540
+ claim.holder?.session_id === sourceAttempt.assignee.session_id;
541
+ const unscopedReleaseHazard = activeClaims.some((claim) => sameHolder(claim) &&
542
+ (claim.subtask_id === null || claim.subtask_id === undefined) &&
543
+ (claim.attempt === null ||
544
+ claim.attempt === undefined ||
545
+ claim.attempt === authorization.source.attempt)) ||
546
+ activeClaims.some((claim) => sameHolder(claim) &&
547
+ claim.subtask_id === authorization.source.assignment_id &&
548
+ (claim.attempt === null || claim.attempt === undefined));
549
+ if (unscopedReleaseHazard)
550
+ return outcome('repair-required', 'source_claim_scope_is_not_attempt_pinned');
551
+ const releaseClaimIds = activeClaims
552
+ .filter((claim) => sameHolder(claim) &&
553
+ claim.subtask_id === authorization.source.assignment_id &&
554
+ claim.attempt === authorization.source.attempt)
555
+ .map((claim) => claim.claim_id);
556
+ const coordinatorIdentity = {
557
+ agent_id: coordinator.agent_id,
558
+ session_id: coordinator.session_id,
559
+ harness: coordinator.harness ?? null,
560
+ ...(coordinator.cwd ? { cwd: coordinator.cwd } : {}),
561
+ };
562
+ const idsNow = protocolMessageIds(authDigest);
563
+ const assignMessage = buildMessage({
564
+ message_id: idsNow.assign,
565
+ type: 'ASSIGN',
566
+ task_id: record.task_id,
567
+ from: coordinatorIdentity,
568
+ to: authorization.next.assignee,
569
+ subtask_id: authorization.next.assignment_id,
570
+ attempt: authorization.next.attempt,
571
+ owner_epoch: authorization.coordinator.owner_epoch,
572
+ body: {
573
+ assignment_ref: authorization.next.assignment_ref,
574
+ assignment_digest: authorization.next.assignment_digest,
575
+ input_revision: authorization.next.input_revision,
576
+ approval: {
577
+ approved_by: authorization.approval.approved_by,
578
+ reason: authorization.approval.reason,
579
+ },
580
+ },
581
+ });
582
+ intent = {
583
+ authorization_digest: authDigest,
584
+ event_id: request.event_id,
585
+ source: authorization.source,
586
+ source_plan: expanded.plan,
587
+ next_role_plan: updatedRolePlan,
588
+ next_plan: updatedExpansion.plan,
589
+ next_execution: updatedExecution,
590
+ release_claim_ids: releaseClaimIds,
591
+ close_source: !alreadyClosed,
592
+ assign_message: assignMessage,
593
+ stop_receipt: stopReceipt,
594
+ state: 'prepared',
595
+ };
596
+ journal.intents.push(intent);
597
+ try {
598
+ persistJournal(journalFile, journal);
599
+ }
600
+ catch {
601
+ return outcome('reconciliation-required', 'repair_intent_persist_failed');
602
+ }
603
+ }
604
+ if (intent.close_source && intent.state === 'prepared') {
605
+ if (!validStopReceipt(intent.stop_receipt, record.task_id, authorization.coordinator, authorization.source, sourceAttempt.assignee, (deps.now ?? Date.now)())) {
606
+ const fresh = (await deps.confirm_stopped?.({
607
+ task_id: record.task_id,
608
+ coordinator: authorization.coordinator,
609
+ source: authorization.source,
610
+ assignee: sourceAttempt.assignee,
611
+ })) ?? null;
612
+ if (!validStopReceipt(fresh, record.task_id, authorization.coordinator, authorization.source, sourceAttempt.assignee, (deps.now ?? Date.now)()))
613
+ return outcome('repair-required', 'live_supported_stop_receipt_missing_or_stale');
614
+ intent.stop_receipt = fresh;
615
+ try {
616
+ persistJournal(journalFile, journal);
617
+ }
618
+ catch {
619
+ return outcome('reconciliation-required', 'repair_intent_persist_failed');
620
+ }
621
+ }
622
+ if (!closeSourceAfterVerifiedStop(request.task_root, authorization, sourceAttempt.assignee, intent.stop_receipt, (deps.now ?? Date.now)()))
623
+ return outcome('reconciliation-required', 'source_close_protocol_refused');
624
+ intent.state = 'source-closed';
625
+ try {
626
+ persistJournal(journalFile, journal);
627
+ }
628
+ catch {
629
+ return outcome('reconciliation-required', 'source_closed_but_repair_journal_update_failed');
630
+ }
631
+ }
632
+ if (intent.state === 'prepared' && !intent.close_source)
633
+ intent.state = 'source-closed';
634
+ if (intent.state === 'source-closed') {
635
+ if (!releaseSourceClaims(request.task_root, intent.release_claim_ids, sourceAttempt.assignee))
636
+ return outcome('reconciliation-required', 'source_resource_claim_release_refused');
637
+ intent.state = 'source-claims-released';
638
+ try {
639
+ persistJournal(journalFile, journal);
640
+ }
641
+ catch {
642
+ return outcome('reconciliation-required', 'source_claims_released_but_repair_journal_update_failed');
643
+ }
644
+ }
645
+ if (intent.state === 'source-claims-released') {
646
+ if (!submitIdempotently(request.task_root, intent.assign_message))
647
+ return outcome('reconciliation-required', 'next_assignment_protocol_refused');
648
+ intent.state = 'assignment-applied';
649
+ try {
650
+ persistJournal(journalFile, journal);
651
+ }
652
+ catch {
653
+ return outcome('reconciliation-required', 'assignment_applied_but_repair_journal_update_failed');
654
+ }
655
+ }
656
+ if (intent.state === 'assignment-applied') {
657
+ if (!acquireNextClaims(request.task_root, authorization))
658
+ return outcome('reconciliation-required', 'next_attempt_resource_claims_unavailable');
659
+ intent.state = 'next-claims-acquired';
660
+ try {
661
+ persistJournal(journalFile, journal);
662
+ }
663
+ catch {
664
+ return outcome('reconciliation-required', 'next_claims_acquired_but_repair_journal_update_failed');
665
+ }
666
+ }
667
+ if (intent.state === 'next-claims-acquired') {
668
+ if (!writeUpdatedPlan(request.plan_file, intent.source_plan, intent.next_plan))
669
+ return outcome('reconciliation-required', 'assignment_applied_but_plan_update_failed');
670
+ intent.state = 'complete';
671
+ try {
672
+ persistJournal(journalFile, journal);
673
+ }
674
+ catch {
675
+ return outcome('reconciliation-required', 'plan_updated_but_repair_journal_update_failed');
676
+ }
677
+ }
678
+ return outcome('repaired', 'fresh_assignment_registered_resource_claimed_and_work_plan_updated', intent.next_role_plan, updatedExpansion, intent.next_execution);
679
+ }
680
+ /**
681
+ * Read-only recovery of a caller-authorized plan. Journal data alone is never authority:
682
+ * promotion requires the live task record, current coordinator, registered assignment and
683
+ * already-published task-local WorkPlan to match the exact policy transition.
684
+ */
685
+ export async function recoverRoleRepairPlan(rolePlan, execution, policy, journalFile, taskRoot, planFile, maxAttempts) {
686
+ const unchanged = { role_plan: rolePlan, execution };
687
+ if (!policy ||
688
+ !Array.isArray(policy.authorizations) ||
689
+ policy.authorizations.length === 0 ||
690
+ policy.authorizations.some((item) => !isRecord(item)) ||
691
+ !path.isAbsolute(journalFile) ||
692
+ !requireTaskLocalFile(taskRoot, journalFile) ||
693
+ !requireTaskLocalFile(taskRoot, planFile))
694
+ return unchanged;
695
+ const journal = readJournal(`${journalFile}.repair.json`);
696
+ if (!journal)
697
+ return unchanged;
698
+ const opened = await openTaskStore(taskRoot);
699
+ if (!opened.ok || (await assertTaskStoreCurrent(opened.value)) !== null)
700
+ return unchanged;
701
+ const record = opened.value.record;
702
+ const coordinator = record.coordinator;
703
+ if (!coordinator || record.task_id !== rolePlan.task_id)
704
+ return unchanged;
705
+ let currentRolePlan = rolePlan;
706
+ let currentExecution = execution;
707
+ let currentExpansion = expandRolePlan(currentRolePlan);
708
+ if (!currentExpansion.ok)
709
+ return unchanged;
710
+ const visited = new Set();
711
+ let recovered = null;
712
+ for (let pass = 0; pass < policy.authorizations.length; pass += 1) {
713
+ let advanced = false;
714
+ for (const authorization of policy.authorizations) {
715
+ const authDigest = stableDigest(authorization);
716
+ if (visited.has(authDigest))
717
+ continue;
718
+ const sourceNode = currentExpansion.plan.nodes.find((node) => node.node_id === authorization.source.node_id &&
719
+ node.assignment_id === authorization.source.assignment_id &&
720
+ node.attempt === authorization.source.attempt);
721
+ if (!sourceNode)
722
+ continue;
723
+ visited.add(authDigest);
724
+ if (!validAuthorization(authorization, sourceNode, coordinator, maxAttempts, authorization.next.resources) ||
725
+ currentRolePlan.owner_epoch !== coordinator.owner_epoch)
726
+ continue;
727
+ const nextRolePlan = nextRolePlanForRecovery(currentRolePlan, authorization);
728
+ if (!nextRolePlan)
729
+ continue;
730
+ const nextExecutionPins = nextExecution(currentExecution, authorization, sourceNode);
731
+ if (!nextExecutionPins)
732
+ continue;
733
+ const nextExpansion = expandRolePlan(nextRolePlan);
734
+ if (!nextExpansion.ok)
735
+ continue;
736
+ const intent = journal.intents.find((entry) => entry.authorization_digest === authDigest);
737
+ if (!intent ||
738
+ (intent.state !== 'complete' &&
739
+ intent.state !== 'next-claims-acquired') ||
740
+ !sameJson(intent.source, authorization.source) ||
741
+ !sameJson(intent.source_plan, currentExpansion.plan) ||
742
+ !sameJson(intent.next_role_plan, nextRolePlan) ||
743
+ !sameJson(intent.next_plan, nextExpansion.plan) ||
744
+ !sameJson(intent.next_execution, nextExecutionPins))
745
+ continue;
746
+ const ids = protocolMessageIds(authDigest);
747
+ if (intent.assign_message.message_id !== ids.assign ||
748
+ intent.assign_message.type !== 'ASSIGN' ||
749
+ intent.assign_message.task_id !== record.task_id ||
750
+ intent.assign_message.subtask_id !== authorization.next.assignment_id ||
751
+ intent.assign_message.attempt !== authorization.next.attempt ||
752
+ intent.assign_message.owner_epoch !== coordinator.owner_epoch)
753
+ continue;
754
+ const sourceAttempt = (record.attempts ?? []).find((attempt) => attempt.subtask_id === authorization.source.assignment_id &&
755
+ attempt.attempt === authorization.source.attempt);
756
+ const nextAttempt = (record.attempts ?? []).find((attempt) => attempt.subtask_id === authorization.next.assignment_id &&
757
+ attempt.attempt === authorization.next.attempt);
758
+ const assignmentRecord = (record.messages ?? []).find((entry) => entry.message_id === intent.assign_message.message_id);
759
+ if (!sourceAttempt ||
760
+ !CLOSED_STATES.includes(sourceAttempt.state) ||
761
+ sourceAttempt.input_revision !== authorization.source.input_revision ||
762
+ sourceAttempt.owner_epoch !== coordinator.owner_epoch ||
763
+ !sameIdentity(sourceAttempt.assignee, sourceNode.assignee) ||
764
+ !nextAttempt ||
765
+ nextAttempt.assignment_message_id !==
766
+ intent.assign_message.message_id ||
767
+ nextAttempt.owner_epoch !== coordinator.owner_epoch ||
768
+ nextAttempt.input_revision !== authorization.next.input_revision ||
769
+ !sameIdentity(nextAttempt.assignee, authorization.next.assignee) ||
770
+ !sameJson(nextAttempt.approval, {
771
+ approved_by: authorization.approval.approved_by,
772
+ reason: authorization.approval.reason,
773
+ }) ||
774
+ !assignmentRecord ||
775
+ assignmentRecord.type !== 'ASSIGN' ||
776
+ assignmentRecord.state !== 'applied')
777
+ continue;
778
+ let claimsReady = true;
779
+ for (const access of ['read', 'write']) {
780
+ const resources = authorization.next.resources.filter((resource) => resource.access === access);
781
+ if (!resources.length)
782
+ continue;
783
+ const claims = matchingClaimKeys(taskRoot, resources, authorization.next.assignee, authorization.next.assignment_id, authorization.next.attempt);
784
+ if (!claims ||
785
+ claims.expected.length !== claims.existing.length ||
786
+ !claims.expected.every((key) => claims.existing.includes(key))) {
787
+ claimsReady = false;
788
+ break;
789
+ }
790
+ }
791
+ if (!claimsReady)
792
+ continue;
793
+ const workPlanRead = readBoundedFile(planFile, 512 * 1024);
794
+ if (!workPlanRead.ok)
795
+ continue;
796
+ let workPlanPayload;
797
+ try {
798
+ workPlanPayload = JSON.parse(workPlanRead.text);
799
+ }
800
+ catch {
801
+ continue;
802
+ }
803
+ const validatedPlan = validateWorkPlan(workPlanPayload);
804
+ if (!validatedPlan.ok ||
805
+ !sameJson(validatedPlan.plan, nextExpansion.plan))
806
+ continue;
807
+ recovered = { role_plan: nextRolePlan, execution: nextExecutionPins };
808
+ currentRolePlan = nextRolePlan;
809
+ currentExecution = nextExecutionPins;
810
+ currentExpansion = nextExpansion;
811
+ advanced = true;
812
+ break;
813
+ }
814
+ if (!advanced)
815
+ break;
816
+ }
817
+ return recovered ?? unchanged;
818
+ }
819
+ function nextRolePlanForRecovery(rolePlan, authorization) {
820
+ return nextRolePlan(rolePlan, authorization);
821
+ }