samagotchi 0.141.0 → 0.143.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 (165) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +111 -7
  3. data/README.md +2 -1
  4. data/bin/chi +2 -0
  5. data/docs/architecture.md +25 -289
  6. data/docs/bundle-authoring.md +7 -6
  7. data/docs/bundles.md +1232 -0
  8. data/docs/cli.md +51 -13
  9. data/docs/configuration.md +68 -30
  10. data/docs/context.md +12 -12
  11. data/docs/guardrails.md +27 -20
  12. data/docs/hooks.md +28 -9
  13. data/docs/internals/attached-context.md +225 -0
  14. data/docs/internals/background-tasks.md +1 -1
  15. data/docs/internals/bridge.md +380 -0
  16. data/docs/internals/bundles.md +6 -3
  17. data/docs/internals/context-telemetry.md +1 -1
  18. data/docs/internals/engine.md +31 -19
  19. data/docs/internals/participants.md +13 -12
  20. data/docs/internals/prompt-caching.md +12 -12
  21. data/docs/internals/remote-sessions.md +1 -1
  22. data/docs/internals/{tool-guardrails.md → tool-output-limits.md} +21 -2
  23. data/docs/internals/web-push.md +1 -1
  24. data/docs/memory.md +33 -3
  25. data/docs/plugins.md +127 -1231
  26. data/docs/sessions.md +51 -27
  27. data/docs/sub-agent.md +6 -6
  28. data/docs/system-one.md +122 -0
  29. data/docs/testing.md +3 -1
  30. data/lib/samagotchi/bridge.rb +2 -0
  31. data/lib/samagotchi/bundle_command/api_calls.rb +132 -0
  32. data/lib/samagotchi/bundle_command/check.rb +50 -34
  33. data/lib/samagotchi/bundle_command/installed_check.rb +86 -0
  34. data/lib/samagotchi/bundle_command/scaffold.rb +1 -0
  35. data/lib/samagotchi/bundle_settings_check.rb +58 -0
  36. data/lib/samagotchi/bundles/agents-md/manifest.yml +4 -2
  37. data/lib/samagotchi/bundles/agents-md/plugin.rb +1 -1
  38. data/lib/samagotchi/bundles/btw/manifest.yml +4 -2
  39. data/lib/samagotchi/bundles/btw/plugin.rb +1 -1
  40. data/lib/samagotchi/bundles/check-in/manifest.yml +7 -3
  41. data/lib/samagotchi/bundles/check-in/plugin.rb +101 -11
  42. data/lib/samagotchi/bundles/claude-peer/manifest.yml +3 -3
  43. data/lib/samagotchi/bundles/claude-peer/plugin.rb +1 -1
  44. data/lib/samagotchi/bundles/claude-peer/scripts/pair +1 -1
  45. data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -2
  46. data/lib/samagotchi/bundles/codex-peer/plugin.rb +1 -1
  47. data/lib/samagotchi/bundles/coordinator/manifest.yml +2 -2
  48. data/lib/samagotchi/bundles/coordinator/plugin.rb +1 -1
  49. data/lib/samagotchi/bundles/core/manifest.yml +2 -2
  50. data/lib/samagotchi/bundles/cost-cap/manifest.yml +6 -3
  51. data/lib/samagotchi/bundles/cost-cap/plugin.rb +5 -7
  52. data/lib/samagotchi/bundles/github-pr/manifest.yml +3 -1
  53. data/lib/samagotchi/bundles/known-names/manifest.yml +3 -1
  54. data/lib/samagotchi/bundles/loop-guard/manifest.yml +4 -2
  55. data/lib/samagotchi/bundles/loop-guard/plugin.rb +1 -1
  56. data/lib/samagotchi/bundles/mcp/manifest.yml +4 -2
  57. data/lib/samagotchi/bundles/mcp/plugin.rb +2 -2
  58. data/lib/samagotchi/bundles/ruby/manifest.yml +2 -2
  59. data/lib/samagotchi/bundles/ruby/plugin.rb +1 -1
  60. data/lib/samagotchi/bundles/skills/manifest.yml +4 -2
  61. data/lib/samagotchi/bundles/skills/plugin.rb +1 -1
  62. data/lib/samagotchi/bundles/slow-commands/manifest.yml +4 -2
  63. data/lib/samagotchi/bundles/slow-commands/plugin.rb +1 -1
  64. data/lib/samagotchi/bundles/source-links/manifest.yml +3 -1
  65. data/lib/samagotchi/bundles/system/config_modification_protocol.md +3 -3
  66. data/lib/samagotchi/bundles/system/manifest.yml +4 -4
  67. data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
  68. data/lib/samagotchi/bundles/system/self_map.md +3 -3
  69. data/lib/samagotchi/bundles/work-cap/manifest.yml +6 -3
  70. data/lib/samagotchi/bundles/work-cap/plugin.rb +11 -13
  71. data/lib/samagotchi/config.rb +74 -14
  72. data/lib/samagotchi/context_candidate_choice.rb +20 -3
  73. data/lib/samagotchi/context_candidates.rb +4 -1
  74. data/lib/samagotchi/context_candidates_log.rb +36 -1
  75. data/lib/samagotchi/context_judge.rb +6 -2
  76. data/lib/samagotchi/context_status.rb +1 -2
  77. data/lib/samagotchi/engine.rb +96 -70
  78. data/lib/samagotchi/extension_load.rb +11 -4
  79. data/lib/samagotchi/guardrail_wiring.rb +36 -31
  80. data/lib/samagotchi/guardrails/chi_dirs.rb +7 -4
  81. data/lib/samagotchi/guardrails/path_resolve.rb +1 -1
  82. data/lib/samagotchi/guardrails/protected_paths.rb +7 -4
  83. data/lib/samagotchi/guardrails_command.rb +3 -3
  84. data/lib/samagotchi/hook_routes.rb +37 -2
  85. data/lib/samagotchi/hooks/bundle_loader.rb +20 -4
  86. data/lib/samagotchi/hooks/registry.rb +23 -5
  87. data/lib/samagotchi/host_model.rb +3 -3
  88. data/lib/samagotchi/host_registry.rb +14 -7
  89. data/lib/samagotchi/judgments_command.rb +155 -0
  90. data/lib/samagotchi/kernel_loop.rb +20 -3
  91. data/lib/samagotchi/llm/chat_loop.rb +79 -10
  92. data/lib/samagotchi/llm/errors.rb +7 -5
  93. data/lib/samagotchi/llm/stray_tool_call.rb +68 -0
  94. data/lib/samagotchi/log_line.rb +1 -1
  95. data/lib/samagotchi/memory_bundle/builder.rb +4 -2
  96. data/lib/samagotchi/memory_bundle/entry_size.rb +100 -0
  97. data/lib/samagotchi/memory_bundle/installed_bundle.rb +9 -2
  98. data/lib/samagotchi/memory_bundle/installed_copy.rb +92 -0
  99. data/lib/samagotchi/memory_bundle/installer.rb +2 -0
  100. data/lib/samagotchi/memory_bundle/manifest.rb +23 -13
  101. data/lib/samagotchi/memory_bundle/provenance.rb +4 -1
  102. data/lib/samagotchi/memory_bundle/shipped_update.rb +13 -1
  103. data/lib/samagotchi/model_catalog.rb +3 -2
  104. data/lib/samagotchi/models_command.rb +1 -1
  105. data/lib/samagotchi/plugin/api.rb +41 -7
  106. data/lib/samagotchi/plugin/context.rb +59 -1
  107. data/lib/samagotchi/plugin/judgments.rb +72 -0
  108. data/lib/samagotchi/plugin/loader.rb +6 -3
  109. data/lib/samagotchi/plugin/peer.rb +17 -22
  110. data/lib/samagotchi/plugin_session_state.rb +32 -5
  111. data/lib/samagotchi/prompt_warmup.rb +1 -9
  112. data/lib/samagotchi/run_options.rb +1 -0
  113. data/lib/samagotchi/self_report.rb +36 -0
  114. data/lib/samagotchi/session_access/contract.rb +1 -1
  115. data/lib/samagotchi/session_access/local.rb +1 -1
  116. data/lib/samagotchi/session_access/remote.rb +3 -3
  117. data/lib/samagotchi/session_archive_command.rb +3 -3
  118. data/lib/samagotchi/session_commands.rb +7 -4
  119. data/lib/samagotchi/session_config.rb +9 -3
  120. data/lib/samagotchi/session_continue.rb +1 -1
  121. data/lib/samagotchi/session_manager.rb +25 -5
  122. data/lib/samagotchi/session_retention.rb +10 -0
  123. data/lib/samagotchi/side_judgments.rb +12 -1
  124. data/lib/samagotchi/system_one/answer.rb +63 -11
  125. data/lib/samagotchi/system_one/approval_label.rb +56 -0
  126. data/lib/samagotchi/system_one/approval_question.rb +2 -0
  127. data/lib/samagotchi/system_one/client.rb +43 -17
  128. data/lib/samagotchi/system_one/fake_backend.rb +1 -1
  129. data/lib/samagotchi/system_one/http_backend.rb +12 -4
  130. data/lib/samagotchi/system_one/judgment_settings.rb +61 -0
  131. data/lib/samagotchi/system_one/judgments.rb +243 -0
  132. data/lib/samagotchi/system_one/log.rb +25 -2
  133. data/lib/samagotchi/system_one/recording.rb +117 -0
  134. data/lib/samagotchi/system_one/runs.rb +179 -0
  135. data/lib/samagotchi/system_one/runs_store.rb +230 -0
  136. data/lib/samagotchi/system_one.rb +9 -0
  137. data/lib/samagotchi/terminal_ui/attach_link.rb +2 -2
  138. data/lib/samagotchi/terminal_ui/attached_loop/exit_commands.rb +1 -1
  139. data/lib/samagotchi/terminal_ui/attached_loop.rb +1 -1
  140. data/lib/samagotchi/terminal_ui/local_link.rb +1 -1
  141. data/lib/samagotchi/terminal_ui/remote_link.rb +1 -2
  142. data/lib/samagotchi/testing/bundle.rb +9 -2
  143. data/lib/samagotchi/testing/host.rb +2 -1
  144. data/lib/samagotchi/testing/judgments.rb +176 -0
  145. data/lib/samagotchi/testing/recorder.rb +5 -0
  146. data/lib/samagotchi/testing/recording_api.rb +1 -1
  147. data/lib/samagotchi/tool_output_cut.rb +26 -6
  148. data/lib/samagotchi/tool_response.rb +1 -1
  149. data/lib/samagotchi/tool_runner.rb +63 -4
  150. data/lib/samagotchi/toolkit_suggest.rb +11 -2
  151. data/lib/samagotchi/tools/memory.rb +28 -0
  152. data/lib/samagotchi/turn_note.rb +9 -0
  153. data/lib/samagotchi/update_command.rb +49 -16
  154. data/lib/samagotchi/version.rb +1 -1
  155. data/lib/samagotchi/web/app.rb +1 -0
  156. data/lib/samagotchi/web/auto_update.rb +79 -49
  157. data/lib/samagotchi/web/auto_update_record.rb +59 -0
  158. data/lib/samagotchi/web/public/app.js +8 -4
  159. data/lib/samagotchi/web/public/hash_intent.js +24 -0
  160. data/lib/samagotchi/web/public/index.html +3 -1
  161. data/lib/samagotchi/web/public/revise_stack.js +14 -5
  162. data/lib/samagotchi/web/session_control.rb +3 -3
  163. data/lib/samagotchi/web/update_check.rb +7 -6
  164. data/lib/samagotchi/web/update_ops.rb +80 -25
  165. metadata +23 -2
data/docs/plugins.md CHANGED
@@ -5,8 +5,8 @@ tools, hooks and background setup to every session. Its hooks are ordinary
5
5
  bundle hooks ([hooks.md](hooks.md)). Its commands and tools work like chi's
6
6
  own.
7
7
 
