samagotchi 0.87.0 → 0.89.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -1
  3. data/docs/bundle-authoring.md +19 -0
  4. data/docs/cli.md +44 -5
  5. data/docs/guardrails.md +6 -1
  6. data/docs/internals/bundles.md +10 -0
  7. data/docs/sessions.md +4 -4
  8. data/docs/sub-agent.md +9 -0
  9. data/lib/samagotchi/bundle_command/test_run.rb +2 -2
  10. data/lib/samagotchi/bundle_command/try/run.rb +162 -0
  11. data/lib/samagotchi/bundle_command/try/sandbox.rb +159 -0
  12. data/lib/samagotchi/bundle_command/try/trace.rb +152 -0
  13. data/lib/samagotchi/bundle_command.rb +216 -10
  14. data/lib/samagotchi/bundle_session_settings.rb +2 -0
  15. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  16. data/lib/samagotchi/child_reports.rb +100 -7
  17. data/lib/samagotchi/context_providers.rb +1 -2
  18. data/lib/samagotchi/guardrails/parent_policy.rb +20 -0
  19. data/lib/samagotchi/hooks/bundle_loader.rb +2 -0
  20. data/lib/samagotchi/installed_gem.rb +11 -0
  21. data/lib/samagotchi/memory_bundle/asset_installer.rb +25 -13
  22. data/lib/samagotchi/memory_bundle/installed_bundle.rb +6 -3
  23. data/lib/samagotchi/memory_bundle/installer.rb +22 -3
  24. data/lib/samagotchi/memory_bundle/provenance.rb +21 -6
  25. data/lib/samagotchi/relay.rb +34 -2
  26. data/lib/samagotchi/relay_desk.rb +13 -0
  27. data/lib/samagotchi/session_manager.rb +2 -10
  28. data/lib/samagotchi/terminal_ui/attach_launcher.rb +1 -1
  29. data/lib/samagotchi/terminal_ui/question_prompt.rb +2 -1
  30. data/lib/samagotchi/terminal_ui.rb +1 -1
  31. data/lib/samagotchi/tools/delegate_relay.rb +69 -15
  32. data/lib/samagotchi/tools/delegate_wait.rb +3 -1
  33. data/lib/samagotchi/version.rb +1 -1
  34. data/lib/samagotchi/web/public/notify.js +6 -2
  35. data/lib/samagotchi/web/public/question_card.js +2 -0
  36. data/lib/samagotchi/web/public/question_cards.js +25 -18
  37. data/lib/samagotchi/worker.rb +51 -9
  38. metadata +24 -1
@@ -4,6 +4,7 @@ require_relative "../session"
4
4
  require_relative "../bridge_client"
5
5
  require_relative "../relay"
6
6
  require_relative "../guardrails/approval"
7
+ require_relative "../guardrails/parent_policy"
7
8
  require_relative "../log"
8
9
  require_relative "../question_answer"
9
10
 
@@ -35,10 +36,39 @@ module Samagotchi
35
36
 
36
37
  module_function
37
38
 
39
+ # Kinds a relay may carry.
40
+ HOOK_KIND = "hook"
41
+
38
42
  # @param relay [#open_question, #relay_desk] Peers#relay
43
+ # @param strategy [Guardrails::ParentPolicy::Strategy] the child's
44
+ # (ParentPolicy.for_session(child))
39
45
  # @return [Boolean] a relay can take this question
40
- def relayable?(relay, question)
41
- !relay.nil? && question.is_a?(PendingQuestion) && question.kind.to_s == Guardrails::Approval::KIND
46
+ # @param child_id [String, nil] the asking child: a hook's question
47
+ # already answered on one of this parent's relays
48
+ # (RelayDesk#settled?) isn't relayed again; an approval is (as before
49
+ # P4: a refused or undelivered answer is offered again)
50
+ def relayable?(relay, question, strategy:, child_id: nil)
51
+ return false if relay.nil? || !relay?(question, strategy: strategy)
52
+ return true unless child_id && question.kind.to_s == HOOK_KIND
53
+
54
+ !relay.relay_desk.settled?(child_id, question.id)
55
+ end
56
+
57
+ # Whether a delegate's question goes to the parent's user rather than
58
+ # its model: every approval, and a hook's question the parent can't
59
+ # usefully answer under the child's strategy
60
+ # (Guardrails::ParentPolicy.parent_decides?). The one place that says
61
+ # which kinds the relay carries: the wait (DelegateWait.call), the
62
+ # look at the other children (Others#waiting) and the delegate
63
+ # reports (ChildReports) all ask it.
64
+ def relay?(question, strategy:)
65
+ return false unless question.is_a?(PendingQuestion)
66
+
67
+ case question.kind.to_s
68
+ when Guardrails::Approval::KIND then true
69
+ when HOOK_KIND then !Guardrails::ParentPolicy.parent_decides?(question, strategy: strategy)
70
+ else false
71
+ end
42
72
  end
