samagotchi 0.35.0 → 0.37.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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +57 -1
  3. data/README.md +2 -2
  4. data/bin/chi +1 -1
  5. data/docs/broadcast.md +91 -12
  6. data/docs/configuration.md +8 -1
  7. data/docs/context.md +37 -0
  8. data/docs/guardrails.md +26 -3
  9. data/docs/hooks.md +1 -1
  10. data/docs/memory.md +3 -0
  11. data/docs/plugins.md +51 -4
  12. data/lib/samagotchi/broadcast/triage.rb +267 -0
  13. data/lib/samagotchi/broadcast/triage_log.rb +165 -0
  14. data/lib/samagotchi/broadcast/triage_model.rb +56 -0
  15. data/lib/samagotchi/broadcast_command.rb +273 -69
  16. data/lib/samagotchi/bundles/coordinator/manifest.yml +7 -6
  17. data/lib/samagotchi/bundles/coordinator/plugin.rb +72 -4
  18. data/lib/samagotchi/bundles/coordinator/skill_coordinator.md +59 -11
  19. data/lib/samagotchi/bundles/github-pr/manifest.yml +3 -3
  20. data/lib/samagotchi/bundles/github-pr/plugin.rb +444 -3
  21. data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +11 -0
  22. data/lib/samagotchi/bundles/guardrails/manifest.yml +5 -4
  23. data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
  24. data/lib/samagotchi/bundles/mcp/plugin.rb +72 -27
  25. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  26. data/lib/samagotchi/bundles/system/memory_guide.md +4 -2
  27. data/lib/samagotchi/cli/command.rb +27 -3
  28. data/lib/samagotchi/config.rb +9 -0
  29. data/lib/samagotchi/context_command.rb +0 -17
  30. data/lib/samagotchi/engine.rb +4 -6
  31. data/lib/samagotchi/guardrail_wiring.rb +7 -2
  32. data/lib/samagotchi/guardrails/approval.rb +5 -2
  33. data/lib/samagotchi/guardrails/approvals.rb +9 -5
  34. data/lib/samagotchi/guardrails/parent_approvals.rb +6 -0
  35. data/lib/samagotchi/guardrails/rules.rb +19 -6
  36. data/lib/samagotchi/guardrails/targets.rb +83 -11
  37. data/lib/samagotchi/idle_client.rb +48 -28
  38. data/lib/samagotchi/idle_recap.rb +6 -5
  39. data/lib/samagotchi/idle_target.rb +95 -0
  40. data/lib/samagotchi/llm/openai_chat.rb +29 -3
  41. data/lib/samagotchi/log_line.rb +1 -1
  42. data/lib/samagotchi/memory_bundle/index_updater.rb +6 -3
  43. data/lib/samagotchi/note_command.rb +0 -21
  44. data/lib/samagotchi/recap_setup.rb +38 -52
  45. data/lib/samagotchi/send_command.rb +0 -22
  46. data/lib/samagotchi/session_commands.rb +1 -0
  47. data/lib/samagotchi/tool_activity.rb +14 -3
  48. data/lib/samagotchi/tool_declarations.rb +8 -4
  49. data/lib/samagotchi/tools/builtins.rb +1 -1
  50. data/lib/samagotchi/tools/memory.rb +103 -2
  51. data/lib/samagotchi/version.rb +1 -1
  52. data/lib/samagotchi/web/public/question_card.js +4 -3
  53. metadata +5 -1
@@ -3,6 +3,7 @@
3
3
  require_relative "config"
4
4
  require_relative "log"
5
5
  require_relative "idle_recap"
6
+ require_relative "idle_target"
6
7
  require_relative "recap_store"
7
8
 
8
9
  module Samagotchi
@@ -41,60 +42,21 @@ module Samagotchi
41
42
  base_url = string_config(kwarg_config, :base_url) || registry_string("recap.base_url")
42
43
  host_ref = string_config(kwarg_config, :host_ref) || string_config(kwarg_config, :host) || registry_string("recap.host_ref")
