anima-core 0.3.0 → 1.0.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 (269) hide show
  1. checksums.yaml +4 -4
  2. data/.reek.yml +27 -1
  3. data/CHANGELOG.md +4 -0
  4. data/README.md +219 -25
  5. data/agents/codebase-analyzer.md +88 -0
  6. data/agents/codebase-pattern-finder.md +83 -0
  7. data/agents/documentation-researcher.md +59 -0
  8. data/agents/thoughts-analyzer.md +102 -0
  9. data/agents/web-search-researcher.md +71 -0
  10. data/anima-core.gemspec +3 -0
  11. data/app/channels/session_channel.rb +76 -28
  12. data/app/jobs/agent_request_job.rb +24 -0
  13. data/app/jobs/analytical_brain_job.rb +33 -0
  14. data/app/jobs/count_event_tokens_job.rb +1 -1
  15. data/app/models/concerns/event/broadcasting.rb +20 -2
  16. data/app/models/event.rb +1 -1
  17. data/app/models/goal.rb +91 -0
  18. data/app/models/session.rb +347 -22
  19. data/config/application.rb +2 -0
  20. data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
  21. data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
  22. data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
  23. data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
  24. data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
  25. data/db/migrate/20260315140843_create_goals.rb +16 -0
  26. data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
  27. data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
  28. data/lib/agent_loop.rb +65 -9
  29. data/lib/agents/definition.rb +116 -0
  30. data/lib/agents/registry.rb +106 -0
  31. data/lib/analytical_brain/runner.rb +276 -0
  32. data/lib/analytical_brain/tools/activate_skill.rb +52 -0
  33. data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
  34. data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
  35. data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
  36. data/lib/analytical_brain/tools/finish_goal.rb +62 -0
  37. data/lib/analytical_brain/tools/read_workflow.rb +58 -0
  38. data/lib/analytical_brain/tools/rename_session.rb +63 -0
  39. data/lib/analytical_brain/tools/set_goal.rb +60 -0
  40. data/lib/analytical_brain/tools/update_goal.rb +60 -0
  41. data/lib/analytical_brain.rb +23 -0
  42. data/lib/anima/cli/mcp/secrets.rb +76 -0
  43. data/lib/anima/cli/mcp.rb +197 -0
  44. data/lib/anima/cli.rb +4 -0
  45. data/lib/anima/installer.rb +168 -0
  46. data/lib/anima/settings.rb +226 -0
  47. data/lib/anima/version.rb +1 -1
  48. data/lib/anima.rb +9 -0
  49. data/lib/credential_store.rb +103 -0
  50. data/lib/environment_probe.rb +232 -0
  51. data/lib/llm/client.rb +29 -10
  52. data/lib/mcp/client_manager.rb +86 -0
  53. data/lib/mcp/config.rb +213 -0
  54. data/lib/mcp/health_check.rb +77 -0
  55. data/lib/mcp/secrets.rb +73 -0
  56. data/lib/mcp/stdio_transport.rb +206 -0
  57. data/lib/providers/anthropic.rb +8 -7
  58. data/lib/shell_session.rb +11 -10
  59. data/lib/skills/definition.rb +97 -0
  60. data/lib/skills/registry.rb +105 -0
  61. data/lib/tools/edit.rb +3 -4
  62. data/lib/tools/mcp_tool.rb +114 -0
  63. data/lib/tools/read.rb +15 -16
  64. data/lib/tools/registry.rb +14 -12
  65. data/lib/tools/request_feature.rb +121 -0
  66. data/lib/tools/return_result.rb +81 -0
  67. data/lib/tools/spawn_specialist.rb +109 -0
  68. data/lib/tools/spawn_subagent.rb +111 -0
  69. data/lib/tools/subagent_prompts.rb +12 -0
  70. data/lib/tools/web_get.rb +8 -9
  71. data/lib/tui/app.rb +332 -43
  72. data/lib/tui/message_store.rb +20 -0
  73. data/lib/tui/screens/chat.rb +207 -20
  74. data/lib/workflows/definition.rb +97 -0
  75. data/lib/workflows/registry.rb +89 -0
  76. data/skills/activerecord/SKILL.md +255 -0
  77. data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
  78. data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
  79. data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
  80. data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
  81. data/skills/activerecord/examples/associations/self_referential.rb +302 -0
  82. data/skills/activerecord/examples/associations/through_associations.rb +203 -0
  83. data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
  84. data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
  85. data/skills/activerecord/examples/basics/inheritance.rb +377 -0
  86. data/skills/activerecord/examples/basics/type_casting.rb +317 -0
  87. data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
  88. data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
  89. data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
  90. data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
  91. data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
  92. data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
  93. data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
  94. data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
  95. data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
  96. data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
  97. data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
  98. data/skills/activerecord/examples/querying/optimization.rb +275 -0
  99. data/skills/activerecord/examples/querying/scopes.rb +260 -0
  100. data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
  101. data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
  102. data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
  103. data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
  104. data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
  105. data/skills/activerecord/references/associations.md +709 -0
  106. data/skills/activerecord/references/basics.md +622 -0
  107. data/skills/activerecord/references/callbacks.md +738 -0
  108. data/skills/activerecord/references/migrations.md +657 -0
  109. data/skills/activerecord/references/querying.md +655 -0
  110. data/skills/activerecord/references/validations.md +596 -0
  111. data/skills/dragonruby/SKILL.md +250 -0
  112. data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
  113. data/skills/dragonruby/examples/audio/background_music.rb +29 -0
  114. data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
  115. data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
  116. data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
  117. data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
  118. data/skills/dragonruby/examples/core/hello_world.rb +24 -0
  119. data/skills/dragonruby/examples/core/labels.rb +22 -0
  120. data/skills/dragonruby/examples/core/sprites.rb +35 -0
  121. data/skills/dragonruby/examples/core/state_management.rb +29 -0
  122. data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
  123. data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
  124. data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
  125. data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
  126. data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
  127. data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
  128. data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
  129. data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
  130. data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
  131. data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
  132. data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
  133. data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
  134. data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
  135. data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
  136. data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
  137. data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
  138. data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
  139. data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
  140. data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
  141. data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
  142. data/skills/dragonruby/examples/input/controller_input.rb +28 -0
  143. data/skills/dragonruby/examples/input/directional_input.rb +24 -0
  144. data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
  145. data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
  146. data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
  147. data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
  148. data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
  149. data/skills/dragonruby/examples/rendering/labels.rb +32 -0
  150. data/skills/dragonruby/examples/rendering/layering.rb +51 -0
  151. data/skills/dragonruby/examples/rendering/solids.rb +61 -0
  152. data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
  153. data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
  154. data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
  155. data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
  156. data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
  157. data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
  158. data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
  159. data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
  160. data/skills/dragonruby/references/audio.md +396 -0
  161. data/skills/dragonruby/references/core.md +385 -0
  162. data/skills/dragonruby/references/distribution.md +434 -0
  163. data/skills/dragonruby/references/entities.md +516 -0
  164. data/skills/dragonruby/references/game-logic/persistence.md +386 -0
  165. data/skills/dragonruby/references/game-logic/state.md +389 -0
  166. data/skills/dragonruby/references/input.md +414 -0
  167. data/skills/dragonruby/references/rendering/animation.md +467 -0
  168. data/skills/dragonruby/references/rendering/primitives.md +403 -0
  169. data/skills/dragonruby/references/scenes.md +443 -0
  170. data/skills/draper-decorators/SKILL.md +344 -0
  171. data/skills/draper-decorators/examples/application_decorator.rb +61 -0
  172. data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
  173. data/skills/draper-decorators/examples/model_decorator.rb +152 -0
  174. data/skills/draper-decorators/references/anti-patterns.md +640 -0
  175. data/skills/draper-decorators/references/patterns.md +507 -0
  176. data/skills/draper-decorators/references/testing.md +559 -0
  177. data/skills/gh-issue.md +182 -0
  178. data/skills/mcp-server/SKILL.md +177 -0
  179. data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
  180. data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
  181. data/skills/mcp-server/examples/http_client.rb +48 -0
  182. data/skills/mcp-server/examples/http_server.rb +97 -0
  183. data/skills/mcp-server/examples/rails_integration.rb +88 -0
  184. data/skills/mcp-server/examples/stdio_server.rb +108 -0
  185. data/skills/mcp-server/examples/streaming_client.rb +95 -0
  186. data/skills/mcp-server/references/gotchas.md +183 -0
  187. data/skills/mcp-server/references/prompts.md +98 -0
  188. data/skills/mcp-server/references/resources.md +53 -0
  189. data/skills/mcp-server/references/server.md +140 -0
  190. data/skills/mcp-server/references/tools.md +146 -0
  191. data/skills/mcp-server/references/transport.md +104 -0
  192. data/skills/ratatui-ruby/SKILL.md +315 -0
  193. data/skills/ratatui-ruby/references/core-concepts.md +340 -0
  194. data/skills/ratatui-ruby/references/events.md +387 -0
  195. data/skills/ratatui-ruby/references/frameworks.md +522 -0
  196. data/skills/ratatui-ruby/references/layout.md +423 -0
  197. data/skills/ratatui-ruby/references/styling.md +268 -0
  198. data/skills/ratatui-ruby/references/testing.md +433 -0
  199. data/skills/ratatui-ruby/references/widgets.md +532 -0
  200. data/skills/rspec/SKILL.md +340 -0
  201. data/skills/rspec/examples/core/basic_structure.rb +69 -0
  202. data/skills/rspec/examples/core/configuration.rb +126 -0
  203. data/skills/rspec/examples/core/hooks.rb +126 -0
  204. data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
  205. data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
  206. data/skills/rspec/examples/core/shared_examples.rb +145 -0
  207. data/skills/rspec/examples/factory_bot/associations.rb +314 -0
  208. data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
  209. data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
  210. data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
  211. data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
  212. data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
  213. data/skills/rspec/examples/factory_bot/traits.rb +293 -0
  214. data/skills/rspec/examples/factory_bot/transients.rb +229 -0
  215. data/skills/rspec/examples/matchers/change.rb +115 -0
  216. data/skills/rspec/examples/matchers/collections.rb +154 -0
  217. data/skills/rspec/examples/matchers/comparisons.rb +79 -0
  218. data/skills/rspec/examples/matchers/composing.rb +155 -0
  219. data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
  220. data/skills/rspec/examples/matchers/equality.rb +58 -0
  221. data/skills/rspec/examples/matchers/errors.rb +136 -0
  222. data/skills/rspec/examples/matchers/output.rb +103 -0
  223. data/skills/rspec/examples/matchers/predicates.rb +87 -0
  224. data/skills/rspec/examples/matchers/truthiness.rb +101 -0
  225. data/skills/rspec/examples/matchers/types.rb +82 -0
  226. data/skills/rspec/examples/matchers/yield.rb +147 -0
  227. data/skills/rspec/examples/mocks/any_instance.rb +172 -0
  228. data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
  229. data/skills/rspec/examples/mocks/constants.rb +177 -0
  230. data/skills/rspec/examples/mocks/doubles.rb +139 -0
  231. data/skills/rspec/examples/mocks/expectations.rb +137 -0
  232. data/skills/rspec/examples/mocks/message_chains.rb +173 -0
  233. data/skills/rspec/examples/mocks/ordering.rb +144 -0
  234. data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
  235. data/skills/rspec/examples/mocks/responses.rb +223 -0
  236. data/skills/rspec/examples/mocks/spies.rb +149 -0
  237. data/skills/rspec/examples/mocks/stubbing.rb +133 -0
  238. data/skills/rspec/examples/rails/channels.rb +250 -0
  239. data/skills/rspec/examples/rails/controller_specs.rb +302 -0
  240. data/skills/rspec/examples/rails/helper_specs.rb +245 -0
  241. data/skills/rspec/examples/rails/job_specs.rb +256 -0
  242. data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
  243. data/skills/rspec/examples/rails/matchers.rb +374 -0
  244. data/skills/rspec/examples/rails/model_specs.rb +193 -0
  245. data/skills/rspec/examples/rails/request_specs.rb +275 -0
  246. data/skills/rspec/examples/rails/routing_specs.rb +276 -0
  247. data/skills/rspec/examples/rails/system_specs.rb +294 -0
  248. data/skills/rspec/examples/rails/transactions.rb +254 -0
  249. data/skills/rspec/examples/rails/view_specs.rb +252 -0
  250. data/skills/rspec/references/core.md +816 -0
  251. data/skills/rspec/references/factory_bot.md +641 -0
  252. data/skills/rspec/references/matchers.md +516 -0
  253. data/skills/rspec/references/mocks.md +381 -0
  254. data/skills/rspec/references/rails.md +528 -0
  255. data/templates/soul.md +40 -0
  256. data/workflows/commit.md +45 -0
  257. data/workflows/create_handoff.md +98 -0
  258. data/workflows/create_note.md +82 -0
  259. data/workflows/create_plan.md +457 -0
  260. data/workflows/decompose_ticket.md +109 -0
  261. data/workflows/feature.md +91 -0
  262. data/workflows/implement_plan.md +87 -0
  263. data/workflows/iterate_plan.md +247 -0
  264. data/workflows/research_codebase.md +210 -0
  265. data/workflows/resume_handoff.md +217 -0
  266. data/workflows/review_pr.md +320 -0
  267. data/workflows/thoughts_init.md +71 -0
  268. data/workflows/validate_plan.md +166 -0
  269. metadata +284 -1
