anima-core 0.2.1 → 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 (280) hide show
  1. checksums.yaml +4 -4
  2. data/.reek.yml +27 -1
  3. data/CHANGELOG.md +19 -0
  4. data/README.md +213 -43
  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 +195 -45
  12. data/app/decorators/user_message_decorator.rb +16 -5
  13. data/app/jobs/agent_request_job.rb +55 -2
  14. data/app/jobs/analytical_brain_job.rb +33 -0
  15. data/app/jobs/count_event_tokens_job.rb +15 -4
  16. data/app/models/concerns/event/broadcasting.rb +81 -0
  17. data/app/models/event.rb +20 -1
  18. data/app/models/goal.rb +91 -0
  19. data/app/models/session.rb +366 -21
  20. data/config/application.rb +2 -0
  21. data/config/initializers/event_subscribers.rb +0 -1
  22. data/config/routes.rb +0 -6
  23. data/db/migrate/20260313010000_add_status_to_events.rb +8 -0
  24. data/db/migrate/20260313020000_add_processing_to_sessions.rb +7 -0
  25. data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
  26. data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
  27. data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
  28. data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
  29. data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
  30. data/db/migrate/20260315140843_create_goals.rb +16 -0
  31. data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
  32. data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
  33. data/lib/agent_loop.rb +65 -6
  34. data/lib/agents/definition.rb +116 -0
  35. data/lib/agents/registry.rb +106 -0
  36. data/lib/analytical_brain/runner.rb +276 -0
  37. data/lib/analytical_brain/tools/activate_skill.rb +52 -0
  38. data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
  39. data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
  40. data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
  41. data/lib/analytical_brain/tools/finish_goal.rb +62 -0
  42. data/lib/analytical_brain/tools/read_workflow.rb +58 -0
  43. data/lib/analytical_brain/tools/rename_session.rb +63 -0
  44. data/lib/analytical_brain/tools/set_goal.rb +60 -0
  45. data/lib/analytical_brain/tools/update_goal.rb +60 -0
  46. data/lib/analytical_brain.rb +23 -0
  47. data/lib/anima/cli/mcp/secrets.rb +76 -0
  48. data/lib/anima/cli/mcp.rb +197 -0
  49. data/lib/anima/cli.rb +5 -40
  50. data/lib/anima/installer.rb +168 -0
  51. data/lib/anima/settings.rb +226 -0
  52. data/lib/anima/version.rb +1 -1
  53. data/lib/anima.rb +9 -0
  54. data/lib/credential_store.rb +103 -0
  55. data/lib/environment_probe.rb +232 -0
  56. data/lib/events/subscribers/persister.rb +1 -0
  57. data/lib/events/user_message.rb +17 -0
  58. data/lib/llm/client.rb +29 -10
  59. data/lib/mcp/client_manager.rb +86 -0
  60. data/lib/mcp/config.rb +213 -0
  61. data/lib/mcp/health_check.rb +77 -0
  62. data/lib/mcp/secrets.rb +73 -0
  63. data/lib/mcp/stdio_transport.rb +206 -0
  64. data/lib/providers/anthropic.rb +11 -20
  65. data/lib/shell_session.rb +11 -10
  66. data/lib/skills/definition.rb +97 -0
  67. data/lib/skills/registry.rb +105 -0
  68. data/lib/tools/edit.rb +226 -0
  69. data/lib/tools/mcp_tool.rb +114 -0
  70. data/lib/tools/read.rb +151 -0
  71. data/lib/tools/registry.rb +14 -12
  72. data/lib/tools/request_feature.rb +121 -0
  73. data/lib/tools/return_result.rb +81 -0
  74. data/lib/tools/spawn_specialist.rb +109 -0
  75. data/lib/tools/spawn_subagent.rb +111 -0
  76. data/lib/tools/subagent_prompts.rb +12 -0
  77. data/lib/tools/web_get.rb +8 -9
  78. data/lib/tools/write.rb +86 -0
  79. data/lib/tui/app.rb +985 -26
  80. data/lib/tui/cable_client.rb +69 -31
  81. data/lib/tui/message_store.rb +103 -8
  82. data/lib/tui/screens/chat.rb +293 -45
  83. data/lib/workflows/definition.rb +97 -0
  84. data/lib/workflows/registry.rb +89 -0
  85. data/skills/activerecord/SKILL.md +255 -0
  86. data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
  87. data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
  88. data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
  89. data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
  90. data/skills/activerecord/examples/associations/self_referential.rb +302 -0
  91. data/skills/activerecord/examples/associations/through_associations.rb +203 -0
  92. data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
  93. data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
  94. data/skills/activerecord/examples/basics/inheritance.rb +377 -0
  95. data/skills/activerecord/examples/basics/type_casting.rb +317 -0
  96. data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
  97. data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
  98. data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
  99. data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
  100. data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
  101. data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
  102. data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
  103. data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
  104. data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
  105. data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
  106. data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
  107. data/skills/activerecord/examples/querying/optimization.rb +275 -0
  108. data/skills/activerecord/examples/querying/scopes.rb +260 -0
  109. data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
  110. data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
  111. data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
  112. data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
  113. data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
  114. data/skills/activerecord/references/associations.md +709 -0
  115. data/skills/activerecord/references/basics.md +622 -0
  116. data/skills/activerecord/references/callbacks.md +738 -0
  117. data/skills/activerecord/references/migrations.md +657 -0
  118. data/skills/activerecord/references/querying.md +655 -0
  119. data/skills/activerecord/references/validations.md +596 -0
  120. data/skills/dragonruby/SKILL.md +250 -0
  121. data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
  122. data/skills/dragonruby/examples/audio/background_music.rb +29 -0
  123. data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
  124. data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
  125. data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
  126. data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
  127. data/skills/dragonruby/examples/core/hello_world.rb +24 -0
  128. data/skills/dragonruby/examples/core/labels.rb +22 -0
  129. data/skills/dragonruby/examples/core/sprites.rb +35 -0
  130. data/skills/dragonruby/examples/core/state_management.rb +29 -0
  131. data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
  132. data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
  133. data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
  134. data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
  135. data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
  136. data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
  137. data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
  138. data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
  139. data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
  140. data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
  141. data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
  142. data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
  143. data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
  144. data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
  145. data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
  146. data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
  147. data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
  148. data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
  149. data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
  150. data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
  151. data/skills/dragonruby/examples/input/controller_input.rb +28 -0
  152. data/skills/dragonruby/examples/input/directional_input.rb +24 -0
  153. data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
  154. data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
  155. data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
  156. data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
  157. data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
  158. data/skills/dragonruby/examples/rendering/labels.rb +32 -0
  159. data/skills/dragonruby/examples/rendering/layering.rb +51 -0
  160. data/skills/dragonruby/examples/rendering/solids.rb +61 -0
  161. data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
  162. data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
  163. data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
  164. data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
  165. data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
  166. data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
  167. data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
  168. data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
  169. data/skills/dragonruby/references/audio.md +396 -0
  170. data/skills/dragonruby/references/core.md +385 -0
  171. data/skills/dragonruby/references/distribution.md +434 -0
  172. data/skills/dragonruby/references/entities.md +516 -0
  173. data/skills/dragonruby/references/game-logic/persistence.md +386 -0
  174. data/skills/dragonruby/references/game-logic/state.md +389 -0
  175. data/skills/dragonruby/references/input.md +414 -0
  176. data/skills/dragonruby/references/rendering/animation.md +467 -0
  177. data/skills/dragonruby/references/rendering/primitives.md +403 -0
  178. data/skills/dragonruby/references/scenes.md +443 -0
  179. data/skills/draper-decorators/SKILL.md +344 -0
  180. data/skills/draper-decorators/examples/application_decorator.rb +61 -0
  181. data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
  182. data/skills/draper-decorators/examples/model_decorator.rb +152 -0
  183. data/skills/draper-decorators/references/anti-patterns.md +640 -0
  184. data/skills/draper-decorators/references/patterns.md +507 -0
  185. data/skills/draper-decorators/references/testing.md +559 -0
  186. data/skills/gh-issue.md +182 -0
  187. data/skills/mcp-server/SKILL.md +177 -0
  188. data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
  189. data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
  190. data/skills/mcp-server/examples/http_client.rb +48 -0
  191. data/skills/mcp-server/examples/http_server.rb +97 -0
  192. data/skills/mcp-server/examples/rails_integration.rb +88 -0
  193. data/skills/mcp-server/examples/stdio_server.rb +108 -0
  194. data/skills/mcp-server/examples/streaming_client.rb +95 -0
  195. data/skills/mcp-server/references/gotchas.md +183 -0
  196. data/skills/mcp-server/references/prompts.md +98 -0
  197. data/skills/mcp-server/references/resources.md +53 -0
  198. data/skills/mcp-server/references/server.md +140 -0
  199. data/skills/mcp-server/references/tools.md +146 -0
  200. data/skills/mcp-server/references/transport.md +104 -0
  201. data/skills/ratatui-ruby/SKILL.md +315 -0
  202. data/skills/ratatui-ruby/references/core-concepts.md +340 -0
  203. data/skills/ratatui-ruby/references/events.md +387 -0
  204. data/skills/ratatui-ruby/references/frameworks.md +522 -0
  205. data/skills/ratatui-ruby/references/layout.md +423 -0
  206. data/skills/ratatui-ruby/references/styling.md +268 -0
  207. data/skills/ratatui-ruby/references/testing.md +433 -0
  208. data/skills/ratatui-ruby/references/widgets.md +532 -0
  209. data/skills/rspec/SKILL.md +340 -0
  210. data/skills/rspec/examples/core/basic_structure.rb +69 -0
  211. data/skills/rspec/examples/core/configuration.rb +126 -0
  212. data/skills/rspec/examples/core/hooks.rb +126 -0
  213. data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
  214. data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
  215. data/skills/rspec/examples/core/shared_examples.rb +145 -0
  216. data/skills/rspec/examples/factory_bot/associations.rb +314 -0
  217. data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
  218. data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
  219. data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
  220. data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
  221. data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
  222. data/skills/rspec/examples/factory_bot/traits.rb +293 -0
  223. data/skills/rspec/examples/factory_bot/transients.rb +229 -0
  224. data/skills/rspec/examples/matchers/change.rb +115 -0
  225. data/skills/rspec/examples/matchers/collections.rb +154 -0
  226. data/skills/rspec/examples/matchers/comparisons.rb +79 -0
  227. data/skills/rspec/examples/matchers/composing.rb +155 -0
  228. data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
  229. data/skills/rspec/examples/matchers/equality.rb +58 -0
  230. data/skills/rspec/examples/matchers/errors.rb +136 -0
  231. data/skills/rspec/examples/matchers/output.rb +103 -0
  232. data/skills/rspec/examples/matchers/predicates.rb +87 -0
  233. data/skills/rspec/examples/matchers/truthiness.rb +101 -0
  234. data/skills/rspec/examples/matchers/types.rb +82 -0
  235. data/skills/rspec/examples/matchers/yield.rb +147 -0
  236. data/skills/rspec/examples/mocks/any_instance.rb +172 -0
  237. data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
  238. data/skills/rspec/examples/mocks/constants.rb +177 -0
  239. data/skills/rspec/examples/mocks/doubles.rb +139 -0
  240. data/skills/rspec/examples/mocks/expectations.rb +137 -0
  241. data/skills/rspec/examples/mocks/message_chains.rb +173 -0
  242. data/skills/rspec/examples/mocks/ordering.rb +144 -0
  243. data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
  244. data/skills/rspec/examples/mocks/responses.rb +223 -0
  245. data/skills/rspec/examples/mocks/spies.rb +149 -0
  246. data/skills/rspec/examples/mocks/stubbing.rb +133 -0
  247. data/skills/rspec/examples/rails/channels.rb +250 -0
  248. data/skills/rspec/examples/rails/controller_specs.rb +302 -0
  249. data/skills/rspec/examples/rails/helper_specs.rb +245 -0
  250. data/skills/rspec/examples/rails/job_specs.rb +256 -0
  251. data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
  252. data/skills/rspec/examples/rails/matchers.rb +374 -0
  253. data/skills/rspec/examples/rails/model_specs.rb +193 -0
  254. data/skills/rspec/examples/rails/request_specs.rb +275 -0
  255. data/skills/rspec/examples/rails/routing_specs.rb +276 -0
  256. data/skills/rspec/examples/rails/system_specs.rb +294 -0
  257. data/skills/rspec/examples/rails/transactions.rb +254 -0
  258. data/skills/rspec/examples/rails/view_specs.rb +252 -0
  259. data/skills/rspec/references/core.md +816 -0
  260. data/skills/rspec/references/factory_bot.md +641 -0
  261. data/skills/rspec/references/matchers.md +516 -0
  262. data/skills/rspec/references/mocks.md +381 -0
  263. data/skills/rspec/references/rails.md +528 -0
  264. data/templates/soul.md +40 -0
  265. data/workflows/commit.md +45 -0
  266. data/workflows/create_handoff.md +98 -0
  267. data/workflows/create_note.md +82 -0
  268. data/workflows/create_plan.md +457 -0
  269. data/workflows/decompose_ticket.md +109 -0
  270. data/workflows/feature.md +91 -0
  271. data/workflows/implement_plan.md +87 -0
  272. data/workflows/iterate_plan.md +247 -0
  273. data/workflows/research_codebase.md +210 -0
  274. data/workflows/resume_handoff.md +217 -0
  275. data/workflows/review_pr.md +320 -0
  276. data/workflows/thoughts_init.md +71 -0
  277. data/workflows/validate_plan.md +166 -0
  278. metadata +290 -3
  279. data/app/controllers/api/sessions_controller.rb +0 -25
  280. data/lib/events/subscribers/action_cable_bridge.rb +0 -59