43
73
 
44
74
  # Relay the child's pending approval and wait until it settles.
@@ -79,10 +109,11 @@ module Samagotchi
79
109
 
80
110
  # Record the parent's answer, tell the child, and say what came of it.
81
111
  def settle(answer, fields, client:, desk:, relay_id:, qid:)
82
- what = what_text(fields[:approval])
112
+ what = fields[:kind] == HOOK_KIND ? hook_what(fields) : what_text(fields[:approval])
113
+ hook = fields[:kind] == HOOK_KIND
83
114
  if answer.is_a?(Hash) && answer[:error] == "cancelled"
84
115
  desk.close(relay_id)
85
- return closed(answer[:reason].to_s, what, client: client, relay_id: relay_id, qid: qid)
116
+ return closed(answer[:reason].to_s, what, client: client, relay_id: relay_id, qid: qid, hook: hook)
86
117
  end
87
118
 
88
119
  dismissed = !answer.is_a?(QuestionAnswer)
@@ -90,17 +121,18 @@ module Samagotchi
90
121
  desk.record(relay_id, selected_indices: dismissed ? [] : answer.selected_indices,
91
122
  freeform: dismissed ? nil : answer.freeform, dismissed: dismissed, by: by)
92
123
  response = post(client, "answered", relay_id, qid)
93
- Outcome.new(line: line(what, delivered(response, answer, fields, dismissed)), stopped: false)
124
+ Outcome.new(line: line(what, delivered(response, answer, fields, dismissed), hook: hook), stopped: false)
94
125
  end
95
126
 
96
- def closed(reason, what, client:, relay_id:, qid:)
127
+ def closed(reason, what, client:, relay_id:, qid:, hook: false)
97
128
  case reason
98
- when "answered_on_child" then Outcome.new(line: line(what, "answered on the child"), stopped: false)
99
- when "child_gone" then Outcome.new(line: line(what, "not delivered (the child's worker is gone)"), stopped: false)
129
+ when "answered_on_child" then Outcome.new(line: line(what, "answered on the child", hook: hook), stopped: false)
130
+ when "child_gone"
131
+ Outcome.new(line: line(what, "not delivered (the child's worker is gone)", hook: hook), stopped: false)
100
132
  else
101
133
  # A Stop: the child's question stays open there.
102
134
  post(client, "closed", relay_id, qid, reason: "stopped")
103
- Outcome.new(line: line(what, "still open (your turn was stopped)"), stopped: true)
135
+ Outcome.new(line: line(what, "still open (your turn was stopped)", hook: hook), stopped: true)
104
136
  end
105
137
  end
106
138
 
@@ -108,10 +140,16 @@ module Samagotchi
108
140
  def delivered(response, answer, fields, dismissed)
109
141
  return "not delivered (the child's worker is gone)" unless response
110
142
 
143
+ hook = fields[:kind] == HOOK_KIND
111
144
  case response.status
112
- when 200 then dismissed ? "dismissed (denied)" : verdict(answer, fields)
145
+ when 200
146
+ # A dismissed hook question: the hook decides what no answer means.
147
+ return dismissed ? "dismissed" : answer.selected.join(", ") if hook
148
+
149
+ dismissed ? "dismissed (denied)" : verdict(answer, fields)
113
150
  when 409 then "answered on the child"
114
- when 403 then "refused by the child (a parent agent may not allow it); it waits for the user there"
151
+ when 403
152
+ "refused by the child (a parent agent may not #{hook ? "pick it" : "allow it"}); it waits for the user there"
115
153
  else "not delivered (#{response.json&.fetch("error", nil) || response.status})"
116
154
  end
117
155
  end
