@jairodri/huckleberry-cli 1.0.1 → 1.1.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 (745) hide show
  1. package/README.md +85 -0
  2. package/agents/cos.json +2 -5
  3. package/agents/hermes/AGENTS.md +25 -49
  4. package/agents/hermes/fix-kimaki/SKILL.md +19 -92
  5. package/agents/hermes/hermes-fixer.md +44 -74
  6. package/agents/hermes.json +6 -18
  7. package/kanban/SKILL.md +18 -18
  8. package/kanban/package.json +1 -1
  9. package/kanban/src/dispatcher.mjs +1 -1
  10. package/kanban/templates/blog-social-campaign.yaml +6 -2
  11. package/kanban/templates/social-campaign.yaml +9 -4
  12. package/mcp/jev-calibration.mjs +335 -0
  13. package/mcp/jev-client.mjs +119 -0
  14. package/mcp/jev-thresholds.json +27 -0
  15. package/package.json +13 -9
  16. package/scripts/doctor-watchdog.sh +73 -0
  17. package/scripts/install-services.sh +34 -27
  18. package/scripts/self-improve-weekly.sh +34 -0
  19. package/scripts/sync-skills.mjs +121 -0
  20. package/skills/ab-test-setup/SKILL.md +266 -0
  21. package/skills/ab-test-setup/evals/evals.json +105 -0
  22. package/skills/ab-test-setup/references/sample-size-guide.md +263 -0
  23. package/skills/ab-test-setup/references/test-templates.md +277 -0
  24. package/skills/agent-reach/SKILL.md +132 -0
  25. package/skills/agent-reach/references/career.md +29 -0
  26. package/skills/agent-reach/references/dev.md +62 -0
  27. package/skills/agent-reach/references/search.md +33 -0
  28. package/skills/agent-reach/references/social.md +232 -0
  29. package/skills/agent-reach/references/video.md +130 -0
  30. package/skills/agent-reach/references/web.md +50 -0
  31. package/skills/ai-agent-seo/SKILL.md +450 -0
  32. package/skills/ai-cold-email-intelligence/SKILL.md +54 -0
  33. package/skills/ai-cold-email-intelligence/references/prompts.md +21 -0
  34. package/skills/ai-consensus-builder/SKILL.md +324 -0
  35. package/skills/ai-seo/SKILL.md +399 -0
  36. package/skills/ai-seo/evals/evals.json +90 -0
  37. package/skills/ai-seo/references/content-patterns.md +284 -0
  38. package/skills/ai-seo/references/platform-ranking-factors.md +152 -0
  39. package/skills/ai-visibility-audit/SKILL.md +322 -0
  40. package/skills/analytics-tracking/SKILL.md +309 -0
  41. package/skills/analytics-tracking/evals/evals.json +90 -0
  42. package/skills/analytics-tracking/references/event-library.md +260 -0
  43. package/skills/analytics-tracking/references/ga4-implementation.md +300 -0
  44. package/skills/analytics-tracking/references/gtm-implementation.md +390 -0
  45. package/skills/article-generator/SKILL.md +216 -0
  46. package/skills/auto-indexing/SKILL.md +179 -0
  47. package/skills/auto-indexing/scripts/auto-indexing.js +257 -0
  48. package/skills/auto-skill/SKILL.md +151 -0
  49. package/skills/auto-skill/correction-queue.js +102 -0
  50. package/skills/auto-skill/dedup.js +97 -0
  51. package/skills/auto-skill/skill-router.js +224 -0
  52. package/skills/autoresearch/SKILL.md +334 -0
  53. package/skills/autoresearch/autoresearch-autoresearch/SKILL.md.baseline +329 -0
  54. package/skills/autoresearch/autoresearch-autoresearch/changelog.md +34 -0
  55. package/skills/autoresearch/autoresearch-autoresearch/dashboard.html +116 -0
  56. package/skills/autoresearch/autoresearch-autoresearch/evals.md +39 -0
  57. package/skills/autoresearch/autoresearch-autoresearch/results.json +44 -0
  58. package/skills/autoresearch/autoresearch-autoresearch/results.tsv +3 -0
  59. package/skills/autoresearch/eval-guide.md +121 -0
  60. package/skills/b2b-cold-outreach/SKILL.md +322 -0
  61. package/skills/birdclaw/SKILL.md +82 -0
  62. package/skills/brainstorm/SKILL.md +168 -0
  63. package/skills/brand-kit-playground/SKILL.md +312 -0
  64. package/skills/brand-kit-playground/references/html-architecture.md +316 -0
  65. package/skills/brand-kit-playground/references/steal-patterns.md +88 -0
  66. package/skills/brand-voice/SKILL.md +223 -0
  67. package/skills/brand-voice/references/honesty-as-positioning.md +60 -0
  68. package/skills/brand-voice/references/voice-modes.md +1158 -0
  69. package/skills/business-advisory-council/README.md +136 -0
  70. package/skills/business-advisory-council/SKILL.md +94 -0
  71. package/skills/business-advisory-council/business-advisory-council.py +492 -0
  72. package/skills/business-advisory-council/discord-components-decision.json +8 -0
  73. package/skills/business-advisory-council/examples/steve-conley-partnership.json +6 -0
  74. package/skills/business-advisory-council/steve-conley-call-brief.json +6 -0
  75. package/skills/churn-prevention/SKILL.md +305 -0
  76. package/skills/churn-prevention/evals/evals.json +93 -0
  77. package/skills/churn-prevention/references/cancel-flow-patterns.md +316 -0
  78. package/skills/churn-prevention/references/dunning-playbook.md +408 -0
  79. package/skills/claw-compactor/LICENSE +21 -0
  80. package/skills/claw-compactor/README.md +459 -0
  81. package/skills/claw-compactor/SKILL.md +329 -0
  82. package/skills/claw-compactor/benchmark/RESULTS.md +104 -0
  83. package/skills/claw-compactor/benchmark/__init__.py +1 -0
  84. package/skills/claw-compactor/benchmark/compressors.py +520 -0
  85. package/skills/claw-compactor/benchmark/data/sample_01_devops.json +82 -0
  86. package/skills/claw-compactor/benchmark/data/sample_02_trading.json +54 -0
  87. package/skills/claw-compactor/benchmark/data/sample_03_ml_short.json +38 -0
  88. package/skills/claw-compactor/benchmark/data/sample_04_mixed_long.json +318 -0
  89. package/skills/claw-compactor/benchmark/data/sample_05_sysadmin.json +56 -0
  90. package/skills/claw-compactor/benchmark/evaluate.py +278 -0
  91. package/skills/claw-compactor/benchmark/report.py +241 -0
  92. package/skills/claw-compactor/benchmark/results/benchmark_results.json +405 -0
  93. package/skills/claw-compactor/benchmark/run_benchmark.py +238 -0
  94. package/skills/claw-compactor/engram.yaml +49 -0
  95. package/skills/claw-compactor/proxy/README.md +87 -0
  96. package/skills/claw-compactor/proxy/dashboard.html +804 -0
  97. package/skills/claw-compactor/proxy/event-log.mjs +213 -0
  98. package/skills/claw-compactor/proxy/fair-queue.mjs +288 -0
  99. package/skills/claw-compactor/proxy/metrics-store.mjs +425 -0
  100. package/skills/claw-compactor/proxy/package-lock.json +125 -0
  101. package/skills/claw-compactor/proxy/package.json +15 -0
  102. package/skills/claw-compactor/proxy/portal.html +360 -0
  103. package/skills/claw-compactor/proxy/process-registry.mjs +447 -0
  104. package/skills/claw-compactor/proxy/rate-limiter.mjs +117 -0
  105. package/skills/claw-compactor/proxy/redis-client.mjs +75 -0
  106. package/skills/claw-compactor/proxy/retry.mjs +182 -0
  107. package/skills/claw-compactor/proxy/server.mjs +2100 -0
  108. package/skills/claw-compactor/proxy/session-affinity.mjs +177 -0
  109. package/skills/claw-compactor/proxy/start.sh +23 -0
  110. package/skills/claw-compactor/proxy/test/event-log.test.mjs +237 -0
  111. package/skills/claw-compactor/proxy/test/helpers.mjs +103 -0
  112. package/skills/claw-compactor/proxy/test/integration.test.mjs +297 -0
  113. package/skills/claw-compactor/proxy/test/metrics-store.test.mjs +243 -0
  114. package/skills/claw-compactor/proxy/test/process-registry.test.mjs +390 -0
  115. package/skills/claw-compactor/proxy/test/rate-limiter.test.mjs +121 -0
  116. package/skills/claw-compactor/proxy/test/redis-client.test.mjs +62 -0
  117. package/skills/claw-compactor/proxy/test/token-tracker.test.mjs +200 -0
  118. package/skills/claw-compactor/proxy/token-tracker.mjs +482 -0
  119. package/skills/claw-compactor/pyproject.toml +26 -0
  120. package/skills/claw-compactor/references/README.md +44 -0
  121. package/skills/claw-compactor/references/architecture.md +197 -0
  122. package/skills/claw-compactor/references/benchmarks.md +94 -0
  123. package/skills/claw-compactor/references/compression-prompts.md +39 -0
  124. package/skills/claw-compactor/references/compression-techniques.md +219 -0
  125. package/skills/claw-compactor/references/testing.md +160 -0
  126. package/skills/claw-compactor/scripts/audit_memory.py +216 -0
  127. package/skills/claw-compactor/scripts/compress_memory.py +236 -0
  128. package/skills/claw-compactor/scripts/compressed_context.py +280 -0
  129. package/skills/claw-compactor/scripts/dedup_memory.py +147 -0
  130. package/skills/claw-compactor/scripts/dictionary_compress.py +170 -0
  131. package/skills/claw-compactor/scripts/engram-auto.sh +152 -0
  132. package/skills/claw-compactor/scripts/engram_auto.py +695 -0
  133. package/skills/claw-compactor/scripts/engram_cli.py +474 -0
  134. package/skills/claw-compactor/scripts/estimate_tokens.py +131 -0
  135. package/skills/claw-compactor/scripts/generate_summary_tiers.py +292 -0
  136. package/skills/claw-compactor/scripts/lib/__init__.py +34 -0
  137. package/skills/claw-compactor/scripts/lib/config.py +289 -0
  138. package/skills/claw-compactor/scripts/lib/dedup.py +119 -0
  139. package/skills/claw-compactor/scripts/lib/dictionary.py +281 -0
  140. package/skills/claw-compactor/scripts/lib/engram.py +793 -0
  141. package/skills/claw-compactor/scripts/lib/engram_prompts.py +175 -0
  142. package/skills/claw-compactor/scripts/lib/engram_storage.py +254 -0
  143. package/skills/claw-compactor/scripts/lib/exceptions.py +24 -0
  144. package/skills/claw-compactor/scripts/lib/markdown.py +312 -0
  145. package/skills/claw-compactor/scripts/lib/rle.py +165 -0
  146. package/skills/claw-compactor/scripts/lib/tokenizer_optimizer.py +188 -0
  147. package/skills/claw-compactor/scripts/lib/tokens.py +68 -0
  148. package/skills/claw-compactor/scripts/mem_compress.py +788 -0
  149. package/skills/claw-compactor/scripts/observation_compressor.py +346 -0
  150. package/skills/claw-compactor/tests/__init__.py +0 -0
  151. package/skills/claw-compactor/tests/conftest.py +94 -0
  152. package/skills/claw-compactor/tests/test_audit_comprehensive.py +123 -0
  153. package/skills/claw-compactor/tests/test_audit_memory.py +67 -0
  154. package/skills/claw-compactor/tests/test_benchmark.py +122 -0
  155. package/skills/claw-compactor/tests/test_cli_commands.py +228 -0
  156. package/skills/claw-compactor/tests/test_compress_memory.py +109 -0
  157. package/skills/claw-compactor/tests/test_compress_memory_comprehensive.py +191 -0
  158. package/skills/claw-compactor/tests/test_compressed_context.py +145 -0
  159. package/skills/claw-compactor/tests/test_config.py +54 -0
  160. package/skills/claw-compactor/tests/test_dedup_memory.py +124 -0
  161. package/skills/claw-compactor/tests/test_dictionary.py +327 -0
  162. package/skills/claw-compactor/tests/test_dictionary_comprehensive.py +241 -0
  163. package/skills/claw-compactor/tests/test_engram.py +1091 -0
  164. package/skills/claw-compactor/tests/test_engram_auto.py +529 -0
  165. package/skills/claw-compactor/tests/test_error_handling.py +194 -0
  166. package/skills/claw-compactor/tests/test_estimate_tokens.py +99 -0
  167. package/skills/claw-compactor/tests/test_generate_summary_tiers.py +105 -0
  168. package/skills/claw-compactor/tests/test_integration.py +249 -0
  169. package/skills/claw-compactor/tests/test_lib_dedup.py +135 -0
  170. package/skills/claw-compactor/tests/test_lib_markdown.py +388 -0
  171. package/skills/claw-compactor/tests/test_lib_tokens.py +82 -0
  172. package/skills/claw-compactor/tests/test_main_entry.py +176 -0
  173. package/skills/claw-compactor/tests/test_markdown_advanced.py +284 -0
  174. package/skills/claw-compactor/tests/test_new_features.py +77 -0
  175. package/skills/claw-compactor/tests/test_observation_comprehensive.py +261 -0
  176. package/skills/claw-compactor/tests/test_observation_compressor.py +128 -0
  177. package/skills/claw-compactor/tests/test_performance.py +172 -0
  178. package/skills/claw-compactor/tests/test_pipeline.py +109 -0
  179. package/skills/claw-compactor/tests/test_real_workspace.py +161 -0
  180. package/skills/claw-compactor/tests/test_rle.py +114 -0
  181. package/skills/claw-compactor/tests/test_rle_comprehensive.py +151 -0
  182. package/skills/claw-compactor/tests/test_roundtrip.py +253 -0
  183. package/skills/claw-compactor/tests/test_roundtrip_comprehensive.py +192 -0
  184. package/skills/claw-compactor/tests/test_tiers_comprehensive.py +171 -0
  185. package/skills/claw-compactor/tests/test_token_economics.py +237 -0
  186. package/skills/claw-compactor/tests/test_tokenizer_optimizer.py +175 -0
  187. package/skills/claw-compactor/tests/test_tokenizer_optimizer_comprehensive.py +241 -0
  188. package/skills/claw-compactor/tests/test_tokens.py +27 -0
  189. package/skills/cmo/CHANGELOG.md +61 -0
  190. package/skills/cmo/SKILL.md +500 -0
  191. package/skills/cmo/references/ai-slop-patterns.md +56 -0
  192. package/skills/cmo/references/analytics-guide.md +152 -0
  193. package/skills/cmo/references/ideas-library.md +197 -0
  194. package/skills/cmo/rules/brand-file-map.md +198 -0
  195. package/skills/cmo/rules/brand-memory.md +158 -0
  196. package/skills/cmo/rules/cli-runtime-index.md +95 -0
  197. package/skills/cmo/rules/command-reference.md +167 -0
  198. package/skills/cmo/rules/communication.md +15 -0
  199. package/skills/cmo/rules/context-switch.md +101 -0
  200. package/skills/cmo/rules/ecosystem.md +125 -0
  201. package/skills/cmo/rules/error-recovery.md +98 -0
  202. package/skills/cmo/rules/learning-loop.md +170 -0
  203. package/skills/cmo/rules/monorepo.md +78 -0
  204. package/skills/cmo/rules/output-format.md +124 -0
  205. package/skills/cmo/rules/persona.md +15 -0
  206. package/skills/cmo/rules/playbooks.md +327 -0
  207. package/skills/cmo/rules/progressive-enhancement.md +231 -0
  208. package/skills/cmo/rules/publish-index.md +106 -0
  209. package/skills/cmo/rules/quality-gate.md +90 -0
  210. package/skills/cmo/rules/recency-grounding.md +124 -0
  211. package/skills/cmo/rules/safety.md +80 -0
  212. package/skills/cmo/rules/studio-api-index.md +138 -0
  213. package/skills/cmo/rules/studio-integration.md +147 -0
  214. package/skills/cmo/rules/sub-agents.md +167 -0
  215. package/skills/cold-email/SKILL.md +240 -0
  216. package/skills/cold-email/evals/evals.json +94 -0
  217. package/skills/cold-email/references/benchmarks.md +83 -0
  218. package/skills/cold-email/references/follow-up-sequences.md +81 -0
  219. package/skills/cold-email/references/frameworks.md +90 -0
  220. package/skills/cold-email/references/personalization.md +79 -0
  221. package/skills/cold-email/references/subject-lines.md +53 -0
  222. package/skills/competitive-intel/SKILL.md +433 -0
  223. package/skills/competitive-intel/references/competitive-frameworks.md +145 -0
  224. package/skills/competitor-alternatives/SKILL.md +362 -0
  225. package/skills/competitor-alternatives/evals/evals.json +93 -0
  226. package/skills/competitor-alternatives/references/content-architecture.md +271 -0
  227. package/skills/competitor-alternatives/references/templates.md +223 -0
  228. package/skills/concreteness-fixer/SKILL.md +483 -0
  229. package/skills/concreteness-fixer/scripts/validation/concreteness-validator.js +221 -0
  230. package/skills/concreteness-fixer/utils/execFileNoThrow.js +16 -0
  231. package/skills/content-atomizer/SKILL.md +424 -0
  232. package/skills/content-atomizer/references/hook-strategies.md +56 -0
  233. package/skills/content-atomizer/references/platform-specs.md +118 -0
  234. package/skills/content-atomizer/references/platforms/bluesky.md +64 -0
  235. package/skills/content-atomizer/references/platforms/instagram.md +77 -0
  236. package/skills/content-atomizer/references/platforms/linkedin.md +120 -0
  237. package/skills/content-atomizer/references/platforms/reddit.md +73 -0
  238. package/skills/content-atomizer/references/platforms/threads.md +64 -0
  239. package/skills/content-atomizer/references/platforms/tiktok.md +55 -0
  240. package/skills/content-atomizer/references/platforms/twitter.md +123 -0
  241. package/skills/content-atomizer/references/platforms/youtube.md +56 -0
  242. package/skills/content-atomizer/references/tweet-patterns.md +40 -0
  243. package/skills/content-repost-manager/SKILL.md +245 -0
  244. package/skills/content-repost-manager/scripts/content-repost-scan.py +133 -0
  245. package/skills/context-recovery/.clawdhub/origin.json +7 -0
  246. package/skills/context-recovery/SKILL.md +198 -0
  247. package/skills/context-recovery/_meta.json +6 -0
  248. package/skills/conversion-flow-cro/SKILL.md +248 -0
  249. package/skills/conversion-flow-cro/references/flow-benchmarks.md +158 -0
  250. package/skills/copy-editing/SKILL.md +474 -0
  251. package/skills/copy-editing/evals/evals.json +89 -0
  252. package/skills/copy-editing/references/plain-english-alternatives.md +394 -0
  253. package/skills/copywriting/SKILL.md +349 -0
  254. package/skills/copywriting/evals/evals.json +111 -0
  255. package/skills/copywriting/references/awareness-level-guide.md +227 -0
  256. package/skills/copywriting/references/copy-frameworks.md +389 -0
  257. package/skills/copywriting/references/natural-transitions.md +272 -0
  258. package/skills/creative/SKILL.md +365 -0
  259. package/skills/creative/modes/ad-creative.md +1748 -0
  260. package/skills/creative/modes/product-photo.md +1520 -0
  261. package/skills/creative/modes/product-video.md +1657 -0
  262. package/skills/creative/modes/social-graphics.md +1511 -0
  263. package/skills/creative/modes/talking-head.md +2208 -0
  264. package/skills/creative/references/VISUAL_INTELLIGENCE.md +1233 -0
  265. package/skills/deep-serp-article/SKILL.md +677 -0
  266. package/skills/deep-serp-article/SKILL.md.bak +530 -0
  267. package/skills/earned-media/SKILL.md +260 -0
  268. package/skills/email-sequences/SKILL.md +446 -0
  269. package/skills/email-sequences/references/sequence-templates.md +1217 -0
  270. package/skills/firecrawl/.skill-meta.json +13 -0
  271. package/skills/firecrawl/SKILL.md +154 -0
  272. package/skills/firecrawl/rules/install.md +63 -0
  273. package/skills/firecrawl/rules/security.md +26 -0
  274. package/skills/form-cro/SKILL.md +429 -0
  275. package/skills/form-cro/evals/evals.json +90 -0
  276. package/skills/free-tool-strategy/SKILL.md +325 -0
  277. package/skills/free-tool-strategy/evals/evals.json +90 -0
  278. package/skills/free-tool-strategy/references/tool-types.md +253 -0
  279. package/skills/frontend-slides/SKILL.md +248 -0
  280. package/skills/frontend-slides/STYLE_PRESETS.md +347 -0
  281. package/skills/frontend-slides/animation-patterns.md +110 -0
  282. package/skills/frontend-slides/html-template.md +347 -0
  283. package/skills/frontend-slides/scripts/extract-pptx.py +96 -0
  284. package/skills/frontend-slides/viewport-base.css +153 -0
  285. package/skills/guardrail-framework/SKILL.md +319 -0
  286. package/skills/hook-generator/SKILL.md +438 -0
  287. package/skills/hook-generator/database/2026-PLATFORM-RESEARCH.md +211 -0
  288. package/skills/hook-generator/database/instagram-hooks.json +591 -0
  289. package/skills/hook-generator/database/linkedin-hooks.json +502 -0
  290. package/skills/hook-generator/database/medium-hooks.json +181 -0
  291. package/skills/hook-generator/database/tiktok-hooks.json +337 -0
  292. package/skills/hook-generator/database/twitter-hooks.json +567 -0
  293. package/skills/hook-generator/database/youtube-hooks.json +275 -0
  294. package/skills/hook-generator/package-lock.json +24 -0
  295. package/skills/hook-generator/package.json +5 -0
  296. package/skills/hook-generator/references/hook-generation-system.md +257 -0
  297. package/skills/hook-generator/references/hook-performance-analysis.json +153 -0
  298. package/skills/hook-generator/references/hooks-library.md +128 -0
  299. package/skills/hook-generator/references/platform-hook-frameworks.md +275 -0
  300. package/skills/hook-generator/scripts/generate-hooks-unified.js +722 -0
  301. package/skills/hook-generator/scripts/generate-hooks.js +225 -0
  302. package/skills/hook-generator/scripts/generate.sh +36 -0
  303. package/skills/hook-generator/scripts/ralph-hook-enhancer.js +864 -0
  304. package/skills/hook-generator/scripts/test-gemini-output.js +51 -0
  305. package/skills/humanizer/README.md +120 -0
  306. package/skills/humanizer/SKILL.md +699 -0
  307. package/skills/image-gen/SKILL.md +277 -0
  308. package/skills/image-gen/references/prompt-patterns.md +114 -0
  309. package/skills/image-gen/references/visual-metaphors.md +51 -0
  310. package/skills/impeccable/SKILL.md +86 -0
  311. package/skills/impeccable/reference/adapt.md +312 -0
  312. package/skills/impeccable/reference/adapt.native.md +58 -0
  313. package/skills/impeccable/reference/android.md +40 -0
  314. package/skills/impeccable/reference/animate.md +86 -0
  315. package/skills/impeccable/reference/audit.md +136 -0
  316. package/skills/impeccable/reference/audit.native.md +139 -0
  317. package/skills/impeccable/reference/bolder.md +31 -0
  318. package/skills/impeccable/reference/clarify.md +94 -0
  319. package/skills/impeccable/reference/colorize.md +86 -0
  320. package/skills/impeccable/reference/craft-floor.md +42 -0
  321. package/skills/impeccable/reference/craft.md +5 -0
  322. package/skills/impeccable/reference/critique.md +788 -0
  323. package/skills/impeccable/reference/degraded/asset-producer.md +90 -0
  324. package/skills/impeccable/reference/degraded/documenter.md +24 -0
  325. package/skills/impeccable/reference/degraded/finish-reviewer.md +37 -0
  326. package/skills/impeccable/reference/degraded/manual-edit-applier.md +92 -0
  327. package/skills/impeccable/reference/delight.md +70 -0
  328. package/skills/impeccable/reference/distill.md +111 -0
  329. package/skills/impeccable/reference/doctor.md +53 -0
  330. package/skills/impeccable/reference/document.md +416 -0
  331. package/skills/impeccable/reference/extract.md +69 -0
  332. package/skills/impeccable/reference/harden.md +336 -0
  333. package/skills/impeccable/reference/hooks.md +105 -0
  334. package/skills/impeccable/reference/init.md +125 -0
  335. package/skills/impeccable/reference/ios.md +45 -0
  336. package/skills/impeccable/reference/layout.md +84 -0
  337. package/skills/impeccable/reference/live-setup.md +102 -0
  338. package/skills/impeccable/reference/live.md +323 -0
  339. package/skills/impeccable/reference/new-work.md +110 -0
  340. package/skills/impeccable/reference/onboard.md +234 -0
  341. package/skills/impeccable/reference/operate.md +61 -0
  342. package/skills/impeccable/reference/optimize.md +258 -0
  343. package/skills/impeccable/reference/overdrive.md +127 -0
  344. package/skills/impeccable/reference/polish.md +97 -0
  345. package/skills/impeccable/reference/quieter.md +99 -0
  346. package/skills/impeccable/reference/routing.md +18 -0
  347. package/skills/impeccable/reference/shape.md +59 -0
  348. package/skills/impeccable/reference/typeset.md +80 -0
  349. package/skills/impeccable/reference/visualize.md +49 -0
  350. package/skills/impeccable/scripts/command-metadata.json +94 -0
  351. package/skills/impeccable/scripts/concept-seed.mjs +558 -0
  352. package/skills/impeccable/scripts/context-signals.mjs +334 -0
  353. package/skills/impeccable/scripts/context.mjs +1467 -0
  354. package/skills/impeccable/scripts/critique-storage.mjs +213 -0
  355. package/skills/impeccable/scripts/detect-csp.mjs +198 -0
  356. package/skills/impeccable/scripts/detect.mjs +21 -0
  357. package/skills/impeccable/scripts/detector/browser/injected/index.mjs +2023 -0
  358. package/skills/impeccable/scripts/detector/cli/main.mjs +438 -0
  359. package/skills/impeccable/scripts/detector/design-system.mjs +1046 -0
  360. package/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +8283 -0
  361. package/skills/impeccable/scripts/detector/detect-antipatterns.mjs +50 -0
  362. package/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +372 -0
  363. package/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +1168 -0
  364. package/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +1186 -0
  365. package/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +264 -0
  366. package/skills/impeccable/scripts/detector/engines/visual/screenshot-contrast.mjs +189 -0
  367. package/skills/impeccable/scripts/detector/findings.mjs +18 -0
  368. package/skills/impeccable/scripts/detector/node/file-system.mjs +203 -0
  369. package/skills/impeccable/scripts/detector/profile/profiler.mjs +166 -0
  370. package/skills/impeccable/scripts/detector/registry/antipatterns.mjs +617 -0
  371. package/skills/impeccable/scripts/detector/rules/checks.mjs +5580 -0
  372. package/skills/impeccable/scripts/detector/shared/color.mjs +124 -0
  373. package/skills/impeccable/scripts/detector/shared/constants.mjs +112 -0
  374. package/skills/impeccable/scripts/detector/shared/fonts.mjs +30 -0
  375. package/skills/impeccable/scripts/detector/shared/inline-ignores.mjs +148 -0
  376. package/skills/impeccable/scripts/detector/shared/page.mjs +7 -0
  377. package/skills/impeccable/scripts/doctor.mjs +336 -0
  378. package/skills/impeccable/scripts/embed-prompt.mjs +133 -0
  379. package/skills/impeccable/scripts/generate-image.mjs +240 -0
  380. package/skills/impeccable/scripts/hook-admin.mjs +801 -0
  381. package/skills/impeccable/scripts/hook-before-edit.mjs +508 -0
  382. package/skills/impeccable/scripts/hook-lib.mjs +2153 -0
  383. package/skills/impeccable/scripts/hook.mjs +78 -0
  384. package/skills/impeccable/scripts/lib/artifact-schema.mjs +93 -0
  385. package/skills/impeccable/scripts/lib/composition-catalog.mjs +200 -0
  386. package/skills/impeccable/scripts/lib/concept-catalog.mjs +384 -0
  387. package/skills/impeccable/scripts/lib/design-parser.mjs +925 -0
  388. package/skills/impeccable/scripts/lib/impeccable-config.mjs +658 -0
  389. package/skills/impeccable/scripts/lib/impeccable-paths.mjs +137 -0
  390. package/skills/impeccable/scripts/lib/is-generated.mjs +69 -0
  391. package/skills/impeccable/scripts/lib/provider.mjs +5 -0
  392. package/skills/impeccable/scripts/lib/roll-selection.mjs +362 -0
  393. package/skills/impeccable/scripts/lib/staleness-deep.mjs +478 -0
  394. package/skills/impeccable/scripts/lib/staleness-notice.mjs +169 -0
  395. package/skills/impeccable/scripts/lib/staleness.mjs +457 -0
  396. package/skills/impeccable/scripts/lib/surface-briefs.mjs +151 -0
  397. package/skills/impeccable/scripts/lib/target-args.mjs +42 -0
  398. package/skills/impeccable/scripts/lib/target-slug.mjs +33 -0
  399. package/skills/impeccable/scripts/lib/template-extensions.mjs +146 -0
  400. package/skills/impeccable/scripts/live/accept-css.mjs +617 -0
  401. package/skills/impeccable/scripts/live/accept-verify.mjs +60 -0
  402. package/skills/impeccable/scripts/live/browser-script-parts.mjs +55 -0
  403. package/skills/impeccable/scripts/live/completion.mjs +28 -0
  404. package/skills/impeccable/scripts/live/event-validation.mjs +199 -0
  405. package/skills/impeccable/scripts/live/frameworks/astro.mjs +47 -0
  406. package/skills/impeccable/scripts/live/frameworks/detect-utils.mjs +73 -0
  407. package/skills/impeccable/scripts/live/frameworks/index.mjs +143 -0
  408. package/skills/impeccable/scripts/live/frameworks/journal.mjs +197 -0
  409. package/skills/impeccable/scripts/live/frameworks/nextjs.mjs +49 -0
  410. package/skills/impeccable/scripts/live/frameworks/nuxt.mjs +161 -0
  411. package/skills/impeccable/scripts/live/frameworks/script-src.mjs +17 -0
  412. package/skills/impeccable/scripts/live/frameworks/static-html.mjs +26 -0
  413. package/skills/impeccable/scripts/live/frameworks/sveltekit.mjs +71 -0
  414. package/skills/impeccable/scripts/live/frameworks/tag-strategy.mjs +247 -0
  415. package/skills/impeccable/scripts/live/frameworks/tanstack-start.mjs +70 -0
  416. package/skills/impeccable/scripts/live/frameworks/vite-generic.mjs +42 -0
  417. package/skills/impeccable/scripts/live/generation-preflight.mjs +149 -0
  418. package/skills/impeccable/scripts/live/insert-ui.mjs +458 -0
  419. package/skills/impeccable/scripts/live/instructions.mjs +142 -0
  420. package/skills/impeccable/scripts/live/manual-apply.mjs +939 -0
  421. package/skills/impeccable/scripts/live/manual-edit-routes.mjs +357 -0
  422. package/skills/impeccable/scripts/live/manual-edits-buffer.mjs +152 -0
  423. package/skills/impeccable/scripts/live/poll-lanes.mjs +14 -0
  424. package/skills/impeccable/scripts/live/roots.mjs +508 -0
  425. package/skills/impeccable/scripts/live/session-store.mjs +563 -0
  426. package/skills/impeccable/scripts/live/source-lock.mjs +105 -0
  427. package/skills/impeccable/scripts/live/source-search.mjs +105 -0
  428. package/skills/impeccable/scripts/live/svelte-ast.mjs +961 -0
  429. package/skills/impeccable/scripts/live/svelte-component.mjs +1342 -0
  430. package/skills/impeccable/scripts/live/sveltekit-adapter.mjs +316 -0
  431. package/skills/impeccable/scripts/live/tanstack-adapter.mjs +280 -0
  432. package/skills/impeccable/scripts/live/ui-core.mjs +180 -0
  433. package/skills/impeccable/scripts/live/vocabulary.mjs +171 -0
  434. package/skills/impeccable/scripts/live-accept.mjs +954 -0
  435. package/skills/impeccable/scripts/live-browser-dom.js +146 -0
  436. package/skills/impeccable/scripts/live-browser-session.js +123 -0
  437. package/skills/impeccable/scripts/live-browser.js +12520 -0
  438. package/skills/impeccable/scripts/live-commit-manual-edits.mjs +1244 -0
  439. package/skills/impeccable/scripts/live-complete.mjs +107 -0
  440. package/skills/impeccable/scripts/live-copy-edit-agent.mjs +683 -0
  441. package/skills/impeccable/scripts/live-discard-manual-edits.mjs +51 -0
  442. package/skills/impeccable/scripts/live-inject.mjs +503 -0
  443. package/skills/impeccable/scripts/live-insert.mjs +292 -0
  444. package/skills/impeccable/scripts/live-manual-edit-evidence.mjs +368 -0
  445. package/skills/impeccable/scripts/live-poll.mjs +429 -0
  446. package/skills/impeccable/scripts/live-resume.mjs +123 -0
  447. package/skills/impeccable/scripts/live-server.mjs +1669 -0
  448. package/skills/impeccable/scripts/live-status.mjs +71 -0
  449. package/skills/impeccable/scripts/live-target.mjs +30 -0
  450. package/skills/impeccable/scripts/live-wrap.mjs +927 -0
  451. package/skills/impeccable/scripts/live.mjs +359 -0
  452. package/skills/impeccable/scripts/modern-screenshot.umd.js +14 -0
  453. package/skills/impeccable/scripts/palette.mjs +628 -0
  454. package/skills/impeccable/scripts/pin.mjs +221 -0
  455. package/skills/impeccable/scripts/serve-question.mjs +932 -0
  456. package/skills/impeccable/scripts/surface-brief.mjs +74 -0
  457. package/skills/inkstone-admin/SKILL.md +66 -0
  458. package/skills/jev-hook-gate/SKILL.md +111 -0
  459. package/skills/keyword-research/SKILL.md +503 -0
  460. package/skills/keyword-research/references/dr-cap-and-confidence.md +62 -0
  461. package/skills/keyword-research/references/keyword-examples.md +769 -0
  462. package/skills/landing-page-architect/SKILL.md +585 -0
  463. package/skills/landing-page-architect/references/architecture-reference.md +32 -0
  464. package/skills/launch-strategy/SKILL.md +384 -0
  465. package/skills/launch-strategy/evals/evals.json +91 -0
  466. package/skills/launch-strategy/references/launch-checklist.md +209 -0
  467. package/skills/launch-strategy/references/launch-timeline.md +159 -0
  468. package/skills/lead-email-craft/SKILL.md +522 -0
  469. package/skills/lead-magnets/SKILL.md +310 -0
  470. package/skills/lead-magnets/references/benchmarks.md +129 -0
  471. package/skills/lead-magnets/references/format-guide.md +196 -0
  472. package/skills/link-building-seo/SKILL.md +210 -0
  473. package/skills/lint-report.txt +1 -0
  474. package/skills/manifest.json +129 -0
  475. package/skills/marketing-demo/SKILL.md +249 -0
  476. package/skills/marketing-demo/references/remotion-examples.md +281 -0
  477. package/skills/marketing-ideas/SKILL.md +167 -0
  478. package/skills/marketing-ideas/evals/evals.json +90 -0
  479. package/skills/marketing-ideas/references/ideas-by-category.md +366 -0
  480. package/skills/marketing-psychology/SKILL.md +374 -0
  481. package/skills/marketing-psychology/evals/evals.json +88 -0
  482. package/skills/marketing-psychology/references/case-studies.md +148 -0
  483. package/skills/narrative-research-article/SKILL.md +469 -0
  484. package/skills/narrative-research-article/references/lanusse-patterns.md +166 -0
  485. package/skills/notebooklm/SKILL.md +674 -0
  486. package/skills/off-page-seo/SKILL.md +219 -0
  487. package/skills/onboarding-cro/SKILL.md +220 -0
  488. package/skills/onboarding-cro/evals/evals.json +92 -0
  489. package/skills/onboarding-cro/references/experiments.md +258 -0
  490. package/skills/original-research/SKILL.md +388 -0
  491. package/skills/page-cro/SKILL.md +269 -0
  492. package/skills/page-cro/references/cro-benchmarks.md +140 -0
  493. package/skills/paper-marketing/SKILL.md +448 -0
  494. package/skills/paper-marketing/references/agent-quality-rubric.md +68 -0
  495. package/skills/paper-marketing/references/brand-system-template.md +120 -0
  496. package/skills/paper-marketing/references/paper-mcp-rules.md +71 -0
  497. package/skills/paper-marketing/references/platform-conventions.md +43 -0
  498. package/skills/paywall-upgrade-cro/SKILL.md +227 -0
  499. package/skills/paywall-upgrade-cro/evals/evals.json +93 -0
  500. package/skills/paywall-upgrade-cro/references/experiments.md +164 -0
  501. package/skills/platform-formatting/SKILL.md +51 -0
  502. package/skills/popup-cro/SKILL.md +454 -0
  503. package/skills/popup-cro/evals/evals.json +94 -0
  504. package/skills/positioning-angles/SKILL.md +524 -0
  505. package/skills/positioning-angles/references/ad-matrix-and-examples.md +176 -0
  506. package/skills/positioning-angles/references/angle-frameworks.md +187 -0
  507. package/skills/positioning-angles/references/dunford-positioning.md +135 -0
  508. package/skills/positioning-angles/references/hormozi-offer.md +247 -0
  509. package/skills/positioning-angles/references/schwartz-sophistication.md +166 -0
  510. package/skills/positioning-angles/references/unique-mechanism.md +162 -0
  511. package/skills/pricing-strategy/SKILL.md +346 -0
  512. package/skills/pricing-strategy/evals/evals.json +90 -0
  513. package/skills/pricing-strategy/references/research-methods.md +152 -0
  514. package/skills/pricing-strategy/references/tier-structure.md +307 -0
  515. package/skills/programmatic-seo/SKILL.md +238 -0
  516. package/skills/programmatic-seo/evals/evals.json +94 -0
  517. package/skills/programmatic-seo/references/playbooks.md +308 -0
  518. package/skills/project-rag/SKILL.md +127 -0
  519. package/skills/quality-gate/SKILL.md +519 -0
  520. package/skills/quality-gate/judge-personas/buyer.md +47 -0
  521. package/skills/quality-gate/judge-personas/expert.md +47 -0
  522. package/skills/quality-gate/judge-personas/outsider.md +47 -0
  523. package/skills/quality-gate/judge-personas/scanner.md +41 -0
  524. package/skills/quality-gate/judge-personas/skeptic.md +40 -0
  525. package/skills/referral-program/SKILL.md +290 -0
  526. package/skills/referral-program/evals/evals.json +89 -0
  527. package/skills/referral-program/references/affiliate-programs.md +163 -0
  528. package/skills/referral-program/references/program-examples.md +143 -0
  529. package/skills/review-sites/SKILL.md +279 -0
  530. package/skills/revops/SKILL.md +343 -0
  531. package/skills/revops/evals/evals.json +91 -0
  532. package/skills/revops/references/automation-playbooks.md +290 -0
  533. package/skills/revops/references/lifecycle-definitions.md +278 -0
  534. package/skills/revops/references/routing-rules.md +203 -0
  535. package/skills/revops/references/scoring-models.md +247 -0
  536. package/skills/saas-product-seo/SKILL.md +324 -0
  537. package/skills/sales-enablement/SKILL.md +349 -0
  538. package/skills/sales-enablement/evals/evals.json +91 -0
  539. package/skills/sales-enablement/references/deck-frameworks.md +263 -0
  540. package/skills/sales-enablement/references/demo-scripts.md +355 -0
  541. package/skills/sales-enablement/references/objection-library.md +270 -0
  542. package/skills/sales-enablement/references/one-pager-templates.md +208 -0
  543. package/skills/schema-markup/SKILL.md +179 -0
  544. package/skills/schema-markup/evals/evals.json +87 -0
  545. package/skills/schema-markup/references/schema-examples.md +398 -0
  546. package/skills/self-improve/SKILL.md +166 -0
  547. package/skills/self-improving-agent/README.md +243 -0
  548. package/skills/self-improving-agent/SKILL.md +437 -0
  549. package/skills/self-improving-agent/scripts/__init__.py +16 -0
  550. package/skills/self-improving-agent/scripts/__pycache__/auto_detect.cpython-314.pyc +0 -0
  551. package/skills/self-improving-agent/scripts/__pycache__/nlm_curation.cpython-314.pyc +0 -0
  552. package/skills/self-improving-agent/scripts/__pycache__/self_improving_agent.cpython-314.pyc +0 -0
  553. package/skills/self-improving-agent/scripts/auto_detect.py +270 -0
  554. package/skills/self-improving-agent/scripts/init_expertise_registry.py +89 -0
  555. package/skills/self-improving-agent/scripts/nlm_curation.py +259 -0
  556. package/skills/self-improving-agent/scripts/self-improving-agent +113 -0
  557. package/skills/self-improving-agent/scripts/self_improving_agent.py +603 -0
  558. package/skills/self-improving-agent/scripts/test_curation_scorer.py +20 -0
  559. package/skills/self-improving-agent/scripts/test_self_improving.py +184 -0
  560. package/skills/send-email/SKILL.md +426 -0
  561. package/skills/send-email/references/batch-email-examples.md +807 -0
  562. package/skills/send-email/references/best-practices.md +453 -0
  563. package/skills/send-email/references/installation.md +123 -0
  564. package/skills/send-email/references/single-email-examples.md +470 -0
  565. package/skills/send-email/references/webhooks.md +221 -0
  566. package/skills/seo-audit/SKILL.md +362 -0
  567. package/skills/seo-audit/references/ai-writing-detection.md +200 -0
  568. package/skills/seo-audit/references/architecture-patterns.md +271 -0
  569. package/skills/seo-audit/references/schema-templates.md +427 -0
  570. package/skills/seo-content/SKILL.md +510 -0
  571. package/skills/seo-content/references/anti-ai-detection.md +184 -0
  572. package/skills/seo-content/references/content-structures.md +148 -0
  573. package/skills/seo-content/references/eeat-examples.md +235 -0
  574. package/skills/seo-content/references/on-page-seo.md +261 -0
  575. package/skills/seo-content/references/pseo-playbooks.md +226 -0
  576. package/skills/seo-content/references/schema-templates.md +104 -0
  577. package/skills/seo-content/references/serp-analysis-format.md +151 -0
  578. package/skills/seo-machine/SKILL.md +331 -0
  579. package/skills/seo-machine/assets/brand-template.md +73 -0
  580. package/skills/seo-machine/assets/link-inventory-template.md +74 -0
  581. package/skills/seo-machine/assets/roadmap-template.md +184 -0
  582. package/skills/seo-machine/references/api-stack-recipes.md +230 -0
  583. package/skills/seo-machine/references/exa-recipes.md +260 -0
  584. package/skills/seo-machine/references/manual-research.md +73 -0
  585. package/skills/seo-machine/references/methodology.md +87 -0
  586. package/skills/seo-machine/references/off-page.md +123 -0
  587. package/skills/seo-machine/references/patterns/alternatives.md +176 -0
  588. package/skills/seo-machine/references/patterns/compare.md +105 -0
  589. package/skills/seo-machine/references/patterns/playbooks.md +106 -0
  590. package/skills/seo-machine/references/patterns/use-case.md +109 -0
  591. package/skills/seo-machine/references/quality-bars.md +99 -0
  592. package/skills/seo-machine/references/stacks/astro.md +156 -0
  593. package/skills/seo-machine/references/stacks/detection.md +61 -0
  594. package/skills/seo-machine/references/stacks/markdown-fallback.md +181 -0
  595. package/skills/seo-machine/references/stacks/nextjs.md +108 -0
  596. package/skills/seo-machine/references/stacks/rails-inertia.md +99 -0
  597. package/skills/seo-machine/references/striking-distance.md +116 -0
  598. package/skills/seo-machine/references/technical-audit.md +153 -0
  599. package/skills/seo-machine/scripts/link_audit.py +265 -0
  600. package/skills/seo-machine/scripts/tech_audit.py +221 -0
  601. package/skills/seo-machine/scripts/word_count.py +154 -0
  602. package/skills/sharp-thesis-validator/SKILL.md +393 -0
  603. package/skills/sharp-thesis-validator/examples.md +186 -0
  604. package/skills/signup-flow-cro/SKILL.md +359 -0
  605. package/skills/signup-flow-cro/evals/evals.json +88 -0
  606. package/skills/site-architecture/SKILL.md +357 -0
  607. package/skills/site-architecture/evals/evals.json +88 -0
  608. package/skills/site-architecture/references/mermaid-templates.md +216 -0
  609. package/skills/site-architecture/references/navigation-patterns.md +305 -0
  610. package/skills/site-architecture/references/site-type-templates.md +293 -0
  611. package/skills/skill-self-improvement/SKILL.md +151 -0
  612. package/skills/slideshow-script/SKILL.md +274 -0
  613. package/skills/slideshow-script/references/content-spec-schema.md +77 -0
  614. package/skills/slideshow-script/references/frameworks.md +84 -0
  615. package/skills/slideshow-script/references/paips-framework.md +37 -0
  616. package/skills/social-campaign/SKILL.md +419 -0
  617. package/skills/social-campaign/references/paper-mcp-workflow.md +48 -0
  618. package/skills/social-content/SKILL.md +359 -0
  619. package/skills/social-content/evals/evals.json +92 -0
  620. package/skills/social-content/references/platforms.md +170 -0
  621. package/skills/social-content/references/post-templates.md +177 -0
  622. package/skills/social-content/references/reverse-engineering.md +195 -0
  623. package/skills/social-worker/SKILL.md +204 -0
  624. package/skills/startup-launcher/SKILL.md +252 -0
  625. package/skills/startup-launcher/references/file-router.md +64 -0
  626. package/skills/startup-launcher/references/platform-matrix.md +65 -0
  627. package/skills/startup-launcher/references/platforms/alternativeto.md +283 -0
  628. package/skills/startup-launcher/references/platforms/appsumo.md +495 -0
  629. package/skills/startup-launcher/references/platforms/auraplusplus.md +736 -0
  630. package/skills/startup-launcher/references/platforms/betalist.md +98 -0
  631. package/skills/startup-launcher/references/platforms/capterra.md +276 -0
  632. package/skills/startup-launcher/references/platforms/crunchbase.md +514 -0
  633. package/skills/startup-launcher/references/platforms/dealify.md +291 -0
  634. package/skills/startup-launcher/references/platforms/dealmirror.md +313 -0
  635. package/skills/startup-launcher/references/platforms/desifounder.md +490 -0
  636. package/skills/startup-launcher/references/platforms/devhunt.md +210 -0
  637. package/skills/startup-launcher/references/platforms/devpost.md +448 -0
  638. package/skills/startup-launcher/references/platforms/fazier.md +56 -0
  639. package/skills/startup-launcher/references/platforms/firsto.md +489 -0
  640. package/skills/startup-launcher/references/platforms/g2.md +226 -0
  641. package/skills/startup-launcher/references/platforms/hackernews.md +99 -0
  642. package/skills/startup-launcher/references/platforms/hackernoon.md +385 -0
  643. package/skills/startup-launcher/references/platforms/indiedeals.md +367 -0
  644. package/skills/startup-launcher/references/platforms/indiehackers.md +80 -0
  645. package/skills/startup-launcher/references/platforms/kenmoo.md +231 -0
  646. package/skills/startup-launcher/references/platforms/labstartups.md +618 -0
  647. package/skills/startup-launcher/references/platforms/launchboard.md +307 -0
  648. package/skills/startup-launcher/references/platforms/launchfast.md +560 -0
  649. package/skills/startup-launcher/references/platforms/launchigniter.md +56 -0
  650. package/skills/startup-launcher/references/platforms/launchingnext.md +674 -0
  651. package/skills/startup-launcher/references/platforms/launchit.md +574 -0
  652. package/skills/startup-launcher/references/platforms/launchyourapp.md +677 -0
  653. package/skills/startup-launcher/references/platforms/libhunt.md +220 -0
  654. package/skills/startup-launcher/references/platforms/ltdhunt.md +221 -0
  655. package/skills/startup-launcher/references/platforms/microlaunch.md +45 -0
  656. package/skills/startup-launcher/references/platforms/openalternative.md +195 -0
  657. package/skills/startup-launcher/references/platforms/peerlist.md +56 -0
  658. package/skills/startup-launcher/references/platforms/peerpush.md +64 -0
  659. package/skills/startup-launcher/references/platforms/primeclub.md +282 -0
  660. package/skills/startup-launcher/references/platforms/productcanyon.md +190 -0
  661. package/skills/startup-launcher/references/platforms/producthunt.md +98 -0
  662. package/skills/startup-launcher/references/platforms/rankinpublic.md +315 -0
  663. package/skills/startup-launcher/references/platforms/rockethub.md +374 -0
  664. package/skills/startup-launcher/references/platforms/saasgenius.md +175 -0
  665. package/skills/startup-launcher/references/platforms/saashub.md +205 -0
  666. package/skills/startup-launcher/references/platforms/saasmantra.md +221 -0
  667. package/skills/startup-launcher/references/platforms/saaspirate.md +287 -0
  668. package/skills/startup-launcher/references/platforms/saaswarrior.md +190 -0
  669. package/skills/startup-launcher/references/platforms/saaszilla.md +194 -0
  670. package/skills/startup-launcher/references/platforms/selected.md +925 -0
  671. package/skills/startup-launcher/references/platforms/sideprojectors.md +86 -0
  672. package/skills/startup-launcher/references/platforms/softonic.md +240 -0
  673. package/skills/startup-launcher/references/platforms/sourceforge.md +251 -0
  674. package/skills/startup-launcher/references/platforms/stacksocial.md +244 -0
  675. package/skills/startup-launcher/references/platforms/startupstash.md +326 -0
  676. package/skills/startup-launcher/references/platforms/theresanaiforthat.md +253 -0
  677. package/skills/startup-launcher/references/platforms/tinylaunch.md +70 -0
  678. package/skills/startup-launcher/references/platforms/tinystartups.md +72 -0
  679. package/skills/startup-launcher/references/platforms/toolfolio.md +291 -0
  680. package/skills/startup-launcher/references/platforms/trustmrr.md +194 -0
  681. package/skills/startup-launcher/references/platforms/trylaunch.md +531 -0
  682. package/skills/startup-launcher/references/platforms/uneed.md +57 -0
  683. package/skills/startup-launcher/references/templates/asset-checklist.md +218 -0
  684. package/skills/startup-launcher/references/templates/launch-tracker.md +195 -0
  685. package/skills/startup-launcher/references/templates/product-brief.md +98 -0
  686. package/skills/summarize/.skill-meta.json +13 -0
  687. package/skills/summarize/SKILL.md +170 -0
  688. package/skills/summarize/rules/install.md +157 -0
  689. package/skills/summarize/rules/usage.md +207 -0
  690. package/skills/topical-authority/SKILL.md +307 -0
  691. package/skills/topical-authority/references/jake-ward-framework.md +201 -0
  692. package/skills/topical-authority/references/serp-clustering-algorithm.md +325 -0
  693. package/skills/topical-authority/scripts/orchestrator.js +367 -0
  694. package/skills/topical-authority/scripts/seo/analyze-competitor-coverage.js +3 -0
  695. package/skills/topical-authority/scripts/seo/audit-internal-links.js +3 -0
  696. package/skills/topical-authority/scripts/seo/build-clusters-from-serp.js +3 -0
  697. package/skills/topical-authority/scripts/seo/extract-keywords-from-forums.js +3 -0
  698. package/skills/topical-authority/scripts/seo/extract-paa-questions.js +3 -0
  699. package/skills/topical-authority/scripts/seo/fetch-serp-urls.js +3 -0
  700. package/skills/topical-authority/scripts/seo/gemini-serp-clustering.js +3 -0
  701. package/skills/topical-authority/scripts/seo/identify-content-gaps.js +3 -0
  702. package/skills/topical-authority/scripts/seo/identify-research-gaps.js +3 -0
  703. package/skills/topical-authority/scripts/seo/scrape-quora-questions.js +3 -0
  704. package/skills/topical-authority/scripts/seo/scrape-reddit-pain-points.js +3 -0
  705. package/skills/topical-authority/scripts/seo/weave-internal-links.js +3 -0
  706. package/skills/visual-style/SKILL.md +199 -0
  707. package/skills/voice-brand-alignment/SKILL.md +70 -0
  708. package/skills/voice-brand-alignment/references/ai-slop-patterns.md +405 -0
  709. package/skills/voice-brand-alignment/scripts/validation/gemini-voice-validator.js +96 -0
  710. package/skills/voice-extraction/SKILL.md +234 -0
  711. package/skills/voice-extraction/references/extraction-prompt.md +155 -0
  712. package/skills/voice-extraction/references/voice-dna-template.md +168 -0
  713. package/skills/voice-extraction/references/voice-elements.md +259 -0
  714. package/skills/voice-extraction/scripts/build-corpus.js +120 -0
  715. package/skills/voice-extraction/scripts/collect-sources.sh +168 -0
  716. package/skills/warm-outbound/SKILL.md +299 -0
  717. package/skills/warm-outbound/templates/warm-message.md +39 -0
  718. package/skills/weekly-newsletter-digest/SKILL.md +78 -0
  719. package/skills/x-playwriter/SKILL.md +428 -0
  720. package/skills/x-playwriter/scripts/x-browser.js +185 -0
  721. package/skills/x-post-generator-openclaw/SKILL.md +138 -0
  722. package/skills/x-post-generator-openclaw/scripts/generate-x-post.js +537 -0
  723. package/skills/x-posting-playwriter/SKILL.md +203 -0
  724. package/skills/x-research/SKILL.md +129 -0
  725. package/skills/x-research/x-research.sh +85 -0
  726. package/skills/youtube-summarize/SKILL.md +90 -0
  727. package/skills/youtube-summarize/scripts/youtube-summarize.sh +143 -0
  728. package/src/cli.js +349 -172
  729. package/src/harness/deepseek-adapter.js +102 -0
  730. package/src/harness/deepseek.js +225 -0
  731. package/src/harness/opencode-adapter.js +174 -0
  732. package/src/harness/pi-adapter.js +172 -0
  733. package/src/harness/registry.js +121 -0
  734. package/src/harness/telemetry.js +102 -0
  735. package/src/harness/types.js +165 -0
  736. package/src/interview.js +36 -4
  737. package/src/postinstall.js +10 -12
  738. package/templates/AGENTS.md +17 -17
  739. package/templates/ORG.md +1 -1
  740. package/templates/TOOLS.md +7 -2
  741. package/templates/opencode.json +26 -29
  742. package/wiki/doctor.md +54 -0
  743. package/wiki/inkstone.md +4 -4
  744. package/wiki/opencode.md +3 -3
  745. package/skills/curated.txt +0 -68
