@rubix0270/arboris 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (451) hide show
  1. package/package.json +25 -37
  2. package/run.mjs +10 -0
  3. package/dist/cli.mjs +0 -383
  4. package/manifest.json +0 -323
  5. package/prisma/skills/accessibility/SKILL.md +0 -147
  6. package/prisma/skills/agent-architecture-audit/SKILL.md +0 -257
  7. package/prisma/skills/agent-eval/SKILL.md +0 -146
  8. package/prisma/skills/agent-harness-construction/SKILL.md +0 -74
  9. package/prisma/skills/agent-introspection-debugging/SKILL.md +0 -154
  10. package/prisma/skills/agent-payment-x402/SKILL.md +0 -225
  11. package/prisma/skills/agent-self-evaluation/SKILL.md +0 -182
  12. package/prisma/skills/agent-self-evaluation/examples/high-score-example.md +0 -87
  13. package/prisma/skills/agent-self-evaluation/examples/low-score-example.md +0 -86
  14. package/prisma/skills/agent-self-evaluation/references/evaluation-criteria.md +0 -71
  15. package/prisma/skills/agent-self-evaluation/references/hook-integration.md +0 -64
  16. package/prisma/skills/agent-self-evaluation/scripts/evaluate.py +0 -408
  17. package/prisma/skills/agent-self-evaluation/templates/evaluation-report.md +0 -86
  18. package/prisma/skills/agent-sort/SKILL.md +0 -216
  19. package/prisma/skills/agentic-engineering/SKILL.md +0 -64
  20. package/prisma/skills/agentic-os/SKILL.md +0 -388
  21. package/prisma/skills/ai-first-engineering/SKILL.md +0 -52
  22. package/prisma/skills/ai-regression-testing/SKILL.md +0 -386
  23. package/prisma/skills/android-clean-architecture/SKILL.md +0 -340
  24. package/prisma/skills/angular-developer/SKILL.md +0 -155
  25. package/prisma/skills/angular-developer/references/angular-animations.md +0 -160
  26. package/prisma/skills/angular-developer/references/angular-aria.md +0 -410
  27. package/prisma/skills/angular-developer/references/cli.md +0 -86
  28. package/prisma/skills/angular-developer/references/component-harnesses.md +0 -59
  29. package/prisma/skills/angular-developer/references/component-styling.md +0 -91
  30. package/prisma/skills/angular-developer/references/components.md +0 -117
  31. package/prisma/skills/angular-developer/references/creating-services.md +0 -97
  32. package/prisma/skills/angular-developer/references/data-resolvers.md +0 -69
  33. package/prisma/skills/angular-developer/references/define-routes.md +0 -67
  34. package/prisma/skills/angular-developer/references/defining-providers.md +0 -72
  35. package/prisma/skills/angular-developer/references/di-fundamentals.md +0 -120
  36. package/prisma/skills/angular-developer/references/e2e-testing.md +0 -56
  37. package/prisma/skills/angular-developer/references/effects.md +0 -83
  38. package/prisma/skills/angular-developer/references/hierarchical-injectors.md +0 -43
  39. package/prisma/skills/angular-developer/references/host-elements.md +0 -80
  40. package/prisma/skills/angular-developer/references/injection-context.md +0 -63
  41. package/prisma/skills/angular-developer/references/inputs.md +0 -101
  42. package/prisma/skills/angular-developer/references/linked-signal.md +0 -59
  43. package/prisma/skills/angular-developer/references/loading-strategies.md +0 -61
  44. package/prisma/skills/angular-developer/references/mcp.md +0 -108
  45. package/prisma/skills/angular-developer/references/navigate-to-routes.md +0 -69
  46. package/prisma/skills/angular-developer/references/outputs.md +0 -86
  47. package/prisma/skills/angular-developer/references/reactive-forms.md +0 -122
  48. package/prisma/skills/angular-developer/references/rendering-strategies.md +0 -44
  49. package/prisma/skills/angular-developer/references/resource.md +0 -77
  50. package/prisma/skills/angular-developer/references/route-animations.md +0 -56
  51. package/prisma/skills/angular-developer/references/route-guards.md +0 -52
  52. package/prisma/skills/angular-developer/references/router-lifecycle.md +0 -45
  53. package/prisma/skills/angular-developer/references/router-testing.md +0 -87
  54. package/prisma/skills/angular-developer/references/show-routes-with-outlets.md +0 -68
  55. package/prisma/skills/angular-developer/references/signal-forms.md +0 -795
  56. package/prisma/skills/angular-developer/references/signals-overview.md +0 -94
  57. package/prisma/skills/angular-developer/references/tailwind-css.md +0 -69
  58. package/prisma/skills/angular-developer/references/template-driven-forms.md +0 -114
  59. package/prisma/skills/angular-developer/references/testing-fundamentals.md +0 -65
  60. package/prisma/skills/api-connector-builder/SKILL.md +0 -121
  61. package/prisma/skills/api-design/SKILL.md +0 -524
  62. package/prisma/skills/architecture-decision-records/SKILL.md +0 -180
  63. package/prisma/skills/article-writing/SKILL.md +0 -80
  64. package/prisma/skills/automation-audit-ops/SKILL.md +0 -143
  65. package/prisma/skills/autonomous-agent-harness/SKILL.md +0 -274
  66. package/prisma/skills/autonomous-loops/SKILL.md +0 -611
  67. package/prisma/skills/backend-patterns/SKILL.md +0 -562
  68. package/prisma/skills/benchmark/SKILL.md +0 -94
  69. package/prisma/skills/benchmark-methodology/SKILL.md +0 -190
  70. package/prisma/skills/benchmark-optimization-loop/SKILL.md +0 -70
  71. package/prisma/skills/blender-motion-state-inspection/SKILL.md +0 -165
  72. package/prisma/skills/blueprint/SKILL.md +0 -106
  73. package/prisma/skills/brand-discovery/SKILL.md +0 -145
  74. package/prisma/skills/brand-discovery/references/10_purpose-why.md +0 -40
  75. package/prisma/skills/brand-discovery/references/20_positioning.md +0 -44
  76. package/prisma/skills/brand-discovery/references/30_audience-niche.md +0 -52
  77. package/prisma/skills/brand-discovery/references/40_personality-archetype.md +0 -57
  78. package/prisma/skills/brand-discovery/references/50_voice-tone.md +0 -59
  79. package/prisma/skills/brand-discovery/references/60_narrative-story.md +0 -50
  80. package/prisma/skills/brand-discovery/references/70_founder-tension.md +0 -49
  81. package/prisma/skills/brand-discovery/references/90_SYNTHESIS.md +0 -133
  82. package/prisma/skills/brand-voice/SKILL.md +0 -98
  83. package/prisma/skills/brand-voice/references/voice-profile-schema.md +0 -55
  84. package/prisma/skills/browser-qa/SKILL.md +0 -105
  85. package/prisma/skills/bun-runtime/SKILL.md +0 -85
  86. package/prisma/skills/canary-watch/SKILL.md +0 -108
  87. package/prisma/skills/carrier-relationship-management/SKILL.md +0 -212
  88. package/prisma/skills/cisco-ios-patterns/SKILL.md +0 -164
  89. package/prisma/skills/ck/SKILL.md +0 -148
  90. package/prisma/skills/ck/commands/forget.mjs +0 -44
  91. package/prisma/skills/ck/commands/info.mjs +0 -24
  92. package/prisma/skills/ck/commands/init.mjs +0 -143
  93. package/prisma/skills/ck/commands/list.mjs +0 -40
  94. package/prisma/skills/ck/commands/migrate.mjs +0 -202
  95. package/prisma/skills/ck/commands/resume.mjs +0 -36
  96. package/prisma/skills/ck/commands/save.mjs +0 -210
  97. package/prisma/skills/ck/commands/shared.mjs +0 -387
  98. package/prisma/skills/ck/hooks/session-start.mjs +0 -224
  99. package/prisma/skills/claude-devfleet/SKILL.md +0 -112
  100. package/prisma/skills/click-path-audit/SKILL.md +0 -245
  101. package/prisma/skills/clickhouse-io/SKILL.md +0 -440
  102. package/prisma/skills/code-tour/SKILL.md +0 -254
  103. package/prisma/skills/codebase-onboarding/SKILL.md +0 -234
  104. package/prisma/skills/codehealth-mcp/SKILL.md +0 -167
  105. package/prisma/skills/coding-standards/SKILL.md +0 -551
  106. package/prisma/skills/competitive-platform-analysis/SKILL.md +0 -214
  107. package/prisma/skills/competitive-report-structure/SKILL.md +0 -162
  108. package/prisma/skills/compose-multiplatform-patterns/SKILL.md +0 -300
  109. package/prisma/skills/config-gc/SKILL.md +0 -120
  110. package/prisma/skills/configure-ecc/SKILL.md +0 -385
  111. package/prisma/skills/connections-optimizer/SKILL.md +0 -190
  112. package/prisma/skills/content-engine/SKILL.md +0 -132
  113. package/prisma/skills/content-hash-cache-pattern/SKILL.md +0 -162
  114. package/prisma/skills/context-budget/SKILL.md +0 -136
  115. package/prisma/skills/continuous-agent-loop/SKILL.md +0 -46
  116. package/prisma/skills/continuous-learning/SKILL.md +0 -132
  117. package/prisma/skills/continuous-learning/config.json +0 -18
  118. package/prisma/skills/continuous-learning/evaluate-session.sh +0 -69
  119. package/prisma/skills/continuous-learning-v2/SKILL.md +0 -361
  120. package/prisma/skills/continuous-learning-v2/agents/observer-loop.sh +0 -359
  121. package/prisma/skills/continuous-learning-v2/agents/observer.md +0 -189
  122. package/prisma/skills/continuous-learning-v2/agents/session-guardian.sh +0 -150
  123. package/prisma/skills/continuous-learning-v2/agents/start-observer.sh +0 -248
  124. package/prisma/skills/continuous-learning-v2/config.json +0 -8
  125. package/prisma/skills/continuous-learning-v2/hooks/observe.sh +0 -585
  126. package/prisma/skills/continuous-learning-v2/scripts/detect-project.sh +0 -322
  127. package/prisma/skills/continuous-learning-v2/scripts/instinct-cli.py +0 -1956
  128. package/prisma/skills/continuous-learning-v2/scripts/lib/homunculus-dir.sh +0 -31
  129. package/prisma/skills/continuous-learning-v2/scripts/migrate-homunculus.sh +0 -68
  130. package/prisma/skills/continuous-learning-v2/scripts/test_parse_instinct.py +0 -1421
  131. package/prisma/skills/cost-aware-llm-pipeline/SKILL.md +0 -184
  132. package/prisma/skills/cost-tracking/SKILL.md +0 -97
  133. package/prisma/skills/council/SKILL.md +0 -204
  134. package/prisma/skills/cpp-coding-standards/SKILL.md +0 -724
  135. package/prisma/skills/cpp-testing/SKILL.md +0 -325
  136. package/prisma/skills/crosspost/SKILL.md +0 -112
  137. package/prisma/skills/csharp-testing/SKILL.md +0 -322
  138. package/prisma/skills/customer-billing-ops/SKILL.md +0 -141
  139. package/prisma/skills/customs-trade-compliance/SKILL.md +0 -263
  140. package/prisma/skills/dart-flutter-patterns/SKILL.md +0 -564
  141. package/prisma/skills/dashboard-builder/SKILL.md +0 -109
  142. package/prisma/skills/data-scraper-agent/SKILL.md +0 -765
  143. package/prisma/skills/data-throughput-accelerator/SKILL.md +0 -73
  144. package/prisma/skills/database-migrations/SKILL.md +0 -430
  145. package/prisma/skills/deep-research/SKILL.md +0 -160
  146. package/prisma/skills/defi-amm-security/SKILL.md +0 -167
  147. package/prisma/skills/delivery-gate/SKILL.md +0 -126
  148. package/prisma/skills/delivery-gate/hooks/quality-gate.py +0 -220
  149. package/prisma/skills/deployment-patterns/SKILL.md +0 -428
  150. package/prisma/skills/design-system/SKILL.md +0 -83
  151. package/prisma/skills/django-celery/SKILL.md +0 -458
  152. package/prisma/skills/django-patterns/SKILL.md +0 -735
  153. package/prisma/skills/django-security/SKILL.md +0 -644
  154. package/prisma/skills/django-tdd/SKILL.md +0 -730
  155. package/prisma/skills/django-verification/SKILL.md +0 -470
  156. package/prisma/skills/dmux-workflows/SKILL.md +0 -192
  157. package/prisma/skills/docker-patterns/SKILL.md +0 -365
  158. package/prisma/skills/documentation-lookup/SKILL.md +0 -91
  159. package/prisma/skills/dotnet-patterns/SKILL.md +0 -322
  160. package/prisma/skills/dynamic-workflow-mode/SKILL.md +0 -124
  161. package/prisma/skills/e2e-testing/SKILL.md +0 -327
  162. package/prisma/skills/ecc-guide/SKILL.md +0 -190
  163. package/prisma/skills/ecc-recipes/SKILL.md +0 -149
  164. package/prisma/skills/ecc-tools-cost-audit/SKILL.md +0 -161
  165. package/prisma/skills/email-ops/SKILL.md +0 -122
  166. package/prisma/skills/energy-procurement/SKILL.md +0 -228
  167. package/prisma/skills/enterprise-agent-ops/SKILL.md +0 -51
  168. package/prisma/skills/error-handling/SKILL.md +0 -377
  169. package/prisma/skills/eval-harness/SKILL.md +0 -271
  170. package/prisma/skills/evm-token-decimals/SKILL.md +0 -131
  171. package/prisma/skills/exa-search/SKILL.md +0 -108
  172. package/prisma/skills/fal-ai-media/SKILL.md +0 -289
  173. package/prisma/skills/fastapi-patterns/SKILL.md +0 -514
  174. package/prisma/skills/finance-billing-ops/SKILL.md +0 -128
  175. package/prisma/skills/flox-environments/SKILL.md +0 -497
  176. package/prisma/skills/flutter-dart-code-review/SKILL.md +0 -436
  177. package/prisma/skills/foundation-models-on-device/SKILL.md +0 -243
  178. package/prisma/skills/frontend-a11y/SKILL.md +0 -446
  179. package/prisma/skills/frontend-design-direction/SKILL.md +0 -93
  180. package/prisma/skills/frontend-patterns/SKILL.md +0 -657
  181. package/prisma/skills/frontend-slides/SKILL.md +0 -185
  182. package/prisma/skills/frontend-slides/STYLE_PRESETS.md +0 -330
  183. package/prisma/skills/frontend-slides/animation-patterns.md +0 -122
  184. package/prisma/skills/frontend-slides/html-template.md +0 -419
  185. package/prisma/skills/frontend-slides/scripts/export-pdf.sh +0 -418
  186. package/prisma/skills/frontend-slides/scripts/extract-pptx.py +0 -96
  187. package/prisma/skills/frontend-slides/viewport-base.css +0 -153
  188. package/prisma/skills/fsharp-testing/SKILL.md +0 -281
  189. package/prisma/skills/gan-style-harness/SKILL.md +0 -279
  190. package/prisma/skills/gateguard/SKILL.md +0 -133
  191. package/prisma/skills/generating-python-installer/SKILL.md +0 -820
  192. package/prisma/skills/git-workflow/SKILL.md +0 -716
  193. package/prisma/skills/github-ops/SKILL.md +0 -145
  194. package/prisma/skills/golang-patterns/SKILL.md +0 -675
  195. package/prisma/skills/golang-testing/SKILL.md +0 -721
  196. package/prisma/skills/google-workspace-ops/SKILL.md +0 -96
  197. package/prisma/skills/growth-log/SKILL.md +0 -128
  198. package/prisma/skills/healthcare-cdss-patterns/SKILL.md +0 -246
  199. package/prisma/skills/healthcare-emr-patterns/SKILL.md +0 -160
  200. package/prisma/skills/healthcare-eval-harness/SKILL.md +0 -208
  201. package/prisma/skills/healthcare-phi-compliance/SKILL.md +0 -146
  202. package/prisma/skills/hermes-imports/SKILL.md +0 -89
  203. package/prisma/skills/hexagonal-architecture/SKILL.md +0 -277
  204. package/prisma/skills/hipaa-compliance/SKILL.md +0 -79
  205. package/prisma/skills/homelab-network-readiness/SKILL.md +0 -170
  206. package/prisma/skills/homelab-network-setup/SKILL.md +0 -130
  207. package/prisma/skills/homelab-pihole-dns/SKILL.md +0 -275
  208. package/prisma/skills/homelab-vlan-segmentation/SKILL.md +0 -312
  209. package/prisma/skills/homelab-wireguard-vpn/SKILL.md +0 -306
  210. package/prisma/skills/hookify-rules/SKILL.md +0 -128
  211. package/prisma/skills/inherit-legacy-style/SKILL.md +0 -157
  212. package/prisma/skills/intent-driven-development/SKILL.md +0 -360
  213. package/prisma/skills/inventory-demand-planning/SKILL.md +0 -247
  214. package/prisma/skills/investor-materials/SKILL.md +0 -97
  215. package/prisma/skills/investor-outreach/SKILL.md +0 -92
  216. package/prisma/skills/ios-icon-gen/SKILL.md +0 -158
  217. package/prisma/skills/ios-icon-gen/scripts/generate_icons.swift +0 -258
  218. package/prisma/skills/ios-icon-gen/scripts/iconify_gen.sh +0 -235
  219. package/prisma/skills/iterative-retrieval/SKILL.md +0 -212
  220. package/prisma/skills/ito-basket-compare/SKILL.md +0 -64
  221. package/prisma/skills/ito-data-atlas-agent/SKILL.md +0 -64
  222. package/prisma/skills/ito-market-intelligence/SKILL.md +0 -61
  223. package/prisma/skills/ito-trade-planner/SKILL.md +0 -68
  224. package/prisma/skills/java-coding-standards/SKILL.md +0 -384
  225. package/prisma/skills/jira-integration/SKILL.md +0 -303
  226. package/prisma/skills/jpa-patterns/SKILL.md +0 -152
  227. package/prisma/skills/knowledge-ops/SKILL.md +0 -155
  228. package/prisma/skills/kotlin-coroutines-flows/SKILL.md +0 -285
  229. package/prisma/skills/kotlin-exposed-patterns/SKILL.md +0 -720
  230. package/prisma/skills/kotlin-ktor-patterns/SKILL.md +0 -690
  231. package/prisma/skills/kotlin-patterns/SKILL.md +0 -712
  232. package/prisma/skills/kotlin-testing/SKILL.md +0 -825
  233. package/prisma/skills/kubernetes-patterns/SKILL.md +0 -756
  234. package/prisma/skills/laravel-patterns/SKILL.md +0 -416
  235. package/prisma/skills/laravel-plugin-discovery/SKILL.md +0 -230
  236. package/prisma/skills/laravel-security/SKILL.md +0 -948
  237. package/prisma/skills/laravel-tdd/SKILL.md +0 -675
  238. package/prisma/skills/laravel-verification/SKILL.md +0 -180
  239. package/prisma/skills/latency-critical-systems/SKILL.md +0 -74
  240. package/prisma/skills/lead-intelligence/SKILL.md +0 -322
  241. package/prisma/skills/lead-intelligence/agents/enrichment-agent.md +0 -85
  242. package/prisma/skills/lead-intelligence/agents/mutual-mapper.md +0 -75
  243. package/prisma/skills/lead-intelligence/agents/outreach-drafter.md +0 -98
  244. package/prisma/skills/lead-intelligence/agents/signal-scorer.md +0 -60
  245. package/prisma/skills/liquid-glass-design/SKILL.md +0 -279
  246. package/prisma/skills/llm-trading-agent-security/SKILL.md +0 -147
  247. package/prisma/skills/logistics-exception-management/SKILL.md +0 -222
  248. package/prisma/skills/loop-design-check/SKILL.md +0 -143
  249. package/prisma/skills/mailtrap-email-integration/SKILL.md +0 -77
  250. package/prisma/skills/make-interfaces-feel-better/SKILL.md +0 -152
  251. package/prisma/skills/manim-video/SKILL.md +0 -90
  252. package/prisma/skills/manim-video/assets/network_graph_scene.py +0 -52
  253. package/prisma/skills/market-research/SKILL.md +0 -76
  254. package/prisma/skills/marketing-campaign/SKILL.md +0 -114
  255. package/prisma/skills/mcp-server-patterns/SKILL.md +0 -70
  256. package/prisma/skills/messages-ops/SKILL.md +0 -105
  257. package/prisma/skills/ml-adoption-playbook/SKILL.md +0 -57
  258. package/prisma/skills/mle-workflow/SKILL.md +0 -347
  259. package/prisma/skills/motion-advanced/SKILL.md +0 -596
  260. package/prisma/skills/motion-foundations/SKILL.md +0 -299
  261. package/prisma/skills/motion-patterns/SKILL.md +0 -434
  262. package/prisma/skills/motion-ui/SKILL.md +0 -576
  263. package/prisma/skills/mysql-patterns/SKILL.md +0 -413
  264. package/prisma/skills/nanoclaw-repl/SKILL.md +0 -34
  265. package/prisma/skills/nestjs-patterns/SKILL.md +0 -231
  266. package/prisma/skills/netmiko-ssh-automation/SKILL.md +0 -174
  267. package/prisma/skills/network-bgp-diagnostics/SKILL.md +0 -168
  268. package/prisma/skills/network-config-validation/SKILL.md +0 -211
  269. package/prisma/skills/network-interface-health/SKILL.md +0 -153
  270. package/prisma/skills/nextjs-turbopack/SKILL.md +0 -58
  271. package/prisma/skills/nodejs-keccak256/SKILL.md +0 -103
  272. package/prisma/skills/nutrient-document-processing/SKILL.md +0 -168
  273. package/prisma/skills/nuxt4-patterns/SKILL.md +0 -101
  274. package/prisma/skills/openclaw-persona-forge/SKILL.md +0 -289
  275. package/prisma/skills/openclaw-persona-forge/gacha.py +0 -224
  276. package/prisma/skills/openclaw-persona-forge/gacha.sh +0 -5
  277. package/prisma/skills/openclaw-persona-forge/references/avatar-style.md +0 -124
  278. package/prisma/skills/openclaw-persona-forge/references/boundary-rules.md +0 -53
  279. package/prisma/skills/openclaw-persona-forge/references/error-handling.md +0 -53
  280. package/prisma/skills/openclaw-persona-forge/references/identity-tension.md +0 -48
  281. package/prisma/skills/openclaw-persona-forge/references/naming-system.md +0 -39
  282. package/prisma/skills/openclaw-persona-forge/references/output-template.md +0 -166
  283. package/prisma/skills/opensource-pipeline/SKILL.md +0 -256
  284. package/prisma/skills/orch-add-feature/SKILL.md +0 -45
  285. package/prisma/skills/orch-build-mvp/SKILL.md +0 -49
  286. package/prisma/skills/orch-change-feature/SKILL.md +0 -43
  287. package/prisma/skills/orch-fix-defect/SKILL.md +0 -43
  288. package/prisma/skills/orch-pipeline/SKILL.md +0 -121
  289. package/prisma/skills/orch-refine-code/SKILL.md +0 -44
  290. package/prisma/skills/parallel-execution-optimizer/SKILL.md +0 -73
  291. package/prisma/skills/perl-patterns/SKILL.md +0 -505
  292. package/prisma/skills/perl-security/SKILL.md +0 -504
  293. package/prisma/skills/perl-testing/SKILL.md +0 -476
  294. package/prisma/skills/plan-orchestrate/SKILL.md +0 -263
  295. package/prisma/skills/plankton-code-quality/SKILL.md +0 -237
  296. package/prisma/skills/postgres-patterns/SKILL.md +0 -148
  297. package/prisma/skills/prediction-market-oracle-research/SKILL.md +0 -64
  298. package/prisma/skills/prediction-market-risk-review/SKILL.md +0 -61
  299. package/prisma/skills/prisma-patterns/SKILL.md +0 -401
  300. package/prisma/skills/product-capability/SKILL.md +0 -142
  301. package/prisma/skills/product-lens/SKILL.md +0 -93
  302. package/prisma/skills/production-audit/SKILL.md +0 -207
  303. package/prisma/skills/production-scheduling/SKILL.md +0 -238
  304. package/prisma/skills/project-flow-ops/SKILL.md +0 -112
  305. package/prisma/skills/prompt-optimizer/SKILL.md +0 -398
  306. package/prisma/skills/python-patterns/SKILL.md +0 -751
  307. package/prisma/skills/python-testing/SKILL.md +0 -817
  308. package/prisma/skills/pytorch-patterns/SKILL.md +0 -397
  309. package/prisma/skills/quality-nonconformance/SKILL.md +0 -260
  310. package/prisma/skills/quarkus-patterns/SKILL.md +0 -723
  311. package/prisma/skills/quarkus-security/SKILL.md +0 -468
  312. package/prisma/skills/quarkus-tdd/SKILL.md +0 -812
  313. package/prisma/skills/quarkus-verification/SKILL.md +0 -480
  314. package/prisma/skills/ralphinho-rfc-pipeline/SKILL.md +0 -68
  315. package/prisma/skills/react-native-patterns/SKILL.md +0 -326
  316. package/prisma/skills/react-patterns/SKILL.md +0 -342
  317. package/prisma/skills/react-performance/SKILL.md +0 -575
  318. package/prisma/skills/react-testing/SKILL.md +0 -424
  319. package/prisma/skills/recsys-pipeline-architect/SKILL.md +0 -115
  320. package/prisma/skills/recursive-decision-ledger/SKILL.md +0 -80
  321. package/prisma/skills/redis-patterns/SKILL.md +0 -404
  322. package/prisma/skills/regex-vs-llm-structured-text/SKILL.md +0 -221
  323. package/prisma/skills/remotion-video-creation/SKILL.md +0 -43
  324. package/prisma/skills/remotion-video-creation/rules/3d.md +0 -86
  325. package/prisma/skills/remotion-video-creation/rules/animations.md +0 -29
  326. package/prisma/skills/remotion-video-creation/rules/assets/charts-bar-chart.tsx +0 -173
  327. package/prisma/skills/remotion-video-creation/rules/assets/text-animations-typewriter.tsx +0 -100
  328. package/prisma/skills/remotion-video-creation/rules/assets/text-animations-word-highlight.tsx +0 -108
  329. package/prisma/skills/remotion-video-creation/rules/assets.md +0 -78
  330. package/prisma/skills/remotion-video-creation/rules/audio.md +0 -172
  331. package/prisma/skills/remotion-video-creation/rules/calculate-metadata.md +0 -104
  332. package/prisma/skills/remotion-video-creation/rules/can-decode.md +0 -75
  333. package/prisma/skills/remotion-video-creation/rules/charts.md +0 -58
  334. package/prisma/skills/remotion-video-creation/rules/compositions.md +0 -146
  335. package/prisma/skills/remotion-video-creation/rules/display-captions.md +0 -126
  336. package/prisma/skills/remotion-video-creation/rules/extract-frames.md +0 -229
  337. package/prisma/skills/remotion-video-creation/rules/fonts.md +0 -152
  338. package/prisma/skills/remotion-video-creation/rules/get-audio-duration.md +0 -58
  339. package/prisma/skills/remotion-video-creation/rules/get-video-dimensions.md +0 -68
  340. package/prisma/skills/remotion-video-creation/rules/get-video-duration.md +0 -58
  341. package/prisma/skills/remotion-video-creation/rules/gifs.md +0 -138
  342. package/prisma/skills/remotion-video-creation/rules/images.md +0 -130
  343. package/prisma/skills/remotion-video-creation/rules/import-srt-captions.md +0 -67
  344. package/prisma/skills/remotion-video-creation/rules/lottie.md +0 -67
  345. package/prisma/skills/remotion-video-creation/rules/measuring-dom-nodes.md +0 -34
  346. package/prisma/skills/remotion-video-creation/rules/measuring-text.md +0 -143
  347. package/prisma/skills/remotion-video-creation/rules/sequencing.md +0 -106
  348. package/prisma/skills/remotion-video-creation/rules/tailwind.md +0 -11
  349. package/prisma/skills/remotion-video-creation/rules/text-animations.md +0 -20
  350. package/prisma/skills/remotion-video-creation/rules/timing.md +0 -179
  351. package/prisma/skills/remotion-video-creation/rules/transcribe-captions.md +0 -19
  352. package/prisma/skills/remotion-video-creation/rules/transitions.md +0 -122
  353. package/prisma/skills/remotion-video-creation/rules/trimming.md +0 -52
  354. package/prisma/skills/remotion-video-creation/rules/videos.md +0 -171
  355. package/prisma/skills/repo-scan/SKILL.md +0 -79
  356. package/prisma/skills/research-ops/SKILL.md +0 -113
  357. package/prisma/skills/returns-reverse-logistics/SKILL.md +0 -240
  358. package/prisma/skills/rules-distill/SKILL.md +0 -265
  359. package/prisma/skills/rules-distill/scripts/scan-rules.sh +0 -58
  360. package/prisma/skills/rules-distill/scripts/scan-skills.sh +0 -129
  361. package/prisma/skills/rust-patterns/SKILL.md +0 -500
  362. package/prisma/skills/rust-testing/SKILL.md +0 -501
  363. package/prisma/skills/safety-guard/SKILL.md +0 -76
  364. package/prisma/skills/santa-method/SKILL.md +0 -307
  365. package/prisma/skills/scientific-db-pubmed-database/SKILL.md +0 -176
  366. package/prisma/skills/scientific-db-uspto-database/SKILL.md +0 -178
  367. package/prisma/skills/scientific-pkg-gget/SKILL.md +0 -167
  368. package/prisma/skills/scientific-thinking-literature-review/SKILL.md +0 -193
  369. package/prisma/skills/scientific-thinking-scholar-evaluation/SKILL.md +0 -161
  370. package/prisma/skills/search-first/SKILL.md +0 -183
  371. package/prisma/skills/security-bounty-hunter/SKILL.md +0 -100
  372. package/prisma/skills/security-review/SKILL.md +0 -504
  373. package/prisma/skills/security-review/cloud-infrastructure-security.md +0 -361
  374. package/prisma/skills/security-scan/SKILL.md +0 -166
  375. package/prisma/skills/seo/SKILL.md +0 -155
  376. package/prisma/skills/skill-comply/SKILL.md +0 -59
  377. package/prisma/skills/skill-comply/fixtures/compliant_trace.jsonl +0 -5
  378. package/prisma/skills/skill-comply/fixtures/noncompliant_trace.jsonl +0 -3
  379. package/prisma/skills/skill-comply/fixtures/tdd_spec.yaml +0 -44
  380. package/prisma/skills/skill-comply/prompts/classifier.md +0 -24
  381. package/prisma/skills/skill-comply/prompts/scenario_generator.md +0 -62
  382. package/prisma/skills/skill-comply/prompts/spec_generator.md +0 -42
  383. package/prisma/skills/skill-comply/pyproject.toml +0 -15
  384. package/prisma/skills/skill-comply/scripts/__init__.py +0 -0
  385. package/prisma/skills/skill-comply/scripts/classifier.py +0 -85
  386. package/prisma/skills/skill-comply/scripts/grader.py +0 -124
  387. package/prisma/skills/skill-comply/scripts/parser.py +0 -107
  388. package/prisma/skills/skill-comply/scripts/report.py +0 -170
  389. package/prisma/skills/skill-comply/scripts/run.py +0 -127
  390. package/prisma/skills/skill-comply/scripts/runner.py +0 -194
  391. package/prisma/skills/skill-comply/scripts/scenario_generator.py +0 -70
  392. package/prisma/skills/skill-comply/scripts/spec_generator.py +0 -72
  393. package/prisma/skills/skill-comply/scripts/utils.py +0 -13
  394. package/prisma/skills/skill-comply/tests/test_grader.py +0 -197
  395. package/prisma/skills/skill-comply/tests/test_parser.py +0 -90
  396. package/prisma/skills/skill-comply/tests/test_runner.py +0 -172
  397. package/prisma/skills/skill-scout/SKILL.md +0 -141
  398. package/prisma/skills/skill-stocktake/SKILL.md +0 -195
  399. package/prisma/skills/skill-stocktake/scripts/quick-diff.sh +0 -87
  400. package/prisma/skills/skill-stocktake/scripts/save-results.sh +0 -56
  401. package/prisma/skills/skill-stocktake/scripts/scan.sh +0 -170
  402. package/prisma/skills/social-graph-ranker/SKILL.md +0 -155
  403. package/prisma/skills/social-publisher/SKILL.md +0 -130
  404. package/prisma/skills/springboot-patterns/SKILL.md +0 -315
  405. package/prisma/skills/springboot-security/SKILL.md +0 -273
  406. package/prisma/skills/springboot-tdd/SKILL.md +0 -159
  407. package/prisma/skills/springboot-verification/SKILL.md +0 -232
  408. package/prisma/skills/strategic-compact/SKILL.md +0 -136
  409. package/prisma/skills/swift-actor-persistence/SKILL.md +0 -144
  410. package/prisma/skills/swift-concurrency-6-2/SKILL.md +0 -216
  411. package/prisma/skills/swift-protocol-di-testing/SKILL.md +0 -191
  412. package/prisma/skills/swiftui-patterns/SKILL.md +0 -259
  413. package/prisma/skills/taste/SKILL.md +0 -264
  414. package/prisma/skills/taste/references/genre-taxonomy.md +0 -87
  415. package/prisma/skills/tdd-workflow/SKILL.md +0 -583
  416. package/prisma/skills/team-agent-orchestration/SKILL.md +0 -111
  417. package/prisma/skills/team-builder/SKILL.md +0 -169
  418. package/prisma/skills/terminal-ops/SKILL.md +0 -110
  419. package/prisma/skills/tinystruct-patterns/SKILL.md +0 -279
  420. package/prisma/skills/tinystruct-patterns/references/architecture.md +0 -90
  421. package/prisma/skills/tinystruct-patterns/references/data-handling.md +0 -60
  422. package/prisma/skills/tinystruct-patterns/references/database.md +0 -99
  423. package/prisma/skills/tinystruct-patterns/references/routing.md +0 -64
  424. package/prisma/skills/tinystruct-patterns/references/system-usage.md +0 -97
  425. package/prisma/skills/tinystruct-patterns/references/testing.md +0 -72
  426. package/prisma/skills/token-budget-advisor/SKILL.md +0 -134
  427. package/prisma/skills/ui-demo/SKILL.md +0 -466
  428. package/prisma/skills/ui-to-vue/SKILL.md +0 -135
  429. package/prisma/skills/uncloud/SKILL.md +0 -344
  430. package/prisma/skills/unified-notifications-ops/SKILL.md +0 -188
  431. package/prisma/skills/verification-loop/SKILL.md +0 -127
  432. package/prisma/skills/video-editing/SKILL.md +0 -311
  433. package/prisma/skills/videodb/SKILL.md +0 -375
  434. package/prisma/skills/videodb/reference/api-reference.md +0 -550
  435. package/prisma/skills/videodb/reference/capture-reference.md +0 -407
  436. package/prisma/skills/videodb/reference/capture.md +0 -101
  437. package/prisma/skills/videodb/reference/editor.md +0 -443
  438. package/prisma/skills/videodb/reference/generative.md +0 -331
  439. package/prisma/skills/videodb/reference/rtstream-reference.md +0 -564
  440. package/prisma/skills/videodb/reference/rtstream.md +0 -65
  441. package/prisma/skills/videodb/reference/search.md +0 -230
  442. package/prisma/skills/videodb/reference/streaming.md +0 -406
  443. package/prisma/skills/videodb/reference/use-cases.md +0 -118
  444. package/prisma/skills/videodb/scripts/ws_listener.py +0 -282
  445. package/prisma/skills/visa-doc-translate/README.md +0 -86
  446. package/prisma/skills/visa-doc-translate/SKILL.md +0 -117
  447. package/prisma/skills/vite-patterns/SKILL.md +0 -450
  448. package/prisma/skills/vue-patterns/SKILL.md +0 -471
  449. package/prisma/skills/windows-desktop-e2e/SKILL.md +0 -888
  450. package/prisma/skills/workspace-surface-audit/SKILL.md +0 -126
  451. package/prisma/skills/x-api/SKILL.md +0 -235
