create-principles-disciple 1.144.27 → 1.145.1

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 (347) hide show
  1. package/LICENSE +73 -21
  2. package/NOTICE +2 -0
  3. package/README.md +1 -1
  4. package/_release/product-identity.json +2 -2
  5. package/codex-adapter/dist/index.d.ts +2 -2
  6. package/codex-adapter/dist/index.js +1 -1
  7. package/codex-adapter/dist/ingestion/admission.js +16 -0
  8. package/codex-adapter/dist/ingestion/codex-version.d.ts +10 -4
  9. package/codex-adapter/dist/ingestion/codex-version.js +10 -4
  10. package/codex-adapter/dist/ingestion/explicit-owner-statement.d.ts +48 -0
  11. package/codex-adapter/dist/ingestion/explicit-owner-statement.js +123 -0
  12. package/codex-adapter/dist/ingestion/ingestion.js +50 -1
  13. package/codex-adapter/dist/pd-hook.js +66 -3
  14. package/codex-adapter/dist/worker/workspace-worker.d.ts +14 -0
  15. package/codex-adapter/dist/worker/workspace-worker.js +194 -11
  16. package/codex-adapter/package.json +6 -4
  17. package/console/dist/server/models/ApprovalsConsoleModel.d.ts +2 -2
  18. package/console/dist/server/models/ApprovalsConsoleModel.js +1 -1
  19. package/console/dist/server/models/ApprovalsGroupedConsoleModel.js +1 -1
  20. package/console/dist/server/models/PrinciplesConsoleModel.d.ts +26 -1
  21. package/console/dist/server/models/PrinciplesConsoleModel.js +18 -1
  22. package/console/dist/server/routes/activations.js +4 -4
  23. package/console/dist/server/routes/approvals.js +2 -2
  24. package/console/dist/server/routes/failed-tasks.d.ts +9 -0
  25. package/console/dist/server/routes/failed-tasks.js +116 -7
  26. package/console/dist/server/routes/principles.js +73 -1
  27. package/console/dist/shared/prompt-injection-contract.d.ts +1 -1
  28. package/console/dist/ui/api.d.ts +5 -5
  29. package/console/dist/ui/i18n/en.json +13 -2
  30. package/console/dist/ui/i18n/zh-CN.json +13 -2
  31. package/console/dist/ui/pages/activation/ActivationPage.js +2 -1
  32. package/console/dist/ui/pages/failed-tasks/FailedTasksPage.js +42 -4
  33. package/console/dist/ui/pages/focus/FocusPage.js +3 -3
  34. package/console/dist/ui/pages/principles/PrinciplesPage.js +7 -2
  35. package/console/dist/ui/utils/validators.d.ts +6 -2
  36. package/console/dist/ui/utils/validators.js +19 -4
  37. package/console/dist/web/assets/app.css +26 -0
  38. package/console/dist/web/assets/app.js +24 -24
  39. package/console/package.json +6 -5
  40. package/core/dist/host/host-installer.d.ts +3 -1
  41. package/core/dist/host/host-installer.d.ts.map +1 -1
  42. package/core/dist/host/host-installer.js.map +1 -1
  43. package/core/dist/runtime-v2/admission-gate.d.ts +8 -0
  44. package/core/dist/runtime-v2/admission-gate.d.ts.map +1 -1
  45. package/core/dist/runtime-v2/admission-gate.js +3 -2
  46. package/core/dist/runtime-v2/admission-gate.js.map +1 -1
  47. package/core/dist/runtime-v2/config/pd-config-defaults.d.ts.map +1 -1
  48. package/core/dist/runtime-v2/config/pd-config-defaults.js +16 -1
  49. package/core/dist/runtime-v2/config/pd-config-defaults.js.map +1 -1
  50. package/core/dist/runtime-v2/context-payload.d.ts +43 -3
  51. package/core/dist/runtime-v2/context-payload.d.ts.map +1 -1
  52. package/core/dist/runtime-v2/context-payload.js +15 -1
  53. package/core/dist/runtime-v2/context-payload.js.map +1 -1
  54. package/core/dist/runtime-v2/evidence-guards.d.ts +6 -0
  55. package/core/dist/runtime-v2/evidence-guards.d.ts.map +1 -1
  56. package/core/dist/runtime-v2/evidence-guards.js +11 -0
  57. package/core/dist/runtime-v2/evidence-guards.js.map +1 -1
  58. package/core/dist/runtime-v2/evolution/evolution-types.d.ts +4 -4
  59. package/core/dist/runtime-v2/evolution/evolution-types.d.ts.map +1 -1
  60. package/core/dist/runtime-v2/evolution/evolution-types.js +1 -1
  61. package/core/dist/runtime-v2/evolution/evolution-types.js.map +1 -1
  62. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.d.ts.map +1 -1
  63. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js +6 -0
  64. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js.map +1 -1
  65. package/core/dist/runtime-v2/feature-flags/feature-flag-lifecycle.d.ts.map +1 -1
  66. package/core/dist/runtime-v2/feature-flags/feature-flag-lifecycle.js +8 -0
  67. package/core/dist/runtime-v2/feature-flags/feature-flag-lifecycle.js.map +1 -1
  68. package/core/dist/runtime-v2/index.d.ts +5 -2
  69. package/core/dist/runtime-v2/index.d.ts.map +1 -1
  70. package/core/dist/runtime-v2/index.js +3 -1
  71. package/core/dist/runtime-v2/index.js.map +1 -1
  72. package/core/dist/runtime-v2/internalization/evaluator-output.d.ts +11 -0
  73. package/core/dist/runtime-v2/internalization/evaluator-output.d.ts.map +1 -1
  74. package/core/dist/runtime-v2/internalization/evaluator-output.js +10 -0
  75. package/core/dist/runtime-v2/internalization/evaluator-output.js.map +1 -1
  76. package/core/dist/runtime-v2/internalization/evaluator-prompt-builder.d.ts +3 -3
  77. package/core/dist/runtime-v2/internalization/evaluator-prompt-builder.d.ts.map +1 -1
  78. package/core/dist/runtime-v2/internalization/evaluator-prompt-builder.js +49 -5
  79. package/core/dist/runtime-v2/internalization/evaluator-prompt-builder.js.map +1 -1
  80. package/core/dist/runtime-v2/internalization/evaluator-runner.d.ts.map +1 -1
  81. package/core/dist/runtime-v2/internalization/evaluator-runner.js +13 -16
  82. package/core/dist/runtime-v2/internalization/evaluator-runner.js.map +1 -1
  83. package/core/dist/runtime-v2/internalization/owner-decision-review.d.ts.map +1 -1
  84. package/core/dist/runtime-v2/internalization/owner-decision-review.js +5 -1
  85. package/core/dist/runtime-v2/internalization/owner-decision-review.js.map +1 -1
  86. package/core/dist/runtime-v2/internalization/owner-review.d.ts +2 -2
  87. package/core/dist/runtime-v2/internalization/owner-review.js +2 -2
  88. package/core/dist/runtime-v2/internalization/scribe-output.d.ts +12 -0
  89. package/core/dist/runtime-v2/internalization/scribe-output.d.ts.map +1 -1
  90. package/core/dist/runtime-v2/internalization/scribe-output.js +20 -0
  91. package/core/dist/runtime-v2/internalization/scribe-output.js.map +1 -1
  92. package/core/dist/runtime-v2/internalization/scribe-prompt-builder.d.ts +32 -1
  93. package/core/dist/runtime-v2/internalization/scribe-prompt-builder.d.ts.map +1 -1
  94. package/core/dist/runtime-v2/internalization/scribe-prompt-builder.js +38 -1
  95. package/core/dist/runtime-v2/internalization/scribe-prompt-builder.js.map +1 -1
  96. package/core/dist/runtime-v2/internalization-queue-read-model.d.ts +6 -0
  97. package/core/dist/runtime-v2/internalization-queue-read-model.d.ts.map +1 -1
  98. package/core/dist/runtime-v2/internalization-queue-read-model.js +8 -1
  99. package/core/dist/runtime-v2/internalization-queue-read-model.js.map +1 -1
  100. package/core/dist/runtime-v2/intervention-evidence-normalizer.d.ts.map +1 -1
  101. package/core/dist/runtime-v2/intervention-evidence-normalizer.js +10 -3
  102. package/core/dist/runtime-v2/intervention-evidence-normalizer.js.map +1 -1
  103. package/core/dist/runtime-v2/pain-ingress.d.ts +1 -1
  104. package/core/dist/runtime-v2/pain-ingress.d.ts.map +1 -1
  105. package/core/dist/runtime-v2/pain-signal-bridge.d.ts +68 -2
  106. package/core/dist/runtime-v2/pain-signal-bridge.d.ts.map +1 -1
  107. package/core/dist/runtime-v2/pain-signal-bridge.js +541 -18
  108. package/core/dist/runtime-v2/pain-signal-bridge.js.map +1 -1
  109. package/core/dist/runtime-v2/pain-signal-runtime-factory.d.ts.map +1 -1
  110. package/core/dist/runtime-v2/pain-signal-runtime-factory.js +4 -0
  111. package/core/dist/runtime-v2/pain-signal-runtime-factory.js.map +1 -1
  112. package/core/dist/runtime-v2/product-telemetry/snapshot-contract.d.ts +2 -2
  113. package/core/dist/runtime-v2/product-telemetry/snapshot-contract.d.ts.map +1 -1
  114. package/core/dist/runtime-v2/product-telemetry/snapshot-contract.js +2 -2
  115. package/core/dist/runtime-v2/product-telemetry/snapshot-contract.js.map +1 -1
  116. package/core/dist/runtime-v2/store/candidate/admission-document.d.ts +90 -0
  117. package/core/dist/runtime-v2/store/candidate/admission-document.d.ts.map +1 -0
  118. package/core/dist/runtime-v2/store/candidate/admission-document.js +142 -0
  119. package/core/dist/runtime-v2/store/candidate/admission-document.js.map +1 -0
  120. package/core/dist/runtime-v2/store/candidate/candidate-store.d.ts +53 -0
  121. package/core/dist/runtime-v2/store/candidate/candidate-store.d.ts.map +1 -1
  122. package/core/dist/runtime-v2/store/candidate/memory-candidate-store.d.ts +35 -0
  123. package/core/dist/runtime-v2/store/candidate/memory-candidate-store.d.ts.map +1 -1
  124. package/core/dist/runtime-v2/store/candidate/memory-candidate-store.js +48 -0
  125. package/core/dist/runtime-v2/store/candidate/memory-candidate-store.js.map +1 -1
  126. package/core/dist/runtime-v2/store/candidate/sqlite-candidate-store.d.ts +44 -0
  127. package/core/dist/runtime-v2/store/candidate/sqlite-candidate-store.d.ts.map +1 -1
  128. package/core/dist/runtime-v2/store/candidate/sqlite-candidate-store.js +136 -0
  129. package/core/dist/runtime-v2/store/candidate/sqlite-candidate-store.js.map +1 -1
  130. package/core/dist/runtime-v2/store/context/sqlite-context-assembler.js.map +1 -1
  131. package/core/dist/runtime-v2/store/intervention-evidence-schema.js +1 -1
  132. package/core/dist/runtime-v2/store/runtime-state-manager.d.ts +65 -0
  133. package/core/dist/runtime-v2/store/runtime-state-manager.d.ts.map +1 -1
  134. package/core/dist/runtime-v2/store/runtime-state-manager.js +46 -0
  135. package/core/dist/runtime-v2/store/runtime-state-manager.js.map +1 -1
  136. package/core/dist/runtime-v2/store/sqlite-connection.d.ts.map +1 -1
  137. package/core/dist/runtime-v2/store/sqlite-connection.js +42 -0
  138. package/core/dist/runtime-v2/store/sqlite-connection.js.map +1 -1
  139. package/core/dist/runtime-v2/store/sqlite-intervention-evidence-store.d.ts +1 -1
  140. package/core/dist/runtime-v2/store/sqlite-intervention-evidence-store.d.ts.map +1 -1
  141. package/core/dist/runtime-v2/store/sqlite-intervention-evidence-store.js.map +1 -1
  142. package/core/dist/runtime-v2/store/task/sqlite-family-reopen.d.ts +19 -0
  143. package/core/dist/runtime-v2/store/task/sqlite-family-reopen.d.ts.map +1 -0
  144. package/core/dist/runtime-v2/store/task/sqlite-family-reopen.js +36 -0
  145. package/core/dist/runtime-v2/store/task/sqlite-family-reopen.js.map +1 -0
  146. package/core/dist/runtime-v2/store/task/sqlite-task-store.d.ts +23 -0
  147. package/core/dist/runtime-v2/store/task/sqlite-task-store.d.ts.map +1 -1
  148. package/core/dist/runtime-v2/store/task/sqlite-task-store.js +50 -0
  149. package/core/dist/runtime-v2/store/task/sqlite-task-store.js.map +1 -1
  150. package/core/dist/runtime-v2/task-status.d.ts +3 -1
  151. package/core/dist/runtime-v2/task-status.d.ts.map +1 -1
  152. package/core/dist/runtime-v2/task-status.js +1 -1
  153. package/core/dist/runtime-v2/task-status.js.map +1 -1
  154. package/core/dist/runtime-v2/trajectory-schema.d.ts.map +1 -1
  155. package/core/dist/runtime-v2/trajectory-schema.js +11 -0
  156. package/core/dist/runtime-v2/trajectory-schema.js.map +1 -1
  157. package/core/dist/runtime-v2/types/event-types.d.ts +2 -2
  158. package/core/dist/runtime-v2/types/event-types.d.ts.map +1 -1
  159. package/core/dist/runtime-v2/types/event-types.js +1 -1
  160. package/core/dist/runtime-v2/types/event-types.js.map +1 -1
  161. package/core/dist/runtime-v2/types/evidence-chain-contract.d.ts +1 -1
  162. package/core/dist/runtime-v2/types/evidence-chain-contract.d.ts.map +1 -1
  163. package/core/dist/runtime-v2/types/evidence-chain-contract.js +2 -2
  164. package/core/dist/runtime-v2/types/evidence-chain-contract.js.map +1 -1
  165. package/core/dist/runtime-v2/types/intervention-evidence-contract.d.ts +3 -3
  166. package/core/dist/runtime-v2/types/intervention-evidence-contract.d.ts.map +1 -1
  167. package/core/dist/runtime-v2/types/intervention-evidence-contract.js +1 -0
  168. package/core/dist/runtime-v2/types/intervention-evidence-contract.js.map +1 -1
  169. package/core/dist/telemetry-event.d.ts +2 -2
  170. package/core/dist/telemetry-event.d.ts.map +1 -1
  171. package/core/dist/telemetry-event.js +4 -0
  172. package/core/dist/telemetry-event.js.map +1 -1
  173. package/core/dist/trajectory-types.d.ts +2 -2
  174. package/core/dist/trajectory-types.d.ts.map +1 -1
  175. package/core/package.json +7 -5
  176. package/dist/index.d.ts.map +1 -1
  177. package/dist/index.js +84 -8
  178. package/dist/index.js.map +1 -1
  179. package/dist/installer.d.ts +42 -11
  180. package/dist/installer.d.ts.map +1 -1
  181. package/dist/installer.js +140 -36
  182. package/dist/installer.js.map +1 -1
  183. package/dist/installers/index.d.ts +18 -7
  184. package/dist/installers/index.d.ts.map +1 -1
  185. package/dist/installers/index.js +13 -4
  186. package/dist/installers/index.js.map +1 -1
  187. package/dist/installers/workbuddy-host-installer.d.ts +246 -0
  188. package/dist/installers/workbuddy-host-installer.d.ts.map +1 -0
  189. package/dist/installers/workbuddy-host-installer.js +1419 -0
  190. package/dist/installers/workbuddy-host-installer.js.map +1 -0
  191. package/dist/mvp-config.d.ts +24 -0
  192. package/dist/mvp-config.d.ts.map +1 -1
  193. package/dist/mvp-config.js +36 -0
  194. package/dist/mvp-config.js.map +1 -1
  195. package/dist/prompts.d.ts +13 -0
  196. package/dist/prompts.d.ts.map +1 -1
  197. package/dist/prompts.js +45 -8
  198. package/dist/prompts.js.map +1 -1
  199. package/dist/uninstaller.d.ts +1 -1
  200. package/dist/uninstaller.d.ts.map +1 -1
  201. package/dist/uninstaller.js +29 -4
  202. package/dist/uninstaller.js.map +1 -1
  203. package/host-runtime/dist/governance-signal-admission.d.ts +63 -0
  204. package/host-runtime/dist/governance-signal-admission.js +322 -53
  205. package/host-runtime/dist/host-liveness-contract.js +7 -1
  206. package/host-runtime/dist/index.d.ts +1 -0
  207. package/host-runtime/dist/index.js +5 -1
  208. package/host-runtime/dist/internalization-consumer-cycle.d.ts +12 -0
  209. package/host-runtime/dist/internalization-consumer-cycle.js +43 -1
  210. package/host-runtime/dist/intervention-evidence-ingress.d.ts +1 -1
  211. package/host-runtime/dist/product-telemetry/service.js +1 -1
  212. package/host-runtime/dist/production-pain-evidence.d.ts +59 -1
  213. package/host-runtime/dist/production-pain-evidence.js +200 -17
  214. package/host-runtime/dist/prompt-activation-deliverability.js +5 -4
  215. package/host-runtime/dist/prompt-injection-event-evidence.d.ts +1 -1
  216. package/host-runtime/dist/prompt-injection-event-evidence.js +5 -1
  217. package/host-runtime/dist/prompt-injection-projection.d.ts +14 -5
  218. package/host-runtime/dist/prompt-injection-projection.js +30 -16
  219. package/host-runtime/dist/workbuddy-worker-status.d.ts +24 -0
  220. package/host-runtime/dist/workbuddy-worker-status.js +52 -0
  221. package/host-runtime/package.json +7 -5
  222. package/install-layout/dist/index.d.ts +18 -1
  223. package/install-layout/dist/index.js +15 -2
  224. package/install-layout/package.json +5 -3
  225. package/package.json +9 -7
  226. package/pd-cli/dist/commands/candidate.d.ts.map +1 -1
  227. package/pd-cli/dist/commands/candidate.js +44 -4
  228. package/pd-cli/dist/commands/candidate.js.map +1 -1
  229. package/pd-cli/dist/commands/codex-worker.d.ts.map +1 -1
  230. package/pd-cli/dist/commands/codex-worker.js +30 -1
  231. package/pd-cli/dist/commands/codex-worker.js.map +1 -1
  232. package/pd-cli/dist/commands/config-doctor.d.ts.map +1 -1
  233. package/pd-cli/dist/commands/config-doctor.js +19 -4
  234. package/pd-cli/dist/commands/config-doctor.js.map +1 -1
  235. package/pd-cli/dist/commands/diagnose.d.ts.map +1 -1
  236. package/pd-cli/dist/commands/diagnose.js +4 -1
  237. package/pd-cli/dist/commands/diagnose.js.map +1 -1
  238. package/pd-cli/dist/commands/health-codex-install.d.ts +85 -0
  239. package/pd-cli/dist/commands/health-codex-install.d.ts.map +1 -0
  240. package/pd-cli/dist/commands/health-codex-install.js +207 -0
  241. package/pd-cli/dist/commands/health-codex-install.js.map +1 -0
  242. package/pd-cli/dist/commands/health-codex.d.ts.map +1 -1
  243. package/pd-cli/dist/commands/health-codex.js +78 -0
  244. package/pd-cli/dist/commands/health-codex.js.map +1 -1
  245. package/pd-cli/dist/commands/pain-list.d.ts +3 -2
  246. package/pd-cli/dist/commands/pain-list.d.ts.map +1 -1
  247. package/pd-cli/dist/commands/pain-list.js +10 -10
  248. package/pd-cli/dist/commands/pain-list.js.map +1 -1
  249. package/pd-cli/dist/commands/pain-record.d.ts +2 -0
  250. package/pd-cli/dist/commands/pain-record.d.ts.map +1 -1
  251. package/pd-cli/dist/commands/pain-record.js +36 -0
  252. package/pd-cli/dist/commands/pain-record.js.map +1 -1
  253. package/pd-cli/dist/commands/pain-retry.d.ts +6 -0
  254. package/pd-cli/dist/commands/pain-retry.d.ts.map +1 -1
  255. package/pd-cli/dist/commands/pain-retry.js +152 -5
  256. package/pd-cli/dist/commands/pain-retry.js.map +1 -1
  257. package/pd-cli/dist/commands/runtime-activation.d.ts +1 -1
  258. package/pd-cli/dist/commands/runtime-activation.d.ts.map +1 -1
  259. package/pd-cli/dist/commands/runtime-activation.js +3 -3
  260. package/pd-cli/dist/commands/runtime-activation.js.map +1 -1
  261. package/pd-cli/dist/commands/workbuddy-worker.d.ts +19 -0
  262. package/pd-cli/dist/commands/workbuddy-worker.d.ts.map +1 -0
  263. package/pd-cli/dist/commands/workbuddy-worker.js +168 -0
  264. package/pd-cli/dist/commands/workbuddy-worker.js.map +1 -0
  265. package/pd-cli/dist/index.js +29 -1
  266. package/pd-cli/dist/index.js.map +1 -1
  267. package/pd-cli/dist/resolve-workspace.d.ts +14 -2
  268. package/pd-cli/dist/resolve-workspace.d.ts.map +1 -1
  269. package/pd-cli/dist/resolve-workspace.js +88 -21
  270. package/pd-cli/dist/resolve-workspace.js.map +1 -1
  271. package/pd-cli/dist/services/admission-write-gate.d.ts +24 -0
  272. package/pd-cli/dist/services/admission-write-gate.d.ts.map +1 -0
  273. package/pd-cli/dist/services/admission-write-gate.js +41 -0
  274. package/pd-cli/dist/services/admission-write-gate.js.map +1 -0
  275. package/pd-cli/dist/services/pd-config-loader.d.ts +39 -7
  276. package/pd-cli/dist/services/pd-config-loader.d.ts.map +1 -1
  277. package/pd-cli/dist/services/pd-config-loader.js +111 -18
  278. package/pd-cli/dist/services/pd-config-loader.js.map +1 -1
  279. package/pd-cli/dist/services/rulehost-pipeline-runner.d.ts +10 -0
  280. package/pd-cli/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
  281. package/pd-cli/dist/services/rulehost-pipeline-runner.js +59 -4
  282. package/pd-cli/dist/services/rulehost-pipeline-runner.js.map +1 -1
  283. package/pd-cli/package.json +8 -5
  284. package/plugin/dist/bundle.js +564 -509
  285. package/plugin/dist/governance-audit.js +1 -1
  286. package/plugin/dist/openclaw.plugin.json +1 -1
  287. package/plugin/dist/rulehost-evidence.js +1 -1
  288. package/plugin/openclaw.plugin.json +1 -1
  289. package/plugin/package.json +7 -5
  290. package/release-manager/dist/index.d.ts.map +1 -1
  291. package/release-manager/dist/index.js +84 -8
  292. package/release-manager/dist/index.js.map +1 -1
  293. package/release-manager/dist/installer.d.ts +42 -11
  294. package/release-manager/dist/installer.d.ts.map +1 -1
  295. package/release-manager/dist/installer.js +140 -36
  296. package/release-manager/dist/installer.js.map +1 -1
  297. package/release-manager/dist/installers/index.d.ts +18 -7
  298. package/release-manager/dist/installers/index.d.ts.map +1 -1
  299. package/release-manager/dist/installers/index.js +13 -4
  300. package/release-manager/dist/installers/index.js.map +1 -1
  301. package/release-manager/dist/installers/workbuddy-host-installer.d.ts +246 -0
  302. package/release-manager/dist/installers/workbuddy-host-installer.d.ts.map +1 -0
  303. package/release-manager/dist/installers/workbuddy-host-installer.js +1419 -0
  304. package/release-manager/dist/installers/workbuddy-host-installer.js.map +1 -0
  305. package/release-manager/dist/mvp-config.d.ts +24 -0
  306. package/release-manager/dist/mvp-config.d.ts.map +1 -1
  307. package/release-manager/dist/mvp-config.js +36 -0
  308. package/release-manager/dist/mvp-config.js.map +1 -1
  309. package/release-manager/dist/prompts.d.ts +13 -0
  310. package/release-manager/dist/prompts.d.ts.map +1 -1
  311. package/release-manager/dist/prompts.js +45 -8
  312. package/release-manager/dist/prompts.js.map +1 -1
  313. package/release-manager/dist/uninstaller.d.ts +1 -1
  314. package/release-manager/dist/uninstaller.d.ts.map +1 -1
  315. package/release-manager/dist/uninstaller.js +29 -4
  316. package/release-manager/dist/uninstaller.js.map +1 -1
  317. package/release-manager/package.json +8 -6
  318. package/workbuddy-adapter/dist/codec/index.d.ts +2 -0
  319. package/workbuddy-adapter/dist/codec/index.js +2 -0
  320. package/workbuddy-adapter/dist/codec/input-decoder.d.ts +30 -0
  321. package/workbuddy-adapter/dist/codec/input-decoder.js +232 -0
  322. package/workbuddy-adapter/dist/codec/output-encoder.d.ts +62 -0
  323. package/workbuddy-adapter/dist/codec/output-encoder.js +75 -0
  324. package/workbuddy-adapter/dist/host-adapter.d.ts +33 -0
  325. package/workbuddy-adapter/dist/host-adapter.js +23 -0
  326. package/workbuddy-adapter/dist/index.d.ts +16 -0
  327. package/workbuddy-adapter/dist/index.js +16 -0
  328. package/workbuddy-adapter/dist/pd-hook.d.ts +17 -0
  329. package/workbuddy-adapter/dist/pd-hook.js +505 -0
  330. package/workbuddy-adapter/dist/tool-semantics.d.ts +22 -0
  331. package/workbuddy-adapter/dist/tool-semantics.js +33 -0
  332. package/workbuddy-adapter/dist/workbuddy-evidence-recorder.d.ts +50 -0
  333. package/workbuddy-adapter/dist/workbuddy-evidence-recorder.js +238 -0
  334. package/workbuddy-adapter/dist/worker/workspace-worker.d.ts +41 -0
  335. package/workbuddy-adapter/dist/worker/workspace-worker.js +219 -0
  336. package/workbuddy-adapter/dist/workspace-resolution.d.ts +15 -0
  337. package/workbuddy-adapter/dist/workspace-resolution.js +53 -0
  338. package/workbuddy-adapter/package.json +64 -0
  339. package/workbuddy-adapter/plugin/.codebuddy-plugin/plugin.json +6 -0
  340. package/workbuddy-adapter/plugin/hooks/hooks.json +62 -0
  341. package/workbuddy-adapter/plugin/scripts/wrapper.cjs +79 -0
  342. package/console/dist/ui/utils/navigation.d.ts +0 -9
  343. package/console/dist/ui/utils/navigation.js +0 -7
  344. package/core/dist/runtime-v2/internalization/dimension-coverage-policy.d.ts +0 -54
  345. package/core/dist/runtime-v2/internalization/dimension-coverage-policy.d.ts.map +0 -1
  346. package/core/dist/runtime-v2/internalization/dimension-coverage-policy.js +0 -70
  347. package/core/dist/runtime-v2/internalization/dimension-coverage-policy.js.map +0 -1
