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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +111 -7
- data/README.md +2 -1
- data/bin/chi +2 -0
- data/docs/architecture.md +25 -289
- data/docs/bundle-authoring.md +7 -6
- data/docs/bundles.md +1232 -0
- data/docs/cli.md +51 -13
- data/docs/configuration.md +68 -30
- data/docs/context.md +12 -12
- data/docs/guardrails.md +27 -20
- data/docs/hooks.md +28 -9
- data/docs/internals/attached-context.md +225 -0
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bridge.md +380 -0
- data/docs/internals/bundles.md +6 -3
- data/docs/internals/context-telemetry.md +1 -1
- data/docs/internals/engine.md +31 -19
- data/docs/internals/participants.md +13 -12
- data/docs/internals/prompt-caching.md +12 -12
- data/docs/internals/remote-sessions.md +1 -1
- data/docs/internals/{tool-guardrails.md → tool-output-limits.md} +21 -2
- data/docs/internals/web-push.md +1 -1
- data/docs/memory.md +33 -3
- data/docs/plugins.md +127 -1231
- data/docs/sessions.md +51 -27
- data/docs/sub-agent.md +6 -6
- data/docs/system-one.md +122 -0
- data/docs/testing.md +3 -1
- data/lib/samagotchi/bridge.rb +2 -0
- data/lib/samagotchi/bundle_command/api_calls.rb +132 -0
- data/lib/samagotchi/bundle_command/check.rb +50 -34
- data/lib/samagotchi/bundle_command/installed_check.rb +86 -0
- data/lib/samagotchi/bundle_command/scaffold.rb +1 -0
- data/lib/samagotchi/bundle_settings_check.rb +58 -0
- data/lib/samagotchi/bundles/agents-md/manifest.yml +4 -2
- data/lib/samagotchi/bundles/agents-md/plugin.rb +1 -1
- data/lib/samagotchi/bundles/btw/manifest.yml +4 -2
- data/lib/samagotchi/bundles/btw/plugin.rb +1 -1
- data/lib/samagotchi/bundles/check-in/manifest.yml +7 -3
- data/lib/samagotchi/bundles/check-in/plugin.rb +101 -11
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +3 -3
- data/lib/samagotchi/bundles/claude-peer/plugin.rb +1 -1
- data/lib/samagotchi/bundles/claude-peer/scripts/pair +1 -1
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/codex-peer/plugin.rb +1 -1
- data/lib/samagotchi/bundles/coordinator/manifest.yml +2 -2
- data/lib/samagotchi/bundles/coordinator/plugin.rb +1 -1
- data/lib/samagotchi/bundles/core/manifest.yml +2 -2
- data/lib/samagotchi/bundles/cost-cap/manifest.yml +6 -3
- data/lib/samagotchi/bundles/cost-cap/plugin.rb +5 -7
- data/lib/samagotchi/bundles/github-pr/manifest.yml +3 -1
- data/lib/samagotchi/bundles/known-names/manifest.yml +3 -1
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +4 -2
- data/lib/samagotchi/bundles/loop-guard/plugin.rb +1 -1
- data/lib/samagotchi/bundles/mcp/manifest.yml +4 -2
- data/lib/samagotchi/bundles/mcp/plugin.rb +2 -2
- data/lib/samagotchi/bundles/ruby/manifest.yml +2 -2
- data/lib/samagotchi/bundles/ruby/plugin.rb +1 -1
- data/lib/samagotchi/bundles/skills/manifest.yml +4 -2
- data/lib/samagotchi/bundles/skills/plugin.rb +1 -1
- data/lib/samagotchi/bundles/slow-commands/manifest.yml +4 -2
- data/lib/samagotchi/bundles/slow-commands/plugin.rb +1 -1
- data/lib/samagotchi/bundles/source-links/manifest.yml +3 -1
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +3 -3
- data/lib/samagotchi/bundles/system/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/bundles/system/self_map.md +3 -3
- data/lib/samagotchi/bundles/work-cap/manifest.yml +6 -3
- data/lib/samagotchi/bundles/work-cap/plugin.rb +11 -13
- data/lib/samagotchi/config.rb +74 -14
- data/lib/samagotchi/context_candidate_choice.rb +20 -3
- data/lib/samagotchi/context_candidates.rb +4 -1
- data/lib/samagotchi/context_candidates_log.rb +36 -1
- data/lib/samagotchi/context_judge.rb +6 -2
- data/lib/samagotchi/context_status.rb +1 -2
- data/lib/samagotchi/engine.rb +96 -70
- data/lib/samagotchi/extension_load.rb +11 -4
- data/lib/samagotchi/guardrail_wiring.rb +36 -31
- data/lib/samagotchi/guardrails/chi_dirs.rb +7 -4
- data/lib/samagotchi/guardrails/path_resolve.rb +1 -1
- data/lib/samagotchi/guardrails/protected_paths.rb +7 -4
- data/lib/samagotchi/guardrails_command.rb +3 -3
- data/lib/samagotchi/hook_routes.rb +37 -2
- data/lib/samagotchi/hooks/bundle_loader.rb +20 -4
- data/lib/samagotchi/hooks/registry.rb +23 -5
- data/lib/samagotchi/host_model.rb +3 -3
- data/lib/samagotchi/host_registry.rb +14 -7
- data/lib/samagotchi/judgments_command.rb +155 -0
- data/lib/samagotchi/kernel_loop.rb +20 -3
- data/lib/samagotchi/llm/chat_loop.rb +79 -10
- data/lib/samagotchi/llm/errors.rb +7 -5
- data/lib/samagotchi/llm/stray_tool_call.rb +68 -0
- data/lib/samagotchi/log_line.rb +1 -1
- data/lib/samagotchi/memory_bundle/builder.rb +4 -2
- data/lib/samagotchi/memory_bundle/entry_size.rb +100 -0
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +9 -2
- data/lib/samagotchi/memory_bundle/installed_copy.rb +92 -0
- data/lib/samagotchi/memory_bundle/installer.rb +2 -0
- data/lib/samagotchi/memory_bundle/manifest.rb +23 -13
- data/lib/samagotchi/memory_bundle/provenance.rb +4 -1
- data/lib/samagotchi/memory_bundle/shipped_update.rb +13 -1
- data/lib/samagotchi/model_catalog.rb +3 -2
- data/lib/samagotchi/models_command.rb +1 -1
- data/lib/samagotchi/plugin/api.rb +41 -7
- data/lib/samagotchi/plugin/context.rb +59 -1
- data/lib/samagotchi/plugin/judgments.rb +72 -0
- data/lib/samagotchi/plugin/loader.rb +6 -3
- data/lib/samagotchi/plugin/peer.rb +17 -22
- data/lib/samagotchi/plugin_session_state.rb +32 -5
- data/lib/samagotchi/prompt_warmup.rb +1 -9
- data/lib/samagotchi/run_options.rb +1 -0
- data/lib/samagotchi/self_report.rb +36 -0
- data/lib/samagotchi/session_access/contract.rb +1 -1
- data/lib/samagotchi/session_access/local.rb +1 -1
- data/lib/samagotchi/session_access/remote.rb +3 -3
- data/lib/samagotchi/session_archive_command.rb +3 -3
- data/lib/samagotchi/session_commands.rb +7 -4
- data/lib/samagotchi/session_config.rb +9 -3
- data/lib/samagotchi/session_continue.rb +1 -1
- data/lib/samagotchi/session_manager.rb +25 -5
- data/lib/samagotchi/session_retention.rb +10 -0
- data/lib/samagotchi/side_judgments.rb +12 -1
- data/lib/samagotchi/system_one/answer.rb +63 -11
- data/lib/samagotchi/system_one/approval_label.rb +56 -0
- data/lib/samagotchi/system_one/approval_question.rb +2 -0
- data/lib/samagotchi/system_one/client.rb +43 -17
- data/lib/samagotchi/system_one/fake_backend.rb +1 -1
- data/lib/samagotchi/system_one/http_backend.rb +12 -4
- data/lib/samagotchi/system_one/judgment_settings.rb +61 -0
- data/lib/samagotchi/system_one/judgments.rb +243 -0
- data/lib/samagotchi/system_one/log.rb +25 -2
- data/lib/samagotchi/system_one/recording.rb +117 -0
- data/lib/samagotchi/system_one/runs.rb +179 -0
- data/lib/samagotchi/system_one/runs_store.rb +230 -0
- data/lib/samagotchi/system_one.rb +9 -0
- data/lib/samagotchi/terminal_ui/attach_link.rb +2 -2
- data/lib/samagotchi/terminal_ui/attached_loop/exit_commands.rb +1 -1
- data/lib/samagotchi/terminal_ui/attached_loop.rb +1 -1
- data/lib/samagotchi/terminal_ui/local_link.rb +1 -1
- data/lib/samagotchi/terminal_ui/remote_link.rb +1 -2
- data/lib/samagotchi/testing/bundle.rb +9 -2
- data/lib/samagotchi/testing/host.rb +2 -1
- data/lib/samagotchi/testing/judgments.rb +176 -0
- data/lib/samagotchi/testing/recorder.rb +5 -0
- data/lib/samagotchi/testing/recording_api.rb +1 -1
- data/lib/samagotchi/tool_output_cut.rb +26 -6
- data/lib/samagotchi/tool_response.rb +1 -1
- data/lib/samagotchi/tool_runner.rb +63 -4
- data/lib/samagotchi/toolkit_suggest.rb +11 -2
- data/lib/samagotchi/tools/memory.rb +28 -0
- data/lib/samagotchi/turn_note.rb +9 -0
- data/lib/samagotchi/update_command.rb +49 -16
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +1 -0
- data/lib/samagotchi/web/auto_update.rb +79 -49
- data/lib/samagotchi/web/auto_update_record.rb +59 -0
- data/lib/samagotchi/web/public/app.js +8 -4
- data/lib/samagotchi/web/public/hash_intent.js +24 -0
- data/lib/samagotchi/web/public/index.html +3 -1
- data/lib/samagotchi/web/public/revise_stack.js +14 -5
- data/lib/samagotchi/web/session_control.rb +3 -3
- data/lib/samagotchi/web/update_check.rb +7 -6
- data/lib/samagotchi/web/update_ops.rb +80 -25
- 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
|
|
9
|
-
[
|
|
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
|
|
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
|
|
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.
|