@@ -0,0 +1,102 @@
1
+ ---
2
+ name: thoughts-analyzer
3
+ description: Extracts decisions and actionable insights from project history in thoughts/. Filters exploration noise, returns what was decided, why, and whether conclusions are still valid.
4
+ tools: read, bash
5
+ ---
6
+
7
+ You are a specialist at extracting HIGH-VALUE insights from thoughts documents. Your job is to deeply analyze documents and return only the most relevant, actionable information while filtering out noise.
8
+
9
+ **Scope**: You ONLY search in the local `./thoughts/` directory, following all symlinks. Do not search or read files outside of it. If the search relates to other projects, you may also look in `~/thoughts` directly. Never fall back to searching the broader codebase.
10
+
11
+ ## Core Responsibilities
12
+
13
+ 1. **Extract Key Insights**
14
+ - Identify main decisions and conclusions
15
+ - Find actionable recommendations
16
+ - Note important constraints or requirements
17
+ - Capture critical technical details
18
+
19
+ 2. **Filter Aggressively**
20
+ - Skip tangential mentions
21
+ - Ignore outdated information
22
+ - Remove redundant content
23
+ - Focus on what matters NOW
24
+
25
+ 3. **Validate Relevance**
26
+ - Question if information is still applicable
27
+ - Note when context has likely changed
28
+ - Distinguish decisions from explorations
29
+
30
+ ## Search Strategy
31
+
32
+ Use `bash` with find and grep to discover and search thought documents. Subdirectories in `./thoughts/` are typically symlinks — use `find -L` to follow them.
33
+
34
+ 1. `ls -la ./thoughts/` — discover subdirs (shared/, username/, global/)
35
+ 2. `find -L ./thoughts/ -name "*.md"` — find all documents following symlinks
36
+ 3. `grep -rn "keyword" ./thoughts/` — search for specific topics
37
+
38
+ Then use `read` to analyze documents in detail.
39
+
40
+ ## Analysis Strategy
41
+
42
+ ### Step 1: Read with Purpose
43
+ - Read the entire document first
44
+ - Identify the document's main goal
45
+ - Note the date and context
46
+ - Understand what question it was answering
47
+
48
+ ### Step 2: Extract Strategically
49
+ Focus on:
50
+ - **Decisions made**: "We decided to..."
51
+ - **Trade-offs analyzed**: "X vs Y because..."
52
+ - **Constraints identified**: "We must..." "We cannot..."
53
+ - **Lessons learned**: "We discovered that..."
54
+ - **Technical specifications**: Specific values, configs, approaches
55
+
56
+ ### Step 3: Filter Ruthlessly
57
+ Remove:
58
+ - Exploratory rambling without conclusions
59
+ - Options that were rejected
60
+ - Temporary workarounds that were replaced
61
+ - Information superseded by newer documents
62
+
63
+ ## Output Format
64
+
65
+ ```
66
+ ## Analysis of: [Document Path]
67
+
68
+ ### Document Context
69
+ - **Date**: [When written]
70
+ - **Purpose**: [Why this document exists]
71
+ - **Status**: [Still relevant / implemented / superseded?]
72
+
73
+ ### Key Decisions
74
+ 1. **[Decision Topic]**: [Specific decision made]
75
+ - Rationale: [Why]
76
+ - Impact: [What this enables/prevents]
77
+
78
+ ### Critical Constraints
79
+ - **[Constraint]**: [Limitation and why]
80
+
81
+ ### Actionable Insights
82
+ - [Something that should guide current implementation]
83
+
84
+ ### Still Open/Unclear
85
+ - [Unresolved questions]
86
+
87
+ ### Relevance Assessment
88
+ [Is this still applicable and why]
89
+ ```
90
+
91
+ ## Quality Filters
92
+
93
+ ### Include Only If:
94
+ - It answers a specific question
95
+ - It documents a firm decision
96
+ - It reveals a non-obvious constraint
97
+ - It provides concrete technical details
98
+
99
+ ### Exclude If:
100
+ - It's just exploring possibilities
101
+ - It's been clearly superseded
102
+ - It's too vague to action
@@ -0,0 +1,71 @@
1
+ ---
2
+ name: web-search-researcher
3
+ description: Deep web research specialist. Fetches and analyzes web content to find accurate, up-to-date information on any topic.
4
+ tools: web_get, bash, read
5
+ color: yellow
6
+ ---
7
+
8
+ You are an expert web research specialist. Use `web_get` to fetch web pages and extract information. Use `bash` for processing and `read` for examining local files when needed.
9
+
10
+ ## Core Responsibilities
11
+
12
+ 1. **Analyze the Query**: Break down the request to identify:
13
+ - Key search terms and concepts
14
+ - Types of sources likely to have answers
15
+ - Multiple angles to ensure comprehensive coverage
16
+
17
+ 2. **Fetch and Analyze Content**:
18
+ - Use `web_get` to retrieve content from known documentation URLs
19
+ - Prioritize official documentation and authoritative sources
20
+ - Extract specific quotes and sections relevant to the query
21
+ - Note publication dates to ensure currency
22
+
23
+ 3. **Synthesize Findings**:
24
+ - Organize information by relevance and authority
25
+ - Include exact quotes with proper attribution
26
+ - Provide direct links to sources
27
+ - Highlight conflicting information or version-specific details
28
+
29
+ ## Research Strategies
30
+
31
+ ### For API/Library Documentation:
32
+ - Fetch official docs directly when URLs are known
33
+ - Look for changelog or release notes for version-specific information
34
+ - Find code examples in official repositories
35
+
36
+ ### For Technical Solutions:
37
+ - Fetch Stack Overflow answers and GitHub issues
38
+ - Look for blog posts describing similar implementations
39
+ - Cross-reference multiple sources
40
+
41
+ ### For Best Practices:
42
+ - Look for content from recognized experts or organizations
43
+ - Cross-reference multiple sources to identify consensus
44
+
45
+ ## Output Format
46
+
47
+ ```
48
+ ## Summary
49
+ [Brief overview of key findings]
50
+
51
+ ## Detailed Findings
52
+
53
+ ### [Topic/Source 1]
54
+ **Source**: [Name with URL]
55
+ **Key Information**:
56
+ - Finding with context
57
+ - Another relevant point
58
+
59
+ ## Additional Resources
60
+ - [URL] - Brief description
61
+
62
+ ## Gaps or Limitations
63
+ [Information that couldn't be found]
64
+ ```
65
+
66
+ ## Quality Guidelines
67
+
68
+ - **Accuracy**: Quote sources accurately and provide direct links
69
+ - **Relevance**: Focus on information that directly addresses the query
70
+ - **Currency**: Note publication dates and version information
71
+ - **Authority**: Prioritize official sources and recognized experts
data/anima-core.gemspec CHANGED
@@ -29,13 +29,16 @@ Gem::Specification.new do |spec|
29
29
  spec.require_paths = ["lib"]
