@phyzess/pappra 0.1.0-beta.14 → 0.1.0-beta.19

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 (365) hide show
  1. package/README.md +133 -14
  2. package/assets/integrations/skills/pappra/SKILL.md +56 -7
  3. package/assets/integrations/skills/pappra/references/collaboration.md +646 -0
  4. package/assets/integrations/skills/pappra/references/environment.md +367 -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-request.d.ts +50 -0
  62. package/dist/app/collab-session-transition-request.js +339 -0
  63. package/dist/app/collab-session-transition-validation.d.ts +19 -0
  64. package/dist/app/collab-session-transition-validation.js +44 -0
  65. package/dist/app/collab-session-transition.d.ts +181 -0
  66. package/dist/app/collab-session-transition.js +2970 -0
  67. package/dist/app/collab-state.d.ts +7 -0
  68. package/dist/app/collab-state.js +162 -0
  69. package/dist/app/collab-task-store.d.ts +22 -0
  70. package/dist/app/collab-task-store.js +204 -0
  71. package/dist/app/collab-work-execution.d.ts +127 -0
  72. package/dist/app/collab-work-execution.js +1027 -0
  73. package/dist/app/collab-work-plan.d.ts +87 -0
  74. package/dist/app/collab-work-plan.js +437 -0
  75. package/dist/app/collaboration.d.ts +8 -2
  76. package/dist/app/collaboration.js +101 -12
  77. package/dist/app/doctor.js +104 -3
  78. package/dist/app/host-mcp.js +34 -8
  79. package/dist/app/host-probe.js +4 -4
  80. package/dist/app/init.d.ts +58 -4
  81. package/dist/app/init.js +768 -155
  82. package/dist/app/managed-pi-bootstrap-admission.d.ts +14 -0
  83. package/dist/app/managed-pi-bootstrap-admission.js +104 -0
  84. package/dist/app/managed-pi-bootstrap-refresh.d.ts +10 -0
  85. package/dist/app/managed-pi-bootstrap-refresh.js +78 -0
  86. package/dist/app/managed-pi-delivery.d.ts +8 -0
  87. package/dist/app/managed-pi-delivery.js +110 -0
  88. package/dist/app/managed-pi-handoff-receiver.d.ts +15 -0
  89. package/dist/app/managed-pi-handoff-receiver.js +263 -0
  90. package/dist/app/managed-pi-handoff-reference.d.ts +3 -0
  91. package/dist/app/managed-pi-handoff-reference.js +60 -0
  92. package/dist/app/managed-pi-herdr.d.ts +21 -0
  93. package/dist/app/managed-pi-herdr.js +185 -0
  94. package/dist/app/managed-pi-initial-admission.d.ts +28 -0
  95. package/dist/app/managed-pi-initial-admission.js +416 -0
  96. package/dist/app/managed-pi-initial-launch.d.ts +5 -0
  97. package/dist/app/managed-pi-initial-launch.js +61 -0
  98. package/dist/app/managed-pi-native-observation.d.ts +36 -0
  99. package/dist/app/managed-pi-native-observation.js +191 -0
  100. package/dist/app/managed-pi-readiness.d.ts +29 -0
  101. package/dist/app/managed-pi-readiness.js +288 -0
  102. package/dist/app/managed-pi-repair-stop.d.ts +21 -0
  103. package/dist/app/managed-pi-repair-stop.js +371 -0
  104. package/dist/app/managed-pi-resume-admission.d.ts +32 -0
  105. package/dist/app/managed-pi-resume-admission.js +618 -0
  106. package/dist/app/managed-pi-retirement.d.ts +28 -0
  107. package/dist/app/managed-pi-retirement.js +112 -0
  108. package/dist/app/managed-pi-sdk-control.d.ts +7 -0
  109. package/dist/app/managed-pi-sdk-control.js +169 -0
  110. package/dist/app/managed-pi-sdk-runtime.d.ts +3 -0
  111. package/dist/app/managed-pi-sdk-runtime.js +483 -0
  112. package/dist/app/managed-pi-source-admission.d.ts +14 -0
  113. package/dist/app/managed-pi-source-admission.js +323 -0
  114. package/dist/app/managed-pi-transition-host.d.ts +36 -0
  115. package/dist/app/managed-pi-transition-host.js +292 -0
  116. package/dist/app/managed-pi.d.ts +54 -0
  117. package/dist/app/managed-pi.js +248 -0
  118. package/dist/app/shell-readiness.d.ts +66 -0
  119. package/dist/app/shell-readiness.js +368 -0
  120. package/dist/app/task-pin.d.ts +2 -23
  121. package/dist/app/task-pin.js +2 -113
  122. package/dist/cli/collaboration-router.d.ts +17 -0
  123. package/dist/cli/collaboration-router.js +604 -0
  124. package/dist/cli/commands/collaboration-binding.d.ts +13 -0
  125. package/dist/cli/commands/collaboration-binding.js +118 -0
  126. package/dist/cli/commands/collaboration-bootstrap.d.ts +10 -0
  127. package/dist/cli/commands/collaboration-bootstrap.js +84 -0
  128. package/dist/cli/commands/collaboration-capabilities.d.ts +13 -0
  129. package/dist/cli/commands/collaboration-capabilities.js +127 -0
  130. package/dist/cli/commands/collaboration-handoff.d.ts +13 -0
  131. package/dist/cli/commands/collaboration-handoff.js +102 -0
  132. package/dist/cli/commands/collaboration-message.d.ts +17 -0
  133. package/dist/cli/commands/collaboration-message.js +177 -0
  134. package/dist/cli/commands/collaboration-plan.d.ts +8 -0
  135. package/dist/cli/commands/collaboration-plan.js +108 -0
  136. package/dist/cli/commands/collaboration-preflight.d.ts +10 -0
  137. package/dist/cli/commands/collaboration-preflight.js +167 -0
  138. package/dist/cli/commands/collaboration-recovery-run.d.ts +46 -0
  139. package/dist/cli/commands/collaboration-recovery-run.js +224 -0
  140. package/dist/cli/commands/collaboration-recovery.d.ts +11 -0
  141. package/dist/cli/commands/collaboration-recovery.js +104 -0
  142. package/dist/cli/commands/collaboration-resources.d.ts +11 -0
  143. package/dist/cli/commands/collaboration-resources.js +96 -0
  144. package/dist/cli/commands/collaboration-role-plan.d.ts +9 -0
  145. package/dist/cli/commands/collaboration-role-plan.js +69 -0
  146. package/dist/cli/commands/collaboration-role-run.d.ts +11 -0
  147. package/dist/cli/commands/collaboration-role-run.js +128 -0
  148. package/dist/cli/commands/collaboration-rollover.d.ts +10 -0
  149. package/dist/cli/commands/collaboration-rollover.js +103 -0
  150. package/dist/cli/commands/collaboration-state.d.ts +10 -0
  151. package/dist/cli/commands/collaboration-state.js +96 -0
  152. package/dist/cli/commands/collaboration-transition.d.ts +10 -0
  153. package/dist/cli/commands/collaboration-transition.js +75 -0
  154. package/dist/cli/commands/collaboration-work-run.d.ts +34 -0
  155. package/dist/cli/commands/collaboration-work-run.js +178 -0
  156. package/dist/cli/commands/collaboration.d.ts +4 -0
  157. package/dist/cli/commands/collaboration.js +39 -4
  158. package/dist/cli/commands/continuity.js +2 -6
  159. package/dist/cli/commands/docs.d.ts +2 -0
  160. package/dist/cli/commands/docs.js +89 -15
  161. package/dist/cli/commands/host.d.ts +7 -1
  162. package/dist/cli/commands/host.js +122 -7
  163. package/dist/cli/commands/init.d.ts +20 -3
  164. package/dist/cli/commands/init.js +897 -232
  165. package/dist/cli/commands/mcp.d.ts +1 -0
  166. package/dist/cli/commands/mcp.js +11 -5
  167. package/dist/cli/commands/repair.d.ts +12 -0
  168. package/dist/cli/commands/repair.js +41 -0
  169. package/dist/cli/commands/runtime.d.ts +1 -1
  170. package/dist/cli/commands/runtime.js +139 -38
  171. package/dist/cli/commands/session.d.ts +7 -0
  172. package/dist/cli/commands/session.js +82 -0
  173. package/dist/cli/commands/task.js +19 -5
  174. package/dist/cli/options.d.ts +806 -0
  175. package/dist/cli/options.js +293 -0
  176. package/dist/cli/run.js +280 -310
  177. package/dist/code/identity.d.ts +7 -0
  178. package/dist/code/identity.js +7 -3
  179. package/dist/code/registration.js +7 -0
  180. package/dist/code/service.d.ts +9 -0
  181. package/dist/code/service.js +16 -3
  182. package/dist/code/text.js +19 -7
  183. package/dist/collab/admission.d.ts +91 -66
  184. package/dist/collab/admission.js +21 -6
  185. package/dist/collab/canonical.d.ts +1 -1
  186. package/dist/collab/canonical.js +44 -3
  187. package/dist/collab/envelope.d.ts +2 -2
  188. package/dist/collab/envelope.js +12 -12
  189. package/dist/collab/first-work.d.ts +3 -0
  190. package/dist/collab/first-work.js +15 -0
  191. package/dist/collab/handoff.d.ts +16 -0
  192. package/dist/collab/handoff.js +39 -5
  193. package/dist/collab/index.d.ts +97 -1
  194. package/dist/collab/index.js +9 -1
  195. package/dist/collab/messages.js +126 -3
  196. package/dist/collab/persisted-messages.d.ts +123 -0
  197. package/dist/collab/persisted-messages.js +496 -0
  198. package/dist/collab/preflight.d.ts +1 -1
  199. package/dist/collab/resources.d.ts +13 -3
  200. package/dist/collab/resources.js +37 -6
  201. package/dist/collab/rollover.d.ts +311 -0
  202. package/dist/collab/rollover.js +1024 -0
  203. package/dist/collab/store.d.ts +1 -0
  204. package/dist/collab/store.js +1 -4
  205. package/dist/collab/types.d.ts +2 -354
  206. package/dist/collab/validation.d.ts +2 -10
  207. package/dist/collab/validation.js +2 -303
  208. package/dist/collab/work-plan.d.ts +105 -0
  209. package/dist/collab/work-plan.js +843 -0
  210. package/dist/continuity/context.d.ts +11 -2
  211. package/dist/continuity/context.js +100 -13
  212. package/dist/continuity/handoff-bundle.d.ts +296 -0
  213. package/dist/continuity/handoff-bundle.js +477 -0
  214. package/dist/continuity/index.d.ts +2 -0
  215. package/dist/continuity/index.js +1 -0
  216. package/dist/continuity/paths.js +10 -2
  217. package/dist/continuity/service.js +91 -15
  218. package/dist/continuity/task-pin.d.ts +24 -0
  219. package/dist/continuity/task-pin.js +590 -0
  220. package/dist/doctor.d.ts +1 -0
  221. package/dist/doctor.js +6 -0
  222. package/dist/experimental/index.d.ts +13 -0
  223. package/dist/experimental/index.js +25 -0
  224. package/dist/experimental/pi.d.ts +4 -0
  225. package/dist/experimental/pi.js +12 -0
  226. package/dist/hosts/claude/native.js +1 -1
  227. package/dist/hosts/codex/native.js +1 -1
  228. package/dist/hosts/codex/protocol.js +1 -1
  229. package/dist/hosts/integration/bootstrap-config.d.ts +58 -0
  230. package/dist/hosts/integration/bootstrap-config.js +254 -0
  231. package/dist/hosts/integration/bundle-export.d.ts +44 -0
  232. package/dist/hosts/integration/bundle-export.js +366 -3
  233. package/dist/hosts/integration/bundle.js +3 -0
  234. package/dist/hosts/integration/hook.js +36 -9
  235. package/dist/hosts/integration/pappra-bootstrap-extension.mjs +80 -0
  236. package/dist/hosts/integration/pappra-extension-runtime.mjs +59 -24631
  237. package/dist/hosts/integration/pappra-hook.mjs +61 -24749
  238. package/dist/hosts/integration/pappra-recovery-extension.mjs +259 -0
  239. package/dist/hosts/integration/pappra-session-transition-extension.mjs +276 -0
  240. package/dist/hosts/integration/pi-bootstrap-extension.d.ts +36 -0
  241. package/dist/hosts/integration/pi-bootstrap-extension.js +101 -0
  242. package/dist/hosts/integration/pi-bootstrap-guard.d.ts +125 -0
  243. package/dist/hosts/integration/pi-bootstrap-guard.js +438 -0
  244. package/dist/hosts/integration/pi-bootstrap-runtime.d.ts +63 -0
  245. package/dist/hosts/integration/pi-bootstrap-runtime.js +223 -0
  246. package/dist/hosts/integration/pi-native-events.d.ts +112 -0
  247. package/dist/hosts/integration/pi-native-events.js +348 -0
  248. package/dist/hosts/integration/pi-rollover-extension.d.ts +166 -0
  249. package/dist/hosts/integration/pi-rollover-extension.js +431 -0
  250. package/dist/hosts/integration/recovery-config.d.ts +48 -0
  251. package/dist/hosts/integration/recovery-config.js +117 -0
  252. package/dist/hosts/integration/service.d.ts +6 -1
  253. package/dist/hosts/integration/service.js +485 -35
  254. package/dist/hosts/integration/support.d.ts +7 -0
  255. package/dist/hosts/integration/support.js +9 -1
  256. package/dist/hosts/integration/templates.d.ts +3 -0
  257. package/dist/hosts/integration/templates.js +26 -3
  258. package/dist/hosts/pi/managed-host-identity.d.ts +2 -0
  259. package/dist/hosts/pi/managed-host-identity.js +40 -0
  260. package/dist/hosts/pi/managed-provider-diagnostics.d.ts +6 -0
  261. package/dist/hosts/pi/managed-provider-diagnostics.js +184 -0
  262. package/dist/hosts/pi/managed-sdk-session.d.ts +335 -0
  263. package/dist/hosts/pi/managed-sdk-session.js +1639 -0
  264. package/dist/hosts/pi/managed-session-extension.d.ts +20 -0
  265. package/dist/hosts/pi/managed-session-extension.js +84 -0
  266. package/dist/hosts/pi/managed-session.d.ts +177 -0
  267. package/dist/hosts/pi/managed-session.js +1557 -0
  268. package/dist/hosts/pi/protocol.js +1 -1
  269. package/dist/hosts/pi/query-result.js +1 -1
  270. package/dist/hosts/pi/resume-capability.d.ts +41 -0
  271. package/dist/hosts/pi/resume-capability.js +33 -0
  272. package/dist/hosts/pi/rpc.d.ts +182 -134
  273. package/dist/hosts/pi/rpc.js +230 -37
  274. package/dist/hosts/pilot.d.ts +90 -67
  275. package/dist/hosts/pilot.js +5 -1
  276. package/dist/hosts/version.d.ts +3 -0
  277. package/dist/hosts/version.js +18 -0
  278. package/dist/index.d.ts +0 -1
  279. package/dist/index.js +0 -1
  280. package/dist/infra/bounded-file.d.ts +19 -0
  281. package/dist/infra/bounded-file.js +66 -0
  282. package/dist/infra/fs.js +10 -2
  283. package/dist/infra/git.d.ts +29 -0
  284. package/dist/infra/git.js +45 -9
  285. package/dist/infra/json-property.d.ts +2 -0
  286. package/dist/infra/json-property.js +69 -0
  287. package/dist/infra/json.d.ts +4 -0
  288. package/dist/infra/json.js +24 -0
  289. package/dist/infra/owned-process-group.js +1 -1
  290. package/dist/infra/private-lock.d.ts +16 -0
  291. package/dist/infra/private-lock.js +148 -0
  292. package/dist/infra/private-state.d.ts +7 -0
  293. package/dist/infra/private-state.js +146 -0
  294. package/dist/infra/process-identity.d.ts +12 -0
  295. package/dist/infra/process-identity.js +142 -0
  296. package/dist/infra/validation.d.ts +5 -0
  297. package/dist/infra/validation.js +15 -0
  298. package/dist/init.d.ts +1 -0
  299. package/dist/init.js +6 -0
  300. package/dist/install/apply.d.ts +9 -1
  301. package/dist/install/apply.js +176 -12
  302. package/dist/install/dependencies.d.ts +38 -0
  303. package/dist/install/dependencies.js +282 -0
  304. package/dist/install/ownership.js +30 -1
  305. package/dist/install/references.d.ts +31 -0
  306. package/dist/install/references.js +208 -0
  307. package/dist/install/repair.d.ts +37 -0
  308. package/dist/install/repair.js +798 -0
  309. package/dist/install/reset-local.d.ts +16 -0
  310. package/dist/install/reset-local.js +304 -0
  311. package/dist/install/rollback.d.ts +3 -0
  312. package/dist/install/rollback.js +9 -3
  313. package/dist/install/runtime.d.ts +10 -6
  314. package/dist/install/runtime.js +299 -59
  315. package/dist/install/source-fingerprint.d.ts +2 -0
  316. package/dist/install/source-fingerprint.js +103 -0
  317. package/dist/knowledge/catalog.d.ts +1 -0
  318. package/dist/knowledge/catalog.js +143 -10
  319. package/dist/knowledge/journal.js +17 -7
  320. package/dist/knowledge/paths.js +2 -1
  321. package/dist/knowledge/refs.js +4 -0
  322. package/dist/knowledge/register-batch.d.ts +4 -0
  323. package/dist/knowledge/register-batch.js +225 -0
  324. package/dist/knowledge/relocate.d.ts +5 -0
  325. package/dist/knowledge/relocate.js +92 -0
  326. package/dist/knowledge/scan.js +103 -47
  327. package/dist/knowledge/service.d.ts +3 -0
  328. package/dist/knowledge/service.js +33 -1
  329. package/dist/knowledge/write.d.ts +4 -1
  330. package/dist/knowledge/write.js +1 -1
  331. package/dist/mcp/server.d.ts +4 -2
  332. package/dist/mcp/server.js +87 -136
  333. package/dist/schemas/collaboration-state-validation.d.ts +6 -0
  334. package/dist/schemas/collaboration-state-validation.js +342 -0
  335. package/dist/schemas/collaboration-state.d.ts +436 -0
  336. package/dist/schemas/collaboration-state.js +1 -0
  337. package/dist/schemas/collaboration.d.ts +335 -0
  338. package/dist/schemas/collaboration.js +272 -0
  339. package/dist/schemas/continuity.d.ts +8 -0
  340. package/dist/schemas/continuity.js +13 -3
  341. package/dist/schemas/doctor.d.ts +1 -0
  342. package/dist/schemas/doctor.js +1 -0
  343. package/dist/schemas/document.d.ts +61 -0
  344. package/dist/schemas/document.js +9 -0
  345. package/dist/schemas/errors.d.ts +1 -0
  346. package/dist/schemas/errors.js +1 -0
  347. package/dist/schemas/host.d.ts +24 -0
  348. package/dist/schemas/host.js +19 -0
  349. package/dist/schemas/install.d.ts +8 -2
  350. package/dist/schemas/install.js +11 -1
  351. package/dist/schemas/managed-pi-execution.d.ts +16 -0
  352. package/dist/schemas/managed-pi-execution.js +2 -0
  353. package/dist/schemas/outcome.d.ts +9 -5
  354. package/dist/workspace/config-file.js +1 -3
  355. package/dist/workspace/documents.js +12 -2
  356. package/dist/workspace/repositories.d.ts +7 -0
  357. package/dist/workspace/repositories.js +14 -5
  358. package/package.json +37 -35
  359. package/skills/pappra/SKILL.md +56 -7
  360. package/skills/pappra/references/collaboration.md +646 -0
  361. package/skills/pappra/references/environment.md +367 -0
  362. package/skills/pappra/references/hosts.md +20 -0
  363. package/skills/pappra/references/operations.md +36 -5
  364. package/dist/app/collab-from.d.ts +0 -35
  365. package/dist/app/collab-from.js +0 -84
