anima-core 1.2.0 → 1.4.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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/.reek.yml +14 -8
  3. data/README.md +96 -23
  4. data/agents/codebase-analyzer.md +1 -1
  5. data/agents/codebase-pattern-finder.md +1 -1
  6. data/agents/documentation-researcher.md +1 -1
  7. data/agents/thoughts-analyzer.md +1 -1
  8. data/agents/web-search-researcher.md +2 -2
  9. data/app/channels/session_channel.rb +53 -35
  10. data/app/decorators/tool_call_decorator.rb +7 -7
  11. data/app/decorators/user_message_decorator.rb +3 -17
  12. data/app/jobs/agent_request_job.rb +15 -6
  13. data/app/jobs/passive_recall_job.rb +6 -11
  14. data/app/models/concerns/message/broadcasting.rb +1 -0
  15. data/app/models/goal.rb +14 -0
  16. data/app/models/message.rb +13 -31
  17. data/app/models/pending_message.rb +191 -0
  18. data/app/models/secret.rb +72 -0
  19. data/app/models/session.rb +480 -271
  20. data/bin/inspect-cassette +144 -0
  21. data/bin/release +212 -0
  22. data/bin/with-llms +20 -0
  23. data/config/database.yml +1 -0
  24. data/config/environments/test.rb +5 -0
  25. data/config/initializers/time_nanoseconds.rb +11 -0
  26. data/db/cable_structure.sql +9 -0
  27. data/db/migrate/20260328100000_create_secrets.rb +15 -0
  28. data/db/migrate/20260328152142_add_evicted_at_to_goals.rb +6 -0
  29. data/db/migrate/20260329120000_create_pending_messages.rb +11 -0
  30. data/db/migrate/20260330120000_add_source_to_pending_messages.rb +8 -0
  31. data/db/migrate/20260401180000_add_api_metrics_to_messages.rb +7 -0
  32. data/db/migrate/20260401210935_remove_recalled_message_ids_from_sessions.rb +5 -0
  33. data/db/migrate/20260403080031_add_initial_cwd_to_sessions.rb +5 -0
  34. data/db/queue_structure.sql +61 -0
  35. data/db/structure.sql +120 -0
  36. data/lib/agent_loop.rb +53 -51
  37. data/lib/agents/definition.rb +1 -1
  38. data/lib/analytical_brain/runner.rb +19 -6
  39. data/lib/analytical_brain/tools/activate_skill.rb +2 -2
  40. data/lib/analytical_brain/tools/assign_nickname.rb +1 -1
  41. data/lib/analytical_brain/tools/deactivate_skill.rb +2 -1
  42. data/lib/analytical_brain/tools/deactivate_workflow.rb +2 -1
  43. data/lib/analytical_brain/tools/finish_goal.rb +3 -0
  44. data/lib/analytical_brain/tools/goal_messaging.rb +28 -0
  45. data/lib/analytical_brain/tools/read_workflow.rb +2 -2
  46. data/lib/analytical_brain/tools/set_goal.rb +5 -1
  47. data/lib/analytical_brain/tools/update_goal.rb +5 -1
  48. data/lib/anima/cli/mcp/secrets.rb +4 -4
  49. data/lib/anima/cli/mcp.rb +4 -4
  50. data/lib/anima/cli.rb +41 -13
  51. data/lib/anima/installer.rb +20 -1
  52. data/lib/anima/settings.rb +37 -2
  53. data/lib/anima/version.rb +1 -1
  54. data/lib/anima.rb +1 -1
  55. data/lib/credential_store.rb +17 -66
  56. data/lib/events/agent_message.rb +14 -0
  57. data/lib/events/base.rb +1 -1
  58. data/lib/events/subscribers/persister.rb +12 -18
  59. data/lib/events/subscribers/subagent_message_router.rb +18 -9
  60. data/lib/events/user_message.rb +2 -13
  61. data/lib/llm/client.rb +91 -50
  62. data/lib/mcp/config.rb +2 -2
  63. data/lib/mcp/secrets.rb +7 -8
  64. data/lib/mneme/compressed_viewport.rb +9 -5
  65. data/lib/mneme/passive_recall.rb +85 -16
  66. data/lib/mneme/runner.rb +15 -4
  67. data/lib/providers/anthropic.rb +112 -7
  68. data/lib/shell_session.rb +239 -18
  69. data/lib/tools/base.rb +22 -0
  70. data/lib/tools/bash.rb +61 -7
  71. data/lib/tools/edit.rb +2 -2
  72. data/lib/tools/mark_goal_completed.rb +85 -0
  73. data/lib/tools/read.rb +2 -1
  74. data/lib/tools/recall.rb +98 -0
  75. data/lib/tools/registry.rb +41 -7
  76. data/lib/tools/remember.rb +1 -1
  77. data/lib/tools/response_truncator.rb +70 -0
  78. data/lib/tools/spawn_specialist.rb +11 -8
  79. data/lib/tools/spawn_subagent.rb +19 -13
  80. data/lib/tools/subagent_prompts.rb +41 -5
  81. data/lib/tools/think.rb +23 -0
  82. data/lib/tools/write.rb +1 -1
  83. data/lib/tui/app.rb +545 -137
  84. data/lib/tui/braille_spinner.rb +152 -0
  85. data/lib/tui/cable_client.rb +13 -20
  86. data/lib/tui/decorators/base_decorator.rb +40 -11
  87. data/lib/tui/decorators/bash_decorator.rb +3 -3
  88. data/lib/tui/decorators/edit_decorator.rb +7 -4
  89. data/lib/tui/decorators/read_decorator.rb +6 -8
  90. data/lib/tui/decorators/think_decorator.rb +4 -6
  91. data/lib/tui/decorators/web_get_decorator.rb +4 -3
  92. data/lib/tui/decorators/write_decorator.rb +7 -4
  93. data/lib/tui/flash.rb +19 -14
  94. data/lib/tui/formatting.rb +33 -0
  95. data/lib/tui/input_buffer.rb +6 -6
  96. data/lib/tui/message_store.rb +159 -27
  97. data/lib/tui/performance_logger.rb +2 -3
  98. data/lib/tui/screens/chat.rb +302 -103
  99. data/lib/tui/settings.rb +86 -0
  100. data/skills/activerecord/SKILL.md +1 -1
  101. data/skills/dragonruby/SKILL.md +1 -1
  102. data/skills/draper-decorators/SKILL.md +1 -1
  103. data/skills/gh-issue.md +1 -1
  104. data/skills/mcp-server/SKILL.md +1 -1
  105. data/skills/ratatui-ruby/SKILL.md +1 -1
  106. data/skills/rspec/SKILL.md +1 -1
  107. data/templates/config.toml +30 -1
  108. data/templates/tui.toml +209 -0
  109. metadata +24 -3
  110. data/config/initializers/fts5_schema_dump.rb +0 -21
  111. data/lib/environment_probe.rb +0 -232
