samagotchi 0.121.0 → 0.122.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 (127) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +110 -1
  3. data/bin/chi +7 -3
  4. data/docs/architecture.md +37 -11
  5. data/docs/bundle-authoring.md +2 -1
  6. data/docs/cli.md +11 -4
  7. data/docs/configuration.md +36 -7
  8. data/docs/guardrails.md +63 -19
  9. data/docs/internals/anthropic.md +1 -1
  10. data/docs/internals/background-tasks.md +1 -1
  11. data/docs/internals/engine.md +2 -0
  12. data/docs/internals/participants.md +16 -8
  13. data/docs/internals/remote-sessions.md +15 -11
  14. data/docs/internals/web-push.md +22 -11
  15. data/docs/memory.md +33 -0
  16. data/docs/plugins.md +8 -3
  17. data/docs/sessions.md +21 -0
  18. data/docs/testing.md +41 -1
  19. data/lib/samagotchi/answer_command.rb +1 -1
  20. data/lib/samagotchi/bridge/turn_accumulator.rb +3 -1
  21. data/lib/samagotchi/bridge.rb +12 -11
  22. data/lib/samagotchi/bridge_client/event_stream.rb +14 -26
  23. data/lib/samagotchi/bridge_client/stream_open.rb +85 -0
  24. data/lib/samagotchi/bridge_client/stream_request.rb +8 -2
  25. data/lib/samagotchi/bridge_client.rb +52 -65
  26. data/lib/samagotchi/bridge_secret.rb +9 -7
  27. data/lib/samagotchi/bundle_command/check.rb +27 -2
  28. data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -2
  29. data/lib/samagotchi/bundles/claude-peer/scripts/pair +20 -6
  30. data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -2
  31. data/lib/samagotchi/bundles/codex-peer/scripts/pair +14 -3
  32. data/lib/samagotchi/bundles/github-pr/manifest.yml +2 -2
  33. data/lib/samagotchi/bundles/github-pr/plugin.rb +2 -6
  34. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  35. data/lib/samagotchi/bundles/system/memory_guide.md +13 -0
  36. data/lib/samagotchi/child_move.rb +5 -7
  37. data/lib/samagotchi/children_status.rb +2 -1
  38. data/lib/samagotchi/config.rb +5 -0
  39. data/lib/samagotchi/engine.rb +107 -1
  40. data/lib/samagotchi/event_fanout.rb +0 -1
  41. data/lib/samagotchi/guardrail_wiring.rb +24 -6
  42. data/lib/samagotchi/guardrails/parent_approvals.rb +9 -0
  43. data/lib/samagotchi/guardrails_command.rb +41 -6
  44. data/lib/samagotchi/image_store.rb +6 -3
  45. data/lib/samagotchi/interrupted_turn.rb +6 -3
  46. data/lib/samagotchi/live_versions.rb +6 -7
  47. data/lib/samagotchi/log_line.rb +1 -1
  48. data/lib/samagotchi/memory_bundle/profile.rb +1 -1
  49. data/lib/samagotchi/memory_bundle/uninstaller.rb +6 -2
  50. data/lib/samagotchi/memory_orphans.rb +11 -0
  51. data/lib/samagotchi/participant_mode.rb +11 -0
  52. data/lib/samagotchi/plugin/api.rb +14 -8
  53. data/lib/samagotchi/plugin/sessions.rb +13 -4
  54. data/lib/samagotchi/process_group.rb +13 -0
  55. data/lib/samagotchi/push_command.rb +1 -1
  56. data/lib/samagotchi/relay_verifier.rb +2 -3
  57. data/lib/samagotchi/relay_watcher.rb +2 -2
  58. data/lib/samagotchi/remote_attach_command.rb +1 -1
  59. data/lib/samagotchi/reply_wait/poll.rb +8 -2
  60. data/lib/samagotchi/reply_wait.rb +4 -3
  61. data/lib/samagotchi/review_command.rb +1 -1
  62. data/lib/samagotchi/review_start.rb +6 -2
  63. data/lib/samagotchi/run_options.rb +2 -0
  64. data/lib/samagotchi/session.rb +18 -1
  65. data/lib/samagotchi/session_access/contract.rb +1 -1
  66. data/lib/samagotchi/session_access/local.rb +18 -8
  67. data/lib/samagotchi/session_access/remote.rb +37 -12
  68. data/lib/samagotchi/session_access/web_bridge_client.rb +38 -19
  69. data/lib/samagotchi/session_access.rb +21 -9
  70. data/lib/samagotchi/session_continue.rb +2 -3
  71. data/lib/samagotchi/session_manager.rb +133 -29
  72. data/lib/samagotchi/session_setup.rb +8 -4
  73. data/lib/samagotchi/sessions_command.rb +10 -5
  74. data/lib/samagotchi/steer.rb +7 -6
  75. data/lib/samagotchi/system_one/client.rb +12 -6
  76. data/lib/samagotchi/system_one/fake_backend.rb +0 -2
  77. data/lib/samagotchi/system_one/log.rb +44 -9
  78. data/lib/samagotchi/system_one/self_protect.rb +39 -43
  79. data/lib/samagotchi/terminal_ui/attach_launcher.rb +8 -3
  80. data/lib/samagotchi/terminal_ui/attached_loop.rb +4 -2
  81. data/lib/samagotchi/terminal_ui/local_link.rb +28 -19
  82. data/lib/samagotchi/terminal_ui.rb +2 -1
  83. data/lib/samagotchi/tool_runner.rb +2 -1
  84. data/lib/samagotchi/toolkit.rb +95 -10
  85. data/lib/samagotchi/toolkit_binding.rb +115 -7
  86. data/lib/samagotchi/toolkit_command.rb +15 -3
  87. data/lib/samagotchi/toolkit_report_command.rb +318 -0
  88. data/lib/samagotchi/toolkit_suggest.rb +211 -0
  89. data/lib/samagotchi/tools/delegate.rb +1 -1
  90. data/lib/samagotchi/tools/delegate_relay.rb +9 -7
  91. data/lib/samagotchi/tools/memory.rb +1 -1
  92. data/lib/samagotchi/turn_checkpoint.rb +15 -9
  93. data/lib/samagotchi/version.rb +1 -1
  94. data/lib/samagotchi/web/app.rb +127 -69
  95. data/lib/samagotchi/web/participant_access.rb +30 -8
  96. data/lib/samagotchi/web/participant_review.rb +8 -5
  97. data/lib/samagotchi/web/participant_view.rb +7 -3
  98. data/lib/samagotchi/web/participants_admin.rb +13 -0
  99. data/lib/samagotchi/web/principal.rb +3 -4
  100. data/lib/samagotchi/web/public/app.js +1 -1
  101. data/lib/samagotchi/web/public/bubbles.js +3 -2
  102. data/lib/samagotchi/web/public/notifications.js +21 -1
  103. data/lib/samagotchi/web/public/participant.html +3 -2
  104. data/lib/samagotchi/web/public/participant.js +25 -10
  105. data/lib/samagotchi/web/public/participants_panel.js +46 -11
  106. data/lib/samagotchi/web/public/push.js +4 -3
  107. data/lib/samagotchi/web/public/push_devices.js +17 -3
  108. data/lib/samagotchi/web/public/review_history.js +14 -2
  109. data/lib/samagotchi/web/public/review_start.js +1 -3
  110. data/lib/samagotchi/web/public/session_actions.js +14 -2
  111. data/lib/samagotchi/web/public/storage.js +16 -0
  112. data/lib/samagotchi/web/public/turn_events.js +2 -1
  113. data/lib/samagotchi/web/public/turn_html.js +1 -1
  114. data/lib/samagotchi/web/push/client.rb +5 -0
  115. data/lib/samagotchi/web/push/store.rb +16 -3
  116. data/lib/samagotchi/web/push_routes.rb +3 -2
  117. data/lib/samagotchi/web/server.rb +2 -2
  118. data/lib/samagotchi/web/session_access_routes.rb +28 -20
  119. data/lib/samagotchi/web/session_control.rb +4 -2
  120. data/lib/samagotchi/web/session_create.rb +4 -4
  121. data/lib/samagotchi/web/session_hub.rb +6 -4
  122. data/lib/samagotchi/web/session_summary.rb +8 -16
  123. data/lib/samagotchi/web/start_dir.rb +10 -5
  124. data/lib/samagotchi/worker.rb +17 -3
  125. data/lib/samagotchi/worker_link.rb +214 -0
  126. data/lib/samagotchi/worker_sidecar.rb +15 -37
  127. metadata +6 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 78051f4f253f400d0b791fe2694f0dc3d07a19e7d1268ef30557ff3a780d8d64