@@ -0,0 +1,69 @@
1
+ /** Replace one top-level JSON property while retaining unrelated bytes and style. */
2
+ export function updateJsonProperty(existing, key, value) {
3
+ if (existing === null)
4
+ return Buffer.from(`${JSON.stringify({ [key]: value }, null, 2)}\n`);
5
+ const text = existing.toString('utf8');
6
+ const parsed = JSON.parse(text);
7
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
8
+ throw new Error('json_object_required');
9
+ const newline = text.includes('\r\n') ? '\r\n' : '\n';
10
+ let indent = '';
11
+ const skipSpace = (at) => {
12
+ while (/\s/.test(text[at] ?? '') && at < text.length)
13
+ at += 1;
14
+ return at;
15
+ };
16
+ const stringEnd = (start) => {
17
+ let at = start + 1;
18
+ while (at < text.length) {
19
+ if (text[at] === '\\')
20
+ at += 2;
21
+ else if (text[at++] === '"')
22
+ return at;
23
+ }
24
+ throw new Error('json_string_unterminated');
25
+ };
26
+ const render = (prefix) => {
27
+ const unit = indent || (text.includes('\n') ? ' ' : undefined);
28
+ return (JSON.stringify(value, null, unit)?.replaceAll('\n', `${newline}${prefix}`) ?? 'null');
29
+ };
30
+ let at = skipSpace(text.indexOf('{') + 1);
31
+ while (text[at] !== '}') {
32
+ const endKey = stringEnd(at);
33
+ const property = JSON.parse(text.slice(at, endKey));
34
+ const line = text.slice(text.lastIndexOf('\n', at) + 1, at);
35
+ if (/^[ \t]+$/.test(line) && (!indent || line.length < indent.length))
36
+ indent = line;
37
+ const startValue = skipSpace(skipSpace(endKey) + 1);
38
+ let endValue = startValue;
39
+ let depth = 0;
40
+ while (endValue < text.length) {
41
+ const character = text[endValue];
42
+ if (character === '"') {
43
+ endValue = stringEnd(endValue);
44
+ continue;
45
+ }
46
+ if (depth === 0 && (character === ',' || character === '}'))
47
+ break;
48
+ if (character === '[' || character === '{')
49
+ depth += 1;
50
+ if (character === ']' || character === '}')
51
+ depth -= 1;
52
+ endValue += 1;
53
+ }
54
+ let contentEnd = endValue;
55
+ while (/\s/.test(text[contentEnd - 1] ?? '') && contentEnd > startValue)
56
+ contentEnd -= 1;
57
+ if (property === key)
58
+ return Buffer.from(text.slice(0, startValue) +
59
+ render(/^[ \t]*$/.test(line) ? line : '') +
60
+ text.slice(contentEnd));
61
+ at = skipSpace(endValue + (text[endValue] === ',' ? 1 : 0));
62
+ }
63
+ const prefix = text.slice(0, at).trimEnd();
64
+ const separator = Object.keys(parsed).length > 0 ? ',' : '';
65
+ const insertion = text.includes('\n')
66
+ ? `${separator}${newline}${indent || ' '}${JSON.stringify(key)}: ${render(indent || ' ')}${newline}`
67
+ : `${separator}${JSON.stringify(key)}:${JSON.stringify(value)}`;
68
+ return Buffer.from(prefix + insertion + text.slice(at));
69
+ }
@@ -0,0 +1,4 @@
1
+ /** Canonicalize the value JSON actually persists, including omitted object keys and array holes. */
2
+ export declare function canonicalJson(value: unknown): string;
3
+ /** Raw SHA-256 hex for contracts that pin canonical JSON rather than insertion order. */
4
+ export declare function canonicalJsonDigest(value: unknown): string;
@@ -0,0 +1,24 @@
1
+ import { createHash } from 'node:crypto';
2
+ /** Canonicalize the value JSON actually persists, including omitted object keys and array holes. */
3
+ export function canonicalJson(value) {
4
+ const serialized = JSON.stringify(value);
5
+ if (serialized === undefined)
6
+ throw new TypeError('Canonical JSON requires a JSON-serializable value');
7
+ const persisted = JSON.parse(serialized);
8
+ return encode(persisted);
9
+ }
10
+ function encode(value) {
11
+ if (value === null || typeof value !== 'object')
12
+ return JSON.stringify(value);
13
+ if (Array.isArray(value))
14
+ return `[${value.map(encode).join(',')}]`;
15
+ const record = value;
16
+ return `{${Object.keys(record)
17
+ .sort()
18
+ .map((key) => `${JSON.stringify(key)}:${encode(record[key])}`)
19
+ .join(',')}}`;
20
+ }
21
+ /** Raw SHA-256 hex for contracts that pin canonical JSON rather than insertion order. */
22
+ export function canonicalJsonDigest(value) {
23
+ return createHash('sha256').update(canonicalJson(value)).digest('hex');
24
+ }
@@ -1,4 +1,4 @@
1
- import { errorCode } from '../collab/validation.js';
1
+ import { errorCode } from './validation.js';
2
2
  import { execFileSync, spawn } from 'node:child_process';