@@ -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
@@ -14,19 +14,25 @@ class SessionChannel < ApplicationCable::Channel
14
14
  MAX_LIST_LIMIT = 50
15
15
 
16
16
  # Subscribes the client to the session-specific stream.
17
- # Rejects the subscription if no valid session_id is provided.
18
- # Transmits the current view_mode and chat history to the subscribing client.
17
+ # When a valid session_id is provided, subscribes to that session.
18
+ # When omitted or zero, resolves to the most recent session (creating
19
+ # one if none exist) — this is the CQRS-compliant path where the
20
+ # server owns session resolution instead of a REST endpoint.
19
21
  #
20
- # @param params [Hash] must include :session_id (positive integer)
22
+ # Always transmits a session_changed signal so the client learns
23
+ # the authoritative session ID, followed by view_mode and history.
24
+ #
25
+ # @param params [Hash] optional :session_id (positive integer)
21
26
  def subscribed
22
- @current_session_id = params[:session_id].to_i
23
- if @current_session_id > 0
24
- stream_from stream_name
25
- transmit_view_mode
26
- transmit_history
27
- else
28
- reject
29
- end
27
+ @current_session_id = resolve_session_id
28
+ stream_from stream_name
29
+
30
+ session = Session.find_by(id: @current_session_id)
31
+ return unless session
32
+
33
+ transmit_session_changed(session)
34
+ transmit_view_mode(session)
35
+ transmit_history(session)
30
36
  end