43
44
  model = string_config(kwarg_config, :model) || registry_string("recap.model")
44
- label = model
45
45
 
46
- # The model as any model ref (ModelRef): its alias applied once, and
47
- # its host (own or the alias's) picks the recap host when host_ref
48
- # doesn't.
49
- parsed = model && host_registry.model_ref(model)
50
- target = nil
51
- if base_url.nil? && host_ref.nil? && model.nil?
52
- target = -> { session_target.call }
53
- elsif base_url.nil? && host_ref.nil? && !parsed.host_name
54
- # A model naming no host goes where a bare --model goes
55
- # (HostRegistry#host_for_model: the default host unless another's
56
- # list has it), resolved at each attempt.
57
- target = lambda do
58
- resolved = host_registry.resolve(model)
59
- { base_url: resolved.openai_base_url, api_key_env: resolved.entry.api_key_env,
60
- model: resolved.bare_model, label: label.to_s.strip }
61
- end
62
- else
63
- host_ref ||= parsed.host_name if parsed && base_url.nil?
64
- # If host_ref given, derive base_url (the host's OpenAI base) and its
65
- # API key variable from the host_registry entry
66
- api_key_env = nil
67
- if host_ref && !host_ref.empty?
68
- entry = host_registry.find_entry(host_ref)
69
- if entry
70
- base_url = entry.openai_base_url
71
- api_key_env = entry.api_key_env
72
- named = parsed&.host_conflict || parsed&.host_name
73
- if named && named != entry.name
74
- Log.warn(:recap, "model_host_mismatch",
75
- echo: "Warning: recap model '#{model}' names host '#{named}', not recap.host_ref '#{host_ref}'; recap disabled.",
76
- model: model, host_ref: host_ref)
77
- return nil
78
- end
79
- model = parsed.id if parsed
80
- else
81
- Log.warn(:recap, "host_ref_unknown", echo: "Warning: recap host_ref '#{host_ref}' not found in hosts:; recap disabled.", host_ref: host_ref)
82
- return nil
83
- end
84
- elsif parsed && !parsed.host_name
85
- model = parsed.id
86
- end
46
+ settings = { model: model, host_ref: host_ref, base_url: base_url, host_registry: host_registry }
47
+ target = if base_url.nil? && host_ref.nil? && model.nil?
48
+ -> { session_target.call }
49
+ elsif !IdleTarget.host_named?(**settings)
50
+ # A model naming no host goes where a bare --model goes
51
+ # (HostRegistry#host_for_model: the default host unless
52
+ # another's list has it), resolved at each attempt.
53
+ -> { IdleTarget.resolve(**settings) }
54
+ else
55
+ fixed = fixed_target(settings)
56
+ return nil unless fixed
87
57
 
88
- if base_url.to_s.strip.empty? || model.to_s.strip.empty?
89
- Log.warn(:recap, "recap_unconfigured",
90
- echo: "Warning: SAMAGOTCHI session recap is enabled but base_url/model are missing; recap disabled. " \
91
- "Set recap: {host_ref:, model:} or SAMAGOTCHI_RECAP_BASE_URL and SAMAGOTCHI_RECAP_MODEL (or pass recap: {base_url:, model:}), " \
92
- "or leave them all out to recap with the session's own model.")
93
- return nil
94
- end
95
- fixed = { base_url: base_url.to_s.strip, api_key_env: api_key_env, model: model.to_s.strip, label: label.to_s.strip }
96
- target = -> { fixed }
97
- end
58
+ -> { fixed }
59
+ end
98
60
 
99
61
  IdleRecap.new(
100
62
  engine: engine,
@@ -107,6 +69,30 @@ module Samagotchi
107
69
  )
108
70
  end
109
71
 