30
30
 
31
31
  spec.add_dependency "draper", "~> 4.0"
32
+ spec.add_dependency "faraday", "~> 2.0"
32
33
  spec.add_dependency "foreman", "~> 0.88"
33
34
  spec.add_dependency "httparty", "~> 0.24"
35
+ spec.add_dependency "mcp", "~> 0.8"
34
36
  spec.add_dependency "puma", "~> 6.0"
35
37
  spec.add_dependency "rails", "~> 8.1"
36
38
  spec.add_dependency "ratatui_ruby", "~> 1.4"
37
39
  spec.add_dependency "solid_cable", "~> 3.0"
38
40
  spec.add_dependency "solid_queue", "~> 1.1"
39
41
  spec.add_dependency "sqlite3", "~> 2.0"
42
+ spec.add_dependency "toml-rb", "~> 4.0"
40
43
  spec.add_dependency "websocket-client-simple", "~> 0.8"
41
44
  end
@@ -82,22 +82,18 @@ class SessionChannel < ApplicationCable::Channel
82
82
  ActionCable.server.broadcast(stream_name, {"action" => "user_message_recalled", "event_id" => event_id})
83
83
  end
84
84
 
85
- # Returns recent sessions with metadata for session picker UI.
85
+ # Returns recent root sessions with nested child metadata for session picker UI.
86
+ # Filters to root sessions only (no parent_session_id). Child sessions are
87
+ # nested under their parent with name and status information.
86
88
  #