4
- data.tar.gz: c0bf5fbba8c530df5bf56a3947addddc0a9952a39f2077ddd5f59129683ba8f6
3
+ metadata.gz: 9ba0370c70db9c98a500766ef59cd8cedbf574852c60625679ed84c6a78d50d0
4
+ data.tar.gz: 937ffbf1066bc4acd34aaaf0ac19680c5099191f0bc87b8681863b362a280940
5
5
  SHA512:
6
- metadata.gz: c438bac307263d1a56cd9a97b56a2c3ab7175d9be0944c4e327fb92a57c9fdd9ffb82bdac8ec7048ac780a4a9e5f3545a3186dfcb3fec7842e687e2d47099307
7
- data.tar.gz: 2738c2cc9f4f81f34c3c88f02568066dced57578a88ac6d1e301ca62508a3e343179a97a7d6ef9d6778f463a00648d2aa0f0b1aded820e8bce7442b2601a0d8a
6
+ metadata.gz: c351af56a92b5df2cfabbd2d8a52ffefd76b1d7be74a92ffacd25f035d5ab6951214aa083a4d016beefd924cf2794d802c91ddf9ca4b437c860716f99e13ea76
7
+ data.tar.gz: f00a99184817060ca6d8741bee006bbdcacc3438896a48801a49e6694f4cb2c41868cbabca6f7cd27f807a475f752bf1a62acc580104159036dc033f7611965c
data/CHANGELOG.md CHANGED
@@ -7,6 +7,113 @@ 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.122.0] - 2026-10-27
11
+
12
+ ### Added
13
+
14
+ - Toolkit kits accept an optional `description:` (config `toolkit.kits` and `chi.toolkit`): what the kit is for, read
15
+ by the toolkit suggestion, never sent in a prompt.
16
+ - A memory named `toolkit_<name>` defines a kit (one YAML block with `base`, `add`, `remove`, `groups` and a required
17
+ `description`); project memories win over system ones, config and bundle kits over both, and `/toolkit` shows the
18
+ source. chi's memory guide says when to write one.
19
+ - Toolkit suggestion audit (`toolkit.suggest.mode: audit`, off by default): System 1 is asked which described kit fits
20
+ a new session's first prompt, in the background, without changing the session or delaying its turn; `chi toolkit
21
+ report` grades the suggestions against the tools each session then used.
22
+
23
+ ### Fixed
24
+
25
+ - `chi sessions restart` on a stopped (SIGSTOPped, Ctrl-Z) worker refuses at once with its pid and `kill -CONT PID`,
26
+ instead of ~70 s of silence before "didn't take the restart".
27
+ - `chi --remote NAME sessions restart` refusals keep their reasons (what the worker still has running) and name the
28
+ remote `chi --remote NAME sessions stop` command.
29
+ - chi web's stream, and a remote terminal, say a running worker "didn't answer" instead of "no live worker".
30
+ - The model's hint about a same-named project's memory folder asks the user first instead of offering an `mv`.
31
+ - A participant's wait answers revoked (401) when their grant disappears mid-wait, not "chi web is stopping".
32
+ - A bad `web.start_dir` says it came from config.yml (not `--start-dir`), and a relative one resolves from home.
33
+ - A remote `chi note` whose reply has no status is a clear refusal instead of a crash.
34
+
35
+ Update with `chi update`, then restart `chi web` and the session workers (`chi sessions restart`).
36
+
37
+ ## [0.121.1] - 2026-10-27
38
+
39
+ ### Fixed
40
+
41
+ - `chi sessions restart` can recover an unreachable, secret-refusing, newer-protocol or stuck-start worker without a
42
+ stop marker, but refuses when disk shows a running turn, pending question, queued input or running background task.
43
+ - chi web now opens a session stream before answering: a missing, leaving or unreachable worker gets 503, a Bridge
44
+ refusal gets 502 instead of an empty 200 or SSE frame, and the opened socket closes when the stream ends. Remote
45
+ stream retries distinguish 503 from a 502 `bridge_secret`, and `/stats` reports secret refusals consistently.
46
+ - A plugin's `ctx.sessions.fork` reports a child chi won't start as `Samagotchi::Plugin::Sessions::Error` with chi's
47
+ own reason (the session's model is gone from the config, or its toolkit is a `toolkit.kits` kit with an invalid
48
+ `groups:`) instead of handing the bundle the raw `Toolkit::Refused` or `ModelProfile::MissingModel`.
49
+ - A `toolkit.kits` kit whose `groups:` isn't a name or a list (a map such as `{chrome: true}`) no longer loads
50
+ silently with its base's groups: chi, `chi send --new`, chi web and `delegate` refuse a new session on it, naming
51
+ the kit and the reason, and a session already saved on it reaches no tool-index group until config.yml is fixed.
52
+ - A worker's liveness is one rule everywhere, the owner lock plus the sidecar's pid, with no port probe: after a
53
+ `kill -9` whose port another session's Bridge took, `chi send`, `--attach` and chi web no longer take the dead
54
+ worker for live (they wake a new one). A worker on a newer sidecar protocol is not driven by an older chi: a wake
55
+ answers 409 `worker_newer`.
56
+ - System 1 no longer auto-allows a command that changes chi's own config through a spelling its check missed
57
+ (`$XDG_CONFIG_HOME/samagotchi/config.yml`, `$HOME/.config/…`, `memories/../config.yml`, `cd ~/.config && …`): it
58
+ refuses by the same test that makes an ask the user's alone, so such a call goes to the user. Memory files still
59
+ can be auto-allowed.
60
+ - `chi guardrails replay` no longer judges an ask again that System 1 already judged live (audit or on mode), which
61
+ counted each such decision twice and let a rule pass `min_record` early: a live label now carries the card's
62
+ `question_id`, and a live line without one is matched by its session and call digest.
63
+ - Stopping a turn while System 1 judges one of its approvals no longer opens an approval card for the stopped turn
64
+ (nor logs the cancel as a failed judgment): the call is denied with "The turn was stopped".
65
+ - A participant no longer reads a one-time link code (`chc_…`, as in an invite link a reply quotes) or a Bridge's
66
+ `X-Chi-Bridge` secret: both are redacted like `chp_` tokens.
67
+ - An agent participant's `chi --remote … send --wait` goes on after chi web restarts: chi web reads whose turn it is
68
+ from the claimed input file, the last turn or the refused turn's record, not only its memory (it answered 403).
69
+ - chi web's participants routes (list, invite, revoke, cap) answer a grants store chi can't write (unreadable, or a
70
+ newer chi's) with 409 `grants_unwritable` and the store's own message, not a 500 with a backtrace on every refresh.
71
+ - A participant name or an annotate preset holding `\&`, `` \` `` or `\'` no longer breaks chi web's page: it was read
72
+ as a backreference when the page's attributes went in.
73
+ - One push subscription with a `p256dh` key off the P-256 curve no longer stops every push: chi web refuses such a
74
+ subscribe (400 `invalid_subscription`), and one already on disk fails that device alone, so the other devices still
75
+ get the push, `POST /api/push/test` answers that device as failed instead of a 500, and `chi web push test` prints
76
+ its failure instead of a backtrace.
77
+ - The notifications sheet's switches always show what chi web has: "this device" no longer stays on after iOS's
78
+ Don't Allow, nor "Long turns done" moved after its change failed.
79
+ - Turning push off on a flaky connection no longer drops this phone's subscription while chi web keeps the device:
80
+ chi web is told first, and a failure leaves the bell on with the error. The Home Screen app whose first `GET
81
+ /api/push` failed (chi web restarting, a phone just woken) tries again when it comes back to the front instead of
82
+ staying in tab mode until a reload.
83
+ - `chi --help` lists `chi guardrails replay` (seeding System 1's record), the one subcommand it left out.
84
+ - The participants panel's Cap form no longer sets a $0 cap when its field is cleared, nor sends a negative or
85
+ over-$1000 one: it refuses as the Invite's cap field does ("A cap is dollars, 0 to 1000.").
86
+ - A double tap on the web's Stop no longer asks twice (two confirms, or two stops): a second Stop of the same session
87
+ while the first one asks the worker what still runs, or stops it, does nothing.
88
+ - A participant's comment box keeps what they typed while their last comment was on its way (it was cleared when the
89
+ comment was accepted), and an error under the box (a refusal, "Images can't go in a review comment yet") goes at
90
+ their next edit or send instead of staying until a comment went through.
91
+ - An invite whose participants panel closed before chi web answered no longer loses its one-time link (the grant was
92
+ made, so the owner had to revoke and invite again): a sticky toast names it with Copy link.
93
+ - A participant's thread no longer jumps when a read fails and again when it recovers: "Can't reach the session —
94
+ retrying…" (and "This session is gone.") shows over the thread's top instead of as a new row in the header.
95
+ - A participant's wait on a turn whose worker died in it is no longer refused (403) after chi web restarts: the turn's
96
+ canceled (`worker_stopped`) record keeps whose turn it was.
97
+ - claude-peer (0.4.5): `pair` run as `bash pair` or by zsh no longer refuses every call with its usage (only `sh
98
+ pair` kept the id and the `--inbox-socket` path), and a carriage return or another control character in what it
99
+ pushes (the cwd) is escaped, so chi's JSON read no longer refuses the push.
100
+ - codex-peer (0.3.4): a carriage return or another control character in what `pair` pushes (the cwd) is escaped, as
101
+ claude-peer's is, so chi's JSON read no longer refuses the push.
102
+ - A toolkit's `groups:` match source groups in any case and with `{a,b}` braces, as the guardrails rules' `models:`
103
+ do (`groups: [Chrome]` missed `chrome`), and a config kit's `groups:` that isn't a name or a list (a map) is refused
104
+ with a line, the kit loading without it, instead of matching its stringified pairs.
105
+ - A plugin's `chi.toolkit(groups:)` that isn't a name, a list or `:all` (a map) is a load error ("toolkit NAME: groups
106
+ must be a list of group names or globs"), as a config kit's is refused, instead of matching its stringified pairs.
107
+ - `chi bundle check`'s `requires-chi-floor` sees a keyword added later: `chi.toolkit(prompt:)` (0.72.0),
108
+ `chi.toolkit(groups:)` (0.121.0), `ctx.ask_model(model:)` (0.80.0) and `ctx.sessions.send(delivery:)` (0.64.0).
109
+ - A remote `chi share` (or `share ls`/`cap`/`revoke`) whose chi web can't read its grants store prints the store's own
110
+ line, as a local one does, instead of "… answered 409 grants_unwritable: …".
111
+ - `chi web push test`'s line for a push that failed before the push service answered reads "failed (12 ms)", not
112
+ "failed (12 ms)".
113
+
114
+ Update with `chi update` (claude-peer 0.4.5, codex-peer 0.3.4, github-pr 0.5.1), then restart `chi web` and
115
+ `chi sessions restart` the running sessions.
116
+
10
117
  ## [0.121.0] - 2026-10-26
11
118
 
12
119
  ### Added
@@ -4551,7 +4658,9 @@ and long-lived sessions.
4551
4658
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
4552
4659
  hotkey panel that send selected text or the clipboard to your sessions.
4553
4660
 
4554
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.121.0...HEAD
4661
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.122.0...HEAD
4662
+ [0.122.0]: https://github.com/dm1try/samagotchi/compare/v0.121.1...v0.122.0
4663
+ [0.121.1]: https://github.com/dm1try/samagotchi/compare/v0.121.0...v0.121.1
4555
4664
  [0.121.0]: https://github.com/dm1try/samagotchi/compare/v0.120.0...v0.121.0
4556
4665
  [0.120.0]: https://github.com/dm1try/samagotchi/compare/v0.119.0...v0.120.0
4557
4666
  [0.119.0]: https://github.com/dm1try/samagotchi/compare/v0.118.0...v0.119.0
data/bin/chi CHANGED
@@ -70,6 +70,8 @@ SUBCOMMANDS = {
70
70
  "bundle" => ["samagotchi/bundle_command", "BundleCommand"],
71
71
  # the System 1 record: replay the asks this machine's sessions already answered
72
72
  "guardrails" => ["samagotchi/guardrails_command", "GuardrailsCommand"],
73
+ # audit suggestions for which toolkit fits a session's first prompt
74
+ "toolkit" => ["samagotchi/toolkit_report_command", "ToolkitReportCommand"],
73
75
  # other machines' chi web, for chi --remote NAME (add, ls, rm)
74
76
  "remote" => ["samagotchi/remote_command", "RemoteCommand"]
75
77
  }.freeze
@@ -230,7 +232,8 @@ if options[:web]
230
232
  working_set: Samagotchi::Config.get("web.working_set"),
231
233
  annotate_presets: Samagotchi::Config.get("web.annotate_presets"),
232
234
  editor: Samagotchi::Config.get("web.editor"),
233
- start_dir: options[:start_dir] || Samagotchi::Config.get("web.start_dir"))
235
+ start_dir: options[:start_dir] || Samagotchi::Config.get("web.start_dir"),
236
+ start_dir_from: options[:start_dir] ? :flag : :config)
234
237
  end