@@ -0,0 +1,1419 @@
1
+ /**
2
+ * WorkBuddyHostInstaller — implements HostInstaller for WorkBuddy Desktop
3
+ * (ADR-0020 third host, SPEC workbuddy-host-adapter v0.3.0 §4.1/§6).
4
+ *
5
+ * Registration model (frozen Phase 0 probe P01, 2026-10-12 — see
6
+ * packages/workbuddy-adapter/tests/fixtures/host-contract/manifest-facts.json):
7
+ * - PD owns a local marketplace directory (~/.pd/workbuddy-marketplace/) with
8
+ * .codebuddy-plugin/marketplace.json + the plugin skeleton copied from the
9
+ * bundled @principles/workbuddy-adapter payload (source must stay inside the
10
+ * marketplace root — the engine rejects escapes).
11
+ * - Registration runs through the REAL engine commands when reachable:
12
+ * node <engine> plugin marketplace add <dir>
13
+ * node <engine> plugin install principles-disciple@principles-disciple-market
14
+ * - When the engine binary is unreachable, a direct-write path may register in
15
+ * the frozen formats instead (enabledPlugins["name@marketplace"]:true in
16
+ * settings.json + installed_plugins.json v2 + versioned cache copy). The
17
+ * result always reports WHICH mode ran (rc-9 — never a silent degradation).
18
+ *
19
+ * Install ≠ enable (OD-03): install registers the plugin but NEVER flips the
20
+ * host.workbuddy feature flag (quiet, default OFF). Explicit enable is a
21
+ * separate, user-visible step (enableGovernance below).
22
+ *
23
+ * Governance workspace (OD-07, frozen by probe P06): install explicitly
24
+ * creates ~/.workbuddy/pd-workspace via install-layout's
25
+ * getWorkbuddyWorkspaceDir() and registers it in ~/.pd/install.json. The dir is
26
+ * NEVER auto-reused from Codex/OpenClaw state and NEVER deleted on uninstall
27
+ * (it owns the governance database).
28
+ *
29
+ * Uninstall deletes ONLY provably-PD-owned files: our marketplace dir and our
30
+ * versioned cache dirs. It preserves the governance database, other plugins,
31
+ * other hosts' runtimes and the shared worker.
32
+ *
33
+ * Runtime Contract Rules:
34
+ * - rc-1-treat-as-unknown: settings.json / installed_plugins.json / config.yaml
35
+ * are parsed as unknown and validated with guards.
36
+ * - rc-2-no-as-bypass: membership checks use Object.hasOwn + typeof guards.
37
+ * - rc-3-fail-loud-missing: a malformed settings.json/registry STOPS all
38
+ * writes (zero modification) with reason + nextAction.
39
+ * - rc-5-object-hasown-not-in: untrusted key membership via Object.hasOwn.
40
+ * - rc-8-safe-serialization: only strings/numbers recorded in results.
41
+ * - rc-9-no-silent-fallback: engine-unreachable fallback is reported in the
42
+ * result reason; every result carries reason + nextAction.
43
+ */
44
+ import { spawnSync } from 'node:child_process';
45
+ import { existsSync, mkdirSync, readFileSync, rmSync, cpSync, writeFileSync } from 'node:fs';
46
+ import * as path from 'node:path';
47
+ import * as yaml from 'js-yaml';
48
+ import { getInstallLayoutPaths, getWorkbuddyWorkspaceDir, mergeInstallManifestWorkspaces, parseInstallManifest, INSTALL_LAYOUT_VERSION, } from '@principles/install-layout';
49
+ import { atomicReplaceTextFile } from '../utils/config-file-io.js';
50
+ import { getHomeDir, generateConfigYamlContent, getConfigYamlPath, resolveDefaultModelForProvider, isValidHttpBaseUrl } from '../mvp-config.js';
51
+ // Type-only import from @principles/core — erased at compile time; this
52
+ // package ships self-contained without @principles/core (PRI-625/PRI-669).
53
+ // ─── Frozen host contract constants ─────────────────────────────────────────
54
+ /** Plugin name inside the PD-owned marketplace (kebab-case per plugin.json contract). */
55
+ export const WORKBUDDY_PLUGIN_NAME = 'principles-disciple';
56
+ /** PD-owned local marketplace name. enabledPlugins keys use "plugin@marketplace". */
57
+ export const WORKBUDDY_MARKETPLACE_NAME = 'principles-disciple-market';
58
+ /** Registry/settings key: "principles-disciple@principles-disciple-market". */
59
+ export const WORKBUDDY_PLUGIN_KEY = `${WORKBUDDY_PLUGIN_NAME}@${WORKBUDDY_MARKETPLACE_NAME}`;
60
+ /** Engine CLI shipped with WorkBuddy Desktop (probe P01 frozen path). */
61
+ const WORKBUDDY_ENGINE_RELATIVE = ['resources', 'app.asar.unpacked', 'cli', 'dist', 'codebuddy-lite-wb.mjs'];
62
+ /** Engine command success markers (frozen by probe P01 regexes). */
63
+ const MARKETPLACE_ADD_OK = /added successfully|already/i;
64
+ const PLUGIN_INSTALL_OK = /Successfully installed/i;
65
+ const PLUGIN_UNINSTALL_OK = /Successfully uninstalled/i;
66
+ const ENGINE_COMMAND_TIMEOUT_MS = 120_000;
67
+ /** `pd runtime init` for the governance workspace (schema + config + state.db). */
68
+ const GOVERNANCE_INIT_TIMEOUT_MS = 180_000;
69
+ // ─── Type guards (rc-2: no `as` bypass) ─────────────────────────────────────
70
+ function isRecord(value) {
71
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
72
+ }
73
+ export function resolveWorkbuddyPaths(homeDir) {
74
+ const layout = getInstallLayoutPaths(homeDir);
75
+ // The frozen governance dir (~/.workbuddy/pd-workspace) defines the host
76
+ // config root; deriving configDir from it keeps one path authority.
77
+ //
78
+ // HOST BOUNDARY (review P3, probe P0 fact): this installer targets the
79
+ // WorkBuddy DESKTOP host, whose registration root is ~/.workbuddy (real
80
+ // desktop plugins observed there). The standalone CLI engine's user-scope
81
+ // root is ~/.codebuddy (CODEBUDDY_CONFIG_DIR); a CLI-only user's engine
82
+ // will not read a registration written here. WORKBUDDY_CONFIG_DIR remains
83
+ // the isolation override (Phase 0 lab discipline).
84
+ const configDir = process.env.WORKBUDDY_CONFIG_DIR && process.env.WORKBUDDY_CONFIG_DIR.length > 0
85
+ ? path.resolve(process.env.WORKBUDDY_CONFIG_DIR)
86
+ : path.dirname(getWorkbuddyWorkspaceDir(homeDir));
87
+ return {
88
+ homeDir: layout.homeDir,
89
+ configDir,
90
+ settingsPath: path.join(configDir, 'settings.json'),
91
+ registryPath: path.join(configDir, 'plugins', 'installed_plugins.json'),
92
+ cachePluginRoot: path.join(configDir, 'plugins', 'cache', WORKBUDDY_MARKETPLACE_NAME, WORKBUDDY_PLUGIN_NAME),
93
+ marketplaceRoot: path.join(layout.pdDir, 'workbuddy-marketplace'),
94
+ governanceDir: getWorkbuddyWorkspaceDir(homeDir),
95
+ runtimeAdapterDir: layout.workbuddyAdapterDir,
96
+ installManifestPath: layout.manifest,
97
+ };
98
+ }
99
+ /**
100
+ * Resolve the WorkBuddy engine CLI path.
101
+ *
102
+ * Order: PD_WORKBUDDY_ENGINE / WB_ENGINE env override (probe convention) →
103
+ * the Desktop install location under Program Files (frozen by probe P01).
104
+ * The engine is NOT exposed on PATH — absence is a detectable, reportable
105
+ * fact, never a guess.
106
+ */
107
+ export function resolveWorkbuddyEnginePath() {
108
+ for (const envName of ['PD_WORKBUDDY_ENGINE', 'WB_ENGINE']) {
109
+ const value = process.env[envName];
110
+ if (value && value.length > 0 && existsSync(value))
111
+ return value;
112
+ }
113
+ const roots = [
114
+ process.env.ProgramFiles,
115
+ process.env['ProgramFiles(x86)'],
116
+ process.env.SystemDrive ? path.join(process.env.SystemDrive, 'Program Files') : undefined,
117
+ ].filter((v) => typeof v === 'string' && v.length > 0);
118
+ for (const root of roots) {
119
+ const candidate = path.join(root, 'WorkBuddy', ...WORKBUDDY_ENGINE_RELATIVE);
120
+ if (existsSync(candidate))
121
+ return candidate;
122
+ }
123
+ return undefined;
124
+ }
125
+ /** Read + validate a host JSON file. Malformed content is a HARD STOP (rc-3). */
126
+ export function readHostJsonFile(filePath) {
127
+ let raw;
128
+ try {
129
+ raw = readFileSync(filePath, 'utf-8');
130
+ }
131
+ catch {
132
+ return { ok: true, value: undefined, existed: false };
133
+ }
134
+ let parsed;
135
+ try {
136
+ parsed = JSON.parse(raw);
137
+ }
138
+ catch (error) {
139
+ return {
140
+ ok: false,
141
+ path: filePath,
142
+ reason: `workbuddy_settings_unparsable: ${filePath} exists but is not valid JSON (${error instanceof Error ? error.message : String(error)}). No PD write was performed.`,
143
+ nextAction: `Fix or remove the malformed file manually, then re-run: npx create-principles-disciple --host workbuddy. PD never merges into a config it cannot parse.`,
144
+ };
145
+ }
146
+ if (!isRecord(parsed)) {
147
+ return {
148
+ ok: false,
149
+ path: filePath,
150
+ reason: `workbuddy_settings_unparsable: ${filePath} parsed to ${Array.isArray(parsed) ? 'an array' : typeof parsed}, expected an object. No PD write was performed.`,
151
+ nextAction: `Fix or remove the malformed file manually, then re-run: npx create-principles-disciple --host workbuddy.`,
152
+ };
153
+ }
154
+ return { ok: true, value: parsed, existed: true };
155
+ }
156
+ /**
157
+ * Atomic, backup-protected JSON write for host config files.
158
+ *
159
+ * Discipline (SPEC §6): parse-failure callers never reach this function; the
160
+ * write itself backs up the previous bytes, replaces atomically (temp file +
161
+ * rename), and restores the backup when the replacement fails.
162
+ */
163
+ export function writeHostJsonFileProtected(filePath, content) {
164
+ let backupPath;
165
+ if (existsSync(filePath)) {
166
+ backupPath = `${filePath}.pd-backup-${Date.now()}`;
167
+ try {
168
+ cpSync(filePath, backupPath);
169
+ }
170
+ catch (error) {
171
+ return {
172
+ ok: false,
173
+ reason: `workbuddy_settings_backup_failed: could not back up ${filePath} (${error instanceof Error ? error.message : String(error)}). No write was performed.`,
174
+ nextAction: `Free the file (close WorkBuddy) and re-run, or back it up manually and re-run.`,
175
+ };
176
+ }
177
+ }
178
+ try {
179
+ // The registry/settings may live in directories the host has not created
180
+ // yet (direct-write on a fresh config dir) — creating the PARENT is part
181
+ // of creating a new host file, not a mutation of existing host state.
182
+ mkdirSync(path.dirname(filePath), { recursive: true });
183
+ atomicReplaceTextFile(filePath, content);
184
+ if (backupPath !== undefined) {
185
+ try {
186
+ rmSync(backupPath, { force: true });
187
+ }
188
+ catch { /* best effort — the write already succeeded */ }
189
+ }
190
+ return { ok: true };
191
+ }
192
+ catch (error) {
193
+ // Restore the backup before reporting — the file must not stay half-written.
194
+ let restoreError;
195
+ if (backupPath !== undefined) {
196
+ try {
197
+ cpSync(backupPath, filePath);
198
+ rmSync(backupPath, { force: true });
199
+ }
200
+ catch (restoreErr) {
201
+ restoreError = restoreErr instanceof Error ? restoreErr.message : String(restoreErr);
202
+ }
203
+ }
204
+ return {
205
+ ok: false,
206
+ reason: `workbuddy_settings_write_failed: atomic replace of ${filePath} failed (${error instanceof Error ? error.message : String(error)})${restoreError !== undefined ? `; backup restore also failed (${restoreError}) — the previous content is preserved at ${backupPath}` : '; previous content restored from backup'}.`,
207
+ nextAction: restoreError !== undefined && backupPath !== undefined
208
+ ? `Manually restore ${backupPath} to ${filePath}, then re-run the installer.`
209
+ : `Re-run the installer after closing WorkBuddy (the file may be locked).`,
210
+ };
211
+ }
212
+ }
213
+ export function findWorkbuddyPluginPayload(pluginDir, runtimeAdapterDir) {
214
+ for (const root of [path.join(pluginDir, 'workbuddy-adapter', 'plugin'), path.join(runtimeAdapterDir, 'plugin')]) {
215
+ const manifestPath = path.join(root, '.codebuddy-plugin', 'plugin.json');
216
+ const hooksPath = path.join(root, 'hooks', 'hooks.json');
217
+ if (!existsSync(manifestPath) || !existsSync(hooksPath))
218
+ continue;
219
+ let manifest;
220
+ try {
221
+ manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
222
+ }
223
+ catch {
224
+ return { reason: `workbuddy_plugin_manifest_unparsable: ${manifestPath} is not valid JSON. Refusing to register a plugin whose identity cannot be verified.` };
225
+ }
226
+ if (!isRecord(manifest) || manifest.name !== WORKBUDDY_PLUGIN_NAME) {
227
+ return { reason: `workbuddy_plugin_manifest_invalid: ${manifestPath} must declare name "${WORKBUDDY_PLUGIN_NAME}" (got ${isRecord(manifest) ? JSON.stringify(manifest.name) : 'unparsable manifest'}).` };
228
+ }
229
+ let hooks;
230
+ try {
231
+ hooks = JSON.parse(readFileSync(hooksPath, 'utf-8'));
232
+ }
233
+ catch {
234
+ return { reason: `workbuddy_plugin_hooks_unparsable: ${hooksPath} is not valid JSON. Refusing to register a plugin whose hook wiring cannot be verified.` };
235
+ }
236
+ if (!isRecord(hooks) || !isRecord(hooks.hooks)) {
237
+ return { reason: `workbuddy_plugin_hooks_invalid: ${hooksPath} must contain a "hooks" object (frozen manifest-facts shape).` };
238
+ }
239
+ const version = typeof manifest.version === 'string' && manifest.version.length > 0 ? manifest.version : '0.0.0';
240
+ return { payload: { pluginDir: root, version } };
241
+ }
242
+ return {
243
+ reason: `workbuddy_adapter_payload_missing: no WorkBuddy plugin skeleton found (looked for workbuddy-adapter/plugin/.codebuddy-plugin/plugin.json under the installer payload and ${runtimeAdapterDir}). The @principles/workbuddy-adapter component is not part of this installer build.`,
244
+ };
245
+ }
246
+ // ─── Install manifest merge (hosts ∪ workbuddy, workspaces ∪ governance) ────
247
+ /** Read install.json preserving unknown keys (PRI-709 P0-1 merge semantics). */
248
+ function readInstallJsonRecord(manifestPath) {
249
+ try {
250
+ const parsed = JSON.parse(readFileSync(manifestPath, 'utf-8'));
251
+ return isRecord(parsed) ? parsed : undefined;
252
+ }
253
+ catch {
254
+ return undefined;
255
+ }
256
+ }
257
+ /**
258
+ * Merge 'workbuddy' + the governance workspace into ~/.pd/install.json.
259
+ *
260
+ * Set-union semantics: existing hosts (codex/openclaw) and workspaces are
261
+ * always preserved; unknown top-level keys (the update side owns
262
+ * channel/autoCheck/releaseMetadataUrl — PRI-709 P0-1) are carried through.
263
+ * Malformed existing manifest → hard stop, zero modification.
264
+ */
265
+ export function mergeWorkbuddyIntoInstallManifest(manifestPath, governanceDir) {
266
+ let current;
267
+ let existed = false;
268
+ try {
269
+ current = JSON.parse(readFileSync(manifestPath, 'utf-8'));
270
+ existed = true;
271
+ }
272
+ catch (error) {
273
+ if (existsSync(manifestPath)) {
274
+ return {
275
+ ok: false,
276
+ reason: `install_manifest_unparsable: ${manifestPath} exists but is not valid JSON (${error instanceof Error ? error.message : String(error)}). No write was performed.`,
277
+ nextAction: `Repair or remove ~/.pd/install.json manually, then re-run the installer.`,
278
+ };
279
+ }
280
+ }
281
+ const parsed = parseInstallManifest(current);
282
+ if (existed && current !== undefined && !parsed.manifest) {
283
+ return {
284
+ ok: false,
285
+ reason: `install_manifest_malformed: ${parsed.error ?? 'unknown'}. No write was performed.`,
286
+ nextAction: `Repair ~/.pd/install.json (layoutVersion ${INSTALL_LAYOUT_VERSION}, mode canonical, hosts array) and re-run.`,
287
+ };
288
+ }
289
+ const hosts = [...new Set([...(parsed.manifest?.hosts ?? []), 'workbuddy'])];
290
+ const workspaces = mergeInstallManifestWorkspaces(parsed.manifest, governanceDir);
291
+ let existingRecord = {};
292
+ if (existed) {
293
+ const raw = readInstallJsonRecord(manifestPath);
294
+ if (raw !== undefined)
295
+ existingRecord = raw;
296
+ }
297
+ const payload = {
298
+ ...existingRecord,
299
+ layoutVersion: INSTALL_LAYOUT_VERSION,
300
+ mode: 'canonical',
301
+ hosts,
302
+ workspaces,
303
+ };
304
+ try {
305
+ mkdirSync(path.dirname(manifestPath), { recursive: true });
306
+ writeFileSync(manifestPath, JSON.stringify(payload, null, 2) + '\n', 'utf-8');
307
+ return { ok: true };
308
+ }
309
+ catch (error) {
310
+ return {
311
+ ok: false,
312
+ reason: `install_manifest_write_failed: ${manifestPath} could not be written (${error instanceof Error ? error.message : String(error)}).`,
313
+ nextAction: `Check permissions on ~/.pd and re-run the installer.`,
314
+ };
315
+ }
316
+ }
317
+ /** Remove 'workbuddy' from manifest hosts (uninstall bookkeeping; workspaces/governance data preserved). */
318
+ function removeWorkbuddyFromInstallManifestHosts(manifestPath) {
319
+ try {
320
+ const parsed = JSON.parse(readFileSync(manifestPath, 'utf-8'));
321
+ if (!isRecord(parsed) || !Array.isArray(parsed.hosts))
322
+ return;
323
+ const hosts = parsed.hosts.filter((h) => typeof h === 'string' && h !== 'workbuddy');
324
+ const payload = { ...parsed, hosts };
325
+ writeFileSync(manifestPath, JSON.stringify(payload, null, 2) + '\n', 'utf-8');
326
+ }
327
+ catch {
328
+ // Uninstall must not fail on manifest bookkeeping; the preserved-state
329
+ // report below still names the governance dir. rc-9: surfaced via paths.
330
+ }
331
+ }
332
+ // ─── Feature flag override (config.yaml line-targeted edit) ─────────────────
333
+ const FEATURES_LINE = /^features:\s*(?:#.*)?$/;
334
+ const FEATURES_EMPTY_MAPPING_LINE = /^features:\s*\{\}\s*(?:#.*)?$/;
335
+ const WORKBUDDY_KEY_LINE = /^ {2}host\.workbuddy:\s*(?:#.*)?$/;
336
+ const WORKBUDDY_ENABLED_LINE = /^ {4}enabled:\s*(?:true|false)\s*(?:#.*)?$/;
337
+ const WORKBUDDY_CATEGORY_LINE = /^ {4}category:(?:\s.*)?(?:#.*)?$/;
338
+ /**
339
+ * Registry-required category for the host.workbuddy flag entry. SSoT:
340
+ * packages/principles-core/src/runtime-v2/feature-flags/feature-flag-contract.ts
341
+ * ({ id: 'host.workbuddy', category: 'quiet', ... }). The production config
342
+ * validator (pd-config-validate) REJECTS feature entries without a category,
343
+ * so an enabled-only override makes every hook invocation degrade (review R1).
344
+ * tests/workbuddy-governance-chain.test.ts pins this literal against
345
+ * DEFAULT_FEATURE_FLAGS to catch registry drift.
346
+ */
347
+ const WORKBUDDY_FLAG_CATEGORY = 'quiet';
348
+ function stripYamlComment(line) {
349
+ const hash = line.indexOf('#');
350
+ return (hash === -1 ? line : line.slice(0, hash)).trim();
351
+ }
352
+ function isEmptyFeaturesMapping(line) {
353
+ return stripYamlComment(line).replace(/\s+/g, '') === 'features:' + String.fromCharCode(123, 125);
354
+ }
355
+ /**
356
+ * Enable/disable `features.host.workbuddy.enabled` in the GOVERNANCE
357
+ * workspace's .pd/config.yaml with a line-targeted edit (comments and unknown
358
+ * keys preserved), creating a loader-valid config from the installer template
359
+ * when absent. Atomic write; round-trip verified.
360
+ */
361
+ export function setWorkbuddyGovernanceFlag(governanceDir, enabled) {
362
+ const configDir = path.join(governanceDir, '.pd');
363
+ const configPath = path.join(configDir, 'config.yaml');
364
+ let raw;
365
+ if (existsSync(configPath)) {
366
+ try {
367
+ raw = readFileSync(configPath, 'utf-8');
368
+ }
369
+ catch (error) {
370
+ return { ok: false, reason: `config_read_failed: ${configPath} (${error instanceof Error ? error.message : String(error)})`, nextAction: 'Fix file permissions and retry.' };
371
+ }
372
+ // A config that cannot parse is a hard stop (rc-3) — never edited blind.
373
+ try {
374
+ yaml.load(raw);
375
+ }
376
+ catch (error) {
377
+ return {
378
+ ok: false,
379
+ reason: `config_unparsable: ${configPath} is not valid YAML (${error instanceof Error ? error.message : String(error)}). No write was performed.`,
380
+ nextAction: 'Fix or delete the malformed config.yaml, then retry the enable.',
381
+ };
382
+ }
383
+ }
384
+ else {
385
+ raw = generateConfigYamlContent();
386
+ }
387
+ const lines = raw.split('\n');
388
+ let keyIndex = -1;
389
+ for (let i = 0; i < lines.length; i++) {
390
+ const line = lines[i];
391
+ if (typeof line === 'string' && WORKBUDDY_KEY_LINE.test(line)) {
392
+ keyIndex = i;
393
+ break;
394
+ }
395
+ }
396
+ if (keyIndex >= 0) {
397
+ // Remediation (review 2026-10-13 §3 P2 + round-3 P2): locate the existing
398
+ // `enabled` and `category` fields WITHIN the flag's mapping block. The
399
+ // block boundary is INDENTATION, not field identity: it spans every
400
+ // non-blank line indented ≥ 4 spaces after the 2-space `host.workbuddy:`
401
+ // key, plus the comments/blank lines interleaved with them, and ends at
402
+ // the first structural line indented < 4 spaces (dedent / next key) or
403
+ // EOF. Legal entry fields other than the two we manage — e.g. `source:`
404
+ // (PRI-637 provenance, FeatureFlagEntry source?: 'owner' | 'system') —
405
+ // live INSIDE the block and must NEVER end the scan: the previous
406
+ // first-unrecognized-line break mistook `source: owner` for the block end
407
+ // and spliced duplicate keys above it, so the rewrite verification threw
408
+ // (config_rewrite_invalid) and enable/disable was refused with the stale
409
+ // value preserved.
410
+ let enabledIndex = -1;
411
+ let categoryIndex = -1;
412
+ for (let i = keyIndex + 1; i < lines.length; i++) {
413
+ const line = lines[i];
414
+ if (typeof line !== 'string')
415
+ break; // EOF — block ended
416
+ if (line.trim() === '' || /^\s*#/.test(line))
417
+ continue; // blank / comment — transparent, inside the block
418
+ if (!/^ {4}/.test(line))
419
+ break; // dedent — mapping entry ended
420
+ if (WORKBUDDY_ENABLED_LINE.test(line)) {
421
+ enabledIndex = i;
422
+ continue;
423
+ }
424
+ if (WORKBUDDY_CATEGORY_LINE.test(line)) {
425
+ categoryIndex = i;
426
+ continue;
427
+ }
428
+ // Any other legal field (source: …) or nested content — inside the
429
+ // block: keep scanning, never touch it.
430
+ }
431
+ if (enabledIndex >= 0) {
432
+ // In-place flip; nothing else in the block moves.
433
+ lines[enabledIndex] = ` enabled: ${enabled}`;
434
+ }
435
+ else {
436
+ // No enabled field anywhere in the block: insert it under the key line.
437
+ lines.splice(keyIndex + 1, 0, ` enabled: ${enabled}`);
438
+ if (categoryIndex > keyIndex)
439
+ categoryIndex += 1;
440
+ }
441
+ // R1 write contract: EVERY entry this function writes carries the
442
+ // schema-required category (production validator requirement). Insert it
443
+ // only when the block has none — never alongside an existing one.
444
+ if (categoryIndex >= 0) {
445
+ lines[categoryIndex] = ` category: ${WORKBUDDY_FLAG_CATEGORY}`;
446
+ }
447
+ else if (enabledIndex >= 0) {
448
+ lines.splice(enabledIndex + 1, 0, ` category: ${WORKBUDDY_FLAG_CATEGORY}`);
449
+ }
450
+ else {
451
+ lines.splice(keyIndex + 2, 0, ` category: ${WORKBUDDY_FLAG_CATEGORY}`);
452
+ }
453
+ }
454
+ else {
455
+ // Match BOTH an existing mapping header (`features:`) and the empty flow
456
+ // mapping the installer template writes (`features: {}`) — appending a
457
+ // SECOND features block instead would create a duplicate YAML key.
458
+ const featuresIndex = lines.findIndex((line) => FEATURES_LINE.test(line) || FEATURES_EMPTY_MAPPING_LINE.test(line));
459
+ if (featuresIndex >= 0) {
460
+ const featuresLine = lines[featuresIndex];
461
+ if (typeof featuresLine === 'string' && isEmptyFeaturesMapping(featuresLine)) {
462
+ lines[featuresIndex] = 'features:';
463
+ }
464
+ lines.splice(featuresIndex + 1, 0, ` host.workbuddy:`, ` enabled: ${enabled}`, ` category: ${WORKBUDDY_FLAG_CATEGORY}`);
465
+ }
466
+ else {
467
+ // No features: mapping at all — append a block at the end.
468
+ lines.push('features:', ` host.workbuddy:`, ` enabled: ${enabled}`, ` category: ${WORKBUDDY_FLAG_CATEGORY}`);
469
+ }
470
+ }
471
+ const next = lines.join('\n');
472
+ // Round-trip verification: the rewritten config must parse with the flag at
473
+ // the intended value AND the registry-required category before it is
474
+ // committed (same discipline as codex-setup).
475
+ let verify;
476
+ try {
477
+ verify = yaml.load(next);
478
+ }
479
+ catch (error) {
480
+ return { ok: false, reason: `config_rewrite_invalid: verification parse failed (${error instanceof Error ? error.message : String(error)}). Nothing was written.`, nextAction: 'Report this failure — the flag edit produced invalid YAML.' };
481
+ }
482
+ const flagOk = isRecord(verify)
483
+ && isRecord(verify.features)
484
+ && isRecord(verify.features['host.workbuddy'])
485
+ && verify.features['host.workbuddy'].enabled === enabled
486
+ && verify.features['host.workbuddy'].category === WORKBUDDY_FLAG_CATEGORY;
487
+ if (!flagOk) {
488
+ return { ok: false, reason: 'config_rewrite_mismatch: round-trip verification did not observe the intended flag value + category. Nothing was written.', nextAction: 'Report this failure with the config.yaml content.' };
489
+ }
490
+ try {
491
+ mkdirSync(configDir, { recursive: true });
492
+ atomicReplaceTextFile(configPath, next);
493
+ }
494
+ catch (error) {
495
+ return { ok: false, reason: `config_write_failed: ${configPath} (${error instanceof Error ? error.message : String(error)})`, nextAction: 'Fix permissions/free the file and retry.' };
496
+ }
497
+ return { ok: true, configPath, enabled };
498
+ }
499
+ /** Read the explicit host.workbuddy override state (null = no explicit override → registry default OFF). */
500
+ export function readWorkbuddyGovernanceFlagOverride(governanceDir) {
501
+ const configPath = path.join(governanceDir, '.pd', 'config.yaml');
502
+ if (!existsSync(configPath))
503
+ return null;
504
+ try {
505
+ const parsed = yaml.load(readFileSync(configPath, 'utf-8'));
506
+ if (!isRecord(parsed) || !isRecord(parsed.features))
507
+ return null;
508
+ const entry = parsed.features['host.workbuddy'];
509
+ if (!isRecord(entry) || typeof entry.enabled !== 'boolean')
510
+ return null;
511
+ return entry.enabled;
512
+ }
513
+ catch {
514
+ return null;
515
+ }
516
+ }
517
+ // ─── Governance LLM runtime profile (round-3 remediation P1) ────────────────
518
+ /**
519
+ * Profile id the installer writes — the same id `generateConfigYamlContent`
520
+ * pre-fills for the project config (Fix-4 shape), so the WorkBuddy governance
521
+ * profile and the other hosts' project profile have ONE shape.
522
+ */
523
+ const GOVERNANCE_PROFILE_ID = 'pd.default';
524
+ /**
525
+ * Apply the install-collected LLM runtime profile (--provider/--api-key-env/
526
+ * --model/--base-url) to the GOVERNANCE workspace's .pd/config.yaml — the
527
+ * single config the WorkBuddy worker/console/hook chain reads (round-3 P1:
528
+ * the main installer previously wrote the profile into the PROJECT
529
+ * config.yaml while this host's governance workspace kept the runtime-init
530
+ * defaults, so the diagnostician had no usable profile).
531
+ *
532
+ * Semantics (frozen remediation contract):
533
+ * - explicit install profile ⇒ override `runtimeProfiles.pd.default` (same
534
+ * write shape as generateConfigYamlContent; unknown extra fields of an
535
+ * existing pd.default such as baseUrl/maxTokens are preserved) and point
536
+ * `internalAgents.defaultRuntime` + the diagnostician binding at it.
537
+ * Other profiles and every other agent binding are preserved; the
538
+ * diagnostician's `enabled` kill switch is never touched (PRI-638: Owner
539
+ * capability authority stays with the Owner).
540
+ * - Model (round-4 S2): an explicit non-empty --model wins; when --model is
541
+ * omitted, an existing non-empty pd.default model is PRESERVED
542
+ * (existing-coverage contract) and otherwise the shared per-provider
543
+ * default (resolveDefaultModelForProvider) is applied so the profile is
544
+ * genuinely ready. An explicit BLANK --model fails loud — it never passes
545
+ * validation and never writes an empty model.
546
+ * - Pending-config honesty (round-4 S1): agents whose EXPLICIT per-agent
547
+ * runtimeProfile still points elsewhere after the override (legacy init
548
+ * rows indistinguishable from Owner choices) are returned as
549
+ * `pendingAgents` for the installer result — kept as-is, never silently
550
+ * rebound.
551
+ * - Only `apiKeyEnv` (an environment variable NAME) is ever written — no key
552
+ * material reaches config, logs or errors.
553
+ * - The leading comment header of the existing config is preserved; the
554
+ * rewrite carries an explicit installer marker so the provenance of the
555
+ * override stays observable. (Inline comments inside the rewritten nested
556
+ * blocks cannot survive a parse-mutate-dump edit; the previous content is
557
+ * only ever replaced atomically AFTER a full round-trip verification.)
558
+ * - Round-trip verified loader-grade (JSON_SCHEMA parse + non-empty
559
+ * provider/model/apiKeyEnv + binding assertions) BEFORE the atomic replace;
560
+ * any mismatch or write failure leaves the file untouched and fails the
561
+ * install loudly (rc-3/rc-9).
562
+ *
563
+ * Module-private: the only writer/reader is install() below (step 6.5); the
564
+ * regression suite exercises it through the REAL install() boundary.
565
+ */
566
+ function applyWorkbuddyGovernanceRuntimeProfile(governanceDir, profile) {
567
+ // rc-1/rc-3: validate here — upstream CLI validation is not trusted as a
568
+ // substitute for a guard at the write boundary.
569
+ const provider = typeof profile.provider === 'string' ? profile.provider.trim() : '';
570
+ const apiKeyEnv = typeof profile.apiKeyEnv === 'string' ? profile.apiKeyEnv.trim() : '';
571
+ if (provider.length === 0 || apiKeyEnv.length === 0) {
572
+ return {
573
+ ok: false,
574
+ reason: `workbuddy_governance_profile_invalid: --provider/--api-key-env must both be non-empty (got provider=${JSON.stringify(provider || undefined)}, apiKeyEnv=${JSON.stringify(apiKeyEnv || undefined)}).`,
575
+ nextAction: 'Re-run the installer with both --provider <openai|anthropic|deepseek> and --api-key-env <ENV_VAR_NAME>.',
576
+ };
577
+ }
578
+ // Round-4 R3 / round-5 S3: an explicit --base-url must be a usable http(s)
579
+ // endpoint — pi-ai treats its PRESENCE as "custom endpoint, skip the model
580
+ // catalog", so a junk value would silently break every governance LLM call.
581
+ // EXPLICIT blank fails loud (same rule as blank --model); validity is real
582
+ // URL parsing via the ONE shared validation (a prefix regex let `https://`
583
+ // through — round-5 independent repro round5-badurl-results.json).
584
+ const baseUrlRaw = typeof profile.baseUrl === 'string' ? profile.baseUrl : undefined;
585
+ const baseUrl = baseUrlRaw !== undefined ? baseUrlRaw.trim() : '';
586
+ if (baseUrlRaw !== undefined && baseUrl.length === 0) {
587
+ return {
588
+ ok: false,
589
+ reason: `workbuddy_governance_profile_invalid: --base-url was given but blank. Pass a valid http(s) endpoint URL, or omit --base-url to use the provider default.`,
590
+ nextAction: 'Re-run the installer with --base-url <https endpoint> (or omit it).',
591
+ };
592
+ }
593
+ if (baseUrl.length > 0 && !isValidHttpBaseUrl(baseUrl)) {
594
+ return {
595
+ ok: false,
596
+ reason: `workbuddy_governance_profile_invalid: --base-url "${baseUrl}" is not a valid http(s) URL with a hostname.`,
597
+ nextAction: 'Re-run the installer with --base-url <https endpoint> (or omit it to use the provider default).',
598
+ };
599
+ }
600
+ // Round-4 S2: a blank --model is an EXPLICIT invalid value — reject it
601
+ // instead of silently treating it as omitted (an empty written model would
602
+ // fail the resolver later with needs_setup).
603
+ if (typeof profile.model === 'string' && profile.model.trim().length === 0) {
604
+ return {
605
+ ok: false,
606
+ reason: `workbuddy_governance_profile_invalid: --model was given but blank. Pass a non-empty model id, or omit --model to keep the existing configured model (or apply the per-provider default).`,
607
+ nextAction: 'Re-run the installer with --model <model-id> (or without --model).',
608
+ };
609
+ }
610
+ const modelArg = typeof profile.model === 'string' ? profile.model.trim() : '';
611
+ const configPath = getConfigYamlPath(governanceDir);
612
+ if (!existsSync(configPath)) {
613
+ return {
614
+ ok: false,
615
+ reason: `workbuddy_governance_profile_read_failed: ${configPath} does not exist after governance initialization.`,
616
+ nextAction: `Re-run the installer; if it repeats, run "pd runtime init --confirm --workspace ${governanceDir}" manually to see the underlying error.`,
617
+ };
618
+ }
619
+ let raw;
620
+ try {
621
+ raw = readFileSync(configPath, 'utf-8');
622
+ }
623
+ catch (error) {
624
+ return {
625
+ ok: false,
626
+ reason: `workbuddy_governance_profile_read_failed: ${configPath} (${error instanceof Error ? error.message : String(error)}).`,
627
+ nextAction: 'Close other tools holding the config file, check permissions, then re-run the installer.',
628
+ };
629
+ }
630
+ // A config that cannot parse is a hard stop (rc-3) — never edited blind.
631
+ let parsed;
632
+ try {
633
+ parsed = yaml.load(raw);
634
+ }
635
+ catch (error) {
636
+ return {
637
+ ok: false,
638
+ reason: `workbuddy_governance_profile_unparsable: ${configPath} is not valid YAML (${error instanceof Error ? error.message : String(error)}). No write was performed.`,
639
+ nextAction: 'Fix or delete the malformed governance config.yaml, then re-run the installer.',
640
+ };
641
+ }
642
+ // rc-2: structural guards with explicit narrowing — the sections the edit
643
+ // touches must already exist (a loader-valid config always carries them).
644
+ const configRecord = isRecord(parsed) ? parsed : undefined;
645
+ const runtimeProfiles = configRecord !== undefined && isRecord(configRecord.runtimeProfiles) ? configRecord.runtimeProfiles : undefined;
646
+ const internalAgents = configRecord !== undefined && isRecord(configRecord.internalAgents) ? configRecord.internalAgents : undefined;
647
+ const agentBindings = internalAgents !== undefined && isRecord(internalAgents.agents) ? internalAgents.agents : undefined;
648
+ if (runtimeProfiles === undefined || internalAgents === undefined || agentBindings === undefined) {
649
+ return {
650
+ ok: false,
651
+ reason: `workbuddy_governance_profile_invalid_structure: ${configPath} is missing runtimeProfiles/internalAgents (the loader would reject it). No write was performed.`,
652
+ nextAction: `Restore a loader-valid governance config ("pd runtime init --confirm --workspace ${governanceDir}" never overwrites an existing file — fix the sections manually), then re-run the installer.`,
653
+ };
654
+ }
655
+ // Preserve unknown extra fields of an existing pd.default (baseUrl,
656
+ // timeoutMs, maxTokens, …); the install-collected fields win.
657
+ const existingProfile = isRecord(runtimeProfiles[GOVERNANCE_PROFILE_ID]) ? runtimeProfiles[GOVERNANCE_PROFILE_ID] : {};
658
+ // Round-4 S2 model resolution: explicit --model wins; omitted → preserve
659
+ // the existing valid model (existing-coverage contract); otherwise apply
660
+ // the shared per-provider default. No path writes an empty model.
661
+ const existingModel = typeof existingProfile.model === 'string' ? existingProfile.model.trim() : '';
662
+ let model = modelArg;
663
+ if (model.length === 0) {
664
+ if (existingModel.length > 0) {
665
+ model = existingModel;
666
+ }
667
+ else {
668
+ model = resolveDefaultModelForProvider(provider);
669
+ if (model.length === 0) {
670
+ return {
671
+ ok: false,
672
+ reason: `workbuddy_governance_profile_model_unresolvable: --model was omitted and provider '${provider}' has no known default model.`,
673
+ nextAction: 'Re-run the installer with --model <model-id> for the selected provider.',
674
+ };
675
+ }
676
+ }
677
+ }
678
+ runtimeProfiles[GOVERNANCE_PROFILE_ID] = {
679
+ ...existingProfile,
680
+ type: 'pi-ai',
681
+ provider,
682
+ model,
683
+ apiKeyEnv,
684
+ // Custom endpoint only when collected; an existing pd.default.baseUrl
685
+ // survives via the spread above when the arg is omitted.
686
+ ...(baseUrl.length > 0 ? { baseUrl } : {}),
687
+ };
688
+ internalAgents.defaultRuntime = GOVERNANCE_PROFILE_ID;
689
+ // Preserve the diagnostician's other binding fields (its `enabled` kill
690
+ // switch stays Owner-authoritative — PRI-638 — never re-enabled here).
691
+ const existingBinding = isRecord(agentBindings.diagnostician) ? agentBindings.diagnostician : {};
692
+ agentBindings.diagnostician = { ...existingBinding, runtimeProfile: GOVERNANCE_PROFILE_ID };
693
+ // Round-4 S1 pending-config honesty: agents whose EXPLICIT per-agent row
694
+ // still points elsewhere (legacy init rows / Owner rows — indistinguishable
695
+ // here, so never overridden) keep resolving to their own profile. Report
696
+ // them; the installer result carries the honest state and the rebind path.
697
+ // Data-driven from the config's own agents map — no second agent-name list.
698
+ const pendingAgents = Object.keys(agentBindings).filter((name) => {
699
+ const binding = agentBindings[name];
700
+ return isRecord(binding)
701
+ && typeof binding.runtimeProfile === 'string'
702
+ && binding.runtimeProfile !== GOVERNANCE_PROFILE_ID;
703
+ });
704
+ // Preserve the leading comment header (the "single source of truth" banner
705
+ // pd runtime init writes) and mark the installer override.
706
+ const headerLines = [];
707
+ for (const line of raw.split('\n')) {
708
+ const trimmed = line.trim();
709
+ if (trimmed === '' && headerLines.length === 0)
710
+ continue;
711
+ if (trimmed.startsWith('#'))
712
+ headerLines.push(line);
713
+ else
714
+ break;
715
+ }
716
+ const marker = '# runtimeProfiles.pd.default + internalAgents default/diagnostician binding written by the Principles Disciple installer (--provider/--api-key-env/--model/--base-url).';
717
+ const body = yaml.dump(parsed, { lineWidth: -1, quoteStyle: 'double' });
718
+ const next = headerLines.length > 0 ? `${headerLines.join('\n')}\n${marker}\n${body}` : `${marker}\n${body}`;
719
+ // Round-trip verification, loader-grade: parse with the SAME schema the
720
+ // production loader uses and assert exactly what the install promised
721
+ // before anything touches the disk.
722
+ let verify;
723
+ try {
724
+ verify = yaml.load(next, { schema: yaml.JSON_SCHEMA });
725
+ }
726
+ catch (error) {
727
+ return {
728
+ ok: false,
729
+ reason: `workbuddy_governance_profile_rewrite_invalid: verification parse failed (${error instanceof Error ? error.message : String(error)}). Nothing was written.`,
730
+ nextAction: 'Report this failure — the profile edit produced invalid YAML.',
731
+ };
732
+ }
733
+ const verifiedRecord = isRecord(verify) ? verify : undefined;
734
+ const verifiedProfiles = verifiedRecord !== undefined && isRecord(verifiedRecord.runtimeProfiles) ? verifiedRecord.runtimeProfiles : undefined;
735
+ const verifiedInternal = verifiedRecord !== undefined && isRecord(verifiedRecord.internalAgents) ? verifiedRecord.internalAgents : undefined;
736
+ const verifiedAgentBindings = verifiedInternal !== undefined && isRecord(verifiedInternal.agents) ? verifiedInternal.agents : undefined;
737
+ const verifiedProfile = verifiedProfiles !== undefined ? verifiedProfiles[GOVERNANCE_PROFILE_ID] : undefined;
738
+ const verifiedBinding = verifiedAgentBindings !== undefined ? verifiedAgentBindings.diagnostician : undefined;
739
+ const verifyOk = isRecord(verifiedProfile)
740
+ && verifiedProfile.type === 'pi-ai'
741
+ && verifiedProfile.provider === provider && provider.length > 0
742
+ && verifiedProfile.model === model && typeof model === 'string' && model.length > 0
743
+ && verifiedProfile.apiKeyEnv === apiKeyEnv && apiKeyEnv.length > 0
744
+ // A requested custom endpoint must survive the round trip (dropping it
745
+ // would silently reintroduce provider catalog validation).
746
+ && (baseUrl.length === 0 || verifiedProfile.baseUrl === baseUrl)
747
+ && verifiedInternal !== undefined
748
+ && verifiedInternal.defaultRuntime === GOVERNANCE_PROFILE_ID
749
+ && isRecord(verifiedBinding)
750
+ && verifiedBinding.runtimeProfile === GOVERNANCE_PROFILE_ID;
751
+ if (!verifyOk) {
752
+ return {
753
+ ok: false,
754
+ reason: 'workbuddy_governance_profile_rewrite_mismatch: round-trip verification did not observe the requested profile + binding. Nothing was written.',
755
+ nextAction: 'Report this failure with the governance config.yaml content.',
756
+ };
757
+ }
758
+ try {
759
+ mkdirSync(path.dirname(configPath), { recursive: true });
760
+ atomicReplaceTextFile(configPath, next);
761
+ }
762
+ catch (error) {
763
+ return {
764
+ ok: false,
765
+ reason: `workbuddy_governance_profile_write_failed: could not write ${configPath} (${error instanceof Error ? error.message : String(error)}). The previous governance config was left unchanged.`,
766
+ nextAction: `Check permissions under ${path.dirname(configPath)}, close tools holding the file, then re-run the installer.`,
767
+ };
768
+ }
769
+ return { ok: true, configPath, profileId: GOVERNANCE_PROFILE_ID, model, pendingAgents };
770
+ }
771
+ // ─── WorkBuddyHostInstaller ─────────────────────────────────────────────────
772
+ /** Materialize the PD-owned marketplace dir (source never escapes its root). */
773
+ function materializeMarketplace(marketplaceRoot, pluginDir) {
774
+ rmSync(marketplaceRoot, { recursive: true, force: true });
775
+ const marketplaceManifestDir = path.join(marketplaceRoot, '.codebuddy-plugin');
776
+ mkdirSync(marketplaceManifestDir, { recursive: true });
777
+ const marketplaceManifest = {
778
+ name: WORKBUDDY_MARKETPLACE_NAME,
779
+ plugins: [{ name: WORKBUDDY_PLUGIN_NAME, source: `./${WORKBUDDY_PLUGIN_NAME}` }],
780
+ };
781
+ writeFileSync(path.join(marketplaceManifestDir, 'marketplace.json'), JSON.stringify(marketplaceManifest, null, 2) + '\n', 'utf-8');
782
+ // Plugin skeleton copied INSIDE the marketplace root — the engine rejects
783
+ // sources that escape it ("Path escapes marketplace root", frozen facts).
784
+ cpSync(pluginDir, path.join(marketplaceRoot, WORKBUDDY_PLUGIN_NAME), { recursive: true });
785
+ }
786
+ export class WorkBuddyHostInstaller {
787
+ hostId = 'workbuddy';
788
+ deps;
789
+ constructor(deps = {}) {
790
+ const homeDir = deps.homeDir ?? getHomeDir;
791
+ this.deps = {
792
+ homeDir,
793
+ enginePath: deps.enginePath ?? resolveWorkbuddyEnginePath,
794
+ pdCliEntry: deps.pdCliEntry ?? (() => {
795
+ // install-layout SSoT: the shared runtime pd-cli the main installer
796
+ // deploys and invokes (getInstallLayoutPaths(home).pdCliDir).
797
+ const entry = path.join(getInstallLayoutPaths(homeDir()).pdCliDir, 'dist', 'index.js');
798
+ return existsSync(entry) ? entry : undefined;
799
+ }),
800
+ runCommand: deps.runCommand ?? ((file, args, opts) => {
801
+ const result = spawnSync(file, [...args], {
802
+ encoding: 'utf-8',
803
+ timeout: opts?.timeoutMs ?? ENGINE_COMMAND_TIMEOUT_MS,
804
+ windowsHide: true,
805
+ ...(opts?.env !== undefined ? { env: { ...process.env, ...opts.env } } : {}),
806
+ });
807
+ return {
808
+ status: result.status,
809
+ stdout: typeof result.stdout === 'string' ? result.stdout : '',
810
+ stderr: typeof result.stderr === 'string' ? result.stderr : '',
811
+ error: result.error instanceof Error ? result.error.message : result.error !== undefined ? String(result.error) : undefined,
812
+ };
813
+ }),
814
+ };
815
+ }
816
+ paths() {
817
+ return resolveWorkbuddyPaths(this.deps.homeDir());
818
+ }
819
+ /**
820
+ * Run `node <engine> <args>`; returns undefined when the invocation itself
821
+ * failed.
822
+ *
823
+ * S2 (review 2026-10-13): every engine command is pinned to the DECLARED
824
+ * config root by injecting BOTH isolation variables (the Phase 0 P0
825
+ * recipe). The real engine prefers CODEBUDDY_CONFIG_DIR; without the
826
+ * injection it registered into ~/.codebuddy / a pre-existing CodeBuddy root
827
+ * while this installer verified the declared WorkBuddy root — command
828
+ * success and target registration diverged. WORKBUDDY_CONFIG_DIR is
829
+ * injected as well so engine versions that honor the WorkBuddy-scoped
830
+ * variable stay on the same root.
831
+ */
832
+ runEngine(args) {
833
+ const engine = this.deps.enginePath();
834
+ if (engine === undefined)
835
+ return undefined;
836
+ const { configDir } = this.paths();
837
+ const result = this.deps.runCommand(process.execPath, [engine, ...args], {
838
+ env: {
839
+ CODEBUDDY_CONFIG_DIR: configDir,
840
+ WORKBUDDY_CONFIG_DIR: configDir,
841
+ },
842
+ });
843
+ return { ...result, engine };
844
+ }
845
+ // ── Preconditions ──────────────────────────────────────────────────────────
846
+ /** Hooks run via `node` on PATH through Git Bash (frozen hook-runtime-contract shellAndRuntime). */
847
+ preflightRuntime() {
848
+ const nodeCheck = this.deps.runCommand('node', ['--version'], { timeoutMs: 15_000 });
849
+ if (nodeCheck.status !== 0) {
850
+ return {
851
+ ok: false,
852
+ reason: `node_not_available: WorkBuddy command hooks start with "node" resolved from PATH, but invoking it failed (${nodeCheck.error ?? `exit ${nodeCheck.status}`}). PD cannot install a hook chain that would never start.`,
853
+ nextAction: 'Install Node.js (>= 20) and make "node" resolvable on PATH, then re-run the installer.',
854
+ };
855
+ }
856
+ if (process.platform === 'win32') {
857
+ const bashCheck = this.deps.runCommand('bash', ['-c', 'exit 0'], { timeoutMs: 15_000 });
858
+ if (bashCheck.status !== 0) {
859
+ return {
860
+ ok: false,
861
+ reason: `git_bash_not_available: WorkBuddy executes command hooks through Git Bash on Windows, but "bash" is not resolvable (${bashCheck.error ?? `exit ${bashCheck.status}`}). Hooks would fail to launch.`,
862
+ nextAction: 'Install Git for Windows (or otherwise provide bash.exe on PATH), then re-run the installer.',
863
+ };
864
+ }
865
+ }
866
+ return { ok: true };
867
+ }
868
+ // ─── install ──────────────────────────────────────────────────────────────
869
+ async install(ctx) {
870
+ const p = this.paths();
871
+ const details = this.detectDetails();
872
+ // 1. Preflight: host presence + hook runtime prerequisites.
873
+ if (!details.hostFound && !details.engineFound) {
874
+ return {
875
+ success: false,
876
+ hostId: this.hostId,
877
+ configPath: p.settingsPath,
878
+ configAction: 'skipped',
879
+ reason: 'workbuddy_host_not_found: neither a WorkBuddy config directory nor the WorkBuddy engine was found on this machine.',
880
+ nextAction: 'Install WorkBuddy Desktop (the engine ships inside it), start it once, then re-run: npx create-principles-disciple --host workbuddy.',
881
+ };
882
+ }
883
+ const runtime = this.preflightRuntime();
884
+ if (!runtime.ok) {
885
+ return { success: false, hostId: this.hostId, configPath: p.settingsPath, configAction: 'skipped', reason: runtime.reason, nextAction: runtime.nextAction };
886
+ }
887
+ // 2. Preflight the host JSON files BEFORE any write: a malformed
888
+ // settings.json / installed_plugins.json must stop the install with
889
+ // ZERO side effects (rc-3) — not fail halfway after the marketplace
890
+ // dir was already materialized.
891
+ const settingsPre = readHostJsonFile(p.settingsPath);
892
+ if (!settingsPre.ok) {
893
+ return { success: false, hostId: this.hostId, configPath: p.settingsPath, configAction: 'skipped', reason: settingsPre.reason, nextAction: settingsPre.nextAction };
894
+ }
895
+ const registryPre = readHostJsonFile(p.registryPath);
896
+ if (!registryPre.ok) {
897
+ return { success: false, hostId: this.hostId, configPath: p.settingsPath, configAction: 'skipped', reason: registryPre.reason, nextAction: registryPre.nextAction };
898
+ }
899
+ // 3. Locate the bundled plugin skeleton (adapter payload).
900
+ const payloadFind = findWorkbuddyPluginPayload(ctx.pluginDir, p.runtimeAdapterDir);
901
+ if (!payloadFind.payload) {
902
+ return {
903
+ success: false,
904
+ hostId: this.hostId,
905
+ configPath: p.settingsPath,
906
+ configAction: 'skipped',
907
+ reason: payloadFind.reason ?? 'workbuddy_adapter_payload_missing',
908
+ nextAction: 'Update the installer (npx create-principles-disciple@latest) — this build does not carry the @principles/workbuddy-adapter component.',
909
+ };
910
+ }
911
+ const { payload } = payloadFind;
912
+ // 4. Materialize the PD-owned marketplace (source stays inside its root).
913
+ try {
914
+ materializeMarketplace(p.marketplaceRoot, payload.pluginDir);
915
+ }
916
+ catch (error) {
917
+ return {
918
+ success: false,
919
+ hostId: this.hostId,
920
+ configPath: p.settingsPath,
921
+ configAction: 'skipped',
922
+ reason: `workbuddy_marketplace_write_failed: ${error instanceof Error ? error.message : String(error)}`,
923
+ nextAction: `Check permissions under ${path.dirname(p.marketplaceRoot)} and re-run the installer.`,
924
+ };
925
+ }
926
+ // 5. Register: engine commands first; observable direct-write fallback.
927
+ const registration = this.registerPlugin(details, payload.version);
928
+ if (!registration.ok) {
929
+ return {
930
+ success: false,
931
+ hostId: this.hostId,
932
+ configPath: p.settingsPath,
933
+ configAction: 'skipped',
934
+ reason: registration.reason,
935
+ nextAction: registration.nextAction,
936
+ };
937
+ }
938
+ // 6. Initialize the governance workspace (OD-07 explicit creation) with
939
+ // the FULL loader-valid state the hook/console/worker chain reads:
940
+ // .pd/config.yaml, .pd/state.db and the canonical .state/trajectory.db
941
+ // schema. Review 2026-10-13 R1: this step previously only mkdir'd the
942
+ // directory, so install reported success while the hook chain saw an
943
+ // uninitialized workspace.
944
+ const governance = this.initializeGovernanceWorkspace(p.governanceDir);
945
+ if (!governance.ok) {
946
+ return {
947
+ success: false,
948
+ hostId: this.hostId,
949
+ configPath: p.settingsPath,
950
+ configAction: registration.settingsAction,
951
+ reason: governance.reason,
952
+ nextAction: governance.nextAction,
953
+ };
954
+ }
955
+ // 6.5 Round-3 remediation P1: the install-collected LLM runtime profile
956
+ // (--provider/--api-key-env/--model) is GOVERNANCE state for this
957
+ // host — it must land in the governance workspace config the
958
+ // worker/console/hook chain actually reads. (The main installer no
959
+ // longer writes it into the project config.yaml for this host, so
960
+ // this is the single writer — no second truth.) Without a profile the
961
+ // governance config keeps its runtime-init default untouched.
962
+ // Round-4 S1/S2: the applied result carries the model actually
963
+ // written (explicit / preserved / per-provider default) and the
964
+ // agents whose explicit bindings still resolve elsewhere — both are
965
+ // reported on the installer result surface, never silently smoothed.
966
+ let profileNote = '';
967
+ let profilePendingAgents = [];
968
+ if (ctx.runtimeProfile) {
969
+ const applied = applyWorkbuddyGovernanceRuntimeProfile(p.governanceDir, ctx.runtimeProfile);
970
+ if (!applied.ok) {
971
+ return {
972
+ success: false,
973
+ hostId: this.hostId,
974
+ configPath: p.settingsPath,
975
+ configAction: registration.settingsAction,
976
+ reason: applied.reason,
977
+ nextAction: applied.nextAction,
978
+ };
979
+ }
980
+ profileNote = ` LLM runtime profile '${applied.profileId}' (provider ${ctx.runtimeProfile.provider}, model ${applied.model}) written to ${applied.configPath}.`;
981
+ if (applied.pendingAgents.length > 0) {
982
+ profilePendingAgents = applied.pendingAgents;
983
+ profileNote += ` Learning agent(s) ${applied.pendingAgents.join(', ')} still bound to their explicit runtime profiles — they keep resolving to their current runtime, not '${applied.profileId}'.`;
984
+ }
985
+ }
986
+ // 7. Record host + governance workspace in the install manifest (set union).
987
+ const manifest = mergeWorkbuddyIntoInstallManifest(p.installManifestPath, p.governanceDir);
988
+ if (!manifest.ok) {
989
+ return {
990
+ success: false,
991
+ hostId: this.hostId,
992
+ configPath: p.settingsPath,
993
+ configAction: registration.settingsAction,
994
+ reason: manifest.reason,
995
+ nextAction: manifest.nextAction,
996
+ };
997
+ }
998
+ // 8. Install ≠ enable (OD-03): the host.workbuddy flag stays at its quiet
999
+ // default (OFF). No config.yaml override is written here.
1000
+ const effectiveNote = 'Registration takes effect in NEW WorkBuddy sessions (or /reload-plugins in a running CLI session).';
1001
+ // Round-4 S1: honest pending-config surface — legacy init rows and Owner
1002
+ // rows are kept (no silent override); the rebind path is stated, never
1003
+ // executed behind the Owner's back.
1004
+ const pendingRebindNote = profilePendingAgents.length > 0
1005
+ ? ` Pending config: agent(s) ${profilePendingAgents.join(', ')} still bind to their explicit runtime profiles; to rebind them to the install profile, set internalAgents.agents.<name>.runtimeProfile to '${GOVERNANCE_PROFILE_ID}' (or remove the per-agent runtimeProfile row so they inherit internalAgents.defaultRuntime) in ${path.join(p.governanceDir, '.pd', 'config.yaml')}.`
1006
+ : '';
1007
+ return {
1008
+ success: true,
1009
+ hostId: this.hostId,
1010
+ configPath: p.settingsPath,
1011
+ configAction: registration.settingsAction,
1012
+ reason: `Registered ${WORKBUDDY_PLUGIN_KEY} via ${registration.mode}${registration.mode === 'engine' ? '' : ` (engine unreachable: ${registration.engineNote})`}. Governance workspace initialized at ${p.governanceDir}${governance.dbInit === 'skipped' ? ` (${governance.note})` : ''}.${profileNote} PD governance stays DISABLED until you explicitly enable it.`,
1013
+ nextAction: `${effectiveNote}${governance.dbInit === 'skipped' ? ` After the install, run "pd runtime init --workspace ${p.governanceDir}" to create the governance databases.` : ''} To enable PD governance, set host.workbuddy: { enabled: true } in ${path.join(p.governanceDir, '.pd', 'config.yaml')} (the console flag surface or the installer enable API shows the target config first).${pendingRebindNote}`,
1014
+ };
1015
+ }
1016
+ /**
1017
+ * Initialize the OD-07 governance workspace with the FULL state the
1018
+ * hook/console/worker chain reads (review 2026-10-13 R1).
1019
+ *
1020
+ * Preferred path: the SHARED initializer — `pd-cli runtime init --confirm
1021
+ * --workspace <governanceDir>` — the exact entry the main installer runs for
1022
+ * the generic workspace. It generates .pd/config.yaml from the core default
1023
+ * config authority, .pd/state.db, and .state/trajectory.db with the
1024
+ * canonical schema (idempotent; an existing config.yaml is never
1025
+ * overwritten). Failure is a LOUD install failure — a registration without
1026
+ * working governance state is exactly the R1 defect.
1027
+ *
1028
+ * Degraded path (pd-cli entry not resolvable — standalone host-installer
1029
+ * use): the installer lays down the loader-valid default config from its own
1030
+ * authority (generateConfigYamlContent, the same content the main installer
1031
+ * writes for other hosts) so the hook CONFIG chain works, and reports the
1032
+ * database initialization as an observable skip with an explicit next
1033
+ * action — never a silent mkdir-only "success".
1034
+ */
1035
+ initializeGovernanceWorkspace(governanceDir) {
1036
+ try {
1037
+ mkdirSync(governanceDir, { recursive: true });
1038
+ }
1039
+ catch (error) {
1040
+ return {
1041
+ ok: false,
1042
+ reason: `workbuddy_governance_dir_failed: could not create ${governanceDir} (${error instanceof Error ? error.message : String(error)}). The plugin is registered but governance state has no home.`,
1043
+ nextAction: `Create ${governanceDir} manually (or free the parent directory) and re-run the installer.`,
1044
+ };
1045
+ }
1046
+ const configPath = path.join(governanceDir, '.pd', 'config.yaml');
1047
+ const pdCli = this.deps.pdCliEntry();
1048
+ if (pdCli !== undefined) {
1049
+ const init = this.deps.runCommand(process.execPath, [pdCli, 'runtime', 'init', '--confirm', '--workspace', governanceDir], { timeoutMs: GOVERNANCE_INIT_TIMEOUT_MS });
1050
+ if (init.status !== 0) {
1051
+ return {
1052
+ ok: false,
1053
+ reason: `workbuddy_governance_init_failed: pd runtime init exited ${init.status}: ${(init.stderr || init.stdout).trim().slice(0, 300)}`,
1054
+ nextAction: `Run "node ${pdCli} runtime init --confirm --workspace ${governanceDir}" manually to see the full error, fix it, then re-run the installer.`,
1055
+ };
1056
+ }
1057
+ // Evidence over assumption: the artifacts the hook chain requires must
1058
+ // actually exist after the initializer reported success.
1059
+ if (!existsSync(configPath) || !existsSync(path.join(governanceDir, '.state', 'trajectory.db'))) {
1060
+ return {
1061
+ ok: false,
1062
+ reason: `workbuddy_governance_init_incomplete: runtime init reported success but ${configPath} or ${path.join(governanceDir, '.state', 'trajectory.db')} is missing.`,
1063
+ nextAction: 'Re-run the installer; if it repeats, report the pd runtime init output.',
1064
+ };
1065
+ }
1066
+ return { ok: true, dbInit: 'initialized' };
1067
+ }
1068
+ // Degraded path: no pd-cli runtime entry. Lay down the loader-valid
1069
+ // default config from the installer's own authority; never overwrite an
1070
+ // existing config (preservation — the runtime init contract matches).
1071
+ if (!existsSync(configPath)) {
1072
+ try {
1073
+ mkdirSync(path.dirname(configPath), { recursive: true });
1074
+ atomicReplaceTextFile(configPath, generateConfigYamlContent());
1075
+ }
1076
+ catch (error) {
1077
+ return {
1078
+ ok: false,
1079
+ reason: `workbuddy_governance_config_failed: could not write ${configPath} (${error instanceof Error ? error.message : String(error)}).`,
1080
+ nextAction: `Check permissions under ${governanceDir} and re-run the installer.`,
1081
+ };
1082
+ }
1083
+ }
1084
+ return {
1085
+ ok: true,
1086
+ dbInit: 'skipped',
1087
+ note: `governance database initialization SKIPPED: pd-cli runtime entry not found — run "pd runtime init --workspace ${governanceDir}" after the install; until then pain evidence degrades with an explicit trajectory_db_not_found note.`,
1088
+ };
1089
+ }
1090
+ registerPlugin(details, version) {
1091
+ const p = this.paths();
1092
+ // Idempotent fast path: already registered AND enabled AND cached.
1093
+ if (details.registered && details.pluginEnabled && existsSync(p.cachePluginRoot)) {
1094
+ return { ok: true, mode: 'already-registered', settingsAction: 'preserved' };
1095
+ }
1096
+ // Engine path (preferred — real marketplace/cache/settings chain).
1097
+ if (details.engineFound) {
1098
+ const add = this.runEngine(['plugin', 'marketplace', 'add', p.marketplaceRoot]);
1099
+ if (add !== undefined && add.status === 0 && MARKETPLACE_ADD_OK.test(add.stdout + add.stderr)) {
1100
+ if (details.registered) {
1101
+ // Already installed: engine marketplace re-add is enough; installing
1102
+ // again is not idempotent-tested, so preserve the existing entry.
1103
+ return { ok: true, mode: 'engine', settingsAction: 'preserved' };
1104
+ }
1105
+ const install = this.runEngine(['plugin', 'install', WORKBUDDY_PLUGIN_KEY]);
1106
+ if (install !== undefined && install.status === 0 && PLUGIN_INSTALL_OK.test(install.stdout + install.stderr)) {
1107
+ let after = this.detectDetails();
1108
+ if (after.registered && !after.pluginEnabled) {
1109
+ // Engine-version reality (observed on 2.161.0, 2026-10-13): the
1110
+ // engine honors the payload manifest's `defaultEnabled` and
1111
+ // installs hooks-carrying plugins with enabledPlugins[key]=false.
1112
+ // The installer contract (P01 frozen facts + this installer's own
1113
+ // direct-write path) is installed ⇒ enabledPlugins[key]=true —
1114
+ // the explicit host-level act of installing. PD governance stays
1115
+ // OFF either way (OD-03: features.host.workbuddy is a separate,
1116
+ // later, user-visible enable).
1117
+ const enabled = this.enableRegisteredPlugin();
1118
+ if (!enabled.ok) {
1119
+ return { ok: false, reason: enabled.reason, nextAction: enabled.nextAction };
1120
+ }
1121
+ after = this.detectDetails();
1122
+ }
1123
+ if (after.registered && after.pluginEnabled) {
1124
+ return { ok: true, mode: 'engine', settingsAction: after.settingsExisted ? 'updated' : 'created' };
1125
+ }
1126
+ return {
1127
+ ok: false,
1128
+ reason: `workbuddy_engine_install_unverified: engine reported success but the frozen-format state (enabledPlugins + installed_plugins.json) does not carry ${WORKBUDDY_PLUGIN_KEY} afterwards.`,
1129
+ nextAction: `Run "node <engine> plugin list" to inspect the host state, then re-run the installer.`,
1130
+ };
1131
+ }
1132
+ return {
1133
+ ok: false,
1134
+ reason: `workbuddy_engine_install_failed: ${install !== undefined ? (install.stderr || install.stdout).trim().slice(0, 300) || `exit ${install.status}` : 'engine invocation failed'}.`,
1135
+ nextAction: `Resolve the engine error above (run "node <engine> plugin install ${WORKBUDDY_PLUGIN_KEY}" manually to see full output), then re-run the installer.`,
1136
+ };
1137
+ }
1138
+ // Engine present but the command failed → direct-write fallback with an
1139
+ // observable note (rc-9). Fall through.
1140
+ const note = add !== undefined
1141
+ ? `engine marketplace add failed: ${((add.stderr || add.stdout).trim().slice(0, 200) || `exit ${add.status}`)}`
1142
+ : 'engine invocation failed';
1143
+ const direct = this.directWriteRegistration(details, version, note);
1144
+ if (direct.ok)
1145
+ return direct;
1146
+ return { ok: false, reason: direct.reason, nextAction: direct.nextAction };
1147
+ }
1148
+ // Engine unreachable: direct-write path, strictly aligned with the frozen
1149
+ // formats, and always reported as a distinct mode.
1150
+ const direct = this.directWriteRegistration(details, version, 'engine binary not found');
1151
+ if (direct.ok)
1152
+ return direct;
1153
+ return { ok: false, reason: direct.reason, nextAction: direct.nextAction };
1154
+ }
1155
+ /**
1156
+ * Set enabledPlugins[OUR_KEY]=true in settings.json, preserving every other
1157
+ * key (same protected write discipline as the direct-write registration).
1158
+ * Converges the engine install path (which honors the payload manifest's
1159
+ * defaultEnabled) onto the installer's frozen installed ⇒ enabled contract.
1160
+ */
1161
+ enableRegisteredPlugin() {
1162
+ const p = this.paths();
1163
+ const settings = readHostJsonFile(p.settingsPath);
1164
+ if (!settings.ok)
1165
+ return { ok: false, reason: settings.reason, nextAction: settings.nextAction };
1166
+ const nextSettings = { ...(settings.value ?? {}) };
1167
+ const enabledPlugins = isRecord(nextSettings.enabledPlugins) ? { ...nextSettings.enabledPlugins } : {};
1168
+ enabledPlugins[WORKBUDDY_PLUGIN_KEY] = true;
1169
+ nextSettings.enabledPlugins = enabledPlugins;
1170
+ const write = writeHostJsonFileProtected(p.settingsPath, JSON.stringify(nextSettings, null, 2) + '\n');
1171
+ if (!write.ok)
1172
+ return { ok: false, reason: write.reason, nextAction: write.nextAction };
1173
+ return { ok: true };
1174
+ }
1175
+ /**
1176
+ * Direct-write registration for engine-unreachable machines, aligned with the
1177
+ * frozen P01 formats: enabledPlugins[key]=true in settings.json (other keys
1178
+ * and other plugins preserved), installed_plugins.json v2 entry
1179
+ * {scope:'user', installPath, version, installedAt}, and the versioned cache
1180
+ * copy under plugins/cache/<marketplace>/<plugin>/<version>/.
1181
+ */
1182
+ directWriteRegistration(details, version, engineNote) {
1183
+ const p = this.paths();
1184
+ const settings = readHostJsonFile(p.settingsPath);
1185
+ if (!settings.ok)
1186
+ return { ok: false, reason: settings.reason, nextAction: settings.nextAction };
1187
+ const registry = readHostJsonFile(p.registryPath);
1188
+ if (!registry.ok)
1189
+ return { ok: false, reason: registry.reason, nextAction: registry.nextAction };
1190
+ // settings.json merge
1191
+ const nextSettings = { ...(settings.value ?? {}) };
1192
+ const enabledPlugins = isRecord(nextSettings.enabledPlugins) ? { ...nextSettings.enabledPlugins } : {};
1193
+ enabledPlugins[WORKBUDDY_PLUGIN_KEY] = true;
1194
+ nextSettings.enabledPlugins = enabledPlugins;
1195
+ const settingsWrite = writeHostJsonFileProtected(p.settingsPath, JSON.stringify(nextSettings, null, 2) + '\n');
1196
+ if (!settingsWrite.ok)
1197
+ return { ok: false, reason: settingsWrite.reason, nextAction: settingsWrite.nextAction };
1198
+ const settingsAction = settings.existed ? 'updated' : 'created';
1199
+ // installed_plugins.json v2 merge (create with version 2 when absent)
1200
+ const nextRegistry = { ...(registry.value ?? {}) };
1201
+ if (typeof nextRegistry.version !== 'number')
1202
+ nextRegistry.version = 2;
1203
+ const plugins = isRecord(nextRegistry.plugins) ? { ...nextRegistry.plugins } : {};
1204
+ const cacheVersionDir = path.join(p.cachePluginRoot, version);
1205
+ plugins[WORKBUDDY_PLUGIN_KEY] = {
1206
+ scope: 'user',
1207
+ installPath: cacheVersionDir,
1208
+ version,
1209
+ installedAt: new Date().toISOString(),
1210
+ };
1211
+ nextRegistry.plugins = plugins;
1212
+ const registryWrite = writeHostJsonFileProtected(p.registryPath, JSON.stringify(nextRegistry, null, 2) + '\n');
1213
+ if (!registryWrite.ok)
1214
+ return { ok: false, reason: registryWrite.reason, nextAction: registryWrite.nextAction };
1215
+ // Versioned cache copy from our marketplace source.
1216
+ try {
1217
+ rmSync(p.cachePluginRoot, { recursive: true, force: true });
1218
+ cpSync(path.join(p.marketplaceRoot, WORKBUDDY_PLUGIN_NAME), cacheVersionDir, { recursive: true });
1219
+ }
1220
+ catch (error) {
1221
+ return {
1222
+ ok: false,
1223
+ reason: `workbuddy_cache_write_failed: ${error instanceof Error ? error.message : String(error)}. Settings and registry were updated; the cache copy is incomplete.`,
1224
+ nextAction: `Re-run the installer (idempotent), or run the engine command manually: node <engine> plugin install ${WORKBUDDY_PLUGIN_KEY}.`,
1225
+ };
1226
+ }
1227
+ return { ok: true, mode: 'direct-write', settingsAction, engineNote };
1228
+ }
1229
+ // ─── enable / disable (explicit, user-visible — OD-03) ─────────────────────
1230
+ /** Human-readable preview of exactly what enabling will change (shown BEFORE any write). */
1231
+ describeGovernanceEnable() {
1232
+ const p = this.paths();
1233
+ return {
1234
+ flag: 'host.workbuddy',
1235
+ configPath: path.join(p.governanceDir, '.pd', 'config.yaml'),
1236
+ value: true,
1237
+ takesEffect: 'next hook invocation (hooks read the flag fresh per event)',
1238
+ };
1239
+ }
1240
+ /** Explicit enable of PD governance (install never calls this). */
1241
+ enableGovernance() {
1242
+ const p = this.paths();
1243
+ return setWorkbuddyGovernanceFlag(p.governanceDir, true);
1244
+ }
1245
+ /** Explicit disable of PD governance (reversible rollback path). */
1246
+ disableGovernance() {
1247
+ const p = this.paths();
1248
+ return setWorkbuddyGovernanceFlag(p.governanceDir, false);
1249
+ }
1250
+ // ─── uninstall ────────────────────────────────────────────────────────────
1251
+ async uninstall(_ctx) {
1252
+ const p = this.paths();
1253
+ const details = this.detectDetails();
1254
+ const removedPaths = [];
1255
+ const preservedPaths = [];
1256
+ const failedPaths = [];
1257
+ const notes = [];
1258
+ // 1. Disable first so hooks stop even if later steps fail (belt).
1259
+ if (details.pluginEnabled) {
1260
+ const disable = details.engineFound
1261
+ ? this.runEngine(['plugin', 'disable', WORKBUDDY_PLUGIN_KEY])
1262
+ : undefined;
1263
+ if (disable === undefined || disable.status !== 0) {
1264
+ notes.push('engine disable command unavailable or failed — proceeding to full uninstall/removal.');
1265
+ }
1266
+ }
1267
+ // 2. Unregister via the engine when possible.
1268
+ if (details.registered && details.engineFound) {
1269
+ const uninstall = this.runEngine(['plugin', 'uninstall', WORKBUDDY_PLUGIN_KEY]);
1270
+ const engineUninstalled = uninstall !== undefined && uninstall.status === 0 && PLUGIN_UNINSTALL_OK.test(uninstall.stdout + uninstall.stderr);
1271
+ if (!engineUninstalled && uninstall !== undefined) {
1272
+ notes.push(`engine uninstall did not confirm success (${(uninstall.stderr || uninstall.stdout).trim().slice(0, 200) || `exit ${uninstall.status}`}).`);
1273
+ }
1274
+ }
1275
+ // 3. Direct-write removal of the frozen-format entries when the engine did
1276
+ // not (or could not) remove them. Only OUR key is touched.
1277
+ const afterEngine = this.detectDetails();
1278
+ if (afterEngine.registered || afterEngine.pluginEnabled) {
1279
+ const removal = this.directWriteRemoval();
1280
+ if (!removal.ok) {
1281
+ failedPaths.push(p.settingsPath, p.registryPath);
1282
+ preservedPaths.push(p.settingsPath, p.registryPath);
1283
+ return {
1284
+ success: false,
1285
+ hostId: this.hostId,
1286
+ removedPaths,
1287
+ preservedPaths,
1288
+ reason: removal.reason,
1289
+ nextAction: removal.nextAction,
1290
+ };
1291
+ }
1292
+ removedPaths.push(p.settingsPath, p.registryPath);
1293
+ }
1294
+ // 4. Remove OUR cache subtree (provable: cache/<OUR_MARKETPLACE>/<OUR_PLUGIN>).
1295
+ if (existsSync(p.cachePluginRoot)) {
1296
+ try {
1297
+ rmSync(p.cachePluginRoot, { recursive: true, force: true });
1298
+ removedPaths.push(p.cachePluginRoot);
1299
+ }
1300
+ catch (error) {
1301
+ failedPaths.push(p.cachePluginRoot);
1302
+ preservedPaths.push(p.cachePluginRoot);
1303
+ notes.push(`cache removal failed: ${error instanceof Error ? error.message : String(error)}`);
1304
+ }
1305
+ }
1306
+ // 5. Remove the PD-owned marketplace dir.
1307
+ if (existsSync(p.marketplaceRoot)) {
1308
+ try {
1309
+ rmSync(p.marketplaceRoot, { recursive: true, force: true });
1310
+ removedPaths.push(p.marketplaceRoot);
1311
+ }
1312
+ catch (error) {
1313
+ failedPaths.push(p.marketplaceRoot);
1314
+ preservedPaths.push(p.marketplaceRoot);
1315
+ notes.push(`marketplace removal failed: ${error instanceof Error ? error.message : String(error)}`);
1316
+ }
1317
+ }
1318
+ // 6. Manifest bookkeeping: drop 'workbuddy' from hosts. The governance
1319
+ // workspace entry and ALL governance data are preserved (§18 default).
1320
+ removeWorkbuddyFromInstallManifestHosts(p.installManifestPath);
1321
+ const governancePreservedByDesign = existsSync(p.governanceDir);
1322
+ if (governancePreservedByDesign) {
1323
+ preservedPaths.push(p.governanceDir);
1324
+ }
1325
+ // Success = every INTENDED removal happened. The governance database is
1326
+ // preserved BY DESIGN (never a failure); only failed removals fail.
1327
+ return {
1328
+ success: failedPaths.length === 0,
1329
+ hostId: this.hostId,
1330
+ removedPaths,
1331
+ preservedPaths,
1332
+ reason: failedPaths.length > 0
1333
+ ? `Partially uninstalled: ${failedPaths.join(', ')} could not be removed. ${notes.join(' ')}`
1334
+ : `Uninstalled ${WORKBUDDY_PLUGIN_KEY}. ${notes.join(' ')}`.trim(),
1335
+ nextAction: [
1336
+ 'Registration removal takes effect in NEW WorkBuddy sessions (or /reload-plugins in a running CLI session).',
1337
+ governancePreservedByDesign
1338
+ ? `The governance database at ${p.governanceDir} is PRESERVED by design — delete it manually only after exporting what you need.`
1339
+ : '',
1340
+ ].filter(Boolean).join(' '),
1341
+ };
1342
+ }
1343
+ /** Remove only OUR entries from settings.json + installed_plugins.json (frozen formats). */
1344
+ directWriteRemoval() {
1345
+ const p = this.paths();
1346
+ const settings = readHostJsonFile(p.settingsPath);
1347
+ if (!settings.ok)
1348
+ return { ok: false, reason: settings.reason, nextAction: settings.nextAction };
1349
+ if (settings.existed && isRecord(settings.value?.enabledPlugins) && Object.hasOwn(settings.value.enabledPlugins, WORKBUDDY_PLUGIN_KEY)) {
1350
+ const nextSettings = { ...settings.value };
1351
+ const enabledPlugins = { ...nextSettings.enabledPlugins };
1352
+ delete enabledPlugins[WORKBUDDY_PLUGIN_KEY];
1353
+ nextSettings.enabledPlugins = enabledPlugins;
1354
+ const write = writeHostJsonFileProtected(p.settingsPath, JSON.stringify(nextSettings, null, 2) + '\n');
1355
+ if (!write.ok)
1356
+ return { ok: false, reason: write.reason, nextAction: write.nextAction };
1357
+ }
1358
+ const registry = readHostJsonFile(p.registryPath);
1359
+ if (!registry.ok)
1360
+ return { ok: false, reason: registry.reason, nextAction: registry.nextAction };
1361
+ if (registry.existed && isRecord(registry.value?.plugins) && Object.hasOwn(registry.value.plugins, WORKBUDDY_PLUGIN_KEY)) {
1362
+ const nextRegistry = { ...registry.value };
1363
+ const plugins = { ...nextRegistry.plugins };
1364
+ delete plugins[WORKBUDDY_PLUGIN_KEY];
1365
+ nextRegistry.plugins = plugins;
1366
+ const write = writeHostJsonFileProtected(p.registryPath, JSON.stringify(nextRegistry, null, 2) + '\n');
1367
+ if (!write.ok)
1368
+ return { ok: false, reason: write.reason, nextAction: write.nextAction };
1369
+ }
1370
+ return { ok: true };
1371
+ }
1372
+ // ─── detect ───────────────────────────────────────────────────────────────
1373
+ detect() {
1374
+ return this.detectDetails();
1375
+ }
1376
+ /** Detailed detect: install presence vs host discovery vs registration vs enablement vs runtime reachability. */
1377
+ detectDetails() {
1378
+ const p = this.paths();
1379
+ const enginePath = this.deps.enginePath();
1380
+ const hostFound = existsSync(p.configDir);
1381
+ const engineFound = enginePath !== undefined;
1382
+ const settings = readHostJsonFile(p.settingsPath);
1383
+ const registry = readHostJsonFile(p.registryPath);
1384
+ let registered = false;
1385
+ if (registry.ok && registry.existed && isRecord(registry.value?.plugins)) {
1386
+ registered = Object.hasOwn(registry.value.plugins, WORKBUDDY_PLUGIN_KEY);
1387
+ }
1388
+ let pluginEnabled = false;
1389
+ if (settings.ok && settings.existed && isRecord(settings.value?.enabledPlugins)) {
1390
+ pluginEnabled = settings.value.enabledPlugins[WORKBUDDY_PLUGIN_KEY] === true;
1391
+ }
1392
+ const runtimeReachable = existsSync(path.join(p.runtimeAdapterDir, 'package.json')) && existsSync(path.join(p.runtimeAdapterDir, 'dist'));
1393
+ const governanceInitialized = existsSync(p.governanceDir);
1394
+ const installed = existsSync(p.marketplaceRoot) || registered || pluginEnabled;
1395
+ const result = {
1396
+ installed,
1397
+ hostFound,
1398
+ engineFound,
1399
+ ...(enginePath !== undefined ? { enginePath } : {}),
1400
+ registered,
1401
+ pluginEnabled,
1402
+ flagOverride: readWorkbuddyGovernanceFlagOverride(p.governanceDir),
1403
+ runtimeReachable,
1404
+ governanceInitialized,
1405
+ settingsExisted: settings.ok && settings.existed,
1406
+ paths: [
1407
+ { exists: hostFound, path: p.configDir, name: 'WorkBuddy config dir', type: 'dir' },
1408
+ { exists: engineFound, path: enginePath ?? '(engine not found)', name: 'WorkBuddy engine CLI', type: 'file' },
1409
+ { exists: existsSync(p.marketplaceRoot), path: p.marketplaceRoot, name: 'PD WorkBuddy marketplace (PD-owned)', type: 'dir' },
1410
+ { exists: registered, path: p.registryPath, name: 'installed_plugins.json carries PD registration', type: 'file' },
1411
+ { exists: pluginEnabled, path: p.settingsPath, name: 'settings.json enabledPlugins entry', type: 'file' },
1412
+ { exists: runtimeReachable, path: p.runtimeAdapterDir, name: 'PD runtime workbuddy-adapter', type: 'dir' },
1413
+ { exists: governanceInitialized, path: p.governanceDir, name: 'Governance workspace (OD-07)', type: 'dir' },
1414
+ ],
1415
+ };
1416
+ return result;
1417
+ }
1418
+ }
1419
+ //# sourceMappingURL=workbuddy-host-installer.js.map