samagotchi 0.102.0 → 0.103.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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -1
  3. data/bin/chi +23 -191
  4. data/docs/architecture.md +54 -0
  5. data/docs/cli.md +114 -7
  6. data/docs/configuration.md +7 -1
  7. data/docs/guardrails.md +2 -1
  8. data/docs/internals/engine.md +1 -1
  9. data/docs/internals/llm-context-forget.md +11 -1
  10. data/docs/internals/remote-sessions.md +303 -0
  11. data/docs/internals/working-set.md +95 -11
  12. data/docs/plugins.md +26 -2
  13. data/docs/sessions.md +12 -5
  14. data/docs/sub-agent.md +8 -0
  15. data/lib/samagotchi/answer_command.rb +27 -25
  16. data/lib/samagotchi/bridge.rb +24 -4
  17. data/lib/samagotchi/bridge_client/event_stream.rb +81 -12
  18. data/lib/samagotchi/bridge_client/stream_request.rb +77 -0
  19. data/lib/samagotchi/bridge_client.rb +22 -38
  20. data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -2
  21. data/lib/samagotchi/bundles/claude-peer/scripts/pair +30 -1
  22. data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -2
  23. data/lib/samagotchi/bundles/codex-peer/scripts/pair +49 -4
  24. data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +7 -4
  25. data/lib/samagotchi/bundles/guardrails/manifest.yml +1 -1
  26. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  27. data/lib/samagotchi/chi_invocation.rb +43 -0
  28. data/lib/samagotchi/cli/command.rb +3 -2
  29. data/lib/samagotchi/cli/flags.rb +5 -0
  30. data/lib/samagotchi/cli/parent_wait.rb +22 -13
  31. data/lib/samagotchi/context_command.rb +80 -73
  32. data/lib/samagotchi/engine.rb +12 -8
  33. data/lib/samagotchi/idle_recap.rb +7 -1
  34. data/lib/samagotchi/kernel_loop.rb +13 -1
  35. data/lib/samagotchi/launch_mode.rb +22 -0
  36. data/lib/samagotchi/llm_context_forget.rb +13 -6
  37. data/lib/samagotchi/note_command.rb +8 -7
  38. data/lib/samagotchi/parent_report.rb +55 -43
  39. data/lib/samagotchi/remote_command.rb +170 -0
  40. data/lib/samagotchi/remotes.rb +130 -0
  41. data/lib/samagotchi/run_options.rb +233 -0
  42. data/lib/samagotchi/send_command/busy.rb +58 -0
  43. data/lib/samagotchi/send_command/parent_gate.rb +23 -10
  44. data/lib/samagotchi/send_command/wait_target.rb +16 -3
  45. data/lib/samagotchi/send_command.rb +173 -108
  46. data/lib/samagotchi/session.rb +30 -5
  47. data/lib/samagotchi/session_access/contract.rb +129 -0
  48. data/lib/samagotchi/session_access/dispatch.rb +133 -0
  49. data/lib/samagotchi/session_access/http_transport.rb +109 -0
  50. data/lib/samagotchi/session_access/local.rb +349 -0
  51. data/lib/samagotchi/session_access/remote.rb +638 -0
  52. data/lib/samagotchi/session_access/web_bridge_client.rb +181 -0
  53. data/lib/samagotchi/session_access.rb +141 -0
  54. data/lib/samagotchi/session_archive_command.rb +9 -5
  55. data/lib/samagotchi/session_commands.rb +20 -15
  56. data/lib/samagotchi/session_delete_command.rb +8 -4
  57. data/lib/samagotchi/session_manager.rb +7 -2
  58. data/lib/samagotchi/session_setup.rb +11 -6
  59. data/lib/samagotchi/sessions_command.rb +36 -14
  60. data/lib/samagotchi/terminal_ui/attach_launcher.rb +51 -15
  61. data/lib/samagotchi/terminal_ui/attach_link.rb +104 -0
  62. data/lib/samagotchi/terminal_ui/attached_loop.rb +80 -75
  63. data/lib/samagotchi/terminal_ui/local_link.rb +130 -0
  64. data/lib/samagotchi/terminal_ui/version_lines.rb +5 -3
  65. data/lib/samagotchi/terminal_ui.rb +2 -2
  66. data/lib/samagotchi/title_command.rb +9 -11
  67. data/lib/samagotchi/version.rb +1 -1
  68. data/lib/samagotchi/web/app.rb +360 -69
  69. data/lib/samagotchi/web/public/data.js +3 -2
  70. data/lib/samagotchi/web/public/index.html +20 -4
  71. data/lib/samagotchi/web/public/stage_frame.js +7 -2
  72. data/lib/samagotchi/web/public/stage_view.js +3 -2
  73. data/lib/samagotchi/web/public/stream_handlers.js +3 -1
  74. data/lib/samagotchi/web/public/working_set.js +159 -28
  75. data/lib/samagotchi/web/public/working_set_rail.js +39 -12
  76. data/lib/samagotchi/web/public/working_set_view.js +91 -33
  77. data/lib/samagotchi/web/session_access_routes.rb +343 -0
  78. data/lib/samagotchi/worker_sidecar.rb +12 -5
  79. data/lib/samagotchi/working_set.rb +88 -14
  80. data/lib/samagotchi/working_set_command.rb +133 -30
  81. data/lib/samagotchi/working_set_pin.rb +34 -0
  82. data/lib/samagotchi/working_set_steer.rb +191 -27
  83. metadata +19 -1
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require_relative "config"
5
+ require_relative "model_profile"
6
+ require_relative "thinking"
7
+ require_relative "version"
8
+
9
+ module Samagotchi
10
+ # bin/chi's run flags: the one OptionParser for `chi [options]` (a
11
+ # session, chi scratch, chi web), with the checks it runs before and
12
+ # around the parse (underscored flags, a flag where -p's text should be,
13
+ # --no-tool). What is wrong is said on stderr and exits, as bin/chi did.
14
+ module RunOptions
15
+ # The flags an underscored spelling is refused for (--recap_base_url),
16
+ # with the config registry's.
17
+ KNOWN = %w[--prompt --resume --attach --shared --no-shared --model --thinking --toolkit --tool --no-tool --set --parent-answers --memory --mute --append-system-prompt --llm-context --llm-context-apply --llm-context-budget --no-interrupt --non-interactive --verbose --no-default-input --port --open --new-token].freeze
18
+
19
+ module_function
20
+
21
+ # Parse +argv+ in place: what the parser leaves is bin/chi's to read
22
+ # (`web`, or a stray word).
23
+ # @param argv [Array<String>] bin/chi's ARGV (changed)
24
+ # @return [Array(Hash, Hash)] the options and the config overrides
25
+ # (Config.reload!'s cli_overrides)
26
+ def parse(argv)
27
+ # no_interrupt / no_default_input start from config.yml or the env; the flags below turn them on.
28
+ options = { verbose: false, no_interrupt: Samagotchi::Config.get("no_interrupt"), non_interactive: false, no_default_input: Samagotchi::Config.get("no_default_input"), web: false, web_port: nil, web_open: false, web_markdown: nil }
29
+ cli_overrides = {}
30
+
31
+ # Strict kebab enforcement: --recap_base_url is unknown (OptionParser would otherwise accept _ as -)
32
+ known = KNOWN + Samagotchi::Config.cli_entries.map(&:cli_flag)
33
+ argv.each do |arg|
34
+ next unless arg.start_with?("--") && arg.include?("_")
35
+
36
+ # Allow --no-interrupt etc. already dash; underscore is always rejected for config flags
37
+ # We check raw argv before OptionParser normalizes
38
+ base = arg.split("=", 2).first
39
+ # If dashed version is a known flag (registry or hardcoded), reject underscore variant
40
+ dashed = base.tr("_", "-")
41
+ if known.include?(dashed) || known.include?(base)
42
+ warn "Unknown option: #{arg} (did you mean #{dashed}?)"
43
+ exit 1
44
+ end
45
+ end
46
+
47
+ parser = build_parser(options, cli_overrides)
48
+ # --no-tool NAME (or --no-tool=NAME), up to a "--". A flag that takes a
49
+ # value (-p, --model …: the parser's own list) keeps its next word, so
50
+ # `chi -p "--no-tool=x is broken"` is a prompt. A tool name never starts
51
+ # with "-": `--no-tool --` or `--no-tool --model` is a missing argument.
52
+ switches = parser_switches(parser)
53
+ switch_flags = switches.flat_map { |switch| switch.long + switch.short }
54
+ known_flags = (known + switch_flags).uniq
55
+ takes_value = switches.select { |switch| switch.is_a?(OptionParser::Switch::RequiredArgument) }
56
+ .flat_map { |switch| switch.long + switch.short }
57
+ reject_prompt_flag = lambda do |flag, value|
58
+ if known_flags.include?(value)
59
+ warn "Error: #{flag} needs the prompt text right after it (got the flag #{value}) (see chi --help)"
60
+ exit 2
61
+ end
62
+ end
63
+ index = 0
64
+ while index < argv.size && argv[index] != "--"
65
+ arg = argv[index]
66
+ if %w[-p --prompt].include?(arg)
67
+ reject_prompt_flag.call(arg, argv[index + 1])
68
+ index += 2
69
+ elsif arg.start_with?("--prompt=")
70
+ reject_prompt_flag.call("--prompt", arg.delete_prefix("--prompt="))
71
+ index += 1
72
+ elsif arg.start_with?("-p") && arg != "-p"
73
+ reject_prompt_flag.call("-p", arg.delete_prefix("-p").delete_prefix("="))
74
+ index += 1
75
+ else
76
+ index += takes_value.include?(arg) ? 2 : 1
77
+ end
78
+ end
79
+ missing_no_tool = lambda do
80
+ warn "Error: missing argument: --no-tool (see chi --help)"
81
+ exit 1
82
+ end
83
+ index = 0
84
+ while index < argv.size && argv[index] != "--"
85
+ arg = argv[index]
86
+ if arg == "--no-tool"
87
+ value = argv[index + 1]
88
+ missing_no_tool.call if value.nil? || value.start_with?("-")
89
+ (options[:no_tools] ||= []) << argv.delete_at(index + 1)
90
+ argv.delete_at(index)
91
+ elsif arg.start_with?("--no-tool=")
92
+ value = arg.delete_prefix("--no-tool=")
93
+ missing_no_tool.call if value.empty? || value.start_with?("-")
94
+ options[:no_tools] = (options[:no_tools] || []) << value
95
+ argv.delete_at(index)
96
+ else
97
+ index += takes_value.include?(arg) ? 2 : 1
98
+ end
99
+ end
100
+ begin
101
+ parser.parse!(argv)
102
+ rescue OptionParser::ParseError => e
103
+ warn "Error: #{e.message} (see chi --help)"
104
+ exit 1
105
+ end
106
+
107
+ [options, cli_overrides]
108
+ end
109
+
110
+ # Every switch the parser takes, long and short, a --[no-]x as --x and
111
+ # --no-x (--no-tool too, read before the parse), for what classifies the
112
+ # run flags.
113
+ # @return [Array<String>]
114
+ def switches
115
+ flags = parser_switches(build_parser({}, {})).flat_map { |switch| switch.long + switch.short }
116
+ flags.flat_map { |flag| flag.include?("[no-]") ? [flag.sub("[no-]", ""), flag.sub("[no-]", "no-")] : [flag] }
117
+ .push("--no-tool").uniq
118
+ end
119
+
120
+ # The parser's switches (OptionParser::Switch), short and long.
121
+ def parser_switches(parser)
122
+ parser.instance_variable_get(:@stack).flat_map { |list| list.list }
123
+ .select { |switch| switch.is_a?(OptionParser::Switch) }
124
+ end
125
+
126
+ # @param options [Hash] filled in by the parse
127
+ # @param cli_overrides [Hash] the config keys the flags set
128
+ def build_parser(options, cli_overrides)
129
+ OptionParser.new do |opts|
130
+ opts.banner = <<~BANNER
131
+ Usage: chi [options]
132
+ chi bootstrap [HOST[:PORT]|URL] first setup: find the model server, write config.yml
133
+ chi scratch [options] a one-time session in this terminal: nothing is kept
134
+ chi web [--port PORT] [--open] [--scope=all] the web UI
135
+ chi sessions <list|stop|restart|archive|unarchive|delete|prune|clean|stats>
136
+ chi note [--source NAME] [-m TEXT] ID...|--all background context for sessions
137
+ chi broadcast [-m TEXT] [--all] [--dry-run] | log | deliver a note for every session it may concern
138
+ chi context <add|push|ls|show|refresh|rm|mute|unmute> live external text attached to sessions
139
+ chi send [-m TEXT] ID...|--new a message into sessions, as if typed there
140
+ chi answer ID --question QID --option N answer a question a session waits on
141
+ chi memory <ls|rm> [--scope project|system] the memories; rm moves one to the bundle trash
142
+ chi bundle <install|upgrade|uninstall|status|diff|list|build|trash|new|test|check|try>
143
+ chi desktop <install|upgrade|uninstall|status> macOS "Send to chi" helper
144
+ chi models [--format json] [TEXT] the models --model takes, from every host
145
+ chi remote <add|ls|rm> other machines' chi web, for chi --remote NAME
146
+ chi --remote NAME <send|answer|note|context|sessions> … those commands on that machine (or CHI_REMOTE=NAME)
147
+ chi self version, paths, model and bundles
148
+ chi update [--dry-run] update this chi, its bundles and the desktop helper
149
+ (each subcommand takes --help)
150
+
151
+ BANNER
152
+ opts.on("-p", "--prompt PROMPT", "Run PROMPT as the first turn, then stay in the REPL") { |p| options[:prompt] = p }
153
+ opts.on("--resume SESSION_ID", "Resume a previous session by its ID (or a unique prefix of it)") { |s| options[:resume] = s }
154
+ opts.on("--attach SESSION_ID", "Attach to a session's worker, waking one if it has exited; ID or a unique prefix") { |s| options[:attach] = s }
155
+ opts.on("--[no-]shared", "Run the session in a background worker and attach to it, so `chi web` can share it (the default, config session.shared; with --resume: that session); --no-shared runs a plain in-process REPL") { |v| options[:shared] = v }
156
+ opts.on("--model NAME", "Model for this run, not saved (overrides --resume's; config default.model) [env SAMAGOTCHI_DEFAULT_MODEL]") { |m| options[:model] = m; cli_overrides["default.model"] = m }
157
+ opts.on("--profile NAME", Samagotchi::ModelProfile::NAMES,
158
+ "Prompt profile for every model in this run (#{Samagotchi::ModelProfile::NAMES.join("|")}) [alias for --model-profile]") { |p| cli_overrides["model.profile"] = p }
159
+ opts.on("--thinking LEVEL", Samagotchi::Thinking::LEVELS.map(&:to_s), "This session's own thinking level: off|low|medium|high|default (saved in the session, " \
160
+ "before the model's; default unsets it; /thinking changes it). With chi web: the default for the sessions " \
161
+ "its workers run, below a session's own [env SAMAGOTCHI_THINKING_LEVEL]") { |l| options[:thinking] = l }
162
+ opts.on("--toolkit NAME", "This session's toolkit, the tools its prompts declare: full|lean|child|a config or bundle one " \
163
+ "(saved in the session; config toolkit.default; /toolkit changes it)") { |n| options[:toolkit] = n }
164
+ opts.on("--tool NAME", "Also declare this tool on top of the toolkit (repeatable; a comma list too)") { |t| (options[:tools] ||= []) << t }
165
+ # --no-tool is taken out of ARGV before parse! (OptionParser would read it
166
+ # as --tool's negation); this is its help line.
167
+ opts.separator " --no-tool NAME Leave this tool out of the toolkit (repeatable; a comma list too)"
168
+ opts.on("--set KEY=VALUE", "A setting for this session, saved in it (repeatable): a core key a session may set " \
169
+ "(recap.enabled, recap.sentences; typed by the key, over CLI, env and config.yml), or BUNDLE.KEY over " \
170
+ "config.yml's bundles: (a key the bundle's manifest lists in session_settings:; numbers and booleans " \
171
+ "(true, off, no) are typed, the rest is text). A wrong key or value is refused: " \
172
+ "--set recap.enabled=false, --set cost-cap.cap=3") do |v|
173
+ (options[:sets] ||= []) << v
174
+ end
175
+ opts.on("--parent-answers LEVEL", "What a parent agent may answer on this session's questions: " \
176
+ "hard|standard|soft, only stricter than config.yml's guardrails.parent_answers (saved in the " \
177
+ "session; only you loosen it, with /parent-answers in the terminal or the web)") do |v|
178
+ options[:parent_answers] = v
179
+ end
180
+ opts.on("--memory NAME", "Preload a memory entry into the system prompt (repeatable; a comma list too)") { |m| (options[:memories] ||= []) << m }
181
+ opts.on("--append-system-prompt FILE", "Put FILE's text into the system prompt, after AGENTS.md (repeatable; saved in the session; " \
182
+ "config append_system_prompt: is the baseline)") { |f| (options[:append_system_prompt] ||= []) << f }
183
+ opts.on("--mute NAME", "Hide a memory from this session: not in the prompt, refused by memory_read (repeatable; a comma list too)") { |m| (options[:muted] ||= []) << m }
184
+ opts.on("--llm-context LAYERS", "This session's own LLM context strategy: none, or layers such as stale,forget (saved in the session, " \
185
+ "before the model's; /llm-context changes it)") { |v| (options[:llm_context] ||= {})[:strategy] = v }
186
+ opts.on("--llm-context-apply RULE", "This session's own apply rule for it: payoff|next_request|turn_end") { |v| (options[:llm_context] ||= {})[:apply] = v }
187
+ opts.on("--llm-context-budget TOKENS", "This session's own context budget: tokens (64000, 64k) or off") { |v| (options[:llm_context] ||= {})[:budget_tokens] = v }
188
+ opts.on("--no-interrupt", "Raise the tool call limit to 1000 iterations (useful for long tasks)") { options[:no_interrupt] = true; cli_overrides["no_interrupt"] = true }
189
+ opts.on("--non-interactive", "Run a single prompt and exit (no REPL, implies --no-interrupt)") { options[:non_interactive] = true }
190
+ opts.on("-v", "--verbose", "Print raw LLM responses and tool calls/results (runs a plain REPL, not attached)") { options[:verbose] = true }
191
+ opts.on("--no-default-input", "Skip default REPL input prefill") { options[:no_default_input] = true; cli_overrides["no_default_input"] = true }
192
+ opts.on("--port PORT", "Port for `chi web` (default: 4567, env SAMAGOTCHI_WEB_PORT)") { |p| options[:web_port] = p.to_i; cli_overrides["web.port"] = p.to_s }
193
+ opts.on("--open", "Open browser for `chi web`") { options[:web_open] = true }
194
+ opts.on("--new-token", "`chi web`: replace the LAN access token (web.host: lan); links and QR codes made with the old one stop working") { options[:new_token] = true }
195
+ opts.on("--scope VALUE", %w[project all],
196
+ "Which sessions `chi web` and `chi sessions list` show: project (the default: this folder's git project; " \
197
+ "every session outside a repo) or all") { |v| options[:scope] = v }
198
+ # Accepted values of string entries that the registry doesn't type as enums.
199
+ value_hints = { "status.line" => "on|off", "web.host" => "127.0.0.1|::1|localhost|lan|IP", "recap.sentences" => "N|N-M",
200
+ "web.annotate_presets" => "text|text", "web.editor" => "vscode|vscode-insiders|cursor|zed|none|URL" }
201
+ # Universal config flags (implicit convention: ENV SAMAGOTCHI_* ↔ YAML dotted ↔ CLI --kebab)
202
+ # Generated from Samagotchi::Config registry — Option A: snake leaf in YAML, kebab in CLI.
203
+ Samagotchi::Config.cli_entries.each do |entry|
204
+ next if %w[no_interrupt no_default_input].include?(entry.key)
205
+ next if entry.key == "web.port" # already handled as --port
206
+ next if entry.key == "default.model" # alias --model above
207
+ next if entry.key == "web.markdown" # handled below with explicit inverse flags
208
+
209
+ case entry.type
210
+ when :bool
211
+ # --[no-]: a bool that defaults to on is only useful switched off.
212
+ opts.on("--[no-]#{entry.cli_flag.delete_prefix("--")}", "Set #{entry.key} (bool, default #{entry.default ? "on" : "off"}) [env #{entry.env_key}]") { |v| cli_overrides[entry.key] = v }
213
+ when :integer, :float, :string, :enum
214
+ desc = "Set #{entry.key} [env #{entry.env_key}]"
215
+ desc += " (#{entry.enum_values.join("|")})" if entry.type == :enum
216
+ desc += " (#{value_hints[entry.key]})" if value_hints.key?(entry.key)
217
+ desc += "; only without a hosts: config" if %w[server.host server.port].include?(entry.key)
218
+ if entry.type == :enum
219
+ opts.on("#{entry.cli_flag} VALUE", entry.enum_values, desc) { |v| cli_overrides[entry.key] = v }
220
+ else
221
+ opts.on("#{entry.cli_flag} VALUE", desc) { |v| cli_overrides[entry.key] = v }
222
+ end
223
+ end
224
+ end
225
+ opts.on("--web-markdown", "Render finalized LLM Markdown in `chi web` (requires optional commonmarker gem)") { options[:web_markdown] = true; cli_overrides["web.markdown"] = true }
226
+ opts.on("--no-web-markdown", "Disable Markdown rendering in `chi web`") { options[:web_markdown] = false; cli_overrides["web.markdown"] = false }
227
+ # Strict underscore rejection: --recap_base_url etc. are not registered and will raise OptionParser::InvalidOption
228
+ opts.on("--version", "Print chi's version") { puts "chi #{Samagotchi::VERSION}"; exit }
229
+ opts.on("-h", "--help", "Show help") { puts opts; exit }
230
+ end
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../session"
4
+ require_relative "../parent_report"
5
+ require_relative "../chi_invocation"
6
+ require_relative "../guardrails/parent_policy"
7
+
8
+ module Samagotchi
9
+ class SendCommand
10
+ # Why chi send --wait refuses to send into a session a turn runs in: a
11
+ # message that misses the running turn runs next, and the running
12
+ # one's reply would come back as the answer. With --queue the message
13
+ # waits for its own turn, so only a question the turn waits on refuses
14
+ # it (it would queue behind the question). The one wording for the
15
+ # CLI and chi web's wait prepare route, which sends these fields so a
16
+ # chi --remote client words it with its own commands (#message's via:).
17
+ # @!attribute error [String] "waiting" (a question or an approval waits
18
+ # on chi answer) or "busy" (a turn runs)
19
+ # @!attribute session_id [String]
20
+ # @!attribute question [PendingQuestion, nil] what waits ("waiting")
21
+ # @!attribute strategy [Guardrails::ParentPolicy::Strategy, nil] what a
22
+ # parent may pick on it: config.yml tightened by the session's own
23
+ # parent_answers
24
+ Busy = Data.define(:error, :session_id, :question, :strategy) do
25
+ # @param session [Session] the session as just read
26
+ # @param owner [OwnerLock::Owner, nil] the worker or REPL that owns it
27
+ # @param queue [Boolean] chi send --queue
28
+ # @return [Busy, nil] nil when the message may go in
29
+ def self.for(session, owner:, queue:)
30
+ return nil unless session.status == Session::STATUS_RUNNING && owner
31
+
32
+ waiting = session.waiting_question(live: owner.worker?)
33
+ return nil if queue && !waiting
34
+ return new(error: "busy", session_id: session.id, question: nil, strategy: nil) unless waiting
35
+
36
+ new(error: "waiting", session_id: session.id, question: session.pending_question,
37
+ strategy: Guardrails::ParentPolicy.for_session(session))
38
+ end
39
+
40
+ # What the user is told, after the session's short id.
41
+ # @param via [ChiInvocation] how its commands are spelled
42
+ def message(via: ChiInvocation::HERE)
43
+ return "busy: a turn is running; wait or #{via.remote? ? "open it in #{via.web}" : "attach"}" unless error == "waiting"
44
+
45
+ id = session_id
46
+ # The whole question (its options, its tags) for what a parent may
47
+ # give.
48
+ q = ParentReport.question(question, strategy: strategy)
49
+ answer = ParentReport.answer_with(id, q, via: via)
50
+ if ParentReport.approval?(q)
51
+ "waiting for an approval: #{ParentReport.approval_advice(q, strategy: strategy)} (#{answer})"
52
+ else
53
+ "waiting for an answer: #{answer} (or --text); #{via.shows(id)} shows it"
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -33,12 +33,11 @@ module Samagotchi
33
33
  # A /continue yes (or bare /continue) from a parent agent's process.
