samagotchi 0.148.0 → 0.150.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 +62 -1
- data/README.md +4 -2
- data/docs/architecture.md +3 -3
- data/docs/bundles.md +38 -1
- data/docs/cli.md +76 -24
- data/docs/configuration.md +8 -6
- data/docs/guardrails.md +2 -1
- data/docs/hooks.md +2 -1
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bridge.md +8 -0
- data/docs/internals/bundles.md +42 -14
- data/docs/internals/objectives.md +269 -0
- data/docs/internals/web-frontend.md +4 -1
- data/docs/internals/web-push.md +2 -2
- data/docs/internals/working-set.md +5 -2
- data/docs/memory.md +47 -9
- data/docs/plugins.md +10 -2
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +45 -2
- data/docs/system-one.md +17 -1
- data/docs/testing.md +5 -0
- data/lib/samagotchi/atomic_file.rb +3 -0
- data/lib/samagotchi/bootstrap_command.rb +5 -2
- data/lib/samagotchi/bridge/card_store.rb +52 -7
- data/lib/samagotchi/bridge.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +91 -68
- data/lib/samagotchi/bundler_env.rb +24 -0
- data/lib/samagotchi/bundles/coordinator/manifest.yml +6 -4
- data/lib/samagotchi/bundles/coordinator/plugin.rb +3 -2
- data/lib/samagotchi/bundles/coordinator/skill_coordinator.md +9 -0
- data/lib/samagotchi/bundles/decision-card/manifest.yml +13 -0
- data/lib/samagotchi/bundles/decision-card/plugin.rb +280 -0
- data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
- data/lib/samagotchi/bundles/github-pr/manifest.yml +3 -3
- data/lib/samagotchi/bundles/github-pr/plugin.rb +2 -1
- data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +3 -1
- data/lib/samagotchi/bundles/known-names/manifest.yml +3 -2
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +2 -2
- data/lib/samagotchi/bundles/source-links/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/memory_guide.md +5 -5
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/chi_home.rb +4 -2
- data/lib/samagotchi/chi_shim.rb +2 -1
- data/lib/samagotchi/child_env.rb +10 -3
- data/lib/samagotchi/config.rb +7 -1
- data/lib/samagotchi/context_note.rb +17 -5
- data/lib/samagotchi/engine.rb +113 -6
- data/lib/samagotchi/engine_peers.rb +11 -0
- data/lib/samagotchi/git_env.rb +43 -0
- data/lib/samagotchi/guardrails/context.rb +2 -0
- data/lib/samagotchi/guardrails/pair_boundary.rb +3 -2
- data/lib/samagotchi/guardrails/targets.rb +4 -1
- data/lib/samagotchi/llm/chat_loop.rb +1 -1
- data/lib/samagotchi/memory_bundle/asset_installer.rb +16 -2
- data/lib/samagotchi/memory_bundle/file_standing.rb +167 -0
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +62 -1
- data/lib/samagotchi/memory_bundle/installer.rb +161 -52
- data/lib/samagotchi/memory_bundle/listing.rb +4 -3
- data/lib/samagotchi/memory_bundle/merger.rb +209 -8
- data/lib/samagotchi/memory_bundle/placeholder.rb +2 -1
- data/lib/samagotchi/memory_bundle/profile.rb +7 -3
- data/lib/samagotchi/memory_bundle/provenance.rb +178 -20
- data/lib/samagotchi/memory_bundle/remerge.rb +167 -0
- data/lib/samagotchi/memory_bundle/shipped_history.rb +79 -0
- data/lib/samagotchi/memory_bundle/shipped_update.rb +89 -24
- data/lib/samagotchi/memory_bundle/source.rb +3 -3
- data/lib/samagotchi/memory_bundle/status.rb +21 -3
- data/lib/samagotchi/memory_bundle/system_bundle.rb +98 -33
- data/lib/samagotchi/memory_bundle/text_merge.rb +399 -0
- data/lib/samagotchi/memory_bundle/trash.rb +58 -2
- data/lib/samagotchi/memory_bundle/uninstaller.rb +26 -6
- data/lib/samagotchi/note_command.rb +1 -1
- data/lib/samagotchi/note_delivery.rb +6 -2
- data/lib/samagotchi/objectives.rb +395 -0
- data/lib/samagotchi/objectives_audit.rb +115 -0
- data/lib/samagotchi/objectives_check.rb +365 -0
- data/lib/samagotchi/objectives_command.rb +192 -0
- data/lib/samagotchi/plugin/api.rb +2 -1
- data/lib/samagotchi/plugin/context.rb +21 -1
- data/lib/samagotchi/project_env.rb +3 -2
- data/lib/samagotchi/self_report.rb +2 -0
- data/lib/samagotchi/session_commands.rb +15 -0
- data/lib/samagotchi/session_continue.rb +3 -0
- data/lib/samagotchi/session_inbox.rb +14 -6
- data/lib/samagotchi/session_manager.rb +1 -1
- data/lib/samagotchi/steer.rb +24 -4
- data/lib/samagotchi/testing/boundary.rb +2 -0
- data/lib/samagotchi/testing/host.rb +1 -0
- data/lib/samagotchi/testing/minitest.rb +3 -0
- data/lib/samagotchi/testing/rspec.rb +3 -0
- data/lib/samagotchi/testing/sandbox.rb +3 -1
- data/lib/samagotchi/tool_activity.rb +17 -0
- data/lib/samagotchi/tool_declarations.rb +34 -0
- data/lib/samagotchi/tools/builtin_calls.rb +2 -1
- data/lib/samagotchi/tools/builtins.rb +4 -1
- data/lib/samagotchi/tools/delegate.rb +2 -1
- data/lib/samagotchi/tools/objectives_tool.rb +75 -0
- data/lib/samagotchi/tools/peers.rb +3 -0
- data/lib/samagotchi/tools/task_runtime.rb +13 -10
- data/lib/samagotchi/turn_flow.rb +3 -1
- data/lib/samagotchi/update_command.rb +76 -28
- data/lib/samagotchi/update_hint.rb +4 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/auto_update.rb +8 -8
- data/lib/samagotchi/web/auto_update_record.rb +6 -4
- data/lib/samagotchi/web/message_display.rb +9 -0
- data/lib/samagotchi/web/participant_view.rb +2 -2
- data/lib/samagotchi/web/public/app.js +15 -3
- data/lib/samagotchi/web/public/block_open.js +13 -0
- data/lib/samagotchi/web/public/bubbles.js +4 -2
- data/lib/samagotchi/web/public/cards_place.js +30 -9
- data/lib/samagotchi/web/public/commands.js +4 -1
- data/lib/samagotchi/web/public/details_motion.js +2 -1
- data/lib/samagotchi/web/public/index.html +20 -14
- data/lib/samagotchi/web/public/objectives.css +80 -0
- data/lib/samagotchi/web/public/objectives_entry.js +22 -0
- data/lib/samagotchi/web/public/objectives_model.js +100 -0
- data/lib/samagotchi/web/public/objectives_moves.js +145 -0
- data/lib/samagotchi/web/public/objectives_strip.js +242 -0
- data/lib/samagotchi/web/public/objectives_wiring.js +62 -0
- data/lib/samagotchi/web/public/stage_model.js +5 -6
- data/lib/samagotchi/web/public/stage_view.js +8 -7
- data/lib/samagotchi/web/public/stream_handlers.js +6 -1
- data/lib/samagotchi/web/public/turn_block.js +34 -19
- data/lib/samagotchi/web/public/turn_html.js +4 -1
- data/lib/samagotchi/web/public/turn_model.js +1 -1
- data/lib/samagotchi/web/public/update_sheet.js +8 -3
- data/lib/samagotchi/web/public/working_set_view.js +10 -7
- data/lib/samagotchi/web/push/notifier.rb +7 -9
- data/lib/samagotchi/web/session_hub.rb +2 -1
- data/lib/samagotchi/web/session_show.rb +12 -1
- data/lib/samagotchi/web/update_ops.rb +2 -1
- data/lib/samagotchi/worker.rb +3 -0
- data/lib/samagotchi/worker_inbound.rb +16 -8
- metadata +22 -1
|
@@ -16,6 +16,8 @@ module Samagotchi
|
|
|
16
16
|
# Context#saved_session reads it; Sessions and AttachedContext load it too.
|
|
17
17
|
autoload :Session, File.expand_path("../session", __dir__)
|
|
18
18
|
autoload :PluginSessionState, File.expand_path("../plugin_session_state", __dir__)
|
|
19
|
+
# Context#objectives reads it; the kit shouldn't carry its files.
|
|
20
|
+
autoload :Objectives, File.expand_path("../objectives", __dir__)
|
|
19
21
|
|
|
20
22
|
module Plugin
|
|
21
23
|
# Loaded on first ctx.sessions / ctx.context (or a bundle naming
|
|
@@ -73,7 +75,7 @@ module Samagotchi
|
|
|
73
75
|
:ask_model, :model_name, :model_key, :state_dir, :scratch, :setup, :prompt_files,
|
|
74
76
|
:steer, :stop_turn, :stop_generation, :frontend, :toolkit, :declared_tools, :use_toolkit, :load_tools,
|
|
75
77
|
:cancel_controller, :cost, :usage, :turn_author, :cache_confirm, :model_names,
|
|
76
|
-
:candidate_proposed, :judgments, keyword_init: true)
|
|
78
|
+
:candidate_proposed, :judgments, :turn_running, keyword_init: true)
|
|
77
79
|
|
|
78
80
|
# ctx.ask_model failed: the model couldn't be reached, timed out, or
|
|
79
81
|
# sent nothing usable. The message says why, for the user.
|
|
@@ -321,6 +323,11 @@ module Samagotchi
|
|
|
321
323
|
Array(@host.messages.call).map { |message| message.dup.freeze }.freeze
|
|
322
324
|
end
|
|
323
325
|
|
|
326
|
+
# Whether a turn runs now. Its :after_turn hooks run inside it, so a thread
|
|
327
|
+
# one of them starts can use this to wait for the end: a card shown before
|
|
328
|
+
# then belongs to that turn's block.
|
|
329
|
+
def turn_running? = !!@host.turn_running&.call
|
|
330
|
+
|
|
324
331
|
# Whether #messages leaves out a running turn (the REPL mid-turn), so
|
|
325
332
|
# a plugin can say what its answer is about.
|
|
326
333
|
def messages_partial? = !!@host.messages_partial&.call
|
|
@@ -380,6 +387,19 @@ module Samagotchi
|
|
|
380
387
|
@host.card.call(source: @bundle, title: title, body: body, actions: actions, level: level, id: id)
|
|
381
388
|
end
|
|
382
389
|
|
|
390
|
+
# The session's objectives (docs/internals/objectives.md), read now: the
|
|
391
|
+
# newest list the conversation holds, an edit not yet absorbed over it.
|
|
392
|
+
# Mid-turn a worker's conversation leaves the running turn's tool calls
|
|
393
|
+
# out, so a move shows at the turn's end.
|
|
394
|
+
# @return [Objectives::List] empty when the session has none (or there
|
|
395
|
+
# is no session yet); use its methods, not its class
|
|
396
|
+
def objectives
|
|
397
|
+
id = session_id
|
|
398
|
+
return Objectives::List.new unless id
|
|
399
|
+
|
|
400
|
+
Objectives.current(id, messages, state_dir: @host.state_dir&.call || Session.default_state_dir)
|
|
401
|
+
end
|
|
402
|
+
|
|
383
403
|
# A side answer (plan D7): one request to the session's current model
|
|
384
404
|
# on its host, with no tools and thinking off. It writes nothing,
|
|
385
405
|
# fires no hooks, and doesn't touch the conversation. It blocks until
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "open3"
|
|
5
|
+
require_relative "bundler_env"
|
|
5
6
|
require_relative "chi_home"
|
|
6
7
|
require_relative "chi_shim"
|
|
7
8
|
require_relative "config"
|
|
@@ -57,14 +58,14 @@ module Samagotchi
|
|
|
57
58
|
RESERVED_PREFIXES = %w[XDG_ SAMAGOTCHI_].freeze
|
|
58
59
|
# Never set from an ORIG_ENV record (a tool can write it), only unset:
|
|
59
60
|
# loader and Bundler variables.
|
|
60
|
-
RESTORE_NEVER = /\A(?:DYLD_|LD_|RUBYOPT\z|RUBYLIB\z|RUBYGEMS_
|
|
61
|
+
RESTORE_NEVER = /\A(?:DYLD_|LD_|RUBYOPT\z|RUBYLIB\z|RUBYGEMS_|#{BundlerEnv::PREFIX})/
|
|
61
62
|
# Never passed to `mise env`: MISE_YES answers its trust prompt (it
|
|
62
63
|
# would trust, and remember, an untrusted repo's mise.toml); CI too.
|
|
63
64
|
MISE_UNSET = %w[MISE_YES CI].freeze
|
|
64
65
|
TIMEOUT = 5
|
|
65
66
|
# What execute and tasks set themselves (Tools::TaskRuntime's
|
|
66
67
|
# sanitized env): never taken from the project's.
|
|
67
|
-
TOOLS_DROP =
|
|
68
|
+
TOOLS_DROP = BundlerEnv::CHILD_RUBY
|
|
68
69
|
|
|
69
70
|
# What #compute found: +env+ the tools' overlay ({key => value, or nil
|
|
70
71
|
# to unset}; frozen, with the marker and the ORIG_ENV record),
|
|
@@ -494,6 +494,8 @@ module Samagotchi
|
|
|
494
494
|
version = bundle.version || "?"
|
|
495
495
|
label = "#{name} #{version}"
|
|
496
496
|
label += " (shipped #{shipped})" if name == MemoryBundle::SystemBundle::BUNDLE_NAME
|
|
497
|
+
behind = bundle.behind_files
|
|
498
|
+
label += " (#{MemoryBundle::FileStanding.behind_count(behind.size, unsure: behind.any?(&:unsure))})" if behind.any?
|
|
497
499
|
label
|
|
498
500
|
end
|
|
499
501
|
installed.empty? ? "(none installed; shipped system bundle #{shipped || "?"})" : installed.join(", ")
|
|
@@ -27,6 +27,10 @@ require_relative "system_one/approval_label"
|
|
|
27
27
|
require_relative "system_one/record"
|
|
28
28
|
|
|
29
29
|
module Samagotchi
|
|
30
|
+
# Loaded on first use (a /objectives line): keeps the bundle test kit's
|
|
31
|
+
# load budget (spec/samagotchi/testing/sandbox_spec.rb).
|
|
32
|
+
autoload :ObjectivesCommand, File.expand_path("objectives_command", __dir__)
|
|
33
|
+
|
|
30
34
|
# The session commands a REPL and a session worker both run: /model,
|
|
31
35
|
# /models, /llm-context, /guardrails, /context, !rollback, !cmd and the answer to a continue offer. They act on
|
|
32
36
|
# the Engine and its TurnFlow; the host prints the result's output and
|
|
@@ -50,6 +54,8 @@ module Samagotchi
|
|
|
50
54
|
REASONING_HISTORY_COMMAND = ReasoningHistoryCommand::NAME
|
|
51
55
|
WORKING_SET_COMMAND = WorkingSetCommand::NAME
|
|
52
56
|
RECAP_COMMAND = RecapCommand::NAME
|
|
57
|
+
# A literal: ObjectivesCommand loads on first use (the autoload above).
|
|
58
|
+
OBJECTIVES_COMMAND = "/objectives"
|
|
53
59
|
# A remote catalog has hundreds of ids (OpenRouter ~380): plain /models
|
|
54
60
|
# shows this many per host; /models <text> lists every match.
|
|
55
61
|
MODELS_PER_HOST = 20
|
|
@@ -134,6 +140,9 @@ module Samagotchi
|
|
|
134
140
|
# Only a name: renaming a running session doesn't wait for its turn.
|
|
135
141
|
registry.register(TITLE_COMMAND, "show or set this session's title (--clear: back to its first message)",
|
|
136
142
|
id: :title, anytime: true, match: ->(text) { text.match?(%r{\A/title(?:\s+.*)?\z}) }) { |text| title(text) }
|
|
143
|
+
# An edit only writes a note file (it goes in between turns): mid-turn too.
|
|
144
|
+
registry.register(OBJECTIVES_COMMAND, "show or edit what this session is to achieve (set A | B, add TEXT, edit ID TEXT, done ID…, drop ID…, undo ID…, check ID)",
|
|
145
|
+
id: :objectives, anytime: true) { |text| objectives(text) }
|
|
137
146
|
registry.register(HELP_COMMAND, "list the commands, the bundles' too", anytime: true) { |_text| reply(help_listing) }
|
|
138
147
|
registry.register("/stats", "show the session's stats", local: true)
|
|
139
148
|
# /recap with words (on|off|default, or a slip: the usage) runs here:
|
|
@@ -703,6 +712,12 @@ module Samagotchi
|
|
|
703
712
|
reply(output, status: status, changed: changed ? [:title] : [])
|
|
704
713
|
end
|
|
705
714
|
|
|
715
|
+
# /objectives (ObjectivesCommand): nothing to save, an edit is a note file.
|
|
716
|
+
def objectives(text)
|
|
717
|
+
output, status = ObjectivesCommand.new(engine: @engine).run(text.delete_prefix(OBJECTIVES_COMMAND))
|
|
718
|
+
reply(output, status: status)
|
|
719
|
+
end
|
|
720
|
+
|
|
706
721
|
# /recap on|off|default (RecapCommand); :recap in changed when this
|
|
707
722
|
# session's own recap.enabled moved. It doesn't save: its host does on
|
|
708
723
|
# a changed result (Worker#run_command, the REPL).
|
|
@@ -5,6 +5,7 @@ require_relative "session"
|
|
|
5
5
|
require_relative "session_setup"
|
|
6
6
|
require_relative "session_inbox"
|
|
7
7
|
require_relative "session_chain"
|
|
8
|
+
require_relative "objectives"
|
|
8
9
|
require_relative "child_move"
|
|
9
10
|
require_relative "context_move"
|
|
10
11
|
require_relative "archive_store"
|
|
@@ -209,6 +210,8 @@ module Samagotchi
|
|
|
209
210
|
ChildMove.apply(move, from: previous.id, to: link.id, state_dir: state_dir)
|
|
210
211
|
# Its movable context too, with a note saying what moved and stayed.
|
|
211
212
|
moved_context = ContextMove.call(from: previous.id, to: link.id, mode: :continue, state_dir: state_dir)
|
|
213
|
+
# Its objectives last: the list reads as the latest word.
|
|
214
|
+
Objectives.carry(previous, to: link.id, state_dir: state_dir)
|
|
212
215
|
end
|
|
213
216
|
link = SessionManager.spawn_session(
|
|
214
217
|
prompt: start, preview: preview, title: previous.title, working_directory: previous.working_directory,
|
|
@@ -5,6 +5,7 @@ require "json"
|
|
|
5
5
|
require "time"
|
|
6
6
|
require "securerandom"
|
|
7
7
|
require_relative "atomic_file"
|
|
8
|
+
require_relative "context_note"
|
|
8
9
|
require_relative "delivery"
|
|
9
10
|
require_relative "log"
|
|
10
11
|
require_relative "session"
|
|
@@ -83,11 +84,13 @@ module Samagotchi
|
|
|
83
84
|
# to the conversation between turns. It never starts a turn.
|
|
84
85
|
# @param source [String] where it came from ("cli", "slack", "session")
|
|
85
86
|
# @param from_session [String, nil] the sending session, for a peer's note
|
|
87
|
+
# @param objectives [Hash, nil] an objectives note's payload
|
|
88
|
+
# (Objectives::List#to_payload)
|
|
86
89
|
# @param now [Time] the time the file is named and stamped by (a spec's clock)
|
|
87
90
|
# @return [String] the note file's path
|
|
88
91
|
# @raise [NoteRejected] for an empty note or one over 16 KiB (never cut)
|
|
89
92
|
def self.write_note(session_id, text:, source: "cli", from_session: nil, from_cwd: nil, state_dir: nil,
|
|
90
|
-
now: Time.now)
|
|
93
|
+
objectives: nil, now: Time.now)
|
|
91
94
|
body = checked_text(text)
|
|
92
95
|
sd = state_dir || Session.default_state_dir
|
|
93
96
|
notes_dir = File.join(Session.session_dir(session_id, state_dir: sd), NOTES_DIR)
|
|
@@ -98,7 +101,7 @@ module Samagotchi
|
|
|
98
101
|
name = "#{now.strftime("%Y%m%d%H%M%S%9N")}-#{SecureRandom.hex(3)}.json"
|
|
99
102
|
path = File.join(notes_dir, name)
|
|
100
103
|
record = { "text" => body, "source" => source.to_s, "from_session" => from_session,
|
|
101
|
-
"from_cwd" => from_cwd, "created_at" => now.iso8601 }.compact
|
|
104
|
+
"from_cwd" => from_cwd, "created_at" => now.iso8601, "objectives" => objectives }.compact
|
|
102
105
|
AtomicFile.write(path, JSON.generate(record))
|
|
103
106
|
path
|
|
104
107
|
end
|
|
@@ -134,15 +137,20 @@ module Samagotchi
|
|
|
134
137
|
end
|
|
135
138
|
|
|
136
139
|
# @return [Hash, nil] {note_id:, text:, source:, from_session:, from_cwd:,
|
|
137
|
-
# created_at:},
|
|
140
|
+
# created_at:}, with an objectives note's objectives: and its header:
|
|
141
|
+
# (ContextNote.message takes them), or nil for a file that holds no
|
|
142
|
+
# usable note
|
|
138
143
|
def self.read_note(note_file)
|
|
139
144
|
data = JSON.parse(File.read(note_file))
|
|
140
145
|
text = data["text"].to_s.strip
|
|
141
146
|
return nil if text.empty?
|
|
142
147
|
|
|
143
|
-
{ note_id: File.basename(note_file).sub(/\.json(\.processing)?\z/, ""), text: text,
|
|
144
|
-
|
|
145
|
-
|
|
148
|
+
note = { note_id: File.basename(note_file).sub(/\.json(\.processing)?\z/, ""), text: text,
|
|
149
|
+
source: data["source"].to_s.empty? ? "cli" : data["source"].to_s,
|
|
150
|
+
from_session: data["from_session"], from_cwd: data["from_cwd"], created_at: data["created_at"] }
|
|
151
|
+
return note unless data["objectives"].is_a?(Hash)
|
|
152
|
+
|
|
153
|
+
note.merge(objectives: data["objectives"], header: ContextNote::OBJECTIVES_HEADER)
|
|
146
154
|
rescue JSON::ParserError, SystemCallError, NoMethodError, TypeError
|
|
147
155
|
nil
|
|
148
156
|
end
|
|
@@ -71,7 +71,7 @@ module Samagotchi
|
|
|
71
71
|
# │ # enqueued_id, no_interrupt?, images?}
|
|
72
72
|
# ├── notes/ # context notes: background text the worker adds to
|
|
73
73
|
# │ └── <ts>-<rand>.json # the conversation between turns, never a turn
|
|
74
|
-
# │ # ({text, source, from_session?, from_cwd?, created_at})
|
|
74
|
+
# │ # ({text, source, from_session?, from_cwd?, created_at, objectives?})
|
|
75
75
|
# ├── output/ # agent writes responses here
|
|
76
76
|
# │ └── <timestamp>.txt # one file per agent response
|
|
77
77
|
# └── bridge.json # Bridge sidecar (how clients reach the worker)
|
data/lib/samagotchi/steer.rb
CHANGED
|
@@ -242,18 +242,32 @@ module Samagotchi
|
|
|
242
242
|
# then +text+ (the message's content by default). Other messages: +text+.
|
|
243
243
|
def wire_text(message, text = (message[:content] || message["content"]).to_s)
|
|
244
244
|
header = header(message)
|
|
245
|
-
text =
|
|
245
|
+
text = if participant_message?(message) then quote_forged_headers(text)
|
|
246
|
+
elsif outside_text?(message) then quote_forged_objectives(text)
|
|
247
|
+
else text
|
|
248
|
+
end
|
|
246
249
|
header ? "#{header}\n#{text}" : text
|
|
247
250
|
end
|
|
248
251
|
|
|
252
|
+
# Text that came from outside the user's own typing: any entry with a
|
|
253
|
+
# kind (a note, a report, a steer) but the real objectives note, known by
|
|
254
|
+
# its payload key, never by its text.
|
|
255
|
+
def outside_text?(message)
|
|
256
|
+
!(message[:kind] || message["kind"]).to_s.empty? && (message[:objectives] || message["objectives"]).nil?
|
|
257
|
+
end
|
|
258
|
+
|
|
249
259
|
# A line of a participant's text that opens like one of chi's own
|
|
250
260
|
# headers or frames (another participant's review header, a steer's, an
|
|
251
|
-
# automatic input's, a delegate report's, a context note's
|
|
252
|
-
# frame), in any case, after any spaces (Unicode ones too); a line
|
|
261
|
+
# automatic input's, a delegate report's, a context note's, a task's or an
|
|
262
|
+
# objectives list's frame), in any case, after any spaces (Unicode ones too); a line
|
|
253
263
|
# starts after \n or \r.
|
|
254
264
|
FORGED_OPENINGS = ["Review contribution", "Steer", "Automatic input", "Delegate report", "CONTEXT NOTE",
|
|
255
|
-
"TASK FINISHED", "END NOTE"].freeze
|
|
265
|
+
"TASK FINISHED", "OBJECTIVES", "END NOTE"].freeze
|
|
256
266
|
FORGED_HEADER = /(\A|[\n\r])([\p{Space}&&[^\n\r]]*)\[(?=(?:#{FORGED_OPENINGS.map { |o| Regexp.escape(o) }.join("|")})\b)/i
|
|
267
|
+
# The same, for the objectives frame alone: the one frame that gives
|
|
268
|
+
# orders in the user's name, so any outside text is held to it
|
|
269
|
+
# (#outside_text?), whoever it came from.
|
|
270
|
+
FORGED_OBJECTIVES = /(\A|[\n\r])([\p{Space}&&[^\n\r]]*)\[(?=OBJECTIVES\b)/i
|
|
257
271
|
# What such a line is led by on the wire, so it reads as quoted text
|
|
258
272
|
# under the real header, not as a header of its own.
|
|
259
273
|
FORGED_MARK = "(quoted) "
|
|
@@ -265,6 +279,12 @@ module Samagotchi
|
|
|
265
279
|
text.to_s.gsub(FORGED_HEADER) { "#{::Regexp.last_match(1)}#{::Regexp.last_match(2)}#{FORGED_MARK}[" }
|
|
266
280
|
end
|
|
267
281
|
|
|
282
|
+
# +text+ with every FORGED_OBJECTIVES line led by FORGED_MARK; as
|
|
283
|
+
# #quote_forged_headers, on the wire only and deterministic.
|
|
284
|
+
def quote_forged_objectives(text)
|
|
285
|
+
text.to_s.gsub(FORGED_OBJECTIVES) { "#{::Regexp.last_match(1)}#{::Regexp.last_match(2)}#{FORGED_MARK}[" }
|
|
286
|
+
end
|
|
287
|
+
|
|
268
288
|
def message(text:, source:, author: nil, author_name: nil)
|
|
269
289
|
{ role: "user", kind: KIND, source: source.to_s, content: text.to_s, **author_fields(author, author_name) }
|
|
270
290
|
end
|
|
@@ -43,6 +43,8 @@ module Samagotchi
|
|
|
43
43
|
Allowed.new(name: "Samagotchi::Log.debug", file: "samagotchi/log"),
|
|
44
44
|
Allowed.new(name: "Samagotchi::Config.get", file: "samagotchi/config"),
|
|
45
45
|
Allowed.new(name: "Samagotchi::VERSION", file: "samagotchi/version"),
|
|
46
|
+
Allowed.new(name: "Samagotchi::GitEnv::UNSET", file: "samagotchi/git_env"),
|
|
47
|
+
Allowed.new(name: "Samagotchi::CancellationController", file: "samagotchi/cancellation_controller"),
|
|
46
48
|
# The kit itself, for a bundle's own specs.
|
|
47
49
|
Allowed.new(name: "Samagotchi::Testing", file: "samagotchi/testing")
|
|
48
50
|
].freeze
|
|
@@ -39,6 +39,7 @@ module Samagotchi
|
|
|
39
39
|
notify: ->(text, level, label, fallback_for: nil) { recorder.notify(text, level, label, fallback_for) },
|
|
40
40
|
ask_user: ->(question) { recorder.ask_user(question) },
|
|
41
41
|
cancelled: -> { recorder[:cancelled] },
|
|
42
|
+
turn_running: -> { recorder[:running] },
|
|
42
43
|
card: ->(**card) { recorder.card(**card) },
|
|
43
44
|
ask_model: lambda { |request, timeout:, max_tokens:, cancel_controller:, model: nil, source: nil|
|
|
44
45
|
recorder.ask_model(request, timeout: timeout, max_tokens: max_tokens, cancel_controller: cancel_controller,
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
require "rspec/core"
|
|
4
4
|
require_relative "../testing"
|
|
5
5
|
|
|
6
|
+
# Not in testing.rb: `chi bundle check` loads that into a real chi.
|
|
7
|
+
Samagotchi::GitEnv.scrub!
|
|
8
|
+
|
|
6
9
|
# The bundle test kit in RSpec (EXPERIMENTAL, docs/plugins.md): example groups
|
|
7
10
|
# tagged :bundle get Samagotchi::Testing::Helpers (load_bundle,
|
|
8
11
|
# shipped_bundle, sandbox), and each of their examples runs in its own
|
|
@@ -7,12 +7,13 @@ require_relative "../log"
|
|
|
7
7
|
require_relative "../paths"
|
|
8
8
|
require_relative "../sandbox_layout"
|
|
9
9
|
require_relative "fake_executable"
|
|
10
|
+
require_relative "../git_env"
|
|
10
11
|
|
|
11
12
|
module Samagotchi
|
|
12
13
|
module Testing
|
|
13
14
|
# The environment a bundle test runs in: temp config, state, data and cache
|
|
14
15
|
# homes (config.yml, memories, ctx.data_dir, the debug log), HOME and the peer
|
|
15
|
-
# CLIs' folders, git without the user's config, no Keychain, none of
|
|
16
|
+
# CLIs' folders, git without the user's config or an inherited repo (GitEnv), no Keychain, none of
|
|
16
17
|
# the user's SAMAGOTCHI_* settings, a bin/ first on PATH for fake
|
|
17
18
|
# commands. ENV is put back on #leave.
|
|
18
19
|
#
|
|
@@ -166,6 +167,7 @@ module Samagotchi
|
|
|
166
167
|
# ENV at +layout+'s folders, none of the SAMAGOTCHI_* settings but KEPT.
|
|
167
168
|
def point_env(layout)
|
|
168
169
|
ENV.each_key.select { |key| key.start_with?("SAMAGOTCHI_") && !KEPT.include?(key) }.each { |key| ENV.delete(key) }
|
|
170
|
+
GitEnv.clear(ENV)
|
|
169
171
|
File.write(File.join(@root, "gitconfig"), "")
|
|
170
172
|
ENV.update(
|
|
171
173
|
**layout.env,
|
|
@@ -23,6 +23,7 @@ require_relative "tools/context_read"
|
|
|
23
23
|
require_relative "tools/delegate"
|
|
24
24
|
require_relative "tools/delegate_result"
|
|
25
25
|
require_relative "tools/forget_outputs"
|
|
26
|
+
require_relative "tools/objectives_tool"
|
|
26
27
|
|
|
27
28
|
module Samagotchi
|
|
28
29
|
# The one-line summary of a tool call that the UIs show ("reading file",
|
|
@@ -85,6 +86,15 @@ module Samagotchi
|
|
|
85
86
|
event
|
|
86
87
|
end
|
|
87
88
|
|
|
89
|
+
def objectives_proposal?(call) = call[:action].to_s.strip.downcase == Tools::ObjectivesTool::PROPOSE
|
|
90
|
+
|
|
91
|
+
# An objectives row's words: "O2 note · evidence", or "propose done O2 note".
|
|
92
|
+
def objectives_title(call)
|
|
93
|
+
return "#{Tools::ObjectivesTool::PROPOSE} #{call[:change]} #{call[:id]} #{call[:text]} · #{call[:note]}".squeeze(" ") if objectives_proposal?(call)
|
|
94
|
+
|
|
95
|
+
"#{call[:id]} #{call[:note]}#{" · #{call[:evidence]}" unless call[:evidence].to_s.strip.empty?}"
|
|
96
|
+
end
|
|
97
|
+
|
|
88
98
|
# An execute's description, cut as a row's title; nil for the rest.
|
|
89
99
|
def command_description(tool_name, call)
|
|
90
100
|
return nil unless call.is_a?(Hash) && ToolView::TOOLS.include?(tool_name)
|
|
@@ -112,6 +122,7 @@ module Samagotchi
|
|
|
112
122
|
when Tools::Read::NAME, Tools::MemoryRead::NAME then call[:content]
|
|
113
123
|
when Tools::Write::NAME, Tools::Edit::NAME, Tools::MemoryWrite::NAME then call[:path]
|
|
114
124
|
when Tools::Execute::NAME, Tools::TaskCreate::NAME then command_title(call[:content])
|
|
125
|
+
when Tools::ObjectivesTool::NAME then objectives_title(call)
|
|
115
126
|
end
|
|
116
127
|
text = text.to_s.strip
|
|
117
128
|
return nil if text.empty?
|
|
@@ -205,6 +216,7 @@ module Samagotchi
|
|
|
205
216
|
when Tools::Delegate::NAME then "delegating"
|
|
206
217
|
when Tools::DelegateResult::NAME then "waiting for a delegate"
|
|
207
218
|
when Tools::ForgetOutputs::NAME then forget_outputs_action(call)
|
|
219
|
+
when Tools::ObjectivesTool::NAME then objectives_proposal?(call) ? "proposing a change" : "recording a move"
|
|
208
220
|
else registry_entry(registry, tool_name)&.label || "calling tool"
|
|
209
221
|
end
|
|
210
222
|
end
|
|
@@ -430,6 +442,11 @@ module Samagotchi
|
|
|
430
442
|
parts << "restore=#{preview_tool_param(request.restore.join(","))}" if request.restore?
|
|
431
443
|
parts << "note=#{preview_tool_param(request.note)}" unless request.note.empty?
|
|
432
444
|
parts.empty? ? nil : parts.join(" ")
|
|
445
|
+
when Tools::ObjectivesTool::NAME
|
|
446
|
+
parts = ["id=#{preview_tool_param(call[:id])}", "note=#{preview_tool_param(call[:note])}"]
|
|
447
|
+
parts.unshift("change=#{preview_tool_param(call[:change])}") if objectives_proposal?(call)
|
|
448
|
+
parts << "evidence=#{preview_tool_param(call[:evidence])}" unless call[:evidence].to_s.strip.empty?
|
|
449
|
+
parts.join(" ")
|
|
433
450
|
when Tools::AskUserQuestion::NAME
|
|
434
451
|
parts = ["question=#{preview_tool_param(call[:question] || call[:content])}"]
|
|
435
452
|
opts = call[:options]
|
|
@@ -440,6 +440,40 @@ module Samagotchi
|
|
|
440
440
|
},
|
|
441
441
|
required: %w[question options]
|
|
442
442
|
}
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
name: "objectives",
|
|
446
|
+
description: "Record a move on one of the objectives in the OBJECTIVES note: in a line, what you did or found that moves it, so the user can follow. One when an objective really moved, not per step. Or propose a change to the list (action propose): the user sees a card and applies it or not. This tool doesn't edit the list, change a status or mark anything done, and it approves nothing; the user edits the list with /objectives. Asked to change it, propose it or say so, instead of saying you changed it.",
|
|
447
|
+
parameters: {
|
|
448
|
+
type: "object",
|
|
449
|
+
properties: {
|
|
450
|
+
action: {
|
|
451
|
+
type: "string",
|
|
452
|
+
description: "move or propose"
|
|
453
|
+
},
|
|
454
|
+
id: {
|
|
455
|
+
type: "string",
|
|
456
|
+
description: "The objective's id, e.g. O2 (not for a proposed add)"
|
|
457
|
+
},
|
|
458
|
+
note: {
|
|
459
|
+
type: "string",
|
|
460
|
+
description: "A move: what moved, in a line. A proposal: why, in a line"
|
|
461
|
+
},
|
|
462
|
+
evidence: {
|
|
463
|
+
type: "string",
|
|
464
|
+
description: "Optional, a move's: what shows it (a commit, a test count, a file)"
|
|
465
|
+
},
|
|
466
|
+
change: {
|
|
467
|
+
type: "string",
|
|
468
|
+
description: "A proposal's: add, reword, done or drop"
|
|
469
|
+
},
|
|
470
|
+
text: {
|
|
471
|
+
type: "string",
|
|
472
|
+
description: "A proposed add's or reword's new text"
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
required: %w[action note]
|
|
476
|
+
}
|
|
443
477
|
}
|
|
444
478
|
].freeze
|
|
445
479
|
|
|
@@ -53,7 +53,8 @@ module Samagotchi
|
|
|
53
53
|
"delegate" => { content: "task", fallback: :prefix_or_raw },
|
|
54
54
|
"ask_user_question" => { content: "question", also: %w[question], verbatim: %w[options], options: true,
|
|
55
55
|
fallback: :raw },
|
|
56
|
-
"forget_outputs" => { content: "note", also: %w[note], verbatim: %w[ids keep restore] }
|
|
56
|
+
"forget_outputs" => { content: "note", also: %w[note], verbatim: %w[ids keep restore] },
|
|
57
|
+
"objectives" => { content: "note", also: %w[note] }
|
|
57
58
|
}.freeze
|
|
58
59
|
|
|
59
60
|
# A name the model uses for a built-in, mapped to that built-in (a model
|
|
@@ -25,6 +25,7 @@ require_relative "delegate"
|
|
|
25
25
|
require_relative "delegate_result"
|
|
26
26
|
require_relative "ask_user_question"
|
|
27
27
|
require_relative "forget_outputs"
|
|
28
|
+
require_relative "objectives_tool"
|
|
28
29
|
require_relative "../guardrails/parent_approvals"
|
|
29
30
|
require_relative "../guardrails/pair_boundary"
|
|
30
31
|
require_relative "../model_profile"
|
|
@@ -58,7 +59,8 @@ module Samagotchi
|
|
|
58
59
|
ContextRead,
|
|
59
60
|
Delegate,
|
|
60
61
|
DelegateResult,
|
|
61
|
-
AskUserQuestion
|
|
62
|
+
AskUserQuestion,
|
|
63
|
+
ObjectivesTool
|
|
62
64
|
].freeze
|
|
63
65
|
# The built-ins an LLM context layer adds (offered only under it).
|
|
64
66
|
LAYER_CLASSES = [ForgetOutputs].freeze
|
|
@@ -115,6 +117,7 @@ module Samagotchi
|
|
|
115
117
|
DelegateResult.call(call[:content], session: call[:session], timeout: call[:timeout], peers: kctx.peers)
|
|
116
118
|
end,
|
|
117
119
|
AskUserQuestion::NAME => ->(call, kctx) { kctx.ask_user_question(call) },
|
|
120
|
+
ObjectivesTool::NAME => ->(call, kctx) { ObjectivesTool.call(call, peers: kctx.peers) },
|
|
118
121
|
ForgetOutputs::NAME => ->(call, kctx) { kctx.forget_outputs(call) }
|
|
119
122
|
}.freeze
|
|
120
123
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../client_id"
|
|
4
|
+
require_relative "../git_env"
|
|
4
5
|
require_relative "../session"
|
|
5
6
|
require_relative "../config"
|
|
6
7
|
require_relative "../model_profile"
|
|
@@ -272,7 +273,7 @@ module Samagotchi
|
|
|
272
273
|
git_dir = MemoryPaths.git_dir(folder)
|
|
273
274
|
return false if git_dir && within?(folder, File.realpath(git_dir))
|
|
274
275
|
|
|
275
|
-
out = IO.popen(["git", "-C", folder, "rev-parse", "--is-inside-work-tree"], err: File::NULL, &:read)
|
|
276
|
+
out = IO.popen(GitEnv::UNSET, ["git", "-C", folder, "rev-parse", "--is-inside-work-tree"], err: File::NULL, &:read)
|
|
276
277
|
out.strip == "true"
|
|
277
278
|
rescue SystemCallError
|
|
278
279
|
false
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../tool_response"
|
|
4
|
+
|
|
5
|
+
module Samagotchi
|
|
6
|
+
# Read at a call only: the builtins' file list loads this, Session not with it.
|
|
7
|
+
autoload :Objectives, File.expand_path("../objectives", __dir__)
|
|
8
|
+
|
|
9
|
+
module Tools
|
|
10
|
+
# The objectives tool's two actions. A move: what the model did or found for
|
|
11
|
+
# an objective. Information only: it never changes a status or the list
|
|
12
|
+
# (the user does, /objectives), and nothing is written here: the call in
|
|
13
|
+
# history is the record, which Objectives.build reads back. A proposal: a
|
|
14
|
+
# change to the list the user applies from a card with one tap, the card's
|
|
15
|
+
# action being the /objectives line; the model never changes the list by
|
|
16
|
+
# itself. See docs/internals/objectives.md.
|
|
17
|
+
class ObjectivesTool
|
|
18
|
+
NAME = "objectives"
|
|
19
|
+
MOVE = "move"
|
|
20
|
+
PROPOSE = "propose"
|
|
21
|
+
PROPOSED = "Proposed %s. The list is unchanged: the user sees a card and applies it or not."
|
|
22
|
+
RECORDED = "Recorded a move on %s."
|
|
23
|
+
EDITED_BY_USER = "The user edits the list with /objectives."
|
|
24
|
+
NONE = "There are no objectives here, so nothing was recorded. #{EDITED_BY_USER}".freeze
|
|
25
|
+
|
|
26
|
+
def self.name = NAME
|
|
27
|
+
|
|
28
|
+
# Whether +output+, a saved run, is this tool's answer that it recorded a
|
|
29
|
+
# move on +id+ (Objectives.moves_in). Positive: a run that recorded
|
|
30
|
+
# nothing (no list, a refusal, a denial, an interrupt) has no such answer.
|
|
31
|
+
def self.recorded?(output, id)
|
|
32
|
+
output.to_s.sub(ToolResponse::LEAD, "").start_with?(RECORDED % id)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param call [Hash] the internal call (BuiltinCalls: action:, id:,
|
|
36
|
+
# note:, evidence:)
|
|
37
|
+
# @param peers [#objectives, nil] the session's list
|
|
38
|
+
# @return [String] the tool result
|
|
39
|
+
def self.call(call, peers:)
|
|
40
|
+
action = call[:action].to_s.strip.downcase
|
|
41
|
+
unless action.empty? || [MOVE, PROPOSE].include?(action)
|
|
42
|
+
return "Error: this tool records a move (action: move) or proposes a change (action: propose); it edits nothing. #{EDITED_BY_USER}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
list = peers&.objectives
|
|
46
|
+
return NONE if list.nil? || list.empty?
|
|
47
|
+
return propose(list, call, peers) if action == PROPOSE
|
|
48
|
+
|
|
49
|
+
id = Objectives.move_id(call)
|
|
50
|
+
refusal(list, id, call) || "#{RECORDED % id} The list is unchanged: this tool doesn't edit it. #{EDITED_BY_USER}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.propose(list, call, peers)
|
|
54
|
+
proposal = Objectives::Proposal.against(list, change: call[:change].to_s.strip.downcase, id: Objectives.move_id(call),
|
|
55
|
+
text: Objectives.clean(call[:text]), note: Objectives.clean(call[:note]))
|
|
56
|
+
refused = proposal.refusal(list)
|
|
57
|
+
return "Error: #{refused}. Nothing was proposed." if refused
|
|
58
|
+
|
|
59
|
+
return "Error: at most #{Objectives::PROPOSALS_PER_TURN} proposals a turn. Nothing more was proposed." unless peers.propose_objective(proposal)
|
|
60
|
+
|
|
61
|
+
format(PROPOSED, proposal.line.delete_prefix("/objectives "))
|
|
62
|
+
end
|
|
63
|
+
private_class_method :propose
|
|
64
|
+
|
|
65
|
+
def self.refusal(list, id, call)
|
|
66
|
+
item = list.find(id)
|
|
67
|
+
return "Error: no objective #{id} (this session's: #{list.items.map(&:id).join(", ")}). Nothing was recorded." unless item
|
|
68
|
+
return "Error: #{id} was dropped, so nothing was recorded." if item.status == Objectives::DROPPED
|
|
69
|
+
|
|
70
|
+
"Error: a move needs a note: what moved, in a line." if Objectives.clean(call[:note]).empty?
|
|
71
|
+
end
|
|
72
|
+
private_class_method :refusal
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -27,6 +27,9 @@ module Samagotchi
|
|
|
27
27
|
# A person's message waits for the turn's next boundary: never here
|
|
28
28
|
# (Engine::PeerView asks the running turn).
|
|
29
29
|
def input_waiting? = false
|
|
30
|
+
|
|
31
|
+
# The session's objectives: none here (Engine::PeerView reads them).
|
|
32
|
+
def objectives = nil
|
|
30
33
|
end
|
|
31
34
|
end
|
|
32
35
|
end
|
|
@@ -6,6 +6,7 @@ require "securerandom"
|
|
|
6
6
|
require "shellwords"
|
|
7
7
|
require "time"
|
|
8
8
|
require_relative "../atomic_file"
|
|
9
|
+
require_relative "../bundler_env"
|
|
9
10
|
require_relative "../child_env"
|
|
10
11
|
require_relative "../log"
|
|
11
12
|
require_relative "../process_group"
|
|
@@ -28,7 +29,7 @@ module Samagotchi
|
|
|
28
29
|
STOP_POLL_INTERVAL_SEC = 0.1
|
|
29
30
|
METADATA_VERSION = TaskRecord::METADATA_VERSION
|
|
30
31
|
OUTPUT_TAIL_READ_BYTES = 16 * 1024
|
|
31
|
-
SANITIZED_ENV_KEYS =
|
|
32
|
+
SANITIZED_ENV_KEYS = BundlerEnv::CHILD_RUBY
|
|
32
33
|
# Set by chi (Builtins.parent_env); the model's env can't change it.
|
|
33
34
|
MARKER_ENV_KEYS = ChildEnv::SESSION_MARKERS
|
|
34
35
|
# task_wait and task_get add it to a task the user stopped (the web's
|
|
@@ -448,15 +449,16 @@ module Samagotchi
|
|
|
448
449
|
# chi's own Ruby/Bundler setup, kept out of a command's environment
|
|
449
450
|
# (execute and task_create both spawn with it): run under `bundle
|
|
450
451
|
# exec`, chi would otherwise hand a project's `ruby -e` or
|
|
451
|
-
# `bundle exec` its Gemfile (RUBYOPT=-rbundler/setup, BUNDLE_GEMFILE
|
|
452
|
+
# `bundle exec` its Gemfile (RUBYOPT=-rbundler/setup, BUNDLE_GEMFILE,
|
|
453
|
+
# BUNDLER_SETUP) and lockfile.
|
|
452
454
|
def sanitized_spawn_env
|
|
453
|
-
{
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
455
|
+
SANITIZED_ENV_KEYS.to_h { |key| [key, nil] }.merge("RUBYOPT" => sanitized_rubyopt)
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# The model's way out: a Ruby or Bundler variable it does want, for one command.
|
|
459
|
+
def sanitized_key_error(key)
|
|
460
|
+
"Error: env key is reserved: #{key} (chi clears it for every command; to set it for this one, " \
|
|
461
|
+
"put it in the command: #{key}=value cmd)"
|
|
460
462
|
end
|
|
461
463
|
|
|
462
464
|
def spawn_env_for(overrides)
|
|
@@ -469,7 +471,8 @@ module Samagotchi
|
|
|
469
471
|
overrides.each do |key, value|
|
|
470
472
|
return [nil, "Error: env keys and values must be strings"] unless key.is_a?(String) && value.is_a?(String)
|
|
471
473
|
return [nil, "Error: env keys and values cannot contain NUL bytes"] if key.include?("\0") || value.include?("\0")
|
|
472
|
-
return [nil, "Error: env key is reserved: #{key}"] if
|
|
474
|
+
return [nil, "Error: env key is reserved: #{key}"] if MARKER_ENV_KEYS.include?(key)
|
|
475
|
+
return [nil, sanitized_key_error(key)] if SANITIZED_ENV_KEYS.include?(key)
|
|
473
476
|
|
|
474
477
|
normalized[key] = value
|
|
475
478
|
end
|
data/lib/samagotchi/turn_flow.rb
CHANGED
|
@@ -191,7 +191,8 @@ module Samagotchi
|
|
|
191
191
|
# Restore the pre-turn checkpoint (!rollback after a Ctrl-C). A pending
|
|
192
192
|
# offer goes too: the turn it offered to continue is gone. (The REPL
|
|
193
193
|
# reads every line as an answer while an offer is pending; a worker
|
|
194
|
-
# also takes !rollback then.)
|
|
194
|
+
# also takes !rollback then.) :turn_rolled_back tells the cards' count
|
|
195
|
+
# (Bridge::CardStore) that the turn's prompt is gone.
|
|
195
196
|
# @return [Boolean] false when there is none
|
|
196
197
|
def rollback!
|
|
197
198
|
return false unless @checkpoint
|
|
@@ -199,6 +200,7 @@ module Samagotchi
|
|
|
199
200
|
restore(@checkpoint)
|
|
200
201
|
@checkpoint = nil
|
|
201
202
|
@offer = nil
|
|
203
|
+
@engine.announce({ type: :turn_rolled_back })
|
|
202
204
|
true
|
|
203
205
|
end
|
|
204
206
|
|