samagotchi 0.105.0 → 0.106.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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -1
  3. data/docs/architecture.md +6 -3
  4. data/docs/cli.md +49 -8
  5. data/docs/configuration.md +9 -5
  6. data/docs/guardrails.md +56 -0
  7. data/docs/hooks.md +9 -0
  8. data/docs/internals/engine.md +22 -4
  9. data/docs/internals/participants.md +206 -28
  10. data/docs/internals/prompt-caching.md +2 -1
  11. data/docs/internals/working-set.md +8 -2
  12. data/docs/plugins.md +2 -0
  13. data/docs/testing.md +3 -1
  14. data/lib/samagotchi/bridge/turn_accumulator.rb +13 -3
  15. data/lib/samagotchi/bridge.rb +35 -1
  16. data/lib/samagotchi/bridge_client.rb +4 -1
  17. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  18. data/lib/samagotchi/client_id.rb +21 -9
  19. data/lib/samagotchi/config.rb +16 -1
  20. data/lib/samagotchi/continue_offer.rb +13 -3
  21. data/lib/samagotchi/delivery.rb +10 -0
  22. data/lib/samagotchi/engine.rb +62 -6
  23. data/lib/samagotchi/guardrail_wiring.rb +13 -3
  24. data/lib/samagotchi/guardrails/review_boundary.rb +148 -0
  25. data/lib/samagotchi/guardrails.rb +1 -0
  26. data/lib/samagotchi/hook_routes.rb +5 -2
  27. data/lib/samagotchi/hooks/registry.rb +12 -3
  28. data/lib/samagotchi/idle_recap.rb +9 -0
  29. data/lib/samagotchi/parent_report.rb +17 -5
  30. data/lib/samagotchi/participant_messages.rb +16 -0
  31. data/lib/samagotchi/participants.rb +29 -6
  32. data/lib/samagotchi/participants_watch.rb +90 -0
  33. data/lib/samagotchi/plugin/context.rb +7 -1
  34. data/lib/samagotchi/plugin/side_question.rb +9 -1
  35. data/lib/samagotchi/reply_wait/poll.rb +18 -3
  36. data/lib/samagotchi/run_options.rb +1 -1
  37. data/lib/samagotchi/send_command.rb +41 -3
  38. data/lib/samagotchi/session_access/contract.rb +2 -0
  39. data/lib/samagotchi/session_access/local.rb +31 -9
  40. data/lib/samagotchi/session_access/remote.rb +19 -2
  41. data/lib/samagotchi/session_access.rb +6 -3
  42. data/lib/samagotchi/session_commands.rb +6 -0
  43. data/lib/samagotchi/session_inbox.rb +31 -5
  44. data/lib/samagotchi/session_manager.rb +43 -3
  45. data/lib/samagotchi/session_metrics/side_totals.rb +21 -7
  46. data/lib/samagotchi/session_metrics.rb +88 -1
  47. data/lib/samagotchi/share_command.rb +61 -22
  48. data/lib/samagotchi/side_usage.rb +4 -2
  49. data/lib/samagotchi/steer.rb +37 -1
  50. data/lib/samagotchi/terminal_ui/attach_link.rb +5 -0
  51. data/lib/samagotchi/terminal_ui/attached_loop.rb +54 -7
  52. data/lib/samagotchi/terminal_ui/formatting.rb +66 -5
  53. data/lib/samagotchi/terminal_ui/local_link.rb +7 -0
  54. data/lib/samagotchi/terminal_ui/participant_commands.rb +77 -0
  55. data/lib/samagotchi/terminal_ui/remote_link.rb +6 -2
  56. data/lib/samagotchi/terminal_ui/status_row.rb +11 -2
  57. data/lib/samagotchi/terminal_ui.rb +5 -0
  58. data/lib/samagotchi/testing/host.rb +4 -2
  59. data/lib/samagotchi/testing/recorder.rb +3 -1
  60. data/lib/samagotchi/toolkit.rb +21 -3
  61. data/lib/samagotchi/version.rb +1 -1
  62. data/lib/samagotchi/web/app.rb +45 -20
  63. data/lib/samagotchi/web/grants.rb +85 -5
  64. data/lib/samagotchi/web/participant_access.rb +206 -35
  65. data/lib/samagotchi/web/participant_review.rb +165 -0
  66. data/lib/samagotchi/web/participant_stream_filter.rb +157 -0
  67. data/lib/samagotchi/web/participant_view.rb +415 -0
  68. data/lib/samagotchi/web/public/app.js +15 -1
  69. data/lib/samagotchi/web/public/bubbles.js +3 -3
  70. data/lib/samagotchi/web/public/data.js +20 -0
  71. data/lib/samagotchi/web/public/index.html +71 -27
  72. data/lib/samagotchi/web/public/info_bar.js +12 -5
  73. data/lib/samagotchi/web/public/info_bar_model.js +8 -5
  74. data/lib/samagotchi/web/public/participant.html +119 -0
  75. data/lib/samagotchi/web/public/participant.js +300 -0
  76. data/lib/samagotchi/web/public/participant_model.js +160 -0
  77. data/lib/samagotchi/web/public/participant_names.js +21 -0
  78. data/lib/samagotchi/web/public/participants_panel.js +311 -0
  79. data/lib/samagotchi/web/public/stream_handlers.js +8 -1
  80. data/lib/samagotchi/web/public/tokens.css +12 -0
  81. data/lib/samagotchi/web/public/turn_events.js +16 -5
  82. data/lib/samagotchi/web/public/turn_html.js +8 -4
  83. data/lib/samagotchi/web/public/turn_view.js +2 -1
  84. data/lib/samagotchi/web/public/working_set_view.js +3 -3
  85. data/lib/samagotchi/web/qr.rb +14 -0
  86. data/lib/samagotchi/web/running.rb +52 -0
  87. data/lib/samagotchi/web/server.rb +13 -0
  88. data/lib/samagotchi/web/session_access_routes.rb +3 -0
  89. data/lib/samagotchi/worker.rb +107 -5
  90. data/lib/samagotchi/worker_inbound.rb +12 -4
  91. metadata +15 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e0920f477e95206617da7d0d43e39dc981b071b34f456c60a7b669e4836875be