87
89
  # @param data [Hash] optional "limit" (default 10, max 50)
88
90
  def list_sessions(data)
89
91
  limit = (data["limit"] || DEFAULT_LIST_LIMIT).to_i.clamp(1, MAX_LIST_LIMIT)
90
- sessions = Session.recent(limit)
91
- counts = Event.where(session_id: sessions.select(:id)).llm_messages.group(:session_id).count
92
+ sessions = Session.root_sessions.recent(limit).includes(:child_sessions)
93
+ all_ids = sessions.flat_map { |session| [session.id] + session.child_sessions.map(&:id) }
94
+ counts = Event.where(session_id: all_ids).llm_messages.group(:session_id).count
92
95
 
93
- result = sessions.map do |session|
94
- {
95
- id: session.id,
96
- created_at: session.created_at.iso8601,
97
- updated_at: session.updated_at.iso8601,
98
- message_count: counts[session.id] || 0
99
- }
100
- end
96
+ result = sessions.map { |session| serialize_session_with_children(session, counts) }
101
97
  transmit({"action" => "sessions_list", "sessions" => result})
102
98
  end
103
99
 
@@ -177,14 +173,22 @@ class SessionChannel < ApplicationCable::Channel
177
173
  # Used on initial subscription and after session switches so the
178
174
  # client can handle both paths with a single code path.
