samagotchi 0.36.0 → 0.38.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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +63 -1
  3. data/README.md +3 -3
  4. data/bin/chi +1 -1
  5. data/docs/broadcast.md +95 -13
  6. data/docs/configuration.md +9 -2
  7. data/docs/desktop.md +24 -2
  8. data/docs/guardrails.md +17 -2
  9. data/docs/hooks.md +3 -2
  10. data/docs/internals/engine.md +4 -2
  11. data/docs/plugins.md +107 -34
  12. data/lib/samagotchi/broadcast/scope_card.rb +15 -2
  13. data/lib/samagotchi/broadcast/triage.rb +284 -0
  14. data/lib/samagotchi/broadcast/triage_log.rb +170 -0
  15. data/lib/samagotchi/broadcast/triage_model.rb +56 -0
  16. data/lib/samagotchi/broadcast_command.rb +275 -69
  17. data/lib/samagotchi/bundles/mcp/manifest.yml +5 -4
  18. data/lib/samagotchi/bundles/mcp/plugin.rb +439 -92
  19. data/lib/samagotchi/bundles/system/config_modification_protocol.md +3 -2
  20. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  21. data/lib/samagotchi/bundles/system/self_map.md +1 -1
  22. data/lib/samagotchi/cli/command.rb +27 -3
  23. data/lib/samagotchi/config.rb +9 -0
  24. data/lib/samagotchi/context_command.rb +0 -17
  25. data/lib/samagotchi/desktop/macos/App.swift +5 -1
  26. data/lib/samagotchi/desktop/macos/Broadcast.swift +97 -0
  27. data/lib/samagotchi/desktop/macos/ChiRunner.swift +12 -3
  28. data/lib/samagotchi/desktop/macos/Panel.swift +123 -26
  29. data/lib/samagotchi/desktop/macos.rb +27 -5
  30. data/lib/samagotchi/desktop_command.rb +1 -1
  31. data/lib/samagotchi/engine.rb +7 -8
  32. data/lib/samagotchi/guardrail_wiring.rb +7 -2
  33. data/lib/samagotchi/guardrails/approval.rb +5 -2
  34. data/lib/samagotchi/guardrails/approvals.rb +9 -5
  35. data/lib/samagotchi/guardrails/rules.rb +4 -2
  36. data/lib/samagotchi/guardrails/targets.rb +76 -11
  37. data/lib/samagotchi/idle_client.rb +48 -28
  38. data/lib/samagotchi/idle_recap.rb +6 -5
  39. data/lib/samagotchi/idle_target.rb +95 -0
  40. data/lib/samagotchi/kernel_loop.rb +6 -4
  41. data/lib/samagotchi/llm/openai_chat.rb +29 -3
  42. data/lib/samagotchi/log_line.rb +1 -1
  43. data/lib/samagotchi/note_command.rb +0 -21
  44. data/lib/samagotchi/plugin/api.rb +5 -3
  45. data/lib/samagotchi/recap_setup.rb +38 -52
  46. data/lib/samagotchi/send_command.rb +0 -22
  47. data/lib/samagotchi/tool_response.rb +5 -5
  48. data/lib/samagotchi/tool_runner.rb +18 -9
  49. data/lib/samagotchi/update_command.rb +1 -1
  50. data/lib/samagotchi/version.rb +1 -1
  51. data/lib/samagotchi/web/public/question_card.js +4 -3
  52. metadata +6 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1a970676ddf9dacd7bd644f66dcdf28f68a49c6cb24caf2bc785656cb6a2d286
4
- data.tar.gz: 8babf6d8562a213f07c3053b7efde79aae43ba4d51fef8ce16b933d17894acd0
3
+ metadata.gz: 35846da55e0107127da528067ad179a59cbd7a23eee66c1b770a17d1504c717b
4
+ data.tar.gz: e28ce71127da751668251ddfca44dc8b05a97e038f5d03045357f1cdbac26f20
5
5
  SHA512:
6
- metadata.gz: 9727a14cff5153a0a061d9e787aebd0e0ea7494781e63386405b261fb4391d04bef56d4fefa346c9d0f2081d49fe7eddeadc3f6bb1b4ef8edc6478f5d273db6f
7
- data.tar.gz: 3dc9840ef7eba85ea3160bb2ac84412e9be6219c4e9bdbabc89796d2ee677adeb7903d4ef1002d82cae12daefac3ca6545f1f6519466bcfda164e2c126fd699e
6
+ metadata.gz: b2be4346aaaa73f05518a5b62ffcfca820f94ea571b5524d1d8013224ff43ee1648951e40ee4290f80f088da6ffec4c26c4de6c8a6905ccc83340919173fe330
7
+ data.tar.gz: 01027d62cb5f9b423196f386c68fc4cbb46d8c73866540e1aacb9778f3a414013a059c5df99df099d528574cb1a5a5d57b0cb764002de0a4524148fd91ad66e8
data/CHANGELOG.md CHANGED
@@ -8,6 +8,66 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.38.0] - 2026-10-07
12
+
13
+ ### Added
14
+
15
+ - The desktop helper's panel has an **Everyone it concerns** row (⌘B): ⌘⏎ there runs `chi broadcast` with the note
16
+ (your line, then the selection; a first line like `shopfront/checkout` keeps it to that project), instead of a
17
+ note to the sessions you pick. The panel shows "broadcasting…", then the summary line, and stays open; `chi
18
+ broadcast log` has the details. It may take `broadcast.triage_deadline` + 10 s: after changing the deadline, run
19
+ `chi update`. Rebuilt by `chi update`. See [docs/desktop.md](docs/desktop.md#everyone-it-concerns).
20
+
21
+ ### Changed
22
+
23
+ - The mcp bundle (0.6.0) no longer declares every MCP tool to the model. It gets two fixed tools: `find_mcp_tools`
24
+ searches the servers' tools by keywords and answers the best 5 with their schemas, and `mcp_call` calls one as
25
+ `<server>/<tool>`. Requests get smaller (about half on a 30-tool server, in a spike on a local model), and the
26
+ model's tools no longer change when a server starts, refreshes or fails, so the prompt cache keeps; a tool turn
27
+ takes a few more steps (search, then call). A failed call answers the tool's schema, so a call can be fixed on the
28
+ next step. A server's new optional `description:` names it in the search tool, else the first sentence of its
29
+ instructions. Guardrail rules on `mcp_<server>_<tool>` names still match (`mcp_call` acts as that tool), and the
30
+ question names `<server>: <tool>`; approvals stored under the old names don't carry over. Hooks and loop-guard's
31
+ `ignore_tools` see `mcp_call`. `/mcp` lists the tools as `<server>/<tool>` and says what every request carries.
32
+ Needs chi 0.37.0. See [docs/plugins.md](docs/plugins.md#the-mcp-bundle).
33
+
34
+ ### Fixed
35
+
36
+ - `chi broadcast`'s triage ends at most a second after `broadcast.triage_deadline` (requests that ignore the cancel
37
+ could hold it up to ~4 s past it), turns logprobs off for a host only when its 400 was about them, and two broadcasts
38
+ at once no longer both rotate the triage log (one failed, or the old lines were lost). A scope card's folder is right
39
+ under a symlinked path (`/tmp` on macOS read `../../../tmp/…`), and the summary line shortens a reason that quotes a
40
+ setting (`3 unchecked: no triage model: broadcast.triage_host_ref`).
41
+ - `max_tool_output_chars` caps the tool outputs the model gets back on the native loop too (a host without
42
+ `api: openai`): it fed each output whole, so one long result could fill the context. On both loops a cut output now
43
+ ends with `[cut: N of M chars; read it in parts]`, so the model knows it didn't get it all (the chat loop cut it
44
+ silently); the tool row shows the same text.
45
+
46
+ Update with `chi update` (mcp 0.6.0; it also rebuilds the desktop helper for the new row).
47
+
48
+ ## [0.37.0] - 2026-10-07
49
+
50
+ ### Added
51
+
52
+ - For plugin authors: a tool's `targets:` may return `acts_as:` (the tool a call stands for, like an MCP tool behind
53
+ a dispatcher), `args:` (the arguments it acts with) and `label:` (the name the approval question shows). A guardrail
54
+ rule's `tool:` then matches either name, and "allow this call" is keyed by both names and those arguments. It can't
55
+ name one of chi's own tools or another bundle's (dropped and logged). See [docs/plugins.md](docs/plugins.md#guardrails).
56
+ - `chi broadcast` asks a small triage model about the sessions no tag matched, instead of skipping them: it reads the
57
+ note and each session's scope card and answers yes or no ("model: yes (p 0.91)" when the host gives logprobs,
58
+ graded by `broadcast.threshold`). The model is `broadcast.triage_model` (with `triage_host_ref` or
59
+ `triage_base_url`), else the recap's, else `default.model`; a ~9B instruct model or larger works well. Up to
60
+ `broadcast.triage_parallel` (4) requests run at once, all within `broadcast.triage_deadline` (20 s): a session not
61
+ judged by then, or whose request failed, gets the note anyway, and the summary line counts it ("1 unchecked: triage
62
+ deadline"). A first line naming one project ("shopfront/checkout") keeps the note to that project's sessions.
63
+ `--dry-run` runs triage too. See [docs/broadcast.md](docs/broadcast.md#triage).
64
+ - `chi broadcast` prints the broadcast's id and logs each recipient's verdict (`~/.local/state/samagotchi/broadcast/log.jsonl`,
65
+ rotated at 2 MiB). `chi broadcast log [--last N] [--format json]` shows the last broadcasts and who got them, and
66
+ `chi broadcast deliver BROADCAST_ID ID...` gives one to sessions it skipped, after all. See
67
+ [docs/broadcast.md](docs/broadcast.md#the-triage-log).
68
+
69
+ Update with `chi update` (mcp 0.5.1).
70
+
11
71
  ## [0.36.0] - 2026-10-07
12
72
 
13
73
  ### Added
@@ -1782,7 +1842,9 @@ and long-lived sessions.
1782
1842
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
1783
1843
  hotkey panel that send selected text or the clipboard to your sessions.
1784
1844
 
1785
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.36.0...HEAD
1845
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.38.0...HEAD
1846
+ [0.38.0]: https://github.com/dm1try/samagotchi/compare/v0.37.0...v0.38.0
1847
+ [0.37.0]: https://github.com/dm1try/samagotchi/compare/v0.36.0...v0.37.0
1786
1848
  [0.36.0]: https://github.com/dm1try/samagotchi/compare/v0.35.0...v0.36.0
1787
1849
  [0.35.0]: https://github.com/dm1try/samagotchi/compare/v0.34.0...v0.35.0
1788
1850
  [0.34.0]: https://github.com/dm1try/samagotchi/compare/v0.33.0...v0.34.0
data/README.md CHANGED
@@ -120,7 +120,7 @@ chi web --open # web UI: this project's sessions (--s
120
120
  chi web --web-host lan # the web UI on your phone too: scan the QR code it prints
121
121
  chi sessions list # this project's saved sessions (--scope=all: every one)
122
122
  pbpaste | chi note --source slack <id> # background context for a session (no turn)
123
- chi broadcast -m "payments API is down" # the same for every session it concerns (a shared ticket, PR or link)
123
+ chi broadcast -m "payments API is down" # the same for every session it concerns (a shared tag, or a triage model says so)
124
124
  chi context add <PR URL> <id> # attached context: chi keeps it fresh, the agent hears when it changes
125
125
  pbpaste | chi send -m "same bug?" <id> # a message to a session, the clipboard quoted above it
126
126
  chi send --new --wait -m "review feat/x" # a new session you can watch in the web; prints the answer
@@ -179,7 +179,7 @@ only tells you (`context.wake`). See [Attached context](docs/context.md).
179
179
  `chi desktop install` builds a small native helper: select text in any app →
180
180
  Services → **Send to chi** (or ⌃⌥⌘N with the clipboard) → pick sessions in a
181
181
  Spotlight-like panel → ⏎ sends it quoted under your question as a message, ⌘⏎ as a
182
- context note. It needs the
182
+ context note (on the "Everyone it concerns" row, a `chi broadcast`). It needs the
183
183
  Command Line Tools. See [Desktop helper](docs/desktop.md).
184
184
 
185
185
  ### Guardrails
@@ -203,7 +203,7 @@ once, for the session, for the repo, or for the whole rule in the repo.
203
203
  - [Memory](docs/memory.md): scopes and model-specific overlays
204
204
  - [Sessions](docs/sessions.md): storage, retention, `chi sessions`
205
205
  - [Attached context](docs/context.md): `chi context`, sources, scopes, waking, the github-pr bundle, safety
206
- - [Broadcast](docs/broadcast.md): `chi broadcast`, who a note reaches, tags, what a session gets
206
+ - [Broadcast](docs/broadcast.md): `chi broadcast`, who a note reaches, tags, triage, what a session gets, the triage log
207
207
  - [chi as a sub-agent](docs/sub-agent.md): `chi send --wait --format json`, `chi answer`, instructions for a parent agent
208
208
  - [Desktop helper](docs/desktop.md): `chi desktop`, the macOS "Send to chi" Service and hotkey
209
209
  - [Guardrails](docs/guardrails.md): allow / ask / deny for tool calls, rules, approvals
data/bin/chi CHANGED
@@ -97,7 +97,7 @@ parser = OptionParser.new do |opts|
97
97
  chi web [--port PORT] [--open] [--scope=all] the web UI
98
98
  chi sessions <list|stop|archive|unarchive|delete|prune|clean>
99
99
  chi note [--source NAME] [-m TEXT] ID...|--all background context for sessions
100
- chi broadcast [-m TEXT] [--all] [--dry-run] a note for every session it may concern
100
+ chi broadcast [-m TEXT] [--all] [--dry-run] | log | deliver a note for every session it may concern
101
101
  chi context <add|push|ls|show|refresh|rm|mute|unmute> live external text attached to sessions
102
102
  chi send [-m TEXT] ID...|--new a message into sessions, as if typed there
103
103
  chi answer ID --question QID --option N answer a question a session waits on
data/docs/broadcast.md CHANGED
@@ -12,9 +12,13 @@ chi broadcast -m "payments API returns 500 since 14:00 (PAY-123)"
12
12
  pbpaste | chi broadcast # the note from stdin
13
13
  chi broadcast --dry-run -m "…" # who would get it and why; nothing is delivered
14
14
  chi broadcast --all -m "deploy freeze until 18:00" # every recipient, tag or not
15
+ chi broadcast log # the last broadcasts and who got them
16
+ chi broadcast deliver b-7f3a1c9e c0ffee12 # one it skipped, after all
15
17
  ```
16
18
 
17
- To tell one session or a few you picked, use `chi note ID...` instead.
19
+ To tell one session or a few you picked, use `chi note ID...` instead. From
20
+ any app, the desktop helper's **Everyone it concerns** row broadcasts the
21
+ selection: see [Desktop helper](desktop.md#everyone-it-concerns).
18
22
 
19
23
  ## Who it reaches
20
24
 
@@ -47,30 +51,69 @@ so `acme/shop#42` doesn't match `acme/other#42`; `PR #42` with no repo matches
47
51
  pull request 42 of any repo. Words the default ticket pattern would take that
48
52
  are no ticket (`UTF-8`, `SHA-256`, `ISO-8601`, `GPT-4`, …) are left out.
49
53
 
50
- A session that shares no tag is skipped for now, listed as "no tag match (not
51
- checked: no triage yet)": a small triage model that reads the note and each
52
- session's scope card is the next step. Until then, `--all` reaches them all.
54
+ A session that shares no tag goes to triage.
55
+
56
+ ## Triage
57
+
58
+ A small model reads the note and each other recipient's scope card (below)
59
+ and answers yes or no: would the agent working there want to know? A yes
60
+ delivers the note, a no skips the session ("model: no"). `--all` skips tags
61
+ and triage and reaches every recipient.
62
+
63
+ - **The model:** `broadcast.triage_model` (with `broadcast.triage_host_ref` or
64
+ `broadcast.triage_base_url`, as the `recap.*` settings take them); with none
65
+ of them set, the [recap's](sessions.md) model, else `default.model`. A ~9B
66
+ dense instruct model or larger works well (qwen3.5-9b missed no session in
67
+ our tests and sent about one extra note per broadcast of 20 sessions);
68
+ 4B-class models say yes to nearly everything. A local model keeps the
69
+ cards (your prompts, recaps) on your machine; a remote triage host is your
70
+ explicit choice. Thinking is off, and each answer is a word.
71
+ - **A score, when the host gives one:** with logprobs (OpenAI, OpenRouter
72
+ for some models), the verdict is P(yes) of the answer's first token, shown as
73
+ "model: yes (p 0.83)", and a session needs `broadcast.threshold` (0.5). A
74
+ higher threshold drops real "this blocks you" notes first. A host that
75
+ refuses logprobs (llama.cpp's, some gateways) is asked again without them:
76
+ the plain answer counts as 1 or 0.
77
+ - **A scope line:** when the note's first line names exactly one of the
78
+ recipients' projects as a word (`shopfront/checkout`, `infra / ci`), the
79
+ other projects' sessions, and sessions outside any project, are skipped:
80
+ "scope line names shopfront". Anything fuzzier ("for payments folks") is the
81
+ model's job; it sees the line too. A one-line note counts as its own first
82
+ line: with a project named after a common word (`web`, `docs`), "the web
83
+ composer docs moved" names it and keeps the note there.
84
+ - **Fail open:** at most `broadcast.triage_parallel` (4) requests run at a
85
+ time, all within `broadcast.triage_deadline` (20 s; a local server's first
86
+ request after a pause can take 10 s). A session not judged by then, one
87
+ whose request failed, or one the model answered with neither yes nor no
88
+ gets the note anyway, "unchecked: triage deadline", and the summary line
89
+ counts them: a missed session is worse than an extra note. Without a
90
+ triage model to ask (settings that don't resolve) every such session gets
91
+ it unchecked, and chi says why.
53
92
 
54
93
  ## What it prints
55
94
 
56
95
  One line per recipient, the ones that got it first, then a summary:
57
96
 
58
97
  ```
59
- broadcast "payments API returns 500 since 14:00 (PAY-123)"
98
+ broadcast b-7f3a1c9e "payments API returns 500 since 14:00 (PAY-123)"
60
99
  3f2a1c9e delivered ticket PAY-123 matches (branch)
61
100
  91ab02c4 delivered link notion.so/team/checkout-v2 matches (messages); waits for its next start
62
- c0ffee12 skipped no tag match (not checked: no triage yet)
101
+ 5e1f0a77 delivered model: yes (p 0.91)
102
+ 77aa0b3c delivered unchecked: triage deadline
103
+ c0ffee12 skipped model: no
63
104
  d00dad00 skipped open in a chi REPL
64
- delivered 2 · skipped 2
105
+ delivered 4 · skipped 2 · 1 unchecked: triage deadline
65
106
  ```
66
107
 
67
- A session a worker runs adds the note within a few seconds (after a running
108
+ The first line names the broadcast (`b-7f3a1c9e`): `chi broadcast log` and
109
+ `chi broadcast deliver` take it. A session a worker runs adds the note within a few seconds (after a running
68
110
  turn); one with no worker gets it at its next start ("waits for its next
69
111
  start"). Nothing starts a turn anywhere.
70
112
 
71
113
  `--dry-run` prints the note's tags, each recipient's verdict ("would get it" or
72
- "skipped") and its scope card: what chi knows about it from local state, and
73
- what a triage model will read.
114
+ "skipped"; triage runs) and its scope card: what chi knows about it from local
115
+ state, and what the triage model reads. A line before them names the triage model
116
+ and the setting it came from.
74
117
 
75
118
  ```
76
119
  3f2a1c9e would get it ticket PAY-123 matches (branch)
@@ -83,7 +126,8 @@ what a triage model will read.
83
126
  ```
84
127
 
85
128
  Exit status: 0 when it ran (skipped sessions included), 1 when it was refused,
86
- no session is active or a delivery failed, 2 for a usage error.
129
+ no session is active or a delivery failed, 2 for a usage error. `chi broadcast
130
+ deliver` exits 0 when every session named has the note now, 1 otherwise.
87
131
 
88
132
  ## What a session gets
89
133
 
@@ -96,7 +140,8 @@ payments API returns 500 since 14:00 (PAY-123)
96
140
  [END NOTE]
97
141
  ```
98
142
 
99
- With `--all` the line says "with every active session". The date is there
143
+ With `--all` the line says "with every active session"; one that triage
144
+ delivered says "with the sessions it may concern." and no reason. The date is there
100
145
  because a session with no worker may read it days later. The system prompt
101
146
  tells the model what a note from `broadcast` is: if it affects its current
102
147
  work, it says so briefly in its next answer, otherwise it ignores it, and it
@@ -107,9 +152,38 @@ plain note can't pass for a broadcast.
107
152
  The note and its line together are capped at 16 KiB like any note; the text
108
153
  may take up to 15872 bytes.
109
154
 
155
+ ## The triage log
156
+
157
+ Each broadcast (not a `--dry-run`) leaves one line in
158
+ `$XDG_STATE_HOME/samagotchi/broadcast/log.jsonl` (`~/.local/state/…`): its
159
+ id, time, your text, the note's tags and each recipient's verdict (result,
160
+ reason, P(yes), who decided, the session's tags). Scope cards are not kept:
161
+ they quote your prompts. Past 2 MiB the file becomes `log.jsonl.1`, one old
162
+ file kept (`log.jsonl.lock` keeps two broadcasts at once from both rotating
163
+ it). Sessions don't read it: a broadcast stays fire-and-forget.
164
+
165
+ ```sh
166
+ chi broadcast log # the last 5 broadcasts
167
+ chi broadcast log --last 20 --format json
168
+ ```
169
+
170
+ ```
171
+ b-7f3a1c9e 2026-10-06 14:20 "payments API returns 500 since 14:00 (PAY-123)"
172
+ 3f2a1c9e delivered ticket PAY-123 matches (branch)
173
+ c0ffee12 skipped model: no
174
+ delivered 1 · skipped 1
175
+ 2026-10-06 14:31 delivered anyway: c0ffee12
176
+ ```
177
+
178
+ When triage skipped a session that needed the note, `chi broadcast deliver
179
+ BROADCAST_ID ID...` (an id or the start of one, for both) gives it the logged
180
+ text, dated when it was shared and saying you passed it on. A session that
181
+ got it already is left alone. The log keeps it as a correction: labels for
182
+ tuning `broadcast.threshold`, or for a classifier later.
183
+
110
184
  ## For you, not for an agent
111
185
 
112
- `chi broadcast` refuses to run inside a chi session (where
186
+ `chi broadcast` (`log` and `deliver` too) refuses to run inside a chi session (where
113
187
  `SAMAGOTCHI_PARENT_SESSION` is set): "chi broadcast is for your user, not an
114
188
  agent". With the guardrails bundle, the `chi-broadcast` rule also asks before
115
189
  any shell command that runs it, once at a time, and only you may allow it
@@ -121,9 +195,17 @@ any shell command that runs it, once at a time, and only you may allow it
121
195
  |---|---|---|
122
196
  | `broadcast.active_hours` | `8` | A session with no worker or REPL counts when its last turn ended within this many hours. |
123
197
  | `broadcast.ticket_pattern` | `\b[A-Z][A-Z0-9]+-\d+\b` | A Ruby regex for ticket ids; an invalid one warns and the default is used. |
198
+ | `broadcast.triage_model` | recap's, else `default.model` | The triage model, a model ref (`box:qwen3.5-9b`). |
199
+ | `broadcast.triage_host_ref` | none | A `hosts:` name for it. |
200
+ | `broadcast.triage_base_url` | none | An OpenAI API base for it instead. |
201
+ | `broadcast.triage_parallel` | `4` | Triage requests at a time. |
202
+ | `broadcast.triage_deadline` | `20` | Seconds triage may take in all; the rest get the note unchecked. |
203
+ | `broadcast.threshold` | `0.5` | The P(yes) a session needs when the host gives logprobs. |
124
204
 
125
205
  ```yaml
126
206
  broadcast:
127
207
  active_hours: 12
128
208
  ticket_pattern: '\b(?:PAY|OPS)-\d+\b'
209
+ triage_model: box:qwen3.5-9b
210
+ triage_deadline: 30
129
211
  ```
@@ -742,7 +742,8 @@ One record is one line, plus indented payload lines at debug level:
742
742
  control characters (terminal colours in tool output) are escaped.
743
743
  - Tags: `turn` (a session's event trail), `http` (model requests),
744
744
  `worker`, `bridge`, `web`, `attached`, `repl`, `idle`, `recap`, `hooks`,
745
- `plugins`, `guardrails`, `config`, `memory`, `model` (debug dumps), `context` (attached context fetches).
745
+ `plugins`, `guardrails`, `config`, `memory`, `model` (debug dumps), `context` (attached context fetches),
746
+ `broadcast` (chi broadcast's triage).
746
747
  - The format is parsed by `Samagotchi::LogLine` (`parse`, `each_record`);
747
748
  keep tools that read it on that parser.
748
749
 
@@ -884,6 +885,12 @@ described in their own sections.
884
885
  | `recap.sentences` | `2-4` | yes | Recap length, `N` or `N-M` (1–10). |
885
886
  | `broadcast.active_hours` | `8` | | `chi broadcast` reaches a session a worker or a chi REPL runs, or whose last turn ended within this many hours. See [Broadcast](broadcast.md). |
886
887
  | `broadcast.ticket_pattern` | `\b[A-Z][A-Z0-9]+-\d+\b` | | A Ruby regex for ticket ids; one in the note and in a session's branch (any case) or prompts delivers the broadcast there. See [Broadcast](broadcast.md). |
888
+ | `broadcast.triage_model` | recap's | | The model that judges a recipient no tag matched (a model ref, as `recap.model`); unset: the recap's model, else `default.model`. A ~9B dense instruct model or larger; 4B-class ones say yes to nearly everything. See [Broadcast](broadcast.md#triage). |
889
+ | `broadcast.triage_host_ref` | none | | A `hosts:` name for the triage model. |
890
+ | `broadcast.triage_base_url` | none | | An OpenAI API base for the triage model instead (`http://h:8081/v1`). |
891
+ | `broadcast.triage_parallel` | `4` | | Triage requests at a time. |
892
+ | `broadcast.triage_deadline` | `20` | | Seconds triage may take in all; a recipient not judged by then gets the note unchecked. |
893
+ | `broadcast.threshold` | `0.5` | | The P(yes) a recipient needs, when the triage host gives logprobs; a plain yes or no is 1 or 0. |
887
894
  | `session.shared` | `true` | | Plain `chi` runs its session in a worker and attaches; `--no-shared` per run. |
888
895
  | `session.idle_exit_minutes` | `30` | yes | An unused worker exits after this; `0` = never. |
889
896
  | `session.keep_empty` | `false` | | Keep sessions nothing happened in. |
@@ -917,7 +924,7 @@ described in their own sections.
917
924
  | `thinking.level` | `default` | `--thinking` | `off`, `low`, `medium`, `high` or `default` for every model; the flag and env outrank the `models:`/`hosts:` entries, the file's value doesn't. See "Thinking". |
918
925
  | `models.<key>.thinking`, `hosts.<name>.thinking` | none | | A model's or host's level. See "Thinking". |
919
926
  | `hosts.<name>.remote` | by address | | `true`/`false`: treat the host as a remote provider or a local server. See "Remote or local". |
920
- | `max_tool_output_chars` | `10000` | yes | Tool output kept in the conversation; a top-level key (see below). |
927
+ | `max_tool_output_chars` | `10000` | yes | Characters of each tool output kept in the conversation (both loops); a longer one is cut and ends with `[cut: N of M chars; read it in parts]`. A top-level key (see below). |
921
928
  | `cache.warmup` | `auto` | | `auto`: after a turn, send the next turn's prompt (up to the next message) to a local llama.cpp host on the native loop, so the next turn prefills only its message; never a remote or `api: openai` host. `off`: never. See [prompt caching](internals/prompt-caching.md#the-turn-end-warm-up). |
922
929
  | `cache.ttl` | `5m` | | How long Anthropic keeps the prompt-cache breakpoints of a Claude model on a remote host: `5m` (Anthropic's default) or `1h`. A 1h cache write costs 2× the input price (5m: 1.25×), a read the same 0.1×. See "Remote or local". |
923
930
  | `cache.key` | `off` | | `session`: send the session id as `prompt_cache_key` to OpenAI's API and OpenRouter, which route and cache by it; no other host gets it. `off`: none. |
data/docs/desktop.md CHANGED
@@ -38,6 +38,22 @@ note needs a session. After the send the panel shows `started <id>…` for 3 s.
38
38
  the model the new session starts on (`chi self --model`: `default.model` from config.yml, or its env override; an
39
39
  alias as the model it names).
40
40
 
41
+ ### Everyone it concerns
42
+
43
+ The second row, **Everyone it concerns** (⌘B), shares a note with every session it may concern instead of the ones
44
+ you pick: ⌘⏎ (the **Broadcast** button) runs [`chi broadcast`](broadcast.md) with the line, then the text, on stdin.
45
+ chi picks the sessions (a shared ticket, PR or link, else a small triage model), so the preselected session doesn't
46
+ count: the row is ticked alone, like the new row. Write it the way you'd tell a colleague: a first line such as
47
+ `shopfront/checkout` keeps the note to that project's sessions, and the rest is the selection.
48
+
49
+ - The panel shows "broadcasting…", then the broadcast's id and chi's summary line (`b-7f3a1c9e: delivered 4 ·
50
+ skipped 2 · 1 unchecked: triage deadline`), and **stays open**: Esc closes. Which sessions got it and why:
51
+ `chi broadcast log` in a terminal.
52
+ - A broadcast is a text note: ⏎ and images beep. The source field and Send are hidden (its source is `broadcast`).
53
+ - It may take `broadcast.triage_deadline` (20 s) and 10 s more before the helper stops it ("the note may be partly
54
+ delivered"). The helper reads that from its launch file: after changing the deadline, run `chi update` (or
55
+ `chi desktop upgrade`).
56
+
41
57
  ### Choosing the model
42
58
 
43
59
  Click the model on the new row, or press **⌘M**, to choose another one for the new session. The chooser takes the
@@ -179,7 +195,9 @@ exports. So `install` writes `~/Library/Application Support/Chi Helper/launch.js
179
195
  - the absolute path of the Ruby running chi and of chi itself (the gem's wrapper, or a checkout's `bin/chi`);
180
196
  - `LANG=en_US.UTF-8`;
181
197
  - only these variables, and only when they are set: `XDG_CONFIG_HOME`, `XDG_STATE_HOME`, `GEM_HOME`, `GEM_PATH`,
182
- `RUBYLIB`. No tokens and no `SAMAGOTCHI_*` settings go in.
198
+ `RUBYLIB`. No tokens and no `SAMAGOTCHI_*` settings go in;
199
+ - the settings the panel needs from config.yml, as chi read them then: the `kitty:` section (when
200
+ `kitty.listen_on` is set) and `broadcast_timeout` (`broadcast.triage_deadline` + 10 s).
183
201
 
184
202
  The file freezes the install shell's values. If you install with a temporary `XDG_STATE_HOME`, the helper keeps
185
203
  using it. `status` prints the baked dirs.
@@ -194,10 +212,14 @@ The helper uses only these commands, so it could ship on its own later:
194
212
  - `chi send [-m LINE] [--image PATH]... ID...` and `chi send --new --dir DIR [--model NAME] [-m LINE] [--image PATH]...`
195
213
  with the text on stdin (or none), and `chi note [--source NAME] ID...` with the text on stdin → one line per session
196
214
  on stdout; exit 0 means all sent or queued, 1 means some were refused or failed.
215
+ - `chi broadcast` with the text on stdin → its id first (`broadcast b-…`), a line per recipient, then the summary line
216
+ (`delivered N · skipped M…`); exit 0 when it ran, 1 when refused or a delivery failed.
197
217
  - `chi self --model` → the model a new session starts on (the panel's hint).
198
218
  - `chi models --format json` → the models the hosts offer (the new-session model picker).
199
219
 
200
- Each call is stopped after 10 s, a send with images after 30 s. A stopped `chi note` says the note may be partly delivered.
220
+ Each call is stopped after 10 s, a send with images after 30 s, a broadcast after the launch file's
221
+ `broadcast_timeout` (`broadcast.triage_deadline` + 10 s; 30 s when it has none). A stopped `chi note` or `chi
222
+ broadcast` says the note may be partly delivered.
201
223
 
202
224
  ## Troubleshooting
203
225
 
data/docs/guardrails.md CHANGED
@@ -289,12 +289,27 @@ A tool name may be a glob, so one rule covers a plugin's tools (an MCP server's,
289
289
  say): `tool: "mcp_*"` or `tool: ["mcp_{git,gh}_*", web_fetch]` (`*`, `?`, `[…]` and
290
290
  `{a,b}`, matched with `File.fnmatch`). `/guardrails` lists the glob as given.
291
291
  A plugin tool whose `targets:` name no command or path (an MCP tool) is asked
292
- about with its arguments (`mcp_x_sum: a=20 b=22`), and an approval of "this
292
+ about with its arguments (`x: sum: a=20 b=22`), and an approval of "this
293
293
  call" is keyed by them.
294
294
 
295
+ A plugin tool that runs other tools for the model (a dispatcher such as
296
+ `mcp_call`) can say, in its `targets:`, which tool a call acts as (`acts_as:`)
297
+ and with which arguments (`args:`). A rule's `tool:` then matches the call's
298
+ own name **or** the tool it acts as: `tool: mcp_github_*` fires for an
299
+ `mcp_call` that acts as `mcp_github_merge_pull_request`, and `tool: mcp_call`
300
+ fires whatever it acts as. The question names the tool it acts as (by the
301
+ `label:` its `targets:` gives, if any: display only) and shows its arguments, and an approval of "this call" is keyed by the real name, the
302
+ alias and those arguments (`/guardrails` lists it as
303
+ `mcp_call>mcp_github_merge_pull_request:number=7 …`), so it never covers a
304
+ direct call of that tool, nor a direct call's approval it. A tool can't act as
305
+ one of chi's own tools or another bundle's tool: that `acts_as:` is dropped
306
+ (and logged as `plugin_acts_as_dropped`), and the call is matched by its own
307
+ name only. Hooks and loop-guard's `ignore_tools` still see the call's own
308
+ name.
309
+
295
310
  ```yaml
296
311
  - id: mcp-ask
297
- tool: "mcp_*" # every MCP tool (the mcp bundle's mcp_<server>_<tool>)
312
+ tool: "mcp_*" # every MCP call (mcp_call, acting as mcp_<server>_<tool>); not a find_mcp_tools search
298
313
  verdict: ask
299
314
  reason: an MCP server's tool
300
315
  ```
data/docs/hooks.md CHANGED
@@ -56,7 +56,7 @@ The plugin class must respond to `#call(event)` — duck-typed, no base class re
56
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) }` |
57
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 }` |
58
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 }` |
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" }`; `stopped` is a `task_wait` the turn's Stop ended (the task runs on) or whose task was stopped |
59
+ | `:after_tool_call` | After tool execution | `{ type: :after_tool_call, iteration: N, tool: "read", output: "..." (the text the model gets: capped at max_tool_output_chars, a cut one ending in a [cut: N of M chars …] line), status: "ok" \| "error" \| "blocked" \| "stopped" }`; `stopped` is a `task_wait` the turn's Stop ended (the task runs on) or whose task was stopped |
60
60
  | `:session_end` | After each turn `:after_turn` fires for, after it (turn-level lifecycle) | `{ type: :session_end, session_id: "..." }` |
61
61
 
62
62
  Every event also carries the hook runtime (next section): `hook:` (the label
@@ -444,7 +444,8 @@ stripped of surrounding whitespace, except file text, an edit's
444
444
  A `write`'s `content:` is the file text; an `edit` carries `old_text:` and
445
445
  `new_text:` (its `content:` is `""`). A plugin or MCP tool's call has its
446
446
  arguments whole on `args:` (string keys) instead, and its `content:` is those
447
- arguments as JSON.
447
+ arguments as JSON. An MCP tool's call is the mcp bundle's `mcp_call`: `tool:
448
+ "mcp_call"`, with `args: {"tool" => "<server>/<tool>", "args" => {…}}`.
448
449
 
449
450
  ## Bundle Hooks (unified workflow bundle)
450
451
 
@@ -192,7 +192,7 @@ One iteration with tool calls:
192
192
  → returns {output:, capped_output:, truncated:, activity:, images:, shown_params:,
193
193
  shown_label:, diff:}
194
194
  → :tool_dispatch_completed {iteration, call_count}
195
- → native: ToolResponse.joined(runs), one entry with the full outputs
195
+ → native: ToolResponse.joined(runs), one entry with the capped outputs
196
196
  chat: ToolResponse.single(run, tool_call_id:) per call, with the capped output
197
197
  ```
198
198
 
@@ -207,7 +207,9 @@ One iteration with tool calls:
207
207
  - For edit and write: diffs the file before and after, and refreshes a memory's index line
208
208
  (`MemoryBundle::IndexSync`).
209
209
  - Attaches returned images, at most `MAX_IMAGES_PER_RESULT` (4) per result.
210
- - Scrubs bytes that aren't UTF-8, and caps the event's output at `max_tool_output_chars`.
210
+ - Scrubs bytes that aren't UTF-8, and caps the output at `max_tool_output_chars`: a longer one is cut and ends
211
+ with `[cut: N of M chars; read it in parts]` (N kept of M, the whole within the cap unless the cap is shorter
212
+ than that line). The model (both loops), the event and the after_tool_call hook get that text.
211
213
  - Emits `:tool_call_completed` {iteration, call_count, call_index, tool, output, output_truncated, activity,
212
214
  images, diff, waited_ms, view}, then fires `:after_tool_call` {iteration, tool, output, status}.
213
215