@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/app/init.js CHANGED
@@ -1,23 +1,25 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { basename, dirname, join, resolve } from 'node:path';
3
- import { mkdir, open, readFile, rename, rm, rmdir, writeFile, } from 'node:fs/promises';
3
+ import { mkdir, open, readFile, readdir, rename, rm, rmdir, writeFile, } from 'node:fs/promises';
4
4
  import { createDiagnostic, hasErrors, pointer, } from '../schemas/diagnostics.js';
5
5
  import { diagnosticCodes } from '../schemas/errors.js';
6
6
  import { INIT_CHOICES_SCHEMA, initPlanSchema, parseInitChoices, } from '../schemas/init.js';
7
7
  import { machineConfigSchema } from '../schemas/machine.js';
8
- import { isExternalRepositoryPath, isPathInside, isPortableRelativePath, normalizePortableRelativePath, resolvePortableRelativePath, samePath, } from '../schemas/paths.js';
8
+ import { isExternalRepositoryPath, isPathInside, isPortableRelativePath, normalizePortableRelativePath, resolvePortableRelativePath, samePath, toWorkspaceRelativePath, } from '../schemas/paths.js';
9
9
  import { WORKSPACE_SCHEMA, validateWorkspaceConfigSemantics, workspaceConfigSchema, } from '../schemas/workspace.js';
10
10
  import { getPackageInfo, getPackageRoot } from '../package-info.js';
11
11
  import { spawnSync } from 'node:child_process';
12
12
  import { executableOnPath } from '../hosts/detect.js';
13
13
  import { HOST_SUPPORT } from '../hosts/integration/support.js';
14
14
  import { observePath, resolveRealpath, sha256Digest } from '../infra/fs.js';
15
+ import { inspectGitRepository, } from '../infra/git.js';
15
16
  import { inspectMachineConfig, inspectWorkspaceConfig, } from '../workspace/config.js';
16
17
  import { discoverWorkspace, } from '../workspace/discover.js';
18
+ import { isExcludedPath } from '../workspace/ignore.js';
17
19
  import { resolveWorkspaceRoot } from '../workspace/resolve.js';
18
20
  import { buildInitPlan } from '../install/plan.js';
19
- import { backupPath, ensureStatePathBoundary, readWorkspaceManifest, writeWorkspaceManifest, } from '../install/ownership.js';
20
- import { beginManagedApply, completeManagedApply, failManagedApply, recordManagedFile, updateMachineRuntime, } from '../install/apply.js';
21
+ import { backupPath, ensureStatePathBoundary, writeBytesAtomic, writeWorkspaceManifest, } from '../install/ownership.js';
22
+ import { beginManagedApply, completeManagedApply, failManagedApply, inspectWorkspaceInstallReadiness, recordManagedFile, updateMachineRuntime, verifyWorkspaceInstallReady, } from '../install/apply.js';
21
23
  import { installRuntime, } from '../install/runtime.js';
22
24
  const workspaceConfigRelativePath = '.pappra/workspace.json';
23
25
  const machineConfigRelativePath = '.pappra/local/machine.json';