@@ -1,1421 +0,0 @@
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
- _validate_import_url = _mod._validate_import_url
48
- _update_registry = _mod._update_registry
49
- _write_registry = _mod._write_registry
50
- _remove_project_storage = _mod._remove_project_storage
51
- _confidence_bar = _mod._confidence_bar
52
-
53
-
54
- # ─────────────────────────────────────────────
55
- # Fixtures
56
- # ─────────────────────────────────────────────
57
-
58
- SAMPLE_INSTINCT_YAML = """\
59
- ---
60
- id: test-instinct
61
- trigger: "when writing tests"
62
- confidence: 0.8
63
- domain: testing
64
- scope: project
65
- ---
66
-
67
- ## Action
68
- Always write tests first.
69
-
70
- ## Evidence
71
- TDD leads to better design.
72
- """
73
-
74
- SAMPLE_GLOBAL_INSTINCT_YAML = """\
75
- ---
76
- id: global-instinct
77
- trigger: "always"
78
- confidence: 0.9
79
- domain: security
80
- scope: global
81
- ---
82
-
83
- ## Action
84
- Validate all user input.
85
- """
86
-
87
-
88
- @pytest.fixture
89
- def project_tree(tmp_path):
90
- """Create a realistic project directory tree for testing."""
91
- homunculus = tmp_path / ".claude" / "homunculus"
92
- projects_dir = homunculus / "projects"
93
- global_personal = homunculus / "instincts" / "personal"
94
- global_inherited = homunculus / "instincts" / "inherited"
95
- global_evolved = homunculus / "evolved"
96
-
97
- for d in [
98
- global_personal, global_inherited,
99
- global_evolved / "skills", global_evolved / "commands", global_evolved / "agents",
100
- projects_dir,
101
- ]:
102
- d.mkdir(parents=True, exist_ok=True)
103
-
104
- return {
105
- "root": tmp_path,
106
- "homunculus": homunculus,
107
- "projects_dir": projects_dir,
108
- "global_personal": global_personal,
109
- "global_inherited": global_inherited,
110
- "global_evolved": global_evolved,
111
- "registry_file": homunculus / "projects.json",
112
- }
113
-
114
-
115
- @pytest.fixture
116
- def patch_globals(project_tree, monkeypatch):
117
- """Patch module-level globals to use tmp_path-based directories."""
118
- monkeypatch.setattr(_mod, "HOMUNCULUS_DIR", project_tree["homunculus"])
119
- monkeypatch.setattr(_mod, "PROJECTS_DIR", project_tree["projects_dir"])
120
- monkeypatch.setattr(_mod, "REGISTRY_FILE", project_tree["registry_file"])
121
- monkeypatch.setattr(_mod, "GLOBAL_PERSONAL_DIR", project_tree["global_personal"])
122
- monkeypatch.setattr(_mod, "GLOBAL_INHERITED_DIR", project_tree["global_inherited"])
123
- monkeypatch.setattr(_mod, "GLOBAL_EVOLVED_DIR", project_tree["global_evolved"])
124
- monkeypatch.setattr(_mod, "GLOBAL_OBSERVATIONS_FILE", project_tree["homunculus"] / "observations.jsonl")
125
- return project_tree
126
-
127
-
128
- def _make_project(tree, pid="abc123", pname="test-project"):
129
- """Create project directory structure and return a project dict."""
130
- project_dir = tree["projects_dir"] / pid
131
- personal_dir = project_dir / "instincts" / "personal"
132
- inherited_dir = project_dir / "instincts" / "inherited"
133
- for d in [personal_dir, inherited_dir,
134
- project_dir / "evolved" / "skills",
135
- project_dir / "evolved" / "commands",
136
- project_dir / "evolved" / "agents",
137
- project_dir / "observations.archive"]:
138
- d.mkdir(parents=True, exist_ok=True)
139
-
140
- return {
141
- "id": pid,
142
- "name": pname,
143
- "root": str(tree["root"] / "fake-repo"),
144
- "remote": "https://github.com/test/test-project.git",
145
- "project_dir": project_dir,
146
- "instincts_personal": personal_dir,
147
- "instincts_inherited": inherited_dir,
148
- "evolved_dir": project_dir / "evolved",
149
- "observations_file": project_dir / "observations.jsonl",
150
- }
151
-
152
-
153
- # ─────────────────────────────────────────────
154
- # parse_instinct_file tests
155
- # ─────────────────────────────────────────────
156
-
157
- MULTI_SECTION = """\
158
- ---
159
- id: instinct-a
160
- trigger: "when coding"
161
- confidence: 0.9
162
- domain: general
163
- ---
164
-
165
- ## Action
166
- Do thing A.
167
-
168
- ## Examples
169
- - Example A1
170
-
171
- ---
172
- id: instinct-b
173
- trigger: "when testing"
174
- confidence: 0.7
175
- domain: testing
176
- ---
177
-
178
- ## Action
179
- Do thing B.
180
- """
181
-
182
-
183
- def test_multiple_instincts_preserve_content():
184
- result = parse_instinct_file(MULTI_SECTION)
185
- assert len(result) == 2
186
- assert "Do thing A." in result[0]["content"]
187
- assert "Example A1" in result[0]["content"]
188
- assert "Do thing B." in result[1]["content"]
189
-
190
-
191
- def test_single_instinct_preserves_content():
192
- content = """\
193
- ---
194
- id: solo
195
- trigger: "when reviewing"
196
- confidence: 0.8
197
- domain: review
198
- ---
199
-
200
- ## Action
201
- Check for security issues.
202
-
203
- ## Evidence
204
- Prevents vulnerabilities.
205
- """
206
- result = parse_instinct_file(content)
207
- assert len(result) == 1
208
- assert "Check for security issues." in result[0]["content"]
209
- assert "Prevents vulnerabilities." in result[0]["content"]
210
-
211
-
212
- def test_empty_content_no_error():
213
- content = """\
214
- ---
215
- id: empty
216
- trigger: "placeholder"
217
- confidence: 0.5
218
- domain: general
219
- ---
220
- """
221
- result = parse_instinct_file(content)
222
- assert len(result) == 1
223
- assert result[0]["content"] == ""
224
-
225
-
226
- def test_parse_no_id_skipped():
227
- """Instincts without an 'id' field should be silently dropped."""
228
- content = """\
229
- ---
230
- trigger: "when doing nothing"
231
- confidence: 0.5
232
- ---
233
-
234
- No id here.
235
- """
236
- result = parse_instinct_file(content)
237
- assert len(result) == 0
238
-
239
-
240
- def test_parse_confidence_is_float():
241
- content = """\
242
- ---
243
- id: float-check
244
- trigger: "when parsing"
245
- confidence: 0.42
246
- domain: general
247
- ---
248
-
249
- Body.
250
- """
251
- result = parse_instinct_file(content)
252
- assert isinstance(result[0]["confidence"], float)
253
- assert result[0]["confidence"] == pytest.approx(0.42)
254
-
255
-
256
- def test_parse_trigger_strips_quotes():
257
- content = """\
258
- ---
259
- id: quote-check
260
- trigger: "when quoting"
261
- confidence: 0.5
262
- domain: general
263
- ---
264
-
265
- Body.
266
- """
267
- result = parse_instinct_file(content)
268
- assert result[0]["trigger"] == "when quoting"
269
-
270
-
271
- def test_parse_empty_string():
272
- result = parse_instinct_file("")
273
- assert result == []
274
-
275
-
276
- def test_parse_garbage_input():
277
- result = parse_instinct_file("this is not yaml at all\nno frontmatter here")
278
- assert result == []
279
-
280
-
281
- # ─────────────────────────────────────────────
282
- # _validate_file_path tests
283
- # ─────────────────────────────────────────────
284
-
285
- def test_validate_normal_path(tmp_path):
286
- test_file = tmp_path / "test.yaml"
287
- test_file.write_text("hello")
288
- result = _validate_file_path(str(test_file), must_exist=True)
289
- assert result == test_file.resolve()
290
-
291
-
292
- def test_validate_rejects_etc():
293
- with pytest.raises(ValueError, match="system directory"):
294
- _validate_file_path("/etc/passwd")
295
-
296
-
297
- def test_validate_rejects_var_log():
298
- with pytest.raises(ValueError, match="system directory"):
299
- _validate_file_path("/var/log/syslog")
300
-
301
-
302
- def test_validate_rejects_usr():
303
- with pytest.raises(ValueError, match="system directory"):
304
- _validate_file_path("/usr/local/bin/foo")
305
-
306
-
307
- def test_validate_rejects_proc():
308
- with pytest.raises(ValueError, match="system directory"):
309
- _validate_file_path("/proc/self/status")
310
-
311
-
312
- def test_validate_must_exist_fails(tmp_path):
313
- with pytest.raises(ValueError, match="does not exist"):
314
- _validate_file_path(str(tmp_path / "nonexistent.yaml"), must_exist=True)
315
-
316
-
317
- def test_validate_home_expansion(tmp_path):
318
- """Tilde expansion should work."""
319
- result = _validate_file_path("~/test.yaml")
320
- assert str(result).startswith(str(Path.home()))
321
-
322
-
323
- def test_validate_relative_path(tmp_path, monkeypatch):
324
- """Relative paths should be resolved."""
325
- monkeypatch.chdir(tmp_path)
326
- test_file = tmp_path / "rel.yaml"
327
- test_file.write_text("content")
328
- result = _validate_file_path("rel.yaml", must_exist=True)
329
- assert result == test_file.resolve()
330
-
331
-
332
- def test_validate_import_url_rejects_http():
333
- """Remote imports should not downgrade to plaintext HTTP."""
334
- with pytest.raises(ValueError, match="require https"):
335
- _validate_import_url("http://example.com/instincts.yaml")
336
-
337
-
338
- def test_validate_import_url_rejects_private_hosts(monkeypatch):
339
- """Remote imports should not resolve to private or loopback addresses."""
340
- monkeypatch.setattr(
341
- _mod.socket,
342
- "getaddrinfo",
343
- lambda *args, **kwargs: [(None, None, None, None, ("127.0.0.1", 443))],
344
- )
345
- with pytest.raises(ValueError, match="non-public address"):
346
- _validate_import_url("https://example.com/instincts.yaml")
347
-
348
-
349
- def test_validate_import_url_allows_public_https(monkeypatch):
350
- monkeypatch.setattr(
351
- _mod.socket,
352
- "getaddrinfo",
353
- lambda *args, **kwargs: [(None, None, None, None, ("93.184.216.34", 443))],
354
- )
355
- assert _validate_import_url("https://example.com/instincts.yaml") == "https://example.com/instincts.yaml"
356
-
357
-
358
- # ─────────────────────────────────────────────
359
- # detect_project tests
360
- # ─────────────────────────────────────────────
361
-
362
- def test_detect_project_global_fallback(patch_globals, monkeypatch):
363
- """When no git and no env var, should return global project."""
364
- monkeypatch.delenv("CLAUDE_PROJECT_DIR", raising=False)
365
-
366
- # Mock subprocess.run to simulate git not available
367
- def mock_run(*args, **kwargs):
368
- raise FileNotFoundError("git not found")
369
-
370
- monkeypatch.setattr("subprocess.run", mock_run)
371
-
372
- project = detect_project()
373
- assert project["id"] == "global"
374
- assert project["name"] == "global"
375
-
376
-
377
- def test_detect_project_from_env(patch_globals, monkeypatch, tmp_path):
378
- """CLAUDE_PROJECT_DIR env var should be used as project root."""
379
- fake_repo = tmp_path / "my-repo"
380
- fake_repo.mkdir()
381
- monkeypatch.setenv("CLAUDE_PROJECT_DIR", str(fake_repo))
382
-
383
- # Mock git remote to return a URL
384
- def mock_run(cmd, **kwargs):
385
- if "rev-parse" in cmd:
386
- return SimpleNamespace(returncode=0, stdout=str(fake_repo) + "\n", stderr="")
387
- if "get-url" in cmd:
388
- return SimpleNamespace(returncode=0, stdout="https://github.com/test/my-repo.git\n", stderr="")
389
- return SimpleNamespace(returncode=1, stdout="", stderr="")
390
-
391
- monkeypatch.setattr("subprocess.run", mock_run)
392
-
393
- project = detect_project()
394
- assert project["id"] != "global"
395
- assert project["name"] == "my-repo"
396
-
397
-
398
- def test_detect_project_git_timeout(patch_globals, monkeypatch):
399
- """Git timeout should fall through to global."""
400
- monkeypatch.delenv("CLAUDE_PROJECT_DIR", raising=False)
401
- import subprocess as sp
402
-
403
- def mock_run(cmd, **kwargs):
404
- raise sp.TimeoutExpired(cmd, 5)
405
-
406
- monkeypatch.setattr("subprocess.run", mock_run)
407
-
408
- project = detect_project()
409
- assert project["id"] == "global"
410
-
411
-
412
- def test_detect_project_creates_directories(patch_globals, monkeypatch, tmp_path):
413
- """detect_project should create the project dir structure."""
414
- fake_repo = tmp_path / "structured-repo"
415
- fake_repo.mkdir()
416
- monkeypatch.setenv("CLAUDE_PROJECT_DIR", str(fake_repo))
417
-
418
- def mock_run(cmd, **kwargs):
419
- if "rev-parse" in cmd:
420
- return SimpleNamespace(returncode=0, stdout=str(fake_repo) + "\n", stderr="")
421
- if "get-url" in cmd:
422
- return SimpleNamespace(returncode=1, stdout="", stderr="no remote")
423
- return SimpleNamespace(returncode=1, stdout="", stderr="")
424
-
425
- monkeypatch.setattr("subprocess.run", mock_run)
426
-
427
- project = detect_project()
428
- assert project["instincts_personal"].exists()
429
- assert project["instincts_inherited"].exists()
430
- assert (project["evolved_dir"] / "skills").exists()
431
-
432
-
433
- # ─────────────────────────────────────────────
434
- # _load_instincts_from_dir tests
435
- # ─────────────────────────────────────────────
436
-
437
- def test_load_from_empty_dir(tmp_path):
438
- result = _load_instincts_from_dir(tmp_path, "personal", "project")
439
- assert result == []
440
-
441
-
442
- def test_load_from_nonexistent_dir(tmp_path):
443
- result = _load_instincts_from_dir(tmp_path / "does-not-exist", "personal", "project")
444
- assert result == []
445
-
446
-
447
- def test_load_annotates_metadata(tmp_path):
448
- """Loaded instincts should have _source_file, _source_type, _scope_label."""
449
- yaml_file = tmp_path / "test.yaml"
450
- yaml_file.write_text(SAMPLE_INSTINCT_YAML)
451
-
452
- result = _load_instincts_from_dir(tmp_path, "personal", "project")
453
- assert len(result) == 1
454
- assert result[0]["_source_file"] == str(yaml_file)
455
- assert result[0]["_source_type"] == "personal"
456
- assert result[0]["_scope_label"] == "project"
457
-
458
-
459
- def test_load_defaults_scope_from_label(tmp_path):
460
- """If an instinct has no 'scope' in frontmatter, it should default to scope_label."""
461
- no_scope_yaml = """\
462
- ---
463
- id: no-scope
464
- trigger: "test"
465
- confidence: 0.5
466
- domain: general
467
- ---
468
-
469
- Body.
470
- """
471
- (tmp_path / "no-scope.yaml").write_text(no_scope_yaml)
472
- result = _load_instincts_from_dir(tmp_path, "inherited", "global")
473
- assert result[0]["scope"] == "global"
474
-
475
-
476
- def test_load_preserves_explicit_scope(tmp_path):
477
- """If frontmatter has explicit scope, it should be preserved."""
478
- yaml_file = tmp_path / "test.yaml"
479
- yaml_file.write_text(SAMPLE_INSTINCT_YAML)
480
-
481
- result = _load_instincts_from_dir(tmp_path, "personal", "global")
482
- # Frontmatter says scope: project, scope_label is global
483
- # The explicit scope should be preserved (not overwritten)
484
- assert result[0]["scope"] == "project"
485
-
486
-
487
- def test_load_handles_corrupt_file(tmp_path, capsys):
488
- """Corrupt YAML files should be warned about but not crash."""
489
- # A file that will cause parse_instinct_file to return empty
490
- (tmp_path / "good.yaml").write_text(SAMPLE_INSTINCT_YAML)
491
- (tmp_path / "bad.yaml").write_text("not yaml\nno frontmatter")
492
-
493
- result = _load_instincts_from_dir(tmp_path, "personal", "project")
494
- # bad.yaml has no valid instincts (no id), so only good.yaml contributes
495
- assert len(result) == 1
496
- assert result[0]["id"] == "test-instinct"
497
-
498
-
499
- def test_load_supports_yml_extension(tmp_path):
500
- yml_file = tmp_path / "test.yml"
501
- yml_file.write_text(SAMPLE_INSTINCT_YAML)
502
-
503
- result = _load_instincts_from_dir(tmp_path, "personal", "project")
504
- ids = {i["id"] for i in result}
505
- assert "test-instinct" in ids
506
-
507
-
508
- def test_load_supports_md_extension(tmp_path):
509
- md_file = tmp_path / "legacy-instinct.md"
510
- md_file.write_text(SAMPLE_INSTINCT_YAML)
511
-
512
- result = _load_instincts_from_dir(tmp_path, "personal", "project")
513
- ids = {i["id"] for i in result}
514
- assert "test-instinct" in ids
515
-
516
-
517
- def test_load_instincts_from_dir_uses_utf8_encoding(tmp_path, monkeypatch):
518
- yaml_file = tmp_path / "test.yaml"
519
- yaml_file.write_text("placeholder")
520
- calls = []
521
-
522
- def fake_read_text(self, *args, **kwargs):
523
- calls.append(kwargs.get("encoding"))
524
- return SAMPLE_INSTINCT_YAML
525
-
526
- monkeypatch.setattr(Path, "read_text", fake_read_text)
527
- result = _load_instincts_from_dir(tmp_path, "personal", "project")
528
- assert result[0]["id"] == "test-instinct"
529
- assert calls == ["utf-8"]
530
-
531
-
532
- # ─────────────────────────────────────────────
533
- # load_all_instincts tests
534
- # ─────────────────────────────────────────────
535
-
536
- def test_load_all_project_and_global(patch_globals):
537
- """Should load from both project and global directories."""
538
- tree = patch_globals
539
- project = _make_project(tree)
540
-
541
- # Write a project instinct
542
- (project["instincts_personal"] / "proj.yaml").write_text(SAMPLE_INSTINCT_YAML)
543
- # Write a global instinct
544
- (tree["global_personal"] / "glob.yaml").write_text(SAMPLE_GLOBAL_INSTINCT_YAML)
545
-
546
- result = load_all_instincts(project)
547
- ids = {i["id"] for i in result}
548
- assert "test-instinct" in ids
549
- assert "global-instinct" in ids
550
-
551
-
552
- def test_load_all_project_overrides_global(patch_globals):
553
- """When project and global have same ID, project wins."""
554
- tree = patch_globals
555
- project = _make_project(tree)
556
-
557
- # Same ID but different confidence
558
- proj_yaml = SAMPLE_INSTINCT_YAML.replace("id: test-instinct", "id: shared-id")
559
- proj_yaml = proj_yaml.replace("confidence: 0.8", "confidence: 0.9")
560
- glob_yaml = SAMPLE_GLOBAL_INSTINCT_YAML.replace("id: global-instinct", "id: shared-id")
561
- glob_yaml = glob_yaml.replace("confidence: 0.9", "confidence: 0.3")
562
-
563
- (project["instincts_personal"] / "shared.yaml").write_text(proj_yaml)
564
- (tree["global_personal"] / "shared.yaml").write_text(glob_yaml)
565
-
566
- result = load_all_instincts(project)
567
- shared = [i for i in result if i["id"] == "shared-id"]
568
- assert len(shared) == 1
569
- assert shared[0]["_scope_label"] == "project"
570
- assert shared[0]["confidence"] == 0.9
571
-
572
-
573
- def test_load_all_global_only(patch_globals):
574
- """Global project should only load global instincts."""
575
- tree = patch_globals
576
- (tree["global_personal"] / "glob.yaml").write_text(SAMPLE_GLOBAL_INSTINCT_YAML)
577
-
578
- global_project = {
579
- "id": "global",
580
- "name": "global",
581
- "root": "",
582
- "project_dir": tree["homunculus"],
583
- "instincts_personal": tree["global_personal"],
584
- "instincts_inherited": tree["global_inherited"],
585
- "evolved_dir": tree["global_evolved"],
586
- "observations_file": tree["homunculus"] / "observations.jsonl",
587
- }
588
-
589
- result = load_all_instincts(global_project)
590
- assert len(result) == 1
591
- assert result[0]["id"] == "global-instinct"
592
-
593
-
594
- def test_load_project_only_excludes_global(patch_globals):
595
- """load_project_only_instincts should NOT include global instincts."""
596
- tree = patch_globals
597
- project = _make_project(tree)
598
-
599
- (project["instincts_personal"] / "proj.yaml").write_text(SAMPLE_INSTINCT_YAML)
600
- (tree["global_personal"] / "glob.yaml").write_text(SAMPLE_GLOBAL_INSTINCT_YAML)
601
-
602
- result = load_project_only_instincts(project)
603
- ids = {i["id"] for i in result}
604
- assert "test-instinct" in ids
605
- assert "global-instinct" not in ids
606
-
607
-
608
- def test_load_project_only_global_fallback_loads_global(patch_globals):
609
- """Global fallback should return global instincts for project-only queries."""
610
- tree = patch_globals
611
- (tree["global_personal"] / "glob.yaml").write_text(SAMPLE_GLOBAL_INSTINCT_YAML)
612
-
613
- global_project = {
614
- "id": "global",
615
- "name": "global",
616
- "root": "",
617
- "project_dir": tree["homunculus"],
618
- "instincts_personal": tree["global_personal"],
619
- "instincts_inherited": tree["global_inherited"],
620
- "evolved_dir": tree["global_evolved"],
621
- "observations_file": tree["homunculus"] / "observations.jsonl",
622
- }
623
-
624
- result = load_project_only_instincts(global_project)
625
- assert len(result) == 1
626
- assert result[0]["id"] == "global-instinct"
627
-
628
-
629
- def test_load_all_empty(patch_globals):
630
- """No instincts at all should return empty list."""
631
- tree = patch_globals
632
- project = _make_project(tree)
633
-
634
- result = load_all_instincts(project)
635
- assert result == []
636
-
637
-
638
- # ─────────────────────────────────────────────
639
- # cmd_status tests
640
- # ─────────────────────────────────────────────
641
-
642
- def test_cmd_status_no_instincts(patch_globals, monkeypatch, capsys):
643
- """Status with no instincts should print fallback message."""
644
- tree = patch_globals
645
- project = _make_project(tree)
646
- monkeypatch.setattr(_mod, "detect_project", lambda: project)
647
-
648
- args = SimpleNamespace()
649
- ret = cmd_status(args)
650
- assert ret == 0
651
- out = capsys.readouterr().out
652
- assert "No instincts found." in out
653
-
654
-
655
- def test_cmd_status_with_instincts(patch_globals, monkeypatch, capsys):
656
- """Status should show project and global instinct counts."""
657
- tree = patch_globals
658
- project = _make_project(tree)
659
- monkeypatch.setattr(_mod, "detect_project", lambda: project)
660
-
661
- (project["instincts_personal"] / "proj.yaml").write_text(SAMPLE_INSTINCT_YAML)
662
- (tree["global_personal"] / "glob.yaml").write_text(SAMPLE_GLOBAL_INSTINCT_YAML)
663
-
664
- args = SimpleNamespace()
665
- ret = cmd_status(args)
666
- assert ret == 0
667
- out = capsys.readouterr().out
668
- assert "INSTINCT STATUS" in out
669
- assert "Project instincts: 1" in out
670
- assert "Global instincts: 1" in out
671
- assert "PROJECT-SCOPED" in out
672
- assert "GLOBAL" in out
673
-
674
-
675
- def test_confidence_bar_uses_unicode_when_supported():
676
- """Confidence bars should retain block glyphs on UTF-8 streams."""
677
- stream = SimpleNamespace(encoding="utf-8")
678
- assert _confidence_bar(0.8, stream=stream) == "\u2588" * 8 + "\u2591" * 2
679
-
680
-
681
- def test_confidence_bar_uses_ascii_when_stream_rejects_block_glyphs():
682
- """Windows cp1252 streams cannot encode block glyphs."""
683
- stream = SimpleNamespace(encoding="cp1252")
684
- assert _confidence_bar(0.8, stream=stream) == "########.."
685
-
686
-
687
- def test_print_instincts_by_domain_is_cp1252_safe(monkeypatch):
688
- """Status rendering should not crash on Windows cp1252 stdout."""
689
- raw = io.BytesIO()
690
- stream = io.TextIOWrapper(raw, encoding="cp1252")
691
- monkeypatch.setattr(_mod.sys, "stdout", stream)
692
-
693
- _mod._print_instincts_by_domain([{
694
- "id": "windows-safe",
695
- "trigger": "when stdout uses cp1252",
696
- "confidence": 0.8,
697
- "domain": "platform",
698
- "scope": "project",
699
- }])
700
-
701
- stream.flush()
702
- out = raw.getvalue().decode("cp1252")
703
- assert "########.." in out
704
- assert "\u2588" not in out
705
- assert "\u2591" not in out
706
-
707
-
708
- def test_cmd_status_returns_int(patch_globals, monkeypatch):
709
- """cmd_status should always return an int."""
710
- tree = patch_globals
711
- project = _make_project(tree)
712
- monkeypatch.setattr(_mod, "detect_project", lambda: project)
713
-
714
- args = SimpleNamespace()
715
- ret = cmd_status(args)
716
- assert isinstance(ret, int)
717
-
718
-
719
- # ─────────────────────────────────────────────
720
- # cmd_projects tests
721
- # ─────────────────────────────────────────────
722
-
723
- def test_cmd_projects_empty_registry(patch_globals, capsys):
724
- """No projects should print helpful message."""
725
- args = SimpleNamespace()
726
- ret = cmd_projects(args)
727
- assert ret == 0
728
- out = capsys.readouterr().out
729
- assert "No projects registered yet." in out
730
-
731
-
732
- def test_cmd_projects_with_registry(patch_globals, capsys):
733
- """Should list projects from registry."""
734
- tree = patch_globals
735
-
736
- # Create a project dir with instincts
737
- pid = "test123abc"
738
- project = _make_project(tree, pid=pid, pname="my-app")
739
- (project["instincts_personal"] / "inst.yaml").write_text(SAMPLE_INSTINCT_YAML)
740
-
741
- # Write registry
742
- registry = {
743
- pid: {
744
- "name": "my-app",
745
- "root": "/home/user/my-app",
746
- "remote": "https://github.com/user/my-app.git",
747
- "last_seen": "2025-01-15T12:00:00Z",
748
- }
749
- }
750
- tree["registry_file"].write_text(json.dumps(registry))
751
-
752
- args = SimpleNamespace()
753
- ret = cmd_projects(args)
754
- assert ret == 0
755
- out = capsys.readouterr().out
756
- assert "my-app" in out
757
- assert pid in out
758
- assert "1 personal" in out
759
-
760
-
761
- # ─────────────────────────────────────────────
762
- # _promote_specific tests
763
- # ─────────────────────────────────────────────
764
-
765
- def test_promote_specific_not_found(patch_globals, capsys):
766
- """Promoting nonexistent instinct should fail."""
767
- tree = patch_globals
768
- project = _make_project(tree)
769
-
770
- ret = _promote_specific(project, "nonexistent", force=True)
771
- assert ret == 1
772
- out = capsys.readouterr().out
773
- assert "not found" in out
774
-
775
-
776
- def test_promote_specific_rejects_invalid_id(patch_globals, capsys):
777
- """Path-like instinct IDs should be rejected before file writes."""
778
- tree = patch_globals
779
- project = _make_project(tree)
780
-
781
- ret = _promote_specific(project, "../escape", force=True)
782
- assert ret == 1
783
- err = capsys.readouterr().err
784
- assert "Invalid instinct ID" in err
785
-
786
-
787
- def test_promote_specific_already_global(patch_globals, capsys):
788
- """Promoting an instinct that already exists globally should fail."""
789
- tree = patch_globals
790
- project = _make_project(tree)
791
-
792
- # Write same-id instinct in both project and global
793
- (project["instincts_personal"] / "shared.yaml").write_text(SAMPLE_INSTINCT_YAML)
794
- global_yaml = SAMPLE_INSTINCT_YAML # same id: test-instinct
795
- (tree["global_personal"] / "shared.yaml").write_text(global_yaml)
796
-
797
- ret = _promote_specific(project, "test-instinct", force=True)
798
- assert ret == 1
799
- out = capsys.readouterr().out
800
- assert "already exists in global" in out
801
-
802
-
803
- def test_promote_specific_success(patch_globals, capsys):
804
- """Promote a project instinct to global with --force."""
805
- tree = patch_globals
806
- project = _make_project(tree)
807
-
808
- (project["instincts_personal"] / "inst.yaml").write_text(SAMPLE_INSTINCT_YAML)
809
-
810
- ret = _promote_specific(project, "test-instinct", force=True)
811
- assert ret == 0
812
- out = capsys.readouterr().out
813
- assert "Promoted" in out
814
-
815
- # Verify file was created in global dir
816
- promoted_file = tree["global_personal"] / "test-instinct.yaml"
817
- assert promoted_file.exists()
818
- content = promoted_file.read_text()
819
- assert "scope: global" in content
820
- assert "promoted_from: abc123" in content
821
-
822
-
823
- # ─────────────────────────────────────────────
824
- # _promote_auto tests
825
- # ─────────────────────────────────────────────
826
-
827
- def test_promote_auto_no_candidates(patch_globals, capsys):
828
- """Auto-promote with no cross-project instincts should say so."""
829
- tree = patch_globals
830
- project = _make_project(tree)
831
-
832
- # Empty registry
833
- tree["registry_file"].write_text("{}")
834
-
835
- ret = _promote_auto(project, force=True, dry_run=False)
836
- assert ret == 0
837
- out = capsys.readouterr().out
838
- assert "No instincts qualify" in out
839
-
840
-
841
- def test_promote_auto_dry_run(patch_globals, capsys):
842
- """Dry run should list candidates but not write files."""
843
- tree = patch_globals
844
-
845
- # Create two projects with the same high-confidence instinct
846
- p1 = _make_project(tree, pid="proj1", pname="project-one")
847
- p2 = _make_project(tree, pid="proj2", pname="project-two")
848
-
849
- high_conf_yaml = """\
850
- ---
851
- id: cross-project-instinct
852
- trigger: "when reviewing"
853
- confidence: 0.95
854
- domain: security
855
- scope: project
856
- ---
857
-
858
- ## Action
859
- Always review for injection.
860
- """
861
- (p1["instincts_personal"] / "cross.yaml").write_text(high_conf_yaml)
862
- (p2["instincts_personal"] / "cross.yaml").write_text(high_conf_yaml)
863
-
864
- # Write registry
865
- registry = {
866
- "proj1": {"name": "project-one", "root": "/a", "remote": "", "last_seen": "2025-01-01T00:00:00Z"},
867
- "proj2": {"name": "project-two", "root": "/b", "remote": "", "last_seen": "2025-01-01T00:00:00Z"},
868
- }
869
- tree["registry_file"].write_text(json.dumps(registry))
870
-
871
- project = p1
872
- ret = _promote_auto(project, force=True, dry_run=True)
873
- assert ret == 0
874
- out = capsys.readouterr().out
875
- assert "DRY RUN" in out
876
- assert "cross-project-instinct" in out
877
-
878
- # Verify no file was created
879
- assert not (tree["global_personal"] / "cross-project-instinct.yaml").exists()
880
-
881
-
882
- def test_promote_auto_writes_file(patch_globals, capsys):
883
- """Auto-promote with force should write global instinct file."""
884
- tree = patch_globals
885
-
886
- p1 = _make_project(tree, pid="proj1", pname="project-one")
887
- p2 = _make_project(tree, pid="proj2", pname="project-two")
888
-
889
- high_conf_yaml = """\
890
- ---
891
- id: universal-pattern
892
- trigger: "when coding"
893
- confidence: 0.85
894
- domain: general
895
- scope: project
896
- ---
897
-
898
- ## Action
899
- Use descriptive variable names.
900
- """
901
- (p1["instincts_personal"] / "uni.yaml").write_text(high_conf_yaml)
902
- (p2["instincts_personal"] / "uni.yaml").write_text(high_conf_yaml)
903
-
904
- registry = {
905
- "proj1": {"name": "project-one", "root": "/a", "remote": "", "last_seen": "2025-01-01T00:00:00Z"},
906
- "proj2": {"name": "project-two", "root": "/b", "remote": "", "last_seen": "2025-01-01T00:00:00Z"},
907
- }
908
- tree["registry_file"].write_text(json.dumps(registry))
909
-
910
- ret = _promote_auto(p1, force=True, dry_run=False)
911
- assert ret == 0
912
-
913
- promoted = tree["global_personal"] / "universal-pattern.yaml"
914
- assert promoted.exists()
915
- content = promoted.read_text()
916
- assert "scope: global" in content
917
- assert "auto-promoted" in content
918
-
919
-
920
- def test_promote_auto_skips_invalid_id(patch_globals, capsys):
921
- tree = patch_globals
922
-
923
- p1 = _make_project(tree, pid="proj1", pname="project-one")
924
- p2 = _make_project(tree, pid="proj2", pname="project-two")
925
-
926
- bad_id_yaml = """\
927
- ---
928
- id: ../escape
929
- trigger: "when coding"
930
- confidence: 0.9
931
- domain: general
932
- scope: project
933
- ---
934
-
935
- ## Action
936
- Invalid id should be skipped.
937
- """
938
- (p1["instincts_personal"] / "bad.yaml").write_text(bad_id_yaml)
939
- (p2["instincts_personal"] / "bad.yaml").write_text(bad_id_yaml)
940
-
941
- registry = {
942
- "proj1": {"name": "project-one", "root": "/a", "remote": "", "last_seen": "2025-01-01T00:00:00Z"},
943
- "proj2": {"name": "project-two", "root": "/b", "remote": "", "last_seen": "2025-01-01T00:00:00Z"},
944
- }
945
- tree["registry_file"].write_text(json.dumps(registry))
946
-
947
- ret = _promote_auto(p1, force=True, dry_run=False)
948
- assert ret == 0
949
- err = capsys.readouterr().err
950
- assert "Skipping invalid instinct ID" in err
951
- assert not (tree["global_personal"] / "../escape.yaml").exists()
952
-
953
-
954
- # ─────────────────────────────────────────────
955
- # _find_cross_project_instincts tests
956
- # ─────────────────────────────────────────────
957
-
958
- def test_find_cross_project_empty_registry(patch_globals):
959
- tree = patch_globals
960
- tree["registry_file"].write_text("{}")
961
- result = _find_cross_project_instincts()
962
- assert result == {}
963
-
964
-
965
- def test_find_cross_project_single_project(patch_globals):
966
- """Single project should return nothing (need 2+)."""
967
- tree = patch_globals
968
- p1 = _make_project(tree, pid="proj1", pname="project-one")
969
- (p1["instincts_personal"] / "inst.yaml").write_text(SAMPLE_INSTINCT_YAML)
970
-
971
- registry = {"proj1": {"name": "project-one", "root": "/a", "remote": "", "last_seen": "2025-01-01T00:00:00Z"}}
972
- tree["registry_file"].write_text(json.dumps(registry))
973
-
974
- result = _find_cross_project_instincts()
975
- assert result == {}
976
-
977
-
978
- def test_find_cross_project_shared_instinct(patch_globals):
979
- """Same instinct ID in 2 projects should be found."""
980
- tree = patch_globals
981
- p1 = _make_project(tree, pid="proj1", pname="project-one")
982
- p2 = _make_project(tree, pid="proj2", pname="project-two")
983
-
984
- (p1["instincts_personal"] / "shared.yaml").write_text(SAMPLE_INSTINCT_YAML)
985
- (p2["instincts_personal"] / "shared.yaml").write_text(SAMPLE_INSTINCT_YAML)
986
-
987
- registry = {
988
- "proj1": {"name": "project-one", "root": "/a", "remote": "", "last_seen": "2025-01-01T00:00:00Z"},
989
- "proj2": {"name": "project-two", "root": "/b", "remote": "", "last_seen": "2025-01-01T00:00:00Z"},
990
- }
991
- tree["registry_file"].write_text(json.dumps(registry))
992
-
993
- result = _find_cross_project_instincts()
994
- assert "test-instinct" in result
995
- assert len(result["test-instinct"]) == 2
996
-
997
-
998
- # ─────────────────────────────────────────────
999
- # load_registry tests
1000
- # ─────────────────────────────────────────────
1001
-
1002
- def test_load_registry_missing_file(patch_globals):
1003
- result = load_registry()
1004
- assert result == {}
1005
-
1006
-
1007
- def test_load_registry_corrupt_json(patch_globals):
1008
- tree = patch_globals
1009
- tree["registry_file"].write_text("not json at all {{{")
1010
- result = load_registry()
1011
- assert result == {}
1012
-
1013
-
1014
- def test_load_registry_valid(patch_globals):
1015
- tree = patch_globals
1016
- data = {"abc": {"name": "test", "root": "/test"}}
1017
- tree["registry_file"].write_text(json.dumps(data))
1018
- result = load_registry()
1019
- assert result == data
1020
-
1021
-
1022
- def test_load_registry_uses_utf8_encoding(monkeypatch):
1023
- calls = []
1024
-
1025
- def fake_open(path, mode="r", *args, **kwargs):
1026
- calls.append(kwargs.get("encoding"))
1027
- return io.StringIO("{}")
1028
-
1029
- monkeypatch.setattr(_mod, "open", fake_open, raising=False)
1030
- assert load_registry() == {}
1031
- assert calls == ["utf-8"]
1032
-
1033
-
1034
- def test_validate_instinct_id():
1035
- assert _validate_instinct_id("good-id_1.0")
1036
- assert not _validate_instinct_id("../bad")
1037
- assert not _validate_instinct_id("bad/name")
1038
- assert not _validate_instinct_id(".hidden")
1039
-
1040
-
1041
- def test_update_registry_atomic_replaces_file(patch_globals):
1042
- tree = patch_globals
1043
- _update_registry("abc123", "demo", "/repo", "https://example.com/repo.git")
1044
- data = json.loads(tree["registry_file"].read_text())
1045
- assert "abc123" in data
1046
- leftovers = list(tree["registry_file"].parent.glob(".projects.json.tmp.*"))
1047
- assert leftovers == []
1048
-
1049
-
1050
- def test_update_registry_matches_shell_schema(patch_globals):
1051
- # Issue #2299: the Python writer must emit the same field set as the shell
1052
- # counterpart in detect-project.sh (id, name, root, remote, created_at,
1053
- # last_seen) so a projects.json entry has a consistent shape regardless of
1054
- # which path wrote it.
1055
- tree = patch_globals
1056
- _update_registry("abc123", "demo", "/repo", "https://example.com/repo.git")
1057
- entry = json.loads(tree["registry_file"].read_text())["abc123"]
1058
- assert set(entry) == {"id", "name", "root", "remote", "created_at", "last_seen"}
1059
- assert entry["id"] == "abc123"
1060
- assert entry["name"] == "demo"
1061
- assert entry["root"] == "/repo"
1062
- assert entry["remote"] == "https://example.com/repo.git"
1063
- # On the initial write both timestamps come from the same `now`, so the
1064
- # first-write contract is created_at == last_seen.
1065
- assert entry["created_at"]
1066
- assert entry["created_at"] == entry["last_seen"]
1067
-
1068
-
1069
- def test_update_registry_preserves_created_at(patch_globals):
1070
- # created_at is stamped on first write and preserved on subsequent updates,
1071
- # while last_seen advances — matching entry.get("created_at", now) in the
1072
- # shell counterpart.
1073
- tree = patch_globals
1074
- _update_registry("abc123", "demo", "/repo", "https://example.com/repo.git")
1075
- first = json.loads(tree["registry_file"].read_text())["abc123"]
1076
-
1077
- _update_registry("abc123", "demo-renamed", "/repo", "https://example.com/repo.git")
1078
- second = json.loads(tree["registry_file"].read_text())["abc123"]
1079
-
1080
- assert second["created_at"] == first["created_at"]
1081
- assert second["name"] == "demo-renamed"
1082
- assert second["last_seen"] >= first["last_seen"]
1083
-
1084
-
1085
- def test_update_registry_heals_malformed_entry(patch_globals):
1086
- # Issue #2299 follow-up: a non-dict value for the project id (e.g. a
1087
- # corrupt registry) must not crash _update_registry. The entry is healed by
1088
- # the rewrite, preserving the old unconditional-overwrite behavior.
1089
- tree = patch_globals
1090
- tree["registry_file"].write_text(json.dumps({"abc123": None}), encoding="utf-8")
1091
- _update_registry("abc123", "demo", "/repo", "https://example.com/repo.git")
1092
- entry = json.loads(tree["registry_file"].read_text())["abc123"]
1093
- assert isinstance(entry, dict)
1094
- assert entry["id"] == "abc123"
1095
- assert entry["created_at"]
1096
- assert entry["created_at"] == entry["last_seen"]
1097
-
1098
-
1099
- def test_update_registry_heals_non_dict_registry(patch_globals):
1100
- # Issue #2299 follow-up: a top-level registry that is valid JSON but not a
1101
- # mapping (e.g. a list or string from a corrupt projects.json) must not
1102
- # crash _update_registry before the per-entry guard runs. The whole file is
1103
- # healed by the rewrite, preserving the old unconditional-overwrite behavior.
1104
- tree = patch_globals
1105
- tree["registry_file"].write_text(json.dumps(["oops"]), encoding="utf-8")
1106
- _update_registry("abc123", "demo", "/repo", "https://example.com/repo.git")
1107
- registry = json.loads(tree["registry_file"].read_text())
1108
- assert isinstance(registry, dict)
1109
- entry = registry["abc123"]
1110
- assert entry["id"] == "abc123"
1111
- assert entry["created_at"] == entry["last_seen"]
1112
-
1113
-
1114
- def test_write_registry_atomic_no_tmp_leftovers(patch_globals):
1115
- # Issue #2294: _write_registry now holds the registry lock like
1116
- # _update_registry. It must still write atomically with no stray tmp files.
1117
- tree = patch_globals
1118
- _write_registry({"keep": {"name": "demo", "root": "/repo", "remote": ""}})
1119
- data = json.loads(tree["registry_file"].read_text())
1120
- assert data == {"keep": {"name": "demo", "root": "/repo", "remote": ""}}
1121
- leftovers = list(tree["registry_file"].parent.glob(".projects.json.tmp.*"))
1122
- assert leftovers == []
1123
-
1124
-
1125
- def test_remove_project_storage_deletes_contained_dir(patch_globals):
1126
- tree = patch_globals
1127
- target = tree["projects_dir"] / "proj-1"
1128
- (target / "instincts").mkdir(parents=True)
1129
- (target / "instincts" / "x.md").write_text("hi", encoding="utf-8")
1130
- _remove_project_storage("proj-1")
1131
- assert not target.exists()
1132
-
1133
-
1134
- def test_remove_project_storage_missing_dir_is_noop(patch_globals):
1135
- # No raise when the contained dir simply does not exist.
1136
- _remove_project_storage("never-created")
1137
-
1138
-
1139
- def test_remove_project_storage_blocks_traversal(patch_globals):
1140
- # Issue #2297: defense-in-depth — a traversal id must be refused even when a
1141
- # caller skips _validate_project_id, so this can never delete outside
1142
- # PROJECTS_DIR.
1143
- with pytest.raises(ValueError):
1144
- _remove_project_storage("../../etc")
1145
-
1146
-
1147
- def test_remove_project_storage_blocks_root_itself(patch_globals):
1148
- with pytest.raises(ValueError):
1149
- _remove_project_storage(".")
1150
-
1151
-
1152
- # ─────────────────────────────────────────────
1153
- # Issue #2302 coverage:
1154
- # _normalize_remote_url, _promote_specific dry-run,
1155
- # projects delete/gc/merge, cmd_prune
1156
- # ─────────────────────────────────────────────
1157
-
1158
- _normalize_remote_url = _mod._normalize_remote_url
1159
- _cmd_projects_delete = _mod._cmd_projects_delete
1160
- _cmd_projects_gc = _mod._cmd_projects_gc
1161
- _cmd_projects_merge = _mod._cmd_projects_merge
1162
- cmd_prune = _mod.cmd_prune
1163
-
1164
-
1165
- # ── _normalize_remote_url ────────────────────
1166
-
1167
- def test_normalize_remote_url_empty_returns_empty():
1168
- assert _normalize_remote_url("") == ""
1169
- assert _normalize_remote_url(None) == ""
1170
-
1171
-
1172
- def test_normalize_remote_url_scp_form():
1173
- # scp-style host:path -> host/path, credentials/.git stripped, lowercased
1174
- assert _normalize_remote_url("git@github.com:Test/Repo.git") == "github.com/test/repo"
1175
-
1176
-
1177
- def test_normalize_remote_url_https_strips_credentials_and_scheme():
1178
- assert (
1179
- _normalize_remote_url("https://user:token@github.com/test/repo.git")
1180
- == "github.com/test/repo"
1181
- )
1182
-
1183
-
1184
- def test_normalize_remote_url_network_is_lowercased():
1185
- assert _normalize_remote_url("https://GitHub.com/Owner/Project") == "github.com/owner/project"
1186
-
1187
-
1188
- def test_normalize_remote_url_trailing_slash_and_dotgit_stripped():
1189
- assert _normalize_remote_url("https://github.com/a/b.git/") == "github.com/a/b"
1190
-
1191
-
1192
- def test_normalize_remote_url_file_scheme_preserves_case():
1193
- # Local file paths are not network URLs: scheme is stripped but case is preserved.
1194
- assert _normalize_remote_url("file:///srv/Repos/My-Repo/") == "/srv/Repos/My-Repo"
1195
-
1196
-
1197
- def test_normalize_remote_url_idempotent():
1198
- once = _normalize_remote_url("https://user@github.com/Test/Repo.git")
1199
- assert _normalize_remote_url(once) == once
1200
-
1201
-
1202
- # ── _promote_specific dry-run ────────────────
1203
-
1204
- def test_promote_specific_dry_run_writes_nothing(patch_globals, capsys):
1205
- """dry_run returns 0, prints [DRY RUN], and writes no global file."""
1206
- tree = patch_globals
1207
- project = _make_project(tree)
1208
- (project["instincts_personal"] / "inst.yaml").write_text(SAMPLE_INSTINCT_YAML)
1209
-
1210
- ret = _promote_specific(project, "test-instinct", force=True, dry_run=True)
1211
- assert ret == 0
1212
- out = capsys.readouterr().out
1213
- assert "[DRY RUN]" in out
1214
- assert not (tree["global_personal"] / "test-instinct.yaml").exists()
1215
- assert list(tree["global_personal"].iterdir()) == []
1216
-
1217
-
1218
- # ── projects delete ──────────────────────────
1219
-
1220
- def test_projects_delete_rejects_invalid_id(patch_globals, capsys):
1221
- args = SimpleNamespace(project_id="../escape", dry_run=False, force=True)
1222
- assert _cmd_projects_delete(args) == 1
1223
- assert "Invalid project ID" in capsys.readouterr().err
1224
-
1225
-
1226
- def test_projects_delete_not_found(patch_globals, capsys):
1227
- args = SimpleNamespace(project_id="ghost123", dry_run=False, force=True)
1228
- assert _cmd_projects_delete(args) == 1
1229
- assert "not found" in capsys.readouterr().err
1230
-
1231
-
1232
- def test_projects_delete_dry_run_keeps_registry_and_storage(patch_globals, capsys):
1233
- tree = patch_globals
1234
- _make_project(tree, pid="proj1", pname="p1")
1235
- tree["registry_file"].write_text(json.dumps({"proj1": {"name": "p1"}}))
1236
-
1237
- args = SimpleNamespace(project_id="proj1", dry_run=True, force=False)
1238
- assert _cmd_projects_delete(args) == 0
1239
- assert "[DRY RUN]" in capsys.readouterr().out
1240
- assert (tree["projects_dir"] / "proj1").exists()
1241
- assert "proj1" in json.loads(tree["registry_file"].read_text())
1242
-
1243
-
1244
- def test_projects_delete_force_removes_registry_and_storage(patch_globals, capsys):
1245
- tree = patch_globals
1246
- _make_project(tree, pid="proj1", pname="p1")
1247
- tree["registry_file"].write_text(json.dumps({"proj1": {"name": "p1"}}))
1248
-
1249
- args = SimpleNamespace(project_id="proj1", dry_run=False, force=True)
1250
- assert _cmd_projects_delete(args) == 0
1251
- assert "Deleted project" in capsys.readouterr().out
1252
- assert not (tree["projects_dir"] / "proj1").exists()
1253
- assert "proj1" not in json.loads(tree["registry_file"].read_text())
1254
-
1255
-
1256
- # ── projects gc ──────────────────────────────
1257
-
1258
- def test_projects_gc_no_candidates(patch_globals, capsys):
1259
- tree = patch_globals
1260
- tree["registry_file"].write_text("{}")
1261
- args = SimpleNamespace(dry_run=False, force=True)
1262
- assert _cmd_projects_gc(args) == 0
1263
- assert "No zero-value project entries" in capsys.readouterr().out
1264
-
1265
-
1266
- def test_projects_gc_dry_run_keeps_entry(patch_globals, capsys):
1267
- tree = patch_globals
1268
- _make_project(tree, pid="empty1", pname="e1") # zero instincts/observations
1269
- tree["registry_file"].write_text(json.dumps({"empty1": {"name": "e1"}}))
1270
-
1271
- args = SimpleNamespace(dry_run=True, force=False)
1272
- assert _cmd_projects_gc(args) == 0
1273
- assert "[DRY RUN]" in capsys.readouterr().out
1274
- assert "empty1" in json.loads(tree["registry_file"].read_text())
1275
- # dry-run must not touch storage on disk
1276
- assert (tree["projects_dir"] / "empty1").exists()
1277
-
1278
-
1279
- def test_projects_gc_force_removes_only_zero_value(patch_globals, capsys):
1280
- tree = patch_globals
1281
- _make_project(tree, pid="empty1", pname="e1")
1282
- full = _make_project(tree, pid="full1", pname="f1")
1283
- (full["instincts_personal"] / "inst.yaml").write_text(SAMPLE_INSTINCT_YAML)
1284
- tree["registry_file"].write_text(
1285
- json.dumps({"empty1": {"name": "e1"}, "full1": {"name": "f1"}})
1286
- )
1287
-
1288
- args = SimpleNamespace(dry_run=False, force=True)
1289
- assert _cmd_projects_gc(args) == 0
1290
- reg = json.loads(tree["registry_file"].read_text())
1291
- assert "empty1" not in reg
1292
- assert "full1" in reg
1293
- assert not (tree["projects_dir"] / "empty1").exists()
1294
- assert (tree["projects_dir"] / "full1").exists()
1295
-
1296
-
1297
- # ── projects merge ───────────────────────────
1298
-
1299
- def test_projects_merge_rejects_same_id(patch_globals, capsys):
1300
- args = SimpleNamespace(from_id="dup", into_id="dup", dry_run=False, force=True)
1301
- assert _cmd_projects_merge(args) == 1
1302
- assert "into itself" in capsys.readouterr().err
1303
-
1304
-
1305
- def test_projects_merge_missing_source(patch_globals, capsys):
1306
- tree = patch_globals
1307
- tree["registry_file"].write_text(json.dumps({"dest": {"name": "d"}}))
1308
- args = SimpleNamespace(from_id="src", into_id="dest", dry_run=False, force=True)
1309
- assert _cmd_projects_merge(args) == 1
1310
- assert "Source project" in capsys.readouterr().err
1311
-
1312
-
1313
- def test_projects_merge_missing_destination(patch_globals, capsys):
1314
- tree = patch_globals
1315
- # Source present, destination absent — exercises the symmetric error branch.
1316
- tree["registry_file"].write_text(json.dumps({"src": {"name": "s"}}))
1317
- args = SimpleNamespace(from_id="src", into_id="dest", dry_run=False, force=True)
1318
- assert _cmd_projects_merge(args) == 1
1319
- assert "Destination project" in capsys.readouterr().err
1320
-
1321
-
1322
- def test_projects_merge_dry_run_no_changes(patch_globals, capsys):
1323
- tree = patch_globals
1324
- src = _make_project(tree, pid="src", pname="s")
1325
- _make_project(tree, pid="dest", pname="d")
1326
- (src["instincts_personal"] / "i.yaml").write_text(SAMPLE_INSTINCT_YAML)
1327
- tree["registry_file"].write_text(json.dumps({"src": {"name": "s"}, "dest": {"name": "d"}}))
1328
-
1329
- args = SimpleNamespace(from_id="src", into_id="dest", dry_run=True, force=False)
1330
- assert _cmd_projects_merge(args) == 0
1331
- assert "[DRY RUN]" in capsys.readouterr().out
1332
- reg = json.loads(tree["registry_file"].read_text())
1333
- assert "src" in reg and "dest" in reg
1334
- assert (tree["projects_dir"] / "src").exists()
1335
- # dry-run must not copy any instinct into the destination storage
1336
- assert not list((tree["projects_dir"] / "dest" / "instincts" / "personal").glob("*.yaml"))
1337
-
1338
-
1339
- def test_projects_merge_force_moves_and_removes_source(patch_globals, capsys):
1340
- tree = patch_globals
1341
- src = _make_project(tree, pid="src", pname="s")
1342
- _make_project(tree, pid="dest", pname="d")
1343
- (src["instincts_personal"] / "i.yaml").write_text(SAMPLE_INSTINCT_YAML)
1344
- tree["registry_file"].write_text(json.dumps({"src": {"name": "s"}, "dest": {"name": "d"}}))
1345
-
1346
- args = SimpleNamespace(from_id="src", into_id="dest", dry_run=False, force=True)
1347
- assert _cmd_projects_merge(args) == 0
1348
- reg = json.loads(tree["registry_file"].read_text())
1349
- assert "src" not in reg
1350
- assert "dest" in reg
1351
- assert not (tree["projects_dir"] / "src").exists()
1352
- moved = list((tree["projects_dir"] / "dest" / "instincts" / "personal").glob("*.yaml"))
1353
- assert len(moved) >= 1
1354
-
1355
-
1356
- # ── cmd_prune ────────────────────────────────
1357
-
1358
- def _pending_item(path, age_days):
1359
- return {
1360
- "path": path,
1361
- "created": None,
1362
- "age_days": age_days,
1363
- "name": path.stem,
1364
- "parent_dir": str(path.parent),
1365
- }
1366
-
1367
-
1368
- def test_cmd_prune_dry_run_keeps_files(monkeypatch, tmp_path, capsys):
1369
- f_old = tmp_path / "old.yaml"
1370
- f_old.write_text("x", encoding="utf-8")
1371
- f_new = tmp_path / "new.yaml"
1372
- f_new.write_text("y", encoding="utf-8")
1373
- items = [_pending_item(f_old, 40), _pending_item(f_new, 5)]
1374
- monkeypatch.setattr(_mod, "_collect_pending_instincts", lambda: items)
1375
-
1376
- args = SimpleNamespace(max_age=30, dry_run=True, quiet=False)
1377
- assert cmd_prune(args) == 0
1378
- assert "[DRY RUN]" in capsys.readouterr().out
1379
- assert f_old.exists()
1380
- assert f_new.exists()
1381
-
1382
-
1383
- def test_cmd_prune_deletes_only_expired(monkeypatch, tmp_path, capsys):
1384
- f_old = tmp_path / "old.yaml"
1385
- f_old.write_text("x", encoding="utf-8")
1386
- f_new = tmp_path / "new.yaml"
1387
- f_new.write_text("y", encoding="utf-8")
1388
- items = [_pending_item(f_old, 40), _pending_item(f_new, 5)]
1389
- monkeypatch.setattr(_mod, "_collect_pending_instincts", lambda: items)
1390
-
1391
- args = SimpleNamespace(max_age=30, dry_run=False, quiet=False)
1392
- assert cmd_prune(args) == 0
1393
- assert not f_old.exists()
1394
- assert f_new.exists()
1395
- assert "Pruned 1" in capsys.readouterr().out
1396
-
1397
-
1398
- def test_cmd_prune_quiet_suppresses_output(monkeypatch, tmp_path, capsys):
1399
- f_old = tmp_path / "old.yaml"
1400
- f_old.write_text("x", encoding="utf-8")
1401
- items = [_pending_item(f_old, 99)]
1402
- monkeypatch.setattr(_mod, "_collect_pending_instincts", lambda: items)
1403
-
1404
- args = SimpleNamespace(max_age=30, dry_run=False, quiet=True)
1405
- assert cmd_prune(args) == 0
1406
- assert not f_old.exists()
1407
- captured = capsys.readouterr()
1408
- assert captured.out == ""
1409
- assert captured.err == ""
1410
-
1411
-
1412
- def test_cmd_prune_empty_pending_nothing_to_do(monkeypatch, capsys):
1413
- # Nothing pending at all: the non-dry-run, non-quiet branch must report
1414
- # "nothing to do" (not "[DRY RUN]"), return 0, and not crash.
1415
- monkeypatch.setattr(_mod, "_collect_pending_instincts", lambda: [])
1416
-
1417
- args = SimpleNamespace(max_age=30, dry_run=False, quiet=False)
1418
- assert cmd_prune(args) == 0
1419
- out = capsys.readouterr().out
1420
- assert "No pending instincts older than 30 days." in out
1421
- assert "[DRY RUN]" not in out