31
37
 
32
38
  # Receives messages from clients and broadcasts them to all session subscribers.
@@ -37,32 +43,57 @@ class SessionChannel < ApplicationCable::Channel
37
43
  end
38
44
 
39
45
  # Processes user input: persists the message and enqueues LLM processing.
46
+ # When the session is actively processing an agent request, the message
47
+ # is queued as "pending" and picked up after the current loop completes.
40
48
  #
41
49
  # @param data [Hash] must include "content" with the user's message text
42
50
  def speak(data)
43
51
  content = data["content"].to_s.strip
44
- return if content.empty? || !Session.exists?(@current_session_id)
52
+ return if content.empty?
45
53
 
46
- Events::Bus.emit(Events::UserMessage.new(content: content, session_id: @current_session_id))
47
- AgentRequestJob.perform_later(@current_session_id)
54
+ session = Session.find_by(id: @current_session_id)
55
+ return unless session
56
+
57
+ if session.processing?
58
+ Events::Bus.emit(Events::UserMessage.new(content: content, session_id: @current_session_id, status: Event::PENDING_STATUS))
59
+ else
60
+ Events::Bus.emit(Events::UserMessage.new(content: content, session_id: @current_session_id))
61
+ AgentRequestJob.perform_later(@current_session_id)
62
+ end
48
63
  end