3
3
  // Scoped Unix process-group helper. It exists to extend a stop beyond the single owned parent
4
4
  // into the exact process group that parent created, and nothing else.
@@ -0,0 +1,16 @@
1
+ /** Publish only a complete, fsynced lock; a crash cannot expose an empty owner record. */
2
+ export declare function publishPrivateLock(file: string, bytes: Buffer, revalidate?: () => Promise<void>): Promise<void>;
3
+ export interface PrivateLockObservation {
4
+ readonly file: string;
5
+ readonly state: 'missing' | 'alive' | 'dead' | 'unknown';
6
+ readonly token: string | null;
7
+ readonly digest: string | null;
8
+ readonly identity: {
9
+ readonly dev: number;
10
+ readonly ino: number;
11
+ } | null;
12
+ }
13
+ /** An old PID-only lock is reclaimable only after ESRCH, never by its age. */
14
+ export declare function inspectPrivateLock(file: string): Promise<PrivateLockObservation>;
15
+ /** One immutable claim wins a stale owner boundary; interrupted claimants can be succeeded. */
16
+ export declare function reclaimDeadPrivateLock(file: string, revalidate: () => Promise<void>): Promise<boolean>;
@@ -0,0 +1,148 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { constants } from 'node:fs';
3
+ import { link, lstat, open, unlink } from 'node:fs/promises';
4
+ import { sha256Digest } from './fs.js';
5
+ import { captureProcessIdentity, isLocalProcessIdentity, observeProcessIdentity, } from './process-identity.js';
6
+ /** Publish only a complete, fsynced lock; a crash cannot expose an empty owner record. */
7
+ export async function publishPrivateLock(file, bytes, revalidate) {
8
+ const temporary = `${file}.pending-${process.pid}-${randomUUID()}`;
9
+ let handle;
10
+ try {
11
+ handle = await open(temporary, constants.O_WRONLY |
12
+ constants.O_CREAT |
13
+ constants.O_EXCL |
14
+ constants.O_NOFOLLOW, 0o600);
15
+ await handle.writeFile(bytes);
16
+ await handle.sync();
17
+ await revalidate?.();
18
+ await link(temporary, file);
19
+ await revalidate?.();
20
+ const published = await lstat(file);
21
+ const staged = await handle.stat();
22
+ if (!published.isFile() ||
23
+ published.dev !== staged.dev ||
24
+ published.ino !== staged.ino)
25
+ throw new Error(`Published lock identity changed: ${file}`);
26
+ }
27
+ finally {
28
+ await handle?.close();
29
+ await unlink(temporary).catch(() => undefined);
30
+ }
31
+ }
32
+ /** An old PID-only lock is reclaimable only after ESRCH, never by its age. */
33
+ export async function inspectPrivateLock(file) {
34
+ const empty = {
35
+ file,
36
+ state: 'unknown',
37
+ token: null,
38
+ digest: null,
39
+ identity: null,
40
+ };
41
+ let handle;
42
+ try {
43
+ handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
44
+ const stats = await handle.stat();
45
+ if (!stats.isFile() || stats.size > 64 * 1024)
46
+ return empty;
47
+ const bytes = Buffer.alloc(64 * 1024 + 1);
48
+ const read = await handle.read(bytes, 0, bytes.length, 0);
49
+ if (read.bytesRead > 64 * 1024)
50
+ return empty;
51
+ const content = bytes.subarray(0, read.bytesRead);
52
+ let value;
53
+ try {
54
+ value = JSON.parse(content.toString('utf8'));
55
+ }
56
+ catch {
57
+ return empty;
58
+ }
59
+ if (typeof value !== 'object' ||
60
+ value === null ||
61
+ !('pid' in value) ||
62
+ !('token' in value) ||
63
+ typeof value.pid !== 'number' ||
64
+ !Number.isSafeInteger(value.pid) ||
65
+ value.pid <= 0 ||
66
+ typeof value.token !== 'string' ||
67
+ !value.token)
68
+ return empty;
69
+ let state = 'unknown';
70
+ if ('process_identity' in value && value.process_identity !== undefined) {
71
+ state =
72
+ isLocalProcessIdentity(value.process_identity) &&
73
+ value.process_identity.pid === value.pid
74
+ ? observeProcessIdentity(value.process_identity)
75
+ : 'unknown';
76
+ }
77
+ else {
78
+ try {
79
+ process.kill(value.pid, 0);
80
+ // PID existence alone cannot bind an imported lock to that process birth.
81
+ state = 'unknown';
82
+ }
83
+ catch (error) {
84
+ if (error.code === 'ESRCH')
85
+ state = 'dead';
86
+ }
87
+ }
88
+ return {
89
+ file,
90
+ state,
91
+ token: value.token,
92
+ digest: sha256Digest(content),
93
+ identity: { dev: stats.dev, ino: stats.ino },
94
+ };
95
+ }
96
+ catch (error) {
97
+ return error.code === 'ENOENT'
98
+ ? { ...empty, state: 'missing' }
99
+ : empty;
100
+ }
101
+ finally {
102
+ await handle?.close();
103
+ }
104
+ }
105
+ function sameLock(a, b) {
106
+ return (a.state === b.state &&
107
+ a.token === b.token &&
108
+ a.digest === b.digest &&
109
+ a.identity?.dev === b.identity?.dev &&
110
+ a.identity?.ino === b.identity?.ino);
111
+ }
112
+ /** One immutable claim wins a stale owner boundary; interrupted claimants can be succeeded. */
113
+ export async function reclaimDeadPrivateLock(file, revalidate) {
114
+ await revalidate();
115
+ const original = await inspectPrivateLock(file);
116
+ if (original.state === 'missing')
117
+ return false;
118
+ if (original.state !== 'dead' || original.digest === null)
119
+ throw new Error(`Lock owner is alive or unverified; preserve ${file}`);
120
+ const base = `${file}.reclaim-${original.digest.slice(7)}`;
121
+ let claim = base;
122
+ for (let hop = 0; hop < 64; hop += 1) {
123
+ await revalidate();
124
+ const before = await inspectPrivateLock(file);
125
+ if (before.state === 'missing')
126
+ return false;
127
+ if (!sameLock(original, before))
128
+ throw new Error(`Lock changed during recovery: ${file}`);
129
+ try {
130
+ await publishPrivateLock(claim, Buffer.from(`${JSON.stringify({ token: randomUUID(), pid: process.pid, process_identity: captureProcessIdentity() ?? undefined })}\n`), revalidate);
131
+ }
132
+ catch (error) {
133
+ if (error.code !== 'EEXIST')
134
+ throw error;
135
+ const owner = await inspectPrivateLock(claim);
136
+ if (owner.state !== 'dead')
137
+ throw new Error(`Another or unverified repair owns ${file}`);
138
+ claim = `${base}.retry-${sha256Digest(JSON.stringify([owner.digest, owner.identity])).slice(7)}`;
139
+ continue;
140
+ }
141
+ await revalidate();
142
+ if (!sameLock(original, await inspectPrivateLock(file)))
143
+ throw new Error(`Lock changed after recovery reservation: ${file}`);
144
+ await unlink(file);
145
+ return true;
146
+ }
147
+ throw new Error(`Too many interrupted lock repairs; preserve ${file}`);
148
+ }
@@ -0,0 +1,7 @@
1
+ import { type Outcome } from '../schemas/outcome.js';
2
+ /**
3
+ * Prepare private state before its first write, independently of init. Existing
4
+ * ignore rules are preserved; an appended final wildcard prevents earlier
5
+ * negations from exposing machine paths, checkpoints, or task metadata.
6
+ */
7
+ export declare function ensurePrivateState(root: string, directory?: 'local' | 'local-archive'): Promise<Outcome<null>>;
@@ -0,0 +1,146 @@
1
+ import { constants } from 'node:fs';
2
+ import { mkdir, open } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { diagnosticCodes } from '../schemas/errors.js';
5
+ import { errorMessage, failed, succeeded, } from '../schemas/outcome.js';
6
+ import { observePath, resolveRealpath } from './fs.js';
7
+ const MAX_IGNORE_BYTES = 64 * 1024;
8
+ function hasFinalIgnoreAll(text) {
9
+ const rules = text
10
+ .split('\n')
11
+ .map((line) => line.replace(/\r$/, ''))
12
+ .filter((line) => line !== '' && !line.startsWith('#'));
13
+ return rules.at(-1) === '*';
14
+ }
15
+ /**
16
+ * Prepare private state before its first write, independently of init. Existing
17
+ * ignore rules are preserved; an appended final wildcard prevents earlier
18
+ * negations from exposing machine paths, checkpoints, or task metadata.
19
+ */
20
+ export async function ensurePrivateState(root, directory = 'local') {
21
+ if (directory !== 'local' && directory !== 'local-archive')
22
+ return failed(diagnosticCodes.initTargetUnsafe, 'Unknown private-state namespace.', { nextAction: 'Use a supported private-state directory.' });
23
+ const canonical = await resolveRealpath(root);
24
+ if (canonical === null)
25
+ return failed(diagnosticCodes.initTargetUnsafe, `Cannot resolve the private-state workspace: ${root}`, { nextAction: 'Use an existing workspace root.' });
26
+ const pinned = [];
27
+ const assertCurrent = async () => {
28
+ for (const identity of pinned) {
29
+ const current = await observePath(identity.path);
30
+ if (current.kind !== 'directory' ||
31
+ current.stats?.dev !== identity.dev ||
32
+ current.stats.ino !== identity.ino) {
33
+ throw new Error(`Private-state namespace changed: ${identity.path}`);
34
+ }
35
+ }
36
+ };
37
+ const local = join(canonical, '.pappra', directory);
38
+ const ignore = join(local, '.gitignore');
39
+ try {
40
+ for (const path of [canonical, join(canonical, '.pappra'), local]) {
41
+ await assertCurrent();
42
+ let observation = await observePath(path);
43
+ if (observation.kind === 'missing' && path !== canonical) {
44
+ await mkdir(path, { mode: 0o700 }).catch(async (error) => {
45
+ if ((await observePath(path)).kind !== 'directory')
46
+ throw error;
47
+ });
48
+ observation = await observePath(path);
49
+ }
50
+ if (observation.kind !== 'directory' ||
51
+ observation.stats === null ||
52
+ (await resolveRealpath(path)) !== path) {
53
+ throw new Error(`Private-state namespace is redirected or not a directory: ${path}`);
54
+ }
55
+ pinned.push({
56
+ path,
57
+ dev: observation.stats.dev,
58
+ ino: observation.stats.ino,
59
+ });
60
+ }
61
+ await assertCurrent();
62
+ let observation = await observePath(ignore);
63
+ if (observation.kind === 'missing') {
64
+ let created;
65
+ try {
66
+ created = await open(ignore, constants.O_CREAT |
67
+ constants.O_EXCL |
68
+ constants.O_WRONLY |
69
+ constants.O_APPEND |
70
+ constants.O_NOFOLLOW, 0o600);
71
+ }
72
+ catch (error) {
73
+ if ((await observePath(ignore)).kind === 'missing')
74
+ throw error;
75
+ }
76
+ if (created !== undefined) {
77
+ try {
78
+ await assertCurrent();
79
+ await created.writeFile('*\n');
80
+ await created.sync();
81
+ const stats = await created.stat();
82
+ observation = await observePath(ignore);
83
+ if (observation.kind !== 'file' ||
84
+ observation.stats?.dev !== stats.dev ||
85
+ observation.stats.ino !== stats.ino) {
86
+ throw new Error(`Private-state ignore file changed during creation: ${ignore}`);
87
+ }
88
+ await assertCurrent();
89
+ }
90
+ finally {
91
+ await created.close();
92
+ }
93
+ }
94
+ observation = await observePath(ignore);
95
+ }
96
+ if (observation.kind !== 'file' || observation.stats === null)
97
+ throw new Error(`Private-state ignore path is not a regular file: ${ignore}`);
98
+ const handle = await open(ignore, constants.O_RDWR | constants.O_APPEND | constants.O_NOFOLLOW);
99
+ try {
100
+ const stats = await handle.stat();
101
+ if (!stats.isFile() ||
102
+ stats.dev !== observation.stats.dev ||
103
+ stats.ino !== observation.stats.ino) {
104
+ throw new Error(`Private-state ignore identity changed: ${ignore}`);
105
+ }
106
+ const readText = async () => {
107
+ const bytes = Buffer.alloc(MAX_IGNORE_BYTES + 1);
108
+ let length = 0;
109
+ while (length < bytes.length) {
110
+ const read = await handle.read(bytes, length, bytes.length - length, length);
111
+ if (read.bytesRead === 0)
112
+ break;
113
+ length += read.bytesRead;
114
+ }
115
+ if (length > MAX_IGNORE_BYTES)
116
+ throw new Error(`Private-state ignore file exceeds ${MAX_IGNORE_BYTES} bytes: ${ignore}`);
117
+ return new TextDecoder('utf-8', { fatal: true }).decode(bytes.subarray(0, length));
118
+ };
119
+ const text = await readText();
120
+ await assertCurrent();
121
+ if (!hasFinalIgnoreAll(text)) {
122
+ // O_APPEND preserves existing bytes even if another writer extends the
123
+ // file after it was read; no user file is replaced or truncated.
124
+ await handle.writeFile(`${text.endsWith('\n') || text === '' ? '' : '\n'}*\n`);
125
+ await handle.sync();
126
+ }
127
+ const current = await observePath(ignore);
128
+ if (current.kind !== 'file' ||
129
+ current.stats?.dev !== stats.dev ||
130
+ current.stats.ino !== stats.ino ||
131
+ !hasFinalIgnoreAll(await readText())) {
132
+ throw new Error(`Private-state ignore file changed during preparation: ${ignore}`);
133
+ }
134
+ await assertCurrent();
135
+ return succeeded(null);
136
+ }
137
+ finally {
138
+ await handle.close();
139
+ }
140
+ }
141
+ catch (error) {
142
+ return failed(diagnosticCodes.initTargetUnsafe, `Cannot prepare private state: ${errorMessage(error)}`, {
143
+ nextAction: `Restore real .pappra/${directory} directories and a writable regular .gitignore; existing state and rules were preserved.`,
144
+ });
145
+ }
146
+ }
@@ -0,0 +1,12 @@
1
+ export interface LocalProcessIdentity {
2
+ readonly schema: 'pappra/local-process/1';
3
+ readonly pid: number;
4
+ readonly machine: string;
5
+ readonly boot: string;
6
+ readonly namespace: string;
7
+ readonly start: string;
8
+ }
9
+ export declare function captureProcessIdentity(pid?: number): LocalProcessIdentity | null;
10
+ /** Identity mismatches prove replacement only on this exact physical machine. */
11
+ export declare function isLocalProcessIdentity(value: unknown): value is LocalProcessIdentity;
12
+ export declare function observeProcessIdentity(value: unknown): 'alive' | 'dead' | 'unknown';
@@ -0,0 +1,142 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import { spawnSync } from 'node:child_process';
4
+ let host;
5
+ let self;
6
+ const hash = (value) => `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`;
7
+ function command(file, args) {
8
+ const result = spawnSync(file, args, {
9
+ encoding: 'utf8',
10
+ env: { ...process.env, LC_ALL: 'C', LANG: 'C' },
11
+ timeout: 2000,
12
+ maxBuffer: 32 * 1024,
13
+ shell: false,
14
+ });
15
+ return !result.error && result.status === 0 ? result.stdout.trim() : null;
16
+ }
17
+ function hostIdentity() {
18
+ if (host !== undefined)
19
+ return host;
20
+ try {
21
+ if (process.platform === 'linux') {
22
+ const machine = fs.readFileSync('/etc/machine-id', 'utf8').trim();
23
+ const boot = fs
24
+ .readFileSync('/proc/sys/kernel/random/boot_id', 'utf8')
25
+ .trim();
26
+ const namespace = fs.readlinkSync('/proc/self/ns/pid');
27
+ if (!/^[a-f0-9]{32}$/i.test(machine) ||
28
+ !/^[a-f0-9-]{36}$/i.test(boot) ||
29
+ !/^pid:\[[0-9]+\]$/.test(namespace))
30
+ return (host = null);
31
+ return (host = {
32
+ machine: hash(machine.toLowerCase()),
33
+ boot: hash(boot.toLowerCase()),
34
+ namespace,
35
+ });
36
+ }
37
+ if (process.platform === 'darwin') {
38
+ const boot = command('/usr/sbin/sysctl', ['-n', 'kern.bootsessionuuid']);
39
+ const platform = command('/usr/sbin/ioreg', [
40
+ '-rd1',
41
+ '-c',
42
+ 'IOPlatformExpertDevice',
43
+ ]);
44
+ const machine = platform?.match(/"IOPlatformUUID"\s*=\s*"([a-f0-9-]{36})"/i)?.[1];
45
+ if (!boot || !/^[a-f0-9-]{36}$/i.test(boot) || !machine)
46
+ return (host = null);
47
+ return (host = {
48
+ machine: hash(machine.toLowerCase()),
49
+ boot: hash(boot.toLowerCase()),
50
+ namespace: 'native',
51
+ });
52
+ }
53
+ }
54
+ catch {
55
+ /* Missing OS identity is not evidence of a dead owner. */
56
+ }
57
+ return (host = null);
58
+ }
59
+ function processStart(pid) {
60
+ try {
61
+ if (process.platform === 'linux') {
62
+ const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf8');
63
+ const close = stat.lastIndexOf(')');
64
+ const fields = stat
65
+ .slice(close + 2)
66
+ .trim()
67
+ .split(/\s+/);
68
+ const ticks = fields[19];
69
+ return close > 0 && ticks && /^[0-9]+$/.test(ticks) ? ticks : null;
70
+ }
71
+ if (process.platform === 'darwin') {
72
+ const start = command('/bin/ps', ['-p', String(pid), '-o', 'lstart=']);
73
+ return start &&
74
+ /^[A-Za-z]{3} [A-Za-z]{3}\s+[0-9]+ [0-9:]{8} [0-9]{4}$/.test(start)
75
+ ? start
76
+ : null;
77
+ }
78
+ }
79
+ catch {
80
+ /* The caller distinguishes exit from unavailable observation. */
81
+ }
82
+ return null;
83
+ }
84
+ export function captureProcessIdentity(pid = process.pid) {
85
+ if (!Number.isSafeInteger(pid) || pid <= 0)
86
+ return null;
87
+ if (pid === process.pid && self !== undefined)
88
+ return self;
89
+ const currentHost = hostIdentity();
90
+ const start = processStart(pid);
91
+ const identity = currentHost && start
92
+ ? {
93
+ schema: 'pappra/local-process/1',
94
+ pid,
95
+ ...currentHost,
96
+ start,
97
+ }
98
+ : null;
99
+ if (pid === process.pid)
100
+ self = identity;
101
+ return identity;
102
+ }
103
+ /** Identity mismatches prove replacement only on this exact physical machine. */
104
+ export function isLocalProcessIdentity(value) {
105
+ if (typeof value !== 'object' || value === null)
106
+ return false;
107
+ const row = value;
108
+ return !(row.schema !== 'pappra/local-process/1' ||
109
+ !Number.isSafeInteger(row.pid) ||
110
+ Number(row.pid) <= 0 ||
111
+ typeof row.machine !== 'string' ||
112
+ typeof row.boot !== 'string' ||
113
+ typeof row.namespace !== 'string' ||
114
+ typeof row.start !== 'string' ||
115
+ row.start.length === 0 ||
116
+ row.start.length > 128 ||
117
+ !/^sha256:[0-9a-f]{64}$/.test(String(row.machine)) ||
118
+ !/^sha256:[0-9a-f]{64}$/.test(String(row.boot)) ||
119
+ !/^(?:native|pid:\[[0-9]+\])$/.test(String(row.namespace)));
120
+ }
121
+ export function observeProcessIdentity(value) {
122
+ if (!isLocalProcessIdentity(value))
123
+ return 'unknown';
124
+ const row = value;
125
+ const currentHost = hostIdentity();
126
+ if (!currentHost || row.machine !== currentHost.machine)
127
+ return 'unknown';
128
+ if (row.boot !== currentHost.boot)
129
+ return 'dead';
130
+ if (row.namespace !== currentHost.namespace)
131
+ return 'unknown';
132
+ try {
133
+ process.kill(Number(row.pid), 0);
134
+ }
135
+ catch (error) {
136
+ return error.code === 'ESRCH'
137
+ ? 'dead'
138
+ : 'unknown';
139
+ }
140
+ const start = processStart(Number(row.pid));
141
+ return start === null ? 'unknown' : start === row.start ? 'alive' : 'dead';
142
+ }
@@ -0,0 +1,5 @@
1
+ /** Decode external JSON as unknown; domain validators establish usable shapes. */
2
+ export declare function object(value: unknown): value is Record<string, unknown>;
3
+ export declare function text(value: unknown): value is string;
4
+ export declare function errorCode(error: unknown): string | undefined;
5
+ export declare function parseJson(text: string): unknown;
@@ -0,0 +1,15 @@
1
+ /** Decode external JSON as unknown; domain validators establish usable shapes. */
2
+ export function object(value) {
3
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
4
+ }
5
+ export function text(value) {
6
+ return typeof value === 'string' && value.trim().length > 0;
7
+ }
8
+ export function errorCode(error) {
9
+ return object(error) && typeof error['code'] === 'string'
10
+ ? error['code']
11
+ : undefined;
12
+ }
13
+ export function parseJson(text) {
14
+ return JSON.parse(text);
15
+ }
package/dist/init.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './app/init.js';
package/dist/init.js ADDED
@@ -0,0 +1,6 @@
1
+ // Core entrypoint for workspace initialization.
2
+ //
3
+ // The implementation lives in `src/app/init.ts`; this barrel keeps the
4
+ // published `@phyzess/pappra/init` path independent of the internal `app/`
5
+ // layout.
6
+ export * from './app/init.js';
@@ -2,6 +2,7 @@ import type { Diagnostic } from '../schemas/diagnostics.js';
2
2
  import { type InstallJournal, type ManagedFileEntry, type RuntimeManifest, type WorkspaceInstallManifest } from '../schemas/install.js';