235
238
 
236
239
  # --resume and --attach take a unique id prefix too, like git.
@@ -391,7 +394,7 @@ if launch == :attached
391
394
  toolkit_flags: options[:toolkit_flags] || [],
392
395
  settings: options[:settings],
393
396
  parent_answers: options[:parent_answers])
394
- rescue Samagotchi::TerminalUI::AttachLauncher::Error => e
397
+ rescue Samagotchi::TerminalUI::AttachLauncher::Error, Samagotchi::Toolkit::Refused => e
395
398
  warn "Error: #{e.message}"
396
399
  exit 1
397
400
  rescue Interrupt
@@ -427,7 +430,8 @@ begin
427
430
  # limit, or Ctrl-C cancelled it.
428
431
  exit 1 if %i[turn_failed empty_answer iteration_limit].include?(ended)
429
432
  exit Samagotchi::CLI::Exit::INTERRUPTED if ended == :interrupted
430
- rescue Samagotchi::TerminalUI::SessionBusy, Samagotchi::TerminalUI::SessionNotFound, Samagotchi::ModelProfile::MissingModel => e
433
+ rescue Samagotchi::TerminalUI::SessionBusy, Samagotchi::TerminalUI::SessionNotFound, Samagotchi::ModelProfile::MissingModel,
434
+ Samagotchi::Toolkit::Refused => e
431
435
  warn "Error: #{e.message}"
432
436
  exit 1
433
437
  rescue Samagotchi::LLM::ProviderError => e
data/docs/architecture.md CHANGED
@@ -393,14 +393,18 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
393
393
  worker.
394
394
  - **The secret.** Every request, on every route (the stream, the reads such as `state` and `stats`, the writes, and
395
395
  an unknown path), names the Bridge's secret in `X-Chi-Bridge`; one without it, or with another one, is
396
- `403 bridge_secret` with one message (`BridgeSecret::REFUSED`: restart chi web, re-attach terminals and restart
397
- older session workers), after the browser and proxy checks and before any route (`Bridge#secret_ok?`,
396
+ `403 bridge_secret` with one session-specific message (`BridgeSecret.refused`): "session ID's worker refused
397
+ this chi's request (it didn't name its secret): a chi web or terminal started before chi was updated can't
398
+ reach it; restart chi web, re-attach with `chi --attach ID`, or restart the worker with `chi sessions restart
399
+ ID`." `chi sessions restart` can also recover when the worker cannot be asked: TERM + resume if disk shows no
400
+ running turn, pending question or approval, queued input, or running background task. The check is after the
401
+ browser and proxy checks and before any route (`Bridge#secret_ok?`,
398
402
  `OpenSSL.secure_compare`). No route is left open: what the reads show is the session itself. Only a request that