179
175
  #
176
+ # Payload: session_id, name, parent_session_id, message_count,
177
+ # view_mode, active_skills, goals.
178
+ #
180
179
  # @param session [Session] the session to announce
181
180
  # @return [void]
182
181
  def transmit_session_changed(session)
183
182
  transmit({
184
183
  "action" => "session_changed",
185
184
  "session_id" => session.id,
185
+ "name" => session.name,
186
+ "parent_session_id" => session.parent_session_id,
186
187
  "message_count" => session.events.llm_messages.count,
187
- "view_mode" => session.view_mode
188
+ "view_mode" => session.view_mode,
189
+ "active_skills" => session.active_skills,
190
+ "active_workflow" => session.active_workflow,
191
+ "goals" => session.goals_summary
188
192
  })
189
193
  end
190
194
 
@@ -217,25 +221,50 @@ class SessionChannel < ApplicationCable::Channel
217
221
  # reconstruct tool call counters on reconnect.
218
222
  # In debug mode, prepends the assembled system prompt as a special block.
219
223
  #
224
+ # Snapshots the viewport so subsequent event broadcasts can compute
225
+ # eviction diffs accurately.
226
+ #
220
227
  # @param session [Session] the session whose history to transmit
221
228
  def transmit_history(session)
222
229
  transmit_system_prompt(session) if session.view_mode == "debug"
223
230
 