8
- This is the first version of the plugin API. More of it comes later: see
9
- [Not yet](#not-yet).
8
+ This page is the plugin API and its reference. What each shipped bundle does for a user (btw, mcp, cost-cap,
9
+ claude-peer…) is in [bundles.md](bundles.md).
10
10
 
11
11
  Writing a bundle? Start with [bundle-authoring.md](bundle-authoring.md): the `chi bundle new` / `test` / `check`
12
12
  loop, the event shapes, the kit, the pitfalls.
@@ -33,6 +33,9 @@ requires_chi: ">= 0.1.28" # optional: a gem-style requirement (">= 0.1.28, <
33
33
  # to the chi being released (docs/releasing.md)
34
34
  session_settings: [cap] # optional: the settings a session may set (chi --set my-bundle.cap=3);
35
35
  # {cap: number} types them: number (or off), boolean, string or any
36
+ settings: [greeting] # optional: the keys config.yml bundles: my-bundle: may have, written the same way
37
+ # (any also takes a mapping or a list). With it, another key warns; a session
38
+ # setting needn't be listed twice
36
39
  needs: [gh] # optional: outside commands it relies on (see memory.md#bundles-that-need-outside-commands)
37
40
  ```
38
41
 
@@ -79,6 +82,10 @@ Settings work as they do for hooks ([hooks.md](hooks.md#settings)). An
79
82
  sees a session's own `--set` values. A value a session may set is read from `ctx.settings` in the handler or
80
83
  command that uses it (see [Per-session settings](#per-session-settings)).
81
84
 
85
+ A config.yml `bundles:` name that no installed bundle has shows a warning when a session starts, in every UI (`config>`…);
86
+ so does a key or a value's type the manifest's `settings:` rules out. It warns only: the session goes on, and a
87
+ bundle with no `settings:` has its keys left alone. `chi bundle check --set` warns the same way (`settings`).
88
+
82
89
  ## The API: `register(chi)`
83
90
 
84
91
  ### `chi.command(name, description, anytime: false) { |args, ctx| … }`
@@ -465,7 +472,8 @@ attaches refs to it ([Refs on the answer](hooks.md#refs-on-the-answer-display-on
465
472
  `:question_asked`, `event[:refs]` attaches refs to the reply that asks a question
466
473
  ([Refs on a question](hooks.md#refs-on-a-question-display-only)). Its label is
467
474
  `plugin.rb (bundle my-bundle)`. The block may take only the event. If it
468
- raises, the error is logged and the hook is skipped.
475
+ raises, the error is logged and the hook is skipped; the first failure per
476
+ event in a worker also shows once in every UI (`plugins> plugin.rb (bundle x) before_turn hook failed (…)`).
469
477
 
470
478
  To act from the block, use `ctx`: `ctx.notify`, `ctx.ask_user`,
471
479
  `ctx.steer`, `ctx.stop_turn`, `ctx.stop_generation`. Inside the block they
@@ -509,7 +517,7 @@ end
509
517
  ```
510
518
 
511
519
  loop-guard's thinking watch is built on this ([The loop-guard
512
- bundle](#the-loop-guard-bundle)).
520
+ bundle](bundles.md#the-loop-guard-bundle)).
513
521
 
514
522
  ### `chi.service(name, eager: false) { |svc| … }`
515
523
 
@@ -547,7 +555,7 @@ end
547
555
  - `kill -9` runs nothing: a child process is orphaned then. Most stdio
548
556
  servers leave when their stdin closes, which it does as chi's process
549
557
  ends. The mcp bundle also stops a server that doesn't (see
550
- [The mcp bundle](#the-mcp-bundle), Stop).
558
+ [The mcp bundle](bundles.md#the-mcp-bundle), Stop).
551
559
 
552
560
  ### `chi.init(label, provides_tools: false, quiet: false, timeout: nil, failed: nil) { |ctx| … }`
553
561
 
@@ -787,6 +795,11 @@ file can't make itself a peer's, and uninstalling the bundle ends it.
787
795
  Subclasses of `Samagotchi::Plugin::Peer` (below) declare their `OUTBOX`
788
796
  source. Needs chi 0.67.0 (guard it with `chi.respond_to?(:peer_source)`).
789
797
 
798
+ ### `chi.judgment(name, questions:, positive: nil, negative: nil)`
799
+
800
+ A cheap System 1 question this bundle asks with `ctx.judge` or `ctx.judge_later`: see
801
+ [Judgments](#judgments-chijudgment-and-ctxjudge).
802
+
790
803
  ### `chi.ctx`
791
804
 
792
805
  The plugin's context (the `ctx` its handlers get), for `register` itself:
@@ -831,7 +844,8 @@ full label, `plugin.rb (bundle my-bundle)`.
831
844
  | `ctx.usage` | the session's work so far, read now, as a frozen value: `turns` (finished ones and the running one), `tool_calls` and `tool_calls_by_tool` (every call, running ones included; canonical names: an alias counts under its tool, a `call_tool` call under the inner tool), `steps` (the loop rounds that dispatched tools, `/stats`' iterations) and `active_ms` (`active_minutes` too): work time, the generations' time plus the finished tool calls' time, never the waits between them (a question, an approval, the user's answer to `ask_user_question`), with `tool_ms_by_tool` its tool part by tool, so a waiting tool's time (`task_wait`) can be left out. Its counts are the ones `/stats` and `chi sessions stats` show, earlier workers' included. Use its methods, not its class. Needs chi 0.100.0 (guard it with `ctx.respond_to?(:usage)`) |
832
845
  | `ctx.turn_author` | who wrote the running turn when it isn't the session's owner: a participant's `"p:<id>"` (`chi share`); nil for the owner's turns and between turns. Every hook event of such a turn carries the same as `author:`. The handler runs with the owner's power whatever the author ([hooks](hooks.md#participants-turns)). Needs chi 0.105.0 (guard it with `ctx.respond_to?(:turn_author)`) |
833
846
  | `ctx.settings` | the bundle's settings, frozen: config.yml's `bundles: <bundle>:` with the session's own `--set` values laid over it ([Per-session settings](#per-session-settings)); the config alone until the session is bound (in `register` and `chi.ctx`) |
834
- | `ctx.data_dir` | `$XDG_STATE_HOME/samagotchi/plugins/<bundle>/`, created on first use. A session's own state goes in `sessions/<id>.json` or `sessions/<id>/` there: chi removes it when the session is deleted, discarded or pruned |
847
+ | `ctx.data_dir` | `$XDG_STATE_HOME/samagotchi/plugins/<bundle>/`, created on first use: what the bundle keeps across sessions |
848
+ | `ctx.session_dir` | this session's own folder there, created on use, read now; nil with no session yet. chi removes it when the session is deleted, discarded or pruned, so don't build the path yourself. A `sessions/<id>.json` an older version of your bundle kept moves in as `state.json` (chi still removes one left there). Needs chi newer than 0.142.0 |
835
849
  | `ctx.log` | `ctx.log.info(:event, key: value)`: debug-log records tagged `plugins`, with `bundle=<bundle>` |
836
850
  | `ctx.messages` | the conversation, as a frozen copy, without the system prompt. While a turn runs, a session worker's (attached, web) adds that turn so far: its prompt, the model's text and the lines merged into it (no tool calls or thinking); the REPL's is the conversation before that turn. An entry may carry `at`, when it happened (docs/sessions.md, Message times) |
837
851
  | `ctx.messages_partial?` | whether `ctx.messages` leaves out a running turn (the REPL mid-turn), so a plugin can say what its answer is about |
@@ -844,6 +858,7 @@ full label, `plugin.rb (bundle my-bundle)`.
844
858
  | `ctx.stop_turn(reason)` | stop the running turn after a warn notice with the reason; true when it stopped one now. In a `:before_tool_call` block it also denies that call |
845
859
  | `ctx.stop_generation(reason)` | cut the generation that is streaming: the turn goes on and the model is asked again ([hooks.md](hooks.md#watching-the-stream)). Shows nothing: post your own notice. True when it cut one now |
846
860
  | `ctx.ask_model(messages:, prompt:, …)` | a side answer from the session's model: see [Side answers](#side-answers-ctxask_model) |
861
+ | `ctx.judge(name, state:)`, `ctx.judge_later(name, state:) { |result| … }`, `ctx.judgments` | a cheap System 1 question this bundle declared, saved as a run: see [Judgments](#judgments-chijudgment-and-ctxjudge) |
847
862
  | `ctx.http` | an HTTP client for calling services (`get`, `post_json`, `request`): see [Calling services](#calling-services-ctxhttp) |
848
863
  | `ctx.sessions` | fork, send to, read, list and stop other sessions: see [Other sessions](#other-sessions-ctxsessions) |
849
864
  | `ctx.toolkit` | the name of the session's toolkit (`full` for a session from before toolkits); nil with no session |
@@ -983,6 +998,99 @@ Each call counts in the session's side requests, under the bundle's name: `/stat
983
998
  web's ctx tooltip (`2 (btw 1, recap 1) · …`), with its tokens and its cost (reported, else estimated from the target
984
999
  model's price), never in the session's own totals.
985
1000
 
1001
+ ## Judgments: `chi.judgment` and `ctx.judge`
1002
+
1003
+ A judgment is a small question for [System 1](system-one.md), the cheap judgment model: a yes/no with a probability, a
1004
+ choice, or a score, in about the time a tool call takes. Where `ctx.ask_model` gets free text from the session's model,
1005
+ a judgment gets a number from the model the user set for this, and every ask is saved, so a later comparison or
1006
+ fine-tune has what was sent and what came of it. It is chi's own layer (approvals use the same `Client` and runs) and
1007
+ gives a bundle no new power. Needs chi 0.142.0 (guard it with `chi.respond_to?(:judgment)`).
1008
+
1009
+ ```ruby
1010
+ def register(chi)
1011
+ chi.judgment "progress", questions: { "progress" => { type: "noul", instructions: "Is this turn still making progress?" } },
1012
+ positive: "keep_going", negative: "stop"
1013
+
1014
+ chi.on(:after_tool_call) do |_event, ctx|
1015
+ @run = ctx.judge_later("progress", state: { prompt: @prompt, calls: @calls }) do |result|
1016
+ next if result.no_judgment? || result.audit?
1017
+
1018
+ ctx.notify("this turn looks stuck") if result.p < 0.2
1019
+ end
1020
+ end
1021
+
1022
+ chi.command("/going", "Say the turn is fine") do |_args, ctx|
1023
+ ctx.judgments.label(@run, "keep_going", by: "command")
1024
+ nil
1025
+ end
1026
+ end
1027
+ ```
1028
+
1029
+ **It is off until the user says so.** The full name is `<bundle>.<name>` (`check-in.progress`), and it asks nothing until
1030
+ config.yml's [`system_one.judgments`](configuration.md#bundles-judgments) gives it a mode, `audit` or `on`, which it can
1031
+ also give a host and a model of its own. Neither `audit` nor `on` changes anything by itself: `result.audit?` is the bundle's
1032
+ to honour (in audit, look and don't act). A full name is dot-separated words of a-z, 0-9, `_` and `-`, so a bundle whose
1033
+ name has a capital or a dot fails to load on this call. `approvals`, `context`, `guardrails` and `toolkit` as a first word and
1034
+ `.replay` at the end are core's.
1035
+
1036
+ `chi.judgment`:
1037
+
1038
+ - `questions:` is a map of id to `{type:, instructions:, criteria:}`: `"noul"`, `"choice"` (criteria: `{name => description or
1039
+ nil}`) or `"score"` (criteria: a list of levels). Or a callable given `ctx`, called at each ask, where they vary per
1040
+ session. A plain map is checked when declared, a bad one failing the plugin's load; a callable's result at each ask, raising there.
1041
+ - `positive:` and `negative:` name the label values that say the answer was right and wrong (a word or a list). They
1042
+ are saved in each run for a later comparison, and don't limit `label`.
1043
+
1044
+ `ctx.judge(name, state:)` asks and waits, at most `system_one.timeout_ms`. `name` is as declared, without the bundle.
1045
+ `state:` is a String, Hash or Array: what the question is about, sent to the System 1 host and saved as sent (bytes that
1046
+ aren't UTF-8 replaced). It stops when the turn is stopped, as `ctx.http` does. **It returns a Result, and doesn't raise for
1047
+ anything that goes wrong on the way:**
1048
+
1049
+ | On the Result | |
1050
+ |---|---|
1051
+ | `no_judgment?` | no answers |
1052
+ | `answers` | `{id => answer}`: `.noul`, `.choice`, `.score` (a score is the probability-weighted level index, not a level number), `.level` (the name of the most likely level, when the response has a legend), `.confidence`, `.probabilities` |
1053
+ | `p(id)` | a `noul` question's probability of yes (nil for no answer, or another type); no id: the only question's |
1054
+ | `audit?`, `mode` | the judgment's mode: `"off"`, `"audit"` or `"on"` |
1055
+ | `run_id` | for `ctx.judgments`; nil when no run was saved |
1056
+ | `reason` | why there's no answer, below; nil with answers |
1057
+
1058
+ | `reason` | When | A run? |
1059
+ |---|---|---|
1060
+ | `:off` | no mode set | no |
1061
+ | `:no_host` | no `system_one.host`, nor one of its own, or a config still on the moved `guardrails.system_one` keys | no |
1062
+ | `:no_session` | no session bound yet | no |
1063
+ | `:failed` | the host couldn't be reached, timed out, refused, or sent something unusable | yes, with `failure` |
1064
+ | `:cancelled` | the turn was stopped during the ask | yes, with `failure: "cancelled"` |
1065
+ | `:busy` | `judge_later` only: this judgment's last one still runs | yes, `asked: false` |
1066
+
1067
+ An undeclared name or a bad `state:` (not a String, Hash or Array, or nested too deep) raises `ArgumentError`, as `ask_model` does for a bad model, and so does `ctx.judge` in a
1068
+ `:generation_progress` handler, whose stream it would hold.
1069
+
1070
+ `ctx.judge_later(name, state:) { |result| … }` asks on its own thread and **returns the run's id at once** (nil when no run will be
1071
+ saved). One per judgment runs at a time. The block is optional, and gets the Result once, on another thread, however
1072
+ the ask went, also when nothing was asked. A block that raises is logged.
1073
+
1074
+ - **The block's `ctx.*` act on the session as it is then**, not as the handler's event: the turn may have ended, or the
1075
+ worker moved to another session. Compare `ctx.session_id` with the one you started from before acting.
1076
+ - A `judge_later` still running when chi shuts down gets a second, then it and its run are lost. On `chi -p
1077
+ --non-interactive` this is the usual end of a slow ask.
1078
+ - Snapshot what you need: the block sees only what it closes over.
1079
+
1080
+ `ctx.judgments.label(run_id, value, by: nil, strength: nil, question: nil)` saves what came of a run, as a later line on it;
1081
+ `ctx.judgments.acted(run_id, what)` saves what the bundle did about it. Both return whether they saved anything. `value`
1082
+ and `what` are the bundle's own words (up to 200 characters), mapped only at analysis. `by:` says what said it, always after
1083
+ the bundle's name (`check-in:card`). `strength:` is `"strong"` (the user said so) or `"weak"` (they may have clicked out of habit).
1084
+ A `nil` id saves nothing and isn't an error, so a run that wasn't saved needs no check; an id of anything this bundle didn't declare raises `ArgumentError`.
1085
+
1086
+ **What's saved.** A run holds the state as sent: prompts, commands and whatever else the bundle put there, in the
1087
+ user's state dir, for as long as sessions last ([System 1's saved runs](system-one.md)). Nothing is saved for a scratch
1088
+ session or in a participant's turn, later lines included (those asks still go to the host). `system_one.runs: off` stops all of it.
1089
+
1090
+ **Cost.** A judgment stays out of the session's spend, as all of System 1's do, and **a bundle's judgments on a hosted host aren't under a
1091
+ cost cap**. At about $0.000015 a question that's accepted; ask at a human pace all the same, not per stream batch. The tokens a host reports are
1092
+ saved in the run (`usage`).
1093
+
986
1094
  ## Calling services: `ctx.http`
987
1095
 
988
1096
  ```ruby
@@ -1204,7 +1312,7 @@ gets a note when its text changes.
1204
1312
  `Samagotchi::Plugin::AttachedContext::Error` with the reason (no session
1205
1313
  yet, no provider for the URL, a bad name).
1206
1314
 
1207
- The [github-pr bundle](#the-github-pr-bundle) proposes the branch's PR from
1315
+ The [github-pr bundle](bundles.md#the-github-pr-bundle) proposes the branch's PR from
1208
1316
  a quiet `chi.init` task.
1209
1317
 
1210
1318
  ### Context providers in the manifest
@@ -1244,6 +1352,9 @@ are the rest of the surface they may use. A change to any of them gets a
1244
1352
  CHANGELOG line and bumps the shipped bundles that use it. Anything else under
1245
1353
  `Samagotchi::` is internal.
1246
1354
 
1355
+ **New API ships with its first caller:** a `chi.*` or `ctx.*` method arrives in the same change as a shipped bundle
1356
+ that calls it, or for a real external bundle that does. No method on the chance that someone will want it.
1357
+
1247
1358
  **Stable**
1248
1359
 
1249
1360
  1. `Samagotchi::Plugin::Api` / `chi.*` and `ctx` as documented above (`ctx.cost`'s and `ctx.usage`'s values by their methods), including
@@ -1257,8 +1368,8 @@ CHANGELOG line and bumps the shipped bundles that use it. Anything else under
1257
1368
  `check_hint!`, `paired_lines`, `transport_line`, `peer_dir` and
1258
1369
  `setup_paragraph` (optionally `skill_substitutions`), and sets `NAME`,
1259
1370
  `APP`, `COMMAND`, `USAGE`, `OUTBOX` and `SCRIPTS_DIRS`. These constants are
1260
- read as `self.class::X`. See the [claude-peer](#the-claude-peer-bundle) and
1261
- [codex-peer](#the-codex-peer-bundle) bundles.
1371
+ read as `self.class::X`. See the [claude-peer](bundles.md#the-claude-peer-bundle) and
1372
+ [codex-peer](bundles.md#the-codex-peer-bundle) bundles.
1262
1373
  3. `Samagotchi::Tools::ToolIndex::Tool` and `Samagotchi::Tools::ToolIndex::Group`
1263
1374
  values, `Samagotchi::Tools::ToolIndex.schema_note` and
1264
1375
  `Samagotchi::Tools::ToolIndex.spec_tokens` (used by the mcp bundle).
@@ -1278,12 +1389,10 @@ fails on a reference in neither (docs/testing.md).
1278
1389
  **Used by shipped bundles today, but internal** (may change without notice; a
1279
1390
  bundle that needs one should ask for an API instead):
1280
1391
 
1281
- - `Samagotchi::Tools::MemoryRead.memories_dir`, `Samagotchi::MemoryPaths.scope_dir`,
1282
- `Samagotchi::ConfigFile.global_path` and
1283
- `Samagotchi::Config.validate_yaml_sections`.
1392
+ - `Samagotchi::Tools::MemoryRead.memories_dir` and `Samagotchi::MemoryPaths.scope_dir`.
1284
1393
  - `Samagotchi::Ref` / `Samagotchi::RefAction` constants and
1285
1394
  `Samagotchi::TokenUsage::CHARS_PER_TOKEN`.
1286
- - `Samagotchi::ProcessGroup.stop`, `Samagotchi::Guardrails::ShellLex` and
1395
+ - `Samagotchi::ProcessGroup.stop`, `Samagotchi::ProjectEnv`, `Samagotchi::Guardrails::ShellLex` and
1287
1396
  `Samagotchi::Guardrails::GitInfo`.
1288
1397
  - `Samagotchi::Tools::Execute.const_get` reflection.
1289
1398
 
@@ -1313,6 +1422,11 @@ bundle.notices.map(&:text) # also cards, steers, s
1313
1422
  bundle.questions.last.effects # an ask_user's tags as given: { "Continue" => :spend }
1314
1423
  bundle.answer({ selected: "yes" }) # the next ask_user's answer
1315
1424
  bundle.model_answers << "a summary" # the next ask_model's answer
1425
+ bundle.judgments.answer("progress", noul: 0.9) # ctx.judge's answer from now on; question: for one of several, or a block given the state
1426
+ bundle.judgments.fail("progress") # a failed backend; mode("progress", "off"), no_host("progress") too
1427
+ bundle.judgments.wait # judge_later's threads and blocks are done
1428
+ bundle.judgments.asks.last.state # the asks that reached System 1: name, state, questions, cancel
1429
+ bundle.judgments.runs("progress").last["asked"] # the runs as chi saves them; labels("progress"), acted_lines("progress") too
1316
1430
  bundle.sessions.on(:children) { |all:| [] } # ctx.sessions, scripted and recorded
1317
1431
  bundle.http.stub(:post, "http://127.0.0.1:9000/classify", json: { label: "safe" }) # ctx.http, never the network
1318
1432
  bundle.set(running: false) # the session's values: cwd:, messages:, cancelled: …
@@ -1397,1187 +1511,6 @@ docs/testing.md). **Minitest:** `require "samagotchi/testing/minitest"` and subc
1397
1511
  writes a test to start from. `chi bundle try PATH -p TEXT` runs it against a real model for one turn
1398
1512
  ([cli.md](cli.md#trying-a-bundle)), and `chi bundle install --dev PATH` links it into your own sessions.
1399
1513
 
1400
- ## The btw bundle
1401
-
1402
- `chi bundle install btw` (or the `dev` profile) installs the bundle shipped with chi. It is written
1403
- only against this API (`lib/samagotchi/bundles/btw/plugin.rb`).
1404
-
1405
- - `/btw <question>` asks the session's model a side question about the
1406
- conversation, even while a turn runs. A card `btw: <question>` shows
1407
- "thinking…" at once, and the same card then shows the answer. Nothing else
1408
- sees the answer.
1409
- - The card's **Keep as session** runs `/btw keep <id>`. It forks the
1410
- conversation, the question and the answer into an idle child session, and
1411
- shows a card `kept as <id>`.
1412
- - The last 10 answers can be kept, while the session's worker (or REPL)
1413
- runs. After that, or after a restart, `keep` says expired.
1414
- - In the REPL, a question asked during a turn is about the conversation
1415
- before that turn, and the card says so. In a worker it includes the turn
1416
- so far.
1417
- - Settings: `bundles: btw: {max_tokens: 1024, timeout: 120}`.
1418
- - It ships no memory, so it adds no line to the prompt's memory index. Its
1419
- 0.1.0 shipped `btw.md` as one, and `chi bundle upgrade btw` leaves that file
1420
- (and its index line) behind. Drop it with `chi bundle uninstall btw`, then
1421
- `chi bundle install btw`. After an upgrade already ran, delete
1422
- `$XDG_CONFIG_HOME/samagotchi/memories/btw.md` (`$XDG_CONFIG_HOME` defaults to
1423
- `~/.config`) and its `**btw**` line in `index.md` there by hand.
1424
-
1425
- ## The mcp bundle
1426
-
1427
- `chi bundle install mcp` (or the `dev` profile) installs the bundle shipped with chi. It is written
1428
- only against this API (`lib/samagotchi/bundles/mcp/plugin.rb`), and gives
1429
- the model the tools of [MCP](https://modelcontextprotocol.io) servers. It
1430
- has no memory file, and the servers' tools aren't declared to the model one
1431
- by one: the bundle publishes them into chi's tool index
1432
- ([chi.tool_source](#chitool_sourcecall-acts_as-label-wait-prefix), a group
1433
- per server), and the model has its two tools, `find_tools` and `call_tool` (~250 tokens in
1434
- every request plus a line per server, whatever the servers have; `/mcp`
1435
- says how many). On `api: anthropic` each tool also goes out deferred as
1436
- `mcp_<server>_<tool>`, so a `find_tools` hit loads it and the model calls it
1437
- by that name, as `call_tool` would run it. Stdio servers only, for now.
1438
-
1439
- ```yaml
1440
- # config.yml
1441
- bundles:
1442
- mcp:
1443
- timeout: 60 # seconds per tool call (default 60)
1444
- startup_timeout: 10 # seconds for initialize and tools/list (default 10)
1445
- servers:
1446
- everything:
1447
- command: [npx, -y, "@modelcontextprotocol/server-everything"]
1448
- files:
1449
- command: [npx, -y, "@modelcontextprotocol/server-filesystem", ~/scratch]
1450
- env: {NODE_OPTIONS: "--no-warnings"} # added to chi's environment
1451
- cwd: ~/scratch # default: where chi runs
1452
- tools: [read_*, list_directory] # optional: only these (globs)
1453
- description: files in ~/scratch # optional: its line in find_tools
1454
- timeout: 120 # optional: this server's per-call timeout
1455
- chrome:
1456
- command: [npx, -y, "chrome-devtools-mcp@latest", --slim, --headless]
1457
- attach_image_paths: true # the default; false leaves a path as text
1458
- start: lazy # the default; eager: start it with every session
1459
- ```
1460
-
1461
- - **Start: from a cache, on the first call.** A server's `tools/list` is
1462
- saved in the bundle's data dir (`$XDG_STATE_HOME/samagotchi/plugins/mcp/
1463
- tools-<server>.json`), keyed by a digest of its `command`, `env` (names
1464
- and values: only the digest is stored) and `cwd`. A session with a saved
1465
- list can search its tools at once and **doesn't start the server**: the
1466
- first call of one of its tools does (the call's row shows the wait). So a
1467
- session that never uses MCP spawns nothing, and a new session opens without
1468
- waiting for `npx`. If the live list differs from the saved one, the saved
1469
- one is replaced, and so is what a search finds. The model's tools never
1470
- change, so the prompt cache keeps. The saved file also keeps the first
1471
- sentence of the server's `instructions` (from `initialize`), for its line
1472
- in `find_tools`.
1473
- - **The first run** (no saved list, or the config changed) starts the
1474
- server in an [init task](#chiinitlabel-provides_tools-false-quiet-false-timeout-nil-failed-nil--ctx--):
1475
- every UI shows `Starting MCP server x (first run, saving its tools)`, and
1476
- a turn sent meanwhile waits for its tools. A server that doesn't start,
1477
- answer or list its tools within `startup_timeout` (each step) is a warn
1478
- card, `…: failed`, and its calls fail until chi restarts. The rest of chi
1479
- works as usual.
1480
- - **Freshness.** A saved list older than a day is still used, and a quiet
1481
- background task lists the tools again with a server of its own (then
1482
- stops it), saves them, and replaces the searched ones if they changed.
1483
- One worker does it at a time.
1484
- - **A server that says its tools changed** (`notifications/tools/list_changed`)
1485
- is asked for its `tools/list` again; the saved list and the searched tools
1486
- are replaced.
1487
- - **A cached server that doesn't start** (the command is gone, it crashes)
1488
- fails that call with `Error: MCP server x didn't start: …` and one notice;
1489
- later calls answer the same at once until chi restarts, and a search shows
1490
- its tools as `(failed: …)`. The saved list stays: the next session tries
1491
- again.
1492
- - **`start: eager`** on a server starts it with every session (in an init
1493
- task, after the Bridge is up), even when that session's toolkit does not
1494
- reach its group. A first-run init task still blocks the first turn until
1495
- it finishes, even for an unreached group. A stale-cache refresh likewise
1496
- starts its short-lived server regardless of the toolkit. Switching away
1497
- from a group refuses its calls without starting a lazy cached server, and
1498
- does not stop a server already running; switching back reuses it.
1499
- - **`npx -y …@latest` checks the npm registry on every start** (~3.4 s for
1500
- `chrome-devtools-mcp`, against ~0.9 s for the installed binary). The cache
1501
- hides it from new sessions, but not from the first call. For a faster first
1502
- call, install the server once and run it directly:
1503
-
1504
- ```yaml
1505
- chrome:
1506
- command: [chrome-devtools-mcp, --slim, --headless] # after npm i -g chrome-devtools-mcp
1507
- ```
1508
-
1509
- - **Tools: search, then call.** The servers' tools are kept in chi's tool index;
1510
- the model has two fixed tools:
1511
- - `find_tools(query, group?)` searches it by keywords over the
1512
- server's name, the tool's name and its description, and answers up to 5
1513
- tools, each with its name `<server>/<tool>`, its description and its
1514
- whole `inputSchema`. A word counts more the fewer tools have it (a word
1515
- most of a browser server's tools share, "page", counts little), and
1516
- three times as much in a name as in a description. An empty query (and
1517
- a search with no match) lists every server's tool names, without
1518
- schemas. A tool whose `inputSchema` (or a property's schema) isn't an
1519
- object shows
1520
- `(bad schema)` (and is said once, as a notice), a failed server's tools
1521
- `(failed: …)`, a first-run server still starting `(starting, try
1522
- again)`. Its description names each server on one line, never its
1523
- tools: the server's `description:`, else the first sentence of its
1524
- `instructions`, and how many tools it has (`- chrome (26 tools)` with
1525
- neither: tool names there get called blind, without a search; a first
1526
- run's server is just its name until the next session).
1527
- - `call_tool(tool, args)` calls one: `tool` is `<server>/<tool>`, and
1528
- `mcp_<server>_<tool>` works too (looked up, not parsed). Two tools whose
1529
- `mcp_<server>_<tool>` comes out the same (servers `git-hub` and
1530
- `git_hub`, names alike up to the 48-character cut) are both refused,
1531
- since guardrail rules and approvals couldn't tell them apart: a search
1532
- and `/mcp` mark them `(name clash with …)`, and there is one notice.
1533
- `args` are typed by the tool's
1534
- `inputSchema` (numbers and booleans given as text, JSON text for an
1535
- object). A name it doesn't know answers the closest ones (`Error: no
1536
- tool chrome/open_url. Closest: chrome/new_page, …`). Its row shows
1537
- `mcp chrome/take_screenshot fullPage=true`.
1538
- - A tool turn takes a few more steps than when every tool was declared
1539
- (the P1 spike: about 1.5 more on a 30-tool server), and each request is
1540
- smaller (about half there).
1541
- - **Calls.** A call is `tools/call`. The text blocks of the answer are joined;
1542
- audio or a resource without text is a short placeholder
1543
- (`[audio: audio/wav]`). `isError` makes it `Error: …`; it, a JSON-RPC
1544
- error and `args` that aren't an object end with the tool's schema
1545
- (`chrome/click's inputSchema: {…}`), so a call made without a search can
1546
- be fixed on the next step. A call that takes longer than the timeout is
1547
- an `Error:`, and a cancelled turn stops the wait; both send
1548
- `notifications/cancelled` to the server.
1549
- - **Images.** An `image` block goes to the model as a picture
1550
- ([Returning images](#returning-images): at most 4 per call, a line
1551
- instead when the model can't see images). Its place in the text is a line,
1552
- `[image 1: image/png, attached]`, so the model knows the order. A text
1553
- block that is **the absolute path of an image file** is attached too
1554
- (`[image 1: screenshot.png, attached]` after the text), and so is an
1555
- absolute path ending in `.png`, `.jpg`, `.jpeg`, `.gif` or `.webp` inside
1556
- a sentence (`Saved screenshot to /tmp/shot.png.`; such a path can't hold a
1557
- space). Either way only when the file is an image by its bytes and is
1558
- under the system temp dir or the server's `cwd`: a server's text can't
1559
- pull in any image on disk. `chrome-devtools-mcp --slim` answers
1560
- `screenshot` that way; without `--slim`, `take_screenshot` returns an image
1561
- block. `attach_image_paths: false` on a server leaves such paths as text.
1562
- - **A server that exits** fails the call it was on with `Error: MCP server
1563
- x is not running (…)`, and there is one notice. Its next call starts it
1564
- again (its tools stay), up to 3 times a session; after the third restart
1565
- the notice says so, and its calls fail at once until chi restarts (a new
1566
- session, or the worker's next start).
1567
- - **Stop.** The servers stop with chi ([Shutdown](#shutdown)): stdin is
1568
- closed, then TERM and KILL go to the server's process group. A chi
1569
- killed with `kill -9` (or crashing past Ruby) stops nothing itself; a
1570
- server that ignores its stdin's EOF would live on then. So each server's
1571
- group also holds a watchdog, a `/bin/sh` waiting on a pipe only chi
1572
- holds: when that pipe closes without chi letting it go, it gives the
1573
- server a second, then TERMs and KILLs its group (the server's helpers
1574
- too). A server that leaves its group (`setsid`) is out of its reach.
1575
- - **`/mcp`** (anytime) shows a card with the servers, their state (cached
1576
- (not started), running with its pid, failed with its count, stopped) and
1577
- their tools as `<server>/<tool>`, the name `call_tool` takes.
1578
- A cached or running server's line says roughly how many tokens its tool
1579
- definitions take (`~4,232 tokens`), which only a search's answer carries
1580
- now; the last line says what every request carries (`find_tools` and
1581
- `call_tool`) and totals the servers'. It is an estimate:
1582
- the definitions' JSON as the chat API gets it (`api: openai`), divided by
1583
- 4. The native prompts (Gemma, Qwen) render a flatter schema, so there they
1584
- take less. The log records each server's estimate when it changes
1585
- (`mcp_tools_estimated`).
1586
- - The server's stderr goes to the debug log (`plugins` records, bundle=mcp).
1587
- - **Guardrails.** A rule's `tool:` can be a glob, so one rule covers every
1588
- MCP tool (a search is `find_tools`, outside `mcp_*`, so it isn't
1589
- asked about):
1590
-
1591
- ```yaml
1592
- guardrails:
1593
- rules:
1594
- - id: mcp-ask
1595
- tool: "mcp_*"
1596
- verdict: ask
1597
- reason: an MCP server's tool
1598
- ```
1599
-
1600
- `call_tool`'s `targets:` act as the tool it calls, by its
1601
- `mcp_<server>_<tool>` name ([acts_as:](#guardrails)), so `tool:
1602
- "mcp_github_*"` or `tool: mcp_github_merge_pull_request` match a
1603
- `call_tool` to it, as they matched the tool when it was declared by that
1604
- name. A call made before the server's tools are known (a first run
1605
- still starting) is matched by the name the model gave, so the rule fires
1606
- before the call waits; if the tool it then finds isn't that one, the
1607
- call is refused. The question shows the inner arguments under `<server>: <tool>`
1608
- (`everything: get_sum: a=20 b=22`), and "Allow this call for the
1609
- session" (or in this repo) allows that tool with those arguments only.
1610
- (Approvals stored before 0.6.0, under the old name, don't carry over, nor
1611
- ones stored before 0.7.0 under `mcp_call`: each MCP tool is asked about once more.)
1612
- Two things see `call_tool`, not the MCP tool: hooks (a `before_tool_call`
1613
- event's `tool:` is `"call_tool"`, the tool in its args; a built-in reached
1614
- through `call_tool` is the exception, it fires as that built-in), and loop-guard's
1615
- `ignore_tools` (`call_tool` ignores every MCP call; calls to different
1616
- tools, or with different args, stay distinct, since the tool is in the
1617
- args).
1618
-
1619
- ## The loop-guard bundle
1620
-
1621
- `chi bundle install loop-guard` installs the bundle shipped with chi (`chi
1622
- bootstrap` installs it with the `core` profile). It is
1623
- written only against this API (`lib/samagotchi/bundles/loop-guard/plugin.rb`),
1624
- with `chi.on` hooks, and has no memory file. It guards two kinds of loop:
1625
- repeated tool calls (below) and [thinking that repeats
1626
- itself](#thinking-that-repeats-itself).
1627
-
1628
- A local model can run the same tool call again and again in one turn: each
1629
- step's thinking starts over, so it never notices it already tried. (A real
1630
- one ran `find . -name 'config.yml'` ten times, getting nothing each time.)
1631
- loop-guard breaks that:
1632
-
1633
- - A call is keyed by its tool and its arguments (whitespace collapsed), and
1634
- its result by a hash of the output. When a call has already returned the
1635
- same result `deny_after` times this turn (default 2), the next identical
1636
- call is **denied** with advice, so the 3rd one is caught:
1637
-
1638
- ```
1639
- [execute] Error: denied by guardrail (bundle loop-guard): repeated call. The user was not asked. You already ran this exact call 2 times this turn and it returned the same result each time (exit: 0 (no output)). Don't repeat it. Try a different approach, or tell the user what you're stuck on.
1640
- ```
1641
-
1642
- The user sees one line per call per turn: `loop-guard> loop: execute find
1643
- . -name 'config.yml' 2>/dev/null repeated, denied`.
1644
- - At the `stop_after`-th deny in a turn (default 4) the turn is **stopped**
1645
- (core's own "stopped" notice), and a card lists the repeated calls, so the
1646
- user can say what to try instead.
1647
- - A denied call has no result: a deny (loop-guard's, known-names', a rule's)
1648
- never counts as the call's result, so the deny sticks.
1649
- - The counts are per turn, and a count is the turn's total, not a run of
1650
- consecutive repeats: the loop usually has other calls in between. A new
1651
- turn (a prompt, a continue, a reminder) starts from zero, since a new user
1652
- message can make an old call right again. A steering message merged into
1653
- a running turn doesn't reset them.
1654
- - The polling tools, where repeating is the point, are ignored.
1655
-
1656
- ```yaml
1657
- # config.yml
1658
- bundles:
1659
- loop-guard:
1660
- deny_after: 2 # same call, same result this many times: deny the next one
1661
- stop_after: 4 # stop the turn at this many denies
1662
- ignore_tools: [task_wait, task_get, delegate_result, list_sessions, list_reminders, context_read, forget_outputs]
1663
- mode: deny # deny | notify: notify only warns, once per call per turn
1664
- ```
1665
-
1666
- Its hooks run at the default priority (100), after known-names (50), so in
1667
- known-names' `correct` mode loop-guard keys the corrected call.
1668
-
1669
- ### Thinking that repeats itself
1670
-
1671
- A model can also go in circles inside one generation: its thinking says the
1672
- same few sentences again and again ("Wait, let me check once more…") for
1673
- minutes, until the provider's output cap ends it with no answer. loop-guard
1674
- watches the thinking while it streams ([Watching the
1675
- stream](hooks.md#watching-the-stream)) and cuts it:
1676
-
1677
- - The thinking is cut into sentences. Two sentences of `min_words` or more
1678
- count as the same when their words and word pairs mostly match
1679
- (`similarity`), so a reworded round ("Hmm, …", a synonym, a swapped
1680
- clause) still counts.
1681
- - A loop is a cycle of 1 to `max_period` sentences seen `repeats` times in a
1682
- row, over at least `min_span_sentences` sentences and `min_span_chars`
1683
- chars; or one sentence `max_same` times anywhere in the generation. One
1684
- sentence over and over must be near-identical (`similarity` + 0.4), and a
1685
- longer cycle must hold different sentences: a list of templated sentences
1686
- ("Now I need to open the file <path> and …" for 14 paths) is no loop.
1687
- - Short sentences (under `min_words`) are a loop of their own when
1688
- `short_run` of them come in a row with at most `short_distinct` different
1689
- ones among them ("I'll write it. Go. OK. Writing. Go. OK."). Normal
1690
- thinking has short sentences too ("Hmm.", "Fine."), but spread out or all
1691
- different (code lines); a longer sentence ends the run, and lines inside a
1692
- ```` ``` ```` code block don't count.
1693
- - Any `window_sentences` sentences in a row, short or long, holding at most
1694
- `window_distinct` different ones are a loop too: a cycle of 7 or 8 short
1695
- sentences, or one where a longer sentence ("I'll write the spec file
1696
- now.") keeps ending the short run. Real thinking holds 40 or more
1697
- different sentences in any 48; DeepSeek's write loops hold 4 to 9.
1698
- - Nothing triggers before `min_chars` of thinking. The watch sees a loop
1699
- within one batch (2000 chars, or a second) of its third cycle.
1700
-
1701
- What happens (`action: retry`, the default):
1702
-
1703
- 1. The first loop in a turn: the generation is cut and the model asked
1704
- again, with a hidden note that it was cut off. The user sees the loop's
1705
- first sentence quoted,
1706
- `loop-guard> thinking repeats itself ("Wait, the count of the letter r…", 3 sentences ×3, 4k chars, 8 s): cut`
1707
- (a short run or a window says how many different sentences it held:
1708
- "12 different sentences in 48"), and `↻ cut by loop-guard, asking again (1/1)`.
1709
- 2. If the retry loops too, the turn is stopped ("stopped the turn: …", "■
1710
- turn stopped by loop-guard") with a card that quotes the repeated
1711
- sentences.
1712
- 3. A loop is forgotten after `forget_after` good steps in a row (generations
1713
- with no loop): a model that recovered and made progress gets its next
1714
- loop cut and retried again, not the turn stopped. `forget_after: 0`
1715
- never forgets: the turn's second loop stops it.
1716
-
1717
- The cut uses the `retry.empty_answer` budget: with `retry.empty_answer: 0`
1718
- the first loop ends the turn as stopped by loop-guard, with the notice and
1719
- no card. `action: stop` stops the turn at the first loop; `action: notify` only
1720
- warns, once per generation.
1721
-
1722
- The cut thinking never goes back to the model, but its last 20k chars are kept
1723
- in the session's folder, `thinking_tails.jsonl` (with a generation that ran to
1724
- the provider's output cap, or that a stop_turn ended): an archived session
1725
- keeps them. A turn's record counts its cuts and cap hits (`cuts`, `capped` in
1726
- `analytics.json`; `/stats` shows "thinking cuts" and "output cap hits").
1727
-
1728
- ```yaml
1729
- bundles:
1730
- loop-guard:
1731
- thinking:
1732
- watch: true # false: the tool-call guard only
1733
- action: retry # retry (cut, ask again; then stop) | stop | notify
1734
- min_chars: 2000 # thinking this long before anything triggers
1735
- repeats: 3 # a cycle seen this many times is a loop
1736
- max_period: 6 # cycles of up to this many sentences
1737
- similarity: 0.5 # 0..1, how alike two sentences must be to count as the same
1738
- min_span_sentences: 6 # a loop spans at least this many sentences…
1739
- min_span_chars: 600 # …and this many chars
1740
- max_same: 8 # one sentence this many times in one generation is a loop
1741
- min_words: 5 # shorter sentences only count toward a short run
1742
- short_run: 24 # this many short sentences in a row…
1743
- short_distinct: 6 # …with at most this many different ones is a loop
1744
- window_sentences: 48 # any this many sentences in a row…
1745
- window_distinct: 12 # …with at most this many different ones is a loop
1746
- forget_after: 10 # good steps in a row that forget a loop; 0: never
1747
- ```
1748
-
1749
- Not watched:
1750
-
1751
- - thinking that arrives as answer text: Gemma 4 on the raw-prompt path (no
1752
- close marker), a chat provider that puts `<think>` in the content;
1753
- - a chat host with streaming off (no chunks);
1754
- - loops in the visible answer;
1755
- - a runaway without sentences, such as an endless comma list of numbers: it
1756
- ends at the output cap as an empty answer, which `retry.empty_answer`
1757
- retries after the fact.
1758
-
1759
- A real small model's circular re-checking that repeats one sentence 8 times
1760
- is cut even if it would have found an answer later; `max_same` sets how
1761
- patient that is.
1762
-
1763
- Not caught (yet):
1764
-
1765
- - near-duplicates, such as `find . -name 'config*'` after `'config.yml'`;
1766
- - loops across turns;
1767
- - alternating calls (A, B, A, B) that each return something new.
1768
-
1769
- ## The cost-cap bundle
1770
-
1771
- `chi bundle install cost-cap` installs the bundle shipped with chi (`chi bootstrap` installs it with the `core`
1772
- profile; an existing install gets it with `chi update`). It caps what one session spends, so neither you nor the
1773
- model has to watch `chi sessions stats`: the harness watches the money and the model never sees any of it (no tool,
1774
- no steer, no prompt text). It is inert without a cap, and there is none by default.
1775
-
1776
- ```yaml
1777
- # config.yml
1778
- bundles:
1779
- cost-cap:
1780
- default: 5.00 # USD cap for a session with none set; absent/0/off: no cap
1781
- child: 2.00 # for delegate children; absent: default
1782
- warn_at: 0.8 # tell the user (only) once at this share of the cap; 0/off: no warning
1783
- ```
1784
-
1785
- - **What counts:** `ctx.cost`'s total: what the provider reported (`usage.cost`), else the estimate from the
1786
- model's configured price (`hosts.<name>.models.<id>.price`), plus side requests (the idle recap, `/btw`,
1787
- `ask_model`), including what earlier workers of the session spent.
1788
- - **When it checks:** before each generation, so before the money is spent. A generation that crosses the cap is
1789
- caught at the start of the next one: the overshoot is at most one generation (plus any side requests between).
1790
- A turn whose last generation crossed it ends normally, and the next turn's first generation asks.
1791
- - **At `warn_at` of the cap** you get one notice, `cost-cap> $2.41 of $3.00 spent (80%)`; a raised cap warns again.
1792
- - **At the cap** the turn pauses on a question (header "Cost cap"): `Cost cap reached: $3.04 spent of $3.00.
1793
- Continue raises it to $6.00.` with **Continue** (another cap's worth: $3 → $6 → $9, as many steps as it takes to
1794
- pass the spend, saved for the session) and **Stop** (the turn stops, `canceled` with `stopped_by: cost-cap`, and a
1795
- card shows the spend, reported, estimated and side). With no one to ask (`--non-interactive`) it stops. A parent
1796
- agent answering it (`chi answer`) may Continue under `guardrails.parent_answers: soft` or `standard`, and only Stop
1797
- under `hard` (config.yml's, or the session's own when stricter) (Continue is tagged `:spend`; see [Guardrails](guardrails.md#answers-from-a-parent-agent)). `/cap N` is
1798
- a command, not an answer: a parent that sends it (`chi send ID -m '/cap 10'`) raises the cap under any strategy.
1799
- - **The cap of a session:** the one `/cap` or a Continue set for it, else its own `cap:` (`chi --set
1800
- cost-cap.cap=3`, `chi send --new --set cost-cap.cap=3`; `--set cost-cap.cap=off`, `0` or a negative number for none), else `child:` for a
1801
- delegate child, else `default:`. A cap is a number (`3`, `.5`, `1e1`; in config.yml and `/cap`, `"$2"` too, which `--set` refuses). `--set` refuses
1802
- another value (`cost-cap.cap=five`: exit 2); one in config.yml (or set before this check) that doesn't read
1803
- (`default: five`) gets one notice, `cost-cap: default: "five" isn't a number or off; no cap`, and a `cap:` or
1804
- `child:` that doesn't read gives way to the next key (`…; using default: ($5.00)`), never a silent no cap. Delegates and continued links each have their own cap (a continued link keeps
1805
- the `--set` cap, not a `/cap` or Continue); there is no family cap.
1806
- - **A host it can't watch:** a model that reports no cost and has no configured price (a local server, a free
1807
- model that reports 0) reads unknown: one notice, `the $3.00 cap can't be enforced: <model> reports no cost and has
1808
- no price`, and it never stops for an unknown cost. A session with some priced generations counts the unpriced
1809
- ones as $0.
1810
-
1811
- **`/cap`** (works while a turn runs; the next generation reads it):
1812
-
1813
- - `/cap`: `cost-cap: $1.20 spent of $3.00 (reported $1.10, estimated $0.10, side $0.02)`, or `cost-cap: no cap
1814
- ($1.20 spent)`, with `(can't be enforced: no cost reported or priced)` when it can't be.
1815
- - `/cap 5`, `/cap $5` or `/cap .5`: the cap (and what Continue adds) becomes $5.00 for this session. A cap at or under the
1816
- spend is allowed: the next generation asks.
1817
- - `/cap off` (or `/cap 0`): no cap for this session, over the config's `default:`.
1818
-
1819
- A parent agent starts a session with a cap of its own with `--set` (`chi send --new --set cost-cap.cap=3 -m "task"`;
1820
- `cost-cap.cap` is the bundle's one session setting), or by sending `/cap N` as a first message
1821
- (`chi send --new -m '/cap 3'`).
1822
- At the cap, `chi send --wait` exits 3 with a question of kind `hook` (header "Cost cap"); `chi answer ID --question
1823
- Q --option Continue` (or `--option 1`) goes on, `--option Stop` stops the turn ([sub-agent.md](sub-agent.md)). A
1824
- parent may Continue: nothing restricts it, as with the step limit. A background delegate child at its cap waits on
1825
- its own card and doesn't wake its parent ([sessions.md](sessions.md#delegating)).
1826
-
1827
- The state is one file per session, `<data_dir>/sessions/<session id>.json` (`{"cap": 3.0, "step": 3.0}`), removed
1828
- with the session. A file that is there but doesn't read (not JSON, or not a mapping) is logged
1829
- (`costcap_state_unreadable`) and gets one notice, `cost-cap: <path> doesn't read, its /cap cap is ignored; using
1830
- default: ($5.00)`: the settings' cap is used, never a silent drop; the next `/cap` or Continue rewrites it, logged
1831
- (`costcap_state_replaced`) and said (`cost-cap: <path> didn't read; replaced with the new cap`).
1832
-
1833
- Its sibling for work instead of money, tool calls and active minutes across the session's turns, is
1834
- [the work-cap bundle](#the-work-cap-bundle): the cap a local model (no cost) can have.
1835
-
1836
- ## The work-cap bundle
1837
-
1838
- `chi bundle install work-cap` installs the bundle shipped with chi (`chi bootstrap` installs it with the `core`
1839
- profile; an existing install gets it with `chi update`). It caps the work one session does across its turns: tool
1840
- calls and active minutes. The per-turn meters reset every turn (the step limit, check-in, loop-guard), and a
1841
- Continue on the step-limit question starts the next 300 steps; a parent agent may answer that Continue itself
1842
- (`chi answer`), so a child it keeps continuing has no overall bound but cost-cap's `$`, and on a local model none.
1843
- work-cap is that bound, with cost-cap's shape: the harness watches, the model never sees any of it. It is inert
1844
- without a cap, and there is none by default.
1845
-
1846
- ```yaml
1847
- # config.yml
1848
- bundles:
1849
- work-cap:
1850
- child: {tool_calls: 500, minutes: 90} # delegate children with no own value
1851
- default: {} # every other session (and a delegate without child:); empty: no cap
1852
- warn_at: 0.8 # tell the user (only) once at this share of a cap; 0/off: no warning
1853
- ignore_tools: [task_wait, task_get, delegate, delegate_result] # not counted: waiting
1854
- ```
1855
-
1856
- - **What counts** (`ctx.usage`): **tool calls**, the session's own across its turns and workers, less
1857
- `ignore_tools` (the waiting tools by default, `delegate` among them since it blocks on its child, so a session
1858
- waiting on a long task or a child isn't capped for waiting); and **active minutes**, work time: the generations' time plus the finished tool calls' time (less
1859
- `ignore_tools`' time), never a wait (a question, its own included, an approval, the user's answer to
1860
- `ask_user_question`), so idle time and a Continue answered hours later don't count.
1861
- - **When it checks:** before each generation, like cost-cap: the overshoot is at most one generation's tool calls.
1862
- When the step limit and a work cap fall in the same turn, the work cap asks first, and its Stop ends the turn.
1863
- - **At `warn_at` of a cap** you get one notice per meter, `work-cap> 320 of 400 tool calls (80%)`; a raised cap
1864
- warns again.
1865
- - **At a cap** the turn pauses on a question (header "Work cap") that names the session's totals, so a parent
1866
- answering it decides on the whole picture: `Work cap reached: 412 tool calls of 400 (6 turns, 47 min active,
1867
- $1.20). Continue raises it to 800.` **Continue** raises the cap by another cap's worth (as many steps as it
1868
- takes to pass the work, saved for the session), **Stop** stops the turn (`canceled` with `stopped_by: work-cap`)
1869
- with a card showing the totals. Both meters at once: one question naming both, Continue raises each. With no one
1870
- to ask (`--non-interactive`) it stops. Continue is tagged `:spend`, as cost-cap's: a parent agent may Continue
1871
- under `guardrails.parent_answers: soft` or `standard`, only Stop under `hard` (see
1872
- [Guardrails](guardrails.md#answers-from-a-parent-agent)).
1873
- - **The caps of a session**, each meter on its own: the one `/workcap` or a Continue set, else its own value (`chi
1874
- --set work-cap.tool_calls=400`, `work-cap.minutes=60`, `chi send --new --set …`; `off`, `0` or a negative number
1875
- for none), else `child:`'s for a delegate child, else `default:`'s. Minutes in config.yml read `90`, `90m` or
1876
- `1.5h`. A value that doesn't read (`tool_calls: many`, a `default:` that isn't a mapping) gets one notice,
1877
- `work-cap: tool_calls: "many" isn't a number or off; using default.tool_calls: (400)`, and the next value is used,
1878
- never a silent no cap.
1879
-
1880
- **`/workcap`** (works while a turn runs; the next generation reads it):
1881
-
1882
- - `/workcap`: `work-cap: 320 of 400 tool calls (6 turns, 47 min active, $1.20)`, or `work-cap: no cap (…)` with the
1883
- totals.
1884
- - `/workcap 400`: the tool-call cap (and what Continue adds) becomes 400 for this session; `/workcap 90m` or
1885
- `/workcap 2h`: the active-minutes cap.
1886
- - `/workcap off` (or `/workcap 0`): no work cap for this session, over the config.
1887
-
1888
- The state is one file per session, `<data_dir>/sessions/<session id>.json` (`{"tool_calls": {"cap": 800, "step":
1889
- 400}}`, a meter's `"cap": null` for off). A file that is there but doesn't read (not JSON, or not a mapping) is
1890
- logged (`workcap_state_unreadable`) and gets one notice, `work-cap: <path> doesn't read, its /workcap caps are
1891
- ignored; using default.tool_calls: (400)`: the settings' caps are used, never a silent drop; the next `/workcap` or
1892
- Continue rewrites it, logged (`workcap_state_replaced`) and said (`work-cap: <path> didn't read; replaced with the
1893
- new cap`).
1894
-
1895
- ## The check-in bundle
1896
-
1897
- `chi bundle install check-in` installs the bundle shipped with chi (`chi
1898
- bootstrap` installs it with the `core` profile). It is
1899
- written only against this API (`lib/samagotchi/bundles/check-in/plugin.rb`):
1900
- `chi.on` hooks, `ctx.card`, `ctx.steer`, `ctx.stop_turn` and one anytime
1901
- command. It has no memory file.
1902
-
1903
- A turn can run a long time on its own, reading and searching, without saying
1904
- what it has found. check-in counts the turn's tool calls, and when there are
1905
- `after` of them with no answer yet (then every `every` more) it checks in:
1906
-
1907
- - **`mode: ask`** (the default): a card, one per turn, updated in place at
1908
- each check-in: "50 tool calls, no answer yet", how long the turn has run
1909
- and its last few tools, and three actions:
1910
- - **Nudge** (`/checkin nudge`): puts `message` into the running turn
1911
- (`ctx.steer`); the model reads it at its next step and answers or says
1912
- what is left. Every UI shows `check-in> nudged: …`. One nudge per
1913
- check-in: another click (or a typed `/checkin nudge`) answers
1914
- "check-in: already nudged at N tool calls".
1915
- - **Keep going** (`/checkin later`): closes the card until the next
1916
- check-in.
1917
- - **Stop** (`/checkin stop`): stops the turn.
1918
-
1919
- When the turn ends the card loses its actions ("The turn ended after N tool
1920
- calls"), so no stale buttons stay; a card left by a worker that exited
1921
- mid-turn comes back without them too. In the attached terminal the actions are
1922
- `→ /checkin nudge` lines to type; the command runs beside the turn.
1923
- - **`mode: nudge`**: nudges the model by itself, with a notice line.
1924
- - **`mode: notify`**: a notice line only.
1925
-
1926
- The count is per turn: a new turn (a prompt, a continue, a reminder) starts
1927
- from zero; steering merged into the running turn doesn't reset it (a cap
1928
- across the session's turns is [the work-cap bundle](#the-work-cap-bundle)). The polling
1929
- tools are not counted. A nudge that arrives after the model's final answer is
1930
- dropped (logged), never restarting a turn that is done; the card's "Nudged the
1931
- model at N tool calls" then becomes "The answer came first; nudge not sent."
1932
-
1933
- ```yaml
1934
- # config.yml
1935
- bundles:
1936
- check-in:
1937
- after: 50 # tool calls in one turn before the first check-in
1938
- every: 50 # then again every this many more
1939
- mode: ask # ask | nudge | notify
1940
- message: "You've made {calls} tool calls in this turn without answering. Say briefly what you've found so far and what's left, then answer now or continue."
1941
- ignore_tools: [task_wait, task_get, delegate_result]
1942
- ```
1943
-
1944
- `{calls}` in `message` is the count. The default asks for what has been found
1945
- and what is left, not "how's it going?": a small model tends to answer that
1946
- with a line and carry on unchanged.
1947
-
1948
- `/checkin` (anytime) for this session. What it changes (on/off, the mode,
1949
- the threshold) is saved with the session, in the bundle's data dir
1950
- (`plugins/check-in/sessions/<id>.json`), so it outlasts a worker's restart
1951
- and goes with the session; another session starts from config.yml:
1952
-
1953
- | | |
1954
- |---|---|
1955
- | `/checkin` | on or off, the mode, the threshold and this turn's count |
1956
- | `/checkin on` / `off` | check in, or not |
1957
- | `/checkin 30` | check in after 30 tool calls, then every 30 |
1958
- | `/checkin mode ask` / `nudge` / `notify` | the mode |
1959
- | `/checkin nudge` / `later` / `stop` | the card's actions, also by hand |
1960
-
1961
- ## The slow-commands bundle
1962
-
1963
- `chi bundle install slow-commands` installs the bundle shipped with chi (`chi
1964
- bootstrap` installs it with the `core` profile). It is written only against
1965
- this API (`lib/samagotchi/bundles/slow-commands/plugin.rb`), with `chi.on`
1966
- hooks and a `/slow-commands` command, and has no memory file.
1967
-
1968
- At its limit (120 s, `execute.timeout_sec`) `execute` hands a command still
1969
- running over as a background task (`execute.on_timeout: task`, the default),
1970
- or kills it (`kill`). A full test suite or a build takes longer, so each run
1971
- first blocks the turn for those two minutes (and, in kill mode, loses them,
1972
- then starts again with `task_create` and `task_wait`); the next session does
1973
- the same. slow-commands remembers such a command for the project:
1974
-
1975
- - An `execute` that **timed out** or was **handed over** at the limit, or ran **`warn_ratio` of the limit** or
1976
- longer (default 0.8, so 96 s of 120), is recorded as slow. A later run of
1977
- it under half the limit **with exit 0** drops the entry; a fast failure (a
1978
- load error) doesn't, since it says nothing about how long the suite takes.
1979
- Nor does one through a filter the key leaves out (`rspec 2>&1 | tail -30`
1980
- exits with `tail`'s 0, and a `| head` can end it early).
1981
- A call the user stopped, or that was denied, records nothing.
1982
- - The **next identical `execute`** is **denied** with advice:
1983
-
1984
- ```
1985
- [execute] Error: denied by guardrail (bundle slow-commands): slow command. The user was not asked. This command ran past the execute limit (120 s) here on 2026-10-09. Start it with task_create, then task_wait (timeout 600). A finished task of this command took 4m10s.
1986
- ```
1987
-
1988
- The last sentence is there when a task of the same command (and cwd)
1989
- finished with exit 0 in the project's `tmp/tasks`; a longer one raises the
1990
- `task_wait` timeout it suggests. The user sees one line:
1991
- `slow-commands> execute bundle exec rspec: ran past 120 s before, denied`.
1992
- - **Once per command per turn**: the same `execute` again in that turn runs.
1993
- The model insisting is a signal (maybe the suite got faster), and that run
1994
- records the new truth. A new turn denies again.
1995
- - In kill mode (`execute.on_timeout: kill`), a **`timeout N` past the limit**
1996
- is denied up front, with no history: execute stops the command first, so
1997
- `timeout` never fires. In task mode it runs on as a task until `timeout`
1998
- fires. `timeout`'s exit codes (124, 137) mean nothing special here.
1999
-
2000
- A command is the same when its project and its text match. The project is
2001
- the session's git checkout (else its working directory). The text is the
2002
- command's words and operators (`a && b` is not `a b`), the call's `cwd:`
2003
- folded in as `cd web && …`, and a trailing `2>&1`, `| tail …`, `| head …`
2004
- or `| grep …` dropped (they don't change how long it runs). Arguments stay:
2005
- `bundle exec rspec` (the suite) is slow, `bundle exec rspec
2006
- spec/foo_spec.rb` isn't the same command, and stays the fast path. A
2007
- command that ends inside a quote, `$(` or heredoc is never recorded, nor
2008
- one with a `$(…)`, backticks or a heredoc `cat` or `tee` reads (the key
2009
- can't tell two of those apart). A heredoc script (`python3 - <<'EOF'`)
2010
- keeps its body in the key, so each script is its own command.
2011
-
2012
- Wrappers a model adds by habit don't make a new command: at the start of
2013
- each simple command (so after a leading `cd X &&` too), `time [-p]` and
2014
- `timeout`/`gtimeout` with its options and duration are left out, so
2015
- `timeout 300 bundle exec rspec` and `bundle exec rspec` are one entry. `X=…`
2016
- assignments stay, sorted (`SPEC=… rake spec` is not `rake spec`), except
2017
- `NO_COLOR`, `FORCE_COLOR` and `TERM` (`CI` stays: jest or vitest without it
2018
- can sit in watch mode); a plain `env X=… cmd` is `X=…
2019
- cmd`, and an `env` with an option (`-C`, `-i`, `-u`) stays as typed. The
2020
- list shows each entry as it was last typed.
2021
-
2022
- Since 0.2.0 (chi 0.59.0) a hand-over counts as timed out, and the advice's
2023
- duration also reads a handed-over task, whose `exit_code` file is in the
2024
- state dir (the record's `exit_code_path`).
2025
-
2026
- Entries live in `$XDG_STATE_HOME/samagotchi/plugins/slow-commands/commands.json`,
2027
- shared by every session (another session's record counts at once) and
2028
- forgotten after `ttl_days`.
2029
-
2030
- ```yaml
2031
- # config.yml
2032
- bundles:
2033
- slow-commands:
2034
- mode: deny # deny | notify (warn the user, never deny)
2035
- warn_ratio: 0.8 # a run this share of the execute limit or longer is slow
2036
- ttl_days: 14 # entries older than this are forgotten
2037
- ```
2038
-
2039
- | | |
2040
- |---|---|
2041
- | `/slow-commands` | this project's slow commands, numbered |
2042
- | `/slow-commands forget 2` / `forget all` | drop one (a wrong entry) or all of them |
2043
-
2044
- ## The agents-md bundle
2045
-
2046
- `chi bundle install agents-md` (or the `core` profile) installs the bundle shipped
2047
- with chi (`lib/samagotchi/bundles/agents-md/plugin.rb`): two `chi.on` hooks
2048
- (`:before_turn` rebuilds the steered set, `:after_tool_call` steers), and no memory file.
2049
-
2050
- chi's system prompt holds only the `AGENTS.md` chain from the checkout top down
2051
- to the session's cwd, so an `AGENTS.md` in a directory the session doesn't run
2052
- in (say `pkg/inner/` while the session runs in `pkg/`) is never seen. The
2053
- bundle closes that gap in history, not in the system prompt: the first `read`,
2054
- `write` or `edit` (its `tools` setting) of a file under a directory with an
2055
- `AGENTS.md` steers that file into the running turn, at the loop's next
2056
- boundary, as its own user message — with a first line that says which
2057
- directory it belongs to:
2058
-
2059
- ```
2060
- AGENTS.md for pkg/inner/ (applies to work under pkg/inner/):
2061
- <content>
2062
- ```
2063
-
2064
- - It steers the **outermost candidates first** (each directory from the
2065
- touched file's directory up to, but not including, the checkout top), so a
2066
- deep file can bring in several `AGENTS.md`s at once, outermost to nearest.
2067
- - It **never re-injects**: each directory steers once per session, and a
2068
- `before_turn` pass over the conversation rebuilds that set from the steers
2069
- already there. A steer the model answered past (never injected) is absent,
2070
- so it fires again on the next touch; a resume, a woken worker or a fork
2071
- pick the set up from their messages. It never stops generation or thinking.
2072
- - **Scope:** the touched file's own git root must be the session's checkout:
2073
- a file in another repository, a nested repository or a submodule is
2074
- ignored. Paths compare by `File.realpath`, so a symlinked cwd (a macOS
2075
- `/tmp`) still matches.
2076
- - Directories whose `AGENTS.md` is already in the system prompt (`ctx.prompt_files`:
2077
- the chain and appended files) are skipped, as is a file at the checkout top
2078
- (its `AGENTS.md` is the chain's first).
2079
-
2080
- ```yaml
2081
- # config.yml
2082
- bundles:
2083
- agents-md:
2084
- tools: [read, write, edit] # the calls that count as a touch
2085
- max_chars: 16000 # the content is cut here, with a "[cut at N
2086
- # of M chars: read <absolute path> for the rest]" last line
2087
- ```
2088
-
2089
- `skip_agents_md: true` disables the bundle, as it does the chain's loading
2090
- (see [Configuration](configuration.md#project-specific-description)).
2091
-
2092
- ## The skills bundle
2093
-
2094
- `chi bundle install skills` (or the `dev` profile) installs the bundle shipped with chi
2095
- (`lib/samagotchi/bundles/skills/plugin.rb`): one anytime command, `chi.on`
2096
- hooks, `ctx.sessions.send`, `ctx.notify` and `ctx.steer`. It has no memory
2097
- file; skills themselves work without it ([docs/memory.md](memory.md#skills)).
2098
-
2099
- | | |
2100
- |---|---|
2101
- | `/skill save [name] [--system]` | sends this session a request to save what was just done as `skill_<name>` (chi picks a name when none is given), project scope unless `--system`. The request holds the skill's shape, so the result is the same with a model that never read the memory guide. It runs as a turn; sent while a turn runs, it joins that turn at its next step (the request says to finish the task first). An existing skill is updated. In a `--no-shared` REPL, which takes no messages, the command shows the request to send yourself |
2102
- | `/skill list` | the `skill_*` memories of both scopes, with the date and description from the index |
2103
- | `/skill show <name>` | one skill as saved (project first, as `memory_read` looks) |
2104
- | `/skill diff <name> [N]` | the skill now against its N-th newest older version (default 1: before the last change), unified |
2105
-
2106
- **History.** Before `memory_write`, `write` or `edit` changes a
2107
- `skill_<name>.md` in a memories folder, the file as it was is kept under
2108
- `$XDG_STATE_HOME/samagotchi/plugins/skills/history/<scope>/<name>/` (`system`,
2109
- or `project-<project folder>`), the newest `history_keep`. It is state, not a
2110
- memory: `chi bundle build` and a synced `~/.config` never see it. After the
2111
- call a line says what happened:
2112
-
2113
- ```
2114
- skills> skill release saved (project, 14 lines)
2115
- skills> skill release updated (+2 −1): 1. Run `scripts/verify.sh`; stop if it fails. · /skill diff release
2116
- ```
2117
-
2118
- The line is the file on disk changing, whatever the tool answered; a denied
2119
- write shows nothing.
2120
-
2121
- A skill read in the turn is also watched for changes made another way (an
2122
- `execute` running `sed`, a script): after each other tool call, and at the
2123
- turn's end, its file is compared with the content last seen (at the read, or
2124
- after the last write). A change shows the same line, keeps the content last
2125
- seen as a version (so `/skill diff` has it) and counts as the skill updated.
2126
- A plain read keeps no version.
2127
-
2128
- **The nudge** (`nudge: true`). Some models, finding a skill's step broken,
2129
- skip it and go on without fixing the skill. In a turn that read a skill
2130
- (`memory_read` of a `skill_*` name, or `read` of its file), the first failing
2131
- tool call after it (an `execute` that exited non-zero, except a grep-family
2132
- search that found nothing, a tool error) steers
2133
- the model once (a call a guardrail or the user denied doesn't count as a
2134
- failed step): *"A step of skill release failed. Find out why before skipping
2135
- it; if the skill is out of date, fix it now: edit the step that changed in
2136
- its file (or memory_write the whole skill) and add a Changelog line."* No
2137
- steer once a skill read this turn has changed, by any tool. If the turn ends
2138
- with a failed step and no skill read changed, one line says so: `skill
2139
- release was followed, a step failed, the skill wasn't updated`. A failure
2140
- unrelated to the skill (a test
2141
- meant to fail) can set it off too: once per turn, and only after a skill was
2142
- read.
2143
-
2144
- ```yaml
2145
- # config.yml
2146
- bundles:
2147
- skills:
2148
- history_keep: 20 # older versions kept per skill
2149
- nudge: true # steer once when a followed skill's step fails
2150
- ```
2151
-
2152
- ## The github-pr bundle
2153
-
2154
- ```sh
2155
- chi bundle install github-pr # in the dev profile; needs gh, logged in
2156
- ```
2157
-
2158
- Proposes the branch's open GitHub pull request to a session as a candidate
2159
- `pr-<n>` when its worker starts (not in a scratch session; quietly nothing
2160
- without `gh`, a repository or an open PR): it is attached once you mention it
2161
- (`#N`, the branch or its URL; [Candidates](context.md#candidates)). It resolves
2162
- PR URLs for `chi context add`. Its script prints the PR as text
2163
- and a summary of what changed in counts, authors and states only; it wakes
2164
- the session for a review requesting changes, checks turning red, or the PR
2165
- merged or closed. In the web, a `path:line` in a worker's answer links to
2166
- that line of the session's PR ([Line links](context.md#line-links); `line_links:
2167
- always` for the REPL and `chi -p` too), and its ref menu offers the file
2168
- here and a "PR diff" action (`event[:refs]`). `auto_attach: attach` attaches
2169
- it at once (the old default), `off` does nothing ([Propose, attach or
2170
- off](context.md#propose-attach-or-off)).
2171
- See [Attached context](context.md#github-prs-the-github-pr-bundle).
2172
-
2173
- ## The coordinator bundle
2174
-
2175
- ```sh
2176
- chi bundle install coordinator # in the dev profile
2177
- ```
2178
-
2179
- chi as a coordinator of parallel work, the way you would run several agents
2180
- yourself: split the work, one git worktree and child session per task, keep
2181
- talking while they run, check each report, and merge only with your OK. It
2182
- works in a session with a worker (plain `chi`, the web). A `--no-shared` REPL
2183
- or `-p` gets no delegate reports, so the children's replies wouldn't come back
2184
- by themselves there.
2185
-
2186
- - **The skill.** `skill_coordinator` (system scope; its index line says what
2187
- it is for, from the manifest's `description:`) holds the steps. The model
2188
- creates each worktree with `execute` (`git worktree add ../<repo>-<task> -b
2189
- <branch>`), starts a child there with `delegate wait: false, cwd:`, tells
2190
- you what started and ends its turn. When a child's report comes, it checks
2191
- the branch (`git log`, `git diff --stat`, the tests the child names) before
2192
- telling you. It asks before each merge (`ask_user_question`), checks that its
2193
- own folder is on the default branch and that branch's log first (another
2194
- agent may have merged the branch already), merges `--ff-only` and never
2195
- pushes unless you ask. After a merge it stops the
2196
- child and removes only the worktrees and branches it made.
2197
- - **A handoff memory.** The conversation is lost when the session ends, so
2198
- the skill keeps a project memory `handoff_<epic-slug>` with only what git
2199
- and the session list can't rebuild: the split (branch, worktree, child id
2200
- per task), your decisions, its verdicts and the follow-ups. Its index
2201
- description is the status, descriptive only and naming the session that
2202
- owns it ("OPEN coordinator handoff calc-v2 (session ab12cd34): 2/3 merged",
2203
- later "DONE: …"): every session in the repo sees that line. The model saves
2204
- a decision there before acting on it, and changes the status with
2205
- `memory_write`'s description-only form. When everything is done it marks
2206
- the handoff DONE and asks whether to remove it (`memory_write remove:
2207
- true`, which the guardrails bundle asks you to confirm). Children are told
2208
- not to touch it.
2209
- - **Resume.** A new coordinator session (or one asked "where were we?")
2210
- reads the open handoff, checks it against git and the session list (git
2211
- wins; mismatches are reported) and goes on from the first open step.
2212
- - **`/coordinate <goal>`** sends this session a turn asking the model to
2213
- follow the skill for the goal (in a `--no-shared` REPL it shows the text to
2214
- send yourself). Asking "do this in parallel" works too: the index line leads
2215
- the model to the skill. **`/coordinate resume`** asks it to resume this
2216
- project's open handoff; with several open it shows a card with a Resume
2217
- action each (`/coordinate resume <name>` picks one), with none it says so.
2218
- **`/coordinate end`** ends the day: the model checks each branch, brings
2219
- its handoff up to date (state per branch, decisions, verdicts,
2220
- follow-ups, each open child's id, branch and worktree), keeps it OPEN unless everything is done, and gives a short
2221
- report ending with the `/coordinate resume <slug>` to pick it up next
2222
- time. It starts, merges and stops nothing: running children stay running
2223
- and are named in the report.
2224
- - **Sprint review (experimental, 0.6.0).** **`/coordinate review [name]`**
2225
- starts a review session of this coordinator session (`ctx.sessions.review`:
2226
- its recap, never its transcript; this session keeps running) seeded with the
2227
- open handoff's body (the one named, the only one, or a card with a Review
2228
- action each when several are open; with none, the recap alone), and prints
2229
- the `chi share <id> --name NAME` line. A participant (a manager, a teammate)
2230
- brings new context there; the skill's Sprint review section (copied into
2231
- the seed: a participant's turn reads project memories only, and the skill
2232
- is a system one) has the review's model answer it with a **hand-off diff** (the handoff's lines as
2233
- they would change, in a `diff` block, and what it means per branch), in a
2234
- turn that may only read. Your **Apply** under it (the web's) quotes the
2235
- diff in your turn there: the model brings the handoff up to date and
2236
- re-plans in words, and starts, stops or merges nothing from the review
2237
- session: the coordinator session picks the change up from the handoff,
2238
- merges still asked. Experimental: it may go if it doesn't earn its place.
2239
- - **The coordinator toolkit.** The bundle declares `coordinator`: `lean`
2240
- plus `delegate`, `delegate_result` and `list_sessions`
2241
- ([Toolkits](configuration.md#toolkits)). `/coordinate` puts a fresh session
2242
- on it before it asks the model (`ctx.use_toolkit`). When chi refuses (a reply
2243
- or a turn already there) and the session's toolkit has no `delegate`, the
2244
- reply says to run `/toolkit coordinator`. A continue of a coordinator link
2245
- keeps the toolkit.
2246
- - **The coding toolkit.** The bundle (0.5.0) also declares `coding`: `child`
2247
- with the prompt text `- Be concise in your responses` / `- Show file paths
2248
- clearly when working with files`. The skill starts build and fix children
2249
- on it (`delegate` with `toolkit: "coding"`); reviews, research and plans
2250
- stay on `toolkit.child`, as they need the length.
2251
- - **`/children [all]`** shows the session's children as a card, newest first,
2252
- one line each: `` `ab12cd34` · running · fix/flaky · "fix the flaky spec" ``,
2253
- `` `9a8b7c6d` · done · feat/x · reported · "All 12 specs pass" `` (`not
2254
- reported yet` when the parent wasn't given that reply), `` `77aa66bb` ·
2255
- waiting (approval) · open it: chi --attach 77aa66bb ``. A fork is marked
2256
- `fork`; `all` adds archived children. It runs during a turn too. The card
2257
- has Refresh and a `Stop <id>` per running or waiting child (up to 5);
2258
- `/children stop <id>` stops one of this session's own children (by session
2259
- id, as `chi sessions stop` does) and shows the card again.
2260
- - **Children stay in their worktree.** A child asks before it changes
2261
- anything outside its worktree (your checkout, a sibling's), in every
2262
- guardrails mode and without the guardrails bundle ([a delegate child's
2263
- boundary](guardrails.md#a-delegate-childs-boundary)); you answer on its
2264
- card, or on the parent's while it waits.
2265
- - **Merges.** The parent's merge in its own checkout is a plain `git
2266
- merge`; to be asked before every merge, add a rule ([Rules in
2267
- config.yml](guardrails.md#rules-in-configyml)):
2268
-
2269
- ```yaml
2270
- guardrails:
2271
- rules:
2272
- - id: git-merge
2273
- tool: shell
2274
- command: '\bgit\s+merge\b'
2275
- verdict: ask
2276
- reason: merging a child's branch
2277
- ```
2278
-
2279
- ## The claude-peer bundle
2280
-
2281
- ```sh
2282
- chi bundle install claude-peer # in the dev profile
2283
- ```
2284
-
2285
- A live link between a chi session and a Claude Code session you already work
2286
- with: chi sends it a part of the work (a plan step, a diff, a finding) to
2287
- validate or as context, and Claude's answer comes back into chi. This is
2288
- Claude with the context it has, not a fresh review run. Nothing runs until a
2289
- Claude pairs; without one the bundle costs one tool declaration.
2290
-
2291
- - **Pairing is Claude's.** chi can't see Claude's inbox, so Claude attaches
2292
- two pushed sources to the chi session: `claude-inbox`, whose hint is its
2293
- per-session inbox socket (`CLAUDE_CODE_MESSAGING_SOCKET`, Claude Code
2294
- 2.1.224 or later), and `claude-outbox`, where its answers come. You never
2295
- run that yourself:
2296
- 1. `/claude install-skill` (once) shows a card with the target,
2297
- `$CLAUDE_CONFIG_DIR/skills/chi-peer/SKILL.md` (else `~/.claude/…`),
2298
- whether one is there and, when it differs, a short diff; its Install (or
2299
- Replace) button runs `/claude install-skill --yes`, which copies the
2300
- bundle's `scripts/chi-peer-SKILL.md` there with the installed
2301
- `scripts/pair` path written in. The copy happens inside chi, where no
2302
- guardrail sees it: the button is your consent. The card also offers the
2303
- Claude Code permission rule `Read(~/.local/state/samagotchi/plugins/claude-peer/**)`
2304
- (your state folder), so Claude isn't asked before reading a long part's
2305
- file; chi never writes Claude's settings.
2306
- 2. In Claude Code you say "pair with chi <id or chi web URL>". The skill runs
2307
- `sh <bundle>/scripts/pair <id or URL>`, which attaches `claude-inbox` (the
2308
- socket as its hint, pushed as JSON so a re-pair after a Claude restart
2309
- refreshes it) and `claude-outbox`, and lists the session's context. A
2310
- re-pair keeps Claude's last answer.
2311
- 3. Claude answers a validate or a question with a JSON push to
2312
- `claude-outbox` (`text`, `summary`, `wake: true`), which wakes chi, the
2313
- first answer too (on chi 0.62 the first one arrives as a note without
2314
- waking); the model reads it with `context_read claude-outbox`.
2315
- - **The tool `claude_send(kind, title, text)`**, `kind` being `validate`,
2316
- `context` or `question`, writes one JSON line to that socket. The message
2317
- starts with a header, `chi <id> · <kind> · <title>`. A text up to 4,000
2318
- characters follows inline; a longer one goes to a file,
2319
- `$XDG_STATE_HOME/samagotchi/plugins/claude-peer/sessions/<id>/<HHMMSS>-<slug>.md`
2320
- (removed with the session), and the message names it (`payload: <path>`).
2321
- The tool is declared whether or not a Claude is paired, so pairing doesn't
2322
- change the prompt. Its errors say what to do: no `claude-inbox` (pair from
2323
- Claude), the socket gone or refusing (that Claude session ended or
2324
- restarted: pair again), a text over 200,000 characters.
2325
- - **`/claude`** (an anytime command, so it works mid-turn) sends without
2326
- the model:
2327
- - `/claude` alone shows a card: paired or not, the socket and whether its
2328
- file is there (it goes when that Claude session ends), the last send, and
2329
- Claude's last answer (the `claude-outbox` summary). When Claude's installed
2330
- chi-peer skill differs from what `/claude install-skill` would write (an
2331
- older bundle's, or your edit; a skill installed by a chi with another state
2332
- dir or bundle path differs too), it says so: install it again (Replace),
2333
- then pair again.
2334
- - `/claude validate <text>` and `/claude context <text>` send the text as it
2335
- is, titled by its first line (past a web quote's `From …:` label and a
2336
- quoted line's `> `): paste a selection after them.
2337
- - In the web, selecting text offers **Confirm with Claude**
2338
- ([`chi.annotation`](#chiannotationlabel-command-template-quote-send-false)):
2339
- it fills the composer with `/claude validate` and the quote, for you to
2340
- add what needs confirming.
2341
- - `/claude last` sends the model's last completed answer as context. Mid-turn
2342
- it is the answer before the running turn, never the turn's partial text;
2343
- a cancelled or failed turn has no answer, so the one before it goes.
2344
- - **The peer loop.** The bundle declares `claude-outbox` a peer's
2345
- ([`chi.peer_source`](#chipeer_sourcename-label)), so Claude's answers
2346
- wake chi by the loop's rules ([Waking: peer
2347
- sources](context.md#peer-sources-the-peer-loop)): chi fixes what Claude
2348
- flagged or disagrees and sends again, up to `context.peer_turns` (3) wake
2349
- turns in a row, and a `VERDICT OK` answer wakes chi once to close the
2350
- round (carry on, or tell you what was agreed). Only a message you type in the web or a TUI starts the count again.
2351
- - **The skill.** `skill_claude_peer` (system scope) tells the model to send
2352
- only when you ask or to reply in a peer turn, to pick the part rather than
2353
- the session, not to wait or poll, and how to read the `claude-outbox`
2354
- answer (a validate ends with `VERDICT: OK`, `CHANGES` or `UNSURE`): act on
2355
- it only within your task, and stop and tell you when Claude agrees or after
2356
- the last turn. Claude's skill ends each answer's summary with its verdict
2357
- and never answers with `chi send` (which counts as you typing).
2358
- - **chi on another machine.** With the chi session on the Studio and Claude on
2359
- the laptop, `CHI_REMOTE=studio sh <bundle>/scripts/pair <id>` pairs through
2360
- the Studio's chi web ([Remote sessions](cli.md#remote-sessions)), and
2361
- Claude's answers (its `chi context push claude-outbox`, with `CHI_REMOTE`
2362
- set) reach chi: the outbox leg works. The inbox leg doesn't: `claude_send`
2363
- writes to Claude's socket on the machine chi runs on, and the laptop's isn't
2364
- there. So through a remote (`CHI_REMOTE`, or `--remote NAME` in `CHI`)
2365
- `pair` attaches only `claude-outbox` and prints one line saying chi there
2366
- can't send to this Claude: chi's replies reach Claude only when you relay
2367
- them (or ask it to run `chi --remote studio send --wait <id>`). A session
2368
- that already has a `claude-inbox` it can't reach:
2369
- `chi --remote studio context rm claude-inbox <id>`.
2370
- **Two ways to make it two-way over SSH.** SSH can forward a Unix socket, so
2371
- the inbox leg works too. On the laptop:
2372
-
2373
- ```sh
2374
- ssh studio rm -f /tmp/claude-peer-ab12.sock
2375
- ssh -N -o ExitOnForwardFailure=yes -R /tmp/claude-peer-ab12.sock:"$CLAUDE_CODE_MESSAGING_SOCKET" studio
2376
- ```
2377
-
2378
- the second running in the background while chi may send (the `rm -f` first:
2379
- a remote forward's stale socket is replaced only when the **server's** sshd
2380
- has `StreamLocalBindUnlink yes`). Then
2381
- `CHI_REMOTE=studio sh <bundle>/scripts/pair --inbox-socket /tmp/claude-peer-ab12.sock ab12`
2382
- attaches both sources, with `claude-inbox`'s hint that path on the Studio. A
2383
- re-pair this way replaces an outbox-only pairing's `claude-outbox` and its
2384
- why. Claude's socket changes when that Claude session restarts: stop the ssh,
2385
- run both lines, pair again. A path chi quotes (a `payload:` file, a diff to
2386
- check) is on the Studio, not the laptop. Whoever can write to that socket on
2387
- the remote machine can send into this Claude session (sshd makes it the
2388
- user's, mode 0600 by default): fine on your own machine or pod, not a shared
2389
- host. Claude's own skill has the same under "chi on another machine over SSH".
2390
- - **A continue.** The pairing's two sources are pushed, so they stay with
2391
- the old session when you continue it ([Movable
2392
- context](context.md#peers-and-a-continue)). The new session's worker posts
2393
- Claude one message, from a quiet init task, to the socket the old
2394
- `claude-inbox` last had:
2395
-
2396
- ```
2397
- chi <old> · moved · continued as <new id>
2398
- This chi session was continued as <new id>. Pair with <new id> the way you paired with <old>, then answer there (chi context push claude-outbox <new id>).
2399
- ```
2400
-
2401
- Claude's skill pairs again with the new id the same way it paired before
2402
- (the same `CHI_REMOTE` and `--inbox-socket`). It is posted once (a marker
2403
- under the bundle's `sessions/<id>/`, so a worker restart doesn't post it
2404
- again); a post that fails (that Claude session ended) is logged and
2405
- dropped. An outbox-only pairing has no inbox to post to: Claude's next
2406
- answer to the old id is refused with the new one (`<old> was continued as
2407
- <id>`), and the skill pairs again and pushes the answer there. The new
2408
- session's note says which: `Left with 3f2a9c1e: claude-inbox, claude-outbox
2409
- (Claude's pairing: chi asks Claude to pair again with this session).`, or
2410
- `(Claude pairs again when it next answers)`.
2411
- - **Asking first.** The tool has no `targets:`, so guardrails match it by name
2412
- only: to be asked before each send, add a rule ([Rules in
2413
- config.yml](guardrails.md#rules-in-configyml)):
2414
-
2415
- ```yaml
2416
- guardrails:
2417
- rules:
2418
- - id: claude-send
2419
- tool: claude_send
2420
- verdict: ask
2421
- reason: sends session content to Claude Code
2422
- ```
2423
-
2424
- - **Limits.** The socket's format (`{"type":"user","message":{"role":"user","content":"…"}}`)
2425
- is undocumented: it was found in, and checked on, Claude Code 2.1.296. A
2426
- line Claude Code can't parse is dropped silently, so a Claude Code update
2427
- could break this without an error. No auth line is sent: it is optional on
2428
- macOS and Linux, and native Windows, which needs it, isn't supported.
2429
- Claude's `crossSessionInbound` setting decides what it does with a peer
2430
- message: an interactive auto-mode session delivers it; a session that
2431
- bypasses permissions holds it for approval (in `-p` it expires after 5
2432
- minutes). It is local only (a Unix socket), but it does hand session
2433
- content to another agent: only what the model or you picked.
2434
- - **Needs chi 0.67.0** for the loop (`chi update`): the plugin is built on
2435
- `Samagotchi::Plugin::Peer` (lib/samagotchi/plugin/peer.rb), the base both
2436
- peer bundles share (the tool, `/claude`, the cards, payloads), and each
2437
- bundle sets its strings and transport.
2438
-
2439
- ## The codex-peer bundle
2440
-
2441
- ```sh
2442
- chi bundle install codex-peer # in the dev profile
2443
- ```
2444
-
2445
- The [claude-peer bundle](#the-claude-peer-bundle) for an OpenAI Codex CLI
2446
- session: chi sends the Codex session you work with a part of the work to
2447
- validate or as context, and Codex's answer comes back into chi. It is a copy
2448
- of claude-peer with the transport swapped; what differs is below, the rest
2449
- (kinds, payload files, the last send, `/codex last`, the status card, the
2450
- skill install card) works the same.
2451
-
2452
- - **Sending is `codex queue`.** `codex_send(kind, title, text)` and
2453
- `/codex validate|context <text>` run
2454
- `codex queue --thread <uuid> --message <text>` (an argv, no shell), which
2455
- wakes an idle Codex TUI: the message runs there as a new user turn (checked
2456
- on Codex 0.162.1; a message queued mid-turn runs after the turn). The
2457
- thread is the `codex-inbox` context's hint, Codex's `CODEX_THREAD_ID`, which
2458
- must look like a UUID. `codex` is `$CODEX`, else `codex` on the PATH: a
2459
- worker started by `chi web` or launchd may have another PATH than your
2460
- shell, so set `CODEX=` where chi runs if the error says there's no codex.
2461
- The tool and the anytime `/codex` wait for `codex queue` up to 10 s; a
2462
- queue that takes longer is killed (its process group) and the send fails
2463
- with a timeout error, never a hang. Other errors say what to do: no
2464
- `codex-inbox` (pair from Codex), a thread Codex has no record of (that
2465
- Codex session is gone: pair again), an ephemeral thread
2466
- (`codex exec --ephemeral` can't take queued messages), or `codex queue`'s
2467
- first error line. A Codex session that was closed (`/quit`) is not gone:
2468
- Codex's app-server still takes the queue (exit 0), chi says sent, and no
2469
- answer comes; the message waits for that session (`codex resume` there, or
2470
- pair again from a new one). The status card shows the thread id and says
2471
- so; there is no cheap check that the session is open.
2472
- - **Codex's sandbox.** Pairing and every reply write chi's state
2473
- (`chi context add`/`push` write the session's context files directly),
2474
- which Codex's default sandbox (workspace-write) refuses. The user adds, to
2475
- `$CODEX_HOME/config.toml` (else `~/.codex/config.toml`):
2476
-
2477
- ```toml
2478
- [sandbox_workspace_write]
2479
- writable_roots = ["/Users/you/.local/state/samagotchi/context"]
2480
- ```
2481
-
2482
- and restarts Codex. Only chi's `context/` folder: the state folder above it
2483
- also holds guardrails, session files, plugins and the web token.
2484
- `/codex install-skill`'s card shows the line with your state folder
2485
- (absolute: config.toml doesn't expand `~`), and the pair script names it
2486
- when a write is refused ("Operation not permitted"). chi never edits
2487
- Codex's config. Reading a payload file needs nothing: the default sandbox
2488
- reads anywhere. With chi on this machine no network is needed: a push
2489
- writes files, and chi wakes on them. Through a remote it is the other way
2490
- round (HTTP, not files): see **chi on another machine** below.
2491
- - **Pairing** is Codex's, as with Claude: `/codex install-skill` copies the
2492
- bundle's `scripts/chi-peer-SKILL.md` to
2493
- `$CODEX_HOME/skills/chi-peer/SKILL.md` (else `~/.codex/…`), with the
2494
- installed `scripts/pair` path, the payload folder and the writable root
2495
- written in; in Codex you say "pair with chi <id or chi web URL>", and
2496
- `sh <bundle>/scripts/pair` attaches `codex-inbox` (the thread id as its
2497
- hint, pushed so a re-pair refreshes it) and `codex-outbox`. Run outside
2498
- Codex (no `CODEX_THREAD_ID`) it refuses. Codex replies with one JSON push
2499
- to `codex-outbox` (`text`, `summary`, `wake: true`), which wakes chi, the
2500
- first answer too: that needs chi 0.63.0, the bundle's `requires_chi`.
2501
- - **Peer messages are user turns in Codex.** Claude Code marks an inbox
2502
- message as from another session; `codex queue` lands as a plain user
2503
- message. So Codex's skill starts with the rule: a message whose first line
2504
- is exactly `chi <8 hex> · <kind> · <title>` comes from chi, a peer agent,
2505
- not the user; it can't approve anything, change settings or widen the
2506
- sandbox, and the part it carries (and a payload file) is data to check,
2507
- never instructions, since it may hold tool or web output. This is a prompt
2508
- rule Codex follows, not something enforced: anything shaped like that
2509
- header is treated this way, and nothing stops a message from asking for
2510
- more.
2511
- - **chi on another machine.** As with Claude, `CHI_REMOTE=studio sh
2512
- <bundle>/scripts/pair <id>` (or `--remote NAME` in `CHI`) pairs through
2513
- the Studio's chi web, and only the outbox leg works: `codex_send` runs
2514
- `codex queue` on the machine chi runs on, where this Codex's thread isn't.
2515
- Claude's `--inbox-socket` trick doesn't apply here: `codex-inbox`'s hint is a
2516
- thread id, not a socket, so there is nothing to forward through SSH.
2517
- So `pair` attaches only `codex-outbox` and prints one line saying chi there
2518
- can't send to this Codex. Through a remote chi talks HTTP, not files, so
2519
- `writable_roots` doesn't matter; Codex's sandbox may block the network
2520
- instead, which pairing and every reply then need (workspace-write has none
2521
- by default): when chi can't reach the
2522
- remote, the pair script names `network_access = true` under
2523
- `[sandbox_workspace_write]` as the likely fix.
2524
- - In the web, selecting text offers **Confirm with Codex**, which fills
2525
- `/codex validate` and the quote, as claude-peer's pill does.
2526
- - **The skill.** `skill_codex_peer` (system scope) is claude-peer's with
2527
- Codex's names, and on a missing answer points to Codex being mid-turn or
2528
- its sandbox (the `writable_roots` line). To be asked before each send, a
2529
- guardrails rule with `tool: codex_send` (see the claude-peer section).
2530
- - **The peer loop** works as claude-peer's: `codex-outbox` is declared a
2531
- peer's, Codex's answers wake chi up to `context.peer_turns` turns in a row,
2532
- and `VERDICT OK` wakes chi once to close the round. Needs chi 0.67.0 (the closing
2533
- wake: the chi released with this bundle).
2534
- - **A continue** works as claude-peer's: the new session queues Codex the
2535
- `moved` message on the thread the old `codex-inbox` named, once, and
2536
- Codex's skill pairs again with the new id (the same `CHI_REMOTE`, if any).
2537
- A pairing through a remote has no inbox: Codex pairs again when its next
2538
- push is refused. A closed Codex session still takes the message; it waits
2539
- for `codex resume` there.
2540
-
2541
- ## The ruby bundle
2542
-
2543
- `chi bundle install ruby` (or the `dev` profile) installs the bundle shipped
2544
- with chi (`lib/samagotchi/bundles/ruby/plugin.rb`): one `chi.on` hook
2545
- (`:after_tool_call`), and no memory file.
2546
-
2547
- An RSpec run that finds no examples prints `0 examples, 0 failures`, which
2548
- reads green: the model takes the suite as passing and moves on with code no
2549
- test ever loaded. It usually means a path that matched no file, a filter
2550
- (`-e`, `--tag`) that matched no example, or a `.rspec --pattern` that hid
2551
- the specs (a nested project's `.rspec` overrides one). The bundle notices
2552
- that run and steers a hint into the running turn, which the user sees as
2553
- `ruby> nudged: …`:
2554
-
2555
- ```
2556
- rspec ran 0 examples: check the path, the filter (-e/--tag) or the .rspec --pattern
2557
- ```
2558
-
2559
- It is a nudge for the model, not a denial: the turn goes on, and the model
2560
- can say why the run was empty. It never runs anything.
2561
-
2562
- - **The command.** Only an `execute` counts, and its command must run
2563
- rspec: one of its simple commands (`&&`, `||`, `;`, `|`, a newline, so
2564
- after a leading `cd x &&` too) starts with `rspec`, a path ending in
2565
- `/rspec` (`bin/rspec`, `./bin/rspec`), or `parallel_rspec`, once a leading
2566
- `X=…` assignment, `bundle exec` and `time` are left out. A command that
2567
- only names rspec (`grep rspec README`, `echo "bundle exec rspec"`) is not
2568
- a run.
2569
- - **The output.** Every `<N> examples, <M> failures` line in the execute's
2570
- output counts (RSpec's summary, with or without `, N pending`), and the
2571
- hint comes when all of them are 0. `parallel_rspec` prints one per
2572
- process, so a single process with examples leaves its 0 out of the total.
2573
- - **No summary line, no hint.** A spec that fails to load, a path that
2574
- doesn't exist, a command killed at its limit: the output already says what
2575
- is wrong, and the hint would only repeat it.
2576
-
2577
- Nothing is remembered, so the next 0-example run hints again: it can be
2578
- the same mistake on another command, or a different one. A hint the model
2579
- answers before joining the turn is dropped, as any steer is.
2580
-
2581
1514
  ## Shutdown
2582
1515
 
2583
1516
  When the REPL exits, or a session's worker exits (an idle exit, `/exit`, a
@@ -2629,37 +1562,6 @@ gem.
2629
1562
  - Guardrail rules keyed by a tool's name apply to plugin tools, as they do to
2630
1563
  chi's own tools; path and command rules see what `targets:` says.
2631
1564
 
2632
- ## The model-notes bundle
2633
-
2634
- `chi bundle install model-notes` (or the `dev` profile) installs reviewed
2635
- [model notes](memory.md#model-notes) into the system scope: working habits
2636
- for one model family, loaded into the prompt only for the models a note's
2637
- `models:` line names. It has no plugin or hook. It ships two notes:
2638
-
2639
- | note | models | for |
2640
- |---|---|---|
2641
- | `model_notes_deepseek` | `*deepseek-v4*` | DeepSeek V4 / V4.1: explore briefly then act, commit one item at a time, stop digging, `memory_read` for memories, file tools rather than heredocs that guardrail rules ask about |
2642
- | `model_notes_tone` | `*` | every model: answer first, in plain words; no preambles, restated questions or closing offers; lists only for steps or parallel items |
2643
-
2644
- - **Reviewed before it ships.** A note is generic (no repo or machine facts),
2645
- says nothing the harness already does (a tool's description, an alias),
2646
- talks to the model as "you", stays under 1,500 characters, and the user
2647
- approves its text. A model may propose one; it never edits the bundle.
2648
- `notes/README.md` in the bundle records each note's sources, the
2649
- `script/model_notes_report.rb` numbers and the approval.
2650
- - **Globs per measured version.** A glob is tried against the id with no host
2651
- prefix and against its model key, case-insensitively, and `*` also matches
2652
- `/`: `*deepseek-v4*` covers `deepseek/deepseek-v4.1-flash` on any provider,
2653
- MLX and GGUF names, but not `deepseek-ai/DeepSeek-R1-Distill-Qwen-32B`.
2654
- - **Tested.** `spec/model_ids.yml` lists real id forms and the exact notes
2655
- each loads (negative rows included); the spec installs the bundle and
2656
- resolves each through `sandbox.model_notes_for`
2657
- ([Testing a bundle](#testing-a-bundle-experimental)). It also checks each
2658
- note's shape, size and that the tools it names are ones chi declares.
2659
- - **Your own notes:** a same-name note you have wins and keeps its place
2660
- through upgrades; other notes stack with the bundle's
2661
- ([Model notes](memory.md#model-notes)).
2662
-
2663
1565
  ## The bundle commands
2664
1566
 
2665
1567
  - `chi bundle install <dir>`: copies the plugin to
@@ -2681,9 +1583,3 @@ for one model family, loaded into the prompt only for the models a note's
2681
1583
  - `chi bundle new`, `test` and `check`: start a bundle, run its tests on chi's Ruby, check it as an install takes
2682
1584
  it ([cli.md](cli.md#writing-a-bundle-new-test-check)); `chi bundle try PATH -p TEXT`: run it for one turn in a
2683
1585
  sandbox and trace what it did ([cli.md](cli.md#trying-a-bundle)). The path: [bundle-authoring.md](bundle-authoring.md).
2684
-
2685
- ## Not yet
2686
-
2687
- These are planned:
2688
-
2689
- - `chi.prompt` for sections of the system prompt.