@skillsmith/core 0.9.0 → 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.
Files changed (313) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/src/api/client.d.ts.map +1 -1
  4. package/dist/src/api/client.js +6 -0
  5. package/dist/src/api/client.js.map +1 -1
  6. package/dist/src/api/client.test.d.ts +9 -0
  7. package/dist/src/api/client.test.d.ts.map +1 -0
  8. package/dist/src/api/client.test.js +81 -0
  9. package/dist/src/api/client.test.js.map +1 -0
  10. package/dist/src/exports/services.d.ts +3 -1
  11. package/dist/src/exports/services.d.ts.map +1 -1
  12. package/dist/src/exports/services.js +7 -1
  13. package/dist/src/exports/services.js.map +1 -1
  14. package/dist/src/index.d.ts +3 -3
  15. package/dist/src/index.d.ts.map +1 -1
  16. package/dist/src/index.js +5 -2
  17. package/dist/src/index.js.map +1 -1
  18. package/dist/src/install/agent-config-merge.json-array.d.ts +29 -0
  19. package/dist/src/install/agent-config-merge.json-array.d.ts.map +1 -0
  20. package/dist/src/install/agent-config-merge.json-array.js +100 -0
  21. package/dist/src/install/agent-config-merge.json-array.js.map +1 -0
  22. package/dist/src/install/agent-config-merge.json.d.ts +37 -0
  23. package/dist/src/install/agent-config-merge.json.d.ts.map +1 -0
  24. package/dist/src/install/agent-config-merge.json.js +134 -0
  25. package/dist/src/install/agent-config-merge.json.js.map +1 -0
  26. package/dist/src/install/agent-config-merge.toml-block.d.ts +48 -0
  27. package/dist/src/install/agent-config-merge.toml-block.d.ts.map +1 -0
  28. package/dist/src/install/agent-config-merge.toml-block.js +107 -0
  29. package/dist/src/install/agent-config-merge.toml-block.js.map +1 -0
  30. package/dist/src/install/agent-config-merge.types.d.ts +87 -0
  31. package/dist/src/install/agent-config-merge.types.d.ts.map +1 -0
  32. package/dist/src/install/agent-config-merge.types.js +83 -0
  33. package/dist/src/install/agent-config-merge.types.js.map +1 -0
  34. package/dist/src/install/agent-config-merge.yaml.d.ts +46 -0
  35. package/dist/src/install/agent-config-merge.yaml.d.ts.map +1 -0
  36. package/dist/src/install/agent-config-merge.yaml.js +133 -0
  37. package/dist/src/install/agent-config-merge.yaml.js.map +1 -0
  38. package/dist/src/install/agent-config-merge.yaml.test.d.ts +2 -0
  39. package/dist/src/install/agent-config-merge.yaml.test.d.ts.map +1 -0
  40. package/dist/src/install/agent-config-merge.yaml.test.js +210 -0
  41. package/dist/src/install/agent-config-merge.yaml.test.js.map +1 -0
  42. package/dist/src/install/agent-harness-targets.d.ts +98 -0
  43. package/dist/src/install/agent-harness-targets.d.ts.map +1 -0
  44. package/dist/src/install/agent-harness-targets.js +154 -0
  45. package/dist/src/install/agent-harness-targets.js.map +1 -0
  46. package/dist/src/install/agent-home-relocate.d.ts +29 -0
  47. package/dist/src/install/agent-home-relocate.d.ts.map +1 -0
  48. package/dist/src/install/agent-home-relocate.js +38 -0
  49. package/dist/src/install/agent-home-relocate.js.map +1 -0
  50. package/dist/src/install/agent-manifest-path-guard.d.ts +55 -0
  51. package/dist/src/install/agent-manifest-path-guard.d.ts.map +1 -0
  52. package/dist/src/install/agent-manifest-path-guard.js +109 -0
  53. package/dist/src/install/agent-manifest-path-guard.js.map +1 -0
  54. package/dist/src/install/agent-manifest-path-guard.test.d.ts +2 -0
  55. package/dist/src/install/agent-manifest-path-guard.test.d.ts.map +1 -0
  56. package/dist/src/install/agent-manifest-path-guard.test.js +72 -0
  57. package/dist/src/install/agent-manifest-path-guard.test.js.map +1 -0
  58. package/dist/src/install/agent-manifest.d.ts +58 -0
  59. package/dist/src/install/agent-manifest.d.ts.map +1 -0
  60. package/dist/src/install/agent-manifest.js +103 -0
  61. package/dist/src/install/agent-manifest.js.map +1 -0
  62. package/dist/src/install/agent-pack-installer.d.ts +39 -0
  63. package/dist/src/install/agent-pack-installer.d.ts.map +1 -0
  64. package/dist/src/install/agent-pack-installer.entry.d.ts +55 -0
  65. package/dist/src/install/agent-pack-installer.entry.d.ts.map +1 -0
  66. package/dist/src/install/agent-pack-installer.entry.js +90 -0
  67. package/dist/src/install/agent-pack-installer.entry.js.map +1 -0
  68. package/dist/src/install/agent-pack-installer.fs-helpers.d.ts +33 -0
  69. package/dist/src/install/agent-pack-installer.fs-helpers.d.ts.map +1 -0
  70. package/dist/src/install/agent-pack-installer.fs-helpers.js +59 -0
  71. package/dist/src/install/agent-pack-installer.fs-helpers.js.map +1 -0
  72. package/dist/src/install/agent-pack-installer.harness.d.ts +66 -0
  73. package/dist/src/install/agent-pack-installer.harness.d.ts.map +1 -0
  74. package/dist/src/install/agent-pack-installer.harness.js +310 -0
  75. package/dist/src/install/agent-pack-installer.harness.js.map +1 -0
  76. package/dist/src/install/agent-pack-installer.js +221 -0
  77. package/dist/src/install/agent-pack-installer.js.map +1 -0
  78. package/dist/src/install/agent-pack-installer.preserve.test.d.ts +11 -0
  79. package/dist/src/install/agent-pack-installer.preserve.test.d.ts.map +1 -0
  80. package/dist/src/install/agent-pack-installer.preserve.test.js +72 -0
  81. package/dist/src/install/agent-pack-installer.preserve.test.js.map +1 -0
  82. package/dist/src/install/agent-pack-installer.test.d.ts +2 -0
  83. package/dist/src/install/agent-pack-installer.test.d.ts.map +1 -0
  84. package/dist/src/install/agent-pack-installer.test.js +350 -0
  85. package/dist/src/install/agent-pack-installer.test.js.map +1 -0
  86. package/dist/src/install/agent-pack-installer.types.d.ts +51 -0
  87. package/dist/src/install/agent-pack-installer.types.d.ts.map +1 -0
  88. package/dist/src/install/agent-pack-installer.types.js +16 -0
  89. package/dist/src/install/agent-pack-installer.types.js.map +1 -0
  90. package/dist/src/install/agent-pack-uninstaller.d.ts +43 -0
  91. package/dist/src/install/agent-pack-uninstaller.d.ts.map +1 -0
  92. package/dist/src/install/agent-pack-uninstaller.js +113 -0
  93. package/dist/src/install/agent-pack-uninstaller.js.map +1 -0
  94. package/dist/src/install/agent-pack-uninstaller.test.d.ts +2 -0
  95. package/dist/src/install/agent-pack-uninstaller.test.d.ts.map +1 -0
  96. package/dist/src/install/agent-pack-uninstaller.test.js +212 -0
  97. package/dist/src/install/agent-pack-uninstaller.test.js.map +1 -0
  98. package/dist/src/install/index.d.ts +7 -0
  99. package/dist/src/install/index.d.ts.map +1 -1
  100. package/dist/src/install/index.js +7 -0
  101. package/dist/src/install/index.js.map +1 -1
  102. package/dist/src/install/paths.d.ts +1 -1
  103. package/dist/src/install/paths.d.ts.map +1 -1
  104. package/dist/src/install/paths.js +4 -0
  105. package/dist/src/install/paths.js.map +1 -1
  106. package/dist/src/install/paths.test.js +15 -0
  107. package/dist/src/install/paths.test.js.map +1 -1
  108. package/dist/src/journal/hash.d.ts +13 -0
  109. package/dist/src/journal/hash.d.ts.map +1 -0
  110. package/dist/src/journal/hash.js +16 -0
  111. package/dist/src/journal/hash.js.map +1 -0
  112. package/dist/src/journal/index.d.ts +15 -0
  113. package/dist/src/journal/index.d.ts.map +1 -0
  114. package/dist/src/journal/index.js +15 -0
  115. package/dist/src/journal/index.js.map +1 -0
  116. package/dist/src/journal/journal.test.d.ts +16 -0
  117. package/dist/src/journal/journal.test.d.ts.map +1 -0
  118. package/dist/src/journal/journal.test.js +171 -0
  119. package/dist/src/journal/journal.test.js.map +1 -0
  120. package/dist/src/journal/path.d.ts +44 -0
  121. package/dist/src/journal/path.d.ts.map +1 -0
  122. package/dist/src/journal/path.js +61 -0
  123. package/dist/src/journal/path.js.map +1 -0
  124. package/dist/src/journal/reader.d.ts +38 -0
  125. package/dist/src/journal/reader.d.ts.map +1 -0
  126. package/dist/src/journal/reader.js +93 -0
  127. package/dist/src/journal/reader.js.map +1 -0
  128. package/dist/src/journal/types.d.ts +87 -0
  129. package/dist/src/journal/types.d.ts.map +1 -0
  130. package/dist/src/journal/types.js +21 -0
  131. package/dist/src/journal/types.js.map +1 -0
  132. package/dist/src/journal/writer.d.ts +52 -0
  133. package/dist/src/journal/writer.d.ts.map +1 -0
  134. package/dist/src/journal/writer.js +125 -0
  135. package/dist/src/journal/writer.js.map +1 -0
  136. package/dist/src/paywall-triggers/index.d.ts +13 -0
  137. package/dist/src/paywall-triggers/index.d.ts.map +1 -0
  138. package/dist/src/paywall-triggers/index.js +13 -0
  139. package/dist/src/paywall-triggers/index.js.map +1 -0
  140. package/dist/src/paywall-triggers/path.d.ts +47 -0
  141. package/dist/src/paywall-triggers/path.d.ts.map +1 -0
  142. package/dist/src/paywall-triggers/path.js +73 -0
  143. package/dist/src/paywall-triggers/path.js.map +1 -0
  144. package/dist/src/paywall-triggers/store.d.ts +75 -0
  145. package/dist/src/paywall-triggers/store.d.ts.map +1 -0
  146. package/dist/src/paywall-triggers/store.js +122 -0
  147. package/dist/src/paywall-triggers/store.js.map +1 -0
  148. package/dist/src/paywall-triggers/store.test.d.ts +2 -0
  149. package/dist/src/paywall-triggers/store.test.d.ts.map +1 -0
  150. package/dist/src/paywall-triggers/store.test.js +121 -0
  151. package/dist/src/paywall-triggers/store.test.js.map +1 -0
  152. package/dist/src/paywall-triggers/types.d.ts +24 -0
  153. package/dist/src/paywall-triggers/types.d.ts.map +1 -0
  154. package/dist/src/paywall-triggers/types.js +13 -0
  155. package/dist/src/paywall-triggers/types.js.map +1 -0
  156. package/dist/src/security/scanner/SecurityScanner.compound.d.ts +13 -0
  157. package/dist/src/security/scanner/SecurityScanner.compound.d.ts.map +1 -0
  158. package/dist/src/security/scanner/SecurityScanner.compound.js +127 -0
  159. package/dist/src/security/scanner/SecurityScanner.compound.js.map +1 -0
  160. package/dist/src/security/scanner/SecurityScanner.d.ts.map +1 -1
  161. package/dist/src/security/scanner/SecurityScanner.js +10 -1
  162. package/dist/src/security/scanner/SecurityScanner.js.map +1 -1
  163. package/dist/src/security/scanner/SecurityScanner.pii.d.ts +23 -0
  164. package/dist/src/security/scanner/SecurityScanner.pii.d.ts.map +1 -0
  165. package/dist/src/security/scanner/SecurityScanner.pii.js +152 -0
  166. package/dist/src/security/scanner/SecurityScanner.pii.js.map +1 -0
  167. package/dist/src/security/scanner/SecurityScanner.scanners.d.ts +5 -12
  168. package/dist/src/security/scanner/SecurityScanner.scanners.d.ts.map +1 -1
  169. package/dist/src/security/scanner/SecurityScanner.scanners.js +63 -137
  170. package/dist/src/security/scanner/SecurityScanner.scanners.js.map +1 -1
  171. package/dist/src/security/scanner/patterns.d.ts +2 -0
  172. package/dist/src/security/scanner/patterns.d.ts.map +1 -1
  173. package/dist/src/security/scanner/patterns.js +79 -11
  174. package/dist/src/security/scanner/patterns.js.map +1 -1
  175. package/dist/src/security/scanner/types.d.ts +2 -0
  176. package/dist/src/security/scanner/types.d.ts.map +1 -1
  177. package/dist/src/services/agent-pack/agent-pack.test.d.ts +18 -0
  178. package/dist/src/services/agent-pack/agent-pack.test.d.ts.map +1 -0
  179. package/dist/src/services/agent-pack/agent-pack.test.js +344 -0
  180. package/dist/src/services/agent-pack/agent-pack.test.js.map +1 -0
  181. package/dist/src/services/agent-pack/hooks.d.ts +29 -0
  182. package/dist/src/services/agent-pack/hooks.d.ts.map +1 -0
  183. package/dist/src/services/agent-pack/hooks.js +139 -0
  184. package/dist/src/services/agent-pack/hooks.js.map +1 -0
  185. package/dist/src/services/agent-pack/index.d.ts +26 -0
  186. package/dist/src/services/agent-pack/index.d.ts.map +1 -0
  187. package/dist/src/services/agent-pack/index.js +109 -0
  188. package/dist/src/services/agent-pack/index.js.map +1 -0
  189. package/dist/src/services/agent-pack/prompt-source.d.ts +47 -0
  190. package/dist/src/services/agent-pack/prompt-source.d.ts.map +1 -0
  191. package/dist/src/services/agent-pack/prompt-source.js +182 -0
  192. package/dist/src/services/agent-pack/prompt-source.js.map +1 -0
  193. package/dist/src/services/agent-pack/shims.d.ts +40 -0
  194. package/dist/src/services/agent-pack/shims.d.ts.map +1 -0
  195. package/dist/src/services/agent-pack/shims.js +96 -0
  196. package/dist/src/services/agent-pack/shims.js.map +1 -0
  197. package/dist/src/services/agent-pack/skill-md.d.ts +35 -0
  198. package/dist/src/services/agent-pack/skill-md.d.ts.map +1 -0
  199. package/dist/src/services/agent-pack/skill-md.js +89 -0
  200. package/dist/src/services/agent-pack/skill-md.js.map +1 -0
  201. package/dist/src/services/agent-pack/types.d.ts +118 -0
  202. package/dist/src/services/agent-pack/types.d.ts.map +1 -0
  203. package/dist/src/services/agent-pack/types.js +61 -0
  204. package/dist/src/services/agent-pack/types.js.map +1 -0
  205. package/dist/src/services/agent-tool-profile.d.ts +59 -0
  206. package/dist/src/services/agent-tool-profile.d.ts.map +1 -0
  207. package/dist/src/services/agent-tool-profile.js +76 -0
  208. package/dist/src/services/agent-tool-profile.js.map +1 -0
  209. package/dist/src/services/agent-tool-profile.test.d.ts +2 -0
  210. package/dist/src/services/agent-tool-profile.test.d.ts.map +1 -0
  211. package/dist/src/services/agent-tool-profile.test.js +28 -0
  212. package/dist/src/services/agent-tool-profile.test.js.map +1 -0
  213. package/dist/src/services/bundled-sibling-scan.d.ts +111 -0
  214. package/dist/src/services/bundled-sibling-scan.d.ts.map +1 -0
  215. package/dist/src/services/bundled-sibling-scan.js +170 -0
  216. package/dist/src/services/bundled-sibling-scan.js.map +1 -0
  217. package/dist/src/services/skill-installation.io.d.ts +16 -5
  218. package/dist/src/services/skill-installation.io.d.ts.map +1 -1
  219. package/dist/src/services/skill-installation.io.js +44 -20
  220. package/dist/src/services/skill-installation.io.js.map +1 -1
  221. package/dist/src/services/skill-installation.policy.d.ts +96 -0
  222. package/dist/src/services/skill-installation.policy.d.ts.map +1 -0
  223. package/dist/src/services/skill-installation.policy.js +144 -0
  224. package/dist/src/services/skill-installation.policy.js.map +1 -0
  225. package/dist/src/services/skill-installation.service.d.ts.map +1 -1
  226. package/dist/src/services/skill-installation.service.js +9 -3
  227. package/dist/src/services/skill-installation.service.js.map +1 -1
  228. package/dist/src/sources/index.d.ts +1 -0
  229. package/dist/src/sources/index.d.ts.map +1 -1
  230. package/dist/src/sources/index.js +4 -0
  231. package/dist/src/sources/index.js.map +1 -1
  232. package/dist/src/sync/index.d.ts +1 -1
  233. package/dist/src/sync/index.d.ts.map +1 -1
  234. package/dist/src/sync/index.js +1 -1
  235. package/dist/src/sync/index.js.map +1 -1
  236. package/dist/src/sync/inventory-client.d.ts +16 -0
  237. package/dist/src/sync/inventory-client.d.ts.map +1 -1
  238. package/dist/src/sync/inventory-client.js +54 -0
  239. package/dist/src/sync/inventory-client.js.map +1 -1
  240. package/dist/src/sync/inventory-client.test.js +67 -1
  241. package/dist/src/sync/inventory-client.test.js.map +1 -1
  242. package/dist/src/sync/inventory-collector.d.ts.map +1 -1
  243. package/dist/src/sync/inventory-collector.js +32 -5
  244. package/dist/src/sync/inventory-collector.js.map +1 -1
  245. package/dist/src/sync/inventory-collector.test.js +50 -1
  246. package/dist/src/sync/inventory-collector.test.js.map +1 -1
  247. package/dist/src/sync/inventory-types.d.ts +9 -0
  248. package/dist/src/sync/inventory-types.d.ts.map +1 -1
  249. package/dist/src/sync/inventory-types.js +3 -0
  250. package/dist/src/sync/inventory-types.js.map +1 -1
  251. package/dist/src/telemetry/agent-marker.d.ts +137 -0
  252. package/dist/src/telemetry/agent-marker.d.ts.map +1 -0
  253. package/dist/src/telemetry/agent-marker.js +242 -0
  254. package/dist/src/telemetry/agent-marker.js.map +1 -0
  255. package/dist/src/telemetry/agent-marker.test.d.ts +13 -0
  256. package/dist/src/telemetry/agent-marker.test.d.ts.map +1 -0
  257. package/dist/src/telemetry/agent-marker.test.js +202 -0
  258. package/dist/src/telemetry/agent-marker.test.js.map +1 -0
  259. package/dist/src/telemetry/index.d.ts +2 -1
  260. package/dist/src/telemetry/index.d.ts.map +1 -1
  261. package/dist/src/telemetry/index.js +6 -1
  262. package/dist/src/telemetry/index.js.map +1 -1
  263. package/dist/src/telemetry/posthog.d.ts +10 -0
  264. package/dist/src/telemetry/posthog.d.ts.map +1 -1
  265. package/dist/src/telemetry/posthog.js +5 -1
  266. package/dist/src/telemetry/posthog.js.map +1 -1
  267. package/dist/src/telemetry/wrap.d.ts +28 -7
  268. package/dist/src/telemetry/wrap.d.ts.map +1 -1
  269. package/dist/src/telemetry/wrap.gate.test.d.ts +22 -0
  270. package/dist/src/telemetry/wrap.gate.test.d.ts.map +1 -0
  271. package/dist/src/telemetry/wrap.gate.test.js +197 -0
  272. package/dist/src/telemetry/wrap.gate.test.js.map +1 -0
  273. package/dist/src/telemetry/wrap.js +111 -26
  274. package/dist/src/telemetry/wrap.js.map +1 -1
  275. package/dist/src/telemetry/wrap.marker.test.d.ts +16 -0
  276. package/dist/src/telemetry/wrap.marker.test.d.ts.map +1 -0
  277. package/dist/src/telemetry/wrap.marker.test.js +185 -0
  278. package/dist/src/telemetry/wrap.marker.test.js.map +1 -0
  279. package/dist/src/telemetry/wrap.test.js +4 -0
  280. package/dist/src/telemetry/wrap.test.js.map +1 -1
  281. package/dist/tests/SecurityScanner.exec.test.js +60 -0
  282. package/dist/tests/SecurityScanner.exec.test.js.map +1 -1
  283. package/dist/tests/security/ContinuousSecurity.test.js +10 -4
  284. package/dist/tests/security/ContinuousSecurity.test.js.map +1 -1
  285. package/dist/tests/security/SecurityScanner.chmod-evasion.test.d.ts +2 -0
  286. package/dist/tests/security/SecurityScanner.chmod-evasion.test.d.ts.map +1 -0
  287. package/dist/tests/security/SecurityScanner.chmod-evasion.test.js +126 -0
  288. package/dist/tests/security/SecurityScanner.chmod-evasion.test.js.map +1 -0
  289. package/dist/tests/security/chmod-compound.test.d.ts +2 -0
  290. package/dist/tests/security/chmod-compound.test.d.ts.map +1 -0
  291. package/dist/tests/security/chmod-compound.test.js +188 -0
  292. package/dist/tests/security/chmod-compound.test.js.map +1 -0
  293. package/dist/tests/security/pii-detection.test.js +24 -0
  294. package/dist/tests/security/pii-detection.test.js.map +1 -1
  295. package/dist/tests/security/scanner-regression-guard.test.d.ts +23 -7
  296. package/dist/tests/security/scanner-regression-guard.test.d.ts.map +1 -1
  297. package/dist/tests/security/scanner-regression-guard.test.js +27 -11
  298. package/dist/tests/security/scanner-regression-guard.test.js.map +1 -1
  299. package/dist/tests/security/sensitive-path-fp.test.d.ts +2 -0
  300. package/dist/tests/security/sensitive-path-fp.test.d.ts.map +1 -0
  301. package/dist/tests/security/sensitive-path-fp.test.js +142 -0
  302. package/dist/tests/security/sensitive-path-fp.test.js.map +1 -0
  303. package/dist/tests/services/bundled-sibling-scan.test.d.ts +2 -0
  304. package/dist/tests/services/bundled-sibling-scan.test.d.ts.map +1 -0
  305. package/dist/tests/services/bundled-sibling-scan.test.js +139 -0
  306. package/dist/tests/services/bundled-sibling-scan.test.js.map +1 -0
  307. package/dist/tests/unit/services/skill-installation.io.test.js +166 -0
  308. package/dist/tests/unit/services/skill-installation.io.test.js.map +1 -1
  309. package/dist/tests/unit/services/skill-installation.policy.test.d.ts +8 -0
  310. package/dist/tests/unit/services/skill-installation.policy.test.d.ts.map +1 -0
  311. package/dist/tests/unit/services/skill-installation.policy.test.js +151 -0
  312. package/dist/tests/unit/services/skill-installation.policy.test.js.map +1 -0
  313. 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":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AACxD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAEnC,OAAO,EAAE,oBAAoB,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAA;AAG9C;;;;;;GAMG;AACH,SAAS,eAAe,CAAC,MAA0C;IACjE,MAAM,OAAO,GAAG;QACd,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,EAAE,EAAE,MAAM,CAAC,EAAE;QACb,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,WAAW,EAAE,MAAM,CAAC,WAAW;QAC/B,WAAW,EAAE,MAAM,CAAC,WAAW;QAC/B,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,SAAS,EAAE,MAAM,CAAC,SAAS;KAC5B,CAAA;IACD,OAAO,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAA;AAChC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAA0C;IAC1E,OAAO,SAAS,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,CAAA;AAC3C,CAAC;AAeD,MAAM,UAAU,kBAAkB,CAAC,GAAY;IAC7C,OAAO,CACL,GAAG,YAAY,KAAK;QACnB,GAAwC,CAAC,IAAI,KAAK,4BAA4B,CAChF,CAAA;AACH,CAAC;AAED,KAAK,UAAU,iBAAiB,CAAC,QAAgB;IAC/C,IAAI,GAAW,CAAA;IACf,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,oBAAoB,CAAA;IAC7B,CAAC;IAED,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IACtE,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,oBAAoB,CAAA;IAEnD,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAE,CAAA;IACzC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAA2B,CAAA;QAC7D,IAAI,OAAO,MAAM,CAAC,WAAW,KAAK,QAAQ,IAAI,MAAM,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC5E,OAAO,MAAM,CAAC,WAAW,CAAA;QAC3B,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,+CAA+C;IACjD,CAAC;IAED,MAAM,GAAG,GAAG,IAAI,KAAK,CACnB,4FAA4F,CAClE,CAAA;IAC5B,GAAG,CAAC,IAAI,GAAG,4BAA4B,CAAA;IACvC,MAAM,GAAG,CAAA;AACX,CAAC;AAED,4EAA4E;AAC5E,mEAAmE;AACnE,IAAI,UAAU,GAAkB,OAAO,CAAC,OAAO,EAAE,CAAA;AAEjD;;;;;;;;GAQG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAyB;IAC3D,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAA;IACnD,qEAAqE;IACrE,UAAU,GAAG,IAAI,CAAC,IAAI,CACpB,GAAG,EAAE,CAAC,SAAS,EACf,GAAG,EAAE,CAAC,SAAS,CAChB,CAAA;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED,KAAK,UAAU,QAAQ,CAAC,KAAyB;IAC/C,MAAM,QAAQ,GAAG,kBAAkB,EAAE,CAAA;IACrC,MAAM,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAEnD,uEAAuE;IACvE,qEAAqE;IACrE,MAAM,QAAQ,GAAG,MAAM,iBAAiB,CAAC,QAAQ,CAAC,CAAA;IAElD,MAAM,WAAW,GAAuC,EAAE,GAAG,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAA;IACzF,MAAM,MAAM,GAAkB,EAAE,GAAG,WAAW,EAAE,WAAW,EAAE,iBAAiB,CAAC,WAAW,CAAC,EAAE,CAAA;IAE7F,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,IAAI,CAAA;IAC1C,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAA;IACxC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;QACtC,MAAM,MAAM,CAAC,IAAI,EAAE,CAAA;IACrB,CAAC;YAAS,CAAC;QACT,MAAM,MAAM,CAAC,KAAK,EAAE,CAAA;IACtB,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC"}
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=store.test.d.ts.map
@@ -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