72
+ # The recap's pinned host and model (IdleTarget.resolve), or nil after
73
+ # a warning when they can't be resolved (recap stays off).
74
+ # @return [IdleTarget, nil]
75
+ def self.fixed_target(settings)
76
+ IdleTarget.resolve(**settings)
77
+ rescue IdleTarget::Unresolved => e
78
+ case e.reason
79
+ when :host_ref_unknown
80
+ Log.warn(:recap, "host_ref_unknown", echo: "Warning: recap host_ref '#{e.host_ref}' not found in hosts:; recap disabled.",
81
+ host_ref: e.host_ref)
82
+ when :model_host_mismatch
83
+ Log.warn(:recap, "model_host_mismatch",
84
+ echo: "Warning: recap model '#{e.model}' names host '#{e.named}', not recap.host_ref '#{e.host_ref}'; recap disabled.",
85
+ model: e.model, host_ref: e.host_ref)
86
+ else
87
+ Log.warn(:recap, "recap_unconfigured",
88
+ echo: "Warning: SAMAGOTCHI session recap is enabled but base_url/model are missing; recap disabled. " \
89
+ "Set recap: {host_ref:, model:} or SAMAGOTCHI_RECAP_BASE_URL and SAMAGOTCHI_RECAP_MODEL (or pass recap: {base_url:, model:}), " \
90
+ "or leave them all out to recap with the session's own model.")
91
+ end
92
+ nil
93
+ end
94
+ private_class_method :fixed_target
95
+
110
96
  # recap.sentences as [min, max]; an invalid value warns and falls back to
111
97
  # the default range (the recap stays on).
112
98
  def self.recap_sentences(value)
@@ -225,21 +225,6 @@ module Samagotchi
225
225
  "#{ContextQuote.block(context)}#{message}"
226
226
  end
227
227
 
228
- # Only a pipe or a file is read. A terminal means nobody piped anything
229
- # in, and a socket a launcher or an agent's shell passes down may never
230
- # close: with -m, waiting on either would hang a script. (A pipe the
231
- # caller never closes still hangs, as it would for cat.)
232
- def read_stdin
233
- return nil if @stdin.respond_to?(:tty?) && @stdin.tty?
234
-
235
- if @stdin.respond_to?(:stat)
236
- stat = @stdin.stat
237
- return nil unless stat.pipe? || stat.file?
238
- end
239
-
240
- @stdin.read
241
- end
242
-
243
228
  # A worker session like the web start page's: saved as running with the
244
229
  # message before its worker spawns, so lists and the web show it at
245
230
  # once. The full id, so a script can pass it on. The model's host is
@@ -477,12 +462,5 @@ module Samagotchi
477
462
  else " with #{@images.size} images"
478
463
  end
479
464
  end
480
-
481
- # The text as UTF-8 whatever the locale says: with no LANG/LC_* (an app
482
- # started from Finder, launchd) stdin reads as US-ASCII and ARGV as
483
- # binary. Invalid bytes become U+FFFD rather than an error.
484
- def utf8(text)
485
- text&.dup&.force_encoding(Encoding::UTF_8)&.scrub
486
- end
487
465
  end
488
466
  end
@@ -277,6 +277,7 @@ module Samagotchi
277
277
  ("git #{rule.git}" if rule.git),
278
278
  ("touches #{rule.touches}" if rule.touches),
279
279
  ("rm #{rule.rm}" if rule.rm),
280
+ ("memory #{rule.memory}" if rule.memory),
280
281
  ("models #{rule.models.join(",")}" if rule.models),
281
282
  ("modes #{rule.modes.join(",")}" if rule.modes),
282
283
  ("skip_read_only" if rule.skip_read_only)
@@ -39,7 +39,7 @@ module Samagotchi
39
39
  # worker runs in its session's working directory)
40
40
  def tool_activity_event(tool_name, call, result, registry: nil, cwd: Dir.pwd)