34
34
  def self.parent_continue?(prompt) = TurnFlow.continue_yes?(prompt) && Guardrails::ParentApprovals.parent_process?
35
35
 
36
- # @return [String, nil] the first target's refusal of a parent's
37
- # /continue yes; nil for any other line, or when none refuses
38
- def continue_refusal(prompt)
39
- return nil unless self.class.parent_continue?(prompt)
40
-
41
- target_values.each do |value|
36
+ # The first refusal of a parent's /continue yes among targets whose
37
+ # own parent_answers are +values+, held to this process's config.yml
38
+ # (chi web's for a remote chi send); nil when none refuses.
39
+ def self.continue_refusal_for(values)
40
+ values.each do |value|
42
41
  strategy = Guardrails::ParentPolicy.effective(value)
43
42
  refusal = Guardrails::ParentPolicy.continue_refusal(strategy: strategy)
44
43
  return refusal if refusal
@@ -46,12 +45,26 @@ module Samagotchi
46
45
  nil
47
46
  end
48
47
 
48
+ # Whether +prompt+ is a /parent-answers line.
49
+ def self.parent_answers_line?(prompt) = SessionCommands.builtin_registry.lookup(prompt)&.id == :parent_answers
50
+
51
+ # Whether +prompt+ is a /parent-answers line that loosens one of
52
+ # +values+ (targets' own parent_answers).
53
+ def self.loosens_any?(prompt, values)
54
+ parent_answers_line?(prompt) && values.any? { |value| ParentAnswersCommand.loosens?(prompt, value) }
55
+ end
56
+
57
+ # @return [String, nil] the first target's refusal of a parent's
58
+ # /continue yes; nil for any other line, or when none refuses
59
+ def continue_refusal(prompt)
60
+ return nil unless self.class.parent_continue?(prompt)
61
+
62
+ self.class.continue_refusal_for(target_values)
63
+ end
64
+
49
65
  # Whether +prompt+ is a /parent-answers line that loosens a target's
