@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,477 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { join } from 'node:path';
3
+ import { z } from 'zod';
4
+ import { checkpointSchema, isValidTaskId, taskIdSchema, } from '../schemas/continuity.js';
5
+ import { diagnosticCodes } from '../schemas/errors.js';
6
+ import { failed, succeeded } from '../schemas/outcome.js';
7
+ import { readFileBounded, resolveRealpath, sha256Digest } from '../infra/fs.js';
8
+ import { continuityTaskPaths, ensurePrivateTaskBoundary, readArtifactPath, readContinuityBytes, writeContinuityBytes, } from './paths.js';
9
+ import { loadWorkspaceConfig } from '../workspace/config.js';
10
+ /**
11
+ * Linked private handoff bundles.
12
+ *
13
+ * A bundle is a separately versioned manifest that *links* one existing durable
14
+ * checkpoint to a collaboration task/attempt and the source native session, and
15
+ * carries the bounded additional handoff snapshot (claims, pending work, next
16
+ * action, recovery). It never duplicates checkpoint content, transfers
17
+ * authority, launches a replacement, archives a session or grants execution.
18
+ * Reading or verifying a bundle is purely advisory: it never returns execution
19
+ * permission or native control. The later rollover orchestrator (E3-B) must
20
+ * independently revalidate current authority, quiescence, ACK and transfer.
21
+ *
22
+ * Bundles are published immutably and exclusively under the existing
23
+ * workspace-private continuity task namespace
24
+ * (`.pappra/local/tasks/<continuityTaskId>/handoff-bundles/<bundleId>.json`),
25
+ * reusing the guarded continuity filesystem helpers. There is no latest
26
+ * pointer, alternate task record or mutable authority store.
27
+ */
28
+ export const HANDOFF_BUNDLE_SCHEMA = 'pappra/handoff-bundle/1';
29
+ /** Subdirectory holding immutable handoff bundles inside a private task dir. */
30
+ export const HANDOFF_BUNDLES_DIRNAME = 'handoff-bundles';
31
+ /**
32
+ * Upper bound for a serialized bundle. The manifest only links a checkpoint and
33
+ * carries a bounded snapshot, so it stays well under the shared continuity
34
+ * payload cap; an oversized file is rejected without inspecting its content.
35
+ */
36
+ export const HANDOFF_BUNDLE_MAX_BYTES = 256 * 1024;
37
+ /**
38
+ * Collaboration identifiers follow the message-envelope contract (up to 128),
39
+ * which is deliberately wider than the continuity task limit (64). The two are
40
+ * never truncated into one another; when they differ the manifest must carry an
41
+ * explicit association string instead of an invented mapping.
42
+ */
43
+ const COLLABORATION_ID_MAX_LENGTH = 128;
44
+ const COLLABORATION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
45
+ const digestSchema = z.string().regex(/^sha256:[0-9a-f]{64}$/);
46
+ const collaborationIdSchema = z
47
+ .string()
48
+ .min(1)
49
+ .max(COLLABORATION_ID_MAX_LENGTH)
50
+ .regex(COLLABORATION_ID_PATTERN);
51
+ /** The bound reference to one existing, schema-valid checkpoint. */
52
+ export const handoffCheckpointReferenceSchema = z.strictObject({
53
+ checkpointId: z.uuid(),
54
+ taskId: taskIdSchema,
55
+ workspaceId: z.uuid(),
56
+ /** Private relative reference inside the task dir, e.g. `checkpoints/<id>.json`. */
57
+ ref: z.string().min(1).max(2048),
58
+ digest: digestSchema,
59
+ authorSession: z.string().max(256).nullable(),
60
+ contextCompleteness: z.enum(['complete', 'partial']),
61
+ });
62
+ /** The collaboration task/attempt/input/owner-epoch this handoff belongs to. */
63
+ export const handoffCollaborationBindingSchema = z.strictObject({
64
+ taskId: collaborationIdSchema,
65
+ attempt: z.number().int().positive(),
66
+ ownerEpoch: z.number().int().positive(),
67
+ subtaskId: z.string().min(1).max(128).nullable(),
68
+ inputRevision: z.string().max(256).nullable(),
69
+ /**
70
+ * Explicit, producer-authored association used only when the collaboration id
71
+ * differs from the continuity task id; never a derived mapping.
72
+ */
73
+ association: z.string().min(1).max(2048).nullable(),
74
+ });
75
+ /** The source native session. Model/provider are advisory observations only. */
76
+ export const handoffSourceNativeSessionSchema = z.strictObject({
77
+ harness: z.enum(['pi', 'codex', 'claude', 'unknown']),
78
+ sessionId: z.string().min(1).max(256),
79
+ agentId: z.string().min(1).max(128).nullable(),
80
+ paneId: z.string().max(128).nullable(),
81
+ surface: z.string().max(64).nullable(),
82
+ configurationId: z.string().max(128).nullable(),
83
+ modelObservation: z.strictObject({
84
+ model: z.string().max(256).nullable(),
85
+ provider: z.string().max(256).nullable(),
86
+ reasoning: z.string().max(64).nullable(),
87
+ }),
88
+ });
89
+ const pendingCollectionSchema = z.strictObject({
90
+ items: z.array(z.string().min(1).max(2048)).max(64),
91
+ completeness: z.enum(['complete', 'partial', 'unknown']),
92
+ });
93
+ const scopedClaimSchema = z.strictObject({
94
+ claimId: z.string().min(1).max(128),
95
+ resource: z.string().min(1).max(2048),
96
+ resourceEpoch: z.number().int().nonnegative(),
97
+ holder: z.string().min(1).max(128).nullable(),
98
+ });
99
+ /** The bounded additional snapshot carried alongside the linked checkpoint. */
100
+ export const handoffSnapshotSchema = z.strictObject({
101
+ claims: z.array(scopedClaimSchema).max(64),
102
+ claimsCompleteness: z.enum(['complete', 'partial', 'unknown']),
103
+ pendingTools: pendingCollectionSchema,
104
+ pendingMessages: pendingCollectionSchema,
105
+ pendingApprovals: pendingCollectionSchema,
106
+ nextAction: z.string().min(1).max(4096),
107
+ recovery: z.array(z.string().min(1).max(4096)).max(32),
108
+ /** A bundle never carries execution authority; this is an observation. */
109
+ executionAuthority: z.literal('none'),
110
+ /** Snapshots are advisory; partial/unknown context is preserved, not healed. */
111
+ advisory: z.literal(true),
112
+ });
113
+ /** The complete linked handoff bundle manifest. */
114
+ export const handoffBundleSchema = z
115
+ .strictObject({
116
+ schema: z.literal(HANDOFF_BUNDLE_SCHEMA),
117
+ bundleId: z.uuid(),
118
+ createdAt: z.iso.datetime({ offset: true }),
119
+ continuityTaskId: taskIdSchema,
120
+ checkpoint: handoffCheckpointReferenceSchema,
121
+ collaboration: handoffCollaborationBindingSchema,
122
+ sourceNativeSession: handoffSourceNativeSessionSchema,
123
+ handoff: handoffSnapshotSchema,
124
+ limitations: z.array(z.string().min(1).max(4096)).max(32),
125
+ notes: z.string().min(1).max(4096).nullable(),
126
+ })
127
+ .superRefine((bundle, ctx) => {
128
+ // The linked checkpoint must live in this private continuity namespace.
129
+ if (bundle.checkpoint.taskId !== bundle.continuityTaskId) {
130
+ ctx.addIssue({
131
+ code: 'custom',
132
+ path: ['checkpoint', 'taskId'],
133
+ message: 'checkpoint.taskId must equal continuityTaskId; the bundle does not map across task namespaces',
134
+ });
135
+ }
136
+ // Differing collaboration/continuity ids require an explicit association.
137
+ if (bundle.collaboration.taskId !== bundle.continuityTaskId &&
138
+ bundle.collaboration.association === null) {
139
+ ctx.addIssue({
140
+ code: 'custom',
141
+ path: ['collaboration', 'association'],
142
+ message: 'collaboration.taskId differs from continuityTaskId; an explicit association is required (no mapping is invented)',
143
+ });
144
+ }
145
+ });
146
+ const HANDOFF_ARTIFACT_MAX_BYTES = 8 * 1024 * 1024;
147
+ function invalid(message, nextAction) {
148
+ return failed(diagnosticCodes.document_record_invalid, message, {
149
+ nextAction,
150
+ });
151
+ }
152
+ /** Deterministic key-sorted serialization for stable bytes and digests. */
153
+ function sortDeep(value) {
154
+ if (Array.isArray(value))
155
+ return value.map((item) => sortDeep(item));
156
+ if (value !== null && typeof value === 'object') {
157
+ const source = value;
158
+ const out = {};
159
+ for (const key of Object.keys(source).sort()) {
160
+ out[key] = sortDeep(source[key]);
161
+ }
162
+ return out;
163
+ }
164
+ return value;
165
+ }
166
+ export function serializeHandoffBundle(bundle) {
167
+ return Buffer.from(`${JSON.stringify(sortDeep(bundle), null, 2)}\n`, 'utf8');
168
+ }
169
+ /**
170
+ * A checkpoint reference is a private relative path only. Absolute paths,
171
+ * parent redirection and NUL bytes are refused before any filesystem access;
172
+ * the guarded continuity boundary re-validates the resolved path as well.
173
+ */
174
+ function resolveCheckpointPath(root, continuityTaskId, ref) {
175
+ if (ref.length === 0 ||
176
+ ref.startsWith('/') ||
177
+ ref.includes('\0') ||
178
+ ref.split('/').includes('..')) {
179
+ return null;
180
+ }
181
+ let taskDir;
182
+ try {
183
+ taskDir = continuityTaskPaths(root, continuityTaskId).taskDir;
184
+ }
185
+ catch {
186
+ return null;
187
+ }
188
+ return join(taskDir, ref);
189
+ }
190
+ export function handoffBundleRelativeRef(bundleId) {
191
+ return `${HANDOFF_BUNDLES_DIRNAME}/${bundleId}.json`;
192
+ }
193
+ function handoffBundlePath(root, continuityTaskId, bundleId) {
194
+ return resolveCheckpointPath(root, continuityTaskId, handoffBundleRelativeRef(bundleId));
195
+ }
196
+ /** Build and validate a bundle manifest without touching the filesystem. */
197
+ export function buildHandoffBundle(input) {
198
+ const bundleId = input.bundleId ?? randomUUID();
199
+ const createdAt = (input.now?.() ?? new Date()).toISOString();
200
+ const candidate = {
201
+ schema: HANDOFF_BUNDLE_SCHEMA,
202
+ bundleId,
203
+ createdAt,
204
+ continuityTaskId: input.continuityTaskId,
205
+ checkpoint: input.checkpoint,
206
+ collaboration: input.collaboration,
207
+ sourceNativeSession: input.sourceNativeSession,
208
+ handoff: input.handoff,
209
+ limitations: [...(input.limitations ?? [])],
210
+ notes: input.notes ?? null,
211
+ };
212
+ const parsed = handoffBundleSchema.safeParse(candidate);
213
+ if (!parsed.success) {
214
+ return invalid('Handoff bundle failed shape or association validation.', 'Fix the reported field; identifiers are never truncated and no mapping is invented.');
215
+ }
216
+ return succeeded(parsed.data);
217
+ }
218
+ /** Parse untrusted bytes into a validated bundle without leaking content. */
219
+ export function parseHandoffBundle(raw) {
220
+ if (raw === null || typeof raw !== 'object') {
221
+ return invalid('Handoff bundle is not an object.', 'Provide a well-formed handoff bundle manifest.');
222
+ }
223
+ const schema = raw.schema;
224
+ if (schema !== HANDOFF_BUNDLE_SCHEMA) {
225
+ return failed(diagnosticCodes.configUnsupportedSchema, 'Handoff bundle schema is missing or unsupported.', {
226
+ nextAction: `Use a ${HANDOFF_BUNDLE_SCHEMA} manifest; other versions are not interpreted.`,
227
+ });
228
+ }
229
+ const parsed = handoffBundleSchema.safeParse(raw);
230
+ if (!parsed.success) {
231
+ return invalid('Handoff bundle failed shape or association validation.', 'Repair the malformed bundle; private content is not echoed.');
232
+ }
233
+ return succeeded(parsed.data);
234
+ }
235
+ async function loadReferencedCheckpoint(root, bundle) {
236
+ const reference = bundle.checkpoint;
237
+ const path = resolveCheckpointPath(root, bundle.continuityTaskId, reference.ref);
238
+ if (path === null) {
239
+ return failed(diagnosticCodes.document_path_unsafe, 'Checkpoint reference is not a safe private relative path.', {
240
+ nextAction: 'Reference a checkpoint inside the private task directory; redirections are not followed.',
241
+ });
242
+ }
243
+ const read = await readContinuityBytes(root, bundle.continuityTaskId, path);
244
+ if (!read.ok) {
245
+ if (read.code === diagnosticCodes.fileNotFound) {
246
+ return failed(diagnosticCodes.fileNotFound, 'Referenced checkpoint is missing; the handoff bundle is not complete.', {
247
+ nextAction: 'Reference an existing checkpoint; a missing checkpoint is never reported as complete.',
248
+ });
249
+ }
250
+ return read;
251
+ }
252
+ if (sha256Digest(read.value) !== reference.digest) {
253
+ return failed(diagnosticCodes.document_file_changed, 'Referenced checkpoint bytes do not match the bound digest.', {
254
+ nextAction: 'Rebuild the bundle from the current checkpoint; changed bytes are never reported as complete.',
255
+ });
256
+ }
257
+ let raw;
258
+ try {
259
+ raw = JSON.parse(read.value.toString('utf8'));
260
+ }
261
+ catch {
262
+ return failed(diagnosticCodes.document_source_invalid, 'Referenced checkpoint is not valid JSON.', { nextAction: 'Reference a valid checkpoint file.' });
263
+ }
264
+ const parsed = checkpointSchema.safeParse(raw);
265
+ if (!parsed.success) {
266
+ return failed(diagnosticCodes.document_source_invalid, 'Referenced checkpoint does not satisfy the current checkpoint schema.', { nextAction: 'Reference a checkpoint valid under the current schema.' });
267
+ }
268
+ const checkpoint = parsed.data;
269
+ // Enforce task/workspace/source-session association by identity, not path.
270
+ const associationMismatch = checkpoint.checkpointId !== reference.checkpointId ||
271
+ checkpoint.taskId !== reference.taskId ||
272
+ checkpoint.workspaceId !== reference.workspaceId ||
273
+ checkpoint.authorSession !== reference.authorSession ||
274
+ checkpoint.contextCompleteness !== reference.contextCompleteness;
275
+ if (associationMismatch) {
276
+ return failed(diagnosticCodes.document_source_invalid, 'Referenced checkpoint identity does not match the bound association.', {
277
+ nextAction: 'Bind the exact checkpoint id/task/workspace/author-session; no mapping is inferred.',
278
+ });
279
+ }
280
+ if (checkpoint.authorSession !== null &&
281
+ bundle.sourceNativeSession.sessionId !== checkpoint.authorSession) {
282
+ return failed(diagnosticCodes.document_source_invalid, 'Source native session does not match the checkpoint author session.', {
283
+ nextAction: 'Hand off from the session that authored the checkpoint, or record the correct source session.',
284
+ });
285
+ }
286
+ // Bind the checkpoint to the actual registered workspace of this root, not to
287
+ // the producer's own two matching copies of a workspace id. Inspect the
288
+ // current workspace configuration safely; a missing or invalid workspace
289
+ // stays unavailable and is never silently initialized.
290
+ const workspace = await loadWorkspaceConfig(root);
291
+ if (!workspace.ok) {
292
+ return failed(workspace.code, 'Current workspace configuration is unavailable; the checkpoint workspace identity cannot be confirmed.', {
293
+ nextAction: 'Run from a registered workspace; a missing or invalid workspace is never assumed or initialized.',
294
+ });
295
+ }
296
+ if (workspace.value.workspaceId !== checkpoint.workspaceId) {
297
+ return failed(diagnosticCodes.document_source_invalid, 'Checkpoint workspace id does not match the current registered workspace.', {
298
+ nextAction: 'Use a checkpoint produced in this workspace; cross-workspace references are refused.',
299
+ });
300
+ }
301
+ return succeeded(checkpoint);
302
+ }
303
+ /**
304
+ * Validate the referenced checkpoint, then publish the bundle immutably and
305
+ * exclusively. A missing, changed or mismatched checkpoint fails before any
306
+ * write; an existing bundle id is never replaced.
307
+ */
308
+ export async function saveHandoffBundle(input) {
309
+ if (!isValidTaskId(input.continuityTaskId)) {
310
+ return invalid('Invalid continuity task id for handoff bundle.', 'Use a continuity task id of up to 64 safe characters.');
311
+ }
312
+ const built = buildHandoffBundle(input);
313
+ if (!built.ok)
314
+ return built;
315
+ const bundle = built.value;
316
+ const checkpoint = await loadReferencedCheckpoint(input.root, bundle);
317
+ if (!checkpoint.ok)
318
+ return checkpoint;
319
+ const bytes = serializeHandoffBundle(bundle);
320
+ if (bytes.length > HANDOFF_BUNDLE_MAX_BYTES) {
321
+ return failed(diagnosticCodes.fileTooLarge, 'Serialized handoff bundle exceeds the maximum size.', {
322
+ nextAction: 'Reduce the snapshot collections; oversized bundles are refused without storing content.',
323
+ });
324
+ }
325
+ const path = handoffBundlePath(input.root, bundle.continuityTaskId, bundle.bundleId);
326
+ if (path === null) {
327
+ return failed(diagnosticCodes.document_path_unsafe, 'Computed handoff bundle path is unsafe.', { nextAction: 'Use a valid private continuity task namespace.' });
328
+ }
329
+ // Re-validate the private boundary immediately before the exclusive publish so
330
+ // a parent redirected after the initial check (TOCTOU) cannot place the file
331
+ // outside the task namespace. The caller's own seam still runs first.
332
+ const writeOptions = {
333
+ beforeExclusivePublish: async () => {
334
+ await input.writeOptions?.beforeExclusivePublish?.();
335
+ const recheck = await ensurePrivateTaskBoundary(input.root, bundle.continuityTaskId, path);
336
+ if (!recheck.ok) {
337
+ throw new Error(recheck.message);
338
+ }
339
+ },
340
+ };
341
+ const write = await writeContinuityBytes(input.root, bundle.continuityTaskId, path, bytes, true, writeOptions);
342
+ if (!write.ok)
343
+ return write;
344
+ return succeeded({
345
+ bundleId: bundle.bundleId,
346
+ path,
347
+ relativeRef: handoffBundleRelativeRef(bundle.bundleId),
348
+ digest: write.value.digest,
349
+ bundle,
350
+ });
351
+ }
352
+ /** Read and validate one stored bundle; private content is never leaked. */
353
+ export async function readHandoffBundle(options) {
354
+ if (!isValidTaskId(options.continuityTaskId)) {
355
+ return invalid('Invalid continuity task id for handoff bundle.', 'Use a continuity task id of up to 64 safe characters.');
356
+ }
357
+ const path = handoffBundlePath(options.root, options.continuityTaskId, options.bundleId);
358
+ if (path === null) {
359
+ return failed(diagnosticCodes.document_path_unsafe, 'Handoff bundle id does not resolve to a safe private path.', {
360
+ nextAction: 'Use a valid bundle id inside the private task namespace.',
361
+ });
362
+ }
363
+ const read = await readContinuityBytes(options.root, options.continuityTaskId, path);
364
+ if (!read.ok)
365
+ return read;
366
+ if (read.value.length > HANDOFF_BUNDLE_MAX_BYTES) {
367
+ return failed(diagnosticCodes.fileTooLarge, 'Stored handoff bundle exceeds the maximum size.', {
368
+ nextAction: 'Inspect the oversized bundle explicitly; it is not parsed.',
369
+ });
370
+ }
371
+ let raw;
372
+ try {
373
+ raw = JSON.parse(read.value.toString('utf8'));
374
+ }
375
+ catch {
376
+ return invalid('Stored handoff bundle is not valid JSON.', 'Inspect the malformed bundle; private content is not echoed.');
377
+ }
378
+ const parsed = parseHandoffBundle(raw);
379
+ if (!parsed.ok)
380
+ return parsed;
381
+ if (parsed.value.bundleId !== options.bundleId) {
382
+ return invalid('Stored handoff bundle id does not match its filename.', 'Inspect the bundle; its id and location must agree.');
383
+ }
384
+ if (parsed.value.continuityTaskId !== options.continuityTaskId) {
385
+ return failed(diagnosticCodes.document_source_invalid, 'Stored handoff bundle targets a different continuity task than requested.', {
386
+ nextAction: 'Read a bundle under its own continuity task; a copied manifest is not followed across namespaces.',
387
+ });
388
+ }
389
+ return succeeded({
390
+ bundle: parsed.value,
391
+ digest: sha256Digest(read.value),
392
+ path,
393
+ });
394
+ }
395
+ function completenessOf(bundle) {
396
+ return {
397
+ claims: bundle.handoff.claimsCompleteness,
398
+ pendingTools: bundle.handoff.pendingTools.completeness,
399
+ pendingMessages: bundle.handoff.pendingMessages.completeness,
400
+ pendingApprovals: bundle.handoff.pendingApprovals.completeness,
401
+ };
402
+ }
403
+ async function checkCheckpointArtifacts(root, checkpoint) {
404
+ const checks = [];
405
+ // Resolve the canonical workspace root so an artifact missing under a
406
+ // non-canonical root (for example a /var -> /private/var symlink) is still
407
+ // classified as missing rather than unsafe.
408
+ const canonicalRoot = (await resolveRealpath(root)) ?? root;
409
+ for (const artifact of checkpoint.artifacts) {
410
+ const resolved = await readArtifactPath(canonicalRoot, artifact.path);
411
+ if (!resolved.ok) {
412
+ checks.push({ path: artifact.path, status: 'unsafe' });
413
+ continue;
414
+ }
415
+ const read = await readFileBounded(resolved.value, HANDOFF_ARTIFACT_MAX_BYTES);
416
+ if (!read.ok) {
417
+ checks.push({
418
+ path: artifact.path,
419
+ status: read.code === diagnosticCodes.fileNotFound ? 'missing' : 'unreadable',
420
+ });
421
+ continue;
422
+ }
423
+ checks.push({
424
+ path: artifact.path,
425
+ status: read.value.digest === artifact.digest ? 'unchanged' : 'changed',
426
+ });
427
+ }
428
+ return checks;
429
+ }
430
+ function contextIntegrityOf(checkpoint, artifactChecks) {
431
+ if (artifactChecks.some((check) => check.status === 'unsafe' || check.status === 'unreadable')) {
432
+ return 'unknown';
433
+ }
434
+ if (checkpoint.contextCompleteness !== 'complete' ||
435
+ artifactChecks.some((check) => check.status !== 'unchanged')) {
436
+ return 'incomplete';
437
+ }
438
+ return 'complete';
439
+ }
440
+ /**
441
+ * Re-validate a bundle against its referenced checkpoint. Any missing, changed,
442
+ * mismatched or invalid checkpoint fails verification, so a defective bundle is
443
+ * never reported as complete. Success never implies execution authority.
444
+ */
445
+ export async function verifyHandoffBundle(options) {
446
+ let bundle;
447
+ if ('bundle' in options) {
448
+ const reparsed = parseHandoffBundle(options.bundle);
449
+ if (!reparsed.ok)
450
+ return reparsed;
451
+ bundle = reparsed.value;
452
+ }
453
+ else {
454
+ const read = await readHandoffBundle(options);
455
+ if (!read.ok)
456
+ return read;
457
+ bundle = read.value.bundle;
458
+ }
459
+ const checkpoint = await loadReferencedCheckpoint(options.root, bundle);
460
+ if (!checkpoint.ok)
461
+ return checkpoint;
462
+ const artifactChecks = await checkCheckpointArtifacts(options.root, checkpoint.value);
463
+ return succeeded({
464
+ bundleId: bundle.bundleId,
465
+ continuityTaskId: bundle.continuityTaskId,
466
+ checkpointPresent: true,
467
+ checkpointDigestMatches: true,
468
+ checkpointSchemaValid: true,
469
+ associationValid: true,
470
+ workspaceIdentityMatches: true,
471
+ checkpointContextCompleteness: checkpoint.value.contextCompleteness,
472
+ artifactChecks,
473
+ contextIntegrity: contextIntegrityOf(checkpoint.value, artifactChecks),
474
+ executionAllowed: false,
475
+ completeness: completenessOf(bundle),
476
+ });
477
+ }
@@ -1,3 +1,5 @@
1
1
  export { CheckpointService } from './service.js';
