samagotchi 0.84.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.
Files changed (107) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -1
  3. data/docs/architecture.md +44 -6
  4. data/docs/bundle-authoring.md +125 -0
  5. data/docs/cli.md +54 -4
  6. data/docs/configuration.md +8 -5
  7. data/docs/guardrails.md +63 -35
  8. data/docs/hooks.md +20 -4
  9. data/docs/internals/anthropic.md +2 -2
  10. data/docs/internals/bundles.md +11 -2
  11. data/docs/internals/context-telemetry.md +19 -1
  12. data/docs/internals/engine.md +4 -3
  13. data/docs/memory.md +1 -1
  14. data/docs/plugins.md +59 -2
  15. data/docs/releasing.md +1 -1
  16. data/docs/sessions.md +8 -6
  17. data/docs/sub-agent.md +35 -24
  18. data/docs/testing.md +7 -2
  19. data/lib/samagotchi/answer_command.rb +21 -25
  20. data/lib/samagotchi/bridge/client_event.rb +50 -0
  21. data/lib/samagotchi/bridge/sse_writer.rb +6 -2
  22. data/lib/samagotchi/bridge/turn_accumulator.rb +3 -0
  23. data/lib/samagotchi/bridge.rb +76 -12
  24. data/lib/samagotchi/bridge_client.rb +14 -2
  25. data/lib/samagotchi/bundle_command/check.rb +348 -0
  26. data/lib/samagotchi/bundle_command/scaffold.rb +135 -0
  27. data/lib/samagotchi/bundle_command/test_run.rb +89 -0
  28. data/lib/samagotchi/bundle_command.rb +125 -2
  29. data/lib/samagotchi/bundles/core/manifest.yml +2 -2
  30. data/lib/samagotchi/bundles/cost-cap/manifest.yml +4 -4
  31. data/lib/samagotchi/bundles/cost-cap/plugin.rb +4 -1
  32. data/lib/samagotchi/bundles/dev/manifest.yml +2 -1
  33. data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +4 -2
  34. data/lib/samagotchi/bundles/guardrails/manifest.yml +1 -1
  35. data/lib/samagotchi/bundles/ruby/manifest.yml +11 -0
  36. data/lib/samagotchi/bundles/ruby/plugin.rb +74 -0
  37. data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
  38. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  39. data/lib/samagotchi/bundles/system/self_map.md +2 -1
  40. data/lib/samagotchi/client_id.rb +1 -1
  41. data/lib/samagotchi/config.rb +195 -115
  42. data/lib/samagotchi/context_status.rb +1 -1
  43. data/lib/samagotchi/continue_offer.rb +2 -1
  44. data/lib/samagotchi/engine.rb +5 -5
  45. data/lib/samagotchi/guardrail_wiring.rb +4 -0
  46. data/lib/samagotchi/guardrails/approval.rb +12 -2
  47. data/lib/samagotchi/guardrails/load_failures.rb +17 -3
  48. data/lib/samagotchi/guardrails/parent_approvals.rb +14 -70
  49. data/lib/samagotchi/guardrails/parent_policy.rb +258 -0
  50. data/lib/samagotchi/guardrails/verdict.rb +6 -3
  51. data/lib/samagotchi/hook_routes.rb +5 -3
  52. data/lib/samagotchi/hooks/bundle_loader.rb +3 -1
  53. data/lib/samagotchi/hooks/registry.rb +13 -6
  54. data/lib/samagotchi/hooks.rb +5 -0
  55. data/lib/samagotchi/kernel_loop.rb +10 -3
  56. data/lib/samagotchi/llm/anthropic_assembly.rb +1 -1
  57. data/lib/samagotchi/llm/chat_loop.rb +5 -1
  58. data/lib/samagotchi/log_subscriber.rb +1 -0
  59. data/lib/samagotchi/memory_bundle/shas.rb +127 -0
  60. data/lib/samagotchi/memory_bundle.rb +1 -0
  61. data/lib/samagotchi/parent_report.rb +92 -16
  62. data/lib/samagotchi/pending_question.rb +8 -4
  63. data/lib/samagotchi/plugin/api.rb +16 -0
  64. data/lib/samagotchi/plugin/context.rb +22 -12
  65. data/lib/samagotchi/plugin/loader.rb +5 -3
  66. data/lib/samagotchi/question_desk.rb +37 -24
  67. data/lib/samagotchi/queued_command.rb +5 -4
  68. data/lib/samagotchi/relay.rb +5 -1
  69. data/lib/samagotchi/relay_verifier.rb +4 -5
  70. data/lib/samagotchi/reply_wait.rb +22 -6
  71. data/lib/samagotchi/send_command.rb +34 -8
  72. data/lib/samagotchi/session_commands.rb +21 -7
  73. data/lib/samagotchi/session_inbox.rb +68 -0
  74. data/lib/samagotchi/terminal_ui/attach_launcher.rb +1 -1
  75. data/lib/samagotchi/terminal_ui/attached_loop.rb +102 -12
  76. data/lib/samagotchi/terminal_ui/thinking_line.rb +5 -9
  77. data/lib/samagotchi/terminal_ui.rb +15 -9
  78. data/lib/samagotchi/testing/boundary.rb +0 -3
  79. data/lib/samagotchi/testing/bundle.rb +7 -0
  80. data/lib/samagotchi/testing/host.rb +6 -4
  81. data/lib/samagotchi/testing/install.rb +119 -0
  82. data/lib/samagotchi/testing/recorder.rb +13 -4
  83. data/lib/samagotchi/testing.rb +1 -0
  84. data/lib/samagotchi/tools/builtins.rb +2 -1
  85. data/lib/samagotchi/tools/delegate_wait.rb +6 -1
  86. data/lib/samagotchi/tools/task_runtime.rb +5 -3
  87. data/lib/samagotchi/turn_checkpoint.rb +64 -2
  88. data/lib/samagotchi/turn_flow.rb +10 -0
  89. data/lib/samagotchi/version.rb +1 -1
  90. data/lib/samagotchi/web/app.rb +76 -10
  91. data/lib/samagotchi/web/public/app.js +17 -0
  92. data/lib/samagotchi/web/public/bubbles.js +9 -0
  93. data/lib/samagotchi/web/public/chunk_router.js +8 -19
  94. data/lib/samagotchi/web/public/commands.js +1 -0
  95. data/lib/samagotchi/web/public/ctx.js +12 -26
  96. data/lib/samagotchi/web/public/data.js +11 -0
  97. data/lib/samagotchi/web/public/index.html +9 -0
  98. data/lib/samagotchi/web/public/stage_view.js +6 -0
  99. data/lib/samagotchi/web/public/stream_handlers.js +4 -1
  100. data/lib/samagotchi/web/public/turn_events.js +4 -0
  101. data/lib/samagotchi/web/public/turn_html.js +9 -1
  102. data/lib/samagotchi/web/public/unqueue.js +56 -0
  103. data/lib/samagotchi/web/server.rb +4 -0
  104. data/lib/samagotchi/worker.rb +19 -5
  105. data/lib/samagotchi/worker_inbound.rb +4 -4
  106. metadata +12 -2
  107. data/lib/samagotchi/guardrails/parent_continue.rb +0 -43
@@ -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.2
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.83.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.1.1
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:2ea7f76af1ea1c1b0a9a8b0b4b11d6e85a4a816fae224ef5b220cfd20fb48f42
10
- # ctx.cost is new in chi 0.83.0
11
- requires_chi: ">= 0.83.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)}")
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: dev
3
- version: 0.5.1
3
+ version: 0.6.0
4
4
  scope: system
5
5
  description: "Developer tools"
6
6
  trust_level: reviewed
@@ -15,3 +15,4 @@ includes:
15
15
  - coordinator
16
16
  - claude-peer
17
17
  - codex-peer
18
+ - ruby
@@ -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 approval with chi answer, which holds it
178
- # to guardrails.parent_approvals. These shapes go around that: chi --attach
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.1
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), `parent_approvals` (`off`, the default: a parent agent's `chi answer` can only deny an approval; `once`: it may give "Allow once" too; 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.
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.84.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:4ff5708e51f55089de47b107643ae0b74aee04cb96111226823a8d44fb4bc33d
9
- config_modification_protocol.md: sha256:66792f788cdbb8ee0252f2c387c96bd75266817bb75f721aaa181b602db97137
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,
@@ -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::ParentApprovals).
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"