49
64
 
50
- # Returns recent sessions with metadata for session picker UI.
65
+ # Recalls the most recent pending message for editing. Deletes the
66
+ # pending event and broadcasts the recall so all clients remove it.
67
+ #
68
+ # @param data [Hash] must include "event_id" (positive integer)
69
+ def recall_pending(data)
70
+ event_id = data["event_id"].to_i
71
+ return if event_id <= 0
72
+
73
+ event = Event.find_by(
74
+ id: event_id,
75
+ session_id: @current_session_id,
76
+ event_type: "user_message",
77
+ status: Event::PENDING_STATUS
78
+ )
79
+ return unless event
80
+
81
+ event.destroy!
82
+ ActionCable.server.broadcast(stream_name, {"action" => "user_message_recalled", "event_id" => event_id})
83
+ end
84
+
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.
51
88
  #
52
89
  # @param data [Hash] optional "limit" (default 10, max 50)
53
90
  def list_sessions(data)
54
91
  limit = (data["limit"] || DEFAULT_LIST_LIMIT).to_i.clamp(1, MAX_LIST_LIMIT)
55
- sessions = Session.recent(limit)
56
- 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
57
95
 
58
- result = sessions.map do |session|
59
- {
60
- id: session.id,
61
- created_at: session.created_at.iso8601,
62
- updated_at: session.updated_at.iso8601,
63
- message_count: counts[session.id] || 0
64
- }
65
- end
96
+ result = sessions.map { |session| serialize_session_with_children(session, counts) }
66
97
  transmit({"action" => "sessions_list", "sessions" => result})
