samagotchi 0.122.0 → 0.124.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 +58 -1
  3. data/bin/chi +13 -2
  4. data/docs/architecture.md +36 -7
  5. data/docs/cli.md +48 -3
  6. data/docs/configuration.md +31 -6
  7. data/docs/desktop.md +51 -3
  8. data/docs/guardrails.md +112 -15
  9. data/docs/internals/engine.md +11 -4
  10. data/docs/internals/participants.md +67 -17
  11. data/docs/internals/prompt-caching.md +19 -7
  12. data/docs/internals/web-push.md +7 -3
  13. data/docs/sessions.md +7 -3
  14. data/docs/testing.md +2 -1
  15. data/lib/samagotchi/bridge.rb +4 -1
  16. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  17. data/lib/samagotchi/config.rb +7 -1
  18. data/lib/samagotchi/delivery.rb +7 -0
  19. data/lib/samagotchi/desktop/macos/App.swift +9 -1
  20. data/lib/samagotchi/desktop/macos/ChiRunner.swift +129 -17
  21. data/lib/samagotchi/desktop/macos/Images.swift +4 -3
  22. data/lib/samagotchi/desktop/macos/Panel.swift +315 -80
  23. data/lib/samagotchi/desktop/macos/Sessions.swift +272 -0
  24. data/lib/samagotchi/engine.rb +200 -29
  25. data/lib/samagotchi/gem_update.rb +41 -4
  26. data/lib/samagotchi/guardrail_wiring.rb +45 -12
  27. data/lib/samagotchi/guardrails/approval.rb +42 -4
  28. data/lib/samagotchi/guardrails/gate.rb +1 -1
  29. data/lib/samagotchi/guardrails/pair_boundary.rb +172 -0
  30. data/lib/samagotchi/guardrails/pair_command.rb +365 -0
  31. data/lib/samagotchi/guardrails/review_paths.rb +41 -7
  32. data/lib/samagotchi/guardrails/shell_lex.rb +7 -5
  33. data/lib/samagotchi/guardrails/shell_paths.rb +10 -1
  34. data/lib/samagotchi/guardrails/verdict.rb +7 -0
  35. data/lib/samagotchi/guardrails.rb +1 -0
  36. data/lib/samagotchi/guardrails_command.rb +6 -1
  37. data/lib/samagotchi/kernel_loop.rb +20 -0
  38. data/lib/samagotchi/llm/chat_loop.rb +4 -0
  39. data/lib/samagotchi/participant_mode.rb +77 -3
  40. data/lib/samagotchi/participants.rb +7 -5
  41. data/lib/samagotchi/review_apply.rb +10 -2
  42. data/lib/samagotchi/review_start.rb +38 -10
  43. data/lib/samagotchi/run_options.rb +43 -2
  44. data/lib/samagotchi/session_access/local.rb +5 -2
  45. data/lib/samagotchi/session_manager.rb +11 -0
  46. data/lib/samagotchi/session_participants.rb +18 -3
  47. data/lib/samagotchi/share_command.rb +2 -2
  48. data/lib/samagotchi/steer.rb +7 -1
  49. data/lib/samagotchi/system_one/client.rb +10 -2
  50. data/lib/samagotchi/system_prompt.rb +20 -4
  51. data/lib/samagotchi/terminal_ui/question_prompt.rb +17 -1
  52. data/lib/samagotchi/tool_runner.rb +10 -1
  53. data/lib/samagotchi/toolkit_binding.rb +34 -0
  54. data/lib/samagotchi/toolkit_report_command.rb +6 -4
  55. data/lib/samagotchi/toolkit_suggest.rb +181 -15
  56. data/lib/samagotchi/tools/builtins.rb +14 -1
  57. data/lib/samagotchi/update_command.rb +47 -19
  58. data/lib/samagotchi/version.rb +1 -1
  59. data/lib/samagotchi/web/app.rb +204 -9
  60. data/lib/samagotchi/web/grants.rb +17 -5
  61. data/lib/samagotchi/web/message_display.rb +6 -1
  62. data/lib/samagotchi/web/participant_review.rb +3 -1
  63. data/lib/samagotchi/web/participant_view.rb +12 -4
  64. data/lib/samagotchi/web/participants_admin.rb +10 -6
  65. data/lib/samagotchi/web/public/app.js +73 -10
  66. data/lib/samagotchi/web/public/data.js +17 -0
  67. data/lib/samagotchi/web/public/delivery_line.js +8 -6
  68. data/lib/samagotchi/web/public/index.html +21 -2
  69. data/lib/samagotchi/web/public/participant.html +1 -1
  70. data/lib/samagotchi/web/public/participant.js +10 -12
  71. data/lib/samagotchi/web/public/participant_model.js +20 -8
  72. data/lib/samagotchi/web/public/participant_names.js +29 -8
  73. data/lib/samagotchi/web/public/participants_panel.js +17 -3
  74. data/lib/samagotchi/web/public/question_card.js +12 -2
  75. data/lib/samagotchi/web/public/question_cards.js +7 -0
  76. data/lib/samagotchi/web/public/stream_handlers.js +6 -2
  77. data/lib/samagotchi/web/public/toasts.js +13 -5
  78. data/lib/samagotchi/web/public/tools_overview.js +42 -0
  79. data/lib/samagotchi/web/public/turn_events.js +5 -4
  80. data/lib/samagotchi/web/public/turn_html.js +2 -2
  81. data/lib/samagotchi/web/public/turn_view.js +6 -4
  82. data/lib/samagotchi/web/public/update_notice.js +33 -4
  83. data/lib/samagotchi/web/public/update_sheet.js +158 -0
  84. data/lib/samagotchi/web/push/notifier.rb +28 -1
  85. data/lib/samagotchi/web/server.rb +121 -8
  86. data/lib/samagotchi/web/session_access_routes.rb +2 -1
  87. data/lib/samagotchi/web/session_hub.rb +52 -3
  88. data/lib/samagotchi/web/tunnel/tailscale.rb +8 -2
  89. data/lib/samagotchi/web/update_check.rb +186 -0
  90. data/lib/samagotchi/worker.rb +44 -6
  91. metadata +7 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9ba0370c70db9c98a500766ef59cd8cedbf574852c60625679ed84c6a78d50d0
4
- data.tar.gz: 937ffbf1066bc4acd34aaaf0ac19680c5099191f0bc87b8681863b362a280940
3
+ metadata.gz: e85ff88270cf7569d21ff95bba739199ceb3c53d32675b1648a18363ca5482cd
4
+ data.tar.gz: d9f539982d7c41b94ad40d17368041f2debed8040ebc48a27776486fe2476dc9
5
5
  SHA512:
