@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,1006 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { acquireInstallLock, ensureStatePathBoundary, releaseInstallLock, writeBytesAtomic, } from '../install/ownership.js';
4
+ import { inspectWorkspaceConfig } from '../workspace/config.js';
5
+ import { resolveWorkspaceRoot } from '../workspace/resolve.js';
6
+ import { readBoundedFile } from './bounded-file.js';
7
+ import { openTaskStore } from './collab-task-store.js';
8
+ export const COLLAB_BINDINGS_SCHEMA = 'pappra/collaboration-bindings/1';
9
+ export const COLLAB_BINDING_RESULT_SCHEMA = 'pappra/collaboration-binding-result/1';
10
+ const MAX_INDEX_BYTES = 1024 * 1024;
11
+ const MAX_ROOTS_BYTES = 1024 * 1024;
12
+ const MAX_ROOTS = 32;
13
+ const MAX_OUTPUT_ROWS = 50;
14
+ const TASK_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
15
+ function failure(code, message, nextAction = null, details) {
16
+ return {
17
+ ok: false,
18
+ code,
19
+ message,
20
+ nextAction,
21
+ ...(details === undefined ? {} : { details }),
22
+ };
23
+ }
24
+ function isPlainObject(value) {
25
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
26
+ }
27
+ function safeAlias(value) {
28
+ return typeof value === 'string' && TASK_ID_PATTERN.test(value);
29
+ }
30
+ function emptyTable() {
31
+ return Object.create(null);
32
+ }
33
+ function own(table, key) {
34
+ return Object.hasOwn(table, key);
35
+ }
36
+ function absoluteText(value) {
37
+ return (typeof value === 'string' && value.length > 0 && path.isAbsolute(value));
38
+ }
39
+ function validTimestamp(value) {
40
+ return (typeof value === 'string' &&
41
+ /(?:Z|[+-]\d\d:\d\d)$/.test(value) &&
42
+ Number.isFinite(Date.parse(value)));
43
+ }
44
+ function indexFile(root) {
45
+ return path.join(root, '.pappra', 'local', 'collab-bindings.json');
46
+ }
47
+ function indexLockFile(root) {
48
+ return path.join(root, '.pappra', 'local', 'collab-bindings.lock');
49
+ }
50
+ async function selectWorkspace(rootOption) {
51
+ if (rootOption !== null && !path.isAbsolute(rootOption))
52
+ return failure('invalid_options', '--root must be an absolute workspace path.', 'Pass an absolute root or omit it to use the current workspace.');
53
+ const resolution = await resolveWorkspaceRoot({
54
+ startPath: rootOption ?? process.cwd(),
55
+ });
56
+ if (resolution.state !== 'registered' || resolution.root === null)
57
+ return failure('workspace_unavailable', `No usable registered workspace was selected (${resolution.state}).`, 'Pass --root for a registered workspace or run pappra init.', resolution.diagnostics.map((item) => item.code));
58
+ const config = await inspectWorkspaceConfig(resolution.root, {
59
+ missing: 'error',
60
+ });
61
+ if (config.value === null)
62
+ return failure('workspace_unavailable', 'The selected workspace configuration is missing or invalid.', 'Repair workspace.json explicitly; binding metadata is not read or written.', config.diagnostics.map((item) => item.code));
63
+ return {
64
+ ok: true,
65
+ value: {
66
+ root: config.root,
67
+ workspace_id: config.value.workspaceId,
68
+ },
69
+ };
70
+ }
71
+ function validateBindingEntry(alias, value, errors) {
72
+ if (!safeAlias(alias)) {
73
+ errors.push(`binding_entry_invalid:${alias}:unsafe_alias`);
74
+ return null;
75
+ }
76
+ if (!isPlainObject(value)) {
77
+ errors.push(`binding_entry_invalid:${alias}:not_an_object`);
78
+ return null;
79
+ }
80
+ const keys = Object.keys(value).sort();
81
+ const expected = [
82
+ 'alias',
83
+ 'bound_at',
84
+ 'domain_id',
85
+ 'domain_root',
86
+ 'protocol_task_id',
87
+ 'record',
88
+ 'record_created_at',
89
+ 'record_realpath',
90
+ 'task_root',
91
+ ];
92
+ if (keys.join('|') !== expected.join('|')) {
93
+ errors.push(`binding_entry_invalid:${alias}:unexpected_fields`);
94
+ return null;
95
+ }
96
+ if (value.alias !== alias) {
97
+ errors.push(`binding_entry_invalid:${alias}:alias_mismatch`);
98
+ return null;
99
+ }
100
+ if (!absoluteText(value.record) ||
101
+ path.basename(value.record) !== 'task.json' ||
102
+ !absoluteText(value.record_realpath) ||
103
+ !absoluteText(value.task_root) ||
104
+ !absoluteText(value.domain_root)) {
105
+ errors.push(`binding_entry_invalid:${alias}:path_invalid`);
106
+ return null;
107
+ }
108
+ if (!safeAlias(value.protocol_task_id)) {
109
+ errors.push(`binding_entry_invalid:${alias}:protocol_task_id_invalid`);
110
+ return null;
111
+ }
112
+ if (typeof value.domain_id !== 'string' || !value.domain_id.trim()) {
113
+ errors.push(`binding_entry_invalid:${alias}:domain_id_invalid`);
114
+ return null;
115
+ }
116
+ if (!validTimestamp(value.record_created_at)) {
117
+ errors.push(`binding_entry_invalid:${alias}:record_created_at_invalid`);
118
+ return null;
119
+ }
120
+ if (!validTimestamp(value.bound_at)) {
121
+ errors.push(`binding_entry_invalid:${alias}:bound_at_invalid`);
122
+ return null;
123
+ }
124
+ return {
125
+ alias,
126
+ record: value.record,
127
+ record_realpath: value.record_realpath,
128
+ task_root: value.task_root,
129
+ domain_root: value.domain_root,
130
+ protocol_task_id: value.protocol_task_id,
131
+ domain_id: value.domain_id,
132
+ record_created_at: value.record_created_at,
133
+ bound_at: value.bound_at,
134
+ };
135
+ }
136
+ function validateTombstone(alias, value, errors) {
137
+ if (!safeAlias(alias)) {
138
+ errors.push(`unbound_entry_invalid:${alias}:unsafe_alias`);
139
+ return null;
140
+ }
141
+ if (!isPlainObject(value) || value.alias !== alias) {
142
+ errors.push(`unbound_entry_invalid:${alias}:not_an_object`);
143
+ return null;
144
+ }
145
+ const keys = Object.keys(value).sort();
146
+ if (keys.join('|') !== 'alias|at|previous') {
147
+ errors.push(`unbound_entry_invalid:${alias}:unexpected_fields`);
148
+ return null;
149
+ }
150
+ if (!validTimestamp(value.at)) {
151
+ errors.push(`unbound_entry_invalid:${alias}:at_invalid`);
152
+ return null;
153
+ }
154
+ let previous = null;
155
+ if (value.previous !== null) {
156
+ previous = validateBindingEntry(alias, value.previous, errors);
157
+ if (!previous)
158
+ return null;
159
+ }
160
+ return { alias, at: value.at, previous };
161
+ }
162
+ function parseIndex(text, workspaceId) {
163
+ let parsed;
164
+ try {
165
+ parsed = JSON.parse(text);
166
+ }
167
+ catch {
168
+ return failure('bindings_invalid', 'The collaboration bindings index is not valid JSON.', 'Inspect and repair the index explicitly; it was not overwritten.');
169
+ }
170
+ const errors = [];
171
+ if (!isPlainObject(parsed)) {
172
+ return failure('bindings_invalid', 'The collaboration bindings index is not a JSON object.', 'Inspect and repair the index explicitly; it was not overwritten.');
173
+ }
174
+ const keys = Object.keys(parsed).sort();
175
+ if (keys.join('|') !== 'bindings|schema|unbound|workspace_id') {
176
+ errors.push('bindings_unexpected_fields');
177
+ }
178
+ if (parsed.schema !== COLLAB_BINDINGS_SCHEMA)
179
+ errors.push('bindings_schema_mismatch');
180
+ if (parsed.workspace_id !== workspaceId)
181
+ errors.push('bindings_workspace_mismatch');
182
+ if (!isPlainObject(parsed.bindings))
183
+ errors.push('bindings_field_invalid');
184
+ if (!isPlainObject(parsed.unbound))
185
+ errors.push('unbound_field_invalid');
186
+ if (errors.length)
187
+ return failure('bindings_invalid', 'The collaboration bindings index has an unsupported shape or workspace identity.', 'Inspect and repair the index explicitly; it was not overwritten.', errors);
188
+ const bindings = emptyTable();
189
+ for (const [alias, value] of Object.entries(parsed.bindings)) {
190
+ const entry = validateBindingEntry(alias, value, errors);
191
+ if (entry)
192
+ bindings[alias] = entry;
193
+ }
194
+ const unbound = emptyTable();
195
+ for (const [alias, value] of Object.entries(parsed.unbound)) {
196
+ const tombstone = validateTombstone(alias, value, errors);
197
+ if (tombstone)
198
+ unbound[alias] = tombstone;
199
+ }
200
+ if (Object.keys(bindings).some((alias) => own(unbound, alias)))
201
+ errors.push('binding_unbound_overlap');
202
+ if (errors.length)
203
+ return failure('bindings_invalid', 'The collaboration bindings index contains invalid entries.', 'Inspect and repair the index explicitly; it was not overwritten.', errors);
204
+ return {
205
+ ok: true,
206
+ value: {
207
+ schema: COLLAB_BINDINGS_SCHEMA,
208
+ workspace_id: workspaceId,
209
+ bindings,
210
+ unbound,
211
+ },
212
+ };
213
+ }
214
+ async function readIndex(root, workspaceId) {
215
+ const file = indexFile(root);
216
+ const boundary = await ensureStatePathBoundary(root, file);
217
+ if (!boundary.ok)
218
+ return failure('bindings_invalid', 'The workspace private state path is redirected.', 'Restore .pappra/local as a real directory inside the workspace.', [boundary.code]);
219
+ let stat = null;
220
+ try {
221
+ stat = fs.lstatSync(file);
222
+ }
223
+ catch (error) {
224
+ const code = typeof error === 'object' && error !== null && 'code' in error
225
+ ? String(error.code)
226
+ : null;
227
+ if (code !== null && (code === 'ENOENT' || code === 'ENOTDIR'))
228
+ return {
229
+ ok: true,
230
+ value: {
231
+ present: false,
232
+ table: {
233
+ schema: COLLAB_BINDINGS_SCHEMA,
234
+ workspace_id: workspaceId,
235
+ bindings: emptyTable(),
236
+ unbound: emptyTable(),
237
+ },
238
+ },
239
+ };
240
+ return failure('bindings_invalid', 'The collaboration bindings index could not be inspected.', 'Inspect permissions and the private state path; nothing was written.');
241
+ }
242
+ if (stat.isSymbolicLink() || !stat.isFile() || stat.size > MAX_INDEX_BYTES)
243
+ return failure('bindings_invalid', 'The collaboration bindings index is not a supported regular file within 1 MiB.', 'Restore a regular index inside .pappra/local; nothing was written.');
244
+ const read = readBoundedFile(file, MAX_INDEX_BYTES);
245
+ if (!read.ok)
246
+ return failure('bindings_invalid', 'The collaboration bindings index could not be read as a bounded regular file.', 'Inspect the index and private state; nothing was written.');
247
+ const parsed = parseIndex(read.text, workspaceId);
248
+ if (!parsed.ok)
249
+ return parsed;
250
+ return { ok: true, value: { present: true, table: parsed.value } };
251
+ }
252
+ function tableJson(table) {
253
+ return {
254
+ schema: COLLAB_BINDINGS_SCHEMA,
255
+ workspace_id: table.workspace_id,
256
+ bindings: { ...table.bindings },
257
+ unbound: { ...table.unbound },
258
+ };
259
+ }
260
+ function mapTargetFailure(error) {
261
+ if (error.code === 'task_record_missing')
262
+ return failure('target_missing', 'The bound task record is missing.', 'Inspect the target path; no fallback record is selected.');
263
+ return failure('target_invalid', 'The target is not a usable authoritative collaboration record.', 'Inspect the record and task layout; no repair was attempted.', [error.code]);
264
+ }
265
+ async function inspectTarget(recordPath) {
266
+ if (!absoluteText(recordPath) || path.basename(recordPath) !== 'task.json')
267
+ return failure('target_invalid', 'A binding target must be an absolute <task_root>/task.json path.', 'Pass an absolute authoritative task.json path.');
268
+ const opened = await openTaskStore(path.dirname(recordPath));
269
+ if (!opened.ok)
270
+ return mapTargetFailure(opened);
271
+ let recordRealpath;
272
+ try {
273
+ recordRealpath = fs.realpathSync(recordPath);
274
+ }
275
+ catch {
276
+ return failure('target_missing', 'The bound task record is missing.', 'Inspect the target path; no fallback record is selected.');
277
+ }
278
+ if (recordRealpath !== opened.value.recordFile)
279
+ return failure('target_invalid', 'The supplied record path is not the authoritative record for its task root.', 'Pass the real <task_root>/task.json path.');
280
+ const record = opened.value.record;
281
+ if (!safeAlias(record.task_id) ||
282
+ !absoluteText(record.domain_root) ||
283
+ typeof record.domain_id !== 'string' ||
284
+ !record.domain_id.trim() ||
285
+ !validTimestamp(record.created_at))
286
+ return failure('target_invalid', 'The task record does not expose a valid collaboration identity.', 'Inspect the record; it was not repaired or imported.');
287
+ const attempts = Array.isArray(record.attempts) ? record.attempts : [];
288
+ const attemptRows = attempts.slice(0, MAX_OUTPUT_ROWS).map((item) => {
289
+ const attempt = item;
290
+ const native = isPlainObject(attempt.native) ? attempt.native : null;
291
+ const assignee = isPlainObject(attempt.assignee) ? attempt.assignee : null;
292
+ return {
293
+ subtask_id: typeof attempt.subtask_id === 'string' ? attempt.subtask_id : null,
294
+ attempt: typeof attempt.attempt === 'number' ? attempt.attempt : null,
295
+ state: typeof attempt.state === 'string' ? attempt.state : 'unknown',
296
+ ownership: typeof attempt.ownership === 'string' ? attempt.ownership : null,
297
+ assignee: assignee
298
+ ? {
299
+ agent_id: assignee.agent_id ?? null,
300
+ session_id: assignee.session_id ?? null,
301
+ harness: assignee.harness ?? null,
302
+ }
303
+ : null,
304
+ native: native
305
+ ? {
306
+ runner: native.runner ?? null,
307
+ session_id: native.session_id ?? null,
308
+ run_id: native.run_id ?? null,
309
+ mission_id: native.mission_id ?? null,
310
+ observation: native.observation ?? 'unknown',
311
+ evidence_ref: native.evidence_ref ?? null,
312
+ }
313
+ : null,
314
+ };
315
+ });
316
+ const target = {
317
+ record: recordPath,
318
+ record_realpath: opened.value.recordFile,
319
+ task_root: opened.value.taskRoot,
320
+ domain_root: opened.value.domainRoot,
321
+ protocol_task_id: record.task_id,
322
+ domain_id: record.domain_id,
323
+ record_created_at: record.created_at,
324
+ state_revision: record.state_revision,
325
+ coordinator_epoch: record.coordinator?.owner_epoch ?? null,
326
+ dispatch_frozen: record.dispatch_frozen === true,
327
+ attempts: attemptRows,
328
+ attempt_count: attempts.length,
329
+ truncated: attempts.length > MAX_OUTPUT_ROWS,
330
+ };
331
+ return { ok: true, value: { target, record } };
332
+ }
333
+ function sameTargetIdentity(left, right) {
334
+ return (left.record_realpath === right.record_realpath &&
335
+ left.task_root === right.task_root &&
336
+ left.domain_root === right.domain_root &&
337
+ left.protocol_task_id === right.protocol_task_id &&
338
+ left.domain_id === right.domain_id &&
339
+ left.record_created_at === right.record_created_at);
340
+ }
341
+ function bindingMatches(binding, target) {
342
+ return sameTargetIdentity(pinnedIdentity(binding), target);
343
+ }
344
+ function pinnedIdentity(binding) {
345
+ return {
346
+ record: binding.record,
347
+ record_realpath: binding.record_realpath,
348
+ task_root: binding.task_root,
349
+ domain_root: binding.domain_root,
350
+ protocol_task_id: binding.protocol_task_id,
351
+ domain_id: binding.domain_id,
352
+ record_created_at: binding.record_created_at,
353
+ };
354
+ }
355
+ async function inspectBinding(binding) {
356
+ const inspected = await inspectTarget(binding.record);
357
+ if (!inspected.ok) {
358
+ return {
359
+ status: inspected.code === 'target_missing'
360
+ ? 'binding-missing'
361
+ : 'binding-malformed',
362
+ target: null,
363
+ errors: [inspected.code],
364
+ };
365
+ }
366
+ if (!bindingMatches(binding, inspected.value.target))
367
+ return {
368
+ status: 'binding-identity-changed',
369
+ target: null,
370
+ errors: ['record_identity_changed'],
371
+ };
372
+ return {
373
+ status: 'binding',
374
+ target: inspected.value,
375
+ errors: [],
376
+ };
377
+ }
378
+ function bounded(items) {
379
+ return {
380
+ rows: items.slice(0, MAX_OUTPUT_ROWS),
381
+ total: items.length,
382
+ omitted: Math.max(0, items.length - MAX_OUTPUT_ROWS),
383
+ truncated: items.length > MAX_OUTPUT_ROWS,
384
+ };
385
+ }
386
+ export async function listBindings(options) {
387
+ const workspace = await selectWorkspace(options.root);
388
+ if (!workspace.ok)
389
+ return workspace;
390
+ const index = await readIndex(workspace.value.root, workspace.value.workspace_id);
391
+ if (!index.ok)
392
+ return index;
393
+ const aliases = Object.keys(index.value.table.bindings).sort();
394
+ const limited = bounded(aliases);
395
+ const entries = [];
396
+ for (const alias of limited.rows) {
397
+ const binding = index.value.table.bindings[alias];
398
+ const checked = await inspectBinding(binding);
399
+ entries.push({
400
+ alias,
401
+ status: checked.status,
402
+ record: binding.record,
403
+ record_realpath: binding.record_realpath,
404
+ task_root: binding.task_root,
405
+ domain_root: binding.domain_root,
406
+ protocol_task_id: binding.protocol_task_id,
407
+ domain_id: binding.domain_id,
408
+ record_created_at: binding.record_created_at,
409
+ bound_at: binding.bound_at,
410
+ errors: checked.errors,
411
+ });
412
+ }
413
+ const tombstones = bounded(Object.keys(index.value.table.unbound).sort());
414
+ return {
415
+ ok: true,
416
+ value: {
417
+ schema: COLLAB_BINDING_RESULT_SCHEMA,
418
+ ok: true,
419
+ action: 'list',
420
+ workspace_id: workspace.value.workspace_id,
421
+ workspace_root: workspace.value.root,
422
+ index_path: indexFile(workspace.value.root),
423
+ present: index.value.present,
424
+ bindings: entries,
425
+ unbound: tombstones.rows.map((alias) => {
426
+ const tombstone = index.value.table.unbound[alias];
427
+ return {
428
+ alias,
429
+ at: tombstone.at,
430
+ previous: tombstone.previous
431
+ ? pinnedIdentity(tombstone.previous)
432
+ : null,
433
+ };
434
+ }),
435
+ counts: {
436
+ bindings: limited.total,
437
+ unbound: tombstones.total,
438
+ },
439
+ omitted: {
440
+ bindings: limited.omitted,
441
+ unbound: tombstones.omitted,
442
+ },
443
+ truncated: limited.truncated || tombstones.truncated,
444
+ limits: ['pointer_metadata_only', 'no_protocol_content'],
445
+ },
446
+ };
447
+ }
448
+ export async function resolveBinding(options) {
449
+ if (!safeAlias(options.taskId))
450
+ return failure('invalid_options', '--task-id must be a safe collaboration alias of 128 characters or fewer.', 'Use a single path segment of letters, digits, dot, dash or underscore.');
451
+ const workspace = await selectWorkspace(options.root);
452
+ if (!workspace.ok)
453
+ return workspace;
454
+ const index = await readIndex(workspace.value.root, workspace.value.workspace_id);
455
+ if (!index.ok)
456
+ return index;
457
+ // An explicit roots file is part of the request: validate it even when an
458
+ // explicit binding or tombstone wins precedence, so a malformed option never
459
+ // hides behind a usable pointer.
460
+ let roots = null;
461
+ if (options.roots !== null) {
462
+ const loaded = await loadRoots(options.roots);
463
+ if (!loaded.ok)
464
+ return loaded;
465
+ roots = loaded.value;
466
+ }
467
+ const alias = options.taskId;
468
+ const table = index.value.table;
469
+ if (own(table.bindings, alias)) {
470
+ const binding = table.bindings[alias];
471
+ const checked = await inspectBinding(binding);
472
+ return {
473
+ ok: true,
474
+ value: {
475
+ schema: COLLAB_BINDING_RESULT_SCHEMA,
476
+ ok: checked.status === 'binding',
477
+ status: checked.status,
478
+ action: 'resolve',
479
+ alias,
480
+ source: 'binding',
481
+ usable: checked.status === 'binding',
482
+ scope: checked.status === 'binding' ? 'persisted-history' : null,
483
+ liveStatus: 'unknown',
484
+ workspace_id: workspace.value.workspace_id,
485
+ workspace_root: workspace.value.root,
486
+ index_path: indexFile(workspace.value.root),
487
+ pointer: pinnedIdentity(binding),
488
+ target: checked.target?.target ?? null,
489
+ candidates: [],
490
+ errors: checked.errors,
491
+ limits: ['persisted_history_only', 'native_live_status_unknown'],
492
+ },
493
+ };
494
+ }
495
+ if (own(table.unbound, alias)) {
496
+ const marker = table.unbound[alias];
497
+ return {
498
+ ok: true,
499
+ value: {
500
+ schema: COLLAB_BINDING_RESULT_SCHEMA,
501
+ ok: true,
502
+ status: 'unbound',
503
+ action: 'resolve',
504
+ alias,
505
+ source: null,
506
+ usable: false,
507
+ scope: null,
508
+ liveStatus: 'unknown',
509
+ workspace_id: workspace.value.workspace_id,
510
+ workspace_root: workspace.value.root,
511
+ index_path: indexFile(workspace.value.root),
512
+ pointer: marker.previous ? pinnedIdentity(marker.previous) : null,
513
+ target: null,
514
+ candidates: [],
515
+ errors: [`explicitly_unbound_at: ${marker.at}`],
516
+ limits: ['explicit_tombstone', 'no_discovery_fallback'],
517
+ },
518
+ };
519
+ }
520
+ if (roots !== null) {
521
+ const discovery = await discoverByRoots(alias, roots);
522
+ if (!discovery.ok)
523
+ return discovery;
524
+ return {
525
+ ok: true,
526
+ value: {
527
+ schema: COLLAB_BINDING_RESULT_SCHEMA,
528
+ ok: discovery.value.status === 'discovery-unique',
529
+ status: discovery.value.status,
530
+ action: 'resolve',
531
+ alias,
532
+ source: discovery.value.status === 'discovery-unique' ? 'discovery' : null,
533
+ usable: discovery.value.status === 'discovery-unique',
534
+ scope: discovery.value.status === 'discovery-unique'
535
+ ? 'persisted-history'
536
+ : null,
537
+ liveStatus: 'unknown',
538
+ workspace_id: workspace.value.workspace_id,
539
+ workspace_root: workspace.value.root,
540
+ index_path: indexFile(workspace.value.root),
541
+ pointer: null,
542
+ target: discovery.value.target ?? null,
543
+ candidates: discovery.value.candidates,
544
+ errors: discovery.value.errors,
545
+ limits: ['explicit_roots_only', 'native_live_status_unknown'],
546
+ },
547
+ };
548
+ }
549
+ return {
550
+ ok: true,
551
+ value: {
552
+ schema: COLLAB_BINDING_RESULT_SCHEMA,
553
+ ok: true,
554
+ status: 'none',
555
+ action: 'resolve',
556
+ alias,
557
+ source: null,
558
+ usable: false,
559
+ scope: null,
560
+ liveStatus: 'unknown',
561
+ workspace_id: workspace.value.workspace_id,
562
+ workspace_root: workspace.value.root,
563
+ index_path: indexFile(workspace.value.root),
564
+ pointer: null,
565
+ target: null,
566
+ candidates: [],
567
+ errors: [],
568
+ limits: ['no_binding_or_explicit_roots'],
569
+ },
570
+ };
571
+ }
572
+ async function loadRoots(file) {
573
+ if (!path.isAbsolute(file))
574
+ return failure('roots_invalid', '--roots must be an absolute roots.json path.', 'Pass an absolute roots file.');
575
+ const read = readBoundedFile(file, MAX_ROOTS_BYTES);
576
+ if (!read.ok)
577
+ return failure('roots_invalid', 'roots.json must be a readable regular JSON file within 1 MiB.', 'Inspect the roots file; nothing was written.');
578
+ let parsed;
579
+ try {
580
+ parsed = JSON.parse(read.text);
581
+ }
582
+ catch {
583
+ return failure('roots_invalid', 'roots.json is not valid JSON.', 'Provide a strict array of absolute collaboration domain roots.');
584
+ }
585
+ if (!Array.isArray(parsed) || parsed.length > MAX_ROOTS)
586
+ return failure('roots_invalid', `roots.json must be an array of at most ${MAX_ROOTS} absolute roots.`, 'Fix the roots file; nothing was written.');
587
+ const roots = [];
588
+ for (const root of parsed) {
589
+ if (!absoluteText(root))
590
+ return failure('roots_invalid', 'Each discovery root must be an absolute path.', 'Fix the roots file; nothing was written.');
591
+ roots.push(root);
592
+ }
593
+ return { ok: true, value: [...new Set(roots)].sort() };
594
+ }
595
+ async function discoverByRoots(alias, roots) {
596
+ const errors = [];
597
+ const candidates = [];
598
+ const seen = new Set();
599
+ for (const root of roots) {
600
+ let canonicalRoot;
601
+ try {
602
+ canonicalRoot = fs.realpathSync(root);
603
+ }
604
+ catch {
605
+ errors.push('root_unavailable');
606
+ continue;
607
+ }
608
+ let observation;
609
+ try {
610
+ observation = fs.statSync(canonicalRoot);
611
+ }
612
+ catch {
613
+ errors.push('root_unavailable');
614
+ continue;
615
+ }
616
+ if (!observation.isDirectory()) {
617
+ errors.push('root_not_directory');
618
+ continue;
619
+ }
620
+ const tasksDir = path.join(canonicalRoot, 'tasks');
621
+ let tasksStats;
622
+ try {
623
+ tasksStats = fs.lstatSync(tasksDir);
624
+ }
625
+ catch (error) {
626
+ const code = typeof error === 'object' && error !== null && 'code' in error
627
+ ? String(error.code)
628
+ : null;
629
+ if (code !== null && (code === 'ENOENT' || code === 'ENOTDIR'))
630
+ continue;
631
+ errors.push('root_unavailable');
632
+ continue;
633
+ }
634
+ if (tasksStats.isSymbolicLink() || !tasksStats.isDirectory()) {
635
+ errors.push('root_tasks_not_regular');
636
+ continue;
637
+ }
638
+ const candidate = path.join(tasksDir, alias, 'task.json');
639
+ let candidateStats;
640
+ try {
641
+ candidateStats = fs.lstatSync(candidate);
642
+ }
643
+ catch (error) {
644
+ const code = typeof error === 'object' && error !== null && 'code' in error
645
+ ? String(error.code)
646
+ : null;
647
+ if (code !== null && (code === 'ENOENT' || code === 'ENOTDIR'))
648
+ continue;
649
+ errors.push('candidate_unreadable');
650
+ continue;
651
+ }
652
+ if (candidateStats.isSymbolicLink() || !candidateStats.isFile()) {
653
+ errors.push('candidate_not_regular');
654
+ continue;
655
+ }
656
+ const inspected = await inspectTarget(candidate);
657
+ if (!inspected.ok) {
658
+ errors.push(inspected.code === 'target_missing'
659
+ ? 'candidate_unreadable'
660
+ : 'candidate_invalid');
661
+ continue;
662
+ }
663
+ if (inspected.value.target.protocol_task_id !== alias) {
664
+ errors.push('candidate_task_id_mismatch');
665
+ continue;
666
+ }
667
+ if (inspected.value.target.domain_root !== canonicalRoot ||
668
+ inspected.value.target.task_root !== path.join(tasksDir, alias)) {
669
+ errors.push('candidate_identity_mismatch');
670
+ continue;
671
+ }
672
+ if (seen.has(inspected.value.target.record_realpath))
673
+ continue;
674
+ seen.add(inspected.value.target.record_realpath);
675
+ candidates.push({
676
+ target: inspected.value,
677
+ candidate: inspected.value.target.record_realpath,
678
+ });
679
+ }
680
+ candidates.sort((left, right) => left.candidate.localeCompare(right.candidate));
681
+ const paths = candidates.map((item) => item.candidate);
682
+ if (errors.length)
683
+ return {
684
+ ok: true,
685
+ value: {
686
+ status: 'discovery-incomplete',
687
+ target: null,
688
+ candidates: paths,
689
+ errors: [...new Set(errors)].sort(),
690
+ },
691
+ };
692
+ if (candidates.length === 1)
693
+ return {
694
+ ok: true,
695
+ value: {
696
+ status: 'discovery-unique',
697
+ target: candidates[0].target.target,
698
+ candidates: paths,
699
+ errors: [],
700
+ },
701
+ };
702
+ if (candidates.length > 1)
703
+ return {
704
+ ok: true,
705
+ value: {
706
+ status: 'discovery-ambiguous',
707
+ target: null,
708
+ candidates: paths,
709
+ errors: ['multiple_discovery_candidates'],
710
+ },
711
+ };
712
+ return {
713
+ ok: true,
714
+ value: {
715
+ status: 'none',
716
+ target: null,
717
+ candidates: [],
718
+ errors: [],
719
+ },
720
+ };
721
+ }
722
+ async function withBindingLock(root, task) {
723
+ const lockPath = indexLockFile(root);
724
+ const boundary = await ensureStatePathBoundary(root, lockPath);
725
+ if (!boundary.ok)
726
+ return failure('bindings_invalid', 'The binding lock path is redirected.', 'Restore .pappra/local as a real directory inside the workspace.', [boundary.code]);
727
+ let existing = null;
728
+ try {
729
+ existing = fs.lstatSync(lockPath);
730
+ }
731
+ catch (error) {
732
+ const code = typeof error === 'object' && error !== null && 'code' in error
733
+ ? String(error.code)
734
+ : null;
735
+ if (code !== null && code !== 'ENOENT' && code !== 'ENOTDIR')
736
+ return failure('bindings_invalid', 'The binding lock path could not be inspected.', 'Inspect .pappra/local; nothing was written.');
737
+ }
738
+ if (existing && !existing.isFile())
739
+ return failure('bindings_invalid', 'The binding lock path is not a regular file.', 'Remove or repair the lock explicitly; it was not overwritten.');
740
+ // Reuse the existing workspace private-state lock helper. The install lock
741
+ // schema is intentionally unchanged; the operation label is its existing
742
+ // local-configuration value while this dedicated lock path carries the intent.
743
+ let acquired = await acquireInstallLock(lockPath, 'set-auto-resume', indexFile(root), root);
744
+ const deadline = Date.now() + 2_000;
745
+ while (!acquired.ok &&
746
+ acquired.status === 'locked' &&
747
+ Date.now() < deadline) {
748
+ await new Promise((resolve) => setTimeout(resolve, 25));
749
+ acquired = await acquireInstallLock(lockPath, 'set-auto-resume', indexFile(root), root);
750
+ }
751
+ if (!acquired.ok)
752
+ return failure(acquired.status === 'locked' ? 'bindings_locked' : 'bindings_invalid', acquired.message ?? 'The binding lock could not be acquired.', 'Inspect the lock; it is never reclaimed by age.');
753
+ try {
754
+ return await task();
755
+ }
756
+ finally {
757
+ await releaseInstallLock(lockPath, acquired.lock.token);
758
+ }
759
+ }
760
+ async function writeIndex(root, table) {
761
+ const document = tableJson(table);
762
+ const bytes = Buffer.from(`${JSON.stringify(document, null, 2)}\n`, 'utf8');
763
+ if (bytes.byteLength > MAX_INDEX_BYTES)
764
+ return failure('bindings_capacity', `The collaboration bindings index would exceed its ${MAX_INDEX_BYTES}-byte publication limit.`, 'Review an explicit bounded index-size plan before retrying; the index was not replaced and no entries were discarded.', [`serialized_bytes=${bytes.byteLength}`, `max_bytes=${MAX_INDEX_BYTES}`]);
765
+ const written = await writeBytesAtomic(indexFile(root), bytes, root);
766
+ if (!written.ok)
767
+ return failure('bindings_write_failed', 'The collaboration bindings index could not be written atomically.', 'Inspect .pappra/local and permissions; the previous index is preserved if it was valid.');
768
+ return { ok: true, value: null };
769
+ }
770
+ function bindingForTarget(alias, target, boundAt) {
771
+ return {
772
+ alias,
773
+ record: target.record,
774
+ record_realpath: target.record_realpath,
775
+ task_root: target.task_root,
776
+ domain_root: target.domain_root,
777
+ protocol_task_id: target.protocol_task_id,
778
+ domain_id: target.domain_id,
779
+ record_created_at: target.record_created_at,
780
+ bound_at: boundAt,
781
+ };
782
+ }
783
+ export async function bindAlias(options) {
784
+ if (!safeAlias(options.taskId))
785
+ return failure('invalid_options', '--task-id must be a safe collaboration alias of 128 characters or fewer.', 'Use a single path segment of letters, digits, dot, dash or underscore.');
786
+ if (!absoluteText(options.record) ||
787
+ path.basename(options.record) !== 'task.json')
788
+ return failure('invalid_options', '--record must be an absolute <task_root>/task.json path.', 'Pass the authoritative record path.');
789
+ const recordPath = options.record;
790
+ const workspace = await selectWorkspace(options.root);
791
+ if (!workspace.ok)
792
+ return workspace;
793
+ const initial = await readIndex(workspace.value.root, workspace.value.workspace_id);
794
+ if (!initial.ok)
795
+ return initial;
796
+ const target = await inspectTarget(recordPath);
797
+ if (!target.ok)
798
+ return target;
799
+ const alias = options.taskId;
800
+ const existing = own(initial.value.table.bindings, alias)
801
+ ? initial.value.table.bindings[alias]
802
+ : null;
803
+ const targetIdentity = target.value.target;
804
+ const conflict = existing !== null && !bindingMatches(existing, targetIdentity);
805
+ if (conflict)
806
+ return failure('binding_conflict', 'An existing binding has a different or changed target identity.', 'Unbind the alias explicitly before binding a replacement.');
807
+ const wouldChange = existing === null || existing.record !== recordPath;
808
+ if (!options.apply)
809
+ return {
810
+ ok: true,
811
+ value: {
812
+ schema: COLLAB_BINDING_RESULT_SCHEMA,
813
+ ok: true,
814
+ status: 'planned',
815
+ action: 'bind',
816
+ applied: false,
817
+ workspace_id: workspace.value.workspace_id,
818
+ workspace_root: workspace.value.root,
819
+ index_path: indexFile(workspace.value.root),
820
+ alias,
821
+ target: targetIdentity,
822
+ would_change: wouldChange,
823
+ limits: ['plan_validates_alias_index_and_target_only', 'no_write'],
824
+ },
825
+ };
826
+ const applied = await withBindingLock(workspace.value.root, async () => {
827
+ const index = await readIndex(workspace.value.root, workspace.value.workspace_id);
828
+ if (!index.ok)
829
+ return index;
830
+ const freshTarget = await inspectTarget(recordPath);
831
+ if (!freshTarget.ok)
832
+ return freshTarget;
833
+ if (!sameTargetIdentity(targetIdentity, freshTarget.value.target))
834
+ return failure('binding_identity_changed', 'The target immutable identity changed between initial inspection and apply.', 'Re-inspect the task and explicitly bind the intended incarnation; the index was not written.', ['target_identity_changed']);
835
+ const current = own(index.value.table.bindings, alias)
836
+ ? index.value.table.bindings[alias]
837
+ : null;
838
+ if (current && !bindingMatches(current, freshTarget.value.target))
839
+ return failure('binding_conflict', 'The target identity changed before the binding could be applied.', 'Unbind the alias explicitly before binding a replacement.');
840
+ if (current && current.record === recordPath)
841
+ return {
842
+ ok: true,
843
+ value: {
844
+ schema: COLLAB_BINDING_RESULT_SCHEMA,
845
+ ok: true,
846
+ status: 'unchanged',
847
+ action: 'bind',
848
+ applied: false,
849
+ workspace_id: workspace.value.workspace_id,
850
+ workspace_root: workspace.value.root,
851
+ index_path: indexFile(workspace.value.root),
852
+ alias,
853
+ target: freshTarget.value.target,
854
+ limits: ['pointer_unchanged'],
855
+ },
856
+ };
857
+ const table = {
858
+ ...index.value.table,
859
+ bindings: { ...index.value.table.bindings },
860
+ unbound: { ...index.value.table.unbound },
861
+ };
862
+ table.bindings[alias] = bindingForTarget(alias, freshTarget.value.target, new Date().toISOString());
863
+ delete table.unbound[alias];
864
+ const written = await writeIndex(workspace.value.root, table);
865
+ if (!written.ok)
866
+ return written;
867
+ return {
868
+ ok: true,
869
+ value: {
870
+ schema: COLLAB_BINDING_RESULT_SCHEMA,
871
+ ok: true,
872
+ status: 'bound',
873
+ action: 'bind',
874
+ applied: true,
875
+ workspace_id: workspace.value.workspace_id,
876
+ workspace_root: workspace.value.root,
877
+ index_path: indexFile(workspace.value.root),
878
+ alias,
879
+ target: freshTarget.value.target,
880
+ limits: ['pointer_metadata_only'],
881
+ },
882
+ };
883
+ });
884
+ return applied;
885
+ }
886
+ export async function unbindAlias(options) {
887
+ if (!safeAlias(options.taskId))
888
+ return failure('invalid_options', '--task-id must be a safe collaboration alias of 128 characters or fewer.', 'Use a single path segment of letters, digits, dot, dash or underscore.');
889
+ const workspace = await selectWorkspace(options.root);
890
+ if (!workspace.ok)
891
+ return workspace;
892
+ const initial = await readIndex(workspace.value.root, workspace.value.workspace_id);
893
+ if (!initial.ok)
894
+ return initial;
895
+ const alias = options.taskId;
896
+ const existing = own(initial.value.table.bindings, alias)
897
+ ? initial.value.table.bindings[alias]
898
+ : null;
899
+ const already = own(initial.value.table.unbound, alias);
900
+ if (!options.apply)
901
+ return {
902
+ ok: true,
903
+ value: {
904
+ schema: COLLAB_BINDING_RESULT_SCHEMA,
905
+ ok: true,
906
+ status: 'planned',
907
+ action: 'unbind',
908
+ applied: false,
909
+ workspace_id: workspace.value.workspace_id,
910
+ workspace_root: workspace.value.root,
911
+ index_path: indexFile(workspace.value.root),
912
+ alias,
913
+ would_remove: existing !== null,
914
+ would_suppress: existing !== null || !already,
915
+ already_unbound: already,
916
+ limits: ['plan_validates_alias_and_index_only', 'no_write'],
917
+ },
918
+ };
919
+ const applied = await withBindingLock(workspace.value.root, async () => {
920
+ const index = await readIndex(workspace.value.root, workspace.value.workspace_id);
921
+ if (!index.ok)
922
+ return index;
923
+ const current = own(index.value.table.bindings, alias)
924
+ ? index.value.table.bindings[alias]
925
+ : null;
926
+ const alreadyNow = own(index.value.table.unbound, alias);
927
+ if (!current) {
928
+ if (alreadyNow)
929
+ return {
930
+ ok: true,
931
+ value: {
932
+ schema: COLLAB_BINDING_RESULT_SCHEMA,
933
+ ok: true,
934
+ status: 'already-unbound',
935
+ action: 'unbind',
936
+ applied: false,
937
+ workspace_id: workspace.value.workspace_id,
938
+ workspace_root: workspace.value.root,
939
+ index_path: indexFile(workspace.value.root),
940
+ alias,
941
+ limits: ['pointer_unchanged'],
942
+ },
943
+ };
944
+ const table = {
945
+ ...index.value.table,
946
+ bindings: { ...index.value.table.bindings },
947
+ unbound: { ...index.value.table.unbound },
948
+ };
949
+ table.unbound[alias] = {
950
+ alias,
951
+ at: new Date().toISOString(),
952
+ previous: null,
953
+ };
954
+ const written = await writeIndex(workspace.value.root, table);
955
+ if (!written.ok)
956
+ return written;
957
+ return {
958
+ ok: true,
959
+ value: {
960
+ schema: COLLAB_BINDING_RESULT_SCHEMA,
961
+ ok: true,
962
+ status: 'unbound',
963
+ action: 'unbind',
964
+ applied: true,
965
+ workspace_id: workspace.value.workspace_id,
966
+ workspace_root: workspace.value.root,
967
+ index_path: indexFile(workspace.value.root),
968
+ alias,
969
+ previous: null,
970
+ limits: ['tombstone_only'],
971
+ },
972
+ };
973
+ }
974
+ const table = {
975
+ ...index.value.table,
976
+ bindings: { ...index.value.table.bindings },
977
+ unbound: { ...index.value.table.unbound },
978
+ };
979
+ delete table.bindings[alias];
980
+ table.unbound[alias] = {
981
+ alias,
982
+ at: new Date().toISOString(),
983
+ previous: current,
984
+ };
985
+ const written = await writeIndex(workspace.value.root, table);
986
+ if (!written.ok)
987
+ return written;
988
+ return {
989
+ ok: true,
990
+ value: {
991
+ schema: COLLAB_BINDING_RESULT_SCHEMA,
992
+ ok: true,
993
+ status: 'unbound',
994
+ action: 'unbind',
995
+ applied: true,
996
+ workspace_id: workspace.value.workspace_id,
997
+ workspace_root: workspace.value.root,
998
+ index_path: indexFile(workspace.value.root),
999
+ alias,
1000
+ previous: pinnedIdentity(current),
1001
+ limits: ['tombstone_metadata_only'],
1002
+ },
1003
+ };
1004
+ });
1005
+ return applied;
1006
+ }