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.
- package/LICENSE +73 -21
- package/NOTICE +2 -0
- package/README.md +1 -1
- package/_release/product-identity.json +2 -2
- package/codex-adapter/dist/index.d.ts +2 -2
- package/codex-adapter/dist/index.js +1 -1
- package/codex-adapter/dist/ingestion/admission.js +16 -0
- package/codex-adapter/dist/ingestion/codex-version.d.ts +10 -4
- package/codex-adapter/dist/ingestion/codex-version.js +10 -4
- package/codex-adapter/dist/ingestion/explicit-owner-statement.d.ts +48 -0
- package/codex-adapter/dist/ingestion/explicit-owner-statement.js +123 -0
- package/codex-adapter/dist/ingestion/ingestion.js +50 -1
- package/codex-adapter/dist/pd-hook.js +66 -3
- package/codex-adapter/dist/worker/workspace-worker.d.ts +14 -0
- package/codex-adapter/dist/worker/workspace-worker.js +194 -11
- package/codex-adapter/package.json +6 -4
- package/console/dist/server/models/ApprovalsConsoleModel.d.ts +2 -2
- package/console/dist/server/models/ApprovalsConsoleModel.js +1 -1
- package/console/dist/server/models/ApprovalsGroupedConsoleModel.js +1 -1
- package/console/dist/server/models/PrinciplesConsoleModel.d.ts +26 -1
- package/console/dist/server/models/PrinciplesConsoleModel.js +18 -1
- package/console/dist/server/routes/activations.js +4 -4
- package/console/dist/server/routes/approvals.js +2 -2
- package/console/dist/server/routes/failed-tasks.d.ts +9 -0
- package/console/dist/server/routes/failed-tasks.js +116 -7
- package/console/dist/server/routes/principles.js +73 -1
- package/console/dist/shared/prompt-injection-contract.d.ts +1 -1
- package/console/dist/ui/api.d.ts +5 -5
- package/console/dist/ui/i18n/en.json +13 -2
- package/console/dist/ui/i18n/zh-CN.json +13 -2
- package/console/dist/ui/pages/activation/ActivationPage.js +2 -1
- package/console/dist/ui/pages/failed-tasks/FailedTasksPage.js +42 -4
- package/console/dist/ui/pages/focus/FocusPage.js +3 -3
- package/console/dist/ui/pages/principles/PrinciplesPage.js +7 -2
- package/console/dist/ui/utils/validators.d.ts +6 -2
- package/console/dist/ui/utils/validators.js +19 -4
- package/console/dist/web/assets/app.css +26 -0
- package/console/dist/web/assets/app.js +24 -24
- package/console/package.json +6 -5
- package/core/dist/host/host-installer.d.ts +3 -1
- package/core/dist/host/host-installer.d.ts.map +1 -1
- package/core/dist/host/host-installer.js.map +1 -1
- package/core/dist/runtime-v2/admission-gate.d.ts +8 -0
- package/core/dist/runtime-v2/admission-gate.d.ts.map +1 -1
- package/core/dist/runtime-v2/admission-gate.js +3 -2
- package/core/dist/runtime-v2/admission-gate.js.map +1 -1
- package/core/dist/runtime-v2/config/pd-config-defaults.d.ts.map +1 -1
- package/core/dist/runtime-v2/config/pd-config-defaults.js +16 -1
- package/core/dist/runtime-v2/config/pd-config-defaults.js.map +1 -1
- package/core/dist/runtime-v2/context-payload.d.ts +43 -3
- package/core/dist/runtime-v2/context-payload.d.ts.map +1 -1
- package/core/dist/runtime-v2/context-payload.js +15 -1
- package/core/dist/runtime-v2/context-payload.js.map +1 -1
- package/core/dist/runtime-v2/evidence-guards.d.ts +6 -0
- package/core/dist/runtime-v2/evidence-guards.d.ts.map +1 -1
- package/core/dist/runtime-v2/evidence-guards.js +11 -0
- package/core/dist/runtime-v2/evidence-guards.js.map +1 -1
- package/core/dist/runtime-v2/evolution/evolution-types.d.ts +4 -4
- package/core/dist/runtime-v2/evolution/evolution-types.d.ts.map +1 -1
- package/core/dist/runtime-v2/evolution/evolution-types.js +1 -1
- package/core/dist/runtime-v2/evolution/evolution-types.js.map +1 -1
- package/core/dist/runtime-v2/feature-flags/feature-flag-contract.d.ts.map +1 -1
- package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js +6 -0
- package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js.map +1 -1
- package/core/dist/runtime-v2/feature-flags/feature-flag-lifecycle.d.ts.map +1 -1
- package/core/dist/runtime-v2/feature-flags/feature-flag-lifecycle.js +8 -0
- package/core/dist/runtime-v2/feature-flags/feature-flag-lifecycle.js.map +1 -1
- package/core/dist/runtime-v2/index.d.ts +5 -2
- package/core/dist/runtime-v2/index.d.ts.map +1 -1
- package/core/dist/runtime-v2/index.js +3 -1
- package/core/dist/runtime-v2/index.js.map +1 -1
- package/core/dist/runtime-v2/internalization/evaluator-output.d.ts +11 -0
- package/core/dist/runtime-v2/internalization/evaluator-output.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/evaluator-output.js +10 -0
- package/core/dist/runtime-v2/internalization/evaluator-output.js.map +1 -1
- package/core/dist/runtime-v2/internalization/evaluator-prompt-builder.d.ts +3 -3
- package/core/dist/runtime-v2/internalization/evaluator-prompt-builder.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/evaluator-prompt-builder.js +49 -5
- package/core/dist/runtime-v2/internalization/evaluator-prompt-builder.js.map +1 -1
- package/core/dist/runtime-v2/internalization/evaluator-runner.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/evaluator-runner.js +13 -16
- package/core/dist/runtime-v2/internalization/evaluator-runner.js.map +1 -1
- package/core/dist/runtime-v2/internalization/owner-decision-review.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/owner-decision-review.js +5 -1
- package/core/dist/runtime-v2/internalization/owner-decision-review.js.map +1 -1
- package/core/dist/runtime-v2/internalization/owner-review.d.ts +2 -2
- package/core/dist/runtime-v2/internalization/owner-review.js +2 -2
- package/core/dist/runtime-v2/internalization/scribe-output.d.ts +12 -0
- package/core/dist/runtime-v2/internalization/scribe-output.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/scribe-output.js +20 -0
- package/core/dist/runtime-v2/internalization/scribe-output.js.map +1 -1
- package/core/dist/runtime-v2/internalization/scribe-prompt-builder.d.ts +32 -1
- package/core/dist/runtime-v2/internalization/scribe-prompt-builder.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/scribe-prompt-builder.js +38 -1
- package/core/dist/runtime-v2/internalization/scribe-prompt-builder.js.map +1 -1
- package/core/dist/runtime-v2/internalization-queue-read-model.d.ts +6 -0
- package/core/dist/runtime-v2/internalization-queue-read-model.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization-queue-read-model.js +8 -1
- package/core/dist/runtime-v2/internalization-queue-read-model.js.map +1 -1
- package/core/dist/runtime-v2/intervention-evidence-normalizer.d.ts.map +1 -1
- package/core/dist/runtime-v2/intervention-evidence-normalizer.js +10 -3
- package/core/dist/runtime-v2/intervention-evidence-normalizer.js.map +1 -1
- package/core/dist/runtime-v2/pain-ingress.d.ts +1 -1
- package/core/dist/runtime-v2/pain-ingress.d.ts.map +1 -1
- package/core/dist/runtime-v2/pain-signal-bridge.d.ts +68 -2
- package/core/dist/runtime-v2/pain-signal-bridge.d.ts.map +1 -1
- package/core/dist/runtime-v2/pain-signal-bridge.js +541 -18
- package/core/dist/runtime-v2/pain-signal-bridge.js.map +1 -1
- package/core/dist/runtime-v2/pain-signal-runtime-factory.d.ts.map +1 -1
- package/core/dist/runtime-v2/pain-signal-runtime-factory.js +4 -0
- package/core/dist/runtime-v2/pain-signal-runtime-factory.js.map +1 -1
- package/core/dist/runtime-v2/product-telemetry/snapshot-contract.d.ts +2 -2
- package/core/dist/runtime-v2/product-telemetry/snapshot-contract.d.ts.map +1 -1
- package/core/dist/runtime-v2/product-telemetry/snapshot-contract.js +2 -2
- package/core/dist/runtime-v2/product-telemetry/snapshot-contract.js.map +1 -1
- package/core/dist/runtime-v2/store/candidate/admission-document.d.ts +90 -0
- package/core/dist/runtime-v2/store/candidate/admission-document.d.ts.map +1 -0
- package/core/dist/runtime-v2/store/candidate/admission-document.js +142 -0
- package/core/dist/runtime-v2/store/candidate/admission-document.js.map +1 -0
- package/core/dist/runtime-v2/store/candidate/candidate-store.d.ts +53 -0
- package/core/dist/runtime-v2/store/candidate/candidate-store.d.ts.map +1 -1
- package/core/dist/runtime-v2/store/candidate/memory-candidate-store.d.ts +35 -0
- package/core/dist/runtime-v2/store/candidate/memory-candidate-store.d.ts.map +1 -1
- package/core/dist/runtime-v2/store/candidate/memory-candidate-store.js +48 -0
- package/core/dist/runtime-v2/store/candidate/memory-candidate-store.js.map +1 -1
- package/core/dist/runtime-v2/store/candidate/sqlite-candidate-store.d.ts +44 -0
- package/core/dist/runtime-v2/store/candidate/sqlite-candidate-store.d.ts.map +1 -1
- package/core/dist/runtime-v2/store/candidate/sqlite-candidate-store.js +136 -0
- package/core/dist/runtime-v2/store/candidate/sqlite-candidate-store.js.map +1 -1
- package/core/dist/runtime-v2/store/context/sqlite-context-assembler.js.map +1 -1
- package/core/dist/runtime-v2/store/intervention-evidence-schema.js +1 -1
- package/core/dist/runtime-v2/store/runtime-state-manager.d.ts +65 -0
- package/core/dist/runtime-v2/store/runtime-state-manager.d.ts.map +1 -1
- package/core/dist/runtime-v2/store/runtime-state-manager.js +46 -0
- package/core/dist/runtime-v2/store/runtime-state-manager.js.map +1 -1
- package/core/dist/runtime-v2/store/sqlite-connection.d.ts.map +1 -1
- package/core/dist/runtime-v2/store/sqlite-connection.js +42 -0
- package/core/dist/runtime-v2/store/sqlite-connection.js.map +1 -1
- package/core/dist/runtime-v2/store/sqlite-intervention-evidence-store.d.ts +1 -1
- package/core/dist/runtime-v2/store/sqlite-intervention-evidence-store.d.ts.map +1 -1
- package/core/dist/runtime-v2/store/sqlite-intervention-evidence-store.js.map +1 -1
- package/core/dist/runtime-v2/store/task/sqlite-family-reopen.d.ts +19 -0
- package/core/dist/runtime-v2/store/task/sqlite-family-reopen.d.ts.map +1 -0
- package/core/dist/runtime-v2/store/task/sqlite-family-reopen.js +36 -0
- package/core/dist/runtime-v2/store/task/sqlite-family-reopen.js.map +1 -0
- package/core/dist/runtime-v2/store/task/sqlite-task-store.d.ts +23 -0
- package/core/dist/runtime-v2/store/task/sqlite-task-store.d.ts.map +1 -1
- package/core/dist/runtime-v2/store/task/sqlite-task-store.js +50 -0
- package/core/dist/runtime-v2/store/task/sqlite-task-store.js.map +1 -1
- package/core/dist/runtime-v2/task-status.d.ts +3 -1
- package/core/dist/runtime-v2/task-status.d.ts.map +1 -1
- package/core/dist/runtime-v2/task-status.js +1 -1
- package/core/dist/runtime-v2/task-status.js.map +1 -1
- package/core/dist/runtime-v2/trajectory-schema.d.ts.map +1 -1
- package/core/dist/runtime-v2/trajectory-schema.js +11 -0
- package/core/dist/runtime-v2/trajectory-schema.js.map +1 -1
- package/core/dist/runtime-v2/types/event-types.d.ts +2 -2
- package/core/dist/runtime-v2/types/event-types.d.ts.map +1 -1
- package/core/dist/runtime-v2/types/event-types.js +1 -1
- package/core/dist/runtime-v2/types/event-types.js.map +1 -1
- package/core/dist/runtime-v2/types/evidence-chain-contract.d.ts +1 -1
- package/core/dist/runtime-v2/types/evidence-chain-contract.d.ts.map +1 -1
- package/core/dist/runtime-v2/types/evidence-chain-contract.js +2 -2
- package/core/dist/runtime-v2/types/evidence-chain-contract.js.map +1 -1
- package/core/dist/runtime-v2/types/intervention-evidence-contract.d.ts +3 -3
- package/core/dist/runtime-v2/types/intervention-evidence-contract.d.ts.map +1 -1
- package/core/dist/runtime-v2/types/intervention-evidence-contract.js +1 -0
- package/core/dist/runtime-v2/types/intervention-evidence-contract.js.map +1 -1
- package/core/dist/telemetry-event.d.ts +2 -2
- package/core/dist/telemetry-event.d.ts.map +1 -1
- package/core/dist/telemetry-event.js +4 -0
- package/core/dist/telemetry-event.js.map +1 -1
- package/core/dist/trajectory-types.d.ts +2 -2
- package/core/dist/trajectory-types.d.ts.map +1 -1
- package/core/package.json +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +84 -8
- package/dist/index.js.map +1 -1
- package/dist/installer.d.ts +42 -11
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +140 -36
- package/dist/installer.js.map +1 -1
- package/dist/installers/index.d.ts +18 -7
- package/dist/installers/index.d.ts.map +1 -1
- package/dist/installers/index.js +13 -4
- package/dist/installers/index.js.map +1 -1
- package/dist/installers/workbuddy-host-installer.d.ts +246 -0
- package/dist/installers/workbuddy-host-installer.d.ts.map +1 -0
- package/dist/installers/workbuddy-host-installer.js +1419 -0
- package/dist/installers/workbuddy-host-installer.js.map +1 -0
- package/dist/mvp-config.d.ts +24 -0
- package/dist/mvp-config.d.ts.map +1 -1
- package/dist/mvp-config.js +36 -0
- package/dist/mvp-config.js.map +1 -1
- package/dist/prompts.d.ts +13 -0
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +45 -8
- package/dist/prompts.js.map +1 -1
- package/dist/uninstaller.d.ts +1 -1
- package/dist/uninstaller.d.ts.map +1 -1
- package/dist/uninstaller.js +29 -4
- package/dist/uninstaller.js.map +1 -1
- package/host-runtime/dist/governance-signal-admission.d.ts +63 -0
- package/host-runtime/dist/governance-signal-admission.js +322 -53
- package/host-runtime/dist/host-liveness-contract.js +7 -1
- package/host-runtime/dist/index.d.ts +1 -0
- package/host-runtime/dist/index.js +5 -1
- package/host-runtime/dist/internalization-consumer-cycle.d.ts +12 -0
- package/host-runtime/dist/internalization-consumer-cycle.js +43 -1
- package/host-runtime/dist/intervention-evidence-ingress.d.ts +1 -1
- package/host-runtime/dist/product-telemetry/service.js +1 -1
- package/host-runtime/dist/production-pain-evidence.d.ts +59 -1
- package/host-runtime/dist/production-pain-evidence.js +200 -17
- package/host-runtime/dist/prompt-activation-deliverability.js +5 -4
- package/host-runtime/dist/prompt-injection-event-evidence.d.ts +1 -1
- package/host-runtime/dist/prompt-injection-event-evidence.js +5 -1
- package/host-runtime/dist/prompt-injection-projection.d.ts +14 -5
- package/host-runtime/dist/prompt-injection-projection.js +30 -16
- package/host-runtime/dist/workbuddy-worker-status.d.ts +24 -0
- package/host-runtime/dist/workbuddy-worker-status.js +52 -0
- package/host-runtime/package.json +7 -5
- package/install-layout/dist/index.d.ts +18 -1
- package/install-layout/dist/index.js +15 -2
- package/install-layout/package.json +5 -3
- package/package.json +9 -7
- package/pd-cli/dist/commands/candidate.d.ts.map +1 -1
- package/pd-cli/dist/commands/candidate.js +44 -4
- package/pd-cli/dist/commands/candidate.js.map +1 -1
- package/pd-cli/dist/commands/codex-worker.d.ts.map +1 -1
- package/pd-cli/dist/commands/codex-worker.js +30 -1
- package/pd-cli/dist/commands/codex-worker.js.map +1 -1
- package/pd-cli/dist/commands/config-doctor.d.ts.map +1 -1
- package/pd-cli/dist/commands/config-doctor.js +19 -4
- package/pd-cli/dist/commands/config-doctor.js.map +1 -1
- package/pd-cli/dist/commands/diagnose.d.ts.map +1 -1
- package/pd-cli/dist/commands/diagnose.js +4 -1
- package/pd-cli/dist/commands/diagnose.js.map +1 -1
- package/pd-cli/dist/commands/health-codex-install.d.ts +85 -0
- package/pd-cli/dist/commands/health-codex-install.d.ts.map +1 -0
- package/pd-cli/dist/commands/health-codex-install.js +207 -0
- package/pd-cli/dist/commands/health-codex-install.js.map +1 -0
- package/pd-cli/dist/commands/health-codex.d.ts.map +1 -1
- package/pd-cli/dist/commands/health-codex.js +78 -0
- package/pd-cli/dist/commands/health-codex.js.map +1 -1
- package/pd-cli/dist/commands/pain-list.d.ts +3 -2
- package/pd-cli/dist/commands/pain-list.d.ts.map +1 -1
- package/pd-cli/dist/commands/pain-list.js +10 -10
- package/pd-cli/dist/commands/pain-list.js.map +1 -1
- package/pd-cli/dist/commands/pain-record.d.ts +2 -0
- package/pd-cli/dist/commands/pain-record.d.ts.map +1 -1
- package/pd-cli/dist/commands/pain-record.js +36 -0
- package/pd-cli/dist/commands/pain-record.js.map +1 -1
- package/pd-cli/dist/commands/pain-retry.d.ts +6 -0
- package/pd-cli/dist/commands/pain-retry.d.ts.map +1 -1
- package/pd-cli/dist/commands/pain-retry.js +152 -5
- package/pd-cli/dist/commands/pain-retry.js.map +1 -1
- package/pd-cli/dist/commands/runtime-activation.d.ts +1 -1
- package/pd-cli/dist/commands/runtime-activation.d.ts.map +1 -1
- package/pd-cli/dist/commands/runtime-activation.js +3 -3
- package/pd-cli/dist/commands/runtime-activation.js.map +1 -1
- package/pd-cli/dist/commands/workbuddy-worker.d.ts +19 -0
- package/pd-cli/dist/commands/workbuddy-worker.d.ts.map +1 -0
- package/pd-cli/dist/commands/workbuddy-worker.js +168 -0
- package/pd-cli/dist/commands/workbuddy-worker.js.map +1 -0
- package/pd-cli/dist/index.js +29 -1
- package/pd-cli/dist/index.js.map +1 -1
- package/pd-cli/dist/resolve-workspace.d.ts +14 -2
- package/pd-cli/dist/resolve-workspace.d.ts.map +1 -1
- package/pd-cli/dist/resolve-workspace.js +88 -21
- package/pd-cli/dist/resolve-workspace.js.map +1 -1
- package/pd-cli/dist/services/admission-write-gate.d.ts +24 -0
- package/pd-cli/dist/services/admission-write-gate.d.ts.map +1 -0
- package/pd-cli/dist/services/admission-write-gate.js +41 -0
- package/pd-cli/dist/services/admission-write-gate.js.map +1 -0
- package/pd-cli/dist/services/pd-config-loader.d.ts +39 -7
- package/pd-cli/dist/services/pd-config-loader.d.ts.map +1 -1
- package/pd-cli/dist/services/pd-config-loader.js +111 -18
- package/pd-cli/dist/services/pd-config-loader.js.map +1 -1
- package/pd-cli/dist/services/rulehost-pipeline-runner.d.ts +10 -0
- package/pd-cli/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
- package/pd-cli/dist/services/rulehost-pipeline-runner.js +59 -4
- package/pd-cli/dist/services/rulehost-pipeline-runner.js.map +1 -1
- package/pd-cli/package.json +8 -5
- package/plugin/dist/bundle.js +564 -509
- package/plugin/dist/governance-audit.js +1 -1
- package/plugin/dist/openclaw.plugin.json +1 -1
- package/plugin/dist/rulehost-evidence.js +1 -1
- package/plugin/openclaw.plugin.json +1 -1
- package/plugin/package.json +7 -5
- package/release-manager/dist/index.d.ts.map +1 -1
- package/release-manager/dist/index.js +84 -8
- package/release-manager/dist/index.js.map +1 -1
- package/release-manager/dist/installer.d.ts +42 -11
- package/release-manager/dist/installer.d.ts.map +1 -1
- package/release-manager/dist/installer.js +140 -36
- package/release-manager/dist/installer.js.map +1 -1
- package/release-manager/dist/installers/index.d.ts +18 -7
- package/release-manager/dist/installers/index.d.ts.map +1 -1
- package/release-manager/dist/installers/index.js +13 -4
- package/release-manager/dist/installers/index.js.map +1 -1
- package/release-manager/dist/installers/workbuddy-host-installer.d.ts +246 -0
- package/release-manager/dist/installers/workbuddy-host-installer.d.ts.map +1 -0
- package/release-manager/dist/installers/workbuddy-host-installer.js +1419 -0
- package/release-manager/dist/installers/workbuddy-host-installer.js.map +1 -0
- package/release-manager/dist/mvp-config.d.ts +24 -0
- package/release-manager/dist/mvp-config.d.ts.map +1 -1
- package/release-manager/dist/mvp-config.js +36 -0
- package/release-manager/dist/mvp-config.js.map +1 -1
- package/release-manager/dist/prompts.d.ts +13 -0
- package/release-manager/dist/prompts.d.ts.map +1 -1
- package/release-manager/dist/prompts.js +45 -8
- package/release-manager/dist/prompts.js.map +1 -1
- package/release-manager/dist/uninstaller.d.ts +1 -1
- package/release-manager/dist/uninstaller.d.ts.map +1 -1
- package/release-manager/dist/uninstaller.js +29 -4
- package/release-manager/dist/uninstaller.js.map +1 -1
- package/release-manager/package.json +8 -6
- package/workbuddy-adapter/dist/codec/index.d.ts +2 -0
- package/workbuddy-adapter/dist/codec/index.js +2 -0
- package/workbuddy-adapter/dist/codec/input-decoder.d.ts +30 -0
- package/workbuddy-adapter/dist/codec/input-decoder.js +232 -0
- package/workbuddy-adapter/dist/codec/output-encoder.d.ts +62 -0
- package/workbuddy-adapter/dist/codec/output-encoder.js +75 -0
- package/workbuddy-adapter/dist/host-adapter.d.ts +33 -0
- package/workbuddy-adapter/dist/host-adapter.js +23 -0
- package/workbuddy-adapter/dist/index.d.ts +16 -0
- package/workbuddy-adapter/dist/index.js +16 -0
- package/workbuddy-adapter/dist/pd-hook.d.ts +17 -0
- package/workbuddy-adapter/dist/pd-hook.js +505 -0
- package/workbuddy-adapter/dist/tool-semantics.d.ts +22 -0
- package/workbuddy-adapter/dist/tool-semantics.js +33 -0
- package/workbuddy-adapter/dist/workbuddy-evidence-recorder.d.ts +50 -0
- package/workbuddy-adapter/dist/workbuddy-evidence-recorder.js +238 -0
- package/workbuddy-adapter/dist/worker/workspace-worker.d.ts +41 -0
- package/workbuddy-adapter/dist/worker/workspace-worker.js +219 -0
- package/workbuddy-adapter/dist/workspace-resolution.d.ts +15 -0
- package/workbuddy-adapter/dist/workspace-resolution.js +53 -0
- package/workbuddy-adapter/package.json +64 -0
- package/workbuddy-adapter/plugin/.codebuddy-plugin/plugin.json +6 -0
- package/workbuddy-adapter/plugin/hooks/hooks.json +62 -0
- package/workbuddy-adapter/plugin/scripts/wrapper.cjs +79 -0
- package/console/dist/ui/utils/navigation.d.ts +0 -9
- package/console/dist/ui/utils/navigation.js +0 -7
- package/core/dist/runtime-v2/internalization/dimension-coverage-policy.d.ts +0 -54
- package/core/dist/runtime-v2/internalization/dimension-coverage-policy.d.ts.map +0 -1
- package/core/dist/runtime-v2/internalization/dimension-coverage-policy.js +0 -70
- 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
|