@@ -1,232 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "etc"
4
- require "open3"
5
- require "timeout"
6
- require "json"
7
- require "pathname"
8
- require "uri"
9
-
10
- # Probes the shell environment and assembles a lightweight metadata block
11
- # for injection into the system prompt. Gives the agent awareness of its
12
- # working directory, OS, Git status, and nearby project files — without
13
- # loading any file content.
14
- #
15
- # @example
16
- # EnvironmentProbe.to_prompt("/home/user/projects/my-app")
17
- # # => "## Environment\n\nOS: Arch Linux (pacman, yay)\n..."
18
- class EnvironmentProbe
19
- # Assembles the environment context block for a given working directory.
20
- #
21
- # @param pwd [String, nil] current working directory
22
- # @return [String, nil] Markdown-formatted environment block, or nil when pwd is unknown
23
- def self.to_prompt(pwd)
24
- new(pwd).to_prompt
25
- end
26
-
27
- # @param pwd [String, nil] current working directory
28
- def initialize(pwd)
29
- @pwd = pwd
30
- end
31
-
32
- # @return [String, nil] Markdown-formatted environment block
33
- def to_prompt
34
- return unless @pwd
35
-
36
- sections = [os_section, working_directory_section, project_files_section].compact
37
- return if sections.empty?
38
-
39
- "## Environment\n\n#{sections.join("\n\n")}"
40
- end
41
-
42
- private
43
-
44
- # @return [String] OS name with package manager hint
45
- def os_section
46
- sysname = Etc.uname[:sysname]
47
- "OS: #{format_os(sysname)}"
48
- end
49
-
50
- # @param sysname [String] kernel name from uname (e.g. "Linux", "Darwin")
51
- # @return [String] human-readable OS description
52
- def format_os(sysname)
53
- case sysname
54
- when "Linux"
55
- distro = detect_linux_distro || "Linux"
56
- pkg = detect_package_manager
57
- pkg ? "#{distro} (#{pkg})" : distro
58
- when "Darwin"
59
- "macOS (Homebrew)"
60
- else
61
- sysname
62
- end
63
- end
64
-
65
- # Reads PRETTY_NAME from /etc/os-release.
66
- #
67
- # @return [String, nil] distro name, or nil on non-Linux / missing file
68
- def detect_linux_distro
69
- return unless File.exist?("/etc/os-release")
70
-
71
- File.foreach("/etc/os-release") do |line|
72
- if line.start_with?("PRETTY_NAME=")
73
- return line.split("=", 2).last.strip.delete('"')
74
- end
75
- end
76
- nil
77
- end
78
-
79
- # Returns the primary package manager(s) for the current system.
80
- # Arch-based systems list both pacman and yay when present;
81
- # other families return the first match.
82
- #
83
- # @return [String, nil] comma-separated package manager names
84
- def detect_package_manager
85
- managers = []
86
- managers << "pacman" if File.exist?("/usr/bin/pacman")
87
- managers << "yay" if File.exist?("/usr/bin/yay")
88
- return managers.join(", ") if managers.any?
89
-
90
- return "apt" if File.exist?("/usr/bin/apt")
91
- return "dnf" if File.exist?("/usr/bin/dnf")
92
- return "Homebrew" if File.exist?("/opt/homebrew/bin/brew") || File.exist?("/usr/local/bin/brew")
93
-
94
- nil
95
- end
96
-
97
- # @return [String] CWD line plus optional Git metadata
98
- def working_directory_section
99
- lines = ["CWD: #{@pwd}"]
100
- append_git_lines(lines)
101
- lines.join("\n")
102
- end
103
-
104
- # Appends Git metadata lines (remote, branch, PR) to the output array.
105
- #
106
- # @param lines [Array<String>] accumulator for output lines
107
- # @return [void]
108
- def append_git_lines(lines)
109
- git = detect_git
110
- return unless git
111
-
112
- remote = git[:remote]
113
- branch = git[:branch]
114
- pr_number = git[:pr_number]
115
-
116
- lines << "Git: #{git[:repo_name]} (#{remote})" if remote
117
- lines << "Branch: #{branch}" if branch
118
- lines << "PR: ##{pr_number} (#{git[:pr_state]})" if pr_number
119
- end
120
-
121
- # Detects Git repo metadata: remote, branch, and open PR.
122
- #
123
- # @return [Hash{Symbol => String}, nil] keys: :remote, :repo_name, :branch,
124
- # and optionally :pr_number (Integer) and :pr_state (String); nil when not in a repo
125
- def detect_git
126
- _, status = Open3.capture2("git", "-C", @pwd, "rev-parse", "--is-inside-work-tree", err: File::NULL)
127
- return unless status.success?
128
-
129
- info = {}
130
- detect_git_remote(info)
131
- detect_git_branch(info)
132
- info
133
- rescue Errno::ENOENT
134
- nil
135
- end
136
-
137
- # Populates :remote and :repo_name on the info hash.
138
- def detect_git_remote(info)
139
- remote, = Open3.capture2("git", "-C", @pwd, "remote", "get-url", "origin", err: File::NULL)
140
- remote = remote.strip
141
- return unless remote.present?
142
-
143
- info[:remote] = remote
144
- info[:repo_name] = extract_repo_name(remote)
145
- end
146
-
147
- # Populates :branch, :pr_number, and :pr_state on the info hash.
148
- def detect_git_branch(info)
149
- branch, = Open3.capture2("git", "-C", @pwd, "rev-parse", "--abbrev-ref", "HEAD", err: File::NULL)
150
- branch = branch.strip
151
- return unless branch.present?
152
-
153
- info[:branch] = branch
154
- pr = detect_pr(branch)
155
- info.merge!(pr) if pr
156
- end
157
-
158
- # Extracts owner/repo from a Git remote URL.
159
- #
160
- # @param remote_url [String] SSH or HTTPS remote URL
161
- # @return [String] "owner/repo" path, or the raw URL when parsing fails
162
- def extract_repo_name(remote_url)
163
- path = if remote_url.match?(%r{\A\w+://})
164
- URI.parse(remote_url).path
165
- else
166
- # SSH format: git@host:owner/repo.git
167
- remote_url.split(":").last
168
- end
169
- path.delete_prefix("/").delete_suffix(".git")
170
- rescue URI::InvalidURIError
171
- remote_url
172
- end
173
-
174
- # Queries GitHub for an open PR on the given branch via the gh CLI.
175
- #
176
- # @param branch [String] branch name
177
- # @return [Hash, nil] with :pr_number and :pr_state, or nil
178
- # @note Returns nil on timeout, missing gh CLI, or JSON parse errors
179
- def detect_pr(branch)
180
- Timeout.timeout(Anima::Settings.web_request_timeout) do
181
- output, status = Open3.capture2(
182
- "gh", "pr", "list", "--head", branch,
183
- "--json", "number,state", "--limit", "1",
184
- chdir: @pwd, err: File::NULL
185
- )
186
- return unless status.success?
187
-
188
- pr = JSON.parse(output).first
189
- return unless pr
190
-
191
- {pr_number: pr["number"], pr_state: pr["state"].downcase}
192
- end
193
- rescue Timeout::Error, Errno::ENOENT, JSON::ParserError
194
- nil
195
- end
196
-
197
- # Scans for well-known project files up to a configurable depth.
198
- #
199
- # @return [String, nil] project files section, or nil when none found
200
- def project_files_section
201
- found = scan_project_files
202
- return if found.empty?
203
-
204
- header = "Project files that may contain useful context:"
205
- entries = found.map { |path| "- #{path}" }
206
- [header, *entries, "Use read_file to examine these when needed."].join("\n")
207
- end
208
-
209
- # Scans the working directory for whitelisted filenames.
210
- #
211
- # @return [Array<String>] sorted relative paths
212
- def scan_project_files
213
- base = Pathname.new(@pwd)
214
-
215
- glob_patterns.flat_map { |pattern| Dir.glob(pattern) }
216
- .map { |full_path| Pathname.new(full_path).relative_path_from(base).to_s }
217
- .sort
218
- .uniq
219
- end
220
-
221
- # Builds glob patterns for each whitelisted filename at each depth level.
222
- #
223
- # @return [Array<String>] glob patterns
224
- def glob_patterns
225
- whitelist = Anima::Settings.project_files_whitelist
226
- max_depth = Anima::Settings.project_files_max_depth
227
-
228
- whitelist.product((0..max_depth).to_a).map do |filename, depth|
229
- File.join(@pwd, Array.new(depth, "*"), filename)
230
- end
231
- end
232
- end