4
- data.tar.gz: 1f9219bb12b92c2178ab15d57fddfaaee3654972d9b90e2ce11b8265ddab90cf
3
+ metadata.gz: 13a61f40d26663d6ecd66b89b94d1406f8495e161a5bbdd33a319a41e46dbfb3
4
+ data.tar.gz: 2b0ab74468e6020dad2b60b12abba06809933621234080f0ef75f8fbc6243ccc
5
5
  SHA512:
6
- metadata.gz: 5c7625cff1523fe72e997ec3265465f9385b7a7fe59e8a3f6b097f8015e84f1b32441582218c75b4145f2f8ffb2b58ea7909c30f65efd4dbac114650c4b65f55
7
- data.tar.gz: 3f2afdc87fa1a48efebbc14138016f2da28502e74b3a69d59ddd2c1ae5c37041af2192a0a071cf78325a14c2f630afe28e42cfed03b625f103fbf88f2037533c
6
+ metadata.gz: d53a1f7c2f4347d9688e3d5f4000400297b343d2089536dd9efececcaf30cacceec91834639fed0750848e098c4845e4622c44f6b1df49180a84422dfc153430
7
+ data.tar.gz: 83dce80d3f6ebaa03b1bc84c6d84f025bb6055a9125c0e6ade2a7ad427922c8c2dbeb631991500e9b3be9454161917229d2f0b5f27b55f0a36e7afd0f3ab76c2
data/CHANGELOG.md CHANGED
@@ -8,6 +8,52 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.106.0] - 2026-10-23
12
+
13
+ ### Added
14
+
15
+ - Participants (experimental) can now ask questions and comment: a participant's message through chi web is a
16
+ review turn of its own, queued behind the running one, that the model answers read-only. It may read the session's
17
+ project (never its secret files, `~/.ssh` or chi's own state and config) and project memories, and proposes
18
+ changes as text for the owner to apply; writes, commands, fetches, notes, reminders, plugin and MCP tools are
19
+ denied, never asked about, and every approval or question stays the owner's. A line starting `!` or `/` and images
20
+ are refused. Each participant gets one running and 3 waiting turns (`web.participants.queue`) and a spending cap,
21
+ `chi share --cap DOLLARS` (default $2, `web.participants.cap`); at the cap their next comment is refused and the
22
+ owner gets a notice, and `chi share cap ID P DOLLARS` raises it. `/stats` shows each participant's spend, their
23
+ turns and the side requests they caused. Hooks see `author:` on a participant's turn and plugins
24
+ `ctx.turn_author`; a hook still runs with the owner's power. A participant now sees the conversation and comments
25
+ from their page (desktop and phone): prompts and replies with who wrote each, one-line tool rows (the tool and a
26
+ project file's path, never its arguments or output), the reply streaming in, their own queued comments (Take back),
27
+ "waiting for the owner" while you answer a question, and every refusal in words. They never see tool output,
28
+ thinking, approvals, questions, memories, notes, the working set, cost or host paths: one server-side filter covers
29
+ their stream and reads, and redacts secret-shaped text as well. On your page, a participant's prompts carry their
30
+ name, and the info bar's participants chip opens a panel to invite (a link to copy, share or scan), see each one's
31
+ spend, turns and last visit, change a cap, revoke, and open the participant view: their page for this session, as
32
+ they see it (an agent participant can wait on its turns' replies through chi web's API). The `review`
33
+ toolkit is new and reserved: config's `toolkit.kits.review` is refused. A session whose worker predates this
34
+ release takes no participant's turn until it is restarted. In the attached terminal a participant's message reads
35
+ `Alex ▸ review> …`, a line says when someone is invited, revoked or expires, the status row counts them, and
36
+ `/share` and `/participants` (list, revoke, cap) run `chi share` on the session there. See [docs/cli.md](docs/cli.md#participants) and
37
+ [docs/guardrails.md](docs/guardrails.md#a-participants-review-boundary).
38
+
39
+ ### Fixed
40
+
41
+ - Web, dark theme: the working set's rail (its rule and count), the list's header rows (`files · 2`) and its ops
42
+ text and buttons (drop, pin, add) are easier to read.
43
+ - Web: `!rollback` (and a canceled continue) updates the working set at once; one that emptied it no longer leaves
44
+ the dropped items in the rail and the list until the next turn.
45
+ - `chi share`'s link names the port the running chi web listens on (recorded in `$XDG_STATE_HOME/samagotchi/web.json`),
46
+ not only config's `web.port`.
47
+ - A participant's `chi --remote NAME send -m` prints the review turn's id, and `send --wait -m` waits on it instead of
48
+ being refused as busy; a participant's turn held for the owner reads `waiting_for_owner` (exit 3).
49
+ - `chi send --wait --format json`: a turn still running names the wait on its turn id.
50
+ - The attached terminal no longer announces your own `/share` or `/participants revoke` a second time.
51
+ - Web: the session bar's first line keeps one width through a turn: the status word takes the same room for idle and
52
+ running, the working set's chip shows from the session's start (`set 0`), and every chip is one height. A short
53
+ folder path no longer hops between the bar's rows mid-turn and moves the stage.
54
+
55
+ Update with `chi update`.
56
+
11
57
  ## [0.105.0] - 2026-10-23
12
58
 
13
59
  ### Changed
@@ -3965,7 +4011,8 @@ and long-lived sessions.
3965
4011
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
3966
4012
  hotkey panel that send selected text or the clipboard to your sessions.
3967
4013
 
3968
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.105.0...HEAD
4014
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.106.0...HEAD
4015
+ [0.106.0]: https://github.com/dm1try/samagotchi/compare/v0.105.0...v0.106.0
3969
4016
  [0.105.0]: https://github.com/dm1try/samagotchi/compare/v0.104.0...v0.105.0
3970
4017
  [0.104.0]: https://github.com/dm1try/samagotchi/compare/v0.103.0...v0.104.0
3971
4018
  [0.103.0]: https://github.com/dm1try/samagotchi/compare/v0.102.0...v0.103.0
data/docs/architecture.md CHANGED
@@ -470,7 +470,10 @@ The per-session port is OS-assigned (bound to `0`) and published to a `bridge.js
470
470
  for client discovery. `chi web`'s `GET /api/sessions/:id/stream` proxies this bridge
471
471
  (503 `not_live` when the worker is not running; full history of any session is served by
472
472
  `GET /api/sessions/:id/output`), as a byte pipe for the owner: `App#proxy_stream_body` gives `ProxyStreamBody` the
473
- request principal's frame filter (`App#stream_filter`, nil for the owner, the only principal today). Resume/ring-buffer state is **in-memory**, per worker: a cursor from
473
+ request's frame filter (`App#stream_filter`: nil for the owner; for a participant, and the owner's participant view
474
+ `?as=participant`, a `Web::ParticipantStreamFilter`, which cuts the Bridge's chunks into frames and writes each again
475
+ from `Web::ParticipantView.event`'s allowlist, dropping every other event; only `from_seq` and `snapshot` reach the
476
+ Bridge; [internals/participants.md](internals/participants.md#what-a-participant-sees)). Resume/ring-buffer state is **in-memory**, per worker: a cursor from
474
477
  an earlier worker gets the `reset` snapshot, not a replay.
475
478
 
476
479
  **Session hub.** The cross-session layer (which sessions exist, who owns them, what changed)
@@ -513,8 +516,8 @@ calling the `SessionAccess::Local` operation of the same name, so a remote backe
513
516
  token-gated like the rest (LAN mode: `Authorization: Bearer`), and one that takes an id is a `:session` route (an id
514
517
  that isn't one, or no such session: 404 `not_found`). Every `ROUTES` row also names who may use it, checked against
515
518
  the request's `Web::Principal` (`App#principal_for`): `owner`, or `participant` for a route a participant may use too.
516
- A participant (a `Web::Grants` token for one session) may use three: `GET /`, `GET /api/info` and `GET /api/sessions/:id`,
517
- each answered as theirs; every other route is `owner` (Permission below, and the participants routes in
519
+ A participant (a `Web::Grants` token for one session) may use the page (`GET /`), `GET /api/info`, their session's
520
+ show, review and filtered stream, and their turns and waits, each answered as theirs; every other route is `owner` (Permission below, and the participants routes in
518
521
  [internals/participants.md](internals/participants.md)); `spec/web/principal_spec.rb` fails on a row without one. How a request flows end to end, which checks run where and the trust
519
522
  model: [internals/remote-sessions.md](internals/remote-sessions.md).
520
523
 
data/docs/cli.md CHANGED
@@ -22,7 +22,7 @@
22
22
  - `chi send [-m TEXT] [--image PATH]... [--cut | --queue] (ID|PREFIX)...` — send a message to sessions as if typed there: a turn starts (or a running one takes it at its next step; `--cut` asks it to cut long thinking for it; `--queue` runs it as a turn of its own after the running one, and `--queue --wait` waits for that turn's own answer); piped stdin goes above `-m` as quoted context, and `--image` attaches images (see [Sessions: Sending a message](sessions.md#sending-a-message)); `--new` starts a session with it instead (`--new --continues ID|last:ID` the next link of that session's chain, see [Session chains](sessions.md#session-chains)), and `--wait` prints the answer (`--wait ID` with no message waits for the next reply without sending — to an idle session whose last turn already ended, that ended turn's reply or end comes back; `--wait --turn T ID` with no message waits for exactly turn T, whether running, queued or last ended; `--format json` includes its `turn_id` when known, for waiting again after exit 3 or 4; exit 3 a question waits, its options on stderr; exit 4 `--timeout` passed with the turn still running; see [Starting a session](sessions.md#starting-a-session))
23
23
  - `chi answer ID --question QID (--option N|LABEL)... [--text T] [--timeout S] [--format json]` (or `--dismiss`) — answer the question a session waits on (the one `chi send --wait` exited 3 with), then wait and print what comes next as `chi send --wait` does: the reply (0), the next question (3), still running after `--timeout` (4). `--option` is 1-based or the label, repeated on a multi-select question; `--text` is free text, or a Deny's reason; `--dismiss` leaves it unanswered and the model finishes its reply. A question no longer open (the web answered first) isn't answered again: it waits for that turn's reply. An option the question doesn't offer exits 2; a worker that is gone exits 1 (`send the task again: chi send --wait -m "…" ID`). What a parent may pick follows `guardrails.parent_answers`, or the session's own `parent_answers` when stricter (`soft`, the default: a deny, a plain answer, Continue and "Allow once"; `standard`: no allow; `hard`: no Continue either), never a wider scope or an allow on chi's own config; a refused pick exits 1 with what to answer instead (a convention, not a security boundary; see [Guardrails](guardrails.md#answers-from-a-parent-agent))
24
24
  - `chi remote add NAME URL [--token-file F | --token-stdin]`, `chi remote ls`, `chi remote rm NAME` — the other machines whose chi web the session commands reach with `--remote NAME` (see [Remote sessions](#remote-sessions))
25
- - `chi share ID --name NAME [--agent] [--expires 3d]`, `chi share ls [ID]`, `chi share revoke ID [P] | --all` — invite a participant to review a session through chi web on the LAN (a link of their own, 7 days), list them, revoke them (see [Participants](#participants))
25
+ - `chi share ID --name NAME [--agent] [--expires 3d] [--cap 2]`, `chi share ls [ID]`, `chi share cap ID P DOLLARS`, `chi share revoke ID [P] | --all` — invite a participant to review a session through chi web on the LAN (a link of their own, 7 days, a $2 spending cap), list them, change one's cap, revoke them (see [Participants](#participants))
26
26
  - `chi --remote NAME send|answer|note|context|sessions|share …` (or `CHI_REMOTE=NAME`) — those commands on that machine's sessions, through its chi web (see [Remote sessions](#remote-sessions))
27
27
  - `chi --remote NAME --attach ID` (`--resume ID`, `--dir DIR [-p TEXT]`) — the attached TUI on a session of that machine (see [Remote sessions](#remote-sessions))
28
28
  - `chi memory ls [--scope project|system] [--format text|json]` — the memories of both scopes (this folder's project first), one row each: scope, name, size, date, `from <bundle>`, the index description and its model overlays; `chi memory rm NAME [--scope project|system]` removes one as `memory_write` with `remove: true` does: its file and model overlays go to the bundle trash (the line names the dir; `chi bundle trash` lists and empties it) and its index line is dropped. NAME in both scopes without `--scope` exits 2 naming both; a missing NAME or a bundle's memory (`chi bundle uninstall` removes it) exits 1 (see [Memory: Listing and removing from the shell](memory.md#listing-and-removing-from-the-shell))
@@ -1274,10 +1274,11 @@ A session's owner can invite **participants**, people or agents, to review it th
1274
1274
 
1275
1275
  ```sh
1276
1276
  chi share 3f9a1c2e --name Alex
1277
- # p_7c01d2aa Alex (human, reviewer) · session 3f9a1c2e · expires 2026-10-30 14:02
1277
+ # p_7c01d2aa Alex (human, reviewer) · session 3f9a1c2e · expires 2026-10-30 14:02 · cap $2.00
1278
1278
  # http://192.168.1.29:4567/?token=chp_…#s=3f9a1c2e-…
1279
1279
  # (shown once: chi keeps only its digest; chi share revoke 3f9a1c2e p_7c01d2aa ends it)
1280
1280
  chi share ls # every session's participants (--format json; never a token)
1281
+ chi share cap 3f9a1c2e p_7c01d2aa 5 # Alex's turns may cost you $5, from their next one
1281
1282
  chi share revoke 3f9a1c2e # every link to that session; add a participant id for one
1282
1283
  chi share revoke --all # every link to every session
1283
1284
  ```
@@ -1285,13 +1286,52 @@ chi share revoke --all # every link to every session
1285
1286
  - The link is printed once (with a QR code at a terminal): chi keeps only a digest of its token. Send it to the
1286
1287
  participant; opening it sets their browser's cookie, as the owner's LAN link does. `--agent` marks an agent
1287
1288
  participant, which uses the token as `Authorization: Bearer` (e.g. `chi --remote` with that token).
1288
- - It works for 7 days (`--expires Nh` or `Nd`, 1 hour to 30 days), until it is revoked, or until the session is
1289
- archived or deleted. A revoked or expired link shows "This review link has expired or was revoked."
1290
- - For now a participant sees a page with the session's title and status; comments and the review itself come in
1291
- a later chi. They can't reach anything else: every other route answers them 403, another session 404.
1289
+ - It works for 7 days (`--expires Nh` or `Nd`, 1 hour to 30 days; `web.participants.expires`), until it is revoked,
1290
+ or until the session is archived or deleted. A revoked or expired link shows "This review link has expired or was
1291
+ revoked."
1292
+ - **Review turns (experimental).** A participant can ask questions and comment through chi web's `POST …/turn`: each
1293
+ message is a turn of its own, queued behind the running one, that the model answers **read-only**. It may read the
1294
+ project (never its secret files or chi's own), and proposes changes as text or a diff for you to apply; it can't
1295
+ write, run, fetch, send notes or set reminders, and any approval or question is yours. A line starting `!` or `/`
1296
+ and images are refused. One running and 3 waiting turns per participant (`web.participants.queue`). You pay: each
1297
+ participant has a spending cap (`--cap DOLLARS`, default $2, `web.participants.cap`), counted from their turns'
1298
+ cost and the side requests they cause; at the cap their next comment is refused and you get a notice, and `chi
1299
+ share cap` raises it. `/stats` shows each participant's spend. Their turns go only to a worker with the review
1300
+ boundary: restart an older one. While your turn waits at the step limit (Continue or Stop), their turns wait too
1301
+ (still queued, still counted): they run, in order, once you continue, stop or send a turn of your own.
1302
+ - **The participant page.** Their link opens a page of its own (never yours): the session's title and status, who
1303
+ else takes part, and the conversation as a participant sees it: every prompt and reply with who wrote it, each
1304
+ step's tools as one line (`read docs/plan.md`: the tool and a project file's path, never arguments or output), and
1305
+ the reply streaming in while a turn runs. A "Comment or ask" box sends their comment; their queued comments show
1306
+ with **Take back**; "waiting for the owner" shows while you answer a question or an approval of the turn; a refused
1307
+ comment (a `!` or `/` line, too many waiting, their cap reached) says why. On a phone it is the thread first with
1308
+ the box pinned, tool lines folded. They never see tool output or arguments, thinking, approvals or questions,
1309
+ memories, context notes, plugin and delegate input, cards, command output, the working set, cost, timing or host
1310
+ paths; secret-shaped text (API keys, tokens, private keys) is redacted too. An agent participant can wait on its
1311
+ turns' replies through the API, or with chi: its link's token in a remote, `chi --remote NAME send --wait -m "…" ID`,
1312
+ or `send -m`, which prints the turn id `--wait --turn T` takes. They can't reach anything else: every other route answers them 403, another
1313
+ session 404.
1314
+ - **Your side of it.** In the session bar, the participants chip (`invite` while nobody has a link, then `2
1315
+ participants`, with a green dot while one is on their page) opens the panel: each participant's state, last visit,
1316
+ spend against their cap, turns and expiry, with **Cap** and **Revoke** (revoking ends their page and stream and
1317
+ drops their queued comments). **Invite** takes a name, person or agent, the expiry and a cap, and shows the link
1318
+ once, with Copy, Share (a phone's share sheet) and its QR code. **Participant view ↗** opens their page for this
1319
+ session in a new tab, read-only, through the same server filter: exactly what they see. A participant's prompt
1320
+ shows their name in their colour, live and after a reload.
1292
1321
  - `chi --remote studio share …` invites on studio: the link names studio's LAN address. Locally, the link names
1293
- this machine's chi web; without one on the LAN, chi says the link opens on this machine only.
1322
+ this machine's running chi web, on the port it listens on (it records it in `$XDG_STATE_HOME/samagotchi/web.json`;
1323
+ `web.port` when none answers there); without one on the LAN, chi says the link opens on this machine only.
1294
1324
  - The owner's own token and the `chi web --new-token` rotation are separate: rotating it ends no participant's link.
1325
+ - **In the attached terminal.** A participant's message reads `Alex ▸ review> is step 3 right?`, the name in that
1326
+ participant's own colour (queued, running, and the last exchange when you join); yours stay as they were. A line
1327
+ says when someone is invited, revoked or their link expires (`participants> Alex joined (p_7c01d2aa)`,
1328
+ `participants> Alex left: their link was revoked`), within a few seconds and between turns, and the status row
1329
+ counts who is in the review (`2 participants`). The same commands work from there, on the attached session:
1330
+ `/share NAME [--agent] [--expires 3d] [--cap 2]` (the link and its QR code, once), `/participants` (the list),
1331
+ `/participants revoke [P]` (one participant, or everyone) and `/participants cap P DOLLARS`. They are `chi share`'s
1332
+ own, so under `chi --remote NAME` they reach that machine's chi web, as `chi --remote NAME share` does; an invite or
1333
+ a revoke made with them isn't announced again by the `participants>` line. The REPL
1334
+ (`--no-shared`) answers them with a note: a participant's turns run on the session's worker.
1295
1335
 
1296
1336
  How it works inside (grants, the gate, the routes, the audit trail):
1297
1337
  [internals/participants.md](internals/participants.md).
@@ -1417,7 +1457,7 @@ changes:
1417
1457
 
1418
1458
  ```
1419
1459
  status> Coordinator | model=Qwen3.6-35B | ↳ 3f2a1c9e | ctx=12.3% (under20) | mem: notes, cli_usage | muted: gh-helper
1420
- status> model=Qwen3.6-35B | ⑂ 2 running · 1 waiting | ctx=8.1% (under20) | mem: skill_coordinator
1460
+ status> model=Qwen3.6-35B | ⑂ 2 running · 1 waiting | 2 participants | ctx=8.1% (under20) | mem: skill_coordinator
1421
1461
  ```
1422
1462
 
1423
1463
  - The session's title first, when it has one (`/title`; not the first message a session without one is named by),
@@ -1434,6 +1474,7 @@ status> model=Qwen3.6-35B | ⑂ 2 running · 1 waiting | ctx=8.1% (under20) | me
1434
1474
  ends and when a delegate's report merges into a turn, the REPL whenever it draws the row. A
1435
1475
  child that starts waiting on an approval (which doesn't wake the parent) shows at the next of
1436
1476
  those.
1477
+ - `N participants`: who is in the session's review now ([Participants](#participants)), attached only.
1437
1478
  - `ctx=`: the kernel's context estimate and its bucket, updated during a turn (on hosts that
1438
1479
  report none, `api: openai`, at the turn's end).
1439
1480
  - `mem:`: the memories the session read, with its `--memory` list; `muted:` its `--mute` list.
@@ -157,7 +157,7 @@ or `chi web` restart.
157
157
  | `toolkit.default`, `toolkit.child` | Session creation | The chosen toolkit is saved; changing the defaults does not move existing sessions. |
158
158
  | `toolkit.kits`, `execute.description`, `llm_context.policy` | Worker start | These are held by the engine's toolkit or tool registry. |
159
159
  | `skip_agents_md` | Prompt rebuild | A running engine caches its built prompt; a new worker or `/model` rebuild uses the changed value. |
160
- | `context.*` except `context.every_seconds`, `llm_context.*` except `llm_context.policy`, `steer.cut_after`, `memory.index_warn_tokens`, `status.line` | Next matching turn, display, poll or write | Readers resolve these settings when they perform that operation. Session-specific LLM context and thinking values remain saved in the session. |
160
+ | `context.*` except `context.every_seconds`, `llm_context.*` except `llm_context.policy`, `steer.cut_after`, `memory.index_warn_tokens`, `status.line`, `web.participants.*` | Next matching turn, display, poll or write | Readers resolve these settings when they perform that operation. Session-specific LLM context and thinking values remain saved in the session. A participant's grant keeps the expiry and cap it was issued with; `web.participants.*` is what the next invite gets, and the cap and queue of a grant from before caps. |
161
161
  | `context.every_seconds` | Next context poll | The poller checks it for each source that has no explicit interval. |
162
162
  | `guardrails.*` | Next tool call or approval | Guardrail rules and disable lists are reloaded when the config file changes; rule settings take effect on that evaluation. `guardrails.parent_answers` is checked when a parent answers a question. |
163
163
  | `hooks`, `bundles` | Worker start | Hooks, plugins and their bundle settings load with the engine. |
@@ -165,7 +165,7 @@ or `chi web` restart.
165
165
  | `session.shared` | Next `chi` invocation | It selects attached-worker or in-process mode for that invocation. |
166
166
  | `session.idle_exit_minutes`, `session.keep_empty`, `session.max_children`, `session.delegate_reports`, `session.task_wakes`, `session.max_wakes` | Worker start or next matching lifecycle decision | Existing workers keep their idle-exit timer; the other values are read for the next corresponding decision. |
167
167
  | `session.retention_*`, `session.max_count`, `session.keep_status`, `session.sweep_interval_hours` | Next retention sweep | `chi web` sweeps on its configured schedule and on `GET /api/sessions`; `chi sessions prune` uses the values for that invocation. The sweep interval is checked on the next hub tick. |
168
- | `web.*` | `chi web` start | Restart `chi web` to change its bind address, port or page settings. |
168
+ | `web.*` except `web.participants.*` | `chi web` start | Restart `chi web` to change its bind address, port or page settings. |
169
169
  | `log.*` | Process start | Each process holds its configured log sink and level after first use; restart that process. |
170
170
  | `history.file` | Next history access | The TUI and web composer resolve its path when they read or append history. |
171
171
  | `broadcast.*` | Next `chi broadcast` | Each broadcast invocation reads its targeting and triage settings. |
@@ -1391,7 +1391,7 @@ toolkit:
1391
1391
  default: lean # the sessions you start (unset: full)
1392
1392
  child: child # delegate children (unset: full)
1393
1393
  kits: # your own, on a base
1394
- review: { base: lean, add: [web_fetch, delegate], remove: [memory_write] }
1394
+ audit: { base: lean, add: [web_fetch, delegate], remove: [memory_write] }
1395
1395
  terse: { base: lean, prompt: "- Be concise in your responses" }
1396
1396
  ```
1397
1397
 
@@ -1402,13 +1402,14 @@ toolkit:
1402
1402
  | `child` | `lean` plus `send_note`, without `memory_write`, `delegate`, `delegate_result` |
1403
1403
  | `coordinator` | `lean` plus `delegate`, `delegate_result`, `list_sessions` (declared by the coordinator bundle) |
1404
1404
  | `coding` | `child`, with the prompt text `- Be concise in your responses` / `- Show file paths clearly when working with files` (declared by the coordinator bundle, for build and fix children) |
1405
+ | `review` | `read`, `memory_read` (not `forget_outputs`, even under the `forget` layer): what a [participant's](internals/participants.md#turns) turn may call, whatever the session declares (the review boundary, [guardrails](guardrails.md#a-participants-review-boundary)). Reserved: `toolkit.kits.review` is refused ("review is reserved for participants' turns"), and a bundle can't declare it. A session may run on it too, as any kit |
1405
1406
 
1406
1407
  - **Plugin tools** are declared in every toolkit unless its `remove:` names them, and so is `forget_outputs` under
1407
1408
  the `forget` layer. A `kits:` entry takes `base:` (default `full`), `add:`, `remove:` and `prompt:` (any other
1408
- key, a typo or a `tools:` list, is ignored with a warning); it can't reuse a built-in's name. A bundle declares its own with `chi.toolkit` ([plugins](plugins.md#chitoolkitname-base-add--remove--prompt-nil)).
1409
+ key, a typo or a `tools:` list, is ignored with a warning); it can't reuse a built-in's name (`review`'s least of all: it is the participants' boundary). A bundle declares its own with `chi.toolkit` ([plugins](plugins.md#chitoolkitname-base-add--remove--prompt-nil)).
1409
1410
  - **A kit's prompt text.** `prompt:` (text, several lines allowed) goes into the system prompt of the sessions on
1410
1411
  that kit, verbatim, with no header: after AGENTS.md and the appended files, before the memory indexes. A kit
1411
- without one inherits its base's (`review: {base: terse, add: [web_fetch]}` stays concise); `prompt: ""` clears
1412
+ without one inherits its base's (`audit: {base: terse, add: [web_fetch]}` stays concise); `prompt: ""` clears
1412
1413
  it; a session's own `--tool` / `/toolkit add` never touch it. Sessions on the same kit share it in the prompt
1413
1414
  cache; `full` and every kit without a prompt send the bytes they sent before. Keep it to a line or two that
1414
1415
  fits the kit's work (the coordinator bundle's `coding`: be concise): one that isn't text or is over 2000
@@ -1719,6 +1720,9 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
1719
1720
  | `web.working_set` | `true` | yes | The stage view's working set: the files, memories and URLs a turn brought into the model's context, in a rail in the stage, a footprint line under each finished turn and a `set N` chip in place of the mem chip; `false` brings the mem chip back. `?working_set=1` or `?working_set=0` on the page URL overrides it for that page load; the turn view ignores it. See [CLI: the working set](cli.md#the-working-set). |
1720
1721
  | `web.annotate_presets` | `Agreed\|Could you please elaborate?` | yes | Quick replies next to Annotate in `chi web`, `\|`-separated (a YAML list works too); `""` in the file or on the CLI leaves only Annotate (an empty env value means the default). See [CLI](cli.md#web-annotate-presets). |
1721
1722
  | `web.editor` | `vscode` | yes | What a file ref in `chi web` (a file tool row's title, a diff line) opens, for a viewer on this machine: a preset (`vscode`, `vscode-insiders`, `cursor`, `zed`) or a URL template with `{path}` and an optional `{line}`; `none` turns refs off. See [Opening files from chi web](#opening-files-from-chi-web). |
1723
+ | `web.participants.expires` | `7d` | | How long a participant's link lasts unless `chi share --expires` says otherwise: `Nh` or `Nd`, 1h to 30d. See [participants](internals/participants.md). |
1724
+ | `web.participants.cap` | `2.0` | | What a participant's turns, and the side calls they cause, may cost you in dollars unless `chi share --cap` says otherwise ($0 to $1000); at the cap their next comment is refused (429 `cap_reached`) and you get a notice. `chi share cap ID P DOLLARS` changes one grant's. |
1725
+ | `web.participants.queue` | `3` | | How many of a participant's turns may wait behind their running one (0 to 20); one more is refused (429 `too_many_turns`). |
1722
1726
  | `history.file` | state dir | | Prompt history path, shared by the TUI and the web composer. |
1723
1727
  | `no_interrupt` | `false` | `--no-interrupt` | Raise the step limit of a turn to 1000 (or `turn.max_iterations`, when larger); a top-level key. |
1724
1728
  | `no_default_input` | `false` | `--no-default-input` | Don't pre-fill `default.input`; a top-level key. |
data/docs/guardrails.md CHANGED
@@ -298,6 +298,50 @@ card still names `child-boundary`. The parent (never a delegate), forks and
298
298
  ordinary sessions don't get it. Like the rules it reads command text: a
299
299
  `sh -c` body, a script or `$(…)` isn't looked into.
300
300
 
301
+ ## A participant's review boundary
302
+
303
+ Built in, for a [participant's](internals/participants.md#turns) turns only (a
304
+ turn whose author is `p:<id>`), in every mode (also with
305
+ `guardrails.enabled: false`) and without the guardrails bundle:
306
+ `Guardrails::ReviewBoundary`, rule `review-boundary`, source `core`. It comes
307
+ first among the core checks, before the hooks' asks and the rules, and it
308
+ **denies, never asks**: nobody on the participant's side may approve, and the
309
+ owner's approval would give the participant's text the owner's tools. A
310
+ participant's turn may call only the `review` kit's tools:
311
+
312
+ - `read`, under the session's project root (the work tree top of its folder)
313
+ and never a secret: chi's state and config dirs (sessions, the grants, the
314
+ web token, memories and their `.bundles`, `config.yml`, hooks), `~/.ssh`,
315
+ `~/.gnupg`, `~/.aws`, and the project's `.env*`, `.netrc`, `*.pem`, `*.key`,
316
+ `*.p12`, anything named `credentials` (`.git-credentials` too), `.npmrc`,
317
+ `.pypirc`, ssh keys (`id_rsa`, `id_ed25519`, …), `*.tfstate`,
318
+ `secrets.yml`/`.json`, `.docker/config.json`, `.kube/config` and any config
319
+ under `.git` (a submodule's too). The path is checked as given and as it
320
+ resolves, so a symlink in the project to `~/.ssh` is `~/.ssh`. A session
321
+ whose folder is the home folder or above it reads nothing.
322
+ - `memory_read` with `scope: project` (a read without a scope would fall back
323
+ to the system memories). The prompt's preloaded memories and the memory
324
+ index are in the system prompt anyway: a review session should start
325
+ without personal ones.
326
+
327
+ Everything else is denied with "a participant's review turn may only read …",
328
+ and the model is told to propose the change as text or a diff for the owner
329
+ to apply: `write`, `edit`, `memory_write`, `execute`, the task tools,
330
+ `delegate`, `send_note`, `list_sessions`, `register_reminder` (it would fire
331
+ an owner turn later), `web_fetch`, `context_read`, `ask_user_question`,
332
+ `forget_outputs` (it would replace the owner's earlier tool outputs with the
333
+ participant's note, which later owner turns would read as their own), and
334
+ every plugin or MCP tool, including through `call_tool` (a built-in reached
335
+ through `call_tool` is checked as that built-in). The session's declared tools
336
+ stay its own (no prompt rebuild per author), so the model may still try a
337
+ write; the turn's header ("This turn may only read") keeps that rare.
338
+
339
+ The kit is `Toolkit::REVIEW`, a frozen built-in: config's `toolkit.kits.review`
340
+ is refused with one message ([configuration](configuration.md#toolkits)), so
341
+ config can't widen the boundary. Hooks still run on a participant's turn, with
342
+ the owner's power ([hooks](hooks.md#participants-turns)); a hook that rewrites a
343
+ call is checked on the call it leaves.
344
+
301
345
  ## Rules in config.yml
302
346
 
303
347
  ```yaml
@@ -704,6 +748,18 @@ That is the residual risk, documented rather than engineered away: on a
704
748
  session with sensitive tools, quote the proposal you want applied in your own
705
749
  turn instead.
706
750
 
751
+ Within its own turn a participant's text can't act: the review boundary
752
+ leaves it reads of the project only, and an injection that convinces the
753
+ model still meets the boundary (containment over detection). What it can do
754
+ is make the model *say* what the history holds: a review turn's model sees the
755
+ whole conversation, earlier tool outputs included, so start a review in a
756
+ fresh session (or a continue), not on a working session full of outputs. A
757
+ line of a participant's text that looks like one of chi's own headers goes
758
+ out quoted (`(quoted) [Steer from the user, …]`), under the real one, in any
759
+ case and after any spaces (chi's context-note and task frames too). The owner's
760
+ due reminders wait for the owner's next turn: they never go into a
761
+ participant's.
762
+
707
763
  ## Limits
708
764
 
709
765
  Text matching on shell commands stops accidents, not a model set on getting
data/docs/hooks.md CHANGED
@@ -74,6 +74,15 @@ of the hook about to run) and the callables `notify:`, `ask_user:`,
74
74
  undefined behaviour. `:before_tool_call` carries no messages (the gate stays
75
75
  cheap).
76
76
 
77
+ ## Participants' turns
78
+
79
+ A [participant's](internals/participants.md#turns) review turn fires the same hooks, and every event of it carries
80
+ `author: "p:<id>"` (the owner's carry no `author:` key, as before); a plugin reads the same as `ctx.turn_author`
81
+ (nil for the owner's turns and between turns). **A hook runs with the owner's power whatever the author**: it may
82
+ read, write and run what the owner's chi may, and `notify`, `ask_user` and `steer` reach the owner. A bundle that
83
+ acts on a turn's text (a prompt rewrite, a fetch of what it names) should check `author:` first: the text may be a
84
+ participant's. The review boundary checks a tool call after the hooks, on the call they leave.
85
+
77
86
  ## What a hook can do: the runtime
78
87
 
79
88
  Besides reading (and, on `:before_tool_call`, voting on) its event, a hook
@@ -481,7 +481,9 @@ TUI. Errors in the sink or an observer are caught; they never break the turn.
481
481
 
482
482
  `Engine#announce` puts an event that belongs to no turn (`ANNOUNCEABLE_EVENTS`: `turn_enqueued`, `turn_dequeued`
483
483
  (a queued message taken back before it ran, `{enqueued_id, client_id}`: the remover's), `card`,
484
- `hook_notice`, `guardrail_warning`, `plugin_init_started`, and more) to the observers only. `:side_usage`
484
+ `hook_notice`, `guardrail_warning`, `plugin_init_started`, `participants_changed` (`{participants, joined, left}`:
485
+ who is in the review now and who was invited, revoked or expired since the worker last looked; `ParticipantsWatch`,
486
+ [participants](participants.md#the-owners-terminal)), and more) to the observers only. `:side_usage`
485
487
  (`{kind, side}`: a side request's usage, the recap's, `/btw`'s or `ask_model`'s, and the session's side sums) goes
486
488
  to the observers the same way from `Engine#note_side_usage` (any thread), without being announceable.
487
489
 
@@ -535,9 +537,25 @@ Where a participant's author (`"p:<id>"`; `Steer.participant_author?`: any autho
535
537
  `Steer.review_header`, both loops through `Steer.wire_text`; see prompt-caching.md). Saved raw.
536
538
  - **`Session#last_turn.author`** (`LastTurn#origin` stays the category) and the turn's analytics record's `author`
537
539
  (`SessionMetrics`).
538
- - **Commands and queued turns**: `QueuedCommand#author` (a `/word` input file, `WorkerInbound#take_input`, or a
539
- `/word` POST /turn, `Bridge#queue_command_request`), in `command_queued`; `turn_enqueued`; the accumulator's
540
- queued rows.
540
+ - **Queued turns**: `turn_enqueued`; the accumulator's queued rows. A participant's `/word` or `!line` is never a
541
+ command: `Bridge#handle_post_turn` answers it 403 and `WorkerInbound#queue_as_command` drops it (so
542
+ `QueuedCommand#author` stays nil on every path now).
543
+ - **Delivery**: a participant's input file always waits for a turn of its own (`SessionInbox.waits_for_turn_end?`),
544
+ so `Worker#pending_input_drain` never merges it; the Bridge queues their `POST /turn` whatever its delivery and
545
+ counts their running and queued turns against chi web's `pending_limit` (429 `too_many_turns`).
546
+ `Worker#run_prompt` checks their link and cap (`participant_refusal`: `participants.json`'s entry and cap against
547
+ `SessionMetrics#spend`) before their turn starts and after it ends (the owner's notice, once per cap). A Continue
548
+ keeps the offer turn's author (`ContinueOffer#run_continue_turn`, from `last_turn.author`).
549
+ - **The gate**: `GuardrailWiring#review_boundary` (`Guardrails::ReviewBoundary`) is the first core check; it reads
550
+ `#turn_author` and denies a participant's turn all but `Toolkit::REVIEW_TOOLS` (not `forget_outputs`)
551
+ (docs/guardrails.md). The declared tools stay the session's (D3: no rebuild per author).
552
+ - **Hooks and plugins**: `Engine#turn_author` (the running turn's participant author, nil otherwise) is the hook
553
+ runtime's `turn_author` (`Hooks::Registry#with_runtime` adds `author:` to every event of such a turn) and the
554
+ plugin host's (`ctx.turn_author`).
555
+ - **Spend**: the analytics turn record's `author` feeds `SessionMetrics#spend` / `spend_by_author` (the snapshot's
556
+ `spend_by_author`, `/stats`); `Engine#note_side_usage` stamps a side request's author (`#side_author`: the running
557
+ turn's, the recap's after it, else the owner's) into `SideTotals#by_author`.
558
+ - **Side models**: `IdleRecap::TranscriptFilter` and `Plugin::SideQuestion` name a participant's message as theirs.
541
559
  - **The system prompt**: `Engine#refresh_participants!` at a turn's start (after `apply_toolkit!`) rebuilds the
542
560
  prompt when the session gains its first active participant or loses its last (the participants line).
543
561