41
41
  event = {
42
- action: tool_activity_action(tool_name, registry: registry),
42
+ action: tool_activity_action(tool_name, registry: registry, call: call),
43
43
  tool: tool_name,
44
44
  params: tool_activity_params(tool_name, call, registry: registry),
45
45
  status: tool_activity_status(result, tool_name)
@@ -151,14 +151,15 @@ module Samagotchi
151
151
  parts.drop(base.length).join("/")
152
152
  end
153
153
 
154
- def tool_activity_action(tool_name, registry: nil)
154
+ # @param call [Hash, nil] the call, for a label that depends on its form
155
+ def tool_activity_action(tool_name, registry: nil, call: nil)
155
156
  case tool_name
156
157
  when Tools::Execute::NAME then "running command"
157
158
  when Tools::Read::NAME then "reading file"
158
159
  when Tools::Write::NAME then "writing file"
159
160
  when Tools::Edit::NAME then "editing file"
160
161
  when Tools::MemoryRead::NAME then "reading memory"
161
- when Tools::MemoryWrite::NAME then "saving memory"
162
+ when Tools::MemoryWrite::NAME then memory_write_action(call)
162
163
  when Tools::TaskCreate::NAME then "starting background task"
163
164
  when Tools::TaskGet::NAME then "checking task"
164
165
  when Tools::TaskList::NAME then "listing tasks"
@@ -182,6 +183,15 @@ module Samagotchi
182
183
  # whose task was stopped (the stop-task button, or the model's task_stop),
183
184
  # and for a command the user's Stop killed or kept from starting: a
184
185
  # cancel, not a failure (its text still starts "Error:" for the model).
186
+ # memory_write's label: a call with only a description changes the index line.
187
+ def memory_write_action(call)
188
+ return "saving memory" unless call.is_a?(Hash)
189
+ return "removing memory" if call[:remove].to_s.strip.downcase == "true"
190
+ return "updating memory description" if call[:content].to_s.empty? && !call[:description].to_s.strip.empty?
191
+
192
+ "saving memory"
193
+ end
194
+
185
195
  def tool_activity_status(result, tool_name = nil)
186
196
  text = result.to_s
187
197
  return "stopped" if text == Tools::Execute::NOT_RUN_ON_STOP
@@ -232,6 +242,7 @@ module Samagotchi
232
242
  parts << "scope=#{preview_tool_param(scope)}" unless scope.empty?
233
243
  desc = call[:description].to_s.strip
234
244
  parts << "description=#{preview_tool_param(desc)}" unless desc.empty?
245
+ parts << "remove=true" if call[:remove].to_s.strip.downcase == "true"
235
246
  parts.join(" ")
236
247
  when Tools::TaskCreate::NAME
237
248
  parts = ["command=#{preview_tool_param(call[:content])}"]
@@ -127,7 +127,7 @@ module Samagotchi
127
127
  },
128
128
  {
129
129
  name: "memory_write",
130
- description: "Write or update a memory entry in scoped memories. Scope is required: project or system. Use the `name` parameter for the entry name (use `name`, NOT `path` — the file tools use `path`); each scope's index.md is auto-maintained (one managed line per entry); use name \"index\" to write the index file verbatim. For a small change to an existing memory (a step, a line), `edit` its file (<scope dir>/<name>.md) instead of rewriting it all; its index line is refreshed either way. If the user asks to save guidance for the current model only, pass current_model_only: true to save a model-specific overlay.",
130
+ description: "Write or update a memory entry in scoped memories. Scope is required: project or system. Use the `name` parameter for the entry name (use `name`, NOT `path` — the file tools use `path`); each scope's index.md is auto-maintained (one managed line per entry); use name \"index\" to write the index file verbatim. For a small change to an existing memory (a step, a line), `edit` its file (<scope dir>/<name>.md) instead of rewriting it all; its index line is refreshed either way. To change only an entry's index line, pass name, scope and description without content; to remove an entry, name, scope and remove: true. If the user asks to save guidance for the current model only, pass current_model_only: true to save a model-specific overlay.",
131
131
  parameters: {
132
132
  type: "object",
133
133
  properties: {
@@ -137,7 +137,7 @@ module Samagotchi
137
137
  },
138
138
  content: {
139
139
  type: "string",
140
- description: "Markdown content to write"
140
+ description: "Markdown content to write; leave it out to change only the description"
141
141
  },
142
142
  scope: {
143
143
  type: "string",
@@ -145,14 +145,18 @@ module Samagotchi
145
145
  },
146
146
  description: {
147
147
  type: "string",
148
- description: "Optional short description appended to the managed index line"
148
+ description: "Optional one-line description (at most 200 characters) appended to the managed index line; without content, it changes only that line"
149
149
  },
150
150
  current_model_only: {
151
151
  type: "boolean",
152
152
  description: "Set true to save this entry as a model-specific overlay for the current model only (<name>.<model>.md); it is auto-appended when the entry is read under that model and never listed in the index."
153
+ },
154
+ remove: {
155
+ type: "boolean",
156
+ description: "Set true, with only name and scope, to remove the entry: its file (and model overlays) move to the bundle trash and its index line is dropped. Only when the user asked or a skill's steps say so."
153
157
  }
154
158
  },
155
- required: %w[name content scope]
159
+ required: %w[name scope]
156
160
  }