67
98
  end
68
99
 
@@ -86,6 +117,23 @@ class SessionChannel < ApplicationCable::Channel
86
117
  transmit_error("Session not found")
87
118
  end
88
119
 
120
+ # Validates and saves an Anthropic subscription token to encrypted credentials.
121
+ # Format-validated and API-validated before storage. The token never enters the
122
+ # LLM context window — it flows directly from WebSocket to encrypted credentials.
123
+ #
124
+ # @param data [Hash] must include "token" (Anthropic subscription token string)
125
+ def save_token(data)
126
+ token = data["token"].to_s.strip
127
+
128
+ Providers::Anthropic.validate_token_format!(token)
129
+ Providers::Anthropic.validate_token_api!(token)
130
+ write_anthropic_token(token)
131
+
132
+ transmit({"action" => "token_saved"})
133
+ rescue Providers::Anthropic::TokenFormatError, Providers::Anthropic::AuthenticationError => error
134
+ transmit({"action" => "token_error", "message" => error.message})
135
+ end
136
+
89
137
  # Changes the session's view mode and re-broadcasts the viewport.
90
138
  # All clients on the session receive the mode change and fresh history.
91
139
  #
@@ -109,6 +157,41 @@ class SessionChannel < ApplicationCable::Channel
109
157
  "session_#{@current_session_id}"
110
158
  end
111
159
 
160
+ # Resolves the session to subscribe to. Uses the client-provided ID
161
+ # when valid, otherwise falls back to the most recent session or
162
+ # creates a new one.
163
+ #
164
+ # @return [Integer] resolved session ID
165
+ def resolve_session_id
166
+ id = params[:session_id].to_i
167
+ return id if id > 0
168
+
169
+ (Session.recent(1).first || Session.create!).id
170
+ end
171
+
172
+ # Transmits session metadata as a session_changed signal.
173
+ # Used on initial subscription and after session switches so the
174
+ # client can handle both paths with a single code path.
175
+ #
176
+ # Payload: session_id, name, parent_session_id, message_count,
177
+ # view_mode, active_skills, goals.
178
+ #
179
+ # @param session [Session] the session to announce
180
+ # @return [void]
181
+ def transmit_session_changed(session)
182
+ transmit({
183
+ "action" => "session_changed",
184
+ "session_id" => session.id,
185
+ "name" => session.name,
186
+ "parent_session_id" => session.parent_session_id,
187
+ "message_count" => session.events.llm_messages.count,
188
+ "view_mode" => session.view_mode,
189
+ "active_skills" => session.active_skills,
190
+ "active_workflow" => session.active_workflow,
191
+ "goals" => session.goals_summary
192
+ })
193
+ end
194
+
112
195
  # Switches the channel to a different session: stops current stream,