@@ -96,6 +98,33 @@ function candidateKey(value) {
96
98
  export async function resolveInitRoot(rootPath) {
97
99
  const requestedRoot = resolve(rootPath);
98
100
  const resolution = await resolveWorkspaceRoot({ startPath: requestedRoot });
101
+ if (resolution.state === 'incomplete' && resolution.root !== null) {
102
+ const canonicalRequested = await resolveRealpath(requestedRoot);
103
+ const marker = join(resolution.root, '.pappra');
104
+ const local = join(marker, 'local');
105
+ const markerObservation = await observePath(marker);
106
+ const localObservation = await observePath(local);
107
+ const configObservation = await observePath(join(marker, 'workspace.json'));
108
+ if (canonicalRequested !== null &&
109
+ samePath(canonicalRequested, resolution.root) &&
110
+ markerObservation.kind === 'directory' &&
111
+ configObservation.kind === 'missing' &&
112
+ ['missing', 'directory'].includes(localObservation.kind) &&
113
+ (await readdir(marker)).every((name) => name === 'local')) {
114
+ const ready = await inspectWorkspaceInstallReadiness(resolution.root);
115
+ if (!ready.ok)
116
+ return failure('unavailable', ready.code, ready.message, [], ready.nextAction);
117
+ return {
118
+ ok: true,
119
+ value: {
120
+ requestedRoot,
121
+ root: resolution.root,
122
+ registered: false,
123
+ diagnostics: resolution.diagnostics.filter((entry) => entry.code !== diagnosticCodes.workspaceRegistrationIncomplete),
124
+ },
125
+ };
126
+ }
127
+ }
99
128
  if (resolution.state === 'unavailable' || resolution.state === 'incomplete') {
100
129
  return failureFromDiagnostics('unavailable', resolution.diagnostics, diagnosticCodes.workspaceStartInvalid, `Cannot initialize from ${requestedRoot}.`);
101
130
  }
@@ -145,6 +174,9 @@ export async function inspectInitContext(rootPath) {
145
174
  if (!rootResult.ok)
146
175
  return rootResult;
147
176
  const root = rootResult.value.root;
177
+ const installReady = await inspectWorkspaceInstallReadiness(root);
178
+ if (!installReady.ok)
179
+ return failure('unavailable', installReady.code, installReady.message, [], installReady.nextAction);
148
180
  const rootIdentity = await observeRootIdentity(root);
149
181
  if (rootIdentity === null) {
150
182
  return failure('unavailable', diagnosticCodes.workspaceRootInvalid, `Workspace root is no longer a directory: ${root}`, rootResult.value.diagnostics, 'Retry init from an existing workspace root; no plan was produced.');
@@ -252,6 +284,327 @@ const derivedExcludes = [
252
284
  'test-results',
253
285
  ];
254
286
  const derivedValidationScripts = ['lint', 'typecheck', 'test'];
287
+ /**
288
+ * Pure discovery-based selections shared by derived and interactive setup.
289
+ * Mode-specific repositories, validation, graph and host choices stay with
290
+ * their callers.
291
+ */
292
+ export function deriveCommonInitDefaults(discovery) {
293
+ const knowledgeCandidates = listKnowledgeCandidates(discovery);
294
+ const knowledgeSources = [];
295
+ for (const name of ['AGENTS.md', 'CLAUDE.md']) {
296
+ const candidate = knowledgeCandidates.find((entry) => entry.path.toLowerCase() === name.toLowerCase());
297
+ if (candidate !== undefined)
298
+ knowledgeSources.push(candidate.path);
299
+ }
300
+ const docs = knowledgeCandidates.find((entry) => entry.path === 'docs');
301
+ if (docs !== undefined)
302
+ knowledgeSources.push(docs.path);
303
+ return {
304
+ name: basename(discovery.root),
305
+ knowledge: {
306
+ sources: knowledgeSources,
307
+ newDocuments: '.pappra/knowledge',
308
+ taskVisibility: 'private',
309
+ },
310
+ code: { symbols: 'auto', prepare: 'on-demand' },
311
+ excludes: [...derivedExcludes],
312
+ };
313
+ }
314
+ /**
315
+ * Converts an existing valid workspace/machine pair into a lossless editable
316
+ * selection draft. Extensions and continuity are intentionally not part of
317
+ * InitChoices; plan reconstruction preserves them from the existing config.
318
+ */
319
+ export function existingInitChoices(config, machine) {
320
+ return {
321
+ schema: INIT_CHOICES_SCHEMA,
322
+ name: config.name,
323
+ workspaceId: config.workspaceId,
324
+ repositories: config.repositories.map((repository) => ({
325
+ path: repository.path,
326
+ id: repository.id,
327
+ access: repository.access,
328
+ })),
329
+ knowledge: {
330
+ sources: [...config.knowledge.sources],
331
+ newDocuments: config.knowledge.newDocuments,
332
+ taskVisibility: config.knowledge.taskVisibility,
333
+ },
334
+ code: { ...config.code },
335
+ validation: config.validation.map((validation) => `workspace-config:${validation.id}`),
336
+ excludes: [...config.excludes],
337
+ hosts: [...(machine?.selectedHosts ?? [])],
338
+ };
339
+ }
340
+ function portableFieldError(label, value) {
341
+ if (value.includes('\0') || value.includes('\\')) {
342
+ return `${label} must use forward slashes and must not contain NUL or backslashes.`;
343
+ }
344
+ if (value.startsWith('/') ||
345
+ /^[A-Za-z]:/.test(value) ||
346
+ value.startsWith('\\\\')) {
347
+ return `${label} must be relative to the workspace root, not absolute.`;
348
+ }
349
+ if (!isPortableRelativePath(value)) {
350
+ return `${label} must be a workspace-relative path that does not escape the root.`;
351
+ }
352
+ return null;
353
+ }
354
+ /**
355
+ * Immediate UI field feedback using the same name schema, portable-path rules,
356
+ * literal-prefix exclusion policy and workspace-relative semantics as planning.
357
+ */
358
+ export function validateInitField(field, value) {
359
+ if (field === 'name') {
360
+ const parsed = workspaceConfigSchema.shape.name.safeParse(value.trim());
361
+ return parsed.success
362
+ ? null
363
+ : (parsed.error.issues[0]?.message ?? 'Use a non-empty workspace name.');
364
+ }
365
+ if (field === 'newDocuments') {
366
+ const parsed = workspaceConfigSchema.shape.knowledge.shape.newDocuments.safeParse(value);
367
+ if (!parsed.success) {
368
+ return (parsed.error.issues[0]?.message ??
369
+ 'Use a workspace-relative new document destination.');
370
+ }
371
+ return portableFieldError('New document destination', value);
372
+ }
373
+ if (field === 'repositoryPath') {
374
+ return portableFieldError('Repository path', value);
375
+ }
376
+ if (field === 'knowledgePath') {
377
+ return portableFieldError('Knowledge source path', value);
378
+ }
379
+ for (const entry of value
380
+ .split(/[,\n]/)
381
+ .map((item) => item.trim())
382
+ .filter((item) => item !== '')) {
383
+ const pathError = portableFieldError('Exclude entry', entry);
384
+ if (pathError !== null)
385
+ return pathError;
386
+ if (/[*?[\]{}]/.test(entry)) {
387
+ return `Exclude entry "${entry}" contains glob metacharacters; use literal workspace-relative prefixes.`;
388
+ }
389
+ }
390
+ return null;
391
+ }
392
+ async function nestedWorkspaceBoundaryForPath(root, path, kind) {
393
+ let current = kind === 'directory' ? path : dirname(path);
394
+ while (!samePath(current, root)) {
395
+ if (!isPathInside(root, current))
396
+ return null;
397
+ const marker = await observePath(join(current, '.pappra'));
398
+ if (marker.kind !== 'missing')
399
+ return current;
400
+ const parent = dirname(current);
401
+ if (samePath(parent, current) || !isPathInside(root, parent))
402
+ return null;
403
+ current = parent;
404
+ }
405
+ return null;
406
+ }
407
+ function explicitPathDiagnostic(code, message, location, hint) {
408
+ return createDiagnostic({
409
+ code,
410
+ severity: 'error',
411
+ message,
412
+ source: 'init explicit path inspection',
413
+ pointer: location,
414
+ hint,
415
+ });
416
+ }
417
+ /** Inspects one user-entered path only; it never broadens workspace discovery. */
418
+ export async function inspectInitPathAddition(root, kind, inputPath, excludes = [], location = null) {
419
+ const label = kind === 'repository' ? 'Repository path' : 'Knowledge source path';
420
+ const pathError = portableFieldError(label, inputPath);
421
+ if (pathError !== null) {
422
+ return {
423
+ ok: false,
424
+ diagnostics: [
425
+ explicitPathDiagnostic(diagnosticCodes.initInvalidInput, pathError, location, 'Enter an existing workspace-relative path using forward slashes; absolute and escaping paths are not accepted.'),
426
+ ],
427
+ };
428
+ }
429
+ if (inputPath === '~' || inputPath.startsWith('~/')) {
430
+ return {
431
+ ok: false,
432
+ diagnostics: [
433
+ explicitPathDiagnostic(diagnosticCodes.initInvalidInput, `${label} must not use a home-relative path: ${inputPath}`, location, 'Enter a path relative to the resolved workspace root.'),
434
+ ],
435
+ };
436
+ }
437
+ if (kind === 'repository' && isExternalRepositoryPath(inputPath)) {
438
+ return {
439
+ ok: false,
440
+ diagnostics: [
441
+ explicitPathDiagnostic(diagnosticCodes.initInvalidInput, 'Explicit repository addition does not create external repository mappings.', location, 'Add an existing checkout located inside the workspace root.'),
442
+ ],
443
+ };
444
+ }
445
+ const requestedPath = normalizePortableRelativePath(inputPath);
446
+ const candidatePath = resolvePortableRelativePath(root, requestedPath);
447
+ if (!isPathInside(root, candidatePath)) {
448
+ return {
449
+ ok: false,
450
+ diagnostics: [
451
+ explicitPathDiagnostic(diagnosticCodes.initInvalidInput, `${label} escapes the workspace root: ${inputPath}`, location, 'Keep the path within the resolved workspace root.'),
452
+ ],
453
+ };
454
+ }
455
+ try {
456
+ const initial = await observePath(candidatePath);
457
+ if (initial.kind === 'missing') {
458
+ return {
459
+ ok: false,
460
+ diagnostics: [
461
+ explicitPathDiagnostic(kind === 'repository'
462
+ ? diagnosticCodes.repositoryPathMissing
463
+ : diagnosticCodes.knowledgeSourceMissing, `${label} does not exist: ${candidatePath}`, location, 'Correct the path or cancel the addition; init does not create the location.'),
464
+ ],
465
+ };
466
+ }
467
+ const realPath = await resolveRealpath(candidatePath);
468
+ if (realPath === null) {
469
+ return {
470
+ ok: false,
471
+ diagnostics: [
472
+ explicitPathDiagnostic(kind === 'repository'
473
+ ? diagnosticCodes.repositoryPathMissing
474
+ : diagnosticCodes.knowledgeSourceNotReadable, `Cannot resolve ${label.toLowerCase()}: ${candidatePath}`, location, 'Check the entered path and permissions.'),
475
+ ],
476
+ };
477
+ }
478
+ if (!isPathInside(root, realPath)) {
479
+ return {
480
+ ok: false,
481
+ diagnostics: [
482
+ explicitPathDiagnostic(kind === 'repository'
483
+ ? diagnosticCodes.repositorySymlinkEscape
484
+ : diagnosticCodes.knowledgeSourceSymlinkEscape, `${label} resolves outside the workspace root: ${candidatePath} -> ${realPath}`, location, 'Keep the resolved target inside the workspace; explicit additions do not create external mappings.'),
485
+ ],
486
+ };
487
+ }
488
+ const target = await observePath(realPath);
489
+ if (target.kind !== 'file' && target.kind !== 'directory') {
490
+ return {
491
+ ok: false,
492
+ diagnostics: [
493
+ explicitPathDiagnostic(kind === 'repository'
494
+ ? diagnosticCodes.repositoryNotGit
495
+ : diagnosticCodes.knowledgeSourceNotReadable, `${label} is not an existing regular file or directory: ${realPath}`, location, 'Special files, FIFOs and unreadable locations are not accepted.'),
496
+ ],
497
+ };
498
+ }
499
+ if (kind === 'repository' && target.kind !== 'directory') {
500
+ return {
501
+ ok: false,
502
+ diagnostics: [
503
+ explicitPathDiagnostic(diagnosticCodes.repositoryNotGit, `Repository path is not a checkout directory: ${realPath}`, location, 'Enter the exact existing Git checkout root, not a file or a subdirectory.'),
504
+ ],
505
+ };
506
+ }
507
+ const nested = await nestedWorkspaceBoundaryForPath(root, realPath, target.kind);
508
+ if (nested !== null) {
509
+ return {
510
+ ok: false,
511
+ diagnostics: [
512
+ explicitPathDiagnostic(kind === 'repository'
513
+ ? diagnosticCodes.repositoryNestedWorkspace
514
+ : diagnosticCodes.discoveryNestedWorkspace, `${label} is inside nested workspace ${nested}.`, location, 'Configure the nested workspace separately; outer init does not absorb its paths.'),
515
+ ],
516
+ };
517
+ }
518
+ const canonicalPath = toWorkspaceRelativePath(root, realPath);
519
+ if (canonicalPath === null) {
520
+ return {
521
+ ok: false,
522
+ diagnostics: [
523
+ explicitPathDiagnostic(kind === 'repository'
524
+ ? diagnosticCodes.repositorySymlinkEscape
525
+ : diagnosticCodes.knowledgeSourceSymlinkEscape, `${label} has no safe workspace-relative identity: ${realPath}`, location, 'Keep the resolved target inside the workspace root.'),
526
+ ],
527
+ };
528
+ }
529
+ if (isExcludedPath(canonicalPath, excludes)) {
530
+ const label = kind === 'repository' ? 'Repository' : 'Knowledge source';
531
+ return {
532
+ ok: false,
533
+ diagnostics: [
534
+ explicitPathDiagnostic(kind === 'repository'
535
+ ? diagnosticCodes.initRepositoryUnknown
536
+ : diagnosticCodes.initKnowledgeUnknown, `${label} is excluded by workspace policy: ${canonicalPath}`, location, 'Remove or change the exclude before adding this path.'),
537
+ ],
538
+ };
539
+ }
540
+ let git = null;
541
+ if (kind === 'repository') {
542
+ const inspected = await inspectGitRepository(realPath);
543
+ if (!inspected.ok) {
544
+ return {
545
+ ok: false,
546
+ diagnostics: [
547
+ createDiagnostic({
548
+ code: inspected.code,
549
+ severity: 'error',
550
+ message: inspected.message,
551
+ source: 'init explicit repository path',
552
+ pointer: location,
553
+ hint: inspected.nextAction ??
554
+ 'Check the exact repository path and Git metadata.',
555
+ }),
556
+ ],
557
+ };
558
+ }
559
+ if (!samePath(inspected.value.root, realPath)) {
560
+ return {
561
+ ok: false,
562
+ diagnostics: [
563
+ explicitPathDiagnostic(diagnosticCodes.repositoryNotGit, `Entered location is not the exact repository root: ${realPath}; Git resolved ${inspected.value.root}.`, location, 'Enter the checkout root itself; init does not substitute a parent repository.'),
564
+ ],
565
+ };
566
+ }
567
+ if (!isPathInside(root, inspected.value.root)) {
568
+ return {
569
+ ok: false,
570
+ diagnostics: [
571
+ explicitPathDiagnostic(diagnosticCodes.repositorySymlinkEscape, `Repository resolves outside the workspace root: ${inspected.value.root}`, location, 'Use a workspace-local checkout; external mapping creation is not part of init path addition.'),
572
+ ],
573
+ };
574
+ }
575
+ git = inspected.value;
576
+ }
577
+ if (target.stats === null) {
578
+ return {
579
+ ok: false,
580
+ diagnostics: [
581
+ explicitPathDiagnostic(diagnosticCodes.fileUnreadable, `Cannot snapshot ${label.toLowerCase()}: ${realPath}`, location, 'Check the path and permissions, then try the explicit addition again.'),
582
+ ],
583
+ };
584
+ }
585
+ return {
586
+ ok: true,
587
+ observation: {
588
+ kind,
589
+ requestedPath,
590
+ path: canonicalPath,
591
+ realPath,
592
+ targetKind: target.kind,
593
+ device: target.stats.dev,
594
+ inode: target.stats.ino,
595
+ git,
596
+ },
597
+ };
598
+ }
599
+ catch (error) {
600
+ return {
601
+ ok: false,
602
+ diagnostics: [
603
+ explicitPathDiagnostic(diagnosticCodes.fileUnreadable, `Cannot inspect ${label.toLowerCase()} ${candidatePath}: ${error instanceof Error ? error.message : String(error)}`, location, 'Check permissions and the path; init does not open document bodies during path addition.'),
604
+ ],
605
+ };
606
+ }
607
+ }
255
608
  export function detectedHostVersion(command, pathEnv) {
256
609
  const executable = executableOnPath(command, pathEnv);
257
610
  if (executable === null)
@@ -290,27 +643,15 @@ export async function deriveInitChoices(discovery, options = {}) {
290
643
  });
291
644
  return { path, access };
292
645
  });
646
+ const common = deriveCommonInitDefaults(discovery);
293
647
  const knowledgeCandidates = listKnowledgeCandidates(discovery);
294
- const rootRuleFiles = ['AGENTS.md', 'CLAUDE.md'];
295
- const knowledgeSources = [];
296
- for (const name of rootRuleFiles) {
297
- const candidate = knowledgeCandidates.find((entry) => entry.path.toLowerCase() === name.toLowerCase());
298
- if (candidate === undefined)
299
- continue;
300
- knowledgeSources.push(candidate.path);
648
+ for (const source of common.knowledge.sources) {
301
649
  rules.push({
302
650
  field: 'knowledge.sources',
303
- value: candidate.path,
304
- reason: 'workspace-root rule file reported by discovery',
305
- });
306
- }
307
- const docs = knowledgeCandidates.find((entry) => entry.path === 'docs');
308
- if (docs !== undefined) {
309
- knowledgeSources.push(docs.path);
310
- rules.push({
311
- field: 'knowledge.sources',
312
- value: docs.path,
313
- reason: 'documentation directory reported by discovery',
651
+ value: source,
652
+ reason: source === 'docs'
653
+ ? 'documentation directory reported by discovery'
654
+ : 'workspace-root rule file reported by discovery',
314
655
  });
315
656
  }
316
657
  const codegraphDetected = executableOnPath('codegraph', pathEnv) !== null &&
@@ -325,12 +666,12 @@ export async function deriveInitChoices(discovery, options = {}) {
325
666
  });
326
667
  rules.push({
327
668
  field: 'code.symbols',
328
- value: 'auto',
669
+ value: common.code.symbols,
329
670
  reason: 'symbol providers are detected passively at use time',
330
671
  });
331
672
  rules.push({
332
673
  field: 'code.prepare',
333
- value: 'on-demand',
674
+ value: common.code.prepare,
334
675
  reason: 'index preparation stays an explicit, bounded action',
335
676
  });
336
677
  const validation = [];
@@ -347,7 +688,7 @@ export async function deriveInitChoices(discovery, options = {}) {
347
688
  }
348
689
  rules.push({
349
690
  field: 'excludes',
350
- value: derivedExcludes.join(', '),
691
+ value: common.excludes.join(', '),
351
692
  reason: 'built-in defaults keep generated and dependency trees out of scope',
352
693
  });
353
694
  const hosts = [];
@@ -371,7 +712,7 @@ export async function deriveInitChoices(discovery, options = {}) {
371
712
  reason: `host executable reports the pinned version ${version}`,
372
713
  });
373
714
  }
