@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
@@ -1,18 +1,54 @@
1
- import { basename, resolve } from 'node:path';
1
+ import { join, resolve } from 'node:path';
2
2
  import * as prompts from '@clack/prompts';
3
3
  import { diagnosticCodes } from '../../schemas/errors.js';
4
4
  import { INIT_CHOICES_SCHEMA, parseInitChoices, } from '../../schemas/init.js';
5
5
  import { readTextFileBounded } from '../../infra/fs.js';
6
- import { getPackageInfo } from '../../package-info.js';
7
- import { installRuntime } from '../../install/runtime.js';
8
- import { applyInitPlan, bindAndRecordRuntime, ensureDefaultRuntime, inspectInitContext, listKnowledgeCandidates, deriveInitChoices, listRepositoryCandidates, planWorkspaceInit, } from '../../app/init.js';
6
+ import { getPackageInfo, getPackageRoot } from '../../package-info.js';
7
+ import { installRuntime, runtimePaths } from '../../install/runtime.js';
8
+ import { applyInitPlan, bindAndRecordRuntime, ensureDefaultRuntime, inspectInitContext, listKnowledgeCandidates, deriveCommonInitDefaults, deriveInitChoices, existingInitChoices, listRepositoryCandidates, validateInitField, inspectInitPathAddition, planWorkspaceInit, } from '../../app/init.js';
9
+ const addExistingRepositoryAction = '../__pappra_add_existing_repository_action__';
10
+ const addExistingKnowledgeAction = '../__pappra_add_existing_knowledge_action__';
11
+ const collectedPathObservations = new WeakMap();
12
+ function initPathObservationKey(kind, path) {
13
+ return `${kind}:${path}`;
14
+ }
15
+ function repositoryCandidateFromPathObservation(observation) {
16
+ if (observation.kind !== 'repository' || observation.git === null) {
17
+ return null;
18
+ }
19
+ return {
20
+ key: observation.path,
21
+ id: null,
22
+ external: false,
23
+ access: 'read',
24
+ root: observation.git.root,
25
+ kind: observation.git.kind,
26
+ dirty: observation.git.dirty,
27
+ configured: false,
28
+ };
29
+ }
30
+ function knowledgeCandidateFromPathObservation(observation) {
31
+ if (observation.kind !== 'knowledge')
32
+ return null;
33
+ return {
34
+ path: observation.path,
35
+ kind: observation.targetKind,
36
+ origin: 'explicit-path',
37
+ repositoryId: null,
38
+ };
39
+ }
9
40
  export const clackWizardUi = {
10
41
  intro: (message) => prompts.intro(message),
11
42
  outro: (message) => prompts.outro(message),
12
43
  note: (message, title) => prompts.note(message, title),
13
- text: async (message, initialValue) => prompts.text({
44
+ text: async (message, initialValue, validate) => prompts.text({
14
45
  message,
15
- ...(initialValue === undefined ? {} : { defaultValue: initialValue }),
46
+ ...(initialValue === undefined ? {} : { initialValue }),
47
+ ...(validate === undefined
48
+ ? {}
49
+ : {
50
+ validate: (value) => value === undefined ? undefined : validate(value),
51
+ }),
16
52
  }),
17
53
  select: async (message, options, initialValue) => prompts.select({
18
54
  message,
@@ -41,37 +77,22 @@ async function promptValue(promise, ui) {
41
77
  }
42
78
  return value;
43
79
  }
44
- function existingChoices(config, hosts) {
45
- if (config === null)
46
- return null;
47
- return {
48
- schema: INIT_CHOICES_SCHEMA,
49
- name: config.name,
50
- workspaceId: config.workspaceId,
51
- repositories: config.repositories.map((repository) => ({
52
- path: repository.path,
53
- id: repository.id,
54
- access: repository.access,
55
- })),
56
- knowledge: config.knowledge,
57
- code: config.code,
58
- validation: [],
59
- excludes: config.excludes,
60
- hosts: [...new Set(hosts)],
61
- };
62
- }
63
80
  async function askHosts(ui, initial) {
64
- return promptValue(ui.multiselect('Select hosts to record (P03 records only; no installation or wiring)', [
65
- { value: 'pi', label: 'Pi', hint: 'Not detected or verified in P03' },
81
+ return promptValue(ui.multiselect('Select hosts to record (declaration only; no installation, detection or wiring)', [
82
+ {
83
+ value: 'pi',
84
+ label: 'Pi',
85
+ hint: 'Declaration only; not detected or verified',
86
+ },
66
87
  {
67
88
  value: 'codex',
68
89
  label: 'Codex',
69
- hint: 'Not detected or verified in P03',
90
+ hint: 'Declaration only; not detected or verified',
70
91
  },
71
92
  {
72
93
  value: 'claude',
73
94
  label: 'Claude',
74
- hint: 'Not detected or verified in P03',
95
+ hint: 'Declaration only; not detected or verified',
75
96
  },
76
97
  ], initial), ui);
77
98
  }
@@ -79,163 +100,618 @@ async function askHosts(ui, initial) {
79
100
  * UI-only selection collection. All validation and plan generation remains in
80
101
  * the app service; cancelling this function never writes to the workspace.
81
102
  */
82
- export async function collectInitChoices(context, ui) {
103
+ export async function collectInitChoices(context, ui, options = {}) {
83
104
  ui.intro('Pappra workspace init');
84
105
  const existingConfig = context.workspaceInspection.value;
85
106
  const existingMachine = context.machineInspection.value;
86
- if (existingConfig !== null) {
107
+ if (existingConfig !== null &&
108
+ options.reconfigure !== true &&
109
+ options.draft === undefined) {
87
110
  const hosts = existingMachine?.selectedHosts ?? (await askHosts(ui, []));
88
111
  if (hosts === null)
89
112
  return null;
90
- const choices = existingChoices(existingConfig, hosts);
91
- if (choices !== null) {
92
- ui.note('Existing valid workspace configuration is reused. Only missing local state is planned; no existing file is overwritten.', 'Existing configuration');
93
- ui.outro('Selections complete. Review the plan before applying.');
94
- }
113
+ const choices = {
114
+ ...existingInitChoices(existingConfig, existingMachine),
115
+ hosts: [...new Set(hosts)],
116
+ };
117
+ ui.note('Existing valid workspace configuration is reused. Only missing local state is planned; no existing file is overwritten.', 'Existing configuration');
118
+ collectedPathObservations.set(choices, options.pathObservations ?? []);
119
+ ui.outro('Selections complete. Review the plan before applying.');
95
120
  return choices;
96
121
  }
122
+ const seed = options.draft ??
123
+ (existingConfig === null
124
+ ? undefined
125
+ : existingInitChoices(existingConfig, existingMachine));
126
+ const common = deriveCommonInitDefaults(context.discovery);
97
127
  const repositoryCandidates = listRepositoryCandidates(context.discovery);
98
- const selectedRepositoryKeys = repositoryCandidates.length === 0
99
- ? []
100
- : await promptValue(ui.multiselect('Select repositories in this workspace', repositoryCandidates.map((candidate) => ({
101
- value: candidate.key,
102
- label: `${candidate.key}${candidate.external ? ' (external)' : ''}`,
103
- hint: `${candidate.kind}; ${candidate.external ? 'explicit external mapping' : 'discovered checkout'}${candidate.dirty === true ? '; dirty' : ''}`,
104
- })), repositoryCandidates.length === 1
105
- ? [repositoryCandidates[0].key]
106
- : []), ui);
107
- if (selectedRepositoryKeys === null)
108
- return null;
109
- const repositories = [];
110
- for (const key of selectedRepositoryKeys) {
111
- const candidate = repositoryCandidates.find((entry) => entry.key === key);
112
- if (candidate === undefined)
113
- continue;
114
- const access = await promptValue(ui.select(`Repository access policy for ${candidate.key}`, [
128
+ const knowledgeCandidates = listKnowledgeCandidates(context.discovery);
129
+ const pathObservations = new Map();
130
+ for (const observation of options.pathObservations ?? []) {
131
+ pathObservations.set(initPathObservationKey(observation.kind, observation.path), observation);
132
+ }
133
+ const explicitRepositoryCandidates = new Map();
134
+ const explicitKnowledgeCandidates = new Map();
135
+ for (const observation of pathObservations.values()) {
136
+ const repository = repositoryCandidateFromPathObservation(observation);
137
+ if (repository !== null) {
138
+ explicitRepositoryCandidates.set(repository.key, repository);
139
+ }
140
+ const knowledge = knowledgeCandidateFromPathObservation(observation);
141
+ if (knowledge !== null) {
142
+ explicitKnowledgeCandidates.set(knowledge.path, knowledge);
143
+ }
144
+ }
145
+ const knownKnowledge = new Set([
146
+ ...knowledgeCandidates.map((candidate) => candidate.path),
147
+ ...explicitKnowledgeCandidates.keys(),
148
+ ]);
149
+ const initialRepositories = seed !== undefined
150
+ ? seed.repositories.map((repository) => ({ ...repository }))
151
+ : repositoryCandidates.length === 1
152
+ ? [
153
+ {
154
+ path: repositoryCandidates[0].key,
155
+ access: repositoryCandidates[0].access,
156
+ ...(repositoryCandidates[0].id === null
157
+ ? {}
158
+ : { id: repositoryCandidates[0].id }),
159
+ },
160
+ ]
161
+ : [];
162
+ const draft = {
163
+ schema: INIT_CHOICES_SCHEMA,
164
+ ...(seed?.workspaceId === undefined
165
+ ? {}
166
+ : { workspaceId: seed.workspaceId }),
167
+ name: seed?.name ?? common.name,
168
+ repositories: initialRepositories,
169
+ knowledge: {
170
+ ...(seed?.knowledge ?? common.knowledge),
171
+ sources: [...(seed?.knowledge.sources ?? common.knowledge.sources)],
172
+ },
173
+ code: seed?.code ?? {
174
+ graph: 'none',
175
+ symbols: common.code.symbols,
176
+ prepare: common.code.prepare,
177
+ },
178
+ validation: [...(seed?.validation ?? [])],
179
+ excludes: [...(seed?.excludes ?? common.excludes)],
180
+ hosts: [...(seed?.hosts ?? [])],
181
+ };
182
+ if (!options.reviewOnly)
183
+ ui.note(`Workspace root: ${context.root}\nMode: ${existingConfig === null
184
+ ? 'new workspace'
185
+ : 'editing existing workspace settings'}\nSelections are a read-only draft until the later plan confirmation. Existing knowledge source files/folders are discovered and registered in place later; the new document destination is only for future authored documents. No files are created or changed while collecting choices.`, 'Workspace context');
186
+ const promptText = async (message, initialValue, field) => {
187
+ const value = await promptValue(ui.text(message, initialValue, field === undefined
188
+ ? undefined
189
+ : (candidate) => validateInitField(field, candidate) ?? undefined), ui);
190
+ return typeof value === 'symbol' ? null : value;
191
+ };
192
+ const promptExplicitPath = async (kind) => {
193
+ const field = kind === 'repository' ? 'repositoryPath' : 'knowledgePath';
194
+ let initialValue = '';
195
+ for (;;) {
196
+ const value = await ui.text(kind === 'repository'
197
+ ? `Existing repository path under workspace root ${context.root}`
198
+ : `Existing knowledge file or folder path under workspace root ${context.root}`, initialValue, (candidate) => validateInitField(field, candidate) ?? undefined);
199
+ if (typeof value !== 'string') {
200
+ ui.note('Path addition cancelled; the current draft selections are unchanged.', 'Path addition');
201
+ return null;
202
+ }
203
+ const inspected = await inspectInitPathAddition(context.root, kind, value, draft.excludes);
204
+ if (inspected.ok)
205
+ return inspected.observation;
206
+ ui.note(diagnosticLines(inspected.diagnostics), 'Path not added');
207
+ initialValue = value;
208
+ }
209
+ };
210
+ const editWorkspace = async () => {
211
+ const name = await promptText('Workspace name', draft.name ?? '', 'name');
212
+ if (name === null)
213
+ return false;
214
+ draft.name = name;
215
+ return true;
216
+ };
217
+ const editRepositories = async () => {
218
+ const buildCandidateMap = () => {
219
+ const candidates = new Map();
220
+ for (const candidate of repositoryCandidates) {
221
+ candidates.set(candidate.key, candidate);
222
+ }
223
+ for (const candidate of explicitRepositoryCandidates.values()) {
224
+ if (!candidates.has(candidate.key))
225
+ candidates.set(candidate.key, candidate);
226
+ }
227
+ return candidates;
228
+ };
229
+ const buildOptions = (candidates) => {
230
+ const result = [...candidates.values()].map((candidate) => ({
231
+ value: candidate.key,
232
+ label: `${candidate.key}${candidate.external ? ' (external)' : ''}`,
233
+ hint: `${candidate.kind}; ${candidate.external ? 'explicit external mapping' : 'discovered or explicitly inspected checkout'}${candidate.dirty === true ? '; dirty' : ''}`,
234
+ }));
235
+ for (const repository of draft.repositories) {
236
+ if (result.some((candidate) => candidate.value === repository.path)) {
237
+ continue;
238
+ }
239
+ result.push({
240
+ value: repository.path,
241
+ label: `${repository.path} (configured; not discovered)`,
242
+ hint: 'Retained existing selection; planning fails if it cannot be resolved',
243
+ });
244
+ }
245
+ result.push({
246
+ value: addExistingRepositoryAction,
247
+ label: 'Add existing repository…',
248
+ hint: `Inspect one workspace-relative checkout beneath ${context.root}; no global or ancestor discovery`,
249
+ });
250
+ return result;
251
+ };
252
+ let selectedKeys = [
253
+ ...new Set(draft.repositories.map((repository) => repository.path)),
254
+ ];
255
+ for (;;) {
256
+ const candidates = buildCandidateMap();
257
+ const selected = await promptValue(ui.multiselect(`Select repositories in this workspace (resolved root: ${context.root})`, buildOptions(candidates), selectedKeys), ui);
258
+ if (selected === null)
259
+ return false;
260
+ selectedKeys = [
261
+ ...new Set(selected.filter((key) => key !== addExistingRepositoryAction)),
262
+ ];
263
+ if (!selected.includes(addExistingRepositoryAction))
264
+ break;
265
+ const observation = await promptExplicitPath('repository');
266
+ if (observation === null)
267
+ continue;
268
+ const candidate = repositoryCandidateFromPathObservation(observation);
269
+ if (candidate === null) {
270
+ ui.note('The entered location did not produce a repository observation.', 'Path not added');
271
+ continue;
272
+ }
273
+ const observationKey = initPathObservationKey('repository', observation.path);
274
+ pathObservations.set(observationKey, observation);
275
+ const existing = [...buildCandidateMap().values()].find((entry) => entry.key === candidate.key || entry.root === candidate.root);
276
+ if (existing !== undefined) {
277
+ if (!selectedKeys.includes(existing.key))
278
+ selectedKeys.push(existing.key);
279
+ ui.note(`That checkout is already available as ${existing.key}; it was selected without adding a duplicate.`, 'Repository already available');
280
+ continue;
281
+ }
282
+ explicitRepositoryCandidates.set(candidate.key, candidate);
283
+ if (!selectedKeys.includes(candidate.key))
284
+ selectedKeys.push(candidate.key);
285
+ ui.note(`Added ${candidate.key} as an existing workspace-local checkout. The selected access policy remains a separate workspace choice; no files were written.`, 'Repository added');
286
+ }
287
+ const candidates = buildCandidateMap();
288
+ const repositories = [];
289
+ for (const key of selectedKeys) {
290
+ const candidate = candidates.get(key);
291
+ const seeded = draft.repositories.find((repository) => repository.path === key);
292
+ const access = await promptValue(ui.select(`Repository access policy for ${key} (workspace policy only; not a host permission or command execution)`, [
293
+ { value: 'read', label: 'read', hint: 'Least-privilege policy' },
294
+ {
295
+ value: 'write',
296
+ label: 'write',
297
+ hint: 'Workspace policy cap only; host permissions still apply',
298
+ },
299
+ ], seeded?.access ?? candidate?.access ?? 'read'), ui);
300
+ if (access === null)
301
+ return false;
302
+ const id = seeded?.id ?? candidate?.id ?? null;
303
+ repositories.push({
304
+ path: key,
305
+ access: access,
306
+ ...(id === null ? {} : { id }),
307
+ });
308
+ }
309
+ draft.repositories = repositories;
310
+ return true;
311
+ };
312
+ const editKnowledge = async () => {
313
+ const rank = (path, isDirectory) => isDirectory ? 1 : path.includes('/') ? 2 : 0;
314
+ const buildCandidateMap = () => {
315
+ const candidates = new Map();
316
+ for (const candidate of knowledgeCandidates) {
317
+ candidates.set(candidate.path, candidate);
318
+ }
319
+ for (const candidate of explicitKnowledgeCandidates.values()) {
320
+ if (!candidates.has(candidate.path))
321
+ candidates.set(candidate.path, candidate);
322
+ }
323
+ return candidates;
324
+ };
325
+ const buildOptions = (candidates) => {
326
+ const result = [...candidates.values()]
327
+ .map((candidate) => ({
328
+ candidate,
329
+ rank: rank(candidate.path, candidate.kind === 'directory'),
330
+ }))
331
+ .sort((left, right) => left.rank - right.rank ||
332
+ left.candidate.path.localeCompare(right.candidate.path))
333
+ .map(({ candidate }) => {
334
+ const explicit = candidate.origin === 'explicit-path';
335
+ return {
336
+ value: candidate.path,
337
+ label: candidate.kind === 'directory'
338
+ ? `Folder: ${candidate.path}${explicit ? ' (explicitly inspected)' : ' (covers supported files beneath)'}`
339
+ : candidate.path.includes('/')
340
+ ? `File: ${candidate.path}${explicit ? ' (explicitly inspected)' : ''}`
341
+ : `Root file: ${candidate.path}${explicit ? ' (explicitly inspected)' : ''}`,
342
+ hint: explicit
343
+ ? candidate.kind === 'directory'
344
+ ? 'Existing source path inspected only; no documents were enumerated or read. Empty folders are valid.'
345
+ : 'Existing source path inspected only; init did not read its document body or copy/move it.'
346
+ : candidate.kind === 'directory'
347
+ ? 'Includes supported discovered documents beneath it, subject to exclusions, limits and symlink boundaries; does not promise the entire filesystem tree.'
348
+ : candidate.path.includes('/')
349
+ ? 'Select this file directly; its folder is not selected implicitly.'
350
+ : 'Existing root file; selecting it does not copy or register it now.',
351
+ };
352
+ });
353
+ for (const source of draft.knowledge.sources) {
354
+ if (result.some((candidate) => candidate.value === source))
355
+ continue;
356
+ result.push({
357
+ value: source,
358
+ label: `Retained configured: ${source} (unavailable/unverified)`,
359
+ hint: 'Existing configured selection retained; not freshly discovered or verified. You may remove it explicitly.',
360
+ });
361
+ }
362
+ result.push({
363
+ value: addExistingKnowledgeAction,
364
+ label: 'Add existing knowledge file or folder…',
365
+ hint: `Inspect one workspace-relative source beneath ${context.root}; no document bodies are read`,
366
+ });
367
+ return result;
368
+ };
369
+ let sources = [...draft.knowledge.sources];
370
+ for (;;) {
371
+ const candidates = buildCandidateMap();
372
+ const selected = await promptValue(ui.multiselect(`Select knowledge sources under workspace root ${context.root}`, buildOptions(candidates), sources), ui);
373
+ if (selected === null)
374
+ return false;
375
+ sources = [
376
+ ...new Set(selected.filter((source) => source !== addExistingKnowledgeAction)),
377
+ ];
378
+ if (!selected.includes(addExistingKnowledgeAction))
379
+ break;
380
+ const observation = await promptExplicitPath('knowledge');
381
+ if (observation === null)
382
+ continue;
383
+ const candidate = knowledgeCandidateFromPathObservation(observation);
384
+ if (candidate === null) {
385
+ ui.note('The entered location did not produce a knowledge-source observation.', 'Path not added');
386
+ continue;
387
+ }
388
+ const observationKey = initPathObservationKey('knowledge', observation.path);
389
+ pathObservations.set(observationKey, observation);
390
+ const existing = buildCandidateMap().get(candidate.path);
391
+ if (existing !== undefined) {
392
+ if (!sources.includes(existing.path))
393
+ sources.push(existing.path);
394
+ ui.note(`${existing.path} is already an available knowledge source; it was selected without adding a duplicate.`, 'Knowledge source already available');
395
+ continue;
396
+ }
397
+ explicitKnowledgeCandidates.set(candidate.path, candidate);
398
+ knownKnowledge.add(candidate.path);
399
+ sources.push(candidate.path);
400
+ ui.note(`Added ${candidate.kind} source ${candidate.path}. It remains in place; init did not read document bodies or create, copy, move, or register documents. Empty folders are valid knowledge sources.`, 'Knowledge source added');
401
+ }
402
+ sources = [...new Set(sources)];
403
+ ui.note(`Existing source folders cover supported discovered documents beneath them, subject to exclusions, limits and symlink boundaries. The new document destination is a workspace-relative path under ${context.root}; it is not scanned/imported and is not created here.`, 'Knowledge sources and destination');
404
+ const newDocuments = await promptText('New authored-document destination (workspace-relative; not created here)', draft.knowledge.newDocuments, 'newDocuments');
405
+ if (newDocuments === null)
406
+ return false;
407
+ const visibility = await promptValue(ui.select('Task document visibility policy (declaration only; no existing record is shared or migrated)', [
115
408
  {
116
- value: 'read',
117
- label: 'read',
118
- hint: 'Default least-privilege policy',
409
+ value: 'private',
410
+ label: 'private',
411
+ hint: 'Default; raw evidence and checkpoints remain private',
119
412
  },
120
413
  {
121
- value: 'write',
122
- label: 'write',
123
- hint: 'Policy cap only; host permissions still apply',
414
+ value: 'shared',
415
+ label: 'shared',
416
+ hint: 'Task documents may be committed according to workspace policy',
124
417
  },
125
- ], candidate.access), ui);
126
- if (access === null)
127
- return null;
128
- repositories.push({
129
- path: candidate.key,
130
- access: access,
131
- ...(candidate.id === null ? {} : { id: candidate.id }),
132
- });
133
- }
134
- const knowledgeCandidates = listKnowledgeCandidates(context.discovery);
135
- const knowledgeSources = knowledgeCandidates.length === 0
136
- ? []
137
- : await promptValue(ui.multiselect('Select existing rules and documents to register as knowledge sources', knowledgeCandidates.map((candidate) => ({
138
- value: candidate.path,
139
- label: candidate.path,
140
- hint: `${candidate.kind}${candidate.origin === null ? '' : `; discovered by ${candidate.origin}`}`,
141
- })), knowledgeCandidates
142
- .filter((candidate) => ['AGENTS.md', 'CLAUDE.md', 'README.md', 'docs'].includes(candidate.path))
143
- .map((candidate) => candidate.path)), ui);
144
- if (knowledgeSources === null)
145
- return null;
146
- const newDocuments = await promptValue(ui.text('New knowledge directory', '.pappra/knowledge'), ui);
147
- if (newDocuments === null)
148
- return null;
149
- const taskVisibility = await promptValue(ui.select('Task document visibility', [
150
- {
151
- value: 'private',
152
- label: 'private',
153
- hint: 'Checkpoints and raw evidence remain private by default',
154
- },
155
- {
156
- value: 'shared',
157
- label: 'shared',
158
- hint: 'Task documents may be committed according to workspace policy',
159
- },
160
- ], 'private'), ui);
161
- if (taskVisibility === null)
162
- return null;
163
- const graph = await promptValue(ui.select('Code graph preference', [
164
- { value: 'none', label: 'none', hint: 'No graph provider' },
165
- {
166
- value: 'codegraph',
167
- label: 'codegraph',
168
- hint: 'Preference only; P03 does not install or index CodeGraph',
169
- },
170
- ], 'none'), ui);
171
- if (graph === null)
172
- return null;
173
- const symbols = await promptValue(ui.select('Symbol provider preference', [
174
- { value: 'auto', label: 'auto', hint: 'Use available existing tools' },
175
- {
176
- value: 'serena',
177
- label: 'serena',
178
- hint: 'Preference only; no installation is performed',
179
- },
180
- { value: 'none', label: 'none' },
181
- ], 'auto'), ui);
182
- if (symbols === null)
183
- return null;
184
- const prepare = await promptValue(ui.select('Index preparation preference', [
185
- {
186
- value: 'on-demand',
187
- label: 'on-demand',
188
- hint: 'Prepare only when a later authorized operation asks',
189
- },
190
- {
191
- value: 'manual',
192
- label: 'manual',
193
- hint: 'Never prepare automatically',
194
- },
195
- ], 'on-demand'), ui);
196
- if (prepare === null)
197
- return null;
198
- const validationCandidates = context.discovery.validationCandidates;
199
- const selectedValidation = validationCandidates.length === 0
200
- ? []
201
- : await promptValue(ui.multiselect('Select validation command candidates (none are executed or verified)', validationCandidates.map((candidate) => ({
418
+ ], draft.knowledge.taskVisibility), ui);
419
+ if (visibility === null)
420
+ return false;
421
+ draft.knowledge = {
422
+ sources: [...sources],
423
+ newDocuments,
424
+ taskVisibility: visibility,
425
+ };
426
+ return true;
427
+ };
428
+ const editAdvanced = async () => {
429
+ const graph = await promptValue(ui.select('Code graph preference (optional; no installation or indexing here)', [
430
+ { value: 'none', label: 'none', hint: 'No graph provider' },
431
+ {
432
+ value: 'codegraph',
433
+ label: 'codegraph',
434
+ hint: 'Preference only; nothing is installed or indexed',
435
+ },
436
+ ], draft.code.graph), ui);
437
+ if (graph === null)
438
+ return false;
439
+ const symbols = await promptValue(ui.select('Symbol provider preference (optional; no installation here)', [
440
+ {
441
+ value: 'auto',
442
+ label: 'auto',
443
+ hint: 'Use available existing tools',
444
+ },
445
+ { value: 'serena', label: 'serena', hint: 'Preference only' },
446
+ { value: 'none', label: 'none' },
447
+ ], draft.code.symbols), ui);
448
+ if (symbols === null)
449
+ return false;
450
+ const prepare = await promptValue(ui.select('Index preparation preference (optional; nothing is prepared here)', [
451
+ {
452
+ value: 'on-demand',
453
+ label: 'on-demand',
454
+ hint: 'Prepare only when a later authorized operation asks',
455
+ },
456
+ {
457
+ value: 'manual',
458
+ label: 'manual',
459
+ hint: 'Never prepare automatically',
460
+ },
461
+ ], draft.code.prepare), ui);
462
+ if (prepare === null)
463
+ return false;
464
+ const validationOptions = context.discovery.validationCandidates.map((candidate) => ({
202
465
  value: candidate.id,
203
466
  label: `${candidate.id}`,
204
- hint: `${candidate.origin}; ${candidate.executable} ${candidate.args.join(' ')}; not verified`,
205
- })), []), ui);
206
- if (selectedValidation === null)
207
- return null;
208
- const hosts = await askHosts(ui, []);
209
- if (hosts === null)
210
- return null;
211
- const excludesText = await promptValue(ui.text('Additional workspace-relative exclude prefixes (comma separated)', ''), ui);
212
- if (excludesText === null)
213
- return null;
214
- const name = await promptValue(ui.text('Workspace name', basename(context.root) || 'workspace'), ui);
215
- if (name === null)
216
- return null;
217
- ui.note('Validation candidates and hosts are declarations only. P03 does not resolve PATH, install providers, prepare indexes, or wire global configuration.', 'Limits');
218
- ui.outro('Selections complete. Review the plan before applying.');
219
- return {
220
- schema: INIT_CHOICES_SCHEMA,
221
- name,
222
- repositories,
223
- knowledge: {
224
- sources: [...knowledgeSources],
225
- newDocuments,
226
- taskVisibility: taskVisibility,
227
- },
228
- code: {
467
+ hint: `${candidate.origin}; ${candidate.executable} ${candidate.args.join(' ')}; not executed or verified`,
468
+ }));
469
+ for (const candidateId of draft.validation) {
470
+ if (validationOptions.some((candidate) => candidate.value === candidateId)) {
471
+ continue;
472
+ }
473
+ validationOptions.push({
474
+ value: candidateId,
475
+ label: `${candidateId} (configured; not discovered)`,
476
+ hint: 'Retained existing selection; planning fails if it cannot be bound',
477
+ });
478
+ }
479
+ const selectedValidation = validationOptions.length === 0
480
+ ? []
481
+ : await promptValue(ui.multiselect('Select validation command candidates (optional; none are executed or verified)', validationOptions, draft.validation), ui);
482
+ if (selectedValidation === null)
483
+ return false;
484
+ const hosts = await askHosts(ui, draft.hosts);
485
+ if (hosts === null)
486
+ return false;
487
+ const excludesText = await promptText('Workspace-relative exclude prefixes (complete seeded list; comma separated; literal prefixes only)', draft.excludes.join(', '), 'excludes');
488
+ if (excludesText === null)
489
+ return false;
490
+ draft.code = {
229
491
  graph: graph,
230
492
  symbols: symbols,
231
493
  prepare: prepare,
232
- },
233
- validation: [...selectedValidation],
234
- excludes: excludesText
494
+ };
495
+ draft.validation = [...selectedValidation];
496
+ draft.hosts = [...new Set(hosts)];
497
+ draft.excludes = excludesText
235
498
  .split(/[,\n]/)
236
499
  .map((value) => value.trim())
237
- .filter((value) => value !== ''),
238
- hosts: [...new Set(hosts)],
500
+ .filter((value) => value !== '');
501
+ ui.note('Advanced choices only record preferences. No validation command, host probe, installation, index preparation or global wiring runs here.', 'Advanced choices');
502
+ return true;
503
+ };
504
+ const sections = [
505
+ 'workspace',
506
+ 'repositories',
507
+ 'knowledge',
508
+ 'advanced',
509
+ ];
510
+ const editSection = async (section) => {
511
+ if (section === 'workspace')
512
+ return editWorkspace();
513
+ if (section === 'repositories')
514
+ return editRepositories();
515
+ if (section === 'knowledge')
516
+ return editKnowledge();
517
+ return editAdvanced();
518
+ };
519
+ let index = 0;
520
+ let reviewRequested = false;
521
+ while (!reviewRequested &&
522
+ index < sections.length &&
523
+ options.reviewOnly !== true) {
524
+ const section = sections[index];
525
+ if (section !== 'advanced' && !(await editSection(section)))
526
+ return null;
527
+ const last = index === sections.length - 1;
528
+ const navigation = section === 'advanced'
529
+ ? [
530
+ {
531
+ value: 'skip',
532
+ label: 'Keep current advanced settings',
533
+ hint: 'Retain graph/symbols/preparation/validation/hosts/excludes and continue to review',
534
+ },
535
+ {
536
+ value: 'edit',
537
+ label: 'Edit advanced preferences',
538
+ hint: 'Explicitly edit graph, symbols, preparation, validation, hosts and excludes',
539
+ },
540
+ {
541
+ value: 'back',
542
+ label: 'Back',
543
+ hint: 'Return to the knowledge section',
544
+ },
545
+ {
546
+ value: 'cancel',
547
+ label: 'Cancel',
548
+ hint: 'Discard the draft; no files are written',
549
+ },
550
+ ]
551
+ : [
552
+ ...(index > 0
553
+ ? [
554
+ {
555
+ value: 'back',
556
+ label: 'Back',
557
+ hint: 'Return to the previous setup section',
558
+ },
559
+ ]
560
+ : []),
561
+ {
562
+ value: 'next',
563
+ label: last ? 'Review selections' : 'Continue',
564
+ hint: last
565
+ ? 'Review the draft; a detailed plan follows'
566
+ : 'Move to the next setup section',
567
+ },
568
+ {
569
+ value: 'review',
570
+ label: 'Review selections',
571
+ hint: 'Skip remaining setup and review retained values',
572
+ },
573
+ {
574
+ value: 'cancel',
575
+ label: 'Cancel',
576
+ hint: 'Discard the draft; no files are written',
577
+ },
578
+ ];
579
+ const action = await promptValue(ui.select('Section navigation', navigation, section === 'advanced' ? 'skip' : 'next'), ui);
580
+ if (action === null)
581
+ return null;
582
+ if (action === 'cancel') {
583
+ ui.cancel('Init cancelled; no files were written.');
584
+ return null;
585
+ }
586
+ if (action === 'skip') {
587
+ reviewRequested = true;
588
+ }
589
+ else if (action === 'edit') {
590
+ if (!(await editSection(section)))
591
+ return null;
592
+ reviewRequested = true;
593
+ }
594
+ else if (action === 'back') {
595
+ index = Math.max(0, index - 1);
596
+ }
597
+ else if (action === 'review') {
598
+ reviewRequested = true;
599
+ }
600
+ else {
601
+ index += 1;
602
+ }
603
+ }
604
+ const reviewSummary = () => {
605
+ const unavailable = draft.knowledge.sources.map((source) => knownKnowledge.has(source) ? source : `${source} (retained/unverified)`);
606
+ return `Workspace root: ${context.root}\nMode: ${existingConfig === null ? 'new workspace' : 'editing existing settings'}\nName: ${draft.name}\nRepositories (workspace policy): ${draft.repositories.length === 0
607
+ ? '(none)'
608
+ : draft.repositories
609
+ .map((repository) => `${repository.path}=${repository.access}`)
610
+ .join(', ')}\nKnowledge sources (discover/register in place): ${unavailable.length === 0 ? '(none)' : unavailable.join(', ')}\nNew document destination (future authored documents only): ${draft.knowledge.newDocuments}\nDocument policy: ${draft.knowledge.taskVisibility} (raw evidence/checkpoints remain private)\nAdvanced: graph=${draft.code.graph}, symbols=${draft.code.symbols}, prepare=${draft.code.prepare}, validation=${draft.validation.length === 0 ? '(none)' : draft.validation.join(', ')}, hosts=${draft.hosts.length === 0 ? '(none)' : draft.hosts.join(', ')}, excludes=${draft.excludes.length === 0 ? '(none)' : draft.excludes.join(', ')}\nThis is a read-only draft. A detailed file/runtime plan and final apply confirmation follow; review does not authorize writes.`;
611
+ };
612
+ while (true) {
613
+ ui.note(reviewSummary(), 'Review selections');
614
+ const action = await promptValue(ui.select('Edit a section or continue to the detailed plan', [
615
+ { value: 'continue', label: 'Continue to plan' },
616
+ { value: 'workspace', label: 'Edit workspace name' },
617
+ { value: 'repositories', label: 'Edit repositories' },
618
+ { value: 'knowledge', label: 'Edit knowledge sources/destination' },
619
+ { value: 'advanced', label: 'Edit advanced settings' },
620
+ { value: 'cancel', label: 'Cancel' },
621
+ ], 'continue'), ui);
622
+ if (action === null)
623
+ return null;
624
+ if (action === 'cancel') {
625
+ ui.cancel('Init cancelled; no files were written.');
626
+ return null;
627
+ }
628
+ if (action === 'continue') {
629
+ collectedPathObservations.set(draft, [...pathObservations.values()]);
630
+ ui.outro('Selections complete. Review the plan before applying.');
631
+ return draft;
632
+ }
633
+ if (!(await editSection(action)))
634
+ return null;
635
+ }
636
+ collectedPathObservations.set(draft, [...pathObservations.values()]);
637
+ ui.outro('Selections complete. Review the plan before applying.');
638
+ return draft;
639
+ }
640
+ /** Describes the runtime work this invocation would perform without touching it. */
641
+ export function describeInitRuntimeOperation(options, bundle, dataDir) {
642
+ const targetVersion = bundle.scope.runtimeVersion;
643
+ const runtimeRoot = runtimePaths(targetVersion, dataDir).runtimeRoot;
644
+ const machineBindingPath = join(bundle.scope.workspaceRoot, '.pappra', 'local', 'machine.json');
645
+ const packageInfo = getPackageInfo();
646
+ if (options.installRuntime === true) {
647
+ const tarball = options.runtimeTarball ?? null;
648
+ const sourceVersion = options.runtimeVersion ?? targetVersion;
649
+ const versionMismatch = sourceVersion !== targetVersion;
650
+ const source = tarball === null
651
+ ? `${packageInfo.name}@${sourceVersion}`
652
+ : `tarball ${resolve(options.cwd, tarball)} for ${packageInfo.name}@${sourceVersion}`;
653
+ const sourceKind = tarball === null ? 'version' : 'tarball';
654
+ const previewLines = [
655
+ `Explicit ${sourceKind} source: ${source}.`,
656
+ `Workspace target: ${targetVersion} at ${runtimeRoot}.`,
657
+ `Machine runtime binding: ${machineBindingPath}.`,
658
+ versionMismatch
659
+ ? `The requested version ${sourceVersion} does not match the workspace target; after config apply the runtime request will be rejected before installation.`
660
+ : 'On apply, run the package manager and existing runtime verification before recording the machine binding; dependency resolution may require network access.',
661
+ 'This preview does not install, pack, probe or write runtime state.',
662
+ ];
663
+ const confirmationMessage = versionMismatch
664
+ ? `Apply the planned workspace files? The runtime request ${source} does not match workspace target ${targetVersion} and will fail before installation.`
665
+ : `Apply the planned workspace files, then install/verify ${source} at ${runtimeRoot} and bind it in ${machineBindingPath}? Package-manager work may require network access; optional preferences remain deferred.`;
666
+ return {
667
+ mode: sourceKind,
668
+ runtimeRequested: true,
669
+ targetVersion,
670
+ source,
671
+ runtimeRoot,
672
+ machineBindingPath,
673
+ previewLines,
674
+ confirmationMessage,
675
+ };
676
+ }
677
+ if (options.noRuntime === true) {
678
+ return {
679
+ mode: 'disabled',
680
+ runtimeRequested: false,
681
+ targetVersion,
682
+ source: null,
683
+ runtimeRoot: null,
684
+ machineBindingPath,
685
+ previewLines: [
686
+ 'No runtime installation, verification or binding operation will run (--no-runtime).',
687
+ 'The planned workspace/machine configuration files may still be applied; existing runtime files are not written or bound by this invocation.',
688
+ ],
689
+ confirmationMessage: 'Apply the planned workspace/machine configuration files only? No runtime operation will run (--no-runtime); knowledge registration, code indexing/preparation and host integration remain deferred.',
690
+ };
691
+ }
692
+ const source = `current package ${packageInfo.name}@${packageInfo.version} at ${getPackageRoot()}`;
693
+ const versionMismatch = packageInfo.version !== targetVersion;
694
+ const previewLines = [
695
+ `Default self-package source: ${source}.`,
696
+ `Workspace target: ${targetVersion} at ${runtimeRoot}.`,
697
+ `Machine runtime binding: ${machineBindingPath}.`,
698
+ versionMismatch
699
+ ? `The current package version ${packageInfo.version} differs from workspace target ${targetVersion}. A verified runtime for this workspace target may still be reused. If no verified runtime can be reused, the current package source cannot satisfy this target and runtime setup will fail after config apply.`
700
+ : 'On apply, first verify/reuse an existing runtime for the workspace target. Reuse occurs only if the manifest, package contents, executable and smoke check pass; otherwise the current package may be packed and installed.',
701
+ 'This preview does not pack, install, run smoke/version probes or write user-data state; it cannot promise reuse.',
702
+ ];
703
+ const confirmationMessage = versionMismatch
704
+ ? `Apply the planned workspace files, then first verify/reuse a runtime for the workspace target ${targetVersion} at ${runtimeRoot} and bind it in ${machineBindingPath}? A previously installed verified target runtime may still be reused. If no verified target runtime is reusable, the current package cannot satisfy this target; runtime setup will fail after config apply. Knowledge registration, code indexing/preparation and host integration remain deferred.`
705
+ : `Apply the planned workspace files, then verify/reuse a runtime for the workspace target ${targetVersion} at ${runtimeRoot}, or install ${source} if needed, and bind it in ${machineBindingPath}? Reuse requires the manifest, package contents, executable and smoke check to pass; package-manager and smoke-verification work may run. Knowledge registration, code indexing/preparation and host integration remain deferred.`;
706
+ return {
707
+ mode: 'default-self',
708
+ runtimeRequested: true,
709
+ targetVersion,
710
+ source,
711
+ runtimeRoot,
712
+ machineBindingPath,
713
+ previewLines,
714
+ confirmationMessage,
239
715
  };
240
716
  }
241
717
  function runtimeOptionsFailure(message, nextAction) {
@@ -411,7 +887,26 @@ function diagnosticLines(diagnostics) {
411
887
  .map((entry) => ` ${entry.severity} ${entry.code}: ${entry.message}${entry.hint === null ? '' : ` (${entry.hint})`}`)
412
888
  .join('\n');
413
889
  }
414
- function printPlan(output, bundle) {
890
+ function deferredInitWork(bundle) {
891
+ const knowledge = bundle.scope.knowledge.sources.length === 0
892
+ ? 'No knowledge documents are registered by init.'
893
+ : 'Knowledge sources are recorded in workspace settings only; init does not scan or register catalog entries.';
894
+ const code = `Code preferences (graph=${bundle.scope.code.graph}, symbols=${bundle.scope.code.symbols}, prepare=${bundle.scope.code.prepare}) are declarations only; init does not install, index or prepare tools.`;
895
+ const validation = bundle.scope.validation.length === 0
896
+ ? 'No validation command is executed by init.'
897
+ : 'Selected validation commands are recorded only; init does not execute them.';
898
+ const hosts = bundle.scope.hosts.length === 0
899
+ ? 'No host is declared; init does not detect, install or wire hosts.'
900
+ : `Host declarations (${bundle.scope.hosts.join(', ')}) are not probed, installed or wired by init.`;
901
+ return [
902
+ knowledge,
903
+ 'The new-document destination is for future authored documents and is not created by init.',
904
+ code,
905
+ validation,
906
+ hosts,
907
+ ];
908
+ }
909
+ function printPlan(output, bundle, runtimeOperation) {
415
910
  output.stdout(`Pappra init plan ${bundle.plan.planId}\n`);
416
911
  output.stdout(`Workspace: ${bundle.plan.workspaceRoot}\n`);
417
912
  output.stdout(`Config source: ${bundle.scope.configSource}\n`);
@@ -423,15 +918,30 @@ function printPlan(output, bundle) {
423
918
  for (const file of bundle.plan.files) {
424
919
  output.stdout(` ${file.operation} ${file.path} before=${file.expectedBeforeDigest ?? 'absent'} after=${file.nextDigest}\n`);
425
920
  }
426
- output.stdout('External actions (not executed):\n');
427
- if (bundle.plan.externalActions.length === 0) {
921
+ const externalActions = runtimeOperation.mode === 'disabled'
922
+ ? bundle.plan.externalActions.filter((action) => action.kind !== 'install-runtime')
923
+ : bundle.plan.externalActions;
924
+ output.stdout('Plan external actions (not executed during planning):\n');
925
+ if (externalActions.length === 0) {
428
926
  output.stdout(' (none)\n');
429
927
  }
430
928
  else {
431
- for (const action of bundle.plan.externalActions) {
929
+ for (const action of externalActions) {
432
930
  output.stdout(` ${action.kind} ${action.target}${action.version === null ? '' : `@${action.version}`}\n`);
433
931
  }
434
932
  }
933
+ if (runtimeOperation.mode === 'disabled' &&
934
+ bundle.plan.externalActions.some((action) => action.kind === 'install-runtime')) {
935
+ output.stdout(' install-runtime is suppressed by --no-runtime for this invocation.\n');
936
+ }
937
+ output.stdout('\nRuntime operation:\n');
938
+ for (const line of runtimeOperation.previewLines) {
939
+ output.stdout(` ${line}\n`);
940
+ }
941
+ output.stdout('Deferred init work:\n');
942
+ for (const line of deferredInitWork(bundle)) {
943
+ output.stdout(` ${line}\n`);
944
+ }
435
945
  if (bundle.diagnostics.length > 0) {
436
946
  output.stderr(`${diagnosticLines(bundle.diagnostics)}\n`);
437
947
  }
@@ -444,7 +954,7 @@ function printPlan(output, bundle) {
444
954
  output.stdout('No file changes are planned.\n');
445
955
  }
446
956
  }
447
- function printApply(output, result, runtimeRequested) {
957
+ function printApply(output, result, runtimeOperation) {
448
958
  if (!result.ok) {
449
959
  output.stderr(`pappra init apply: ${result.status}\n`);
450
960
  output.stderr(`${diagnosticLines(result.diagnostics)}\n`);
@@ -456,14 +966,22 @@ function printApply(output, result, runtimeRequested) {
456
966
  output.stderr(`Residual planned files require manual inspection: ${result.residualPaths.join(', ')}\n`);
457
967
  }
458
968
  if (result.pendingExternalActions.length > 0) {
459
- output.stderr('Pending external actions (not executed by P03):\n');
969
+ output.stderr('Pending plan actions after the file-apply phase:\n');
460
970
  for (const action of result.pendingExternalActions) {
461
971
  output.stderr(` ${action.kind} ${action.target}${action.version === null ? '' : `@${action.version}`}\n`);
462
972
  }
463
- output.stderr(runtimeRequested
464
- ? 'The explicit runtime action is still pending; it will be attempted next.\n'
465
- : 'Only planned config files were applied. Initialization is not complete until runtime/provider actions are authorized and executed.\n');
466
973
  }
974
+ output.stdout(runtimeOperation.mode === 'disabled'
975
+ ? 'Runtime operation: intentionally deferred by --no-runtime; none was run.\n'
976
+ : 'Runtime operation: scheduled after file application for verification/reuse or installation and machine binding.\n');
977
+ }
978
+ function workspaceApplySummary(bundle, result) {
979
+ if (result.appliedFiles.includes('.pappra/workspace.json')) {
980
+ return 'Workspace configuration applied.';
981
+ }
982
+ return bundle.scope.configSource === 'existing'
983
+ ? 'Existing workspace configuration reused.'
984
+ : 'Workspace configuration unchanged.';
467
985
  }
468
986
  function advisoryNextActions(bundle) {
469
987
  const actions = [];
@@ -478,13 +996,20 @@ function advisoryNextActions(bundle) {
478
996
  }
479
997
  return actions;
480
998
  }
481
- function planJson(bundle) {
999
+ function initNextActions(bundle, runtimeOperation) {
1000
+ const actions = [...advisoryNextActions(bundle)];
1001
+ if (runtimeOperation.mode === 'disabled') {
1002
+ actions.unshift('Runtime operation was intentionally deferred by --no-runtime; rerun init --apply without --no-runtime to verify/reuse or install and bind the runtime.');
1003
+ }
1004
+ return actions;
1005
+ }
1006
+ function planJson(bundle, runtimeOperation) {
482
1007
  return {
483
1008
  command: 'init',
484
1009
  mode: 'plan',
485
1010
  plan: bundle.plan,
486
1011
  scope: bundle.scope,
487
- nextActions: advisoryNextActions(bundle),
1012
+ nextActions: initNextActions(bundle, runtimeOperation),
488
1013
  diagnostics: bundle.diagnostics,
489
1014
  };
490
1015
  }
@@ -548,8 +1073,59 @@ export async function runInitCommand(options, output, deps = {}) {
548
1073
  return writeFailure(output, options, contextResult);
549
1074
  }
550
1075
  const context = contextResult.context;
1076
+ const recoverablePlanCodes = new Set([
1077
+ diagnosticCodes.initValidationUnbound,
1078
+ diagnosticCodes.initValidationUnknown,
1079
+ diagnosticCodes.initKnowledgeUnknown,
1080
+ diagnosticCodes.initRepositoryUnknown,
1081
+ diagnosticCodes.initRepositoryDuplicate,
1082
+ ]);
551
1083
  let choices = null;
552
1084
  if (printChoices) {
1085
+ const existingConfig = context.workspaceInspection.value;
1086
+ if (options.reconfigure === true && existingConfig === null) {
1087
+ return writeFailure(output, options, {
1088
+ ok: false,
1089
+ status: 'needs_input',
1090
+ code: diagnosticCodes.initInputRequired,
1091
+ message: 'Reconfiguration requires an existing valid workspace configuration.',
1092
+ diagnostics: context.diagnostics,
1093
+ nextAction: 'Run pappra init without --reconfigure to create a workspace, or repair the existing workspace configuration first.',
1094
+ });
1095
+ }
1096
+ if (options.reconfigure === true && existingConfig !== null) {
1097
+ const existing = existingInitChoices(existingConfig, context.machineInspection.value);
1098
+ const rules = [
1099
+ {
1100
+ field: 'source',
1101
+ value: 'existing workspace.json/machine.json',
1102
+ reason: 'reconfiguration edits current represented selections; extensions and continuity are preserved by planning',
1103
+ },
1104
+ ];
1105
+ const candidates = {
1106
+ repositories: listRepositoryCandidates(context.discovery).map((candidate) => candidate.key),
1107
+ knowledge: listKnowledgeCandidates(context.discovery).map((candidate) => candidate.path),
1108
+ validation: context.discovery.validationCandidates.map((candidate) => candidate.id),
1109
+ hosts: existing.hosts,
1110
+ };
1111
+ if (options.json) {
1112
+ output.stdout(`${JSON.stringify({
1113
+ ok: true,
1114
+ command: 'init',
1115
+ mode: 'choices',
1116
+ choices: existing,
1117
+ rules,
1118
+ candidates,
1119
+ })}\n`);
1120
+ }
1121
+ else {
1122
+ output.stdout(`${JSON.stringify(existing, null, 2)}\n`);
1123
+ for (const rule of rules) {
1124
+ output.stderr(` ${rule.field} = ${rule.value} (${rule.reason})\n`);
1125
+ }
1126
+ }
1127
+ return 0;
1128
+ }
553
1129
  const derived = await deriveInitChoices(context.discovery);
554
1130
  if (options.json) {
555
1131
  output.stdout(`${JSON.stringify({
@@ -576,54 +1152,124 @@ export async function runInitCommand(options, output, deps = {}) {
576
1152
  choices = loaded.choices;
577
1153
  }
578
1154
  else if (yes) {
579
- choices = (await deriveInitChoices(context.discovery)).choices;
1155
+ if (options.reconfigure === true) {
1156
+ const existingConfig = context.workspaceInspection.value;
1157
+ if (existingConfig === null) {
1158
+ return writeFailure(output, options, {
1159
+ ok: false,
1160
+ status: 'needs_input',
1161
+ code: diagnosticCodes.initInputRequired,
1162
+ message: 'Reconfiguration requires an existing valid workspace configuration.',
1163
+ diagnostics: context.diagnostics,
1164
+ nextAction: 'Run pappra init without --reconfigure to create a workspace, or repair the existing workspace configuration first.',
1165
+ });
1166
+ }
1167
+ choices = existingInitChoices(existingConfig, context.machineInspection.value);
1168
+ }
1169
+ else {
1170
+ choices = (await deriveInitChoices(context.discovery)).choices;
1171
+ }
580
1172
  }
581
1173
  else {
582
- const needsChoices = context.discovery.configState !== 'valid' ||
1174
+ const needsChoices = options.reconfigure === true ||
1175
+ context.discovery.configState !== 'valid' ||
583
1176
  context.discovery.machineState !== 'valid';
584
1177
  if (needsChoices && !interactive) {
585
- const derived = await deriveInitChoices(context.discovery);
1178
+ const existingConfig = context.workspaceInspection.value;
1179
+ const suggested = options.reconfigure === true && existingConfig !== null
1180
+ ? existingInitChoices(existingConfig, context.machineInspection.value)
1181
+ : (await deriveInitChoices(context.discovery)).choices;
1182
+ const candidates = {
1183
+ repositories: listRepositoryCandidates(context.discovery).map((candidate) => candidate.key),
1184
+ knowledge: listKnowledgeCandidates(context.discovery).map((candidate) => candidate.path),
1185
+ validation: context.discovery.validationCandidates.map((candidate) => candidate.id),
1186
+ hosts: suggested.hosts,
1187
+ };
586
1188
  return writeFailure(output, options, {
587
1189
  ok: false,
588
1190
  status: 'needs_input',
589
1191
  code: diagnosticCodes.initNoTty,
590
- message: 'Missing init selections in a non-interactive session.',
1192
+ message: options.reconfigure === true
1193
+ ? 'Reconfiguration requires editing existing selections in a non-interactive session.'
1194
+ : 'Missing init selections in a non-interactive session.',
591
1195
  diagnostics: context.diagnostics,
592
- nextAction: 'Rerun with --yes to apply the derived selections, provide --config with pappra/init-choices/1, or run in a TTY for the interactive wizard.',
1196
+ nextAction: options.reconfigure === true
1197
+ ? 'Rerun with --print-choices --reconfigure to inspect current selections, --yes --reconfigure --apply to keep them, --config with pappra/init-choices/1 and --reconfigure for explicit edits, or use a TTY for the wizard.'
1198
+ : 'Rerun with --print-choices to inspect derived selections, --yes --apply to write them, --config with pappra/init-choices/1, or use a TTY for the wizard.',
593
1199
  details: {
594
- candidates: derived.candidates,
595
- suggestedChoices: derived.choices,
596
- rules: derived.rules,
1200
+ candidates,
1201
+ suggestedChoices: suggested,
1202
+ rules: [],
597
1203
  },
598
1204
  });
599
1205
  }
600
1206
  if (needsChoices && interactive) {
601
- choices = await collectInitChoices(context, deps.ui ?? clackWizardUi);
1207
+ choices = await collectInitChoices(context, deps.ui ?? clackWizardUi, {
1208
+ ...(options.reconfigure === true ? { reconfigure: true } : {}),
1209
+ });
602
1210
  if (choices === null) {
603
1211
  output.stderr('Init cancelled; no files were written.\n');
604
1212
  return 130;
605
1213
  }
606
1214
  }
607
1215
  }
608
- const prepared = await planWorkspaceInit({
1216
+ let prepared = await planWorkspaceInit({
609
1217
  root: context.root,
610
1218
  choices,
611
1219
  context,
1220
+ pathObservations: choices === null ? [] : (collectedPathObservations.get(choices) ?? []),
612
1221
  ...(options.reconfigure === true ? { reconfigure: true } : {}),
613
1222
  });
1223
+ const recoverableChoiceError = !prepared.ok &&
1224
+ interactive &&
1225
+ choices !== null &&
1226
+ prepared.diagnostics.some((entry) => entry.severity === 'error' && recoverablePlanCodes.has(entry.code));
1227
+ if (recoverableChoiceError && prepared.ok === false) {
1228
+ const recoverable = prepared;
1229
+ const retained = choices;
1230
+ output.stderr(`pappra init: ${recoverable.message}\n${diagnosticLines(recoverable.diagnostics)}\n`);
1231
+ output.stderr(' next: Return to the editor to correct the reported selections, or cancel; no files were written.\n');
1232
+ const correction = await collectInitChoices(context, deps.ui ?? clackWizardUi, {
1233
+ ...(options.reconfigure === true ? { reconfigure: true } : {}),
1234
+ ...(retained === null ? {} : { draft: retained }),
1235
+ reviewOnly: true,
1236
+ pathObservations: retained === null
1237
+ ? []
1238
+ : (collectedPathObservations.get(retained) ?? []),
1239
+ });
1240
+ if (correction === null) {
1241
+ output.stderr('Init cancelled; no files were written.\n');
1242
+ return 130;
1243
+ }
1244
+ if (retained !== null &&
1245
+ JSON.stringify(correction) === JSON.stringify(retained)) {
1246
+ return formatFailure(recoverable);
1247
+ }
1248
+ choices = correction;
1249
+ prepared = await planWorkspaceInit({
1250
+ root: context.root,
1251
+ choices,
1252
+ context,
1253
+ pathObservations: collectedPathObservations.get(correction) ?? [],
1254
+ ...(options.reconfigure === true ? { reconfigure: true } : {}),
1255
+ });
1256
+ if (!prepared.ok)
1257
+ return writeFailure(output, options, prepared);
1258
+ }
614
1259
  if (!prepared.ok)
615
1260
  return writeFailure(output, options, prepared);
616
1261
  const bundle = prepared.bundle;
1262
+ const runtimeOperation = describeInitRuntimeOperation(options, bundle);
617
1263
  if (options.json) {
618
1264
  if (!options.apply) {
619
1265
  output.stdout(`${JSON.stringify({
620
1266
  ok: true,
621
- ...planJson(bundle),
1267
+ ...planJson(bundle, runtimeOperation),
622
1268
  })}\n`);
623
1269
  return 0;
624
1270
  }
625
1271
  const applied = await applyInitPlan(bundle);
626
- const runtimeRequested = wantsRuntime || options.noRuntime !== true;
1272
+ const runtimeRequested = runtimeOperation.runtimeRequested;
627
1273
  const runtimeFlow = runtimeRequested && applied.ok && applied.residualPaths.length === 0
628
1274
  ? wantsRuntime
629
1275
  ? await installAndRecordRuntime(bundle.scope.workspaceRoot, bundle.scope.runtimeVersion, options)
@@ -642,32 +1288,36 @@ export async function runInitCommand(options, output, deps = {}) {
642
1288
  apply: applied,
643
1289
  runtime: runtimeFlow?.runtime ?? null,
644
1290
  initComplete,
645
- nextActions: advisoryNextActions(bundle),
1291
+ nextActions: initNextActions(bundle, runtimeOperation),
646
1292
  diagnostics: [
647
1293
  ...bundle.diagnostics,
648
1294
  ...(runtimeFlow?.diagnostics ?? []),
649
1295
  ],
650
1296
  })}\n`);
651
- if (!applied.ok || applied.residualPaths.length > 0)
1297
+ if (!applied.ok)
1298
+ return applied.status === 'failed' ? 3 : 1;
1299
+ if (applied.residualPaths.length > 0)
652
1300
  return 1;
653
1301
  if (runtimeRequested)
654
1302
  return ok ? 0 : 1;
655
- return applied.status === 'external-actions-pending' ? 1 : 0;
1303
+ return 0;
656
1304
  }
657
- printPlan(output, bundle);
1305
+ printPlan(output, bundle, runtimeOperation);
658
1306
  if (!options.apply) {
659
1307
  if (!interactive || bundle.plan.files.length === 0) {
660
- output.stdout('Plan only; no files were written. Re-run with --apply to execute the file changes.\n');
1308
+ output.stdout(runtimeOperation.runtimeRequested
1309
+ ? 'Plan only; no files or runtime operations were performed. Re-run with --apply to apply files and attempt the described runtime operation, even if no files change.\n'
1310
+ : 'Plan only; no files or runtime operations were performed. Re-run with --apply to apply files; --no-runtime will continue to suppress runtime work.\n');
661
1311
  return 0;
662
1312
  }
663
- const confirmed = await promptValue((deps.ui ?? clackWizardUi).confirm('Apply this file plan? External actions will remain pending.', false), deps.ui ?? clackWizardUi);
1313
+ const confirmed = await promptValue((deps.ui ?? clackWizardUi).confirm(runtimeOperation.confirmationMessage, false), deps.ui ?? clackWizardUi);
664
1314
  if (confirmed !== true) {
665
1315
  output.stderr('Init cancelled; no files were written.\n');
666
1316
  return 130;
667
1317
  }
668
1318
  }
669
1319
  const applied = await applyInitPlan(bundle);
670
- printApply(output, applied, wantsRuntime || options.noRuntime !== true);
1320
+ printApply(output, applied, runtimeOperation);
671
1321
  if (!applied.ok) {
672
1322
  return applied.status === 'failed' ? 3 : 1;
673
1323
  }
@@ -675,50 +1325,65 @@ export async function runInitCommand(options, output, deps = {}) {
675
1325
  output.stderr('Managed apply state is not final; runtime actions will not be attempted.\n');
676
1326
  return 1;
677
1327
  }
678
- if (wantsRuntime) {
679
- const runtimeFlow = await installAndRecordRuntime(bundle.scope.workspaceRoot, bundle.scope.runtimeVersion, options);
680
- if (runtimeFlow.runtime !== null) {
681
- output.stdout(`Runtime: ${runtimeFlow.runtime.status} ${runtimeFlow.runtime.version}${runtimeFlow.runtime.runtimeRoot === null
682
- ? ''
683
- : ` at ${runtimeFlow.runtime.runtimeRoot}`}\n`);
684
- if (runtimeFlow.runtime.residualPaths.length > 0) {
685
- output.stderr(`Runtime residual paths: ${runtimeFlow.runtime.residualPaths.join(', ')}\n`);
686
- }
687
- }
688
- if (!runtimeFlow.ok || !runtimeFlow.completion) {
689
- output.stderr(`${diagnosticLines(runtimeFlow.diagnostics)}\n`);
690
- output.stderr('Runtime installation or machine binding did not complete. Initialization is not complete.\n');
691
- return 1;
1328
+ output.stdout(`${workspaceApplySummary(bundle, applied)}\n`);
1329
+ if (!runtimeOperation.runtimeRequested) {
1330
+ for (const line of deferredInitWork(bundle))
1331
+ output.stdout(` ${line}\n`);
1332
+ for (const action of initNextActions(bundle, runtimeOperation)) {
1333
+ output.stdout(` next: ${action}\n`);
692
1334
  }
693
- output.stdout('Runtime installed and verified. Initialization complete for P04 scope; provider/index preferences were not executed.\n');
694
1335
  return 0;
695
1336
  }
696
- if (options.noRuntime !== true) {
697
- const runtimeFlow = await ensureDefaultRuntime(bundle.scope.workspaceRoot, bundle.scope.runtimeVersion);
698
- if (runtimeFlow.runtime !== null) {
699
- output.stdout(`Runtime: ${runtimeFlow.runtime.status} ${runtimeFlow.runtime.version}${runtimeFlow.runtime.runtimeRoot === null
700
- ? ''
701
- : ` at ${runtimeFlow.runtime.runtimeRoot}`}\n`);
702
- if (runtimeFlow.runtime.residualPaths.length > 0) {
703
- output.stderr(`Runtime residual paths: ${runtimeFlow.runtime.residualPaths.join(', ')}\n`);
704
- }
1337
+ const runtimeFlow = wantsRuntime
1338
+ ? await installAndRecordRuntime(bundle.scope.workspaceRoot, bundle.scope.runtimeVersion, options)
1339
+ : await ensureDefaultRuntime(bundle.scope.workspaceRoot, bundle.scope.runtimeVersion);
1340
+ const runtime = runtimeFlow.runtime;
1341
+ if (runtime !== null) {
1342
+ output.stdout(`Runtime operation result: ${runtime.status} ${runtime.version}${runtime.runtimeRoot === null ? '' : ` at ${runtime.runtimeRoot}`}\n`);
1343
+ if (runtime.residualPaths.length > 0) {
1344
+ output.stderr(`Runtime residual paths: ${runtime.residualPaths.join(', ')}\n`);
705
1345
  }
706
- if (!runtimeFlow.ok || !runtimeFlow.completion) {
1346
+ }
1347
+ if (!runtimeFlow.ok || !runtimeFlow.completion) {
1348
+ if (runtimeFlow.diagnostics.length > 0) {
707
1349
  output.stderr(`${diagnosticLines(runtimeFlow.diagnostics)}\n`);
708
- output.stderr('The durable runtime copy could not be installed or bound. Initialization is not complete; --no-runtime leaves this action pending intentionally.\n');
709
- return 1;
710
- }
711
- output.stdout('Durable runtime copy installed and bound. Initialization complete.\n');
712
- for (const action of advisoryNextActions(bundle)) {
713
- output.stdout(` next: ${action}\n`);
714
1350
  }
715
- return 0;
1351
+ output.stderr('Runtime verification/installation or machine binding did not complete. The workspace configuration state reported above remains in place; initialization is incomplete and no rollback was attempted.\n');
1352
+ for (const line of deferredInitWork(bundle))
1353
+ output.stdout(` ${line}\n`);
1354
+ return 1;
1355
+ }
1356
+ output.stdout(runtime?.status === 'reused'
1357
+ ? 'Existing runtime was verified, reused and bound. Initialization complete.\n'
1358
+ : 'Runtime was installed, verified and bound. Initialization complete.\n');
1359
+ for (const line of deferredInitWork(bundle))
1360
+ output.stdout(` ${line}\n`);
1361
+ for (const action of initNextActions(bundle, runtimeOperation)) {
1362
+ output.stdout(` next: ${action}\n`);
716
1363
  }
717
- return applied.status === 'external-actions-pending' ? 1 : 0;
1364
+ return 0;
718
1365
  }
719
- export const initUsage = 'pappra init [--config <file>] [--apply] [--json]\n' +
1366
+ export const initUsage = 'pappra init [--config <file> | --yes | --no-interactive] [--print-choices]\n' +
1367
+ ' [--apply] [--json] [--reconfigure] [--no-runtime]\n' +
720
1368
  ' [--install-runtime (--runtime-tarball <file> | --runtime-version <version>)]\n\n' +
721
- 'Without --config, a TTY opens the interactive wizard. --config reads pappra/init-choices/1 JSON.\n' +
1369
+ 'Without --config or --yes, a TTY opens the wizard when selections are still needed: a valid\n' +
1370
+ 'existing configuration is reused (only missing local state is requested) and --reconfigure\n' +
1371
+ 'opens the editor. --config reads a pappra/init-choices/1 document and stays preview-only\n' +
1372
+ 'without --apply, even on a TTY. --yes and --no-interactive derive selections from discovery.\n' +
722
1373
  'Planning and preview never write. --apply creates planned files through a managed journal.\n' +
1374
+ '--print-choices prints the selections and writes nothing; with --reconfigure it prints the\n' +
1375
+ 'current represented choices instead of newly derived ones, and --yes --reconfigure keeps\n' +
1376
+ 'those current choices rather than deriving them again.\n' +
1377
+ '--reconfigure is the explicit opt-in that rewrites an existing valid workspace.json from the\n' +
1378
+ 'current selections (identity, extensions, continuity and custom validation bindings are kept).\n' +
1379
+ 'Without --reconfigure an existing valid configuration is reused and never overwritten.\n' +
723
1380
  '--install-runtime installs one explicit exact version or local tarball; provider/index/global\n' +
724
- 'wiring remain separate pending actions.\n';
1381
+ 'wiring remain separate pending actions.\n' +
1382
+ '--no-runtime suppresses runtime verification, installation and binding for this invocation.\n' +
1383
+ 'When a runtime action is still required the result is ok:true with status\n' +
1384
+ 'external-actions-pending and exit 0 for the requested configuration-only work; when the workspace already has a bound matching\n' +
1385
+ 'runtime and no file changes are needed the result is ok:true, apply status no-changes and\n' +
1386
+ 'exit 0, while initComplete stays false because this invocation verified no runtime.\n' +
1387
+ 'A TTY run that collected selections without --apply prints the plan and asks one confirmation\n' +
1388
+ 'when config writes are planned; declining writes nothing. --config previews and empty plans\n' +
1389
+ 'do not prompt, and --apply never prompts.\n';