113
196
  # updates the session reference, starts the new stream, and sends
114
197
  # a session_changed signal followed by chat history.
@@ -116,23 +199,18 @@ class SessionChannel < ApplicationCable::Channel
116
199
  stop_all_streams
117
200
  @current_session_id = new_id
118
201
  stream_from stream_name
202
+
119
203
  session = Session.find(new_id)
120
- transmit({
121
- "action" => "session_changed",
122
- "session_id" => new_id,
123
- "message_count" => session.events.llm_messages.count,
124
- "view_mode" => session.view_mode
125
- })
126
- transmit_history
204
+ transmit_session_changed(session)
205
+ transmit_history(session)
127
206
  end
128
207
 
129
208
  # Transmits the current view_mode so the TUI initializes correctly.
130
209
  # Sends `{action: "view_mode", view_mode: <mode>}` to the subscribing client.
210
+ #
211
+ # @param session [Session] the session whose view_mode to transmit
131
212
  # @return [void]
132
- def transmit_view_mode
133
- session = Session.find_by(id: @current_session_id)
134
- return unless session
135
-
213
+ def transmit_view_mode(session)
136
214
  transmit({"action" => "view_mode", "view_mode" => session.view_mode})
137
215
  end
138
216
 
@@ -142,32 +220,64 @@ class SessionChannel < ApplicationCable::Channel
142
220
  # the transmitted payload. Tool events are included so the TUI can
143
221
  # reconstruct tool call counters on reconnect.
144
222
  # In debug mode, prepends the assembled system prompt as a special block.
145
- def transmit_history
146
- session = Session.find_by(id: @current_session_id)
147
- return unless session
148
-
223
+ #
224
+ # Snapshots the viewport so subsequent event broadcasts can compute
225
+ # eviction diffs accurately.
226
+ #
227
+ # @param session [Session] the session whose history to transmit
228
+ def transmit_history(session)
149
229
  transmit_system_prompt(session) if session.view_mode == "debug"
150
230
 
151
- session.viewport_events.each do |event|
152
- transmit(decorate_event_payload(event, session.view_mode))
231
+ each_viewport_event(session) do |event, payload|
232
+ transmit(payload)
153
233
  end
154
234
  end
155
235
 
156
236
  # Broadcasts the re-decorated viewport to all clients on the session stream.
157
237
  # Used after a view mode change to refresh all connected clients.
158
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
+ #
159
243
  # @param session [Session] the session whose viewport to broadcast
160
244
  # @return [void]
161
245
  def broadcast_viewport(session)
162
246
  broadcast_system_prompt(session) if session.view_mode == "debug"
163
247
 
164
- session.viewport_events.each do |event|
165
- 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)
166
250
  end
167
251
  end
168
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)
268
+ end
269
+ end
270
+
271
+ # Decorates an event for transmission to clients. Merges the event's
272
+ # database ID and structured decorator output into the payload.
273
+ # Used by {#transmit_history} and {#broadcast_viewport} for historical
274
+ # and viewport re-broadcast — live broadcasts use {Event::Broadcasting}.
275
+ #
276
+ # @param event [Event] persisted event record
277
+ # @param mode [String] view mode for decoration (default: "basic")
278
+ # @return [Hash] payload with "id" and optional "rendered" key
169
279
  def decorate_event_payload(event, mode = "basic")
170
- payload = event.payload
280
+ payload = event.payload.merge("id" => event.id)
171
281
  decorator = EventDecorator.for(event)
172
282
  return payload unless decorator
173
283
 
@@ -212,6 +322,46 @@ class SessionChannel < ApplicationCable::Channel
212
322
  }
213
323
  end
214
324
 
325
+ # Merges the Anthropic subscription token into encrypted credentials,
326
+ # preserving existing keys (e.g. secret_key_base).
327
+ #
328
+ # @param token [String] validated Anthropic subscription token
329
+ # @return [void]
330
+ def write_anthropic_token(token)
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
+ }
360
+ end
361
+
362
+ entry
363
+ end
364
+
215
365
  def transmit_error(message)
