@skillsmith/core 0.8.2 → 0.10.0
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/CHANGELOG.md +17 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/src/api/client.d.ts.map +1 -1
- package/dist/src/api/client.js +6 -0
- package/dist/src/api/client.js.map +1 -1
- package/dist/src/api/client.test.d.ts +9 -0
- package/dist/src/api/client.test.d.ts.map +1 -0
- package/dist/src/api/client.test.js +81 -0
- package/dist/src/api/client.test.js.map +1 -0
- package/dist/src/config/device-identity.d.ts +113 -0
- package/dist/src/config/device-identity.d.ts.map +1 -0
- package/dist/src/config/device-identity.js +171 -0
- package/dist/src/config/device-identity.js.map +1 -0
- package/dist/src/config/device-identity.test.d.ts +10 -0
- package/dist/src/config/device-identity.test.d.ts.map +1 -0
- package/dist/src/config/device-identity.test.js +213 -0
- package/dist/src/config/device-identity.test.js.map +1 -0
- package/dist/src/config/index.d.ts +9 -0
- package/dist/src/config/index.d.ts.map +1 -1
- package/dist/src/config/index.js.map +1 -1
- package/dist/src/exports/services.d.ts +3 -1
- package/dist/src/exports/services.d.ts.map +1 -1
- package/dist/src/exports/services.js +11 -1
- package/dist/src/exports/services.js.map +1 -1
- package/dist/src/index.d.ts +4 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/install/agent-config-merge.json-array.d.ts +29 -0
- package/dist/src/install/agent-config-merge.json-array.d.ts.map +1 -0
- package/dist/src/install/agent-config-merge.json-array.js +100 -0
- package/dist/src/install/agent-config-merge.json-array.js.map +1 -0
- package/dist/src/install/agent-config-merge.json.d.ts +37 -0
- package/dist/src/install/agent-config-merge.json.d.ts.map +1 -0
- package/dist/src/install/agent-config-merge.json.js +134 -0
- package/dist/src/install/agent-config-merge.json.js.map +1 -0
- package/dist/src/install/agent-config-merge.toml-block.d.ts +48 -0
- package/dist/src/install/agent-config-merge.toml-block.d.ts.map +1 -0
- package/dist/src/install/agent-config-merge.toml-block.js +107 -0
- package/dist/src/install/agent-config-merge.toml-block.js.map +1 -0
- package/dist/src/install/agent-config-merge.types.d.ts +87 -0
- package/dist/src/install/agent-config-merge.types.d.ts.map +1 -0
- package/dist/src/install/agent-config-merge.types.js +83 -0
- package/dist/src/install/agent-config-merge.types.js.map +1 -0
- package/dist/src/install/agent-config-merge.yaml.d.ts +46 -0
- package/dist/src/install/agent-config-merge.yaml.d.ts.map +1 -0
- package/dist/src/install/agent-config-merge.yaml.js +133 -0
- package/dist/src/install/agent-config-merge.yaml.js.map +1 -0
- package/dist/src/install/agent-config-merge.yaml.test.d.ts +2 -0
- package/dist/src/install/agent-config-merge.yaml.test.d.ts.map +1 -0
- package/dist/src/install/agent-config-merge.yaml.test.js +210 -0
- package/dist/src/install/agent-config-merge.yaml.test.js.map +1 -0
- package/dist/src/install/agent-harness-targets.d.ts +98 -0
- package/dist/src/install/agent-harness-targets.d.ts.map +1 -0
- package/dist/src/install/agent-harness-targets.js +154 -0
- package/dist/src/install/agent-harness-targets.js.map +1 -0
- package/dist/src/install/agent-home-relocate.d.ts +29 -0
- package/dist/src/install/agent-home-relocate.d.ts.map +1 -0
- package/dist/src/install/agent-home-relocate.js +38 -0
- package/dist/src/install/agent-home-relocate.js.map +1 -0
- package/dist/src/install/agent-manifest-path-guard.d.ts +55 -0
- package/dist/src/install/agent-manifest-path-guard.d.ts.map +1 -0
- package/dist/src/install/agent-manifest-path-guard.js +109 -0
- package/dist/src/install/agent-manifest-path-guard.js.map +1 -0
- package/dist/src/install/agent-manifest-path-guard.test.d.ts +2 -0
- package/dist/src/install/agent-manifest-path-guard.test.d.ts.map +1 -0
- package/dist/src/install/agent-manifest-path-guard.test.js +72 -0
- package/dist/src/install/agent-manifest-path-guard.test.js.map +1 -0
- package/dist/src/install/agent-manifest.d.ts +58 -0
- package/dist/src/install/agent-manifest.d.ts.map +1 -0
- package/dist/src/install/agent-manifest.js +103 -0
- package/dist/src/install/agent-manifest.js.map +1 -0
- package/dist/src/install/agent-pack-installer.d.ts +39 -0
- package/dist/src/install/agent-pack-installer.d.ts.map +1 -0
- package/dist/src/install/agent-pack-installer.entry.d.ts +55 -0
- package/dist/src/install/agent-pack-installer.entry.d.ts.map +1 -0
- package/dist/src/install/agent-pack-installer.entry.js +90 -0
- package/dist/src/install/agent-pack-installer.entry.js.map +1 -0
- package/dist/src/install/agent-pack-installer.fs-helpers.d.ts +33 -0
- package/dist/src/install/agent-pack-installer.fs-helpers.d.ts.map +1 -0
- package/dist/src/install/agent-pack-installer.fs-helpers.js +59 -0
- package/dist/src/install/agent-pack-installer.fs-helpers.js.map +1 -0
- package/dist/src/install/agent-pack-installer.harness.d.ts +66 -0
- package/dist/src/install/agent-pack-installer.harness.d.ts.map +1 -0
- package/dist/src/install/agent-pack-installer.harness.js +310 -0
- package/dist/src/install/agent-pack-installer.harness.js.map +1 -0
- package/dist/src/install/agent-pack-installer.js +221 -0
- package/dist/src/install/agent-pack-installer.js.map +1 -0
- package/dist/src/install/agent-pack-installer.preserve.test.d.ts +11 -0
- package/dist/src/install/agent-pack-installer.preserve.test.d.ts.map +1 -0
- package/dist/src/install/agent-pack-installer.preserve.test.js +72 -0
- package/dist/src/install/agent-pack-installer.preserve.test.js.map +1 -0
- package/dist/src/install/agent-pack-installer.test.d.ts +2 -0
- package/dist/src/install/agent-pack-installer.test.d.ts.map +1 -0
- package/dist/src/install/agent-pack-installer.test.js +350 -0
- package/dist/src/install/agent-pack-installer.test.js.map +1 -0
- package/dist/src/install/agent-pack-installer.types.d.ts +51 -0
- package/dist/src/install/agent-pack-installer.types.d.ts.map +1 -0
- package/dist/src/install/agent-pack-installer.types.js +16 -0
- package/dist/src/install/agent-pack-installer.types.js.map +1 -0
- package/dist/src/install/agent-pack-uninstaller.d.ts +43 -0
- package/dist/src/install/agent-pack-uninstaller.d.ts.map +1 -0
- package/dist/src/install/agent-pack-uninstaller.js +113 -0
- package/dist/src/install/agent-pack-uninstaller.js.map +1 -0
- package/dist/src/install/agent-pack-uninstaller.test.d.ts +2 -0
- package/dist/src/install/agent-pack-uninstaller.test.d.ts.map +1 -0
- package/dist/src/install/agent-pack-uninstaller.test.js +212 -0
- package/dist/src/install/agent-pack-uninstaller.test.js.map +1 -0
- package/dist/src/install/index.d.ts +8 -1
- package/dist/src/install/index.d.ts.map +1 -1
- package/dist/src/install/index.js +8 -1
- package/dist/src/install/index.js.map +1 -1
- package/dist/src/install/paths.d.ts +17 -1
- package/dist/src/install/paths.d.ts.map +1 -1
- package/dist/src/install/paths.js +26 -0
- package/dist/src/install/paths.js.map +1 -1
- package/dist/src/install/paths.test.d.ts +2 -0
- package/dist/src/install/paths.test.d.ts.map +1 -0
- package/dist/src/install/paths.test.js +76 -0
- package/dist/src/install/paths.test.js.map +1 -0
- package/dist/src/journal/hash.d.ts +13 -0
- package/dist/src/journal/hash.d.ts.map +1 -0
- package/dist/src/journal/hash.js +16 -0
- package/dist/src/journal/hash.js.map +1 -0
- package/dist/src/journal/index.d.ts +15 -0
- package/dist/src/journal/index.d.ts.map +1 -0
- package/dist/src/journal/index.js +15 -0
- package/dist/src/journal/index.js.map +1 -0
- package/dist/src/journal/journal.test.d.ts +16 -0
- package/dist/src/journal/journal.test.d.ts.map +1 -0
- package/dist/src/journal/journal.test.js +171 -0
- package/dist/src/journal/journal.test.js.map +1 -0
- package/dist/src/journal/path.d.ts +44 -0
- package/dist/src/journal/path.d.ts.map +1 -0
- package/dist/src/journal/path.js +61 -0
- package/dist/src/journal/path.js.map +1 -0
- package/dist/src/journal/reader.d.ts +38 -0
- package/dist/src/journal/reader.d.ts.map +1 -0
- package/dist/src/journal/reader.js +93 -0
- package/dist/src/journal/reader.js.map +1 -0
- package/dist/src/journal/types.d.ts +87 -0
- package/dist/src/journal/types.d.ts.map +1 -0
- package/dist/src/journal/types.js +21 -0
- package/dist/src/journal/types.js.map +1 -0
- package/dist/src/journal/writer.d.ts +52 -0
- package/dist/src/journal/writer.d.ts.map +1 -0
- package/dist/src/journal/writer.js +125 -0
- package/dist/src/journal/writer.js.map +1 -0
- package/dist/src/paywall-triggers/index.d.ts +13 -0
- package/dist/src/paywall-triggers/index.d.ts.map +1 -0
- package/dist/src/paywall-triggers/index.js +13 -0
- package/dist/src/paywall-triggers/index.js.map +1 -0
- package/dist/src/paywall-triggers/path.d.ts +47 -0
- package/dist/src/paywall-triggers/path.d.ts.map +1 -0
- package/dist/src/paywall-triggers/path.js +73 -0
- package/dist/src/paywall-triggers/path.js.map +1 -0
- package/dist/src/paywall-triggers/store.d.ts +75 -0
- package/dist/src/paywall-triggers/store.d.ts.map +1 -0
- package/dist/src/paywall-triggers/store.js +122 -0
- package/dist/src/paywall-triggers/store.js.map +1 -0
- package/dist/src/paywall-triggers/store.test.d.ts +2 -0
- package/dist/src/paywall-triggers/store.test.d.ts.map +1 -0
- package/dist/src/paywall-triggers/store.test.js +121 -0
- package/dist/src/paywall-triggers/store.test.js.map +1 -0
- package/dist/src/paywall-triggers/types.d.ts +24 -0
- package/dist/src/paywall-triggers/types.d.ts.map +1 -0
- package/dist/src/paywall-triggers/types.js +13 -0
- package/dist/src/paywall-triggers/types.js.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.compound.d.ts +13 -0
- package/dist/src/security/scanner/SecurityScanner.compound.d.ts.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.compound.js +127 -0
- package/dist/src/security/scanner/SecurityScanner.compound.js.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.d.ts.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.js +10 -1
- package/dist/src/security/scanner/SecurityScanner.js.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.pii.d.ts +23 -0
- package/dist/src/security/scanner/SecurityScanner.pii.d.ts.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.pii.js +152 -0
- package/dist/src/security/scanner/SecurityScanner.pii.js.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.scanners.d.ts +5 -2
- package/dist/src/security/scanner/SecurityScanner.scanners.d.ts.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.scanners.js +63 -70
- package/dist/src/security/scanner/SecurityScanner.scanners.js.map +1 -1
- package/dist/src/security/scanner/patterns.d.ts +2 -0
- package/dist/src/security/scanner/patterns.d.ts.map +1 -1
- package/dist/src/security/scanner/patterns.js +79 -11
- package/dist/src/security/scanner/patterns.js.map +1 -1
- package/dist/src/security/scanner/types.d.ts +2 -0
- package/dist/src/security/scanner/types.d.ts.map +1 -1
- package/dist/src/services/agent-pack/agent-pack.test.d.ts +18 -0
- package/dist/src/services/agent-pack/agent-pack.test.d.ts.map +1 -0
- package/dist/src/services/agent-pack/agent-pack.test.js +344 -0
- package/dist/src/services/agent-pack/agent-pack.test.js.map +1 -0
- package/dist/src/services/agent-pack/hooks.d.ts +29 -0
- package/dist/src/services/agent-pack/hooks.d.ts.map +1 -0
- package/dist/src/services/agent-pack/hooks.js +139 -0
- package/dist/src/services/agent-pack/hooks.js.map +1 -0
- package/dist/src/services/agent-pack/index.d.ts +26 -0
- package/dist/src/services/agent-pack/index.d.ts.map +1 -0
- package/dist/src/services/agent-pack/index.js +109 -0
- package/dist/src/services/agent-pack/index.js.map +1 -0
- package/dist/src/services/agent-pack/prompt-source.d.ts +47 -0
- package/dist/src/services/agent-pack/prompt-source.d.ts.map +1 -0
- package/dist/src/services/agent-pack/prompt-source.js +182 -0
- package/dist/src/services/agent-pack/prompt-source.js.map +1 -0
- package/dist/src/services/agent-pack/shims.d.ts +40 -0
- package/dist/src/services/agent-pack/shims.d.ts.map +1 -0
- package/dist/src/services/agent-pack/shims.js +96 -0
- package/dist/src/services/agent-pack/shims.js.map +1 -0
- package/dist/src/services/agent-pack/skill-md.d.ts +35 -0
- package/dist/src/services/agent-pack/skill-md.d.ts.map +1 -0
- package/dist/src/services/agent-pack/skill-md.js +89 -0
- package/dist/src/services/agent-pack/skill-md.js.map +1 -0
- package/dist/src/services/agent-pack/types.d.ts +118 -0
- package/dist/src/services/agent-pack/types.d.ts.map +1 -0
- package/dist/src/services/agent-pack/types.js +61 -0
- package/dist/src/services/agent-pack/types.js.map +1 -0
- package/dist/src/services/agent-tool-profile.d.ts +59 -0
- package/dist/src/services/agent-tool-profile.d.ts.map +1 -0
- package/dist/src/services/agent-tool-profile.js +76 -0
- package/dist/src/services/agent-tool-profile.js.map +1 -0
- package/dist/src/services/agent-tool-profile.test.d.ts +2 -0
- package/dist/src/services/agent-tool-profile.test.d.ts.map +1 -0
- package/dist/src/services/agent-tool-profile.test.js +28 -0
- package/dist/src/services/agent-tool-profile.test.js.map +1 -0
- package/dist/src/services/bundled-sibling-scan.d.ts +111 -0
- package/dist/src/services/bundled-sibling-scan.d.ts.map +1 -0
- package/dist/src/services/bundled-sibling-scan.js +170 -0
- package/dist/src/services/bundled-sibling-scan.js.map +1 -0
- package/dist/src/services/skill-installation.io.d.ts +16 -5
- package/dist/src/services/skill-installation.io.d.ts.map +1 -1
- package/dist/src/services/skill-installation.io.js +44 -20
- package/dist/src/services/skill-installation.io.js.map +1 -1
- package/dist/src/services/skill-installation.policy.d.ts +96 -0
- package/dist/src/services/skill-installation.policy.d.ts.map +1 -0
- package/dist/src/services/skill-installation.policy.js +144 -0
- package/dist/src/services/skill-installation.policy.js.map +1 -0
- package/dist/src/services/skill-installation.service.d.ts.map +1 -1
- package/dist/src/services/skill-installation.service.js +9 -3
- package/dist/src/services/skill-installation.service.js.map +1 -1
- package/dist/src/sources/index.d.ts +1 -0
- package/dist/src/sources/index.d.ts.map +1 -1
- package/dist/src/sources/index.js +4 -0
- package/dist/src/sources/index.js.map +1 -1
- package/dist/src/sync/index.d.ts +6 -0
- package/dist/src/sync/index.d.ts.map +1 -1
- package/dist/src/sync/index.js +8 -0
- package/dist/src/sync/index.js.map +1 -1
- package/dist/src/sync/inventory-builder.d.ts +21 -0
- package/dist/src/sync/inventory-builder.d.ts.map +1 -0
- package/dist/src/sync/inventory-builder.js +26 -0
- package/dist/src/sync/inventory-builder.js.map +1 -0
- package/dist/src/sync/inventory-client.d.ts +80 -0
- package/dist/src/sync/inventory-client.d.ts.map +1 -0
- package/dist/src/sync/inventory-client.js +204 -0
- package/dist/src/sync/inventory-client.js.map +1 -0
- package/dist/src/sync/inventory-client.test.d.ts +14 -0
- package/dist/src/sync/inventory-client.test.d.ts.map +1 -0
- package/dist/src/sync/inventory-client.test.js +212 -0
- package/dist/src/sync/inventory-client.test.js.map +1 -0
- package/dist/src/sync/inventory-collector.d.ts +35 -0
- package/dist/src/sync/inventory-collector.d.ts.map +1 -0
- package/dist/src/sync/inventory-collector.js +176 -0
- package/dist/src/sync/inventory-collector.js.map +1 -0
- package/dist/src/sync/inventory-collector.test.d.ts +14 -0
- package/dist/src/sync/inventory-collector.test.d.ts.map +1 -0
- package/dist/src/sync/inventory-collector.test.js +156 -0
- package/dist/src/sync/inventory-collector.test.js.map +1 -0
- package/dist/src/sync/inventory-device.d.ts +36 -0
- package/dist/src/sync/inventory-device.d.ts.map +1 -0
- package/dist/src/sync/inventory-device.js +58 -0
- package/dist/src/sync/inventory-device.js.map +1 -0
- package/dist/src/sync/inventory-push.d.ts +45 -0
- package/dist/src/sync/inventory-push.d.ts.map +1 -0
- package/dist/src/sync/inventory-push.js +62 -0
- package/dist/src/sync/inventory-push.js.map +1 -0
- package/dist/src/sync/inventory-push.test.d.ts +16 -0
- package/dist/src/sync/inventory-push.test.d.ts.map +1 -0
- package/dist/src/sync/inventory-push.test.js +104 -0
- package/dist/src/sync/inventory-push.test.js.map +1 -0
- package/dist/src/sync/inventory-types.d.ts +110 -0
- package/dist/src/sync/inventory-types.d.ts.map +1 -0
- package/dist/src/sync/inventory-types.js +45 -0
- package/dist/src/sync/inventory-types.js.map +1 -0
- package/dist/src/telemetry/agent-marker.d.ts +137 -0
- package/dist/src/telemetry/agent-marker.d.ts.map +1 -0
- package/dist/src/telemetry/agent-marker.js +242 -0
- package/dist/src/telemetry/agent-marker.js.map +1 -0
- package/dist/src/telemetry/agent-marker.test.d.ts +13 -0
- package/dist/src/telemetry/agent-marker.test.d.ts.map +1 -0
- package/dist/src/telemetry/agent-marker.test.js +202 -0
- package/dist/src/telemetry/agent-marker.test.js.map +1 -0
- package/dist/src/telemetry/index.d.ts +2 -1
- package/dist/src/telemetry/index.d.ts.map +1 -1
- package/dist/src/telemetry/index.js +6 -1
- package/dist/src/telemetry/index.js.map +1 -1
- package/dist/src/telemetry/posthog.d.ts +10 -0
- package/dist/src/telemetry/posthog.d.ts.map +1 -1
- package/dist/src/telemetry/posthog.js +5 -1
- package/dist/src/telemetry/posthog.js.map +1 -1
- package/dist/src/telemetry/wrap.d.ts +28 -7
- package/dist/src/telemetry/wrap.d.ts.map +1 -1
- package/dist/src/telemetry/wrap.gate.test.d.ts +22 -0
- package/dist/src/telemetry/wrap.gate.test.d.ts.map +1 -0
- package/dist/src/telemetry/wrap.gate.test.js +197 -0
- package/dist/src/telemetry/wrap.gate.test.js.map +1 -0
- package/dist/src/telemetry/wrap.js +111 -26
- package/dist/src/telemetry/wrap.js.map +1 -1
- package/dist/src/telemetry/wrap.marker.test.d.ts +16 -0
- package/dist/src/telemetry/wrap.marker.test.d.ts.map +1 -0
- package/dist/src/telemetry/wrap.marker.test.js +185 -0
- package/dist/src/telemetry/wrap.marker.test.js.map +1 -0
- package/dist/src/telemetry/wrap.test.js +4 -0
- package/dist/src/telemetry/wrap.test.js.map +1 -1
- package/dist/tests/SecurityScanner.exec.test.js +60 -0
- package/dist/tests/SecurityScanner.exec.test.js.map +1 -1
- package/dist/tests/security/ContinuousSecurity.test.js +10 -4
- package/dist/tests/security/ContinuousSecurity.test.js.map +1 -1
- package/dist/tests/security/SecurityScanner.chmod-evasion.test.d.ts +2 -0
- package/dist/tests/security/SecurityScanner.chmod-evasion.test.d.ts.map +1 -0
- package/dist/tests/security/SecurityScanner.chmod-evasion.test.js +126 -0
- package/dist/tests/security/SecurityScanner.chmod-evasion.test.js.map +1 -0
- package/dist/tests/security/chmod-compound.test.d.ts +2 -0
- package/dist/tests/security/chmod-compound.test.d.ts.map +1 -0
- package/dist/tests/security/chmod-compound.test.js +188 -0
- package/dist/tests/security/chmod-compound.test.js.map +1 -0
- package/dist/tests/security/pii-detection.test.js +131 -0
- package/dist/tests/security/pii-detection.test.js.map +1 -1
- package/dist/tests/security/scanner-regression-guard.test.d.ts +23 -7
- package/dist/tests/security/scanner-regression-guard.test.d.ts.map +1 -1
- package/dist/tests/security/scanner-regression-guard.test.js +27 -11
- package/dist/tests/security/scanner-regression-guard.test.js.map +1 -1
- package/dist/tests/security/sensitive-path-fp.test.d.ts +2 -0
- package/dist/tests/security/sensitive-path-fp.test.d.ts.map +1 -0
- package/dist/tests/security/sensitive-path-fp.test.js +142 -0
- package/dist/tests/security/sensitive-path-fp.test.js.map +1 -0
- package/dist/tests/services/bundled-sibling-scan.test.d.ts +2 -0
- package/dist/tests/services/bundled-sibling-scan.test.d.ts.map +1 -0
- package/dist/tests/services/bundled-sibling-scan.test.js +139 -0
- package/dist/tests/services/bundled-sibling-scan.test.js.map +1 -0
- package/dist/tests/unit/services/skill-installation.io.test.js +166 -0
- package/dist/tests/unit/services/skill-installation.io.test.js.map +1 -1
- package/dist/tests/unit/services/skill-installation.policy.test.d.ts +8 -0
- package/dist/tests/unit/services/skill-installation.policy.test.d.ts.map +1 -0
- package/dist/tests/unit/services/skill-installation.policy.test.js +151 -0
- package/dist/tests/unit/services/skill-installation.policy.test.js.map +1 -0
- package/package.json +23 -3
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Change-journal writer — single-writer, fsync'd, hash-chained
|
|
3
|
+
* append (SMI-5456 Wave 1 Step 3 / SMI-5470).
|
|
4
|
+
* @module @skillsmith/core/journal/writer
|
|
5
|
+
*
|
|
6
|
+
* P-5 single-writer invariant (docs/internal/implementation/smi-5456-skillsmith-agent-wave1.md
|
|
7
|
+
* "Shared-State / Coordination Audit"): the journal file is written by the
|
|
8
|
+
* apply-family MCP tools ONLY, and every writer in this process funnels
|
|
9
|
+
* through `appendJournalRecord`, which serializes behind ONE module-scoped
|
|
10
|
+
* promise queue. That queue — not any per-caller locking — is what makes
|
|
11
|
+
* concurrent `apply_namespace_rename` / `apply_recommended_edit` / `undo_apply`
|
|
12
|
+
* calls in the same server process produce well-formed, correctly-chained
|
|
13
|
+
* lines instead of interleaved/torn writes.
|
|
14
|
+
*
|
|
15
|
+
* Durability: each append opens the file, writes the line, calls
|
|
16
|
+
* `FileHandle.sync()` (fsync), then closes — so a record that returns
|
|
17
|
+
* successfully is durable on disk before the caller's apply/undo response
|
|
18
|
+
* goes out, matching the "fsync'd appends" requirement in the plan's P-5
|
|
19
|
+
* table.
|
|
20
|
+
*/
|
|
21
|
+
import { mkdir, open, readFile } from 'node:fs/promises';
|
|
22
|
+
import { dirname } from 'node:path';
|
|
23
|
+
import { JOURNAL_GENESIS_HASH, sha256Hex } from './hash.js';
|
|
24
|
+
import { getJournalFilePath } from './path.js';
|
|
25
|
+
/**
|
|
26
|
+
* Canonical (fixed-key-order) JSON of every hash-chain-relevant field,
|
|
27
|
+
* INCLUDING `prev_hash` but EXCLUDING `record_hash` itself. Plain
|
|
28
|
+
* `JSON.stringify` is sufficient canonicalization here because the object
|
|
29
|
+
* literal below is always constructed with the same key order — a full
|
|
30
|
+
* canonical-JSON library would be overkill for a single fixed shape.
|
|
31
|
+
*/
|
|
32
|
+
function canonicalFields(record) {
|
|
33
|
+
const ordered = {
|
|
34
|
+
schema: record.schema,
|
|
35
|
+
ts: record.ts,
|
|
36
|
+
session_id: record.session_id,
|
|
37
|
+
tool: record.tool,
|
|
38
|
+
action: record.action,
|
|
39
|
+
suggestion_id: record.suggestion_id,
|
|
40
|
+
target_path: record.target_path,
|
|
41
|
+
before_hash: record.before_hash,
|
|
42
|
+
after_hash: record.after_hash,
|
|
43
|
+
approval: record.approval,
|
|
44
|
+
backup_ref: record.backup_ref,
|
|
45
|
+
detail: record.detail,
|
|
46
|
+
prev_hash: record.prev_hash,
|
|
47
|
+
};
|
|
48
|
+
return JSON.stringify(ordered);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Recompute a record's `record_hash` from its other fields. Exported so the
|
|
52
|
+
* reader can independently verify each line without duplicating the
|
|
53
|
+
* canonicalization logic (drift between writer and reader would silently
|
|
54
|
+
* break every chain-verification call).
|
|
55
|
+
*/
|
|
56
|
+
export function computeRecordHash(record) {
|
|
57
|
+
return sha256Hex(canonicalFields(record));
|
|
58
|
+
}
|
|
59
|
+
export function isCorruptTailError(err) {
|
|
60
|
+
return (err instanceof Error &&
|
|
61
|
+
err.kind === 'journal.write.corrupt_tail');
|
|
62
|
+
}
|
|
63
|
+
async function getLastRecordHash(filePath) {
|
|
64
|
+
let raw;
|
|
65
|
+
try {
|
|
66
|
+
raw = await readFile(filePath, 'utf-8');
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return JOURNAL_GENESIS_HASH;
|
|
70
|
+
}
|
|
71
|
+
const lines = raw.split('\n').filter((line) => line.trim().length > 0);
|
|
72
|
+
if (lines.length === 0)
|
|
73
|
+
return JOURNAL_GENESIS_HASH;
|
|
74
|
+
const lastLine = lines[lines.length - 1];
|
|
75
|
+
try {
|
|
76
|
+
const parsed = JSON.parse(lastLine);
|
|
77
|
+
if (typeof parsed.record_hash === 'string' && parsed.record_hash.length > 0) {
|
|
78
|
+
return parsed.record_hash;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// fall through to the corrupt-tail error below
|
|
83
|
+
}
|
|
84
|
+
const err = new Error('journal tail is corrupt or unparseable; refusing to append past an unverifiable chain head');
|
|
85
|
+
err.kind = 'journal.write.corrupt_tail';
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
// P-5 single-writer invariant: every append in this process funnels through
|
|
89
|
+
// this one queue, regardless of which apply-family tool called it.
|
|
90
|
+
let writeQueue = Promise.resolve();
|
|
91
|
+
/**
|
|
92
|
+
* Append a record to the journal. Serialized behind the module-scoped
|
|
93
|
+
* write queue and fsync'd before the returned promise resolves.
|
|
94
|
+
*
|
|
95
|
+
* Callers (the apply-family tools) should treat a rejection as fail-soft —
|
|
96
|
+
* the journal is an evidence trail, not a gate on the user's mutation —
|
|
97
|
+
* and log rather than propagate. See `tools/apply-journal.helpers.ts` in
|
|
98
|
+
* `@skillsmith/mcp-server` for the call-site convention.
|
|
99
|
+
*/
|
|
100
|
+
export function appendJournalRecord(input) {
|
|
101
|
+
const task = writeQueue.then(() => doAppend(input));
|
|
102
|
+
// A rejected append must not wedge the queue for subsequent callers.
|
|
103
|
+
writeQueue = task.then(() => undefined, () => undefined);
|
|
104
|
+
return task;
|
|
105
|
+
}
|
|
106
|
+
async function doAppend(input) {
|
|
107
|
+
const filePath = getJournalFilePath();
|
|
108
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
109
|
+
// Propagates as-is (including `JournalCorruptTailError`) — callers use
|
|
110
|
+
// `isCorruptTailError` to distinguish it from a generic I/O failure.
|
|
111
|
+
const prevHash = await getLastRecordHash(filePath);
|
|
112
|
+
const withoutHash = { ...input, prev_hash: prevHash };
|
|
113
|
+
const record = { ...withoutHash, record_hash: computeRecordHash(withoutHash) };
|
|
114
|
+
const line = JSON.stringify(record) + '\n';
|
|
115
|
+
const handle = await open(filePath, 'a');
|
|
116
|
+
try {
|
|
117
|
+
await handle.appendFile(line, 'utf-8');
|
|
118
|
+
await handle.sync();
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
await handle.close();
|
|
122
|
+
}
|
|
123
|
+
return record;
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=writer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"writer.js","sourceRoot":"","sources":["../../../src/journal/writer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Public barrel for the paywall trigger-state store (SMI-5456
|
|
3
|
+
* Wave 1 Step 5, QD-3). See `store.ts`'s module header for the
|
|
4
|
+
* Wave-1 scope limitation on in-conversation dismissal
|
|
5
|
+
* recording, and `path.ts`'s header for why this module is
|
|
6
|
+
* `paywall-triggers/`, not `triggers/` (a pre-existing,
|
|
7
|
+
* unrelated module owns that name).
|
|
8
|
+
* @module @skillsmith/core/paywall-triggers
|
|
9
|
+
*/
|
|
10
|
+
export { TRIGGER_DISMISSAL_THRESHOLD, TRIGGER_MUTE_DAYS, emptyTriggerState, type TriggerId, type TriggerState, } from './types.js';
|
|
11
|
+
export { TRIGGER_STATE_DIR_ENV_VAR, getNudgeCooldownState, getTriggerStateDirForTests, getTriggerStateFilePath, } from './path.js';
|
|
12
|
+
export { canShowTrigger, isTriggerMuted, loadTriggerState, recordImpression, recordTriggerDismissal, } from './store.js';
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/paywall-triggers/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACL,2BAA2B,EAC3B,iBAAiB,EACjB,iBAAiB,EACjB,KAAK,SAAS,EACd,KAAK,YAAY,GAClB,MAAM,YAAY,CAAA;AAEnB,OAAO,EACL,yBAAyB,EACzB,qBAAqB,EACrB,0BAA0B,EAC1B,uBAAuB,GACxB,MAAM,WAAW,CAAA;AAElB,OAAO,EACL,cAAc,EACd,cAAc,EACd,gBAAgB,EAChB,gBAAgB,EAChB,sBAAsB,GACvB,MAAM,YAAY,CAAA"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Public barrel for the paywall trigger-state store (SMI-5456
|
|
3
|
+
* Wave 1 Step 5, QD-3). See `store.ts`'s module header for the
|
|
4
|
+
* Wave-1 scope limitation on in-conversation dismissal
|
|
5
|
+
* recording, and `path.ts`'s header for why this module is
|
|
6
|
+
* `paywall-triggers/`, not `triggers/` (a pre-existing,
|
|
7
|
+
* unrelated module owns that name).
|
|
8
|
+
* @module @skillsmith/core/paywall-triggers
|
|
9
|
+
*/
|
|
10
|
+
export { TRIGGER_DISMISSAL_THRESHOLD, TRIGGER_MUTE_DAYS, emptyTriggerState, } from './types.js';
|
|
11
|
+
export { TRIGGER_STATE_DIR_ENV_VAR, getNudgeCooldownState, getTriggerStateDirForTests, getTriggerStateFilePath, } from './path.js';
|
|
12
|
+
export { canShowTrigger, isTriggerMuted, loadTriggerState, recordImpression, recordTriggerDismissal, } from './store.js';
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/paywall-triggers/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACL,2BAA2B,EAC3B,iBAAiB,EACjB,iBAAiB,GAGlB,MAAM,YAAY,CAAA;AAEnB,OAAO,EACL,yBAAyB,EACzB,qBAAqB,EACrB,0BAA0B,EAC1B,uBAAuB,GACxB,MAAM,WAAW,CAAA;AAElB,OAAO,EACL,cAAc,EACd,cAAc,EACd,gBAAgB,EAChB,gBAAgB,EAChB,sBAAsB,GACvB,MAAM,YAAY,CAAA"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trigger-state file layout (SMI-5456 Wave 1 Step 5, QD-3).
|
|
3
|
+
*
|
|
4
|
+
* `~/.skillsmith/paywall-triggers/<triggerId>.json` — one file per trigger,
|
|
5
|
+
* mirroring the per-resource-directory convention already used by
|
|
6
|
+
* `journal/path.ts` (`~/.skillsmith/journal/`) and
|
|
7
|
+
* `telemetry/agent-marker.ts` (`~/.skillsmith/agent-markers/`). Named
|
|
8
|
+
* `paywall-triggers` (not `triggers`) both in the module path and the
|
|
9
|
+
* on-disk directory to avoid any collision with the PRE-EXISTING, unrelated
|
|
10
|
+
* `packages/core/src/triggers/` module (`TriggerDetector`/`ContextScorer` —
|
|
11
|
+
* proactive skill-suggestion detection, nothing to do with PRD §8.1's T1-T4
|
|
12
|
+
* paywall triggers).
|
|
13
|
+
*
|
|
14
|
+
* Format compatibility with the SessionStart hook's nudge cooldown
|
|
15
|
+
* (the per-harness `hooks/<harness>/session-start.sh` script, generated by
|
|
16
|
+
* the — locked for this step — `agent-pack/hooks.ts`): that hook writes a
|
|
17
|
+
* PLAIN epoch-seconds integer to
|
|
18
|
+
* `~/.skillsmith/agent-nudge.state` (`SKILLSMITH_AGENT_NUDGE_STATE`
|
|
19
|
+
* override), because it is self-contained POSIX sh with no JSON tooling
|
|
20
|
+
* available. This module's {@link getNudgeCooldownState} reads that EXACT
|
|
21
|
+
* file/format/override so a future consumer (a report, a diagnostic) can
|
|
22
|
+
* inspect the onboarding-nudge cooldown through the same TS surface as the
|
|
23
|
+
* T1-T4 paywall triggers, without asking the hook to change its wire format.
|
|
24
|
+
* The T1-T4 trigger files themselves are independent, JSON-shaped, and own
|
|
25
|
+
* their own directory — see the module header of `store.ts` for why.
|
|
26
|
+
*
|
|
27
|
+
* @module @skillsmith/core/paywall-triggers/path
|
|
28
|
+
*/
|
|
29
|
+
import type { TriggerId } from './types.js';
|
|
30
|
+
/** Env var overriding the trigger-state directory (test isolation). */
|
|
31
|
+
export declare const TRIGGER_STATE_DIR_ENV_VAR = "SKILLSMITH_TRIGGER_STATE_DIR";
|
|
32
|
+
export declare function getTriggerStateFilePath(triggerId: TriggerId): string;
|
|
33
|
+
export declare function getTriggerStateDirForTests(): string;
|
|
34
|
+
/**
|
|
35
|
+
* Read the onboarding SessionStart hook's nudge-cooldown stamp — the SAME
|
|
36
|
+
* file (`~/.skillsmith/agent-nudge.state`, override
|
|
37
|
+
* `SKILLSMITH_AGENT_NUDGE_STATE`) `hooks.ts`'s `renderSessionStartHook`
|
|
38
|
+
* writes a bare epoch-seconds integer into. Read-only: this module never
|
|
39
|
+
* writes this file — that stays the hook's job.
|
|
40
|
+
*
|
|
41
|
+
* @returns the last-nudge epoch-seconds, or null if the file is absent,
|
|
42
|
+
* empty, or not a valid non-negative integer (matches the hook's own
|
|
43
|
+
* tolerant `case "" | *[!0-9]*) last=0` parsing — malformed content is
|
|
44
|
+
* treated as "no prior nudge", never an error).
|
|
45
|
+
*/
|
|
46
|
+
export declare function getNudgeCooldownState(): number | null;
|
|
47
|
+
//# sourceMappingURL=path.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../../../src/paywall-triggers/path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAMH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAE3C,uEAAuE;AACvE,eAAO,MAAM,yBAAyB,iCAAiC,CAAA;AAOvE,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAEpE;AAED,wBAAgB,0BAA0B,IAAI,MAAM,CAEnD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,GAAG,IAAI,CAcrD"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trigger-state file layout (SMI-5456 Wave 1 Step 5, QD-3).
|
|
3
|
+
*
|
|
4
|
+
* `~/.skillsmith/paywall-triggers/<triggerId>.json` — one file per trigger,
|
|
5
|
+
* mirroring the per-resource-directory convention already used by
|
|
6
|
+
* `journal/path.ts` (`~/.skillsmith/journal/`) and
|
|
7
|
+
* `telemetry/agent-marker.ts` (`~/.skillsmith/agent-markers/`). Named
|
|
8
|
+
* `paywall-triggers` (not `triggers`) both in the module path and the
|
|
9
|
+
* on-disk directory to avoid any collision with the PRE-EXISTING, unrelated
|
|
10
|
+
* `packages/core/src/triggers/` module (`TriggerDetector`/`ContextScorer` —
|
|
11
|
+
* proactive skill-suggestion detection, nothing to do with PRD §8.1's T1-T4
|
|
12
|
+
* paywall triggers).
|
|
13
|
+
*
|
|
14
|
+
* Format compatibility with the SessionStart hook's nudge cooldown
|
|
15
|
+
* (the per-harness `hooks/<harness>/session-start.sh` script, generated by
|
|
16
|
+
* the — locked for this step — `agent-pack/hooks.ts`): that hook writes a
|
|
17
|
+
* PLAIN epoch-seconds integer to
|
|
18
|
+
* `~/.skillsmith/agent-nudge.state` (`SKILLSMITH_AGENT_NUDGE_STATE`
|
|
19
|
+
* override), because it is self-contained POSIX sh with no JSON tooling
|
|
20
|
+
* available. This module's {@link getNudgeCooldownState} reads that EXACT
|
|
21
|
+
* file/format/override so a future consumer (a report, a diagnostic) can
|
|
22
|
+
* inspect the onboarding-nudge cooldown through the same TS surface as the
|
|
23
|
+
* T1-T4 paywall triggers, without asking the hook to change its wire format.
|
|
24
|
+
* The T1-T4 trigger files themselves are independent, JSON-shaped, and own
|
|
25
|
+
* their own directory — see the module header of `store.ts` for why.
|
|
26
|
+
*
|
|
27
|
+
* @module @skillsmith/core/paywall-triggers/path
|
|
28
|
+
*/
|
|
29
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
30
|
+
import { join } from 'node:path';
|
|
31
|
+
import { getConfigDir } from '../config/index.js';
|
|
32
|
+
/** Env var overriding the trigger-state directory (test isolation). */
|
|
33
|
+
export const TRIGGER_STATE_DIR_ENV_VAR = 'SKILLSMITH_TRIGGER_STATE_DIR';
|
|
34
|
+
function getTriggerStateDir() {
|
|
35
|
+
const override = process.env[TRIGGER_STATE_DIR_ENV_VAR];
|
|
36
|
+
return override && override.length > 0 ? override : join(getConfigDir(), 'paywall-triggers');
|
|
37
|
+
}
|
|
38
|
+
export function getTriggerStateFilePath(triggerId) {
|
|
39
|
+
return join(getTriggerStateDir(), `${triggerId}.json`);
|
|
40
|
+
}
|
|
41
|
+
export function getTriggerStateDirForTests() {
|
|
42
|
+
return getTriggerStateDir();
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Read the onboarding SessionStart hook's nudge-cooldown stamp — the SAME
|
|
46
|
+
* file (`~/.skillsmith/agent-nudge.state`, override
|
|
47
|
+
* `SKILLSMITH_AGENT_NUDGE_STATE`) `hooks.ts`'s `renderSessionStartHook`
|
|
48
|
+
* writes a bare epoch-seconds integer into. Read-only: this module never
|
|
49
|
+
* writes this file — that stays the hook's job.
|
|
50
|
+
*
|
|
51
|
+
* @returns the last-nudge epoch-seconds, or null if the file is absent,
|
|
52
|
+
* empty, or not a valid non-negative integer (matches the hook's own
|
|
53
|
+
* tolerant `case "" | *[!0-9]*) last=0` parsing — malformed content is
|
|
54
|
+
* treated as "no prior nudge", never an error).
|
|
55
|
+
*/
|
|
56
|
+
export function getNudgeCooldownState() {
|
|
57
|
+
const override = process.env.SKILLSMITH_AGENT_NUDGE_STATE;
|
|
58
|
+
const path = override && override.length > 0 ? override : join(getConfigDir(), 'agent-nudge.state');
|
|
59
|
+
if (!existsSync(path))
|
|
60
|
+
return null;
|
|
61
|
+
let raw;
|
|
62
|
+
try {
|
|
63
|
+
raw = readFileSync(path, 'utf-8').trim();
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
if (!/^[0-9]+$/.test(raw))
|
|
69
|
+
return null;
|
|
70
|
+
const value = Number(raw);
|
|
71
|
+
return Number.isSafeInteger(value) ? value : null;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=path.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path.js","sourceRoot":"","sources":["../../../src/paywall-triggers/path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAClD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEhC,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAGjD,uEAAuE;AACvE,MAAM,CAAC,MAAM,yBAAyB,GAAG,8BAA8B,CAAA;AAEvE,SAAS,kBAAkB;IACzB,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAA;IACvD,OAAO,QAAQ,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,kBAAkB,CAAC,CAAA;AAC9F,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,SAAoB;IAC1D,OAAO,IAAI,CAAC,kBAAkB,EAAE,EAAE,GAAG,SAAS,OAAO,CAAC,CAAA;AACxD,CAAC;AAED,MAAM,UAAU,0BAA0B;IACxC,OAAO,kBAAkB,EAAE,CAAA;AAC7B,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,qBAAqB;IACnC,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAA;IACzD,MAAM,IAAI,GACR,QAAQ,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,mBAAmB,CAAC,CAAA;IACxF,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAA;IAClC,IAAI,GAAW,CAAA;IACf,IAAI,CAAC;QACH,GAAG,GAAG,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAA;IAC1C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;IACD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAA;IACtC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAA;IACzB,OAAO,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA;AACnD,CAAC"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paywall trigger-state store (SMI-5456 Wave 1 Step 5, QD-3).
|
|
3
|
+
*
|
|
4
|
+
* PRD §8.1 defines four paywall triggers (T1 version currency, T2 quota
|
|
5
|
+
* forecast, T3 sharing, T4 security-depth disclosure) with two frequency
|
|
6
|
+
* rules: at most one upgrade prompt per session, and two dismissals of the
|
|
7
|
+
* same trigger suppress it for 30 days. This module persists the state those
|
|
8
|
+
* rules need — per-trigger last-shown session id, dismissal count, and mute
|
|
9
|
+
* expiry — under `~/.skillsmith/paywall-triggers/` (deliberately NOT
|
|
10
|
+
* `~/.skillsmith/triggers/` — see `path.ts`'s header for the pre-existing,
|
|
11
|
+
* unrelated `packages/core/src/triggers/` module this avoids colliding with).
|
|
12
|
+
*
|
|
13
|
+
* WAVE-1 SCOPE LIMITATION (read before wiring this into anything):
|
|
14
|
+
* The trigger RULES live in the pack's prompt wording (PRD §6 change 6) —
|
|
15
|
+
* the agent, running inside whatever harness's model, decides in
|
|
16
|
+
* conversation whether to mention a trigger, and the user's response
|
|
17
|
+
* ("no thanks") is plain conversational text the agent sees, not a
|
|
18
|
+
* structured tool call. There is currently NO MCP tool or CLI command that
|
|
19
|
+
* reports a T1/T2/T4 dismissal from that conversation into this store — no
|
|
20
|
+
* Wave-1 exclusion-9 tool covers it (PRD §10 exclusion 9's only two
|
|
21
|
+
* acknowledged exceptions are the undo tool and the Wave-2 stats surface).
|
|
22
|
+
* That means: {@link recordTriggerDismissal} is real, tested infrastructure
|
|
23
|
+
* with NO live caller in Wave 1. This is intentional staged infrastructure
|
|
24
|
+
* (the same shape the change-journal module shipped in — Step 3, before the
|
|
25
|
+
* apply tools became its first real caller in a later step) — NOT an
|
|
26
|
+
* "unconsumed dismissal-recording pathway" in the house anti-pattern sense,
|
|
27
|
+
* because nothing in this codebase currently calls it as if dismissals were
|
|
28
|
+
* being captured. Do not present in-conversation dismissal tracking as a
|
|
29
|
+
* shipped product behavior (docs, marketing copy, the eval-matrix report)
|
|
30
|
+
* until a real consumer exists. What DOES already work end-to-end in Wave 1:
|
|
31
|
+
* the session-cap half via {@link canShowTrigger}/{@link recordImpression} —
|
|
32
|
+
* though even that has no live caller yet either, since trigger *display*
|
|
33
|
+
* also currently lives entirely in the pack's prompt wording. Both halves
|
|
34
|
+
* are ready for whichever surface (a future tool, or a CLI diagnostic) needs
|
|
35
|
+
* them; the eval-matrix worker (Step 6) should treat "trigger frequency cap
|
|
36
|
+
* enforced server-side" as NOT YET TRUE for the published Tier badges.
|
|
37
|
+
*
|
|
38
|
+
* Single-writer-per-trigger-file note: like the journal (P-5), each trigger
|
|
39
|
+
* file is read-modify-written directly (not append-only, since there's only
|
|
40
|
+
* ever one live state per trigger, not a growing log) — concurrent writers
|
|
41
|
+
* to the SAME trigger id are not expected in Wave 1 (no live caller), so no
|
|
42
|
+
* write-queue serialization is implemented here; add one if/when a consumer
|
|
43
|
+
* with real concurrency (e.g. two parallel MCP requests) lands.
|
|
44
|
+
*
|
|
45
|
+
* @module @skillsmith/core/paywall-triggers/store
|
|
46
|
+
*/
|
|
47
|
+
import { type TriggerId, type TriggerState } from './types.js';
|
|
48
|
+
/** Load a trigger's persisted state, or a fresh empty state if none exists or it is corrupt. */
|
|
49
|
+
export declare function loadTriggerState(triggerId: TriggerId): TriggerState;
|
|
50
|
+
/**
|
|
51
|
+
* Is `triggerId` currently muted (30-day suppression after
|
|
52
|
+
* {@link TRIGGER_DISMISSAL_THRESHOLD} dismissals)?
|
|
53
|
+
*
|
|
54
|
+
* @param now - injectable clock for tests.
|
|
55
|
+
*/
|
|
56
|
+
export declare function isTriggerMuted(state: TriggerState, now?: number): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* P-5 "one prompt per session enforced from persisted state, not memory":
|
|
59
|
+
* true when `triggerId` has NOT already been shown in `sessionId` AND is not
|
|
60
|
+
* currently muted. Concurrent sessions may each independently see `true` and
|
|
61
|
+
* both prompt once — accepted, documented (P-5 table).
|
|
62
|
+
*/
|
|
63
|
+
export declare function canShowTrigger(triggerId: TriggerId, sessionId: string, now?: number): boolean;
|
|
64
|
+
/** Record that `triggerId` was shown in `sessionId` (persisted — the session cap). */
|
|
65
|
+
export declare function recordImpression(triggerId: TriggerId, sessionId: string): TriggerState;
|
|
66
|
+
/**
|
|
67
|
+
* Record a dismissal. On reaching {@link TRIGGER_DISMISSAL_THRESHOLD}
|
|
68
|
+
* dismissals, sets a {@link TRIGGER_MUTE_DAYS}-day mute from `now`.
|
|
69
|
+
*
|
|
70
|
+
* See the module header: no Wave-1 caller reports a real in-conversation
|
|
71
|
+
* dismissal today. This function is correct, tested library code — not an
|
|
72
|
+
* inert stub — for whichever surface becomes that caller.
|
|
73
|
+
*/
|
|
74
|
+
export declare function recordTriggerDismissal(triggerId: TriggerId, now?: number): TriggerState;
|
|
75
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../src/paywall-triggers/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAMH,OAAO,EAIL,KAAK,SAAS,EACd,KAAK,YAAY,EAClB,MAAM,YAAY,CAAA;AAInB,gGAAgG;AAChG,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,SAAS,GAAG,YAAY,CAiBnE;AAQD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,GAAE,MAAmB,GAAG,OAAO,CAErF;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,SAAS,EAAE,SAAS,EACpB,SAAS,EAAE,MAAM,EACjB,GAAG,GAAE,MAAmB,GACvB,OAAO,CAIT;AAED,sFAAsF;AACtF,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,YAAY,CAKtF;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,SAAS,EACpB,GAAG,GAAE,MAAmB,GACvB,YAAY,CAQd"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paywall trigger-state store (SMI-5456 Wave 1 Step 5, QD-3).
|
|
3
|
+
*
|
|
4
|
+
* PRD §8.1 defines four paywall triggers (T1 version currency, T2 quota
|
|
5
|
+
* forecast, T3 sharing, T4 security-depth disclosure) with two frequency
|
|
6
|
+
* rules: at most one upgrade prompt per session, and two dismissals of the
|
|
7
|
+
* same trigger suppress it for 30 days. This module persists the state those
|
|
8
|
+
* rules need — per-trigger last-shown session id, dismissal count, and mute
|
|
9
|
+
* expiry — under `~/.skillsmith/paywall-triggers/` (deliberately NOT
|
|
10
|
+
* `~/.skillsmith/triggers/` — see `path.ts`'s header for the pre-existing,
|
|
11
|
+
* unrelated `packages/core/src/triggers/` module this avoids colliding with).
|
|
12
|
+
*
|
|
13
|
+
* WAVE-1 SCOPE LIMITATION (read before wiring this into anything):
|
|
14
|
+
* The trigger RULES live in the pack's prompt wording (PRD §6 change 6) —
|
|
15
|
+
* the agent, running inside whatever harness's model, decides in
|
|
16
|
+
* conversation whether to mention a trigger, and the user's response
|
|
17
|
+
* ("no thanks") is plain conversational text the agent sees, not a
|
|
18
|
+
* structured tool call. There is currently NO MCP tool or CLI command that
|
|
19
|
+
* reports a T1/T2/T4 dismissal from that conversation into this store — no
|
|
20
|
+
* Wave-1 exclusion-9 tool covers it (PRD §10 exclusion 9's only two
|
|
21
|
+
* acknowledged exceptions are the undo tool and the Wave-2 stats surface).
|
|
22
|
+
* That means: {@link recordTriggerDismissal} is real, tested infrastructure
|
|
23
|
+
* with NO live caller in Wave 1. This is intentional staged infrastructure
|
|
24
|
+
* (the same shape the change-journal module shipped in — Step 3, before the
|
|
25
|
+
* apply tools became its first real caller in a later step) — NOT an
|
|
26
|
+
* "unconsumed dismissal-recording pathway" in the house anti-pattern sense,
|
|
27
|
+
* because nothing in this codebase currently calls it as if dismissals were
|
|
28
|
+
* being captured. Do not present in-conversation dismissal tracking as a
|
|
29
|
+
* shipped product behavior (docs, marketing copy, the eval-matrix report)
|
|
30
|
+
* until a real consumer exists. What DOES already work end-to-end in Wave 1:
|
|
31
|
+
* the session-cap half via {@link canShowTrigger}/{@link recordImpression} —
|
|
32
|
+
* though even that has no live caller yet either, since trigger *display*
|
|
33
|
+
* also currently lives entirely in the pack's prompt wording. Both halves
|
|
34
|
+
* are ready for whichever surface (a future tool, or a CLI diagnostic) needs
|
|
35
|
+
* them; the eval-matrix worker (Step 6) should treat "trigger frequency cap
|
|
36
|
+
* enforced server-side" as NOT YET TRUE for the published Tier badges.
|
|
37
|
+
*
|
|
38
|
+
* Single-writer-per-trigger-file note: like the journal (P-5), each trigger
|
|
39
|
+
* file is read-modify-written directly (not append-only, since there's only
|
|
40
|
+
* ever one live state per trigger, not a growing log) — concurrent writers
|
|
41
|
+
* to the SAME trigger id are not expected in Wave 1 (no live caller), so no
|
|
42
|
+
* write-queue serialization is implemented here; add one if/when a consumer
|
|
43
|
+
* with real concurrency (e.g. two parallel MCP requests) lands.
|
|
44
|
+
*
|
|
45
|
+
* @module @skillsmith/core/paywall-triggers/store
|
|
46
|
+
*/
|
|
47
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
48
|
+
import { dirname } from 'node:path';
|
|
49
|
+
import { getTriggerStateFilePath } from './path.js';
|
|
50
|
+
import { TRIGGER_DISMISSAL_THRESHOLD, TRIGGER_MUTE_DAYS, emptyTriggerState, } from './types.js';
|
|
51
|
+
const MUTE_DURATION_MS = TRIGGER_MUTE_DAYS * 24 * 60 * 60 * 1000;
|
|
52
|
+
/** Load a trigger's persisted state, or a fresh empty state if none exists or it is corrupt. */
|
|
53
|
+
export function loadTriggerState(triggerId) {
|
|
54
|
+
const path = getTriggerStateFilePath(triggerId);
|
|
55
|
+
if (!existsSync(path))
|
|
56
|
+
return emptyTriggerState(triggerId);
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(readFileSync(path, 'utf-8'));
|
|
59
|
+
if (parsed.triggerId !== triggerId)
|
|
60
|
+
return emptyTriggerState(triggerId);
|
|
61
|
+
return {
|
|
62
|
+
schema: 1,
|
|
63
|
+
triggerId,
|
|
64
|
+
lastSessionShown: typeof parsed.lastSessionShown === 'string' ? parsed.lastSessionShown : null,
|
|
65
|
+
dismissalCount: typeof parsed.dismissalCount === 'number' ? parsed.dismissalCount : 0,
|
|
66
|
+
mutedUntil: typeof parsed.mutedUntil === 'number' ? parsed.mutedUntil : null,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return emptyTriggerState(triggerId);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function saveTriggerState(state) {
|
|
74
|
+
const path = getTriggerStateFilePath(state.triggerId);
|
|
75
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
76
|
+
writeFileSync(path, JSON.stringify(state, null, 2) + '\n', { mode: 0o600 });
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Is `triggerId` currently muted (30-day suppression after
|
|
80
|
+
* {@link TRIGGER_DISMISSAL_THRESHOLD} dismissals)?
|
|
81
|
+
*
|
|
82
|
+
* @param now - injectable clock for tests.
|
|
83
|
+
*/
|
|
84
|
+
export function isTriggerMuted(state, now = Date.now()) {
|
|
85
|
+
return state.mutedUntil !== null && now < state.mutedUntil;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* P-5 "one prompt per session enforced from persisted state, not memory":
|
|
89
|
+
* true when `triggerId` has NOT already been shown in `sessionId` AND is not
|
|
90
|
+
* currently muted. Concurrent sessions may each independently see `true` and
|
|
91
|
+
* both prompt once — accepted, documented (P-5 table).
|
|
92
|
+
*/
|
|
93
|
+
export function canShowTrigger(triggerId, sessionId, now = Date.now()) {
|
|
94
|
+
const state = loadTriggerState(triggerId);
|
|
95
|
+
if (isTriggerMuted(state, now))
|
|
96
|
+
return false;
|
|
97
|
+
return state.lastSessionShown !== sessionId;
|
|
98
|
+
}
|
|
99
|
+
/** Record that `triggerId` was shown in `sessionId` (persisted — the session cap). */
|
|
100
|
+
export function recordImpression(triggerId, sessionId) {
|
|
101
|
+
const state = loadTriggerState(triggerId);
|
|
102
|
+
const next = { ...state, lastSessionShown: sessionId };
|
|
103
|
+
saveTriggerState(next);
|
|
104
|
+
return next;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Record a dismissal. On reaching {@link TRIGGER_DISMISSAL_THRESHOLD}
|
|
108
|
+
* dismissals, sets a {@link TRIGGER_MUTE_DAYS}-day mute from `now`.
|
|
109
|
+
*
|
|
110
|
+
* See the module header: no Wave-1 caller reports a real in-conversation
|
|
111
|
+
* dismissal today. This function is correct, tested library code — not an
|
|
112
|
+
* inert stub — for whichever surface becomes that caller.
|
|
113
|
+
*/
|
|
114
|
+
export function recordTriggerDismissal(triggerId, now = Date.now()) {
|
|
115
|
+
const state = loadTriggerState(triggerId);
|
|
116
|
+
const dismissalCount = state.dismissalCount + 1;
|
|
117
|
+
const mutedUntil = dismissalCount >= TRIGGER_DISMISSAL_THRESHOLD ? now + MUTE_DURATION_MS : state.mutedUntil;
|
|
118
|
+
const next = { ...state, dismissalCount, mutedUntil };
|
|
119
|
+
saveTriggerState(next);
|
|
120
|
+
return next;
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../../src/paywall-triggers/store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.test.d.ts","sourceRoot":"","sources":["../../../src/paywall-triggers/store.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for the paywall trigger-state store (SMI-5456 Wave 1
|
|
3
|
+
* Step 5, QD-3). Covers the P-5 "Trigger state" invariant:
|
|
4
|
+
* "One prompt per session enforced from persisted state, not
|
|
5
|
+
* memory" (cap) + "30-day mute expiry" test.
|
|
6
|
+
* @module @skillsmith/core/paywall-triggers/store.test
|
|
7
|
+
*/
|
|
8
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
12
|
+
import { canShowTrigger, getNudgeCooldownState, isTriggerMuted, loadTriggerState, recordImpression, recordTriggerDismissal, TRIGGER_DISMISSAL_THRESHOLD, TRIGGER_MUTE_DAYS, TRIGGER_STATE_DIR_ENV_VAR, } from './index.js';
|
|
13
|
+
let stateDir;
|
|
14
|
+
let prevEnv;
|
|
15
|
+
let prevNudgeEnv;
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
stateDir = mkdtempSync(join(tmpdir(), 'skillsmith-paywall-triggers-'));
|
|
18
|
+
prevEnv = process.env[TRIGGER_STATE_DIR_ENV_VAR];
|
|
19
|
+
process.env[TRIGGER_STATE_DIR_ENV_VAR] = stateDir;
|
|
20
|
+
prevNudgeEnv = process.env.SKILLSMITH_AGENT_NUDGE_STATE;
|
|
21
|
+
});
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
rmSync(stateDir, { recursive: true, force: true });
|
|
24
|
+
if (prevEnv !== undefined)
|
|
25
|
+
process.env[TRIGGER_STATE_DIR_ENV_VAR] = prevEnv;
|
|
26
|
+
else
|
|
27
|
+
delete process.env[TRIGGER_STATE_DIR_ENV_VAR];
|
|
28
|
+
if (prevNudgeEnv !== undefined)
|
|
29
|
+
process.env.SKILLSMITH_AGENT_NUDGE_STATE = prevNudgeEnv;
|
|
30
|
+
else
|
|
31
|
+
delete process.env.SKILLSMITH_AGENT_NUDGE_STATE;
|
|
32
|
+
});
|
|
33
|
+
describe('session cap (persisted, not memory)', () => {
|
|
34
|
+
it('allows showing a trigger the first time in a session', () => {
|
|
35
|
+
expect(canShowTrigger('T1', 'session-a')).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
it('blocks a second impression of the same trigger in the same session', () => {
|
|
38
|
+
recordImpression('T1', 'session-a');
|
|
39
|
+
expect(canShowTrigger('T1', 'session-a')).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
it('the cap is read from persisted state, not in-memory — a fresh loadTriggerState call after recordImpression reflects it', () => {
|
|
42
|
+
recordImpression('T2', 'session-x');
|
|
43
|
+
const state = loadTriggerState('T2');
|
|
44
|
+
expect(state.lastSessionShown).toBe('session-x');
|
|
45
|
+
});
|
|
46
|
+
it('allows the SAME trigger again in a DIFFERENT session (documented: concurrent sessions may each prompt once)', () => {
|
|
47
|
+
recordImpression('T1', 'session-a');
|
|
48
|
+
expect(canShowTrigger('T1', 'session-b')).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
it('caps are independent per trigger id', () => {
|
|
51
|
+
recordImpression('T1', 'session-a');
|
|
52
|
+
expect(canShowTrigger('T3', 'session-a')).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe('30-day mute expiry', () => {
|
|
56
|
+
it('does not mute after one dismissal', () => {
|
|
57
|
+
const state = recordTriggerDismissal('T4');
|
|
58
|
+
expect(state.dismissalCount).toBe(1);
|
|
59
|
+
expect(isTriggerMuted(state)).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
it(`mutes after ${TRIGGER_DISMISSAL_THRESHOLD} dismissals`, () => {
|
|
62
|
+
recordTriggerDismissal('T4');
|
|
63
|
+
const state = recordTriggerDismissal('T4');
|
|
64
|
+
expect(state.dismissalCount).toBe(TRIGGER_DISMISSAL_THRESHOLD);
|
|
65
|
+
expect(isTriggerMuted(state)).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
it('a muted trigger cannot be shown, even in a session it was never shown in', () => {
|
|
68
|
+
recordTriggerDismissal('T1');
|
|
69
|
+
recordTriggerDismissal('T1');
|
|
70
|
+
expect(canShowTrigger('T1', 'brand-new-session')).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
it(`is still muted 1ms before ${TRIGGER_MUTE_DAYS} days elapse`, () => {
|
|
73
|
+
const now = 1_000_000;
|
|
74
|
+
recordTriggerDismissal('T2', now);
|
|
75
|
+
recordTriggerDismissal('T2', now);
|
|
76
|
+
const almostExpired = now + TRIGGER_MUTE_DAYS * 24 * 60 * 60 * 1000 - 1;
|
|
77
|
+
const state = loadTriggerState('T2');
|
|
78
|
+
expect(isTriggerMuted(state, almostExpired)).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
it(`the mute has expired exactly at ${TRIGGER_MUTE_DAYS} days`, () => {
|
|
81
|
+
const now = 1_000_000;
|
|
82
|
+
recordTriggerDismissal('T2', now);
|
|
83
|
+
recordTriggerDismissal('T2', now);
|
|
84
|
+
const exactlyExpired = now + TRIGGER_MUTE_DAYS * 24 * 60 * 60 * 1000;
|
|
85
|
+
const state = loadTriggerState('T2');
|
|
86
|
+
expect(isTriggerMuted(state, exactlyExpired)).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
it('canShowTrigger allows the trigger again once the mute has expired', () => {
|
|
89
|
+
const now = 1_000_000;
|
|
90
|
+
recordTriggerDismissal('T3', now);
|
|
91
|
+
recordTriggerDismissal('T3', now);
|
|
92
|
+
const afterMute = now + TRIGGER_MUTE_DAYS * 24 * 60 * 60 * 1000 + 1;
|
|
93
|
+
expect(canShowTrigger('T3', 'any-session', afterMute)).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
it('dismissal count keeps accumulating past the threshold (never resets)', () => {
|
|
96
|
+
recordTriggerDismissal('T1');
|
|
97
|
+
recordTriggerDismissal('T1');
|
|
98
|
+
const state = recordTriggerDismissal('T1');
|
|
99
|
+
expect(state.dismissalCount).toBe(3);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe('getNudgeCooldownState — format compatibility with the SessionStart hook', () => {
|
|
103
|
+
it('returns null when the nudge-state file does not exist', () => {
|
|
104
|
+
const nudgeFile = join(stateDir, 'agent-nudge.state');
|
|
105
|
+
process.env.SKILLSMITH_AGENT_NUDGE_STATE = nudgeFile;
|
|
106
|
+
expect(getNudgeCooldownState()).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
it('reads a plain epoch-seconds integer written by the hook (no JSON, no wrapper)', () => {
|
|
109
|
+
const nudgeFile = join(stateDir, 'agent-nudge.state');
|
|
110
|
+
process.env.SKILLSMITH_AGENT_NUDGE_STATE = nudgeFile;
|
|
111
|
+
writeFileSync(nudgeFile, '1735689600');
|
|
112
|
+
expect(getNudgeCooldownState()).toBe(1735689600);
|
|
113
|
+
});
|
|
114
|
+
it('treats malformed content as "no prior nudge" (matches the hook\'s own tolerant parsing)', () => {
|
|
115
|
+
const nudgeFile = join(stateDir, 'agent-nudge.state');
|
|
116
|
+
process.env.SKILLSMITH_AGENT_NUDGE_STATE = nudgeFile;
|
|
117
|
+
writeFileSync(nudgeFile, 'not-a-number');
|
|
118
|
+
expect(getNudgeCooldownState()).toBeNull();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
//# sourceMappingURL=store.test.js.map
|