224
- session.viewport_events.each do |event|
225
- transmit(decorate_event_payload(event, session.view_mode))
231
+ each_viewport_event(session) do |event, payload|
232
+ transmit(payload)
226
233
  end
227
234
  end
228
235
 
229
236
  # Broadcasts the re-decorated viewport to all clients on the session stream.
230
237
  # Used after a view mode change to refresh all connected clients.
231
238
  # In debug mode, prepends the assembled system prompt as a special block.
239
+ #
240
+ # Snapshots the viewport so subsequent event broadcasts can compute
241
+ # eviction diffs accurately.
242
+ #
232
243
  # @param session [Session] the session whose viewport to broadcast
233
244
  # @return [void]
234
245
  def broadcast_viewport(session)
235
246
  broadcast_system_prompt(session) if session.view_mode == "debug"
236
247
 
237
- session.viewport_events.each do |event|
238
- ActionCable.server.broadcast(stream_name, decorate_event_payload(event, session.view_mode))
248
+ each_viewport_event(session) do |event, payload|
249
+ ActionCable.server.broadcast(stream_name, payload)
250
+ end
251
+ end
252
+
253
+ # Loads the viewport, snapshots it for eviction tracking, and yields
254
+ # each event with its decorated payload. Snapshot uses snapshot_viewport!
255
+ # (not recalculate_viewport!) because full viewport refreshes don't need
256
+ # eviction diffs — clients clear their store before rendering.
257
+ #
258
+ # @param session [Session] the session whose viewport to iterate
259
+ # @yieldparam event [Event] the persisted event record
260
+ # @yieldparam payload [Hash] decorated payload ready for transmission
261
+ # @return [void]
262
+ def each_viewport_event(session)
263
+ viewport = session.viewport_events
264
+ session.snapshot_viewport!(viewport.map(&:id))
265
+
266
+ viewport.each do |event|
267
+ yield event, decorate_event_payload(event, session.view_mode)
239
268
  end
240
269
  end
241
270
 
@@ -299,19 +328,38 @@ class SessionChannel < ApplicationCable::Channel
299
328
  # @param token [String] validated Anthropic subscription token
300
329
  # @return [void]
301
330
  def write_anthropic_token(token)
302
- creds = Rails.application.credentials
303
- existing = begin
304
- YAML.safe_load(creds.read) || {}
305
- rescue ActiveSupport::EncryptedFile::MissingContentError
306
- {}
331
+ CredentialStore.write("anthropic", "subscription_token" => token)
332
+ end
333
+
334
+ # Serializes a root session with its children for the sessions_list response.
335
+ # Includes a :children key only when the session has child sessions.
336
+ #
337
+ # @param session [Session] root session to serialize
338
+ # @param counts [Hash<Integer, Integer>] session_id => llm_message count
339
+ # @return [Hash] with :id, :created_at, :updated_at, :message_count, and optional :children
340
+ def serialize_session_with_children(session, counts)
341
+ entry = {
342
+ id: session.id,
343
+ name: session.name,
344
+ created_at: session.created_at.iso8601,
345
+ updated_at: session.updated_at.iso8601,
346
+ message_count: counts[session.id] || 0
347
+ }
348
+
349
+ children = session.child_sessions.sort_by(&:created_at)
350
+ return entry unless children.any?
351
+
352
+ entry[:children] = children.map do |child|
353
+ {
354
+ id: child.id,
355
+ name: child.name,
356
+ processing: child.processing?,
357
+ message_count: counts[child.id] || 0,
358
+ created_at: child.created_at.iso8601
359
+ }
307
360
  end
308
- existing["anthropic"] ||= {}
309
- existing["anthropic"]["subscription_token"] = token
310
- creds.write(existing.to_yaml)
311
- # Rails memoizes the decrypted config in @config. Without clearing it,
312
- # subsequent credential reads return stale data. No public API exists
313
- # for cache invalidation as of Rails 8.1.
314
- creds.instance_variable_set(:@config, nil)
361
+
362
+ entry
315
363
  end
316
364
 
317
365
  def transmit_error(message)
@@ -48,12 +48,20 @@ class AgentRequestJob < ApplicationJob
48
48
  # any pending messages after its current loop completes.
49
49
  return unless claim_processing(session_id)
50
50
 
51
+ # Run analytical brain BEFORE the main agent on user messages so
52
+ # activated skills are available for the current response.
53
+ run_analytical_brain_blocking(session)
54
+
51
55
  agent_loop = AgentLoop.new(session: session)
52
56
  loop do
53
57
  agent_loop.run
54
58
  promoted = session.promote_pending_messages!
55
59
  break if promoted == 0
56
60
  end
61
+
62
+ # Non-blocking analytical brain run after agent completes —
63
+ # handles post-response updates (renaming, skill changes).
64
+ session.schedule_analytical_brain!
57
65
  ensure
58
66
  release_processing(session_id)
59
67
  agent_loop&.finalize
