samagotchi 0.85.0 → 0.86.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 +60 -1
- data/docs/architecture.md +34 -5
- data/docs/bundle-authoring.md +125 -0
- data/docs/cli.md +38 -3
- data/docs/configuration.md +6 -5
- data/docs/guardrails.md +63 -35
- data/docs/hooks.md +19 -3
- data/docs/internals/anthropic.md +2 -2
- data/docs/internals/bundles.md +11 -2
- data/docs/internals/context-telemetry.md +19 -1
- data/docs/internals/engine.md +1 -1
- data/docs/memory.md +1 -1
- data/docs/plugins.md +59 -2
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +6 -6
- data/docs/sub-agent.md +35 -24
- data/docs/testing.md +7 -2
- data/lib/samagotchi/answer_command.rb +21 -25
- data/lib/samagotchi/bridge/client_event.rb +50 -0
- data/lib/samagotchi/bridge/sse_writer.rb +6 -2
- data/lib/samagotchi/bridge.rb +22 -11
- data/lib/samagotchi/bridge_client.rb +2 -2
- data/lib/samagotchi/bundle_command/check.rb +348 -0
- data/lib/samagotchi/bundle_command/scaffold.rb +135 -0
- data/lib/samagotchi/bundle_command/test_run.rb +89 -0
- data/lib/samagotchi/bundle_command.rb +125 -2
- data/lib/samagotchi/bundles/core/manifest.yml +2 -2
- data/lib/samagotchi/bundles/cost-cap/manifest.yml +4 -4
- data/lib/samagotchi/bundles/cost-cap/plugin.rb +4 -1
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -1
- data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +4 -2
- data/lib/samagotchi/bundles/guardrails/manifest.yml +1 -1
- data/lib/samagotchi/bundles/ruby/manifest.yml +11 -0
- data/lib/samagotchi/bundles/ruby/plugin.rb +74 -0
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/self_map.md +2 -1
- data/lib/samagotchi/client_id.rb +1 -1
- data/lib/samagotchi/config.rb +195 -115
- data/lib/samagotchi/context_status.rb +1 -1
- data/lib/samagotchi/continue_offer.rb +2 -1
- data/lib/samagotchi/engine.rb +4 -4
- data/lib/samagotchi/guardrail_wiring.rb +4 -0
- data/lib/samagotchi/guardrails/approval.rb +12 -2
- data/lib/samagotchi/guardrails/load_failures.rb +17 -3
- data/lib/samagotchi/guardrails/parent_approvals.rb +14 -70
- data/lib/samagotchi/guardrails/parent_policy.rb +258 -0
- data/lib/samagotchi/guardrails/verdict.rb +6 -3
- data/lib/samagotchi/hook_routes.rb +5 -3
- data/lib/samagotchi/hooks/bundle_loader.rb +3 -1
- data/lib/samagotchi/hooks/registry.rb +13 -6
- data/lib/samagotchi/hooks.rb +5 -0
- data/lib/samagotchi/kernel_loop.rb +5 -2
- data/lib/samagotchi/llm/anthropic_assembly.rb +1 -1
- data/lib/samagotchi/memory_bundle/shas.rb +127 -0
- data/lib/samagotchi/memory_bundle.rb +1 -0
- data/lib/samagotchi/parent_report.rb +87 -14
- data/lib/samagotchi/pending_question.rb +8 -4
- data/lib/samagotchi/plugin/api.rb +16 -0
- data/lib/samagotchi/plugin/context.rb +11 -5
- data/lib/samagotchi/plugin/loader.rb +5 -3
- data/lib/samagotchi/question_desk.rb +37 -24
- data/lib/samagotchi/queued_command.rb +5 -4
- data/lib/samagotchi/relay.rb +5 -1
- data/lib/samagotchi/relay_verifier.rb +4 -5
- data/lib/samagotchi/reply_wait.rb +14 -5
- data/lib/samagotchi/send_command.rb +34 -8
- data/lib/samagotchi/session_commands.rb +14 -3
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +1 -1
- data/lib/samagotchi/terminal_ui/attached_loop.rb +13 -11
- data/lib/samagotchi/terminal_ui/thinking_line.rb +5 -9
- data/lib/samagotchi/terminal_ui.rb +12 -9
- data/lib/samagotchi/testing/boundary.rb +0 -3
- data/lib/samagotchi/testing/bundle.rb +7 -0
- data/lib/samagotchi/testing/host.rb +6 -4
- data/lib/samagotchi/testing/install.rb +119 -0
- data/lib/samagotchi/testing/recorder.rb +13 -4
- data/lib/samagotchi/testing.rb +1 -0
- data/lib/samagotchi/tools/builtins.rb +2 -1
- data/lib/samagotchi/tools/delegate_wait.rb +6 -1
- data/lib/samagotchi/tools/task_runtime.rb +5 -3
- data/lib/samagotchi/turn_checkpoint.rb +64 -2
- data/lib/samagotchi/turn_flow.rb +10 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +48 -10
- data/lib/samagotchi/web/public/chunk_router.js +8 -19
- data/lib/samagotchi/web/public/ctx.js +12 -26
- data/lib/samagotchi/web/public/stream_handlers.js +1 -1
- data/lib/samagotchi/web/server.rb +4 -0
- data/lib/samagotchi/worker.rb +19 -5
- data/lib/samagotchi/worker_inbound.rb +4 -4
- metadata +11 -2
- data/lib/samagotchi/guardrails/parent_continue.rb +0 -43
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
|
|
5
|
+
module Samagotchi
|
|
6
|
+
class BundleCommand
|
|
7
|
+
# `chi bundle test [PATH] [-- ARGS]`: a bundle's tests on the Ruby and
|
|
8
|
+
# the lib chi runs on (RbConfig.ruby, -I chi's lib), which an external
|
|
9
|
+
# bundle can't easily find itself (mise, rbenv, gem paths). test/**/
|
|
10
|
+
# *_test.rb runs on Minitest with the kit's Case; a spec/ with no test/
|
|
11
|
+
# on RSpec with the kit's helpers, as `rake bundles:specs` runs the
|
|
12
|
+
# shipped ones. Outside any Bundler env (Bundler.with_unbundled_env, as
|
|
13
|
+
# SessionManager's unbundled_env_changes): chi's gems don't include minitest.
|
|
14
|
+
# When the framework loads only inside the Bundler env chi itself runs in
|
|
15
|
+
# (a checkout or a vendored install: its gems in vendor/bundle), there.
|
|
16
|
+
# The tests' exit status is #run's.
|
|
17
|
+
class TestRun
|
|
18
|
+
LIB_DIR = File.expand_path("../..", __dir__)
|
|
19
|
+
|
|
20
|
+
def initialize(dir, args: [], stdout: $stdout, stderr: $stderr)
|
|
21
|
+
@dir = File.expand_path(dir)
|
|
22
|
+
@args = args
|
|
23
|
+
@stdout = stdout
|
|
24
|
+
@stderr = stderr
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @return [Integer] exit status
|
|
28
|
+
def run
|
|
29
|
+
tests = Dir.glob(File.join(@dir, "test", "**", "*_test.rb")).sort
|
|
30
|
+
return minitest(tests) if tests.any?
|
|
31
|
+
return rspec if File.directory?(File.join(@dir, "spec"))
|
|
32
|
+
|
|
33
|
+
@stderr.puts "chi bundle test: no test/*_test.rb or spec/ in #{@dir} (chi bundle new NAME writes a test to copy)"
|
|
34
|
+
CLI::Exit::FAILED
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def minitest(tests)
|
|
40
|
+
return missing("minitest", "gem install minitest") unless loads?("minitest")
|
|
41
|
+
|
|
42
|
+
requires = tests.map { |file| "require #{file.inspect}" }.join("; ")
|
|
43
|
+
say("#{tests.size} test file#{"s" unless tests.size == 1}")
|
|
44
|
+
spawn(RbConfig.ruby, "-I", LIB_DIR, "-r", "minitest/autorun", "-r", "samagotchi/testing/minitest", "-e", requires,
|
|
45
|
+
"--", *@args)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def rspec
|
|
49
|
+
return missing("rspec", "gem install rspec") unless loads?("rspec/core")
|
|
50
|
+
|
|
51
|
+
say("spec/ on RSpec")
|
|
52
|
+
spawn(RbConfig.ruby, "-I", LIB_DIR, "-r", "rspec/core", "-e", "exit RSpec::Core::Runner.run(ARGV)", "--",
|
|
53
|
+
"--options", File::NULL, "-r", "samagotchi/testing/rspec", "spec", *@args)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def say(what) = @stdout.puts("chi bundle test: #{what} on #{RbConfig.ruby} (chi's lib #{LIB_DIR})")
|
|
57
|
+
|
|
58
|
+
def missing(lib, hint)
|
|
59
|
+
@stderr.puts "chi bundle test: #{lib} doesn't load on chi's Ruby (#{RbConfig.ruby}); #{hint}"
|
|
60
|
+
CLI::Exit::USAGE
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Whether +lib+ loads unbundled, else in chi's own Bundler env (then
|
|
64
|
+
# #spawn stays in it).
|
|
65
|
+
def loads?(lib)
|
|
66
|
+
check = -> { system(RbConfig.ruby, "-e", "require #{lib.inspect}", out: File::NULL, err: File::NULL) }
|
|
67
|
+
@bundled = false
|
|
68
|
+
return true if unbundled(&check)
|
|
69
|
+
|
|
70
|
+
@bundled = bundler?
|
|
71
|
+
@bundled && check.call
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def spawn(*command)
|
|
75
|
+
@stdout.flush
|
|
76
|
+
run = -> { system(*command, chdir: @dir) }
|
|
77
|
+
@bundled ? run.call : unbundled(&run)
|
|
78
|
+
$?.exitstatus || CLI::Exit::FAILED
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def unbundled(&)
|
|
82
|
+
bundler? ? ::Bundler.with_unbundled_env(&) : yield
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Bundler may be defined without its runtime (RubyGems' own stub).
|
|
86
|
+
def bundler? = defined?(::Bundler) && ::Bundler.respond_to?(:with_unbundled_env)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -10,7 +10,7 @@ module Samagotchi
|
|
|
10
10
|
# subcommands have their own flags).
|
|
11
11
|
class BundleCommand
|
|
12
12
|
USAGE = <<~TEXT
|
|
13
|
-
Usage: chi bundle <install|upgrade|uninstall|status|diff|list|build|trash> [options]
|
|
13
|
+
Usage: chi bundle <install|upgrade|uninstall|status|diff|list|build|trash|new|test|check> [options]
|
|
14
14
|
|
|
15
15
|
install <source> [--scope system|project] [--force]
|
|
16
16
|
Install a memory bundle from a directory, zip, tar archive, or git URL.
|
|
@@ -42,6 +42,16 @@ module Samagotchi
|
|
|
42
42
|
trash [--empty] [--dry-run] [--older-than DAYS]
|
|
43
43
|
List bundle trash (moved files from uninstalls/upgrades).
|
|
44
44
|
--empty deletes all trash folders. --older-than DAYS keeps only recent ones.
|
|
45
|
+
|
|
46
|
+
new NAME [--dir DIR]
|
|
47
|
+
Start a bundle in DIR/NAME (default ./NAME): manifest, plugin, a passing test, README.
|
|
48
|
+
|
|
49
|
+
test [PATH] [-- ARGS]
|
|
50
|
+
Run a bundle's test/*_test.rb (Minitest) or spec/ (RSpec) on chi's Ruby and lib.
|
|
51
|
+
|
|
52
|
+
check [PATH] [--fix] [--set KEY=VALUE]
|
|
53
|
+
Check a bundle you're writing (default: the current dir): manifest, files, sha256 lines,
|
|
54
|
+
requires_chi, and an install + load in a sandbox. --fix rewrites stale sha256 lines.
|
|
45
55
|
TEXT
|
|
46
56
|
|
|
47
57
|
# Each subcommand's --help (and only --help: -h and help are taken as
|
|
@@ -71,6 +81,33 @@ module Samagotchi
|
|
|
71
81
|
chi bundle build --scope system --out ./my-bundle/ identity.md work.md
|
|
72
82
|
TEXT
|
|
73
83
|
TRASH_HELP = "Usage: chi bundle trash [--empty] [--dry-run] [--older-than DAYS]"
|
|
84
|
+
NEW_HELP = <<~TEXT
|
|
85
|
+
Usage: chi bundle new NAME [--dir DIR]
|
|
86
|
+
|
|
87
|
+
Start a bundle in DIR/NAME (default ./NAME; refused when it exists): manifest.yml (shas filled,
|
|
88
|
+
requires_chi this chi), plugin.rb (a /NAME command and an after_tool_call hook), test/NAME_test.rb
|
|
89
|
+
(passes as written) and README.md. Then: chi bundle test, chi bundle check, chi bundle install.
|
|
90
|
+
TEXT
|
|
91
|
+
TEST_HELP = <<~TEXT
|
|
92
|
+
Usage: chi bundle test [PATH] [-- ARGS]
|
|
93
|
+
|
|
94
|
+
Run the tests of the bundle at PATH (default: .) on the Ruby and the lib chi runs on, outside any Bundler env:
|
|
95
|
+
test/**/*_test.rb on Minitest (require "samagotchi/testing/minitest"), or else spec/ on RSpec with the kit
|
|
96
|
+
(-r samagotchi/testing/rspec). ARGS go to the test runner (-- -n /name/, -- -e "a name").
|
|
97
|
+
The tests run the bundle's code as written, with your HOME (the kit's sandbox is per test): run code you'd install.
|
|
98
|
+
Exit status: the tests'; 1 no tests; 2 usage, or minitest / rspec doesn't load.
|
|
99
|
+
TEXT
|
|
100
|
+
CHECK_HELP = <<~TEXT
|
|
101
|
+
Usage: chi bundle check [PATH] [--fix] [--set KEY=VALUE]
|
|
102
|
+
|
|
103
|
+
Check the bundle at PATH (default: .) as an install would take it, one finding per line:
|
|
104
|
+
error|warn|info CODE file[:line]: message (fix: hint)
|
|
105
|
+
It installs and loads the bundle in a sandbox (temp HOME, config and state), not in yours, but its plugin and
|
|
106
|
+
hooks run in this process as written: run it on code you'd install.
|
|
107
|
+
--fix rewrite stale and placeholder sha256 lines in manifest.yml
|
|
108
|
+
--set KEY=VALUE a setting for the load (config.yml bundles: <name>: KEY), repeatable
|
|
109
|
+
Exit status: 0 no errors, 1 errors, 2 usage.
|
|
110
|
+
TEXT
|
|
74
111
|
|
|
75
112
|
# The subcommands' flags: --help only; a word not starting with -- is
|
|
76
113
|
# a positional (install's source, -h included). --scope takes whatever
|
|
@@ -90,6 +127,14 @@ module Samagotchi
|
|
|
90
127
|
BUILD_FLAGS = CLI::Flags.new(help: %w[--help], flag_pattern: /\A--/, dash_values: false) do |f|
|
|
91
128
|
%w[--scope --name --version --description --out].each { |name| f.value name }
|
|
92
129
|
end
|
|
130
|
+
NEW_FLAGS = CLI::Flags.new(help: %w[--help], flag_pattern: /\A--/) do |f|
|
|
131
|
+
f.value "--dir"
|
|
132
|
+
end
|
|
133
|
+
TEST_FLAGS = CLI::Flags.new(help: %w[--help], flag_pattern: /\A-/)
|
|
134
|
+
CHECK_FLAGS = CLI::Flags.new(help: %w[--help], flag_pattern: /\A--/) do |f|
|
|
135
|
+
f.switch "--fix"
|
|
136
|
+
f.value "--set", key: :set, repeat: true
|
|
137
|
+
end
|
|
93
138
|
TRASH_FLAGS = CLI::Flags.new(help: %w[--help], flag_pattern: /\A--/) do |f|
|
|
94
139
|
f.switch "--empty"
|
|
95
140
|
f.switch "--dry-run"
|
|
@@ -121,8 +166,11 @@ module Samagotchi
|
|
|
121
166
|
when "list" then list
|
|
122
167
|
when "build" then build(@argv[1..])
|
|
123
168
|
when "trash" then trash(@argv[1..])
|
|
169
|
+
when "new" then new_bundle(@argv[1..])
|
|
170
|
+
when "test" then test_bundle(@argv[1..])
|
|
171
|
+
when "check" then check(@argv[1..])
|
|
124
172
|
else
|
|
125
|
-
@stderr.puts "Unknown bundle subcommand: #{sub}. Use: install, upgrade, uninstall, status, diff, list, build, trash"
|
|
173
|
+
@stderr.puts "Unknown bundle subcommand: #{sub}. Use: install, upgrade, uninstall, status, diff, list, build, trash, new, test, check"
|
|
126
174
|
CLI::Exit::USAGE
|
|
127
175
|
end
|
|
128
176
|
end
|
|
@@ -749,6 +797,81 @@ module Samagotchi
|
|
|
749
797
|
end
|
|
750
798
|
end
|
|
751
799
|
|
|
800
|
+
def new_bundle(rest)
|
|
801
|
+
parsed = parse_flags(NEW_FLAGS, rest, "new", help: NEW_HELP)
|
|
802
|
+
return parsed if parsed.is_a?(Integer)
|
|
803
|
+
|
|
804
|
+
unless parsed.args.size == 1
|
|
805
|
+
@stderr.puts NEW_HELP.lines.first
|
|
806
|
+
return CLI::Exit::USAGE
|
|
807
|
+
end
|
|
808
|
+
require_relative "bundle_command/scaffold"
|
|
809
|
+
scaffold = Scaffold.new(parsed.args.first, parent: parsed.options.fetch(:dir, "."))
|
|
810
|
+
begin
|
|
811
|
+
written = scaffold.write
|
|
812
|
+
rescue ArgumentError => e
|
|
813
|
+
@stderr.puts "chi bundle new: #{e.message}"
|
|
814
|
+
return CLI::Exit::FAILED
|
|
815
|
+
end
|
|
816
|
+
shown = scaffold.dir.start_with?("#{Dir.pwd}/") ? scaffold.dir.delete_prefix("#{Dir.pwd}/") : scaffold.dir
|
|
817
|
+
@stdout.puts "Created #{shown}/: #{written.join(", ")}"
|
|
818
|
+
@stdout.puts <<~TEXT
|
|
819
|
+
Next: cd #{shown}, edit plugin.rb and the test, then
|
|
820
|
+
chi bundle test its tests, on chi's Ruby and lib
|
|
821
|
+
chi bundle check --fix shas, requires_chi, an install and load in a sandbox
|
|
822
|
+
chi bundle install . then a new session loads it
|
|
823
|
+
TEXT
|
|
824
|
+
CLI::Exit::OK
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
# ARGS after "--" go to the test runner untouched.
|
|
828
|
+
def test_bundle(rest)
|
|
829
|
+
split = rest.index("--")
|
|
830
|
+
own, args = split ? [rest[0...split], rest[(split + 1)..]] : [rest, []]
|
|
831
|
+
parsed = parse_flags(TEST_FLAGS, own, "test", help: TEST_HELP)
|
|
832
|
+
return parsed if parsed.is_a?(Integer)
|
|
833
|
+
|
|
834
|
+
dir = parsed.args.first || "."
|
|
835
|
+
unless parsed.args.size <= 1 && File.directory?(dir)
|
|
836
|
+
@stderr.puts parsed.args.size > 1 ? TEST_HELP.lines.first : "chi bundle test: #{dir} isn't a folder"
|
|
837
|
+
return CLI::Exit::USAGE
|
|
838
|
+
end
|
|
839
|
+
require_relative "bundle_command/test_run"
|
|
840
|
+
TestRun.new(dir, args: args, stdout: @stdout, stderr: @stderr).run
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
def check(rest)
|
|
844
|
+
parsed = parse_flags(CHECK_FLAGS, rest, "check", help: CHECK_HELP)
|
|
845
|
+
return parsed if parsed.is_a?(Integer)
|
|
846
|
+
|
|
847
|
+
settings = check_settings(parsed.options.fetch(:set, [])) or return CLI::Exit::USAGE
|
|
848
|
+
if parsed.args.size > 1
|
|
849
|
+
@stderr.puts "chi bundle check takes one PATH (#{parsed.args.join(" ")})"
|
|
850
|
+
return CLI::Exit::USAGE
|
|
851
|
+
end
|
|
852
|
+
dir = parsed.args.first || "."
|
|
853
|
+
unless File.directory?(dir)
|
|
854
|
+
@stderr.puts "chi bundle check: #{dir} isn't a folder"
|
|
855
|
+
return CLI::Exit::USAGE
|
|
856
|
+
end
|
|
857
|
+
require_relative "bundle_command/check"
|
|
858
|
+
Check.new(dir, fix: parsed.options.fetch(:fix, false), settings: settings, stdout: @stdout).run
|
|
859
|
+
end
|
|
860
|
+
|
|
861
|
+
# --set KEY=VALUE values as typed (BundleSessionSettings.typed), or nil
|
|
862
|
+
# after saying what's wrong.
|
|
863
|
+
def check_settings(pairs)
|
|
864
|
+
require_relative "bundle_session_settings"
|
|
865
|
+
pairs.each_with_object({}) do |pair, out|
|
|
866
|
+
key, sep, value = pair.partition("=")
|
|
867
|
+
unless !sep.empty? && key.match?(BundleSessionSettings::KEY)
|
|
868
|
+
@stderr.puts "chi bundle check: --set takes KEY=VALUE, not #{pair}"
|
|
869
|
+
return nil
|
|
870
|
+
end
|
|
871
|
+
out[key] = BundleSessionSettings.typed(value)
|
|
872
|
+
end
|
|
873
|
+
end
|
|
874
|
+
|
|
752
875
|
# +rest+ through +flags+, stopping at the first --help or error.
|
|
753
876
|
#
|
|
754
877
|
# @return [CLI::Flags::Result, Integer] the result, or the exit status
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: core
|
|
3
|
-
version: 0.3.
|
|
3
|
+
version: 0.3.3
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Recommended safety set"
|
|
6
6
|
trust_level: reviewed
|
|
7
7
|
# Its members' highest floor (cost-cap's)
|
|
8
|
-
requires_chi: ">= 0.
|
|
8
|
+
requires_chi: ">= 0.86.0"
|
|
9
9
|
includes:
|
|
10
10
|
- loop-guard
|
|
11
11
|
- check-in
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cost-cap
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.2.0
|
|
4
4
|
scope: system
|
|
5
5
|
description: "A per-session $ cap: at the cap the turn pauses on Continue (another cap's worth) / Stop, a warning at 80% goes to the user only; /cap shows or sets it"
|
|
6
6
|
trust_level: reviewed
|
|
7
7
|
plugin:
|
|
8
8
|
file: plugin.rb
|
|
9
|
-
sha256: sha256:
|
|
10
|
-
# ctx.cost is new in chi 0.83.0
|
|
11
|
-
requires_chi: ">= 0.
|
|
9
|
+
sha256: sha256:21308746c9cfa3e135909d74e1668ccfc659ea120b3fb41b1ee6a3ce386d8b45
|
|
10
|
+
# ctx.cost is new in chi 0.83.0; ask_user's effects: in the next chi
|
|
11
|
+
requires_chi: ">= 0.86.0"
|
|
12
12
|
# chi --set cost-cap.cap=3: a session's own cap (ctx.settings["cap"])
|
|
13
13
|
session_settings: [cap]
|
|
@@ -28,6 +28,9 @@ require "fileutils"
|
|
|
28
28
|
class Plugin
|
|
29
29
|
USAGE = "usage: /cap [N | $N | off] (USD; /cap alone shows the spend)"
|
|
30
30
|
OPTIONS = %w[Continue Stop].freeze
|
|
31
|
+
# For a parent agent's answer (guardrails.parent_answers): Continue
|
|
32
|
+
# spends more, Stop denies; under hard a parent may only Stop.
|
|
33
|
+
EFFECTS = { "Continue" => :spend, "Stop" => :deny }.freeze
|
|
31
34
|
WARN_AT = 0.8
|
|
32
35
|
|
|
33
36
|
def initialize(_settings)
|
|
@@ -103,7 +106,7 @@ class Plugin
|
|
|
103
106
|
raised = raised_cap(cap, resolved[:step] || cap, cost.total)
|
|
104
107
|
answer = ctx.ask_user(question: "Cost cap reached: #{money(cost.total)} spent of #{money(cap)}. " \
|
|
105
108
|
"Continue raises it to #{money(raised)}.",
|
|
106
|
-
header: "Cost cap", options: OPTIONS)
|
|
109
|
+
header: "Cost cap", options: OPTIONS, effects: EFFECTS)
|
|
107
110
|
if continue?(answer)
|
|
108
111
|
save(ctx, resolved[:session], "cap" => raised, "step" => resolved[:step] || cap)
|
|
109
112
|
ctx.notify("cap raised to #{money(raised)}")
|
|
@@ -174,8 +174,10 @@ rules:
|
|
|
174
174
|
reason: shares a note with every session it may concern; chi broadcast is for the user, not an agent
|
|
175
175
|
|
|
176
176
|
# ── answering chi's own questions around chi answer ───────────────────
|
|
177
|
-
# A parent agent answers a child's
|
|
178
|
-
# to
|
|
177
|
+
# A parent agent answers a child's question with chi answer, which holds it
|
|
178
|
+
# to the option effects a parent may pick. These shapes go around that,
|
|
179
|
+
# answering as the user, so only the user may allow them (core's
|
|
180
|
+
# PROTECTED_RULES, never a parent agent): chi --attach
|
|
179
181
|
# or chi -p with stdin from a pipe, a here-string or a file other than
|
|
180
182
|
# /dev/null (a chi after 0.12 treats those as a parent too, but an older
|
|
181
183
|
# child doesn't), and a POST to the Bridge's or the web's answer route.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: guardrails
|
|
3
|
-
version: 0.9.
|
|
3
|
+
version: 0.9.2
|
|
4
4
|
scope: system
|
|
5
5
|
description: Optional default guardrail rules (git push/rewrites, rm -rf wide, curl | sh, git hooks, chi's own config and attached context from the shell, chi context add --cmd, chi broadcast, answering chi around chi answer, removing a memory, writing a memory that reaches the system prompt (identity, model notes) or a model overlay; in strict mode also git rebase and writes and git in another checkout outside the session's repo), asks only small models get (git checkout --/restore, git stash drop), and a note for the model
|
|
6
6
|
trust_level: reviewed
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ruby
|
|
3
|
+
version: 0.1.0
|
|
4
|
+
scope: system
|
|
5
|
+
description: "An RSpec run with no examples at all (`0 examples, 0 failures`) steers a hint into the turn: check the path, the filter (-e/--tag) or the .rspec --pattern"
|
|
6
|
+
trust_level: reviewed
|
|
7
|
+
plugin:
|
|
8
|
+
file: plugin.rb
|
|
9
|
+
sha256: sha256:f088ae539b1a0c46b35823fb69be42524ce8eac34e185b20ffef7e05d62dbd5f
|
|
10
|
+
# after_tool_call's result: (0.57.0)
|
|
11
|
+
requires_chi: ">= 0.57.0"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The ruby bundle (docs/plugins.md, The ruby bundle): an `execute` that ran
|
|
4
|
+
# RSpec and got no examples at all steers a hint into the running turn.
|
|
5
|
+
#
|
|
6
|
+
# RSpec prints `0 examples, 0 failures` for a run whose path matched no
|
|
7
|
+
# file, whose filter (-e, --tag) matched no example, or whose .rspec
|
|
8
|
+
# --pattern hid the specs (a nested project's .rspec overrides one). The
|
|
9
|
+
# summary reads green, so the model takes the suite as passing and moves on
|
|
10
|
+
# with code no test ever loaded. after_tool_call checks the run: the command
|
|
11
|
+
# runs rspec, and every summary line in its output counted 0 examples.
|
|
12
|
+
#
|
|
13
|
+
# The command: one of its simple commands (split at &&, ||, ;, | and a
|
|
14
|
+
# newline, so after a leading `cd x &&` too) starts with `rspec`,
|
|
15
|
+
# `bin/rspec` (any path ending in /rspec) or `parallel_rspec`, once leading
|
|
16
|
+
# X=… assignments, `bundle exec` and `time` are left out. A program that
|
|
17
|
+
# only names rspec (`grep rspec README`, `echo "bundle exec rspec"`) is not
|
|
18
|
+
# a run. Only `execute` results count.
|
|
19
|
+
#
|
|
20
|
+
# The output: every `<N> examples, <M> failures` line (RSpec's summary,
|
|
21
|
+
# `0 examples, 0 failures` or `, 3 pending` after it) counts, and the hint
|
|
22
|
+
# comes when all of them are 0: parallel_rspec prints one per process, so a
|
|
23
|
+
# single process with examples leaves its 0 out of the total. An output with
|
|
24
|
+
# no summary line (a spec that fails to load, a missing path, a killed run)
|
|
25
|
+
# says what is wrong already: the hint doesn't repeat it.
|
|
26
|
+
#
|
|
27
|
+
# The hint is a steer: its own user message in the conversation at the
|
|
28
|
+
# loop's next boundary, shown as `ruby> nudged: …`, dropped (logged) if the
|
|
29
|
+
# model answers first. Nothing is remembered, so the next 0-example run
|
|
30
|
+
# hints again: a different command, and it may be the same mistake.
|
|
31
|
+
class Plugin
|
|
32
|
+
HINT = "rspec ran 0 examples: check the path, the filter (-e/--tag) or the .rspec --pattern"
|
|
33
|
+
PROGRAMS = %w[rspec parallel_rspec].freeze
|
|
34
|
+
SUMMARY = /\b(\d+) examples?, (\d+) failures?\b/
|
|
35
|
+
# Where a simple command ends: the next one runs its own program.
|
|
36
|
+
SEGMENTS = /&&|\|\||[;|\n]/
|
|
37
|
+
ASSIGNMENT = /\A[A-Za-z_][A-Za-z0-9_]*=/
|
|
38
|
+
|
|
39
|
+
def register(chi)
|
|
40
|
+
chi.on(:after_tool_call) { |event, ctx| after(event, ctx) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def after(event, ctx)
|
|
44
|
+
return unless event[:tool].to_s == "execute"
|
|
45
|
+
return unless self.class.zero_examples?(event[:result])
|
|
46
|
+
|
|
47
|
+
call = event[:call]
|
|
48
|
+
return unless call.is_a?(Hash) && self.class.rspec?(call[:content])
|
|
49
|
+
|
|
50
|
+
ctx.steer(HINT)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Whether one of +command+'s simple commands runs rspec.
|
|
54
|
+
def self.rspec?(command)
|
|
55
|
+
command.to_s.split(SEGMENTS).any? { |segment| PROGRAMS.include?(File.basename(program(segment))) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# The program a simple command starts: leading X=… assignments, `bundle
|
|
59
|
+
# exec` and `time` left out ("" for none).
|
|
60
|
+
def self.program(segment)
|
|
61
|
+
words = segment.split(/\s+/).reject(&:empty?)
|
|
62
|
+
words.shift while words.first&.match?(ASSIGNMENT)
|
|
63
|
+
words.shift(2) if words[0] == "bundle" && words[1] == "exec"
|
|
64
|
+
words.shift if words.first == "time"
|
|
65
|
+
words.first.to_s
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Whether the output has summary lines and every one of them counts 0
|
|
69
|
+
# examples.
|
|
70
|
+
def self.zero_examples?(output)
|
|
71
|
+
counts = output.to_s.scan(SUMMARY).map { |examples, _failures| examples.to_i }
|
|
72
|
+
!counts.empty? && counts.all?(&:zero?)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -60,7 +60,7 @@ Only the nested form is read. An env name used as a top-level key (`SAMAGOTCHI_D
|
|
|
60
60
|
- `hosts:` map of `name → {host, port | url, transport, api, api_key_env, auth, dialect, profile, first_token_timeout, vision, sampling, thinking, enabled, models}` (`ConfigFile.hosts_config`, `host_registry.rb` `HostEntry`). Names lowercased; `url:` (http/https, optional path) replaces host/port, never both; `api_key_env:` names the env var holding the API key (never write a key into config.yml); `auth:` (`bearer|x-api-key`) is the header the key goes in (default `x-api-key` on `api: anthropic`, Bearer elsewhere); `dialect:` (`fireworks`, or `standard` for the default; `api: anthropic` only) adjusts the Messages wire for that provider (Bearer by default, no thinking `display`, no `cache_control`, `x-session-affinity`; `LLM::Dialect`, docs/configuration.md "Anthropic Messages hosts"); `transport` overrides `server.transport`; `first_token_timeout` (seconds, `0` = off; a negative or non-number warns and is ignored) overrides `server.first_token_timeout` for that host; `models:` (a map of model ids, or a plain list) declares ids the host serves whatever its `/v1/models` lists: no "doesn't list" warning, and a bare id routes there as if listed (`HostModel`); an entry may hold `price: {input, output, cache_read?, cache_write?}` in USD per 1M tokens (`ModelPrice`; an unset cache rate is the input rate), which estimates a cost the provider doesn't report or reports as 0 (`cost_estimate`, shown as `~$`), and `served: [ids] | any`, the models a gateway may answer that id with (no served-model warning for them); model settings stay in the root `models:`; a worker reads `hosts:` from the file as it starts (no environment variable sets hosts).
|
|
61
61
|
- `models:` map of model id or alias → `{profile, vision, sampling, thinking, reasoning_history}` (`ConfigFile.model_settings`). `thinking:` (here, on a host, or `thinking.level` for every model) is `off|low|medium|high|default` (`Thinking`; unquoted `off` works, `on` is not a level; `default` = send nothing). Precedence: the session's own level (`/thinking LEVEL`, saved in the session; `chi --thinking` on a session run, `chi send --new --thinking`) > `chi web --thinking`/`SAMAGOTCHI_THINKING_LEVEL` > `models:` > `hosts.<name>.thinking` > `thinking.level` in config.yml > `default`. A `sampling:` key wins over the fields a level sends (`null` drops one); `docs/configuration.md` "Thinking". `reasoning_history:` (here or on a host) is `drop|turn|all`, which of its reasoning a chat host's model gets back. Precedence: the session's own level (`/reasoning-history LEVEL`, saved in the session) > `SAMAGOTCHI_REASONING_HISTORY` > `models:` > `hosts.<name>.reasoning_history` > `turn`; `docs/configuration.md` "Reasoning history". `/thinking` and `/reasoning-history` ask before a change that restarts the prompt cache; `--yes` applies it (`chi send` commands apply directly). `sampling:` (here or on a host) is a map of request fields passed to the provider as written (`temperature`, `top_p`, `presence_penalty`, `repeat_penalty`, …; a model's fields win over its host's per field; `null` = don't send; chi's own fields like `max_tokens`/`stream` are refused with a warning; `docs/configuration.md` "Sampling"). Keys match case-insensitively. `profile` (here or on a host) is `qwen36|gemma4`: the raw prompt format for native hosts. Precedence: `--profile`/`SAMAGOTCHI_MODEL_PROFILE` > `models:` > `hosts.<name>.profile` > the llama.cpp server's chat template > the name (`qwen`/`gemma`) > `qwen36` (`ModelProfile.resolve`). Set one when a model's name hides its family (e.g. a Qwen fine-tune under another name on mlx, which has no template to read).
|
|
62
62
|
- `hooks:` map of `hooks_dir` + per-event lists `{path, on_error}` (`Hooks::Loader.load`). `hooks_dir` may start with `~`.
|
|
63
|
-
- `guardrails:` tool-call rules (`docs/guardrails.md`; read in `Engine#guardrail_rules`, parsed by `Guardrails::Rules.parse`): `enabled` (bool, default true; `false` drops rules and hooks' asks, a deny still applies; config.yml only, like every `guardrails` setting: no env variable), `
|
|
63
|
+
- `guardrails:` tool-call rules (`docs/guardrails.md`; read in `Engine#guardrail_rules`, parsed by `Guardrails::Rules.parse`): `enabled` (bool, default true; `false` drops rules and hooks' asks, a deny still applies; config.yml only, like every `guardrails` setting: no env variable), `parent_answers` (`soft`, the default: a parent agent's `chi answer` may deny, answer, Continue and give "Allow once"; `standard`: no allow; `hard`: deny, Stop and plain answers only; never a wider scope; replaces `parent_approvals` and `turn.parent_continue`; a convention for an honest parent, not a security boundary, and the user's choice: set it only when the user asks), `rules:` (list), `disable:` (a rule id or a list of them, `id` or `bundle:id`, switching off a bundle's or config rule without editing it), `small_models` (`auto`, the default: small = 32B or less read from the name; a glob or list of globs; `[]` for none). A rule takes only `id`, `tool`, `command`, `path`, `git`, `models`, `verdict`, `reason`, `scopes`: `id` required; at least one of `tool` (a name, `shell` = execute + task_create, a `File.fnmatch` glob like `"mcp_*"`, or a list), `command` (a Ruby regex on the shell command), `path` (a glob, or `outside_repo`: outside the session's repo, tmp folders and memory files excepted), `git` (only `outside_repo`: a shell call runs commit/add/reset/… in another checkout); `models` (optional: `small`, a model-name glob, or a list; without it the rule is for every model); `verdict` `ask|deny`; `scopes` (for `ask`) a subset of `once, session, repo, rule`. **Any parse error (an unknown key, a bad regex, no verdict) makes chi deny every tool call** until fixed, so validate with `YAML.safe_load` and keep the list shape. Rules are read again on the next tool call after config.yml or a bundle's rules change: no restart needed. Installed bundles' rule files (`chi bundle install guardrails`) add to them; `/guardrails` lists what loaded.
|
|
64
64
|
|
|
65
65
|
```yaml
|
|
66
66
|
guardrails:
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: samagotchi-system
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.86.0
|
|
4
4
|
scope: system
|
|
5
5
|
description: Default system memories — identity, self map, config modification protocol, memory guide and the delegated-session rules
|
|
6
6
|
files:
|
|
7
7
|
identity.md: sha256:29c2f7df9f6c5acbc5ec17000a4490741e55e426af15ca0d81c8706edd9bd027
|
|
8
|
-
self_map.md: sha256:
|
|
9
|
-
config_modification_protocol.md: sha256:
|
|
8
|
+
self_map.md: sha256:be1b601c550e22730416913eeb0bd47a097e37f3c6743bda709ee495467547a5
|
|
9
|
+
config_modification_protocol.md: sha256:f2551c71a21cc42e8b888e4b9b41f4b0909224e5393731905c1f786c704bad14
|
|
10
10
|
memory_guide.md: sha256:c48eea744db69e4f3e9bb415e9a03a8c78bbcaa1205ad3b1fb157a079f92d22e
|
|
11
11
|
delegated.md: sha256:a0d6ddcd4a8be06526d964898611775fd251cfd9d145a9d16afaccaa503597bd
|
|
@@ -58,7 +58,8 @@
|
|
|
58
58
|
keeps older versions of `skill_*` memories). `chi bundle list`
|
|
59
59
|
shows installed + available; `chi bundle install <name>`. Settings: config.yml `bundles: <name>:`
|
|
60
60
|
(`config_modification_protocol`), read at session start: after an install or a settings change,
|
|
61
|
-
tell the user to restart the session. API and bundle docs: `docs/plugins.md`.
|
|
61
|
+
tell the user to restart the session. API and bundle docs: `docs/plugins.md`. Writing one: read
|
|
62
|
+
`docs/bundle-authoring.md` in my source dir first.
|
|
62
63
|
|
|
63
64
|
## Where things live (relative to the source dir)
|
|
64
65
|
- `docs/`: `cli.md` (flags, subcommands, REPL commands), `configuration.md` (config.yml keys,
|
data/lib/samagotchi/client_id.rb
CHANGED
|
@@ -13,7 +13,7 @@ module Samagotchi
|
|
|
13
13
|
TUI_PREFIX = "tui:"
|
|
14
14
|
# chi send's turns and commands.
|
|
15
15
|
CLI_SEND = "cli:send"
|
|
16
|
-
# chi answer's answers (a parent agent's, Guardrails::
|
|
16
|
+
# chi answer's answers (a parent agent's, Guardrails::ParentPolicy).
|
|
17
17
|
CLI_ANSWER = "cli:answer"
|
|
18
18
|
# chi restart.
|
|
19
19
|
CLI_RESTART = "cli:restart"
|