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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +40 -1
- data/bin/chi +23 -191
- data/docs/architecture.md +54 -0
- data/docs/cli.md +114 -7
- data/docs/configuration.md +7 -1
- data/docs/guardrails.md +2 -1
- data/docs/internals/engine.md +1 -1
- data/docs/internals/llm-context-forget.md +11 -1
- data/docs/internals/remote-sessions.md +303 -0
- data/docs/internals/working-set.md +95 -11
- data/docs/plugins.md +26 -2
- data/docs/sessions.md +12 -5
- data/docs/sub-agent.md +8 -0
- data/lib/samagotchi/answer_command.rb +27 -25
- data/lib/samagotchi/bridge.rb +24 -4
- data/lib/samagotchi/bridge_client/event_stream.rb +81 -12
- data/lib/samagotchi/bridge_client/stream_request.rb +77 -0
- data/lib/samagotchi/bridge_client.rb +22 -38
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/claude-peer/scripts/pair +30 -1
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/codex-peer/scripts/pair +49 -4
- data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +7 -4
- data/lib/samagotchi/bundles/guardrails/manifest.yml +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/chi_invocation.rb +43 -0
- data/lib/samagotchi/cli/command.rb +3 -2
- data/lib/samagotchi/cli/flags.rb +5 -0
- data/lib/samagotchi/cli/parent_wait.rb +22 -13
- data/lib/samagotchi/context_command.rb +80 -73
- data/lib/samagotchi/engine.rb +12 -8
- data/lib/samagotchi/idle_recap.rb +7 -1
- data/lib/samagotchi/kernel_loop.rb +13 -1
- data/lib/samagotchi/launch_mode.rb +22 -0
- data/lib/samagotchi/llm_context_forget.rb +13 -6
- data/lib/samagotchi/note_command.rb +8 -7
- data/lib/samagotchi/parent_report.rb +55 -43
- data/lib/samagotchi/remote_command.rb +170 -0
- data/lib/samagotchi/remotes.rb +130 -0
- data/lib/samagotchi/run_options.rb +233 -0
- data/lib/samagotchi/send_command/busy.rb +58 -0
- data/lib/samagotchi/send_command/parent_gate.rb +23 -10
- data/lib/samagotchi/send_command/wait_target.rb +16 -3
- data/lib/samagotchi/send_command.rb +173 -108
- data/lib/samagotchi/session.rb +30 -5
- data/lib/samagotchi/session_access/contract.rb +129 -0
- data/lib/samagotchi/session_access/dispatch.rb +133 -0
- data/lib/samagotchi/session_access/http_transport.rb +109 -0
- data/lib/samagotchi/session_access/local.rb +349 -0
- data/lib/samagotchi/session_access/remote.rb +638 -0
- data/lib/samagotchi/session_access/web_bridge_client.rb +181 -0
- data/lib/samagotchi/session_access.rb +141 -0
- data/lib/samagotchi/session_archive_command.rb +9 -5
- data/lib/samagotchi/session_commands.rb +20 -15
- data/lib/samagotchi/session_delete_command.rb +8 -4
- data/lib/samagotchi/session_manager.rb +7 -2
- data/lib/samagotchi/session_setup.rb +11 -6
- data/lib/samagotchi/sessions_command.rb +36 -14
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +51 -15
- data/lib/samagotchi/terminal_ui/attach_link.rb +104 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +80 -75
- data/lib/samagotchi/terminal_ui/local_link.rb +130 -0
- data/lib/samagotchi/terminal_ui/version_lines.rb +5 -3
- data/lib/samagotchi/terminal_ui.rb +2 -2
- data/lib/samagotchi/title_command.rb +9 -11
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +360 -69
- data/lib/samagotchi/web/public/data.js +3 -2
- data/lib/samagotchi/web/public/index.html +20 -4
- data/lib/samagotchi/web/public/stage_frame.js +7 -2
- data/lib/samagotchi/web/public/stage_view.js +3 -2
- data/lib/samagotchi/web/public/stream_handlers.js +3 -1
- data/lib/samagotchi/web/public/working_set.js +159 -28
- data/lib/samagotchi/web/public/working_set_rail.js +39 -12
- data/lib/samagotchi/web/public/working_set_view.js +91 -33
- data/lib/samagotchi/web/session_access_routes.rb +343 -0
- data/lib/samagotchi/worker_sidecar.rb +12 -5
- data/lib/samagotchi/working_set.rb +88 -14
- data/lib/samagotchi/working_set_command.rb +133 -30
- data/lib/samagotchi/working_set_pin.rb +34 -0
- data/lib/samagotchi/working_set_steer.rb +191 -27
- 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
|
-
#
|
|
37
|
-
#
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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
|