@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
package/dist/infra/git.js CHANGED
@@ -20,6 +20,7 @@ function sanitizedGitEnvironment() {
20
20
  'GIT_CEILING_DIRECTORIES',
21
21
  'GIT_CONFIG_COUNT',
22
22
  'GIT_CONFIG_PARAMETERS',
23
+ 'GIT_EXTERNAL_DIFF',
23
24
  ]) {
24
25
  delete environment[key];
25
26
  }
@@ -281,17 +282,22 @@ export async function inspectGitRepository(candidateRoot, options = {}) {
281
282
  limitations.push('branch unavailable');
282
283
  }
283
284
  let dirty = null;
284
- const statusResult = await run([
285
- 'status',
286
- '--porcelain=v1',
287
- '-z',
288
- '--untracked-files=normal',
289
- ]);
290
- if (statusResult.ok) {
291
- dirty = statusResult.value.stdout.length > 0;
285
+ if (options.workingTreeState === false) {
286
+ limitations.push('working tree state not requested');
292
287
  }
293
288
  else {
294
- limitations.push(`working tree state unavailable: ${statusResult.message}`);
289
+ const statusResult = await run([
290
+ 'status',
291
+ '--porcelain=v1',
292
+ '-z',
293
+ '--untracked-files=normal',
294
+ ]);
295
+ if (statusResult.ok) {
296
+ dirty = statusResult.value.stdout.length > 0;
297
+ }
298
+ else {
299
+ limitations.push(`working tree state unavailable: ${statusResult.message}`);
300
+ }
295
301
  }
296
302
  const kind = isBare
297
303
  ? 'bare'
@@ -356,3 +362,33 @@ export async function filterGitIgnored(repositoryRoot, candidates, options = {})
356
362
  }
357
363
  return succeeded(ignored);
358
364
  }
365
+ /**
366
+ * Memoizes Git repository probes for exactly one operation.
367
+ *
368
+ * Repository identity (root, git dir, HEAD, branch) is immutable for the
369
+ * duration of a single command, so repeated inspections of the same checkout
370
+ * reuse the first observation instead of spawning equivalent Git processes.
371
+ * Working-tree state is *not* immutable: the scope therefore only lives inside
372
+ * one operation and must never be reused across operations or persisted. Callers
373
+ * that re-observe a tree after mutation (code prepare) keep using the scope for
374
+ * identity probes while the working-tree diff is always re-read.
375
+ */
376
+ export function createGitObservationScope(options = {}) {
377
+ const defaultWorkingTreeState = options.workingTreeState ?? true;
378
+ const observations = new Map();
379
+ return {
380
+ repository(root, inspectOptions = {}) {
381
+ const workingTreeState = inspectOptions.workingTreeState ?? defaultWorkingTreeState;
382
+ const key = JSON.stringify([resolve(root), workingTreeState]);
383
+ const cached = observations.get(key);
384
+ if (cached !== undefined)
385
+ return cached;
386
+ const pending = inspectGitRepository(root, {
387
+ ...inspectOptions,
388
+ workingTreeState,
389
+ });
390
+ observations.set(key, pending);
391
+ return pending;
392
+ },
393
+ };
394
+ }
@@ -0,0 +1,2 @@
1
+ /** Replace one top-level JSON property while retaining unrelated bytes and style. */
2
+ export declare function updateJsonProperty(existing: Buffer | null, key: string, value: unknown): Buffer;
@@ -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;