@heyai-rules/pilo-masterkit 2.1.0 → 3.1.0

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