157
161
  },
158
162
  {
@@ -63,7 +63,7 @@ module Samagotchi
63
63
  MemoryWrite::NAME => lambda do |call, kctx|
64
64
  MemoryWrite.call(call[:content], path: call[:path], scope: call[:scope], description: call[:description],
65
65
  current_model_only: truthy?(call[:current_model_only]),
66
- model_key: kctx.model_key)
66
+ model_key: kctx.model_key, remove: truthy?(call[:remove]))
67
67
  end,
68
68
  Write::NAME => ->(call, _kctx) { Write.call(call[:content], path: call[:path]) },
69
69
  Read::NAME => lambda do |call, _kctx|
@@ -150,13 +150,37 @@ module Samagotchi
150
150
  /^- \*\*#{Regexp.escape(name)}\*\*[ \t]*(?:[·•].*|:.*)?(\r?\n|\z)/
151
151
  end
152
152
 
153
- def self.call(content, path:, scope:, description: nil, current_model_only: false, model_key: nil)
153
+ # The longest description an index line takes: the line lands in
154
+ # every session's prompt for the repo.
155
+ DESCRIPTION_LIMIT = 200
156
+
157
+ # @param remove [Boolean] move the entry (and its model overlays) into
158
+ # the bundle trash and drop its index line (#remove)
159
+ def self.call(content, path:, scope:, description: nil, current_model_only: false, model_key: nil, remove: false)
154
160
  entry_name = path.to_s.strip
155
161
  body = content.to_s
156
162
  return "Error: entry name is required" if entry_name.empty?
157
163
  return MemoryRead.invalid_name_error(entry_name) if MemoryRead.invalid_name?(entry_name)
158
164
  return "Error: scope is required" if scope.to_s.strip.empty?
159
- return "Error: content is required" if body.empty?
165
+
166
+ if remove
167
+ if !body.empty? || !description.to_s.strip.empty? || current_model_only
168
+ return "Error: remove: true takes only name and scope (no content, description or current_model_only)"
169
+ end
170
+
171
+ return remove(entry_name, scope)
172
+ end
173
+
174
+ description = normalize_description(description)
175
+ if description && description.length > DESCRIPTION_LIMIT
176
+ return "Error: description is #{description.length} characters; the limit is #{DESCRIPTION_LIMIT} " \
177
+ "(one line: what the memory is for)"
178
+ end
179
+ if body.empty?
180
+ return "Error: content is required (or pass only description to change the index line)" unless description
181
+
182
+ return update_description(entry_name, scope, description, current_model_only)
183
+ end
160
184
 
161
185
  resolved_scope = MemoryRead.normalize_scope(scope)
162
186
 
@@ -205,6 +229,83 @@ module Samagotchi
205
229
  "Error: #{e.message}"
206
230
  end
207
231
 