374
- const name = basename(discovery.root);
715
+ const name = common.name;
375
716
  rules.push({
376
717
  field: 'name',
377
718
  value: name,
@@ -381,14 +722,14 @@ export async function deriveInitChoices(discovery, options = {}) {
381
722
  schema: INIT_CHOICES_SCHEMA,
382
723
  name,
383
724
  repositories,
384
- knowledge: {
385
- sources: knowledgeSources,
386
- newDocuments: '.pappra/knowledge',
387
- taskVisibility: 'private',
725
+ knowledge: common.knowledge,
726
+ code: {
727
+ graph,
728
+ symbols: common.code.symbols,
729
+ prepare: common.code.prepare,
388
730
  },
389
- code: { graph, symbols: 'auto', prepare: 'on-demand' },
390
731
  validation,
391
- excludes: [...derivedExcludes],
732
+ excludes: [...common.excludes],
392
733
  hosts,
393
734
  };
394
735
  return {
@@ -402,7 +743,7 @@ export async function deriveInitChoices(discovery, options = {}) {
402
743
  },
403
744
  };
404
745
  }
405
- function selectRepositories(choices, discovery, root) {
746
+ function selectRepositories(choices, discovery, root, additionalRepositories = new Map(), reservedRepositoryIds = []) {
406
747
  const diagnostics = [];
407
748
  const byKey = new Map();
408
749
  for (const repository of discovery.repositories) {
@@ -411,6 +752,14 @@ function selectRepositories(choices, discovery, root) {
411
752
  byKey.set(normalizePortableRelativePath(repository.relativePath), repository);
412
753
  }
413
754
  }
755
+ for (const [key, repository] of additionalRepositories) {
756
+ if (!byKey.has(key))
757
+ byKey.set(key, repository);
758
+ }
759
+ const reservedIds = new Set([
760
+ ...reservedRepositoryIds,
761
+ ...choices.repositories.flatMap((choice) => choice.id === undefined ? [] : [choice.id]),
762
+ ]);
414
763
  const usedIds = new Set();
415
764
  const selected = [];
416
765
  const seenRepositories = new Set();
@@ -421,10 +770,10 @@ function selectRepositories(choices, discovery, root) {
421
770
  diagnostics.push(createDiagnostic({
422
771
  code: diagnosticCodes.initRepositoryUnknown,
423
772
  severity: 'error',
424
- message: `Init selection references an undiscovered repository: ${choice.path}`,
773
+ message: `Init selection references an undiscovered or uninspected repository: ${choice.path}`,
425
774
  source: 'init choices',
426
775
  pointer: pointer('repositories', index, 'path'),
427
- hint: 'Select a repository path reported by discovery; Pappra does not invent repository scopes.',
776
+ hint: 'Select a discovered checkout or add an existing workspace-relative checkout through the bounded path action.',
428
777
  }));
429
778
  continue;
430
779
  }
@@ -435,7 +784,7 @@ function selectRepositories(choices, discovery, root) {
435
784
  message: `Repository selection repeats ${repository.root}.`,
436
785
  source: 'init choices',
437
786
  pointer: pointer('repositories', index, 'path'),
438
- hint: 'Keep one selection per discovered checkout.',
787
+ hint: 'Keep one selection per canonical checkout.',
439
788
  }));
440
789
  continue;
441
790
  }
@@ -445,9 +794,8 @@ function selectRepositories(choices, discovery, root) {
445
794
  (repository.relativePath === null || repository.relativePath === '.'
446
795
  ? basename(root)
447
796
  : basename(repository.relativePath));
448
- const id = choice.id
449
- ? choice.id
450
- : uniqueIdentifier(sanitizeIdentifier(derivedBase), usedIds);
797
+ const id = choice.id ??
798
+ uniqueIdentifier(sanitizeIdentifier(derivedBase), new Set([...reservedIds, ...usedIds]));
451
799
  if (choice.id !== undefined) {
452
800
  if (usedIds.has(choice.id)) {
453
801
  diagnostics.push(createDiagnostic({
@@ -460,8 +808,8 @@ function selectRepositories(choices, discovery, root) {
460
808
  }));
461
809
  continue;
462
810
  }
463
- usedIds.add(choice.id);
464
811
  }
812
+ usedIds.add(id);
465
813
  const configPath = external
466
814
  ? repository.configuredPath
467
815
  : (repository.relativePath ?? repository.configuredPath ?? '.');
@@ -486,11 +834,16 @@ function selectRepositories(choices, discovery, root) {
486
834
  }
487
835
  return { selected, diagnostics };
488
836
  }
489
- function selectKnowledgeSources(choices, discovery) {
837
+ function selectKnowledgeSources(choices, discovery, retainedSources = [], explicitSources = new Map()) {
490
838
  const allowed = new Map(listKnowledgeCandidates(discovery).map((candidate) => [
491
839
  candidateKey(candidate.path),
492
840
  candidate.path,
493
841
  ]));
842
+ for (const [key, path] of explicitSources) {
843
+ if (!allowed.has(key))
844
+ allowed.set(key, path);
845
+ }
846
+ const retained = new Map(retainedSources.map((source) => [candidateKey(source), source]));
494
847
  const diagnostics = [];
495
848
  const sources = [];
496
849
  const seen = new Set();
@@ -498,6 +851,22 @@ function selectKnowledgeSources(choices, discovery) {
498
851
  const key = candidateKey(source);
499
852
  const canonical = allowed.get(key);
500
853
  if (canonical === undefined) {
854
+ const kept = retained.get(key);
855
+ if (kept !== undefined) {
856
+ diagnostics.push(createDiagnostic({
857
+ code: diagnosticCodes.initKnowledgeUnknown,
858
+ severity: 'warning',
859
+ message: `Configured knowledge source is retained but not currently discovered: ${kept}`,
860
+ source: 'init choices',
861
+ pointer: pointer('knowledge', 'sources', index),
862
+ hint: 'Keep the source if it is intentionally unavailable, or remove it explicitly.',
863
+ }));
864
+ if (!seen.has(kept)) {
865
+ seen.add(kept);
866
+ sources.push(kept);
867
+ }
868
+ continue;
869
+ }
501
870
  diagnostics.push(createDiagnostic({
502
871
  code: diagnosticCodes.initKnowledgeUnknown,
503
872
  severity: 'error',
@@ -515,13 +884,167 @@ function selectKnowledgeSources(choices, discovery) {
515
884
  }
516
885
  return { sources, diagnostics };
517
886
  }
887
+ function pathObservationKey(kind, path) {
888
+ return `${kind}:${candidateKey(path)}`;
889
+ }
890
+ function sameInitPathObservation(left, right) {
891
+ if (left.kind !== right.kind ||
892
+ candidateKey(left.path) !== candidateKey(right.path) ||
893
+ !samePath(left.realPath, right.realPath) ||
894
+ left.targetKind !== right.targetKind ||
895
+ left.device !== right.device ||
896
+ left.inode !== right.inode) {
897
+ return false;
898
+ }
899
+ if (left.git === null || right.git === null)
900
+ return left.git === right.git;
901
+ return (samePath(left.git.root, right.git.root) &&
902
+ samePath(left.git.gitDir, right.git.gitDir) &&
903
+ samePath(left.git.commonDir, right.git.commonDir) &&
904
+ left.git.kind === right.git.kind);
905
+ }
906
+ function repositorySelectionFromPathObservation(observation) {
907
+ if (observation.kind !== 'repository' || observation.git === null) {
908
+ throw new Error('Repository selection requires a Git path observation.');
909
+ }
910
+ return {
911
+ id: null,
912
+ external: false,
913
+ access: 'read',
914
+ root: observation.git.root,
915
+ relativePath: observation.path,
916
+ configuredPath: observation.path,
917
+ git: observation.git,
918
+ kind: observation.git.kind,
919
+ diagnostics: [],
920
+ };
921
+ }
922
+ async function inspectExplicitInitPathSelections(root, choices, discovery, preservedConfig, priorObservations) {
923
+ const repositories = new Map();
924
+ const knowledgeSources = new Map();
925
+ const observations = [];
926
+ const diagnostics = [];
927
+ const priorByPath = new Map();
928
+ for (const observation of priorObservations) {
929
+ const key = pathObservationKey(observation.kind, observation.path);
930
+ if (!priorByPath.has(key))
931
+ priorByPath.set(key, observation);
932
+ }
933
+ const discoveredRepositories = new Map();
934
+ for (const repository of discovery.repositories) {
935
+ discoveredRepositories.set(repositoryKey(repository), repository);
936
+ if (repository.relativePath !== null) {
937
+ discoveredRepositories.set(candidateKey(repository.relativePath), repository);
938
+ }
939
+ }
940
+ const discoveredKnowledge = new Map(listKnowledgeCandidates(discovery).map((candidate) => [
941
+ candidateKey(candidate.path),
942
+ candidate.path,
943
+ ]));
944
+ const retainedRepositoryPaths = new Set((preservedConfig?.repositories ?? []).map((repository) => candidateKey(repository.path)));
945
+ const retainedKnowledgePaths = new Set((preservedConfig?.knowledge.sources ?? []).map(candidateKey));
946
+ const inspectedChoices = new Set();
947
+ let failureStatus = 'invalid';
948
+ const inspectChoice = async (kind, inputPath, location) => {
949
+ const inputKey = candidateKey(inputPath);
950
+ const inspectionKey = pathObservationKey(kind, inputKey);
951
+ if (inspectedChoices.has(inspectionKey))
952
+ return;
953
+ inspectedChoices.add(inspectionKey);
954
+ const inspected = await inspectInitPathAddition(root, kind, inputPath, choices.excludes, location);
955
+ if (!inspected.ok) {
956
+ diagnostics.push(...inspected.diagnostics);
957
+ return;
958
+ }
959
+ const observation = inspected.observation;
960
+ const prior = priorByPath.get(pathObservationKey(kind, observation.path)) ??
961
+ priorByPath.get(inspectionKey);
962
+ if (prior !== undefined && !sameInitPathObservation(prior, observation)) {
963
+ failureStatus = 'conflict';
964
+ diagnostics.push(createDiagnostic({
965
+ code: diagnosticCodes.initPlanStale,
966
+ severity: 'error',
967
+ message: `Explicit ${kind} path changed after it was inspected: ${observation.path}`,
968
+ source: 'init explicit path inspection',
969
+ pointer: location,
970
+ hint: 'Review the changed location and re-enter it deliberately; init does not refresh path identity automatically.',
971
+ }));
972
+ return;
973
+ }
974
+ observations.push(observation);
975
+ if (kind === 'repository') {
976
+ const discovered = [...discovery.repositories].find((entry) => samePath(entry.root, observation.realPath));
977
+ const source = discovered ?? repositorySelectionFromPathObservation(observation);
978
+ repositories.set(inputKey, source);
979
+ repositories.set(candidateKey(observation.path), source);
980
+ }
981
+ else {
982
+ const canonical = discoveredKnowledge.get(candidateKey(observation.path)) ??
983
+ observation.path;
984
+ knowledgeSources.set(inputKey, canonical);
985
+ knowledgeSources.set(candidateKey(observation.path), canonical);
986
+ }
987
+ };
988
+ for (const [index, choice] of choices.repositories.entries()) {
989
+ const key = candidateKey(choice.path);
990
+ const hasPriorObservation = priorByPath.has(pathObservationKey('repository', key));
991
+ if (!hasPriorObservation &&
992
+ (retainedRepositoryPaths.has(key) ||
993
+ (discoveredRepositories.has(key) &&
994
+ !isExcludedPath(key, choices.excludes)))) {
995
+ continue;
996
+ }
997
+ await inspectChoice('repository', choice.path, pointer('repositories', index, 'path'));
998
+ }
999
+ for (const [index, source] of choices.knowledge.sources.entries()) {
1000
+ const key = candidateKey(source);
1001
+ const hasPriorObservation = priorByPath.has(pathObservationKey('knowledge', key));
1002
+ if (!hasPriorObservation &&
1003
+ (retainedKnowledgePaths.has(key) ||
1004
+ (discoveredKnowledge.has(key) &&
1005
+ !isExcludedPath(key, choices.excludes)))) {
1006
+ continue;
1007
+ }
1008
+ await inspectChoice('knowledge', source, pointer('knowledge', 'sources', index));
1009
+ }
1010
+ if (diagnostics.some((entry) => entry.severity === 'error')) {
1011
+ return { ok: false, status: failureStatus, diagnostics };
1012
+ }
1013
+ return {
1014
+ ok: true,
1015
+ value: { repositories, knowledgeSources, observations },
1016
+ };
1017
+ }
1018
+ async function revalidateExplicitInitPaths(root, observations, excludes) {
1019
+ const diagnostics = [];
1020
+ for (const previous of observations) {
1021
+ const current = await inspectInitPathAddition(root, previous.kind, previous.path, excludes);
1022
+ if (!current.ok ||
1023
+ !sameInitPathObservation(previous, current.observation)) {
1024
+ diagnostics.push(createDiagnostic({
1025
+ code: diagnosticCodes.initPlanStale,
1026
+ severity: 'error',
1027
+ message: `Explicit ${previous.kind} path changed or became unsafe before the final plan: ${previous.path}`,
1028
+ source: 'init explicit path inspection',
1029
+ pointer: null,
1030
+ hint: 'Review and re-enter the exact path; init does not refresh a stale path observation automatically.',
1031
+ }));
1032
+ }
1033
+ }
1034
+ return diagnostics;
1035
+ }
518
1036
  function findSelectedRepositoryForCandidate(candidate, selected) {
519
- for (const repository of selected) {
520
- if (candidate.repositoryId !== null &&
521
- (candidate.repositoryId === repository.originalId ||
522
- candidate.repositoryId === repository.id)) {
523
- return repository;
1037
+ if (candidate.repositoryId !== null) {
1038
+ for (const repository of selected) {
1039
+ if (repository.originalId === candidate.repositoryId)
1040
+ return repository;
524
1041
  }
1042
+ // A configured/discovered repository identity is authoritative. A
1043
+ // different checkout that merely reuses its final selected ID is not a
1044
+ // substitute, and working-directory evidence must not override it.
1045
+ return null;
1046
+ }
1047
+ for (const repository of selected) {
525
1048
  if (candidate.workingDirectory !== null &&
526
1049
  samePath(candidate.workingDirectory, repository.repository.root)) {
527
1050
  return repository;
@@ -529,7 +1052,7 @@ function findSelectedRepositoryForCandidate(candidate, selected) {
529
1052
  }
530
1053
  return null;
531
1054
  }
532
- function selectValidationEntries(choices, discovery, selected) {
1055
+ function selectValidationEntries(choices, discovery, selected, strictExistingBinding = false) {
533
1056
  const byId = new Map(discovery.validationCandidates.map((candidate) => [
534
1057
  candidate.id,
535
1058
  candidate,
@@ -537,6 +1060,12 @@ function selectValidationEntries(choices, discovery, selected) {
537
1060
  const diagnostics = [];
538
1061
  const validation = [];
539
1062
  const usedIds = new Set();
1063
+ const existingIds = new Set();
1064
+ for (const candidate of discovery.validationCandidates) {
1065
+ if (candidate.origin !== 'workspace-config')
1066
+ continue;
1067
+ existingIds.add(candidate.id.slice('workspace-config:'.length));
1068
+ }
540
1069
  const seen = new Set();
541
1070
  for (const [index, candidateId] of choices.validation.entries()) {
542
1071
  if (seen.has(candidateId))
@@ -558,7 +1087,9 @@ function selectValidationEntries(choices, discovery, selected) {
558
1087
  if (repository === null) {
559
1088
  diagnostics.push(createDiagnostic({
560
1089
  code: diagnosticCodes.initValidationUnbound,
561
- severity: 'warning',
1090
+ severity: strictExistingBinding && candidate.origin === 'workspace-config'
1091
+ ? 'error'
1092
+ : 'warning',
562
1093
  message: `Validation candidate "${candidateId}" is not bound to a selected repository.`,
563
1094
  source: 'init choices',
564
1095
  pointer: pointer('validation', index),
@@ -566,9 +1097,32 @@ function selectValidationEntries(choices, discovery, selected) {
566
1097
  }));
567
1098
  continue;
568
1099
  }
569
- const base = sanitizeIdentifier(candidate.id.replace(/^(?:workspace-config|package-script):/, ''));
1100
+ const existing = candidate.origin === 'workspace-config';
1101
+ const base = existing
1102
+ ? candidate.id.slice('workspace-config:'.length)
1103
+ : sanitizeIdentifier(candidate.id.replace(/^(?:workspace-config|package-script):/, ''));
1104
+ let id;
1105
+ if (existing) {
1106
+ if (usedIds.has(base)) {
1107
+ diagnostics.push(createDiagnostic({
1108
+ code: diagnosticCodes.initValidationUnknown,
1109
+ severity: 'error',
1110
+ message: `Configured validation id "${base}" is selected more than once.`,
1111
+ source: 'init choices',
1112
+ pointer: pointer('validation', index),
1113
+ hint: 'Keep one selection per configured validation id.',
1114
+ }));
1115
+ continue;
1116
+ }
1117
+ id = base;
1118
+ usedIds.add(id);
1119
+ }
1120
+ else {
1121
+ id = uniqueIdentifier(base, new Set([...usedIds, ...existingIds]));
1122
+ usedIds.add(id);
1123
+ }
570
1124
  validation.push({
571
- id: uniqueIdentifier(base, usedIds),
1125
+ id,
572
1126
  repositoryId: repository.id,
573
1127
  executable: candidate.executable,
574
1128
  args: [...candidate.args],
@@ -576,13 +1130,13 @@ function selectValidationEntries(choices, discovery, selected) {
576
1130
  }
577
1131
  return { validation, diagnostics };
578
1132
  }
579
- function buildWorkspaceConfig(root, choices, discovery, runtimeVersion) {
1133
+ function buildWorkspaceConfig(root, choices, discovery, runtimeVersion, options = {}) {
580
1134
  const diagnostics = [];
581
- const repositorySelection = selectRepositories(choices, discovery, root);
1135
+ const repositorySelection = selectRepositories(choices, discovery, root, options.explicitRepositories, options.preserveFrom?.repositories.map((repository) => repository.id) ?? []);
582
1136
  diagnostics.push(...repositorySelection.diagnostics);
583
- const knowledgeSelection = selectKnowledgeSources(choices, discovery);
1137
+ const knowledgeSelection = selectKnowledgeSources(choices, discovery, options.preserveFrom?.knowledge.sources ?? [], options.explicitKnowledgeSources);
584
1138
  diagnostics.push(...knowledgeSelection.diagnostics);
585
- const validationSelection = selectValidationEntries(choices, discovery, repositorySelection.selected);
1139
+ const validationSelection = selectValidationEntries(choices, discovery, repositorySelection.selected, options.strictExistingValidation === true);
586
1140
  diagnostics.push(...validationSelection.diagnostics);
587
1141
  if (hasErrors(diagnostics)) {
588
1142
  return {
@@ -594,7 +1148,7 @@ function buildWorkspaceConfig(root, choices, discovery, runtimeVersion) {
594
1148
  const name = choices.name?.trim() ?? '';
595
1149
  const config = {
596
1150
  schema: WORKSPACE_SCHEMA,
597
- workspaceId: choices.workspaceId ?? randomUUID(),
1151
+ workspaceId: options.preserveFrom?.workspaceId ?? choices.workspaceId ?? randomUUID(),
598
1152
  name: name === '' ? basename(root) || 'workspace' : name,
599
1153
  runtimeVersion,
600
1154
  repositories: repositorySelection.selected.map((repository) => ({
@@ -610,7 +1164,10 @@ function buildWorkspaceConfig(root, choices, discovery, runtimeVersion) {
610
1164
  code: choices.code,
611
1165
  validation: [...validationSelection.validation],
612
1166
  excludes: [...new Set(choices.excludes)],
613
- extensions: {},
1167
+ extensions: options.preserveFrom?.extensions ?? {},
1168
+ ...(options.preserveFrom?.continuity === undefined
1169
+ ? {}
1170
+ : { continuity: options.preserveFrom.continuity }),
614
1171
  };
615
1172
  const parsed = workspaceConfigSchema.safeParse(config);
616
1173
  if (!parsed.success) {
@@ -649,13 +1206,15 @@ function buildWorkspaceConfig(root, choices, discovery, runtimeVersion) {
649
1206
  diagnostics,
650
1207
  };
651
1208
  }
652
- function scopeRepositories(config, discovery) {
1209
+ function scopeRepositories(config, discovery, selected = []) {
653
1210
  const byConfiguredPath = new Map(discovery.repositories.map((repository) => [
654
1211
  repository.configuredPath,
655
1212
  repository,
656
1213
  ]));
1214
+ const bySelectedPath = new Map(selected.map((repository) => [candidateKey(repository.path), repository]));
657
1215
  return config.repositories.map((repository) => {
658
- const discovered = byConfiguredPath.get(repository.path);
1216
+ const planned = bySelectedPath.get(candidateKey(repository.path));
1217
+ const discovered = planned?.repository ?? byConfiguredPath.get(repository.path);
659
1218
  return {
660
1219
  id: repository.id,
661
1220
  path: repository.path,
@@ -664,7 +1223,9 @@ function scopeRepositories(config, discovery) {
664
1223
  root: discovered?.root ?? null,
665
1224
  kind: discovered?.kind ?? null,
666
1225
  dirty: discovered?.git.dirty ?? null,
667
- declaredBy: 'existing',
1226
+ declaredBy: planned?.originalId === null
1227
+ ? 'planned'
1228
+ : 'existing',
668
1229
  };
669
1230
  });
670
1231
  }
@@ -802,6 +1363,13 @@ export async function planWorkspaceInit(options) {
802
1363
  choices = parsedChoices.value;
803
1364
  }
804
1365
  const existingConfig = context.workspaceInspection.value;
1366
+ if (existingConfig !== null &&
1367
+ options.reconfigure === true &&
1368
+ choices !== null &&
1369
+ choices.workspaceId !== undefined &&
1370
+ choices.workspaceId !== existingConfig.workspaceId) {
1371
+ return failure('invalid', diagnosticCodes.initInvalidInput, 'Reconfiguration cannot change workspaceId; the existing workspace identity is stable.', context.diagnostics, 'Remove the conflicting workspaceId from the reconfiguration selections, or create a new workspace separately.');
1372
+ }
805
1373
  const existingMachine = context.machineInspection.value;
806
1374
  const diagnostics = [...context.diagnostics];
807
1375
  const runtimeVersion = options.runtimeVersion ??
@@ -814,6 +1382,11 @@ export async function planWorkspaceInit(options) {
814
1382
  let needWorkspaceConfig = false;
815
1383
  let needMachineConfig = false;
816
1384
  let selected = [];
1385
+ let explicitPathSelections = {
1386
+ repositories: new Map(),
1387
+ knowledgeSources: new Map(),
1388
+ observations: [],
1389
+ };
817
1390
  if (existingConfig !== null && options.reconfigure !== true) {
818
1391
  config = existingConfig;
819
1392
  if (choices !== null) {
@@ -839,7 +1412,19 @@ export async function planWorkspaceInit(options) {
839
1412
  const effectiveChoices = existingConfig !== null && choices.workspaceId === undefined
840
1413
  ? { ...choices, workspaceId: existingConfig.workspaceId }
841
1414
  : choices;
842
- const built = buildWorkspaceConfig(context.root, effectiveChoices, context.discovery, runtimeVersion);
1415
+ const explicitPaths = await inspectExplicitInitPathSelections(context.root, effectiveChoices, context.discovery, existingConfig, options.pathObservations ?? []);
1416
+ if (!explicitPaths.ok) {
1417
+ return failureFromDiagnostics(explicitPaths.status, explicitPaths.diagnostics, diagnosticCodes.initInvalidInput, 'Explicit init paths could not be inspected safely.');
1418
+ }
1419
+ explicitPathSelections = explicitPaths.value;
1420
+ const built = buildWorkspaceConfig(context.root, effectiveChoices, context.discovery, runtimeVersion, {
1421
+ ...(existingConfig === null ? {} : { preserveFrom: existingConfig }),
1422
+ ...(options.reconfigure === true
1423
+ ? { strictExistingValidation: true }
1424
+ : {}),
1425
+ explicitRepositories: explicitPaths.value.repositories,
1426
+ explicitKnowledgeSources: explicitPaths.value.knowledgeSources,
1427
+ });
843
1428
  diagnostics.push(...built.diagnostics);
844
1429
  if (built.config === null) {
845
1430
  return failureFromDiagnostics('invalid', diagnostics, diagnosticCodes.initInvalidInput, 'Init selections do not form a valid workspace configuration.');
@@ -848,8 +1433,21 @@ export async function planWorkspaceInit(options) {
848
1433
  selected = built.selected;
849
1434
  needWorkspaceConfig = true;
850
1435
  if (existingMachine !== null) {
851
- machine = existingMachine;
852
- diagnostics.push(info(diagnosticCodes.initExistingConfigReused, 'Existing valid machine.json is reused while workspace.json is created.', context.machineInspection.path, 'Use a future configure flow to change local host or external repository mappings.'));
1436
+ const desiredHosts = [
1437
+ ...new Set(choices.hosts),
1438
+ ];
1439
+ const hostsChanged = desiredHosts.length !== existingMachine.selectedHosts.length ||
1440
+ desiredHosts.some((host) => !existingMachine.selectedHosts.includes(host));
1441
+ if (options.reconfigure === true &&
1442
+ existingConfig !== null &&
1443
+ hostsChanged) {
1444
+ machine = { ...existingMachine, selectedHosts: desiredHosts };
1445
+ needMachineConfig = true;
1446
+ }
1447
+ else {
1448
+ machine = existingMachine;
1449
+ diagnostics.push(info(diagnosticCodes.initExistingConfigReused, 'Existing valid machine.json is reused while workspace.json is created.', context.machineInspection.path, 'Use a future configure flow to change local host or external repository mappings.'));
1450
+ }
853
1451
  }
854
1452
  else {
855
1453
  needMachineConfig = true;
@@ -888,6 +1486,10 @@ export async function planWorkspaceInit(options) {
888
1486
  ? {}
889
1487
  : { discoveredAt: options.discoveredAt }),
890
1488
  });
1489
+ const pathRevalidation = await revalidateExplicitInitPaths(context.root, explicitPathSelections.observations, config.excludes);
1490
+ if (pathRevalidation.length > 0) {
1491
+ return failureFromDiagnostics('conflict', [...diagnostics, ...pathRevalidation], diagnosticCodes.initPlanStale, 'An explicitly added path changed while init was planning.');
1492
+ }
891
1493
  const finalWorkspace = await inspectWorkspaceConfig(context.root, {
892
1494
  missing: 'info',
893
1495
  });
@@ -905,7 +1507,7 @@ export async function planWorkspaceInit(options) {
905
1507
  }
906
1508
  const rootIdentity = context.rootIdentity;
907
1509
  const scopeRepos = existingConfig !== null
908
- ? scopeRepositories(config, context.discovery)
1510
+ ? scopeRepositories(config, context.discovery, selected)
909
1511
  : selected.map((repository) => ({
910
1512
  id: repository.id,
911
1513
  path: repository.path,
@@ -1112,7 +1714,7 @@ async function cleanupCreatedFiles(root, created) {
1112
1714
  return residual;
1113
1715
  }
1114
1716
  /**
1115
- * Applies only create operations from a plan, after revalidating root,
1717
+ * Applies create and managed replacement operations from a plan, after revalidating root,
1116
1718
  * configuration digest, target absence and exact bytes. External actions are
1117
1719
  * reported as pending and never executed here.
1118
1720
  */
@@ -1213,17 +1815,47 @@ export async function applyInitPlan(bundle, options = {}) {
1213
1815
  }
1214
1816
  };
1215
1817
  const restoreReplacements = async () => {
1818
+ const residual = [];
1819
+ const entries = originalManifest === null ? [] : [...originalManifest.entries];
1216
1820
  for (const replacement of [...replacements].reverse()) {
1217
1821
  try {
1218
- await writeFile(replacement.absolutePath, replacement.beforeBytes);
1822
+ const current = await observePath(replacement.absolutePath);
1823
+ if (current.kind !== 'file' ||
1824
+ current.stats === null ||
1825
+ current.stats.dev !== replacement.identity.dev ||
1826
+ current.stats.ino !== replacement.identity.ino ||
1827
+ sha256Digest(await readFile(replacement.absolutePath)) !==
1828
+ replacement.afterDigest) {
1829
+ residual.push(replacement.path);
1830
+ continue;
1831
+ }
1832
+ const restored = await writeBytesAtomic(replacement.absolutePath, replacement.beforeBytes, root);
1833
+ if (!restored.ok)
1834
+ throw new Error(restored.message);
1835
+ const after = await observePath(replacement.absolutePath);
1836
+ if (after.kind !== 'file' || after.stats === null)
1837
+ throw new Error('Restored file is unreadable.');
1838
+ const index = entries.findIndex((entry) => entry.path === replacement.path);
1839
+ if (index >= 0)
1840
+ entries[index] = {
1841
+ ...entries[index],
1842
+ afterDigest: replacement.backupDigest,
1843
+ identity: { dev: after.stats.dev, ino: after.stats.ino },
1844
+ };
1219
1845
  }
1220
1846
  catch {
1221
- // The residual list below and the failure diagnostic report this case.
1847
+ residual.push(replacement.path);
1222
1848
  }
1223
1849
  }
1224
- if (originalManifest !== null) {
1225
- await writeWorkspaceManifest(root, originalManifest).catch(() => undefined);
1850
+ if (originalManifest !== null && replacements.length > 0) {
1851
+ const restored = await writeWorkspaceManifest(root, {
1852
+ ...originalManifest,
1853
+ entries,
1854
+ }).catch(() => ({ ok: false }));
1855
+ if (!restored.ok)
1856
+ residual.push('.pappra/local/install-manifest.json');
1226
1857
  }
1858
+ return residual;
1227
1859
  };
1228
1860
  const rootBeforeWrite = await observePath(root);
1229
1861
  if (rootBeforeWrite.kind !== 'directory' ||
@@ -1233,13 +1865,38 @@ export async function applyInitPlan(bundle, options = {}) {
1233
1865
  return applyFailure('stale', diagnosticCodes.initPlanStale, 'Workspace root identity changed before writing.', 'Retry init from the current workspace root; no files were written.');
1234
1866
  }
1235
1867
  try {
1868
+ if (bundle.plan.files.length > 0) {
1869
+ const managedStart = await beginManagedApply(root, bundle.plan, bundle.writes, bundle.rootIdentity);
1870
+ if (!managedStart.ok) {
1871
+ return {
1872
+ ok: false,
1873
+ status: managedStart.status === 'invalid'
1874
+ ? 'stale'
1875
+ : managedStart.status === 'locked'
1876
+ ? 'conflict'
1877
+ : 'failed',
1878
+ appliedFiles: [],
1879
+ pendingExternalActions: [],
1880
+ residualPaths: managedStart.residualPaths,
1881
+ diagnostics: managedStart.diagnostics,
1882
+ };
1883
+ }
1884
+ managed = managedStart.context;
1885
+ originalManifest = managed.manifest;
1886
+ }
1887
+ else {
1888
+ const ready = await verifyWorkspaceInstallReady(root);
1889
+ if (!ready.ok)
1890
+ return applyFailure('conflict', ready.code, ready.message, ready.nextAction ?? 'Run pappra repair before retrying init.');
1891
+ }
1236
1892
  for (const index of replaceIndexes) {
1237
1893
  const file = bundle.plan.files[index];
1238
1894
  const write = bundle.writes[index];
1239
1895
  await options.beforeWrite?.(file.path, index);
1240
1896
  const absolutePath = resolvePortableRelativePath(root, file.path);
1241
- const backupRef = backupPath(root, bundle.plan.planId, file.path);
1242
- const stateBoundary = await ensureStatePathBoundary(root, backupRef);
1897
+ const backupFile = backupPath(root, managed.installId, file.path);
1898
+ const backupRef = toWorkspaceRelativePath(root, backupFile);
1899
+ const stateBoundary = await ensureStatePathBoundary(root, backupFile);
1243
1900
  if (!stateBoundary.ok)
1244
1901
  throw new Error(stateBoundary.message);
1245
1902
  const beforeBytes = await readFile(absolutePath);
@@ -1248,12 +1905,33 @@ export async function applyInitPlan(bundle, options = {}) {
1248
1905
  throw new Error(`Replacement target changed again: ${file.path}`);
1249
1906
  }
1250
1907
  const observation = await observePath(absolutePath);
1908
+ if (observation.kind !== 'file')
1909
+ throw new Error(`Unsafe replacement target: ${file.path}`);
1251
1910
  const mode = observation.stats === null ? 0o644 : observation.stats.mode & 0o777;
1252
- await mkdir(dirname(backupRef), { recursive: true });
1253
- await writeFile(backupRef, beforeBytes, { mode: 0o600, flag: 'wx' });
1911
+ await mkdir(dirname(backupFile), { recursive: true });
1912
+ await writeFile(backupFile, beforeBytes, { mode: 0o600, flag: 'wx' });
1254
1913
  const temporary = `${absolutePath}.pappra-replace-${randomUUID().slice(0, 8)}`;
1255
- await writeFile(temporary, write.bytes, { mode });
1256
- await rename(temporary, absolutePath);
1914
+ try {
1915
+ await writeFile(temporary, write.bytes, { mode, flag: 'wx' });
1916
+ const staged = await observePath(temporary);
1917
+ if (staged.kind !== 'file' || staged.stats === null)
1918
+ throw new Error('Replacement staging failed.');
1919
+ const recorded = await recordManagedFile(managed, file.path, file.nextDigest, { dev: staged.stats.dev, ino: staged.stats.ino }, { backupRef, backupDigest: sha256Digest(beforeBytes) });
1920
+ if (!recorded.ok)
1921
+ throw new Error(recorded.diagnostics[0]?.message ?? 'Cannot journal replacement.');
1922
+ const beforeRename = await observePath(absolutePath);
1923
+ if (beforeRename.kind !== 'file' ||
1924
+ beforeRename.stats?.dev !== observation.stats?.dev ||
1925
+ beforeRename.stats?.ino !== observation.stats?.ino ||
1926
+ sha256Digest(await readFile(absolutePath)) !==
1927
+ file.expectedBeforeDigest) {
1928
+ throw new Error(`Replacement target changed before publish: ${file.path}`);
1929
+ }
1930
+ await rename(temporary, absolutePath);
1931
+ }
1932
+ finally {
1933
+ await rm(temporary, { force: true }).catch(() => undefined);
1934
+ }
1257
1935
  const after = await observePath(absolutePath);
1258
1936
  if (after.kind !== 'file' || after.stats === null) {
1259
1937
  throw new Error(`Replacement target is unreadable after writing: ${file.path}`);
@@ -1268,68 +1946,7 @@ export async function applyInitPlan(bundle, options = {}) {
1268
1946
  identity: { dev: after.stats.dev, ino: after.stats.ino },
1269
1947
  });
1270
1948
  }
1271
- if (replacements.length > 0) {
1272
- const manifestRead = await readWorkspaceManifest(root);
1273
- if (manifestRead.diagnostics.length > 0) {
1274
- throw new Error(manifestRead.diagnostics[0]?.message ??
1275
- 'Workspace install manifest is unreadable.');
1276
- }
1277
- if (manifestRead.value === null) {
1278
- throw new Error('Workspace install manifest is missing; Pappra does not adopt an unowned replacement.');
1279
- }
1280
- originalManifest = manifestRead.value;
1281
- const entries = [...manifestRead.value.entries];
1282
- const verifiedAt = new Date().toISOString();
1283
- for (const replacement of replacements) {
1284
- const entry = {
1285
- path: replacement.path,
1286
- installId: bundle.plan.planId,
1287
- operation: 'replace-managed',
1288
- beforeDigest: replacement.backupDigest,
1289
- afterDigest: replacement.afterDigest,
1290
- backupRef: replacement.backupRef,
1291
- backupDigest: replacement.backupDigest,
1292
- identity: replacement.identity,
1293
- owner: 'pappra',
1294
- verifiedAt,
1295
- };
1296
- const existingIndex = entries.findIndex((item) => item.path === replacement.path);
1297
- if (existingIndex === -1)
1298
- entries.push(entry);
1299
- else
1300
- entries[existingIndex] = entry;
1301
- }
1302
- const written = await writeWorkspaceManifest(root, {
1303
- ...manifestRead.value,
1304
- entries,
1305
- });
1306
- if (!written.ok)
1307
- throw new Error(written.message);
1308
- }
1309
1949
  if (createIndexes.length > 0) {
1310
- const createPlan = {
1311
- ...bundle.plan,
1312
- files: createIndexes.map((index) => bundle.plan.files[index]),
1313
- };
1314
- const createWrites = createIndexes.map((index) => bundle.writes[index]);
1315
- const managedStart = await beginManagedApply(root, createPlan, createWrites, bundle.rootIdentity);
1316
- if (!managedStart.ok) {
1317
- const status = managedStart.status === 'invalid'
1318
- ? 'stale'
1319
- : managedStart.status === 'locked'
1320
- ? 'conflict'
1321
- : 'failed';
1322
- await restoreReplacements();
1323
- return {
1324
- ok: false,
1325
- status,
1326
- appliedFiles: [],
1327
- pendingExternalActions: [],
1328
- residualPaths: managedStart.residualPaths,
1329
- diagnostics: managedStart.diagnostics,
1330
- };
1331
- }
1332
- managed = managedStart.context;
1333
1950
  for (const index of createIndexes) {
1334
1951
  const file = bundle.plan.files[index];
1335
1952
  const write = bundle.writes[index];
@@ -1356,17 +1973,22 @@ export async function applyInitPlan(bundle, options = {}) {
1356
1973
  finally {
1357
1974
  await handle.close();
1358
1975
  }
1976
+ await options.afterWrite?.(file.path, index);
1359
1977
  created.push(record);
1360
1978
  const recorded = await recordManagedFile(managed, file.path, file.nextDigest, { dev: record.dev, ino: record.ino });
1361
1979
  if (!recorded.ok) {
1362
1980
  throw new Error(recorded.diagnostics[0]?.message ?? `Cannot record ${file.path}.`);
1363
1981
  }
1364
1982
  }
1983
+ }
1984
+ if (managed !== null) {
1365
1985
  const completed = await completeManagedApply(managed);
1366
1986
  if (!completed.ok) {
1367
- const residual = await cleanupCreatedFiles(root, created);
1987
+ const residual = [
1988
+ ...(await cleanupCreatedFiles(root, created)),
1989
+ ...(await restoreReplacements()),
1990
+ ];
1368
1991
  const failed = await failManagedApply(managed, 'Cannot finalize managed installation manifest.', residual);
1369
- await restoreReplacements();
1370
1992
  return {
1371
1993
  ok: false,
1372
1994
  status: 'failed',
@@ -1395,8 +2017,10 @@ export async function applyInitPlan(bundle, options = {}) {
1395
2017
  };
1396
2018
  }
1397
2019
  catch (error) {
1398
- await restoreReplacements();
1399
- const residual = await cleanupCreatedFiles(root, created);
2020
+ const residual = [
2021
+ ...(await restoreReplacements()),
2022
+ ...(await cleanupCreatedFiles(root, created)),
2023
+ ];
1400
2024
  for (const directory of createdDirectories.reverse()) {
1401
2025
  await rmdir(directory).catch(() => undefined);
1402
2026
  }
@@ -1494,24 +2118,13 @@ export async function bindAndRecordRuntime(root, installed) {
1494
2118
  };
1495
2119
  }
1496
2120
  /**
1497
- * Converges the runtime binding by installing a durable copy of the package that
1498
- * is currently executing. Reusing an already verified copy is cheap: the install
1499
- * machinery short-circuits before packing.
2121
+ * Ensures and binds the runtime package that is currently executing.
2122
+ *
2123
+ * The installer reuses an existing runtime only after its manifest, package
2124
+ * contents, executable and smoke checks pass; a regular file is not proof of a
2125
+ * verified durable runtime.
1500
2126
  */
1501
2127
  export async function ensureDefaultRuntime(root, targetVersion) {
1502
- const machine = await inspectMachineConfig(root);
1503
- const binding = machine.value?.runtime ?? null;
1504
- if (binding !== null && binding.version === targetVersion) {
1505
- const executable = await observePath(binding.executable);
1506
- if (executable.kind === 'file') {
1507
- return {
1508
- ok: true,
1509
- completion: true,
1510
- runtime: null,
1511
- diagnostics: [],
1512
- };
1513
- }
1514
- }
1515
2128
  const installed = await installRuntime({
1516
2129
  version: targetVersion,
1517
2130
  source: { kind: 'self', path: getPackageRoot() },