6
- metadata.gz: c351af56a92b5df2cfabbd2d8a52ffefd76b1d7be74a92ffacd25f035d5ab6951214aa083a4d016beefd924cf2794d802c91ddf9ca4b437c860716f99e13ea76
7
- data.tar.gz: f00a99184817060ca6d8741bee006bbdcacc3438896a48801a49e6694f4cb2c41868cbabca6f7cd27f807a475f752bf1a62acc580104159036dc033f7611965c
6
+ metadata.gz: e3496206b25f1d329a3a915c075cefe5e0c3e1fc40b71694128c7d3fee633fc65447686efbb4c8f4f19e0872998cecb2f269db67a7428b9b519a9cc5b6469bd3
7
+ data.tar.gz: 3ed82d4bb9b01288f391e2d68d9eba5b71492fa66f411d7a5fd25276ac68425f8432979fd0d4d59882b741a2dfa79bcb0810fc74e3b2c7e66bb590e0c5bdbccc
data/CHANGELOG.md CHANGED
@@ -7,6 +7,61 @@ and commands may change between minor versions. How releases are made:
7
7
  [docs/releasing.md](docs/releasing.md).
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.124.0] - 2026-10-27
11
+
12
+ ### Added
13
+
14
+ - The desktop helper's panel lists and sends to the sessions of the remotes `chi remote add` keeps: a section per
15
+ remote ("on studio") after this Mac's rows, each row tagged with its machine, "→ studio" in the hint while one is
16
+ ticked, one call per machine at once; a remote that doesn't answer within 4 s or refuses shows one line, and this
17
+ Mac's rows don't wait for it. A remote with a session also gets a "New session on studio in <folder>" row, in its
18
+ newest session's folder, on that remote's default model. The Everyone row, the model chooser and agents stay on
19
+ this Mac.
20
+ - chi web updates chi from its page: the notice's **Update** installs the release it names in a progress sheet
21
+ that lists kept edits, then offers **Restart workers** (each worker on an older chi; a busy one says what holds
22
+ it) and **Restart chi web** (in place: same port, token, flags and Tailscale Serve). `chi update --to VERSION`
23
+ installs an exact release; `--json` prints its rows as JSON.
24
+
25
+ Update with `chi update` (it rebuilds the desktop helper), then restart `chi web` and the session workers
26
+ (`chi sessions restart`). From the next release on, chi web's Update button does this from its page.
27
+
28
+ ## [0.123.0] - 2026-10-27
29
+
30
+ ### Added
31
+
32
+ - chi web checks RubyGems daily and shows the newest aged, stable release newer than both the running and installed
33
+ chi, with its RubyGems page and a command to copy (`chi update`, or the blocker-specific command).
34
+ - chi web sends one 24-hour phone push per newly available chi; it skips the push when a chi web page is in front.
35
+ - `toolkit.suggest.mode: on` starts a new session on the kit System 1 suggests for its first prompt (or a later one
36
+ after `undecided`), waiting at most System 1's timeout before the first request, with a
37
+ `toolkit: NAME (suggested, p=…) · /toolkit to change` notice; anything uncertain keeps the default kit.
38
+ - Pair access for a shared working session: a participant invited to a working session (not a `chi review`
39
+ session) while its worker runs pair turns gets turns that act with the session's tools under the owner's
40
+ guardrails; the grant is stamped `mode: pair` only when the share warning said so. Existing grants, and any invited
41
+ while the worker was older, stay read-only review participants until they are invited again. A pair turn's asks
42
+ reach the owner's card naming who asks, with Allow once only, and System 1 never answers them; a write outside the
43
+ project always asks, whatever the owner approved before. A new core check (`pair-boundary`) denies reading or
44
+ writing secrets and chi's own files (keys, shell startup files, histories, credential files; by a file tool, a
45
+ command, a glob, a variable or a script it hands a shell), running chi, backgrounding commands, reading the
46
+ environment or the keychain, git settings that run commands, sweeping home with find/tar/cp/grep, the owner-voiced
47
+ tools (delegate, reminders, notes, background tasks,
48
+ forgetting outputs, loading tools, memories) and the model's own question. A pair turn stops mid-turn once the
49
+ participant's cap is passed or their link is revoked or expires. A pair turn's commands run without
50
+ secret-named environment variables and are killed at the execute limit, and one that writes outside the project
51
+ asks the owner. Workers name a `pair_turns` feature.
52
+ - Participant turns show a one-line tools overview with expandable, names-only rows that update as a turn runs.
53
+
54
+ ### Changed
55
+
56
+ - Sharing a working session warns what a participant's turns may do there (writes included, asks to you) instead of
57
+ only how many tool outputs they can quote. Its participant messages carry a pair header, a `pair` badge and no
58
+ Apply; review sessions, their messages and their headers are unchanged. The system prompt's participants line,
59
+ the cap notice, the participant page's note and the participants panel follow the session's mode; the delivery
60
+ line says "after their turn".
61
+
62
+ Update with `chi update`, then restart `chi web` and the session workers (`chi sessions restart`); a participant
63
+ invited before this release stays a review participant until invited again.
64
+
10
65
  ## [0.122.0] - 2026-10-27
11
66
 
12
67
  ### Added
@@ -4658,7 +4713,9 @@ and long-lived sessions.
4658
4713
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
4659
4714
  hotkey panel that send selected text or the clipboard to your sessions.
4660
4715
 
4661
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.122.0...HEAD
4716
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.124.0...HEAD
4717
+ [0.124.0]: https://github.com/dm1try/samagotchi/compare/v0.123.0...v0.124.0
4718
+ [0.123.0]: https://github.com/dm1try/samagotchi/compare/v0.122.0...v0.123.0
4662
4719
  [0.122.0]: https://github.com/dm1try/samagotchi/compare/v0.121.1...v0.122.0
4663
4720
  [0.121.1]: https://github.com/dm1try/samagotchi/compare/v0.121.0...v0.121.1
4664
4721
  [0.121.0]: https://github.com/dm1try/samagotchi/compare/v0.120.0...v0.121.0
data/bin/chi CHANGED
@@ -18,6 +18,15 @@ rescue Samagotchi::ConfigFile::RemovedApi => e
18
18
  end
19
19
  require "samagotchi"
20
20
 
21
+ # A web self-restart re-enters through the installed wrapper: its marker and
22
+ # the worker starters to adopt leave ENV before anything spawns with it
23
+ # (Server.take_restart_env!); a restarted chi web skips the first-run hint.
24
+ web_restarted = false
25
+ if ENV.key?("SAMAGOTCHI_WEB_RESTARTED") || ENV.key?("SAMAGOTCHI_WEB_ADOPT_PIDS")
26
+ require "samagotchi/web/server"
27
+ web_restarted = Samagotchi::Web::Server.take_restart_env!
28
+ end
29
+
21
30
  # `chi self`: print version, source dir, config/memory/session paths, model and
22
31
  # bundles (read-only, offline) — lets the agent find its own code and state.
23
32
  if ARGV[0] == "self"
@@ -221,7 +230,7 @@ chi_update_hint = lambda do
221
230
  end
222
231
 
223
232
  if options[:web]
224
- chi_update_hint.call
233
+ chi_update_hint.call unless web_restarted
225
234
  require "samagotchi/web/server"
226
235
  port = options[:web_port] || Samagotchi::Config.get("web.port")
227
236
  markdown = options[:web_markdown]
@@ -233,7 +242,9 @@ if options[:web]
233
242
  annotate_presets: Samagotchi::Config.get("web.annotate_presets"),
234
243
  editor: Samagotchi::Config.get("web.editor"),
235
244
  start_dir: options[:start_dir] || Samagotchi::Config.get("web.start_dir"),
236
- start_dir_from: options[:start_dir] ? :flag : :config)
245
+ start_dir_from: options[:start_dir] ? :flag : :config,
246
+ restarted: web_restarted,
247
+ restart_argv: Samagotchi::RunOptions.web_restart_argv(options))
237
248
  end
238
249
 
239
250
  # --resume and --attach take a unique id prefix too, like git.
data/docs/architecture.md CHANGED
@@ -533,12 +533,41 @@ The snapshot names chi's version: a tab served by another one (its `<body data-v
533
533
  reload in a toast, since an open tab keeps its old JS across a `chi web` upgrade. It also names