@@ -0,0 +1,1091 @@
1
+ """
2
+ tests/test_engram.py — Tests for the Engram (Observational Memory) layer.
3
+
4
+ All LLM calls are mocked so no API key is required.
5
+
6
+ Run with:
7
+ pytest tests/test_engram.py -v
8
+
9
+ Part of claw-compactor / Engram layer. License: MIT.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import sys
16
+ import tempfile
17
+ from pathlib import Path
18
+ from typing import List
19
+ from unittest.mock import MagicMock, patch
20
+
21
+ import pytest
22
+
23
+ # Ensure scripts/ is on path
24
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
25
+
26
+ from lib.engram import (
27
+ EngramEngine,
28
+ _count_messages_tokens,
29
+ _messages_to_text,
30
+ MAX_OBSERVER_INPUT_TOKENS,
31
+ MAX_REFLECTOR_INPUT_TOKENS,
32
+ )
33
+ from lib.engram_storage import EngramStorage
34
+ from lib.engram_prompts import (
35
+ OBSERVER_SYSTEM_PROMPT,
36
+ REFLECTOR_SYSTEM_PROMPT,
37
+ OBSERVER_USER_TEMPLATE,
38
+ REFLECTOR_USER_TEMPLATE,
39
+ )
40
+ from lib.tokens import estimate_tokens
41
+
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # Fixtures
45
+ # ---------------------------------------------------------------------------
46
+
47
+ @pytest.fixture
48
+ def workspace(tmp_path: Path) -> Path:
49
+ """Return a fresh temporary workspace directory."""
50
+ return tmp_path
51
+
52
+
53
+ @pytest.fixture
54
+ def engine(workspace: Path) -> EngramEngine:
55
+ """Return an EngramEngine with very low thresholds for easy triggering."""
56
+ return EngramEngine(
57
+ workspace_path=workspace,
58
+ observer_threshold=50, # low: trigger after ~50 tokens of pending msgs
59
+ reflector_threshold=100, # low: trigger after ~100 tokens of observations
60
+ anthropic_api_key="test-key", # fake key — calls will be mocked
61
+ )
62
+
63
+
64
+ # ---------------------------------------------------------------------------
65
+ # Fake LLM outputs
66
+ # ---------------------------------------------------------------------------
67
+
68
+ FAKE_OBSERVATION = """\
69
+ Date: 2026-03-05
70
+ - 🔴 12:10 User is building OpenCompress / 用户在构建 OpenCompress 项目
71
+ - 🟡 12:11 Using ModernBERT-large for inference
72
+ - 🟢 12:15 User prefers concise replies
73
+ """
74
+
75
+ FAKE_REFLECTION = """\
76
+ ## Persistent Context (long-term patterns & facts)
77
+ - 🔴 User is building OpenCompress, uses ModernBERT-large (repeated)
78
+
79
+ ## Recent Events (chronological, compressed)
80
+ Date: 2026-03-05
81
+ - 🔴 12:10 Building OpenCompress project
82
+ - 🟢 12:15 Prefers concise replies
83
+ """
84
+
85
+
86
+ def _make_engine_with_mock(workspace: Path, mock_response: str, **kwargs) -> EngramEngine:
87
+ """Helper: create engine and patch _call_llm to return mock_response."""
88
+ eng = EngramEngine(
89
+ workspace_path=workspace,
90
+ observer_threshold=kwargs.get("observer_threshold", 50),
91
+ reflector_threshold=kwargs.get("reflector_threshold", 100),
92
+ anthropic_api_key="test-key",
93
+ )
94
+ eng._call_llm = MagicMock(return_value=mock_response) # type: ignore[method-assign]
95
+ return eng
96
+
97
+
98
+ # ---------------------------------------------------------------------------
99
+ # Test 1: add_message auto-triggers observe
100
+ # ---------------------------------------------------------------------------
101
+
102
+ class TestAddMessageAutoObserve:
103
+ """add_message() should auto-trigger the Observer when pending tokens exceed threshold."""
104
+
105
+ def test_auto_observe_triggered(self, workspace: Path) -> None:
106
+ eng = _make_engine_with_mock(workspace, FAKE_OBSERVATION, observer_threshold=10)
107
+
108
+ # Add enough content to exceed the threshold
109
+ status = eng.add_message("t1", role="user", content="A" * 200)
110
+
111
+ assert status["observed"] is True, "Observer should have been triggered"
112
+ assert status["error"] is None
113
+
114
+ # Pending queue should be cleared after observe
115
+ pending = eng.storage.read_pending("t1")
116
+ assert len(pending) == 0
117
+
118
+ # Observation should be saved
119
+ obs = eng.storage.read_observations("t1")
120
+ assert "🔴" in obs or "Date:" in obs or FAKE_OBSERVATION.strip() in obs
121
+
122
+ def test_no_trigger_below_threshold(self, workspace: Path) -> None:
123
+ eng = _make_engine_with_mock(workspace, FAKE_OBSERVATION, observer_threshold=9999)
124
+
125
+ # Small message — should NOT trigger
126
+ status = eng.add_message("t2", role="user", content="Hello")
127
+
128
+ assert status["observed"] is False
129
+ assert status["reflected"] is False
130
+ assert status["pending_tokens"] > 0
131
+
132
+ def test_observe_clears_pending(self, workspace: Path) -> None:
133
+ eng = _make_engine_with_mock(workspace, FAKE_OBSERVATION, observer_threshold=5)
134
+ eng.add_message("t3", role="user", content="X" * 100)
135
+ eng.add_message("t3", role="assistant", content="Y" * 100)
136
+
137
+ # At least one should have triggered observe
138
+ pending = eng.storage.read_pending("t3")
139
+ assert len(pending) == 0
140
+
141
+ def test_reflect_auto_triggered(self, workspace: Path) -> None:
142
+ """Reflector should auto-trigger when accumulated observations exceed threshold."""
143
+ eng = _make_engine_with_mock(
144
+ workspace, FAKE_OBSERVATION,
145
+ observer_threshold=5,
146
+ reflector_threshold=10,
147
+ )
148
+
149
+ # Seed observations directly to exceed reflector threshold
150
+ large_obs = "Date: 2026-01-01\n" + ("- 🔴 12:00 item\n" * 20)
151
+ eng.storage.append_observation("t4", large_obs)
152
+
153
+ # Now add a message — it checks obs tokens after observe
154
+ status = eng.add_message("t4", role="user", content="Trigger reflect " * 5)
155
+ # Either observed or reflected should be true (or both)
156
+ assert status["reflected"] is True or status["observed"] is True
157
+
158
+
159
+ # ---------------------------------------------------------------------------
160
+ # Test 2: observe output format
161
+ # ---------------------------------------------------------------------------
162
+
163
+ class TestObserveOutputFormat:
164
+ """Observer output should contain emoji priorities and date headers."""
165
+
166
+ def test_emoji_format_present(self, workspace: Path) -> None:
167
+ eng = _make_engine_with_mock(workspace, FAKE_OBSERVATION)
168
+ eng.storage.append_message("t5", {"role": "user", "content": "Hello", "timestamp": "12:00"})
169
+
170
+ result = eng.observe("t5")
171
+ assert result is not None
172
+ # Must contain priority emojis
173
+ assert "🔴" in result
174
+ assert "Date:" in result
175
+
176
+ def test_date_line_format(self, workspace: Path) -> None:
177
+ eng = _make_engine_with_mock(workspace, FAKE_OBSERVATION)
178
+ eng.storage.append_message("t6", {"role": "user", "content": "Test", "timestamp": "12:00"})
179
+
180
+ result = eng.observe("t6")
181
+ assert result is not None
182
+ lines = result.strip().splitlines()
183
+ # First non-empty line should match "Date: YYYY-MM-DD"
184
+ date_lines = [l for l in lines if l.startswith("Date:")]
185
+ assert len(date_lines) >= 1
186
+ for dl in date_lines:
187
+ parts = dl.split()
188
+ assert len(parts) == 2
189
+ assert parts[0] == "Date:"
190
+ # Validate date format YYYY-MM-DD
191
+ date_str = parts[1]
192
+ assert len(date_str) == 10
193
+ assert date_str[4] == "-" and date_str[7] == "-"
194
+
195
+ def test_no_pending_returns_none(self, workspace: Path) -> None:
196
+ eng = _make_engine_with_mock(workspace, FAKE_OBSERVATION)
197
+ result = eng.observe("empty-thread")
198
+ assert result is None
199
+
200
+
201
+ # ---------------------------------------------------------------------------
202
+ # Test 3: reflect reduces tokens
203
+ # ---------------------------------------------------------------------------
204
+
205
+ class TestReflectReducesTokens:
206
+ """Reflector output should be smaller than accumulated observations."""
207
+
208
+ def test_reflection_smaller_than_input(self, workspace: Path) -> None:
209
+ # Build up a large observation log
210
+ blocks = []
211
+ for i in range(20):
212
+ blocks.extend([
213
+ "Date: 2026-01-0" + str(i % 9 + 1),
214
+ "- 🔴 12:00 Critical item about project X spanning multiple lines of text",
215
+ "- 🟡 12:30 Important technical detail about ModernBERT-large model inference",
216
+ "- 🟢 13:00 Useful background information about deployment strategy on M3",
217
+ ])
218
+ large_obs = "\n".join(blocks)
219
+
220
+ eng = _make_engine_with_mock(workspace, FAKE_REFLECTION)
221
+ eng.storage.append_observation("t7", large_obs)
222
+
223
+ result = eng.reflect("t7")
224
+ assert result is not None
225
+
226
+ input_tokens = estimate_tokens(large_obs)
227
+ output_tokens = estimate_tokens(result)
228
+
229
+ # Reflection should be smaller (or at worst equal for tiny inputs)
230
+ assert output_tokens <= input_tokens, (
231
+ f"Reflection ({output_tokens} tokens) should be ≤ input ({input_tokens} tokens)"
232
+ )
233
+
234
+ def test_reflection_saved_to_storage(self, workspace: Path) -> None:
235
+ eng = _make_engine_with_mock(workspace, FAKE_REFLECTION)
236
+ eng.storage.append_observation("t8", FAKE_OBSERVATION)
237
+
238
+ eng.reflect("t8")
239
+
240
+ saved = eng.storage.read_reflection("t8")
241
+ assert FAKE_REFLECTION.strip() in saved
242
+
243
+
244
+ # ---------------------------------------------------------------------------
245
+ # Test 4: storage persistence
246
+ # ---------------------------------------------------------------------------
247
+
248
+ class TestStoragePersistence:
249
+ """Storage should persist across EngramEngine instances."""
250
+
251
+ def test_observations_persist(self, workspace: Path) -> None:
252
+ storage = EngramStorage(workspace)
253
+ storage.append_observation("t9", FAKE_OBSERVATION)
254
+
255
+ # Create a new storage instance pointing to same dir
256
+ storage2 = EngramStorage(workspace)
257
+ obs = storage2.read_observations("t9")
258
+ assert FAKE_OBSERVATION.strip() in obs
259
+
260
+ def test_pending_persist(self, workspace: Path) -> None:
261
+ storage = EngramStorage(workspace)
262
+ msg = {"role": "user", "content": "Hello persistent!", "timestamp": "12:00"}
263
+ storage.append_message("t10", msg)
264
+
265
+ storage2 = EngramStorage(workspace)
266
+ pending = storage2.read_pending("t10")
267
+ assert len(pending) == 1
268
+ assert pending[0]["content"] == "Hello persistent!"
269
+
270
+ def test_reflection_overwrites(self, workspace: Path) -> None:
271
+ storage = EngramStorage(workspace)
272
+ storage.write_reflection("t11", "First reflection")
273
+ storage.write_reflection("t11", "Second reflection")
274
+
275
+ content = storage.read_reflection("t11")
276
+ assert "Second reflection" in content
277
+ # First should not appear (it was overwritten)
278
+ assert "First reflection" not in content
279
+
280
+ def test_meta_persists(self, workspace: Path) -> None:
281
+ storage = EngramStorage(workspace)
282
+ storage.append_observation("t12", "test obs")
283
+
284
+ meta = storage.read_meta("t12")
285
+ assert meta["thread_id"] == "t12"
286
+ assert "last_observed_at" in meta
287
+
288
+ def test_clear_pending(self, workspace: Path) -> None:
289
+ storage = EngramStorage(workspace)
290
+ storage.append_message("t13", {"role": "user", "content": "A"})
291
+ storage.append_message("t13", {"role": "user", "content": "B"})
292
+ assert storage.pending_count("t13") == 2
293
+
294
+ storage.clear_pending("t13")
295
+ assert storage.pending_count("t13") == 0
296
+
297
+ def test_list_threads(self, workspace: Path) -> None:
298
+ storage = EngramStorage(workspace)
299
+ for tid in ["alpha", "beta", "gamma"]:
300
+ storage.append_message(tid, {"role": "user", "content": "x"})
301
+ # list_threads only returns threads with meta.json; force meta creation
302
+ for tid in ["alpha", "beta", "gamma"]:
303
+ storage.append_observation(tid, "obs")
304
+
305
+ threads = storage.list_threads()
306
+ for tid in ["alpha", "beta", "gamma"]:
307
+ assert tid in threads
308
+
309
+ def test_atomic_write_no_partial(self, workspace: Path) -> None:
310
+ """Atomic write: even if we verify after write, content is complete."""
311
+ storage = EngramStorage(workspace)
312
+ long_content = "x" * 100_000
313
+ storage.write_reflection("t14", long_content)
314
+
315
+ content = storage.read_reflection("t14")
316
+ assert long_content in content
317
+
318
+
319
+ # ---------------------------------------------------------------------------
320
+ # Test 5: get_context structure
321
+ # ---------------------------------------------------------------------------
322
+
323
+ class TestGetContextStructure:
324
+ """get_context() should return the expected dict structure."""
325
+
326
+ def test_context_keys(self, workspace: Path) -> None:
327
+ eng = _make_engine_with_mock(workspace, FAKE_OBSERVATION)
328
+ ctx = eng.get_context("t15")
329
+
330
+ assert "thread_id" in ctx
331
+ assert "observations" in ctx
332
+ assert "reflection" in ctx
333
+ assert "recent_messages" in ctx
334
+ assert "stats" in ctx
335
+ assert "meta" in ctx
336
+
337
+ def test_stats_keys(self, workspace: Path) -> None:
338
+ eng = _make_engine_with_mock(workspace, FAKE_OBSERVATION)
339
+ ctx = eng.get_context("t16")
340
+
341
+ stats = ctx["stats"]
342
+ assert "observation_tokens" in stats
343
+ assert "reflection_tokens" in stats
344
+ assert "pending_tokens" in stats
345
+ assert "total_tokens" in stats
346
+ assert "pending_count" in stats
347
+
348
+ def test_stats_values_correct(self, workspace: Path) -> None:
349
+ eng = _make_engine_with_mock(workspace, FAKE_OBSERVATION)
350
+ eng.storage.append_observation("t17", FAKE_OBSERVATION)
351
+ eng.storage.write_reflection("t17", FAKE_REFLECTION)
352
+ eng.storage.append_message("t17", {"role": "user", "content": "Pending msg"})
353
+
354
+ ctx = eng.get_context("t17")
355
+ assert ctx["stats"]["observation_tokens"] > 0
356
+ assert ctx["stats"]["reflection_tokens"] > 0
357
+ assert ctx["stats"]["pending_tokens"] > 0
358
+ assert ctx["stats"]["pending_count"] == 1
359
+ assert ctx["stats"]["total_tokens"] == (
360
+ ctx["stats"]["observation_tokens"]
361
+ + ctx["stats"]["reflection_tokens"]
362
+ + ctx["stats"]["pending_tokens"]
363
+ )
364
+
365
+ def test_build_system_context_includes_sections(self, workspace: Path) -> None:
366
+ eng = _make_engine_with_mock(workspace, FAKE_OBSERVATION)
367
+ eng.storage.append_observation("t18", FAKE_OBSERVATION)
368
+ eng.storage.write_reflection("t18", FAKE_REFLECTION)
369
+
370
+ ctx_str = eng.build_system_context("t18")
371
+ assert "Long-Term Memory" in ctx_str or "Reflections" in ctx_str
372
+ assert "Recent Observations" in ctx_str
373
+
374
+ def test_build_system_context_empty_thread(self, workspace: Path) -> None:
375
+ eng = _make_engine_with_mock(workspace, FAKE_OBSERVATION)
376
+ ctx_str = eng.build_system_context("nonexistent-thread")
377
+ assert ctx_str == ""
378
+
379
+
380
+ # ---------------------------------------------------------------------------
381
+ # Test 6: token counting accuracy
382
+ # ---------------------------------------------------------------------------
383
+
384
+ class TestTokenCountingAccuracy:
385
+ """Token counting should be consistent and reasonable."""
386
+
387
+ def test_empty_string_is_zero(self) -> None:
388
+ assert estimate_tokens("") == 0
389
+
390
+ def test_ascii_positive(self) -> None:
391
+ tokens = estimate_tokens("Hello world, this is a test sentence.")
392
+ assert tokens > 0
393
+
394
+ def test_cjk_positive(self) -> None:
395
+ tokens = estimate_tokens("用户在构建 OpenCompress 项目")
396
+ assert tokens > 0
397
+
398
+ def test_longer_text_more_tokens(self) -> None:
399
+ short = estimate_tokens("Hello")
400
+ long_ = estimate_tokens("Hello " * 100)
401
+ assert long_ > short
402
+
403
+ def test_messages_tokens_includes_overhead(self) -> None:
404
+ msgs = [{"role": "user", "content": "Hi"}]
405
+ tokens = _count_messages_tokens(msgs)
406
+ content_tokens = estimate_tokens("Hi")
407
+ # Should include at least 4 tokens of per-message overhead
408
+ assert tokens >= content_tokens + 4
409
+
410
+ def test_mixed_content_positive(self) -> None:
411
+ mixed = "Hello 世界 — testing mixed EN/ZH content with punctuation."
412
+ tokens = estimate_tokens(mixed)
413
+ assert tokens > 5
414
+
415
+ def test_messages_to_text_format(self) -> None:
416
+ msgs = [
417
+ {"role": "user", "content": "Hello", "timestamp": "12:00"},
418
+ {"role": "assistant", "content": "Hi there!", "timestamp": "12:01"},
419
+ ]
420
+ text = _messages_to_text(msgs)
421
+ assert "[1] USER [12:00]" in text
422
+ assert "[2] ASSISTANT [12:01]" in text
423
+ assert "Hello" in text
424
+ assert "Hi there!" in text
425
+
426
+
427
+ # ---------------------------------------------------------------------------
428
+ # Test 7: prompts integrity
429
+ # ---------------------------------------------------------------------------
430
+
431
+ class TestPrompts:
432
+ """Verify prompt contents and templates."""
433
+
434
+ def test_observer_prompt_has_emoji(self) -> None:
435
+ assert "🔴" in OBSERVER_SYSTEM_PROMPT
436
+ assert "🟡" in OBSERVER_SYSTEM_PROMPT
437
+ assert "🟢" in OBSERVER_SYSTEM_PROMPT
438
+
439
+ def test_observer_prompt_has_date_format(self) -> None:
440
+ assert "Date: YYYY-MM-DD" in OBSERVER_SYSTEM_PROMPT
441
+
442
+ def test_reflector_prompt_has_sections(self) -> None:
443
+ assert "Persistent Context" in REFLECTOR_SYSTEM_PROMPT
444
+ assert "Recent Events" in REFLECTOR_SYSTEM_PROMPT
445
+
446
+ def test_observer_template_format(self) -> None:
447
+ result = OBSERVER_USER_TEMPLATE.format(
448
+ current_datetime="2026-03-05 12:00 UTC",
449
+ messages_text="Test message",
450
+ )
451
+ assert "2026-03-05" in result
452
+ assert "Test message" in result
453
+
454
+ def test_reflector_template_format(self) -> None:
455
+ result = REFLECTOR_USER_TEMPLATE.format(
456
+ current_datetime="2026-03-05 12:00 UTC",
457
+ observations_text="Test observation",
458
+ )
459
+ assert "2026-03-05" in result
460
+ assert "Test observation" in result
461
+
462
+
463
+ # ---------------------------------------------------------------------------
464
+ # Test 8: LLM routing
465
+ # ---------------------------------------------------------------------------
466
+
467
+ class TestLLMRouting:
468
+ """Verify correct LLM provider selection."""
469
+
470
+ def test_no_api_key_raises(self, workspace: Path) -> None:
471
+ eng = EngramEngine(
472
+ workspace_path=workspace,
473
+ anthropic_api_key="",
474
+ openai_api_key="",
475
+ )
476
+ with pytest.raises(RuntimeError, match="no API key"):
477
+ eng._call_llm("system", "user")
478
+
479
+ def test_anthropic_key_calls_anthropic(self, workspace: Path) -> None:
480
+ eng = EngramEngine(workspace_path=workspace, anthropic_api_key="key123")
481
+ with patch.object(eng, "_call_anthropic", return_value="result") as mock_ant, \
482
+ patch.object(eng, "_call_openai_compatible", return_value="oai") as mock_oai:
483
+ result = eng._call_llm("sys", "usr")
484
+ mock_ant.assert_called_once()
485
+ mock_oai.assert_not_called()
486
+ assert result == "result"
487
+
488
+ def test_openai_key_calls_openai(self, workspace: Path) -> None:
489
+ eng = EngramEngine(
490
+ workspace_path=workspace,
491
+ anthropic_api_key="",
492
+ openai_api_key="oai-key",
493
+ )
494
+ with patch.object(eng, "_call_anthropic", return_value="ant") as mock_ant, \
495
+ patch.object(eng, "_call_openai_compatible", return_value="result") as mock_oai:
496
+ result = eng._call_llm("sys", "usr")
497
+ mock_oai.assert_called_once()
498
+ mock_ant.assert_not_called()
499
+ assert result == "result"
500
+
501
+ def test_anthropic_preferred_over_openai(self, workspace: Path) -> None:
502
+ eng = EngramEngine(
503
+ workspace_path=workspace,
504
+ anthropic_api_key="ant-key",
505
+ openai_api_key="oai-key",
506
+ )
507
+ with patch.object(eng, "_call_anthropic", return_value="ant-result") as mock_ant, \
508
+ patch.object(eng, "_call_openai_compatible", return_value="oai-result") as mock_oai:
509
+ result = eng._call_llm("sys", "usr")
510
+ mock_ant.assert_called_once()
511
+ mock_oai.assert_not_called()
512
+ assert result == "ant-result"
513
+
514
+
515
+ # ---------------------------------------------------------------------------
516
+ # Test 9: engram_cli integration (smoke tests)
517
+ # ---------------------------------------------------------------------------
518
+
519
+ class TestEngramCLI:
520
+ """Smoke tests for the CLI module."""
521
+
522
+ def test_cli_status_no_threads(self, workspace: Path, capsys) -> None:
523
+ import scripts.engram_cli as cli # type: ignore[import]
524
+ eng = EngramEngine(workspace_path=workspace, anthropic_api_key="test")
525
+ args = MagicMock()
526
+ args.thread = None
527
+ args.json = False
528
+ rc = cli.cmd_status(eng, args)
529
+ assert rc == 0
530
+ captured = capsys.readouterr()
531
+ assert "No Engram threads" in captured.out
532
+
533
+ def test_cli_observe_no_pending(self, workspace: Path, capsys) -> None:
534
+ import scripts.engram_cli as cli # type: ignore[import]
535
+ eng = EngramEngine(workspace_path=workspace, anthropic_api_key="test")
536
+ args = MagicMock()
537
+ args.thread = "empty"
538
+ args.json = False
539
+ rc = cli.cmd_observe(eng, args)
540
+ assert rc == 1 # no pending messages
541
+
542
+ def test_cli_context_empty(self, workspace: Path, capsys) -> None:
543
+ import scripts.engram_cli as cli # type: ignore[import]
544
+ eng = EngramEngine(workspace_path=workspace, anthropic_api_key="test")
545
+ args = MagicMock()
546
+ args.thread = "nonexistent"
547
+ args.json = False
548
+ rc = cli.cmd_context(eng, args)
549
+ assert rc == 1 # no context
550
+
551
+ def test_cli_context_with_data(self, workspace: Path, capsys) -> None:
552
+ import scripts.engram_cli as cli # type: ignore[import]
553
+ eng = EngramEngine(workspace_path=workspace, anthropic_api_key="test")
554
+ eng.storage.append_observation("cli-t1", FAKE_OBSERVATION)
555
+ args = MagicMock()
556
+ args.thread = "cli-t1"
557
+ args.json = False
558
+ rc = cli.cmd_context(eng, args)
559
+ assert rc == 0
560
+ captured = capsys.readouterr()
561
+ assert "Recent Observations" in captured.out
562
+
563
+ def test_cli_ingest_json(self, workspace: Path, tmp_path: Path, capsys) -> None:
564
+ import scripts.engram_cli as cli # type: ignore[import]
565
+ eng = _make_engine_with_mock(workspace, FAKE_OBSERVATION)
566
+
567
+ input_file = tmp_path / "msgs.json"
568
+ messages = [
569
+ {"role": "user", "content": "Hello from ingest test"},
570
+ {"role": "assistant", "content": "Response from ingest test"},
571
+ ]
572
+ input_file.write_text(json.dumps(messages))
573
+
574
+ args = MagicMock()
575
+ args.thread = "ingest-t1"
576
+ args.input = str(input_file)
577
+ args.json = False
578
+
579
+ rc = cli.cmd_ingest(eng, args)
580
+ assert rc == 0
581
+
582
+ # Verify messages were added
583
+ pending = eng.storage.read_pending("ingest-t1")
584
+ assert len(pending) == 2
585
+
586
+ def test_cli_ingest_jsonl(self, workspace: Path, tmp_path: Path, capsys) -> None:
587
+ import scripts.engram_cli as cli # type: ignore[import]
588
+ eng = _make_engine_with_mock(workspace, FAKE_OBSERVATION, observer_threshold=9999)
589
+
590
+ input_file = tmp_path / "msgs.jsonl"
591
+ lines = [
592
+ json.dumps({"role": "user", "content": "Line 1"}),
593
+ json.dumps({"role": "assistant", "content": "Line 2"}),
594
+ ]
595
+ input_file.write_text("\n".join(lines))
596
+
597
+ args = MagicMock()
598
+ args.thread = "ingest-t2"
599
+ args.input = str(input_file)
600
+ args.json = False
601
+
602
+ rc = cli.cmd_ingest(eng, args)
603
+ assert rc == 0
604
+ pending = eng.storage.read_pending("ingest-t2")
605
+ assert len(pending) == 2
606
+
607
+
608
+ # ---------------------------------------------------------------------------
609
+ # Test 10: edge cases
610
+ # ---------------------------------------------------------------------------
611
+
612
+ class TestEdgeCases:
613
+ """Edge cases and robustness tests."""
614
+
615
+ def test_unicode_content(self, workspace: Path) -> None:
616
+ eng = _make_engine_with_mock(workspace, FAKE_OBSERVATION, observer_threshold=9999)
617
+ status = eng.add_message(
618
+ "unicode-t",
619
+ role="user",
620
+ content="Unicode: 中文 🔴 émojis 日本語 한국어",
621
+ )
622
+ assert status["error"] is None
623
+
624
+ pending = eng.storage.read_pending("unicode-t")
625
+ assert len(pending) == 1
626
+ assert "中文" in pending[0]["content"]
627
+
628
+ def test_empty_content(self, workspace: Path) -> None:
629
+ eng = _make_engine_with_mock(workspace, FAKE_OBSERVATION, observer_threshold=9999)
630
+ status = eng.add_message("empty-t", role="user", content="")
631
+ assert status["error"] is None
632
+
633
+ def test_multiple_threads_isolated(self, workspace: Path) -> None:
634
+ eng = _make_engine_with_mock(workspace, FAKE_OBSERVATION, observer_threshold=9999)
635
+ eng.add_message("thread-A", role="user", content="Message for A")
636
+ eng.add_message("thread-B", role="user", content="Message for B")
637
+
638
+ pending_a = eng.storage.read_pending("thread-A")
639
+ pending_b = eng.storage.read_pending("thread-B")
640
+
641
+ assert len(pending_a) == 1
642
+ assert len(pending_b) == 1
643
+ assert pending_a[0]["content"] != pending_b[0]["content"]
644
+
645
+ def test_observation_appends(self, workspace: Path) -> None:
646
+ """Multiple observe runs should append, not overwrite."""
647
+ storage = EngramStorage(workspace)
648
+ storage.append_observation("append-t", "First observation block")
649
+ storage.append_observation("append-t", "Second observation block")
650
+
651
+ obs = storage.read_observations("append-t")
652
+ assert "First observation block" in obs
653
+ assert "Second observation block" in obs
654
+
655
+ def test_corrupt_jsonl_skipped(self, workspace: Path) -> None:
656
+ """Corrupt JSONL lines should be silently skipped."""
657
+ storage = EngramStorage(workspace)
658
+ pending_path = storage._pending_path("corrupt-t")
659
+ pending_path.parent.mkdir(parents=True, exist_ok=True)
660
+ with pending_path.open("w") as f:
661
+ f.write('{"role": "user", "content": "Good line"}\n')
662
+ f.write("NOT VALID JSON\n")
663
+ f.write('{"role": "assistant", "content": "Also good"}\n')
664
+
665
+ messages = storage.read_pending("corrupt-t")
666
+ assert len(messages) == 2
667
+ assert messages[0]["content"] == "Good line"
668
+ assert messages[1]["content"] == "Also good"
669
+
670
+
671
+ # ---------------------------------------------------------------------------
672
+ # Test 11: HTTP retry logic
673
+ # ---------------------------------------------------------------------------
674
+
675
+ class TestHttpRetry:
676
+ """_http_post() should retry on transient errors and not retry on 401/403."""
677
+
678
+ def test_http_retry_on_429(self, workspace: Path) -> None:
679
+ """Should retry on HTTP 429 and eventually succeed."""
680
+ import urllib.error
681
+ from lib.engram import _http_post
682
+
683
+ call_count = 0
684
+
685
+ def fake_urlopen(req, timeout=None):
686
+ nonlocal call_count
687
+ call_count += 1
688
+ if call_count < 3:
689
+ # Simulate 429 on first two attempts
690
+ err = urllib.error.HTTPError(
691
+ url="http://test", code=429,
692
+ msg="Too Many Requests", hdrs=None, fp=None, # type: ignore[arg-type]
693
+ )
694
+ # HTTPError.read() must be callable
695
+ err.read = lambda: b"rate limited"
696
+ raise err
697
+ # Third attempt succeeds: return a file-like object
698
+ import io
699
+ import json as _json
700
+
701
+ class FakeResp:
702
+ def read(self):
703
+ return _json.dumps({"ok": True}).encode()
704
+ def __enter__(self):
705
+ return self
706
+ def __exit__(self, *a):
707
+ pass
708
+
709
+ return FakeResp()
710
+
711
+ with patch("lib.engram._HTTPX_AVAILABLE", False), \
712
+ patch("urllib.request.urlopen", side_effect=fake_urlopen), \
713
+ patch("time.sleep"): # skip actual delays
714
+ result = _http_post("http://test", {}, {}, max_retries=3)
715
+
716
+ assert result == {"ok": True}
717
+ assert call_count == 3
718
+
719
+ def test_http_no_retry_on_401(self, workspace: Path) -> None:
720
+ """Should raise immediately on HTTP 401 (no retry)."""
721
+ import urllib.error
722
+ from lib.engram import _http_post
723
+
724
+ call_count = 0
725
+
726
+ def fake_urlopen(req, timeout=None):
727
+ nonlocal call_count
728
+ call_count += 1
729
+ err = urllib.error.HTTPError(
730
+ url="http://test", code=401,
731
+ msg="Unauthorized", hdrs=None, fp=None, # type: ignore[arg-type]
732
+ )
733
+ err.read = lambda: b"unauthorized"
734
+ raise err
735
+
736
+ with patch("lib.engram._HTTPX_AVAILABLE", False), \
737
+ patch("urllib.request.urlopen", side_effect=fake_urlopen), \
738
+ patch("time.sleep"):
739
+ with pytest.raises(RuntimeError, match="401"):
740
+ _http_post("http://test", {}, {}, max_retries=3)
741
+
742
+ # Must have been called exactly once — no retries
743
+ assert call_count == 1
744
+
745
+
746
+ # ---------------------------------------------------------------------------
747
+ # Test 12: batch_ingest
748
+ # ---------------------------------------------------------------------------
749
+
750
+ class TestBatchIngest:
751
+ """batch_ingest() should write all messages and check thresholds once."""
752
+
753
+ def test_batch_ingest_writes_all_messages(self, workspace: Path) -> None:
754
+ """All messages in the batch should appear in pending storage."""
755
+ eng = _make_engine_with_mock(workspace, FAKE_OBSERVATION, observer_threshold=9999)
756
+ messages = [
757
+ {"role": "user", "content": f"Message {i}", "timestamp": "12:00"}
758
+ for i in range(10)
759
+ ]
760
+ eng.batch_ingest("batch-t1", messages)
761
+ pending = eng.storage.read_pending("batch-t1")
762
+ assert len(pending) == 10
763
+
764
+ def test_batch_ingest_triggers_observe_once(self, workspace: Path) -> None:
765
+ """batch_ingest() should only check thresholds at the end (one observe max)."""
766
+ # Threshold low enough to trigger on the batch, but the mock tracks calls
767
+ eng = _make_engine_with_mock(workspace, FAKE_OBSERVATION, observer_threshold=10)
768
+ observe_calls = []
769
+ original_run_observer = eng._run_observer
770
+
771
+ def counting_run_observer(thread_id, pending):
772
+ observe_calls.append(thread_id)
773
+ return original_run_observer(thread_id, pending)
774
+
775
+ eng._run_observer = counting_run_observer # type: ignore[method-assign]
776
+
777
+ messages = [
778
+ {"role": "user", "content": "X" * 50, "timestamp": "12:00"}
779
+ for _ in range(5)
780
+ ]
781
+ eng.batch_ingest("batch-t2", messages)
782
+ # Should have triggered at most once (at the end), not 5 times
783
+ assert len(observe_calls) <= 1
784
+
785
+ def test_batch_ingest_returns_status(self, workspace: Path) -> None:
786
+ """batch_ingest() should return a valid status dict."""
787
+ eng = _make_engine_with_mock(workspace, FAKE_OBSERVATION, observer_threshold=9999)
788
+ messages = [{"role": "user", "content": "hello"}]
789
+ status = eng.batch_ingest("batch-t3", messages)
790
+ assert "observed" in status
791
+ assert "reflected" in status
792
+ assert "pending_tokens" in status
793
+ assert "error" in status
794
+
795
+
796
+ # ---------------------------------------------------------------------------
797
+ # Test 13: add_message skip observe
798
+ # ---------------------------------------------------------------------------
799
+
800
+ class TestAddMessageSkipObserve:
801
+ """add_message(auto_observe=False) should skip threshold checks."""
802
+
803
+ def test_skip_observe_only_writes_pending(self, workspace: Path) -> None:
804
+ """With auto_observe=False, message is written but observer never fires."""
805
+ eng = _make_engine_with_mock(workspace, FAKE_OBSERVATION, observer_threshold=5)
806
+ # Even with low threshold and large content, no observe should fire
807
+ status = eng.add_message(
808
+ "skip-t1", role="user",
809
+ content="A" * 500, # well above threshold
810
+ auto_observe=False,
811
+ )
812
+ assert status["observed"] is False
813
+ assert status["reflected"] is False
814
+ assert status["error"] is None
815
+
816
+ # Message should still be in pending
817
+ pending = eng.storage.read_pending("skip-t1")
818
+ assert len(pending) == 1
819
+ assert "A" * 100 in pending[0]["content"]
820
+
821
+ def test_auto_observe_true_still_triggers(self, workspace: Path) -> None:
822
+ """Default auto_observe=True should still trigger the observer."""
823
+ eng = _make_engine_with_mock(workspace, FAKE_OBSERVATION, observer_threshold=5)
824
+ status = eng.add_message(
825
+ "skip-t2", role="user",
826
+ content="A" * 500,
827
+ auto_observe=True, # explicit default
828
+ )
829
+ assert status["observed"] is True
830
+
831
+
832
+ # ---------------------------------------------------------------------------
833
+ # Test 14: observer batching
834
+ # ---------------------------------------------------------------------------
835
+
836
+ class TestObserverBatching:
837
+ """_run_observer() should split large message lists into batches."""
838
+
839
+ @staticmethod
840
+ def _make_messages_over_limit(n_messages: int = 2) -> list:
841
+ """Create `n_messages` messages whose combined token count exceeds
842
+ MAX_OBSERVER_INPUT_TOKENS, but each individual message is below the limit.
843
+
844
+ Uses varied realistic text so tiktoken doesn't compress it heavily.
845
+ """
846
+ # Build content that genuinely exceeds the limit when summed.
847
+ # We need total > MAX_OBSERVER_INPUT_TOKENS (80K) and each < 80K.
848
+ # Use enough unique words so tiktoken can't compress heavily.
849
+ words_per_msg = 50_000 # ~50K tokens per message via word diversity
850
+ content = " ".join(
851
+ f"event{i} status{i} detail{i} context{i} result{i}"
852
+ for i in range(words_per_msg // 5)
853
+ )
854
+ return [
855
+ {"role": "user", "content": content, "timestamp": "12:00"}
856
+ for _ in range(n_messages)
857
+ ]
858
+
859
+ def test_small_batch_single_call(self, workspace: Path) -> None:
860
+ """When total tokens < MAX_OBSERVER_INPUT_TOKENS, only one LLM call is made."""
861
+ eng = _make_engine_with_mock(workspace, FAKE_OBSERVATION, observer_threshold=9999)
862
+ call_count = [0]
863
+ original_llm_observe = eng._llm_observe
864
+
865
+ def counting_llm_observe(msgs):
866
+ call_count[0] += 1
867
+ return original_llm_observe(msgs)
868
+
869
+ eng._llm_observe = counting_llm_observe # type: ignore[method-assign]
870
+
871
+ # Small messages — well below 80K total
872
+ small_messages = [
873
+ {"role": "user", "content": f"Short message number {i}", "timestamp": "12:00"}
874
+ for i in range(5)
875
+ ]
876
+ eng._run_observer("batch-obs-t1", small_messages)
877
+
878
+ assert call_count[0] == 1, "Expected single LLM call for small input"
879
+
880
+ def test_large_batch_multiple_calls(self, workspace: Path) -> None:
881
+ """When total tokens > MAX_OBSERVER_INPUT_TOKENS, multiple LLM calls are made."""
882
+ from lib.engram import _count_messages_tokens
883
+
884
+ eng = _make_engine_with_mock(workspace, FAKE_OBSERVATION, observer_threshold=9999)
885
+
886
+ # Build messages that are guaranteed to exceed limit by using mock token counting
887
+ # Patch _count_messages_tokens to return controlled values
888
+ call_count = [0]
889
+
890
+ def counting_llm_observe(msgs):
891
+ call_count[0] += 1
892
+ return f"batch-observation-{call_count[0]}"
893
+
894
+ eng._llm_observe = counting_llm_observe # type: ignore[method-assign]
895
+
896
+ # Patch _count_messages_tokens in engram module to return predictable high values
897
+ import lib.engram as engram_mod
898
+ original_count = engram_mod._count_messages_tokens
899
+
900
+ def fake_count(msgs):
901
+ # Each single message appears to be 60K tokens
902
+ return len(msgs) * 60_000
903
+
904
+ try:
905
+ engram_mod._count_messages_tokens = fake_count
906
+ messages = [
907
+ {"role": "user", "content": "message content", "timestamp": "12:00"}
908
+ for _ in range(3)
909
+ ]
910
+ result = eng._run_observer("batch-obs-t2", messages)
911
+ finally:
912
+ engram_mod._count_messages_tokens = original_count
913
+
914
+ assert call_count[0] > 1, "Expected multiple LLM calls for large input"
915
+ assert "---" in result, "Combined result should contain batch separator"
916
+
917
+ def test_batching_clears_pending(self, workspace: Path) -> None:
918
+ """After batching, the pending queue should be cleared."""
919
+ eng = _make_engine_with_mock(workspace, FAKE_OBSERVATION, observer_threshold=9999)
920
+
921
+ messages = [
922
+ {"role": "user", "content": f"Message {i}", "timestamp": "12:00"}
923
+ for i in range(5)
924
+ ]
925
+ for msg in messages:
926
+ eng.storage.append_message("batch-obs-t3", msg)
927
+
928
+ eng._llm_observe = MagicMock(return_value="obs") # type: ignore[method-assign]
929
+ eng._run_observer("batch-obs-t3", messages)
930
+
931
+ pending = eng.storage.read_pending("batch-obs-t3")
932
+ assert len(pending) == 0, "Pending queue should be empty after observer run"
933
+
934
+ def test_batching_appends_combined_observation(self, workspace: Path) -> None:
935
+ """Batched observations should be combined and appended to storage."""
936
+ import lib.engram as engram_mod
937
+
938
+ eng = _make_engine_with_mock(workspace, FAKE_OBSERVATION, observer_threshold=9999)
939
+
940
+ batch_responses = []
941
+
942
+ def recording_llm_observe(msgs):
943
+ resp = f"observation-for-batch-{len(batch_responses) + 1}"
944
+ batch_responses.append(resp)
945
+ return resp
946
+
947
+ eng._llm_observe = recording_llm_observe # type: ignore[method-assign]
948
+
949
+ # Force batching via mock token counter
950
+ original_count = engram_mod._count_messages_tokens
951
+
952
+ def fake_count(msgs):
953
+ return len(msgs) * 60_000
954
+
955
+ try:
956
+ engram_mod._count_messages_tokens = fake_count
957
+ messages = [
958
+ {"role": "user", "content": f"Message {i}", "timestamp": "12:00"}
959
+ for i in range(3)
960
+ ]
961
+ eng._run_observer("batch-obs-t4", messages)
962
+ finally:
963
+ engram_mod._count_messages_tokens = original_count
964
+
965
+ saved = eng.storage.read_observations("batch-obs-t4")
966
+ for resp in batch_responses:
967
+ assert resp in saved
968
+
969
+ def test_single_oversized_message_doesnt_loop(self, workspace: Path) -> None:
970
+ """A single message that exceeds the limit should still be processed (no infinite loop)."""
971
+ import lib.engram as engram_mod
972
+
973
+ eng = _make_engine_with_mock(workspace, FAKE_OBSERVATION, observer_threshold=9999)
974
+ call_count = [0]
975
+
976
+ def counting_llm_observe(msgs):
977
+ call_count[0] += 1
978
+ if call_count[0] > 5:
979
+ raise RuntimeError("Infinite loop detected!")
980
+ return "oversized-observation"
981
+
982
+ eng._llm_observe = counting_llm_observe # type: ignore[method-assign]
983
+
984
+ # Patch token counter so single message appears to be 200K tokens
985
+ original_count = engram_mod._count_messages_tokens
986
+
987
+ def fake_count(msgs):
988
+ return len(msgs) * 200_000
989
+
990
+ try:
991
+ engram_mod._count_messages_tokens = fake_count
992
+ huge_message = {"role": "user", "content": "Large content", "timestamp": "12:00"}
993
+ result = eng._run_observer("batch-obs-t5", [huge_message])
994
+ finally:
995
+ engram_mod._count_messages_tokens = original_count
996
+
997
+ assert call_count[0] == 1, "Single oversized message should produce exactly one call"
998
+ assert result == "oversized-observation"
999
+
1000
+
1001
+ # ---------------------------------------------------------------------------
1002
+ # Test 15: reflector truncation
1003
+ # ---------------------------------------------------------------------------
1004
+
1005
+ class TestReflectorTruncation:
1006
+ """_run_reflector() should truncate observations that exceed MAX_REFLECTOR_INPUT_TOKENS."""
1007
+
1008
+ def test_small_observations_no_truncation(self, workspace: Path) -> None:
1009
+ """Small observations should pass through unchanged."""
1010
+ eng = _make_engine_with_mock(workspace, FAKE_REFLECTION, observer_threshold=9999)
1011
+
1012
+ received_text = []
1013
+
1014
+ def recording_llm_reflect(obs):
1015
+ received_text.append(obs)
1016
+ return FAKE_REFLECTION
1017
+
1018
+ eng._llm_reflect = recording_llm_reflect # type: ignore[method-assign]
1019
+
1020
+ small_obs = "Date: 2026-01-01\n- 🔴 12:00 Small observation\n"
1021
+ eng._run_reflector("refl-t1", small_obs)
1022
+
1023
+ assert received_text[0] == small_obs, "Small observations should not be modified"
1024
+
1025
+ def test_large_observations_truncated(self, workspace: Path) -> None:
1026
+ """Observations exceeding MAX_REFLECTOR_INPUT_TOKENS should be truncated."""
1027
+ eng = _make_engine_with_mock(workspace, FAKE_REFLECTION, observer_threshold=9999)
1028
+
1029
+ received_tokens = []
1030
+
1031
+ def recording_llm_reflect(obs):
1032
+ received_tokens.append(estimate_tokens(obs))
1033
+ return FAKE_REFLECTION
1034
+
1035
+ eng._llm_reflect = recording_llm_reflect # type: ignore[method-assign]
1036
+
1037
+ # Build observations that are clearly over the limit
1038
+ large_obs = ("- 🔴 12:00 Critical event that happened\n" * 10_000)
1039
+ original_tokens = estimate_tokens(large_obs)
1040
+ assert original_tokens > MAX_REFLECTOR_INPUT_TOKENS, "Test setup: input must exceed limit"
1041
+
1042
+ eng._run_reflector("refl-t2", large_obs)
1043
+
1044
+ assert len(received_tokens) == 1
1045
+ # The truncation loop counts tokens per-line; the rejoined text may be slightly
1046
+ # higher due to newline tokenization interactions — allow a 15% margin.
1047
+ # The key guarantee is that input is dramatically reduced from the original.
1048
+ assert received_tokens[0] <= MAX_REFLECTOR_INPUT_TOKENS * 1.15, (
1049
+ f"Reflector received {received_tokens[0]} tokens, "
1050
+ f"expected roughly <= {MAX_REFLECTOR_INPUT_TOKENS} (with 15% join margin)"
1051
+ )
1052
+ # Must be significantly less than the original (at least 25% reduction)
1053
+ assert received_tokens[0] < original_tokens * 0.75, (
1054
+ f"Truncation had no effect: {received_tokens[0]} vs original {original_tokens}"
1055
+ )
1056
+
1057
+ def test_truncation_keeps_tail(self, workspace: Path) -> None:
1058
+ """Truncation should keep the most recent (tail) content, not the head."""
1059
+ eng = _make_engine_with_mock(workspace, FAKE_REFLECTION, observer_threshold=9999)
1060
+
1061
+ received_text = []
1062
+
1063
+ def recording_llm_reflect(obs):
1064
+ received_text.append(obs)
1065
+ return FAKE_REFLECTION
1066
+
1067
+ eng._llm_reflect = recording_llm_reflect # type: ignore[method-assign]
1068
+
1069
+ # Build content with distinct head and tail markers
1070
+ # Make it large enough to trigger truncation
1071
+ head_lines = ["head-line-AAAA\n"] * 5_000
1072
+ tail_lines = ["tail-line-ZZZZ\n"] * 2_000
1073
+ large_obs = "".join(head_lines) + "".join(tail_lines)
1074
+
1075
+ eng._run_reflector("refl-t3", large_obs)
1076
+
1077
+ result_text = received_text[0]
1078
+ # Tail should be present; head may have been truncated
1079
+ assert "tail-line-ZZZZ" in result_text, "Tail content should be preserved after truncation"
1080
+
1081
+ def test_truncation_reflection_saved(self, workspace: Path) -> None:
1082
+ """Even with truncated input, the reflection should be saved to storage."""
1083
+ eng = _make_engine_with_mock(workspace, FAKE_REFLECTION, observer_threshold=9999)
1084
+ eng._llm_reflect = MagicMock(return_value=FAKE_REFLECTION) # type: ignore[method-assign]
1085
+
1086
+ large_obs = "- 🔴 12:00 Item\n" * 10_000
1087
+
1088
+ eng._run_reflector("refl-t4", large_obs)
1089
+
1090
+ saved = eng.storage.read_reflection("refl-t4")
1091
+ assert FAKE_REFLECTION.strip() in saved