2
2
  export type { ContinuityServiceOptions, NativeRefInput, ResumeCheckpointInput, ResumeCheckpointResult, SaveCheckpointInput, SaveCheckpointResult, ValidationInput, } from './service.js';
3
3
  export type { Checkpoint, CheckpointPointer, ResumePacket, TaskContext, } from '../schemas/continuity.js';
4
+ export { HANDOFF_BUNDLE_SCHEMA, HANDOFF_BUNDLE_MAX_BYTES, HANDOFF_BUNDLES_DIRNAME, handoffBundleSchema, handoffCheckpointReferenceSchema, handoffCollaborationBindingSchema, handoffSourceNativeSessionSchema, handoffSnapshotSchema, handoffBundleRelativeRef, serializeHandoffBundle, buildHandoffBundle, parseHandoffBundle, saveHandoffBundle, readHandoffBundle, verifyHandoffBundle, } from './handoff-bundle.js';
5
+ export type { HandoffBundle, HandoffCheckpointReference, HandoffCollaborationBinding, HandoffSourceNativeSession, HandoffSnapshot, HandoffCompletenessSummary, HandoffArtifactCheck, SaveHandoffBundleInput, SaveHandoffBundleResult, ReadHandoffBundleResult, VerifyHandoffBundleResult, } from './handoff-bundle.js';
@@ -1 +1,2 @@
1
1
  export { CheckpointService } from './service.js';