534
534
  `installed`, the newest chi on this machine (`InstalledVersions`: the highest `samagotchi-<v>.gemspec`
535
535
  in RubyGems' specification dirs, or a checkout's `version.rb` on disk; looked at again on each full
536
- probe, every 10 s), and a change is a `chi` frame `{version, installed}`. A newer one than the
537
- server runs is said in chi web's terminal and on the page: restart chi web (its workers already
538
- run the newest chi, [Sessions](sessions.md)). `GET /api/info` carries `installed` too, and the
539
- feature `newest_workers`; next to `cwd` (the folder chi web runs in) it names `start_dir`, the folder it
540
- starts in (`web.start_dir`, `--start-dir`; `Web::StartDir`), which a second `chi web` given another one
541
- mentions.
536
+ probe, every 10 s). A `chi` frame and the `GET /api/events` snapshot carry the full state
537
+ `{version, installed, available, update_blocked}`; `/api/info` and the session-command wake response
538
+ carry the same fields. `SessionHub#update_available` publishes the checker result under the hub's
539
+ Monitor, but fetching never holds it.
540
+
541
+ `Web::UpdateCheck` runs its own thread beside the hub. When `update.notify: on` (the default), its
542
+ first RubyGems check is about 30 seconds after chi web starts, then it checks at `update.check_hours`
543
+ intervals. A stable release is available only after `update.min_age_hours` and when newer than both
544
+ the running and newest installed chi. Its atomic, mode-0600
545
+ `$XDG_STATE_HOME/samagotchi/update_check.json` keeps the check cooldown and versions already
546
+ announced across restarts. `update.notify: off` starts no checker thread and makes no network
547
+ request. `update_blocked` names why the local gem cannot be updated in place (checkout, Bundler, or
548
+ an unwritable gem directory). The page tells the owner to run `chi update`; a newer `installed`
549
+ version than the server itself still means restart chi web (its workers already run the newest chi,
550
+ [Sessions](sessions.md)). `GET /api/info` also names `newest_workers`; next to `cwd` (the folder chi
551
+ web runs in) it names `start_dir`, the folder it starts in (`web.start_dir`, `--start-dir`;
552
+ `Web::StartDir`), which a second `chi web` given another one mentions.
553
+
554
+ The owner's update routes publish progress and results as `chi_update` events with an
555
+ `operation` (`install` or `workers`), `state`, and `rows`; install events also name the target
556
+ `version` and may include a progress line. The hub retains the latest event for a reconnecting
557
+ tab's snapshot. `POST /api/update` decides itself whether this install can take a gem
558
+ (`UpdateCheck.blocked_reason`: a checkout, Bundler, an unwritable gem dir → 409 `update_blocked`), so
559
+ it holds with `update.notify: off`; a child that prints no rows fails with its own last line, and a
560
+ done install refreshes the hub's installed version (`SessionHub#refresh_installed`) before its `done`.
561
+ Worker restarts target every worker older than the version installed now (refreshed the same way)
562
+ and publish per-session results. Only one of an install, a worker restart pass and a web restart runs
563
+ at a time (409 `update_running`). `POST /api/update/restart_web` (409 `restart_unavailable` without a
564
+ chi wrapper to exec) schedules WEBrick shutdown only after its response is sent; after the hub and
565
+ listener stop, `Server` execs the RubyGems chi wrapper with every flag chi web was started with, as
566
+ `RunOptions`' parser recorded them (`RunOptions.web_restart_argv`), but the one-shot `--open` and
567
+ `--new-token`. The re-entry (`SAMAGOTCHI_WEB_RESTARTED`, taken out of ENV first by
568
+ `Server.take_restart_env!`) skips the first-run update hint, adopts live worker-starter PIDs (workers
569
+ themselves are detached), and adopts an exact Tailscale Serve handler. A failed exec, or a restarted
570
+ chi web that doesn't start for any reason, turns off only that exact adopted handler.
542
571
 
543
572
  The page's scope is in its URL, and the server picks the default: `?dir=<folder>` is that folder's
544
573
  project (the list, the events stream and a new chat follow it), `?scope=all` every session, and a bare
@@ -775,4 +804,4 @@ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/
775
804
  | Stores | `reminder_store.rb` (`ReminderStore`), `recap_store.rb` (`RecapStore`) | A session's reminders, and its idle recap (`recap.json`). |
776
805
  | Setup and update | `bootstrap_command.rb`, `bootstrap/`, `update_command.rb`, `gem_update.rb` | `chi bootstrap` (first config.yml, system bundle, core profile) and `chi update` (the gem, shipped bundles, the macOS helper). |
777
806
  | Process groups | `process_group.rb` (`ProcessGroup`) | A command in a process group of its own, stopped as a group (TERM, grace, KILL): execute, tasks, context fetches, the mcp bundle's servers. |
778
- | Desktop | `desktop.rb`, `desktop/macos.rb`, `desktop_command.rb`, `desktop/macos/*.swift` | `chi desktop`, the macOS "Send to chi" helper ([desktop.md](desktop.md)). The helper's Swift side: the panel (`Panel.swift`), the chi CLI runner (`ChiRunner.swift`), agent terminals behind `AgentTerminal` (`AgentTerminal.swift`, `Kitty.swift`, `Tmux.swift`; `ChiHelper --agents` is their CLI seam). |
807
+ | Desktop | `desktop.rb`, `desktop/macos.rb`, `desktop_command.rb`, `desktop/macos/*.swift` | `chi desktop`, the macOS "Send to chi" helper ([desktop.md](desktop.md)). The helper's Swift side: the panel (`Panel.swift`), the chi CLI runner (`ChiRunner.swift`), the session rows and the chi calls a send makes (`Sessions.swift`: `SendPlan`; `ChiHelper --sessions` and `--send-plan` are their CLI seams), agent terminals behind `AgentTerminal` (`AgentTerminal.swift`, `Kitty.swift`, `Tmux.swift`; `ChiHelper --agents` is their CLI seam). |
data/docs/cli.md CHANGED
@@ -27,18 +27,18 @@
27
27
  - `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))
28
28
  - `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))