232
+ # A description as the index line takes it: whitespace (newlines too)
233
+ # collapsed to single spaces; nil when blank, which keeps the stored one.
234
+ def self.normalize_description(description)
235
+ text = description.to_s.gsub(/\s+/, " ").strip
236
+ text.empty? ? nil : text
237
+ end
238
+
239
+ # The description-only form: name, scope and description, no content.
240
+ # Rewrites that entry's index line (size and date too) and leaves its
241
+ # file alone, so a status kept in the description costs no rewrite of
242
+ # the body.
243
+ def self.update_description(entry_name, scope, description, current_model_only)
244
+ return "Error: the index has no index line of its own; pass content to write it" if entry_name == MEMORY_INDEX
245
+ if current_model_only
246
+ return "Error: a model overlay has no index line; pass content to write the overlay, or drop " \
247
+ "current_model_only to change the base entry's description"
248
+ end
249
+
250
+ resolved_scope = MemoryRead.normalize_scope(scope)
251
+ file_path = File.join(MemoryRead.memories_dir(resolved_scope), "#{entry_name}.md")
252
+ return "Error: no memory '#{entry_name}' in #{resolved_scope} scope; pass content to create it" unless File.file?(file_path)
253
+
254
+ manage_index(resolved_scope, entry_name, File.size(file_path), description)
255
+ "Memory '#{entry_name}' description updated in #{resolved_scope} scope (file unchanged)."
256
+ end
257
+
258
+ # remove: true. Moves <name>.md and its model overlays into one
259
+ # MemoryBundle::Trash dir (memory-<name>-<time>, which `chi bundle
260
+ # trash` lists and empties) and drops the index line; a line whose
261
+ # file is already gone is dropped alone. A memory a bundle owns (an
262
+ # installed bundle's record lists it, or its line says "· from
263
+ # <bundle>") is the uninstaller's, and the index isn't a memory.
264
+ def self.remove(entry_name, scope)
265
+ return "Error: the index can't be removed; it is kept up to date for you" if entry_name == MEMORY_INDEX
266
+
267
+ require_relative "../memory_bundle/index_updater"
268
+ require_relative "../memory_bundle/provenance"
269
+ require_relative "../memory_bundle/trash"
270
+ resolved_scope = MemoryRead.normalize_scope(scope)
271
+ dir = MemoryRead.memories_dir(resolved_scope)
272
+ index = File.join(dir, "#{MEMORY_INDEX}.md")
273
+ index_text = File.file?(index) ? File.read(index, encoding: "UTF-8") : ""
274
+ line = index_text[managed_pattern(entry_name)]
275
+ file = File.join(dir, "#{entry_name}.md")
276
+
277
+ owner = Samagotchi::MemoryBundle::Provenance.claimants("#{entry_name}.md", scope: resolved_scope).first ||
278
+ (line && Samagotchi::MemoryBundle::IndexUpdater.extract_source(line))
279
+ return "Error: memory '#{entry_name}' came from bundle #{owner}; `chi bundle uninstall #{owner}` removes it" if owner
280
+
281
+ unless File.file?(file)
282
+ return "Error: no memory '#{entry_name}' in #{resolved_scope} scope" unless line
283
+
284
+ Samagotchi::MemoryBundle::IndexUpdater.remove_index(resolved_scope, entry_name)
285
+ return "Memory '#{entry_name}' had no file in #{resolved_scope} scope; its dangling index line was dropped."
286
+ end
287
+
288
+ trash = Samagotchi::MemoryBundle::Trash.new("memory-#{entry_name}")
289
+ ([file] + overlay_files(dir, entry_name, index_text)).each { |path| trash.move(path) }
290
+ Samagotchi::MemoryBundle::IndexUpdater.remove_index(resolved_scope, entry_name)
291
+ "Memory '#{entry_name}' removed from #{resolved_scope} scope: moved to #{trash.dir} " \
292
+ "(`chi bundle trash` lists and empties it); index line dropped."
293
+ end
294
+
295
+ # The entry's model overlays: <name>.<key>.md with a key ModelOverlay
296
+ # makes, unless that stem is a memory of its own (names may have dots:
297
+ # handoff_x.v2 has an index line).
298
+ def self.overlay_files(dir, entry_name, index_text)
299
+ pattern = /\A#{Regexp.escape(entry_name)}\.([a-z0-9-]+)\.md\z/
300
+ Dir.children(dir).sort.filter_map do |child|
301
+ key = child[pattern, 1] or next
302
+ next if index_text.match?(managed_pattern("#{entry_name}.#{key}"))
303
+
304
+ path = File.join(dir, child)
305
+ path if File.file?(path)
306
+ end
307
+ end
308
+
208
309
  # Adds or refreshes a single managed line for `entry_name` in the given