@@ -134,7 +172,15 @@ module Samagotchi
134
172
  why.empty? ? "denied" : "denied (#{why.inspect})"
135
173
  end
136
174
 
137
- def line(what, outcome) = "approval relayed to your user: #{what} → #{outcome}"
175
+ def line(what, outcome, hook: false)
176
+ "#{hook ? "hook question" : "approval"} relayed to your user: #{what} → #{outcome}"
177
+ end
178
+
179
+ # A hook card's header as the child asked it (Cost cap), or its hook.
180
+ def hook_what(fields)
181
+ header = fields.dig(:relay, :header).to_s
182
+ header.empty? ? fields[:hook].to_s : header
183
+ end
138
184
 
139
185
  # execute: git push origin main
140
186
  def what_text(facts)
@@ -151,6 +197,9 @@ module Samagotchi
151
197
  # (DelegateWait.relayed_outcomes). Every question is relayed once per
152
198
  # wait.
153
199
  class Others
200
+ # The kinds of question a relay may carry (DelegateRelay.relay? decides).
201
+ KINDS = [Guardrails::Approval::KIND, HOOK_KIND].freeze
202
+
154
203
  # @param parent_id [String]
155
204
  # @param except [String] the child waited on (its own approvals come
156
205
  # through ReplyWait)
@@ -191,11 +240,16 @@ module Samagotchi
191
240
  # oldest question first
192
241
  def waiting
193
242
  rows = SessionManager.children_of(@parent_id, state_dir: @state_dir).select do |row|
194
- row[:id] != @except && row[:waiting] == Guardrails::Approval::KIND && !@done[row[:waiting_id].to_s]
243
+ row[:id] != @except && KINDS.include?(row[:waiting].to_s) && !@done[row[:waiting_id].to_s]
195
244
  end
196
245
  rows.filter_map do |row|
197
- pending = Session.load(row[:id], state_dir: @state_dir).pending_question
198
- [row[:id], pending] if pending && pending.id.to_s == row[:waiting_id].to_s
246
+ child = Session.load(row[:id], state_dir: @state_dir)
247
+ pending = child.pending_question
248
+ next nil unless pending && pending.id.to_s == row[:waiting_id].to_s
249
+ next nil unless DelegateRelay.relayable?(@relay, pending, strategy: Guardrails::ParentPolicy.for_session(child),
250
+ child_id: row[:id])
251
+
252
+ [row[:id], pending]
199
253
  rescue ArgumentError
200
254
  nil
201
255
  end.sort_by { |_, pending| pending.created_at.to_s }
@@ -82,7 +82,9 @@ module Samagotchi
82
82
  owner_grace: owner_grace, interject: others && -> { others.poll },
83
83
  turn: cursor.turn)
84
84
  left -= monotonic - started
85
- unless wait.status == :waiting_for_answer && DelegateRelay.relayable?(relay, wait.question)
85
+ unless wait.status == :waiting_for_answer &&
86
+ DelegateRelay.relayable?(relay, wait.question, strategy: Guardrails::ParentPolicy.for_session(wait.session),
87
+ child_id: child_id)
86
88
  outcomes.concat(relayed_outcomes.delete(key) || [])
87
89
  movement = advance(peers.session_id, child_id, wait, state_dir: sd)
88
90
  if movement&.already_reported
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Samagotchi
4
- VERSION = "0.87.0"
4
+ VERSION = "0.89.0"
5
5
  # Sent as the User-Agent of every HTTP request chi makes.
6
6
  USER_AGENT = "chi/#{VERSION}"
7
7
  end
