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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +116 -1
  3. data/README.md +20 -7
  4. data/bin/chi +9 -9
  5. data/docs/architecture.md +4 -2
  6. data/docs/cli.md +34 -10
  7. data/docs/configuration.md +79 -15
  8. data/docs/desktop.md +10 -6
  9. data/docs/guardrails.md +74 -7
  10. data/docs/hooks.md +13 -9
  11. data/docs/memory.md +5 -3
  12. data/docs/plugins.md +31 -16
  13. data/docs/sessions.md +9 -6
  14. data/docs/sub-agent.md +128 -0
  15. data/lib/samagotchi/answer_command.rb +220 -0
  16. data/lib/samagotchi/bridge/card_store.rb +94 -6
  17. data/lib/samagotchi/bridge.rb +14 -2
  18. data/lib/samagotchi/bridge_client.rb +8 -3
  19. data/lib/samagotchi/bundle_command.rb +18 -10
  20. data/lib/samagotchi/bundles/check-in/manifest.yml +2 -2
  21. data/lib/samagotchi/bundles/check-in/plugin.rb +68 -5
  22. data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +19 -0
  23. data/lib/samagotchi/bundles/guardrails/manifest.yml +2 -2
  24. data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
  25. data/lib/samagotchi/bundles/mcp/plugin.rb +166 -34
  26. data/lib/samagotchi/bundles/system/config_modification_protocol.md +5 -5
  27. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  28. data/lib/samagotchi/cli/command.rb +3 -2
  29. data/lib/samagotchi/cli/exit.rb +23 -0
  30. data/lib/samagotchi/cli/parent_wait.rb +82 -0
  31. data/lib/samagotchi/config.rb +81 -24
  32. data/lib/samagotchi/context_window.rb +30 -5
  33. data/lib/samagotchi/desktop_command.rb +5 -3
  34. data/lib/samagotchi/engine.rb +54 -17
  35. data/lib/samagotchi/guardrails/parent_approvals.rb +158 -0
  36. data/lib/samagotchi/hooks/bundle_loader.rb +10 -3
  37. data/lib/samagotchi/host_registry.rb +31 -7
  38. data/lib/samagotchi/kernel_loop.rb +1 -1
  39. data/lib/samagotchi/llm/chat_loop.rb +5 -3
  40. data/lib/samagotchi/llm/openai_chat.rb +9 -1
  41. data/lib/samagotchi/llm/turn_settings.rb +7 -5
  42. data/lib/samagotchi/memory_bundle/installer.rb +12 -0
  43. data/lib/samagotchi/memory_bundle/status.rb +9 -1
  44. data/lib/samagotchi/memory_paths.rb +8 -0
  45. data/lib/samagotchi/model_profile.rb +30 -16
  46. data/lib/samagotchi/parent_report.rb +213 -0
  47. data/lib/samagotchi/plugin/api.rb +15 -2
  48. data/lib/samagotchi/prompt_literal_guard.rb +4 -2
  49. data/lib/samagotchi/question_desk.rb +29 -2
  50. data/lib/samagotchi/recap_setup.rb +15 -5
  51. data/lib/samagotchi/reply_wait.rb +5 -1
  52. data/lib/samagotchi/send_command.rb +50 -75
  53. data/lib/samagotchi/session.rb +15 -0
  54. data/lib/samagotchi/session_manager.rb +40 -7
  55. data/lib/samagotchi/sessions_command.rb +13 -3
  56. data/lib/samagotchi/system_prompt.rb +11 -3
  57. data/lib/samagotchi/terminal_ui/attach_launcher.rb +33 -5
  58. data/lib/samagotchi/terminal_ui/attached_loop.rb +53 -14
  59. data/lib/samagotchi/terminal_ui.rb +30 -3
  60. data/lib/samagotchi/thought_stream_splitter.rb +4 -4
  61. data/lib/samagotchi/tool_call_parser.rb +5 -6
  62. data/lib/samagotchi/tools/builtin_calls.rb +14 -3
  63. data/lib/samagotchi/tools/builtins.rb +14 -2
  64. data/lib/samagotchi/tools/delegate_wait.rb +32 -13
  65. data/lib/samagotchi/tools/execute.rb +8 -8
  66. data/lib/samagotchi/tools/memory.rb +3 -3
  67. data/lib/samagotchi/tools/task_create.rb +3 -2
  68. data/lib/samagotchi/tools/task_runtime.rb +7 -2
  69. data/lib/samagotchi/utf8_default.rb +29 -0
  70. data/lib/samagotchi/version.rb +1 -1
  71. data/lib/samagotchi/web/app.rb +22 -10
  72. data/lib/samagotchi/web/message_parts.rb +2 -1
  73. data/lib/samagotchi/web/public/activity.js +6 -1
  74. data/lib/samagotchi/web/public/app.js +39 -6
  75. data/lib/samagotchi/web/public/index.html +8 -1
  76. data/lib/samagotchi/web/public/notify.js +16 -3
  77. data/lib/samagotchi/web/public/question_cards.js +3 -1
  78. data/lib/samagotchi/web/public/sessions_list.js +37 -0
  79. data/lib/samagotchi/web/public/turn_view.js +6 -9
  80. data/lib/samagotchi/web/server.rb +2 -1
  81. data/lib/samagotchi/web/session_summary.rb +3 -11
  82. 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`, `-p` without `--non-interactive`): at the `? ` prompt.
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; and shell commands that name chi's config, hooks or guardrails or
195
- `.git/hooks`. It denies writes into `.git/hooks`. The rules are in
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 (`~/.config/samagotchi/config.yml`):
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` | First turn of the session | `{ type: :session_start, session_id: "..." }` |
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 cancelled (not after one that failed) | `{ 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
+ | `: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 every turn (turn-level lifecycle) | `{ type: :session_end, session_id: "..." }` |
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(mode: :assist)
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
- - A `fail_closed` `:before_tool_call` hook is required: if it is missing, fails to load or its sha256 differs, chi denies every tool call until it is fixed.
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 `~/.config/samagotchi/memories/.bundles/<name>/hooks/` and persists metadata + `trust_level` + `source_commit` (git HEAD) to provenance.
463
- - `Engine.new` loads `config.yml` hooks first, then bundle hooks via `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.
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: `~/.config/samagotchi/memories/projects/<name>_<hash>/`, one folder per
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: `~/.config/samagotchi/memories`
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 full model name (lowercase, replace non-alphanumeric with `-`, squeeze dashes) to derive the file key. For example, `qwen3.6-35b-a3b` → `qwen3-6-35b-a3b`.
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` (a–z, 0–9, `_` and `-`).
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 and hook notices, for a UI that joins
548
- later. The web shows them where they arrived after a reload (a turn's
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. They live as long as the worker: an idle exit or a restart forgets
552
- them, and they are not saved with the session.
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
- `~/.config/samagotchi/memories/btw.md` and its `**btw**` line in `index.md`
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 **only the absolute path of an image file** is attached too
726
- (`[image 1: screenshot.png, attached]` after the path), but only when the
727
- file is under the system temp dir or the server's `cwd`: a server's text
728
- can't pull in any image on disk. `chrome-devtools-mcp --slim` answers
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 its calls with `Error: MCP server x is not
732
- running (…)`, and there is one notice. It is not restarted until chi
733
- restarts (a new session, or the worker's next start).
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, until its worker restarts:
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 24h on `GET /api/sessions` (Web). No background thread or cron. Manual prune is always available.
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 is a usage error (exit 2) and nothing is sent. 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.
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), with the line `waiting for an answer: …; open it: chi --attach ID or the web`, and the session keeps waiting; 1 the turn ended without a reply (canceled, failed or empty), the worker failed or vanished, the session was stopped, or `--timeout S` passed; 130 on Ctrl-C, which leaves the turn running (`still running: chi --attach ID`). There is no default timeout.
170
- - `--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 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
+ - 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 opens it; left alone it idle-exits after `session.idle_exit_minutes` like any worker. These are ordinary sessions: delete them like any other.
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: `done` with the reply after a `---` line; `waiting_for_answer` as soon as the child opens a question or a guardrail approval (nothing is answered for it: attach or use the web, then wait again); `canceled` when the parent's own turn is canceled (the child keeps running; the next turn can wait again); `no_reply` when the child's turn ended with nothing to say (canceled, failed or empty: its session shows what happened); `error` (its worker crashed) or `stopped` (`chi sessions stop`) at once; `running` on timeout. A reply saved before the child's worker idle-exited is returned at once.
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
+ ```