samagotchi 0.142.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 +66 -7
- data/README.md +1 -0
- data/docs/architecture.md +13 -6
- data/docs/bundle-authoring.md +6 -6
- data/docs/bundles.md +1232 -0
- data/docs/cli.md +36 -6
- data/docs/configuration.md +11 -12
- data/docs/context.md +10 -10
- data/docs/guardrails.md +7 -9
- data/docs/hooks.md +19 -2
- data/docs/internals/attached-context.md +2 -3
- data/docs/internals/bundles.md +5 -3
- data/docs/internals/engine.md +9 -3
- data/docs/internals/prompt-caching.md +12 -12
- data/docs/internals/web-push.md +1 -1
- data/docs/memory.md +2 -2
- data/docs/plugins.md +26 -1251
- data/docs/sessions.md +2 -2
- data/docs/sub-agent.md +6 -6
- data/docs/system-one.md +2 -2
- 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 -6
- data/lib/samagotchi/bundles/check-in/plugin.rb +7 -14
- 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 +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/self_map.md +2 -2
- data/lib/samagotchi/bundles/work-cap/manifest.yml +6 -3
- data/lib/samagotchi/bundles/work-cap/plugin.rb +11 -13
- data/lib/samagotchi/context_candidate_choice.rb +12 -3
- data/lib/samagotchi/context_candidates.rb +4 -1
- data/lib/samagotchi/context_candidates_log.rb +11 -0
- data/lib/samagotchi/context_status.rb +1 -2
- data/lib/samagotchi/engine.rb +33 -68
- data/lib/samagotchi/extension_load.rb +8 -2
- 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/kernel_loop.rb +7 -2
- data/lib/samagotchi/llm/chat_loop.rb +5 -0
- data/lib/samagotchi/llm/stray_tool_call.rb +23 -16
- data/lib/samagotchi/memory_bundle/builder.rb +4 -2
- 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 +14 -11
- data/lib/samagotchi/plugin/context.rb +10 -0
- data/lib/samagotchi/plugin/loader.rb +4 -2
- 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/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 +4 -4
- data/lib/samagotchi/session_config.rb +9 -3
- data/lib/samagotchi/session_continue.rb +1 -1
- data/lib/samagotchi/session_manager.rb +15 -6
- data/lib/samagotchi/system_one/judgments.rb +8 -25
- data/lib/samagotchi/system_one/log.rb +21 -0
- data/lib/samagotchi/system_one/recording.rb +1 -1
- 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/judgments.rb +0 -9
- 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 +8 -1
data/docs/bundles.md
ADDED
|
@@ -0,0 +1,1232 @@
|
|
|
1
|
+
# Bundles
|
|
2
|
+
|
|
3
|
+
What each shipped bundle does for you, its commands, settings and files. A bundle's code is written only against
|
|
4
|
+
the plugin API ([plugins.md](plugins.md)); install, upgrade and the `chi bundle` commands are in
|
|
5
|
+
[plugins.md](plugins.md#the-bundle-commands) and [cli.md](cli.md). Two shipped bundles are documented with the
|
|
6
|
+
feature they belong to: **source-links** in [hooks.md](hooks.md#the-source-links-bundle) and **known-names** in
|
|
7
|
+
[guardrails.md](guardrails.md#the-known-names-bundle).
|
|
8
|
+
|
|
9
|
+
## The btw bundle
|
|
10
|
+
|
|
11
|
+
`chi bundle install btw` (or the `dev` profile) installs the bundle shipped with chi. It is written
|
|
12
|
+
only against this API (`lib/samagotchi/bundles/btw/plugin.rb`).
|
|
13
|
+
|
|
14
|
+
- `/btw <question>` asks the session's model a side question about the
|
|
15
|
+
conversation, even while a turn runs. A card `btw: <question>` shows
|
|
16
|
+
"thinking…" at once, and the same card then shows the answer. Nothing else
|
|
17
|
+
sees the answer.
|
|
18
|
+
- The card's **Keep as session** runs `/btw keep <id>`. It forks the
|
|
19
|
+
conversation, the question and the answer into an idle child session, and
|
|
20
|
+
shows a card `kept as <id>`.
|
|
21
|
+
- The last 10 answers can be kept, while the session's worker (or REPL)
|
|
22
|
+
runs. After that, or after a restart, `keep` says expired.
|
|
23
|
+
- In the REPL, a question asked during a turn is about the conversation
|
|
24
|
+
before that turn, and the card says so. In a worker it includes the turn
|
|
25
|
+
so far.
|
|
26
|
+
- Settings: `bundles: btw: {max_tokens: 1024, timeout: 120}`.
|
|
27
|
+
- It ships no memory, so it adds no line to the prompt's memory index. Its
|
|
28
|
+
0.1.0 shipped `btw.md` as one, and `chi bundle upgrade btw` leaves that file
|
|
29
|
+
(and its index line) behind. Drop it with `chi bundle uninstall btw`, then
|
|
30
|
+
`chi bundle install btw`. After an upgrade already ran, delete
|
|
31
|
+
`$XDG_CONFIG_HOME/samagotchi/memories/btw.md` (`$XDG_CONFIG_HOME` defaults to
|
|
32
|
+
`~/.config`) and its `**btw**` line in `index.md` there by hand.
|
|
33
|
+
|
|
34
|
+
## The mcp bundle
|
|
35
|
+
|
|
36
|
+
`chi bundle install mcp` (or the `dev` profile) installs the bundle shipped with chi. It is written
|
|
37
|
+
only against this API (`lib/samagotchi/bundles/mcp/plugin.rb`), and gives
|
|
38
|
+
the model the tools of [MCP](https://modelcontextprotocol.io) servers. It
|
|
39
|
+
has no memory file, and the servers' tools aren't declared to the model one
|
|
40
|
+
by one: the bundle publishes them into chi's tool index
|
|
41
|
+
([chi.tool_source](plugins.md#chitool_sourcecall-acts_as-label-wait-prefix), a group
|
|
42
|
+
per server), and the model has its two tools, `find_tools` and `call_tool` (~250 tokens in
|
|
43
|
+
every request plus a line per server, whatever the servers have; `/mcp`
|
|
44
|
+
says how many). On `api: anthropic` each tool also goes out deferred as
|
|
45
|
+
`mcp_<server>_<tool>`, so a `find_tools` hit loads it and the model calls it
|
|
46
|
+
by that name, as `call_tool` would run it. Stdio servers only, for now.
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
# config.yml
|
|
50
|
+
bundles:
|
|
51
|
+
mcp:
|
|
52
|
+
timeout: 60 # seconds per tool call (default 60)
|
|
53
|
+
startup_timeout: 10 # seconds for initialize and tools/list (default 10)
|
|
54
|
+
servers:
|
|
55
|
+
everything:
|
|
56
|
+
command: [npx, -y, "@modelcontextprotocol/server-everything"]
|
|
57
|
+
files:
|
|
58
|
+
command: [npx, -y, "@modelcontextprotocol/server-filesystem", ~/scratch]
|
|
59
|
+
env: {NODE_OPTIONS: "--no-warnings"} # added to chi's environment
|
|
60
|
+
cwd: ~/scratch # default: where chi runs
|
|
61
|
+
tools: [read_*, list_directory] # optional: only these (globs)
|
|
62
|
+
description: files in ~/scratch # optional: its line in find_tools
|
|
63
|
+
timeout: 120 # optional: this server's per-call timeout
|
|
64
|
+
chrome:
|
|
65
|
+
command: [npx, -y, "chrome-devtools-mcp@latest", --slim, --headless]
|
|
66
|
+
attach_image_paths: true # the default; false leaves a path as text
|
|
67
|
+
start: lazy # the default; eager: start it with every session
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
- **Start: from a cache, on the first call.** A server's `tools/list` is
|
|
71
|
+
saved in the bundle's data dir (`$XDG_STATE_HOME/samagotchi/plugins/mcp/
|
|
72
|
+
tools-<server>.json`), keyed by a digest of its `command`, `env` (names
|
|
73
|
+
and values: only the digest is stored) and `cwd`. A session with a saved
|
|
74
|
+
list can search its tools at once and **doesn't start the server**: the
|
|
75
|
+
first call of one of its tools does (the call's row shows the wait). So a
|
|
76
|
+
session that never uses MCP spawns nothing, and a new session opens without
|
|
77
|
+
waiting for `npx`. If the live list differs from the saved one, the saved
|
|
78
|
+
one is replaced, and so is what a search finds. The model's tools never
|
|
79
|
+
change, so the prompt cache keeps. The saved file also keeps the first
|
|
80
|
+
sentence of the server's `instructions` (from `initialize`), for its line
|
|
81
|
+
in `find_tools`.
|
|
82
|
+
- **The first run** (no saved list, or the config changed) starts the
|
|
83
|
+
server in an [init task](plugins.md#chiinitlabel-provides_tools-false-quiet-false-timeout-nil-failed-nil--ctx--):
|
|
84
|
+
every UI shows `Starting MCP server x (first run, saving its tools)`, and
|
|
85
|
+
a turn sent meanwhile waits for its tools. A server that doesn't start,
|
|
86
|
+
answer or list its tools within `startup_timeout` (each step) is a warn
|
|
87
|
+
card, `…: failed`, and its calls fail until chi restarts. The rest of chi
|
|
88
|
+
works as usual.
|
|
89
|
+
- **Freshness.** A saved list older than a day is still used, and a quiet
|
|
90
|
+
background task lists the tools again with a server of its own (then
|
|
91
|
+
stops it), saves them, and replaces the searched ones if they changed.
|
|
92
|
+
One worker does it at a time.
|
|
93
|
+
- **A server that says its tools changed** (`notifications/tools/list_changed`)
|
|
94
|
+
is asked for its `tools/list` again; the saved list and the searched tools
|
|
95
|
+
are replaced.
|
|
96
|
+
- **A cached server that doesn't start** (the command is gone, it crashes)
|
|
97
|
+
fails that call with `Error: MCP server x didn't start: …` and one notice;
|
|
98
|
+
later calls answer the same at once until chi restarts, and a search shows
|
|
99
|
+
its tools as `(failed: …)`. The saved list stays: the next session tries
|
|
100
|
+
again.
|
|
101
|
+
- **`start: eager`** on a server starts it with every session (in an init
|
|
102
|
+
task, after the Bridge is up), even when that session's toolkit does not
|
|
103
|
+
reach its group. A first-run init task still blocks the first turn until
|
|
104
|
+
it finishes, even for an unreached group. A stale-cache refresh likewise
|
|
105
|
+
starts its short-lived server regardless of the toolkit. Switching away
|
|
106
|
+
from a group refuses its calls without starting a lazy cached server, and
|
|
107
|
+
does not stop a server already running; switching back reuses it.
|
|
108
|
+
- **`npx -y …@latest` checks the npm registry on every start** (~3.4 s for
|
|
109
|
+
`chrome-devtools-mcp`, against ~0.9 s for the installed binary). The cache
|
|
110
|
+
hides it from new sessions, but not from the first call. For a faster first
|
|
111
|
+
call, install the server once and run it directly:
|
|
112
|
+
|
|
113
|
+
```yaml
|
|
114
|
+
chrome:
|
|
115
|
+
command: [chrome-devtools-mcp, --slim, --headless] # after npm i -g chrome-devtools-mcp
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
- **Tools: search, then call.** The servers' tools are kept in chi's tool index;
|
|
119
|
+
the model has two fixed tools:
|
|
120
|
+
- `find_tools(query, group?)` searches it by keywords over the
|
|
121
|
+
server's name, the tool's name and its description, and answers up to 5
|
|
122
|
+
tools, each with its name `<server>/<tool>`, its description and its
|
|
123
|
+
whole `inputSchema`. A word counts more the fewer tools have it (a word
|
|
124
|
+
most of a browser server's tools share, "page", counts little), and
|
|
125
|
+
three times as much in a name as in a description. An empty query (and
|
|
126
|
+
a search with no match) lists every server's tool names, without
|
|
127
|
+
schemas. A tool whose `inputSchema` (or a property's schema) isn't an
|
|
128
|
+
object shows
|
|
129
|
+
`(bad schema)` (and is said once, as a notice), a failed server's tools
|
|
130
|
+
`(failed: …)`, a first-run server still starting `(starting, try
|
|
131
|
+
again)`. Its description names each server on one line, never its
|
|
132
|
+
tools: the server's `description:`, else the first sentence of its
|
|
133
|
+
`instructions`, and how many tools it has (`- chrome (26 tools)` with
|
|
134
|
+
neither: tool names there get called blind, without a search; a first
|
|
135
|
+
run's server is just its name until the next session).
|
|
136
|
+
- `call_tool(tool, args)` calls one: `tool` is `<server>/<tool>`, and
|
|
137
|
+
`mcp_<server>_<tool>` works too (looked up, not parsed). Two tools whose
|
|
138
|
+
`mcp_<server>_<tool>` comes out the same (servers `git-hub` and
|
|
139
|
+
`git_hub`, names alike up to the 48-character cut) are both refused,
|
|
140
|
+
since guardrail rules and approvals couldn't tell them apart: a search
|
|
141
|
+
and `/mcp` mark them `(name clash with …)`, and there is one notice.
|
|
142
|
+
`args` are typed by the tool's
|
|
143
|
+
`inputSchema` (numbers and booleans given as text, JSON text for an
|
|
144
|
+
object). A name it doesn't know answers the closest ones (`Error: no
|
|
145
|
+
tool chrome/open_url. Closest: chrome/new_page, …`). Its row shows
|
|
146
|
+
`mcp chrome/take_screenshot fullPage=true`.
|
|
147
|
+
- A tool turn takes a few more steps than when every tool was declared
|
|
148
|
+
(the P1 spike: about 1.5 more on a 30-tool server), and each request is
|
|
149
|
+
smaller (about half there).
|
|
150
|
+
- **Calls.** A call is `tools/call`. The text blocks of the answer are joined;
|
|
151
|
+
audio or a resource without text is a short placeholder
|
|
152
|
+
(`[audio: audio/wav]`). `isError` makes it `Error: …`; it, a JSON-RPC
|
|
153
|
+
error and `args` that aren't an object end with the tool's schema
|
|
154
|
+
(`chrome/click's inputSchema: {…}`), so a call made without a search can
|
|
155
|
+
be fixed on the next step. A call that takes longer than the timeout is
|
|
156
|
+
an `Error:`, and a cancelled turn stops the wait; both send
|
|
157
|
+
`notifications/cancelled` to the server.
|
|
158
|
+
- **Images.** An `image` block goes to the model as a picture
|
|
159
|
+
([Returning images](plugins.md#returning-images): at most 4 per call, a line
|
|
160
|
+
instead when the model can't see images). Its place in the text is a line,
|
|
161
|
+
`[image 1: image/png, attached]`, so the model knows the order. A text
|
|
162
|
+
block that is **the absolute path of an image file** is attached too
|
|
163
|
+
(`[image 1: screenshot.png, attached]` after the text), and so is an
|
|
164
|
+
absolute path ending in `.png`, `.jpg`, `.jpeg`, `.gif` or `.webp` inside
|
|
165
|
+
a sentence (`Saved screenshot to /tmp/shot.png.`; such a path can't hold a
|
|
166
|
+
space). Either way only when the file is an image by its bytes and is
|
|
167
|
+
under the system temp dir or the server's `cwd`: a server's text can't
|
|
168
|
+
pull in any image on disk. `chrome-devtools-mcp --slim` answers
|
|
169
|
+
`screenshot` that way; without `--slim`, `take_screenshot` returns an image
|
|
170
|
+
block. `attach_image_paths: false` on a server leaves such paths as text.
|
|
171
|
+
- **A server that exits** fails the call it was on with `Error: MCP server
|
|
172
|
+
x is not running (…)`, and there is one notice. Its next call starts it
|
|
173
|
+
again (its tools stay), up to 3 times a session; after the third restart
|
|
174
|
+
the notice says so, and its calls fail at once until chi restarts (a new
|
|
175
|
+
session, or the worker's next start).
|
|
176
|
+
- **Stop.** The servers stop with chi ([Shutdown](plugins.md#shutdown)): stdin is
|
|
177
|
+
closed, then TERM and KILL go to the server's process group. A chi
|
|
178
|
+
killed with `kill -9` (or crashing past Ruby) stops nothing itself; a
|
|
179
|
+
server that ignores its stdin's EOF would live on then. So each server's
|
|
180
|
+
group also holds a watchdog, a `/bin/sh` waiting on a pipe only chi
|
|
181
|
+
holds: when that pipe closes without chi letting it go, it gives the
|
|
182
|
+
server a second, then TERMs and KILLs its group (the server's helpers
|
|
183
|
+
too). A server that leaves its group (`setsid`) is out of its reach.
|
|
184
|
+
- **`/mcp`** (anytime) shows a card with the servers, their state (cached
|
|
185
|
+
(not started), running with its pid, failed with its count, stopped) and
|
|
186
|
+
their tools as `<server>/<tool>`, the name `call_tool` takes.
|
|
187
|
+
A cached or running server's line says roughly how many tokens its tool
|
|
188
|
+
definitions take (`~4,232 tokens`), which only a search's answer carries
|
|
189
|
+
now; the last line says what every request carries (`find_tools` and
|
|
190
|
+
`call_tool`) and totals the servers'. It is an estimate:
|
|
191
|
+
the definitions' JSON as the chat API gets it (`api: openai`), divided by
|
|
192
|
+
4. The native prompts (Gemma, Qwen) render a flatter schema, so there they
|
|
193
|
+
take less. The log records each server's estimate when it changes
|
|
194
|
+
(`mcp_tools_estimated`).
|
|
195
|
+
- The server's stderr goes to the debug log (`plugins` records, bundle=mcp).
|
|
196
|
+
- **Guardrails.** A rule's `tool:` can be a glob, so one rule covers every
|
|
197
|
+
MCP tool (a search is `find_tools`, outside `mcp_*`, so it isn't
|
|
198
|
+
asked about):
|
|
199
|
+
|
|
200
|
+
```yaml
|
|
201
|
+
guardrails:
|
|
202
|
+
rules:
|
|
203
|
+
- id: mcp-ask
|
|
204
|
+
tool: "mcp_*"
|
|
205
|
+
verdict: ask
|
|
206
|
+
reason: an MCP server's tool
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`call_tool`'s `targets:` act as the tool it calls, by its
|
|
210
|
+
`mcp_<server>_<tool>` name ([acts_as:](plugins.md#guardrails)), so `tool:
|
|
211
|
+
"mcp_github_*"` or `tool: mcp_github_merge_pull_request` match a
|
|
212
|
+
`call_tool` to it, as they matched the tool when it was declared by that
|
|
213
|
+
name. A call made before the server's tools are known (a first run
|
|
214
|
+
still starting) is matched by the name the model gave, so the rule fires
|
|
215
|
+
before the call waits; if the tool it then finds isn't that one, the
|
|
216
|
+
call is refused. The question shows the inner arguments under `<server>: <tool>`
|
|
217
|
+
(`everything: get_sum: a=20 b=22`), and "Allow this call for the
|
|
218
|
+
session" (or in this repo) allows that tool with those arguments only.
|
|
219
|
+
(Approvals stored before 0.6.0, under the old name, don't carry over, nor
|
|
220
|
+
ones stored before 0.7.0 under `mcp_call`: each MCP tool is asked about once more.)
|
|
221
|
+
Two things see `call_tool`, not the MCP tool: hooks (a `before_tool_call`
|
|
222
|
+
event's `tool:` is `"call_tool"`, the tool in its args; a built-in reached
|
|
223
|
+
through `call_tool` is the exception, it fires as that built-in), and loop-guard's
|
|
224
|
+
`ignore_tools` (`call_tool` ignores every MCP call; calls to different
|
|
225
|
+
tools, or with different args, stay distinct, since the tool is in the
|
|
226
|
+
args).
|
|
227
|
+
|
|
228
|
+
## The loop-guard bundle
|
|
229
|
+
|
|
230
|
+
`chi bundle install loop-guard` installs the bundle shipped with chi (`chi
|
|
231
|
+
bootstrap` installs it with the `core` profile). It is
|
|
232
|
+
written only against this API (`lib/samagotchi/bundles/loop-guard/plugin.rb`),
|
|
233
|
+
with `chi.on` hooks, and has no memory file. It guards two kinds of loop:
|
|
234
|
+
repeated tool calls (below) and [thinking that repeats
|
|
235
|
+
itself](#thinking-that-repeats-itself).
|
|
236
|
+
|
|
237
|
+
A local model can run the same tool call again and again in one turn: each
|
|
238
|
+
step's thinking starts over, so it never notices it already tried. (A real
|
|
239
|
+
one ran `find . -name 'config.yml'` ten times, getting nothing each time.)
|
|
240
|
+
loop-guard breaks that:
|
|
241
|
+
|
|
242
|
+
- A call is keyed by its tool and its arguments (whitespace collapsed), and
|
|
243
|
+
its result by a hash of the output. When a call has already returned the
|
|
244
|
+
same result `deny_after` times this turn (default 2), the next identical
|
|
245
|
+
call is **denied** with advice, so the 3rd one is caught:
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
[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.
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
The user sees one line per call per turn: `loop-guard> loop: execute find
|
|
252
|
+
. -name 'config.yml' 2>/dev/null repeated, denied`.
|
|
253
|
+
- At the `stop_after`-th deny in a turn (default 4) the turn is **stopped**
|
|
254
|
+
(core's own "stopped" notice), and a card lists the repeated calls, so the
|
|
255
|
+
user can say what to try instead.
|
|
256
|
+
- A denied call has no result: a deny (loop-guard's, known-names', a rule's)
|
|
257
|
+
never counts as the call's result, so the deny sticks.
|
|
258
|
+
- The counts are per turn, and a count is the turn's total, not a run of
|
|
259
|
+
consecutive repeats: the loop usually has other calls in between. A new
|
|
260
|
+
turn (a prompt, a continue, a reminder) starts from zero, since a new user
|
|
261
|
+
message can make an old call right again. A steering message merged into
|
|
262
|
+
a running turn doesn't reset them.
|
|
263
|
+
- The polling tools, where repeating is the point, are ignored.
|
|
264
|
+
|
|
265
|
+
```yaml
|
|
266
|
+
# config.yml
|
|
267
|
+
bundles:
|
|
268
|
+
loop-guard:
|
|
269
|
+
deny_after: 2 # same call, same result this many times: deny the next one
|
|
270
|
+
stop_after: 4 # stop the turn at this many denies
|
|
271
|
+
ignore_tools: [task_wait, task_get, delegate_result, list_sessions, list_reminders, context_read, forget_outputs]
|
|
272
|
+
mode: deny # deny | notify: notify only warns, once per call per turn
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Its hooks run at the default priority (100), after known-names (50), so in
|
|
276
|
+
known-names' `correct` mode loop-guard keys the corrected call.
|
|
277
|
+
|
|
278
|
+
### Thinking that repeats itself
|
|
279
|
+
|
|
280
|
+
A model can also go in circles inside one generation: its thinking says the
|
|
281
|
+
same few sentences again and again ("Wait, let me check once more…") for
|
|
282
|
+
minutes, until the provider's output cap ends it with no answer. loop-guard
|
|
283
|
+
watches the thinking while it streams ([Watching the
|
|
284
|
+
stream](hooks.md#watching-the-stream)) and cuts it:
|
|
285
|
+
|
|
286
|
+
- The thinking is cut into sentences. Two sentences of `min_words` or more
|
|
287
|
+
count as the same when their words and word pairs mostly match
|
|
288
|
+
(`similarity`), so a reworded round ("Hmm, …", a synonym, a swapped
|
|
289
|
+
clause) still counts.
|
|
290
|
+
- A loop is a cycle of 1 to `max_period` sentences seen `repeats` times in a
|
|
291
|
+
row, over at least `min_span_sentences` sentences and `min_span_chars`
|
|
292
|
+
chars; or one sentence `max_same` times anywhere in the generation. One
|
|
293
|
+
sentence over and over must be near-identical (`similarity` + 0.4), and a
|
|
294
|
+
longer cycle must hold different sentences: a list of templated sentences
|
|
295
|
+
("Now I need to open the file <path> and …" for 14 paths) is no loop.
|
|
296
|
+
- Short sentences (under `min_words`) are a loop of their own when
|
|
297
|
+
`short_run` of them come in a row with at most `short_distinct` different
|
|
298
|
+
ones among them ("I'll write it. Go. OK. Writing. Go. OK."). Normal
|
|
299
|
+
thinking has short sentences too ("Hmm.", "Fine."), but spread out or all
|
|
300
|
+
different (code lines); a longer sentence ends the run, and lines inside a
|
|
301
|
+
```` ``` ```` code block don't count.
|
|
302
|
+
- Any `window_sentences` sentences in a row, short or long, holding at most
|
|
303
|
+
`window_distinct` different ones are a loop too: a cycle of 7 or 8 short
|
|
304
|
+
sentences, or one where a longer sentence ("I'll write the spec file
|
|
305
|
+
now.") keeps ending the short run. Real thinking holds 40 or more
|
|
306
|
+
different sentences in any 48; DeepSeek's write loops hold 4 to 9.
|
|
307
|
+
- Nothing triggers before `min_chars` of thinking. The watch sees a loop
|
|
308
|
+
within one batch (2000 chars, or a second) of its third cycle.
|
|
309
|
+
|
|
310
|
+
What happens (`action: retry`, the default):
|
|
311
|
+
|
|
312
|
+
1. The first loop in a turn: the generation is cut and the model asked
|
|
313
|
+
again, with a hidden note that it was cut off. The user sees the loop's
|
|
314
|
+
first sentence quoted,
|
|
315
|
+
`loop-guard> thinking repeats itself ("Wait, the count of the letter r…", 3 sentences ×3, 4k chars, 8 s): cut`
|
|
316
|
+
(a short run or a window says how many different sentences it held:
|
|
317
|
+
"12 different sentences in 48"), and `↻ cut by loop-guard, asking again (1/1)`.
|
|
318
|
+
2. If the retry loops too, the turn is stopped ("stopped the turn: …", "■
|
|
319
|
+
turn stopped by loop-guard") with a card that quotes the repeated
|
|
320
|
+
sentences.
|
|
321
|
+
3. A loop is forgotten after `forget_after` good steps in a row (generations
|
|
322
|
+
with no loop): a model that recovered and made progress gets its next
|
|
323
|
+
loop cut and retried again, not the turn stopped. `forget_after: 0`
|
|
324
|
+
never forgets: the turn's second loop stops it.
|
|
325
|
+
|
|
326
|
+
The cut uses the `retry.empty_answer` budget: with `retry.empty_answer: 0`
|
|
327
|
+
the first loop ends the turn as stopped by loop-guard, with the notice and
|
|
328
|
+
no card. `action: stop` stops the turn at the first loop; `action: notify` only
|
|
329
|
+
warns, once per generation.
|
|
330
|
+
|
|
331
|
+
The cut thinking never goes back to the model, but its last 20k chars are kept
|
|
332
|
+
in the session's folder, `thinking_tails.jsonl` (with a generation that ran to
|
|
333
|
+
the provider's output cap, or that a stop_turn ended): an archived session
|
|
334
|
+
keeps them. A turn's record counts its cuts and cap hits (`cuts`, `capped` in
|
|
335
|
+
`analytics.json`; `/stats` shows "thinking cuts" and "output cap hits").
|
|
336
|
+
|
|
337
|
+
```yaml
|
|
338
|
+
bundles:
|
|
339
|
+
loop-guard:
|
|
340
|
+
thinking:
|
|
341
|
+
watch: true # false: the tool-call guard only
|
|
342
|
+
action: retry # retry (cut, ask again; then stop) | stop | notify
|
|
343
|
+
min_chars: 2000 # thinking this long before anything triggers
|
|
344
|
+
repeats: 3 # a cycle seen this many times is a loop
|
|
345
|
+
max_period: 6 # cycles of up to this many sentences
|
|
346
|
+
similarity: 0.5 # 0..1, how alike two sentences must be to count as the same
|
|
347
|
+
min_span_sentences: 6 # a loop spans at least this many sentences…
|
|
348
|
+
min_span_chars: 600 # …and this many chars
|
|
349
|
+
max_same: 8 # one sentence this many times in one generation is a loop
|
|
350
|
+
min_words: 5 # shorter sentences only count toward a short run
|
|
351
|
+
short_run: 24 # this many short sentences in a row…
|
|
352
|
+
short_distinct: 6 # …with at most this many different ones is a loop
|
|
353
|
+
window_sentences: 48 # any this many sentences in a row…
|
|
354
|
+
window_distinct: 12 # …with at most this many different ones is a loop
|
|
355
|
+
forget_after: 10 # good steps in a row that forget a loop; 0: never
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Not watched:
|
|
359
|
+
|
|
360
|
+
- thinking that arrives as answer text: Gemma 4 on the raw-prompt path (no
|
|
361
|
+
close marker), a chat provider that puts `<think>` in the content;
|
|
362
|
+
- a chat host with streaming off (no chunks);
|
|
363
|
+
- loops in the visible answer;
|
|
364
|
+
- a runaway without sentences, such as an endless comma list of numbers: it
|
|
365
|
+
ends at the output cap as an empty answer, which `retry.empty_answer`
|
|
366
|
+
retries after the fact.
|
|
367
|
+
|
|
368
|
+
A real small model's circular re-checking that repeats one sentence 8 times
|
|
369
|
+
is cut even if it would have found an answer later; `max_same` sets how
|
|
370
|
+
patient that is.
|
|
371
|
+
|
|
372
|
+
Not caught (yet):
|
|
373
|
+
|
|
374
|
+
- near-duplicates, such as `find . -name 'config*'` after `'config.yml'`;
|
|
375
|
+
- loops across turns;
|
|
376
|
+
- alternating calls (A, B, A, B) that each return something new.
|
|
377
|
+
|
|
378
|
+
## The cost-cap bundle
|
|
379
|
+
|
|
380
|
+
`chi bundle install cost-cap` installs the bundle shipped with chi (`chi bootstrap` installs it with the `core`
|
|
381
|
+
profile; an existing install gets it with `chi update`). It caps what one session spends, so neither you nor the
|
|
382
|
+
model has to watch `chi sessions stats`: the harness watches the money and the model never sees any of it (no tool,
|
|
383
|
+
no steer, no prompt text). It is inert without a cap, and there is none by default.
|
|
384
|
+
|
|
385
|
+
```yaml
|
|
386
|
+
# config.yml
|
|
387
|
+
bundles:
|
|
388
|
+
cost-cap:
|
|
389
|
+
default: 5.00 # USD cap for a session with none set; absent/0/off: no cap
|
|
390
|
+
child: 2.00 # for delegate children; absent: default
|
|
391
|
+
warn_at: 0.8 # tell the user (only) once at this share of the cap; 0/off: no warning
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
- **What counts:** `ctx.cost`'s total: what the provider reported (`usage.cost`), else the estimate from the
|
|
395
|
+
model's configured price (`hosts.<name>.models.<id>.price`), plus side requests (the idle recap, `/btw`,
|
|
396
|
+
`ask_model`), including what earlier workers of the session spent.
|
|
397
|
+
- **When it checks:** before each generation, so before the money is spent. A generation that crosses the cap is
|
|
398
|
+
caught at the start of the next one: the overshoot is at most one generation (plus any side requests between).
|
|
399
|
+
A turn whose last generation crossed it ends normally, and the next turn's first generation asks.
|
|
400
|
+
- **At `warn_at` of the cap** you get one notice, `cost-cap> $2.41 of $3.00 spent (80%)`; a raised cap warns again.
|
|
401
|
+
- **At the cap** the turn pauses on a question (header "Cost cap"): `Cost cap reached: $3.04 spent of $3.00.
|
|
402
|
+
Continue raises it to $6.00.` with **Continue** (another cap's worth: $3 → $6 → $9, as many steps as it takes to
|
|
403
|
+
pass the spend, saved for the session) and **Stop** (the turn stops, `canceled` with `stopped_by: cost-cap`, and a
|
|
404
|
+
card shows the spend, reported, estimated and side). With no one to ask (`--non-interactive`) it stops. A parent
|
|
405
|
+
agent answering it (`chi answer`) may Continue under `guardrails.parent_answers: soft` or `standard`, and only Stop
|
|
406
|
+
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
|
|
407
|
+
a command, not an answer: a parent that sends it (`chi send ID -m '/cap 10'`) raises the cap under any strategy.
|
|
408
|
+
- **The cap of a session:** the one `/cap` or a Continue set for it, else its own `cap:` (`chi --set
|
|
409
|
+
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
|
|
410
|
+
delegate child, else `default:`. A cap is a number (`3`, `.5`, `1e1`; in config.yml and `/cap`, `"$2"` too, which `--set` refuses). `--set` refuses
|
|
411
|
+
another value (`cost-cap.cap=five`: exit 2); one in config.yml (or set before this check) that doesn't read
|
|
412
|
+
(`default: five`) gets one notice, `cost-cap: default: "five" isn't a number or off; no cap`, and a `cap:` or
|
|
413
|
+
`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
|
|
414
|
+
the `--set` cap, not a `/cap` or Continue); there is no family cap.
|
|
415
|
+
- **A host it can't watch:** a model that reports no cost and has no configured price (a local server, a free
|
|
416
|
+
model that reports 0) reads unknown: one notice, `the $3.00 cap can't be enforced: <model> reports no cost and has
|
|
417
|
+
no price`, and it never stops for an unknown cost. A session with some priced generations counts the unpriced
|
|
418
|
+
ones as $0.
|
|
419
|
+
|
|
420
|
+
**`/cap`** (works while a turn runs; the next generation reads it):
|
|
421
|
+
|
|
422
|
+
- `/cap`: `cost-cap: $1.20 spent of $3.00 (reported $1.10, estimated $0.10, side $0.02)`, or `cost-cap: no cap
|
|
423
|
+
($1.20 spent)`, with `(can't be enforced: no cost reported or priced)` when it can't be.
|
|
424
|
+
- `/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
|
|
425
|
+
spend is allowed: the next generation asks.
|
|
426
|
+
- `/cap off` (or `/cap 0`): no cap for this session, over the config's `default:`.
|
|
427
|
+
|
|
428
|
+
A parent agent starts a session with a cap of its own with `--set` (`chi send --new --set cost-cap.cap=3 -m "task"`;
|
|
429
|
+
`cost-cap.cap` is the bundle's one session setting), or by sending `/cap N` as a first message
|
|
430
|
+
(`chi send --new -m '/cap 3'`).
|
|
431
|
+
At the cap, `chi send --wait` exits 3 with a question of kind `hook` (header "Cost cap"); `chi answer ID --question
|
|
432
|
+
Q --option Continue` (or `--option 1`) goes on, `--option Stop` stops the turn ([sub-agent.md](sub-agent.md)). A
|
|
433
|
+
parent may Continue: nothing restricts it, as with the step limit. A background delegate child at its cap waits on
|
|
434
|
+
its own card and doesn't wake its parent ([sessions.md](sessions.md#delegating)).
|
|
435
|
+
|
|
436
|
+
The state is one file per session, `state.json` in `ctx.session_dir` (`{"cap": 3.0, "step": 3.0}`), removed
|
|
437
|
+
with the session. A file that is there but doesn't read (not JSON, or not a mapping) is logged
|
|
438
|
+
(`costcap_state_unreadable`) and gets one notice, `cost-cap: <path> doesn't read, its /cap cap is ignored; using
|
|
439
|
+
default: ($5.00)`: the settings' cap is used, never a silent drop; the next `/cap` or Continue rewrites it, logged
|
|
440
|
+
(`costcap_state_replaced`) and said (`cost-cap: <path> didn't read; replaced with the new cap`).
|
|
441
|
+
|
|
442
|
+
Its sibling for work instead of money, tool calls and active minutes across the session's turns, is
|
|
443
|
+
[the work-cap bundle](#the-work-cap-bundle): the cap a local model (no cost) can have.
|
|
444
|
+
|
|
445
|
+
## The work-cap bundle
|
|
446
|
+
|
|
447
|
+
`chi bundle install work-cap` installs the bundle shipped with chi (`chi bootstrap` installs it with the `core`
|
|
448
|
+
profile; an existing install gets it with `chi update`). It caps the work one session does across its turns: tool
|
|
449
|
+
calls and active minutes. The per-turn meters reset every turn (the step limit, check-in, loop-guard), and a
|
|
450
|
+
Continue on the step-limit question starts the next 300 steps; a parent agent may answer that Continue itself
|
|
451
|
+
(`chi answer`), so a child it keeps continuing has no overall bound but cost-cap's `$`, and on a local model none.
|
|
452
|
+
work-cap is that bound, with cost-cap's shape: the harness watches, the model never sees any of it. It is inert
|
|
453
|
+
without a cap, and there is none by default.
|
|
454
|
+
|
|
455
|
+
```yaml
|
|
456
|
+
# config.yml
|
|
457
|
+
bundles:
|
|
458
|
+
work-cap:
|
|
459
|
+
child: {tool_calls: 500, minutes: 90} # delegate children with no own value
|
|
460
|
+
default: {} # every other session (and a delegate without child:); empty: no cap
|
|
461
|
+
warn_at: 0.8 # tell the user (only) once at this share of a cap; 0/off: no warning
|
|
462
|
+
ignore_tools: [task_wait, task_get, delegate, delegate_result] # not counted: waiting
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
- **What counts** (`ctx.usage`): **tool calls**, the session's own across its turns and workers, less
|
|
466
|
+
`ignore_tools` (the waiting tools by default, `delegate` among them since it blocks on its child, so a session
|
|
467
|
+
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
|
|
468
|
+
`ignore_tools`' time), never a wait (a question, its own included, an approval, the user's answer to
|
|
469
|
+
`ask_user_question`), so idle time and a Continue answered hours later don't count.
|
|
470
|
+
- **When it checks:** before each generation, like cost-cap: the overshoot is at most one generation's tool calls.
|
|
471
|
+
When the step limit and a work cap fall in the same turn, the work cap asks first, and its Stop ends the turn.
|
|
472
|
+
- **At `warn_at` of a cap** you get one notice per meter, `work-cap> 320 of 400 tool calls (80%)`; a raised cap
|
|
473
|
+
warns again.
|
|
474
|
+
- **At a cap** the turn pauses on a question (header "Work cap") that names the session's totals, so a parent
|
|
475
|
+
answering it decides on the whole picture: `Work cap reached: 412 tool calls of 400 (6 turns, 47 min active,
|
|
476
|
+
$1.20). Continue raises it to 800.` **Continue** raises the cap by another cap's worth (as many steps as it
|
|
477
|
+
takes to pass the work, saved for the session), **Stop** stops the turn (`canceled` with `stopped_by: work-cap`)
|
|
478
|
+
with a card showing the totals. Both meters at once: one question naming both, Continue raises each. With no one
|
|
479
|
+
to ask (`--non-interactive`) it stops. Continue is tagged `:spend`, as cost-cap's: a parent agent may Continue
|
|
480
|
+
under `guardrails.parent_answers: soft` or `standard`, only Stop under `hard` (see
|
|
481
|
+
[Guardrails](guardrails.md#answers-from-a-parent-agent)).
|
|
482
|
+
- **The caps of a session**, each meter on its own: the one `/workcap` or a Continue set, else its own value (`chi
|
|
483
|
+
--set work-cap.tool_calls=400`, `work-cap.minutes=60`, `chi send --new --set …`; `off`, `0` or a negative number
|
|
484
|
+
for none), else `child:`'s for a delegate child, else `default:`'s. Minutes in config.yml read `90`, `90m` or
|
|
485
|
+
`1.5h`. A value that doesn't read (`tool_calls: many`, a `default:` that isn't a mapping) gets one notice,
|
|
486
|
+
`work-cap: tool_calls: "many" isn't a number or off; using default.tool_calls: (400)`, and the next value is used,
|
|
487
|
+
never a silent no cap.
|
|
488
|
+
|
|
489
|
+
**`/workcap`** (works while a turn runs; the next generation reads it):
|
|
490
|
+
|
|
491
|
+
- `/workcap`: `work-cap: 320 of 400 tool calls (6 turns, 47 min active, $1.20)`, or `work-cap: no cap (…)` with the
|
|
492
|
+
totals.
|
|
493
|
+
- `/workcap 400`: the tool-call cap (and what Continue adds) becomes 400 for this session; `/workcap 90m` or
|
|
494
|
+
`/workcap 2h`: the active-minutes cap.
|
|
495
|
+
- `/workcap off` (or `/workcap 0`): no work cap for this session, over the config.
|
|
496
|
+
|
|
497
|
+
The state is one file per session, `state.json` in `ctx.session_dir` (`{"tool_calls": {"cap": 800, "step":
|
|
498
|
+
400}}`, a meter's `"cap": null` for off). A file that is there but doesn't read (not JSON, or not a mapping) is
|
|
499
|
+
logged (`workcap_state_unreadable`) and gets one notice, `work-cap: <path> doesn't read, its /workcap caps are
|
|
500
|
+
ignored; using default.tool_calls: (400)`: the settings' caps are used, never a silent drop; the next `/workcap` or
|
|
501
|
+
Continue rewrites it, logged (`workcap_state_replaced`) and said (`work-cap: <path> didn't read; replaced with the
|
|
502
|
+
new cap`).
|
|
503
|
+
|
|
504
|
+
## The check-in bundle
|
|
505
|
+
|
|
506
|
+
`chi bundle install check-in` installs the bundle shipped with chi (`chi
|
|
507
|
+
bootstrap` installs it with the `core` profile). It is
|
|
508
|
+
written only against this API (`lib/samagotchi/bundles/check-in/plugin.rb`):
|
|
509
|
+
`chi.on` hooks, `ctx.card`, `ctx.steer`, `ctx.stop_turn` and one anytime
|
|
510
|
+
command. It has no memory file.
|
|
511
|
+
|
|
512
|
+
A turn can run a long time on its own, reading and searching, without saying
|
|
513
|
+
what it has found. check-in counts the turn's tool calls, and when there are
|
|
514
|
+
`after` of them with no answer yet (then every `every` more) it checks in:
|
|
515
|
+
|
|
516
|
+
- **`mode: ask`** (the default): a card, one per turn, updated in place at
|
|
517
|
+
each check-in: "50 tool calls, no answer yet", how long the turn has run
|
|
518
|
+
and its last few tools, and three actions:
|
|
519
|
+
- **Nudge** (`/checkin nudge`): puts `message` into the running turn
|
|
520
|
+
(`ctx.steer`); the model reads it at its next step and answers or says
|
|
521
|
+
what is left. Every UI shows `check-in> nudged: …`. One nudge per
|
|
522
|
+
check-in: another click (or a typed `/checkin nudge`) answers
|
|
523
|
+
"check-in: already nudged at N tool calls".
|
|
524
|
+
- **Keep going** (`/checkin later`): closes the card until the next
|
|
525
|
+
check-in.
|
|
526
|
+
- **Stop** (`/checkin stop`): stops the turn.
|
|
527
|
+
|
|
528
|
+
When the turn ends the card loses its actions ("The turn ended after N tool
|
|
529
|
+
calls"), so no stale buttons stay; a card left by a worker that exited
|
|
530
|
+
mid-turn comes back without them too. In the attached terminal the actions are
|
|
531
|
+
`→ /checkin nudge` lines to type; the command runs beside the turn.
|
|
532
|
+
- **`mode: nudge`**: nudges the model by itself, with a notice line.
|
|
533
|
+
- **`mode: notify`**: a notice line only.
|
|
534
|
+
|
|
535
|
+
The count is per turn: a new turn (a prompt, a continue, a reminder) starts
|
|
536
|
+
from zero; steering merged into the running turn doesn't reset it (a cap
|
|
537
|
+
across the session's turns is [the work-cap bundle](#the-work-cap-bundle)). The polling
|
|
538
|
+
tools are not counted. A nudge that arrives after the model's final answer is
|
|
539
|
+
dropped (logged), never restarting a turn that is done; the card's "Nudged the
|
|
540
|
+
model at N tool calls" then becomes "The answer came first; nudge not sent."
|
|
541
|
+
|
|
542
|
+
```yaml
|
|
543
|
+
# config.yml
|
|
544
|
+
bundles:
|
|
545
|
+
check-in:
|
|
546
|
+
after: 50 # tool calls in one turn before the first check-in
|
|
547
|
+
every: 50 # then again every this many more
|
|
548
|
+
mode: ask # ask | nudge | notify
|
|
549
|
+
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."
|
|
550
|
+
ignore_tools: [task_wait, task_get, delegate_result]
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
`{calls}` in `message` is the count. The default asks for what has been found
|
|
554
|
+
and what is left, not "how's it going?": a small model tends to answer that
|
|
555
|
+
with a line and carry on unchanged.
|
|
556
|
+
|
|
557
|
+
`/checkin` (anytime) for this session. What it changes (on/off, the mode,
|
|
558
|
+
the threshold) is saved with the session, in the bundle's data dir
|
|
559
|
+
(`plugins/check-in/sessions/<id>/state.json`), so it outlasts a worker's restart
|
|
560
|
+
and goes with the session; another session starts from config.yml:
|
|
561
|
+
|
|
562
|
+
| | |
|
|
563
|
+
|---|---|
|
|
564
|
+
| `/checkin` | on or off, the mode, the threshold and this turn's count |
|
|
565
|
+
| `/checkin on` / `off` | check in, or not |
|
|
566
|
+
| `/checkin 30` | check in after 30 tool calls, then every 30 |
|
|
567
|
+
| `/checkin mode ask` / `nudge` / `notify` | the mode |
|
|
568
|
+
| `/checkin nudge` / `later` / `stop` | the card's actions, also by hand |
|
|
569
|
+
|
|
570
|
+
**Asking System 1 (audit).** Each check-in also asks the cheap judgment model
|
|
571
|
+
`check-in.progress` — "Is this turn still making progress toward what the user
|
|
572
|
+
asked?" — on a side thread, with the owner's prompt, the calls since the last
|
|
573
|
+
check-in (each cut as it is captured, about 4 KB in all), the count and the
|
|
574
|
+
elapsed time. It is off until you give it a mode in
|
|
575
|
+
[`system_one.judgments`](configuration.md#bundles-judgments), and nothing is
|
|
576
|
+
acted on: the answer is saved, and so is what the user then did with the card
|
|
577
|
+
(`nudge`, `keep_going`, `stop`, or `turn_stopped` when the card was still open
|
|
578
|
+
when the turn ended), as a label on that run for a later look. The card, the
|
|
579
|
+
nudge and the notice are the same either way. See
|
|
580
|
+
[System 1's saved runs](system-one.md).
|
|
581
|
+
|
|
582
|
+
## The slow-commands bundle
|
|
583
|
+
|
|
584
|
+
`chi bundle install slow-commands` installs the bundle shipped with chi (`chi
|
|
585
|
+
bootstrap` installs it with the `core` profile). It is written only against
|
|
586
|
+
this API (`lib/samagotchi/bundles/slow-commands/plugin.rb`), with `chi.on`
|
|
587
|
+
hooks and a `/slow-commands` command, and has no memory file.
|
|
588
|
+
|
|
589
|
+
At its limit (120 s, `execute.timeout_sec`) `execute` hands a command still
|
|
590
|
+
running over as a background task (`execute.on_timeout: task`, the default),
|
|
591
|
+
or kills it (`kill`). A full test suite or a build takes longer, so each run
|
|
592
|
+
first blocks the turn for those two minutes (and, in kill mode, loses them,
|
|
593
|
+
then starts again with `task_create` and `task_wait`); the next session does
|
|
594
|
+
the same. slow-commands remembers such a command for the project:
|
|
595
|
+
|
|
596
|
+
- An `execute` that **timed out** or was **handed over** at the limit, or ran **`warn_ratio` of the limit** or
|
|
597
|
+
longer (default 0.8, so 96 s of 120), is recorded as slow. A later run of
|
|
598
|
+
it under half the limit **with exit 0** drops the entry; a fast failure (a
|
|
599
|
+
load error) doesn't, since it says nothing about how long the suite takes.
|
|
600
|
+
Nor does one through a filter the key leaves out (`rspec 2>&1 | tail -30`
|
|
601
|
+
exits with `tail`'s 0, and a `| head` can end it early).
|
|
602
|
+
A call the user stopped, or that was denied, records nothing.
|
|
603
|
+
- The **next identical `execute`** is **denied** with advice:
|
|
604
|
+
|
|
605
|
+
```
|
|
606
|
+
[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.
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
The last sentence is there when a task of the same command (and cwd)
|
|
610
|
+
finished with exit 0 in the project's `tmp/tasks`; a longer one raises the
|
|
611
|
+
`task_wait` timeout it suggests. The user sees one line:
|
|
612
|
+
`slow-commands> execute bundle exec rspec: ran past 120 s before, denied`.
|
|
613
|
+
- **Once per command per turn**: the same `execute` again in that turn runs.
|
|
614
|
+
The model insisting is a signal (maybe the suite got faster), and that run
|
|
615
|
+
records the new truth. A new turn denies again.
|
|
616
|
+
- In kill mode (`execute.on_timeout: kill`), a **`timeout N` past the limit**
|
|
617
|
+
is denied up front, with no history: execute stops the command first, so
|
|
618
|
+
`timeout` never fires. In task mode it runs on as a task until `timeout`
|
|
619
|
+
fires. `timeout`'s exit codes (124, 137) mean nothing special here.
|
|
620
|
+
|
|
621
|
+
A command is the same when its project and its text match. The project is
|
|
622
|
+
the session's git checkout (else its working directory). The text is the
|
|
623
|
+
command's words and operators (`a && b` is not `a b`), the call's `cwd:`
|
|
624
|
+
folded in as `cd web && …`, and a trailing `2>&1`, `| tail …`, `| head …`
|
|
625
|
+
or `| grep …` dropped (they don't change how long it runs). Arguments stay:
|
|
626
|
+
`bundle exec rspec` (the suite) is slow, `bundle exec rspec
|
|
627
|
+
spec/foo_spec.rb` isn't the same command, and stays the fast path. A
|
|
628
|
+
command that ends inside a quote, `$(` or heredoc is never recorded, nor
|
|
629
|
+
one with a `$(…)`, backticks or a heredoc `cat` or `tee` reads (the key
|
|
630
|
+
can't tell two of those apart). A heredoc script (`python3 - <<'EOF'`)
|
|
631
|
+
keeps its body in the key, so each script is its own command.
|
|
632
|
+
|
|
633
|
+
Wrappers a model adds by habit don't make a new command: at the start of
|
|
634
|
+
each simple command (so after a leading `cd X &&` too), `time [-p]` and
|
|
635
|
+
`timeout`/`gtimeout` with its options and duration are left out, so
|
|
636
|
+
`timeout 300 bundle exec rspec` and `bundle exec rspec` are one entry. `X=…`
|
|
637
|
+
assignments stay, sorted (`SPEC=… rake spec` is not `rake spec`), except
|
|
638
|
+
`NO_COLOR`, `FORCE_COLOR` and `TERM` (`CI` stays: jest or vitest without it
|
|
639
|
+
can sit in watch mode); a plain `env X=… cmd` is `X=…
|
|
640
|
+
cmd`, and an `env` with an option (`-C`, `-i`, `-u`) stays as typed. The
|
|
641
|
+
list shows each entry as it was last typed.
|
|
642
|
+
|
|
643
|
+
Since 0.2.0 (chi 0.59.0) a hand-over counts as timed out, and the advice's
|
|
644
|
+
duration also reads a handed-over task, whose `exit_code` file is in the
|
|
645
|
+
state dir (the record's `exit_code_path`).
|
|
646
|
+
|
|
647
|
+
Entries live in `$XDG_STATE_HOME/samagotchi/plugins/slow-commands/commands.json`,
|
|
648
|
+
shared by every session (another session's record counts at once) and
|
|
649
|
+
forgotten after `ttl_days`.
|
|
650
|
+
|
|
651
|
+
```yaml
|
|
652
|
+
# config.yml
|
|
653
|
+
bundles:
|
|
654
|
+
slow-commands:
|
|
655
|
+
mode: deny # deny | notify (warn the user, never deny)
|
|
656
|
+
warn_ratio: 0.8 # a run this share of the execute limit or longer is slow
|
|
657
|
+
ttl_days: 14 # entries older than this are forgotten
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
| | |
|
|
661
|
+
|---|---|
|
|
662
|
+
| `/slow-commands` | this project's slow commands, numbered |
|
|
663
|
+
| `/slow-commands forget 2` / `forget all` | drop one (a wrong entry) or all of them |
|
|
664
|
+
|
|
665
|
+
## The agents-md bundle
|
|
666
|
+
|
|
667
|
+
`chi bundle install agents-md` (or the `core` profile) installs the bundle shipped
|
|
668
|
+
with chi (`lib/samagotchi/bundles/agents-md/plugin.rb`): two `chi.on` hooks
|
|
669
|
+
(`:before_turn` rebuilds the steered set, `:after_tool_call` steers), and no memory file.
|
|
670
|
+
|
|
671
|
+
chi's system prompt holds only the `AGENTS.md` chain from the checkout top down
|
|
672
|
+
to the session's cwd, so an `AGENTS.md` in a directory the session doesn't run
|
|
673
|
+
in (say `pkg/inner/` while the session runs in `pkg/`) is never seen. The
|
|
674
|
+
bundle closes that gap in history, not in the system prompt: the first `read`,
|
|
675
|
+
`write` or `edit` (its `tools` setting) of a file under a directory with an
|
|
676
|
+
`AGENTS.md` steers that file into the running turn, at the loop's next
|
|
677
|
+
boundary, as its own user message — with a first line that says which
|
|
678
|
+
directory it belongs to:
|
|
679
|
+
|
|
680
|
+
```
|
|
681
|
+
AGENTS.md for pkg/inner/ (applies to work under pkg/inner/):
|
|
682
|
+
<content>
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
- It steers the **outermost candidates first** (each directory from the
|
|
686
|
+
touched file's directory up to, but not including, the checkout top), so a
|
|
687
|
+
deep file can bring in several `AGENTS.md`s at once, outermost to nearest.
|
|
688
|
+
- It **never re-injects**: each directory steers once per session, and a
|
|
689
|
+
`before_turn` pass over the conversation rebuilds that set from the steers
|
|
690
|
+
already there. A steer the model answered past (never injected) is absent,
|
|
691
|
+
so it fires again on the next touch; a resume, a woken worker or a fork
|
|
692
|
+
pick the set up from their messages. It never stops generation or thinking.
|
|
693
|
+
- **Scope:** the touched file's own git root must be the session's checkout:
|
|
694
|
+
a file in another repository, a nested repository or a submodule is
|
|
695
|
+
ignored. Paths compare by `File.realpath`, so a symlinked cwd (a macOS
|
|
696
|
+
`/tmp`) still matches.
|
|
697
|
+
- Directories whose `AGENTS.md` is already in the system prompt (`ctx.prompt_files`:
|
|
698
|
+
the chain and appended files) are skipped, as is a file at the checkout top
|
|
699
|
+
(its `AGENTS.md` is the chain's first).
|
|
700
|
+
|
|
701
|
+
```yaml
|
|
702
|
+
# config.yml
|
|
703
|
+
bundles:
|
|
704
|
+
agents-md:
|
|
705
|
+
tools: [read, write, edit] # the calls that count as a touch
|
|
706
|
+
max_chars: 16000 # the content is cut here, with a "[cut at N
|
|
707
|
+
# of M chars: read <absolute path> for the rest]" last line
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
`skip_agents_md: true` disables the bundle, as it does the chain's loading
|
|
711
|
+
(see [Configuration](configuration.md#project-specific-description)).
|
|
712
|
+
|
|
713
|
+
## The skills bundle
|
|
714
|
+
|
|
715
|
+
`chi bundle install skills` (or the `dev` profile) installs the bundle shipped with chi
|
|
716
|
+
(`lib/samagotchi/bundles/skills/plugin.rb`): one anytime command, `chi.on`
|
|
717
|
+
hooks, `ctx.sessions.send`, `ctx.notify` and `ctx.steer`. It has no memory
|
|
718
|
+
file; skills themselves work without it ([docs/memory.md](memory.md#skills)).
|
|
719
|
+
|
|
720
|
+
| | |
|
|
721
|
+
|---|---|
|
|
722
|
+
| `/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 |
|
|
723
|
+
| `/skill list` | the `skill_*` memories of both scopes, with the date and description from the index |
|
|
724
|
+
| `/skill show <name>` | one skill as saved (project first, as `memory_read` looks) |
|
|
725
|
+
| `/skill diff <name> [N]` | the skill now against its N-th newest older version (default 1: before the last change), unified |
|
|
726
|
+
|
|
727
|
+
**History.** Before `memory_write`, `write` or `edit` changes a
|
|
728
|
+
`skill_<name>.md` in a memories folder, the file as it was is kept under
|
|
729
|
+
`$XDG_STATE_HOME/samagotchi/plugins/skills/history/<scope>/<name>/` (`system`,
|
|
730
|
+
or `project-<project folder>`), the newest `history_keep`. It is state, not a
|
|
731
|
+
memory: `chi bundle build` and a synced `~/.config` never see it. After the
|
|
732
|
+
call a line says what happened:
|
|
733
|
+
|
|
734
|
+
```
|
|
735
|
+
skills> skill release saved (project, 14 lines)
|
|
736
|
+
skills> skill release updated (+2 −1): 1. Run `scripts/verify.sh`; stop if it fails. · /skill diff release
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
The line is the file on disk changing, whatever the tool answered; a denied
|
|
740
|
+
write shows nothing.
|
|
741
|
+
|
|
742
|
+
A skill read in the turn is also watched for changes made another way (an
|
|
743
|
+
`execute` running `sed`, a script): after each other tool call, and at the
|
|
744
|
+
turn's end, its file is compared with the content last seen (at the read, or
|
|
745
|
+
after the last write). A change shows the same line, keeps the content last
|
|
746
|
+
seen as a version (so `/skill diff` has it) and counts as the skill updated.
|
|
747
|
+
A plain read keeps no version.
|
|
748
|
+
|
|
749
|
+
**The nudge** (`nudge: true`). Some models, finding a skill's step broken,
|
|
750
|
+
skip it and go on without fixing the skill. In a turn that read a skill
|
|
751
|
+
(`memory_read` of a `skill_*` name, or `read` of its file), the first failing
|
|
752
|
+
tool call after it (an `execute` that exited non-zero, except a grep-family
|
|
753
|
+
search that found nothing, a tool error) steers
|
|
754
|
+
the model once (a call a guardrail or the user denied doesn't count as a
|
|
755
|
+
failed step): *"A step of skill release failed. Find out why before skipping
|
|
756
|
+
it; if the skill is out of date, fix it now: edit the step that changed in
|
|
757
|
+
its file (or memory_write the whole skill) and add a Changelog line."* No
|
|
758
|
+
steer once a skill read this turn has changed, by any tool. If the turn ends
|
|
759
|
+
with a failed step and no skill read changed, one line says so: `skill
|
|
760
|
+
release was followed, a step failed, the skill wasn't updated`. A failure
|
|
761
|
+
unrelated to the skill (a test
|
|
762
|
+
meant to fail) can set it off too: once per turn, and only after a skill was
|
|
763
|
+
read.
|
|
764
|
+
|
|
765
|
+
```yaml
|
|
766
|
+
# config.yml
|
|
767
|
+
bundles:
|
|
768
|
+
skills:
|
|
769
|
+
history_keep: 20 # older versions kept per skill
|
|
770
|
+
nudge: true # steer once when a followed skill's step fails
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
## The github-pr bundle
|
|
774
|
+
|
|
775
|
+
```sh
|
|
776
|
+
chi bundle install github-pr # in the dev profile; needs gh, logged in
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
Proposes the branch's open GitHub pull request to a session as a candidate
|
|
780
|
+
`pr-<n>` when its worker starts (not in a scratch session; quietly nothing
|
|
781
|
+
without `gh`, a repository or an open PR): it is attached once you mention it
|
|
782
|
+
(`#N`, the branch or its URL; [Candidates](context.md#candidates)). It resolves
|
|
783
|
+
PR URLs for `chi context add`. Its script prints the PR as text
|
|
784
|
+
and a summary of what changed in counts, authors and states only; it wakes
|
|
785
|
+
the session for a review requesting changes, checks turning red, or the PR
|
|
786
|
+
merged or closed. In the web, a `path:line` in a worker's answer links to
|
|
787
|
+
that line of the session's PR ([Line links](context.md#line-links); `line_links:
|
|
788
|
+
always` for the REPL and `chi -p` too), and its ref menu offers the file
|
|
789
|
+
here and a "PR diff" action (`event[:refs]`). `auto_attach: attach` attaches
|
|
790
|
+
it at once (the old default), `off` does nothing ([Propose, attach or
|
|
791
|
+
off](context.md#propose-attach-or-off)).
|
|
792
|
+
See [Attached context](context.md#github-prs-the-github-pr-bundle).
|
|
793
|
+
|
|
794
|
+
## The coordinator bundle
|
|
795
|
+
|
|
796
|
+
```sh
|
|
797
|
+
chi bundle install coordinator # in the dev profile
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
chi as a coordinator of parallel work, the way you would run several agents
|
|
801
|
+
yourself: split the work, one git worktree and child session per task, keep
|
|
802
|
+
talking while they run, check each report, and merge only with your OK. It
|
|
803
|
+
works in a session with a worker (plain `chi`, the web). A `--no-shared` REPL
|
|
804
|
+
or `-p` gets no delegate reports, so the children's replies wouldn't come back
|
|
805
|
+
by themselves there.
|
|
806
|
+
|
|
807
|
+
- **The skill.** `skill_coordinator` (system scope; its index line says what
|
|
808
|
+
it is for, from the manifest's `description:`) holds the steps. The model
|
|
809
|
+
creates each worktree with `execute` (`git worktree add ../<repo>-<task> -b
|
|
810
|
+
<branch>`), starts a child there with `delegate wait: false, cwd:`, tells
|
|
811
|
+
you what started and ends its turn. When a child's report comes, it checks
|
|
812
|
+
the branch (`git log`, `git diff --stat`, the tests the child names) before
|
|
813
|
+
telling you. It asks before each merge (`ask_user_question`), checks that its
|
|
814
|
+
own folder is on the default branch and that branch's log first (another
|
|
815
|
+
agent may have merged the branch already), merges `--ff-only` and never
|
|
816
|
+
pushes unless you ask. After a merge it stops the
|
|
817
|
+
child and removes only the worktrees and branches it made.
|
|
818
|
+
- **A handoff memory.** The conversation is lost when the session ends, so
|
|
819
|
+
the skill keeps a project memory `handoff_<epic-slug>` with only what git
|
|
820
|
+
and the session list can't rebuild: the split (branch, worktree, child id
|
|
821
|
+
per task), your decisions, its verdicts and the follow-ups. Its index
|
|
822
|
+
description is the status, descriptive only and naming the session that
|
|
823
|
+
owns it ("OPEN coordinator handoff calc-v2 (session ab12cd34): 2/3 merged",
|
|
824
|
+
later "DONE: …"): every session in the repo sees that line. The model saves
|
|
825
|
+
a decision there before acting on it, and changes the status with
|
|
826
|
+
`memory_write`'s description-only form. When everything is done it marks
|
|
827
|
+
the handoff DONE and asks whether to remove it (`memory_write remove:
|
|
828
|
+
true`, which the guardrails bundle asks you to confirm). Children are told
|
|
829
|
+
not to touch it.
|
|
830
|
+
- **Resume.** A new coordinator session (or one asked "where were we?")
|
|
831
|
+
reads the open handoff, checks it against git and the session list (git
|
|
832
|
+
wins; mismatches are reported) and goes on from the first open step.
|
|
833
|
+
- **`/coordinate <goal>`** sends this session a turn asking the model to
|
|
834
|
+
follow the skill for the goal (in a `--no-shared` REPL it shows the text to
|
|
835
|
+
send yourself). Asking "do this in parallel" works too: the index line leads
|
|
836
|
+
the model to the skill. **`/coordinate resume`** asks it to resume this
|
|
837
|
+
project's open handoff; with several open it shows a card with a Resume
|
|
838
|
+
action each (`/coordinate resume <name>` picks one), with none it says so.
|
|
839
|
+
**`/coordinate end`** ends the day: the model checks each branch, brings
|
|
840
|
+
its handoff up to date (state per branch, decisions, verdicts,
|
|
841
|
+
follow-ups, each open child's id, branch and worktree), keeps it OPEN unless everything is done, and gives a short
|
|
842
|
+
report ending with the `/coordinate resume <slug>` to pick it up next
|
|
843
|
+
time. It starts, merges and stops nothing: running children stay running
|
|
844
|
+
and are named in the report.
|
|
845
|
+
- **Sprint review (experimental, 0.6.0).** **`/coordinate review [name]`**
|
|
846
|
+
starts a review session of this coordinator session (`ctx.sessions.review`:
|
|
847
|
+
its recap, never its transcript; this session keeps running) seeded with the
|
|
848
|
+
open handoff's body (the one named, the only one, or a card with a Review
|
|
849
|
+
action each when several are open; with none, the recap alone), and prints
|
|
850
|
+
the `chi share <id> --name NAME` line. A participant (a manager, a teammate)
|
|
851
|
+
brings new context there; the skill's Sprint review section (copied into
|
|
852
|
+
the seed: a participant's turn reads project memories only, and the skill
|
|
853
|
+
is a system one) has the review's model answer it with a **hand-off diff** (the handoff's lines as
|
|
854
|
+
they would change, in a `diff` block, and what it means per branch), in a
|
|
855
|
+
turn that may only read. Your **Apply** under it (the web's) quotes the
|
|
856
|
+
diff in your turn there: the model brings the handoff up to date and
|
|
857
|
+
re-plans in words, and starts, stops or merges nothing from the review
|
|
858
|
+
session: the coordinator session picks the change up from the handoff,
|
|
859
|
+
merges still asked. Experimental: it may go if it doesn't earn its place.
|
|
860
|
+
- **The coordinator toolkit.** The bundle declares `coordinator`: `lean`
|
|
861
|
+
plus `delegate`, `delegate_result` and `list_sessions`
|
|
862
|
+
([Toolkits](configuration.md#toolkits)). `/coordinate` puts a fresh session
|
|
863
|
+
on it before it asks the model (`ctx.use_toolkit`). When chi refuses (a reply
|
|
864
|
+
or a turn already there) and the session's toolkit has no `delegate`, the
|
|
865
|
+
reply says to run `/toolkit coordinator`. A continue of a coordinator link
|
|
866
|
+
keeps the toolkit.
|
|
867
|
+
- **The coding toolkit.** The bundle (0.5.0) also declares `coding`: `child`
|
|
868
|
+
with the prompt text `- Be concise in your responses` / `- Show file paths
|
|
869
|
+
clearly when working with files`. The skill starts build and fix children
|
|
870
|
+
on it (`delegate` with `toolkit: "coding"`); reviews, research and plans
|
|
871
|
+
stay on `toolkit.child`, as they need the length.
|
|
872
|
+
- **`/children [all]`** shows the session's children as a card, newest first,
|
|
873
|
+
one line each: `` `ab12cd34` · running · fix/flaky · "fix the flaky spec" ``,
|
|
874
|
+
`` `9a8b7c6d` · done · feat/x · reported · "All 12 specs pass" `` (`not
|
|
875
|
+
reported yet` when the parent wasn't given that reply), `` `77aa66bb` ·
|
|
876
|
+
waiting (approval) · open it: chi --attach 77aa66bb ``. A fork is marked
|
|
877
|
+
`fork`; `all` adds archived children. It runs during a turn too. The card
|
|
878
|
+
has Refresh and a `Stop <id>` per running or waiting child (up to 5);
|
|
879
|
+
`/children stop <id>` stops one of this session's own children (by session
|
|
880
|
+
id, as `chi sessions stop` does) and shows the card again.
|
|
881
|
+
- **Children stay in their worktree.** A child asks before it changes
|
|
882
|
+
anything outside its worktree (your checkout, a sibling's), in every
|
|
883
|
+
guardrails mode and without the guardrails bundle ([a delegate child's
|
|
884
|
+
boundary](guardrails.md#a-delegate-childs-boundary)); you answer on its
|
|
885
|
+
card, or on the parent's while it waits.
|
|
886
|
+
- **Merges.** The parent's merge in its own checkout is a plain `git
|
|
887
|
+
merge`; to be asked before every merge, add a rule ([Rules in
|
|
888
|
+
config.yml](guardrails.md#rules-in-configyml)):
|
|
889
|
+
|
|
890
|
+
```yaml
|
|
891
|
+
guardrails:
|
|
892
|
+
rules:
|
|
893
|
+
- id: git-merge
|
|
894
|
+
tool: shell
|
|
895
|
+
command: '\bgit\s+merge\b'
|
|
896
|
+
verdict: ask
|
|
897
|
+
reason: merging a child's branch
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
## The claude-peer bundle
|
|
901
|
+
|
|
902
|
+
```sh
|
|
903
|
+
chi bundle install claude-peer # in the dev profile
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
A live link between a chi session and a Claude Code session you already work
|
|
907
|
+
with: chi sends it a part of the work (a plan step, a diff, a finding) to
|
|
908
|
+
validate or as context, and Claude's answer comes back into chi. This is
|
|
909
|
+
Claude with the context it has, not a fresh review run. Nothing runs until a
|
|
910
|
+
Claude pairs; without one the bundle costs one tool declaration.
|
|
911
|
+
|
|
912
|
+
- **Pairing is Claude's.** chi can't see Claude's inbox, so Claude attaches
|
|
913
|
+
two pushed sources to the chi session: `claude-inbox`, whose hint is its
|
|
914
|
+
per-session inbox socket (`CLAUDE_CODE_MESSAGING_SOCKET`, Claude Code
|
|
915
|
+
2.1.224 or later), and `claude-outbox`, where its answers come. You never
|
|
916
|
+
run that yourself:
|
|
917
|
+
1. `/claude install-skill` (once) shows a card with the target,
|
|
918
|
+
`$CLAUDE_CONFIG_DIR/skills/chi-peer/SKILL.md` (else `~/.claude/…`),
|
|
919
|
+
whether one is there and, when it differs, a short diff; its Install (or
|
|
920
|
+
Replace) button runs `/claude install-skill --yes`, which copies the
|
|
921
|
+
bundle's `scripts/chi-peer-SKILL.md` there with the installed
|
|
922
|
+
`scripts/pair` path written in. The copy happens inside chi, where no
|
|
923
|
+
guardrail sees it: the button is your consent. The card also offers the
|
|
924
|
+
Claude Code permission rule `Read(~/.local/state/samagotchi/plugins/claude-peer/**)`
|
|
925
|
+
(your state folder), so Claude isn't asked before reading a long part's
|
|
926
|
+
file; chi never writes Claude's settings.
|
|
927
|
+
2. In Claude Code you say "pair with chi <id or chi web URL>". The skill runs
|
|
928
|
+
`sh <bundle>/scripts/pair <id or URL>`, which attaches `claude-inbox` (the
|
|
929
|
+
socket as its hint, pushed as JSON so a re-pair after a Claude restart
|
|
930
|
+
refreshes it) and `claude-outbox`, and lists the session's context. A
|
|
931
|
+
re-pair keeps Claude's last answer.
|
|
932
|
+
3. Claude answers a validate or a question with a JSON push to
|
|
933
|
+
`claude-outbox` (`text`, `summary`, `wake: true`), which wakes chi, the
|
|
934
|
+
first answer too (on chi 0.62 the first one arrives as a note without
|
|
935
|
+
waking); the model reads it with `context_read claude-outbox`.
|
|
936
|
+
- **The tool `claude_send(kind, title, text)`**, `kind` being `validate`,
|
|
937
|
+
`context` or `question`, writes one JSON line to that socket. The message
|
|
938
|
+
starts with a header, `chi <id> · <kind> · <title>`. A text up to 4,000
|
|
939
|
+
characters follows inline; a longer one goes to a file,
|
|
940
|
+
`$XDG_STATE_HOME/samagotchi/plugins/claude-peer/sessions/<id>/<HHMMSS>-<slug>.md`
|
|
941
|
+
(removed with the session), and the message names it (`payload: <path>`).
|
|
942
|
+
The tool is declared whether or not a Claude is paired, so pairing doesn't
|
|
943
|
+
change the prompt. Its errors say what to do: no `claude-inbox` (pair from
|
|
944
|
+
Claude), the socket gone or refusing (that Claude session ended or
|
|
945
|
+
restarted: pair again), a text over 200,000 characters.
|
|
946
|
+
- **`/claude`** (an anytime command, so it works mid-turn) sends without
|
|
947
|
+
the model:
|
|
948
|
+
- `/claude` alone shows a card: paired or not, the socket and whether its
|
|
949
|
+
file is there (it goes when that Claude session ends), the last send, and
|
|
950
|
+
Claude's last answer (the `claude-outbox` summary). When Claude's installed
|
|
951
|
+
chi-peer skill differs from what `/claude install-skill` would write (an
|
|
952
|
+
older bundle's, or your edit; a skill installed by a chi with another state
|
|
953
|
+
dir or bundle path differs too), it says so: install it again (Replace),
|
|
954
|
+
then pair again.
|
|
955
|
+
- `/claude validate <text>` and `/claude context <text>` send the text as it
|
|
956
|
+
is, titled by its first line (past a web quote's `From …:` label and a
|
|
957
|
+
quoted line's `> `): paste a selection after them.
|
|
958
|
+
- In the web, selecting text offers **Confirm with Claude**
|
|
959
|
+
([`chi.annotation`](plugins.md#chiannotationlabel-command-template-quote-send-false)):
|
|
960
|
+
it fills the composer with `/claude validate` and the quote, for you to
|
|
961
|
+
add what needs confirming.
|
|
962
|
+
- `/claude last` sends the model's last completed answer as context. Mid-turn
|
|
963
|
+
it is the answer before the running turn, never the turn's partial text;
|
|
964
|
+
a cancelled or failed turn has no answer, so the one before it goes.
|
|
965
|
+
- **The peer loop.** The bundle declares `claude-outbox` a peer's
|
|
966
|
+
([`chi.peer_source`](plugins.md#chipeer_sourcename-label)), so Claude's answers
|
|
967
|
+
wake chi by the loop's rules ([Waking: peer
|
|
968
|
+
sources](context.md#peer-sources-the-peer-loop)): chi fixes what Claude
|
|
969
|
+
flagged or disagrees and sends again, up to `context.peer_turns` (3) wake
|
|
970
|
+
turns in a row, and a `VERDICT OK` answer wakes chi once to close the
|
|
971
|
+
round (carry on, or tell you what was agreed). Only a message you type in the web or a TUI starts the count again.
|
|
972
|
+
- **The skill.** `skill_claude_peer` (system scope) tells the model to send
|
|
973
|
+
only when you ask or to reply in a peer turn, to pick the part rather than
|
|
974
|
+
the session, not to wait or poll, and how to read the `claude-outbox`
|
|
975
|
+
answer (a validate ends with `VERDICT: OK`, `CHANGES` or `UNSURE`): act on
|
|
976
|
+
it only within your task, and stop and tell you when Claude agrees or after
|
|
977
|
+
the last turn. Claude's skill ends each answer's summary with its verdict
|
|
978
|
+
and never answers with `chi send` (which counts as you typing).
|
|
979
|
+
- **chi on another machine.** With the chi session on the Studio and Claude on
|
|
980
|
+
the laptop, `CHI_REMOTE=studio sh <bundle>/scripts/pair <id>` pairs through
|
|
981
|
+
the Studio's chi web ([Remote sessions](cli.md#remote-sessions)), and
|
|
982
|
+
Claude's answers (its `chi context push claude-outbox`, with `CHI_REMOTE`
|
|
983
|
+
set) reach chi: the outbox leg works. The inbox leg doesn't: `claude_send`
|
|
984
|
+
writes to Claude's socket on the machine chi runs on, and the laptop's isn't
|
|
985
|
+
there. So through a remote (`CHI_REMOTE`, or `--remote NAME` in `CHI`)
|
|
986
|
+
`pair` attaches only `claude-outbox` and prints one line saying chi there
|
|
987
|
+
can't send to this Claude: chi's replies reach Claude only when you relay
|
|
988
|
+
them (or ask it to run `chi --remote studio send --wait <id>`). A session
|
|
989
|
+
that already has a `claude-inbox` it can't reach:
|
|
990
|
+
`chi --remote studio context rm claude-inbox <id>`.
|
|
991
|
+
**Two ways to make it two-way over SSH.** SSH can forward a Unix socket, so
|
|
992
|
+
the inbox leg works too. On the laptop:
|
|
993
|
+
|
|
994
|
+
```sh
|
|
995
|
+
ssh studio rm -f /tmp/claude-peer-ab12.sock
|
|
996
|
+
ssh -N -o ExitOnForwardFailure=yes -R /tmp/claude-peer-ab12.sock:"$CLAUDE_CODE_MESSAGING_SOCKET" studio
|
|
997
|
+
```
|
|
998
|
+
|
|
999
|
+
the second running in the background while chi may send (the `rm -f` first:
|
|
1000
|
+
a remote forward's stale socket is replaced only when the **server's** sshd
|
|
1001
|
+
has `StreamLocalBindUnlink yes`). Then
|
|
1002
|
+
`CHI_REMOTE=studio sh <bundle>/scripts/pair --inbox-socket /tmp/claude-peer-ab12.sock ab12`
|
|
1003
|
+
attaches both sources, with `claude-inbox`'s hint that path on the Studio. A
|
|
1004
|
+
re-pair this way replaces an outbox-only pairing's `claude-outbox` and its
|
|
1005
|
+
why. Claude's socket changes when that Claude session restarts: stop the ssh,
|
|
1006
|
+
run both lines, pair again. A path chi quotes (a `payload:` file, a diff to
|
|
1007
|
+
check) is on the Studio, not the laptop. Whoever can write to that socket on
|
|
1008
|
+
the remote machine can send into this Claude session (sshd makes it the
|
|
1009
|
+
user's, mode 0600 by default): fine on your own machine or pod, not a shared
|
|
1010
|
+
host. Claude's own skill has the same under "chi on another machine over SSH".
|
|
1011
|
+
- **A continue.** The pairing's two sources are pushed, so they stay with
|
|
1012
|
+
the old session when you continue it ([Movable
|
|
1013
|
+
context](context.md#peers-and-a-continue)). The new session's worker posts
|
|
1014
|
+
Claude one message, from a quiet init task, to the socket the old
|
|
1015
|
+
`claude-inbox` last had:
|
|
1016
|
+
|
|
1017
|
+
```
|
|
1018
|
+
chi <old> · moved · continued as <new id>
|
|
1019
|
+
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>).
|
|
1020
|
+
```
|
|
1021
|
+
|
|
1022
|
+
Claude's skill pairs again with the new id the same way it paired before
|
|
1023
|
+
(the same `CHI_REMOTE` and `--inbox-socket`). It is posted once (a marker
|
|
1024
|
+
under the bundle's `sessions/<id>/`, so a worker restart doesn't post it
|
|
1025
|
+
again); a post that fails (that Claude session ended) is logged and
|
|
1026
|
+
dropped. An outbox-only pairing has no inbox to post to: Claude's next
|
|
1027
|
+
answer to the old id is refused with the new one (`<old> was continued as
|
|
1028
|
+
<id>`), and the skill pairs again and pushes the answer there. The new
|
|
1029
|
+
session's note says which: `Left with 3f2a9c1e: claude-inbox, claude-outbox
|
|
1030
|
+
(Claude's pairing: chi asks Claude to pair again with this session).`, or
|
|
1031
|
+
`(Claude pairs again when it next answers)`.
|
|
1032
|
+
- **Asking first.** The tool has no `targets:`, so guardrails match it by name
|
|
1033
|
+
only: to be asked before each send, add a rule ([Rules in
|
|
1034
|
+
config.yml](guardrails.md#rules-in-configyml)):
|
|
1035
|
+
|
|
1036
|
+
```yaml
|
|
1037
|
+
guardrails:
|
|
1038
|
+
rules:
|
|
1039
|
+
- id: claude-send
|
|
1040
|
+
tool: claude_send
|
|
1041
|
+
verdict: ask
|
|
1042
|
+
reason: sends session content to Claude Code
|
|
1043
|
+
```
|
|
1044
|
+
|
|
1045
|
+
- **Limits.** The socket's format (`{"type":"user","message":{"role":"user","content":"…"}}`)
|
|
1046
|
+
is undocumented: it was found in, and checked on, Claude Code 2.1.296. A
|
|
1047
|
+
line Claude Code can't parse is dropped silently, so a Claude Code update
|
|
1048
|
+
could break this without an error. No auth line is sent: it is optional on
|
|
1049
|
+
macOS and Linux, and native Windows, which needs it, isn't supported.
|
|
1050
|
+
Claude's `crossSessionInbound` setting decides what it does with a peer
|
|
1051
|
+
message: an interactive auto-mode session delivers it; a session that
|
|
1052
|
+
bypasses permissions holds it for approval (in `-p` it expires after 5
|
|
1053
|
+
minutes). It is local only (a Unix socket), but it does hand session
|
|
1054
|
+
content to another agent: only what the model or you picked.
|
|
1055
|
+
- **Needs chi 0.67.0** for the loop (`chi update`): the plugin is built on
|
|
1056
|
+
`Samagotchi::Plugin::Peer` (lib/samagotchi/plugin/peer.rb), the base both
|
|
1057
|
+
peer bundles share (the tool, `/claude`, the cards, payloads), and each
|
|
1058
|
+
bundle sets its strings and transport.
|
|
1059
|
+
|
|
1060
|
+
## The codex-peer bundle
|
|
1061
|
+
|
|
1062
|
+
```sh
|
|
1063
|
+
chi bundle install codex-peer # in the dev profile
|
|
1064
|
+
```
|
|
1065
|
+
|
|
1066
|
+
The [claude-peer bundle](#the-claude-peer-bundle) for an OpenAI Codex CLI
|
|
1067
|
+
session: chi sends the Codex session you work with a part of the work to
|
|
1068
|
+
validate or as context, and Codex's answer comes back into chi. It is a copy
|
|
1069
|
+
of claude-peer with the transport swapped; what differs is below, the rest
|
|
1070
|
+
(kinds, payload files, the last send, `/codex last`, the status card, the
|
|
1071
|
+
skill install card) works the same.
|
|
1072
|
+
|
|
1073
|
+
- **Sending is `codex queue`.** `codex_send(kind, title, text)` and
|
|
1074
|
+
`/codex validate|context <text>` run
|
|
1075
|
+
`codex queue --thread <uuid> --message <text>` (an argv, no shell), which
|
|
1076
|
+
wakes an idle Codex TUI: the message runs there as a new user turn (checked
|
|
1077
|
+
on Codex 0.162.1; a message queued mid-turn runs after the turn). The
|
|
1078
|
+
thread is the `codex-inbox` context's hint, Codex's `CODEX_THREAD_ID`, which
|
|
1079
|
+
must look like a UUID. `codex` is `$CODEX`, else `codex` on the PATH: a
|
|
1080
|
+
worker started by `chi web` or launchd may have another PATH than your
|
|
1081
|
+
shell, so set `CODEX=` where chi runs if the error says there's no codex.
|
|
1082
|
+
The tool and the anytime `/codex` wait for `codex queue` up to 10 s; a
|
|
1083
|
+
queue that takes longer is killed (its process group) and the send fails
|
|
1084
|
+
with a timeout error, never a hang. Other errors say what to do: no
|
|
1085
|
+
`codex-inbox` (pair from Codex), a thread Codex has no record of (that
|
|
1086
|
+
Codex session is gone: pair again), an ephemeral thread
|
|
1087
|
+
(`codex exec --ephemeral` can't take queued messages), or `codex queue`'s
|
|
1088
|
+
first error line. A Codex session that was closed (`/quit`) is not gone:
|
|
1089
|
+
Codex's app-server still takes the queue (exit 0), chi says sent, and no
|
|
1090
|
+
answer comes; the message waits for that session (`codex resume` there, or
|
|
1091
|
+
pair again from a new one). The status card shows the thread id and says
|
|
1092
|
+
so; there is no cheap check that the session is open.
|
|
1093
|
+
- **Codex's sandbox.** Pairing and every reply write chi's state
|
|
1094
|
+
(`chi context add`/`push` write the session's context files directly),
|
|
1095
|
+
which Codex's default sandbox (workspace-write) refuses. The user adds, to
|
|
1096
|
+
`$CODEX_HOME/config.toml` (else `~/.codex/config.toml`):
|
|
1097
|
+
|
|
1098
|
+
```toml
|
|
1099
|
+
[sandbox_workspace_write]
|
|
1100
|
+
writable_roots = ["/Users/you/.local/state/samagotchi/context"]
|
|
1101
|
+
```
|
|
1102
|
+
|
|
1103
|
+
and restarts Codex. Only chi's `context/` folder: the state folder above it
|
|
1104
|
+
also holds guardrails, session files, plugins and the web token.
|
|
1105
|
+
`/codex install-skill`'s card shows the line with your state folder
|
|
1106
|
+
(absolute: config.toml doesn't expand `~`), and the pair script names it
|
|
1107
|
+
when a write is refused ("Operation not permitted"). chi never edits
|
|
1108
|
+
Codex's config. Reading a payload file needs nothing: the default sandbox
|
|
1109
|
+
reads anywhere. With chi on this machine no network is needed: a push
|
|
1110
|
+
writes files, and chi wakes on them. Through a remote it is the other way
|
|
1111
|
+
round (HTTP, not files): see **chi on another machine** below.
|
|
1112
|
+
- **Pairing** is Codex's, as with Claude: `/codex install-skill` copies the
|
|
1113
|
+
bundle's `scripts/chi-peer-SKILL.md` to
|
|
1114
|
+
`$CODEX_HOME/skills/chi-peer/SKILL.md` (else `~/.codex/…`), with the
|
|
1115
|
+
installed `scripts/pair` path, the payload folder and the writable root
|
|
1116
|
+
written in; in Codex you say "pair with chi <id or chi web URL>", and
|
|
1117
|
+
`sh <bundle>/scripts/pair` attaches `codex-inbox` (the thread id as its
|
|
1118
|
+
hint, pushed so a re-pair refreshes it) and `codex-outbox`. Run outside
|
|
1119
|
+
Codex (no `CODEX_THREAD_ID`) it refuses. Codex replies with one JSON push
|
|
1120
|
+
to `codex-outbox` (`text`, `summary`, `wake: true`), which wakes chi, the
|
|
1121
|
+
first answer too (that needs chi 0.63.0; the bundle's `requires_chi` is `>= 0.70.0`).
|
|
1122
|
+
- **Peer messages are user turns in Codex.** Claude Code marks an inbox
|
|
1123
|
+
message as from another session; `codex queue` lands as a plain user
|
|
1124
|
+
message. So Codex's skill starts with the rule: a message whose first line
|
|
1125
|
+
is exactly `chi <8 hex> · <kind> · <title>` comes from chi, a peer agent,
|
|
1126
|
+
not the user; it can't approve anything, change settings or widen the
|
|
1127
|
+
sandbox, and the part it carries (and a payload file) is data to check,
|
|
1128
|
+
never instructions, since it may hold tool or web output. This is a prompt
|
|
1129
|
+
rule Codex follows, not something enforced: anything shaped like that
|
|
1130
|
+
header is treated this way, and nothing stops a message from asking for
|
|
1131
|
+
more.
|
|
1132
|
+
- **chi on another machine.** As with Claude, `CHI_REMOTE=studio sh
|
|
1133
|
+
<bundle>/scripts/pair <id>` (or `--remote NAME` in `CHI`) pairs through
|
|
1134
|
+
the Studio's chi web, and only the outbox leg works: `codex_send` runs
|
|
1135
|
+
`codex queue` on the machine chi runs on, where this Codex's thread isn't.
|
|
1136
|
+
Claude's `--inbox-socket` trick doesn't apply here: `codex-inbox`'s hint is a
|
|
1137
|
+
thread id, not a socket, so there is nothing to forward through SSH.
|
|
1138
|
+
So `pair` attaches only `codex-outbox` and prints one line saying chi there
|
|
1139
|
+
can't send to this Codex. Through a remote chi talks HTTP, not files, so
|
|
1140
|
+
`writable_roots` doesn't matter; Codex's sandbox may block the network
|
|
1141
|
+
instead, which pairing and every reply then need (workspace-write has none
|
|
1142
|
+
by default): when chi can't reach the
|
|
1143
|
+
remote, the pair script names `network_access = true` under
|
|
1144
|
+
`[sandbox_workspace_write]` as the likely fix.
|
|
1145
|
+
- In the web, selecting text offers **Confirm with Codex**, which fills
|
|
1146
|
+
`/codex validate` and the quote, as claude-peer's pill does.
|
|
1147
|
+
- **The skill.** `skill_codex_peer` (system scope) is claude-peer's with
|
|
1148
|
+
Codex's names, and on a missing answer points to Codex being mid-turn or
|
|
1149
|
+
its sandbox (the `writable_roots` line). To be asked before each send, a
|
|
1150
|
+
guardrails rule with `tool: codex_send` (see the claude-peer section).
|
|
1151
|
+
- **The peer loop** works as claude-peer's: `codex-outbox` is declared a
|
|
1152
|
+
peer's, Codex's answers wake chi up to `context.peer_turns` turns in a row,
|
|
1153
|
+
and `VERDICT OK` wakes chi once to close the round. Needs chi 0.67.0 (the closing
|
|
1154
|
+
wake: the chi released with this bundle).
|
|
1155
|
+
- **A continue** works as claude-peer's: the new session queues Codex the
|
|
1156
|
+
`moved` message on the thread the old `codex-inbox` named, once, and
|
|
1157
|
+
Codex's skill pairs again with the new id (the same `CHI_REMOTE`, if any).
|
|
1158
|
+
A pairing through a remote has no inbox: Codex pairs again when its next
|
|
1159
|
+
push is refused. A closed Codex session still takes the message; it waits
|
|
1160
|
+
for `codex resume` there.
|
|
1161
|
+
|
|
1162
|
+
## The ruby bundle
|
|
1163
|
+
|
|
1164
|
+
`chi bundle install ruby` (or the `dev` profile) installs the bundle shipped
|
|
1165
|
+
with chi (`lib/samagotchi/bundles/ruby/plugin.rb`): one `chi.on` hook
|
|
1166
|
+
(`:after_tool_call`), and no memory file.
|
|
1167
|
+
|
|
1168
|
+
An RSpec run that finds no examples prints `0 examples, 0 failures`, which
|
|
1169
|
+
reads green: the model takes the suite as passing and moves on with code no
|
|
1170
|
+
test ever loaded. It usually means a path that matched no file, a filter
|
|
1171
|
+
(`-e`, `--tag`) that matched no example, or a `.rspec --pattern` that hid
|
|
1172
|
+
the specs (a nested project's `.rspec` overrides one). The bundle notices
|
|
1173
|
+
that run and steers a hint into the running turn, which the user sees as
|
|
1174
|
+
`ruby> nudged: …`:
|
|
1175
|
+
|
|
1176
|
+
```
|
|
1177
|
+
rspec ran 0 examples: check the path, the filter (-e/--tag) or the .rspec --pattern
|
|
1178
|
+
```
|
|
1179
|
+
|
|
1180
|
+
It is a nudge for the model, not a denial: the turn goes on, and the model
|
|
1181
|
+
can say why the run was empty. It never runs anything.
|
|
1182
|
+
|
|
1183
|
+
- **The command.** Only an `execute` counts, and its command must run
|
|
1184
|
+
rspec: one of its simple commands (`&&`, `||`, `;`, `|`, a newline, so
|
|
1185
|
+
after a leading `cd x &&` too) starts with `rspec`, a path ending in
|
|
1186
|
+
`/rspec` (`bin/rspec`, `./bin/rspec`), or `parallel_rspec`, once a leading
|
|
1187
|
+
`X=…` assignment, `bundle exec` and `time` are left out. A command that
|
|
1188
|
+
only names rspec (`grep rspec README`, `echo "bundle exec rspec"`) is not
|
|
1189
|
+
a run.
|
|
1190
|
+
- **The output.** Every `<N> examples, <M> failures` line in the execute's
|
|
1191
|
+
output counts (RSpec's summary, with or without `, N pending`), and the
|
|
1192
|
+
hint comes when all of them are 0. `parallel_rspec` prints one per
|
|
1193
|
+
process, so a single process with examples leaves its 0 out of the total.
|
|
1194
|
+
- **No summary line, no hint.** A spec that fails to load, a path that
|
|
1195
|
+
doesn't exist, a command killed at its limit: the output already says what
|
|
1196
|
+
is wrong, and the hint would only repeat it.
|
|
1197
|
+
|
|
1198
|
+
Nothing is remembered, so the next 0-example run hints again: it can be
|
|
1199
|
+
the same mistake on another command, or a different one. A hint the model
|
|
1200
|
+
answers before joining the turn is dropped, as any steer is.
|
|
1201
|
+
|
|
1202
|
+
## The model-notes bundle
|
|
1203
|
+
|
|
1204
|
+
`chi bundle install model-notes` (or the `dev` profile) installs reviewed
|
|
1205
|
+
[model notes](memory.md#model-notes) into the system scope: working habits
|
|
1206
|
+
for one model family, loaded into the prompt only for the models a note's
|
|
1207
|
+
`models:` line names. It has no plugin or hook. It ships two notes:
|
|
1208
|
+
|
|
1209
|
+
| note | models | for |
|
|
1210
|
+
|---|---|---|
|
|
1211
|
+
| `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 |
|
|
1212
|
+
| `model_notes_tone` | `*` | every model: answer first, in plain words; no preambles, restated questions or closing offers; lists only for steps or parallel items |
|
|
1213
|
+
|
|
1214
|
+
- **Reviewed before it ships.** A note is generic (no repo or machine facts),
|
|
1215
|
+
says nothing the harness already does (a tool's description, an alias),
|
|
1216
|
+
talks to the model as "you", stays under 1,500 characters, and the user
|
|
1217
|
+
approves its text. A model may propose one; it never edits the bundle.
|
|
1218
|
+
`notes/README.md` in the bundle records each note's sources, the
|
|
1219
|
+
`script/model_notes_report.rb` numbers and the approval.
|
|
1220
|
+
- **Globs per measured version.** A glob is tried against the id with no host
|
|
1221
|
+
prefix and against its model key, case-insensitively, and `*` also matches
|
|
1222
|
+
`/`: `*deepseek-v4*` covers `deepseek/deepseek-v4.1-flash` on any provider,
|
|
1223
|
+
MLX and GGUF names, but not `deepseek-ai/DeepSeek-R1-Distill-Qwen-32B`.
|
|
1224
|
+
- **Tested.** `lib/samagotchi/bundles/model-notes/spec/model_ids.yml` lists real id forms and the exact notes
|
|
1225
|
+
each loads (negative rows included); the spec installs the bundle and
|
|
1226
|
+
resolves each through `sandbox.model_notes_for`
|
|
1227
|
+
([Testing a bundle](plugins.md#testing-a-bundle-experimental)). It also checks each
|
|
1228
|
+
note's shape, size and that the tools it names are ones chi declares.
|
|
1229
|
+
- **Your own notes:** a same-name note you have wins and keeps its place
|
|
1230
|
+
through upgrades; other notes stack with the bundle's
|
|
1231
|
+
([Model notes](memory.md#model-notes)).
|
|
1232
|
+
|