399
403
  got past it counts as client activity, so a refused one (an older chi web's polls) never holds the idle exit. Each Bridge makes its own (`SecureRandom.hex(32)`, 64 hex
400
404
  characters) when it starts and writes it to `bridge.json`, which is 0600 from the moment it exists
401
405
  (`AtomicFile` creates the temporary file with that mode, then renames it), so only this machine's user can read
402
406
  it: a forward that adds no proxy header, or another local account, reaches the port but not a session. Clients get
403
- it only from the sidecar: `BridgeClient.discover`/`.wait_for`/`.for_sidecar` take the port and the secret from the
407
+ it only from the sidecar: `WorkerLink.client`/`.await` and `BridgeClient.for_sidecar` take the port and the secret from the
404
408
  same read (chi web makes its client from the sidecar for every request, the attached terminal again on a
405
409
  `worker_changed`), so a respawned worker's new one is used and nothing passes a port by hand. A worker from before
406
410
  the secret writes none and asks for none; a chi web or attached terminal from before it sends none, so a worker
@@ -408,7 +412,7 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
408
412
  (`BridgeClient::Response#secret_refused?`) and shows the one message: chi web's relays answer 502 `bridge_secret`,
409
413
  `chi answer` and a parent's approval relay say it instead of "refused", and an event stream ends with reason
410
414
  `bridge_secret` once rediscovery finds no newer worker (the attached terminal's closing line), never retrying in
411
- silence; chi web's stream proxy passes no refusal on as frames. The secret is never logged (a refusal logs
415
+ silence; chi web's stream proxy answers 502 `bridge_secret`, never passing the refusal on as frames. The secret is never logged (a refusal logs
412
416
  `secret_refused` with the path, at most once a minute), never in an event, a reply, `/api/info` or a session
413
417
  file, and `WorkerSidecar`, `BridgeClient` and `Bridge` leave it out of `#inspect`.
414
418
 
@@ -491,10 +495,30 @@ Every route goes through `Bridge#dispatch`: an id other than the bridge's own se
491
495
  `handler_failed`) rather than dropping the connection.
492
496
 
493
497
  The per-session port is OS-assigned (bound to `0`) and published to a `bridge.json` sidecar
494
- for client discovery, with the Bridge's secret (see above). `chi web`'s `GET /api/sessions/:id/stream` proxies this bridge
495
- (503 `not_live` when the worker is not running; full history of any session is served by
496
- `GET /api/sessions/:id/output`), as a byte pipe for the owner: `App#proxy_stream_body` gives `ProxyStreamBody` the
497
- request's frame filter (`App#stream_filter`: nil for the owner; for a participant, and the owner's participant view
498
+ for client discovery, with the Bridge's secret (see above).
499
+
500
+ **Worker link.** Every client asks `WorkerLink` (`worker_link.rb`) whether a session's worker can be reached, never
501
+ reading `bridge.json` or probing a port itself: `WorkerLink.resolve(session_dir, unlink:, owner:)` answers a typed
502
+ outcome. The rule is the owner lock plus the sidecar's `pid`; there is no port probe (a port can be reused by another
503
+ session's Bridge, a free lock is the kernel's proof the worker died). In order: no lock holder, or a chi REPL's: a
504
+ sidecar is `Stale(:lock_free)` (removed with `unlink: true`, only if it is still the file that was read), else `None`;
505
+ a holder with no kind yet: `Starting`; a leaving worker: `Leaving` (marked before its Bridge stops, on every way out:
506
+ `Worker#teardown` marks it too, with `TEARDOWN_LEAVE_WAIT`); a worker whose sidecar names no port or another
507
+ pid than the holder's (a worker that has not written its own yet): `Starting`, `stuck` once it has held the lock
508
+ past `START_LIMIT` (30 s) with none; a sidecar whose `protocol` is newer than `WorkerLink::PROTOCOL`: `Skew` (a
509
+ newer worker; never driven, `wake` answers 409 `worker_newer`); else `Live` (its sidecar; `#client` makes the one
510
+ `BridgeClient`). A sidecar with no `pid` is an older worker's: the lock alone says it is live. `owner:` is the
511
+ holder as the caller already read it (nil: free), so one read serves a summary and its status. `WorkerLink.client` is
512
+ the Live's client or nil; `WorkerLink.await` polls for a new worker's Live (an `until:` check tells a restart's
513
+ successor from the old worker); chi web's `App#worker_link` / `#await_worker_link` wrap them. The Bridge writes
514
+ `pid` and `protocol` with its port and removes its sidecar on stop only when the file still carries its epoch.
515
+
516
+ `chi web`'s `GET /api/sessions/:id/stream` proxies this bridge
517
+ (503 `not_live` for no worker, a leaving or stuck worker, or an unreachable Bridge; 502 `worker_newer` for skew and
518
+ `bridge_error` / `bridge_secret` for a refused Bridge; 200 only after the Bridge answers; full history of any session
519
+ is served by `GET /api/sessions/:id/output`). It opens the Bridge before answering and gives the one opened socket to
520
+ `App#proxy_stream_body` / `ProxyStreamBody`, which closes it when the body or hijacked client ends. The owner's stream
521
+ is a byte pipe; the body gives it the request's frame filter (`App#stream_filter`: nil for the owner; for a participant, and the owner's participant view
498
522
  `?as=participant`, a `Web::ParticipantStreamFilter`, which cuts the Bridge's chunks into frames and writes each again
499
523
  from `Web::ParticipantView.event`'s allowlist, dropping every other event; only `from_seq` and `snapshot` reach the