@@ -44,8 +44,10 @@ export function waitingOn(summary) {
44
44
  // waiting on directly is seen by its look at the others (DelegateRelay
45
45
  // OTHERS_EVERY, 2 s), and the relay reaches the page with the hub's next
46
46
  // scan (SessionHub SCAN_INTERVAL, 1 s): up to ~3 s. Only these approvals
47
- // wait; a child's approval nobody relays rings this late.
47
+ // wait (and a child's hook question, which may be relayed too); one nobody
48
+ // relays rings this late.
48
49
  export const RELAY_HOLD_MS = 3500;
50
+ const RELAYED_KINDS = new Set(["approval", "hook"]);
49
51
 
50
52
  // What changed between two summaries of one session that needs the user.
51
53
  // @param prev the summary held before (null for a session not seen yet)
@@ -60,7 +62,9 @@ export function attentionFor(prev, next) {
60
62
  // Relayed to the parent already: the parent's card notifies.
61
63
  if (q.relayed_to) return null;
62
64
  const attention = { reason: questionReason(q), sessionId: next.id, key: `${next.id}:${q.id}` };
63
- return next.parent_id && q.kind === "approval" ? { ...attention, holdMs: RELAY_HOLD_MS } : attention;
65
+ // A hook's question may be relayed too (when the parent may only deny it);
66
+ // one the parent's model answers rings as late, which does no harm.
67
+ return next.parent_id && RELAYED_KINDS.has(q.kind) ? { ...attention, holdMs: RELAY_HOLD_MS } : attention;
64
68
  }
65
69
  const card = next.pending_card;
66
70
  if (card && card.id && card.id !== prev?.pending_card?.id) {
@@ -119,6 +119,8 @@ export function resultText(pq, { answer = null, cancelled = false, reason = "" }
119
119
  const relay = relayView(pq);
120
120
  if (cancelled && Object.hasOwn(CLOSED_PLAIN, reason)) return CLOSED_PLAIN[reason];
121
121
  if (cancelled && relay && reason !== "dismissed") return relayClosedText(relay, reason);
122
+ // A relayed hook question's dismissal: the hook decides what it means.
123
+ if (cancelled && relay && !approval) return "Dismissed";
122
124
  if (isContinue(pq)) return continueResultText({ answer, cancelled, reason });
123
125
  if (cancelled) {
124
126
  const label = approval ? "Denied" : "Cancelled";
@@ -17,7 +17,7 @@ import { diffTemplate } from "./diff_view.js";
17
17
  import { render } from "./vendor/lit-html.js";
18
18
  import {
19
19
  answerErrorText, approvalAllowed, approvalView, CONTINUE_REASON_PLACEHOLDER, deniedReason, isContinue, keepsAskingText,
20
- nextCardAction, relayedToView, resultText, summaryText,
20
+ nextCardAction, relayedToView, relayView, resultText, summaryText,
21
21
  } from "./question_card.js";
22
22
  import { sessionHash } from "./route.js";
23
23
  import { clientLabel, isOwn } from "./turn_events.js";
@@ -92,9 +92,12 @@ export function createQuestionCards({
92
92
  if (approval) {
93
93
  card.appendChild(renderApprovalDetails(approval));
94
94
  } else {
95
+ // A delegate's hook question relayed here: who asks, then its own text.
96
+ const relay = relayView(pq);
97
+ if (relay) card.appendChild(renderDelegateLine(relay));
95
98
  const q = doc.createElement("div");
96
99
  q.className = "question-text";
97
- q.textContent = pq.question || "";
100
+ q.textContent = (relay && pq.relay.asked) || pq.question || "";
98
101
  card.appendChild(q);
99
102
  }
100
103
 
@@ -273,28 +276,32 @@ export function createQuestionCards({
273
276
  card.appendChild(err);
274
277
  }
275
278
 
279
+ // "delegate ab12 · its task", the name linked to its session (a relayed
280
+ // approval's or hook question's; relayView).
281
+ function renderDelegateLine(delegate) {
282
+ const who = doc.createElement("div");
283
+ who.className = "approval-delegate";
284
+ const label = doc.createElement("span");
285
+ label.className = "approval-label";
286
+ label.textContent = "delegate ";
287
+ who.appendChild(label);
288
+ const name = doc.createElement("a");
289
+ name.className = "approval-delegate-link";
290
+ name.href = sessionHash(delegate.childId);
291
+ name.title = "Open the delegate's session";
292
+ name.textContent = delegate.who;
293
+ who.appendChild(name);
294
+ if (delegate.task) who.appendChild(doc.createTextNode(` · ${delegate.task}`));
295
+ return who;
296
+ }
297
+
276
298
  // The tool, the command (or paths, or args) as code, where and why; no
277
299
  // code box when there is nothing to show.
278
300
  function renderApprovalDetails(view) {
279
301
  const box = doc.createElement("div");
280
302
  box.className = "approval-details";
281
303
  // A delegate's approval: who asks, and its task.
282
- if (view.delegate) {
283
- const who = doc.createElement("div");
284
- who.className = "approval-delegate";
285
- const label = doc.createElement("span");
286
- label.className = "approval-label";
287
- label.textContent = "delegate ";
288
- who.appendChild(label);
289
- const name = doc.createElement("a");
290
- name.className = "approval-delegate-link";
291
- name.href = sessionHash(view.delegate.childId);
292
- name.title = "Open the delegate's session";
293
- name.textContent = view.delegate.who;
294
- who.appendChild(name);
295
- if (view.delegate.task) who.appendChild(doc.createTextNode(` · ${view.delegate.task}`));
296
- box.appendChild(who);
297
- }
304
+ if (view.delegate) box.appendChild(renderDelegateLine(view.delegate));
298
305
  const tool = doc.createElement("div");
299
306
  tool.className = "approval-tool";
300
307
  tool.textContent = view.tool;
@@ -5,6 +5,7 @@ require "fileutils"
5
5
  require_relative "client_id"
6
6
  require_relative "config"
7
7
  require_relative "engine"
8
+ require_relative "guardrails/parent_policy"
8
9
  require_relative "events"
9
10
  require_relative "session"
10
11
  require_relative "session_inbox"
@@ -308,6 +309,7 @@ module Samagotchi
308
309
  # What failed to load and what plugins showed as they loaded, now that
309
310
  # a UI can be there (a late one gets them in the snapshot).
310
311
  @engine.announce_load_events!
312
+ announce_session_warnings
311
313
  @idle_exit = WorkerIdleExit.new(
312
314
  engine: @engine, bridge: @bridge,
313
315
  timeout_minutes: @idle_exit_minutes || SessionManager.config_idle_exit_minutes,
@@ -468,6 +470,16 @@ module Samagotchi
468
470
  save_or_log(:interrupted_turn) { save_session }
469
471
  end
470
472
 
473
+ # A saved parent_answers that isn't a level reads as hard on every
474
+ # answer without a word; said once here, as the session loads.
475
+ def announce_session_warnings
476
+ value = @session.parent_answers
477
+ return unless Guardrails::ParentPolicy.unknown_level?(value)
478
+
479
+ @engine.announce({ type: :guardrail_warning, label: "parent_answers",
480
+ message: "parent_answers #{value.inspect} in session #{@session_id} isn't hard/standard/soft; hard applies" })
481
+ end
482
+
471
483
  # A question saved by a worker that died while it waited (an
472
484
  # ask_user_question, an approval, a hook's question): the turn that
473
485
  # asked is gone, so nobody can answer it, and left in the file it
@@ -600,7 +612,9 @@ module Samagotchi
600
612
  budget_notice if state == :budget
601
613
  return false unless state == :due
602
614
 
603
- reports, = take_child_reports
615
+ # Unresolved: a relay among them opens its card at the turn's first
616
+ # boundary, on the turn thread (a Stop closes it).
617
+ reports, = take_child_reports(resolve: false)
604
618
  # The turn must not generate with nothing to say: the drain gets this
605
619
  # exact list at its first boundary.
606
620
  return false if reports.empty?
@@ -1011,19 +1025,26 @@ module Samagotchi
1011
1025
  end
1012
1026
 
1013
1027
  # The Engine published a question (after the question desk saved it).
1014
- # In a turn that reports to the parent, the model's own question and
1015
- # the step-limit continue ring it; approvals and hooks' questions are
1016
- # the user's, on this session's card.
1028
+ # In a turn that reports to the parent, the model's own question, the
1029
+ # step-limit continue and a hook's question ring it (the parent reports
1030
+ # or relays a hook's: ChildReports). Approvals don't: the relay takes
1031
+ # them while the parent waits. Nor does a card relayed from this
1032
+ # session's own child (relay set): the hop is the parent's wait.
1017
1033
  def ring_question(event)
1018
1034
  return unless @reporting_turn
1019
1035
 
1020
- kind = event.dig(:pending_question, :kind).to_s
1021
- return unless ["", ContinueOffer::KIND].include?(kind)
1036
+ pending = event[:pending_question] || {}
1037
+ kind = pending[:kind].to_s
1038
+ return unless RINGING_KINDS.include?(kind)
1039
+ return if pending[:relay]
1022
1040
 
1023
1041
  ChildRing.ring(@session, why: "question", state_dir: @state_dir)
1024
1042
  @asked_parent = true
1025
1043
  end
1026
1044
 
1045
+ # The kinds of question that ring a delegate child's parent (#ring_question).
1046
+ RINGING_KINDS = ["", ContinueOffer::KIND, "hook"].freeze
1047
+
1027
1048
  # WorkerInbound.save_or_log: a failed save between turns is logged.
1028
1049
  def save_or_log(at, &) = WorkerInbound.save_or_log(at, &)
1029
1050
 
@@ -1113,17 +1134,37 @@ module Samagotchi
1113
1134
  # the turn as its own).
1114
1135
  # @return [Array(Array<ChildReports::Report>, Hash|nil)] the reports
1115
1136
  # and the mark of their message
1116
- def take_child_reports
1137
+ # A report whose question goes to the parent's user is relayed here
1138
+ # (ChildReports#resolve), before the boundary announces or merges
1139
+ # anything; one whose card a Stop closed is held back until a human's
1140
+ # input (no wake turn re-offers it at once; other wakes go on).
1141
+ # +resolve+ false: a wake turn's take before the turn starts.
1142
+ def take_child_reports(resolve: true)
1117
1143
  if (reports = @wakes.take_handed)
1118
- return [reports, { turn_start: true, turn_id: @turn_id }.compact]
1144
+ return [resolve_relays(reports), { turn_start: true, turn_id: @turn_id }.compact]
1119
1145
  end
1120
1146
 
1121
- [@child_reports.take, nil]
1147
+ taken = @child_reports.take
1148
+ [resolve ? resolve_relays(taken) : taken, nil]
1122
1149
  rescue StandardError => e
1123
1150
  Log.warn(:worker, "delegate_reports_failed", error: e.class.name, msg: e.message)
1124
1151
  [[], nil]
1125
1152
  end
1126
1153
 
1154
+ def resolve_relays(reports)
1155
+ return reports if reports.none?(&:relay)
1156
+
1157
+ relay = @engine.relay_peer
1158
+ stopped = false
1159
+ reports.filter_map do |report|
1160
+ next report unless report.relay
1161
+ # The turn stopped on an earlier card: no more cards in it.
1162
+ next @child_reports.hold_back(report) if stopped
1163
+
1164
+ @child_reports.resolve(report, relay: relay).tap { |resolved| stopped = resolved.nil? }
1165
+ end
1166
+ end
1167
+
1127
1168
  # Check again with the event log held, which the Bridge holds while it
1128
1169
  # queues a POST /turn, then close the Bridge's intake so no client can
1129
1170
  # queue one after the check, and stop the idle jobs (reminder callback,
@@ -1245,6 +1286,7 @@ module Samagotchi
1245
1286
 
1246
1287
  ArchiveStore.user_input(@session_id, state_dir: @state_dir)
1247
1288
  @wakes.human_input!(interactive: ClientId.interactive?(client_id))
1289
+ @child_reports&.release_holds
1248
1290
  end
1249
1291
 
1250
1292
  def stopped_on_disk?
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.87.0
4
+ version: 0.89.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Dedov
@@ -23,6 +23,26 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: '2.9'
26
+ - !ruby/object:Gem::Dependency
27
+ name: minitest
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '5.20'
33
+ - - "<"
34
+ - !ruby/object:Gem::Version
35
+ version: '7'
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '5.20'
43
+ - - "<"
44
+ - !ruby/object:Gem::Version
45
+ version: '7'
26
46
  - !ruby/object:Gem::Dependency
27
47
  name: nokogiri
28
48
  requirement: !ruby/object:Gem::Requirement
@@ -184,6 +204,9 @@ files:
184
204
  - lib/samagotchi/bundle_command/check.rb
185
205
  - lib/samagotchi/bundle_command/scaffold.rb
186
206
  - lib/samagotchi/bundle_command/test_run.rb
207
+ - lib/samagotchi/bundle_command/try/run.rb
208
+ - lib/samagotchi/bundle_command/try/sandbox.rb
209
+ - lib/samagotchi/bundle_command/try/trace.rb
187
210
  - lib/samagotchi/bundle_needs.rb
188
211
  - lib/samagotchi/bundle_session_settings.rb
189
212
  - lib/samagotchi/bundles/agents-md/manifest.yml