29
29
  - `chi review FILE… [--dir DIR] [--model M]`, `chi review --from ID [FILE…]` — start a review session to invite participants to: for files of a project, or as a review of a working session that starts from its recap, not its transcript (see [Review sessions](#review-sessions))
30
- - `chi share ID --name NAME [--identity EMAIL] [--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))
30
+ - `chi share ID --name NAME [--identity EMAIL] [--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 a session through chi web on the LAN (read-only review turns in a review session, pair turns that act under your guardrails in a working session) (a link of their own, 7 days, a $2 spending cap), list them, change one's cap, revoke them (see [Participants](#participants))
31
31
  - `chi --remote NAME send|answer|note|context|sessions|share|review …` (or `CHI_REMOTE=NAME`) — those commands on that machine's sessions, through its chi web (see [Remote sessions](#remote-sessions))
32
32
  - `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))
33
33
  - `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))
34
34
  - `chi desktop install|upgrade|uninstall|status` — the macOS "Send to chi" helper: a Service and a ⌃⌥⌘N hotkey that send text or images to a session (live, stopped or new) as a message (⏎, a turn runs) or a context note (⌘⏎) (see [Desktop helper](desktop.md))
35
35
  - `chi models [--format text|json] [--timeout S] [TEXT]` — list the models every configured host offers, as the names `--model` takes (see [Listing the models](#listing-the-models))
36
36
  - `chi self` — print version, source dir (checkout or installed gem), config/memory/session paths, model/host and bundles. Run by a session's `execute`, the `model` row (and host, loop, profile, thinking, served model) is **that session's** model, labelled `(this session <id8>; default …)`; elsewhere it is the default, `(default)`. `model key` is the memory overlay key (`<name>.<key>.md`), and `model notes` the model notes a prompt of that model loads (`model_notes_deepseek (system, 612 chars)`, `none`; inside a session its `--mute` list is left out). `chi self --model` prints only the model a new session starts on, an alias resolved to its target (the desktop helper's hint)
37
- - `chi update [--dry-run] [--no-gem] [--no-bundles] [--no-desktop]` — update an installed chi: the gem, the system bundle, the shipped bundles you installed and the desktop helper, in one table (see [Updating](#updating))
37
+ - `chi update [--dry-run] [--to VERSION] [--json] [--no-gem] [--no-bundles] [--no-desktop]` — update an installed chi: the gem, the system bundle, the shipped bundles you installed and the desktop helper, in one table (see [Updating](#updating))
38
38
  - `chi bundle install|upgrade|uninstall|status|diff|list|build|trash|new|test|check` — manage memory bundles (see [Bundle hooks](hooks.md#bundle-hooks-unified-workflow-bundle)); `list` shows the installed ones and the ones shipped with chi, which `install <name>` installs; `core` and `dev` are profiles that install a set of them (see [Bundle profiles](memory.md#bundle-profiles-core-and-dev), [Guardrails](guardrails.md), [Plugins](plugins.md#the-btw-bundle), [the mcp bundle](plugins.md#the-mcp-bundle) [the loop-guard bundle](plugins.md#the-loop-guard-bundle), [the check-in bundle](plugins.md#the-check-in-bundle) and [the skills bundle](plugins.md#the-skills-bundle)). `trash` lists and empties the bundle trash (moved files from uninstalls/upgrades). A usage error (an unknown subcommand or flag, a missing argument, a bad `build --scope`) exits 2, as for the other subcommands. Plain `chi` and `chi web` exit 1 on an unknown flag, a flag missing its value or a stray argument. `new`, `test` and `check` are for writing one: see [Writing a bundle](#writing-a-bundle-new-test-check)
39
39
 
40
40
  - `chi guardrails replay [--dry-run] [--threshold T] [--model M]` — seed System 1's record from the approvals this machine's sessions already answered: each ask is judged by the configured model and written to `guardrails/judgments.jsonl` with the answer the user gave, then a per-rule table prints (asks, allowed, denied, would-be auto-allows, false allows, asks with no judgment). A parent agent's answer is not the user's: only its deny is replayed, and only where the parent could have allowed. `--dry-run` judges without writing, `--threshold` overrides `guardrails.system_one.threshold` for the table, `--model` the configured model for this run. Exit 2 when `guardrails.system_one.mode` is off or no `host` is set; a usage error exits 2 (see [System 1](guardrails.md#system-1-experimental))
41
- - `chi toolkit report [--days N]` — grade toolkit suggestion audits: the kit on mode would have run (the suggested kit when it would have applied it, else the default) against the tools the session called from that turn on (only that turn when the toolkit was chosen since), `call_tool` targets included, for both the chat and the raw-prompt loop. Each is *covers*, *short of a tool* (naming the tools), *wider than needed* (a narrower kit offered in the same ask would also have covered) or *ungradable* (calls it can't read back); then a per-kit tally with what on mode would have done. Includes archived sessions and caps the report at 14 days and 500 sessions. Reads `toolkit_suggestions.jsonl` (each ask logs every offered kit's rules, expanded against the called tools here); it never runs the model or changes a session. Local only; `--days` must be a positive whole number.
41
+ - `chi toolkit report [--days N]` — grade toolkit suggestions: the kit on mode would have run (the suggested kit when it would have applied it, else the default; an on-mode line's `did` says what it ran) against the tools the session called from that turn on (only that turn when the toolkit was chosen since), `call_tool` targets included, for both the chat and the raw-prompt loop. Each is *covers*, *short of a tool* (naming the tools), *wider than needed* (a narrower kit offered in the same ask would also have covered) or *ungradable* (calls it can't read back); then a per-kit tally with what on mode would have done. Includes archived sessions and caps the report at 14 days and 500 sessions. Reads `toolkit_suggestions.jsonl` (each ask logs every offered kit's rules, expanded against the called tools here); it never runs the model or changes a session. Local only; `--days` must be a positive whole number.
42
42
 
43
43
  - `chi bundle try PATH -p TEXT [options]` — install a bundle in a temporary, filtered sandbox, run one real model turn,
44
44
  and print its trace (see [Trying a bundle](#trying-a-bundle)).
@@ -233,6 +233,30 @@ Also shipped, not installed: dev (mcp, skills, source-links) (chi bundle install
233
233
  done
234
234
  ```
235
235
 
236
+ - `--to VERSION` installs exactly that gem version instead of checking for the
237
+ newest release. It must be newer than the running chi; an equal or older
238
+ version is refused.
239
+ - `--json` writes the component rows as one JSON array on stdout. Each row has
240
+ `component`, `from`, `to`, `status`, `failed` and `kept` fields; `kept` is an
241
+ array of edited filenames. Install progress is written to stderr.
242
+
243
+ - **chi web update notices.** By default, chi web checks RubyGems about 30
244
+ seconds after it starts and every 24 hours after that. Once a stable release
245
+ is at least 24 hours old and newer than both the running chi and the newest
246
+ installed one, the owner's page shows its version and a link to that
247
+ RubyGems release. When the install is unblocked, **Update** installs that
248
+ exact release and opens a progress sheet with component results and any
249
+ edited files kept. A blocked install instead shows the reason and a command
250
+ to copy. After installation, the notice and the sheet's footer offer
251
+ **Restart workers** and **Restart chi web**; the sheet reports worker results
252
+ (a busy worker's refusal says what holds it) and waits for chi web to
253
+ reconnect, and keeps the other restart in reach. The notice is once per version per browser: a reload or a chi
254
+ web restart doesn't repeat it. `update.notify: off` disables checking;
255
+ `update.check_hours` and
256
+ `update.min_age_hours` tune the interval and release age (see
257
+ [configuration](configuration.md#all-settings)). If the gem can't be updated
258
+ in place, the notice gives the blocker and its command instead.
259
+
236
260
  - **The gem.** It asks rubygems.org for the newest samagotchi (5 s timeout)
237
261
  and, when that's newer, runs `gem install samagotchi` with the gem command
238
262
  of the Ruby chi runs on (the real one, not a mise/rbenv/asdf shim; a chi
@@ -1314,6 +1338,8 @@ CHI_REMOTE=studio chi context push claude-outbox ab12 < answer.json
1314
1338
  studio (http://192.168.1.29:4567): connection refused`, exit 1; with
1315
1339
  `--format json` an `error` object).
1316
1340
  - Plain http, the same as the phone's: use it on your home network.
1341
+ - The macOS desktop helper's panel lists and sends to a remote's sessions too: see
1342
+ [Desktop helper: Remote sessions](desktop.md#remote-sessions).
1317
1343
 
1318
1344
  A session there in this terminal (the attached TUI, through that machine's
1319
1345
  chi web; its worker stays there):
@@ -1423,6 +1449,25 @@ chi share revoke --all # every link to every session
1423
1449
  share cap` raises it. `/stats` shows each participant's spend. Their turns go only to a worker with the review
1424
1450
  boundary: restart an older one. While your turn waits at the step limit (Continue or Stop), their turns wait too
1425
1451
  (still queued, still counted): they run, in order, once you continue, stop or send a turn of your own.
1452
+ - **Pair turns: sharing a working session.** The read-only review turn is a review session's (`chi review`). Invite
1453
+ someone to a working session instead and their turns **act** in it with its tools, writes included, under your
1454
+ guardrails: what your rules allow runs, what they ask about comes to you as "Sam's turn asks: approve tool call?"
1455
+ with **Allow once** only (nothing session-wide or repo-wide is approved from their calls), and System 1 never
1456
+ answers for you. A write outside the project always asks you, whatever you approved before. It can't read or
1457
+ write secrets or chi's own files (keys, shell startup files, histories, credential files; by a file tool, a
1458
+ command, a glob or a variable), run chi, leave a command running in the background, read the environment or the
1459
+ keychain, sweep your home with find/tar/cp/grep, delegate, set or cancel reminders, send notes, start or stop
1460
+ background tasks, load tools, forget outputs or write memories, and it asks its questions in its reply, not on a
1461
+ card. Its commands run without your secret-named environment variables (`KEY`, `TOKEN`, `SECRET`, `PASS`,
1462
+ `CREDENTIAL`, `AUTH` in the name) and are killed at the execute limit rather than left running; a command that
1463
+ writes outside the project asks you, as a write does (the tmp dirs aside). A pair turn stops mid-turn once their
1464
+ cap is passed or their link ends. `chi share` and the panel's Invite
1465
+ warn you with what their turns may do and how many tool outputs the history holds, and only a link issued with
1466
+ that warning acts: links from before, or issued while the session's worker predated pair turns (the warning says
1467
+ so), stay read-only until you restart the worker and invite them again. Their messages carry a `pair` badge and no
1468
+ Apply (they acted already). This is an accident guard for a trusted team, not containment: a pair turn can still
1469
+ send what it reads out through `web_fetch` or your MCP tools. A link without `--identity` gets the same access:
1470
+ share it only with people you'd let type in your session.
1426
1471
  - **The participant page.** Their link opens a page of its own (never yours): the session's title and status, who
1427
1472
  else takes part, and the conversation as a participant sees it: every prompt and reply with who wrote it, each
1428
1473
  step's tools as one line (`read docs/plan.md`: the tool and a project file's path, never arguments or output), and
@@ -163,7 +163,7 @@ or `chi web` restart.
163
163
  | `append_system_prompt` | Worker start for the file list | The config list is not saved in the session file. File text is read when the prompt is built; `/model` rebuilds it, while an already-built prompt is cached. CLI `--append-system-prompt` files are saved in the session. |
164
164
  | `toolkit.default`, `toolkit.child` | Session creation | The chosen toolkit is saved; changing the defaults does not move existing sessions. |
165
165
  | `toolkit.kits`, `execute.description`, `llm_context.policy` | Worker start | These are held by the engine's toolkit or tool registry. |
166
- | `toolkit.suggest.mode`, `toolkit.suggest.threshold`, `toolkit.suggest.defer_turns`, `toolkit.suggest.defer_tokens` | Worker start | Suggestion settings are config-only and are resolved when the worker runs its audit. |
166
+ | `toolkit.suggest.mode`, `toolkit.suggest.threshold`, `toolkit.suggest.defer_turns`, `toolkit.suggest.defer_tokens` | Worker start | Suggestion settings are config-only and are resolved when the worker runs its audit or on-mode ask. |
167
167
  | `skip_agents_md` | Prompt rebuild | A running engine caches its built prompt; a new worker or `/model` rebuild uses the changed value. |
168
168
  | `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. |
169
169
  | `context.every_seconds` | Next context poll | The poller checks it for each source that has no explicit interval. |
@@ -177,7 +177,8 @@ or `chi web` restart.
177
177
  | `log.*` | Process start | Each process holds its configured log sink and level after first use; restart that process. |
178
178
  | `history.file` | Next history access | The TUI and web composer resolve its path when they read or append history. |
179
179
  | `broadcast.*` | Next `chi broadcast` | Each broadcast invocation reads its targeting and triage settings. |
180
- | `update.*` | Next `chi update` | The command reads which components to update when it runs. |
180
+ | `update.gem`, `update.bundles`, `update.desktop` | Next `chi update` | The command reads which components to update when it runs. |
181
+ | `update.notify`, `update.check_hours`, `update.min_age_hours` | `chi web` start | The update checker starts with chi web; restart it to apply changes. |
181
182
  | `kitty.*`, `tmux.*` | `chi desktop upgrade` or desktop update | These settings are copied into the desktop helper's launch file. |
182
183
  | `model.profile`, `reasoning_history` | Not from config.yml | These are CLI/environment settings; `models:` and `hosts:` provide their model- and host-specific forms. |
183
184
 
@@ -1432,13 +1433,34 @@ toolkit:
1432
1433
  prompt, without changing its toolkit or delaying the turn. It offers the configured default as the fallback and
1433
1434
  `undecided` for greetings or check-ins. Only a default-origin session with at least two described candidates is
1434
1435
  asked; chosen or inherited toolkits, delegate children, review sessions and participant turns are skipped. After
1435
- `undecided`, later owner prompts may be asked again within both defer limits. `toolkit.suggest.mode: on` is accepted
1436
- but behaves exactly like `audit` in this build. The default is `off`. System 1's host, model and timeout come from
1437
- `guardrails.system_one.*`; the suggestion threshold and defer limits are its own settings below. Suggestions are
1436
+ `undecided`, later owner prompts may be asked again within both defer limits. The default is `off`. System 1's
1437
+ host, model and timeout come from `guardrails.system_one.*`; the suggestion threshold and defer limits are its own
1438
+ settings below. Suggestions are
1438
1439
  kept in `toolkit_suggestions.jsonl`; `chi toolkit report` grades them against the session's tool calls. One ask
1439
1440
  runs at a time (a turn that comes while one still runs goes unasked, with an info line), and a kit that doesn't
1440
1441
  resolve (a missing base, a cycle) is left out of the candidates. A worker that exits waits about a second for a
1441
1442
  running ask, so a one-shot `-p --non-interactive` run may end before its suggestion is logged.
1443
+ - **On mode.** `toolkit.suggest.mode: on` asks the same question on the same eligible turns, but the turn waits for
1444
+ the answer before it declares any tool, for at most `guardrails.system_one.timeout_ms` from the ask's start (it
1445
+ starts right after the turn does, so plugin and MCP start-up hide most of it). A described kit picked with
1446
+ confidence at or above `toolkit.suggest.threshold` becomes the session's toolkit, saved with `toolkit_origin:
1447
+ suggested`, and the turn's first request declares it; the notice reads
1448
+ `toolkit: web (suggested, p=0.71) · /toolkit to change`. Anything else (the default, a low-confidence or unknown
1449
+ pick, `undecided`, no answer, a failing System 1, which warns once as `[samagotchi:toolkit]`) keeps the default
1450
+ kit. A Stop during the wait (or before it, even with the answer in) ends the turn before its first request, and
1451
+ nothing switches. After an `undecided`, a later prompt within the defer limits may switch the session; that notice
1452
+ says `from this turn`, and that request re-reads the whole prompt (system prompt, tools and conversation, which
1453
+ the defer limits keep small) without `/toolkit`'s cache confirmation. `/toolkit` or `ctx.use_toolkit` marks the
1454
+ toolkit chosen, and nothing is suggested after that; a `/toolkit` sent during a turn runs after it and replaces
1455
+ that turn's suggestion. When System 1's host is the model's own local server (the same `hosts:` name or server
1456
+ root, as audit checks), on mode asks only on a session's first turn, never on the deferred path, and sends no
1457
+ warm-up (one info line). Otherwise an idle worker for an eligible session sends one warm-up ask with the real
1458
+ question and a stand-in prompt so the server has its prefix cached; its answer is dropped and never logged, and
1459
+ the first real ask stops a warm-up still running. Each answered ask's line in `toolkit_suggestions.jsonl` adds
1460
+ `did`: `suggested`, `default`, `defer`, `chosen` (the toolkit was no longer the default), `unavailable` (the
1461
+ picked kit was gone), `timeout` or `canceled` (written when the late answer arrives; a Stop after a timeout
1462
+ doesn't cut it), or `failed` (the turn failed or the switch couldn't be saved); `chi toolkit report` grades
1463
+ on-mode lines by what they did.
1442
1464
  - **Tool-index groups.** `groups:` selects source groups by name or glob (`chrome`, `db*`; any case, `{a,b}` braces,
1443
1465
  as the guardrails rules' `models:`); for the MCP bundle each group is a server. Anything but a name or a list (a
1444
1466
  map such as `{chrome: true}`) refuses every new session on that kit or on a kit built on it, on every surface (chi,
@@ -2003,7 +2025,7 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
2003
2025
  | `context.peer_turns` | `3` | yes | Wake turns in a row a peer agent's answers may start (a peer bundle's outbox source, such as `codex-outbox`), with no message typed in the web or a TUI in between (`chi send` doesn't count); within `session.max_wakes`. Then answers wait as notes, and the UIs hear it once. An answer whose summary ends its verdicts with `VERDICT OK` wakes once to close the round, counted toward `session.max_wakes` only. `0`: a peer source wakes like any other. See [Waking](context.md#waking). |
2004
2026
  | `toolkit.default` | `full` | | The toolkit a new session is stored on: the tools its prompts declare (`full`, `lean`, `child`, one of `toolkit.kits` or a bundle's). `chi --toolkit` picks one per session. See "Toolkits". |
2005
2027
  | `toolkit.child` | `full` | | A delegate child's toolkit, unless its `delegate` call names one (`toolkit:`). See "Toolkits". |
2006
- | `toolkit.suggest.mode` | `off` | | `audit` asks System 1 for a toolkit suggestion without changing the session; `on` currently behaves the same as `audit`. The System 1 host, model and timeout come from `guardrails.system_one.*`. config.yml only. |
2028
+ | `toolkit.suggest.mode` | `off` | | `audit` asks System 1 for a toolkit suggestion without changing the session; `on` waits for it (up to `guardrails.system_one.timeout_ms`) on an eligible turn and starts the session on a confidently suggested kit (Toolkits, On mode). The System 1 host, model and timeout come from `guardrails.system_one.*`. config.yml only. |
2007
2029
  | `toolkit.suggest.threshold` | `0.5` | | Minimum confidence for a non-default toolkit suggestion. config.yml only. |
2008
2030
  | `toolkit.suggest.defer_turns` | `3` | | Maximum owner-prompt number for another ask after `undecided` (the owner's own prompts: not steers, notes, reminders or `!` command output). config.yml only. |
2009
2031
  | `toolkit.suggest.defer_tokens` | `5000` | | Maximum estimated conversation size for another ask after `undecided` (characters / 4). config.yml only. |
@@ -2028,6 +2050,9 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
2028
2050
  | `update.gem` | `true` | | `false`: `chi update` never installs a newer gem (`--no-gem` for one run). See [CLI: Updating](cli.md#updating). |
2029
2051
  | `update.bundles` | `true` | | `false`: `chi update` leaves the shipped bundles to `chi bundle upgrade` (`--no-bundles`). |
2030
2052
  | `update.desktop` | `true` | | `false`: `chi update` leaves the desktop helper alone (`--no-desktop`). |
2053
+ | `update.notify` | `on` | | `on`: chi web checks RubyGems once a day and shows aged stable releases newer than both this chi and the newest installed chi; `off`: no thread or network check. |
2054
+ | `update.check_hours` | `24` | | Hours between RubyGems checks (at least 1). The first check starts about 30 seconds after chi web starts. |
2055
+ | `update.min_age_hours` | `24` | | A release must be this old before chi web offers it; `0` disables the age delay. |
2031
2056
  | `kitty.listen_on` | none | | The desktop helper's kitty socket, copied from `kitty.conf`; unset = no kitty targets. See "Desktop helper: agents in kitty". |
2032
2057
  | `kitty.binary` | `/Applications/kitty.app/Contents/MacOS/kitty` | | The kitty the helper runs `kitty @` with. |
2033
2058
  | `kitty.agents` | `claude\|codex\|gemini\|aider\|opencode\|cursor-agent\|amp\|goose` | | Foreground programs whose kitty windows the helper lists, `\|`-separated (a YAML list works too); `*` lists every window. |
data/docs/desktop.md CHANGED
@@ -41,7 +41,7 @@ alias as the model it names).
41
41
 
42
42
  ### Everyone it concerns
43
43
 
44
- The second row, **Everyone it concerns** (⌘B), shares a note with every session it may concern instead of the ones
44
+ The second row, **Everyone it concerns (this Mac)** (⌘B), shares a note with every session it may concern instead of the ones
45
45
  you pick: ⌘⏎ (the **Broadcast** button) runs [`chi broadcast`](broadcast.md) with the line, then the text, on stdin.
46
46
  chi picks the sessions (a shared ticket, PR or link, else a small triage model), so the preselected session doesn't
47
47
  count: the row is ticked alone, like the new row. Write it the way you'd tell a colleague: a first line such as
@@ -51,6 +51,8 @@ count: the row is ticked alone, like the new row. Write it the way you'd tell a
51
51
  skipped 2 · 1 unchecked: triage deadline`), and **stays open**: Esc closes. Which sessions got it and why:
52
52
  `chi broadcast log` in a terminal.
53
53
  - A broadcast is a text note: ⏎ and images beep. The source field and Send are hidden (its source is `broadcast`).
54
+ - It reaches this Mac's sessions only (`chi broadcast` runs on this machine only), never a
55
+ [remote's](#remote-sessions).
54
56
  - It may take `broadcast.triage_deadline` (20 s) and 10 s more before the helper stops it ("the note may be partly
55
57
  delivered"). The helper reads that from its launch file: after changing the deadline, run `chi update` (or
56
58
  `chi desktop upgrade`).
@@ -106,6 +108,46 @@ thumbnails sit under the message line, 48 px high, the file name as a tooltip; h
106
108
  the panel closes; files from Finder are sent as they are, never touched. A send with images may take up to 30 s
107
109
  (converting, a worker starting) before the panel gives up.
108
110
 
111
+ ## Remote sessions
112
+
113
+ The panel also lists the sessions of the other machines `chi remote add` keeps (see
114
+ [Remote sessions](cli.md#remote-sessions)), say the Studio's from a laptop: select text or take a screenshot here,
115
+ and it lands in a session running there.
116
+
117
+ - **Which:** every remote in `chi remote ls` that has a token, asked at each open (a remote you don't want in the
118
+ panel: `chi remote rm` it). No `chi update` or `chi desktop upgrade` after adding one.
119
+ - **Where:** after this Mac's rows, one section per remote under an **on studio** divider: its live sessions, then up
120
+ to 3 recent ones, as for this Mac. Each row carries a grey **studio** tag and its whole folder path (that machine's
121
+ home may differ from this one's). ⌘1…⌘9 number this Mac's rows first, then the remotes' in order; past nine, a row
122
+ is clicked.
123
+ - **Not twice:** a session shows once, where it is listed first (this Mac, then the remotes in `chi remote ls` order),
124
+ so two remotes for the same Studio (its LAN address and its ts.net name), or a remote that is this Mac, don't
125
+ list or send to it twice.
126
+ - **Wrong machine:** both machines likely have the same paths. While a remote's row is ticked the message line's hint
127
+ ends in **→ studio**, and **→ studio** shows beside Send (it stays there while you type). The last choice is
128
+ preselected while it is still live, a remote's row too; the "only one live session" rule picks this Mac's only,
129
+ never a remote's. A remote that answers after this Mac's list never changes what is ticked: it is preselected only
130
+ when nothing is (so a remembered remote row is ticked when the remote answers first, or when this Mac has live
131
+ sessions but none was remembered).
132
+ - **Keys:** ⏎ and ⌘⏎ work as for this Mac's sessions (`chi --remote studio send …`, `chi --remote studio note …`;
133
+ a note to a stopped session there waits for its next start). Images go along (`--image`: chi uploads them). With
134
+ rows of several machines ticked, the helper runs one call per machine at once and shows one line: `sent · studio:
135
+ sent`. ⌥⏎ (paste) beeps on a remote's row as on this Mac's.
136
+ - **Down or refused:** this Mac's rows show as soon as they are in; each remote's fill in as it answers. A remote's
137
+ two lists run at once and get 4 s together: one that doesn't answer shows **studio: no answer within 4 s** under
138
+ its divider, one that chi gives up on by itself (connection refused, the token refused) the first line of chi's
139
+ error. The previous open's rows for it are never reused: a stale row could send to a stopped worker.
140
+ - **New session there:** a remote with a session has its own new row under its divider, **New session on studio in
141
+ proj**: the folder is that remote's most recently updated session's (live first, then recent), whole and not
142
+ checked on this Mac. A remote with no session has no new row (never a guess at its home). It is ticked alone like
143
+ this Mac's new row (ticking it clears the sessions, and the reverse), is never preselected and has no ⌘ number (⌘0
144
+ stays this Mac's). ⏎ runs `chi --remote studio send --new --dir <that folder>` with the message and images, and
145
+ shows `studio: started 1a2b3c4d…` for 3 s. It starts on the remote's default model, never `--model`: its hint
146
+ says **remote default**, and ⌘M or a click on it beeps with "Model choice is for this Mac's sessions" (`chi models`
147
+ answers for this Mac's hosts).
148
+ - **What stays on this Mac:** the model chooser, the **Everyone it concerns** row (`chi broadcast` is local only)
149
+ and the agents in kitty and tmux.
150
+
109
151
  ## Agents in kitty and tmux
110
152
 
111
153
  The panel can also send to agent CLIs (claude, codex, gemini, aider, …) running in
@@ -276,8 +318,14 @@ The helper uses only these commands, so it could ship on its own later:
276
318
  (`delivered N · skipped M…`); exit 0 when it ran, 1 when refused or a delivery failed.
277
319
  - `chi self --model` → the model a new session starts on (the panel's hint).
278
320
  - `chi models --format json` → the models the hosts offer (the new-session model picker).
279
-
280
- Each call is stopped after 10 s, a send with images after 30 s, a broadcast after the launch file's
321
+ - `chi remote ls --format json` → `[{name, url, token}]` (never the token itself): the remotes whose sessions the
322
+ panel lists. A name goes on only when it has a token and passes chi's own rule (letters, digits, `_`, `-`, not
323
+ starting with `-`). An older chi without `chi remote` lists none.
324
+ - `chi --remote NAME sessions list …`, `chi --remote NAME send …` and `chi --remote NAME note …`: the same calls on a
325
+ remote's sessions. `CHI_REMOTE` is taken out of the helper's environment, so this Mac's calls never go remote.
326
+
327
+ Each call is stopped after 10 s, a remote's two lists after 4 s, a send of text to a remote after 15 s (waking a
328
+ stopped worker there), a send with images after 30 s, a broadcast after the launch file's
281
329
  `broadcast_timeout` (`broadcast.triage_deadline` + 10 s; 30 s when it has none). A stopped `chi note` or `chi
282
330
  broadcast` says the note may be partly delivered.
283
331
 
data/docs/guardrails.md CHANGED
@@ -315,8 +315,11 @@ ordinary sessions don't get it. Like the rules it reads command text: a
315
315
 
316
316
  ## A participant's review boundary
317
317
 
318
- Built in, for a [participant's](internals/participants.md#turns) turns only (a
319
- turn whose author is `p:<id>`), in every mode (also with
318
+ Built in, for a [participant's](internals/participants.md#turns) turns in a
319
+ **review session** only (a turn whose author is `p:<id>`, in a session with a
320
+ `review:` key: `ParticipantMode.of` is `:review`; a working session's
321
+ participant turns get the [pair boundary](#a-participants-pair-boundary)
322
+ instead), in every mode (also with
320
323
  `guardrails.enabled: false`) and without the guardrails bundle:
321
324
  `Guardrails::ReviewBoundary`, rule `review-boundary`, source `core`. It comes
322
325
  right after the load-failure check (rules that don't load deny every call) and before the hooks' asks, the
@@ -359,6 +362,96 @@ config can't widen the boundary. Hooks still run on a participant's turn, with
359
362
  the owner's power ([hooks](hooks.md#participants-turns)); a hook that rewrites a
360
363
  call is checked on the call it leaves.
361
364
 
365
+ ## A participant's pair boundary
366
+
367
+ A participant's turn in a **working session** acts with the session's tools
368
+ under the owner's guardrails, as the owner's own turns do: the rules, the
369
+ protected paths and every approval the owner already granted apply, and an ask
370
+ goes to the owner. It is a pair turn only when all three hold
371
+ (`ParticipantMode.for_turn`, decided once at the turn's start:
372
+ `GuardrailWiring#turn_mode`): the session has no `review:` key, its worker
373
+ names the `pair_turns` feature, and the participant's grant was issued with
374
+ `mode: pair`, which chi share and the web's Invite stamp only when the warning
375
+ they showed said their turns act. A grant from before, or one issued while an
376
+ older worker held the session, keeps review turns until it is invited again.
377
+
378
+ `Guardrails::PairBoundary` (rule `pair-boundary`, source `core`) sits in the
379
+ review boundary's seat in `GuardrailWiring#checks`, in every mode (also with
380
+ `guardrails.enabled: false`), and **denies, never asks**, each with its own
381
+ message:
382
+
383
+ - secrets, read or written: the review boundary's denylist (chi's state and
384
+ config dirs, so `grants.json`, the web token and every `participants.json`;
385
+ `~/.ssh`, `~/.gnupg`, `~/.aws`, `~/.config/gh`, `~/.config/fish`,
386
+ `~/.config/git`, `~/Library/LaunchAgents`, `~/.local/bin`; the home's shell
387
+ startup files, histories, `.gitconfig`, `.netrc`, `.npmrc`, `.pypirc`, `.claude.json`;
388
+ the project's secret files, any config under `.git`) on every file tool's
389
+ path (`read`, `write`, `edit`, a plugin's `targets:` paths) and on the paths
390
+ a command names (`execute`, and any call whose targets carry a command),
391
+ each word resolved as `touches: chi_dirs` resolves it; a glob that matches a
392
+ secret or whose fixed part leads into a denied folder (`~/.ss?/id_*`,
393
+ `~/.config/*/config.yml`, `.en?`), a `$var` word whose fixed part does
394
+ (`~/.ss${d}h`), and a text match for the rest;
395
+ - sweeping a folder that holds secrets: `find`, `tar`, `cp`, `rsync`, `scp`,
396
+ `zip`, `ditto`, `grep`/`rg`/`ag` and the like given home or any folder above
397
+ a denied one;
398
+ - the environment and the keychain: `env` or `printenv` alone, `set`,
399
+ `export`, `declare` or `typeset` that list, `launchctl getenv`, `ps e`,
400
+ `security`, `/proc/*/environ`. On top of it, a pair turn's `execute` runs
401
+ without every variable whose name matches `KEY|TOKEN|SECRET|PASS|CREDENTIAL|AUTH`
402
+ (`PairBoundary::SECRET_ENV_NAME`, case-insensitive), and a command still
403
+ running at the limit is killed, never handed over as a background task;
404
+ - git settings that make git run a command (`core.hooksPath`,
405
+ `core.fsmonitor`, `core.sshCommand`, `core.pager`, `core.editor`, a `!`
406
+ alias) by `git config` or `git -c`, and `git config --global`/`--system`;
407
+ - running chi, by any wrapper (`bin/chi`, `bundle exec chi`, `ruby … bin/chi`,
408
+ `env … chi`, `watch`, `npx`, `sh -c 'chi …'`, `$(chi …)`), in an assignment's
409
+ value or an alias, a function, after `-exec`/`-c`/`-e`, but `chi --version` and
410
+ `chi --help`: its commands act in the owner's name (shares, caps, sends,
411
+ answers, sessions);
412
+ - leaving something running: `&` at a command's end, `nohup`, `setsid`,
413
+ `disown`, `screen -d…`, `tmux new -d`;
414
+ - owner-voiced tools, which act in the owner's name beyond the turn:
415
+ `delegate` and any `delegate_*` tool, `register_reminder`,
416
+ `cancel_reminder`, `send_note`, `forget_outputs`, `task_create`,
417
+ `task_stop`, `find_tools` (it loads tools into the session), and
418
+ `memory_write` (a memory reaches the owner's later sessions);
419
+ - `ask_user_question`, with the advice to ask in the reply: nobody on the
420
+ participant's side answers a card.
421
+
422
+ A script a command hands on is read too (`Guardrails::PairCommand`): one a
423
+ shell runs (`sh -c`, `eval`, a heredoc or here-string) as a command of its
424
+ own, one another interpreter runs (`ruby -e`, `python - <<EOF`) by its text
425
+ (chi, secrets, the environment, detaching). A heredoc written as data (`cat`,
426
+ `tee`, `git commit -F -`) is not looked into: where it goes is checked. A
427
+ command the checks can't read is refused, never let through on a text match.
428
+
429
+ And it **asks the owner** before a `write` or `edit` outside the session's
430
+ project root, and before a command that writes there (a redirection's target,
431
+ `tee`'s files, a `cp`/`mv`/`ln`/`install`/`rsync`/`ditto` destination, or a
432
+ target it can't resolve), every time: the ask is fresh (`Verdict#fresh_ask`: no
433
+ stored approval answers it, and no rule lowers it), so it comes even in auto
434
+ mode. The tmp dirs (`Dir.tmpdir`, `$TMPDIR`, `/tmp`, `/private/tmp`) and
435
+ `/dev/null` neither ask nor deny.
436
+
437
+ A participant's ask names them on the owner's card ("Sam's turn asks: approve
438
+ tool call?", `approval.author`/`author_name`, `approval.asked_by`, the same
439
+ line in the plain question: `Approval::ASKED_BY`) and offers **Allow once**
440
+ only: no session, repo or rule approval is made from a participant's call.
441
+ System 1 never judges it, and `chi guardrails replay` skips it. A pair turn
442
+ stops before its next generation once the participant passed their spending
443
+ cap or their link was revoked or expired
444
+ ([participants](internals/participants.md#limits-and-the-cap)).
445
+
446
+ The trust model is a team's (everyone invited is trusted): the boundary is an
447
+ **accident guard** for secrets and the owner's voice, not containment of a
448
+ hostile participant; a review session is. A pair turn can still reach, through
449
+ the owner's rules: `web_fetch` (an open egress path: what the turn can read it
450
+ can send out in a URL), plugin and MCP tools, `memory_read` and `context_read`,
451
+ `list_sessions` (the owner's other sessions' titles), and anything a script
452
+ does that the command checks don't read (a script file's body, an interpreter's
453
+ `-e` code). Share a working session only with people you would let type in it.
454
+
362
455
  ## Rules in config.yml
363
456
 
364
457
  ```yaml
@@ -666,14 +759,16 @@ a body that isn't JSON, a question with no answer, a request longer than
666
759
  `timeout_ms`. A nil means "ask the user", which is what the caller would
667
760
  have done anyway.
668
761
 
669
- Toolkit suggestions are a separate audit use of System 1. When
670
- `toolkit.suggest.mode` is `audit` (or `on`, which is audit-only in this
671
- build), the engine asks a `choice` question from a background thread after
672
- the turn starts. This uses the same configured host, model and timeout, but
673
- is gated by `toolkit.suggest.mode`, not `guardrails.system_one.mode`, and it
674
- never changes the session's toolkit. Its audit records use the `toolkit` tag
675
- and are written to `toolkit_suggestions.jsonl` beside the guardrail logs.
676
- To avoid competing with the session's model, it skips (one info line) when
762
+ Toolkit suggestions are a second use of System 1. When
763
+ `toolkit.suggest.mode` is `audit` or `on`, the engine asks a `choice`
764
+ question from a background thread after the turn starts. This uses the same
765
+ configured host, model and timeout, but is gated by `toolkit.suggest.mode`,
766
+ not `guardrails.system_one.mode`. Audit never changes the session's toolkit;
767
+ on mode's turn waits for the answer (at most `timeout_ms`, cut by a Stop)
768
+ and may start the session on the suggested kit (configuration.md, Toolkits).
769
+ Its warnings use the `toolkit` tag and its records are written to
770
+ `toolkit_suggestions.jsonl` beside the guardrail logs. To avoid competing
771
+ with the session's model, audit skips (one info line) when
677
772
  the System 1 host is the model's local host: the same `hosts:` name, or
678
773
  another entry for the same server root (scheme, host and port). There is no
679
774
  reliable single-slot signal, so this host-identity check is the available
@@ -697,8 +792,8 @@ user's **earlier asks they answered** (`SystemOne::Similar`) — this rule in
697
792
  this repository first, then this rule anywhere, then any answered ask, ranked
698
793
  by how many words the logged call shares with this one, ties going to the
699
794
  newest. Reading the log is best-effort, so a log that cannot be read simply
700
- offers no examples. A participant's review turn is never judged: there is no
701
- user there to ask. The judgment runs before the card appears, so an ask is
795
+ offers no examples. A participant's turn (review or pair) is never judged:
796
+ there is no user there to ask, and its asks go to the owner. The judgment runs before the card appears, so an ask is
702
797
  slower by up to `timeout_ms` (2000 ms by default).
703
798
 
704
799
  Each judgment is a line in `judgments.jsonl`, beside `approvals.json` in
@@ -985,9 +1080,11 @@ send; [internals/participants.md](internals/participants.md#apply)). A quote
985
1080
  lowers the risk, it doesn't remove it: your line still asks the model to act
986
1081
  on what it quotes, with your tools, so read the quote before you send it.
987
1082
 
988
- Within its own turn a participant's text can't act: the review boundary
989
- leaves it reads of the project only, and an injection that convinces the
990
- model still meets the boundary (containment over detection). What it can do
1083
+ Within its own turn in a review session a participant's text can't act: the
1084
+ review boundary leaves it reads of the project only, and an injection that
1085
+ convinces the model still meets the boundary (containment over detection). In
1086
+ a working session it acts, by design ([the pair boundary](#a-participants-pair-boundary)):
1087
+ invite only people you would let type in your session, under your guardrails. What it can do
991
1088
  is make the model *say* what the history holds: a review turn's model sees the
992
1089
  whole conversation, earlier tool outputs included, so start a review in a
993
1090
  fresh session (or a continue), not on a working session full of outputs. A