@heyai-rules/pilo-masterkit 2.1.0 → 3.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/.agent/agents/PILO_MASTER.md +77 -77
- package/.agent/agents/architect.md +211 -211
- package/.agent/agents/backend-specialist.md +263 -263
- package/.agent/agents/build-error-resolver.md +114 -114
- package/.agent/agents/chief-of-staff.md +151 -151
- package/.agent/agents/code-archaeologist.md +106 -106
- package/.agent/agents/code-reviewer.md +237 -237
- package/.agent/agents/cpp-build-resolver.md +90 -90
- package/.agent/agents/cpp-reviewer.md +72 -72
- package/.agent/agents/csharp-reviewer.md +101 -101
- package/.agent/agents/dart-build-resolver.md +201 -201
- package/.agent/agents/database-architect.md +226 -226
- package/.agent/agents/database-reviewer.md +91 -91
- package/.agent/agents/debugger.md +225 -225
- package/.agent/agents/devops-engineer.md +242 -242
- package/.agent/agents/doc-updater.md +107 -107
- package/.agent/agents/docs-lookup.md +68 -68
- package/.agent/agents/documentation-writer.md +104 -104
- package/.agent/agents/e2e-runner.md +107 -107
- package/.agent/agents/explorer-agent.md +73 -73
- package/.agent/agents/flutter-reviewer.md +243 -243
- package/.agent/agents/frontend-specialist.md +593 -593
- package/.agent/agents/game-developer.md +162 -162
- package/.agent/agents/gan-evaluator.md +209 -209
- package/.agent/agents/gan-generator.md +131 -131
- package/.agent/agents/gan-planner.md +99 -99
- package/.agent/agents/go-build-resolver.md +94 -94
- package/.agent/agents/go-reviewer.md +76 -76
- package/.agent/agents/harness-optimizer.md +35 -35
- package/.agent/agents/healthcare-reviewer.md +83 -83
- package/.agent/agents/java-build-resolver.md +153 -153
- package/.agent/agents/java-reviewer.md +92 -92
- package/.agent/agents/kotlin-build-resolver.md +118 -118
- package/.agent/agents/kotlin-reviewer.md +159 -159
- package/.agent/agents/loop-operator.md +36 -36
- package/.agent/agents/mobile-developer.md +377 -377
- package/.agent/agents/opensource-forker.md +198 -198
- package/.agent/agents/opensource-packager.md +249 -249
- package/.agent/agents/opensource-sanitizer.md +188 -188
- package/.agent/agents/orchestrator.md +416 -416
- package/.agent/agents/penetration-tester.md +188 -188
- package/.agent/agents/performance-optimizer.md +446 -446
- package/.agent/agents/personas/athena-agent/agent.json +10 -10
- package/.agent/agents/personas/athena-agent/athena-backend-logic-architecture-profile.md +3 -3
- package/.agent/agents/personas/athena-agent/context-files/agents.md +1 -1
- package/.agent/agents/personas/athena-agent/context-files/identity.md +1 -1
- package/.agent/agents/personas/athena-agent/context-files/soul.md +1 -1
- package/.agent/agents/personas/athena-agent/context-files/user-predefined.md +1 -1
- package/.agent/agents/personas/athena-agent/user-context-files/system/bootstrap.md +1 -1
- package/.agent/agents/personas/athena-agent/user-context-files/system/user.md +1 -1
- package/.agent/agents/personas/da-vinci-agent/agent.json +10 -10
- package/.agent/agents/personas/da-vinci-agent/context-files/agents.md +1 -1
- package/.agent/agents/personas/da-vinci-agent/context-files/identity.md +1 -1
- package/.agent/agents/personas/da-vinci-agent/context-files/soul.md +1 -1
- package/.agent/agents/personas/da-vinci-agent/context-files/user-predefined.md +1 -1
- package/.agent/agents/personas/da-vinci-agent/da-vinci-frontend-ui-ux-design-profile.md +3 -3
- package/.agent/agents/personas/da-vinci-agent/user-context-files/system/bootstrap.md +1 -1
- package/.agent/agents/personas/da-vinci-agent/user-context-files/system/user.md +1 -1
- package/.agent/agents/personas/duong-tang-agent/agent.json +10 -10
- package/.agent/agents/personas/duong-tang-agent/context-files/agents.md +1 -1
- package/.agent/agents/personas/duong-tang-agent/context-files/identity.md +1 -1
- package/.agent/agents/personas/duong-tang-agent/context-files/soul.md +1 -1
- package/.agent/agents/personas/duong-tang-agent/context-files/user-predefined.md +1 -1
- package/.agent/agents/personas/duong-tang-agent/tang-monk-quality-testing-documentation-profile.md +3 -3
- package/.agent/agents/personas/duong-tang-agent/user-context-files/system/bootstrap.md +1 -1
- package/.agent/agents/personas/duong-tang-agent/user-context-files/system/user.md +1 -1
- package/.agent/agents/personas/gia-cat-luong-agent/agent.json +10 -10
- package/.agent/agents/personas/gia-cat-luong-agent/context-files/agents.md +1 -1
- package/.agent/agents/personas/gia-cat-luong-agent/context-files/identity.md +1 -1
- package/.agent/agents/personas/gia-cat-luong-agent/context-files/soul.md +1 -1
- package/.agent/agents/personas/gia-cat-luong-agent/context-files/user-predefined.md +1 -1
- package/.agent/agents/personas/gia-cat-luong-agent/kongming-research-strategy-analysis-profile.md +3 -3
- package/.agent/agents/personas/gia-cat-luong-agent/user-context-files/system/bootstrap.md +1 -1
- package/.agent/agents/personas/gia-cat-luong-agent/user-context-files/system/user.md +1 -1
- package/.agent/agents/personas/mihata-agent/agent.json +10 -10
- package/.agent/agents/personas/mihata-agent/context-files/agents.md +1 -1
- package/.agent/agents/personas/mihata-agent/context-files/identity.md +1 -1
- package/.agent/agents/personas/mihata-agent/context-files/soul.md +1 -1
- package/.agent/agents/personas/mihata-agent/context-files/user-predefined.md +1 -1
- package/.agent/agents/personas/mihata-agent/mihata-multi-agent-orchestration-profile.md +3 -3
- package/.agent/agents/personas/mihata-agent/user-context-files/system/bootstrap.md +1 -1
- package/.agent/agents/personas/mihata-agent/user-context-files/system/user.md +1 -1
- package/.agent/agents/personas/tesla-agent/agent.json +10 -10
- package/.agent/agents/personas/tesla-agent/context-files/agents.md +1 -1
- package/.agent/agents/personas/tesla-agent/context-files/identity.md +1 -1
- package/.agent/agents/personas/tesla-agent/context-files/soul.md +1 -1
- package/.agent/agents/personas/tesla-agent/context-files/user-predefined.md +1 -1
- package/.agent/agents/personas/tesla-agent/tesla-fullstack-system-optimization-profile.md +3 -3
- package/.agent/agents/personas/tesla-agent/user-context-files/system/bootstrap.md +1 -1
- package/.agent/agents/personas/tesla-agent/user-context-files/system/user.md +1 -1
- package/.agent/agents/personas/tu-ma-y-agent/agent.json +10 -10
- package/.agent/agents/personas/tu-ma-y-agent/context-files/agents.md +1 -1
- package/.agent/agents/personas/tu-ma-y-agent/context-files/identity.md +1 -1
- package/.agent/agents/personas/tu-ma-y-agent/context-files/soul.md +1 -1
- package/.agent/agents/personas/tu-ma-y-agent/context-files/user-predefined.md +1 -1
- package/.agent/agents/personas/tu-ma-y-agent/simayi-feasibility-risk-control-profile.md +3 -3
- package/.agent/agents/personas/tu-ma-y-agent/user-context-files/system/bootstrap.md +1 -1
- package/.agent/agents/personas/tu-ma-y-agent/user-context-files/system/user.md +1 -1
- package/.agent/agents/personas/venti-agent/agent.json +10 -10
- package/.agent/agents/personas/venti-agent/context-files/agents.md +1 -1
- package/.agent/agents/personas/venti-agent/context-files/identity.md +1 -1
- package/.agent/agents/personas/venti-agent/context-files/soul.md +1 -1
- package/.agent/agents/personas/venti-agent/context-files/user-predefined.md +1 -1
- package/.agent/agents/personas/venti-agent/user-context-files/system/bootstrap.md +1 -1
- package/.agent/agents/personas/venti-agent/user-context-files/system/user.md +1 -1
- package/.agent/agents/personas/venti-agent/venti-learning-communication-mentoring-profile.md +3 -3
- package/.agent/agents/planner.md +212 -212
- package/.agent/agents/product-manager.md +112 -112
- package/.agent/agents/product-owner.md +95 -95
- package/.agent/agents/project-planner.md +406 -406
- package/.agent/agents/python-reviewer.md +98 -98
- package/.agent/agents/pytorch-build-resolver.md +120 -120
- package/.agent/agents/qa-automation-engineer.md +103 -103
- package/.agent/agents/refactor-cleaner.md +85 -85
- package/.agent/agents/rust-build-resolver.md +148 -148
- package/.agent/agents/rust-reviewer.md +94 -94
- package/.agent/agents/security-auditor.md +170 -170
- package/.agent/agents/security-reviewer.md +108 -108
- package/.agent/agents/seo-specialist.md +111 -111
- package/.agent/agents/tdd-guide.md +91 -91
- package/.agent/agents/test-engineer.md +158 -158
- package/.agent/agents/typescript-reviewer.md +112 -112
- package/.agent/contexts/dev.md +20 -20
- package/.agent/contexts/research.md +26 -26
- package/.agent/contexts/review.md +22 -22
- package/.agent/hooks/hooks.json +395 -395
- package/.agent/hooks/readme.md +222 -222
- package/.agent/mcp-configs/mcp-servers.json +181 -181
- package/.agent/rules/ARCHITECTURAL_BLUEPRINTS.md +62 -62
- package/.agent/rules/CODE_CRAFTSMANSHIP.md +69 -69
- package/.agent/rules/CORE_RULES.md +72 -72
- package/.agent/rules/PROJECT_MAP.md +58 -58
- package/.agent/rules/QUALITY_ASSURANCE.md +54 -54
- package/.agent/rules/SECURITY_ARMOR.md +44 -44
- package/.agent/rules/VERSION_ORCHESTRATION.md +64 -64
- package/.agent/rules/WORKFLOW_ORCHESTRATION.md +55 -55
- package/.agent/rules/common/agents.md +50 -50
- package/.agent/rules/common/code-review.md +124 -124
- package/.agent/rules/common/coding-style.md +48 -48
- package/.agent/rules/common/development-workflow.md +44 -44
- package/.agent/rules/common/git-workflow.md +24 -24
- package/.agent/rules/common/hooks.md +30 -30
- package/.agent/rules/common/patterns.md +31 -31
- package/.agent/rules/common/performance.md +55 -55
- package/.agent/rules/common/security.md +29 -29
- package/.agent/rules/common/testing.md +29 -29
- package/.agent/rules/cpp/coding-style.md +44 -44
- package/.agent/rules/cpp/hooks.md +39 -39
- package/.agent/rules/cpp/patterns.md +51 -51
- package/.agent/rules/cpp/security.md +51 -51
- package/.agent/rules/cpp/testing.md +44 -44
- package/.agent/rules/csharp/coding-style.md +72 -72
- package/.agent/rules/csharp/hooks.md +25 -25
- package/.agent/rules/csharp/patterns.md +50 -50
- package/.agent/rules/csharp/security.md +58 -58
- package/.agent/rules/csharp/testing.md +46 -46
- package/.agent/rules/dart/coding-style.md +159 -159
- package/.agent/rules/dart/hooks.md +66 -66
- package/.agent/rules/dart/patterns.md +261 -261
- package/.agent/rules/dart/security.md +135 -135
- package/.agent/rules/dart/testing.md +215 -215
- package/.agent/rules/golang/coding-style.md +32 -32
- package/.agent/rules/golang/hooks.md +17 -17
- package/.agent/rules/golang/patterns.md +45 -45
- package/.agent/rules/golang/security.md +34 -34
- package/.agent/rules/golang/testing.md +31 -31
- package/.agent/rules/java/coding-style.md +114 -114
- package/.agent/rules/java/hooks.md +18 -18
- package/.agent/rules/java/patterns.md +146 -146
- package/.agent/rules/java/security.md +100 -100
- package/.agent/rules/java/testing.md +131 -131
- package/.agent/rules/kotlin/coding-style.md +86 -86
- package/.agent/rules/kotlin/hooks.md +17 -17
- package/.agent/rules/kotlin/patterns.md +146 -146
- package/.agent/rules/kotlin/security.md +82 -82
- package/.agent/rules/kotlin/testing.md +128 -128
- package/.agent/rules/perl/coding-style.md +46 -46
- package/.agent/rules/perl/hooks.md +22 -22
- package/.agent/rules/perl/patterns.md +76 -76
- package/.agent/rules/perl/security.md +69 -69
- package/.agent/rules/perl/testing.md +54 -54
- package/.agent/rules/php/coding-style.md +40 -40
- package/.agent/rules/php/hooks.md +24 -24
- package/.agent/rules/php/patterns.md +33 -33
- package/.agent/rules/php/security.md +37 -37
- package/.agent/rules/php/testing.md +39 -39
- package/.agent/rules/python/coding-style.md +42 -42
- package/.agent/rules/python/hooks.md +19 -19
- package/.agent/rules/python/patterns.md +39 -39
- package/.agent/rules/python/security.md +30 -30
- package/.agent/rules/python/testing.md +38 -38
- package/.agent/rules/readme.md +111 -111
- package/.agent/rules/rust/coding-style.md +151 -151
- package/.agent/rules/rust/hooks.md +16 -16
- package/.agent/rules/rust/patterns.md +168 -168
- package/.agent/rules/rust/security.md +141 -141
- package/.agent/rules/rust/testing.md +154 -154
- package/.agent/rules/swift/coding-style.md +47 -47
- package/.agent/rules/swift/hooks.md +20 -20
- package/.agent/rules/swift/patterns.md +66 -66
- package/.agent/rules/swift/security.md +33 -33
- package/.agent/rules/swift/testing.md +45 -45
- package/.agent/rules/typescript/coding-style.md +199 -199
- package/.agent/rules/typescript/hooks.md +22 -22
- package/.agent/rules/typescript/patterns.md +52 -52
- package/.agent/rules/typescript/security.md +28 -28
- package/.agent/rules/typescript/testing.md +18 -18
- package/.agent/rules/web/coding-style.md +96 -96
- package/.agent/rules/web/design-quality.md +63 -63
- package/.agent/rules/web/hooks.md +120 -120
- package/.agent/rules/web/patterns.md +79 -79
- package/.agent/rules/web/performance.md +64 -64
- package/.agent/rules/web/security.md +57 -57
- package/.agent/rules/web/testing.md +55 -55
- package/.agent/rules/zh/agents.md +50 -50
- package/.agent/rules/zh/code-review.md +124 -124
- package/.agent/rules/zh/coding-style.md +48 -48
- package/.agent/rules/zh/development-workflow.md +44 -44
- package/.agent/rules/zh/git-workflow.md +24 -24
- package/.agent/rules/zh/hooks.md +30 -30
- package/.agent/rules/zh/patterns.md +31 -31
- package/.agent/rules/zh/performance.md +55 -55
- package/.agent/rules/zh/readme.md +108 -108
- package/.agent/rules/zh/security.md +29 -29
- package/.agent/rules/zh/testing.md +29 -29
- package/.agent/scripts/auto_preview.py +148 -148
- package/.agent/scripts/checklist.py +217 -217
- package/.agent/scripts/session_manager.py +120 -120
- package/.agent/scripts/verify_all.py +327 -327
- package/.agent/skills/agent-eval/SKILL.md +145 -145
- package/.agent/skills/agent-harness-construction/SKILL.md +73 -73
- package/.agent/skills/agent-payment-x402/SKILL.md +178 -178
- package/.agent/skills/agentic-engineering/SKILL.md +63 -63
- package/.agent/skills/ai-first-engineering/SKILL.md +51 -51
- package/.agent/skills/ai-regression-testing/SKILL.md +385 -385
- package/.agent/skills/android-clean-architecture/SKILL.md +339 -339
- package/.agent/skills/api-design/SKILL.md +523 -523
- package/.agent/skills/api-patterns/SKILL.md +81 -81
- package/.agent/skills/api-patterns/api-style.md +42 -42
- package/.agent/skills/api-patterns/auth.md +24 -24
- package/.agent/skills/api-patterns/documentation.md +26 -26
- package/.agent/skills/api-patterns/graphql.md +41 -41
- package/.agent/skills/api-patterns/rate-limiting.md +31 -31
- package/.agent/skills/api-patterns/response.md +37 -37
- package/.agent/skills/api-patterns/rest.md +40 -40
- package/.agent/skills/api-patterns/scripts/api_validator.py +211 -211
- package/.agent/skills/api-patterns/security-testing.md +122 -122
- package/.agent/skills/api-patterns/trpc.md +41 -41
- package/.agent/skills/api-patterns/versioning.md +22 -22
- package/.agent/skills/app-builder/SKILL.md +75 -75
- package/.agent/skills/app-builder/agent-coordination.md +71 -71
- package/.agent/skills/app-builder/feature-building.md +53 -53
- package/.agent/skills/app-builder/project-detection.md +34 -34
- package/.agent/skills/app-builder/scaffolding.md +118 -118
- package/.agent/skills/app-builder/tech-stack.md +41 -41
- package/.agent/skills/app-builder/templates/SKILL.md +39 -39
- package/.agent/skills/app-builder/templates/astro-static/TEMPLATE.md +76 -76
- package/.agent/skills/app-builder/templates/chrome-extension/TEMPLATE.md +92 -92
- package/.agent/skills/app-builder/templates/cli-tool/TEMPLATE.md +88 -88
- package/.agent/skills/app-builder/templates/electron-desktop/TEMPLATE.md +88 -88
- package/.agent/skills/app-builder/templates/express-api/TEMPLATE.md +83 -83
- package/.agent/skills/app-builder/templates/flutter-app/TEMPLATE.md +90 -90
- package/.agent/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +90 -90
- package/.agent/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +122 -122
- package/.agent/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +122 -122
- package/.agent/skills/app-builder/templates/nextjs-static/TEMPLATE.md +169 -169
- package/.agent/skills/app-builder/templates/nuxt-app/TEMPLATE.md +134 -134
- package/.agent/skills/app-builder/templates/python-fastapi/TEMPLATE.md +83 -83
- package/.agent/skills/app-builder/templates/react-native-app/TEMPLATE.md +119 -119
- package/.agent/skills/architecture/SKILL.md +55 -55
- package/.agent/skills/architecture/context-discovery.md +43 -43
- package/.agent/skills/architecture/examples.md +94 -94
- package/.agent/skills/architecture/pattern-selection.md +68 -68
- package/.agent/skills/architecture/patterns-reference.md +50 -50
- package/.agent/skills/architecture/trade-off-analysis.md +77 -77
- package/.agent/skills/architecture-decision-records/SKILL.md +179 -179
- package/.agent/skills/article-writing/SKILL.md +79 -79
- package/.agent/skills/autonomous-agent-harness/SKILL.md +267 -267
- package/.agent/skills/autonomous-loops/SKILL.md +610 -610
- package/.agent/skills/backend-patterns/SKILL.md +598 -598
- package/.agent/skills/bash-linux/SKILL.md +199 -199
- package/.agent/skills/behavioral-modes/SKILL.md +242 -242
- package/.agent/skills/benchmark/SKILL.md +93 -93
- package/.agent/skills/blueprint/SKILL.md +105 -105
- package/.agent/skills/brainstorming/SKILL.md +163 -163
- package/.agent/skills/brainstorming/dynamic-questioning.md +350 -350
- package/.agent/skills/brand-voice/SKILL.md +97 -97
- package/.agent/skills/brand-voice/references/voice-profile-schema.md +55 -55
- package/.agent/skills/browser-qa/SKILL.md +87 -87
- package/.agent/skills/bun-runtime/SKILL.md +84 -84
- package/.agent/skills/canary-watch/SKILL.md +99 -99
- package/.agent/skills/carrier-relationship-management/SKILL.md +212 -212
- package/.agent/skills/ck/SKILL.md +147 -147
- package/.agent/skills/ck/commands/forget.mjs +44 -44
- package/.agent/skills/ck/commands/info.mjs +24 -24
- package/.agent/skills/ck/commands/init.mjs +143 -143
- package/.agent/skills/ck/commands/list.mjs +40 -40
- package/.agent/skills/ck/commands/migrate.mjs +202 -202
- package/.agent/skills/ck/commands/resume.mjs +36 -36
- package/.agent/skills/ck/commands/save.mjs +210 -210
- package/.agent/skills/ck/commands/shared.mjs +387 -387
- package/.agent/skills/ck/hooks/session-start.mjs +224 -224
- package/.agent/skills/claude-api/SKILL.md +337 -337
- package/.agent/skills/claude-devfleet/SKILL.md +103 -103
- package/.agent/skills/clean-code/SKILL.md +201 -201
- package/.agent/skills/click-path-audit/SKILL.md +244 -244
- package/.agent/skills/clickhouse-io/SKILL.md +439 -439
- package/.agent/skills/code-review-checklist/SKILL.md +109 -109
- package/.agent/skills/codebase-onboarding/SKILL.md +233 -233
- package/.agent/skills/coding-standards/SKILL.md +530 -530
- package/.agent/skills/compose-multiplatform-patterns/SKILL.md +299 -299
- package/.agent/skills/configure-ecc/SKILL.md +367 -367
- package/.agent/skills/connections-optimizer/SKILL.md +189 -189
- package/.agent/skills/content-engine/SKILL.md +131 -131
- package/.agent/skills/content-hash-cache-pattern/SKILL.md +161 -161
- package/.agent/skills/context-budget/SKILL.md +135 -135
- package/.agent/skills/continuous-agent-loop/SKILL.md +45 -45
- package/.agent/skills/continuous-learning/SKILL.md +119 -119
- package/.agent/skills/continuous-learning/config.json +18 -18
- package/.agent/skills/continuous-learning/evaluate-session.sh +69 -69
- package/.agent/skills/continuous-learning-v2/SKILL.md +365 -365
- package/.agent/skills/continuous-learning-v2/agents/observer-loop.sh +271 -271
- package/.agent/skills/continuous-learning-v2/agents/observer.md +198 -198
- package/.agent/skills/continuous-learning-v2/agents/session-guardian.sh +150 -150
- package/.agent/skills/continuous-learning-v2/agents/start-observer.sh +244 -244
- package/.agent/skills/continuous-learning-v2/config.json +8 -8
- package/.agent/skills/continuous-learning-v2/hooks/observe.sh +428 -428
- package/.agent/skills/continuous-learning-v2/scripts/detect-project.sh +228 -228
- package/.agent/skills/continuous-learning-v2/scripts/instinct-cli.py +1426 -1426
- package/.agent/skills/continuous-learning-v2/scripts/test-parse-instinct.py +984 -984
- package/.agent/skills/cost-aware-llm-pipeline/SKILL.md +183 -183
- package/.agent/skills/cpp-coding-standards/SKILL.md +723 -723
- package/.agent/skills/cpp-testing/SKILL.md +324 -324
- package/.agent/skills/crosspost/SKILL.md +111 -111
- package/.agent/skills/csharp-testing/SKILL.md +321 -321
- package/.agent/skills/customer-billing-ops/SKILL.md +140 -140
- package/.agent/skills/customs-trade-compliance/SKILL.md +263 -263
- package/.agent/skills/dart-flutter-patterns/SKILL.md +563 -563
- package/.agent/skills/data-scraper-agent/SKILL.md +764 -764
- package/.agent/skills/database-design/SKILL.md +52 -52
- package/.agent/skills/database-design/database-selection.md +43 -43
- package/.agent/skills/database-design/indexing.md +39 -39
- package/.agent/skills/database-design/migrations.md +48 -48
- package/.agent/skills/database-design/optimization.md +36 -36
- package/.agent/skills/database-design/orm-selection.md +30 -30
- package/.agent/skills/database-design/schema-design.md +56 -56
- package/.agent/skills/database-design/scripts/schema_validator.py +172 -172
- package/.agent/skills/database-migrations/SKILL.md +429 -429
- package/.agent/skills/deep-research/SKILL.md +155 -155
- package/.agent/skills/deployment-patterns/SKILL.md +427 -427
- package/.agent/skills/deployment-procedures/SKILL.md +241 -241
- package/.agent/skills/design-system/SKILL.md +82 -82
- package/.agent/skills/django-patterns/SKILL.md +734 -734
- package/.agent/skills/django-security/SKILL.md +593 -593
- package/.agent/skills/django-tdd/SKILL.md +729 -729
- package/.agent/skills/django-verification/SKILL.md +469 -469
- package/.agent/skills/dmux-workflows/SKILL.md +191 -191
- package/.agent/skills/doc.md +177 -177
- package/.agent/skills/docker-patterns/SKILL.md +364 -364
- package/.agent/skills/documentation-lookup/SKILL.md +90 -90
- package/.agent/skills/documentation-templates/SKILL.md +194 -194
- package/.agent/skills/dotnet-patterns/SKILL.md +321 -321
- package/.agent/skills/e2e-testing/SKILL.md +326 -326
- package/.agent/skills/energy-procurement/SKILL.md +228 -228
- package/.agent/skills/enterprise-agent-ops/SKILL.md +50 -50
- package/.agent/skills/eval-harness/SKILL.md +270 -270
- package/.agent/skills/exa-search/SKILL.md +103 -103
- package/.agent/skills/fal-ai-media/SKILL.md +284 -284
- package/.agent/skills/flutter-dart-code-review/SKILL.md +435 -435
- package/.agent/skills/foundation-models-on-device/SKILL.md +243 -243
- package/.agent/skills/frontend-design/SKILL.md +452 -452
- package/.agent/skills/frontend-design/animation-guide.md +331 -331
- package/.agent/skills/frontend-design/color-system.md +311 -311
- package/.agent/skills/frontend-design/decision-trees.md +418 -418
- package/.agent/skills/frontend-design/motion-graphics.md +306 -306
- package/.agent/skills/frontend-design/scripts/accessibility_checker.py +183 -183
- package/.agent/skills/frontend-design/scripts/ux_audit.py +722 -722
- package/.agent/skills/frontend-design/typography-system.md +345 -345
- package/.agent/skills/frontend-design/ux-psychology.md +1116 -1116
- package/.agent/skills/frontend-design/visual-effects.md +383 -383
- package/.agent/skills/frontend-patterns/SKILL.md +642 -642
- package/.agent/skills/frontend-slides/SKILL.md +184 -184
- package/.agent/skills/frontend-slides/style-presets.md +330 -330
- package/.agent/skills/game-development/2d-games/SKILL.md +119 -119
- package/.agent/skills/game-development/3d-games/SKILL.md +135 -135
- package/.agent/skills/game-development/SKILL.md +167 -167
- package/.agent/skills/game-development/game-art/SKILL.md +185 -185
- package/.agent/skills/game-development/game-audio/SKILL.md +190 -190
- package/.agent/skills/game-development/game-design/SKILL.md +129 -129
- package/.agent/skills/game-development/mobile-games/SKILL.md +108 -108
- package/.agent/skills/game-development/multiplayer/SKILL.md +132 -132
- package/.agent/skills/game-development/pc-games/SKILL.md +144 -144
- package/.agent/skills/game-development/vr-ar/SKILL.md +123 -123
- package/.agent/skills/game-development/web-games/SKILL.md +150 -150
- package/.agent/skills/gan-style-harness/SKILL.md +278 -278
- package/.agent/skills/geo-fundamentals/SKILL.md +156 -156
- package/.agent/skills/geo-fundamentals/scripts/geo_checker.py +289 -289
- package/.agent/skills/git-workflow/SKILL.md +715 -715
- package/.agent/skills/golang-patterns/SKILL.md +674 -674
- package/.agent/skills/golang-testing/SKILL.md +720 -720
- package/.agent/skills/google-workspace-ops/SKILL.md +95 -95
- package/.agent/skills/healthcare-cdss-patterns/SKILL.md +245 -245
- package/.agent/skills/healthcare-emr-patterns/SKILL.md +159 -159
- package/.agent/skills/healthcare-eval-harness/SKILL.md +207 -207
- package/.agent/skills/healthcare-phi-compliance/SKILL.md +145 -145
- package/.agent/skills/hexagonal-architecture/SKILL.md +276 -276
- package/.agent/skills/i18n-localization/SKILL.md +154 -154
- package/.agent/skills/i18n-localization/scripts/i18n_checker.py +241 -241
- package/.agent/skills/intelligent-routing/SKILL.md +335 -335
- package/.agent/skills/inventory-demand-planning/SKILL.md +247 -247
- package/.agent/skills/investor-materials/SKILL.md +96 -96
- package/.agent/skills/investor-outreach/SKILL.md +91 -91
- package/.agent/skills/iterative-retrieval/SKILL.md +211 -211
- package/.agent/skills/java-coding-standards/SKILL.md +147 -147
- package/.agent/skills/jira-integration/SKILL.md +293 -293
- package/.agent/skills/jpa-patterns/SKILL.md +151 -151
- package/.agent/skills/kotlin-coroutines-flows/SKILL.md +284 -284
- package/.agent/skills/kotlin-exposed-patterns/SKILL.md +719 -719
- package/.agent/skills/kotlin-ktor-patterns/SKILL.md +689 -689
- package/.agent/skills/kotlin-patterns/SKILL.md +711 -711
- package/.agent/skills/kotlin-testing/SKILL.md +824 -824
- package/.agent/skills/laravel-patterns/SKILL.md +415 -415
- package/.agent/skills/laravel-plugin-discovery/SKILL.md +229 -229
- package/.agent/skills/laravel-security/SKILL.md +285 -285
- package/.agent/skills/laravel-tdd/SKILL.md +283 -283
- package/.agent/skills/laravel-verification/SKILL.md +179 -179
- package/.agent/skills/lead-intelligence/SKILL.md +321 -321
- package/.agent/skills/lead-intelligence/agents/enrichment-agent.md +85 -85
- package/.agent/skills/lead-intelligence/agents/mutual-mapper.md +75 -75
- package/.agent/skills/lead-intelligence/agents/outreach-drafter.md +98 -98
- package/.agent/skills/lead-intelligence/agents/signal-scorer.md +60 -60
- package/.agent/skills/lint-and-validate/SKILL.md +45 -45
- package/.agent/skills/lint-and-validate/scripts/lint_runner.py +184 -184
- package/.agent/skills/lint-and-validate/scripts/type_coverage.py +173 -173
- package/.agent/skills/liquid-glass-design/SKILL.md +279 -279
- package/.agent/skills/logistics-exception-management/SKILL.md +222 -222
- package/.agent/skills/manim-video/SKILL.md +89 -89
- package/.agent/skills/manim-video/assets/network-graph-scene.py +52 -52
- package/.agent/skills/market-research/SKILL.md +75 -75
- package/.agent/skills/mcp-server-patterns/SKILL.md +67 -67
- package/.agent/skills/mobile-design/SKILL.md +394 -394
- package/.agent/skills/mobile-design/decision-trees.md +516 -516
- package/.agent/skills/mobile-design/mobile-backend.md +491 -491
- package/.agent/skills/mobile-design/mobile-color-system.md +420 -420
- package/.agent/skills/mobile-design/mobile-debugging.md +122 -122
- package/.agent/skills/mobile-design/mobile-design-thinking.md +357 -357
- package/.agent/skills/mobile-design/mobile-navigation.md +458 -458
- package/.agent/skills/mobile-design/mobile-performance.md +767 -767
- package/.agent/skills/mobile-design/mobile-testing.md +356 -356
- package/.agent/skills/mobile-design/mobile-typography.md +433 -433
- package/.agent/skills/mobile-design/platform-android.md +666 -666
- package/.agent/skills/mobile-design/platform-ios.md +561 -561
- package/.agent/skills/mobile-design/scripts/mobile_audit.py +670 -670
- package/.agent/skills/mobile-design/touch-psychology.md +537 -537
- package/.agent/skills/nanoclaw-repl/SKILL.md +33 -33
- package/.agent/skills/nestjs-patterns/SKILL.md +230 -230
- package/.agent/skills/nextjs-react-expert/1-async-eliminating-waterfalls.md +351 -351
- package/.agent/skills/nextjs-react-expert/2-bundle-bundle-size-optimization.md +240 -240
- package/.agent/skills/nextjs-react-expert/3-server-server-side-performance.md +490 -490
- package/.agent/skills/nextjs-react-expert/4-client-client-side-data-fetching.md +264 -264
- package/.agent/skills/nextjs-react-expert/5-rerender-re-render-optimization.md +581 -581
- package/.agent/skills/nextjs-react-expert/6-rendering-rendering-performance.md +432 -432
- package/.agent/skills/nextjs-react-expert/7-js-javascript-performance.md +684 -684
- package/.agent/skills/nextjs-react-expert/8-advanced-advanced-patterns.md +150 -150
- package/.agent/skills/nextjs-react-expert/9-cache-components.md +103 -103
- package/.agent/skills/nextjs-react-expert/SKILL.md +293 -293
- package/.agent/skills/nextjs-react-expert/scripts/convert_rules.py +222 -222
- package/.agent/skills/nextjs-react-expert/scripts/react_performance_checker.py +252 -252
- package/.agent/skills/nextjs-turbopack/SKILL.md +44 -44
- package/.agent/skills/nodejs-best-practices/SKILL.md +333 -333
- package/.agent/skills/nutrient-document-processing/SKILL.md +167 -167
- package/.agent/skills/nuxt4-patterns/SKILL.md +100 -100
- package/.agent/skills/openclaw-persona-forge/SKILL.md +296 -296
- package/.agent/skills/openclaw-persona-forge/gacha.py +224 -224
- package/.agent/skills/openclaw-persona-forge/gacha.sh +5 -5
- package/.agent/skills/openclaw-persona-forge/references/avatar-style.md +124 -124
- package/.agent/skills/openclaw-persona-forge/references/boundary-rules.md +53 -53
- package/.agent/skills/openclaw-persona-forge/references/error-handling.md +53 -53
- package/.agent/skills/openclaw-persona-forge/references/identity-tension.md +48 -48
- package/.agent/skills/openclaw-persona-forge/references/naming-system.md +39 -39
- package/.agent/skills/openclaw-persona-forge/references/output-template.md +166 -166
- package/.agent/skills/opensource-pipeline/SKILL.md +255 -255
- package/.agent/skills/parallel-agents/SKILL.md +175 -175
- package/.agent/skills/performance-profiling/SKILL.md +143 -143
- package/.agent/skills/performance-profiling/scripts/lighthouse_audit.py +76 -76
- package/.agent/skills/perl-patterns/SKILL.md +504 -504
- package/.agent/skills/perl-security/SKILL.md +503 -503
- package/.agent/skills/perl-testing/SKILL.md +475 -475
- package/.agent/skills/plan-writing/SKILL.md +152 -152
- package/.agent/skills/plankton-code-quality/SKILL.md +236 -236
- package/.agent/skills/postgres-patterns/SKILL.md +147 -147
- package/.agent/skills/powershell-windows/SKILL.md +167 -167
- package/.agent/skills/product-lens/SKILL.md +85 -85
- package/.agent/skills/production-scheduling/SKILL.md +238 -238
- package/.agent/skills/project-flow-ops/SKILL.md +111 -111
- package/.agent/skills/project-guidelines-example/SKILL.md +349 -349
- package/.agent/skills/prompt-optimizer/SKILL.md +397 -397
- package/.agent/skills/python-patterns/SKILL.md +750 -750
- package/.agent/skills/python-testing/SKILL.md +816 -816
- package/.agent/skills/pytorch-patterns/SKILL.md +396 -396
- package/.agent/skills/quality-nonconformance/SKILL.md +260 -260
- package/.agent/skills/ralphinho-rfc-pipeline/SKILL.md +67 -67
- package/.agent/skills/red-team-tactics/SKILL.md +199 -199
- package/.agent/skills/regex-vs-llm-structured-text/SKILL.md +220 -220
- package/.agent/skills/remotion-video-creation/SKILL.md +43 -43
- package/.agent/skills/remotion-video-creation/rules/3d.md +86 -86
- package/.agent/skills/remotion-video-creation/rules/animations.md +29 -29
- package/.agent/skills/remotion-video-creation/rules/assets/charts-bar-chart.tsx +173 -173
- package/.agent/skills/remotion-video-creation/rules/assets/text-animations-typewriter.tsx +100 -100
- package/.agent/skills/remotion-video-creation/rules/assets/text-animations-word-highlight.tsx +108 -108
- package/.agent/skills/remotion-video-creation/rules/assets.md +78 -78
- package/.agent/skills/remotion-video-creation/rules/audio.md +172 -172
- package/.agent/skills/remotion-video-creation/rules/calculate-metadata.md +104 -104
- package/.agent/skills/remotion-video-creation/rules/can-decode.md +75 -75
- package/.agent/skills/remotion-video-creation/rules/charts.md +58 -58
- package/.agent/skills/remotion-video-creation/rules/compositions.md +146 -146
- package/.agent/skills/remotion-video-creation/rules/display-captions.md +126 -126
- package/.agent/skills/remotion-video-creation/rules/extract-frames.md +229 -229
- package/.agent/skills/remotion-video-creation/rules/fonts.md +152 -152
- package/.agent/skills/remotion-video-creation/rules/get-audio-duration.md +58 -58
- package/.agent/skills/remotion-video-creation/rules/get-video-dimensions.md +68 -68
- package/.agent/skills/remotion-video-creation/rules/get-video-duration.md +58 -58
- package/.agent/skills/remotion-video-creation/rules/gifs.md +138 -138
- package/.agent/skills/remotion-video-creation/rules/images.md +130 -130
- package/.agent/skills/remotion-video-creation/rules/import-srt-captions.md +67 -67
- package/.agent/skills/remotion-video-creation/rules/lottie.md +67 -67
- package/.agent/skills/remotion-video-creation/rules/measuring-dom-nodes.md +34 -34
- package/.agent/skills/remotion-video-creation/rules/measuring-text.md +143 -143
- package/.agent/skills/remotion-video-creation/rules/sequencing.md +106 -106
- package/.agent/skills/remotion-video-creation/rules/tailwind.md +11 -11
- package/.agent/skills/remotion-video-creation/rules/text-animations.md +20 -20
- package/.agent/skills/remotion-video-creation/rules/timing.md +179 -179
- package/.agent/skills/remotion-video-creation/rules/transcribe-captions.md +19 -19
- package/.agent/skills/remotion-video-creation/rules/transitions.md +122 -122
- package/.agent/skills/remotion-video-creation/rules/trimming.md +52 -52
- package/.agent/skills/remotion-video-creation/rules/videos.md +171 -171
- package/.agent/skills/repo-scan/SKILL.md +63 -63
- package/.agent/skills/returns-reverse-logistics/SKILL.md +240 -240
- package/.agent/skills/rules-distill/SKILL.md +264 -264
- package/.agent/skills/rules-distill/scripts/scan-rules.sh +58 -58
- package/.agent/skills/rules-distill/scripts/scan-skills.sh +129 -129
- package/.agent/skills/rust-patterns/SKILL.md +499 -499
- package/.agent/skills/rust-pro/SKILL.md +175 -175
- package/.agent/skills/rust-testing/SKILL.md +500 -500
- package/.agent/skills/safety-guard/SKILL.md +75 -75
- package/.agent/skills/santa-method/SKILL.md +306 -306
- package/.agent/skills/search-first/SKILL.md +161 -161
- package/.agent/skills/security-review/SKILL.md +495 -495
- package/.agent/skills/security-review/cloud-infrastructure-security.md +361 -361
- package/.agent/skills/security-scan/SKILL.md +165 -165
- package/.agent/skills/seo-fundamentals/SKILL.md +129 -129
- package/.agent/skills/seo-fundamentals/scripts/seo_checker.py +219 -219
- package/.agent/skills/server-management/SKILL.md +161 -161
- package/.agent/skills/skill-comply/SKILL.md +58 -58
- package/.agent/skills/skill-comply/fixtures/compliant-trace.jsonl +5 -5
- package/.agent/skills/skill-comply/fixtures/noncompliant-trace.jsonl +3 -3
- package/.agent/skills/skill-comply/fixtures/tdd-spec.yaml +44 -44
- package/.agent/skills/skill-comply/prompts/classifier.md +24 -24
- package/.agent/skills/skill-comply/prompts/scenario-generator.md +62 -62
- package/.agent/skills/skill-comply/prompts/spec-generator.md +42 -42
- package/.agent/skills/skill-comply/pyproject.toml +15 -15
- package/.agent/skills/skill-comply/scripts/classifier.py +85 -85
- package/.agent/skills/skill-comply/scripts/grader.py +122 -122
- package/.agent/skills/skill-comply/scripts/parser.py +107 -107
- package/.agent/skills/skill-comply/scripts/report.py +170 -170
- package/.agent/skills/skill-comply/scripts/run.py +127 -127
- package/.agent/skills/skill-comply/scripts/runner.py +161 -161
- package/.agent/skills/skill-comply/scripts/scenario-generator.py +70 -70
- package/.agent/skills/skill-comply/scripts/spec-generator.py +72 -72
- package/.agent/skills/skill-comply/scripts/utils.py +13 -13
- package/.agent/skills/skill-comply/tests/test-grader.py +137 -137
- package/.agent/skills/skill-comply/tests/test-parser.py +90 -90
- package/.agent/skills/skill-stocktake/SKILL.md +193 -193
- package/.agent/skills/skill-stocktake/scripts/quick-diff.sh +87 -87
- package/.agent/skills/skill-stocktake/scripts/save-results.sh +56 -56
- package/.agent/skills/skill-stocktake/scripts/scan.sh +170 -170
- package/.agent/skills/social-graph-ranker/SKILL.md +154 -154
- package/.agent/skills/springboot-patterns/SKILL.md +314 -314
- package/.agent/skills/springboot-security/SKILL.md +272 -272
- package/.agent/skills/springboot-tdd/SKILL.md +158 -158
- package/.agent/skills/springboot-verification/SKILL.md +231 -231
- package/.agent/skills/strategic-compact/SKILL.md +131 -131
- package/.agent/skills/strategic-compact/suggest-compact.sh +54 -54
- package/.agent/skills/swift-actor-persistence/SKILL.md +143 -143
- package/.agent/skills/swift-concurrency-6-2/SKILL.md +216 -216
- package/.agent/skills/swift-protocol-di-testing/SKILL.md +190 -190
- package/.agent/skills/swiftui-patterns/SKILL.md +259 -259
- package/.agent/skills/systematic-debugging/SKILL.md +109 -109
- package/.agent/skills/tailwind-patterns/SKILL.md +269 -269
- package/.agent/skills/tdd-workflow/SKILL.md +463 -463
- package/.agent/skills/team-builder/SKILL.md +168 -168
- package/.agent/skills/testing-patterns/SKILL.md +178 -178
- package/.agent/skills/testing-patterns/scripts/test_runner.py +219 -219
- package/.agent/skills/token-budget-advisor/SKILL.md +133 -133
- package/.agent/skills/ui-demo/SKILL.md +465 -465
- package/.agent/skills/ui-ux-pro-max/SKILL.md +292 -292
- package/.agent/skills/ui-ux-pro-max/data/charts.csv +26 -26
- package/.agent/skills/ui-ux-pro-max/data/colors.csv +97 -97
- package/.agent/skills/ui-ux-pro-max/data/icons.csv +101 -101
- package/.agent/skills/ui-ux-pro-max/data/landing.csv +31 -31
- package/.agent/skills/ui-ux-pro-max/data/products.csv +96 -96
- package/.agent/skills/ui-ux-pro-max/data/react-performance.csv +45 -45
- package/.agent/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -54
- package/.agent/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -53
- package/.agent/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -56
- package/.agent/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -53
- package/.agent/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -53
- package/.agent/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -51
- package/.agent/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -59
- package/.agent/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -52
- package/.agent/skills/ui-ux-pro-max/data/stacks/react.csv +54 -54
- package/.agent/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -61
- package/.agent/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -54
- package/.agent/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -51
- package/.agent/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -50
- package/.agent/skills/ui-ux-pro-max/data/styles.csv +68 -68
- package/.agent/skills/ui-ux-pro-max/data/typography.csv +57 -57
- package/.agent/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -101
- package/.agent/skills/ui-ux-pro-max/data/ux-guidelines.csv +99 -99
- package/.agent/skills/ui-ux-pro-max/data/web-interface.csv +31 -31
- package/.agent/skills/ui-ux-pro-max/scripts/core.py +253 -253
- package/.agent/skills/ui-ux-pro-max/scripts/design_system.py +1067 -1067
- package/.agent/skills/ui-ux-pro-max/scripts/search.py +114 -114
- package/.agent/skills/verification-loop/SKILL.md +126 -126
- package/.agent/skills/video-editing/SKILL.md +310 -310
- package/.agent/skills/videodb/SKILL.md +374 -374
- package/.agent/skills/videodb/reference/api-reference.md +550 -550
- package/.agent/skills/videodb/reference/capture-reference.md +407 -407
- package/.agent/skills/videodb/reference/capture.md +101 -101
- package/.agent/skills/videodb/reference/editor.md +443 -443
- package/.agent/skills/videodb/reference/generative.md +331 -331
- package/.agent/skills/videodb/reference/rtstream-reference.md +564 -564
- package/.agent/skills/videodb/reference/rtstream.md +65 -65
- package/.agent/skills/videodb/reference/search.md +230 -230
- package/.agent/skills/videodb/reference/streaming.md +406 -406
- package/.agent/skills/videodb/reference/use-cases.md +118 -118
- package/.agent/skills/videodb/scripts/ws-listener.py +282 -282
- package/.agent/skills/visa-doc-translate/SKILL.md +117 -117
- package/.agent/skills/visa-doc-translate/readme.md +86 -86
- package/.agent/skills/vulnerability-scanner/SKILL.md +276 -276
- package/.agent/skills/vulnerability-scanner/checklists.md +121 -121
- package/.agent/skills/vulnerability-scanner/scripts/security_scan.py +458 -458
- package/.agent/skills/web-design-guidelines/SKILL.md +57 -57
- package/.agent/skills/webapp-testing/SKILL.md +187 -187
- package/.agent/skills/webapp-testing/scripts/playwright_runner.py +173 -173
- package/.agent/skills/workspace-surface-audit/SKILL.md +125 -125
- package/.agent/skills/x-api/SKILL.md +230 -230
- package/.agent/tasks/lessons.md +40 -40
- package/.agent/tasks/todo.md +33 -33
- package/.agent/tasks/two-track-merge-contract.md +1 -1
- package/.agent/workflows/aside.md +164 -164
- package/.agent/workflows/brainstorm.md +113 -113
- package/.agent/workflows/build-fix.md +62 -62
- package/.agent/workflows/checkpoint.md +74 -74
- package/.agent/workflows/claw.md +23 -23
- package/.agent/workflows/clean-memory.md +34 -34
- package/.agent/workflows/code-review.md +289 -289
- package/.agent/workflows/context-budget.md +23 -23
- package/.agent/workflows/cpp-build.md +173 -173
- package/.agent/workflows/cpp-review.md +132 -132
- package/.agent/workflows/cpp-test.md +251 -251
- package/.agent/workflows/create.md +59 -59
- package/.agent/workflows/debug.md +103 -103
- package/.agent/workflows/deploy.md +176 -176
- package/.agent/workflows/devfleet.md +23 -23
- package/.agent/workflows/docs.md +23 -23
- package/.agent/workflows/e2e.md +268 -268
- package/.agent/workflows/enhance.md +63 -63
- package/.agent/workflows/eval.md +23 -23
- package/.agent/workflows/evolve.md +178 -178
- package/.agent/workflows/flutter-build.md +164 -164
- package/.agent/workflows/flutter-review.md +116 -116
- package/.agent/workflows/flutter-test.md +144 -144
- package/.agent/workflows/gan-build.md +99 -99
- package/.agent/workflows/gan-design.md +35 -35
- package/.agent/workflows/go-build.md +183 -183
- package/.agent/workflows/go-review.md +148 -148
- package/.agent/workflows/go-test.md +268 -268
- package/.agent/workflows/gradle-build.md +70 -70
- package/.agent/workflows/harness-audit.md +73 -73
- package/.agent/workflows/init-docs.md +46 -46
- package/.agent/workflows/instinct-export.md +66 -66
- package/.agent/workflows/instinct-import.md +114 -114
- package/.agent/workflows/instinct-status.md +59 -59
- package/.agent/workflows/jira.md +106 -106
- package/.agent/workflows/kotlin-build.md +174 -174
- package/.agent/workflows/kotlin-review.md +140 -140
- package/.agent/workflows/kotlin-test.md +312 -312
- package/.agent/workflows/learn-eval.md +116 -116
- package/.agent/workflows/learn.md +70 -70
- package/.agent/workflows/loop-start.md +32 -32
- package/.agent/workflows/loop-status.md +24 -24
- package/.agent/workflows/model-route.md +26 -26
- package/.agent/workflows/multi-backend.md +158 -158
- package/.agent/workflows/multi-execute.md +315 -315
- package/.agent/workflows/multi-frontend.md +158 -158
- package/.agent/workflows/multi-plan.md +268 -268
- package/.agent/workflows/multi-workflow.md +191 -191
- package/.agent/workflows/orchestrate.md +135 -135
- package/.agent/workflows/plan.md +117 -117
- package/.agent/workflows/pm2.md +272 -272
- package/.agent/workflows/preview.md +81 -81
- package/.agent/workflows/projects.md +39 -39
- package/.agent/workflows/promote.md +41 -41
- package/.agent/workflows/prompt-optimize.md +23 -23
- package/.agent/workflows/prp-commit.md +112 -112
- package/.agent/workflows/prp-implement.md +385 -385
- package/.agent/workflows/prp-plan.md +502 -502
- package/.agent/workflows/prp-pr.md +184 -184
- package/.agent/workflows/prp-prd.md +447 -447
- package/.agent/workflows/prune.md +31 -31
- package/.agent/workflows/python-review.md +297 -297
- package/.agent/workflows/quality-gate.md +29 -29
- package/.agent/workflows/refactor-clean.md +80 -80
- package/.agent/workflows/resume-session.md +156 -156
- package/.agent/workflows/rules-distill.md +20 -20
- package/.agent/workflows/rust-build.md +187 -187
- package/.agent/workflows/rust-review.md +142 -142
- package/.agent/workflows/rust-test.md +308 -308
- package/.agent/workflows/santa-loop.md +175 -175
- package/.agent/workflows/save-session.md +275 -275
- package/.agent/workflows/sessions.md +333 -333
- package/.agent/workflows/setup-pm.md +80 -80
- package/.agent/workflows/skill-create.md +174 -174
- package/.agent/workflows/skill-health.md +54 -54
- package/.agent/workflows/status.md +86 -86
- package/.agent/workflows/tdd.md +231 -231
- package/.agent/workflows/test-coverage.md +69 -69
- package/.agent/workflows/test.md +144 -144
- package/.agent/workflows/ui-ux-pro-max.md +295 -295
- package/.agent/workflows/update-codemaps.md +72 -72
- package/.agent/workflows/update-docs.md +84 -84
- package/.agent/workflows/verify.md +23 -23
- package/LICENSE +176 -176
- package/README.md +144 -144
- package/package.json +1 -1
- package/scripts/release-check.js +55 -55
- package/src/bin/cli.js +424 -354
- package/src/lib/installer.js +223 -11
|
@@ -1,984 +1,984 @@
|
|
|
1
|
-
"""Tests for continuous-learning-v2 instinct-cli.py
|
|
2
|
-
|
|
3
|
-
Covers:
|
|
4
|
-
- parse_instinct_file() — content preservation, edge cases
|
|
5
|
-
- _validate_file_path() — path traversal blocking
|
|
6
|
-
- detect_project() — project detection with mocked git/env
|
|
7
|
-
- load_all_instincts() — loading from project + global dirs, dedup
|
|
8
|
-
- _load_instincts_from_dir() — directory scanning
|
|
9
|
-
- cmd_projects() — listing projects from registry
|
|
10
|
-
- cmd_status() — status display
|
|
11
|
-
- _promote_specific() — single instinct promotion
|
|
12
|
-
- _promote_auto() — auto-promotion across projects
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
import importlib.util
|
|
16
|
-
import io
|
|
17
|
-
import json
|
|
18
|
-
import os
|
|
19
|
-
import sys
|
|
20
|
-
from pathlib import Path
|
|
21
|
-
from types import SimpleNamespace
|
|
22
|
-
from unittest import mock
|
|
23
|
-
|
|
24
|
-
import pytest
|
|
25
|
-
|
|
26
|
-
# Load instinct-cli.py (hyphenated filename requires importlib)
|
|
27
|
-
_spec = importlib.util.spec_from_file_location(
|
|
28
|
-
"instinct_cli",
|
|
29
|
-
os.path.join(os.path.dirname(__file__), "instinct-cli.py"),
|
|
30
|
-
)
|
|
31
|
-
_mod = importlib.util.module_from_spec(_spec)
|
|
32
|
-
_spec.loader.exec_module(_mod)
|
|
33
|
-
|
|
34
|
-
parse_instinct_file = _mod.parse_instinct_file
|
|
35
|
-
_validate_file_path = _mod._validate_file_path
|
|
36
|
-
detect_project = _mod.detect_project
|
|
37
|
-
load_all_instincts = _mod.load_all_instincts
|
|
38
|
-
load_project_only_instincts = _mod.load_project_only_instincts
|
|
39
|
-
_load_instincts_from_dir = _mod._load_instincts_from_dir
|
|
40
|
-
cmd_status = _mod.cmd_status
|
|
41
|
-
cmd_projects = _mod.cmd_projects
|
|
42
|
-
_promote_specific = _mod._promote_specific
|
|
43
|
-
_promote_auto = _mod._promote_auto
|
|
44
|
-
_find_cross_project_instincts = _mod._find_cross_project_instincts
|
|
45
|
-
load_registry = _mod.load_registry
|
|
46
|
-
_validate_instinct_id = _mod._validate_instinct_id
|
|
47
|
-
_update_registry = _mod._update_registry
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
# ─────────────────────────────────────────────
|
|
51
|
-
# Fixtures
|
|
52
|
-
# ─────────────────────────────────────────────
|
|
53
|
-
|
|
54
|
-
SAMPLE_INSTINCT_YAML = """\
|
|
55
|
-
---
|
|
56
|
-
id: test-instinct
|
|
57
|
-
trigger: "when writing tests"
|
|
58
|
-
confidence: 0.8
|
|
59
|
-
domain: testing
|
|
60
|
-
scope: project
|
|
61
|
-
---
|
|
62
|
-
|
|
63
|
-
## Action
|
|
64
|
-
Always write tests first.
|
|
65
|
-
|
|
66
|
-
## Evidence
|
|
67
|
-
TDD leads to better design.
|
|
68
|
-
"""
|
|
69
|
-
|
|
70
|
-
SAMPLE_GLOBAL_INSTINCT_YAML = """\
|
|
71
|
-
---
|
|
72
|
-
id: global-instinct
|
|
73
|
-
trigger: "always"
|
|
74
|
-
confidence: 0.9
|
|
75
|
-
domain: security
|
|
76
|
-
scope: global
|
|
77
|
-
---
|
|
78
|
-
|
|
79
|
-
## Action
|
|
80
|
-
Validate all user input.
|
|
81
|
-
"""
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
@pytest.fixture
|
|
85
|
-
def project_tree(tmp_path):
|
|
86
|
-
"""Create a realistic project directory tree for testing."""
|
|
87
|
-
homunculus = tmp_path / ".claude" / "homunculus"
|
|
88
|
-
projects_dir = homunculus / "projects"
|
|
89
|
-
global_personal = homunculus / "instincts" / "personal"
|
|
90
|
-
global_inherited = homunculus / "instincts" / "inherited"
|
|
91
|
-
global_evolved = homunculus / "evolved"
|
|
92
|
-
|
|
93
|
-
for d in [
|
|
94
|
-
global_personal, global_inherited,
|
|
95
|
-
global_evolved / "skills", global_evolved / "commands", global_evolved / "agents",
|
|
96
|
-
projects_dir,
|
|
97
|
-
]:
|
|
98
|
-
d.mkdir(parents=True, exist_ok=True)
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
"root": tmp_path,
|
|
102
|
-
"homunculus": homunculus,
|
|
103
|
-
"projects_dir": projects_dir,
|
|
104
|
-
"global_personal": global_personal,
|
|
105
|
-
"global_inherited": global_inherited,
|
|
106
|
-
"global_evolved": global_evolved,
|
|
107
|
-
"registry_file": homunculus / "projects.json",
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
@pytest.fixture
|
|
112
|
-
def patch_globals(project_tree, monkeypatch):
|
|
113
|
-
"""Patch module-level globals to use tmp_path-based directories."""
|
|
114
|
-
monkeypatch.setattr(_mod, "HOMUNCULUS_DIR", project_tree["homunculus"])
|
|
115
|
-
monkeypatch.setattr(_mod, "PROJECTS_DIR", project_tree["projects_dir"])
|
|
116
|
-
monkeypatch.setattr(_mod, "REGISTRY_FILE", project_tree["registry_file"])
|
|
117
|
-
monkeypatch.setattr(_mod, "GLOBAL_PERSONAL_DIR", project_tree["global_personal"])
|
|
118
|
-
monkeypatch.setattr(_mod, "GLOBAL_INHERITED_DIR", project_tree["global_inherited"])
|
|
119
|
-
monkeypatch.setattr(_mod, "GLOBAL_EVOLVED_DIR", project_tree["global_evolved"])
|
|
120
|
-
monkeypatch.setattr(_mod, "GLOBAL_OBSERVATIONS_FILE", project_tree["homunculus"] / "observations.jsonl")
|
|
121
|
-
return project_tree
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def _make_project(tree, pid="abc123", pname="test-project"):
|
|
125
|
-
"""Create project directory structure and return a project dict."""
|
|
126
|
-
project_dir = tree["projects_dir"] / pid
|
|
127
|
-
personal_dir = project_dir / "instincts" / "personal"
|
|
128
|
-
inherited_dir = project_dir / "instincts" / "inherited"
|
|
129
|
-
for d in [personal_dir, inherited_dir,
|
|
130
|
-
project_dir / "evolved" / "skills",
|
|
131
|
-
project_dir / "evolved" / "commands",
|
|
132
|
-
project_dir / "evolved" / "agents",
|
|
133
|
-
project_dir / "observations.archive"]:
|
|
134
|
-
d.mkdir(parents=True, exist_ok=True)
|
|
135
|
-
|
|
136
|
-
return {
|
|
137
|
-
"id": pid,
|
|
138
|
-
"name": pname,
|
|
139
|
-
"root": str(tree["root"] / "fake-repo"),
|
|
140
|
-
"remote": "https://github.com/test/test-project.git",
|
|
141
|
-
"project_dir": project_dir,
|
|
142
|
-
"instincts_personal": personal_dir,
|
|
143
|
-
"instincts_inherited": inherited_dir,
|
|
144
|
-
"evolved_dir": project_dir / "evolved",
|
|
145
|
-
"observations_file": project_dir / "observations.jsonl",
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
# ─────────────────────────────────────────────
|
|
150
|
-
# parse_instinct_file tests
|
|
151
|
-
# ─────────────────────────────────────────────
|
|
152
|
-
|
|
153
|
-
MULTI_SECTION = """\
|
|
154
|
-
---
|
|
155
|
-
id: instinct-a
|
|
156
|
-
trigger: "when coding"
|
|
157
|
-
confidence: 0.9
|
|
158
|
-
domain: general
|
|
159
|
-
---
|
|
160
|
-
|
|
161
|
-
## Action
|
|
162
|
-
Do thing A.
|
|
163
|
-
|
|
164
|
-
## Examples
|
|
165
|
-
- Example A1
|
|
166
|
-
|
|
167
|
-
---
|
|
168
|
-
id: instinct-b
|
|
169
|
-
trigger: "when testing"
|
|
170
|
-
confidence: 0.7
|
|
171
|
-
domain: testing
|
|
172
|
-
---
|
|
173
|
-
|
|
174
|
-
## Action
|
|
175
|
-
Do thing B.
|
|
176
|
-
"""
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
def test_multiple_instincts_preserve_content():
|
|
180
|
-
result = parse_instinct_file(MULTI_SECTION)
|
|
181
|
-
assert len(result) == 2
|
|
182
|
-
assert "Do thing A." in result[0]["content"]
|
|
183
|
-
assert "Example A1" in result[0]["content"]
|
|
184
|
-
assert "Do thing B." in result[1]["content"]
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
def test_single_instinct_preserves_content():
|
|
188
|
-
content = """\
|
|
189
|
-
---
|
|
190
|
-
id: solo
|
|
191
|
-
trigger: "when reviewing"
|
|
192
|
-
confidence: 0.8
|
|
193
|
-
domain: review
|
|
194
|
-
---
|
|
195
|
-
|
|
196
|
-
## Action
|
|
197
|
-
Check for security issues.
|
|
198
|
-
|
|
199
|
-
## Evidence
|
|
200
|
-
Prevents vulnerabilities.
|
|
201
|
-
"""
|
|
202
|
-
result = parse_instinct_file(content)
|
|
203
|
-
assert len(result) == 1
|
|
204
|
-
assert "Check for security issues." in result[0]["content"]
|
|
205
|
-
assert "Prevents vulnerabilities." in result[0]["content"]
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
def test_empty_content_no_error():
|
|
209
|
-
content = """\
|
|
210
|
-
---
|
|
211
|
-
id: empty
|
|
212
|
-
trigger: "placeholder"
|
|
213
|
-
confidence: 0.5
|
|
214
|
-
domain: general
|
|
215
|
-
---
|
|
216
|
-
"""
|
|
217
|
-
result = parse_instinct_file(content)
|
|
218
|
-
assert len(result) == 1
|
|
219
|
-
assert result[0]["content"] == ""
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
def test_parse_no_id_skipped():
|
|
223
|
-
"""Instincts without an 'id' field should be silently dropped."""
|
|
224
|
-
content = """\
|
|
225
|
-
---
|
|
226
|
-
trigger: "when doing nothing"
|
|
227
|
-
confidence: 0.5
|
|
228
|
-
---
|
|
229
|
-
|
|
230
|
-
No id here.
|
|
231
|
-
"""
|
|
232
|
-
result = parse_instinct_file(content)
|
|
233
|
-
assert len(result) == 0
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
def test_parse_confidence_is_float():
|
|
237
|
-
content = """\
|
|
238
|
-
---
|
|
239
|
-
id: float-check
|
|
240
|
-
trigger: "when parsing"
|
|
241
|
-
confidence: 0.42
|
|
242
|
-
domain: general
|
|
243
|
-
---
|
|
244
|
-
|
|
245
|
-
Body.
|
|
246
|
-
"""
|
|
247
|
-
result = parse_instinct_file(content)
|
|
248
|
-
assert isinstance(result[0]["confidence"], float)
|
|
249
|
-
assert result[0]["confidence"] == pytest.approx(0.42)
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
def test_parse_trigger_strips_quotes():
|
|
253
|
-
content = """\
|
|
254
|
-
---
|
|
255
|
-
id: quote-check
|
|
256
|
-
trigger: "when quoting"
|
|
257
|
-
confidence: 0.5
|
|
258
|
-
domain: general
|
|
259
|
-
---
|
|
260
|
-
|
|
261
|
-
Body.
|
|
262
|
-
"""
|
|
263
|
-
result = parse_instinct_file(content)
|
|
264
|
-
assert result[0]["trigger"] == "when quoting"
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
def test_parse_empty_string():
|
|
268
|
-
result = parse_instinct_file("")
|
|
269
|
-
assert result == []
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
def test_parse_garbage_input():
|
|
273
|
-
result = parse_instinct_file("this is not yaml at all\nno frontmatter here")
|
|
274
|
-
assert result == []
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
# ─────────────────────────────────────────────
|
|
278
|
-
# _validate_file_path tests
|
|
279
|
-
# ─────────────────────────────────────────────
|
|
280
|
-
|
|
281
|
-
def test_validate_normal_path(tmp_path):
|
|
282
|
-
test_file = tmp_path / "test.yaml"
|
|
283
|
-
test_file.write_text("hello")
|
|
284
|
-
result = _validate_file_path(str(test_file), must_exist=True)
|
|
285
|
-
assert result == test_file.resolve()
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
def test_validate_rejects_etc():
|
|
289
|
-
with pytest.raises(ValueError, match="system directory"):
|
|
290
|
-
_validate_file_path("/etc/passwd")
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
def test_validate_rejects_var_log():
|
|
294
|
-
with pytest.raises(ValueError, match="system directory"):
|
|
295
|
-
_validate_file_path("/var/log/syslog")
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
def test_validate_rejects_usr():
|
|
299
|
-
with pytest.raises(ValueError, match="system directory"):
|
|
300
|
-
_validate_file_path("/usr/local/bin/foo")
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
def test_validate_rejects_proc():
|
|
304
|
-
with pytest.raises(ValueError, match="system directory"):
|
|
305
|
-
_validate_file_path("/proc/self/status")
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
def test_validate_must_exist_fails(tmp_path):
|
|
309
|
-
with pytest.raises(ValueError, match="does not exist"):
|
|
310
|
-
_validate_file_path(str(tmp_path / "nonexistent.yaml"), must_exist=True)
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
def test_validate_home_expansion(tmp_path):
|
|
314
|
-
"""Tilde expansion should work."""
|
|
315
|
-
result = _validate_file_path("~/test.yaml")
|
|
316
|
-
assert str(result).startswith(str(Path.home()))
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
def test_validate_relative_path(tmp_path, monkeypatch):
|
|
320
|
-
"""Relative paths should be resolved."""
|
|
321
|
-
monkeypatch.chdir(tmp_path)
|
|
322
|
-
test_file = tmp_path / "rel.yaml"
|
|
323
|
-
test_file.write_text("content")
|
|
324
|
-
result = _validate_file_path("rel.yaml", must_exist=True)
|
|
325
|
-
assert result == test_file.resolve()
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
# ─────────────────────────────────────────────
|
|
329
|
-
# detect_project tests
|
|
330
|
-
# ─────────────────────────────────────────────
|
|
331
|
-
|
|
332
|
-
def test_detect_project_global_fallback(patch_globals, monkeypatch):
|
|
333
|
-
"""When no git and no env var, should return global project."""
|
|
334
|
-
monkeypatch.delenv("CLAUDE_PROJECT_DIR", raising=False)
|
|
335
|
-
|
|
336
|
-
# Mock subprocess.run to simulate git not available
|
|
337
|
-
def mock_run(*args, **kwargs):
|
|
338
|
-
raise FileNotFoundError("git not found")
|
|
339
|
-
|
|
340
|
-
monkeypatch.setattr("subprocess.run", mock_run)
|
|
341
|
-
|
|
342
|
-
project = detect_project()
|
|
343
|
-
assert project["id"] == "global"
|
|
344
|
-
assert project["name"] == "global"
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
def test_detect_project_from_env(patch_globals, monkeypatch, tmp_path):
|
|
348
|
-
"""CLAUDE_PROJECT_DIR env var should be used as project root."""
|
|
349
|
-
fake_repo = tmp_path / "my-repo"
|
|
350
|
-
fake_repo.mkdir()
|
|
351
|
-
monkeypatch.setenv("CLAUDE_PROJECT_DIR", str(fake_repo))
|
|
352
|
-
|
|
353
|
-
# Mock git remote to return a URL
|
|
354
|
-
def mock_run(cmd, **kwargs):
|
|
355
|
-
if "rev-parse" in cmd:
|
|
356
|
-
return SimpleNamespace(returncode=0, stdout=str(fake_repo) + "\n", stderr="")
|
|
357
|
-
if "get-url" in cmd:
|
|
358
|
-
return SimpleNamespace(returncode=0, stdout="https://github.com/test/my-repo.git\n", stderr="")
|
|
359
|
-
return SimpleNamespace(returncode=1, stdout="", stderr="")
|
|
360
|
-
|
|
361
|
-
monkeypatch.setattr("subprocess.run", mock_run)
|
|
362
|
-
|
|
363
|
-
project = detect_project()
|
|
364
|
-
assert project["id"] != "global"
|
|
365
|
-
assert project["name"] == "my-repo"
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
def test_detect_project_git_timeout(patch_globals, monkeypatch):
|
|
369
|
-
"""Git timeout should fall through to global."""
|
|
370
|
-
monkeypatch.delenv("CLAUDE_PROJECT_DIR", raising=False)
|
|
371
|
-
import subprocess as sp
|
|
372
|
-
|
|
373
|
-
def mock_run(cmd, **kwargs):
|
|
374
|
-
raise sp.TimeoutExpired(cmd, 5)
|
|
375
|
-
|
|
376
|
-
monkeypatch.setattr("subprocess.run", mock_run)
|
|
377
|
-
|
|
378
|
-
project = detect_project()
|
|
379
|
-
assert project["id"] == "global"
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
def test_detect_project_creates_directories(patch_globals, monkeypatch, tmp_path):
|
|
383
|
-
"""detect_project should create the project dir structure."""
|
|
384
|
-
fake_repo = tmp_path / "structured-repo"
|
|
385
|
-
fake_repo.mkdir()
|
|
386
|
-
monkeypatch.setenv("CLAUDE_PROJECT_DIR", str(fake_repo))
|
|
387
|
-
|
|
388
|
-
def mock_run(cmd, **kwargs):
|
|
389
|
-
if "rev-parse" in cmd:
|
|
390
|
-
return SimpleNamespace(returncode=0, stdout=str(fake_repo) + "\n", stderr="")
|
|
391
|
-
if "get-url" in cmd:
|
|
392
|
-
return SimpleNamespace(returncode=1, stdout="", stderr="no remote")
|
|
393
|
-
return SimpleNamespace(returncode=1, stdout="", stderr="")
|
|
394
|
-
|
|
395
|
-
monkeypatch.setattr("subprocess.run", mock_run)
|
|
396
|
-
|
|
397
|
-
project = detect_project()
|
|
398
|
-
assert project["instincts_personal"].exists()
|
|
399
|
-
assert project["instincts_inherited"].exists()
|
|
400
|
-
assert (project["evolved_dir"] / "skills").exists()
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
# ─────────────────────────────────────────────
|
|
404
|
-
# _load_instincts_from_dir tests
|
|
405
|
-
# ─────────────────────────────────────────────
|
|
406
|
-
|
|
407
|
-
def test_load_from_empty_dir(tmp_path):
|
|
408
|
-
result = _load_instincts_from_dir(tmp_path, "personal", "project")
|
|
409
|
-
assert result == []
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
def test_load_from_nonexistent_dir(tmp_path):
|
|
413
|
-
result = _load_instincts_from_dir(tmp_path / "does-not-exist", "personal", "project")
|
|
414
|
-
assert result == []
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
def test_load_annotates_metadata(tmp_path):
|
|
418
|
-
"""Loaded instincts should have _source_file, _source_type, _scope_label."""
|
|
419
|
-
yaml_file = tmp_path / "test.yaml"
|
|
420
|
-
yaml_file.write_text(SAMPLE_INSTINCT_YAML)
|
|
421
|
-
|
|
422
|
-
result = _load_instincts_from_dir(tmp_path, "personal", "project")
|
|
423
|
-
assert len(result) == 1
|
|
424
|
-
assert result[0]["_source_file"] == str(yaml_file)
|
|
425
|
-
assert result[0]["_source_type"] == "personal"
|
|
426
|
-
assert result[0]["_scope_label"] == "project"
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
def test_load_defaults_scope_from_label(tmp_path):
|
|
430
|
-
"""If an instinct has no 'scope' in frontmatter, it should default to scope_label."""
|
|
431
|
-
no_scope_yaml = """\
|
|
432
|
-
---
|
|
433
|
-
id: no-scope
|
|
434
|
-
trigger: "test"
|
|
435
|
-
confidence: 0.5
|
|
436
|
-
domain: general
|
|
437
|
-
---
|
|
438
|
-
|
|
439
|
-
Body.
|
|
440
|
-
"""
|
|
441
|
-
(tmp_path / "no-scope.yaml").write_text(no_scope_yaml)
|
|
442
|
-
result = _load_instincts_from_dir(tmp_path, "inherited", "global")
|
|
443
|
-
assert result[0]["scope"] == "global"
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
def test_load_preserves_explicit_scope(tmp_path):
|
|
447
|
-
"""If frontmatter has explicit scope, it should be preserved."""
|
|
448
|
-
yaml_file = tmp_path / "test.yaml"
|
|
449
|
-
yaml_file.write_text(SAMPLE_INSTINCT_YAML)
|
|
450
|
-
|
|
451
|
-
result = _load_instincts_from_dir(tmp_path, "personal", "global")
|
|
452
|
-
# Frontmatter says scope: project, scope_label is global
|
|
453
|
-
# The explicit scope should be preserved (not overwritten)
|
|
454
|
-
assert result[0]["scope"] == "project"
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
def test_load_handles_corrupt_file(tmp_path, capsys):
|
|
458
|
-
"""Corrupt YAML files should be warned about but not crash."""
|
|
459
|
-
# A file that will cause parse_instinct_file to return empty
|
|
460
|
-
(tmp_path / "good.yaml").write_text(SAMPLE_INSTINCT_YAML)
|
|
461
|
-
(tmp_path / "bad.yaml").write_text("not yaml\nno frontmatter")
|
|
462
|
-
|
|
463
|
-
result = _load_instincts_from_dir(tmp_path, "personal", "project")
|
|
464
|
-
# bad.yaml has no valid instincts (no id), so only good.yaml contributes
|
|
465
|
-
assert len(result) == 1
|
|
466
|
-
assert result[0]["id"] == "test-instinct"
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
def test_load_supports_yml_extension(tmp_path):
|
|
470
|
-
yml_file = tmp_path / "test.yml"
|
|
471
|
-
yml_file.write_text(SAMPLE_INSTINCT_YAML)
|
|
472
|
-
|
|
473
|
-
result = _load_instincts_from_dir(tmp_path, "personal", "project")
|
|
474
|
-
ids = {i["id"] for i in result}
|
|
475
|
-
assert "test-instinct" in ids
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
def test_load_supports_md_extension(tmp_path):
|
|
479
|
-
md_file = tmp_path / "legacy-instinct.md"
|
|
480
|
-
md_file.write_text(SAMPLE_INSTINCT_YAML)
|
|
481
|
-
|
|
482
|
-
result = _load_instincts_from_dir(tmp_path, "personal", "project")
|
|
483
|
-
ids = {i["id"] for i in result}
|
|
484
|
-
assert "test-instinct" in ids
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
def test_load_instincts_from_dir_uses_utf8_encoding(tmp_path, monkeypatch):
|
|
488
|
-
yaml_file = tmp_path / "test.yaml"
|
|
489
|
-
yaml_file.write_text("placeholder")
|
|
490
|
-
calls = []
|
|
491
|
-
|
|
492
|
-
def fake_read_text(self, *args, **kwargs):
|
|
493
|
-
calls.append(kwargs.get("encoding"))
|
|
494
|
-
return SAMPLE_INSTINCT_YAML
|
|
495
|
-
|
|
496
|
-
monkeypatch.setattr(Path, "read_text", fake_read_text)
|
|
497
|
-
result = _load_instincts_from_dir(tmp_path, "personal", "project")
|
|
498
|
-
assert result[0]["id"] == "test-instinct"
|
|
499
|
-
assert calls == ["utf-8"]
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
# ─────────────────────────────────────────────
|
|
503
|
-
# load_all_instincts tests
|
|
504
|
-
# ─────────────────────────────────────────────
|
|
505
|
-
|
|
506
|
-
def test_load_all_project_and_global(patch_globals):
|
|
507
|
-
"""Should load from both project and global directories."""
|
|
508
|
-
tree = patch_globals
|
|
509
|
-
project = _make_project(tree)
|
|
510
|
-
|
|
511
|
-
# Write a project instinct
|
|
512
|
-
(project["instincts_personal"] / "proj.yaml").write_text(SAMPLE_INSTINCT_YAML)
|
|
513
|
-
# Write a global instinct
|
|
514
|
-
(tree["global_personal"] / "glob.yaml").write_text(SAMPLE_GLOBAL_INSTINCT_YAML)
|
|
515
|
-
|
|
516
|
-
result = load_all_instincts(project)
|
|
517
|
-
ids = {i["id"] for i in result}
|
|
518
|
-
assert "test-instinct" in ids
|
|
519
|
-
assert "global-instinct" in ids
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
def test_load_all_project_overrides_global(patch_globals):
|
|
523
|
-
"""When project and global have same ID, project wins."""
|
|
524
|
-
tree = patch_globals
|
|
525
|
-
project = _make_project(tree)
|
|
526
|
-
|
|
527
|
-
# Same ID but different confidence
|
|
528
|
-
proj_yaml = SAMPLE_INSTINCT_YAML.replace("id: test-instinct", "id: shared-id")
|
|
529
|
-
proj_yaml = proj_yaml.replace("confidence: 0.8", "confidence: 0.9")
|
|
530
|
-
glob_yaml = SAMPLE_GLOBAL_INSTINCT_YAML.replace("id: global-instinct", "id: shared-id")
|
|
531
|
-
glob_yaml = glob_yaml.replace("confidence: 0.9", "confidence: 0.3")
|
|
532
|
-
|
|
533
|
-
(project["instincts_personal"] / "shared.yaml").write_text(proj_yaml)
|
|
534
|
-
(tree["global_personal"] / "shared.yaml").write_text(glob_yaml)
|
|
535
|
-
|
|
536
|
-
result = load_all_instincts(project)
|
|
537
|
-
shared = [i for i in result if i["id"] == "shared-id"]
|
|
538
|
-
assert len(shared) == 1
|
|
539
|
-
assert shared[0]["_scope_label"] == "project"
|
|
540
|
-
assert shared[0]["confidence"] == 0.9
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
def test_load_all_global_only(patch_globals):
|
|
544
|
-
"""Global project should only load global instincts."""
|
|
545
|
-
tree = patch_globals
|
|
546
|
-
(tree["global_personal"] / "glob.yaml").write_text(SAMPLE_GLOBAL_INSTINCT_YAML)
|
|
547
|
-
|
|
548
|
-
global_project = {
|
|
549
|
-
"id": "global",
|
|
550
|
-
"name": "global",
|
|
551
|
-
"root": "",
|
|
552
|
-
"project_dir": tree["homunculus"],
|
|
553
|
-
"instincts_personal": tree["global_personal"],
|
|
554
|
-
"instincts_inherited": tree["global_inherited"],
|
|
555
|
-
"evolved_dir": tree["global_evolved"],
|
|
556
|
-
"observations_file": tree["homunculus"] / "observations.jsonl",
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
result = load_all_instincts(global_project)
|
|
560
|
-
assert len(result) == 1
|
|
561
|
-
assert result[0]["id"] == "global-instinct"
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
def test_load_project_only_excludes_global(patch_globals):
|
|
565
|
-
"""load_project_only_instincts should NOT include global instincts."""
|
|
566
|
-
tree = patch_globals
|
|
567
|
-
project = _make_project(tree)
|
|
568
|
-
|
|
569
|
-
(project["instincts_personal"] / "proj.yaml").write_text(SAMPLE_INSTINCT_YAML)
|
|
570
|
-
(tree["global_personal"] / "glob.yaml").write_text(SAMPLE_GLOBAL_INSTINCT_YAML)
|
|
571
|
-
|
|
572
|
-
result = load_project_only_instincts(project)
|
|
573
|
-
ids = {i["id"] for i in result}
|
|
574
|
-
assert "test-instinct" in ids
|
|
575
|
-
assert "global-instinct" not in ids
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
def test_load_project_only_global_fallback_loads_global(patch_globals):
|
|
579
|
-
"""Global fallback should return global instincts for project-only queries."""
|
|
580
|
-
tree = patch_globals
|
|
581
|
-
(tree["global_personal"] / "glob.yaml").write_text(SAMPLE_GLOBAL_INSTINCT_YAML)
|
|
582
|
-
|
|
583
|
-
global_project = {
|
|
584
|
-
"id": "global",
|
|
585
|
-
"name": "global",
|
|
586
|
-
"root": "",
|
|
587
|
-
"project_dir": tree["homunculus"],
|
|
588
|
-
"instincts_personal": tree["global_personal"],
|
|
589
|
-
"instincts_inherited": tree["global_inherited"],
|
|
590
|
-
"evolved_dir": tree["global_evolved"],
|
|
591
|
-
"observations_file": tree["homunculus"] / "observations.jsonl",
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
result = load_project_only_instincts(global_project)
|
|
595
|
-
assert len(result) == 1
|
|
596
|
-
assert result[0]["id"] == "global-instinct"
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
def test_load_all_empty(patch_globals):
|
|
600
|
-
"""No instincts at all should return empty list."""
|
|
601
|
-
tree = patch_globals
|
|
602
|
-
project = _make_project(tree)
|
|
603
|
-
|
|
604
|
-
result = load_all_instincts(project)
|
|
605
|
-
assert result == []
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
# ─────────────────────────────────────────────
|
|
609
|
-
# cmd_status tests
|
|
610
|
-
# ─────────────────────────────────────────────
|
|
611
|
-
|
|
612
|
-
def test_cmd_status_no_instincts(patch_globals, monkeypatch, capsys):
|
|
613
|
-
"""Status with no instincts should print fallback message."""
|
|
614
|
-
tree = patch_globals
|
|
615
|
-
project = _make_project(tree)
|
|
616
|
-
monkeypatch.setattr(_mod, "detect_project", lambda: project)
|
|
617
|
-
|
|
618
|
-
args = SimpleNamespace()
|
|
619
|
-
ret = cmd_status(args)
|
|
620
|
-
assert ret == 0
|
|
621
|
-
out = capsys.readouterr().out
|
|
622
|
-
assert "No instincts found." in out
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
def test_cmd_status_with_instincts(patch_globals, monkeypatch, capsys):
|
|
626
|
-
"""Status should show project and global instinct counts."""
|
|
627
|
-
tree = patch_globals
|
|
628
|
-
project = _make_project(tree)
|
|
629
|
-
monkeypatch.setattr(_mod, "detect_project", lambda: project)
|
|
630
|
-
|
|
631
|
-
(project["instincts_personal"] / "proj.yaml").write_text(SAMPLE_INSTINCT_YAML)
|
|
632
|
-
(tree["global_personal"] / "glob.yaml").write_text(SAMPLE_GLOBAL_INSTINCT_YAML)
|
|
633
|
-
|
|
634
|
-
args = SimpleNamespace()
|
|
635
|
-
ret = cmd_status(args)
|
|
636
|
-
assert ret == 0
|
|
637
|
-
out = capsys.readouterr().out
|
|
638
|
-
assert "INSTINCT STATUS" in out
|
|
639
|
-
assert "Project instincts: 1" in out
|
|
640
|
-
assert "Global instincts: 1" in out
|
|
641
|
-
assert "PROJECT-SCOPED" in out
|
|
642
|
-
assert "GLOBAL" in out
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
def test_cmd_status_returns_int(patch_globals, monkeypatch):
|
|
646
|
-
"""cmd_status should always return an int."""
|
|
647
|
-
tree = patch_globals
|
|
648
|
-
project = _make_project(tree)
|
|
649
|
-
monkeypatch.setattr(_mod, "detect_project", lambda: project)
|
|
650
|
-
|
|
651
|
-
args = SimpleNamespace()
|
|
652
|
-
ret = cmd_status(args)
|
|
653
|
-
assert isinstance(ret, int)
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
# ─────────────────────────────────────────────
|
|
657
|
-
# cmd_projects tests
|
|
658
|
-
# ─────────────────────────────────────────────
|
|
659
|
-
|
|
660
|
-
def test_cmd_projects_empty_registry(patch_globals, capsys):
|
|
661
|
-
"""No projects should print helpful message."""
|
|
662
|
-
args = SimpleNamespace()
|
|
663
|
-
ret = cmd_projects(args)
|
|
664
|
-
assert ret == 0
|
|
665
|
-
out = capsys.readouterr().out
|
|
666
|
-
assert "No projects registered yet." in out
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
def test_cmd_projects_with_registry(patch_globals, capsys):
|
|
670
|
-
"""Should list projects from registry."""
|
|
671
|
-
tree = patch_globals
|
|
672
|
-
|
|
673
|
-
# Create a project dir with instincts
|
|
674
|
-
pid = "test123abc"
|
|
675
|
-
project = _make_project(tree, pid=pid, pname="my-app")
|
|
676
|
-
(project["instincts_personal"] / "inst.yaml").write_text(SAMPLE_INSTINCT_YAML)
|
|
677
|
-
|
|
678
|
-
# Write registry
|
|
679
|
-
registry = {
|
|
680
|
-
pid: {
|
|
681
|
-
"name": "my-app",
|
|
682
|
-
"root": "/home/user/my-app",
|
|
683
|
-
"remote": "https://github.com/user/my-app.git",
|
|
684
|
-
"last_seen": "2025-01-15T12:00:00Z",
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
tree["registry_file"].write_text(json.dumps(registry))
|
|
688
|
-
|
|
689
|
-
args = SimpleNamespace()
|
|
690
|
-
ret = cmd_projects(args)
|
|
691
|
-
assert ret == 0
|
|
692
|
-
out = capsys.readouterr().out
|
|
693
|
-
assert "my-app" in out
|
|
694
|
-
assert pid in out
|
|
695
|
-
assert "1 personal" in out
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
# ─────────────────────────────────────────────
|
|
699
|
-
# _promote_specific tests
|
|
700
|
-
# ─────────────────────────────────────────────
|
|
701
|
-
|
|
702
|
-
def test_promote_specific_not_found(patch_globals, capsys):
|
|
703
|
-
"""Promoting nonexistent instinct should fail."""
|
|
704
|
-
tree = patch_globals
|
|
705
|
-
project = _make_project(tree)
|
|
706
|
-
|
|
707
|
-
ret = _promote_specific(project, "nonexistent", force=True)
|
|
708
|
-
assert ret == 1
|
|
709
|
-
out = capsys.readouterr().out
|
|
710
|
-
assert "not found" in out
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
def test_promote_specific_rejects_invalid_id(patch_globals, capsys):
|
|
714
|
-
"""Path-like instinct IDs should be rejected before file writes."""
|
|
715
|
-
tree = patch_globals
|
|
716
|
-
project = _make_project(tree)
|
|
717
|
-
|
|
718
|
-
ret = _promote_specific(project, "../escape", force=True)
|
|
719
|
-
assert ret == 1
|
|
720
|
-
err = capsys.readouterr().err
|
|
721
|
-
assert "Invalid instinct ID" in err
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
def test_promote_specific_already_global(patch_globals, capsys):
|
|
725
|
-
"""Promoting an instinct that already exists globally should fail."""
|
|
726
|
-
tree = patch_globals
|
|
727
|
-
project = _make_project(tree)
|
|
728
|
-
|
|
729
|
-
# Write same-id instinct in both project and global
|
|
730
|
-
(project["instincts_personal"] / "shared.yaml").write_text(SAMPLE_INSTINCT_YAML)
|
|
731
|
-
global_yaml = SAMPLE_INSTINCT_YAML # same id: test-instinct
|
|
732
|
-
(tree["global_personal"] / "shared.yaml").write_text(global_yaml)
|
|
733
|
-
|
|
734
|
-
ret = _promote_specific(project, "test-instinct", force=True)
|
|
735
|
-
assert ret == 1
|
|
736
|
-
out = capsys.readouterr().out
|
|
737
|
-
assert "already exists in global" in out
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
def test_promote_specific_success(patch_globals, capsys):
|
|
741
|
-
"""Promote a project instinct to global with --force."""
|
|
742
|
-
tree = patch_globals
|
|
743
|
-
project = _make_project(tree)
|
|
744
|
-
|
|
745
|
-
(project["instincts_personal"] / "inst.yaml").write_text(SAMPLE_INSTINCT_YAML)
|
|
746
|
-
|
|
747
|
-
ret = _promote_specific(project, "test-instinct", force=True)
|
|
748
|
-
assert ret == 0
|
|
749
|
-
out = capsys.readouterr().out
|
|
750
|
-
assert "Promoted" in out
|
|
751
|
-
|
|
752
|
-
# Verify file was created in global dir
|
|
753
|
-
promoted_file = tree["global_personal"] / "test-instinct.yaml"
|
|
754
|
-
assert promoted_file.exists()
|
|
755
|
-
content = promoted_file.read_text()
|
|
756
|
-
assert "scope: global" in content
|
|
757
|
-
assert "promoted_from: abc123" in content
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
# ─────────────────────────────────────────────
|
|
761
|
-
# _promote_auto tests
|
|
762
|
-
# ─────────────────────────────────────────────
|
|
763
|
-
|
|
764
|
-
def test_promote_auto_no_candidates(patch_globals, capsys):
|
|
765
|
-
"""Auto-promote with no cross-project instincts should say so."""
|
|
766
|
-
tree = patch_globals
|
|
767
|
-
project = _make_project(tree)
|
|
768
|
-
|
|
769
|
-
# Empty registry
|
|
770
|
-
tree["registry_file"].write_text("{}")
|
|
771
|
-
|
|
772
|
-
ret = _promote_auto(project, force=True, dry_run=False)
|
|
773
|
-
assert ret == 0
|
|
774
|
-
out = capsys.readouterr().out
|
|
775
|
-
assert "No instincts qualify" in out
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
def test_promote_auto_dry_run(patch_globals, capsys):
|
|
779
|
-
"""Dry run should list candidates but not write files."""
|
|
780
|
-
tree = patch_globals
|
|
781
|
-
|
|
782
|
-
# Create two projects with the same high-confidence instinct
|
|
783
|
-
p1 = _make_project(tree, pid="proj1", pname="project-one")
|
|
784
|
-
p2 = _make_project(tree, pid="proj2", pname="project-two")
|
|
785
|
-
|
|
786
|
-
high_conf_yaml = """\
|
|
787
|
-
---
|
|
788
|
-
id: cross-project-instinct
|
|
789
|
-
trigger: "when reviewing"
|
|
790
|
-
confidence: 0.95
|
|
791
|
-
domain: security
|
|
792
|
-
scope: project
|
|
793
|
-
---
|
|
794
|
-
|
|
795
|
-
## Action
|
|
796
|
-
Always review for injection.
|
|
797
|
-
"""
|
|
798
|
-
(p1["instincts_personal"] / "cross.yaml").write_text(high_conf_yaml)
|
|
799
|
-
(p2["instincts_personal"] / "cross.yaml").write_text(high_conf_yaml)
|
|
800
|
-
|
|
801
|
-
# Write registry
|
|
802
|
-
registry = {
|
|
803
|
-
"proj1": {"name": "project-one", "root": "/a", "remote": "", "last_seen": "2025-01-01T00:00:00Z"},
|
|
804
|
-
"proj2": {"name": "project-two", "root": "/b", "remote": "", "last_seen": "2025-01-01T00:00:00Z"},
|
|
805
|
-
}
|
|
806
|
-
tree["registry_file"].write_text(json.dumps(registry))
|
|
807
|
-
|
|
808
|
-
project = p1
|
|
809
|
-
ret = _promote_auto(project, force=True, dry_run=True)
|
|
810
|
-
assert ret == 0
|
|
811
|
-
out = capsys.readouterr().out
|
|
812
|
-
assert "DRY RUN" in out
|
|
813
|
-
assert "cross-project-instinct" in out
|
|
814
|
-
|
|
815
|
-
# Verify no file was created
|
|
816
|
-
assert not (tree["global_personal"] / "cross-project-instinct.yaml").exists()
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
def test_promote_auto_writes_file(patch_globals, capsys):
|
|
820
|
-
"""Auto-promote with force should write global instinct file."""
|
|
821
|
-
tree = patch_globals
|
|
822
|
-
|
|
823
|
-
p1 = _make_project(tree, pid="proj1", pname="project-one")
|
|
824
|
-
p2 = _make_project(tree, pid="proj2", pname="project-two")
|
|
825
|
-
|
|
826
|
-
high_conf_yaml = """\
|
|
827
|
-
---
|
|
828
|
-
id: universal-pattern
|
|
829
|
-
trigger: "when coding"
|
|
830
|
-
confidence: 0.85
|
|
831
|
-
domain: general
|
|
832
|
-
scope: project
|
|
833
|
-
---
|
|
834
|
-
|
|
835
|
-
## Action
|
|
836
|
-
Use descriptive variable names.
|
|
837
|
-
"""
|
|
838
|
-
(p1["instincts_personal"] / "uni.yaml").write_text(high_conf_yaml)
|
|
839
|
-
(p2["instincts_personal"] / "uni.yaml").write_text(high_conf_yaml)
|
|
840
|
-
|
|
841
|
-
registry = {
|
|
842
|
-
"proj1": {"name": "project-one", "root": "/a", "remote": "", "last_seen": "2025-01-01T00:00:00Z"},
|
|
843
|
-
"proj2": {"name": "project-two", "root": "/b", "remote": "", "last_seen": "2025-01-01T00:00:00Z"},
|
|
844
|
-
}
|
|
845
|
-
tree["registry_file"].write_text(json.dumps(registry))
|
|
846
|
-
|
|
847
|
-
ret = _promote_auto(p1, force=True, dry_run=False)
|
|
848
|
-
assert ret == 0
|
|
849
|
-
|
|
850
|
-
promoted = tree["global_personal"] / "universal-pattern.yaml"
|
|
851
|
-
assert promoted.exists()
|
|
852
|
-
content = promoted.read_text()
|
|
853
|
-
assert "scope: global" in content
|
|
854
|
-
assert "auto-promoted" in content
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
def test_promote_auto_skips_invalid_id(patch_globals, capsys):
|
|
858
|
-
tree = patch_globals
|
|
859
|
-
|
|
860
|
-
p1 = _make_project(tree, pid="proj1", pname="project-one")
|
|
861
|
-
p2 = _make_project(tree, pid="proj2", pname="project-two")
|
|
862
|
-
|
|
863
|
-
bad_id_yaml = """\
|
|
864
|
-
---
|
|
865
|
-
id: ../escape
|
|
866
|
-
trigger: "when coding"
|
|
867
|
-
confidence: 0.9
|
|
868
|
-
domain: general
|
|
869
|
-
scope: project
|
|
870
|
-
---
|
|
871
|
-
|
|
872
|
-
## Action
|
|
873
|
-
Invalid id should be skipped.
|
|
874
|
-
"""
|
|
875
|
-
(p1["instincts_personal"] / "bad.yaml").write_text(bad_id_yaml)
|
|
876
|
-
(p2["instincts_personal"] / "bad.yaml").write_text(bad_id_yaml)
|
|
877
|
-
|
|
878
|
-
registry = {
|
|
879
|
-
"proj1": {"name": "project-one", "root": "/a", "remote": "", "last_seen": "2025-01-01T00:00:00Z"},
|
|
880
|
-
"proj2": {"name": "project-two", "root": "/b", "remote": "", "last_seen": "2025-01-01T00:00:00Z"},
|
|
881
|
-
}
|
|
882
|
-
tree["registry_file"].write_text(json.dumps(registry))
|
|
883
|
-
|
|
884
|
-
ret = _promote_auto(p1, force=True, dry_run=False)
|
|
885
|
-
assert ret == 0
|
|
886
|
-
err = capsys.readouterr().err
|
|
887
|
-
assert "Skipping invalid instinct ID" in err
|
|
888
|
-
assert not (tree["global_personal"] / "../escape.yaml").exists()
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
# ─────────────────────────────────────────────
|
|
892
|
-
# _find_cross_project_instincts tests
|
|
893
|
-
# ─────────────────────────────────────────────
|
|
894
|
-
|
|
895
|
-
def test_find_cross_project_empty_registry(patch_globals):
|
|
896
|
-
tree = patch_globals
|
|
897
|
-
tree["registry_file"].write_text("{}")
|
|
898
|
-
result = _find_cross_project_instincts()
|
|
899
|
-
assert result == {}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
def test_find_cross_project_single_project(patch_globals):
|
|
903
|
-
"""Single project should return nothing (need 2+)."""
|
|
904
|
-
tree = patch_globals
|
|
905
|
-
p1 = _make_project(tree, pid="proj1", pname="project-one")
|
|
906
|
-
(p1["instincts_personal"] / "inst.yaml").write_text(SAMPLE_INSTINCT_YAML)
|
|
907
|
-
|
|
908
|
-
registry = {"proj1": {"name": "project-one", "root": "/a", "remote": "", "last_seen": "2025-01-01T00:00:00Z"}}
|
|
909
|
-
tree["registry_file"].write_text(json.dumps(registry))
|
|
910
|
-
|
|
911
|
-
result = _find_cross_project_instincts()
|
|
912
|
-
assert result == {}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
def test_find_cross_project_shared_instinct(patch_globals):
|
|
916
|
-
"""Same instinct ID in 2 projects should be found."""
|
|
917
|
-
tree = patch_globals
|
|
918
|
-
p1 = _make_project(tree, pid="proj1", pname="project-one")
|
|
919
|
-
p2 = _make_project(tree, pid="proj2", pname="project-two")
|
|
920
|
-
|
|
921
|
-
(p1["instincts_personal"] / "shared.yaml").write_text(SAMPLE_INSTINCT_YAML)
|
|
922
|
-
(p2["instincts_personal"] / "shared.yaml").write_text(SAMPLE_INSTINCT_YAML)
|
|
923
|
-
|
|
924
|
-
registry = {
|
|
925
|
-
"proj1": {"name": "project-one", "root": "/a", "remote": "", "last_seen": "2025-01-01T00:00:00Z"},
|
|
926
|
-
"proj2": {"name": "project-two", "root": "/b", "remote": "", "last_seen": "2025-01-01T00:00:00Z"},
|
|
927
|
-
}
|
|
928
|
-
tree["registry_file"].write_text(json.dumps(registry))
|
|
929
|
-
|
|
930
|
-
result = _find_cross_project_instincts()
|
|
931
|
-
assert "test-instinct" in result
|
|
932
|
-
assert len(result["test-instinct"]) == 2
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
# ─────────────────────────────────────────────
|
|
936
|
-
# load_registry tests
|
|
937
|
-
# ─────────────────────────────────────────────
|
|
938
|
-
|
|
939
|
-
def test_load_registry_missing_file(patch_globals):
|
|
940
|
-
result = load_registry()
|
|
941
|
-
assert result == {}
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
def test_load_registry_corrupt_json(patch_globals):
|
|
945
|
-
tree = patch_globals
|
|
946
|
-
tree["registry_file"].write_text("not json at all {{{")
|
|
947
|
-
result = load_registry()
|
|
948
|
-
assert result == {}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
def test_load_registry_valid(patch_globals):
|
|
952
|
-
tree = patch_globals
|
|
953
|
-
data = {"abc": {"name": "test", "root": "/test"}}
|
|
954
|
-
tree["registry_file"].write_text(json.dumps(data))
|
|
955
|
-
result = load_registry()
|
|
956
|
-
assert result == data
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
def test_load_registry_uses_utf8_encoding(monkeypatch):
|
|
960
|
-
calls = []
|
|
961
|
-
|
|
962
|
-
def fake_open(path, mode="r", *args, **kwargs):
|
|
963
|
-
calls.append(kwargs.get("encoding"))
|
|
964
|
-
return io.StringIO("{}")
|
|
965
|
-
|
|
966
|
-
monkeypatch.setattr(_mod, "open", fake_open, raising=False)
|
|
967
|
-
assert load_registry() == {}
|
|
968
|
-
assert calls == ["utf-8"]
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
def test_validate_instinct_id():
|
|
972
|
-
assert _validate_instinct_id("good-id_1.0")
|
|
973
|
-
assert not _validate_instinct_id("../bad")
|
|
974
|
-
assert not _validate_instinct_id("bad/name")
|
|
975
|
-
assert not _validate_instinct_id(".hidden")
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
def test_update_registry_atomic_replaces_file(patch_globals):
|
|
979
|
-
tree = patch_globals
|
|
980
|
-
_update_registry("abc123", "demo", "/repo", "https://example.com/repo.git")
|
|
981
|
-
data = json.loads(tree["registry_file"].read_text())
|
|
982
|
-
assert "abc123" in data
|
|
983
|
-
leftovers = list(tree["registry_file"].parent.glob(".projects.json.tmp.*"))
|
|
984
|
-
assert leftovers == []
|
|
1
|
+
"""Tests for continuous-learning-v2 instinct-cli.py
|
|
2
|
+
|
|
3
|
+
Covers:
|
|
4
|
+
- parse_instinct_file() — content preservation, edge cases
|
|
5
|
+
- _validate_file_path() — path traversal blocking
|
|
6
|
+
- detect_project() — project detection with mocked git/env
|
|
7
|
+
- load_all_instincts() — loading from project + global dirs, dedup
|
|
8
|
+
- _load_instincts_from_dir() — directory scanning
|
|
9
|
+
- cmd_projects() — listing projects from registry
|
|
10
|
+
- cmd_status() — status display
|
|
11
|
+
- _promote_specific() — single instinct promotion
|
|
12
|
+
- _promote_auto() — auto-promotion across projects
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import importlib.util
|
|
16
|
+
import io
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from types import SimpleNamespace
|
|
22
|
+
from unittest import mock
|
|
23
|
+
|
|
24
|
+
import pytest
|
|
25
|
+
|
|
26
|
+
# Load instinct-cli.py (hyphenated filename requires importlib)
|
|
27
|
+
_spec = importlib.util.spec_from_file_location(
|
|
28
|
+
"instinct_cli",
|
|
29
|
+
os.path.join(os.path.dirname(__file__), "instinct-cli.py"),
|
|
30
|
+
)
|
|
31
|
+
_mod = importlib.util.module_from_spec(_spec)
|
|
32
|
+
_spec.loader.exec_module(_mod)
|
|
33
|
+
|
|
34
|
+
parse_instinct_file = _mod.parse_instinct_file
|
|
35
|
+
_validate_file_path = _mod._validate_file_path
|
|
36
|
+
detect_project = _mod.detect_project
|
|
37
|
+
load_all_instincts = _mod.load_all_instincts
|
|
38
|
+
load_project_only_instincts = _mod.load_project_only_instincts
|
|
39
|
+
_load_instincts_from_dir = _mod._load_instincts_from_dir
|
|
40
|
+
cmd_status = _mod.cmd_status
|
|
41
|
+
cmd_projects = _mod.cmd_projects
|
|
42
|
+
_promote_specific = _mod._promote_specific
|
|
43
|
+
_promote_auto = _mod._promote_auto
|
|
44
|
+
_find_cross_project_instincts = _mod._find_cross_project_instincts
|
|
45
|
+
load_registry = _mod.load_registry
|
|
46
|
+
_validate_instinct_id = _mod._validate_instinct_id
|
|
47
|
+
_update_registry = _mod._update_registry
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ─────────────────────────────────────────────
|
|
51
|
+
# Fixtures
|
|
52
|
+
# ─────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
SAMPLE_INSTINCT_YAML = """\
|
|
55
|
+
---
|
|
56
|
+
id: test-instinct
|
|
57
|
+
trigger: "when writing tests"
|
|
58
|
+
confidence: 0.8
|
|
59
|
+
domain: testing
|
|
60
|
+
scope: project
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Action
|
|
64
|
+
Always write tests first.
|
|
65
|
+
|
|
66
|
+
## Evidence
|
|
67
|
+
TDD leads to better design.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
SAMPLE_GLOBAL_INSTINCT_YAML = """\
|
|
71
|
+
---
|
|
72
|
+
id: global-instinct
|
|
73
|
+
trigger: "always"
|
|
74
|
+
confidence: 0.9
|
|
75
|
+
domain: security
|
|
76
|
+
scope: global
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Action
|
|
80
|
+
Validate all user input.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@pytest.fixture
|
|
85
|
+
def project_tree(tmp_path):
|
|
86
|
+
"""Create a realistic project directory tree for testing."""
|
|
87
|
+
homunculus = tmp_path / ".claude" / "homunculus"
|
|
88
|
+
projects_dir = homunculus / "projects"
|
|
89
|
+
global_personal = homunculus / "instincts" / "personal"
|
|
90
|
+
global_inherited = homunculus / "instincts" / "inherited"
|
|
91
|
+
global_evolved = homunculus / "evolved"
|
|
92
|
+
|
|
93
|
+
for d in [
|
|
94
|
+
global_personal, global_inherited,
|
|
95
|
+
global_evolved / "skills", global_evolved / "commands", global_evolved / "agents",
|
|
96
|
+
projects_dir,
|
|
97
|
+
]:
|
|
98
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
"root": tmp_path,
|
|
102
|
+
"homunculus": homunculus,
|
|
103
|
+
"projects_dir": projects_dir,
|
|
104
|
+
"global_personal": global_personal,
|
|
105
|
+
"global_inherited": global_inherited,
|
|
106
|
+
"global_evolved": global_evolved,
|
|
107
|
+
"registry_file": homunculus / "projects.json",
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@pytest.fixture
|
|
112
|
+
def patch_globals(project_tree, monkeypatch):
|
|
113
|
+
"""Patch module-level globals to use tmp_path-based directories."""
|
|
114
|
+
monkeypatch.setattr(_mod, "HOMUNCULUS_DIR", project_tree["homunculus"])
|
|
115
|
+
monkeypatch.setattr(_mod, "PROJECTS_DIR", project_tree["projects_dir"])
|
|
116
|
+
monkeypatch.setattr(_mod, "REGISTRY_FILE", project_tree["registry_file"])
|
|
117
|
+
monkeypatch.setattr(_mod, "GLOBAL_PERSONAL_DIR", project_tree["global_personal"])
|
|
118
|
+
monkeypatch.setattr(_mod, "GLOBAL_INHERITED_DIR", project_tree["global_inherited"])
|
|
119
|
+
monkeypatch.setattr(_mod, "GLOBAL_EVOLVED_DIR", project_tree["global_evolved"])
|
|
120
|
+
monkeypatch.setattr(_mod, "GLOBAL_OBSERVATIONS_FILE", project_tree["homunculus"] / "observations.jsonl")
|
|
121
|
+
return project_tree
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _make_project(tree, pid="abc123", pname="test-project"):
|
|
125
|
+
"""Create project directory structure and return a project dict."""
|
|
126
|
+
project_dir = tree["projects_dir"] / pid
|
|
127
|
+
personal_dir = project_dir / "instincts" / "personal"
|
|
128
|
+
inherited_dir = project_dir / "instincts" / "inherited"
|
|
129
|
+
for d in [personal_dir, inherited_dir,
|
|
130
|
+
project_dir / "evolved" / "skills",
|
|
131
|
+
project_dir / "evolved" / "commands",
|
|
132
|
+
project_dir / "evolved" / "agents",
|
|
133
|
+
project_dir / "observations.archive"]:
|
|
134
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
"id": pid,
|
|
138
|
+
"name": pname,
|
|
139
|
+
"root": str(tree["root"] / "fake-repo"),
|
|
140
|
+
"remote": "https://github.com/test/test-project.git",
|
|
141
|
+
"project_dir": project_dir,
|
|
142
|
+
"instincts_personal": personal_dir,
|
|
143
|
+
"instincts_inherited": inherited_dir,
|
|
144
|
+
"evolved_dir": project_dir / "evolved",
|
|
145
|
+
"observations_file": project_dir / "observations.jsonl",
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ─────────────────────────────────────────────
|
|
150
|
+
# parse_instinct_file tests
|
|
151
|
+
# ─────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
MULTI_SECTION = """\
|
|
154
|
+
---
|
|
155
|
+
id: instinct-a
|
|
156
|
+
trigger: "when coding"
|
|
157
|
+
confidence: 0.9
|
|
158
|
+
domain: general
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Action
|
|
162
|
+
Do thing A.
|
|
163
|
+
|
|
164
|
+
## Examples
|
|
165
|
+
- Example A1
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
id: instinct-b
|
|
169
|
+
trigger: "when testing"
|
|
170
|
+
confidence: 0.7
|
|
171
|
+
domain: testing
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Action
|
|
175
|
+
Do thing B.
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_multiple_instincts_preserve_content():
|
|
180
|
+
result = parse_instinct_file(MULTI_SECTION)
|
|
181
|
+
assert len(result) == 2
|
|
182
|
+
assert "Do thing A." in result[0]["content"]
|
|
183
|
+
assert "Example A1" in result[0]["content"]
|
|
184
|
+
assert "Do thing B." in result[1]["content"]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_single_instinct_preserves_content():
|
|
188
|
+
content = """\
|
|
189
|
+
---
|
|
190
|
+
id: solo
|
|
191
|
+
trigger: "when reviewing"
|
|
192
|
+
confidence: 0.8
|
|
193
|
+
domain: review
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Action
|
|
197
|
+
Check for security issues.
|
|
198
|
+
|
|
199
|
+
## Evidence
|
|
200
|
+
Prevents vulnerabilities.
|
|
201
|
+
"""
|
|
202
|
+
result = parse_instinct_file(content)
|
|
203
|
+
assert len(result) == 1
|
|
204
|
+
assert "Check for security issues." in result[0]["content"]
|
|
205
|
+
assert "Prevents vulnerabilities." in result[0]["content"]
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def test_empty_content_no_error():
|
|
209
|
+
content = """\
|
|
210
|
+
---
|
|
211
|
+
id: empty
|
|
212
|
+
trigger: "placeholder"
|
|
213
|
+
confidence: 0.5
|
|
214
|
+
domain: general
|
|
215
|
+
---
|
|
216
|
+
"""
|
|
217
|
+
result = parse_instinct_file(content)
|
|
218
|
+
assert len(result) == 1
|
|
219
|
+
assert result[0]["content"] == ""
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_parse_no_id_skipped():
|
|
223
|
+
"""Instincts without an 'id' field should be silently dropped."""
|
|
224
|
+
content = """\
|
|
225
|
+
---
|
|
226
|
+
trigger: "when doing nothing"
|
|
227
|
+
confidence: 0.5
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
No id here.
|
|
231
|
+
"""
|
|
232
|
+
result = parse_instinct_file(content)
|
|
233
|
+
assert len(result) == 0
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_parse_confidence_is_float():
|
|
237
|
+
content = """\
|
|
238
|
+
---
|
|
239
|
+
id: float-check
|
|
240
|
+
trigger: "when parsing"
|
|
241
|
+
confidence: 0.42
|
|
242
|
+
domain: general
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
Body.
|
|
246
|
+
"""
|
|
247
|
+
result = parse_instinct_file(content)
|
|
248
|
+
assert isinstance(result[0]["confidence"], float)
|
|
249
|
+
assert result[0]["confidence"] == pytest.approx(0.42)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def test_parse_trigger_strips_quotes():
|
|
253
|
+
content = """\
|
|
254
|
+
---
|
|
255
|
+
id: quote-check
|
|
256
|
+
trigger: "when quoting"
|
|
257
|
+
confidence: 0.5
|
|
258
|
+
domain: general
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
Body.
|
|
262
|
+
"""
|
|
263
|
+
result = parse_instinct_file(content)
|
|
264
|
+
assert result[0]["trigger"] == "when quoting"
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def test_parse_empty_string():
|
|
268
|
+
result = parse_instinct_file("")
|
|
269
|
+
assert result == []
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_parse_garbage_input():
|
|
273
|
+
result = parse_instinct_file("this is not yaml at all\nno frontmatter here")
|
|
274
|
+
assert result == []
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# ─────────────────────────────────────────────
|
|
278
|
+
# _validate_file_path tests
|
|
279
|
+
# ─────────────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
def test_validate_normal_path(tmp_path):
|
|
282
|
+
test_file = tmp_path / "test.yaml"
|
|
283
|
+
test_file.write_text("hello")
|
|
284
|
+
result = _validate_file_path(str(test_file), must_exist=True)
|
|
285
|
+
assert result == test_file.resolve()
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def test_validate_rejects_etc():
|
|
289
|
+
with pytest.raises(ValueError, match="system directory"):
|
|
290
|
+
_validate_file_path("/etc/passwd")
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def test_validate_rejects_var_log():
|
|
294
|
+
with pytest.raises(ValueError, match="system directory"):
|
|
295
|
+
_validate_file_path("/var/log/syslog")
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def test_validate_rejects_usr():
|
|
299
|
+
with pytest.raises(ValueError, match="system directory"):
|
|
300
|
+
_validate_file_path("/usr/local/bin/foo")
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def test_validate_rejects_proc():
|
|
304
|
+
with pytest.raises(ValueError, match="system directory"):
|
|
305
|
+
_validate_file_path("/proc/self/status")
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def test_validate_must_exist_fails(tmp_path):
|
|
309
|
+
with pytest.raises(ValueError, match="does not exist"):
|
|
310
|
+
_validate_file_path(str(tmp_path / "nonexistent.yaml"), must_exist=True)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def test_validate_home_expansion(tmp_path):
|
|
314
|
+
"""Tilde expansion should work."""
|
|
315
|
+
result = _validate_file_path("~/test.yaml")
|
|
316
|
+
assert str(result).startswith(str(Path.home()))
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def test_validate_relative_path(tmp_path, monkeypatch):
|
|
320
|
+
"""Relative paths should be resolved."""
|
|
321
|
+
monkeypatch.chdir(tmp_path)
|
|
322
|
+
test_file = tmp_path / "rel.yaml"
|
|
323
|
+
test_file.write_text("content")
|
|
324
|
+
result = _validate_file_path("rel.yaml", must_exist=True)
|
|
325
|
+
assert result == test_file.resolve()
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# ─────────────────────────────────────────────
|
|
329
|
+
# detect_project tests
|
|
330
|
+
# ─────────────────────────────────────────────
|
|
331
|
+
|
|
332
|
+
def test_detect_project_global_fallback(patch_globals, monkeypatch):
|
|
333
|
+
"""When no git and no env var, should return global project."""
|
|
334
|
+
monkeypatch.delenv("CLAUDE_PROJECT_DIR", raising=False)
|
|
335
|
+
|
|
336
|
+
# Mock subprocess.run to simulate git not available
|
|
337
|
+
def mock_run(*args, **kwargs):
|
|
338
|
+
raise FileNotFoundError("git not found")
|
|
339
|
+
|
|
340
|
+
monkeypatch.setattr("subprocess.run", mock_run)
|
|
341
|
+
|
|
342
|
+
project = detect_project()
|
|
343
|
+
assert project["id"] == "global"
|
|
344
|
+
assert project["name"] == "global"
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def test_detect_project_from_env(patch_globals, monkeypatch, tmp_path):
|
|
348
|
+
"""CLAUDE_PROJECT_DIR env var should be used as project root."""
|
|
349
|
+
fake_repo = tmp_path / "my-repo"
|
|
350
|
+
fake_repo.mkdir()
|
|
351
|
+
monkeypatch.setenv("CLAUDE_PROJECT_DIR", str(fake_repo))
|
|
352
|
+
|
|
353
|
+
# Mock git remote to return a URL
|
|
354
|
+
def mock_run(cmd, **kwargs):
|
|
355
|
+
if "rev-parse" in cmd:
|
|
356
|
+
return SimpleNamespace(returncode=0, stdout=str(fake_repo) + "\n", stderr="")
|
|
357
|
+
if "get-url" in cmd:
|
|
358
|
+
return SimpleNamespace(returncode=0, stdout="https://github.com/test/my-repo.git\n", stderr="")
|
|
359
|
+
return SimpleNamespace(returncode=1, stdout="", stderr="")
|
|
360
|
+
|
|
361
|
+
monkeypatch.setattr("subprocess.run", mock_run)
|
|
362
|
+
|
|
363
|
+
project = detect_project()
|
|
364
|
+
assert project["id"] != "global"
|
|
365
|
+
assert project["name"] == "my-repo"
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def test_detect_project_git_timeout(patch_globals, monkeypatch):
|
|
369
|
+
"""Git timeout should fall through to global."""
|
|
370
|
+
monkeypatch.delenv("CLAUDE_PROJECT_DIR", raising=False)
|
|
371
|
+
import subprocess as sp
|
|
372
|
+
|
|
373
|
+
def mock_run(cmd, **kwargs):
|
|
374
|
+
raise sp.TimeoutExpired(cmd, 5)
|
|
375
|
+
|
|
376
|
+
monkeypatch.setattr("subprocess.run", mock_run)
|
|
377
|
+
|
|
378
|
+
project = detect_project()
|
|
379
|
+
assert project["id"] == "global"
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def test_detect_project_creates_directories(patch_globals, monkeypatch, tmp_path):
|
|
383
|
+
"""detect_project should create the project dir structure."""
|
|
384
|
+
fake_repo = tmp_path / "structured-repo"
|
|
385
|
+
fake_repo.mkdir()
|
|
386
|
+
monkeypatch.setenv("CLAUDE_PROJECT_DIR", str(fake_repo))
|
|
387
|
+
|
|
388
|
+
def mock_run(cmd, **kwargs):
|
|
389
|
+
if "rev-parse" in cmd:
|
|
390
|
+
return SimpleNamespace(returncode=0, stdout=str(fake_repo) + "\n", stderr="")
|
|
391
|
+
if "get-url" in cmd:
|
|
392
|
+
return SimpleNamespace(returncode=1, stdout="", stderr="no remote")
|
|
393
|
+
return SimpleNamespace(returncode=1, stdout="", stderr="")
|
|
394
|
+
|
|
395
|
+
monkeypatch.setattr("subprocess.run", mock_run)
|
|
396
|
+
|
|
397
|
+
project = detect_project()
|
|
398
|
+
assert project["instincts_personal"].exists()
|
|
399
|
+
assert project["instincts_inherited"].exists()
|
|
400
|
+
assert (project["evolved_dir"] / "skills").exists()
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
# ─────────────────────────────────────────────
|
|
404
|
+
# _load_instincts_from_dir tests
|
|
405
|
+
# ─────────────────────────────────────────────
|
|
406
|
+
|
|
407
|
+
def test_load_from_empty_dir(tmp_path):
|
|
408
|
+
result = _load_instincts_from_dir(tmp_path, "personal", "project")
|
|
409
|
+
assert result == []
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def test_load_from_nonexistent_dir(tmp_path):
|
|
413
|
+
result = _load_instincts_from_dir(tmp_path / "does-not-exist", "personal", "project")
|
|
414
|
+
assert result == []
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def test_load_annotates_metadata(tmp_path):
|
|
418
|
+
"""Loaded instincts should have _source_file, _source_type, _scope_label."""
|
|
419
|
+
yaml_file = tmp_path / "test.yaml"
|
|
420
|
+
yaml_file.write_text(SAMPLE_INSTINCT_YAML)
|
|
421
|
+
|
|
422
|
+
result = _load_instincts_from_dir(tmp_path, "personal", "project")
|
|
423
|
+
assert len(result) == 1
|
|
424
|
+
assert result[0]["_source_file"] == str(yaml_file)
|
|
425
|
+
assert result[0]["_source_type"] == "personal"
|
|
426
|
+
assert result[0]["_scope_label"] == "project"
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def test_load_defaults_scope_from_label(tmp_path):
|
|
430
|
+
"""If an instinct has no 'scope' in frontmatter, it should default to scope_label."""
|
|
431
|
+
no_scope_yaml = """\
|
|
432
|
+
---
|
|
433
|
+
id: no-scope
|
|
434
|
+
trigger: "test"
|
|
435
|
+
confidence: 0.5
|
|
436
|
+
domain: general
|
|
437
|
+
---
|
|
438
|
+
|
|
439
|
+
Body.
|
|
440
|
+
"""
|
|
441
|
+
(tmp_path / "no-scope.yaml").write_text(no_scope_yaml)
|
|
442
|
+
result = _load_instincts_from_dir(tmp_path, "inherited", "global")
|
|
443
|
+
assert result[0]["scope"] == "global"
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def test_load_preserves_explicit_scope(tmp_path):
|
|
447
|
+
"""If frontmatter has explicit scope, it should be preserved."""
|
|
448
|
+
yaml_file = tmp_path / "test.yaml"
|
|
449
|
+
yaml_file.write_text(SAMPLE_INSTINCT_YAML)
|
|
450
|
+
|
|
451
|
+
result = _load_instincts_from_dir(tmp_path, "personal", "global")
|
|
452
|
+
# Frontmatter says scope: project, scope_label is global
|
|
453
|
+
# The explicit scope should be preserved (not overwritten)
|
|
454
|
+
assert result[0]["scope"] == "project"
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def test_load_handles_corrupt_file(tmp_path, capsys):
|
|
458
|
+
"""Corrupt YAML files should be warned about but not crash."""
|
|
459
|
+
# A file that will cause parse_instinct_file to return empty
|
|
460
|
+
(tmp_path / "good.yaml").write_text(SAMPLE_INSTINCT_YAML)
|
|
461
|
+
(tmp_path / "bad.yaml").write_text("not yaml\nno frontmatter")
|
|
462
|
+
|
|
463
|
+
result = _load_instincts_from_dir(tmp_path, "personal", "project")
|
|
464
|
+
# bad.yaml has no valid instincts (no id), so only good.yaml contributes
|
|
465
|
+
assert len(result) == 1
|
|
466
|
+
assert result[0]["id"] == "test-instinct"
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def test_load_supports_yml_extension(tmp_path):
|
|
470
|
+
yml_file = tmp_path / "test.yml"
|
|
471
|
+
yml_file.write_text(SAMPLE_INSTINCT_YAML)
|
|
472
|
+
|
|
473
|
+
result = _load_instincts_from_dir(tmp_path, "personal", "project")
|
|
474
|
+
ids = {i["id"] for i in result}
|
|
475
|
+
assert "test-instinct" in ids
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def test_load_supports_md_extension(tmp_path):
|
|
479
|
+
md_file = tmp_path / "legacy-instinct.md"
|
|
480
|
+
md_file.write_text(SAMPLE_INSTINCT_YAML)
|
|
481
|
+
|
|
482
|
+
result = _load_instincts_from_dir(tmp_path, "personal", "project")
|
|
483
|
+
ids = {i["id"] for i in result}
|
|
484
|
+
assert "test-instinct" in ids
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def test_load_instincts_from_dir_uses_utf8_encoding(tmp_path, monkeypatch):
|
|
488
|
+
yaml_file = tmp_path / "test.yaml"
|
|
489
|
+
yaml_file.write_text("placeholder")
|
|
490
|
+
calls = []
|
|
491
|
+
|
|
492
|
+
def fake_read_text(self, *args, **kwargs):
|
|
493
|
+
calls.append(kwargs.get("encoding"))
|
|
494
|
+
return SAMPLE_INSTINCT_YAML
|
|
495
|
+
|
|
496
|
+
monkeypatch.setattr(Path, "read_text", fake_read_text)
|
|
497
|
+
result = _load_instincts_from_dir(tmp_path, "personal", "project")
|
|
498
|
+
assert result[0]["id"] == "test-instinct"
|
|
499
|
+
assert calls == ["utf-8"]
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
# ─────────────────────────────────────────────
|
|
503
|
+
# load_all_instincts tests
|
|
504
|
+
# ─────────────────────────────────────────────
|
|
505
|
+
|
|
506
|
+
def test_load_all_project_and_global(patch_globals):
|
|
507
|
+
"""Should load from both project and global directories."""
|
|
508
|
+
tree = patch_globals
|
|
509
|
+
project = _make_project(tree)
|
|
510
|
+
|
|
511
|
+
# Write a project instinct
|
|
512
|
+
(project["instincts_personal"] / "proj.yaml").write_text(SAMPLE_INSTINCT_YAML)
|
|
513
|
+
# Write a global instinct
|
|
514
|
+
(tree["global_personal"] / "glob.yaml").write_text(SAMPLE_GLOBAL_INSTINCT_YAML)
|
|
515
|
+
|
|
516
|
+
result = load_all_instincts(project)
|
|
517
|
+
ids = {i["id"] for i in result}
|
|
518
|
+
assert "test-instinct" in ids
|
|
519
|
+
assert "global-instinct" in ids
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def test_load_all_project_overrides_global(patch_globals):
|
|
523
|
+
"""When project and global have same ID, project wins."""
|
|
524
|
+
tree = patch_globals
|
|
525
|
+
project = _make_project(tree)
|
|
526
|
+
|
|
527
|
+
# Same ID but different confidence
|
|
528
|
+
proj_yaml = SAMPLE_INSTINCT_YAML.replace("id: test-instinct", "id: shared-id")
|
|
529
|
+
proj_yaml = proj_yaml.replace("confidence: 0.8", "confidence: 0.9")
|
|
530
|
+
glob_yaml = SAMPLE_GLOBAL_INSTINCT_YAML.replace("id: global-instinct", "id: shared-id")
|
|
531
|
+
glob_yaml = glob_yaml.replace("confidence: 0.9", "confidence: 0.3")
|
|
532
|
+
|
|
533
|
+
(project["instincts_personal"] / "shared.yaml").write_text(proj_yaml)
|
|
534
|
+
(tree["global_personal"] / "shared.yaml").write_text(glob_yaml)
|
|
535
|
+
|
|
536
|
+
result = load_all_instincts(project)
|
|
537
|
+
shared = [i for i in result if i["id"] == "shared-id"]
|
|
538
|
+
assert len(shared) == 1
|
|
539
|
+
assert shared[0]["_scope_label"] == "project"
|
|
540
|
+
assert shared[0]["confidence"] == 0.9
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def test_load_all_global_only(patch_globals):
|
|
544
|
+
"""Global project should only load global instincts."""
|
|
545
|
+
tree = patch_globals
|
|
546
|
+
(tree["global_personal"] / "glob.yaml").write_text(SAMPLE_GLOBAL_INSTINCT_YAML)
|
|
547
|
+
|
|
548
|
+
global_project = {
|
|
549
|
+
"id": "global",
|
|
550
|
+
"name": "global",
|
|
551
|
+
"root": "",
|
|
552
|
+
"project_dir": tree["homunculus"],
|
|
553
|
+
"instincts_personal": tree["global_personal"],
|
|
554
|
+
"instincts_inherited": tree["global_inherited"],
|
|
555
|
+
"evolved_dir": tree["global_evolved"],
|
|
556
|
+
"observations_file": tree["homunculus"] / "observations.jsonl",
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
result = load_all_instincts(global_project)
|
|
560
|
+
assert len(result) == 1
|
|
561
|
+
assert result[0]["id"] == "global-instinct"
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def test_load_project_only_excludes_global(patch_globals):
|
|
565
|
+
"""load_project_only_instincts should NOT include global instincts."""
|
|
566
|
+
tree = patch_globals
|
|
567
|
+
project = _make_project(tree)
|
|
568
|
+
|
|
569
|
+
(project["instincts_personal"] / "proj.yaml").write_text(SAMPLE_INSTINCT_YAML)
|
|
570
|
+
(tree["global_personal"] / "glob.yaml").write_text(SAMPLE_GLOBAL_INSTINCT_YAML)
|
|
571
|
+
|
|
572
|
+
result = load_project_only_instincts(project)
|
|
573
|
+
ids = {i["id"] for i in result}
|
|
574
|
+
assert "test-instinct" in ids
|
|
575
|
+
assert "global-instinct" not in ids
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def test_load_project_only_global_fallback_loads_global(patch_globals):
|
|
579
|
+
"""Global fallback should return global instincts for project-only queries."""
|
|
580
|
+
tree = patch_globals
|
|
581
|
+
(tree["global_personal"] / "glob.yaml").write_text(SAMPLE_GLOBAL_INSTINCT_YAML)
|
|
582
|
+
|
|
583
|
+
global_project = {
|
|
584
|
+
"id": "global",
|
|
585
|
+
"name": "global",
|
|
586
|
+
"root": "",
|
|
587
|
+
"project_dir": tree["homunculus"],
|
|
588
|
+
"instincts_personal": tree["global_personal"],
|
|
589
|
+
"instincts_inherited": tree["global_inherited"],
|
|
590
|
+
"evolved_dir": tree["global_evolved"],
|
|
591
|
+
"observations_file": tree["homunculus"] / "observations.jsonl",
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
result = load_project_only_instincts(global_project)
|
|
595
|
+
assert len(result) == 1
|
|
596
|
+
assert result[0]["id"] == "global-instinct"
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def test_load_all_empty(patch_globals):
|
|
600
|
+
"""No instincts at all should return empty list."""
|
|
601
|
+
tree = patch_globals
|
|
602
|
+
project = _make_project(tree)
|
|
603
|
+
|
|
604
|
+
result = load_all_instincts(project)
|
|
605
|
+
assert result == []
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
# ─────────────────────────────────────────────
|
|
609
|
+
# cmd_status tests
|
|
610
|
+
# ─────────────────────────────────────────────
|
|
611
|
+
|
|
612
|
+
def test_cmd_status_no_instincts(patch_globals, monkeypatch, capsys):
|
|
613
|
+
"""Status with no instincts should print fallback message."""
|
|
614
|
+
tree = patch_globals
|
|
615
|
+
project = _make_project(tree)
|
|
616
|
+
monkeypatch.setattr(_mod, "detect_project", lambda: project)
|
|
617
|
+
|
|
618
|
+
args = SimpleNamespace()
|
|
619
|
+
ret = cmd_status(args)
|
|
620
|
+
assert ret == 0
|
|
621
|
+
out = capsys.readouterr().out
|
|
622
|
+
assert "No instincts found." in out
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def test_cmd_status_with_instincts(patch_globals, monkeypatch, capsys):
|
|
626
|
+
"""Status should show project and global instinct counts."""
|
|
627
|
+
tree = patch_globals
|
|
628
|
+
project = _make_project(tree)
|
|
629
|
+
monkeypatch.setattr(_mod, "detect_project", lambda: project)
|
|
630
|
+
|
|
631
|
+
(project["instincts_personal"] / "proj.yaml").write_text(SAMPLE_INSTINCT_YAML)
|
|
632
|
+
(tree["global_personal"] / "glob.yaml").write_text(SAMPLE_GLOBAL_INSTINCT_YAML)
|
|
633
|
+
|
|
634
|
+
args = SimpleNamespace()
|
|
635
|
+
ret = cmd_status(args)
|
|
636
|
+
assert ret == 0
|
|
637
|
+
out = capsys.readouterr().out
|
|
638
|
+
assert "INSTINCT STATUS" in out
|
|
639
|
+
assert "Project instincts: 1" in out
|
|
640
|
+
assert "Global instincts: 1" in out
|
|
641
|
+
assert "PROJECT-SCOPED" in out
|
|
642
|
+
assert "GLOBAL" in out
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def test_cmd_status_returns_int(patch_globals, monkeypatch):
|
|
646
|
+
"""cmd_status should always return an int."""
|
|
647
|
+
tree = patch_globals
|
|
648
|
+
project = _make_project(tree)
|
|
649
|
+
monkeypatch.setattr(_mod, "detect_project", lambda: project)
|
|
650
|
+
|
|
651
|
+
args = SimpleNamespace()
|
|
652
|
+
ret = cmd_status(args)
|
|
653
|
+
assert isinstance(ret, int)
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
# ─────────────────────────────────────────────
|
|
657
|
+
# cmd_projects tests
|
|
658
|
+
# ─────────────────────────────────────────────
|
|
659
|
+
|
|
660
|
+
def test_cmd_projects_empty_registry(patch_globals, capsys):
|
|
661
|
+
"""No projects should print helpful message."""
|
|
662
|
+
args = SimpleNamespace()
|
|
663
|
+
ret = cmd_projects(args)
|
|
664
|
+
assert ret == 0
|
|
665
|
+
out = capsys.readouterr().out
|
|
666
|
+
assert "No projects registered yet." in out
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def test_cmd_projects_with_registry(patch_globals, capsys):
|
|
670
|
+
"""Should list projects from registry."""
|
|
671
|
+
tree = patch_globals
|
|
672
|
+
|
|
673
|
+
# Create a project dir with instincts
|
|
674
|
+
pid = "test123abc"
|
|
675
|
+
project = _make_project(tree, pid=pid, pname="my-app")
|
|
676
|
+
(project["instincts_personal"] / "inst.yaml").write_text(SAMPLE_INSTINCT_YAML)
|
|
677
|
+
|
|
678
|
+
# Write registry
|
|
679
|
+
registry = {
|
|
680
|
+
pid: {
|
|
681
|
+
"name": "my-app",
|
|
682
|
+
"root": "/home/user/my-app",
|
|
683
|
+
"remote": "https://github.com/user/my-app.git",
|
|
684
|
+
"last_seen": "2025-01-15T12:00:00Z",
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
tree["registry_file"].write_text(json.dumps(registry))
|
|
688
|
+
|
|
689
|
+
args = SimpleNamespace()
|
|
690
|
+
ret = cmd_projects(args)
|
|
691
|
+
assert ret == 0
|
|
692
|
+
out = capsys.readouterr().out
|
|
693
|
+
assert "my-app" in out
|
|
694
|
+
assert pid in out
|
|
695
|
+
assert "1 personal" in out
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
# ─────────────────────────────────────────────
|
|
699
|
+
# _promote_specific tests
|
|
700
|
+
# ─────────────────────────────────────────────
|
|
701
|
+
|
|
702
|
+
def test_promote_specific_not_found(patch_globals, capsys):
|
|
703
|
+
"""Promoting nonexistent instinct should fail."""
|
|
704
|
+
tree = patch_globals
|
|
705
|
+
project = _make_project(tree)
|
|
706
|
+
|
|
707
|
+
ret = _promote_specific(project, "nonexistent", force=True)
|
|
708
|
+
assert ret == 1
|
|
709
|
+
out = capsys.readouterr().out
|
|
710
|
+
assert "not found" in out
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def test_promote_specific_rejects_invalid_id(patch_globals, capsys):
|
|
714
|
+
"""Path-like instinct IDs should be rejected before file writes."""
|
|
715
|
+
tree = patch_globals
|
|
716
|
+
project = _make_project(tree)
|
|
717
|
+
|
|
718
|
+
ret = _promote_specific(project, "../escape", force=True)
|
|
719
|
+
assert ret == 1
|
|
720
|
+
err = capsys.readouterr().err
|
|
721
|
+
assert "Invalid instinct ID" in err
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
def test_promote_specific_already_global(patch_globals, capsys):
|
|
725
|
+
"""Promoting an instinct that already exists globally should fail."""
|
|
726
|
+
tree = patch_globals
|
|
727
|
+
project = _make_project(tree)
|
|
728
|
+
|
|
729
|
+
# Write same-id instinct in both project and global
|
|
730
|
+
(project["instincts_personal"] / "shared.yaml").write_text(SAMPLE_INSTINCT_YAML)
|
|
731
|
+
global_yaml = SAMPLE_INSTINCT_YAML # same id: test-instinct
|
|
732
|
+
(tree["global_personal"] / "shared.yaml").write_text(global_yaml)
|
|
733
|
+
|
|
734
|
+
ret = _promote_specific(project, "test-instinct", force=True)
|
|
735
|
+
assert ret == 1
|
|
736
|
+
out = capsys.readouterr().out
|
|
737
|
+
assert "already exists in global" in out
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def test_promote_specific_success(patch_globals, capsys):
|
|
741
|
+
"""Promote a project instinct to global with --force."""
|
|
742
|
+
tree = patch_globals
|
|
743
|
+
project = _make_project(tree)
|
|
744
|
+
|
|
745
|
+
(project["instincts_personal"] / "inst.yaml").write_text(SAMPLE_INSTINCT_YAML)
|
|
746
|
+
|
|
747
|
+
ret = _promote_specific(project, "test-instinct", force=True)
|
|
748
|
+
assert ret == 0
|
|
749
|
+
out = capsys.readouterr().out
|
|
750
|
+
assert "Promoted" in out
|
|
751
|
+
|
|
752
|
+
# Verify file was created in global dir
|
|
753
|
+
promoted_file = tree["global_personal"] / "test-instinct.yaml"
|
|
754
|
+
assert promoted_file.exists()
|
|
755
|
+
content = promoted_file.read_text()
|
|
756
|
+
assert "scope: global" in content
|
|
757
|
+
assert "promoted_from: abc123" in content
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
# ─────────────────────────────────────────────
|
|
761
|
+
# _promote_auto tests
|
|
762
|
+
# ─────────────────────────────────────────────
|
|
763
|
+
|
|
764
|
+
def test_promote_auto_no_candidates(patch_globals, capsys):
|
|
765
|
+
"""Auto-promote with no cross-project instincts should say so."""
|
|
766
|
+
tree = patch_globals
|
|
767
|
+
project = _make_project(tree)
|
|
768
|
+
|
|
769
|
+
# Empty registry
|
|
770
|
+
tree["registry_file"].write_text("{}")
|
|
771
|
+
|
|
772
|
+
ret = _promote_auto(project, force=True, dry_run=False)
|
|
773
|
+
assert ret == 0
|
|
774
|
+
out = capsys.readouterr().out
|
|
775
|
+
assert "No instincts qualify" in out
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
def test_promote_auto_dry_run(patch_globals, capsys):
|
|
779
|
+
"""Dry run should list candidates but not write files."""
|
|
780
|
+
tree = patch_globals
|
|
781
|
+
|
|
782
|
+
# Create two projects with the same high-confidence instinct
|
|
783
|
+
p1 = _make_project(tree, pid="proj1", pname="project-one")
|
|
784
|
+
p2 = _make_project(tree, pid="proj2", pname="project-two")
|
|
785
|
+
|
|
786
|
+
high_conf_yaml = """\
|
|
787
|
+
---
|
|
788
|
+
id: cross-project-instinct
|
|
789
|
+
trigger: "when reviewing"
|
|
790
|
+
confidence: 0.95
|
|
791
|
+
domain: security
|
|
792
|
+
scope: project
|
|
793
|
+
---
|
|
794
|
+
|
|
795
|
+
## Action
|
|
796
|
+
Always review for injection.
|
|
797
|
+
"""
|
|
798
|
+
(p1["instincts_personal"] / "cross.yaml").write_text(high_conf_yaml)
|
|
799
|
+
(p2["instincts_personal"] / "cross.yaml").write_text(high_conf_yaml)
|
|
800
|
+
|
|
801
|
+
# Write registry
|
|
802
|
+
registry = {
|
|
803
|
+
"proj1": {"name": "project-one", "root": "/a", "remote": "", "last_seen": "2025-01-01T00:00:00Z"},
|
|
804
|
+
"proj2": {"name": "project-two", "root": "/b", "remote": "", "last_seen": "2025-01-01T00:00:00Z"},
|
|
805
|
+
}
|
|
806
|
+
tree["registry_file"].write_text(json.dumps(registry))
|
|
807
|
+
|
|
808
|
+
project = p1
|
|
809
|
+
ret = _promote_auto(project, force=True, dry_run=True)
|
|
810
|
+
assert ret == 0
|
|
811
|
+
out = capsys.readouterr().out
|
|
812
|
+
assert "DRY RUN" in out
|
|
813
|
+
assert "cross-project-instinct" in out
|
|
814
|
+
|
|
815
|
+
# Verify no file was created
|
|
816
|
+
assert not (tree["global_personal"] / "cross-project-instinct.yaml").exists()
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
def test_promote_auto_writes_file(patch_globals, capsys):
|
|
820
|
+
"""Auto-promote with force should write global instinct file."""
|
|
821
|
+
tree = patch_globals
|
|
822
|
+
|
|
823
|
+
p1 = _make_project(tree, pid="proj1", pname="project-one")
|
|
824
|
+
p2 = _make_project(tree, pid="proj2", pname="project-two")
|
|
825
|
+
|
|
826
|
+
high_conf_yaml = """\
|
|
827
|
+
---
|
|
828
|
+
id: universal-pattern
|
|
829
|
+
trigger: "when coding"
|
|
830
|
+
confidence: 0.85
|
|
831
|
+
domain: general
|
|
832
|
+
scope: project
|
|
833
|
+
---
|
|
834
|
+
|
|
835
|
+
## Action
|
|
836
|
+
Use descriptive variable names.
|
|
837
|
+
"""
|
|
838
|
+
(p1["instincts_personal"] / "uni.yaml").write_text(high_conf_yaml)
|
|
839
|
+
(p2["instincts_personal"] / "uni.yaml").write_text(high_conf_yaml)
|
|
840
|
+
|
|
841
|
+
registry = {
|
|
842
|
+
"proj1": {"name": "project-one", "root": "/a", "remote": "", "last_seen": "2025-01-01T00:00:00Z"},
|
|
843
|
+
"proj2": {"name": "project-two", "root": "/b", "remote": "", "last_seen": "2025-01-01T00:00:00Z"},
|
|
844
|
+
}
|
|
845
|
+
tree["registry_file"].write_text(json.dumps(registry))
|
|
846
|
+
|
|
847
|
+
ret = _promote_auto(p1, force=True, dry_run=False)
|
|
848
|
+
assert ret == 0
|
|
849
|
+
|
|
850
|
+
promoted = tree["global_personal"] / "universal-pattern.yaml"
|
|
851
|
+
assert promoted.exists()
|
|
852
|
+
content = promoted.read_text()
|
|
853
|
+
assert "scope: global" in content
|
|
854
|
+
assert "auto-promoted" in content
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
def test_promote_auto_skips_invalid_id(patch_globals, capsys):
|
|
858
|
+
tree = patch_globals
|
|
859
|
+
|
|
860
|
+
p1 = _make_project(tree, pid="proj1", pname="project-one")
|
|
861
|
+
p2 = _make_project(tree, pid="proj2", pname="project-two")
|
|
862
|
+
|
|
863
|
+
bad_id_yaml = """\
|
|
864
|
+
---
|
|
865
|
+
id: ../escape
|
|
866
|
+
trigger: "when coding"
|
|
867
|
+
confidence: 0.9
|
|
868
|
+
domain: general
|
|
869
|
+
scope: project
|
|
870
|
+
---
|
|
871
|
+
|
|
872
|
+
## Action
|
|
873
|
+
Invalid id should be skipped.
|
|
874
|
+
"""
|
|
875
|
+
(p1["instincts_personal"] / "bad.yaml").write_text(bad_id_yaml)
|
|
876
|
+
(p2["instincts_personal"] / "bad.yaml").write_text(bad_id_yaml)
|
|
877
|
+
|
|
878
|
+
registry = {
|
|
879
|
+
"proj1": {"name": "project-one", "root": "/a", "remote": "", "last_seen": "2025-01-01T00:00:00Z"},
|
|
880
|
+
"proj2": {"name": "project-two", "root": "/b", "remote": "", "last_seen": "2025-01-01T00:00:00Z"},
|
|
881
|
+
}
|
|
882
|
+
tree["registry_file"].write_text(json.dumps(registry))
|
|
883
|
+
|
|
884
|
+
ret = _promote_auto(p1, force=True, dry_run=False)
|
|
885
|
+
assert ret == 0
|
|
886
|
+
err = capsys.readouterr().err
|
|
887
|
+
assert "Skipping invalid instinct ID" in err
|
|
888
|
+
assert not (tree["global_personal"] / "../escape.yaml").exists()
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
# ─────────────────────────────────────────────
|
|
892
|
+
# _find_cross_project_instincts tests
|
|
893
|
+
# ─────────────────────────────────────────────
|
|
894
|
+
|
|
895
|
+
def test_find_cross_project_empty_registry(patch_globals):
|
|
896
|
+
tree = patch_globals
|
|
897
|
+
tree["registry_file"].write_text("{}")
|
|
898
|
+
result = _find_cross_project_instincts()
|
|
899
|
+
assert result == {}
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def test_find_cross_project_single_project(patch_globals):
|
|
903
|
+
"""Single project should return nothing (need 2+)."""
|
|
904
|
+
tree = patch_globals
|
|
905
|
+
p1 = _make_project(tree, pid="proj1", pname="project-one")
|
|
906
|
+
(p1["instincts_personal"] / "inst.yaml").write_text(SAMPLE_INSTINCT_YAML)
|
|
907
|
+
|
|
908
|
+
registry = {"proj1": {"name": "project-one", "root": "/a", "remote": "", "last_seen": "2025-01-01T00:00:00Z"}}
|
|
909
|
+
tree["registry_file"].write_text(json.dumps(registry))
|
|
910
|
+
|
|
911
|
+
result = _find_cross_project_instincts()
|
|
912
|
+
assert result == {}
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
def test_find_cross_project_shared_instinct(patch_globals):
|
|
916
|
+
"""Same instinct ID in 2 projects should be found."""
|
|
917
|
+
tree = patch_globals
|
|
918
|
+
p1 = _make_project(tree, pid="proj1", pname="project-one")
|
|
919
|
+
p2 = _make_project(tree, pid="proj2", pname="project-two")
|
|
920
|
+
|
|
921
|
+
(p1["instincts_personal"] / "shared.yaml").write_text(SAMPLE_INSTINCT_YAML)
|
|
922
|
+
(p2["instincts_personal"] / "shared.yaml").write_text(SAMPLE_INSTINCT_YAML)
|
|
923
|
+
|
|
924
|
+
registry = {
|
|
925
|
+
"proj1": {"name": "project-one", "root": "/a", "remote": "", "last_seen": "2025-01-01T00:00:00Z"},
|
|
926
|
+
"proj2": {"name": "project-two", "root": "/b", "remote": "", "last_seen": "2025-01-01T00:00:00Z"},
|
|
927
|
+
}
|
|
928
|
+
tree["registry_file"].write_text(json.dumps(registry))
|
|
929
|
+
|
|
930
|
+
result = _find_cross_project_instincts()
|
|
931
|
+
assert "test-instinct" in result
|
|
932
|
+
assert len(result["test-instinct"]) == 2
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
# ─────────────────────────────────────────────
|
|
936
|
+
# load_registry tests
|
|
937
|
+
# ─────────────────────────────────────────────
|
|
938
|
+
|
|
939
|
+
def test_load_registry_missing_file(patch_globals):
|
|
940
|
+
result = load_registry()
|
|
941
|
+
assert result == {}
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def test_load_registry_corrupt_json(patch_globals):
|
|
945
|
+
tree = patch_globals
|
|
946
|
+
tree["registry_file"].write_text("not json at all {{{")
|
|
947
|
+
result = load_registry()
|
|
948
|
+
assert result == {}
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
def test_load_registry_valid(patch_globals):
|
|
952
|
+
tree = patch_globals
|
|
953
|
+
data = {"abc": {"name": "test", "root": "/test"}}
|
|
954
|
+
tree["registry_file"].write_text(json.dumps(data))
|
|
955
|
+
result = load_registry()
|
|
956
|
+
assert result == data
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
def test_load_registry_uses_utf8_encoding(monkeypatch):
|
|
960
|
+
calls = []
|
|
961
|
+
|
|
962
|
+
def fake_open(path, mode="r", *args, **kwargs):
|
|
963
|
+
calls.append(kwargs.get("encoding"))
|
|
964
|
+
return io.StringIO("{}")
|
|
965
|
+
|
|
966
|
+
monkeypatch.setattr(_mod, "open", fake_open, raising=False)
|
|
967
|
+
assert load_registry() == {}
|
|
968
|
+
assert calls == ["utf-8"]
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
def test_validate_instinct_id():
|
|
972
|
+
assert _validate_instinct_id("good-id_1.0")
|
|
973
|
+
assert not _validate_instinct_id("../bad")
|
|
974
|
+
assert not _validate_instinct_id("bad/name")
|
|
975
|
+
assert not _validate_instinct_id(".hidden")
|
|
976
|
+
|
|
977
|
+
|
|
978
|
+
def test_update_registry_atomic_replaces_file(patch_globals):
|
|
979
|
+
tree = patch_globals
|
|
980
|
+
_update_registry("abc123", "demo", "/repo", "https://example.com/repo.git")
|
|
981
|
+
data = json.loads(tree["registry_file"].read_text())
|
|
982
|
+
assert "abc123" in data
|
|
983
|
+
leftovers = list(tree["registry_file"].parent.glob(".projects.json.tmp.*"))
|
|
984
|
+
assert leftovers == []
|