@@ -61,6 +69,22 @@ class AgentRequestJob < ApplicationJob
61
69
 
62
70
  private
63
71
 
72
+ # Runs the analytical brain synchronously before the main agent loop.
73
+ # Respects the blocking_on_user_message setting and session guards
74
+ # (skips sub-agents and sessions with too few messages).
75
+ def run_analytical_brain_blocking(session)
76
+ return unless Anima::Settings.analytical_brain_blocking_on_user_message
77
+ return if session.sub_agent?
78
+
79
+ AnalyticalBrain::Runner.new(session).call
80
+ rescue => error
81
+ # The analytical brain is best-effort: skill activation enhances the
82
+ # response but the main agent must still reply even if it fails.
83
+ msg = "FAILED (blocking) session=#{session.id}: #{error.class}: #{error.message}"
84
+ Rails.logger.error("Analytical brain #{msg}")
85
+ AnalyticalBrain.logger.error("#{msg}\n#{error.backtrace&.first(10)&.join("\n")}")
86
+ end
87
+
64
88
  # Sets the session's processing flag atomically. Returns true if this
65
89
  # job claimed the lock, false if another job already holds it.
66
90
  def claim_processing(session_id)
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Runs the analytical brain — a phantom LLM loop that observes the main
4
+ # session and performs background maintenance (currently: session naming).
5
+ #
6
+ # Replaces {GenerateSessionNameJob} with a tool-based architecture that
7
+ # future tickets will expand with skill activation, goal tracking, etc.
8
+ #
9
+ # Scheduling guards live in {Session#schedule_analytical_brain!} — this
10
+ # job always runs when called.
11
+ #
12
+ # @example
13
+ # AnalyticalBrainJob.perform_later(session.id)
14
+ class AnalyticalBrainJob < ApplicationJob
15
+ queue_as :default
16
+
17
+ retry_on Providers::Anthropic::TransientError,
18
+ wait: :polynomially_longer, attempts: 3
19
+
20
+ discard_on ActiveRecord::RecordNotFound
21
+ discard_on Providers::Anthropic::AuthenticationError
22
+
23
+ # @param session_id [Integer] the main Session to analyze
24
+ def perform(session_id)
25
+ brain_log = AnalyticalBrain.logger
26
+ session = Session.find(session_id)
27
+ brain_log.info("async job started for session=#{session_id}")
28
+ AnalyticalBrain::Runner.new(session).call
29
+ rescue => error
30
+ brain_log.error("FAILED (async) session=#{session_id}: #{error.class}: #{error.message}")
31
+ raise
32
+ end
33
+ end
@@ -18,7 +18,7 @@ class CountEventTokensJob < ApplicationJob
18
18
  messages = [{role: event.api_role, content: event.payload["content"].to_s}]
19
19
 
20
20
  token_count = provider.count_tokens(
21
- model: LLM::Client::DEFAULT_MODEL,
21
+ model: Anima::Settings.model,
22
22
  messages: messages
23
23
  )
24
24
 
@@ -9,6 +9,10 @@
9
9
  # maintain an ID-indexed store for efficient in-place updates (e.g. when
10
10
  # token counts arrive asynchronously from {CountEventTokensJob}).
11
11
  #
12
+ # When a new event pushes old events out of the LLM's context window,
13
+ # the broadcast includes `evicted_event_ids` so clients can remove
14
+ # phantom messages that the agent no longer knows about.
15
+ #
12
16
  # @example Create broadcast payload
13
17
  # {
14
18
  # "type" => "user_message", "content" => "hello", ...,
@@ -16,6 +20,13 @@
16
20
  # "rendered" => { "basic" => { "role" => "user", "content" => "hello" } }
17
21
  # }
18
22
  #
23
+ # @example Broadcast with viewport evictions
24
+ # {
25
+ # "type" => "agent_message", "content" => "...", ...,
26
+ # "id" => 99, "action" => "create",
27
+ # "evicted_event_ids" => [101, 102, 103]
28
+ # }
29
+ #
19
30
  # @example Update broadcast payload (e.g. token count arrives)
20
31
  # {
21
32
  # "type" => "user_message", "content" => "hello", ...,
@@ -44,13 +55,17 @@ module Event::Broadcasting
44
55
  end
45
56
 
46
57
  # Decorates the event for the session's current view mode and broadcasts
47
- # the payload to the session's ActionCable stream.
58
+ # the payload to the session's ActionCable stream. Includes viewport
59
+ # eviction metadata so clients can remove messages the LLM has forgotten.
48
60
  #
49
61
  # @param action [String] ACTION_CREATE or ACTION_UPDATE — tells clients how to handle the event
50
62
  def broadcast_event(action:)
51
63
  return unless session_id
52
64
 
53
- mode = Session.where(id: session_id).pick(:view_mode) || "basic"
65
+ session = Session.find_by(id: session_id)
66
+ return unless session
67
+
68
+ mode = session.view_mode
54
69
  decorator = EventDecorator.for(self)