500
524
  Bridge; [internals/participants.md](internals/participants.md#what-a-participant-sees)). Resume/ring-buffer state is **in-memory**, per worker: a cursor from
@@ -531,7 +555,7 @@ moved through `Session.summary_from_file`. Liveness is probed, since a killed ow
531
555
  file trace: the owner lock every tick for the sessions the projection believes owned, and every
532
556
  session every 10 s, so a `kill -9` shows within a second. The summary (`Web::SessionSummary`,
533
557
  shared with `/api/sessions` and the session view) carries `owner`, `project_root` and
534
- `bridge_up` (the sidecar is there *and* the lock is held: the page attaches its stream on it), and
558
+ `bridge_up` (`WorkerLink` answers Live: a worker holds the lock and its sidecar names its pid; the page attaches its stream on it), and
535
559
  from that sidecar `worker_version` (the chi the worker runs) and `worker_features`
536
560
  (`Bridge::FEATURES`, what a client may ask of it beyond the routes: `restart`, `task_stop`, `reparent`, `unqueue`,
537
561
  `delivery`, a worker that queues a message for after the turn; `SessionManager.deliver_turn` and
@@ -577,12 +601,12 @@ model: [internals/remote-sessions.md](internals/remote-sessions.md).
577
601
  | `GET /api/sessions/:id/worker` | owner | | 200 `{session_id, live, stopped, owner: worker \| tui \| null, worker: {epoch, started_at, version, features} \| null, default_model, parent_strategy}` (`Local#worker`; never starts one; `default_model` is this machine's) | 404 |
578
602
  | `POST /api/sessions/:id/exit` | owner | `client_id` (passed on only when it is an attached TUI's, `tui:…`; any other is dropped), `delete` | 200 the worker's `{status: exiting, session_id, discard}` (`BridgeClient#request_exit`; never wakes one) | 409 `held` with the worker's `{status, reason, tasks, hint}`; 503 `not_live`; 501 `not_supported`; 504 `worker_timeout` |
579
603
  | `POST /api/sessions/:id/recap` | owner | | 200 the worker's `{enabled, saved, request, min_user_turns, session_off}` (`BridgeClient#request_recap`) | 503, 501, 504 as every relay |
580
- | `GET /api/sessions/:id/stats` | owner | | 200 `{session_id, metrics}`: the worker's stats, its state's metrics for a worker from before the stats route | 503 `not_live`; 504 `worker_timeout`; 502 `bridge_error` |
604
+ | `GET /api/sessions/:id/stats` | owner | | 200 `{session_id, metrics}`: the worker's stats, its state's metrics for a worker from before the stats route | 503 `not_live`; 504 `worker_timeout`; 502 `bridge_secret` or `bridge_error` |
581
605
  | `GET /api/sessions/:id/children/counts` | owner | | 200 `{running, waiting, unreported}` (`ChildrenStatus.counts`, the delegates' files here) | 404 |
582
606
  | `GET /api/sessions/:id/turns/:turn_id/records` | owner | | 200 `{tool_records: […], turn_record: {…} \| null}` (`SessionMetrics.saved_tool_records` / `saved_turn_record`, as saved) | 400 `invalid_turn_id`; 404 |
583
607
  | `POST /api/sessions/:id/turn` (`no_interrupt`) | owner | `no_interrupt: true`: the turn runs with the raised iteration limit (`SessionManager.deliver_turn(no_interrupt:)`, to the Bridge or the input file) | as before | |
584
608
  | `POST /api/sessions/:id/command` (its 501) | owner | | | 501 `not_supported` for a builtin the worker lacks adds `command` (`"/title"`) and `message` |
585
- | `GET`/`POST`/`DELETE /api/sessions/:id/participants`, `GET`/`DELETE /api/participants` | owner | `chi share`'s (`name`, `kind`, `ttl`; `?id=p_…`) | the grants, never a token but POST's own once ([internals/participants.md](internals/participants.md)) | 400 `invalid_participant`; 404 |
609
+ | `GET`/`POST`/`DELETE /api/sessions/:id/participants`, `GET`/`DELETE /api/participants` | owner | `chi share`'s (`name`, `kind`, `ttl`; `?id=p_…`) | the grants, never a token but POST's own once ([internals/participants.md](internals/participants.md)) | 400 `invalid_participant`; 404; 409 `grants_unwritable` (the store doesn't read: its words, which a remote `chi share` prints as a local one does) |
586
610
  | `GET /api/sessions/:id/apply[?turn=T]` | owner | `turn`: a turn id or a prefix (none: the latest review turn with a reply) | 200 `{text, turn_id, author, author_name}` (`ReviewApply`): the owner's Apply of a participant's review turn, for the composer, never sent ([internals/participants.md](internals/participants.md)) | 404 `not_found`; 400 `not_review_turn`; 409 `no_reply` |
587
611
  | `POST /api/sessions` (`review`) | owner | `review: {subjects: [project files], from: a session id}`; never with `prompt`, `preview`, `idle` or `continues` (a `from` takes the source's folder and setup) | 201 as a create's: an idle review session (`ReviewStart.start`, or `ReviewStart.fork` with `from`) | 400 `invalid_review` (`ReviewStart::Refused`'s words: a subject, a gone folder); with `from` also 404 `not_found`, 400 `ambiguous_id` |
588
612
 
@@ -723,8 +747,10 @@ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/
723
747
  | Shipped bundles | `bundles/` (`mcp`, `github-pr`, `loop-guard`, `skills`, `source-links`, …) | Bundles that ship with chi. |
724
748
  | Hooks | `hooks/registry.rb` (`Hooks::Registry`), `hooks/loader.rb`, `hook_routes.rb` (`HookRoutes`), `event_routes.rb` (`EventRoutes`) | Hook registration and dispatch; what a hook or plugin does beyond reading its event (notice, question, stop, steer) routed to the UIs (cards and notices through one set of routes) ([hooks.md](hooks.md)). |
725
749
  | Guardrails | `guardrails.rb`, `guardrails/shell_lex.rb`, `guardrails/data_readers.rb`, `guardrails/rules.rb`, `guardrails/protected_paths.rb`, `guardrails/parent_policy.rb` + `guardrails/parent_approvals.rb`, `guardrail_wiring.rb` | The gate before every tool call: shell lexing (and the commands that only read a heredoc as data), YAML rules, the core checks on chi's own files, approvals, what a parent agent may answer (`guardrails.parent_answers`) ([guardrails.md](guardrails.md)). |
750
+ | System 1 | `system_one.rb`, `system_one/` (`SystemOne::Client`, `ApprovalQuestion`, `Log`, `Record`, `SelfProtect`, `Similar`, the backends), `guardrail_wiring.rb` (`GuardrailWiring#request_approval`), `guardrails_command.rb` (`chi guardrails replay`) | The approval judge: a model's `p` for each ask, logged to `judgments.jsonl` with the user's answer; in `on` mode an ask on a listed rule with a clean record is allowed once without the user. Labelled by `/guardrails wrong` (`SessionCommands`) and the web's **should have asked** (the Bridge's `POST guardrails/wrong`) ([guardrails.md](guardrails.md#system-1-experimental)). |
726
751
  | Delegation | `tools/delegate.rb`, `tools/delegate_result.rb`, `tools/delegate_wait.rb`, `child_reports.rb`, `tools/delegate_relay.rb`, `relay_desk.rb`, `relay_verifier.rb`, `relay_watcher.rb` | Child sessions a session starts and waits on, and their approval relay ([sessions.md](sessions.md#delegating)). |
727
752
  | send / broadcast | `send_command.rb` (`SendCommand`), `broadcast_command.rb` (`BroadcastCommand`), `broadcast/` | `chi send` and `chi broadcast` ([broadcast.md](broadcast.md), [sub-agent.md](sub-agent.md)). |
753
+ | Worker link | `worker_link.rb` (`WorkerLink`: `Live`, `Starting`, `Leaving`, `Skew`, `Stale`, `None`), `worker_sidecar.rb` (`WorkerSidecar`) | Whether a session's worker can be reached and the client for it: every Bridge client asks `WorkerLink.resolve` / `.client` / `.await` (*Worker link* above); `WorkerSidecar` is the `bridge.json` it reads. |
728
754
  | Session access | `session_access.rb` (`SessionAccess`: `NewSession`, `SetupWords`, `WaitStart`, `SessionView`, `Created`, `ContextEntry`, `ContextPush`, the errors `RemoteFailed`, `RemoteTooOld`, `Refused`, `BadRequest`), `session_access/local.rb` (`SessionAccess::Local`), `session_access/remote.rb` (`SessionAccess::Remote`), `session_access/http_transport.rb`, `session_access/web_bridge_client.rb` (`SessionAccess::WebBridgeClient`, a remote attach's worker through chi web), `session_access/dispatch.rb` (`SessionAccess::Dispatch`), `session_access/contract.rb` (`SessionAccess::Contract`), `remotes.rb` (`Remotes`), `remote_command.rb` (`chi remote`), `web/session_access_routes.rb` (`Web::SessionAccessRoutes`) | The seam between the session commands and where the sessions live: `Local` is this machine's (the `SessionManager`, `ReplyWait`, `NoteDelivery`, `ContextSources` and Bridge calls the commands made); `Remote` is another machine's, the same methods as requests to its chi web's routes of the same name (Bearer token, `HTTPTransport`), results as Local's value objects and refusals as the classes Local's callees raise, built from the fields chi web sends; waits and prefixes run on the server, a wait asked again per 30 s slice. `Dispatch` is bin/chi's front for `--remote NAME` / `CHI_REMOTE` (taken off before the subcommand lookup): it refuses what the contract table lists `local_only` (exit 2, one message) and runs the command with a `Remote` on the `Remotes` entry (`$XDG_CONFIG_HOME/samagotchi/remotes.yml`, 0600). chi web's routes answer from `Local` (*chi web's routes for the session commands* above). Checks that read this machine's config or files (vision, parent answers, the cwd's project, `SAMAGOTCHI_PARENT_SESSION`) stay in the commands and are skipped under a `Remote`, where chi web's create, turn and wait routes run the vision, parent-answers and busy checks with the server's config and the worker checks a parent's answer. See [internals/remote-sessions.md](internals/remote-sessions.md). |
729
755
  | Idle work | `idle_scheduler.rb` (`IdleScheduler`), `idle_recap.rb` (`IdleRecap`), `idle_reminders.rb` | One poller for an idle session's recap and reminders. |
730
756
  | Attached context | `context_sources.rb` (`ContextSources`), `context_poller.rb`, `context_absorber.rb`, `peer_sources.rb` (`PeerSources`: the sources peer bundles declare) | External sources a session keeps fresh, and the peer loop's wakes ([context.md](context.md)). |
@@ -130,7 +130,8 @@ class, a hand-built `ctx`), that is a gap in the kit: say so to the user rather
130
130
  after the last edit.
131
131
  - **requires_chi.** Use the chi the user runs (`chi --version`). `check` warns when the plugin calls something
132
132
  newer than the floor (`ctx.http` needs 0.80.0, `ctx.cost` 0.83.0, `ctx.usage` 0.100.0, `ctx.sessions.children` 0.34.0). A keyword added
133
- later it can't see: `ctx.ask_model(model:)` needs 0.80.0, `ctx.sessions.send(delivery:)` 0.64.0.
133
+ later counts too (`ctx.ask_model(model:)` 0.80.0, `ctx.sessions.send(delivery:)` 0.64.0, `chi.toolkit(prompt:)`
134
+ 0.72.0, `chi.toolkit(groups:)` 0.121.0), when the call is written on `ctx` / `chi` itself (not through a variable).
134
135
  - **trust_level.** Leave it out. `reviewed` / `trusted` are for someone who reviewed the bundle, not its author.
135
136
  - **Restart.** A session loads bundles when it starts: after an install or a settings change, the user starts a
136
137
  new session (or `chi sessions restart ID`).
data/docs/cli.md CHANGED
@@ -38,6 +38,7 @@
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
42
 
42
43
  - `chi bundle try PATH -p TEXT [options]` — install a bundle in a temporary, filtered sandbox, run one real model turn,
43
44
  and print its trace (see [Trying a bundle](#trying-a-bundle)).
@@ -518,10 +519,16 @@ chi installed, without stopping it: an attached terminal and the web's tabs
518
519
  move to the new worker. It is refused, with the reason, while something would
519
520
  be lost: a turn running or queued, a question or approval waiting, reminders,
520
521
  a `/btw` still running, an approval relayed from a delegate, or background
521
- tasks the session started. A session with no running worker needs none (its
522
- next prompt starts one on the newest chi), and a worker from before restarts
523
- needs `chi sessions stop ID`. The Web UI's `POST /api/sessions/:id/restart`
524
- does the same (409 with `reason` and `detail` when it can't). An attached
522
+ tasks the session started. If the worker cannot be asked (a refused Bridge,
523
+ protocol mismatch or stuck start), chi sends it TERM and resumes only when
524
+ disk shows no running turn, pending question or approval, queued prompt, or
525
+ running background task; otherwise it says to run `chi sessions stop ID`, then
526
+ send any prompt. That fallback may lose reminders, a running `/btw`, and other
527
+ in-memory state. A session with no running worker needs no restart (its next
528
+ prompt starts one on the newest chi), and an older worker without restart
529
+ support needs `chi sessions stop ID`. The Web UI's
530
+ `POST /api/sessions/:id/restart` does the same (409 with `reason` and `detail`
531
+ when it can't). An attached
525
532
  terminal says at the join, in one `chi>` line, when the worker or the terminal
526
533
  runs an older chi than the newest installed (the worker moves with `chi
527
534
  sessions restart ID`, the terminal with `/detach` and `chi --attach ID`), and
@@ -163,6 +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
167
  | `skip_agents_md` | Prompt rebuild | A running engine caches its built prompt; a new worker or `/model` rebuild uses the changed value. |
167
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. |
168
169
  | `context.every_seconds` | Next context poll | The poller checks it for each source that has no explicit interval. |
@@ -1398,7 +1399,7 @@ toolkit:
1398
1399
  default: lean # the sessions you start (unset: full)
1399
1400
  child: child # delegate children (unset: full)
1400
1401
  kits: # your own, on a base
1401
- audit: { base: lean, add: [web_fetch, delegate], remove: [memory_write] }
1402
+ audit: { base: lean, add: [web_fetch, delegate], remove: [memory_write], description: "Code review and inspection" }
1402
1403
  terse: { base: lean, prompt: "- Be concise in your responses" }
1403
1404
  web: { base: lean, groups: [chrome] }
1404
1405
  coding: { base: lean, groups: [] }
@@ -1414,12 +1415,36 @@ toolkit:
1414
1415
  | `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 |
1415
1416
 
1416
1417
  - **Plugin tools** are declared in every toolkit unless its `remove:` names them, and so is `forget_outputs` under
1417
- the `forget` layer. A `kits:` entry takes `base:` (default `full`), `add:`, `remove:`, `prompt:` and `groups:`
1418
+ the `forget` layer. A `kits:` entry takes `base:` (default `full`), `add:`, `remove:`, `prompt:`, `groups:` and
1419
+ `description:`
1418
1420
  (any other key, a typo or a `tools:` list, is ignored with a warning); it can't reuse a built-in's name
1419
1421
  (`review`'s least of all: it is the participants' boundary). A bundle declares its own with `chi.toolkit`
1420
- ([plugins](plugins.md#chitoolkitname-base-add--remove--prompt-nil-groups-nil)).
1421
- - **Tool-index groups.** `groups:` selects source groups by name or glob (`chrome`, `db*`); for the MCP bundle
1422
- each group is a server. Selectors are sorted and deduplicated and match the groups declared when plugins load.
1422
+ ([plugins](plugins.md#chitoolkitname-base-add--remove--prompt-nil-groups-nil-description-nil)).
1423
+ `description:` is optional classifier input, up to 300 characters; it is not included in the session prompt.
1424
+ - **Memory kits.** A memory named `toolkit_<name>.md` defines kit `<name>` with one fenced YAML block containing
1425
+ optional `base:`, `add:`, `remove:` and `groups:`, plus a required `description:` (up to 300 characters).
1426
+ `prompt:` is refused. A memory kit may use a config or bundle kit as its base. Built-ins cannot be redefined.
1427
+ The precedence is built-ins, config, bundles, then memory kits; within memories, project scope wins over system
1428
+ scope. Muted memory kits are skipped, and `/toolkit` shows each one's `memory:project` or `memory:system` source.
1429
+ Kits are read once per worker, so changes take effect when a new worker starts. See
1430
+ [Toolkit memories](memory.md#toolkit-memories) for the format and when to create one.
1431
+ - **Audit suggestions.** `toolkit.suggest.mode: audit` asks System 1 which described kit fits an eligible new session's
1432
+ prompt, without changing its toolkit or delaying the turn. It offers the configured default as the fallback and
1433
+ `undecided` for greetings or check-ins. Only a default-origin session with at least two described candidates is
1434
+ 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
1438
+ kept in `toolkit_suggestions.jsonl`; `chi toolkit report` grades them against the session's tool calls. One ask
1439
+ 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
+ resolve (a missing base, a cycle) is left out of the candidates. A worker that exits waits about a second for a
1441
+ running ask, so a one-shot `-p --non-interactive` run may end before its suggestion is logged.
1442
+ - **Tool-index groups.** `groups:` selects source groups by name or glob (`chrome`, `db*`; any case, `{a,b}` braces,
1443
+ as the guardrails rules' `models:`); for the MCP bundle each group is a server. Anything but a name or a list (a
1444
+ map such as `{chrome: true}`) refuses every new session on that kit or on a kit built on it, on every surface (chi,
1445
+ `chi send --new`, chi web, `delegate`, a continue or a fork): `toolkit.kits.NAME: groups must be a list of group
1446
+ names or globs; fix config.yml to start a session on toolkit KIT`. A session already saved on it keeps running,
1447
+ its kit reaching no group (with a `toolkit>` warning) until config.yml is fixed. Selectors are sorted and deduplicated and match the groups declared when plugins load.
1423
1448
  An omitted key inherits the base's selectors; `full` and `lean` (and `child`, which is on `lean`) reach all
1424
1449
  groups, `review` (the participants' kit) reaches none, `groups: ["*"]` matches every group, and `groups: []`
1425
1450
  reaches none. A kit whose base doesn't resolve (an unknown name, a cycle) falls back to `full`, which reaches
@@ -1947,7 +1972,7 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
1947
1972
  | `guardrails.mode` | `auto` | | Which rules vote: `auto` leaves out the rules tagged `modes: [strict]` (in the guardrails bundle: `git rebase`, writes and git outside the session's repo); `strict` runs them all. An unknown value warns and is `auto`. config.yml only. See [Guardrails](guardrails.md#modes). |
1948
1973
  | `guardrails.small_models` | `auto` | | Which models get the `models: small` rules: `auto` (32B or less by the size in the name, an MoE's active size), a list of globs, or `[]`. config.yml only. See [Guardrails](guardrails.md#rules-for-some-models). |
1949
1974
  | `guardrails.parent_answers` | `soft` | | Which options a parent agent (`chi answer`) may pick on a session's question, by their effect: `hard` (a deny, a Stop, a plain answer), `standard` (+ Continue and other spending options), `soft` (+ "Allow once"); never a wider scope or an allow on chi's own config. An invalid value warns and is `hard`. A session's own `parent_answers` (`chi --parent-answers`, `/parent-answers`) may only tighten it. Replaces `guardrails.parent_approvals` and `turn.parent_continue`. config.yml only: no environment variable (a parent can still pick the config dir with `XDG_CONFIG_HOME`). A convention for an honest parent, not a security boundary. See [Guardrails](guardrails.md#answers-from-a-parent-agent). |
1950
- | `guardrails.system_one.mode` | `off` | | System 1, the cheap judgment that answers an ask before the user sees it: `off` (the default), `audit` (judge every ask, log it, never decide), `on` (also allowed to allow a call once, behind the gates in [System 1](guardrails.md#system-1)). An unknown value warns and is `off`. Experimental. config.yml only. See [Guardrails](guardrails.md#system-1). |
1975
+ | `guardrails.system_one.mode` | `off` | | System 1, the cheap judgment that answers an ask before the user sees it: `off` (the default), `audit` (judge every ask, log it, never decide), `on` (also allowed to allow a call once, behind the gates in [System 1](guardrails.md#system-1-experimental)). An unknown value warns and is `off`. Experimental. config.yml only. See [Guardrails](guardrails.md#system-1-experimental). |
1951
1976
  | `guardrails.system_one.host` | none | | The `hosts:` entry serving `/v1/systemone` (a local Splash, or the hosted Jev API); unset: no judgment. config.yml only. |
1952
1977
  | `guardrails.system_one.model` | none | | The judgment model; unset uses the host's first model, and a host that names none has no judgment. config.yml only. |
1953
1978
  | `guardrails.system_one.rules` | none | | The rule ids System 1 may answer for in `on` mode: `"git-push|git-rebase"` or a YAML list, parsed like `guardrails.small_models`. Nothing is auto-allowed while it is unset; the `chi-answer-piped`, `chi-answer-http` and `chi-broadcast` rules never are, however they are listed. config.yml only. |
@@ -1978,6 +2003,10 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
1978
2003
  | `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). |
1979
2004
  | `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". |
1980
2005
  | `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. |
2007
+ | `toolkit.suggest.threshold` | `0.5` | | Minimum confidence for a non-default toolkit suggestion. config.yml only. |
2008
+ | `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
+ | `toolkit.suggest.defer_tokens` | `5000` | | Maximum estimated conversation size for another ask after `undecided` (characters / 4). config.yml only. |
1981
2010
  | `reasoning_history` | `turn` | | Which of its reasoning a chat host's model gets back: `drop`, `turn`, `all`; environment only (`SAMAGOTCHI_REASONING_HISTORY`), below a session's own level (`/reasoning-history`), above the `models:`/`hosts:` entries. See "Reasoning history". |
1982
2011
  | `thinking.level` | `default` | `--thinking` | `off`, `low`, `medium`, `high` or `default` for every model; the flag and env outrank the `models:`/`hosts:` entries, the file's value doesn't; a session's own level outranks them all, and `--thinking` sets it on a session run (on `chi web` it sets this). See "Thinking". |
1983
2012
  | `models.<key>.thinking`, `hosts.<name>.thinking` | none | | A model's or host's level. See "Thinking". |
@@ -2023,7 +2052,7 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
2023
2052
  | `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). |
2024
2053
  | `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). |
2025
2054
  | `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). |
2026
- | `web.start_dir` | the folder `chi web` runs in | | The folder `chi web` starts in (`chi web --start-dir DIR` for one run): its page (the printed link, a bare `/`, the LAN link and QR code, the Home Screen app) opens on that folder's project, outside a repo on every session, and a new session from the all view starts there; `/?scope=all` is every project. `~` and a relative path work; one that isn't a folder stops `chi web` from starting. `chi web` never changes its own folder. See [CLI](cli.md). |
2055
+ | `web.start_dir` | the folder `chi web` runs in | | The folder `chi web` starts in (`chi web --start-dir DIR` for one run): its page (the printed link, a bare `/`, the LAN link and QR code, the Home Screen app) opens on that folder's project, outside a repo on every session, and a new session from the all view starts there; `/?scope=all` is every project. `~` and a relative path (resolved from your home folder) work; one that isn't a folder stops `chi web` from starting. `chi web` never changes its own folder. See [CLI](cli.md). |
2027
2056
  | `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). |
2028
2057
  | `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. |
2029
2058
  | `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`). |
data/docs/guardrails.md CHANGED
@@ -243,6 +243,16 @@ with its `reason`; `chi answer` exits 1 with the line), checked against the
243
243
  question pending at that moment. Answers from the web and an attached
244
244
  terminal keep every option.
245
245
 
246
+ If `chi answer` reaches a worker that refuses its Bridge secret (`403
247
+ bridge_secret`), it prints a session-specific recovery message rather than
248
+ treating that as a refused answer: `"session ID's worker refused this chi's
249
+ request (it didn't name its secret): a chi web or terminal started before chi
250
+ was updated can't reach it; restart chi web, re-attach with chi --attach ID,
251
+ or restart the worker with chi sessions restart ID"`. `chi sessions restart
252
+ ID` can also recover when the worker cannot be asked, but only if disk shows
253
+ no running turn, pending question or approval, queued prompt, or running
254
+ background task; when one exists, stop it explicitly and then send a prompt.
255
+
246
256
  A parent may also type its answer into chi itself: a piped `chi --attach ID`
247
257
  or `printf '2\n' | chi --no-shared -p …`. So chi treats an answer as a
248
258
  parent's, held to the same setting, when its input isn't a terminal or an
@@ -656,14 +666,29 @@ a body that isn't JSON, a question with no answer, a request longer than
656
666
  `timeout_ms`. A nil means "ask the user", which is what the caller would
657
667
  have done anyway.
658
668
 
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
677
+ the System 1 host is the model's local host: the same `hosts:` name, or
678
+ another entry for the same server root (scheme, host and port). There is no
679
+ reliable single-slot signal, so this host-identity check is the available
680
+ contention guard. The session's own checks come first; the kits and the
681
+ suggestion log are read only for a session that can be asked, the log only
682
+ after an earlier `undecided`.
683
+
659
684
  ### What the audit writes
660
685
 
661
686
  With a client, `GuardrailWiring#request_approval` asks System 1 one `noul`
662
687
  question, `allow`, before the user is asked. Its instruction is a constant
663
688
  in code (`SystemOne::ApprovalQuestion::INSTRUCTIONS`), reviewed like a
664
689
  prompt; the state is the ask's own facts — the rule and its source, the
665
- reason, the tool, the command (or the paths) cut to 2000 characters, the
666
- edit's diff, the cwd, the repo and branch, and the session's title when it
690
+ reason, the tool, the command (or the paths) cut to 2000 characters
691
+ (`ApprovalQuestion::TEXT_CHARS`), the edit's diff, the cwd, the repo and branch, and the session's title when it
667
692
  has one. A memory ask also names what it is about: the entry and its scope,
668
693
  and for a removal the description that entry's index line carries
669
694
  (`Targets#memory`, read into the card by `Guardrails::Approval`): a path
@@ -680,11 +705,13 @@ Each judgment is a line in `judgments.jsonl`, beside `approvals.json` in
680
705
  `$XDG_STATE_HOME/samagotchi/guardrails/` (`SystemOne::Log`): 0600, appended
681
706
  under an exclusive `flock` (several workers write it), and rewritten —
682
707
  keeping the newest half of its lines — once it passes 5 MB. A line is never
683
- rewritten to add its label:
708
+ rewritten to add its label. Its `call` is shorter than the state's: cut to 300
709
+ characters (`ApprovalQuestion::CALL_CHARS`), with `call_digest` the SHA256 of
710
+ the whole call:
684
711
 
685
712
  ```json
686
713
  {"kind":"judgment","id":"…","at":"…","session_id":"…","rule":"git-push","source":"config","tool":"execute","call":"git push origin main","call_digest":"<sha256 of the whole call>","repo_name":"samagotchi","p":0.97,"threshold":0.8,"model":"Qwen3.8-Flash","latency_ms":180,"decision":"audit","origin":"live"}
687
- {"kind":"label","id":"…","at":"…","answer":"allow_once"}
714
+ {"kind":"label","id":"…","at":"…","answer":"allow_once","question_id":"…"}
688
715
  ```
689
716
 
690
717
  `decision` is what the judgment led to: `audit` (audit mode), `asked` (on
@@ -692,7 +719,9 @@ mode, the user was asked), `auto` (on mode, System 1 answered for the user).
692
719
  A judgment that never arrived is logged too, with `p` null, so the failures
693
720
  count as well. The label line, keyed by the judgment's id, is what the user
694
721
  did: `allow_once`, `allow_wide` (any scope wider than once), `deny`, or
695
- `none` for an ask that was cancelled. **A parent agent's answer is not the
722
+ `none` for an ask that was cancelled. A live label carries the card's
723
+ `question_id` when the user answered (the judgment is written before the card
724
+ exists), so a replay knows the ask was judged. **A parent agent's answer is not the
696
725
  user's: its allow labels nothing. Its deny is a label only where the parent
697
726
  could have allowed instead** — the ask is not `user_only` and the session's
698
727
  parent level permits a permit (`Guardrails::ParentPolicy`); a parent that was
@@ -704,6 +733,10 @@ their last label.
704
733
  Everything about the log is best-effort: a write that fails is one
705
734
  `Log.warn(:guardrails, "system_one_log_failed", …)` and the approval goes on
706
735
  as if System 1 were not there.
736
+ A turn stopped while the judge is asked is not a failed judgment: the cancel
737
+ (`LLM::RequestCancelled`) goes through `SystemOne::Client#ask`, no line is
738
+ written, and the call is denied without a card; a turn that stopped as the
739
+ judgment came back opens no card either.
707
740
 
708
741
  `ruby script/system-one-probe HOST [MODEL]` asks three fixed approval-like
709
742
  questions through the client and prints each probability and the latency of
@@ -722,13 +755,18 @@ gate below passes, in this order:
722
755
  questions — `chi-answer-piped`, `chi-answer-http`, `chi-broadcast` — which
723
756
  are never auto-allowed, even when listed
724
757
  (`SystemOne::SelfProtect::NEVER_AUTO`);
725
- 4. the call does not change chi itself (`SystemOne::SelfProtect.refuse?`): no
726
- edit or write whose paths include config.yml in chi's config dir, anything
727
- under the hooks dir, the installed bundles dir or the guardrails store (where
728
- `approvals.json` and `judgments.jsonl` live), and no `execute` or
729
- `task_create` command naming any of them — the `~/.config/samagotchi`
730
- spelling counts too. The memories dir is deliberately not one of these:
731
- reading or appending a memory file can be auto-allowed. A protected rule
758
+ 4. the call does not change chi itself (`SystemOne::SelfProtect.refuse?`), by
759
+ the same test that makes an ask one only the user may answer
760
+ (`ParentApprovals.chi_files?`): no edit or write whose paths are in chi's
761
+ config dir (config.yml), the hooks dir, the installed bundles dir, the
762
+ guardrails store (where `approvals.json` and `judgments.jsonl` live) or the
763
+ attached context dir, and no `execute` or `task_create` command naming any
764
+ of them, however it spells them (`ParentApprovals::CHI_TEXT`:
765
+ `$XDG_CONFIG_HOME/samagotchi/config.yml`, `~/.config/samagotchi/…`, a
766
+ `cd ~/.config` then `samagotchi/config.yml`). The memories dir is
767
+ deliberately not one of these: reading or appending a memory file can be
768
+ auto-allowed, unless the command has a `..` segment (`memories/../config.yml`
769
+ climbs out of it). A protected rule
732
770
  (`Verdict::PROTECTED_RULES`) may be listed by name and System 1 may answer
733
771
  it, but a call that really writes config.yml or the hooks is refused here;
734
772
  5. the judgment's `p` is at least `guardrails.system_one.threshold` (0.8);
@@ -744,11 +782,14 @@ Then the judgment's `decision` is `auto`, the verdict settles as
744
782
  written to `approvals.json` — the next call is judged again. **No question is
745
783
  opened, so no card appears, no `:question_asked` hook fires, and no
746
784
  notification is sent.** The verdict's activity carries
747
- `note: "auto-allowed by System 1 (p=0.97)"`, `judgment_id` and `p`; the line
748
- that shows it in the terminal and the web is P3b, and is not rendered yet, so
749
- today an auto-allowed call simply looks like a call that never needed
750
- approval. If the judgment line cannot be written to the log, the ask goes to
751
- the user.
785
+ `note: "auto-allowed by System 1 (p=0.97)"`, `judgment_id` and `p`, and the
786
+ call's tool row shows it: the terminal's tool line ends in
787
+ `· auto-allowed (System 1, p=0.97)`
788
+ (`TerminalUI::Formatting#format_tool_guardrail_suffix`, live and when
789
+ attaching to a saved turn), and the web's activity row carries
790
+ **auto-allowed · System 1** with `rule …, p=…, allowed once` on hover and the
791
+ owner's **should have asked** button (below). The stage trail shows no mark.
792
+ If the judgment line cannot be written to the log, the ask goes to the user.
752
793
 
753
794
  A delegate child answers from its own session's settings: its System 1 can
754
795
  auto-allow a call even when the child's `parent_answers: hard` bars a parent
@@ -778,8 +819,11 @@ whose `pending_question.approval` is there and that have an `answer` or were
778
819
  `cancelled`. A label is the scope the user took — `allow_once` for "Allow once",
779
820
  `allow_wide` for a wider one, `deny` for anything else, `none` for a cancelled
780
821
  ask. Skipped: a question no one answered, a relay card, which carries no
781
- approval of its own, and a question id a previous replay already logged — so
782
- running it twice adds nothing. A parent agent's answer is not the user's: its
822
+ approval of its own, a question id a previous replay already logged — so
823
+ running it twice adds nothing — and an ask already judged live (audit or on
824
+ mode): by the `question_id` its label carries, or, for a live judgment with
825
+ none (a cancelled ask, an older line), by its session and `call_digest`, one
826
+ card per such line. A parent agent's answer is not the user's: its
783
827
  allow is skipped, and its deny is replayed only where the parent could have
784
828
  allowed (the card is not `user_only` and the session's parent level permits a
785
829
  permit). A cards file that doesn't parse is skipped with one warning line,
@@ -130,7 +130,7 @@ gets a 400 `messages.N.content.M: Invalid signature in thinking block. The block
130
130
  conversation.`, and its last sentence says what changed ("The `system` prompt differs …"). chi's history is
131
131
  append-only within a process, so what changes the prefix is:
132
132
 
133
- 1. **A system-prompt rebuild** (`SystemPrompt#build` is cached per process and key; `PromptBuilder#reset!` drops it):
133
+ 1. **A system-prompt rebuild** (`SystemPrompt#build` is cached per process and key; `SystemPrompt#reset!` drops it):
134
134
  a new process (`--resume`, a respawn after `session.idle_exit_minutes`, a crash) after a memory index line, an
135
135
  AGENTS.md, an appended file, a model note or a bundle memory changed; `/model` (the tail's `Model:` line, its
136
136
  overlays and notes); `/thinking` (another key); a toolkit or profile change; a plugin's `tools_changed!`; the