samagotchi 0.12.0 → 0.14.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 +116 -1
- data/README.md +20 -7
- data/bin/chi +9 -9
- data/docs/architecture.md +4 -2
- data/docs/cli.md +34 -10
- data/docs/configuration.md +79 -15
- data/docs/desktop.md +10 -6
- data/docs/guardrails.md +74 -7
- data/docs/hooks.md +13 -9
- data/docs/memory.md +5 -3
- data/docs/plugins.md +31 -16
- data/docs/sessions.md +9 -6
- data/docs/sub-agent.md +128 -0
- data/lib/samagotchi/answer_command.rb +220 -0
- data/lib/samagotchi/bridge/card_store.rb +94 -6
- data/lib/samagotchi/bridge.rb +14 -2
- data/lib/samagotchi/bridge_client.rb +8 -3
- data/lib/samagotchi/bundle_command.rb +18 -10
- data/lib/samagotchi/bundles/check-in/manifest.yml +2 -2
- data/lib/samagotchi/bundles/check-in/plugin.rb +68 -5
- data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +19 -0
- data/lib/samagotchi/bundles/guardrails/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/plugin.rb +166 -34
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +5 -5
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/cli/command.rb +3 -2
- data/lib/samagotchi/cli/exit.rb +23 -0
- data/lib/samagotchi/cli/parent_wait.rb +82 -0
- data/lib/samagotchi/config.rb +81 -24
- data/lib/samagotchi/context_window.rb +30 -5
- data/lib/samagotchi/desktop_command.rb +5 -3
- data/lib/samagotchi/engine.rb +54 -17
- data/lib/samagotchi/guardrails/parent_approvals.rb +158 -0
- data/lib/samagotchi/hooks/bundle_loader.rb +10 -3
- data/lib/samagotchi/host_registry.rb +31 -7
- data/lib/samagotchi/kernel_loop.rb +1 -1
- data/lib/samagotchi/llm/chat_loop.rb +5 -3
- data/lib/samagotchi/llm/openai_chat.rb +9 -1
- data/lib/samagotchi/llm/turn_settings.rb +7 -5
- data/lib/samagotchi/memory_bundle/installer.rb +12 -0
- data/lib/samagotchi/memory_bundle/status.rb +9 -1
- data/lib/samagotchi/memory_paths.rb +8 -0
- data/lib/samagotchi/model_profile.rb +30 -16
- data/lib/samagotchi/parent_report.rb +213 -0
- data/lib/samagotchi/plugin/api.rb +15 -2
- data/lib/samagotchi/prompt_literal_guard.rb +4 -2
- data/lib/samagotchi/question_desk.rb +29 -2
- data/lib/samagotchi/recap_setup.rb +15 -5
- data/lib/samagotchi/reply_wait.rb +5 -1
- data/lib/samagotchi/send_command.rb +50 -75
- data/lib/samagotchi/session.rb +15 -0
- data/lib/samagotchi/session_manager.rb +40 -7
- data/lib/samagotchi/sessions_command.rb +13 -3
- data/lib/samagotchi/system_prompt.rb +11 -3
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +33 -5
- data/lib/samagotchi/terminal_ui/attached_loop.rb +53 -14
- data/lib/samagotchi/terminal_ui.rb +30 -3
- data/lib/samagotchi/thought_stream_splitter.rb +4 -4
- data/lib/samagotchi/tool_call_parser.rb +5 -6
- data/lib/samagotchi/tools/builtin_calls.rb +14 -3
- data/lib/samagotchi/tools/builtins.rb +14 -2
- data/lib/samagotchi/tools/delegate_wait.rb +32 -13
- data/lib/samagotchi/tools/execute.rb +8 -8
- data/lib/samagotchi/tools/memory.rb +3 -3
- data/lib/samagotchi/tools/task_create.rb +3 -2
- data/lib/samagotchi/tools/task_runtime.rb +7 -2
- data/lib/samagotchi/utf8_default.rb +29 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +22 -10
- data/lib/samagotchi/web/message_parts.rb +2 -1
- data/lib/samagotchi/web/public/activity.js +6 -1
- data/lib/samagotchi/web/public/app.js +39 -6
- data/lib/samagotchi/web/public/index.html +8 -1
- data/lib/samagotchi/web/public/notify.js +16 -3
- data/lib/samagotchi/web/public/question_cards.js +3 -1
- data/lib/samagotchi/web/public/sessions_list.js +37 -0
- data/lib/samagotchi/web/public/turn_view.js +6 -9
- data/lib/samagotchi/web/server.rb +2 -1
- data/lib/samagotchi/web/session_summary.rb +3 -11
- metadata +8 -1
data/docs/guardrails.md
CHANGED
|
@@ -51,11 +51,14 @@ sees these diffs.
|
|
|
51
51
|
|
|
52
52
|
Who answers:
|
|
53
53
|
|
|
54
|
-
- REPL (`chi --no-shared`,
|
|
55
|
-
- A shared session's worker: any attached TUI or web page. With none attached,
|
|
54
|
+
- REPL (`chi --no-shared`, with or without `-p`): at the `? ` prompt.
|
|
55
|
+
- A shared session's worker (plain `chi` and `chi -p …` while `session.shared` is on, the default): any attached TUI or web page. With none attached,
|
|
56
56
|
the approval waits (in the session file) and shows on attach.
|
|
57
57
|
- `-p … --non-interactive`: nobody; the call is denied ("No one to approve it
|
|
58
58
|
(non-interactive run)").
|
|
59
|
+
- A parent agent running chi as a sub-agent (`chi send --wait` exits 3 with
|
|
60
|
+
the approval): `chi answer` may deny it, never allow it by default. See
|
|
61
|
+
[Approvals from a parent agent](#approvals-from-a-parent-agent).
|
|
59
62
|
|
|
60
63
|
The model gets one line on a deny. A rule's or hook's deny reads
|
|
61
64
|
`[execute] Error: denied by guardrail (rule git-push, bundle guardrails): git push publishes commits. The user was not asked. Do not retry it or reach the same result another way; ask the user how to proceed.`
|
|
@@ -79,6 +82,66 @@ with one warning, and chi starts with no stored approvals (more asks, nothing lo
|
|
|
79
82
|
`/guardrails` lists the rules and approvals; `/guardrails revoke N` removes one.
|
|
80
83
|
The file tools can't write the store.
|
|
81
84
|
|
|
85
|
+
### Approvals from a parent agent
|
|
86
|
+
|
|
87
|
+
When another agent (Claude Code, Codex, a script) runs chi with
|
|
88
|
+
`chi send --wait`, an approval comes back to it as exit 3 with `kind:
|
|
89
|
+
approval`, and `chi answer` answers it. A rule with `verdict: ask` means "ask
|
|
90
|
+
me", so by default a parent may only deny: `--option Deny --text "why"`,
|
|
91
|
+
`--text` alone, or `--dismiss`. An Allow is refused (exit 1, `allowing a tool
|
|
92
|
+
call is up to the user: deny it (--option Deny --text WHY), and tell your
|
|
93
|
+
user`). The question block chi prints for an approval says the same: its
|
|
94
|
+
command is a deny, with no `--option N` and no `chi --attach`.
|
|
95
|
+
|
|
96
|
+
```yaml
|
|
97
|
+
guardrails:
|
|
98
|
+
parent_approvals: once # default off
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`once` lets `chi answer --option "Allow once"` through, picked by the
|
|
102
|
+
option's scope, not its label; the wider scopes (this session, this repo or
|
|
103
|
+
directory, the rule) stay with the web and the terminal. An approval that
|
|
104
|
+
doesn't offer "once" can't be allowed from a parent, and neither can one whose
|
|
105
|
+
offered scopes are missing or don't fit its options: then only Deny goes.
|
|
106
|
+
|
|
107
|
+
Whatever the setting, a parent can't allow a call on chi's own files: a
|
|
108
|
+
`write`, `edit` or `execute` on the config dir (config.yml, installed bundles'
|
|
109
|
+
rules), the hooks dir or the approval store, by its rule (`chi-config`,
|
|
110
|
+
`chi-hooks`, `shell-touches-chi`), its paths or its command. "Allow once" on a
|
|
111
|
+
config.yml rewrite would allow everything from then on, so those stay with the
|
|
112
|
+
user: `chi answer` and the worker refuse the allow with `only the user can
|
|
113
|
+
allow it`.
|
|
114
|
+
|
|
115
|
+
The guardrails settings (`enabled`, `small_models`, `parent_approvals`) are
|
|
116
|
+
read from config.yml only: they have no environment variable, and a worker
|
|
117
|
+
chi starts unsets every `SAMAGOTCHI_GUARDRAILS_*` it would inherit. What a
|
|
118
|
+
parent's environment does choose is the config dir: `XDG_CONFIG_HOME` picks
|
|
119
|
+
which config.yml chi reads, and a worker the parent starts or wakes (`chi
|
|
120
|
+
send` to a stopped session) reads the one the parent's `XDG_CONFIG_HOME`
|
|
121
|
+
points at. chi doesn't record the config dir per session.
|
|
122
|
+
|
|
123
|
+
The worker checks it too. `chi answer` marks its answers as a parent's
|
|
124
|
+
(`client_id: "cli:answer"`), and the worker's Bridge refuses such an allow
|
|
125
|
+
beyond what the *worker's* config.yml permits (`403 parent_approval_refused`;
|
|
126
|
+
`chi answer` exits 1 with the reason), checked against the question pending
|
|
127
|
+
at that moment. Answers from the web and an attached terminal keep every
|
|
128
|
+
scope.
|
|
129
|
+
|
|
130
|
+
A parent may also type its answer into chi itself: a piped `chi --attach ID`
|
|
131
|
+
or `printf '2\n' | chi --no-shared -p …`. So chi treats an answer as a
|
|
132
|
+
parent's, held to the same setting, when its input isn't a terminal or an
|
|
133
|
+
agent marker is set: `CLAUDECODE` (Claude Code), `AI_AGENT`, `CODEX_THREAD_ID`
|
|
134
|
+
(Codex CLI), or `SAMAGOTCHI_PARENT_SESSION`,
|
|
135
|
+
which chi's own `execute` and `task_create` export (the session's id) into the
|
|
136
|
+
commands they run. The markers survive a PTY wrapper (`script`, `expect`). A
|
|
137
|
+
person at a terminal with no marker keeps every scope.
|
|
138
|
+
|
|
139
|
+
This guards an honest but eager parent, not a hostile one: it is not a
|
|
140
|
+
security boundary. The worker's Bridge and the web's answer route on localhost
|
|
141
|
+
take an answer from any local process, which can leave the marker out (or
|
|
142
|
+
post through the web) and get every scope. chi's instructions for parents ([chi as a
|
|
143
|
+
sub-agent](sub-agent.md)) tell them to use `chi answer` only.
|
|
144
|
+
|
|
82
145
|
## Protected paths
|
|
83
146
|
|
|
84
147
|
Built in, for `write`, `edit` and `memory_write` (symlinks resolved):
|
|
@@ -93,7 +156,7 @@ commands that name them.
|
|
|
93
156
|
|
|
94
157
|
```yaml
|
|
95
158
|
guardrails:
|
|
96
|
-
enabled: true # false: no rules, and hooks' asks are dropped (a deny still applies)
|
|
159
|
+
enabled: true # false: no rules, and hooks' asks are dropped (a deny still applies); config.yml only
|
|
97
160
|
rules:
|
|
98
161
|
- id: git-push
|
|
99
162
|
tool: shell # execute + task_create; or a tool name, a glob, or a list
|
|
@@ -134,7 +197,7 @@ the rest of chi's config folder still count as outside.
|
|
|
134
197
|
|
|
135
198
|
To switch off single rules (a bundle's, say) without editing its files, list
|
|
136
199
|
them under `disable:`. A plain id switches off every rule with that id; `bundle:id`
|
|
137
|
-
only that bundle's:
|
|
200
|
+
only that bundle's. A single id works without the list (`disable: git-rebase`):
|
|
138
201
|
|
|
139
202
|
```yaml
|
|
140
203
|
guardrails:
|
|
@@ -178,7 +241,7 @@ vote for the current model `off for this model`.
|
|
|
178
241
|
Rules are read again on the next tool call after `config.yml` or an installed
|
|
179
242
|
bundle's rules change, so a long-running worker follows edits without a
|
|
180
243
|
restart. A rule that doesn't parse (an unknown key, a bad regex, no
|
|
181
|
-
verdict, a `disable:` that isn't a list of ids) makes chi **deny every tool call** and say why, rather than run
|
|
244
|
+
verdict, a `disable:` that isn't an id or a list of ids) makes chi **deny every tool call** and say why, rather than run
|
|
182
245
|
without it.
|
|
183
246
|
|
|
184
247
|
## The guardrails bundle
|
|
@@ -191,8 +254,12 @@ installs a default rule set plus a short memory telling the model not to
|
|
|
191
254
|
route around a deny. It asks before `git push`, `reset --hard`, `clean -f`,
|
|
192
255
|
`branch -D`, `rebase`, `filter-branch`/`filter-repo`; `rm -rf` on `/`, `~`,
|
|
193
256
|
`$HOME` or `..` paths; `curl … | sh` and `base64 -d … | sh`; writes outside the
|
|
194
|
-
repo;
|
|
195
|
-
`.git/hooks
|
|
257
|
+
repo; shell commands that name chi's config, hooks or guardrails or
|
|
258
|
+
`.git/hooks`; and answering chi's questions around `chi answer`: `chi --attach`
|
|
259
|
+
or `chi -p` with stdin from a pipe, a here-string or a file
|
|
260
|
+
(`chi-answer-piped`), and `curl`/`wget` to a session's `/answer` route
|
|
261
|
+
(`chi-answer-http`), both once or for the session. `chi answer` itself isn't
|
|
262
|
+
asked about. It denies writes into `.git/hooks`. The rules are in
|
|
196
263
|
`lib/samagotchi/bundles/guardrails/guardrails/rules.yml`.
|
|
197
264
|
|
|
198
265
|
Small models (`models: small`, see above) get two more asks, in
|
data/docs/hooks.md
CHANGED
|
@@ -9,7 +9,8 @@ hooks. See [Plugins](plugins.md).
|
|
|
9
9
|
|
|
10
10
|
## Configuration
|
|
11
11
|
|
|
12
|
-
Add a `hooks:` section to your global config file (
|
|
12
|
+
Add a `hooks:` section to your global config file (`$XDG_CONFIG_HOME/samagotchi/config.yml`, `$XDG_CONFIG_HOME`
|
|
13
|
+
defaulting to `~/.config`):
|
|
13
14
|
|
|
14
15
|
```yaml
|
|
15
16
|
hooks:
|
|
@@ -48,15 +49,15 @@ The plugin class must respond to `#call(event)` — duck-typed, no base class re
|
|
|
48
49
|
|
|
49
50
|
| Event | When it fires | Event payload |
|
|
50
51
|
|-------|--------------|---------------|
|
|
51
|
-
| `:session_start` |
|
|
52
|
+
| `:session_start` | The first turn this chi process runs for the session (so again after `--resume`, a worker that idle-exited and woke, or a restart) | `{ type: :session_start, session_id: "..." }` |
|
|
52
53
|
| `:before_turn` | Before each turn starts | `{ type: :before_turn, session_id: "...", prompt: "..." (nil on a continue), messages: [...] (the history before this turn) }` |
|
|
53
|
-
| `:after_turn` | After a turn completed or was
|
|
54
|
+
| `:after_turn` | After a turn completed or was stopped once the model was asked (not after one that failed, was cut short by an interrupt signal (SIGINT), or was stopped before its `:before_turn` hooks) | `{ type: :after_turn, status: "completed" \| "canceled", present: (see [Presenting the answer](#presenting-the-answer-display-only)), messages: [...] (the conversation the turn stored; a cancelled or empty turn ends it with a `kind: turn_note` system message, and a context line is `kind: context`, see [sessions.md](sessions.md#notes-a-turn-leaves-for-the-model)) }` |
|
|
54
55
|
| `:before_generation` | Before each LLM API call (both loops) | `{ type: :before_generation, iteration: N }` |
|
|
55
56
|
| `:after_generation` | After LLM returns (both loops); not after a generation a hook cut (`stop_generation`) | `{ type: :after_generation, iteration: N, response: "...", messages: [...] (the conversation as sent) }` |
|
|
56
57
|
| `:generation_progress` | While the response streams, in batches (see [Watching the stream](#watching-the-stream)) | `{ type: :generation_progress, iteration: N, thinking: "..." (new since the last fire), text: "..." (new visible text), thinking_chars: N, text_chars: N (this generation so far), elapsed_ms: N }` |
|
|
57
58
|
| `:before_tool_call` | Before tool dispatch (and before `tool_call_started`) | `{ type: :before_tool_call, iteration: N, call: {...}, params: "...", guardrail: Verdict, context: {...}, targets: {...}, blocked: false, block_reason: nil }` |
|
|
58
59
|
| `:after_tool_call` | After tool execution | `{ type: :after_tool_call, iteration: N, tool: "read", output: "..." (capped at max_tool_output_chars), status: "ok" \| "error" \| "blocked" \| "stopped" }` |
|
|
59
|
-
| `:session_end` | After
|
|
60
|
+
| `:session_end` | After each turn `:after_turn` fires for, after it (turn-level lifecycle) | `{ type: :session_end, session_id: "..." }` |
|
|
60
61
|
|
|
61
62
|
Every event also carries the hook runtime (next section): `hook:` (the label
|
|
62
63
|
of the hook about to run) and the callables `notify:`, `ask_user:`,
|
|
@@ -268,7 +269,7 @@ You can also register hooks programmatically during a turn (they are cleared
|
|
|
268
269
|
automatically after each `run_turn`):
|
|
269
270
|
|
|
270
271
|
```ruby
|
|
271
|
-
engine = Samagotchi::Engine.new
|
|
272
|
+
engine = Samagotchi::Engine.new
|
|
272
273
|
engine.register_hook(:before_turn) do |event|
|
|
273
274
|
puts "Turn starting..."
|
|
274
275
|
end
|
|
@@ -404,7 +405,8 @@ stripped of surrounding whitespace, except file text, an edit's
|
|
|
404
405
|
|
|
405
406
|
A `write`'s `content:` is the file text; an `edit` carries `old_text:` and
|
|
406
407
|
`new_text:` (its `content:` is `""`). A plugin or MCP tool's call has its
|
|
407
|
-
arguments whole on `args:` (string keys) instead
|
|
408
|
+
arguments whole on `args:` (string keys) instead, and its `content:` is those
|
|
409
|
+
arguments as JSON.
|
|
408
410
|
|
|
409
411
|
## Bundle Hooks (unified workflow bundle)
|
|
410
412
|
|
|
@@ -439,6 +441,7 @@ hooks:
|
|
|
439
441
|
on_error: log
|
|
440
442
|
priority: 100
|
|
441
443
|
trust_level: reviewed # reviewed | experimental (default)
|
|
444
|
+
requires_chi: ">= 0.12.0" # optional: a gem-style requirement, as for plugins
|
|
442
445
|
needs: [gh] # optional: outside commands the memories use (docs/memory.md#bundles-that-need-outside-commands)
|
|
443
446
|
```
|
|
444
447
|
|
|
@@ -449,7 +452,8 @@ Notes:
|
|
|
449
452
|
- `sha256` is integrity (not authenticity). No signing in v1. Install records the sha256 of the copied file; at `Engine.new` a hook whose file differs is not loaded (reinstall the bundle after editing one by hand).
|
|
450
453
|
- Hook code is the bundle author's source of truth: on upgrade, hooks are overwritten; if the installed file was locally modified, a warning is emitted (`was locally modified; overwriting`).
|
|
451
454
|
- `on_error` defaults to `skip` for every event, `:before_tool_call` included: a raising hook is silent and the call goes ahead. `log` warns. `fail_closed` (only on `:before_tool_call`; on any other event it acts as `skip`) denies the call the hook raised on. Choose `fail_closed` for a hook that is a guardrail, whose failure should stop tool calls rather than let them through.
|
|
452
|
-
-
|
|
455
|
+
- `requires_chi`: when this chi doesn't meet it, none of the bundle's hooks load (each is reported, `not loaded: it requires chi …`), as for a [plugin](plugins.md#loading-and-when-it-fails).
|
|
456
|
+
- A `fail_closed` `:before_tool_call` hook is required: if it is missing, fails to load, its sha256 differs or chi doesn't meet the bundle's `requires_chi`, chi denies every tool call until it is fixed.
|
|
453
457
|
- A bundle can also ship YAML rules in `guardrails/*.yml`; see [Guardrails](guardrails.md#the-guardrails-bundle).
|
|
454
458
|
- Ordering: bundle hooks fire by `(priority, bundle_name, hook_name)` (lower priority first), then plain `config.yml` hooks in registration order.
|
|
455
459
|
- Settings: a hook class with `initialize(settings = {})` gets the bundle's section of `config.yml` `bundles:` (see [Settings](#settings)).
|
|
@@ -459,8 +463,8 @@ Notes:
|
|
|
459
463
|
|
|
460
464
|
Lifecycle:
|
|
461
465
|
|
|
462
|
-
- `chi bundle install <source>` copies `hooks/*.rb` to
|
|
463
|
-
- `Engine.new` loads `config.yml` hooks first, then bundle hooks via `Provenance.
|
|
466
|
+
- `chi bundle install <source>` copies the hook files its manifest's `hooks:` map lists (only those; with no `hooks:` map, every `hooks/*.rb`) to `$XDG_CONFIG_HOME/samagotchi/memories/.bundles/<name>/hooks/` (`$XDG_CONFIG_HOME` defaults to `~/.config`) and persists metadata + `trust_level` + `source_commit` (git HEAD) to provenance.
|
|
467
|
+
- `Engine.new` loads `config.yml` hooks first, then bundle hooks via `MemoryBundle::Provenance.each_installed(holding: :hooks)` → `Hooks::BundleLoader.load`. Bundle hooks are process-scoped (they survive the per-turn `clear_hooks`; only plain hooks are cleared). Experimental bundles emit a one-line startup warning.
|
|
464
468
|
- `chi bundle status`, `diff`, `uninstall`, `build` are hook-aware (counts, metadata, removal).
|
|
465
469
|
|
|
466
470
|
## The source-links bundle
|
data/docs/memory.md
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Samagotchi stores memories in two scopes:
|
|
4
4
|
|
|
5
|
-
- Project scope:
|
|
5
|
+
- Project scope: `$XDG_CONFIG_HOME/samagotchi/memories/projects/<name>_<hash>/`, one folder per
|
|
6
6
|
git repository (named and hashed by its root), shared by all its worktrees and
|
|
7
7
|
subdirectories; outside a repository, one per working directory
|
|
8
|
-
- System scope:
|
|
8
|
+
- System scope: `$XDG_CONFIG_HOME/samagotchi/memories`
|
|
9
|
+
|
|
10
|
+
`$XDG_CONFIG_HOME` falls back to `~/.config` when unset.
|
|
9
11
|
|
|
10
12
|
Tool behavior:
|
|
11
13
|
|
|
@@ -19,7 +21,7 @@ Tool behavior:
|
|
|
19
21
|
|
|
20
22
|
Each memory entry may have a companion file named `<name>.<model-key>.md` in the same scope directory. When the entry is read under a matching model, the overlay body is appended automatically, separated by the standard `---` separator with a `Model-specific guidance (<key>):` header.
|
|
21
23
|
|
|
22
|
-
- **Key derivation**: The harness normalizes the
|
|
24
|
+
- **Key derivation**: The harness normalizes the model name sent to the server, without its host prefix (`box:qwen3.6-35b-a3b` keys as `qwen3.6-35b-a3b`; an alias keys as its target, and the alias's own key is read when the target has no overlay) (lowercase, replace non-alphanumeric with `-`, squeeze dashes) to derive the file key. For example, `qwen3.6-35b-a3b` → `qwen3-6-35b-a3b`.
|
|
23
25
|
- **Saving overlays**: Pass `current_model_only: true` to `memory_write` (the harness resolves the model key automatically). This writes the content as `<name>.<model-key>.md` and skips index maintenance.
|
|
24
26
|
- **Dormancy**: Overlays are only active under the matching model key; other models see the base entry only.
|
|
25
27
|
- **Invariant**: The base entry is the contract. Overlays only add model-specific guidance and never contradict the base protocol.
|
data/docs/plugins.md
CHANGED
|
@@ -24,7 +24,7 @@ version: 1.0.0
|
|
|
24
24
|
plugin:
|
|
25
25
|
file: plugin.rb
|
|
26
26
|
sha256: sha256:6a1a7022… # shasum -a 256 plugin.rb
|
|
27
|
-
requires_chi: ">= 0.1.28" # optional: a gem-style requirement (">= 0.1.28, < 0.2")
|
|
27
|
+
requires_chi: ">= 0.1.28" # optional: a gem-style requirement (">= 0.1.28, < 0.2"); the bundle's hooks/ check it too
|
|
28
28
|
needs: [gh] # optional: outside commands it relies on (see memory.md#bundles-that-need-outside-commands)
|
|
29
29
|
```
|
|
30
30
|
|
|
@@ -73,7 +73,7 @@ Settings work as they do for hooks ([hooks.md](hooks.md#settings)). An
|
|
|
73
73
|
|
|
74
74
|
### `chi.command(name, description, anytime: false) { |args, ctx| … }`
|
|
75
75
|
|
|
76
|
-
This adds a slash command. `name` is `/name
|
|
76
|
+
This adds a slash command. `name` is `/name`: a lowercase letter first, then a–z, 0–9, `_` and `-`, at most 32 characters after the `/`.
|
|
77
77
|
`args` is the text after the name, stripped, or `""` when there is none. The
|
|
78
78
|
block returns the text to show (a String), or nil to show nothing. If the
|
|
79
79
|
block raises, the user sees `/name: <error>`.
|
|
@@ -544,12 +544,16 @@ completed turn stays in its step.
|
|
|
544
544
|
An [anytime command](#anytime-true)'s cards show as it shows them, after its
|
|
545
545
|
line, in every UI, whether a turn runs or not.
|
|
546
546
|
|
|
547
|
-
A worker keeps its last 20 cards
|
|
548
|
-
|
|
547
|
+
A worker keeps its last 20 cards (questions included) and, apart, its
|
|
548
|
+
last 20 hook notices, for a UI that joins later; a question still waiting
|
|
549
|
+
for its answer is always kept. The web shows them where they arrived after a reload (a turn's
|
|
549
550
|
notice as a row of its step, above the call it came before); the attached
|
|
550
551
|
TUI shows the cards and between-turns notices since the last turn when it
|
|
551
|
-
joins.
|
|
552
|
-
|
|
552
|
+
joins. The worker saves them in the session's folder (`cards.json`, the same
|
|
553
|
+
caps), so they outlive it: the web shows them in place for a stopped
|
|
554
|
+
session, and the next worker (an idle exit, a restart, `chi sessions stop`
|
|
555
|
+
then a new turn) starts from them. Load warnings and a question still
|
|
556
|
+
waiting when the worker went are not saved.
|
|
553
557
|
|
|
554
558
|
The event is `{type: :card, id:, source:, title:, body:, level:, actions:,
|
|
555
559
|
in_turn:}` (`source` is the bundle), logged as `card` with its source, id and
|
|
@@ -638,8 +642,8 @@ only against this API (`lib/samagotchi/bundles/btw/plugin.rb`).
|
|
|
638
642
|
0.1.0 shipped `btw.md` as one, and `chi bundle upgrade btw` leaves that file
|
|
639
643
|
(and its index line) behind. Drop it with `chi bundle uninstall btw`, then
|
|
640
644
|
`chi bundle install btw`. After an upgrade already ran, delete
|
|
641
|
-
|
|
642
|
-
there by hand.
|
|
645
|
+
`$XDG_CONFIG_HOME/samagotchi/memories/btw.md` (`$XDG_CONFIG_HOME` defaults to
|
|
646
|
+
`~/.config`) and its `**btw**` line in `index.md` there by hand.
|
|
643
647
|
|
|
644
648
|
## The mcp bundle
|
|
645
649
|
|
|
@@ -690,6 +694,9 @@ bundles:
|
|
|
690
694
|
background task lists the tools again with a server of its own (then
|
|
691
695
|
stops it), saves them, and replaces the tools if they changed. One worker
|
|
692
696
|
does it at a time.
|
|
697
|
+
- **A server that says its tools changed** (`notifications/tools/list_changed`)
|
|
698
|
+
is asked for its `tools/list` again; the saved list and the tools are
|
|
699
|
+
replaced from the next turn on.
|
|
693
700
|
- **A cached server that doesn't start** (the command is gone, it crashes)
|
|
694
701
|
fails that call with `Error: MCP server x didn't start: …` and one notice;
|
|
695
702
|
later calls answer the same at once, and its tools are left out from the
|
|
@@ -722,15 +729,20 @@ bundles:
|
|
|
722
729
|
([Returning images](#returning-images): at most 4 per call, a line
|
|
723
730
|
instead when the model can't see images). Its place in the text is a line,
|
|
724
731
|
`[image 1: image/png, attached]`, so the model knows the order. A text
|
|
725
|
-
block that is **
|
|
726
|
-
(`[image 1: screenshot.png, attached]` after the
|
|
727
|
-
|
|
728
|
-
|
|
732
|
+
block that is **the absolute path of an image file** is attached too
|
|
733
|
+
(`[image 1: screenshot.png, attached]` after the text), and so is an
|
|
734
|
+
absolute path ending in `.png`, `.jpg`, `.jpeg`, `.gif` or `.webp` inside
|
|
735
|
+
a sentence (`Saved screenshot to /tmp/shot.png.`; such a path can't hold a
|
|
736
|
+
space). Either way only when the file is an image by its bytes and is
|
|
737
|
+
under the system temp dir or the server's `cwd`: a server's text can't
|
|
738
|
+
pull in any image on disk. `chrome-devtools-mcp --slim` answers
|
|
729
739
|
`screenshot` that way; without `--slim`, `take_screenshot` returns an image
|
|
730
740
|
block. `attach_image_paths: false` on a server leaves such paths as text.
|
|
731
|
-
- **A server that exits** fails
|
|
732
|
-
running (…)`, and there is one notice.
|
|
733
|
-
|
|
741
|
+
- **A server that exits** fails the call it was on with `Error: MCP server
|
|
742
|
+
x is not running (…)`, and there is one notice. Its next call starts it
|
|
743
|
+
again (its tools stay), up to 3 times a session; after the third restart
|
|
744
|
+
the notice says so, and its calls fail at once until chi restarts (a new
|
|
745
|
+
session, or the worker's next start).
|
|
734
746
|
- **Stop.** The servers stop with chi ([Shutdown](#shutdown)): stdin is
|
|
735
747
|
closed, then TERM and KILL go to the server's process group.
|
|
736
748
|
- **`/mcp`** (anytime) shows a card with the servers, their state (cached
|
|
@@ -923,7 +935,10 @@ bundles:
|
|
|
923
935
|
and what is left, not "how's it going?": a small model tends to answer that
|
|
924
936
|
with a line and carry on unchanged.
|
|
925
937
|
|
|
926
|
-
`/checkin` (anytime) for this session
|
|
938
|
+
`/checkin` (anytime) for this session. What it changes (on/off, the mode,
|
|
939
|
+
the threshold) is saved with the session, in the bundle's data dir
|
|
940
|
+
(`plugins/check-in/sessions/<id>.json`), so it outlasts a worker's restart;
|
|
941
|
+
another session starts from config.yml:
|
|
927
942
|
|
|
928
943
|
| | |
|
|
929
944
|
|---|---|
|
data/docs/sessions.md
CHANGED
|
@@ -25,7 +25,7 @@ A session is deleted if **expired by age OR overflow by count** (unless `keep_st
|
|
|
25
25
|
|
|
26
26
|
<a id="archiving-a-session"></a>**Archiving a session:** `chi sessions archive ID...`, `/archive` in a terminal and the Web UI's `archive` go through `SessionManager.archive_session`: it writes `<id>/archived` (`{"archived_at": …}`; not a session.json field, which every save rewrites) for the session and its delegated children. `Session.list` leaves archived sessions out unless `include_archived:`, and everything built on it follows: `chi sessions list` (`--archived` shows them, `[archived]`), the summaries (`--live`, json/tsv, the desktop panel, the agent's `list_sessions`) and the retention prune, which neither deletes nor counts them against `max_count`. `children_of` (max_children, `delegate_result`) still sees them, and `--resume`/`--attach ID`, `chi send`/`chi note ID` reach them as before. Refused: a turn running in the session or in one of its children (naming the child; 409 `busy` on the web), a plain REPL owning it (`owned_by_tui`), a `chi scratch` session (`scratch`). A live idle worker is stopped first, and the marker is written even if it is still shutting down. Input a human typed brings a session back (`{"unarchived_at": …}`): a prompt from the web, an attached or plain terminal or `chi send` (origin `web:*`, `tui:*`, `cli:send`, none), steering merged into a running turn, a `/continue` answer or a question answered; a delegate's follow-up, a plugin's turn, a reminder, the idle recap, a note and just opening it don't. A child's input brings only the child back. The retention sweep ages an unarchived session from when it was unarchived, so unarchiving an old session doesn't let the next sweep take it. `/archive` leaves the session first, as `/exit` does, then archives it: in an attached terminal the worker is asked to exit (one that stays up for another UI is stopped by the archive; a running turn refuses it), and an empty session is discarded instead; `chi scratch` refuses it. `chi sessions unarchive ID...` and the web's `unarchive` bring back the whole family. Web routes: `POST /api/sessions/:id/archive` (200 `{status, session_id, archived, stopped, discarded}`, ids) and `POST /api/sessions/:id/unarchive` (200 `{status, session_id, unarchived}`). The session hub and `GET /api/sessions` keep archived sessions, `archived: true` in their summaries: the page leaves them out of the strip, its count and the list at render, and "include archived" by the all-sessions search shows them, dimmed with an `archived` badge. The info bar reads `archive · stop · delete` (`unarchive` on an archived one); archiving the open session stays on it, with an Undo toast.
|
|
27
27
|
|
|
28
|
-
**Lazy sweep:** automatic prune runs at most once per
|
|
28
|
+
**Lazy sweep:** automatic prune runs only while `chi web` runs, at most once per `session.sweep_interval_hours` (default 24): the web's session hub gives it a chance on its periodic full scan, and `GET /api/sessions` does too. No cron, and a terminal-only chi never sweeps. Manual prune is always available.
|
|
29
29
|
|
|
30
30
|
**CLI:**
|
|
31
31
|
|
|
@@ -40,6 +40,8 @@ chi sessions prune [--dry-run] [--days N] [--keep N] [--keep-status running,...]
|
|
|
40
40
|
chi sessions clean [--dry-run] [--days N] # test sessions: all, or older than N days
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
A session whose worker waits for an answer (a question, a hook's question or a guardrail approval) reads `waiting` in the status column, and json has `waiting: "question"|"approval"|"hook"` and `waiting_id`, the question's id for `chi answer --question` (both null otherwise). Only while a worker runs it: a question a dead worker left in the file shows the saved status. Answer it with `chi answer`, the web or `chi --attach ID`; `chi sessions stop ID` frees the worker instead.
|
|
44
|
+
|
|
43
45
|
Examples:
|
|
44
46
|
|
|
45
47
|
```sh
|
|
@@ -146,7 +148,7 @@ pbpaste | chi send 3fa2 # the clipboard is the messa
|
|
|
146
148
|
- It goes through the same path as the web composer (`SessionManager.deliver_turn`): the worker's Bridge when it is up, so every attached UI shows it as your message (client id `cli:send`, shown like any user message); the input file when the worker is on its way out. During a running turn it is merged into that turn at the next step, like a message typed then.
|
|
147
149
|
- A session with no worker gets one started (like `--attach` or the web composer). A session open in a plain REPL (`--no-shared`) refuses it. Only sessions on this machine: Bridges listen on 127.0.0.1.
|
|
148
150
|
- Fire and forget: it returns once the message is queued and never prints the answer; that shows in whatever is attached. A guardrail "ask" waits for a UI to answer it, so with nothing attached the turn stalls there until one attaches (`chi --attach ID`).
|
|
149
|
-
- `--image PATH` (repeatable, up to 20) sends images with the message, as the web composer's chips do. Each file is read once before anything is sent (bmp, tiff and heic converted to png, large ones downscaled, as for `@path`); a missing file or one that isn't an image
|
|
151
|
+
- `--image PATH` (repeatable, up to 20) sends images with the message, as the web composer's chips do. Each file is read once before anything is sent (bmp, tiff and heic converted to png, large ones downscaled, as for `@path`); a missing file or one that isn't an image stops the send (exit 1; with `--wait --format json`, an `error` line) and nothing is sent; more than 20 is a usage error (exit 2). Each session gets its own copy in `<session>/images/`. Text is still required (`-m` or stdin; context alone counts), also with `--wait`. The session's model must see images: one known not to (the host's model list says text-only, `vision: false` in `models:` or `hosts:`, a native llama.cpp without a vision model) is refused before anything is sent, with the reason (`<id> refused: gemma can't take images (…); send text only or switch the model (/model)`); the other sessions of the same send still get it, and the exit is 1. With `--new` nothing is started. When chi can't tell (a native host whose `/props` doesn't answer, a list that doesn't say), the message is sent as before and a text-only model fails the turn in the session. A running turn doesn't take images mid-turn: the message runs as the next turn, and the line says `(runs after the current turn)`. With `--new` the session starts idle with the message as its preview, then the message goes in as its first turn once its worker is up (`<id> started with 1 image`); if the worker doesn't come up in 5 s the session is kept, with its id on the `failed:` line.
|
|
150
152
|
- One line per session: `sent`, `sent with 2 images`, `sent (the running turn picks it up)`, `sent (started its worker)`, `refused: …` or `failed: …`. Exit 0 when all were sent, 1 when any was refused, failed or not found, 2 for a usage error. There is no `--all`.
|
|
151
153
|
|
|
152
154
|
The Automator action above works for messages too: swap its last line for `pbpaste | "$chi" send -m "what do you make of this?" $(print -r -- "$picked" | cut -f1)`.
|
|
@@ -166,10 +168,11 @@ chi send --wait 3fa2 # sends nothing: waits fo
|
|
|
166
168
|
- `--new` takes no ids (one new session per call). `--dir DIR` is its folder, and so its project (default the current one); `--model M` its model (default the configured one; the name isn't checked up front, a wrong one fails the worker's first turn).
|
|
167
169
|
- `--new` prints `<full id> started` on stdout. With `--wait` that line (and the `sent` line for an existing session) goes to stderr, so stdout is exactly the answer: the reply of the turn the message started, in full. It is the worker's `output/` file, the same one `delegate` reads; a turn that ends with only tool calls and no text has none.
|
|
168
170
|
- `--wait` takes one session: `--new` or one id. A session with a running turn is refused (`busy: a turn is running; wait or attach`, exit 1): the message would run after it, and its reply would come back as the answer.
|
|
169
|
-
- Exit codes with `--wait`: 0 answered; 3 the turn waits for an answer from you (a question or a guardrail approval),
|
|
170
|
-
- `--
|
|
171
|
+
- Exit codes with `--wait`: 0 answered; 3 the turn waits for an answer from you (a question, a hook's question or a guardrail approval), and the session keeps waiting: stderr gets the whole question (`waiting for an answer (approval): …`, its text, the numbered options, whether free text is allowed) and how to answer it (`chi answer ID --question QID --option N`, or `chi --attach ID`, the web); 4 `--timeout S` passed with the turn still running (it goes on; wait again); 1 the turn ended without a reply (canceled, failed or empty), the worker failed or vanished, or the session was stopped; 130 on Ctrl-C, which leaves the turn running (`still running: chi --attach ID`). There is no default timeout.
|
|
172
|
+
- `--format json` (with `--wait`) prints one JSON object on stdout however the wait ends, for a script or a parent agent (see [chi as a sub-agent](sub-agent.md)): `{"status":"answered","session_id":…,"text":…}`; `{"status":"question",…,"question":{"id","kind","header","text","options","multi_select","allow_freeform","approval"},"answer_with":"chi answer ID --question QID --option N"}`, where `kind` is `question` (the model's), `hook` or `approval` (with the tool, command, cwd, rule, reason and offered scopes); `{"status":"running",…,"detail":…}` (exit 4, and on Ctrl-C); and `failed`, `canceled`, `no_answer`, `error`, `worker_gone` or `stopped` with a `detail` line (exit 1). A failure before the wait (no such session, a busy one) is `error` too. A usage error (exit 2) prints no JSON.
|
|
173
|
+
- `--wait ID` with no message (no `-m`, nothing piped) sends nothing: it waits for the session's next reply and prints and exits as above. A running session is fine (that is the point), so after exit 3, 4 or 130 an agent waits again this way while the user answers in the web or `chi --attach`; the question pending when it starts doesn't count again. An idle session with no worker waits until something wakes one. With `--new`, or two ids, it is a usage error (exit 2).
|
|
171
174
|
- The 16 KiB cap applies: `git diff | chi send --new …` on a big diff is refused; name the branch in the message instead and let the session read it.
|
|
172
|
-
- A session nobody attaches to stalls at its first guardrail ask until someone
|
|
175
|
+
- A session nobody attaches to stalls at its first question or guardrail ask until someone answers it (`chi answer`, the web, `chi --attach`), and its worker stays up meanwhile: it doesn't idle-exit while a question waits (`chi sessions list` shows it as `waiting`; `chi sessions stop ID` frees it). Otherwise it idle-exits after `session.idle_exit_minutes` like any worker. These are ordinary sessions: delete them like any other.
|
|
173
176
|
|
|
174
177
|
## Delegating
|
|
175
178
|
|
|
@@ -177,7 +180,7 @@ The `delegate` tool hands a task to a **child session**: an ordinary chi session
|
|
|
177
180
|
|
|
178
181
|
- `delegate(task, model:, session:, wait:, timeout:)`: starts a child and, by default, waits for its reply (`wait: false` returns at once, so several children can run side by side; `timeout`, default 600 s, leaves a slow child running; 0 checks once without waiting). With `session:` (a child's id or prefix) the task goes to that child as a follow-up instead, delivered like `chi send` (client id `delegate:<parent short id>`, shown as `delegate>` in the child's UIs); only this session's children take one.
|
|
179
182
|
- `delegate_result(session:, timeout:)`: waits for a child's next reply: the one named, or the newest running child (`timeout` as for `delegate`: 0 checks once and returns at once). The reply comes back once; a later call waits for the next one.
|
|
180
|
-
- What a wait returns, `session: <id>` then `status:` and the text: `
|
|
183
|
+
- What a wait returns, `session: <id>` then `status:` and the text, with the status words `chi send --wait --format json` uses: `answered` with the reply after a `---` line; `question` as soon as the child opens a question or a guardrail approval, with the question's text, options and the `chi answer` command for it (nothing is answered for it: `chi answer`, attach or use the web, then wait again); `failed`, `canceled` or `no_answer` when the child's turn ended with nothing to say (its session shows what happened); `error` (its worker crashed) or `stopped` (`chi sessions stop`) at once; `worker_gone` when the child has had no worker for 15 seconds (it crashed without saying so, or idle-exited): `delegate` with `session:` starts it again; `running` on timeout, or when the parent's own turn is canceled (the child keeps running; the next turn can wait again). A reply saved before the child's worker idle-exited is returned at once.
|
|
181
184
|
- Every child starts with the shipped `delegated` system memory preloaded (`preloaded_memory_names: ["system/delegated"]`; `--mute delegated` would hide it): put everything in the final reply, don't delegate further, don't write memories or change config, stay in the folder. Its system prompt also says `Delegated by session <parent id>`. The parent's own `--mute` list does not carry over.
|
|
182
185
|
- Limits: a child cannot `delegate` (depth 1; the tool refuses), and one session may have at most `session.max_children` (default `4`, env `SAMAGOTCHI_SESSION_MAX_CHILDREN`) children running at a time, counted from the session files (a finished or idle-exited child does not count; one whose worker is still starting does, for 15 s). A refused call creates no session. Nothing stops children with the parent: `chi sessions stop ID` does.
|
|
183
186
|
- The link is the session's `parent_id`, written before the worker starts, so a respawn keeps it; `GET /api/sessions` and `/api/sessions/:id` carry it, and `SessionManager.children_of` lists a parent's children.
|
data/docs/sub-agent.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# chi as a sub-agent
|
|
2
|
+
|
|
3
|
+
Another agent (Claude Code, Codex, a script) can hand a task to chi and get the
|
|
4
|
+
answer back, the way you would from a terminal: `chi send --new --wait` starts a
|
|
5
|
+
session in a background worker and prints its reply. The session is an
|
|
6
|
+
ordinary one, so you (the human) can watch it live in `chi web` or join it with
|
|
7
|
+
`chi --attach ID`.
|
|
8
|
+
|
|
9
|
+
When chi's model asks a question (`ask_user_question`), a hook asks one, or a
|
|
10
|
+
guardrail wants an approval, chi stops and hands the question up: the wait ends
|
|
11
|
+
with exit 3 and the question, its options and the command that answers it. The
|
|
12
|
+
worker keeps the turn paused. The parent answers with `chi answer`, from what it
|
|
13
|
+
knows or after asking its own user, and chi goes on from there. The model gets
|
|
14
|
+
the answer as its tool result, as if you had answered in the web.
|
|
15
|
+
|
|
16
|
+
## The commands
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
chi send --new --wait --format json [--dir D] [--model M] [--timeout S] -m "task"
|
|
20
|
+
chi send --wait --format json [--timeout S] [-m "follow-up"] ID
|
|
21
|
+
chi answer ID --question QID (--option N|LABEL)... [--text T] [--timeout S] [--format json]
|
|
22
|
+
chi answer ID --question QID --dismiss [--timeout S] [--format json]
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`chi answer` waits after answering, the same way `chi send --wait` does, so the
|
|
26
|
+
next question comes back as exit 3 again. `--option` is 1-based or the label,
|
|
27
|
+
repeated on a multi-select question. `--text` is free text (where the question
|
|
28
|
+
allows it) or the reason for a Deny. `--dismiss` leaves the question
|
|
29
|
+
unanswered: the model doesn't do what it asked about and finishes its reply.
|
|
30
|
+
|
|
31
|
+
Exit codes, both commands:
|
|
32
|
+
|
|
33
|
+
| Exit | `status` | Meaning |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| 0 | `answered` | `text` is chi's reply |
|
|
36
|
+
| 3 | `question` | chi waits for an answer: `question`, `answer_with` |
|
|
37
|
+
| 4 | `running` | `--timeout` passed; the turn goes on, wait again |
|
|
38
|
+
| 1 | `failed`, `canceled`, `no_answer`, `error`, `worker_gone`, `stopped` | `detail` says what happened |
|
|
39
|
+
| 2 | (no JSON) | usage error, or an option the question doesn't offer |
|
|
40
|
+
| 130 | `running` | Ctrl-C; the turn goes on |
|
|
41
|
+
|
|
42
|
+
With `--format json` stdout is one JSON object (see
|
|
43
|
+
[Starting a session](sessions.md#starting-a-session) for every field):
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{"status":"question","session_id":"297da360-…","question":{"id":"3f1c…","kind":"question",
|
|
47
|
+
"text":"Which file should I read?","options":["README.md","NOTES.md"],"multi_select":false,
|
|
48
|
+
"allow_freeform":false},"answer_with":"chi answer 297da360-… --question 3f1c… --option N"}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`kind` is `question` (the model's), `hook`, or `approval`, which adds the tool,
|
|
52
|
+
command, folder, rule and reason.
|
|
53
|
+
|
|
54
|
+
## Approvals
|
|
55
|
+
|
|
56
|
+
An approval is a guardrail rule with `verdict: ask`: its user said "ask me". So
|
|
57
|
+
a parent may deny it (`--option Deny --text "why"`, `--text` alone, or
|
|
58
|
+
`--dismiss`) but not allow it: `chi answer` refuses an Allow with exit 1 and
|
|
59
|
+
`allowing a tool call is up to the user: deny it (--option Deny --text WHY),
|
|
60
|
+
and tell your user`. For an approval, the question block and `answer_with` give
|
|
61
|
+
the deny command, not `--option N` or `chi --attach`. With
|
|
62
|
+
`guardrails.parent_approvals: once` in chi's config.yml (it has no environment
|
|
63
|
+
variable), "Allow once" goes through; the wider scopes never do,
|
|
64
|
+
and neither does any allow on chi's own config, hooks or guardrail rules. See
|
|
65
|
+
[Guardrails](guardrails.md#approvals-from-a-parent-agent).
|
|
66
|
+
|
|
67
|
+
An answer typed into `chi --attach` or the REPL counts as a parent's too when
|
|
68
|
+
stdin isn't a terminal or an agent marker (`CLAUDECODE`, `AI_AGENT`,
|
|
69
|
+
`CODEX_THREAD_ID`, `SAMAGOTCHI_PARENT_SESSION`) is set.
|
|
70
|
+
|
|
71
|
+
The worker checks it again with its own config.yml. The guardrails settings
|
|
72
|
+
have no environment variables, and a worker drops any `SAMAGOTCHI_GUARDRAILS_*`
|
|
73
|
+
it inherits; but a parent that sets `XDG_CONFIG_HOME` picks which config.yml a
|
|
74
|
+
worker it starts or wakes reads. Still, this guards a parent that follows its instructions,
|
|
75
|
+
not a security boundary: the worker's Bridge and `chi web` on localhost take
|
|
76
|
+
answers from any local process.
|
|
77
|
+
|
|
78
|
+
## Things to know
|
|
79
|
+
|
|
80
|
+
- A question waits for as long as it takes: the worker doesn't idle-exit while
|
|
81
|
+
one is open. `chi sessions list` shows such sessions as `waiting`
|
|
82
|
+
(`--format json`: `waiting_id` is the question's id);
|
|
83
|
+
`chi sessions stop ID` ends one.
|
|
84
|
+
- If the worker dies while a question waits, the question goes with it.
|
|
85
|
+
`chi answer` then exits 1 and says to send the task again
|
|
86
|
+
(`chi send --wait -m "…" ID`); a wait ends with `worker_gone` (exit 1).
|
|
87
|
+
- A follow-up (`chi send --wait -m "…" ID`) to a session waiting for an answer
|
|
88
|
+
isn't sent: it exits 1 with the `chi answer` command for the question.
|
|
89
|
+
- An answer to a question that is no longer open (you answered it in the web
|
|
90
|
+
first) isn't sent; `chi answer` says so and waits for what comes next.
|
|
91
|
+
- `chi send --wait` and `chi answer` don't read stdin unless it's a pipe or a
|
|
92
|
+
file. Agent shells usually pass `/dev/null`, which is fine.
|
|
93
|
+
|
|
94
|
+
## Instructions for the parent agent
|
|
95
|
+
|
|
96
|
+
Paste this into the parent's `CLAUDE.md` or `AGENTS.md`:
|
|
97
|
+
|
|
98
|
+
```markdown
|
|
99
|
+
## Using chi as a sub-agent
|
|
100
|
+
|
|
101
|
+
Start a task: `chi send --new --wait --format json --timeout 500 -m "<task>"`
|
|
102
|
+
(add `--dir <project>`). Read the JSON on stdout:
|
|
103
|
+
|
|
104
|
+
- `status: answered` (exit 0): `text` is chi's reply.
|
|
105
|
+
- `status: question` (exit 3): chi is paused, waiting for an answer. If the answer
|
|
106
|
+
is clear from your task and context, answer it yourself. Otherwise ask your
|
|
107
|
+
user, showing them the question and its options; if you can't ask (no user in
|
|
108
|
+
this run), stop and report the question and options instead of guessing. To
|
|
109
|
+
answer, run the `answer_with` command with `--option N` (1-based, repeat for
|
|
110
|
+
multi-select) and/or `--text "…"`. It returns the same JSON. If you won't
|
|
111
|
+
answer, add `--dismiss` instead: chi does nothing it asked about, finishes
|
|
112
|
+
with what it found, and the reply comes back (exit 0).
|
|
113
|
+
- `question.kind: approval`: chi wants to run something its user's guardrails
|
|
114
|
+
flagged, and allowing it is up to your user. Deny it (the `answer_with`
|
|
115
|
+
command: `--option Deny --text "why"`), and tell your user what it wanted to
|
|
116
|
+
run and why. Never allow it yourself, through `chi answer`, a piped
|
|
117
|
+
`chi --attach` or chi's web API.
|
|
118
|
+
- `status: running` (exit 4): still working. Wait again:
|
|
119
|
+
`chi send --wait --format json --timeout 500 <session_id>`.
|
|
120
|
+
- Anything else (exit 1): report `detail` to your user. `chi --attach <session_id>`
|
|
121
|
+
shows the session.
|
|
122
|
+
|
|
123
|
+
Run each chi command on its own, nothing chained after it: the exit code comes
|
|
124
|
+
back with the output. Talk to chi only through these commands; never call its
|
|
125
|
+
Bridge or web API directly. Follow-ups in the same session:
|
|
126
|
+
`chi send --wait --format json --timeout 500 -m "…" <session_id>`.
|
|
127
|
+
Keep `--timeout` under your shell tool's limit (Claude Code: 600 s per call).
|
|
128
|
+
```
|