arboris-cli 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (451) hide show
  1. package/dist/cli.mjs +382 -0
  2. package/manifest.json +323 -0
  3. package/package.json +22 -10
  4. package/prisma/skills/accessibility/SKILL.md +147 -0
  5. package/prisma/skills/agent-architecture-audit/SKILL.md +257 -0
  6. package/prisma/skills/agent-eval/SKILL.md +146 -0
  7. package/prisma/skills/agent-harness-construction/SKILL.md +74 -0
  8. package/prisma/skills/agent-introspection-debugging/SKILL.md +154 -0
  9. package/prisma/skills/agent-payment-x402/SKILL.md +225 -0
  10. package/prisma/skills/agent-self-evaluation/SKILL.md +182 -0
  11. package/prisma/skills/agent-self-evaluation/examples/high-score-example.md +87 -0
  12. package/prisma/skills/agent-self-evaluation/examples/low-score-example.md +86 -0
  13. package/prisma/skills/agent-self-evaluation/references/evaluation-criteria.md +71 -0
  14. package/prisma/skills/agent-self-evaluation/references/hook-integration.md +64 -0
  15. package/prisma/skills/agent-self-evaluation/scripts/evaluate.py +408 -0
  16. package/prisma/skills/agent-self-evaluation/templates/evaluation-report.md +86 -0
  17. package/prisma/skills/agent-sort/SKILL.md +216 -0
  18. package/prisma/skills/agentic-engineering/SKILL.md +64 -0
  19. package/prisma/skills/agentic-os/SKILL.md +388 -0
  20. package/prisma/skills/ai-first-engineering/SKILL.md +52 -0
  21. package/prisma/skills/ai-regression-testing/SKILL.md +386 -0
  22. package/prisma/skills/android-clean-architecture/SKILL.md +340 -0
  23. package/prisma/skills/angular-developer/SKILL.md +155 -0
  24. package/prisma/skills/angular-developer/references/angular-animations.md +160 -0
  25. package/prisma/skills/angular-developer/references/angular-aria.md +410 -0
  26. package/prisma/skills/angular-developer/references/cli.md +86 -0
  27. package/prisma/skills/angular-developer/references/component-harnesses.md +59 -0
  28. package/prisma/skills/angular-developer/references/component-styling.md +91 -0
  29. package/prisma/skills/angular-developer/references/components.md +117 -0
  30. package/prisma/skills/angular-developer/references/creating-services.md +97 -0
  31. package/prisma/skills/angular-developer/references/data-resolvers.md +69 -0
  32. package/prisma/skills/angular-developer/references/define-routes.md +67 -0
  33. package/prisma/skills/angular-developer/references/defining-providers.md +72 -0
  34. package/prisma/skills/angular-developer/references/di-fundamentals.md +120 -0
  35. package/prisma/skills/angular-developer/references/e2e-testing.md +56 -0
  36. package/prisma/skills/angular-developer/references/effects.md +83 -0
  37. package/prisma/skills/angular-developer/references/hierarchical-injectors.md +43 -0
  38. package/prisma/skills/angular-developer/references/host-elements.md +80 -0
  39. package/prisma/skills/angular-developer/references/injection-context.md +63 -0
  40. package/prisma/skills/angular-developer/references/inputs.md +101 -0
  41. package/prisma/skills/angular-developer/references/linked-signal.md +59 -0
  42. package/prisma/skills/angular-developer/references/loading-strategies.md +61 -0
  43. package/prisma/skills/angular-developer/references/mcp.md +108 -0
  44. package/prisma/skills/angular-developer/references/navigate-to-routes.md +69 -0
  45. package/prisma/skills/angular-developer/references/outputs.md +86 -0
  46. package/prisma/skills/angular-developer/references/reactive-forms.md +122 -0
  47. package/prisma/skills/angular-developer/references/rendering-strategies.md +44 -0
  48. package/prisma/skills/angular-developer/references/resource.md +77 -0
  49. package/prisma/skills/angular-developer/references/route-animations.md +56 -0
  50. package/prisma/skills/angular-developer/references/route-guards.md +52 -0
  51. package/prisma/skills/angular-developer/references/router-lifecycle.md +45 -0
  52. package/prisma/skills/angular-developer/references/router-testing.md +87 -0
  53. package/prisma/skills/angular-developer/references/show-routes-with-outlets.md +68 -0
  54. package/prisma/skills/angular-developer/references/signal-forms.md +795 -0
  55. package/prisma/skills/angular-developer/references/signals-overview.md +94 -0
  56. package/prisma/skills/angular-developer/references/tailwind-css.md +69 -0
  57. package/prisma/skills/angular-developer/references/template-driven-forms.md +114 -0
  58. package/prisma/skills/angular-developer/references/testing-fundamentals.md +65 -0
  59. package/prisma/skills/api-connector-builder/SKILL.md +121 -0
  60. package/prisma/skills/api-design/SKILL.md +524 -0
  61. package/prisma/skills/architecture-decision-records/SKILL.md +180 -0
  62. package/prisma/skills/article-writing/SKILL.md +80 -0
  63. package/prisma/skills/automation-audit-ops/SKILL.md +143 -0
  64. package/prisma/skills/autonomous-agent-harness/SKILL.md +274 -0
  65. package/prisma/skills/autonomous-loops/SKILL.md +611 -0
  66. package/prisma/skills/backend-patterns/SKILL.md +562 -0
  67. package/prisma/skills/benchmark/SKILL.md +94 -0
  68. package/prisma/skills/benchmark-methodology/SKILL.md +190 -0
  69. package/prisma/skills/benchmark-optimization-loop/SKILL.md +70 -0
  70. package/prisma/skills/blender-motion-state-inspection/SKILL.md +165 -0
  71. package/prisma/skills/blueprint/SKILL.md +106 -0
  72. package/prisma/skills/brand-discovery/SKILL.md +145 -0
  73. package/prisma/skills/brand-discovery/references/10_purpose-why.md +40 -0
  74. package/prisma/skills/brand-discovery/references/20_positioning.md +44 -0
  75. package/prisma/skills/brand-discovery/references/30_audience-niche.md +52 -0
  76. package/prisma/skills/brand-discovery/references/40_personality-archetype.md +57 -0
  77. package/prisma/skills/brand-discovery/references/50_voice-tone.md +59 -0
  78. package/prisma/skills/brand-discovery/references/60_narrative-story.md +50 -0
  79. package/prisma/skills/brand-discovery/references/70_founder-tension.md +49 -0
  80. package/prisma/skills/brand-discovery/references/90_SYNTHESIS.md +133 -0
  81. package/prisma/skills/brand-voice/SKILL.md +98 -0
  82. package/prisma/skills/brand-voice/references/voice-profile-schema.md +55 -0
  83. package/prisma/skills/browser-qa/SKILL.md +105 -0
  84. package/prisma/skills/bun-runtime/SKILL.md +85 -0
  85. package/prisma/skills/canary-watch/SKILL.md +108 -0
  86. package/prisma/skills/carrier-relationship-management/SKILL.md +212 -0
  87. package/prisma/skills/cisco-ios-patterns/SKILL.md +164 -0
  88. package/prisma/skills/ck/SKILL.md +148 -0
  89. package/prisma/skills/ck/commands/forget.mjs +44 -0
  90. package/prisma/skills/ck/commands/info.mjs +24 -0
  91. package/prisma/skills/ck/commands/init.mjs +143 -0
  92. package/prisma/skills/ck/commands/list.mjs +40 -0
  93. package/prisma/skills/ck/commands/migrate.mjs +202 -0
  94. package/prisma/skills/ck/commands/resume.mjs +36 -0
  95. package/prisma/skills/ck/commands/save.mjs +210 -0
  96. package/prisma/skills/ck/commands/shared.mjs +387 -0
  97. package/prisma/skills/ck/hooks/session-start.mjs +224 -0
  98. package/prisma/skills/claude-devfleet/SKILL.md +112 -0
  99. package/prisma/skills/click-path-audit/SKILL.md +245 -0
  100. package/prisma/skills/clickhouse-io/SKILL.md +440 -0
  101. package/prisma/skills/code-tour/SKILL.md +254 -0
  102. package/prisma/skills/codebase-onboarding/SKILL.md +234 -0
  103. package/prisma/skills/codehealth-mcp/SKILL.md +167 -0
  104. package/prisma/skills/coding-standards/SKILL.md +551 -0
  105. package/prisma/skills/competitive-platform-analysis/SKILL.md +214 -0
  106. package/prisma/skills/competitive-report-structure/SKILL.md +162 -0
  107. package/prisma/skills/compose-multiplatform-patterns/SKILL.md +300 -0
  108. package/prisma/skills/config-gc/SKILL.md +120 -0
  109. package/prisma/skills/configure-ecc/SKILL.md +385 -0
  110. package/prisma/skills/connections-optimizer/SKILL.md +190 -0
  111. package/prisma/skills/content-engine/SKILL.md +132 -0
  112. package/prisma/skills/content-hash-cache-pattern/SKILL.md +162 -0
  113. package/prisma/skills/context-budget/SKILL.md +136 -0
  114. package/prisma/skills/continuous-agent-loop/SKILL.md +46 -0
  115. package/prisma/skills/continuous-learning/SKILL.md +132 -0
  116. package/prisma/skills/continuous-learning/config.json +18 -0
  117. package/prisma/skills/continuous-learning/evaluate-session.sh +69 -0
  118. package/prisma/skills/continuous-learning-v2/SKILL.md +361 -0
  119. package/prisma/skills/continuous-learning-v2/agents/observer-loop.sh +359 -0
  120. package/prisma/skills/continuous-learning-v2/agents/observer.md +189 -0
  121. package/prisma/skills/continuous-learning-v2/agents/session-guardian.sh +150 -0
  122. package/prisma/skills/continuous-learning-v2/agents/start-observer.sh +248 -0
  123. package/prisma/skills/continuous-learning-v2/config.json +8 -0
  124. package/prisma/skills/continuous-learning-v2/hooks/observe.sh +585 -0
  125. package/prisma/skills/continuous-learning-v2/scripts/detect-project.sh +322 -0
  126. package/prisma/skills/continuous-learning-v2/scripts/instinct-cli.py +1956 -0
  127. package/prisma/skills/continuous-learning-v2/scripts/lib/homunculus-dir.sh +31 -0
  128. package/prisma/skills/continuous-learning-v2/scripts/migrate-homunculus.sh +68 -0
  129. package/prisma/skills/continuous-learning-v2/scripts/test_parse_instinct.py +1421 -0
  130. package/prisma/skills/cost-aware-llm-pipeline/SKILL.md +184 -0
  131. package/prisma/skills/cost-tracking/SKILL.md +97 -0
  132. package/prisma/skills/council/SKILL.md +204 -0
  133. package/prisma/skills/cpp-coding-standards/SKILL.md +724 -0
  134. package/prisma/skills/cpp-testing/SKILL.md +325 -0
  135. package/prisma/skills/crosspost/SKILL.md +112 -0
  136. package/prisma/skills/csharp-testing/SKILL.md +322 -0
  137. package/prisma/skills/customer-billing-ops/SKILL.md +141 -0
  138. package/prisma/skills/customs-trade-compliance/SKILL.md +263 -0
  139. package/prisma/skills/dart-flutter-patterns/SKILL.md +564 -0
  140. package/prisma/skills/dashboard-builder/SKILL.md +109 -0
  141. package/prisma/skills/data-scraper-agent/SKILL.md +765 -0
  142. package/prisma/skills/data-throughput-accelerator/SKILL.md +73 -0
  143. package/prisma/skills/database-migrations/SKILL.md +430 -0
  144. package/prisma/skills/deep-research/SKILL.md +160 -0
  145. package/prisma/skills/defi-amm-security/SKILL.md +167 -0
  146. package/prisma/skills/delivery-gate/SKILL.md +126 -0
  147. package/prisma/skills/delivery-gate/hooks/quality-gate.py +220 -0
  148. package/prisma/skills/deployment-patterns/SKILL.md +428 -0
  149. package/prisma/skills/design-system/SKILL.md +83 -0
  150. package/prisma/skills/django-celery/SKILL.md +458 -0
  151. package/prisma/skills/django-patterns/SKILL.md +735 -0
  152. package/prisma/skills/django-security/SKILL.md +644 -0
  153. package/prisma/skills/django-tdd/SKILL.md +730 -0
  154. package/prisma/skills/django-verification/SKILL.md +470 -0
  155. package/prisma/skills/dmux-workflows/SKILL.md +192 -0
  156. package/prisma/skills/docker-patterns/SKILL.md +365 -0
  157. package/prisma/skills/documentation-lookup/SKILL.md +91 -0
  158. package/prisma/skills/dotnet-patterns/SKILL.md +322 -0
  159. package/prisma/skills/dynamic-workflow-mode/SKILL.md +124 -0
  160. package/prisma/skills/e2e-testing/SKILL.md +327 -0
  161. package/prisma/skills/ecc-guide/SKILL.md +190 -0
  162. package/prisma/skills/ecc-recipes/SKILL.md +149 -0
  163. package/prisma/skills/ecc-tools-cost-audit/SKILL.md +161 -0
  164. package/prisma/skills/email-ops/SKILL.md +122 -0
  165. package/prisma/skills/energy-procurement/SKILL.md +228 -0
  166. package/prisma/skills/enterprise-agent-ops/SKILL.md +51 -0
  167. package/prisma/skills/error-handling/SKILL.md +377 -0
  168. package/prisma/skills/eval-harness/SKILL.md +271 -0
  169. package/prisma/skills/evm-token-decimals/SKILL.md +131 -0
  170. package/prisma/skills/exa-search/SKILL.md +108 -0
  171. package/prisma/skills/fal-ai-media/SKILL.md +289 -0
  172. package/prisma/skills/fastapi-patterns/SKILL.md +514 -0
  173. package/prisma/skills/finance-billing-ops/SKILL.md +128 -0
  174. package/prisma/skills/flox-environments/SKILL.md +497 -0
  175. package/prisma/skills/flutter-dart-code-review/SKILL.md +436 -0
  176. package/prisma/skills/foundation-models-on-device/SKILL.md +243 -0
  177. package/prisma/skills/frontend-a11y/SKILL.md +446 -0
  178. package/prisma/skills/frontend-design-direction/SKILL.md +93 -0
  179. package/prisma/skills/frontend-patterns/SKILL.md +657 -0
  180. package/prisma/skills/frontend-slides/SKILL.md +185 -0
  181. package/prisma/skills/frontend-slides/STYLE_PRESETS.md +330 -0
  182. package/prisma/skills/frontend-slides/animation-patterns.md +122 -0
  183. package/prisma/skills/frontend-slides/html-template.md +419 -0
  184. package/prisma/skills/frontend-slides/scripts/export-pdf.sh +418 -0
  185. package/prisma/skills/frontend-slides/scripts/extract-pptx.py +96 -0
  186. package/prisma/skills/frontend-slides/viewport-base.css +153 -0
  187. package/prisma/skills/fsharp-testing/SKILL.md +281 -0
  188. package/prisma/skills/gan-style-harness/SKILL.md +279 -0
  189. package/prisma/skills/gateguard/SKILL.md +133 -0
  190. package/prisma/skills/generating-python-installer/SKILL.md +820 -0
  191. package/prisma/skills/git-workflow/SKILL.md +716 -0
  192. package/prisma/skills/github-ops/SKILL.md +145 -0
  193. package/prisma/skills/golang-patterns/SKILL.md +675 -0
  194. package/prisma/skills/golang-testing/SKILL.md +721 -0
  195. package/prisma/skills/google-workspace-ops/SKILL.md +96 -0
  196. package/prisma/skills/growth-log/SKILL.md +128 -0
  197. package/prisma/skills/healthcare-cdss-patterns/SKILL.md +246 -0
  198. package/prisma/skills/healthcare-emr-patterns/SKILL.md +160 -0
  199. package/prisma/skills/healthcare-eval-harness/SKILL.md +208 -0
  200. package/prisma/skills/healthcare-phi-compliance/SKILL.md +146 -0
  201. package/prisma/skills/hermes-imports/SKILL.md +89 -0
  202. package/prisma/skills/hexagonal-architecture/SKILL.md +277 -0
  203. package/prisma/skills/hipaa-compliance/SKILL.md +79 -0
  204. package/prisma/skills/homelab-network-readiness/SKILL.md +170 -0
  205. package/prisma/skills/homelab-network-setup/SKILL.md +130 -0
  206. package/prisma/skills/homelab-pihole-dns/SKILL.md +275 -0
  207. package/prisma/skills/homelab-vlan-segmentation/SKILL.md +312 -0
  208. package/prisma/skills/homelab-wireguard-vpn/SKILL.md +306 -0
  209. package/prisma/skills/hookify-rules/SKILL.md +128 -0
  210. package/prisma/skills/inherit-legacy-style/SKILL.md +157 -0
  211. package/prisma/skills/intent-driven-development/SKILL.md +360 -0
  212. package/prisma/skills/inventory-demand-planning/SKILL.md +247 -0
  213. package/prisma/skills/investor-materials/SKILL.md +97 -0
  214. package/prisma/skills/investor-outreach/SKILL.md +92 -0
  215. package/prisma/skills/ios-icon-gen/SKILL.md +158 -0
  216. package/prisma/skills/ios-icon-gen/scripts/generate_icons.swift +258 -0
  217. package/prisma/skills/ios-icon-gen/scripts/iconify_gen.sh +235 -0
  218. package/prisma/skills/iterative-retrieval/SKILL.md +212 -0
  219. package/prisma/skills/ito-basket-compare/SKILL.md +64 -0
  220. package/prisma/skills/ito-data-atlas-agent/SKILL.md +64 -0
  221. package/prisma/skills/ito-market-intelligence/SKILL.md +61 -0
  222. package/prisma/skills/ito-trade-planner/SKILL.md +68 -0
  223. package/prisma/skills/java-coding-standards/SKILL.md +384 -0
  224. package/prisma/skills/jira-integration/SKILL.md +303 -0
  225. package/prisma/skills/jpa-patterns/SKILL.md +152 -0
  226. package/prisma/skills/knowledge-ops/SKILL.md +155 -0
  227. package/prisma/skills/kotlin-coroutines-flows/SKILL.md +285 -0
  228. package/prisma/skills/kotlin-exposed-patterns/SKILL.md +720 -0
  229. package/prisma/skills/kotlin-ktor-patterns/SKILL.md +690 -0
  230. package/prisma/skills/kotlin-patterns/SKILL.md +712 -0
  231. package/prisma/skills/kotlin-testing/SKILL.md +825 -0
  232. package/prisma/skills/kubernetes-patterns/SKILL.md +756 -0
  233. package/prisma/skills/laravel-patterns/SKILL.md +416 -0
  234. package/prisma/skills/laravel-plugin-discovery/SKILL.md +230 -0
  235. package/prisma/skills/laravel-security/SKILL.md +948 -0
  236. package/prisma/skills/laravel-tdd/SKILL.md +675 -0
  237. package/prisma/skills/laravel-verification/SKILL.md +180 -0
  238. package/prisma/skills/latency-critical-systems/SKILL.md +74 -0
  239. package/prisma/skills/lead-intelligence/SKILL.md +322 -0
  240. package/prisma/skills/lead-intelligence/agents/enrichment-agent.md +85 -0
  241. package/prisma/skills/lead-intelligence/agents/mutual-mapper.md +75 -0
  242. package/prisma/skills/lead-intelligence/agents/outreach-drafter.md +98 -0
  243. package/prisma/skills/lead-intelligence/agents/signal-scorer.md +60 -0
  244. package/prisma/skills/liquid-glass-design/SKILL.md +279 -0
  245. package/prisma/skills/llm-trading-agent-security/SKILL.md +147 -0
  246. package/prisma/skills/logistics-exception-management/SKILL.md +222 -0
  247. package/prisma/skills/loop-design-check/SKILL.md +143 -0
  248. package/prisma/skills/mailtrap-email-integration/SKILL.md +77 -0
  249. package/prisma/skills/make-interfaces-feel-better/SKILL.md +152 -0
  250. package/prisma/skills/manim-video/SKILL.md +90 -0
  251. package/prisma/skills/manim-video/assets/network_graph_scene.py +52 -0
  252. package/prisma/skills/market-research/SKILL.md +76 -0
  253. package/prisma/skills/marketing-campaign/SKILL.md +114 -0
  254. package/prisma/skills/mcp-server-patterns/SKILL.md +70 -0
  255. package/prisma/skills/messages-ops/SKILL.md +105 -0
  256. package/prisma/skills/ml-adoption-playbook/SKILL.md +57 -0
  257. package/prisma/skills/mle-workflow/SKILL.md +347 -0
  258. package/prisma/skills/motion-advanced/SKILL.md +596 -0
  259. package/prisma/skills/motion-foundations/SKILL.md +299 -0
  260. package/prisma/skills/motion-patterns/SKILL.md +434 -0
  261. package/prisma/skills/motion-ui/SKILL.md +576 -0
  262. package/prisma/skills/mysql-patterns/SKILL.md +413 -0
  263. package/prisma/skills/nanoclaw-repl/SKILL.md +34 -0
  264. package/prisma/skills/nestjs-patterns/SKILL.md +231 -0
  265. package/prisma/skills/netmiko-ssh-automation/SKILL.md +174 -0
  266. package/prisma/skills/network-bgp-diagnostics/SKILL.md +168 -0
  267. package/prisma/skills/network-config-validation/SKILL.md +211 -0
  268. package/prisma/skills/network-interface-health/SKILL.md +153 -0
  269. package/prisma/skills/nextjs-turbopack/SKILL.md +58 -0
  270. package/prisma/skills/nodejs-keccak256/SKILL.md +103 -0
  271. package/prisma/skills/nutrient-document-processing/SKILL.md +168 -0
  272. package/prisma/skills/nuxt4-patterns/SKILL.md +101 -0
  273. package/prisma/skills/openclaw-persona-forge/SKILL.md +289 -0
  274. package/prisma/skills/openclaw-persona-forge/gacha.py +224 -0
  275. package/prisma/skills/openclaw-persona-forge/gacha.sh +5 -0
  276. package/prisma/skills/openclaw-persona-forge/references/avatar-style.md +124 -0
  277. package/prisma/skills/openclaw-persona-forge/references/boundary-rules.md +53 -0
  278. package/prisma/skills/openclaw-persona-forge/references/error-handling.md +53 -0
  279. package/prisma/skills/openclaw-persona-forge/references/identity-tension.md +48 -0
  280. package/prisma/skills/openclaw-persona-forge/references/naming-system.md +39 -0
  281. package/prisma/skills/openclaw-persona-forge/references/output-template.md +166 -0
  282. package/prisma/skills/opensource-pipeline/SKILL.md +256 -0
  283. package/prisma/skills/orch-add-feature/SKILL.md +45 -0
  284. package/prisma/skills/orch-build-mvp/SKILL.md +49 -0
  285. package/prisma/skills/orch-change-feature/SKILL.md +43 -0
  286. package/prisma/skills/orch-fix-defect/SKILL.md +43 -0
  287. package/prisma/skills/orch-pipeline/SKILL.md +121 -0
  288. package/prisma/skills/orch-refine-code/SKILL.md +44 -0
  289. package/prisma/skills/parallel-execution-optimizer/SKILL.md +73 -0
  290. package/prisma/skills/perl-patterns/SKILL.md +505 -0
  291. package/prisma/skills/perl-security/SKILL.md +504 -0
  292. package/prisma/skills/perl-testing/SKILL.md +476 -0
  293. package/prisma/skills/plan-orchestrate/SKILL.md +263 -0
  294. package/prisma/skills/plankton-code-quality/SKILL.md +237 -0
  295. package/prisma/skills/postgres-patterns/SKILL.md +148 -0
  296. package/prisma/skills/prediction-market-oracle-research/SKILL.md +64 -0
  297. package/prisma/skills/prediction-market-risk-review/SKILL.md +61 -0
  298. package/prisma/skills/prisma-patterns/SKILL.md +401 -0
  299. package/prisma/skills/product-capability/SKILL.md +142 -0
  300. package/prisma/skills/product-lens/SKILL.md +93 -0
  301. package/prisma/skills/production-audit/SKILL.md +207 -0
  302. package/prisma/skills/production-scheduling/SKILL.md +238 -0
  303. package/prisma/skills/project-flow-ops/SKILL.md +112 -0
  304. package/prisma/skills/prompt-optimizer/SKILL.md +398 -0
  305. package/prisma/skills/python-patterns/SKILL.md +751 -0
  306. package/prisma/skills/python-testing/SKILL.md +817 -0
  307. package/prisma/skills/pytorch-patterns/SKILL.md +397 -0
  308. package/prisma/skills/quality-nonconformance/SKILL.md +260 -0
  309. package/prisma/skills/quarkus-patterns/SKILL.md +723 -0
  310. package/prisma/skills/quarkus-security/SKILL.md +468 -0
  311. package/prisma/skills/quarkus-tdd/SKILL.md +812 -0
  312. package/prisma/skills/quarkus-verification/SKILL.md +480 -0
  313. package/prisma/skills/ralphinho-rfc-pipeline/SKILL.md +68 -0
  314. package/prisma/skills/react-native-patterns/SKILL.md +326 -0
  315. package/prisma/skills/react-patterns/SKILL.md +342 -0
  316. package/prisma/skills/react-performance/SKILL.md +575 -0
  317. package/prisma/skills/react-testing/SKILL.md +424 -0
  318. package/prisma/skills/recsys-pipeline-architect/SKILL.md +115 -0
  319. package/prisma/skills/recursive-decision-ledger/SKILL.md +80 -0
  320. package/prisma/skills/redis-patterns/SKILL.md +404 -0
  321. package/prisma/skills/regex-vs-llm-structured-text/SKILL.md +221 -0
  322. package/prisma/skills/remotion-video-creation/SKILL.md +43 -0
  323. package/prisma/skills/remotion-video-creation/rules/3d.md +86 -0
  324. package/prisma/skills/remotion-video-creation/rules/animations.md +29 -0
  325. package/prisma/skills/remotion-video-creation/rules/assets/charts-bar-chart.tsx +173 -0
  326. package/prisma/skills/remotion-video-creation/rules/assets/text-animations-typewriter.tsx +100 -0
  327. package/prisma/skills/remotion-video-creation/rules/assets/text-animations-word-highlight.tsx +108 -0
  328. package/prisma/skills/remotion-video-creation/rules/assets.md +78 -0
  329. package/prisma/skills/remotion-video-creation/rules/audio.md +172 -0
  330. package/prisma/skills/remotion-video-creation/rules/calculate-metadata.md +104 -0
  331. package/prisma/skills/remotion-video-creation/rules/can-decode.md +75 -0
  332. package/prisma/skills/remotion-video-creation/rules/charts.md +58 -0
  333. package/prisma/skills/remotion-video-creation/rules/compositions.md +146 -0
  334. package/prisma/skills/remotion-video-creation/rules/display-captions.md +126 -0
  335. package/prisma/skills/remotion-video-creation/rules/extract-frames.md +229 -0
  336. package/prisma/skills/remotion-video-creation/rules/fonts.md +152 -0
  337. package/prisma/skills/remotion-video-creation/rules/get-audio-duration.md +58 -0
  338. package/prisma/skills/remotion-video-creation/rules/get-video-dimensions.md +68 -0
  339. package/prisma/skills/remotion-video-creation/rules/get-video-duration.md +58 -0
  340. package/prisma/skills/remotion-video-creation/rules/gifs.md +138 -0
  341. package/prisma/skills/remotion-video-creation/rules/images.md +130 -0
  342. package/prisma/skills/remotion-video-creation/rules/import-srt-captions.md +67 -0
  343. package/prisma/skills/remotion-video-creation/rules/lottie.md +67 -0
  344. package/prisma/skills/remotion-video-creation/rules/measuring-dom-nodes.md +34 -0
  345. package/prisma/skills/remotion-video-creation/rules/measuring-text.md +143 -0
  346. package/prisma/skills/remotion-video-creation/rules/sequencing.md +106 -0
  347. package/prisma/skills/remotion-video-creation/rules/tailwind.md +11 -0
  348. package/prisma/skills/remotion-video-creation/rules/text-animations.md +20 -0
  349. package/prisma/skills/remotion-video-creation/rules/timing.md +179 -0
  350. package/prisma/skills/remotion-video-creation/rules/transcribe-captions.md +19 -0
  351. package/prisma/skills/remotion-video-creation/rules/transitions.md +122 -0
  352. package/prisma/skills/remotion-video-creation/rules/trimming.md +52 -0
  353. package/prisma/skills/remotion-video-creation/rules/videos.md +171 -0
  354. package/prisma/skills/repo-scan/SKILL.md +79 -0
  355. package/prisma/skills/research-ops/SKILL.md +113 -0
  356. package/prisma/skills/returns-reverse-logistics/SKILL.md +240 -0
  357. package/prisma/skills/rules-distill/SKILL.md +265 -0
  358. package/prisma/skills/rules-distill/scripts/scan-rules.sh +58 -0
  359. package/prisma/skills/rules-distill/scripts/scan-skills.sh +129 -0
  360. package/prisma/skills/rust-patterns/SKILL.md +500 -0
  361. package/prisma/skills/rust-testing/SKILL.md +501 -0
  362. package/prisma/skills/safety-guard/SKILL.md +76 -0
  363. package/prisma/skills/santa-method/SKILL.md +307 -0
  364. package/prisma/skills/scientific-db-pubmed-database/SKILL.md +176 -0
  365. package/prisma/skills/scientific-db-uspto-database/SKILL.md +178 -0
  366. package/prisma/skills/scientific-pkg-gget/SKILL.md +167 -0
  367. package/prisma/skills/scientific-thinking-literature-review/SKILL.md +193 -0
  368. package/prisma/skills/scientific-thinking-scholar-evaluation/SKILL.md +161 -0
  369. package/prisma/skills/search-first/SKILL.md +183 -0
  370. package/prisma/skills/security-bounty-hunter/SKILL.md +100 -0
  371. package/prisma/skills/security-review/SKILL.md +504 -0
  372. package/prisma/skills/security-review/cloud-infrastructure-security.md +361 -0
  373. package/prisma/skills/security-scan/SKILL.md +166 -0
  374. package/prisma/skills/seo/SKILL.md +155 -0
  375. package/prisma/skills/skill-comply/SKILL.md +59 -0
  376. package/prisma/skills/skill-comply/fixtures/compliant_trace.jsonl +5 -0
  377. package/prisma/skills/skill-comply/fixtures/noncompliant_trace.jsonl +3 -0
  378. package/prisma/skills/skill-comply/fixtures/tdd_spec.yaml +44 -0
  379. package/prisma/skills/skill-comply/prompts/classifier.md +24 -0
  380. package/prisma/skills/skill-comply/prompts/scenario_generator.md +62 -0
  381. package/prisma/skills/skill-comply/prompts/spec_generator.md +42 -0
  382. package/prisma/skills/skill-comply/pyproject.toml +15 -0
  383. package/prisma/skills/skill-comply/scripts/__init__.py +0 -0
  384. package/prisma/skills/skill-comply/scripts/classifier.py +85 -0
  385. package/prisma/skills/skill-comply/scripts/grader.py +124 -0
  386. package/prisma/skills/skill-comply/scripts/parser.py +107 -0
  387. package/prisma/skills/skill-comply/scripts/report.py +170 -0
  388. package/prisma/skills/skill-comply/scripts/run.py +127 -0
  389. package/prisma/skills/skill-comply/scripts/runner.py +194 -0
  390. package/prisma/skills/skill-comply/scripts/scenario_generator.py +70 -0
  391. package/prisma/skills/skill-comply/scripts/spec_generator.py +72 -0
  392. package/prisma/skills/skill-comply/scripts/utils.py +13 -0
  393. package/prisma/skills/skill-comply/tests/test_grader.py +197 -0
  394. package/prisma/skills/skill-comply/tests/test_parser.py +90 -0
  395. package/prisma/skills/skill-comply/tests/test_runner.py +172 -0
  396. package/prisma/skills/skill-scout/SKILL.md +141 -0
  397. package/prisma/skills/skill-stocktake/SKILL.md +195 -0
  398. package/prisma/skills/skill-stocktake/scripts/quick-diff.sh +87 -0
  399. package/prisma/skills/skill-stocktake/scripts/save-results.sh +56 -0
  400. package/prisma/skills/skill-stocktake/scripts/scan.sh +170 -0
  401. package/prisma/skills/social-graph-ranker/SKILL.md +155 -0
  402. package/prisma/skills/social-publisher/SKILL.md +130 -0
  403. package/prisma/skills/springboot-patterns/SKILL.md +315 -0
  404. package/prisma/skills/springboot-security/SKILL.md +273 -0
  405. package/prisma/skills/springboot-tdd/SKILL.md +159 -0
  406. package/prisma/skills/springboot-verification/SKILL.md +232 -0
  407. package/prisma/skills/strategic-compact/SKILL.md +136 -0
  408. package/prisma/skills/swift-actor-persistence/SKILL.md +144 -0
  409. package/prisma/skills/swift-concurrency-6-2/SKILL.md +216 -0
  410. package/prisma/skills/swift-protocol-di-testing/SKILL.md +191 -0
  411. package/prisma/skills/swiftui-patterns/SKILL.md +259 -0
  412. package/prisma/skills/taste/SKILL.md +264 -0
  413. package/prisma/skills/taste/references/genre-taxonomy.md +87 -0
  414. package/prisma/skills/tdd-workflow/SKILL.md +583 -0
  415. package/prisma/skills/team-agent-orchestration/SKILL.md +111 -0
  416. package/prisma/skills/team-builder/SKILL.md +169 -0
  417. package/prisma/skills/terminal-ops/SKILL.md +110 -0
  418. package/prisma/skills/tinystruct-patterns/SKILL.md +279 -0
  419. package/prisma/skills/tinystruct-patterns/references/architecture.md +90 -0
  420. package/prisma/skills/tinystruct-patterns/references/data-handling.md +60 -0
  421. package/prisma/skills/tinystruct-patterns/references/database.md +99 -0
  422. package/prisma/skills/tinystruct-patterns/references/routing.md +64 -0
  423. package/prisma/skills/tinystruct-patterns/references/system-usage.md +97 -0
  424. package/prisma/skills/tinystruct-patterns/references/testing.md +72 -0
  425. package/prisma/skills/token-budget-advisor/SKILL.md +134 -0
  426. package/prisma/skills/ui-demo/SKILL.md +466 -0
  427. package/prisma/skills/ui-to-vue/SKILL.md +135 -0
  428. package/prisma/skills/uncloud/SKILL.md +344 -0
  429. package/prisma/skills/unified-notifications-ops/SKILL.md +188 -0
  430. package/prisma/skills/verification-loop/SKILL.md +127 -0
  431. package/prisma/skills/video-editing/SKILL.md +311 -0
  432. package/prisma/skills/videodb/SKILL.md +375 -0
  433. package/prisma/skills/videodb/reference/api-reference.md +550 -0
  434. package/prisma/skills/videodb/reference/capture-reference.md +407 -0
  435. package/prisma/skills/videodb/reference/capture.md +101 -0
  436. package/prisma/skills/videodb/reference/editor.md +443 -0
  437. package/prisma/skills/videodb/reference/generative.md +331 -0
  438. package/prisma/skills/videodb/reference/rtstream-reference.md +564 -0
  439. package/prisma/skills/videodb/reference/rtstream.md +65 -0
  440. package/prisma/skills/videodb/reference/search.md +230 -0
  441. package/prisma/skills/videodb/reference/streaming.md +406 -0
  442. package/prisma/skills/videodb/reference/use-cases.md +118 -0
  443. package/prisma/skills/videodb/scripts/ws_listener.py +282 -0
  444. package/prisma/skills/visa-doc-translate/README.md +86 -0
  445. package/prisma/skills/visa-doc-translate/SKILL.md +117 -0
  446. package/prisma/skills/vite-patterns/SKILL.md +450 -0
  447. package/prisma/skills/vue-patterns/SKILL.md +471 -0
  448. package/prisma/skills/windows-desktop-e2e/SKILL.md +888 -0
  449. package/prisma/skills/workspace-surface-audit/SKILL.md +126 -0
  450. package/prisma/skills/x-api/SKILL.md +235 -0
  451. package/run.mjs +0 -10
@@ -0,0 +1,1421 @@
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