2
+ export { HANDOFF_BUNDLE_SCHEMA, HANDOFF_BUNDLE_MAX_BYTES, HANDOFF_BUNDLES_DIRNAME, handoffBundleSchema, handoffCheckpointReferenceSchema, handoffCollaborationBindingSchema, handoffSourceNativeSessionSchema, handoffSnapshotSchema, handoffBundleRelativeRef, serializeHandoffBundle, buildHandoffBundle, parseHandoffBundle, saveHandoffBundle, readHandoffBundle, verifyHandoffBundle, } from './handoff-bundle.js';
@@ -3,10 +3,12 @@ import { link, mkdir, open, rename, rm, unlink } from 'node:fs/promises';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { errorMessage, failed, succeeded } from '../schemas/outcome.js';
5
5
  import { diagnosticCodes } from '../schemas/errors.js';
6
+ import { isValidTaskId } from '../schemas/continuity.js';
6
7
  import { isPathInside, resolvePortableRelativePath, samePath, } from '../schemas/paths.js';
7
8
  import { observePath, readFileBounded, readTextFileBounded, resolveRealpath, sha256Digest, } from '../infra/fs.js';
8
9
  import { resolveWorkspaceRoot } from '../workspace/resolve.js';
9
- const TASK_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
10
+ import { ensurePrivateState } from '../infra/private-state.js';
11
+ /* The shared task identifier rule lives in schemas/continuity.ts. */
10
12
  /**
11
13
  * Shared bound for private recovery payloads. Capture, durable writes and
12
14
  * resume reads all use this value; a payload over the bound is captured only
@@ -14,7 +16,7 @@ const TASK_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
14
16
  */