216
366
  transmit({"action" => "error", "message" => message})
217
367
  end
@@ -3,22 +3,33 @@
3
3
  # Decorates user_message events for display in the TUI.
4
4
  # Basic mode returns role and content. Verbose mode adds a timestamp.
5
5
  # Debug mode adds token count (exact when counted, estimated when not).
6
+ # Pending messages include `status: "pending"` so the TUI renders them
7
+ # with a visual indicator (dimmed, clock icon).
6
8
  class UserMessageDecorator < EventDecorator
7
9
  # @return [Hash] structured user message data
8
- # `{role: :user, content: String}`
10
+ # `{role: :user, content: String}` or with `status: "pending"` when queued
9
11
  def render_basic
10
- {role: :user, content: content}
12
+ base = {role: :user, content: content}
13
+ base[:status] = "pending" if pending?
14
+ base
11
15
  end
12
16
 
13
17
  # @return [Hash] structured user message with nanosecond timestamp
14
- # `{role: :user, content: String, timestamp: Integer|nil}`
15
18
  def render_verbose
16
- {role: :user, content: content, timestamp: timestamp}
19
+ base = {role: :user, content: content, timestamp: timestamp}
20
+ base[:status] = "pending" if pending?
21
+ base
17
22
  end
18
23
 
19
24
  # @return [Hash] verbose output plus token count for debugging
20
- # `{role: :user, content: String, timestamp: Integer|nil, tokens: Integer, estimated: Boolean}`
21
25
  def render_debug
22
26
  render_verbose.merge(token_info)
23
27
  end
28
+
29
+ private
30
+
31
+ # @return [Boolean] true when this message is queued but not yet sent to LLM
32
+ def pending?
33
+ payload["status"] == Event::PENDING_STATUS
34
+ end
24
35
  end
@@ -26,23 +26,76 @@ class AgentRequestJob < ApplicationJob
26
26
 
27
27
  discard_on ActiveRecord::RecordNotFound
28
28
  discard_on Providers::Anthropic::AuthenticationError do |job, error|
29
+ session_id = job.arguments.first
30
+ # Persistent system message for the event log
29
31
  Events::Bus.emit(Events::SystemMessage.new(
30
32
  content: "Authentication failed: #{error.message}",
31
- session_id: job.arguments.first
33
+ session_id: session_id
32
34
  ))
35
+ # Transient signal to trigger TUI token setup popup (not persisted)
36
+ ActionCable.server.broadcast(
37
+ "session_#{session_id}",
38
+ {"action" => "authentication_required", "message" => error.message}
39
+ )
33
40
  end
34
41
 
35
42
  # @param session_id [Integer] ID of the session to process
36
43
  def perform(session_id)
37
44
  session = Session.find(session_id)
45
+
46
+ # Atomic: only one job processes a session at a time. If another job
47
+ # is already running, this one exits — the running job will pick up
48
+ # any pending messages after its current loop completes.
49
+ return unless claim_processing(session_id)
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
+
38
55
  agent_loop = AgentLoop.new(session: session)
39
- agent_loop.run
56
+ loop do
57
+ agent_loop.run
58
+ promoted = session.promote_pending_messages!
59
+ break if promoted == 0
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!
40
65
  ensure
66
+ release_processing(session_id)
41
67
  agent_loop&.finalize
42
68
  end
43
69
 
44
70
  private
45
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
+
88
+ # Sets the session's processing flag atomically. Returns true if this
89
+ # job claimed the lock, false if another job already holds it.
90
+ def claim_processing(session_id)
91
+ Session.where(id: session_id, processing: false).update_all(processing: true) == 1
92
+ end
93
+
94
+ # Clears the processing flag so the session can accept new jobs.
95
+ def release_processing(session_id)
96
+ Session.where(id: session_id).update_all(processing: false)
97
+ end
98
+
46
99
  # Emits a system message before each retry so the user sees
47
100
  # "retrying..." instead of nothing.
48
101
  def retry_job(options = {})
@@ -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