@productbrain/cli 0.1.0-beta.1 → 0.1.0-beta.102
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/README.md +127 -0
- package/dist/__tests__/adapters.test.d.ts +2 -0
- package/dist/__tests__/adapters.test.d.ts.map +1 -0
- package/dist/__tests__/adapters.test.js +417 -0
- package/dist/__tests__/adapters.test.js.map +1 -0
- package/dist/__tests__/audit.test.d.ts +2 -0
- package/dist/__tests__/audit.test.d.ts.map +1 -0
- package/dist/__tests__/audit.test.js +394 -0
- package/dist/__tests__/audit.test.js.map +1 -0
- package/dist/__tests__/authority-domains.test.d.ts +2 -0
- package/dist/__tests__/authority-domains.test.d.ts.map +1 -0
- package/dist/__tests__/authority-domains.test.js +48 -0
- package/dist/__tests__/authority-domains.test.js.map +1 -0
- package/dist/__tests__/batch-transformations.test.d.ts +2 -0
- package/dist/__tests__/batch-transformations.test.d.ts.map +1 -0
- package/dist/__tests__/batch-transformations.test.js +263 -0
- package/dist/__tests__/batch-transformations.test.js.map +1 -0
- package/dist/__tests__/capture.test.d.ts +2 -0
- package/dist/__tests__/capture.test.d.ts.map +1 -0
- package/dist/__tests__/capture.test.js +377 -0
- package/dist/__tests__/capture.test.js.map +1 -0
- package/dist/__tests__/config.test.d.ts +8 -0
- package/dist/__tests__/config.test.d.ts.map +1 -0
- package/dist/__tests__/config.test.js +296 -0
- package/dist/__tests__/config.test.js.map +1 -0
- package/dist/__tests__/constants.test.d.ts +2 -0
- package/dist/__tests__/constants.test.d.ts.map +1 -0
- package/dist/__tests__/constants.test.js +141 -0
- package/dist/__tests__/constants.test.js.map +1 -0
- package/dist/__tests__/constellation.test.d.ts +2 -0
- package/dist/__tests__/constellation.test.d.ts.map +1 -0
- package/dist/__tests__/constellation.test.js +254 -0
- package/dist/__tests__/constellation.test.js.map +1 -0
- package/dist/__tests__/context-strategy.test.d.ts +2 -0
- package/dist/__tests__/context-strategy.test.d.ts.map +1 -0
- package/dist/__tests__/context-strategy.test.js +79 -0
- package/dist/__tests__/context-strategy.test.js.map +1 -0
- package/dist/__tests__/envelope-contract.test.d.ts +15 -0
- package/dist/__tests__/envelope-contract.test.d.ts.map +1 -0
- package/dist/__tests__/envelope-contract.test.js +126 -0
- package/dist/__tests__/envelope-contract.test.js.map +1 -0
- package/dist/__tests__/errors.test.d.ts +2 -0
- package/dist/__tests__/errors.test.d.ts.map +1 -0
- package/dist/__tests__/errors.test.js +117 -0
- package/dist/__tests__/errors.test.js.map +1 -0
- package/dist/__tests__/experiment.test.d.ts +6 -0
- package/dist/__tests__/experiment.test.d.ts.map +1 -0
- package/dist/__tests__/experiment.test.js +69 -0
- package/dist/__tests__/experiment.test.js.map +1 -0
- package/dist/__tests__/fields.test.d.ts +2 -0
- package/dist/__tests__/fields.test.d.ts.map +1 -0
- package/dist/__tests__/fields.test.js +238 -0
- package/dist/__tests__/fields.test.js.map +1 -0
- package/dist/__tests__/glossary.test.d.ts +2 -0
- package/dist/__tests__/glossary.test.d.ts.map +1 -0
- package/dist/__tests__/glossary.test.js +32 -0
- package/dist/__tests__/glossary.test.js.map +1 -0
- package/dist/__tests__/handshake-preview.test.d.ts +2 -0
- package/dist/__tests__/handshake-preview.test.d.ts.map +1 -0
- package/dist/__tests__/handshake-preview.test.js +279 -0
- package/dist/__tests__/handshake-preview.test.js.map +1 -0
- package/dist/__tests__/handshake.test.d.ts +2 -0
- package/dist/__tests__/handshake.test.d.ts.map +1 -0
- package/dist/__tests__/handshake.test.js +555 -0
- package/dist/__tests__/handshake.test.js.map +1 -0
- package/dist/__tests__/hook-intents.test.d.ts +2 -0
- package/dist/__tests__/hook-intents.test.d.ts.map +1 -0
- package/dist/__tests__/hook-intents.test.js +184 -0
- package/dist/__tests__/hook-intents.test.js.map +1 -0
- package/dist/__tests__/ingest.test.d.ts +2 -0
- package/dist/__tests__/ingest.test.d.ts.map +1 -0
- package/dist/__tests__/ingest.test.js +185 -0
- package/dist/__tests__/ingest.test.js.map +1 -0
- package/dist/__tests__/init.test.d.ts +7 -0
- package/dist/__tests__/init.test.d.ts.map +1 -0
- package/dist/__tests__/init.test.js +146 -0
- package/dist/__tests__/init.test.js.map +1 -0
- package/dist/__tests__/login.test.d.ts +2 -0
- package/dist/__tests__/login.test.d.ts.map +1 -0
- package/dist/__tests__/login.test.js +167 -0
- package/dist/__tests__/login.test.js.map +1 -0
- package/dist/__tests__/manifest.test.d.ts +6 -0
- package/dist/__tests__/manifest.test.d.ts.map +1 -0
- package/dist/__tests__/manifest.test.js +138 -0
- package/dist/__tests__/manifest.test.js.map +1 -0
- package/dist/__tests__/method-registry.integration.test.d.ts +6 -0
- package/dist/__tests__/method-registry.integration.test.d.ts.map +1 -0
- package/dist/__tests__/method-registry.integration.test.js +18 -0
- package/dist/__tests__/method-registry.integration.test.js.map +1 -0
- package/dist/__tests__/method-registry.test.d.ts +14 -0
- package/dist/__tests__/method-registry.test.d.ts.map +1 -0
- package/dist/__tests__/method-registry.test.js +134 -0
- package/dist/__tests__/method-registry.test.js.map +1 -0
- package/dist/__tests__/onboarding-path-b.test.d.ts +2 -0
- package/dist/__tests__/onboarding-path-b.test.d.ts.map +1 -0
- package/dist/__tests__/onboarding-path-b.test.js +46 -0
- package/dist/__tests__/onboarding-path-b.test.js.map +1 -0
- package/dist/__tests__/onboarding.test.d.ts +6 -0
- package/dist/__tests__/onboarding.test.d.ts.map +1 -0
- package/dist/__tests__/onboarding.test.js +347 -0
- package/dist/__tests__/onboarding.test.js.map +1 -0
- package/dist/__tests__/orient.test.d.ts +2 -0
- package/dist/__tests__/orient.test.d.ts.map +1 -0
- package/dist/__tests__/orient.test.js +196 -0
- package/dist/__tests__/orient.test.js.map +1 -0
- package/dist/__tests__/personal-layer.test.d.ts +12 -0
- package/dist/__tests__/personal-layer.test.d.ts.map +1 -0
- package/dist/__tests__/personal-layer.test.js +304 -0
- package/dist/__tests__/personal-layer.test.js.map +1 -0
- package/dist/__tests__/profiles.test.d.ts +2 -0
- package/dist/__tests__/profiles.test.d.ts.map +1 -0
- package/dist/__tests__/profiles.test.js +212 -0
- package/dist/__tests__/profiles.test.js.map +1 -0
- package/dist/__tests__/promote.test.d.ts +2 -0
- package/dist/__tests__/promote.test.d.ts.map +1 -0
- package/dist/__tests__/promote.test.js +230 -0
- package/dist/__tests__/promote.test.js.map +1 -0
- package/dist/__tests__/prompts.test.d.ts +6 -0
- package/dist/__tests__/prompts.test.d.ts.map +1 -0
- package/dist/__tests__/prompts.test.js +146 -0
- package/dist/__tests__/prompts.test.js.map +1 -0
- package/dist/__tests__/proposals.test.d.ts +2 -0
- package/dist/__tests__/proposals.test.d.ts.map +1 -0
- package/dist/__tests__/proposals.test.js +167 -0
- package/dist/__tests__/proposals.test.js.map +1 -0
- package/dist/__tests__/relate.test.d.ts +2 -0
- package/dist/__tests__/relate.test.d.ts.map +1 -0
- package/dist/__tests__/relate.test.js +103 -0
- package/dist/__tests__/relate.test.js.map +1 -0
- package/dist/__tests__/repo-detect.test.d.ts +2 -0
- package/dist/__tests__/repo-detect.test.d.ts.map +1 -0
- package/dist/__tests__/repo-detect.test.js +215 -0
- package/dist/__tests__/repo-detect.test.js.map +1 -0
- package/dist/__tests__/runner.test.d.ts +2 -0
- package/dist/__tests__/runner.test.d.ts.map +1 -0
- package/dist/__tests__/runner.test.js +219 -0
- package/dist/__tests__/runner.test.js.map +1 -0
- package/dist/__tests__/session-state-machine.test.d.ts +2 -0
- package/dist/__tests__/session-state-machine.test.d.ts.map +1 -0
- package/dist/__tests__/session-state-machine.test.js +154 -0
- package/dist/__tests__/session-state-machine.test.js.map +1 -0
- package/dist/__tests__/session-touch.test.d.ts +2 -0
- package/dist/__tests__/session-touch.test.d.ts.map +1 -0
- package/dist/__tests__/session-touch.test.js +134 -0
- package/dist/__tests__/session-touch.test.js.map +1 -0
- package/dist/__tests__/session.test.d.ts +2 -0
- package/dist/__tests__/session.test.d.ts.map +1 -0
- package/dist/__tests__/session.test.js +46 -0
- package/dist/__tests__/session.test.js.map +1 -0
- package/dist/__tests__/setup-ingest.test.d.ts +2 -0
- package/dist/__tests__/setup-ingest.test.d.ts.map +1 -0
- package/dist/__tests__/setup-ingest.test.js +55 -0
- package/dist/__tests__/setup-ingest.test.js.map +1 -0
- package/dist/__tests__/setup-resolver.test.d.ts +14 -0
- package/dist/__tests__/setup-resolver.test.d.ts.map +1 -0
- package/dist/__tests__/setup-resolver.test.js +228 -0
- package/dist/__tests__/setup-resolver.test.js.map +1 -0
- package/dist/__tests__/setup.test.d.ts +2 -0
- package/dist/__tests__/setup.test.d.ts.map +1 -0
- package/dist/__tests__/setup.test.js +141 -0
- package/dist/__tests__/setup.test.js.map +1 -0
- package/dist/__tests__/spinner-labels.test.d.ts +2 -0
- package/dist/__tests__/spinner-labels.test.d.ts.map +1 -0
- package/dist/__tests__/spinner-labels.test.js +23 -0
- package/dist/__tests__/spinner-labels.test.js.map +1 -0
- package/dist/__tests__/state.test.d.ts +6 -0
- package/dist/__tests__/state.test.d.ts.map +1 -0
- package/dist/__tests__/state.test.js +97 -0
- package/dist/__tests__/state.test.js.map +1 -0
- package/dist/__tests__/strip.test.d.ts +2 -0
- package/dist/__tests__/strip.test.d.ts.map +1 -0
- package/dist/__tests__/strip.test.js +136 -0
- package/dist/__tests__/strip.test.js.map +1 -0
- package/dist/__tests__/surface-profiles.test.d.ts +2 -0
- package/dist/__tests__/surface-profiles.test.d.ts.map +1 -0
- package/dist/__tests__/surface-profiles.test.js +233 -0
- package/dist/__tests__/surface-profiles.test.js.map +1 -0
- package/dist/__tests__/surfaces.test.d.ts +2 -0
- package/dist/__tests__/surfaces.test.d.ts.map +1 -0
- package/dist/__tests__/surfaces.test.js +46 -0
- package/dist/__tests__/surfaces.test.js.map +1 -0
- package/dist/__tests__/update.test.d.ts +2 -0
- package/dist/__tests__/update.test.d.ts.map +1 -0
- package/dist/__tests__/update.test.js +228 -0
- package/dist/__tests__/update.test.js.map +1 -0
- package/dist/__tests__/workspace.test.d.ts +2 -0
- package/dist/__tests__/workspace.test.d.ts.map +1 -0
- package/dist/__tests__/workspace.test.js +328 -0
- package/dist/__tests__/workspace.test.js.map +1 -0
- package/dist/commands/accept.d.ts +18 -0
- package/dist/commands/accept.d.ts.map +1 -0
- package/dist/commands/accept.js +76 -0
- package/dist/commands/accept.js.map +1 -0
- package/dist/commands/admin/cockpit.d.ts +90 -0
- package/dist/commands/admin/cockpit.d.ts.map +1 -0
- package/dist/commands/admin/cockpit.js +618 -0
- package/dist/commands/admin/cockpit.js.map +1 -0
- package/dist/commands/admin/index.d.ts +21 -0
- package/dist/commands/admin/index.d.ts.map +1 -0
- package/dist/commands/admin/index.js +256 -0
- package/dist/commands/admin/index.js.map +1 -0
- package/dist/commands/admin/inspect.d.ts +30 -0
- package/dist/commands/admin/inspect.d.ts.map +1 -0
- package/dist/commands/admin/inspect.js +555 -0
- package/dist/commands/admin/inspect.js.map +1 -0
- package/dist/commands/admin/inspect.test.d.ts +7 -0
- package/dist/commands/admin/inspect.test.d.ts.map +1 -0
- package/dist/commands/admin/inspect.test.js +90 -0
- package/dist/commands/admin/inspect.test.js.map +1 -0
- package/dist/commands/admin/manage.d.ts +8 -0
- package/dist/commands/admin/manage.d.ts.map +1 -0
- package/dist/commands/admin/manage.js +76 -0
- package/dist/commands/admin/manage.js.map +1 -0
- package/dist/commands/admin/seed.d.ts +46 -0
- package/dist/commands/admin/seed.d.ts.map +1 -0
- package/dist/commands/admin/seed.js +729 -0
- package/dist/commands/admin/seed.js.map +1 -0
- package/dist/commands/admin/seed.test.d.ts +11 -0
- package/dist/commands/admin/seed.test.d.ts.map +1 -0
- package/dist/commands/admin/seed.test.js +123 -0
- package/dist/commands/admin/seed.test.js.map +1 -0
- package/dist/commands/audit.d.ts +25 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +188 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/commands/authority-domains.d.ts +140 -0
- package/dist/commands/authority-domains.d.ts.map +1 -0
- package/dist/commands/authority-domains.js +268 -0
- package/dist/commands/authority-domains.js.map +1 -0
- package/dist/commands/brand-pack.d.ts +2 -0
- package/dist/commands/brand-pack.d.ts.map +1 -0
- package/dist/commands/brand-pack.js +25 -0
- package/dist/commands/brand-pack.js.map +1 -0
- package/dist/commands/brief.d.ts +28 -0
- package/dist/commands/brief.d.ts.map +1 -0
- package/dist/commands/brief.js +75 -0
- package/dist/commands/brief.js.map +1 -0
- package/dist/commands/capture.d.ts +30 -0
- package/dist/commands/capture.d.ts.map +1 -0
- package/dist/commands/capture.js +339 -0
- package/dist/commands/capture.js.map +1 -0
- package/dist/commands/chain-walk.d.ts +14 -0
- package/dist/commands/chain-walk.d.ts.map +1 -0
- package/dist/commands/chain-walk.js +38 -0
- package/dist/commands/chain-walk.js.map +1 -0
- package/dist/commands/changes.d.ts +11 -0
- package/dist/commands/changes.d.ts.map +1 -0
- package/dist/commands/changes.js +46 -0
- package/dist/commands/changes.js.map +1 -0
- package/dist/commands/codex-prep.d.ts +12 -0
- package/dist/commands/codex-prep.d.ts.map +1 -0
- package/dist/commands/codex-prep.js +122 -0
- package/dist/commands/codex-prep.js.map +1 -0
- package/dist/commands/collections.d.ts +22 -0
- package/dist/commands/collections.d.ts.map +1 -0
- package/dist/commands/collections.js +77 -0
- package/dist/commands/collections.js.map +1 -0
- package/dist/commands/connect-integration.test.d.ts +7 -0
- package/dist/commands/connect-integration.test.d.ts.map +1 -0
- package/dist/commands/connect-integration.test.js +211 -0
- package/dist/commands/connect-integration.test.js.map +1 -0
- package/dist/commands/connect-screens.d.ts +24 -0
- package/dist/commands/connect-screens.d.ts.map +1 -0
- package/dist/commands/connect-screens.js +97 -0
- package/dist/commands/connect-screens.js.map +1 -0
- package/dist/commands/connect.d.ts +23 -0
- package/dist/commands/connect.d.ts.map +1 -0
- package/dist/commands/connect.js +289 -0
- package/dist/commands/connect.js.map +1 -0
- package/dist/commands/connect.test.d.ts +6 -0
- package/dist/commands/connect.test.d.ts.map +1 -0
- package/dist/commands/connect.test.js +297 -0
- package/dist/commands/connect.test.js.map +1 -0
- package/dist/commands/constellation.d.ts +11 -0
- package/dist/commands/constellation.d.ts.map +1 -0
- package/dist/commands/constellation.js +33 -0
- package/dist/commands/constellation.js.map +1 -0
- package/dist/commands/context.d.ts +2 -1
- package/dist/commands/context.d.ts.map +1 -1
- package/dist/commands/context.js +25 -10
- package/dist/commands/context.js.map +1 -1
- package/dist/commands/cross-cut.d.ts +11 -0
- package/dist/commands/cross-cut.d.ts.map +1 -0
- package/dist/commands/cross-cut.js +23 -0
- package/dist/commands/cross-cut.js.map +1 -0
- package/dist/commands/doctor.d.ts +18 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +232 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/doctor.test.d.ts +8 -0
- package/dist/commands/doctor.test.d.ts.map +1 -0
- package/dist/commands/doctor.test.js +311 -0
- package/dist/commands/doctor.test.js.map +1 -0
- package/dist/commands/fields.d.ts +9 -0
- package/dist/commands/fields.d.ts.map +1 -0
- package/dist/commands/fields.js +30 -0
- package/dist/commands/fields.js.map +1 -0
- package/dist/commands/get.d.ts +8 -1
- package/dist/commands/get.d.ts.map +1 -1
- package/dist/commands/get.js +65 -8
- package/dist/commands/get.js.map +1 -1
- package/dist/commands/handshake.d.ts +142 -0
- package/dist/commands/handshake.d.ts.map +1 -0
- package/dist/commands/handshake.js +1349 -0
- package/dist/commands/handshake.js.map +1 -0
- package/dist/commands/ingest.d.ts +14 -0
- package/dist/commands/ingest.d.ts.map +1 -0
- package/dist/commands/ingest.js +189 -0
- package/dist/commands/ingest.js.map +1 -0
- package/dist/commands/init.d.ts +14 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +109 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/login.d.ts +9 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +116 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/method.d.ts +99 -0
- package/dist/commands/method.d.ts.map +1 -0
- package/dist/commands/method.js +781 -0
- package/dist/commands/method.js.map +1 -0
- package/dist/commands/migrate-setup.d.ts +18 -0
- package/dist/commands/migrate-setup.d.ts.map +1 -0
- package/dist/commands/migrate-setup.js +198 -0
- package/dist/commands/migrate-setup.js.map +1 -0
- package/dist/commands/orient.d.ts +109 -1
- package/dist/commands/orient.d.ts.map +1 -1
- package/dist/commands/orient.js +94 -7
- package/dist/commands/orient.js.map +1 -1
- package/dist/commands/profile.d.ts +47 -0
- package/dist/commands/profile.d.ts.map +1 -0
- package/dist/commands/profile.js +148 -0
- package/dist/commands/profile.js.map +1 -0
- package/dist/commands/promote.d.ts +12 -0
- package/dist/commands/promote.d.ts.map +1 -0
- package/dist/commands/promote.js +113 -0
- package/dist/commands/promote.js.map +1 -0
- package/dist/commands/proposals.d.ts +9 -0
- package/dist/commands/proposals.d.ts.map +1 -0
- package/dist/commands/proposals.js +24 -0
- package/dist/commands/proposals.js.map +1 -0
- package/dist/commands/reject.d.ts +14 -0
- package/dist/commands/reject.d.ts.map +1 -0
- package/dist/commands/reject.js +43 -0
- package/dist/commands/reject.js.map +1 -0
- package/dist/commands/relate.d.ts +16 -0
- package/dist/commands/relate.d.ts.map +1 -0
- package/dist/commands/relate.js +111 -0
- package/dist/commands/relate.js.map +1 -0
- package/dist/commands/search.d.ts +1 -0
- package/dist/commands/search.d.ts.map +1 -1
- package/dist/commands/search.js +10 -4
- package/dist/commands/search.js.map +1 -1
- package/dist/commands/session.d.ts +20 -0
- package/dist/commands/session.d.ts.map +1 -0
- package/dist/commands/session.js +203 -0
- package/dist/commands/session.js.map +1 -0
- package/dist/commands/setup-ingest.d.ts +17 -0
- package/dist/commands/setup-ingest.d.ts.map +1 -0
- package/dist/commands/setup-ingest.js +224 -0
- package/dist/commands/setup-ingest.js.map +1 -0
- package/dist/commands/setup-resolver.d.ts +58 -0
- package/dist/commands/setup-resolver.d.ts.map +1 -0
- package/dist/commands/setup-resolver.js +150 -0
- package/dist/commands/setup-resolver.js.map +1 -0
- package/dist/commands/setup.d.ts +15 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +148 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/update.d.ts +17 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +178 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/commands/usage.d.ts +40 -0
- package/dist/commands/usage.d.ts.map +1 -0
- package/dist/commands/usage.js +232 -0
- package/dist/commands/usage.js.map +1 -0
- package/dist/commands/verify.d.ts +13 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +49 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/commands/welcome.d.ts +21 -0
- package/dist/commands/welcome.d.ts.map +1 -0
- package/dist/commands/welcome.js +50 -0
- package/dist/commands/welcome.js.map +1 -0
- package/dist/commands/workspace.d.ts +113 -0
- package/dist/commands/workspace.d.ts.map +1 -0
- package/dist/commands/workspace.js +263 -0
- package/dist/commands/workspace.js.map +1 -0
- package/dist/formatters/audit.d.ts +46 -0
- package/dist/formatters/audit.d.ts.map +1 -0
- package/dist/formatters/audit.js +81 -0
- package/dist/formatters/audit.js.map +1 -0
- package/dist/formatters/brief.d.ts +112 -0
- package/dist/formatters/brief.d.ts.map +1 -0
- package/dist/formatters/brief.js +179 -0
- package/dist/formatters/brief.js.map +1 -0
- package/dist/formatters/capture.d.ts +48 -0
- package/dist/formatters/capture.d.ts.map +1 -0
- package/dist/formatters/capture.js +77 -0
- package/dist/formatters/capture.js.map +1 -0
- package/dist/formatters/chain-walk.d.ts +33 -0
- package/dist/formatters/chain-walk.d.ts.map +1 -0
- package/dist/formatters/chain-walk.js +54 -0
- package/dist/formatters/chain-walk.js.map +1 -0
- package/dist/formatters/changes.d.ts +25 -0
- package/dist/formatters/changes.d.ts.map +1 -0
- package/dist/formatters/changes.js +60 -0
- package/dist/formatters/changes.js.map +1 -0
- package/dist/formatters/collections.d.ts +40 -0
- package/dist/formatters/collections.d.ts.map +1 -0
- package/dist/formatters/collections.js +93 -0
- package/dist/formatters/collections.js.map +1 -0
- package/dist/formatters/constellation.d.ts +34 -0
- package/dist/formatters/constellation.d.ts.map +1 -0
- package/dist/formatters/constellation.js +38 -0
- package/dist/formatters/constellation.js.map +1 -0
- package/dist/formatters/cross-cut.d.ts +21 -0
- package/dist/formatters/cross-cut.d.ts.map +1 -0
- package/dist/formatters/cross-cut.js +32 -0
- package/dist/formatters/cross-cut.js.map +1 -0
- package/dist/formatters/entry.d.ts +11 -4
- package/dist/formatters/entry.d.ts.map +1 -1
- package/dist/formatters/entry.js +24 -8
- package/dist/formatters/entry.js.map +1 -1
- package/dist/formatters/fields.d.ts +32 -0
- package/dist/formatters/fields.d.ts.map +1 -0
- package/dist/formatters/fields.js +49 -0
- package/dist/formatters/fields.js.map +1 -0
- package/dist/formatters/handshake.d.ts +46 -0
- package/dist/formatters/handshake.d.ts.map +1 -0
- package/dist/formatters/handshake.js +163 -0
- package/dist/formatters/handshake.js.map +1 -0
- package/dist/formatters/orient.d.ts +129 -1
- package/dist/formatters/orient.d.ts.map +1 -1
- package/dist/formatters/orient.js +156 -17
- package/dist/formatters/orient.js.map +1 -1
- package/dist/formatters/promote.d.ts +30 -0
- package/dist/formatters/promote.d.ts.map +1 -0
- package/dist/formatters/promote.js +39 -0
- package/dist/formatters/promote.js.map +1 -0
- package/dist/formatters/proposals.d.ts +45 -0
- package/dist/formatters/proposals.d.ts.map +1 -0
- package/dist/formatters/proposals.js +62 -0
- package/dist/formatters/proposals.js.map +1 -0
- package/dist/formatters/relate.d.ts +14 -0
- package/dist/formatters/relate.d.ts.map +1 -0
- package/dist/formatters/relate.js +16 -0
- package/dist/formatters/relate.js.map +1 -0
- package/dist/formatters/search.d.ts +0 -4
- package/dist/formatters/search.d.ts.map +1 -1
- package/dist/formatters/search.js +4 -1
- package/dist/formatters/search.js.map +1 -1
- package/dist/formatters/session.d.ts +11 -0
- package/dist/formatters/session.d.ts.map +1 -0
- package/dist/formatters/session.js +53 -0
- package/dist/formatters/session.js.map +1 -0
- package/dist/formatters/update.d.ts +17 -0
- package/dist/formatters/update.d.ts.map +1 -0
- package/dist/formatters/update.js +45 -0
- package/dist/formatters/update.js.map +1 -0
- package/dist/formatters/verify.d.ts +11 -0
- package/dist/formatters/verify.d.ts.map +1 -0
- package/dist/formatters/verify.js +11 -0
- package/dist/formatters/verify.js.map +1 -0
- package/dist/generators/__tests__/surface-profiles.test.d.ts +2 -0
- package/dist/generators/__tests__/surface-profiles.test.d.ts.map +1 -0
- package/dist/generators/__tests__/surface-profiles.test.js +89 -0
- package/dist/generators/__tests__/surface-profiles.test.js.map +1 -0
- package/dist/generators/adapters.d.ts +44 -0
- package/dist/generators/adapters.d.ts.map +1 -0
- package/dist/generators/adapters.js +290 -0
- package/dist/generators/adapters.js.map +1 -0
- package/dist/generators/adapters.test.d.ts +2 -0
- package/dist/generators/adapters.test.d.ts.map +1 -0
- package/dist/generators/adapters.test.js +27 -0
- package/dist/generators/adapters.test.js.map +1 -0
- package/dist/generators/archetypes.d.ts +52 -0
- package/dist/generators/archetypes.d.ts.map +1 -0
- package/dist/generators/archetypes.js +153 -0
- package/dist/generators/archetypes.js.map +1 -0
- package/dist/generators/archetypes.test.d.ts +2 -0
- package/dist/generators/archetypes.test.d.ts.map +1 -0
- package/dist/generators/archetypes.test.js +237 -0
- package/dist/generators/archetypes.test.js.map +1 -0
- package/dist/generators/boundary-manifest.d.ts +29 -0
- package/dist/generators/boundary-manifest.d.ts.map +1 -0
- package/dist/generators/boundary-manifest.js +183 -0
- package/dist/generators/boundary-manifest.js.map +1 -0
- package/dist/generators/boundary-manifest.test.d.ts +2 -0
- package/dist/generators/boundary-manifest.test.d.ts.map +1 -0
- package/dist/generators/boundary-manifest.test.js +91 -0
- package/dist/generators/boundary-manifest.test.js.map +1 -0
- package/dist/generators/briefing-md.d.ts +8 -0
- package/dist/generators/briefing-md.d.ts.map +1 -0
- package/dist/generators/briefing-md.js +51 -0
- package/dist/generators/briefing-md.js.map +1 -0
- package/dist/generators/chain-classifier.d.ts +49 -0
- package/dist/generators/chain-classifier.d.ts.map +1 -0
- package/dist/generators/chain-classifier.js +180 -0
- package/dist/generators/chain-classifier.js.map +1 -0
- package/dist/generators/chain-classifier.test.d.ts +2 -0
- package/dist/generators/chain-classifier.test.d.ts.map +1 -0
- package/dist/generators/chain-classifier.test.js +257 -0
- package/dist/generators/chain-classifier.test.js.map +1 -0
- package/dist/generators/chain-rules.d.ts +42 -0
- package/dist/generators/chain-rules.d.ts.map +1 -0
- package/dist/generators/chain-rules.js +144 -0
- package/dist/generators/chain-rules.js.map +1 -0
- package/dist/generators/chain-rules.test.d.ts +2 -0
- package/dist/generators/chain-rules.test.d.ts.map +1 -0
- package/dist/generators/chain-rules.test.js +179 -0
- package/dist/generators/chain-rules.test.js.map +1 -0
- package/dist/generators/context-md.d.ts +8 -0
- package/dist/generators/context-md.d.ts.map +1 -0
- package/dist/generators/context-md.js +134 -0
- package/dist/generators/context-md.js.map +1 -0
- package/dist/generators/handshake-diff.d.ts +67 -0
- package/dist/generators/handshake-diff.d.ts.map +1 -0
- package/dist/generators/handshake-diff.js +183 -0
- package/dist/generators/handshake-diff.js.map +1 -0
- package/dist/generators/handshake-diff.test.d.ts +2 -0
- package/dist/generators/handshake-diff.test.d.ts.map +1 -0
- package/dist/generators/handshake-diff.test.js +264 -0
- package/dist/generators/handshake-diff.test.js.map +1 -0
- package/dist/generators/manifest.d.ts +39 -0
- package/dist/generators/manifest.d.ts.map +1 -0
- package/dist/generators/manifest.js +166 -0
- package/dist/generators/manifest.js.map +1 -0
- package/dist/generators/portable-knowledge.d.ts +165 -0
- package/dist/generators/portable-knowledge.d.ts.map +1 -0
- package/dist/generators/portable-knowledge.js +613 -0
- package/dist/generators/portable-knowledge.js.map +1 -0
- package/dist/generators/portable-knowledge.test.d.ts +2 -0
- package/dist/generators/portable-knowledge.test.d.ts.map +1 -0
- package/dist/generators/portable-knowledge.test.js +927 -0
- package/dist/generators/portable-knowledge.test.js.map +1 -0
- package/dist/generators/surface-profiles.d.ts +49 -0
- package/dist/generators/surface-profiles.d.ts.map +1 -0
- package/dist/generators/surface-profiles.js +98 -0
- package/dist/generators/surface-profiles.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +858 -32
- package/dist/index.js.map +1 -1
- package/dist/lib/activation.d.ts +28 -0
- package/dist/lib/activation.d.ts.map +1 -0
- package/dist/lib/activation.js +57 -0
- package/dist/lib/activation.js.map +1 -0
- package/dist/lib/activation.test.d.ts +6 -0
- package/dist/lib/activation.test.d.ts.map +1 -0
- package/dist/lib/activation.test.js +121 -0
- package/dist/lib/activation.test.js.map +1 -0
- package/dist/lib/canonicalRefs.d.ts +69 -0
- package/dist/lib/canonicalRefs.d.ts.map +1 -0
- package/dist/lib/canonicalRefs.js +83 -0
- package/dist/lib/canonicalRefs.js.map +1 -0
- package/dist/lib/client.d.ts +62 -1
- package/dist/lib/client.d.ts.map +1 -1
- package/dist/lib/client.js +259 -13
- package/dist/lib/client.js.map +1 -1
- package/dist/lib/collectionRegistry.d.ts +38 -0
- package/dist/lib/collectionRegistry.d.ts.map +1 -0
- package/dist/lib/collectionRegistry.js +112 -0
- package/dist/lib/collectionRegistry.js.map +1 -0
- package/dist/lib/config.d.ts +122 -2
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +426 -18
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/connectKeyLabel.d.ts +9 -0
- package/dist/lib/connectKeyLabel.d.ts.map +1 -0
- package/dist/lib/connectKeyLabel.js +12 -0
- package/dist/lib/connectKeyLabel.js.map +1 -0
- package/dist/lib/constants.d.ts +42 -0
- package/dist/lib/constants.d.ts.map +1 -0
- package/dist/lib/constants.js +76 -0
- package/dist/lib/constants.js.map +1 -0
- package/dist/lib/conversation-engine.d.ts +45 -0
- package/dist/lib/conversation-engine.d.ts.map +1 -0
- package/dist/lib/conversation-engine.js +112 -0
- package/dist/lib/conversation-engine.js.map +1 -0
- package/dist/lib/conversation-phases.d.ts +59 -0
- package/dist/lib/conversation-phases.d.ts.map +1 -0
- package/dist/lib/conversation-phases.js +11 -0
- package/dist/lib/conversation-phases.js.map +1 -0
- package/dist/lib/conversation-signals.d.ts +30 -0
- package/dist/lib/conversation-signals.d.ts.map +1 -0
- package/dist/lib/conversation-signals.js +64 -0
- package/dist/lib/conversation-signals.js.map +1 -0
- package/dist/lib/deployment.d.ts +23 -0
- package/dist/lib/deployment.d.ts.map +1 -0
- package/dist/lib/deployment.js +78 -0
- package/dist/lib/deployment.js.map +1 -0
- package/dist/lib/deployment.test.d.ts +5 -0
- package/dist/lib/deployment.test.d.ts.map +1 -0
- package/dist/lib/deployment.test.js +54 -0
- package/dist/lib/deployment.test.js.map +1 -0
- package/dist/lib/errors.d.ts +60 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +69 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/experiment.d.ts +18 -0
- package/dist/lib/experiment.d.ts.map +1 -0
- package/dist/lib/experiment.js +28 -0
- package/dist/lib/experiment.js.map +1 -0
- package/dist/lib/format.d.ts +10 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/lib/format.js +27 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/glossary.d.ts +19 -0
- package/dist/lib/glossary.d.ts.map +1 -0
- package/dist/lib/glossary.js +53 -0
- package/dist/lib/glossary.js.map +1 -0
- package/dist/lib/hook-intents.d.ts +51 -0
- package/dist/lib/hook-intents.d.ts.map +1 -0
- package/dist/lib/hook-intents.js +85 -0
- package/dist/lib/hook-intents.js.map +1 -0
- package/dist/lib/inferSourceDate.d.ts +12 -0
- package/dist/lib/inferSourceDate.d.ts.map +1 -0
- package/dist/lib/inferSourceDate.js +44 -0
- package/dist/lib/inferSourceDate.js.map +1 -0
- package/dist/lib/method-registry.d.ts +32 -0
- package/dist/lib/method-registry.d.ts.map +1 -0
- package/dist/lib/method-registry.js +53 -0
- package/dist/lib/method-registry.js.map +1 -0
- package/dist/lib/normalizeMaterializedFilename.d.ts +28 -0
- package/dist/lib/normalizeMaterializedFilename.d.ts.map +1 -0
- package/dist/lib/normalizeMaterializedFilename.js +56 -0
- package/dist/lib/normalizeMaterializedFilename.js.map +1 -0
- package/dist/lib/normalizeMaterializedFilename.test.d.ts +16 -0
- package/dist/lib/normalizeMaterializedFilename.test.d.ts.map +1 -0
- package/dist/lib/normalizeMaterializedFilename.test.js +90 -0
- package/dist/lib/normalizeMaterializedFilename.test.js.map +1 -0
- package/dist/lib/onboarding-path-b.d.ts +10 -0
- package/dist/lib/onboarding-path-b.d.ts.map +1 -0
- package/dist/lib/onboarding-path-b.js +214 -0
- package/dist/lib/onboarding-path-b.js.map +1 -0
- package/dist/lib/onboarding-phases.d.ts +9 -0
- package/dist/lib/onboarding-phases.d.ts.map +1 -0
- package/dist/lib/onboarding-phases.js +120 -0
- package/dist/lib/onboarding-phases.js.map +1 -0
- package/dist/lib/onboarding-shared.d.ts +81 -0
- package/dist/lib/onboarding-shared.d.ts.map +1 -0
- package/dist/lib/onboarding-shared.js +190 -0
- package/dist/lib/onboarding-shared.js.map +1 -0
- package/dist/lib/onboarding-topics.d.ts +27 -0
- package/dist/lib/onboarding-topics.d.ts.map +1 -0
- package/dist/lib/onboarding-topics.js +57 -0
- package/dist/lib/onboarding-topics.js.map +1 -0
- package/dist/lib/onboarding.d.ts +17 -0
- package/dist/lib/onboarding.d.ts.map +1 -0
- package/dist/lib/onboarding.js +350 -0
- package/dist/lib/onboarding.js.map +1 -0
- package/dist/lib/profiles.d.ts +39 -0
- package/dist/lib/profiles.d.ts.map +1 -0
- package/dist/lib/profiles.js +185 -0
- package/dist/lib/profiles.js.map +1 -0
- package/dist/lib/prompts.d.ts +65 -0
- package/dist/lib/prompts.d.ts.map +1 -0
- package/dist/lib/prompts.js +132 -0
- package/dist/lib/prompts.js.map +1 -0
- package/dist/lib/repo-detect.d.ts +33 -0
- package/dist/lib/repo-detect.d.ts.map +1 -0
- package/dist/lib/repo-detect.js +83 -0
- package/dist/lib/repo-detect.js.map +1 -0
- package/dist/lib/runner.d.ts +33 -0
- package/dist/lib/runner.d.ts.map +1 -0
- package/dist/lib/runner.js +79 -0
- package/dist/lib/runner.js.map +1 -0
- package/dist/lib/session.d.ts +42 -0
- package/dist/lib/session.d.ts.map +1 -0
- package/dist/lib/session.js +109 -0
- package/dist/lib/session.js.map +1 -0
- package/dist/lib/spinner.d.ts +27 -0
- package/dist/lib/spinner.d.ts.map +1 -0
- package/dist/lib/spinner.js +76 -0
- package/dist/lib/spinner.js.map +1 -0
- package/dist/lib/spinner.test.d.ts +2 -0
- package/dist/lib/spinner.test.d.ts.map +1 -0
- package/dist/lib/spinner.test.js +39 -0
- package/dist/lib/spinner.test.js.map +1 -0
- package/dist/lib/state.d.ts +51 -0
- package/dist/lib/state.d.ts.map +1 -0
- package/dist/lib/state.js +90 -0
- package/dist/lib/state.js.map +1 -0
- package/dist/lib/strip.d.ts +12 -0
- package/dist/lib/strip.d.ts.map +1 -0
- package/dist/lib/strip.js +41 -0
- package/dist/lib/strip.js.map +1 -0
- package/dist/lib/style.d.ts +96 -0
- package/dist/lib/style.d.ts.map +1 -0
- package/dist/lib/style.js +169 -0
- package/dist/lib/style.js.map +1 -0
- package/dist/lib/style.test.d.ts +7 -0
- package/dist/lib/style.test.d.ts.map +1 -0
- package/dist/lib/style.test.js +263 -0
- package/dist/lib/style.test.js.map +1 -0
- package/dist/lib/telemetry.d.ts +15 -0
- package/dist/lib/telemetry.d.ts.map +1 -0
- package/dist/lib/telemetry.js +47 -0
- package/dist/lib/telemetry.js.map +1 -0
- package/dist/lib/tokenConstants.d.ts +17 -0
- package/dist/lib/tokenConstants.d.ts.map +1 -0
- package/dist/lib/tokenConstants.js +17 -0
- package/dist/lib/tokenConstants.js.map +1 -0
- package/dist/lib/update-check.d.ts +21 -0
- package/dist/lib/update-check.d.ts.map +1 -0
- package/dist/lib/update-check.js +145 -0
- package/dist/lib/update-check.js.map +1 -0
- package/dist/lib/wizard-surfaces.d.ts +47 -0
- package/dist/lib/wizard-surfaces.d.ts.map +1 -0
- package/dist/lib/wizard-surfaces.js +176 -0
- package/dist/lib/wizard-surfaces.js.map +1 -0
- package/dist/lib/wizard-surfaces.test.d.ts +2 -0
- package/dist/lib/wizard-surfaces.test.d.ts.map +1 -0
- package/dist/lib/wizard-surfaces.test.js +127 -0
- package/dist/lib/wizard-surfaces.test.js.map +1 -0
- package/dist/lib/wizard-trust.d.ts +31 -0
- package/dist/lib/wizard-trust.d.ts.map +1 -0
- package/dist/lib/wizard-trust.js +66 -0
- package/dist/lib/wizard-trust.js.map +1 -0
- package/dist/lib/wizard-trust.test.d.ts +2 -0
- package/dist/lib/wizard-trust.test.d.ts.map +1 -0
- package/dist/lib/wizard-trust.test.js +32 -0
- package/dist/lib/wizard-trust.test.js.map +1 -0
- package/dist/lib/workspace-probe.d.ts +19 -0
- package/dist/lib/workspace-probe.d.ts.map +1 -0
- package/dist/lib/workspace-probe.js +27 -0
- package/dist/lib/workspace-probe.js.map +1 -0
- package/dist/surfaces/registry.d.ts +20 -0
- package/dist/surfaces/registry.d.ts.map +1 -0
- package/dist/surfaces/registry.js +42 -0
- package/dist/surfaces/registry.js.map +1 -0
- package/package.json +15 -5
- package/templates/archetypes/boundary.md +23 -0
- package/templates/archetypes/constraint.md +23 -0
- package/templates/archetypes/convention.md +23 -0
- package/templates/archetypes/policy.md +23 -0
- package/templates/archetypes/quality-gate.md +23 -0
- package/templates/archetypes/workflow.md +23 -0
- package/templates/general/code-integrity.md +11 -0
- package/templates/general/getting-started.md +12 -0
- package/templates/method-registry.json +16 -0
- package/templates/node-ts/code-integrity.md +13 -0
- package/templates/node-ts/testing.md +12 -0
- package/templates/python/code-integrity.md +13 -0
- package/templates/python/testing.md +12 -0
|
@@ -0,0 +1,1349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pb handshake — generate context files for AI developer tools.
|
|
3
|
+
* Context export wiring (read-only filesystem bridge; GLO-63, DEC-161) — not a product surface.
|
|
4
|
+
*/
|
|
5
|
+
import { mkdirSync, writeFileSync, existsSync, readFileSync, readdirSync, copyFileSync, appendFileSync, unlinkSync, statSync } from 'fs';
|
|
6
|
+
import { join, dirname, resolve, basename } from 'path';
|
|
7
|
+
import { homedir } from 'os';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import { createHash } from 'crypto';
|
|
10
|
+
import { getConfigOrGuide } from '../lib/config.js';
|
|
11
|
+
import { select as promptSelect } from '../lib/prompts.js';
|
|
12
|
+
import { composeHooksFromIntents, getHookStatusForSurface } from '../lib/hook-intents.js';
|
|
13
|
+
import { kernelCall, kernelCallWithSession } from '../lib/client.js';
|
|
14
|
+
import { readSession } from '../lib/session.js';
|
|
15
|
+
import { detectRepo, extractWorkspaceProfile } from '../lib/repo-detect.js';
|
|
16
|
+
import { generateContextMd } from '../generators/context-md.js';
|
|
17
|
+
import { generateBriefingMd } from '../generators/briefing-md.js';
|
|
18
|
+
import { MARKER, generateAgentsMd, generateClaudeMd, generateCursorMdc, generateCopilotMd } from '../generators/adapters.js';
|
|
19
|
+
import { readCanonicalSkills, readCanonicalRules, readPersonalLayer, readPersonalSkillsLayer, generateCursorSkill, generateCursorRule, generateCodexSkill, generateCodexSkillIndex, generateClaudeRule, generateClaudeSkillRouter, shouldEmitToTarget, filterByLevel, validateCodexSkills, evaluateConditions, STAGE_TO_MAX_LEVEL, LEVEL_ORDER, } from '../generators/portable-knowledge.js';
|
|
20
|
+
import { generateChainRules } from '../generators/chain-rules.js';
|
|
21
|
+
import { saveHandshakeState, loadPreviousState, diffHandshakeState, formatDiff, buildCurrentState, } from '../generators/handshake-diff.js';
|
|
22
|
+
import { resolveSurfaceProfile } from '../generators/surface-profiles.js';
|
|
23
|
+
import { formatHandshakeReport } from '../formatters/handshake.js';
|
|
24
|
+
import { readManifest, filterByAdoptionState } from '../generators/manifest.js';
|
|
25
|
+
import { generateBoundaryManifest, getBoundaryEnforcementMode } from '../generators/boundary-manifest.js';
|
|
26
|
+
import { loadMethodRegistry } from '../lib/method-registry.js';
|
|
27
|
+
import { CLIError, ErrorCode } from '../lib/errors.js';
|
|
28
|
+
import { trackEvent } from '../lib/telemetry.js';
|
|
29
|
+
import { normalizeMaterializedFilename } from '../lib/normalizeMaterializedFilename.js';
|
|
30
|
+
const MAX_HANDSHAKE_WAIT_MS = 10_000; // 10 seconds
|
|
31
|
+
const POLL_INTERVAL_MS = 500; // 500 ms per poll
|
|
32
|
+
const MAX_POLLS = MAX_HANDSHAKE_WAIT_MS / POLL_INTERVAL_MS; // 20
|
|
33
|
+
// ── WP-379 S4: Dormant marker ─────────────────────────────────────────────────
|
|
34
|
+
/**
|
|
35
|
+
* DORMANT_MARKER — appended to previously-projected asset files when the asset's
|
|
36
|
+
* gate deactivates (e.g. workspace readiness exceeds the max threshold).
|
|
37
|
+
*
|
|
38
|
+
* Contract:
|
|
39
|
+
* - The file is NOT deleted. It persists on disk so that history is preserved
|
|
40
|
+
* and the agent surface remains inspectable.
|
|
41
|
+
* - The marker is appended at the end of the file, idempotent — if it already
|
|
42
|
+
* exists, no second append occurs.
|
|
43
|
+
* - The marker does NOT trigger a drift TEN. Dormant files are intentionally
|
|
44
|
+
* deactivated, not accidentally forked.
|
|
45
|
+
* - The marker is never included in active file writes — only dormant writes.
|
|
46
|
+
*
|
|
47
|
+
* Used by: writeDormantMarker() (write) and hasDormantMarker() (idempotency check).
|
|
48
|
+
* Exported for use in tests.
|
|
49
|
+
*
|
|
50
|
+
* Chain: WP-379 S4.
|
|
51
|
+
*/
|
|
52
|
+
export const DORMANT_MARKER = '<!-- pb-status: dormant -->';
|
|
53
|
+
/**
|
|
54
|
+
* hasDormantMarker — check whether a file on disk already has the dormant marker.
|
|
55
|
+
* Used for idempotency: if the marker is already present, skip the append.
|
|
56
|
+
*/
|
|
57
|
+
function hasDormantMarker(content) {
|
|
58
|
+
return content.includes(DORMANT_MARKER);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* writeDormantMarker — append the dormant marker to a previously-projected file.
|
|
62
|
+
*
|
|
63
|
+
* Idempotent: if DORMANT_MARKER is already present, no-op.
|
|
64
|
+
* Only operates on files that have the auto-gen MARKER — we never touch
|
|
65
|
+
* manually-authored files.
|
|
66
|
+
*
|
|
67
|
+
* @param filePath Absolute path to the file.
|
|
68
|
+
* @returns 'written' | 'already-dormant' | 'skipped' (no auto-gen marker)
|
|
69
|
+
*/
|
|
70
|
+
export function writeDormantMarkerToFile(filePath) {
|
|
71
|
+
if (!existsSync(filePath))
|
|
72
|
+
return 'skipped';
|
|
73
|
+
const content = readFileSync(filePath, 'utf8');
|
|
74
|
+
// Only mark files that were originally projected by pb handshake.
|
|
75
|
+
// Files without the auto-gen MARKER are manually authored — leave them alone.
|
|
76
|
+
if (!content.includes(MARKER))
|
|
77
|
+
return 'skipped';
|
|
78
|
+
if (hasDormantMarker(content))
|
|
79
|
+
return 'already-dormant';
|
|
80
|
+
// Append the marker on its own line. No trailing newline assumption —
|
|
81
|
+
// appendFileSync adds to whatever is already there.
|
|
82
|
+
appendFileSync(filePath, `\n${DORMANT_MARKER}\n`);
|
|
83
|
+
return 'written';
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* deriveDormantFilePaths — compute the set of on-disk file paths that would have
|
|
87
|
+
* been projected for a given dormant asset (by name and assetKind).
|
|
88
|
+
*
|
|
89
|
+
* Assets are projected to one or more surfaces (cursor/claude/codex) depending
|
|
90
|
+
* on shouldEmitToTarget. Since we don't re-run shouldEmitToTarget here, we
|
|
91
|
+
* speculatively probe all known surface paths and let writeDormantMarkerToFile
|
|
92
|
+
* decide whether each exists and has the auto-gen MARKER.
|
|
93
|
+
*
|
|
94
|
+
* @param asset The dormant asset from the server.
|
|
95
|
+
* @param cwd Current working directory (project root).
|
|
96
|
+
* @returns Array of absolute file paths to probe.
|
|
97
|
+
*/
|
|
98
|
+
function deriveDormantFilePaths(asset, cwd) {
|
|
99
|
+
// Defense-in-depth: even though `name` originates from platform-seeded DB
|
|
100
|
+
// entries (not user input), validate it against a strict charset before
|
|
101
|
+
// interpolating into a filesystem path. Reject anything that could traverse
|
|
102
|
+
// out of the expected directories. WP-379 S4 review finding.
|
|
103
|
+
if (!/^[A-Za-z0-9._-]+$/.test(asset.name)) {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
const paths = [];
|
|
107
|
+
const { name, assetKind } = asset;
|
|
108
|
+
if (assetKind === 'skill') {
|
|
109
|
+
// Cursor skill
|
|
110
|
+
paths.push(join(cwd, '.cursor', 'skills', name, 'SKILL.md'));
|
|
111
|
+
// Codex skill
|
|
112
|
+
paths.push(join(cwd, '.codex', 'skills', `${name}.md`));
|
|
113
|
+
}
|
|
114
|
+
else if (assetKind === 'rule' || assetKind === 'hook') {
|
|
115
|
+
// Cursor rule
|
|
116
|
+
paths.push(join(cwd, '.cursor', 'rules', `${name}.mdc`));
|
|
117
|
+
// Claude rule
|
|
118
|
+
paths.push(join(cwd, '.claude', 'rules', `${name}.md`));
|
|
119
|
+
}
|
|
120
|
+
return paths;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Single-shot health probe — calls `workspace.health` and inspects
|
|
124
|
+
* `starterSetupSeeded`. Does NOT poll internally; polling is the caller's
|
|
125
|
+
* responsibility (connect-screens.tsx).
|
|
126
|
+
*
|
|
127
|
+
* Returns:
|
|
128
|
+
* - `seeds-ready` — health query succeeded AND starterSetupSeeded is true
|
|
129
|
+
* - `seeds-pending` — health query succeeded but starterSetupSeeded is false
|
|
130
|
+
* - `probe-failed` — health query threw (network, auth, etc.)
|
|
131
|
+
*/
|
|
132
|
+
export async function probeStarterSetupSeeded() {
|
|
133
|
+
try {
|
|
134
|
+
const health = await kernelCall('workspace.health', {});
|
|
135
|
+
if (health.starterSetupSeeded) {
|
|
136
|
+
return { status: 'seeds-ready' };
|
|
137
|
+
}
|
|
138
|
+
const starterGaps = (health.gaps ?? []).filter((g) => g.kind === 'starter-setup-missing' || g.kind === 'platform-domains-missing');
|
|
139
|
+
return {
|
|
140
|
+
status: 'seeds-pending',
|
|
141
|
+
gaps: starterGaps.length > 0 ? starterGaps : [
|
|
142
|
+
{
|
|
143
|
+
kind: 'starter-setup-missing',
|
|
144
|
+
severity: 'warn',
|
|
145
|
+
message: 'Starter setup seeds are still running.',
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
return {
|
|
152
|
+
status: 'probe-failed',
|
|
153
|
+
error: err instanceof Error ? err.message : String(err),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Poll `probeStarterSetupSeeded` up to MAX_POLLS times (10s at 500ms intervals).
|
|
159
|
+
* Returns the final probe result — caller decides how to render the outcome.
|
|
160
|
+
*
|
|
161
|
+
* Exported so connect-screens.tsx can use it without re-implementing the loop.
|
|
162
|
+
*/
|
|
163
|
+
export async function pollUntilSeedsReady() {
|
|
164
|
+
for (let poll = 0; poll < MAX_POLLS; poll++) {
|
|
165
|
+
const result = await probeStarterSetupSeeded();
|
|
166
|
+
if (result.status === 'seeds-ready')
|
|
167
|
+
return result;
|
|
168
|
+
if (result.status === 'probe-failed')
|
|
169
|
+
return result; // don't retry on auth/network errors
|
|
170
|
+
// seeds-pending — wait before next poll
|
|
171
|
+
if (poll < MAX_POLLS - 1) {
|
|
172
|
+
await new Promise((res) => setTimeout(res, POLL_INTERVAL_MS));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Final probe after exhausting waits — return whatever state we have
|
|
176
|
+
return probeStarterSetupSeeded();
|
|
177
|
+
}
|
|
178
|
+
const LEVELS = {
|
|
179
|
+
guide: {
|
|
180
|
+
label: 'Guide me',
|
|
181
|
+
description: "Explain what you're doing, ask before anything unfamiliar",
|
|
182
|
+
defaultMode: 'auto',
|
|
183
|
+
allow: ['Bash(pb:*)', 'Bash(git:*)'],
|
|
184
|
+
},
|
|
185
|
+
work: {
|
|
186
|
+
label: 'Just work',
|
|
187
|
+
description: 'Ask only when something could be risky or irreversible',
|
|
188
|
+
defaultMode: 'auto',
|
|
189
|
+
allow: ['Bash(pb:*)', 'Bash(git:*)', 'Bash(npm run:*)'],
|
|
190
|
+
},
|
|
191
|
+
silent: {
|
|
192
|
+
label: 'Silent',
|
|
193
|
+
description: "I'll review the diff; don't ask unless it's destructive",
|
|
194
|
+
defaultMode: 'acceptEdits',
|
|
195
|
+
allow: ['Bash(pb:*)', 'Bash(git:*)', 'Bash(npm run:*)', 'Bash(npx:*)'],
|
|
196
|
+
},
|
|
197
|
+
'full-trust': {
|
|
198
|
+
label: 'Full trust',
|
|
199
|
+
description: 'Never ask (your machine, your call)',
|
|
200
|
+
defaultMode: 'bypassPermissions',
|
|
201
|
+
allow: [],
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
// Explicit ordering — determines menu numbering in promptLevel(). Do not reorder.
|
|
205
|
+
const LEVEL_KEYS = ['guide', 'work', 'silent', 'full-trust'];
|
|
206
|
+
// Hook failure contract (TEN-712): all hook commands MUST end with '2>/dev/null || true'
|
|
207
|
+
// so Claude Code always starts even if pb is unavailable. Never remove this suffix.
|
|
208
|
+
const INIT_PERMISSION = 'Bash(pb:*)';
|
|
209
|
+
function readSettings(filePath) {
|
|
210
|
+
if (!existsSync(filePath))
|
|
211
|
+
return {};
|
|
212
|
+
try {
|
|
213
|
+
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
console.error(`Warning: ${filePath} has invalid JSON — starting fresh.`);
|
|
217
|
+
return {};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Team write: hooks + Bash(pb:*) → .claude/settings.json (safe to commit)
|
|
221
|
+
function writeTeamSettings(cwd, dryRun) {
|
|
222
|
+
const claudeDir = join(cwd, '.claude');
|
|
223
|
+
const settingsPath = join(claudeDir, 'settings.json');
|
|
224
|
+
const settings = readSettings(settingsPath);
|
|
225
|
+
const added = [];
|
|
226
|
+
const permissions = (settings.permissions ?? {});
|
|
227
|
+
const allow = permissions.allow ?? [];
|
|
228
|
+
if (!allow.includes(INIT_PERMISSION)) {
|
|
229
|
+
allow.push(INIT_PERMISSION);
|
|
230
|
+
permissions.allow = allow;
|
|
231
|
+
added.push('Bash(pb:*) permission');
|
|
232
|
+
}
|
|
233
|
+
settings.permissions = permissions;
|
|
234
|
+
const hooks = (settings.hooks ?? {});
|
|
235
|
+
const hookAdditions = composeHooksFromIntents(['session-start', 'session-close', 'pre-compact'], hooks);
|
|
236
|
+
for (const addition of hookAdditions) {
|
|
237
|
+
hooks[addition.event] = [...(hooks[addition.event] ?? []), addition.entry];
|
|
238
|
+
added.push(addition.label);
|
|
239
|
+
}
|
|
240
|
+
settings.hooks = hooks;
|
|
241
|
+
if (!dryRun) {
|
|
242
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
243
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
244
|
+
}
|
|
245
|
+
return added;
|
|
246
|
+
}
|
|
247
|
+
// Personal write: trust level → ~/.claude/settings.json (never committed)
|
|
248
|
+
function writePersonalSettings(levelKey, dryRun) {
|
|
249
|
+
const level = LEVELS[levelKey];
|
|
250
|
+
const globalDir = join(homedir(), '.claude');
|
|
251
|
+
const globalPath = join(globalDir, 'settings.json');
|
|
252
|
+
const settings = readSettings(globalPath);
|
|
253
|
+
const added = [];
|
|
254
|
+
const permissions = (settings.permissions ?? {});
|
|
255
|
+
if (permissions.defaultMode !== level.defaultMode) {
|
|
256
|
+
permissions.defaultMode = level.defaultMode;
|
|
257
|
+
added.push(`defaultMode → ${level.defaultMode}`);
|
|
258
|
+
}
|
|
259
|
+
const allow = permissions.allow ?? [];
|
|
260
|
+
for (const entry of level.allow) {
|
|
261
|
+
if (!allow.includes(entry)) {
|
|
262
|
+
allow.push(entry);
|
|
263
|
+
added.push(`allow: ${entry}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
permissions.allow = allow;
|
|
267
|
+
settings.permissions = permissions;
|
|
268
|
+
if (!dryRun) {
|
|
269
|
+
mkdirSync(globalDir, { recursive: true });
|
|
270
|
+
writeFileSync(globalPath, JSON.stringify(settings, null, 2) + '\n');
|
|
271
|
+
}
|
|
272
|
+
return added;
|
|
273
|
+
}
|
|
274
|
+
async function promptLevel() {
|
|
275
|
+
const result = await promptSelect({
|
|
276
|
+
message: 'How much should Claude explain before acting?',
|
|
277
|
+
options: LEVEL_KEYS.map((key) => ({
|
|
278
|
+
value: key,
|
|
279
|
+
label: LEVELS[key].label,
|
|
280
|
+
hint: LEVELS[key].description,
|
|
281
|
+
})),
|
|
282
|
+
});
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
export async function runHandshakeInit(options = {}) {
|
|
286
|
+
const cwd = process.cwd();
|
|
287
|
+
const dryRun = options.dryRun ?? false;
|
|
288
|
+
const suffix = dryRun ? ' (dry run)' : '';
|
|
289
|
+
if (options.level && !LEVEL_KEYS.includes(options.level)) {
|
|
290
|
+
throw new CLIError(`Unknown level "${options.level}".`, {
|
|
291
|
+
code: ErrorCode.VALIDATION_FAILED,
|
|
292
|
+
category: 'validation',
|
|
293
|
+
guidance: `Valid levels: ${LEVEL_KEYS.join(', ')}`,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
console.log('Setting up Claude Code integration...\n');
|
|
297
|
+
// Step 1: Team config — always, no prompt
|
|
298
|
+
const teamAdded = writeTeamSettings(cwd, dryRun);
|
|
299
|
+
const teamPath = join(cwd, '.claude', 'settings.json');
|
|
300
|
+
if (teamAdded.length === 0) {
|
|
301
|
+
console.log(`✓ ${teamPath} — already up to date${suffix}`);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
console.log(`✓ ${teamPath}${suffix}`);
|
|
305
|
+
for (const item of teamAdded)
|
|
306
|
+
console.log(` + ${item}`);
|
|
307
|
+
}
|
|
308
|
+
// Step 2: Personal config — wizard or --level flag
|
|
309
|
+
// Cast is safe: LEVEL_KEYS.includes() validated above; invalid level already threw CLIError.
|
|
310
|
+
const levelKey = options.level ? options.level : await promptLevel();
|
|
311
|
+
const level = LEVELS[levelKey];
|
|
312
|
+
const personalAdded = writePersonalSettings(levelKey, dryRun);
|
|
313
|
+
const personalPath = join(homedir(), '.claude', 'settings.json');
|
|
314
|
+
if (personalAdded.length === 0) {
|
|
315
|
+
console.log(`✓ ${personalPath} — already at "${level.label}"${suffix}`);
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
console.log(`✓ ${personalPath} — set to "${level.label}"${suffix}`);
|
|
319
|
+
for (const item of personalAdded)
|
|
320
|
+
console.log(` + ${item}`);
|
|
321
|
+
}
|
|
322
|
+
console.log('');
|
|
323
|
+
console.log('Team config → commit .claude/settings.json to share hooks with your team');
|
|
324
|
+
console.log('Personal config → ~/.claude/settings.json is private to this machine, never commit');
|
|
325
|
+
console.log('');
|
|
326
|
+
console.log('Hook failure: if pb is unavailable, hooks fail silently — Claude Code always starts.');
|
|
327
|
+
console.log('');
|
|
328
|
+
if (!dryRun)
|
|
329
|
+
console.log('Reload /hooks in Claude Code (or restart) to activate.');
|
|
330
|
+
console.log('Run `pb handshake --init --level <guide|work|silent|full-trust>` to change level.');
|
|
331
|
+
// Step 2b: Report multi-surface hook opt-in status (WP-310 E3b)
|
|
332
|
+
// Reads manifest.hooks.{cursor,copilot} and prints an informational note for
|
|
333
|
+
// each opted-in surface. Silence = no manifest or no hooks flags set.
|
|
334
|
+
// DEC-536: Claude-native default is already wired above; this block only fires
|
|
335
|
+
// when the user has explicitly opted in via manifest.
|
|
336
|
+
const pbDirForManifest = join(cwd, '.productbrain');
|
|
337
|
+
const initManifest = readManifest(pbDirForManifest);
|
|
338
|
+
const multiSurfaceOptIns = [];
|
|
339
|
+
if (initManifest?.hooks?.cursor)
|
|
340
|
+
multiSurfaceOptIns.push('cursor');
|
|
341
|
+
if (initManifest?.hooks?.copilot)
|
|
342
|
+
multiSurfaceOptIns.push('copilot');
|
|
343
|
+
if (multiSurfaceOptIns.length > 0) {
|
|
344
|
+
console.log('');
|
|
345
|
+
for (const surface of multiSurfaceOptIns) {
|
|
346
|
+
const status = getHookStatusForSurface(surface);
|
|
347
|
+
if (status.writable) {
|
|
348
|
+
// Future-proofing path: surface has hook events, would write files.
|
|
349
|
+
console.log(`ℹ ${surface} hooks: opted in — hooks will be written`);
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
console.log(`ℹ ${surface} hooks: opted in — no auto-hooks (${surface} has no session events; run \`pb session start\` manually)`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// Step 3: Scaffold starter templates if .productbrain/rules/ is empty
|
|
357
|
+
const rulesDir = join(cwd, '.productbrain', 'rules');
|
|
358
|
+
const hasExistingRules = existsSync(rulesDir) && readdirSync(rulesDir).filter((f) => f.endsWith('.md')).length > 0;
|
|
359
|
+
if (!hasExistingRules) {
|
|
360
|
+
// Detect stack from repo to pick template set
|
|
361
|
+
const repo = detectRepo(cwd);
|
|
362
|
+
const stack = repo.detectedStack.map((s) => s.toLowerCase());
|
|
363
|
+
let templateSet;
|
|
364
|
+
if (stack.some((s) => ['typescript', 'sveltekit', 'nextjs', 'react'].includes(s))) {
|
|
365
|
+
templateSet = 'node-ts';
|
|
366
|
+
}
|
|
367
|
+
else if (stack.includes('python')) {
|
|
368
|
+
templateSet = 'python';
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
templateSet = 'general';
|
|
372
|
+
}
|
|
373
|
+
// Resolve templates directory relative to this file
|
|
374
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
375
|
+
const __dirname = dirname(__filename);
|
|
376
|
+
// From dist/commands/handshake.js → ../../templates/
|
|
377
|
+
const templatesRoot = resolve(__dirname, '..', '..', 'templates');
|
|
378
|
+
const templateDir = join(templatesRoot, templateSet);
|
|
379
|
+
if (existsSync(templateDir)) {
|
|
380
|
+
const templateFiles = readdirSync(templateDir).filter((f) => f.endsWith('.md'));
|
|
381
|
+
if (templateFiles.length > 0) {
|
|
382
|
+
console.log('');
|
|
383
|
+
console.log(`Scaffolding starter rules from ${templateSet} template...`);
|
|
384
|
+
if (!dryRun) {
|
|
385
|
+
mkdirSync(rulesDir, { recursive: true });
|
|
386
|
+
for (const file of templateFiles) {
|
|
387
|
+
const src = join(templateDir, file);
|
|
388
|
+
const dest = join(rulesDir, file);
|
|
389
|
+
copyFileSync(src, dest);
|
|
390
|
+
console.log(` + .productbrain/rules/${file}`);
|
|
391
|
+
}
|
|
392
|
+
console.log('');
|
|
393
|
+
console.log('Run `pb handshake` to sync the scaffolded rules to your AI tools.');
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
for (const file of templateFiles) {
|
|
397
|
+
console.log(` + .productbrain/rules/${file} (dry run)`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Normalize volatile handshake-only timestamps before comparing generated files.
|
|
406
|
+
* This keeps the visible timestamps in generated artifacts while avoiding
|
|
407
|
+
* meaningless rewrites when semantic content is unchanged.
|
|
408
|
+
*/
|
|
409
|
+
export function normalizeHandshakeContentForComparison(content) {
|
|
410
|
+
return content
|
|
411
|
+
.replace(/<!-- auto-generated by pb handshake — [0-9]{4}-[0-9]{2}-[0-9]{2}T[^>]+ -->/g, '<!-- auto-generated by pb handshake — <TIMESTAMP> -->')
|
|
412
|
+
.replace(/^_Generated: [0-9]{4}-[0-9]{2}-[0-9]{2}T.*_$/gm, '_Generated: <TIMESTAMP>_');
|
|
413
|
+
}
|
|
414
|
+
function shouldWriteAdapter(filePath, force) {
|
|
415
|
+
if (force)
|
|
416
|
+
return true;
|
|
417
|
+
if (!existsSync(filePath))
|
|
418
|
+
return true;
|
|
419
|
+
const content = readFileSync(filePath, 'utf8');
|
|
420
|
+
return content.includes(MARKER);
|
|
421
|
+
}
|
|
422
|
+
function deduplicateEntries(entries) {
|
|
423
|
+
const seen = new Set();
|
|
424
|
+
const result = [];
|
|
425
|
+
for (const e of entries) {
|
|
426
|
+
const key = e.entryId ?? e.name;
|
|
427
|
+
if (!seen.has(key)) {
|
|
428
|
+
seen.add(key);
|
|
429
|
+
result.push(e);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return result;
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* resolveProjectionCollision — WP-379 S5b
|
|
436
|
+
*
|
|
437
|
+
* Marker-scoped orphan unlink: enumerates target dirs (.cursor/rules/,
|
|
438
|
+
* .claude/rules/, .claude/skills/, .codex/skills/); for each file that has
|
|
439
|
+
* the auto-gen MARKER whose lowercase-normalized filename does NOT match any
|
|
440
|
+
* current active-asset materializedFilename, the file is unlinked.
|
|
441
|
+
*
|
|
442
|
+
* User-forked files (no MARKER) are never touched, regardless of name.
|
|
443
|
+
*
|
|
444
|
+
* Linux case-collision disambiguation:
|
|
445
|
+
* 1. Exact match (lowercase name == any canonical name): survives.
|
|
446
|
+
* 2. Case-variant with MARKER (marker file, no exact canonical match): unlinked.
|
|
447
|
+
* 3. Ambiguous (zero exact, multiple case-variants with MARKER):
|
|
448
|
+
* newest mtime wins; all others are unlinked; a collision TEN is
|
|
449
|
+
* appended to the session capture queue (not fired inline).
|
|
450
|
+
*
|
|
451
|
+
* Returns a list of unlink results so the caller can log/report them.
|
|
452
|
+
*
|
|
453
|
+
* @param cwd Project root (absolute path).
|
|
454
|
+
* @param assetNames The current set of canonical asset names from the server
|
|
455
|
+
* (e.g. ["Setup-ProductBrain", "chain-rules"]).
|
|
456
|
+
* @param log Progress log function.
|
|
457
|
+
* @param logErr Error log function.
|
|
458
|
+
*/
|
|
459
|
+
export function resolveProjectionCollision(cwd, assetNames, log, logErr) {
|
|
460
|
+
// Target directories by extension suffix.
|
|
461
|
+
const TARGET_DIRS_BY_EXT = [
|
|
462
|
+
{ dir: join(cwd, '.cursor', 'rules'), ext: '.mdc' },
|
|
463
|
+
{ dir: join(cwd, '.claude', 'rules'), ext: '.md' },
|
|
464
|
+
{ dir: join(cwd, '.claude', 'skills'), ext: '.md' },
|
|
465
|
+
{ dir: join(cwd, '.codex', 'skills'), ext: '.md' },
|
|
466
|
+
];
|
|
467
|
+
// Build a set of normalized canonical names (without extension) for fast lookup.
|
|
468
|
+
// We normalize all asset names to detect case-variant collisions.
|
|
469
|
+
// For each asset name we derive the normalized basename (the part before the ext).
|
|
470
|
+
// canonicalNormalizedNames: Set<normalized-stem> (lowercase + slug).
|
|
471
|
+
const canonicalNormalizedStems = new Set(assetNames.map((n) => normalizeMaterializedFilename(n)));
|
|
472
|
+
const results = [];
|
|
473
|
+
const collisionTens = [];
|
|
474
|
+
for (const { dir, ext } of TARGET_DIRS_BY_EXT) {
|
|
475
|
+
if (!existsSync(dir))
|
|
476
|
+
continue;
|
|
477
|
+
let files;
|
|
478
|
+
try {
|
|
479
|
+
files = readdirSync(dir);
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
continue; // unreadable dir — skip
|
|
483
|
+
}
|
|
484
|
+
// Group files by their normalized stem.
|
|
485
|
+
// normalizedStem → [ { filename, fullPath } ]
|
|
486
|
+
const groups = new Map();
|
|
487
|
+
for (const filename of files) {
|
|
488
|
+
if (!filename.endsWith(ext))
|
|
489
|
+
continue;
|
|
490
|
+
const fullPath = join(dir, filename);
|
|
491
|
+
// Only operate on files that have the auto-gen MARKER.
|
|
492
|
+
let content;
|
|
493
|
+
try {
|
|
494
|
+
content = readFileSync(fullPath, 'utf8');
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
continue; // unreadable file — skip
|
|
498
|
+
}
|
|
499
|
+
if (!content.includes(MARKER))
|
|
500
|
+
continue; // user-forked — never touch
|
|
501
|
+
const stem = basename(filename, ext);
|
|
502
|
+
const normalizedStem = normalizeMaterializedFilename(stem);
|
|
503
|
+
const group = groups.get(normalizedStem) ?? [];
|
|
504
|
+
group.push({ filename, fullPath });
|
|
505
|
+
groups.set(normalizedStem, group);
|
|
506
|
+
}
|
|
507
|
+
// Evaluate each normalized stem group.
|
|
508
|
+
for (const [normalizedStem, members] of groups) {
|
|
509
|
+
const isKnownCanonical = canonicalNormalizedStems.has(normalizedStem);
|
|
510
|
+
if (!isKnownCanonical) {
|
|
511
|
+
// All members of this group are orphans (no canonical asset with this stem).
|
|
512
|
+
// Unlink them all — they're stale projections of an asset no longer in the server.
|
|
513
|
+
for (const { filename, fullPath } of members) {
|
|
514
|
+
try {
|
|
515
|
+
unlinkSync(fullPath);
|
|
516
|
+
log(`Orphan unlinked: ${fullPath}`);
|
|
517
|
+
results.push({ action: 'unlinked', filePath: fullPath, reason: 'orphan-no-canonical-match' });
|
|
518
|
+
}
|
|
519
|
+
catch (err) {
|
|
520
|
+
logErr(`Warning: could not unlink orphan ${fullPath} — ${err instanceof Error ? err.message : String(err)}`);
|
|
521
|
+
results.push({ action: 'kept', filePath: fullPath, reason: 'unlink-failed' });
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
// The stem IS known canonical. Check for case-collision.
|
|
527
|
+
if (members.length === 1) {
|
|
528
|
+
// Single file — no collision.
|
|
529
|
+
results.push({ action: 'kept', filePath: members[0].fullPath, reason: 'canonical-exact' });
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
// Multiple files with the same normalized stem → case-collision.
|
|
533
|
+
// Rule: exact match (filename stem === normalized stem, i.e. already lowercase) wins.
|
|
534
|
+
const exactMatches = members.filter(({ filename }) => {
|
|
535
|
+
const stem = basename(filename, ext);
|
|
536
|
+
return stem === normalizedStem; // lowercase-equal means already normalized
|
|
537
|
+
});
|
|
538
|
+
if (exactMatches.length === 1) {
|
|
539
|
+
// Rule 1: exactly one exact match → keep it, unlink all case-variants.
|
|
540
|
+
const keeper = exactMatches[0];
|
|
541
|
+
results.push({ action: 'kept', filePath: keeper.fullPath, reason: 'case-exact-match-wins' });
|
|
542
|
+
for (const member of members) {
|
|
543
|
+
if (member.fullPath === keeper.fullPath)
|
|
544
|
+
continue;
|
|
545
|
+
try {
|
|
546
|
+
unlinkSync(member.fullPath);
|
|
547
|
+
log(`Case-variant unlinked: ${member.fullPath} (kept: ${keeper.filename})`);
|
|
548
|
+
results.push({ action: 'unlinked', filePath: member.fullPath, reason: 'case-variant-unlinked' });
|
|
549
|
+
}
|
|
550
|
+
catch (err) {
|
|
551
|
+
logErr(`Warning: could not unlink case-variant ${member.fullPath} — ${err instanceof Error ? err.message : String(err)}`);
|
|
552
|
+
results.push({ action: 'kept', filePath: member.fullPath, reason: 'unlink-failed' });
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
// Rule 3: ambiguous — zero exact matches (or multiple exact matches, which
|
|
558
|
+
// can't happen on a case-sensitive FS). Newest mtime wins.
|
|
559
|
+
// Sort by mtime descending: highest mtime = newest = winner.
|
|
560
|
+
const withStats = members.map(({ filename, fullPath }) => {
|
|
561
|
+
try {
|
|
562
|
+
const { mtimeMs } = statSync(fullPath);
|
|
563
|
+
return { filename, fullPath, mtimeMs };
|
|
564
|
+
}
|
|
565
|
+
catch {
|
|
566
|
+
return { filename, fullPath, mtimeMs: 0 };
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
withStats.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
570
|
+
const winner = withStats[0];
|
|
571
|
+
log(`Case-collision ambiguous for ${normalizedStem}${ext}: newest mtime wins (${winner.filename})`);
|
|
572
|
+
results.push({ action: 'collision-ten', filePath: winner.fullPath, reason: 'ambiguous-newest-mtime-wins' });
|
|
573
|
+
const tenMsg = `Handshake case-collision: ambiguous filename for stem "${normalizedStem}${ext}" ` +
|
|
574
|
+
`(${members.map((m) => m.filename).join(', ')}). ` +
|
|
575
|
+
`Kept newest: ${winner.filename}. Consider renaming to ${normalizedStem}${ext}.`;
|
|
576
|
+
collisionTens.push(tenMsg);
|
|
577
|
+
for (const member of withStats.slice(1)) {
|
|
578
|
+
try {
|
|
579
|
+
unlinkSync(member.fullPath);
|
|
580
|
+
log(`Case-variant (ambiguous) unlinked: ${member.fullPath}`);
|
|
581
|
+
results.push({ action: 'unlinked', filePath: member.fullPath, reason: 'ambiguous-case-variant-unlinked' });
|
|
582
|
+
}
|
|
583
|
+
catch (err) {
|
|
584
|
+
logErr(`Warning: could not unlink ambiguous case-variant ${member.fullPath} — ${err instanceof Error ? err.message : String(err)}`);
|
|
585
|
+
results.push({ action: 'kept', filePath: member.fullPath, reason: 'unlink-failed' });
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return { results, collisionTens };
|
|
591
|
+
}
|
|
592
|
+
export async function runHandshake(options = {}) {
|
|
593
|
+
const config = await getConfigOrGuide(() => runHandshake(options));
|
|
594
|
+
if (!config)
|
|
595
|
+
return;
|
|
596
|
+
const cwd = process.cwd();
|
|
597
|
+
const force = options.force ?? false;
|
|
598
|
+
const dryRun = options.dryRun ?? false;
|
|
599
|
+
// Preview mode: default when neither --apply nor --dry-run is passed.
|
|
600
|
+
// --dry-run is kept as a backward-compat alias for preview (same behavior).
|
|
601
|
+
const preview = !options.apply && !dryRun;
|
|
602
|
+
const applyMode = options.apply === true && !dryRun;
|
|
603
|
+
const level = options.level;
|
|
604
|
+
const quiet = options.quiet ?? false;
|
|
605
|
+
const generate = options.generate ?? false;
|
|
606
|
+
const timestamp = new Date().toISOString();
|
|
607
|
+
// Helper: emit progress line only when not quiet (used when handshake is a sub-step)
|
|
608
|
+
const log = (msg) => { if (!quiet)
|
|
609
|
+
process.stdout.write(msg + '\n'); };
|
|
610
|
+
const logErr = (msg) => { if (!quiet)
|
|
611
|
+
process.stderr.write(msg + '\n'); };
|
|
612
|
+
// Validate --level if provided (for handshake content filtering, not --init trust level)
|
|
613
|
+
const VALID_HANDSHAKE_LEVELS = ['beginner', 'intermediate', 'expert'];
|
|
614
|
+
if (level && !VALID_HANDSHAKE_LEVELS.includes(level)) {
|
|
615
|
+
throw new CLIError(`Unknown level "${level}".`, {
|
|
616
|
+
code: ErrorCode.VALIDATION_FAILED,
|
|
617
|
+
category: 'validation',
|
|
618
|
+
guidance: `Valid levels: ${VALID_HANDSHAKE_LEVELS.join(', ')}`,
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
// 1. Detect repo
|
|
622
|
+
const repo = detectRepo(cwd);
|
|
623
|
+
log(`Detecting repo: ${repo.name ?? 'unknown'}${repo.repoSlug ? ` (${repo.repoSlug})` : ''}`);
|
|
624
|
+
if (repo.detectedStack.length > 0) {
|
|
625
|
+
log(`Stack: ${repo.detectedStack.join(', ')}`);
|
|
626
|
+
}
|
|
627
|
+
// 2. Fetch orient view + workspace readiness in parallel (budget max +200ms added latency)
|
|
628
|
+
log('Fetching workspace context...');
|
|
629
|
+
let orientView = null;
|
|
630
|
+
let workspaceProfile = null;
|
|
631
|
+
try {
|
|
632
|
+
const workspaceReadinessPromise = kernelCall('chain.workspaceReadiness', {}).catch(() => null);
|
|
633
|
+
const [orientResult, readinessRaw] = await Promise.all([
|
|
634
|
+
kernelCall('chain.getOrientView', {}).catch((err) => {
|
|
635
|
+
logErr(`Warning: could not fetch workspace context — ${err instanceof Error ? err.message : err}`);
|
|
636
|
+
logErr('Continuing with limited context (Chain search + portable knowledge only).');
|
|
637
|
+
return null;
|
|
638
|
+
}),
|
|
639
|
+
workspaceReadinessPromise,
|
|
640
|
+
]);
|
|
641
|
+
orientView = orientResult;
|
|
642
|
+
workspaceProfile = extractWorkspaceProfile(readinessRaw);
|
|
643
|
+
}
|
|
644
|
+
catch (err) {
|
|
645
|
+
logErr(`Warning: could not fetch workspace context — ${err instanceof Error ? err.message : err}`);
|
|
646
|
+
logErr('Continuing with limited context (Chain search + portable knowledge only).');
|
|
647
|
+
}
|
|
648
|
+
if (workspaceProfile) {
|
|
649
|
+
log(`Workspace profile: stage=${workspaceProfile.stage}, entries=${workspaceProfile.totalEntries}, governance=${workspaceProfile.governanceMode}`);
|
|
650
|
+
}
|
|
651
|
+
// 3. Build search queries from repo context
|
|
652
|
+
const searchQueries = [];
|
|
653
|
+
if (repo.name && repo.name.length >= 2)
|
|
654
|
+
searchQueries.push(repo.name);
|
|
655
|
+
// Add repo slug parts (e.g. "Product-OS" -> "Product-OS")
|
|
656
|
+
if (repo.repoSlug) {
|
|
657
|
+
const repoPart = repo.repoSlug.split('/').pop();
|
|
658
|
+
if (repoPart && repoPart.length >= 2 && repoPart !== repo.name) {
|
|
659
|
+
searchQueries.push(repoPart);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
// Limit to 5 unique queries
|
|
663
|
+
const uniqueQueries = [...new Set(searchQueries.filter((q) => q.length >= 2))].slice(0, 5);
|
|
664
|
+
// 4. Search the Chain
|
|
665
|
+
let matchedEntries = [];
|
|
666
|
+
if (uniqueQueries.length > 0) {
|
|
667
|
+
log(`Searching Chain for: ${uniqueQueries.join(', ')}...`);
|
|
668
|
+
const searchResults = await Promise.all(uniqueQueries.map((q) => kernelCall('chain.searchEntries', { query: q }).catch(() => [])));
|
|
669
|
+
matchedEntries = deduplicateEntries(searchResults.flat());
|
|
670
|
+
}
|
|
671
|
+
// 5. Read canonical skills & rules — DB-first (WP-345 S0c, TEN-1459), FS fallback.
|
|
672
|
+
// Primary: query setup.listAssetsForUser from DB (workspace SSOT).
|
|
673
|
+
// Fallback: read from .productbrain/ filesystem (legacy — used when DB is empty or unavailable).
|
|
674
|
+
const pbDir = join(cwd, '.productbrain');
|
|
675
|
+
let dbSkills = [];
|
|
676
|
+
let dbRules = [];
|
|
677
|
+
let usedDbSource = false;
|
|
678
|
+
let dbAssetRows = [];
|
|
679
|
+
// WP-379 S4: dormant assets (gate-failed) — their on-disk files get the dormant marker.
|
|
680
|
+
let dormantDbAssetRows = [];
|
|
681
|
+
// WP-379 S5b: whether any setup_receipt exists for this workspace (first-run UX gate).
|
|
682
|
+
// undefined when server is pre-S5b (treat as unknown → suppress drift TENs conservatively).
|
|
683
|
+
let hasAnyReceipt = undefined;
|
|
684
|
+
const dbProjectionHashes = new Map();
|
|
685
|
+
try {
|
|
686
|
+
// WP-379 S4: listAssetsForUser now returns { activeAssets, dormantAssets }.
|
|
687
|
+
// Wire format changed from DbAsset[] to { activeAssets: DbAsset[], dormantAssets: DbAsset[] }.
|
|
688
|
+
// Fall back to empty arrays if the server returns the old flat-array shape (graceful degradation).
|
|
689
|
+
const rawResponse = await kernelCall('setup.listAssetsForUser', {}).catch(() => null);
|
|
690
|
+
let dbAssets = [];
|
|
691
|
+
if (rawResponse !== null) {
|
|
692
|
+
if (Array.isArray(rawResponse)) {
|
|
693
|
+
// Pre-S4 server — treat entire response as active assets with no dormant list.
|
|
694
|
+
dbAssets = rawResponse;
|
|
695
|
+
dormantDbAssetRows = [];
|
|
696
|
+
hasAnyReceipt = undefined; // unknown on legacy servers
|
|
697
|
+
}
|
|
698
|
+
else {
|
|
699
|
+
dbAssets = rawResponse.activeAssets ?? [];
|
|
700
|
+
dormantDbAssetRows = rawResponse.dormantAssets ?? [];
|
|
701
|
+
// WP-379 S5b: extract hasAnyReceipt when provided by the server.
|
|
702
|
+
// undefined means pre-S5b server — we treat as unknown (no receipts assumed).
|
|
703
|
+
hasAnyReceipt = rawResponse.hasAnyReceipt;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
if (dbAssets.length > 0) {
|
|
707
|
+
dbAssetRows = dbAssets;
|
|
708
|
+
// Map DB assets to CanonicalSkill/CanonicalRule shapes
|
|
709
|
+
for (const asset of dbAssets) {
|
|
710
|
+
if (asset.disabledByOwner)
|
|
711
|
+
continue;
|
|
712
|
+
if (asset.assetKind === 'skill') {
|
|
713
|
+
dbSkills.push({
|
|
714
|
+
name: asset.name,
|
|
715
|
+
description: asset.description,
|
|
716
|
+
triggers: asset.triggers ?? [],
|
|
717
|
+
body: asset.body,
|
|
718
|
+
sourcePath: `db:${asset.entryId}`,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
else if (asset.assetKind === 'rule' || asset.assetKind === 'hook') {
|
|
722
|
+
dbRules.push({
|
|
723
|
+
name: asset.name,
|
|
724
|
+
description: asset.description,
|
|
725
|
+
autoApply: false,
|
|
726
|
+
body: asset.body,
|
|
727
|
+
sourcePath: `db:${asset.entryId}`,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
usedDbSource = true;
|
|
732
|
+
log(`Setup assets: ${dbSkills.length} skills, ${dbRules.length} rules/hooks from DB (WP-345 DB-first path)`);
|
|
733
|
+
if (dormantDbAssetRows.length > 0) {
|
|
734
|
+
log(`Setup assets: ${dormantDbAssetRows.length} dormant (gate-filtered) asset(s) will be marked on disk`);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
catch {
|
|
739
|
+
// DB source unavailable — silently fall through to FS path
|
|
740
|
+
}
|
|
741
|
+
const allSkills = usedDbSource ? dbSkills : readCanonicalSkills(pbDir);
|
|
742
|
+
const manualRules = usedDbSource ? dbRules : readCanonicalRules(pbDir);
|
|
743
|
+
if (!usedDbSource) {
|
|
744
|
+
log('Setup assets: reading from filesystem (DB source unavailable or empty)');
|
|
745
|
+
}
|
|
746
|
+
// 5a-pre. E4: Resolve semantic refs in DB assets (WP-345, DEC-A, DEC-C, DEC-K)
|
|
747
|
+
// For each asset with semanticRefs[], resolve them via the Convex resolver and
|
|
748
|
+
// replace {{ref:key}} placeholders in the body. Runs in apply mode only (not preview).
|
|
749
|
+
// NG11: PostHog events fire from CLI side only (never inside Convex mutations).
|
|
750
|
+
if (usedDbSource && applyMode) {
|
|
751
|
+
const projectableDbAssets = dbAssetRows.filter((a) => !a.disabledByOwner && (a.assetKind === 'skill' || a.assetKind === 'rule' || a.assetKind === 'hook'));
|
|
752
|
+
const assetsWithRefs = projectableDbAssets.filter((a) => a.semanticRefs && a.semanticRefs.length > 0);
|
|
753
|
+
if (assetsWithRefs.length > 0) {
|
|
754
|
+
log(`Resolving semantic refs for ${assetsWithRefs.length} asset(s)...`);
|
|
755
|
+
// Collect unique ref keys across all assets
|
|
756
|
+
const allRefKeys = [...new Set(assetsWithRefs.flatMap((a) => a.semanticRefs))];
|
|
757
|
+
// Resolve all refs in a single batch call. Shape: SetupRefResolution[]
|
|
758
|
+
// (DEC-767 / WP-354 Build-Order #6 — kind + status discriminator).
|
|
759
|
+
let resolvedRefs = [];
|
|
760
|
+
try {
|
|
761
|
+
resolvedRefs = await kernelCall('setup.resolveSemanticRefs', { semanticRefs: allRefKeys });
|
|
762
|
+
}
|
|
763
|
+
catch (err) {
|
|
764
|
+
trackEvent('setup.refs.resolve_failed', { error: err instanceof Error ? err.message : String(err) });
|
|
765
|
+
logErr(`Warning: could not resolve semantic refs — ${err instanceof Error ? err.message : String(err)}`);
|
|
766
|
+
}
|
|
767
|
+
// Build resolved map: canonicalKey → display name. Only required refs
|
|
768
|
+
// count as unresolved warnings; seed/unknown refs are not gates.
|
|
769
|
+
const resolvedMap = new Map();
|
|
770
|
+
let unresolvedCount = 0;
|
|
771
|
+
for (const result of resolvedRefs) {
|
|
772
|
+
if (result.status === 'resolved' && result.localEntryId) {
|
|
773
|
+
resolvedMap.set(result.ref, result.localEntryId);
|
|
774
|
+
trackEvent('skill.ref.resolved', { ref: result.ref, kind: result.kind });
|
|
775
|
+
}
|
|
776
|
+
else if (result.status === 'unsupported-future') {
|
|
777
|
+
// seed: refs are explicitly future scope per WP-354 Build-Order #6 — not a warning.
|
|
778
|
+
trackEvent('skill.ref.future', { ref: result.ref, kind: result.kind });
|
|
779
|
+
}
|
|
780
|
+
else if (result.required) {
|
|
781
|
+
unresolvedCount++;
|
|
782
|
+
trackEvent('skill.ref.unresolved', { ref: result.ref, kind: result.kind, status: result.status });
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
if (unresolvedCount > 0) {
|
|
786
|
+
logErr(`Warning: ${unresolvedCount} required semantic ref(s) could not be resolved.`);
|
|
787
|
+
}
|
|
788
|
+
// Projection must preserve ref tokens as portable machine-readable refs.
|
|
789
|
+
// resolvedMap is only used for validation/telemetry here; generated setup
|
|
790
|
+
// artifacts keep {{ref:domain:...}} / {{ref:entry:...}} intact.
|
|
791
|
+
}
|
|
792
|
+
// Compute projection hash for each projected DB asset (DEC-K).
|
|
793
|
+
// The stored hash is sha256 of the normalized resolved asset body; DB persistence
|
|
794
|
+
// is deferred until the write loop confirms a matching file was emitted.
|
|
795
|
+
// Strip existing hash trailer and timestamp lines, normalize to LF, then hash.
|
|
796
|
+
const HASH_TRAILER_REGEX = /^<!--\s*pb-hash:.*-->\s*$/gm;
|
|
797
|
+
const TIMESTAMP_REGEX = /^<!--\s*pb-generated-at:.*-->\s*$/gm;
|
|
798
|
+
for (const rawAsset of projectableDbAssets) {
|
|
799
|
+
const resolvedAsset = [...dbSkills, ...dbRules].find((a) => a.sourcePath === `db:${rawAsset.entryId}`);
|
|
800
|
+
if (!resolvedAsset)
|
|
801
|
+
continue;
|
|
802
|
+
try {
|
|
803
|
+
// Build the projected body (what will be written to disk)
|
|
804
|
+
const projectedBody = resolvedAsset.body;
|
|
805
|
+
// Normalize: strip existing hash/timestamp trailers, convert to LF
|
|
806
|
+
const normalized = projectedBody
|
|
807
|
+
.replace(HASH_TRAILER_REGEX, '')
|
|
808
|
+
.replace(TIMESTAMP_REGEX, '')
|
|
809
|
+
.replace(/\r\n/g, '\n')
|
|
810
|
+
.replace(/\r/g, '\n')
|
|
811
|
+
.trimEnd();
|
|
812
|
+
// Compute sha256 hash
|
|
813
|
+
const hash = createHash('sha256').update(normalized, 'utf8').digest('hex');
|
|
814
|
+
const hashTrailer = `<!-- pb-hash: sha256:${hash} -->`;
|
|
815
|
+
// Append hash trailer to the projected body
|
|
816
|
+
resolvedAsset.body = `${normalized}\n${hashTrailer}`;
|
|
817
|
+
dbProjectionHashes.set(rawAsset.entryId, {
|
|
818
|
+
hash: `sha256:${hash}`,
|
|
819
|
+
assetKind: rawAsset.assetKind,
|
|
820
|
+
});
|
|
821
|
+
// Drift detection: compare against last known hash
|
|
822
|
+
if (rawAsset.lastProjectedHash && rawAsset.lastProjectedHash !== `sha256:${hash}`) {
|
|
823
|
+
trackEvent('skill.drift.detected', {
|
|
824
|
+
entryId: rawAsset.entryId,
|
|
825
|
+
assetKind: rawAsset.assetKind,
|
|
826
|
+
});
|
|
827
|
+
log(`Drift detected for asset ${rawAsset.entryId} — projecting updated version.`);
|
|
828
|
+
}
|
|
829
|
+
trackEvent('skill.projection.succeeded', {
|
|
830
|
+
entryId: rawAsset.entryId,
|
|
831
|
+
assetKind: rawAsset.assetKind,
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
catch (err) {
|
|
835
|
+
trackEvent('skill.projection.failed', {
|
|
836
|
+
entryId: rawAsset.entryId,
|
|
837
|
+
assetKind: rawAsset.assetKind,
|
|
838
|
+
});
|
|
839
|
+
logErr(`Warning: projection failed for ${rawAsset.entryId} — ${err instanceof Error ? err.message : String(err)}`);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
// 5a. Optionally fetch and merge Chain-derived rules (--generate flag)
|
|
844
|
+
let chainRulesStats = null;
|
|
845
|
+
let chainGaps = [];
|
|
846
|
+
let allRules = manualRules;
|
|
847
|
+
if (generate) {
|
|
848
|
+
log('Generating Chain-derived rules...');
|
|
849
|
+
const chainResult = await generateChainRules(kernelCall, manualRules);
|
|
850
|
+
if (chainResult.sentinel) {
|
|
851
|
+
// MCP unavailable — inject sentinel rule and warn
|
|
852
|
+
allRules = [...manualRules, chainResult.sentinel];
|
|
853
|
+
logErr('Warning: Chain MCP unavailable — generated rules are disabled. Sentinel rule injected.');
|
|
854
|
+
}
|
|
855
|
+
else {
|
|
856
|
+
// Merge generated rules after manual rules (manual takes precedence on dedup)
|
|
857
|
+
allRules = [...manualRules, ...chainResult.rules];
|
|
858
|
+
chainRulesStats = chainResult.stats;
|
|
859
|
+
chainGaps = chainResult.gaps;
|
|
860
|
+
const { generatedRules, suppressedByManual, suppressedByZeroEntries } = chainResult.stats;
|
|
861
|
+
log(`Chain-derived rules: ${generatedRules} generated, ${suppressedByManual} suppressed by manual, ${suppressedByZeroEntries} gaps`);
|
|
862
|
+
if (chainGaps.length > 0) {
|
|
863
|
+
log(`Gaps (no matching governance entries): ${chainGaps.join(', ')}`);
|
|
864
|
+
}
|
|
865
|
+
// Diff: compare current state against previous run
|
|
866
|
+
const previousState = loadPreviousState(pbDir);
|
|
867
|
+
const currentState = buildCurrentState(chainResult.rules, chainResult.classified);
|
|
868
|
+
saveHandshakeState(pbDir, chainResult.rules, chainResult.classified);
|
|
869
|
+
const diff = diffHandshakeState(currentState, previousState);
|
|
870
|
+
const diffText = formatDiff(diff);
|
|
871
|
+
log(diffText);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
// 5b. Read personal layer (WP-310 E2) — machine-local rules/skills from .productbrain/.local/
|
|
875
|
+
// Returns [] when the directory is absent. All returned entries have persist: 'local'.
|
|
876
|
+
const personalRules = readPersonalLayer(pbDir);
|
|
877
|
+
if (personalRules.length > 0) {
|
|
878
|
+
// Name collision detection — warn when a personal rule overrides a team rule
|
|
879
|
+
const teamRuleNames = new Set(allRules.map((r) => r.name));
|
|
880
|
+
for (const pr of personalRules) {
|
|
881
|
+
if (teamRuleNames.has(pr.name)) {
|
|
882
|
+
logErr(`Personal rule "${pr.name}" overrides team rule (local takes precedence).`);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
// Merge: team rules first, personal rules appended (personal takes precedence via name collision above)
|
|
886
|
+
allRules = [...allRules, ...personalRules];
|
|
887
|
+
}
|
|
888
|
+
const personalSkills = readPersonalSkillsLayer(pbDir);
|
|
889
|
+
if (personalSkills.length > 0) {
|
|
890
|
+
const teamSkillNames = new Set(allSkills.map((s) => s.name));
|
|
891
|
+
for (const ps of personalSkills) {
|
|
892
|
+
if (teamSkillNames.has(ps.name)) {
|
|
893
|
+
logErr(`Personal skill "${ps.name}" overrides team skill (local takes precedence).`);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
allSkills.push(...personalSkills);
|
|
897
|
+
}
|
|
898
|
+
// 5c. Apply manifest-based adoption filter (WP-310 E1)
|
|
899
|
+
// readManifest returns null when manifest.yaml is absent → filterByAdoptionState is a no-op.
|
|
900
|
+
const manifest = readManifest(pbDir);
|
|
901
|
+
// 5d. Load method registry (WP-310 E4) — only when manifest is present.
|
|
902
|
+
let registrySource;
|
|
903
|
+
let registryStale;
|
|
904
|
+
if (manifest) {
|
|
905
|
+
const registryResult = await loadMethodRegistry(manifest.method_source, kernelCall).catch(() => null);
|
|
906
|
+
if (registryResult) {
|
|
907
|
+
registrySource = registryResult.source;
|
|
908
|
+
registryStale = registryResult.stale;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
const adoptionFilteredSkills = filterByAdoptionState(allSkills, manifest);
|
|
912
|
+
const adoptionFilteredRules = filterByAdoptionState(allRules, manifest);
|
|
913
|
+
// Compute adoption counts for report (only meaningful when manifest is present)
|
|
914
|
+
const adoptedRulesCount = manifest ? adoptionFilteredRules.length : undefined;
|
|
915
|
+
const rejectedRulesCount = manifest ? allRules.length - adoptionFilteredRules.length : undefined;
|
|
916
|
+
// Apply level filtering with stage-gating (after adoption filter, before target filtering in write loop)
|
|
917
|
+
// Stage caps the effective level: blank→beginner, seed→intermediate, grounded+→expert.
|
|
918
|
+
// If stage caps below the requested level, log it so the user knows why items were dropped.
|
|
919
|
+
const profileStage = workspaceProfile?.stage;
|
|
920
|
+
const levelFilteredSkills = filterByLevel(adoptionFilteredSkills, level, profileStage);
|
|
921
|
+
const levelFilteredRules = filterByLevel(adoptionFilteredRules, level, profileStage);
|
|
922
|
+
// Log when stage gating changes the effective level
|
|
923
|
+
if (profileStage) {
|
|
924
|
+
const stageCap = STAGE_TO_MAX_LEVEL[profileStage];
|
|
925
|
+
if (stageCap) {
|
|
926
|
+
const requestedIdx = level ? LEVEL_ORDER.indexOf(level) : LEVEL_ORDER.length - 1;
|
|
927
|
+
const capIdx = LEVEL_ORDER.indexOf(stageCap);
|
|
928
|
+
if (capIdx < requestedIdx) {
|
|
929
|
+
log(`Stage "${profileStage}" caps level from ${level || 'expert'} to ${stageCap}`);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
// Apply when-condition filtering (stage-aware, workspace profile + repo context)
|
|
934
|
+
const canonicalSkills = levelFilteredSkills.filter((skill) => {
|
|
935
|
+
const result = evaluateConditions(skill.conditions ?? {}, workspaceProfile, repo);
|
|
936
|
+
if (dryRun && !result.included) {
|
|
937
|
+
log(` EXCLUDED skill ${skill.name}: ${result.reasons.join(', ')}`);
|
|
938
|
+
}
|
|
939
|
+
return result.included;
|
|
940
|
+
});
|
|
941
|
+
const canonicalRules = levelFilteredRules.filter((rule) => {
|
|
942
|
+
const result = evaluateConditions(rule.conditions ?? {}, workspaceProfile, repo);
|
|
943
|
+
if (dryRun && !result.included) {
|
|
944
|
+
log(` EXCLUDED rule ${rule.name}: ${result.reasons.join(', ')}`);
|
|
945
|
+
}
|
|
946
|
+
return result.included;
|
|
947
|
+
});
|
|
948
|
+
if (dryRun && canonicalSkills.length > 0) {
|
|
949
|
+
log(` INCLUDED skills: ${canonicalSkills.map((s) => s.name).join(', ')}`);
|
|
950
|
+
}
|
|
951
|
+
if (dryRun && canonicalRules.length > 0) {
|
|
952
|
+
log(` INCLUDED rules: ${canonicalRules.map((r) => r.name).join(', ')}`);
|
|
953
|
+
}
|
|
954
|
+
if (canonicalSkills.length > 0 || canonicalRules.length > 0) {
|
|
955
|
+
const levelSuffix = level ? ` (level: ${level})` : '';
|
|
956
|
+
const stageSuffix = profileStage ? `, stage: ${profileStage}` : '';
|
|
957
|
+
const stackSuffix = repo.detectedStack.length > 0 ? `, stack: [${repo.detectedStack.join(', ')}]` : '';
|
|
958
|
+
const totalSkills = allSkills.length;
|
|
959
|
+
const totalRules = allRules.length;
|
|
960
|
+
log(`Portable knowledge: ${canonicalSkills.length}/${totalSkills} skills, ${canonicalRules.length}/${totalRules} rules${levelSuffix}${stageSuffix}${stackSuffix}`);
|
|
961
|
+
}
|
|
962
|
+
// 6. Generate file contents
|
|
963
|
+
// Build workspace context for AGENTS.md enrichment (stage, focus, governance, entry count)
|
|
964
|
+
const agentsWorkspaceContext = workspaceProfile
|
|
965
|
+
? {
|
|
966
|
+
stage: workspaceProfile.stage,
|
|
967
|
+
focus: orientView?.strategicContext?.currentBet ?? undefined,
|
|
968
|
+
governanceMode: workspaceProfile.governanceMode,
|
|
969
|
+
totalEntries: workspaceProfile.totalEntries,
|
|
970
|
+
}
|
|
971
|
+
: undefined;
|
|
972
|
+
// Collect codex-targeted skills for AGENTS.md skill directory
|
|
973
|
+
// Exclude persist: 'local' rules — committed adapter files must never include local-only rules.
|
|
974
|
+
const agentsCodexSkills = canonicalSkills
|
|
975
|
+
.filter((s) => shouldEmitToTarget(s, 'codex'))
|
|
976
|
+
.map((s) => ({
|
|
977
|
+
name: s.name,
|
|
978
|
+
description: s.description,
|
|
979
|
+
triggers: s.triggers,
|
|
980
|
+
}));
|
|
981
|
+
// Collect copilot-targeted skills for copilot-instructions.md skill summaries
|
|
982
|
+
const copilotSkills = canonicalSkills
|
|
983
|
+
.filter((s) => shouldEmitToTarget(s, 'copilot'))
|
|
984
|
+
.map((s) => ({
|
|
985
|
+
name: s.name,
|
|
986
|
+
description: s.description,
|
|
987
|
+
triggers: s.triggers,
|
|
988
|
+
}));
|
|
989
|
+
// Collect copilot-targeted rules for copilot-instructions.md rule summaries
|
|
990
|
+
const copilotRules = canonicalRules
|
|
991
|
+
.filter((r) => shouldEmitToTarget(r, 'copilot'))
|
|
992
|
+
.map((r) => ({
|
|
993
|
+
name: r.name,
|
|
994
|
+
description: r.description,
|
|
995
|
+
}));
|
|
996
|
+
const copilotProfile = resolveSurfaceProfile('copilot');
|
|
997
|
+
const copilotOptions = {
|
|
998
|
+
profile: copilotProfile,
|
|
999
|
+
workspaceContext: agentsWorkspaceContext,
|
|
1000
|
+
skills: copilotSkills.length > 0 ? copilotSkills : undefined,
|
|
1001
|
+
rules: copilotRules.length > 0 ? copilotRules : undefined,
|
|
1002
|
+
};
|
|
1003
|
+
const contextContent = orientView ? generateContextMd(orientView, repo, timestamp, workspaceProfile?.stage) : null;
|
|
1004
|
+
const briefingContent = generateBriefingMd(matchedEntries, repo, uniqueQueries, timestamp);
|
|
1005
|
+
const agentsContent = generateAgentsMd(timestamp, {
|
|
1006
|
+
workspaceContext: agentsWorkspaceContext,
|
|
1007
|
+
skills: agentsCodexSkills.length > 0 ? agentsCodexSkills : undefined,
|
|
1008
|
+
});
|
|
1009
|
+
const claudeContent = generateClaudeMd(timestamp);
|
|
1010
|
+
const cursorContent = generateCursorMdc(timestamp);
|
|
1011
|
+
const copilotContent = generateCopilotMd(timestamp, copilotOptions);
|
|
1012
|
+
const boundaryEnforcementMode = getBoundaryEnforcementMode(manifest);
|
|
1013
|
+
const boundaryManifestContent = boundaryEnforcementMode === 'advisory'
|
|
1014
|
+
? null
|
|
1015
|
+
: generateBoundaryManifest(pbDir);
|
|
1016
|
+
// 7. Write files
|
|
1017
|
+
const filesWritten = [];
|
|
1018
|
+
const filesSkipped = [];
|
|
1019
|
+
const previewPlan = [];
|
|
1020
|
+
// Surface filtering: skip adapter writes for targets not in the allowed set
|
|
1021
|
+
const allowedTargets = options.surfaces && options.surfaces.length > 0
|
|
1022
|
+
? new Set(options.surfaces)
|
|
1023
|
+
: null; // null = write all
|
|
1024
|
+
const writes = [
|
|
1025
|
+
...(contextContent ? [{ path: join(cwd, '.productbrain', 'context.md'), relative: '.productbrain/context.md', content: contextContent, dirs: join(cwd, '.productbrain'), isAdapter: false }] : []),
|
|
1026
|
+
{ path: join(cwd, '.productbrain', 'briefing.md'), relative: '.productbrain/briefing.md', content: briefingContent, isAdapter: false },
|
|
1027
|
+
{ path: join(cwd, 'AGENTS.md'), relative: 'AGENTS.md', content: agentsContent, isAdapter: true, target: 'codex' },
|
|
1028
|
+
{ path: join(cwd, 'CLAUDE.md'), relative: 'CLAUDE.md', content: claudeContent, isAdapter: true, target: 'claude' },
|
|
1029
|
+
{ path: join(cwd, '.cursor', 'rules', 'chain.mdc'), relative: '.cursor/rules/chain.mdc', content: cursorContent, dirs: join(cwd, '.cursor', 'rules'), isAdapter: true, target: 'cursor' },
|
|
1030
|
+
{ path: join(cwd, '.github', 'copilot-instructions.md'), relative: '.github/copilot-instructions.md', content: copilotContent, dirs: join(cwd, '.github'), isAdapter: true, target: 'copilot' },
|
|
1031
|
+
...(boundaryManifestContent ? [{
|
|
1032
|
+
path: join(cwd, '.productbrain', 'generated', 'boundaries.json'),
|
|
1033
|
+
relative: '.productbrain/generated/boundaries.json',
|
|
1034
|
+
content: boundaryManifestContent,
|
|
1035
|
+
dirs: join(cwd, '.productbrain', 'generated'),
|
|
1036
|
+
isAdapter: false,
|
|
1037
|
+
}] : []),
|
|
1038
|
+
];
|
|
1039
|
+
// Add Cursor skill copies (filtered by target)
|
|
1040
|
+
const cursorProfile = resolveSurfaceProfile('cursor');
|
|
1041
|
+
for (const skill of canonicalSkills) {
|
|
1042
|
+
if (!shouldEmitToTarget(skill, 'cursor'))
|
|
1043
|
+
continue;
|
|
1044
|
+
const dbAssetEntryId = skill.sourcePath.startsWith('db:') ? skill.sourcePath.slice(3) : undefined;
|
|
1045
|
+
const skillDir = join(cwd, '.cursor', 'skills', skill.name);
|
|
1046
|
+
writes.push({
|
|
1047
|
+
path: join(skillDir, 'SKILL.md'),
|
|
1048
|
+
relative: `.cursor/skills/${skill.name}/SKILL.md`,
|
|
1049
|
+
content: generateCursorSkill(skill, cursorProfile),
|
|
1050
|
+
dirs: skillDir,
|
|
1051
|
+
isAdapter: true,
|
|
1052
|
+
target: 'cursor',
|
|
1053
|
+
dbAssetEntryId,
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
// Add Codex skill copies (projected markdown + index)
|
|
1057
|
+
const codexProfile = resolveSurfaceProfile('codex');
|
|
1058
|
+
const codexSkills = canonicalSkills.filter((s) => shouldEmitToTarget(s, 'codex'));
|
|
1059
|
+
for (const skill of codexSkills) {
|
|
1060
|
+
const dbAssetEntryId = skill.sourcePath.startsWith('db:') ? skill.sourcePath.slice(3) : undefined;
|
|
1061
|
+
writes.push({
|
|
1062
|
+
path: join(cwd, '.codex', 'skills', `${skill.name}.md`),
|
|
1063
|
+
relative: `.codex/skills/${skill.name}.md`,
|
|
1064
|
+
content: generateCodexSkill(skill, codexProfile),
|
|
1065
|
+
dirs: join(cwd, '.codex', 'skills'),
|
|
1066
|
+
isAdapter: true,
|
|
1067
|
+
target: 'codex',
|
|
1068
|
+
dbAssetEntryId,
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
writes.push({
|
|
1072
|
+
path: join(cwd, '.codex', 'skills', 'README.md'),
|
|
1073
|
+
relative: '.codex/skills/README.md',
|
|
1074
|
+
content: generateCodexSkillIndex(codexSkills),
|
|
1075
|
+
dirs: join(cwd, '.codex', 'skills'),
|
|
1076
|
+
isAdapter: true,
|
|
1077
|
+
target: 'codex',
|
|
1078
|
+
});
|
|
1079
|
+
// Validate Codex-projected skills for dead references
|
|
1080
|
+
const codexWarnings = validateCodexSkills(codexSkills);
|
|
1081
|
+
// Add Cursor rule copies (filtered by target)
|
|
1082
|
+
for (const rule of canonicalRules) {
|
|
1083
|
+
if (!shouldEmitToTarget(rule, 'cursor'))
|
|
1084
|
+
continue;
|
|
1085
|
+
const dbAssetEntryId = rule.sourcePath.startsWith('db:') ? rule.sourcePath.slice(3) : undefined;
|
|
1086
|
+
writes.push({
|
|
1087
|
+
path: join(cwd, '.cursor', 'rules', `${rule.name}.mdc`),
|
|
1088
|
+
relative: `.cursor/rules/${rule.name}.mdc`,
|
|
1089
|
+
content: generateCursorRule(rule, cursorProfile),
|
|
1090
|
+
dirs: join(cwd, '.cursor', 'rules'),
|
|
1091
|
+
isAdapter: true,
|
|
1092
|
+
target: 'cursor',
|
|
1093
|
+
dbAssetEntryId,
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
// Add Claude Code rule copies (filtered by target)
|
|
1097
|
+
const claudeProfile = resolveSurfaceProfile('claude');
|
|
1098
|
+
for (const rule of canonicalRules) {
|
|
1099
|
+
if (!shouldEmitToTarget(rule, 'claude'))
|
|
1100
|
+
continue;
|
|
1101
|
+
const dbAssetEntryId = rule.sourcePath.startsWith('db:') ? rule.sourcePath.slice(3) : undefined;
|
|
1102
|
+
writes.push({
|
|
1103
|
+
path: join(cwd, '.claude', 'rules', `${rule.name}.md`),
|
|
1104
|
+
relative: `.claude/rules/${rule.name}.md`,
|
|
1105
|
+
content: generateClaudeRule(rule, claudeProfile),
|
|
1106
|
+
dirs: join(cwd, '.claude', 'rules'),
|
|
1107
|
+
isAdapter: true,
|
|
1108
|
+
target: 'claude',
|
|
1109
|
+
dbAssetEntryId,
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
// Add Claude Code skill router (filtered by target)
|
|
1113
|
+
const claudeSkills = canonicalSkills.filter((s) => shouldEmitToTarget(s, 'claude'));
|
|
1114
|
+
const skillRouterContent = generateClaudeSkillRouter(claudeSkills, claudeProfile);
|
|
1115
|
+
if (skillRouterContent) {
|
|
1116
|
+
writes.push({
|
|
1117
|
+
path: join(cwd, '.claude', 'rules', 'skill-router.md'),
|
|
1118
|
+
relative: '.claude/rules/skill-router.md',
|
|
1119
|
+
content: skillRouterContent,
|
|
1120
|
+
dirs: join(cwd, '.claude', 'rules'),
|
|
1121
|
+
isAdapter: true,
|
|
1122
|
+
target: 'claude',
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
// 7a. WP-379 S5b: Resolve projection collisions before writing.
|
|
1126
|
+
// In apply mode, enumerate target dirs and unlink any auto-generated files
|
|
1127
|
+
// whose normalized name no longer matches any active asset from the server.
|
|
1128
|
+
// This prevents case-variant orphans from accumulating across handshakes.
|
|
1129
|
+
// Runs only when we have a DB asset list (usedDbSource) — without a DB source,
|
|
1130
|
+
// we can't determine which files are canonical vs. orphan.
|
|
1131
|
+
const collisionTensToFire = [];
|
|
1132
|
+
if (applyMode && usedDbSource) {
|
|
1133
|
+
const activeAssetNames = dbAssetRows
|
|
1134
|
+
.filter((a) => !a.disabledByOwner)
|
|
1135
|
+
.map((a) => a.name);
|
|
1136
|
+
const { collisionTens } = resolveProjectionCollision(cwd, activeAssetNames, log, logErr);
|
|
1137
|
+
collisionTensToFire.push(...collisionTens);
|
|
1138
|
+
}
|
|
1139
|
+
const forkedPaths = [];
|
|
1140
|
+
const projectedHashUpdates = new Map();
|
|
1141
|
+
const recordProjectedHash = (entryId) => {
|
|
1142
|
+
if (!applyMode || !entryId)
|
|
1143
|
+
return;
|
|
1144
|
+
const projection = dbProjectionHashes.get(entryId);
|
|
1145
|
+
if (projection)
|
|
1146
|
+
projectedHashUpdates.set(entryId, projection.hash);
|
|
1147
|
+
};
|
|
1148
|
+
for (const w of writes) {
|
|
1149
|
+
// Surface filtering: skip adapter writes for targets not in the allowed set
|
|
1150
|
+
if (allowedTargets && w.target && !allowedTargets.has(w.target)) {
|
|
1151
|
+
filesSkipped.push({ path: w.relative, reason: `filtered (surface: ${w.target})` });
|
|
1152
|
+
if (preview)
|
|
1153
|
+
previewPlan.push({ path: w.relative, status: 'filtered' });
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1156
|
+
if (w.isAdapter && !shouldWriteAdapter(w.path, force)) {
|
|
1157
|
+
filesSkipped.push({ path: w.relative, reason: 'exists without auto-generated marker (use --force to overwrite)' });
|
|
1158
|
+
if (preview) {
|
|
1159
|
+
previewPlan.push({ path: w.relative, status: 'forked' });
|
|
1160
|
+
}
|
|
1161
|
+
else {
|
|
1162
|
+
forkedPaths.push(w.relative);
|
|
1163
|
+
}
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
if (preview || dryRun) {
|
|
1167
|
+
// In preview/dry-run mode: check content to distinguish new/update/unchanged
|
|
1168
|
+
if (existsSync(w.path)) {
|
|
1169
|
+
const current = readFileSync(w.path, 'utf8');
|
|
1170
|
+
const nextNormalized = normalizeHandshakeContentForComparison(w.content);
|
|
1171
|
+
const currentNormalized = normalizeHandshakeContentForComparison(current);
|
|
1172
|
+
if (nextNormalized === currentNormalized) {
|
|
1173
|
+
filesSkipped.push({ path: w.relative, reason: 'unchanged' });
|
|
1174
|
+
if (preview)
|
|
1175
|
+
previewPlan.push({ path: w.relative, status: 'unchanged' });
|
|
1176
|
+
}
|
|
1177
|
+
else {
|
|
1178
|
+
filesWritten.push(w.relative + (dryRun ? ' (dry run)' : ''));
|
|
1179
|
+
if (preview)
|
|
1180
|
+
previewPlan.push({ path: w.relative, status: 'would-update' });
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
else {
|
|
1184
|
+
filesWritten.push(w.relative + (dryRun ? ' (dry run)' : ''));
|
|
1185
|
+
if (preview)
|
|
1186
|
+
previewPlan.push({ path: w.relative, status: 'would-write' });
|
|
1187
|
+
}
|
|
1188
|
+
continue;
|
|
1189
|
+
}
|
|
1190
|
+
if (w.dirs)
|
|
1191
|
+
mkdirSync(w.dirs, { recursive: true });
|
|
1192
|
+
if (existsSync(w.path)) {
|
|
1193
|
+
const current = readFileSync(w.path, 'utf8');
|
|
1194
|
+
const nextNormalized = normalizeHandshakeContentForComparison(w.content);
|
|
1195
|
+
const currentNormalized = normalizeHandshakeContentForComparison(current);
|
|
1196
|
+
if (nextNormalized === currentNormalized) {
|
|
1197
|
+
filesSkipped.push({ path: w.relative, reason: 'unchanged' });
|
|
1198
|
+
recordProjectedHash(w.dbAssetEntryId);
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
writeFileSync(w.path, w.content);
|
|
1203
|
+
filesWritten.push(w.relative);
|
|
1204
|
+
recordProjectedHash(w.dbAssetEntryId);
|
|
1205
|
+
}
|
|
1206
|
+
if (projectedHashUpdates.size > 0) {
|
|
1207
|
+
const updates = [...projectedHashUpdates];
|
|
1208
|
+
const results = await Promise.allSettled(updates.map(([entryId, hash]) => kernelCall('setup.updateLastProjectedHash', { entryId, hash })));
|
|
1209
|
+
results.forEach((result, index) => {
|
|
1210
|
+
if (result.status === 'rejected') {
|
|
1211
|
+
const [entryId] = updates[index];
|
|
1212
|
+
trackEvent('skill.projection.failed', {
|
|
1213
|
+
entryId,
|
|
1214
|
+
reason: result.reason instanceof Error ? result.reason.message : String(result.reason),
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
// 8a. Dormant marker writes (WP-379 S4) — apply mode only.
|
|
1220
|
+
// For each dormant asset (gate-filtered by the server), locate any previously-projected
|
|
1221
|
+
// on-disk files and append the DORMANT_MARKER trailer. Files are NOT deleted.
|
|
1222
|
+
//
|
|
1223
|
+
// Drift TEN exclusion: dormant-marked files have the auto-gen MARKER, so
|
|
1224
|
+
// shouldWriteAdapter() returns true for them. However, because the asset is dormant,
|
|
1225
|
+
// it is NOT in the `writes` array — it was never queued for a fresh write. Therefore,
|
|
1226
|
+
// dormant files will never appear in forkedPaths (forkedPaths only catches files that
|
|
1227
|
+
// ARE in the writes array but fail shouldWriteAdapter). The dormant marker write is a
|
|
1228
|
+
// separate, independent pass that runs BEFORE the drift TEN check — intentionally
|
|
1229
|
+
// after the main write loop to avoid interfering with active asset writes.
|
|
1230
|
+
//
|
|
1231
|
+
// Fail-open: if a dormant marker write fails, log and continue. Never crash the handshake.
|
|
1232
|
+
const dormantMarkedPaths = [];
|
|
1233
|
+
if (applyMode && dormantDbAssetRows.length > 0) {
|
|
1234
|
+
for (const dormantAsset of dormantDbAssetRows) {
|
|
1235
|
+
const candidatePaths = deriveDormantFilePaths(dormantAsset, cwd);
|
|
1236
|
+
for (const filePath of candidatePaths) {
|
|
1237
|
+
try {
|
|
1238
|
+
const markerResult = writeDormantMarkerToFile(filePath);
|
|
1239
|
+
if (markerResult === 'written') {
|
|
1240
|
+
dormantMarkedPaths.push(filePath);
|
|
1241
|
+
log(`Dormant marker written: ${filePath}`);
|
|
1242
|
+
}
|
|
1243
|
+
// 'already-dormant' and 'skipped' are silent no-ops — idempotent.
|
|
1244
|
+
}
|
|
1245
|
+
catch (err) {
|
|
1246
|
+
// Fail-open: dormant marker write is advisory. Log, never throw.
|
|
1247
|
+
logErr(`Warning: could not write dormant marker to ${filePath} — ${err instanceof Error ? err.message : String(err)}`);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
// 8. Drift logging — if apply mode encountered forked adapters and a session is active, log a draft TEN.
|
|
1253
|
+
// Dormant-marked files are NOT forked — they were intentionally deactivated and have the auto-gen MARKER.
|
|
1254
|
+
// They will never appear in forkedPaths because they are excluded from the `writes` array entirely.
|
|
1255
|
+
//
|
|
1256
|
+
// WP-379 S5b — First-run UX rule:
|
|
1257
|
+
// When `hasAnyReceipt` is false OR undefined (unknown / pre-S5b server), drift TENs are suppressed.
|
|
1258
|
+
// Rationale: on the first handshake, users may have pre-existing non-marked files from manual
|
|
1259
|
+
// setup; we must not flood them with TENs before they've even had one successful materialization.
|
|
1260
|
+
// A TEN fires only after setup_receipt.count >= 1 for this workspace.
|
|
1261
|
+
//
|
|
1262
|
+
// Conservative unknown-treatment: if the server does not provide hasAnyReceipt (old server),
|
|
1263
|
+
// we treat it as "no receipt exists" and suppress the TEN. The day-1 experience is more important
|
|
1264
|
+
// than catching every early drift case; the TEN will fire on the next run once the server is updated.
|
|
1265
|
+
const isFirstRun = hasAnyReceipt !== true; // true when no receipts or unknown
|
|
1266
|
+
if (forkedPaths.length > 0) {
|
|
1267
|
+
if (isFirstRun) {
|
|
1268
|
+
log(`Info: ${forkedPaths.length} adapter(s) skipped (user files without auto-gen marker). ` +
|
|
1269
|
+
'Drift TEN suppressed — first run (no setup receipt yet). Files: ' +
|
|
1270
|
+
forkedPaths.join(', '));
|
|
1271
|
+
}
|
|
1272
|
+
else {
|
|
1273
|
+
const session = readSession();
|
|
1274
|
+
if (session) {
|
|
1275
|
+
const names = forkedPaths.join(', ');
|
|
1276
|
+
kernelCallWithSession('chain.createEntry', {
|
|
1277
|
+
collectionSlug: 'tensions',
|
|
1278
|
+
name: `TEN: handshake drift — ${forkedPaths.length} adapter(s) forked, sync blocked`,
|
|
1279
|
+
status: 'draft',
|
|
1280
|
+
data: {
|
|
1281
|
+
description: `pb handshake --apply encountered forked adapters that blocked sync. Files: ${names}. Use --force to overwrite or resolve drift manually.`,
|
|
1282
|
+
},
|
|
1283
|
+
sessionId: session.sessionId,
|
|
1284
|
+
createdBy: `agent:${session.sessionId}`,
|
|
1285
|
+
}).catch(() => { });
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
// 8. Case-collision TENs (WP-379 S5b) — fire after the first-run gate.
|
|
1290
|
+
// These are distinct from drift TENs: they record ambiguous filename collisions
|
|
1291
|
+
// where the "newest mtime wins" heuristic was applied. They fire regardless of
|
|
1292
|
+
// first-run status (collision is a data quality issue, not a drift issue).
|
|
1293
|
+
if (collisionTensToFire.length > 0) {
|
|
1294
|
+
const session = readSession();
|
|
1295
|
+
if (session) {
|
|
1296
|
+
for (const tenDescription of collisionTensToFire) {
|
|
1297
|
+
kernelCallWithSession('chain.createEntry', {
|
|
1298
|
+
collectionSlug: 'tensions',
|
|
1299
|
+
name: `TEN: handshake case-collision — ambiguous filename resolved by mtime`,
|
|
1300
|
+
status: 'draft',
|
|
1301
|
+
data: { description: tenDescription },
|
|
1302
|
+
sessionId: session.sessionId,
|
|
1303
|
+
createdBy: `agent:${session.sessionId}`,
|
|
1304
|
+
}).catch(() => { });
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
// 8b. Setup receipt — record which assets were materialized (apply mode only)
|
|
1309
|
+
// Fail-open: receipt write is advisory, never blocks the handshake.
|
|
1310
|
+
if (applyMode) {
|
|
1311
|
+
const session = readSession();
|
|
1312
|
+
const caller = session ? kernelCallWithSession : kernelCall;
|
|
1313
|
+
try {
|
|
1314
|
+
const receiptResult = await caller('setup.materializeSetup', {});
|
|
1315
|
+
if (receiptResult?.assetCount > 0) {
|
|
1316
|
+
log(`Setup receipt: ${receiptResult.assetCount} asset(s) recorded.`);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
catch (err) {
|
|
1320
|
+
trackEvent('setup.receipt.write_failed', { error: err instanceof Error ? err.message : String(err) });
|
|
1321
|
+
logErr(`Warning: could not write setup receipt — ${err instanceof Error ? err.message : String(err)}`);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
// 9. Report
|
|
1325
|
+
const report = {
|
|
1326
|
+
filesWritten,
|
|
1327
|
+
filesSkipped,
|
|
1328
|
+
matchedEntries,
|
|
1329
|
+
searchQueries: uniqueQueries,
|
|
1330
|
+
repo,
|
|
1331
|
+
codexWarnings: codexWarnings.length > 0 ? codexWarnings : undefined,
|
|
1332
|
+
chainRulesStats: chainRulesStats ?? undefined,
|
|
1333
|
+
chainGaps: chainGaps.length > 0 ? chainGaps : undefined,
|
|
1334
|
+
adoptedCount: adoptedRulesCount,
|
|
1335
|
+
rejectedCount: rejectedRulesCount,
|
|
1336
|
+
personalRuleCount: personalRules.length > 0 ? personalRules.length : undefined,
|
|
1337
|
+
personalSkillCount: personalSkills.length > 0 ? personalSkills.length : undefined,
|
|
1338
|
+
registrySource,
|
|
1339
|
+
registryStale,
|
|
1340
|
+
preview: preview ? true : undefined,
|
|
1341
|
+
previewPlan: preview && previewPlan.length > 0 ? previewPlan : undefined,
|
|
1342
|
+
driftConflicts: forkedPaths.length > 0 ? forkedPaths : undefined,
|
|
1343
|
+
};
|
|
1344
|
+
if (!quiet) {
|
|
1345
|
+
process.stdout.write('\n');
|
|
1346
|
+
process.stdout.write(formatHandshakeReport(report) + '\n');
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
//# sourceMappingURL=handshake.js.map
|