55
70
  broadcast_payload = payload.merge("id" => id, "action" => action)
56
71
 
@@ -58,6 +73,9 @@ module Event::Broadcasting
58
73
  broadcast_payload["rendered"] = {mode => decorator.render(mode)}
59
74
  end
60
75
 
76
+ evicted_ids = session.recalculate_viewport!
77
+ broadcast_payload["evicted_event_ids"] = evicted_ids if evicted_ids.any?
78
+
61
79
  ActionCable.server.broadcast("session_#{session_id}", broadcast_payload)
62
80
  end
63
81
  end
data/app/models/event.rb CHANGED
@@ -20,7 +20,7 @@ class Event < ApplicationRecord
20
20
 
21
21
  TYPES = %w[system_message user_message agent_message tool_call tool_response].freeze
22
22
  LLM_TYPES = %w[user_message agent_message].freeze
23
- CONTEXT_TYPES = %w[user_message agent_message tool_call tool_response].freeze
23
+ CONTEXT_TYPES = %w[system_message user_message agent_message tool_call tool_response].freeze
24
24
  PENDING_STATUS = "pending"
25
25
 
26
26
  ROLE_MAP = {"user_message" => "user", "agent_message" => "assistant"}.freeze
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A persistent objective tracked by the analytical brain during a session.
4
+ # Goals form a two-level hierarchy: root goals represent high-level
5
+ # objectives (semantic episodes), while sub-goals are TODO-style steps
6
+ # rendered as checklist items in the agent's system prompt.
7
+ #
8
+ # The analytical brain creates and completes goals; the main agent sees
9
+ # them in its context window but never manages them directly.
10
+ class Goal < ApplicationRecord
11
+ STATUSES = %w[active completed].freeze
12
+
13
+ belongs_to :session
14
+ belongs_to :parent_goal, class_name: "Goal", optional: true
15
+ has_many :sub_goals, -> { order(:created_at) }, class_name: "Goal", foreign_key: :parent_goal_id, dependent: :destroy
16
+
17
+ validates :description, presence: true
18
+ validates :status, inclusion: {in: STATUSES}
19
+ validate :parent_goal_belongs_to_same_session, if: :parent_goal
20
+ validate :parent_goal_is_root, if: :parent_goal
21
+
22
+ scope :active, -> { where(status: "active") }
23
+ scope :completed, -> { where(status: "completed") }
24
+ scope :root, -> { where(parent_goal_id: nil) }
25
+
26
+ after_commit :broadcast_goals_update
27
+
28
+ # @return [Boolean] true if this goal has been completed
29
+ def completed? = status == "completed"
30
+
31
+ # @return [Boolean] true if this is a root goal (no parent)
32
+ def root? = !parent_goal_id
33
+
34
+ # Cascades completion to all active sub-goals. Called when a root goal
35
+ # is finished — remaining sub-items are implicitly resolved because
36
+ # the semantic episode that spawned them has ended.
37
+ #
38
+ # Uses +update_all+ to avoid N per-record +after_commit+ broadcasts;
39
+ # the caller ({AnalyticalBrain::Tools::FinishGoal}) wraps the whole
40
+ # operation in a transaction so the root goal's single broadcast
41
+ # includes the cascaded state.
42
+ #
43
+ # @return [void]
44
+ def cascade_completion!
45
+ now = Time.current
46
+ sub_goals.active.update_all(status: "completed", completed_at: now, updated_at: now)
47
+ end
48
+
49
+ # Serializes this goal for ActionCable broadcast and TUI display.
50
+ # Includes nested sub-goals for root goals.
51
+ #
52
+ # @return [Hash{String => Object}] with keys "id", "description", "status",
53
+ # and "sub_goals" (Array of Hash with "id", "description", "status")
54
+ def as_summary
55
+ {
56
+ "id" => id,
57
+ "description" => description,
58
+ "status" => status,
59
+ "sub_goals" => sub_goals.map { |sub|
60
+ {"id" => sub.id, "description" => sub.description, "status" => sub.status}
61
+ }
62
+ }
63
+ end
64
+
65
+ private
66
+
67
+ def parent_goal_belongs_to_same_session
68
+ return if parent_goal.session_id == session_id
69
+
70
+ errors.add(:parent_goal, "must belong to the same session")
71
+ end
72
+
73
+ def parent_goal_is_root
74
+ return unless parent_goal.parent_goal_id
75
+
76
+ errors.add(:parent_goal, "cannot nest deeper than two levels")
77
+ end
78
+
79
+ # Broadcasts goal changes to all clients subscribed to this session.
80
+ # Mirrors the Session#broadcast_active_skills_update pattern so the
81
+ # TUI info panel updates reactively.
82
+ #
83
+ # @return [void]
84
+ def broadcast_goals_update
85
+ ActionCable.server.broadcast("session_#{session_id}", {
86
+ "action" => "goals_updated",
87
+ "session_id" => session_id,
88
+ "goals" => session.goals_summary
89
+ })
90
+ end
91
+ end