209
310
  # scope's index.md, preserving every other byte byte-for-byte.
210
311
  # Delegates to IndexUpdater to maintain a single source of truth.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Samagotchi
4
- VERSION = "0.35.0"
4
+ VERSION = "0.37.0"
5
5
  # Sent as the User-Agent of every HTTP request chi makes.
6
6
  USER_AGENT = "chi/#{VERSION}"
7
7
  end
@@ -61,8 +61,9 @@ export function approvalView(pq) {
61
61
  const who = a.rule ? [`rule ${a.rule}`, a.source].filter(Boolean).join(", ") : (a.source || "hook");
62
62
  const reason = a.reason ? String(a.reason) : "(no reason given)";
63
63
  return {
64
- // A plugin tool's label ("chrome: screenshot"), as its row shows it.
65
- tool: a.label || a.tool || "",
64
+ // A plugin tool's label ("chrome: screenshot"), as its row shows it;
65
+ // else the tool the call acts as (an MCP tool behind mcp_call).
66
+ tool: a.label || a.acts_as || a.tool || "",
66
67
  // A tool without a command or path (an MCP tool): its compact args.
67
68
  what: command || paths.join("\n") || a.args || "",
68
69
  isCommand: !!command,
@@ -142,7 +143,7 @@ export function summaryText(pq, { answer = null, cancelled = false, reason = ""
142
143
  const text = asked ? `${truncate(asked, 60)} → ${result}` : result;
143
144
  return truncate(relay ? `${relay.who}: ${text}` : text, 120);
144
145
  }
145
- const label = isApproval(pq) ? `Approve ${pq.approval?.label || pq.approval?.tool || "call"}?`
146
+ const label = isApproval(pq) ? `Approve ${pq.approval?.label || pq.approval?.acts_as || pq.approval?.tool || "call"}?`
146
147
  : isContinue(pq) ? "Step limit" : "Question";
147
148
  return truncate(pq.header || label);
148
149
  }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: samagotchi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.35.0
4
+ version: 0.37.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Dedov
@@ -167,6 +167,9 @@ files:
167
167
  - lib/samagotchi/broadcast/recipients.rb
168
168
  - lib/samagotchi/broadcast/scope_card.rb
169
169
  - lib/samagotchi/broadcast/tags.rb
170
+ - lib/samagotchi/broadcast/triage.rb
171
+ - lib/samagotchi/broadcast/triage_log.rb
172
+ - lib/samagotchi/broadcast/triage_model.rb
170
173
  - lib/samagotchi/broadcast_command.rb
171
174
  - lib/samagotchi/bundle_command.rb
172
175
  - lib/samagotchi/bundle_needs.rb
@@ -284,6 +287,7 @@ files:
284
287
  - lib/samagotchi/idle_recap.rb
285
288
  - lib/samagotchi/idle_reminders.rb
286
289
  - lib/samagotchi/idle_scheduler.rb
290
+ - lib/samagotchi/idle_target.rb
287
291
  - lib/samagotchi/image_store.rb
288
292
  - lib/samagotchi/installed_gem.rb
289
293
  - lib/samagotchi/installed_versions.rb