3
3
  import type { InitPlan } from '../schemas/init.js';
4
4
  import type { PlannedFileContent } from './plan.js';
5
+ import { type Outcome } from '../schemas/outcome.js';
5
6
  export interface FileIdentity {
6
7
  readonly dev: number;
7
8
  readonly ino: number;
@@ -28,7 +29,10 @@ export type ManagedApplyBegin = {
28
29
  readonly residualPaths: readonly string[];
29
30
  };
30
31
  export declare function beginManagedApply(root: string, plan: InitPlan, writes: readonly PlannedFileContent[], rootIdentity: FileIdentity): Promise<ManagedApplyBegin>;
31
- export declare function recordManagedFile(context: ManagedApplyContext, path: string, digest: string, identity: FileIdentity): Promise<{
32
+ export declare function recordManagedFile(context: ManagedApplyContext, path: string, digest: string, identity: FileIdentity, replacement?: {
33
+ readonly backupRef: string;
34
+ readonly backupDigest: string;
35
+ }): Promise<{
32
36
  readonly ok: true;
33
37
  } | {
34
38
  readonly ok: false;
@@ -56,6 +60,10 @@ export interface ManagedMachineUpdateResult {
56
60
  readonly updateInstallId?: string;
57
61
  readonly journalPath?: string;
58
62
  }
63
+ /** Read-only gate shared by initial planning and every apply, including no-op plans. */
64
+ export declare function inspectWorkspaceInstallReadiness(root: string, ownLock?: boolean): Promise<Outcome<null>>;
65
+ /** Serialize a zero-file apply with installation and recovery, without adding an empty journal. */
66
+ export declare function verifyWorkspaceInstallReady(root: string): Promise<Outcome<null>>;
59
67
  /** Updates the managed machine.json runtime location after a verified install. */
60
68
  export declare function updateMachineRuntime(root: string, runtime: RuntimeManifest, options?: UpdateMachineRuntimeOptions): Promise<ManagedMachineUpdateResult>;
61
69
  export interface ManagedWorkspaceState {