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.
- checksums.yaml +4 -4
- data/.reek.yml +27 -1
- data/CHANGELOG.md +4 -0
- data/README.md +219 -25
- data/agents/codebase-analyzer.md +88 -0
- data/agents/codebase-pattern-finder.md +83 -0
- data/agents/documentation-researcher.md +59 -0
- data/agents/thoughts-analyzer.md +102 -0
- data/agents/web-search-researcher.md +71 -0
- data/anima-core.gemspec +3 -0
- data/app/channels/session_channel.rb +76 -28
- data/app/jobs/agent_request_job.rb +24 -0
- data/app/jobs/analytical_brain_job.rb +33 -0
- data/app/jobs/count_event_tokens_job.rb +1 -1
- data/app/models/concerns/event/broadcasting.rb +20 -2
- data/app/models/event.rb +1 -1
- data/app/models/goal.rb +91 -0
- data/app/models/session.rb +347 -22
- data/config/application.rb +2 -0
- data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
- data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
- data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
- data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
- data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
- data/db/migrate/20260315140843_create_goals.rb +16 -0
- data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
- data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
- data/lib/agent_loop.rb +65 -9
- data/lib/agents/definition.rb +116 -0
- data/lib/agents/registry.rb +106 -0
- data/lib/analytical_brain/runner.rb +276 -0
- data/lib/analytical_brain/tools/activate_skill.rb +52 -0
- data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
- data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
- data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
- data/lib/analytical_brain/tools/finish_goal.rb +62 -0
- data/lib/analytical_brain/tools/read_workflow.rb +58 -0
- data/lib/analytical_brain/tools/rename_session.rb +63 -0
- data/lib/analytical_brain/tools/set_goal.rb +60 -0
- data/lib/analytical_brain/tools/update_goal.rb +60 -0
- data/lib/analytical_brain.rb +23 -0
- data/lib/anima/cli/mcp/secrets.rb +76 -0
- data/lib/anima/cli/mcp.rb +197 -0
- data/lib/anima/cli.rb +4 -0
- data/lib/anima/installer.rb +168 -0
- data/lib/anima/settings.rb +226 -0
- data/lib/anima/version.rb +1 -1
- data/lib/anima.rb +9 -0
- data/lib/credential_store.rb +103 -0
- data/lib/environment_probe.rb +232 -0
- data/lib/llm/client.rb +29 -10
- data/lib/mcp/client_manager.rb +86 -0
- data/lib/mcp/config.rb +213 -0
- data/lib/mcp/health_check.rb +77 -0
- data/lib/mcp/secrets.rb +73 -0
- data/lib/mcp/stdio_transport.rb +206 -0
- data/lib/providers/anthropic.rb +8 -7
- data/lib/shell_session.rb +11 -10
- data/lib/skills/definition.rb +97 -0
- data/lib/skills/registry.rb +105 -0
- data/lib/tools/edit.rb +3 -4
- data/lib/tools/mcp_tool.rb +114 -0
- data/lib/tools/read.rb +15 -16
- data/lib/tools/registry.rb +14 -12
- data/lib/tools/request_feature.rb +121 -0
- data/lib/tools/return_result.rb +81 -0
- data/lib/tools/spawn_specialist.rb +109 -0
- data/lib/tools/spawn_subagent.rb +111 -0
- data/lib/tools/subagent_prompts.rb +12 -0
- data/lib/tools/web_get.rb +8 -9
- data/lib/tui/app.rb +332 -43
- data/lib/tui/message_store.rb +20 -0
- data/lib/tui/screens/chat.rb +207 -20
- data/lib/workflows/definition.rb +97 -0
- data/lib/workflows/registry.rb +89 -0
- data/skills/activerecord/SKILL.md +255 -0
- data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
- data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
- data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
- data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
- data/skills/activerecord/examples/associations/self_referential.rb +302 -0
- data/skills/activerecord/examples/associations/through_associations.rb +203 -0
- data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
- data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
- data/skills/activerecord/examples/basics/inheritance.rb +377 -0
- data/skills/activerecord/examples/basics/type_casting.rb +317 -0
- data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
- data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
- data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
- data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
- data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
- data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
- data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
- data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
- data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
- data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
- data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
- data/skills/activerecord/examples/querying/optimization.rb +275 -0
- data/skills/activerecord/examples/querying/scopes.rb +260 -0
- data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
- data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
- data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
- data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
- data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
- data/skills/activerecord/references/associations.md +709 -0
- data/skills/activerecord/references/basics.md +622 -0
- data/skills/activerecord/references/callbacks.md +738 -0
- data/skills/activerecord/references/migrations.md +657 -0
- data/skills/activerecord/references/querying.md +655 -0
- data/skills/activerecord/references/validations.md +596 -0
- data/skills/dragonruby/SKILL.md +250 -0
- data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
- data/skills/dragonruby/examples/audio/background_music.rb +29 -0
- data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
- data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
- data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
- data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
- data/skills/dragonruby/examples/core/hello_world.rb +24 -0
- data/skills/dragonruby/examples/core/labels.rb +22 -0
- data/skills/dragonruby/examples/core/sprites.rb +35 -0
- data/skills/dragonruby/examples/core/state_management.rb +29 -0
- data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
- data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
- data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
- data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
- data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
- data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
- data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
- data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
- data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
- data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
- data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
- data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
- data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
- data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
- data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
- data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
- data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
- data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
- data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
- data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
- data/skills/dragonruby/examples/input/controller_input.rb +28 -0
- data/skills/dragonruby/examples/input/directional_input.rb +24 -0
- data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
- data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
- data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
- data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
- data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
- data/skills/dragonruby/examples/rendering/labels.rb +32 -0
- data/skills/dragonruby/examples/rendering/layering.rb +51 -0
- data/skills/dragonruby/examples/rendering/solids.rb +61 -0
- data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
- data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
- data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
- data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
- data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
- data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
- data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
- data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
- data/skills/dragonruby/references/audio.md +396 -0
- data/skills/dragonruby/references/core.md +385 -0
- data/skills/dragonruby/references/distribution.md +434 -0
- data/skills/dragonruby/references/entities.md +516 -0
- data/skills/dragonruby/references/game-logic/persistence.md +386 -0
- data/skills/dragonruby/references/game-logic/state.md +389 -0
- data/skills/dragonruby/references/input.md +414 -0
- data/skills/dragonruby/references/rendering/animation.md +467 -0
- data/skills/dragonruby/references/rendering/primitives.md +403 -0
- data/skills/dragonruby/references/scenes.md +443 -0
- data/skills/draper-decorators/SKILL.md +344 -0
- data/skills/draper-decorators/examples/application_decorator.rb +61 -0
- data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
- data/skills/draper-decorators/examples/model_decorator.rb +152 -0
- data/skills/draper-decorators/references/anti-patterns.md +640 -0
- data/skills/draper-decorators/references/patterns.md +507 -0
- data/skills/draper-decorators/references/testing.md +559 -0
- data/skills/gh-issue.md +182 -0
- data/skills/mcp-server/SKILL.md +177 -0
- data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
- data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
- data/skills/mcp-server/examples/http_client.rb +48 -0
- data/skills/mcp-server/examples/http_server.rb +97 -0
- data/skills/mcp-server/examples/rails_integration.rb +88 -0
- data/skills/mcp-server/examples/stdio_server.rb +108 -0
- data/skills/mcp-server/examples/streaming_client.rb +95 -0
- data/skills/mcp-server/references/gotchas.md +183 -0
- data/skills/mcp-server/references/prompts.md +98 -0
- data/skills/mcp-server/references/resources.md +53 -0
- data/skills/mcp-server/references/server.md +140 -0
- data/skills/mcp-server/references/tools.md +146 -0
- data/skills/mcp-server/references/transport.md +104 -0
- data/skills/ratatui-ruby/SKILL.md +315 -0
- data/skills/ratatui-ruby/references/core-concepts.md +340 -0
- data/skills/ratatui-ruby/references/events.md +387 -0
- data/skills/ratatui-ruby/references/frameworks.md +522 -0
- data/skills/ratatui-ruby/references/layout.md +423 -0
- data/skills/ratatui-ruby/references/styling.md +268 -0
- data/skills/ratatui-ruby/references/testing.md +433 -0
- data/skills/ratatui-ruby/references/widgets.md +532 -0
- data/skills/rspec/SKILL.md +340 -0
- data/skills/rspec/examples/core/basic_structure.rb +69 -0
- data/skills/rspec/examples/core/configuration.rb +126 -0
- data/skills/rspec/examples/core/hooks.rb +126 -0
- data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
- data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
- data/skills/rspec/examples/core/shared_examples.rb +145 -0
- data/skills/rspec/examples/factory_bot/associations.rb +314 -0
- data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
- data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
- data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
- data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
- data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
- data/skills/rspec/examples/factory_bot/traits.rb +293 -0
- data/skills/rspec/examples/factory_bot/transients.rb +229 -0
- data/skills/rspec/examples/matchers/change.rb +115 -0
- data/skills/rspec/examples/matchers/collections.rb +154 -0
- data/skills/rspec/examples/matchers/comparisons.rb +79 -0
- data/skills/rspec/examples/matchers/composing.rb +155 -0
- data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
- data/skills/rspec/examples/matchers/equality.rb +58 -0
- data/skills/rspec/examples/matchers/errors.rb +136 -0
- data/skills/rspec/examples/matchers/output.rb +103 -0
- data/skills/rspec/examples/matchers/predicates.rb +87 -0
- data/skills/rspec/examples/matchers/truthiness.rb +101 -0
- data/skills/rspec/examples/matchers/types.rb +82 -0
- data/skills/rspec/examples/matchers/yield.rb +147 -0
- data/skills/rspec/examples/mocks/any_instance.rb +172 -0
- data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
- data/skills/rspec/examples/mocks/constants.rb +177 -0
- data/skills/rspec/examples/mocks/doubles.rb +139 -0
- data/skills/rspec/examples/mocks/expectations.rb +137 -0
- data/skills/rspec/examples/mocks/message_chains.rb +173 -0
- data/skills/rspec/examples/mocks/ordering.rb +144 -0
- data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
- data/skills/rspec/examples/mocks/responses.rb +223 -0
- data/skills/rspec/examples/mocks/spies.rb +149 -0
- data/skills/rspec/examples/mocks/stubbing.rb +133 -0
- data/skills/rspec/examples/rails/channels.rb +250 -0
- data/skills/rspec/examples/rails/controller_specs.rb +302 -0
- data/skills/rspec/examples/rails/helper_specs.rb +245 -0
- data/skills/rspec/examples/rails/job_specs.rb +256 -0
- data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
- data/skills/rspec/examples/rails/matchers.rb +374 -0
- data/skills/rspec/examples/rails/model_specs.rb +193 -0
- data/skills/rspec/examples/rails/request_specs.rb +275 -0
- data/skills/rspec/examples/rails/routing_specs.rb +276 -0
- data/skills/rspec/examples/rails/system_specs.rb +294 -0
- data/skills/rspec/examples/rails/transactions.rb +254 -0
- data/skills/rspec/examples/rails/view_specs.rb +252 -0
- data/skills/rspec/references/core.md +816 -0
- data/skills/rspec/references/factory_bot.md +641 -0
- data/skills/rspec/references/matchers.md +516 -0
- data/skills/rspec/references/mocks.md +381 -0
- data/skills/rspec/references/rails.md +528 -0
- data/templates/soul.md +40 -0
- data/workflows/commit.md +45 -0
- data/workflows/create_handoff.md +98 -0
- data/workflows/create_note.md +82 -0
- data/workflows/create_plan.md +457 -0
- data/workflows/decompose_ticket.md +109 -0
- data/workflows/feature.md +91 -0
- data/workflows/implement_plan.md +87 -0
- data/workflows/iterate_plan.md +247 -0
- data/workflows/research_codebase.md +210 -0
- data/workflows/resume_handoff.md +217 -0
- data/workflows/review_pr.md +320 -0
- data/workflows/thoughts_init.md +71 -0
- data/workflows/validate_plan.md +166 -0
- 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
|
-
|
|
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
|
|
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
|
|
225
|
-
transmit(
|
|
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
|
|
238
|
-
ActionCable.server.broadcast(stream_name,
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
309
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
data/app/models/goal.rb
ADDED
|
@@ -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
|