15
17
  export const CONTINUITY_PAYLOAD_MAX_BYTES = 16 * 1024 * 1024;
16
18
  export function continuityTaskPaths(root, taskId) {
17
- if (!TASK_ID.test(taskId) || taskId.length > 64) {
19
+ if (!isValidTaskId(taskId)) {
18
20
  throw new RangeError(`Invalid task id: ${taskId}`);
19
21
  }
20
22
  const taskDir = join(root, '.pappra', 'local', 'tasks', taskId);
@@ -134,6 +136,9 @@ export async function writeContinuityBytes(root, taskId, path, bytes, exclusive,
134
136
  const boundary = await ensurePrivateTaskBoundary(root, taskId, path);
135
137
  if (!boundary.ok)
136
138
  return boundary;
139
+ const privateState = await ensurePrivateState(root);
140
+ if (!privateState.ok)
141
+ return privateState;
137
142
  const temporary = await writeTemporary(path, bytes);
138
143
  if (!temporary.ok) {
139
144
  return failed(diagnosticCodes.fileUnreadable, temporary.message, {
@@ -180,6 +185,9 @@ export async function replaceContinuityBytesIfDigest(root, taskId, path, bytes,
180
185
  const boundary = await ensurePrivateTaskBoundary(root, taskId, path);
181
186
  if (!boundary.ok)
182
187
  return boundary;
188
+ const privateState = await ensurePrivateState(root);
189
+ if (!privateState.ok)
190
+ return privateState;
183
191
  const temporary = await writeTemporary(path, bytes);
184
192
  if (!temporary.ok) {
185
193
  return failed(diagnosticCodes.fileUnreadable, temporary.message, {