@jairodri/huckleberry-cli 1.0.2 → 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.
- package/README.md +85 -0
- package/agents/cos.json +2 -5
- package/agents/hermes/AGENTS.md +25 -49
- package/agents/hermes/fix-kimaki/SKILL.md +19 -92
- package/agents/hermes/hermes-fixer.md +44 -74
- package/agents/hermes.json +6 -18
- package/kanban/SKILL.md +18 -18
- package/kanban/package.json +1 -1
- package/kanban/src/dispatcher.mjs +1 -1
- package/kanban/templates/blog-social-campaign.yaml +6 -2
- package/kanban/templates/social-campaign.yaml +9 -4
- package/mcp/jev-calibration.mjs +335 -0
- package/mcp/jev-client.mjs +119 -0
- package/mcp/jev-thresholds.json +27 -0
- package/package.json +13 -9
- package/scripts/doctor-watchdog.sh +73 -0
- package/scripts/install-services.sh +34 -27
- package/scripts/self-improve-weekly.sh +34 -0
- package/scripts/sync-skills.mjs +121 -0
- package/skills/ab-test-setup/SKILL.md +266 -0
- package/skills/ab-test-setup/evals/evals.json +105 -0
- package/skills/ab-test-setup/references/sample-size-guide.md +263 -0
- package/skills/ab-test-setup/references/test-templates.md +277 -0
- package/skills/agent-reach/SKILL.md +132 -0
- package/skills/agent-reach/references/career.md +29 -0
- package/skills/agent-reach/references/dev.md +62 -0
- package/skills/agent-reach/references/search.md +33 -0
- package/skills/agent-reach/references/social.md +232 -0
- package/skills/agent-reach/references/video.md +130 -0
- package/skills/agent-reach/references/web.md +50 -0
- package/skills/ai-agent-seo/SKILL.md +450 -0
- package/skills/ai-cold-email-intelligence/SKILL.md +54 -0
- package/skills/ai-cold-email-intelligence/references/prompts.md +21 -0
- package/skills/ai-consensus-builder/SKILL.md +324 -0
- package/skills/ai-seo/SKILL.md +399 -0
- package/skills/ai-seo/evals/evals.json +90 -0
- package/skills/ai-seo/references/content-patterns.md +284 -0
- package/skills/ai-seo/references/platform-ranking-factors.md +152 -0
- package/skills/ai-visibility-audit/SKILL.md +322 -0
- package/skills/analytics-tracking/SKILL.md +309 -0
- package/skills/analytics-tracking/evals/evals.json +90 -0
- package/skills/analytics-tracking/references/event-library.md +260 -0
- package/skills/analytics-tracking/references/ga4-implementation.md +300 -0
- package/skills/analytics-tracking/references/gtm-implementation.md +390 -0
- package/skills/article-generator/SKILL.md +216 -0
- package/skills/auto-indexing/SKILL.md +179 -0
- package/skills/auto-indexing/scripts/auto-indexing.js +257 -0
- package/skills/auto-skill/SKILL.md +151 -0
- package/skills/auto-skill/correction-queue.js +102 -0
- package/skills/auto-skill/dedup.js +97 -0
- package/skills/auto-skill/skill-router.js +224 -0
- package/skills/autoresearch/SKILL.md +334 -0
- package/skills/autoresearch/autoresearch-autoresearch/SKILL.md.baseline +329 -0
- package/skills/autoresearch/autoresearch-autoresearch/changelog.md +34 -0
- package/skills/autoresearch/autoresearch-autoresearch/dashboard.html +116 -0
- package/skills/autoresearch/autoresearch-autoresearch/evals.md +39 -0
- package/skills/autoresearch/autoresearch-autoresearch/results.json +44 -0
- package/skills/autoresearch/autoresearch-autoresearch/results.tsv +3 -0
- package/skills/autoresearch/eval-guide.md +121 -0
- package/skills/b2b-cold-outreach/SKILL.md +322 -0
- package/skills/birdclaw/SKILL.md +82 -0
- package/skills/brainstorm/SKILL.md +168 -0
- package/skills/brand-kit-playground/SKILL.md +312 -0
- package/skills/brand-kit-playground/references/html-architecture.md +316 -0
- package/skills/brand-kit-playground/references/steal-patterns.md +88 -0
- package/skills/brand-voice/SKILL.md +223 -0
- package/skills/brand-voice/references/honesty-as-positioning.md +60 -0
- package/skills/brand-voice/references/voice-modes.md +1158 -0
- package/skills/business-advisory-council/README.md +136 -0
- package/skills/business-advisory-council/SKILL.md +94 -0
- package/skills/business-advisory-council/business-advisory-council.py +492 -0
- package/skills/business-advisory-council/discord-components-decision.json +8 -0
- package/skills/business-advisory-council/examples/steve-conley-partnership.json +6 -0
- package/skills/business-advisory-council/steve-conley-call-brief.json +6 -0
- package/skills/churn-prevention/SKILL.md +305 -0
- package/skills/churn-prevention/evals/evals.json +93 -0
- package/skills/churn-prevention/references/cancel-flow-patterns.md +316 -0
- package/skills/churn-prevention/references/dunning-playbook.md +408 -0
- package/skills/claw-compactor/LICENSE +21 -0
- package/skills/claw-compactor/README.md +459 -0
- package/skills/claw-compactor/SKILL.md +329 -0
- package/skills/claw-compactor/benchmark/RESULTS.md +104 -0
- package/skills/claw-compactor/benchmark/__init__.py +1 -0
- package/skills/claw-compactor/benchmark/compressors.py +520 -0
- package/skills/claw-compactor/benchmark/data/sample_01_devops.json +82 -0
- package/skills/claw-compactor/benchmark/data/sample_02_trading.json +54 -0
- package/skills/claw-compactor/benchmark/data/sample_03_ml_short.json +38 -0
- package/skills/claw-compactor/benchmark/data/sample_04_mixed_long.json +318 -0
- package/skills/claw-compactor/benchmark/data/sample_05_sysadmin.json +56 -0
- package/skills/claw-compactor/benchmark/evaluate.py +278 -0
- package/skills/claw-compactor/benchmark/report.py +241 -0
- package/skills/claw-compactor/benchmark/results/benchmark_results.json +405 -0
- package/skills/claw-compactor/benchmark/run_benchmark.py +238 -0
- package/skills/claw-compactor/engram.yaml +49 -0
- package/skills/claw-compactor/proxy/README.md +87 -0
- package/skills/claw-compactor/proxy/dashboard.html +804 -0
- package/skills/claw-compactor/proxy/event-log.mjs +213 -0
- package/skills/claw-compactor/proxy/fair-queue.mjs +288 -0
- package/skills/claw-compactor/proxy/metrics-store.mjs +425 -0
- package/skills/claw-compactor/proxy/package-lock.json +125 -0
- package/skills/claw-compactor/proxy/package.json +15 -0
- package/skills/claw-compactor/proxy/portal.html +360 -0
- package/skills/claw-compactor/proxy/process-registry.mjs +447 -0
- package/skills/claw-compactor/proxy/rate-limiter.mjs +117 -0
- package/skills/claw-compactor/proxy/redis-client.mjs +75 -0
- package/skills/claw-compactor/proxy/retry.mjs +182 -0
- package/skills/claw-compactor/proxy/server.mjs +2100 -0
- package/skills/claw-compactor/proxy/session-affinity.mjs +177 -0
- package/skills/claw-compactor/proxy/start.sh +23 -0
- package/skills/claw-compactor/proxy/test/event-log.test.mjs +237 -0
- package/skills/claw-compactor/proxy/test/helpers.mjs +103 -0
- package/skills/claw-compactor/proxy/test/integration.test.mjs +297 -0
- package/skills/claw-compactor/proxy/test/metrics-store.test.mjs +243 -0
- package/skills/claw-compactor/proxy/test/process-registry.test.mjs +390 -0
- package/skills/claw-compactor/proxy/test/rate-limiter.test.mjs +121 -0
- package/skills/claw-compactor/proxy/test/redis-client.test.mjs +62 -0
- package/skills/claw-compactor/proxy/test/token-tracker.test.mjs +200 -0
- package/skills/claw-compactor/proxy/token-tracker.mjs +482 -0
- package/skills/claw-compactor/pyproject.toml +26 -0
- package/skills/claw-compactor/references/README.md +44 -0
- package/skills/claw-compactor/references/architecture.md +197 -0
- package/skills/claw-compactor/references/benchmarks.md +94 -0
- package/skills/claw-compactor/references/compression-prompts.md +39 -0
- package/skills/claw-compactor/references/compression-techniques.md +219 -0
- package/skills/claw-compactor/references/testing.md +160 -0
- package/skills/claw-compactor/scripts/audit_memory.py +216 -0
- package/skills/claw-compactor/scripts/compress_memory.py +236 -0
- package/skills/claw-compactor/scripts/compressed_context.py +280 -0
- package/skills/claw-compactor/scripts/dedup_memory.py +147 -0
- package/skills/claw-compactor/scripts/dictionary_compress.py +170 -0
- package/skills/claw-compactor/scripts/engram-auto.sh +152 -0
- package/skills/claw-compactor/scripts/engram_auto.py +695 -0
- package/skills/claw-compactor/scripts/engram_cli.py +474 -0
- package/skills/claw-compactor/scripts/estimate_tokens.py +131 -0
- package/skills/claw-compactor/scripts/generate_summary_tiers.py +292 -0
- package/skills/claw-compactor/scripts/lib/__init__.py +34 -0
- package/skills/claw-compactor/scripts/lib/config.py +289 -0
- package/skills/claw-compactor/scripts/lib/dedup.py +119 -0
- package/skills/claw-compactor/scripts/lib/dictionary.py +281 -0
- package/skills/claw-compactor/scripts/lib/engram.py +793 -0
- package/skills/claw-compactor/scripts/lib/engram_prompts.py +175 -0
- package/skills/claw-compactor/scripts/lib/engram_storage.py +254 -0
- package/skills/claw-compactor/scripts/lib/exceptions.py +24 -0
- package/skills/claw-compactor/scripts/lib/markdown.py +312 -0
- package/skills/claw-compactor/scripts/lib/rle.py +165 -0
- package/skills/claw-compactor/scripts/lib/tokenizer_optimizer.py +188 -0
- package/skills/claw-compactor/scripts/lib/tokens.py +68 -0
- package/skills/claw-compactor/scripts/mem_compress.py +788 -0
- package/skills/claw-compactor/scripts/observation_compressor.py +346 -0
- package/skills/claw-compactor/tests/__init__.py +0 -0
- package/skills/claw-compactor/tests/conftest.py +94 -0
- package/skills/claw-compactor/tests/test_audit_comprehensive.py +123 -0
- package/skills/claw-compactor/tests/test_audit_memory.py +67 -0
- package/skills/claw-compactor/tests/test_benchmark.py +122 -0
- package/skills/claw-compactor/tests/test_cli_commands.py +228 -0
- package/skills/claw-compactor/tests/test_compress_memory.py +109 -0
- package/skills/claw-compactor/tests/test_compress_memory_comprehensive.py +191 -0
- package/skills/claw-compactor/tests/test_compressed_context.py +145 -0
- package/skills/claw-compactor/tests/test_config.py +54 -0
- package/skills/claw-compactor/tests/test_dedup_memory.py +124 -0
- package/skills/claw-compactor/tests/test_dictionary.py +327 -0
- package/skills/claw-compactor/tests/test_dictionary_comprehensive.py +241 -0
- package/skills/claw-compactor/tests/test_engram.py +1091 -0
- package/skills/claw-compactor/tests/test_engram_auto.py +529 -0
- package/skills/claw-compactor/tests/test_error_handling.py +194 -0
- package/skills/claw-compactor/tests/test_estimate_tokens.py +99 -0
- package/skills/claw-compactor/tests/test_generate_summary_tiers.py +105 -0
- package/skills/claw-compactor/tests/test_integration.py +249 -0
- package/skills/claw-compactor/tests/test_lib_dedup.py +135 -0
- package/skills/claw-compactor/tests/test_lib_markdown.py +388 -0
- package/skills/claw-compactor/tests/test_lib_tokens.py +82 -0
- package/skills/claw-compactor/tests/test_main_entry.py +176 -0
- package/skills/claw-compactor/tests/test_markdown_advanced.py +284 -0
- package/skills/claw-compactor/tests/test_new_features.py +77 -0
- package/skills/claw-compactor/tests/test_observation_comprehensive.py +261 -0
- package/skills/claw-compactor/tests/test_observation_compressor.py +128 -0
- package/skills/claw-compactor/tests/test_performance.py +172 -0
- package/skills/claw-compactor/tests/test_pipeline.py +109 -0
- package/skills/claw-compactor/tests/test_real_workspace.py +161 -0
- package/skills/claw-compactor/tests/test_rle.py +114 -0
- package/skills/claw-compactor/tests/test_rle_comprehensive.py +151 -0
- package/skills/claw-compactor/tests/test_roundtrip.py +253 -0
- package/skills/claw-compactor/tests/test_roundtrip_comprehensive.py +192 -0
- package/skills/claw-compactor/tests/test_tiers_comprehensive.py +171 -0
- package/skills/claw-compactor/tests/test_token_economics.py +237 -0
- package/skills/claw-compactor/tests/test_tokenizer_optimizer.py +175 -0
- package/skills/claw-compactor/tests/test_tokenizer_optimizer_comprehensive.py +241 -0
- package/skills/claw-compactor/tests/test_tokens.py +27 -0
- package/skills/cmo/CHANGELOG.md +61 -0
- package/skills/cmo/SKILL.md +500 -0
- package/skills/cmo/references/ai-slop-patterns.md +56 -0
- package/skills/cmo/references/analytics-guide.md +152 -0
- package/skills/cmo/references/ideas-library.md +197 -0
- package/skills/cmo/rules/brand-file-map.md +198 -0
- package/skills/cmo/rules/brand-memory.md +158 -0
- package/skills/cmo/rules/cli-runtime-index.md +95 -0
- package/skills/cmo/rules/command-reference.md +167 -0
- package/skills/cmo/rules/communication.md +15 -0
- package/skills/cmo/rules/context-switch.md +101 -0
- package/skills/cmo/rules/ecosystem.md +125 -0
- package/skills/cmo/rules/error-recovery.md +98 -0
- package/skills/cmo/rules/learning-loop.md +170 -0
- package/skills/cmo/rules/monorepo.md +78 -0
- package/skills/cmo/rules/output-format.md +124 -0
- package/skills/cmo/rules/persona.md +15 -0
- package/skills/cmo/rules/playbooks.md +327 -0
- package/skills/cmo/rules/progressive-enhancement.md +231 -0
- package/skills/cmo/rules/publish-index.md +106 -0
- package/skills/cmo/rules/quality-gate.md +90 -0
- package/skills/cmo/rules/recency-grounding.md +124 -0
- package/skills/cmo/rules/safety.md +80 -0
- package/skills/cmo/rules/studio-api-index.md +138 -0
- package/skills/cmo/rules/studio-integration.md +147 -0
- package/skills/cmo/rules/sub-agents.md +167 -0
- package/skills/cold-email/SKILL.md +240 -0
- package/skills/cold-email/evals/evals.json +94 -0
- package/skills/cold-email/references/benchmarks.md +83 -0
- package/skills/cold-email/references/follow-up-sequences.md +81 -0
- package/skills/cold-email/references/frameworks.md +90 -0
- package/skills/cold-email/references/personalization.md +79 -0
- package/skills/cold-email/references/subject-lines.md +53 -0
- package/skills/competitive-intel/SKILL.md +433 -0
- package/skills/competitive-intel/references/competitive-frameworks.md +145 -0
- package/skills/competitor-alternatives/SKILL.md +362 -0
- package/skills/competitor-alternatives/evals/evals.json +93 -0
- package/skills/competitor-alternatives/references/content-architecture.md +271 -0
- package/skills/competitor-alternatives/references/templates.md +223 -0
- package/skills/concreteness-fixer/SKILL.md +483 -0
- package/skills/concreteness-fixer/scripts/validation/concreteness-validator.js +221 -0
- package/skills/concreteness-fixer/utils/execFileNoThrow.js +16 -0
- package/skills/content-atomizer/SKILL.md +424 -0
- package/skills/content-atomizer/references/hook-strategies.md +56 -0
- package/skills/content-atomizer/references/platform-specs.md +118 -0
- package/skills/content-atomizer/references/platforms/bluesky.md +64 -0
- package/skills/content-atomizer/references/platforms/instagram.md +77 -0
- package/skills/content-atomizer/references/platforms/linkedin.md +120 -0
- package/skills/content-atomizer/references/platforms/reddit.md +73 -0
- package/skills/content-atomizer/references/platforms/threads.md +64 -0
- package/skills/content-atomizer/references/platforms/tiktok.md +55 -0
- package/skills/content-atomizer/references/platforms/twitter.md +123 -0
- package/skills/content-atomizer/references/platforms/youtube.md +56 -0
- package/skills/content-atomizer/references/tweet-patterns.md +40 -0
- package/skills/content-repost-manager/SKILL.md +245 -0
- package/skills/content-repost-manager/scripts/content-repost-scan.py +133 -0
- package/skills/context-recovery/.clawdhub/origin.json +7 -0
- package/skills/context-recovery/SKILL.md +198 -0
- package/skills/context-recovery/_meta.json +6 -0
- package/skills/conversion-flow-cro/SKILL.md +248 -0
- package/skills/conversion-flow-cro/references/flow-benchmarks.md +158 -0
- package/skills/copy-editing/SKILL.md +474 -0
- package/skills/copy-editing/evals/evals.json +89 -0
- package/skills/copy-editing/references/plain-english-alternatives.md +394 -0
- package/skills/copywriting/SKILL.md +349 -0
- package/skills/copywriting/evals/evals.json +111 -0
- package/skills/copywriting/references/awareness-level-guide.md +227 -0
- package/skills/copywriting/references/copy-frameworks.md +389 -0
- package/skills/copywriting/references/natural-transitions.md +272 -0
- package/skills/creative/SKILL.md +365 -0
- package/skills/creative/modes/ad-creative.md +1748 -0
- package/skills/creative/modes/product-photo.md +1520 -0
- package/skills/creative/modes/product-video.md +1657 -0
- package/skills/creative/modes/social-graphics.md +1511 -0
- package/skills/creative/modes/talking-head.md +2208 -0
- package/skills/creative/references/VISUAL_INTELLIGENCE.md +1233 -0
- package/skills/deep-serp-article/SKILL.md +677 -0
- package/skills/deep-serp-article/SKILL.md.bak +530 -0
- package/skills/earned-media/SKILL.md +260 -0
- package/skills/email-sequences/SKILL.md +446 -0
- package/skills/email-sequences/references/sequence-templates.md +1217 -0
- package/skills/firecrawl/.skill-meta.json +13 -0
- package/skills/firecrawl/SKILL.md +154 -0
- package/skills/firecrawl/rules/install.md +63 -0
- package/skills/firecrawl/rules/security.md +26 -0
- package/skills/form-cro/SKILL.md +429 -0
- package/skills/form-cro/evals/evals.json +90 -0
- package/skills/free-tool-strategy/SKILL.md +325 -0
- package/skills/free-tool-strategy/evals/evals.json +90 -0
- package/skills/free-tool-strategy/references/tool-types.md +253 -0
- package/skills/frontend-slides/SKILL.md +248 -0
- package/skills/frontend-slides/STYLE_PRESETS.md +347 -0
- package/skills/frontend-slides/animation-patterns.md +110 -0
- package/skills/frontend-slides/html-template.md +347 -0
- package/skills/frontend-slides/scripts/extract-pptx.py +96 -0
- package/skills/frontend-slides/viewport-base.css +153 -0
- package/skills/guardrail-framework/SKILL.md +319 -0
- package/skills/hook-generator/SKILL.md +438 -0
- package/skills/hook-generator/database/2026-PLATFORM-RESEARCH.md +211 -0
- package/skills/hook-generator/database/instagram-hooks.json +591 -0
- package/skills/hook-generator/database/linkedin-hooks.json +502 -0
- package/skills/hook-generator/database/medium-hooks.json +181 -0
- package/skills/hook-generator/database/tiktok-hooks.json +337 -0
- package/skills/hook-generator/database/twitter-hooks.json +567 -0
- package/skills/hook-generator/database/youtube-hooks.json +275 -0
- package/skills/hook-generator/package-lock.json +24 -0
- package/skills/hook-generator/package.json +5 -0
- package/skills/hook-generator/references/hook-generation-system.md +257 -0
- package/skills/hook-generator/references/hook-performance-analysis.json +153 -0
- package/skills/hook-generator/references/hooks-library.md +128 -0
- package/skills/hook-generator/references/platform-hook-frameworks.md +275 -0
- package/skills/hook-generator/scripts/generate-hooks-unified.js +722 -0
- package/skills/hook-generator/scripts/generate-hooks.js +225 -0
- package/skills/hook-generator/scripts/generate.sh +36 -0
- package/skills/hook-generator/scripts/ralph-hook-enhancer.js +864 -0
- package/skills/hook-generator/scripts/test-gemini-output.js +51 -0
- package/skills/humanizer/README.md +120 -0
- package/skills/humanizer/SKILL.md +699 -0
- package/skills/image-gen/SKILL.md +277 -0
- package/skills/image-gen/references/prompt-patterns.md +114 -0
- package/skills/image-gen/references/visual-metaphors.md +51 -0
- package/skills/impeccable/SKILL.md +86 -0
- package/skills/impeccable/reference/adapt.md +312 -0
- package/skills/impeccable/reference/adapt.native.md +58 -0
- package/skills/impeccable/reference/android.md +40 -0
- package/skills/impeccable/reference/animate.md +86 -0
- package/skills/impeccable/reference/audit.md +136 -0
- package/skills/impeccable/reference/audit.native.md +139 -0
- package/skills/impeccable/reference/bolder.md +31 -0
- package/skills/impeccable/reference/clarify.md +94 -0
- package/skills/impeccable/reference/colorize.md +86 -0
- package/skills/impeccable/reference/craft-floor.md +42 -0
- package/skills/impeccable/reference/craft.md +5 -0
- package/skills/impeccable/reference/critique.md +788 -0
- package/skills/impeccable/reference/degraded/asset-producer.md +90 -0
- package/skills/impeccable/reference/degraded/documenter.md +24 -0
- package/skills/impeccable/reference/degraded/finish-reviewer.md +37 -0
- package/skills/impeccable/reference/degraded/manual-edit-applier.md +92 -0
- package/skills/impeccable/reference/delight.md +70 -0
- package/skills/impeccable/reference/distill.md +111 -0
- package/skills/impeccable/reference/doctor.md +53 -0
- package/skills/impeccable/reference/document.md +416 -0
- package/skills/impeccable/reference/extract.md +69 -0
- package/skills/impeccable/reference/harden.md +336 -0
- package/skills/impeccable/reference/hooks.md +105 -0
- package/skills/impeccable/reference/init.md +125 -0
- package/skills/impeccable/reference/ios.md +45 -0
- package/skills/impeccable/reference/layout.md +84 -0
- package/skills/impeccable/reference/live-setup.md +102 -0
- package/skills/impeccable/reference/live.md +323 -0
- package/skills/impeccable/reference/new-work.md +110 -0
- package/skills/impeccable/reference/onboard.md +234 -0
- package/skills/impeccable/reference/operate.md +61 -0
- package/skills/impeccable/reference/optimize.md +258 -0
- package/skills/impeccable/reference/overdrive.md +127 -0
- package/skills/impeccable/reference/polish.md +97 -0
- package/skills/impeccable/reference/quieter.md +99 -0
- package/skills/impeccable/reference/routing.md +18 -0
- package/skills/impeccable/reference/shape.md +59 -0
- package/skills/impeccable/reference/typeset.md +80 -0
- package/skills/impeccable/reference/visualize.md +49 -0
- package/skills/impeccable/scripts/command-metadata.json +94 -0
- package/skills/impeccable/scripts/concept-seed.mjs +558 -0
- package/skills/impeccable/scripts/context-signals.mjs +334 -0
- package/skills/impeccable/scripts/context.mjs +1467 -0
- package/skills/impeccable/scripts/critique-storage.mjs +213 -0
- package/skills/impeccable/scripts/detect-csp.mjs +198 -0
- package/skills/impeccable/scripts/detect.mjs +21 -0
- package/skills/impeccable/scripts/detector/browser/injected/index.mjs +2023 -0
- package/skills/impeccable/scripts/detector/cli/main.mjs +438 -0
- package/skills/impeccable/scripts/detector/design-system.mjs +1046 -0
- package/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +8283 -0
- package/skills/impeccable/scripts/detector/detect-antipatterns.mjs +50 -0
- package/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +372 -0
- package/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +1168 -0
- package/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +1186 -0
- package/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +264 -0
- package/skills/impeccable/scripts/detector/engines/visual/screenshot-contrast.mjs +189 -0
- package/skills/impeccable/scripts/detector/findings.mjs +18 -0
- package/skills/impeccable/scripts/detector/node/file-system.mjs +203 -0
- package/skills/impeccable/scripts/detector/profile/profiler.mjs +166 -0
- package/skills/impeccable/scripts/detector/registry/antipatterns.mjs +617 -0
- package/skills/impeccable/scripts/detector/rules/checks.mjs +5580 -0
- package/skills/impeccable/scripts/detector/shared/color.mjs +124 -0
- package/skills/impeccable/scripts/detector/shared/constants.mjs +112 -0
- package/skills/impeccable/scripts/detector/shared/fonts.mjs +30 -0
- package/skills/impeccable/scripts/detector/shared/inline-ignores.mjs +148 -0
- package/skills/impeccable/scripts/detector/shared/page.mjs +7 -0
- package/skills/impeccable/scripts/doctor.mjs +336 -0
- package/skills/impeccable/scripts/embed-prompt.mjs +133 -0
- package/skills/impeccable/scripts/generate-image.mjs +240 -0
- package/skills/impeccable/scripts/hook-admin.mjs +801 -0
- package/skills/impeccable/scripts/hook-before-edit.mjs +508 -0
- package/skills/impeccable/scripts/hook-lib.mjs +2153 -0
- package/skills/impeccable/scripts/hook.mjs +78 -0
- package/skills/impeccable/scripts/lib/artifact-schema.mjs +93 -0
- package/skills/impeccable/scripts/lib/composition-catalog.mjs +200 -0
- package/skills/impeccable/scripts/lib/concept-catalog.mjs +384 -0
- package/skills/impeccable/scripts/lib/design-parser.mjs +925 -0
- package/skills/impeccable/scripts/lib/impeccable-config.mjs +658 -0
- package/skills/impeccable/scripts/lib/impeccable-paths.mjs +137 -0
- package/skills/impeccable/scripts/lib/is-generated.mjs +69 -0
- package/skills/impeccable/scripts/lib/provider.mjs +5 -0
- package/skills/impeccable/scripts/lib/roll-selection.mjs +362 -0
- package/skills/impeccable/scripts/lib/staleness-deep.mjs +478 -0
- package/skills/impeccable/scripts/lib/staleness-notice.mjs +169 -0
- package/skills/impeccable/scripts/lib/staleness.mjs +457 -0
- package/skills/impeccable/scripts/lib/surface-briefs.mjs +151 -0
- package/skills/impeccable/scripts/lib/target-args.mjs +42 -0
- package/skills/impeccable/scripts/lib/target-slug.mjs +33 -0
- package/skills/impeccable/scripts/lib/template-extensions.mjs +146 -0
- package/skills/impeccable/scripts/live/accept-css.mjs +617 -0
- package/skills/impeccable/scripts/live/accept-verify.mjs +60 -0
- package/skills/impeccable/scripts/live/browser-script-parts.mjs +55 -0
- package/skills/impeccable/scripts/live/completion.mjs +28 -0
- package/skills/impeccable/scripts/live/event-validation.mjs +199 -0
- package/skills/impeccable/scripts/live/frameworks/astro.mjs +47 -0
- package/skills/impeccable/scripts/live/frameworks/detect-utils.mjs +73 -0
- package/skills/impeccable/scripts/live/frameworks/index.mjs +143 -0
- package/skills/impeccable/scripts/live/frameworks/journal.mjs +197 -0
- package/skills/impeccable/scripts/live/frameworks/nextjs.mjs +49 -0
- package/skills/impeccable/scripts/live/frameworks/nuxt.mjs +161 -0
- package/skills/impeccable/scripts/live/frameworks/script-src.mjs +17 -0
- package/skills/impeccable/scripts/live/frameworks/static-html.mjs +26 -0
- package/skills/impeccable/scripts/live/frameworks/sveltekit.mjs +71 -0
- package/skills/impeccable/scripts/live/frameworks/tag-strategy.mjs +247 -0
- package/skills/impeccable/scripts/live/frameworks/tanstack-start.mjs +70 -0
- package/skills/impeccable/scripts/live/frameworks/vite-generic.mjs +42 -0
- package/skills/impeccable/scripts/live/generation-preflight.mjs +149 -0
- package/skills/impeccable/scripts/live/insert-ui.mjs +458 -0
- package/skills/impeccable/scripts/live/instructions.mjs +142 -0
- package/skills/impeccable/scripts/live/manual-apply.mjs +939 -0
- package/skills/impeccable/scripts/live/manual-edit-routes.mjs +357 -0
- package/skills/impeccable/scripts/live/manual-edits-buffer.mjs +152 -0
- package/skills/impeccable/scripts/live/poll-lanes.mjs +14 -0
- package/skills/impeccable/scripts/live/roots.mjs +508 -0
- package/skills/impeccable/scripts/live/session-store.mjs +563 -0
- package/skills/impeccable/scripts/live/source-lock.mjs +105 -0
- package/skills/impeccable/scripts/live/source-search.mjs +105 -0
- package/skills/impeccable/scripts/live/svelte-ast.mjs +961 -0
- package/skills/impeccable/scripts/live/svelte-component.mjs +1342 -0
- package/skills/impeccable/scripts/live/sveltekit-adapter.mjs +316 -0
- package/skills/impeccable/scripts/live/tanstack-adapter.mjs +280 -0
- package/skills/impeccable/scripts/live/ui-core.mjs +180 -0
- package/skills/impeccable/scripts/live/vocabulary.mjs +171 -0
- package/skills/impeccable/scripts/live-accept.mjs +954 -0
- package/skills/impeccable/scripts/live-browser-dom.js +146 -0
- package/skills/impeccable/scripts/live-browser-session.js +123 -0
- package/skills/impeccable/scripts/live-browser.js +12520 -0
- package/skills/impeccable/scripts/live-commit-manual-edits.mjs +1244 -0
- package/skills/impeccable/scripts/live-complete.mjs +107 -0
- package/skills/impeccable/scripts/live-copy-edit-agent.mjs +683 -0
- package/skills/impeccable/scripts/live-discard-manual-edits.mjs +51 -0
- package/skills/impeccable/scripts/live-inject.mjs +503 -0
- package/skills/impeccable/scripts/live-insert.mjs +292 -0
- package/skills/impeccable/scripts/live-manual-edit-evidence.mjs +368 -0
- package/skills/impeccable/scripts/live-poll.mjs +429 -0
- package/skills/impeccable/scripts/live-resume.mjs +123 -0
- package/skills/impeccable/scripts/live-server.mjs +1669 -0
- package/skills/impeccable/scripts/live-status.mjs +71 -0
- package/skills/impeccable/scripts/live-target.mjs +30 -0
- package/skills/impeccable/scripts/live-wrap.mjs +927 -0
- package/skills/impeccable/scripts/live.mjs +359 -0
- package/skills/impeccable/scripts/modern-screenshot.umd.js +14 -0
- package/skills/impeccable/scripts/palette.mjs +628 -0
- package/skills/impeccable/scripts/pin.mjs +221 -0
- package/skills/impeccable/scripts/serve-question.mjs +932 -0
- package/skills/impeccable/scripts/surface-brief.mjs +74 -0
- package/skills/inkstone-admin/SKILL.md +66 -0
- package/skills/jev-hook-gate/SKILL.md +111 -0
- package/skills/keyword-research/SKILL.md +503 -0
- package/skills/keyword-research/references/dr-cap-and-confidence.md +62 -0
- package/skills/keyword-research/references/keyword-examples.md +769 -0
- package/skills/landing-page-architect/SKILL.md +585 -0
- package/skills/landing-page-architect/references/architecture-reference.md +32 -0
- package/skills/launch-strategy/SKILL.md +384 -0
- package/skills/launch-strategy/evals/evals.json +91 -0
- package/skills/launch-strategy/references/launch-checklist.md +209 -0
- package/skills/launch-strategy/references/launch-timeline.md +159 -0
- package/skills/lead-email-craft/SKILL.md +522 -0
- package/skills/lead-magnets/SKILL.md +310 -0
- package/skills/lead-magnets/references/benchmarks.md +129 -0
- package/skills/lead-magnets/references/format-guide.md +196 -0
- package/skills/link-building-seo/SKILL.md +210 -0
- package/skills/lint-report.txt +1 -0
- package/skills/manifest.json +129 -0
- package/skills/marketing-demo/SKILL.md +249 -0
- package/skills/marketing-demo/references/remotion-examples.md +281 -0
- package/skills/marketing-ideas/SKILL.md +167 -0
- package/skills/marketing-ideas/evals/evals.json +90 -0
- package/skills/marketing-ideas/references/ideas-by-category.md +366 -0
- package/skills/marketing-psychology/SKILL.md +374 -0
- package/skills/marketing-psychology/evals/evals.json +88 -0
- package/skills/marketing-psychology/references/case-studies.md +148 -0
- package/skills/narrative-research-article/SKILL.md +469 -0
- package/skills/narrative-research-article/references/lanusse-patterns.md +166 -0
- package/skills/notebooklm/SKILL.md +674 -0
- package/skills/off-page-seo/SKILL.md +219 -0
- package/skills/onboarding-cro/SKILL.md +220 -0
- package/skills/onboarding-cro/evals/evals.json +92 -0
- package/skills/onboarding-cro/references/experiments.md +258 -0
- package/skills/original-research/SKILL.md +388 -0
- package/skills/page-cro/SKILL.md +269 -0
- package/skills/page-cro/references/cro-benchmarks.md +140 -0
- package/skills/paper-marketing/SKILL.md +448 -0
- package/skills/paper-marketing/references/agent-quality-rubric.md +68 -0
- package/skills/paper-marketing/references/brand-system-template.md +120 -0
- package/skills/paper-marketing/references/paper-mcp-rules.md +71 -0
- package/skills/paper-marketing/references/platform-conventions.md +43 -0
- package/skills/paywall-upgrade-cro/SKILL.md +227 -0
- package/skills/paywall-upgrade-cro/evals/evals.json +93 -0
- package/skills/paywall-upgrade-cro/references/experiments.md +164 -0
- package/skills/platform-formatting/SKILL.md +51 -0
- package/skills/popup-cro/SKILL.md +454 -0
- package/skills/popup-cro/evals/evals.json +94 -0
- package/skills/positioning-angles/SKILL.md +524 -0
- package/skills/positioning-angles/references/ad-matrix-and-examples.md +176 -0
- package/skills/positioning-angles/references/angle-frameworks.md +187 -0
- package/skills/positioning-angles/references/dunford-positioning.md +135 -0
- package/skills/positioning-angles/references/hormozi-offer.md +247 -0
- package/skills/positioning-angles/references/schwartz-sophistication.md +166 -0
- package/skills/positioning-angles/references/unique-mechanism.md +162 -0
- package/skills/pricing-strategy/SKILL.md +346 -0
- package/skills/pricing-strategy/evals/evals.json +90 -0
- package/skills/pricing-strategy/references/research-methods.md +152 -0
- package/skills/pricing-strategy/references/tier-structure.md +307 -0
- package/skills/programmatic-seo/SKILL.md +238 -0
- package/skills/programmatic-seo/evals/evals.json +94 -0
- package/skills/programmatic-seo/references/playbooks.md +308 -0
- package/skills/project-rag/SKILL.md +127 -0
- package/skills/quality-gate/SKILL.md +519 -0
- package/skills/quality-gate/judge-personas/buyer.md +47 -0
- package/skills/quality-gate/judge-personas/expert.md +47 -0
- package/skills/quality-gate/judge-personas/outsider.md +47 -0
- package/skills/quality-gate/judge-personas/scanner.md +41 -0
- package/skills/quality-gate/judge-personas/skeptic.md +40 -0
- package/skills/referral-program/SKILL.md +290 -0
- package/skills/referral-program/evals/evals.json +89 -0
- package/skills/referral-program/references/affiliate-programs.md +163 -0
- package/skills/referral-program/references/program-examples.md +143 -0
- package/skills/review-sites/SKILL.md +279 -0
- package/skills/revops/SKILL.md +343 -0
- package/skills/revops/evals/evals.json +91 -0
- package/skills/revops/references/automation-playbooks.md +290 -0
- package/skills/revops/references/lifecycle-definitions.md +278 -0
- package/skills/revops/references/routing-rules.md +203 -0
- package/skills/revops/references/scoring-models.md +247 -0
- package/skills/saas-product-seo/SKILL.md +324 -0
- package/skills/sales-enablement/SKILL.md +349 -0
- package/skills/sales-enablement/evals/evals.json +91 -0
- package/skills/sales-enablement/references/deck-frameworks.md +263 -0
- package/skills/sales-enablement/references/demo-scripts.md +355 -0
- package/skills/sales-enablement/references/objection-library.md +270 -0
- package/skills/sales-enablement/references/one-pager-templates.md +208 -0
- package/skills/schema-markup/SKILL.md +179 -0
- package/skills/schema-markup/evals/evals.json +87 -0
- package/skills/schema-markup/references/schema-examples.md +398 -0
- package/skills/self-improve/SKILL.md +166 -0
- package/skills/self-improving-agent/README.md +243 -0
- package/skills/self-improving-agent/SKILL.md +437 -0
- package/skills/self-improving-agent/scripts/__init__.py +16 -0
- package/skills/self-improving-agent/scripts/__pycache__/auto_detect.cpython-314.pyc +0 -0
- package/skills/self-improving-agent/scripts/__pycache__/nlm_curation.cpython-314.pyc +0 -0
- package/skills/self-improving-agent/scripts/__pycache__/self_improving_agent.cpython-314.pyc +0 -0
- package/skills/self-improving-agent/scripts/auto_detect.py +270 -0
- package/skills/self-improving-agent/scripts/init_expertise_registry.py +89 -0
- package/skills/self-improving-agent/scripts/nlm_curation.py +259 -0
- package/skills/self-improving-agent/scripts/self-improving-agent +113 -0
- package/skills/self-improving-agent/scripts/self_improving_agent.py +603 -0
- package/skills/self-improving-agent/scripts/test_curation_scorer.py +20 -0
- package/skills/self-improving-agent/scripts/test_self_improving.py +184 -0
- package/skills/send-email/SKILL.md +426 -0
- package/skills/send-email/references/batch-email-examples.md +807 -0
- package/skills/send-email/references/best-practices.md +453 -0
- package/skills/send-email/references/installation.md +123 -0
- package/skills/send-email/references/single-email-examples.md +470 -0
- package/skills/send-email/references/webhooks.md +221 -0
- package/skills/seo-audit/SKILL.md +362 -0
- package/skills/seo-audit/references/ai-writing-detection.md +200 -0
- package/skills/seo-audit/references/architecture-patterns.md +271 -0
- package/skills/seo-audit/references/schema-templates.md +427 -0
- package/skills/seo-content/SKILL.md +510 -0
- package/skills/seo-content/references/anti-ai-detection.md +184 -0
- package/skills/seo-content/references/content-structures.md +148 -0
- package/skills/seo-content/references/eeat-examples.md +235 -0
- package/skills/seo-content/references/on-page-seo.md +261 -0
- package/skills/seo-content/references/pseo-playbooks.md +226 -0
- package/skills/seo-content/references/schema-templates.md +104 -0
- package/skills/seo-content/references/serp-analysis-format.md +151 -0
- package/skills/seo-machine/SKILL.md +331 -0
- package/skills/seo-machine/assets/brand-template.md +73 -0
- package/skills/seo-machine/assets/link-inventory-template.md +74 -0
- package/skills/seo-machine/assets/roadmap-template.md +184 -0
- package/skills/seo-machine/references/api-stack-recipes.md +230 -0
- package/skills/seo-machine/references/exa-recipes.md +260 -0
- package/skills/seo-machine/references/manual-research.md +73 -0
- package/skills/seo-machine/references/methodology.md +87 -0
- package/skills/seo-machine/references/off-page.md +123 -0
- package/skills/seo-machine/references/patterns/alternatives.md +176 -0
- package/skills/seo-machine/references/patterns/compare.md +105 -0
- package/skills/seo-machine/references/patterns/playbooks.md +106 -0
- package/skills/seo-machine/references/patterns/use-case.md +109 -0
- package/skills/seo-machine/references/quality-bars.md +99 -0
- package/skills/seo-machine/references/stacks/astro.md +156 -0
- package/skills/seo-machine/references/stacks/detection.md +61 -0
- package/skills/seo-machine/references/stacks/markdown-fallback.md +181 -0
- package/skills/seo-machine/references/stacks/nextjs.md +108 -0
- package/skills/seo-machine/references/stacks/rails-inertia.md +99 -0
- package/skills/seo-machine/references/striking-distance.md +116 -0
- package/skills/seo-machine/references/technical-audit.md +153 -0
- package/skills/seo-machine/scripts/link_audit.py +265 -0
- package/skills/seo-machine/scripts/tech_audit.py +221 -0
- package/skills/seo-machine/scripts/word_count.py +154 -0
- package/skills/sharp-thesis-validator/SKILL.md +393 -0
- package/skills/sharp-thesis-validator/examples.md +186 -0
- package/skills/signup-flow-cro/SKILL.md +359 -0
- package/skills/signup-flow-cro/evals/evals.json +88 -0
- package/skills/site-architecture/SKILL.md +357 -0
- package/skills/site-architecture/evals/evals.json +88 -0
- package/skills/site-architecture/references/mermaid-templates.md +216 -0
- package/skills/site-architecture/references/navigation-patterns.md +305 -0
- package/skills/site-architecture/references/site-type-templates.md +293 -0
- package/skills/skill-self-improvement/SKILL.md +151 -0
- package/skills/slideshow-script/SKILL.md +274 -0
- package/skills/slideshow-script/references/content-spec-schema.md +77 -0
- package/skills/slideshow-script/references/frameworks.md +84 -0
- package/skills/slideshow-script/references/paips-framework.md +37 -0
- package/skills/social-campaign/SKILL.md +419 -0
- package/skills/social-campaign/references/paper-mcp-workflow.md +48 -0
- package/skills/social-content/SKILL.md +359 -0
- package/skills/social-content/evals/evals.json +92 -0
- package/skills/social-content/references/platforms.md +170 -0
- package/skills/social-content/references/post-templates.md +177 -0
- package/skills/social-content/references/reverse-engineering.md +195 -0
- package/skills/social-worker/SKILL.md +204 -0
- package/skills/startup-launcher/SKILL.md +252 -0
- package/skills/startup-launcher/references/file-router.md +64 -0
- package/skills/startup-launcher/references/platform-matrix.md +65 -0
- package/skills/startup-launcher/references/platforms/alternativeto.md +283 -0
- package/skills/startup-launcher/references/platforms/appsumo.md +495 -0
- package/skills/startup-launcher/references/platforms/auraplusplus.md +736 -0
- package/skills/startup-launcher/references/platforms/betalist.md +98 -0
- package/skills/startup-launcher/references/platforms/capterra.md +276 -0
- package/skills/startup-launcher/references/platforms/crunchbase.md +514 -0
- package/skills/startup-launcher/references/platforms/dealify.md +291 -0
- package/skills/startup-launcher/references/platforms/dealmirror.md +313 -0
- package/skills/startup-launcher/references/platforms/desifounder.md +490 -0
- package/skills/startup-launcher/references/platforms/devhunt.md +210 -0
- package/skills/startup-launcher/references/platforms/devpost.md +448 -0
- package/skills/startup-launcher/references/platforms/fazier.md +56 -0
- package/skills/startup-launcher/references/platforms/firsto.md +489 -0
- package/skills/startup-launcher/references/platforms/g2.md +226 -0
- package/skills/startup-launcher/references/platforms/hackernews.md +99 -0
- package/skills/startup-launcher/references/platforms/hackernoon.md +385 -0
- package/skills/startup-launcher/references/platforms/indiedeals.md +367 -0
- package/skills/startup-launcher/references/platforms/indiehackers.md +80 -0
- package/skills/startup-launcher/references/platforms/kenmoo.md +231 -0
- package/skills/startup-launcher/references/platforms/labstartups.md +618 -0
- package/skills/startup-launcher/references/platforms/launchboard.md +307 -0
- package/skills/startup-launcher/references/platforms/launchfast.md +560 -0
- package/skills/startup-launcher/references/platforms/launchigniter.md +56 -0
- package/skills/startup-launcher/references/platforms/launchingnext.md +674 -0
- package/skills/startup-launcher/references/platforms/launchit.md +574 -0
- package/skills/startup-launcher/references/platforms/launchyourapp.md +677 -0
- package/skills/startup-launcher/references/platforms/libhunt.md +220 -0
- package/skills/startup-launcher/references/platforms/ltdhunt.md +221 -0
- package/skills/startup-launcher/references/platforms/microlaunch.md +45 -0
- package/skills/startup-launcher/references/platforms/openalternative.md +195 -0
- package/skills/startup-launcher/references/platforms/peerlist.md +56 -0
- package/skills/startup-launcher/references/platforms/peerpush.md +64 -0
- package/skills/startup-launcher/references/platforms/primeclub.md +282 -0
- package/skills/startup-launcher/references/platforms/productcanyon.md +190 -0
- package/skills/startup-launcher/references/platforms/producthunt.md +98 -0
- package/skills/startup-launcher/references/platforms/rankinpublic.md +315 -0
- package/skills/startup-launcher/references/platforms/rockethub.md +374 -0
- package/skills/startup-launcher/references/platforms/saasgenius.md +175 -0
- package/skills/startup-launcher/references/platforms/saashub.md +205 -0
- package/skills/startup-launcher/references/platforms/saasmantra.md +221 -0
- package/skills/startup-launcher/references/platforms/saaspirate.md +287 -0
- package/skills/startup-launcher/references/platforms/saaswarrior.md +190 -0
- package/skills/startup-launcher/references/platforms/saaszilla.md +194 -0
- package/skills/startup-launcher/references/platforms/selected.md +925 -0
- package/skills/startup-launcher/references/platforms/sideprojectors.md +86 -0
- package/skills/startup-launcher/references/platforms/softonic.md +240 -0
- package/skills/startup-launcher/references/platforms/sourceforge.md +251 -0
- package/skills/startup-launcher/references/platforms/stacksocial.md +244 -0
- package/skills/startup-launcher/references/platforms/startupstash.md +326 -0
- package/skills/startup-launcher/references/platforms/theresanaiforthat.md +253 -0
- package/skills/startup-launcher/references/platforms/tinylaunch.md +70 -0
- package/skills/startup-launcher/references/platforms/tinystartups.md +72 -0
- package/skills/startup-launcher/references/platforms/toolfolio.md +291 -0
- package/skills/startup-launcher/references/platforms/trustmrr.md +194 -0
- package/skills/startup-launcher/references/platforms/trylaunch.md +531 -0
- package/skills/startup-launcher/references/platforms/uneed.md +57 -0
- package/skills/startup-launcher/references/templates/asset-checklist.md +218 -0
- package/skills/startup-launcher/references/templates/launch-tracker.md +195 -0
- package/skills/startup-launcher/references/templates/product-brief.md +98 -0
- package/skills/summarize/.skill-meta.json +13 -0
- package/skills/summarize/SKILL.md +170 -0
- package/skills/summarize/rules/install.md +157 -0
- package/skills/summarize/rules/usage.md +207 -0
- package/skills/topical-authority/SKILL.md +307 -0
- package/skills/topical-authority/references/jake-ward-framework.md +201 -0
- package/skills/topical-authority/references/serp-clustering-algorithm.md +325 -0
- package/skills/topical-authority/scripts/orchestrator.js +367 -0
- package/skills/topical-authority/scripts/seo/analyze-competitor-coverage.js +3 -0
- package/skills/topical-authority/scripts/seo/audit-internal-links.js +3 -0
- package/skills/topical-authority/scripts/seo/build-clusters-from-serp.js +3 -0
- package/skills/topical-authority/scripts/seo/extract-keywords-from-forums.js +3 -0
- package/skills/topical-authority/scripts/seo/extract-paa-questions.js +3 -0
- package/skills/topical-authority/scripts/seo/fetch-serp-urls.js +3 -0
- package/skills/topical-authority/scripts/seo/gemini-serp-clustering.js +3 -0
- package/skills/topical-authority/scripts/seo/identify-content-gaps.js +3 -0
- package/skills/topical-authority/scripts/seo/identify-research-gaps.js +3 -0
- package/skills/topical-authority/scripts/seo/scrape-quora-questions.js +3 -0
- package/skills/topical-authority/scripts/seo/scrape-reddit-pain-points.js +3 -0
- package/skills/topical-authority/scripts/seo/weave-internal-links.js +3 -0
- package/skills/visual-style/SKILL.md +199 -0
- package/skills/voice-brand-alignment/SKILL.md +70 -0
- package/skills/voice-brand-alignment/references/ai-slop-patterns.md +405 -0
- package/skills/voice-brand-alignment/scripts/validation/gemini-voice-validator.js +96 -0
- package/skills/voice-extraction/SKILL.md +234 -0
- package/skills/voice-extraction/references/extraction-prompt.md +155 -0
- package/skills/voice-extraction/references/voice-dna-template.md +168 -0
- package/skills/voice-extraction/references/voice-elements.md +259 -0
- package/skills/voice-extraction/scripts/build-corpus.js +120 -0
- package/skills/voice-extraction/scripts/collect-sources.sh +168 -0
- package/skills/warm-outbound/SKILL.md +299 -0
- package/skills/warm-outbound/templates/warm-message.md +39 -0
- package/skills/weekly-newsletter-digest/SKILL.md +78 -0
- package/skills/x-playwriter/SKILL.md +428 -0
- package/skills/x-playwriter/scripts/x-browser.js +185 -0
- package/skills/x-post-generator-openclaw/SKILL.md +138 -0
- package/skills/x-post-generator-openclaw/scripts/generate-x-post.js +537 -0
- package/skills/x-posting-playwriter/SKILL.md +203 -0
- package/skills/x-research/SKILL.md +129 -0
- package/skills/x-research/x-research.sh +85 -0
- package/skills/youtube-summarize/SKILL.md +90 -0
- package/skills/youtube-summarize/scripts/youtube-summarize.sh +143 -0
- package/src/cli.js +347 -172
- package/src/harness/deepseek-adapter.js +102 -0
- package/src/harness/deepseek.js +225 -0
- package/src/harness/opencode-adapter.js +174 -0
- package/src/harness/pi-adapter.js +172 -0
- package/src/harness/registry.js +121 -0
- package/src/harness/telemetry.js +102 -0
- package/src/harness/types.js +165 -0
- package/src/interview.js +36 -4
- package/src/postinstall.js +10 -12
- package/templates/AGENTS.md +17 -17
- package/templates/ORG.md +1 -1
- package/templates/TOOLS.md +7 -2
- package/templates/opencode.json +26 -29
- package/wiki/doctor.md +54 -0
- package/wiki/inkstone.md +4 -4
- package/wiki/opencode.md +3 -3
- 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
|