50
66
  # own value.
51
- def loosens?(prompt)
52
- SessionCommands.builtin_registry.lookup(prompt)&.id == :parent_answers &&
53
- target_values.any? { |value| ParentAnswersCommand.loosens?(prompt, value) }
54
- end
67
+ def loosens?(prompt) = self.class.parent_answers_line?(prompt) && self.class.loosens_any?(prompt, target_values)
55
68
 
56
69
  private
57
70
 
@@ -45,11 +45,24 @@ module Samagotchi
45
45
  # for its answer), its last turn's end is the answer (ended_before);
46
46
  # with --turn, when that turn is the last one.
47
47
  def self.wait_only(session, session_dir:, requested:)
48
- turn = requested || ReplyWait.own_turn(session, session_dir: session_dir)
49
48
  settled = session.status == Session::STATUS_IDLE && !session.last_turn.nil? && session.pending_question.nil?
50
- ended_before = requested ? session.last_turn&.enqueued_id == requested : settled
51
- new(turn: turn, report_turn_id: turn || (session.last_turn&.enqueued_id if settled), ended_before: ended_before)
49
+ own = requested ? nil : ReplyWait.own_turn(session, session_dir: session_dir)
50
+ wait_only_of(own_turn: own, settled: settled, last_turn_id: session.last_turn&.enqueued_id, requested: requested)
52
51
  end
52
+
53
+ # #wait_only from what SessionAccess#prepare_wait read
54
+ # (SessionAccess::WaitStart): its own_turn, settled and last_turn_id.
55
+ def self.after(start, requested:)
56
+ wait_only_of(own_turn: start.own_turn, settled: start.settled, last_turn_id: start.last_turn_id,
57
+ requested: requested)
58
+ end
59
+
60
+ def self.wait_only_of(own_turn:, settled:, last_turn_id:, requested:)
61
+ turn = requested || own_turn
62
+ ended_before = requested ? last_turn_id == requested : settled
63
+ new(turn: turn, report_turn_id: turn || (last_turn_id if settled), ended_before: ended_before)
64
+ end
65
+ private_class_method :wait_only_of
53
66
  end
54
67
  end
55
68
  end