samagotchi 0.132.0 → 0.134.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +109 -1
  3. data/bin/chi +16 -0
  4. data/docs/architecture.md +28 -1
  5. data/docs/cli.md +12 -1
  6. data/docs/configuration.md +62 -15
  7. data/docs/desktop.md +2 -1
  8. data/docs/guardrails.md +107 -12
  9. data/docs/internals/background-tasks.md +1 -0
  10. data/docs/internals/participants.md +6 -5
  11. data/docs/internals/tool-guardrails.md +2 -0
  12. data/docs/plugins.md +2 -1
  13. data/docs/sessions.md +61 -6
  14. data/docs/testing.md +8 -3
  15. data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
  16. data/lib/samagotchi/bundles/mcp/plugin.rb +10 -3
  17. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  18. data/lib/samagotchi/chi_home.rb +12 -2
  19. data/lib/samagotchi/chi_shim.rb +77 -0
  20. data/lib/samagotchi/child_env.rb +5 -3
  21. data/lib/samagotchi/config.rb +2 -0
  22. data/lib/samagotchi/context_fetch.rb +4 -2
  23. data/lib/samagotchi/desktop/macos.rb +45 -4
  24. data/lib/samagotchi/engine.rb +13 -0
  25. data/lib/samagotchi/guardrails/implicit_writers.rb +143 -0
  26. data/lib/samagotchi/guardrails/pair_boundary.rb +27 -6
  27. data/lib/samagotchi/guardrails/pair_command.rb +546 -57
  28. data/lib/samagotchi/guardrails/parent_approvals.rb +1 -1
  29. data/lib/samagotchi/guardrails/read_only_shell.rb +34 -2
  30. data/lib/samagotchi/guardrails/review_paths.rb +7 -4
  31. data/lib/samagotchi/guardrails/shell_git_dirs.rb +6 -1
  32. data/lib/samagotchi/guardrails/shell_lex.rb +23 -0
  33. data/lib/samagotchi/guardrails/shell_paths.rb +3 -2
  34. data/lib/samagotchi/guardrails/targets.rb +8 -3
  35. data/lib/samagotchi/home_command.rb +33 -21
  36. data/lib/samagotchi/home_doctor.rb +210 -0
  37. data/lib/samagotchi/home_notice.rb +87 -0
  38. data/lib/samagotchi/hooks/loader.rb +5 -0
  39. data/lib/samagotchi/log_line.rb +12 -0
  40. data/lib/samagotchi/project_env.rb +352 -0
  41. data/lib/samagotchi/self_report.rb +35 -0
  42. data/lib/samagotchi/session_commands.rb +4 -2
  43. data/lib/samagotchi/session_manager.rb +18 -0
  44. data/lib/samagotchi/system_one/self_protect.rb +58 -2
  45. data/lib/samagotchi/testing/boundary.rb +3 -1
  46. data/lib/samagotchi/testing/waiting.rb +13 -0
  47. data/lib/samagotchi/toolkit_binding.rb +16 -0
  48. data/lib/samagotchi/tools/builtins.rb +13 -2
  49. data/lib/samagotchi/update_command.rb +7 -3
  50. data/lib/samagotchi/update_hint.rb +6 -1
  51. data/lib/samagotchi/version.rb +1 -1
  52. data/lib/samagotchi/web/app.rb +50 -0
  53. data/lib/samagotchi/web/public/answer_fold.js +262 -0
  54. data/lib/samagotchi/web/public/answer_refs.js +2 -0
  55. data/lib/samagotchi/web/public/app.js +22 -2
  56. data/lib/samagotchi/web/public/cards_place.js +5 -2
  57. data/lib/samagotchi/web/public/chip_popover.js +77 -12
  58. data/lib/samagotchi/web/public/claim_link.js +4 -2
  59. data/lib/samagotchi/web/public/composer.css +13 -1
  60. data/lib/samagotchi/web/public/data.js +8 -4
  61. data/lib/samagotchi/web/public/history_reveal.js +23 -0
  62. data/lib/samagotchi/web/public/index.html +68 -14
  63. data/lib/samagotchi/web/public/level_chip.js +22 -38
  64. data/lib/samagotchi/web/public/llm_context_chip.js +28 -80
  65. data/lib/samagotchi/web/public/refs.js +6 -7
  66. data/lib/samagotchi/web/public/review_start.js +13 -57
  67. data/lib/samagotchi/web/public/send_turn.js +3 -2
  68. data/lib/samagotchi/web/public/start_page.js +117 -97
  69. data/lib/samagotchi/web/public/start_settings.js +309 -0
  70. data/lib/samagotchi/web/public/start_toolkit.js +158 -0
  71. data/lib/samagotchi/web/public/thinking_chip.js +9 -22
  72. data/lib/samagotchi/web/public/timing.js +5 -1
  73. data/lib/samagotchi/web/public/turn_html.js +12 -6
  74. data/lib/samagotchi/web/public/turn_range.js +32 -0
  75. data/lib/samagotchi/web/public/turn_view.js +21 -11
  76. data/lib/samagotchi/web/public/verdict_card.js +5 -13
  77. data/lib/samagotchi/web/public/verify_target.js +7 -10
  78. metadata +11 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ebe264e1dbd5adc620889aa40d66fd458519c64f038a3a3e5de06ee6a6da46e7
4
- data.tar.gz: 3c72148287422c9821c25bc3979c71890fb4abad81af3829650aef3c13549a3f
3
+ metadata.gz: 1400c2aa3cceb5a84b6aa0d5610021b5e5893415afa37f98e8c99efe291de436
4
+ data.tar.gz: eda23134194381fd10460ba0b026ae10aa5a8aaf674fbc9dc4504359b1acb86d
5
5
  SHA512:
6
- metadata.gz: f8ffaf8ca17c125a38af6a30432d8b14a66ea97df8201686acedca8dde11869845b0e2a6abe0f9ece18db4e5848f9340b2ef73910371977fafe9889d430eebad
7
- data.tar.gz: e556232bbad1093cff1779510162a179b1284cc90bd8cc54d8082e2a4dab5e96f98bb6ebae5ef99ab82610a1bd33880c830cfae8b1076d1e6ca841c406ae45b3
6
+ metadata.gz: 7029cb713c02b4c3099ade377bc57618ec6467c2a1f04a4b21c7ebd3fbb6e34705e1ad54fe547672becdc6553824b785c96222e310690fd707a12c0fb057e852
7
+ data.tar.gz: e7400f091254ba9ffdf04f14160e8d6816cbe264016f8be8558c9da091edcc33bf34ed93652c83b6647be728a349b31631db49fc26e483280deda75a7658e5e3
data/CHANGELOG.md CHANGED
@@ -7,6 +7,112 @@ 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.134.0] - 2026-10-30
11
+
12
+ ### Added
13
+
14
+ - Web: a long answer (over ~25 lines or ~2,000 characters) gets a fold row above it: one click folds it to its first
15
+ line and its length, another opens it. Your folds are kept per browser across reloads; a jump into a folded answer
16
+ (a claim link, the working set, "View check") opens it.
17
+
18
+ ### Fixed
19
+
20
+ - Web: the start page's ⚙ and 🧰 buttons say whether their panel is open (`aria-expanded`) to screen readers.
21
+ - Web: an open sheet or popover is placed again when the window resizes (a rotation, a phone's keyboard): its header
22
+ no longer ends up above the screen, and a sheet turns back into a popover on a wide window.
23
+ - Web: on a phone the ⚙ panel's sheet may take 85 % of the screen, so with "Apply and budget" open its closing note is
24
+ no longer cut off at the bottom edge.
25
+ - Web: the ⚙ and 🧰 panels keep the keyboard focus while open (Tab wraps inside, `aria-modal`), their override chips
26
+ say whether the panel is open (`aria-expanded`), and Esc or × gives focus back to the chip that opened it.
27
+ - Web: "default" on the ⚙ panel's Context row now clears a set apply rule and budget too: the chip read "ctx apply
28
+ payoff · budget 64000" while the row showed default, and the new session got them.
29
+ - Web: on a touch screen a tapped row or link in the ⚙ and 🧰 panels (and the ⚙ and 🧰 buttons) no longer keeps its
30
+ hover tint.
31
+ - Web: a sheet's and a popover's × is a 36 px tap target on a touch screen (it was 22x16 px).
32
+ - Web: the Stop sheet's "+N more" no longer sits 74 px to the right (it shared a class with the ⚙ panel's row).
33
+ - Pair turns: a read-only mention of mise's config (`find . -name mise.toml && make`) no longer asks the owner; a
34
+ delete, move or write of it still does, and `.tool-versions` (mise reads it too) is covered the same way.
35
+ - `chi home doctor` checks the desktop helper only when the helper's chi reads this chi's config dir: a run with
36
+ `XDG_CONFIG_HOME` elsewhere no longer reports the installed helper against a home it never boots.
37
+ - The new-version hint ("… the desktop helper can be updated: chi update") follows the same rule: a run with
38
+ `XDG_CONFIG_HOME` elsewhere no longer hints about the installed helper.
39
+ - `chi home set` under `bundle exec` refuses to record its own Ruby, whose gem path is the bundle's, instead of making
40
+ it the home every launch boots (`--ruby` another Ruby still works).
41
+ - `chi update` under `bundle exec`, run on another Ruby than the chi home's, hands over to the home's chi outside the
42
+ bundle instead of loading the bundle into it (which failed noisily).
43
+ - `chi self`'s home row says when this chi runs on the home's Ruby and gems but isn't the home's newest chi (`this chi
44
+ is X`), as the terminal line and `chi home doctor` do.
45
+ - `chi home doctor` reports a worker that couldn't boot the chi home's chi (the debug log's newest `boot_failed` line
46
+ since the home was set, unless a worker started after it), with what to run.
47
+ - Toolkit suggestions (`toolkit.suggest.mode: on`): a suggestion that times out or gets no answer from System 1 now
48
+ says so in a notice (the session runs its default kit) and a timeout logs a warning, instead of falling back silently.
49
+ - Pair turns: a participant can no longer write git's config at all, so no setting makes git run a command later in
50
+ your name (a hook, a filter or driver, a helper, an include): `git config` only reads, parsed strictly (an unknown or
51
+ abbreviated option counts as a write), and `git -c`, `--config-env`, a clone's `-c`/`--config`, `--template` and
52
+ `GIT_TEMPLATE_DIR` are denied, as are a submodule's hooks (`.git/modules/*/hooks`) and the hook installers
53
+ (`pre-commit install`, `husky`, `lefthook install`). A command git runs from its text (`rebase --exec`,
54
+ `submodule foreach`) is checked like any command. Reading git's config (`git config --get`, `-l`) is no longer
55
+ refused.
56
+ - Pair turns: a command that writes outside the project with no word naming where now asks you like any other
57
+ outside write: global installs (`npm -g`, `pip install`, `gem install`, `brew`, `cargo`/`go install`, `pipx`,
58
+ `uv tool`), user config (`npm config set` writes `~/.npmrc`, `gh config set`), `defaults write`, `launchctl`; and
59
+ so does a shell or interpreter reading its script from stdin (`… | sh`, `| bash -x`, `curl … | bash`,
60
+ `sh < run.sh`). Global installs are recognised in any spelling (`npm i --location global`, `python3.12 -m pip`,
61
+ `npm_config_global=true`, `npm link`), with more tools (`deno`, `dotnet tool`, `corepack`, `bundle config set`).
62
+ - System 1 no longer auto-allows a command that names the memories folder and then climbs out of it by a script's
63
+ own calls (`python3 -c '…os.chdir(os.pardir)…'`, `ruby -e '…File.dirname(Dir.pwd)…'`) or runs a script it can't
64
+ see (a file, stdin, `| sh -x`, `python3.12 x.py`, `make`, `awk -f`): it could write `config.yml`. You decide
65
+ those; plain memory edits stay eligible.
66
+ - Guardrails: a `task_create` whose `env:` points git at another config (`GIT_CONFIG_GLOBAL`, `GIT_CONFIG_SYSTEM`,
67
+ `GIT_CONFIG_KEY_n`/`VALUE_n`) is no longer read-only, and a pair turn's command that sets one is denied like
68
+ `git -c`.
69
+ - Pair turns: more spellings that hid a git config write or a command are caught: a git global option that takes a
70
+ value (`--attr-source`, `--shallow-file`, one git adds later), `git-config` and the other dashed builtins, a script
71
+ after a shell's options (`sh -c -- S`, `bash -c -x S`) or in an option cluster (`ruby -we`, `perl -le`, `node -pe`,
72
+ `python3 -Ic`), and a command handed through an environment variable (`GIT_EDITOR`, `GIT_SSH_COMMAND`,
73
+ `GIT_EXTERNAL_DIFF`, `GIT_PAGER`, `EDITOR`, …). `git maintenance register/start`, `git lfs install` and
74
+ `scalar register` now ask you.
75
+
76
+ ## [0.133.0] - 2026-10-30
77
+
78
+ ### Added
79
+
80
+ - Sessions follow their project: a session's worker reads its folder's `mise env` once at start, and `execute`,
81
+ tasks, `!cmd`, MCP servers and context sources run in it, getting the project's Ruby (and node, …) whoever started
82
+ the worker (`session.project_env: mise`, the default; `off` skips mise). chi's own process keeps its
83
+ env (its keys, proxies and config never come from a project). The starter's GEM_HOME/GEM_PATH are dropped when the
84
+ Ruby changes. `chi` inside those tools stays the chi home's (a `chi` script in `$XDG_STATE_HOME/samagotchi/bin`). A
85
+ mise.toml edit needs `chi sessions restart`; a `mise env` that fails (an untrusted mise.toml) changes nothing and
86
+ logs one `project_env` warning. A session started from a tool for another folder never gets this one's env.
87
+ `chi self` shows `home` and `project env`. See docs/sessions.md (Project env).
88
+ - Pair turns: their `execute` runs without the project env; a command that runs `mise` or `direnv`, and a write to
89
+ mise's config (the owner's next worker start runs it), ask the owner; `.envrc` and mise's local files are secrets,
90
+ as `.env` is.
91
+ - Bundles: mcp 0.7.3 starts its servers in the session's project env.
92
+ - `chi home doctor` checks what should follow the chi home, one row each with `ok`/`warn`/`fail` and the command to
93
+ run: the chi you ran, the desktop helper's launch file (`chi desktop upgrade`), sessions saved by a newer chi than
94
+ the home's, the tools' `chi` script, a Gemfile.lock in this project that bundles samagotchi, mise, and the last
95
+ worker's `project_env` warning in this folder (e.g. `mise trust`). See docs/configuration.md (Checking it).
96
+ - A chi run at a terminal on another Ruby than the chi home's says so in one dim line, at most once a day
97
+ (`chi X on <ruby>; the chi home is <ruby> (chi Y) (chi home doctor)`).
98
+ - Web start page: a 🧰 beside ⚙ picks the new session's toolkit. **Auto** (the default, nothing sent) leaves the
99
+ first message to `toolkit.suggest.mode: on`'s suggestion when one can come, else `toolkit.default`; a picked kit is
100
+ sent (no suggestion then) and shows as a `tools: lean` chip (a dot on a phone). The list holds the built-in and
101
+ config kits a new session can start on; `GET /api/models` lists them as `toolkits`. A kit Start refuses is said in
102
+ the list. See docs/configuration.md (Toolkits).
103
+
104
+ ### Changed
105
+
106
+ - Web start page: thinking, LLM context and review files move into one ⚙ panel beside the model picker (a bottom
107
+ sheet on a phone): each a row of segments that applies at once, the first one the picked model's own
108
+ (`default (high)`), the context's apply rule and budget under **Apply and budget ▸**, and **Review files…** as a
109
+ view of its own. Only what is set shows on the row, as accent chips (`think high`, `ctx stale`,
110
+ `review: README.md`) that open the panel; on a phone ⚙ carries a dot instead. Still for the next new session
111
+ only. See docs/configuration.md (A new web chat).
112
+
113
+ Update with `chi update` (mcp 0.7.3), then restart `chi web` and the session workers (`chi sessions restart`). If
114
+ you use the desktop helper, run `chi desktop upgrade` once; `chi home doctor` says what else to do.
115
+
10
116
  ## [0.132.0] - 2026-10-29
11
117
 
12
118
  ### Added
@@ -5037,7 +5143,9 @@ and long-lived sessions.
5037
5143
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
5038
5144
  hotkey panel that send selected text or the clipboard to your sessions.
5039
5145
 
5040
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.132.0...HEAD
5146
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.134.0...HEAD
5147
+ [0.134.0]: https://github.com/dm1try/samagotchi/compare/v0.133.0...v0.134.0
5148
+ [0.133.0]: https://github.com/dm1try/samagotchi/compare/v0.132.0...v0.133.0
5041
5149
  [0.132.0]: https://github.com/dm1try/samagotchi/compare/v0.131.0...v0.132.0
5042
5150
  [0.131.0]: https://github.com/dm1try/samagotchi/compare/v0.130.1...v0.131.0
5043
5151
  [0.130.1]: https://github.com/dm1try/samagotchi/compare/v0.130.0...v0.130.1
data/bin/chi CHANGED
@@ -8,6 +8,14 @@ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
8
8
  require "samagotchi/utf8_default"
9
9
  Samagotchi::Utf8Default.apply!
10
10
 
11
+ # A chi a session's tool runs: back to that worker's own env before anything
12
+ # reads config (its API keys and proxies, not the project's mise env), and
13
+ # the workers it starts never take the project's (docs/sessions.md, Project env).
14
+ if ENV.key?("SAMAGOTCHI_PROJECT_ENV_ORIG")
15
+ require "samagotchi/project_env"
16
+ Samagotchi::ProjectEnv.take_orig!
17
+ end
18
+
11
19
  require "samagotchi/config"
12
20
 
13
21
  begin
@@ -27,6 +35,14 @@ if ENV.key?("SAMAGOTCHI_WEB_RESTARTED") || ENV.key?("SAMAGOTCHI_WEB_ADOPT_PIDS")
27
35
  web_restarted = Samagotchi::Web::Server.take_restart_env!
28
36
  end
29
37
 
38
+ # A person's chi at a terminal on another Ruby than the chi home's: one dim
39
+ # line a day saying so (HomeNotice: never in a worker's tools, a script or
40
+ # machine output).
41
+ unless web_restarted
42
+ require "samagotchi/home_notice"
43
+ Samagotchi::HomeNotice.show
44
+ end
45
+
30
46
  # `chi self`: print version, source dir, config/memory/session paths, model and
31
47
  # bundles (read-only, offline) — lets the agent find its own code and state.
32
48
  if ARGV[0] == "self"
data/docs/architecture.md CHANGED
@@ -158,11 +158,38 @@ home's Ruby is gone, when no `samagotchi-*.gemspec` is under its gem path, and f
158
158
  | `Desktop::MacOS#launch_config` | argv `CHI_BOOT + ["--"]`; `launch_ok?` / `launch_problem` check only absolute argv entries | `[ruby, wrapper or bin/chi]` |
159
159
  | `InstalledVersions`, `Web::UpdateCheck` | the home's specification dirs and gem dir | the running process's |
160
160
 
161
- Unchanged: `ContextProviders`' `{ruby}` (`RbConfig.ruby`: the home's ruby inside a home worker), `chi bundle
161
+ Unchanged: `ContextProviders`' `{ruby}` (`RbConfig.ruby`: the home's ruby inside a home worker; it runs under the
162
+ project env below, so a `mise env` that sets GEM_HOME reaches it), `chi bundle
162
163
  try/test/check` (`-I LIB_DIR`), hooks (in-process). The worker's boot string, `run_session_loop(id, state_dir:)` and
163
164
  `SAMAGOTCHI_BOOT_FALLBACK` stay the cross-version contract (`spec/worker_contract_spec.rb`): the home only adds the
164
165
  prelude in front. An older chi that wakes a session doesn't read home.json and starts its worker as before.
165
166
 
167
+ **The tools a worker runs** follow the project, not the boot, and only they do: a spawned worker
168
+ (`WORKER_PROCESS_ENV`; never an in-process loop) calls `ProjectEnv.compute` in `run_session_loop`, after the owner
169
+ lock and before `Worker.new`, and installs the result (`ProjectEnv.install!`; a frozen `ProjectEnv::Overlay`,
170
+ `{key => value or nil}`). Its ENV is never written: chi's own process (LLM calls, `HostKeys`, Net::HTTP proxies,
171
+ Config, Paths, hooks, plugins) keeps the spawner's env. The overlay is the session folder's `mise env -C DIR --json`
172
+ (mise's `__MISE_*`/`MISE_SHELL` unset; the spawner's GEM_HOME/GEM_PATH unset when the first `ruby` on PATH changes,
173
+ unless mise sets them; never `HOME`, `XDG_*`, `SAMAGOTCHI_*`, `CHI_REMOTE`, `RACK_ENV`, `CI`), `ChiShim`'s dir
174
+ (`$XDG_STATE_HOME/samagotchi/bin`, only a `chi` that execs `Record#boot_argv(run: CHI_BOOT)`) first on PATH when there
175
+ is a home, `SAMAGOTCHI_PROJECT_ENV` (`mise:<ruby>` or `none:<reason>`, for `chi self`) and
176
+ `SAMAGOTCHI_PROJECT_ENV_ORIG` (the worker's own value of each key it changes). It is merged into: `execute` and
177
+ `task_create` for the owner's turns (`Builtins.project_env`, less `TaskRuntime::SANITIZED_ENV_KEYS`; never a
178
+ participant's turn), `!cmd` (`SessionCommands#shell`), context sources (`ContextFetch`) and MCP servers (the mcp
179
+ bundle's `server_env`). A worker started from the worker inherits its clean ENV. A `chi` a tool runs (through the shim or not, a fresh
180
+ process with the tools' env) takes the worker's values back from the record in `bin/chi`, before anything reads
181
+ config (`ProjectEnv.take_orig!`): its own API keys and proxies, and the env of any worker it starts, are the
182
+ worker's (`ChildEnv` still drops the record and marker for a child). `restore_env` never returns a reserved key and
183
+ returns `DYLD_*`/`LD_*`/`RUBYOPT`/`RUBYLIB`/`RUBYGEMS_*`/`BUNDLE*` only as unsets. Hooks run in-process, with the
184
+ worker's env. Specs never reach a real mise: `SAMAGOTCHI_MISE_CLI` (only that path when set) points at a missing
185
+ file. User side: [sessions.md](sessions.md#project-env).
186
+
187
+ **Checking it.** `chi home doctor` (`HomeDoctor`) reads home.json and what should follow it, each condition's text
188
+ from its owner: `ChiHome.problem`, `HomeNotice.mismatch` (also the once-a-day notice `bin/chi` prints at a TTY on
189
+ another Ruby), `Desktop::MacOS#launch_problem` / `#home_problem`, `ChiShim.problem`, `ProjectEnv::NO_MISE` and
190
+ `ProjectEnv.last_warning` (the newest `project_env` / `project_env_applied` record for the folder, from the debug
191
+ log's last 4 MB). Rows: [configuration.md](configuration.md#checking-it-chi-home-doctor).
192
+
166
193
  ## Session retention & ordering
167
194
 
168
195
  - **Files:** `~/.local/state/samagotchi/sessions/<uuid>.json` + `<uuid>/input|output|owner.lock|bridge.json` (XDG-aware), and the `<uuid>/stopped` and `<uuid>/archived` marker files.
data/docs/cli.md CHANGED
@@ -35,7 +35,7 @@
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
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
- - `chi home`, `chi home set [--ruby PATH]`, `chi home doctor` — the chi home: the one Ruby (and its gem path) every chi launch boots, whoever launches it. `chi home` shows it (its Ruby, the newest chi in it, gem home and path, `mise`, when and by which chi it was set, and the chi you ran when that is another one; exit 1 with the problem when it can't boot chi), `set` records this chi's Ruby and gem path (or `--ruby PATH`'s own; it must have samagotchi), `doctor` checks it (for now: its Ruby is there and has samagotchi; exit 1 says what to do). This machine only (see [The chi home](configuration.md#the-chi-home))
38
+ - `chi home`, `chi home set [--ruby PATH]`, `chi home doctor` — the chi home: the one Ruby (and its gem path) every chi launch boots, whoever launches it. `chi home` shows it (its Ruby, the newest chi in it, gem home and path, `mise`, when and by which chi it was set, and the chi you ran when that is another one; exit 1 with the problem when it can't boot chi), `set` records this chi's Ruby and gem path (or `--ruby PATH`'s own; it must have samagotchi), `doctor` checks it and what should follow it, one row each with `ok`, `warn` or `fail` and what to run (exit 1 on a `fail`; see [Checking it](configuration.md#checking-it-chi-home-doctor)). At a terminal, a chi on another Ruby than the home's says so in one dim line a day. This machine only (see [The chi home](configuration.md#the-chi-home))
39
39
  - `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)
40
40
 
41
41
  - `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))
@@ -1026,6 +1026,17 @@ touch screen), and so does each code block of a rendered answer. An answer
1026
1026
  copies its Markdown source, not the rendered text; a code block copies just
1027
1027
  its code; a prompt copies the text as you typed it.
1028
1028
 
1029
+ A long answer (over 25 lines or 2,000 characters of its text) can be folded
1030
+ by hand: a slim "▾ N lines" row sits just above it once its turn has ended.
1031
+ Click it and the answer folds to one row, "▸ its first line · N lines" (the
1032
+ first line of what a hook presented in its place, else its first heading,
1033
+ else its first line); click again to unfold. A delegate report's reply
1034
+ folds the same way. Folding is this browser's choice, kept per session in
1035
+ localStorage (`chi_answer_fold:<session id>`, the last 200 folded answers;
1036
+ the keys of sessions gone from the list are dropped when the list loads);
1037
+ other viewers and the participant page are not affected. A jump into a
1038
+ folded answer (a claim link, say) unfolds it for that page load.
1039
+
1029
1040
  ### Web views
1030
1041
 
1031
1042
  `web.view` picks how the page draws a turn: `stage` (the default: the
@@ -70,6 +70,7 @@ model_aliases:
70
70
  session:
71
71
  shared: true
72
72
  # idle_exit_minutes: 30 # an unused worker exits after this (0 = never)
73
+ # project_env: mise # a worker's tools get its folder's `mise env` (off: the starter's env)
73
74
  # keep_empty: false # true keeps sessions nothing happened in (default: deleted when left)
74
75
  # max_children: 4 # running sessions one session may have delegated at a time (the delegate tool)
75
76
  # delegate_reports: wake # a child's reply reaches its parent by itself: wake (an idle parent runs a turn), queue, off
@@ -171,7 +172,7 @@ or `chi web` restart.
171
172
  | `hooks`, `bundles` | Worker start | Hooks, plugins and their bundle settings load with the engine. |
172
173
  | `recap.*` | Worker start | The engine builds its recap job at start, and again when it binds a session with its own `recap.enabled` or `recap.sentences` (`--set`, see "Per-session settings") and when `/recap on\|off\|default` changes the session's own value. |
173
174
  | `session.shared` | Next `chi` invocation | It selects attached-worker or in-process mode for that invocation. |
174
- | `session.idle_exit_minutes`, `session.keep_empty`, `session.max_children`, `session.delegate_reports`, `session.task_wakes`, `session.max_wakes` | Worker start or next matching lifecycle decision | Existing workers keep their idle-exit timer; the other values are read for the next corresponding decision. |
175
+ | `session.idle_exit_minutes`, `session.project_env`, `session.keep_empty`, `session.max_children`, `session.delegate_reports`, `session.task_wakes`, `session.max_wakes` | Worker start or next matching lifecycle decision | Existing workers keep their idle-exit timer and project env; the other values are read for the next corresponding decision. |
175
176
  | `session.retention_*`, `session.max_count`, `session.keep_status`, `session.sweep_interval_hours` | Next retention sweep | `chi web` sweeps on its configured schedule and on `GET /api/sessions`; `chi sessions prune` uses the values for that invocation. The sweep interval is checked on the next hub tick. |
176
177
  | `web.*` except `web.participants.*` | `chi web` start | Restart `chi web` to change its bind address, port or page settings. |
177
178
  | `log.*` | Process start | Each process holds its configured log sink and level after first use; restart that process. |
@@ -655,7 +656,7 @@ models:
655
656
  came from);
656
657
  - `chi --thinking LEVEL` on a run that starts or resumes a session (the REPL, `-p`, `--resume`, an attached one);
657
658
  - `chi send --new --thinking LEVEL`;
658
- - the web's `think` chip, in the session bar and on the start page (`POST /api/sessions` takes `"thinking": "low"`;
659
+ - the web's `think` chip in the session bar, and the start page's ⚙ panel (`POST /api/sessions` takes `"thinking": "low"`;
659
660
  anything that isn't a level answers `400 invalid_thinking`).
660
661
  - `chi web --thinking LEVEL` is the default for the sessions its workers run, below a session's own. A session started
661
662
  with only the env (`SAMAGOTCHI_THINKING_LEVEL=low chi send --new …`) loses the level when a worker without that env
@@ -1374,7 +1375,8 @@ launches it. It is recorded in `~/.config/samagotchi/home.json` (`$XDG_CONFIG_HO
1374
1375
  - **Setting it.** `chi home set` records the Ruby your `chi` runs on and its gem path (a chruby shell's
1375
1376
  `GEM_HOME`/`GEM_PATH` are part of it: that is where its chi lives); `chi home set --ruby PATH` records another
1376
1377
  Ruby with its own gem dirs. Either needs samagotchi installed there (`gem install samagotchi` with that Ruby
1377
- first). Without a home.json, the first `chi bootstrap` or `chi update` records one, once (from an installed chi,
1378
+ first). Under `bundle exec`, `chi home set` refuses to record its own Ruby (the bundle's gem path is no home);
1379
+ `--ruby` another Ruby still works. Without a home.json, the first `chi bootstrap` or `chi update` records one, once (from an installed chi,
1378
1380
  never under `bundle exec`): of the desktop helper's Ruby (when its newest chi is at least the one running) and the
1379
1381
  Ruby chi runs on, the one with the newer chi. Nothing else ever overwrites it.
1380
1382
  `chi home` shows it; `chi home doctor` checks it (see [CLI](cli.md#commands)).
@@ -1383,8 +1385,9 @@ launches it. It is recorded in `~/.config/samagotchi/home.json` (`$XDG_CONFIG_HO
1383
1385
  runs the home's `chi update`, and installs into the home's gems with the home's `gem`), `chi bootstrap`'s
1384
1386
  commonmarker install, chi web's "installed; restart" notice and the macOS helper after its next `chi desktop
1385
1387
  install` or `upgrade`. The boot sets the home's gem path inside Ruby (`Gem.paths`), never in an environment
1386
- variable: the commands a session runs (`execute`, tasks, `!cmd`, MCP servers) keep the GEM_HOME/GEM_PATH of whoever
1387
- started the worker, or none. How: [How chi launches itself](architecture.md#how-chi-launches-itself).
1388
+ variable: the commands a session runs (`execute`, tasks, `!cmd`, MCP servers) follow the session's project (its
1389
+ `mise env`, [Project env](sessions.md#project-env)), and their `chi` is the home's. How:
1390
+ [How chi launches itself](architecture.md#how-chi-launches-itself).
1388
1391
  - **No home, or one that can't boot chi** (its Ruby gone after a Ruby upgrade, samagotchi uninstalled there, a
1389
1392
  home.json that isn't JSON): each launch uses the Ruby that starts it, as without a home, and the log says why once
1390
1393
  per process (`home_unusable`, `home_invalid`). After a Ruby upgrade: `gem install samagotchi` with the new Ruby,
@@ -1394,9 +1397,34 @@ launches it. It is recorded in `~/.config/samagotchi/home.json` (`$XDG_CONFIG_HO
1394
1397
  `home_older_than_spawner`), but chi web's restart runs the newest chi it knows outside the home instead
1395
1398
  (`restart_home_older`): its flags may be ones the older chi doesn't know.
1396
1399
  - **A home that still fails at boot** (a broken install): the worker writes one `boot_failed` line to the debug log
1397
- (`log.file`) and exits, rather than leave the spawner waiting.
1400
+ (`log.file`) and exits, rather than leave the spawner waiting; `chi home doctor` reports the newest one.
1398
1401
  - Each machine has its own home (a remote's is that machine's).
1399
1402
 
1403
+ ### Checking it: chi home doctor
1404
+
1405
+ `chi home doctor` prints one row per check, `ok`, `warn` or `fail`, each with what to run; it exits 1 when a row
1406
+ fails (a launch can't boot what it should), 0 otherwise. Run it in a project folder to check that project too.
1407
+
1408
+ | Row | When | What to run |
1409
+ |---|---|---|
1410
+ | the home | `fail`: its Ruby is gone (a Ruby upgrade) or has no samagotchi | `gem install samagotchi` with the new Ruby, then `chi home set` from it |
1411
+ | this chi | `warn`: the chi you ran is another Ruby's, or another version, than the home's (`ok` for a source checkout, which starts its own code) | `chi home set` to make this one the home, or run the home's |
1412
+ | the last worker boot | `fail`: the newest worker boot in the debug log since the home was set is a `boot_failed` line (the home's Ruby couldn't load chi), not a worker's `start`; nothing without a log file | `gem install samagotchi` with the home's Ruby (or `chi home set` another), then `chi sessions restart ID` |
1413
+ | the desktop helper (when installed, and its chi reads this chi's config dir: none with `XDG_CONFIG_HOME` elsewhere) | `fail`: its launch file's Ruby or chi is gone; `warn`: it runs `[ruby, chi]` (installed before the home), boots the home as it was before a `chi home set`, or sets `GEM_HOME`/`GEM_PATH` | `chi desktop install --force` / `chi desktop upgrade` |
1414
+ | sessions | `warn`: sessions last saved by a newer chi than the home's (their workers run without what it added) | `chi update` |
1415
+ | the tools' `chi` | `warn`: `$XDG_STATE_HOME/samagotchi/bin/chi` boots another chi than the home | the next worker start rewrites it (`chi sessions restart ID` for a running session) |
1416
+ | this project's Gemfile.lock | `warn`: the nearest Gemfile.lock up to the git root bundles samagotchi (`bundle exec chi` isn't supported) | `bundle remove samagotchi` there; run chi itself |
1417
+ | mise | `ok` with the mise found; without one, `warn` when `session.project_env: mise` is set explicitly (`ok` for the default) | install mise, or `session.project_env: off` |
1418
+ | the last worker here | `warn`: the newest worker record for this folder in the debug log is a `project_env` warning (an untrusted mise.toml, a timeout) | `mise trust` there (or fix what it says), then `chi sessions restart ID` |
1419
+
1420
+ The rows after the home's first one need a home that can boot chi; the last three are checked with or without one.
1421
+
1422
+ **The notice.** An installed chi run by a person at a terminal (stdin and stderr TTYs) on another Ruby, or another
1423
+ version, than the home's prints one dim line on stderr, at most once a day: `chi 0.131.0 on /x/bin/ruby; the chi home
1424
+ is /y/bin/ruby (chi 0.132.0) (chi home doctor)`. Never inside a session's tools, for `--json`, `--format json`,
1425
+ `--non-interactive`, `--version`, `--help`, `chi home`, `chi update` or `chi self`, from a source checkout, or in a
1426
+ test run (`SAMAGOTCHI_ENV`, `RACK_ENV`, `CI`). The day is stamped in `$XDG_STATE_HOME/samagotchi/home_noticed`.
1427
+
1400
1428
  ## Project specific description
1401
1429
 
1402
1430
  If an AGENTS.md file is present in the project root, samagotchi injects its
@@ -1504,7 +1532,9 @@ toolkit:
1504
1532
  suggested`, and the turn's first request declares it; the notice reads
1505
1533
  `toolkit: web (suggested, p=0.71) · /toolkit to change`. Anything else (the default, a low-confidence or unknown
1506
1534
  pick, `undecided`, no answer, a failing System 1, which warns once as `[samagotchi:toolkit]`) keeps the default
1507
- kit. A Stop during the wait (or before it, even with the answer in) ends the turn before its first request, and
1535
+ kit. No answer in time or none at all says so in a warn notice, `toolkit: full (no suggestion: System 1 took over
1536
+ 5000 ms) · /toolkit to change` (or `System 1 gave no answer`), and a timeout logs a `toolkit_suggest_timeout`
1537
+ warning: a slow System 1 host wants a larger `guardrails.system_one.timeout_ms`. A Stop during the wait (or before it, even with the answer in) ends the turn before its first request, and
1508
1538
  nothing switches. After an `undecided`, a later prompt within the defer limits may switch the session; that notice
1509
1539
  says `from this turn`, and that request re-reads the whole prompt (system prompt, tools and conversation, which
1510
1540
  the defer limits keep small) without `/toolkit`'s cache confirmation. `/toolkit` or `ctx.use_toolkit` marks the
@@ -1548,6 +1578,18 @@ toolkit:
1548
1578
  `full`. A continue and a fork keep the toolkit name. `/toolkit` shows and changes it. The kit's tools, prompt
1549
1579
  text and group selectors are part of its definition, not the session file: edits reach a saved name at its next
1550
1580
  worker start.
1581
+ - **A new web chat: 🧰.** The start page's 🧰 (beside ⚙; a bottom sheet on a phone) picks the new session's toolkit.
1582
+ **Auto**, checked by default, sends nothing: the session is on `toolkit.default` with `toolkit_origin: default`, so
1583
+ with `toolkit.suggest.mode: on` its first message may switch it to a suggested kit. Auto is listed only when that
1584
+ can happen as far as chi web can tell (on mode, a `guardrails.system_one.host`, two described config kits); bundle
1585
+ and memory kits can add candidates it doesn't see. A kit picked is sent as `POST /api/sessions`'s `toolkit` and
1586
+ saved as `chosen`: no suggestion, the default kit included (its row says `default`); without Auto, the default's
1587
+ row is the one checked and sends nothing. The list holds the built-ins and config kits a new session can start
1588
+ on: not `review`, `child` or a kit named `undecided`, not a kit whose `groups:` refuses a new session, and not one
1589
+ that doesn't resolve in chi web (a base that is a bundle kit); bundle and memory kits load in the worker, so pick
1590
+ them with `/toolkit`. The configured default is always listed. A pick closes the list and shows a `tools: lean`
1591
+ chip on the row (a dot on 🧰 on a phone); it is for that create only, never remembered. A review session is
1592
+ always `chosen`: with review files set the list says it starts on the default unless you pick a kit.
1551
1593
  - **Left out: reachable or removed.** A built-in missing from a kit's base list (`lean` has no `delegate`; a
1552
1594
  `kits:` entry on `base: lean` or `child` inherits that) is left out softly: it is **reachable**. `find_tools` searches it as group `chi` (`chi/delegate`, with its inputSchema) beside
1553
1595
  the MCP servers' tools, and `call_tool(tool: "chi/delegate", args: {…})` runs it as `delegate` itself, through the
@@ -1667,14 +1709,18 @@ its own). The first target is a long session such as a day-long coordinator, whe
1667
1709
  - **Set it** at start with `chi --llm-context stale,forget --llm-context-apply turn_end --llm-context-budget 64k`
1668
1710
  (the same flags on `chi send --new`), in the session with `/llm-context strategy … apply … budget …`
1669
1711
  (docs/cli.md, "Session commands"), or from the web's `llm ctx` chip in the info bar.
1670
- - **A new web chat:** the start page's `llm ctx` chip, beside the model picker, shows the picked model's values
1671
- and follows a model pick. Its form (titled "new session") sets a choice for that one chat: the chip reads
1672
- `llm ctx stale · new session` in the accent colour, and the create sends it as `POST /api/sessions`'s
1673
- `llm_context` (`{"strategy": "stale,forget", "apply": "turn_end", "budget": "64k"}`, each a word as
1674
- `/llm-context` takes it, `default` for unset; anything else answers `400 invalid_llm_context` and starts
1675
- nothing), so the session file holds it before the first turn runs. It is not remembered: it resets after a
1676
- create, on a reload and when you open a session (a failed create keeps it), and an untouched chip sends nothing
1677
- (the model's values apply, as without the chip).
1712
+ - **A new web chat:** the start page's ⚙ panel (beside the model picker; a bottom sheet on a phone) holds the new
1713
+ session's rarely used settings, each a row of segments that applies at once: **Thinking** and **Context** (the
1714
+ strategy; **Apply and budget ▸** opens the other two fields, with their own Set), the first segment following the
1715
+ picked model (`default (high)`), and **Review files…** (docs/sessions.md, Review sessions). A row the picked model
1716
+ has no values for is hidden. What is set shows on the row as an accent chip (`think high`, `ctx stale`,
1717
+ `review: README.md`) that opens the panel; on a phone ⚙ carries a dot and its label names them. The create sends
1718
+ the context choice as `POST /api/sessions`'s `llm_context` (`{"strategy": "stale,forget", "apply": "turn_end",
1719
+ "budget": "64k"}`, each a word as `/llm-context` takes it, `default` for unset; anything else answers
1720
+ `400 invalid_llm_context` and starts nothing), so the session file holds it before the first turn runs. Nothing
1721
+ is remembered: the choices reset after a create, on a reload and when you open a session (a failed create keeps
1722
+ them); a choice made before picking a model without that row stays (its chip says it is sent anyway), and nothing
1723
+ set sends nothing (the model's values apply).
1678
1724
  - **Values:** `none` and a budget of `off` are values the session sets on purpose (they win over the model's);
1679
1725
  `default` (or `/llm-context reset` for all three) unsets one, back to following the model. A budget is 4k to
1680
1726
  10M tokens (`4000`–`10000000`, or `4k`–`10000k`). The layers are kept in chi's order (`stale`, then `forget`).
@@ -2042,6 +2088,7 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
2042
2088
  | `broadcast.threshold` | `0.5` | | The P(yes) a recipient needs, when the triage host gives logprobs; a plain yes or no is 1 or 0. |
2043
2089
  | `session.shared` | `true` | | Plain `chi` runs its session in a worker and attaches; `--no-shared` per run. |
2044
2090
  | `session.idle_exit_minutes` | `30` | yes | An unused worker exits after this; `0` = never. |
2091
+ | `session.project_env` | `mise` | | The commands a session's worker runs follow its project: `mise` runs them in the session folder's `mise env`, read once at worker start (`chi sessions restart` picks up a mise.toml edit); `off` doesn't ask mise: the tools keep the env of whoever started the worker (with a chi home, the home's `chi` still goes first on their PATH). See [Project env](sessions.md#project-env). |
2045
2092
  | `session.keep_empty` | `false` | | Keep sessions nothing happened in. |
2046
2093
  | `session.max_children` | `4` | | Running delegated sessions one session may have. |
2047
2094
  | `session.delegate_reports` | `wake` | | When a delegate child ends a turn, its reply reaches the parent: `wake` runs an idle parent's turn for it, `queue` waits for the parent's next turn, `off` leaves it to `delegate_result`. |
data/docs/desktop.md CHANGED
@@ -300,7 +300,8 @@ exports. So `install` writes `~/Library/Application Support/Chi Helper/launch.js
300
300
  whose chi may be found only through them, as under chruby): the home's boot sets its gem path itself, and they
301
301
  would reach every command the helper's sessions run (a project's `bundle exec` on the wrong gems). A launch file
302
302
  from an older chi loses `RUBYLIB` at the next `chi update`, and `GEM_HOME`/`GEM_PATH` once it boots the chi home
303
- (`chi desktop upgrade` moves it to the home);
303
+ (`chi desktop upgrade` moves it to the home). `chi home doctor` flags a helper that isn't on the home: a
304
+ `[ruby, chi]` argv, a boot of the home as it was before a `chi home set`, or `GEM_HOME`/`GEM_PATH` in its env;
304
305
  - `PATH`: the shell's PATH when the file is written, with only absolute folders, in order, each once. The helper's
305
306
  chi, the sessions it starts and their `execute` commands can find the tools your terminal finds (mise, Homebrew,
306
307
  `codex`, `node`);
data/docs/guardrails.md CHANGED
@@ -429,14 +429,50 @@ message:
429
429
  get-access-token`, `heroku auth:token`, `kubectl config view --raw`;
430
430
  `PairCommand::CREDENTIAL_PRINTERS`). On top of it, a pair turn's `execute` runs
431
431
  without every variable whose name matches `KEY|TOKEN|SECRET|PASS|CREDENTIAL|AUTH`
432
- (`PairBoundary::SECRET_ENV_NAME`, case-insensitive), and a command still
432
+ (`PairBoundary::SECRET_ENV_NAME`, case-insensitive) and without the session's
433
+ project env ([Project env](sessions.md#project-env)); a command that runs
434
+ `mise` or `direnv` asks the owner (`ENV_LOADER`: they load the project's env
435
+ files, and `.envrc`, `mise.local.toml` and its kin are secrets like `.env`),
436
+ and so does a write (by a file tool or a command) to mise's config, its
437
+ `.tool-versions` included (`MISE_CONFIG`: the owner's next worker start runs
438
+ its `[env]` and installs what it pins; a command asks when a part of it that
439
+ names one may write: `rm`, `mv`, `find … -delete` and `find … | xargs rm`
440
+ do, a read-only part such as `find . -name mise.toml && make` doesn't, the
441
+ list split at `;`, `&&`, `||` and newlines outside `( … )`; mise's own
442
+ writes, `mise use`/`mise set`, ask as runs of mise; a config that changes
443
+ without being named, by `git checkout`, `git stash pop`, `tar x`, `patch` or
444
+ `cp -r dir/. .`, isn't asked about: the owner's next restart runs it);
445
+ and a command still
433
446
  running at the limit is killed, never handed over as a background task;
434
- - git settings that make git run a command (`core.hooksPath`,
435
- `core.fsmonitor`, `core.sshCommand`, `core.pager`, `core.editor`, a `!`
436
- alias) by `git config` or `git -c`, and `git config --global`/`--system`;
447
+ - git config writes, whatever the key (any may make git run a command later
448
+ in the owner's name: hooks, `core.fsmonitor`, a filter, diff or merge
449
+ driver, a credential or gpg helper, `hook.<name>.command`, an include): a
450
+ pair turn's `git config` only reads, parsed strictly
451
+ (`PairCommand#git_config_read?`: `--get`, `--get-all`, `--get-regexp`,
452
+ `--get-urlmatch`, `--get-color(bool)`, `-l`/`--list`, `get`, `list` or a lone
453
+ key, with `--show-origin`, `--show-scope`, `--name-only`, `-z`, `--type=…`,
454
+ `--default`, `--file F` and the like, every word accounted for; any other
455
+ option, an abbreviation git would take (`--glob`, `--comm`), a short form
456
+ (`-t`) or a word too many is a write). Denied too: `git -c`, `--config-env`,
457
+ a clone's, init's or submodule's `-c`/`--config`, an assignment of
458
+ `GIT_CONFIG_*` or `GIT_TEMPLATE_DIR`, `git` under an alias, `git config` after
459
+ `xargs` or in `find -exec`, and an interpreter's script naming `git config`
460
+ or `git -c`, behind any of git's own options (`--attr-source X`,
461
+ `--shallow-file X`; one chi doesn't know is read both with and without the
462
+ next word as its value, `PairCommand.git_command_starts`), and by a
463
+ builtin's dashed name (`git-config`, `…/git-core/git-submodule foreach`:
464
+ judged as `git config`, `git submodule`). A command git runs now from its text (`git submodule foreach`,
465
+ `rebase --exec`, `bisect run`, a `filter-branch` filter, `--upload-pack`,
466
+ `--receive-pack`, `--extcmd`, an `ext::` URL) is read as a command of its
467
+ own and asks as an outside write. A `.gitattributes` write isn't asked
468
+ about: it only selects a driver, and defining one is what's denied;
437
469
  and writing git's hooks: a `write` or `edit` of a path in a `.git/hooks`
438
- folder (as given or as it resolves), or a command that isn't read-only and
439
- names one (`printf … > .git/hooks/pre-commit`, `cp x .git/hooks/`);
470
+ folder, a submodule's `.git/modules/<name>/hooks` too (as given or as it
471
+ resolves), or a command whose part that names one isn't read-only
472
+ (`printf … > .git/hooks/pre-commit`, `cp x .git/hooks/`), and installing
473
+ them: `git init`/`clone --template`, and the hook installers
474
+ (`PairCommand::HOOK_INSTALLERS`: `pre-commit install`, `husky`, `lefthook
475
+ install`, `simple-git-hooks`, `overcommit --install`);
440
476
  - running chi, by any wrapper (`bin/chi`, `bundle exec chi`, `ruby … bin/chi`,
441
477
  `env … chi`, `watch`, `npx`, `sh -c 'chi …'`, `$(chi …)`), in an assignment's
442
478
  value or an alias, a function, after `-exec`/`-c`/`-e`, but `chi --version` and
@@ -476,7 +512,12 @@ message:
476
512
  A script a command hands on is read too (`Guardrails::PairCommand`): one a
477
513
  shell runs (`sh -c`, `eval`, a heredoc or here-string) as a command of its
478
514
  own, one another interpreter runs (`ruby -e`, `python - <<EOF`) by its text
479
- (chi, secrets, the environment, detaching). A heredoc written as data (`cat`,
515
+ (chi, secrets, the environment, detaching). So is a command handed through a
516
+ variable a program runs through a shell (`PairCommand::COMMAND_ENV`:
517
+ `GIT_EDITOR`, `GIT_SEQUENCE_EDITOR`, `EDITOR`, `VISUAL`, `GIT_EXTERNAL_DIFF`,
518
+ `GIT_SSH_COMMAND`, `GIT_SSH`, `GIT_PROXY_COMMAND`, `GIT_PAGER`, `PAGER`,
519
+ `MANPAGER`, `GIT_ASKPASS`, `SSH_ASKPASS`), as a prefix, `env`'s or `export`'s:
520
+ read as `sh -c` would run it, for the refusals and for an outside write. A heredoc written as data (`cat`,
480
521
  `tee`, `git commit -F -`) is not looked into: where it goes is checked. A
481
522
  command the checks can't read is refused, never let through on a text match.
482
523
 
@@ -496,7 +537,44 @@ rule lowers it), so it comes even in auto mode. A command fails closed
496
537
  (`~/oth*`), `git -C`'s folder and the paths in a script it hands on (`sh -c`
497
538
  read as a command, `python -c`/`ruby -e`/a heredoc by the paths in its text)
498
539
  among them. The program's own word doesn't count (`/usr/bin/sed`), nor does
499
- `PATH=…`, nor a message's heredoc (`git commit -m "$(cat <<'EOF' … EOF)"`).
540
+ `PATH=…`, nor a message's heredoc (`git commit -m "$(cat <<'EOF' … EOF)"`);
541
+ and it runs no program that writes to its own place outside with no word
542
+ naming it (`Guardrails::ImplicitWriters`, examples rather than a closed
543
+ set: `npm`/`pnpm`/`bun`/`deno` global in any spelling (`-g`, `--global=true`,
544
+ `--location global`, `npm_config_global=true`), `npm`/`yarn`/`bun link`,
545
+ `npm config set`/`login` (`~/.npmrc`), `yarn global`, `pip install`/`pip
546
+ config` by any interpreter (`python3.12 -m pip`, `pypy3 -mpip`; with or
547
+ without `--user`: its site-packages is wherever its Python is; a project
548
+ venv's `.venv/bin/pip` and `-t`/`--target`/`--prefix` don't count), `uv pip
549
+ install --system`, `gem install`, `bundle config set` without `--local`,
550
+ `brew` but its reading verbs, `defaults write`, `cargo install`, `go
551
+ install`/`go env -w`, `pipx`, `uv tool`/`uv python install`, `dotnet tool
552
+ install -g`, `corepack enable`, `rustup`/`pyenv`/`rbenv`/`asdf`/`volta`/`nvm`
553
+ installs and globals, `code --install-extension`, `gh auth`/`config
554
+ set`/`extension install`, `launchctl` but its reading verbs, `git maintenance register`/`start`, `git
555
+ lfs install`, `scalar register`/`clone`; a project
556
+ install such as `npm install` or `bundle install` and build caches don't
557
+ count), and no shell or interpreter that reads its script from stdin: one
558
+ on the receiving end of a pipe whatever its options (`… | sh`, `| bash -x`,
559
+ `| sh -e`, `| python3 -c 'exec(input())'`), `< file`, `bash -s`, `python3 -`,
560
+ or with no script at all: chi can't see that script. A shell's `-e`, `-x`
561
+ and `-o …` are flags, only `-c` (or a cluster holding it, `-ec`, `-cx`)
562
+ hands it a script: the first word after its options (`sh -c -- S`, `bash -c
563
+ -x S`); an interpreter's own code flag does (`python -c`, `ruby -e`,
564
+ `node -e`/`-p`/`--eval=`, `perl -e`/`-E`, `php -r`), alone or in a cluster
565
+ (`ruby -we`, `perl -le`, `node -pe`, `python3 -Ic`, `python3 -c'…'`;
566
+ `PairCommand.cluster_code_at`: a letter that takes the rest of the word,
567
+ `ruby -rset`, holds no code). A script file it names (`sh
568
+ run.sh`, `ruby x.rb`) is not read.
569
+
570
+ Accepted gaps (an accident guard for a trusted team): a script file the turn
571
+ wrote and then runs; inline scripts read only by their text for paths
572
+ (`find … -exec sh -c '…'`, `flock … sh -c`, `awk 'BEGIN{system(…)}'`,
573
+ `deno eval`); a `GIT_CONFIG_*` set by `printf -v` or `read` and exported
574
+ (it affects that command only); a variable outside `COMMAND_ENV` whose
575
+ value a program runs (`LESSOPEN`, a tool's own `*_EDITOR`); `npx -y`,
576
+ `npm exec`, `pnpm dlx`, `uvx`: they fetch into a cache, which doesn't count,
577
+ and run the package as in-project work.
500
578
 
501
579
  Anything else asks: `rm -rf ~/x`, `sed -i … ~/x`, `curl -o ~/x`, `dd of=~/x`,
502
580
  `touch`/`mkdir` outside, `perl -pi … ~/x`, `cd .. && make`, `cd && make`, and
@@ -623,7 +701,8 @@ repo is in count as outside.
623
701
  `touches: chi_dirs` (shell tools) matches a command that names a path in chi's
624
702
  own folders: the config folder (`config.yml`, `memories/`), the hooks folder,
625
703
  the approval store, installed bundles (`memories/.bundles`), the session repo's
626
- git hooks (`core.hooksPath` honoured) or any `.git/hooks` outside a tmp folder.
704
+ git hooks (`core.hooksPath` honoured) or any `.git/hooks` (a submodule's `.git/modules/<name>/hooks` too)
705
+ outside a tmp folder.
627
706
  Each word is read as a path against the folder the command is in by then
628
707
  (`cd`, `pushd`/`popd` and `( … )` followed), with `~`, `$HOME` and
629
708
  `$XDG_CONFIG_HOME`/`$XDG_STATE_HOME` expanded and symlinks resolved. So
@@ -651,7 +730,9 @@ only `run list|view`, `pr list|view|diff|checks`, `issue list|view` and
651
730
  at a word's start), an env prefix, `sh -c`, `xargs`, `eval`, `sudo` and the
652
731
  like make a command not read-only, as does a `task_create` `env:` that sets `BASH_ENV`, `ENV`, `CDPATH`, `IFS`,
653
732
  `SHELLOPTS`, `BASHOPTS`, `BASH_FUNC_…`, `LD_PRELOAD`, `LD_AUDIT`, `DYLD_INSERT_LIBRARIES`, `GIT_EXTERNAL_DIFF`,
654
- `GIT_CONFIG_PARAMETERS` or `GIT_CONFIG_COUNT` (they change what its words run; `PATH` doesn't count). It errs
733
+ or a git config variable (`GIT_CONFIG`, `GIT_CONFIG_GLOBAL`, `GIT_CONFIG_SYSTEM`, `GIT_CONFIG_PARAMETERS`,
734
+ `GIT_CONFIG_COUNT`, `GIT_CONFIG_KEY_n`/`VALUE_n`: `ShellGitDirs::CONFIG_ENV`) (they change what its words run; `PATH`
735
+ doesn't count; a pair turn's command that assigns a git config variable is denied like `git -c`). It errs
655
736
  towards asking.
656
737
 
657
738
  A tool name may be a glob, so one rule covers a plugin's tools (an MCP server's,
@@ -1055,8 +1136,22 @@ gate below passes, in this order:
1055
1136
  it knew the dir: a script or heredoc body (`python3 -c '…'`, `cat >> x.md
1056
1137
  <<'EOF'`), a phrase (`echo "- a note"`) or a glob with no `/`. So
1057
1138
  `cd memories; cd ..`, `cd "${PWD%/*}"`, `cd $(dirname $PWD)` or `cd -`
1058
- loses the exemption, and the memories text then refuses it. A script body
1059
- is not looked into beyond its text: an accident guard, not containment.
1139
+ loses the exemption, and the memories text then refuses it. So does a
1140
+ script a shell or interpreter runs that climbs by its own calls
1141
+ (`SelfProtect::SCRIPT_CLIMB`: `os.pardir`, `dirname`, `chdir`, `Dir.pwd`,
1142
+ `Path.parent`, `rindex`, a slice from the end), and one chi can't see: a
1143
+ script file (`cd memories && python3 x.py`) or stdin (`… | python3`,
1144
+ `sh < f`, any interpreter or shell a pipe feeds, `| sh -x` too), by any
1145
+ version's name (`python3.12`, `pypy3`, `Rscript`, `tclsh`), or a program
1146
+ that runs a file (`SelfProtect::FILE_RUNNERS`: `make`, `rake`, `awk -f`,
1147
+ `deno run`, `npm run`). A heredoc only written (`cat >> x.md <<'EOF'`) is
1148
+ data whatever it says. A script body is not looked into beyond its text:
1149
+ an accident guard, not containment (a script can build a path some other
1150
+ way: `p.replace("memories", "config.yml")`, `p[0:len(p)-8]`, `.sub`,
1151
+ `.slice` aren't caught). The price is some harmless commands the user now
1152
+ answers: `python3 scripts/lint.py <memories>/notes.md`, `cat notes.md |
1153
+ python3 -c …` from the memories folder, a climb word written as data by a
1154
+ script (`File.write(".../notes.md", "dirname")`).
1060
1155
  A protected rule
1061
1156
  (`Verdict::PROTECTED_RULES`) may be listed by name and System 1 may answer
1062
1157
  it, but a call that really writes config.yml or the hooks is refused here;
@@ -45,6 +45,7 @@ Behavior:
45
45
  - `execute` and `task_create` both spawn with chi's own Ruby/Bundler setup scrubbed (`TaskRuntime.sanitized_spawn_env`): `RUBYOPT` loses its `-r…bundler/setup` flags (others stay) and `RUBYLIB`, `BUNDLE_GEMFILE`, `BUNDLE_BIN_PATH`, `BUNDLER_VERSION` are unset, so a project's `ruby -e` or `bundle exec` uses its own Gemfile when chi runs under `bundle exec`. `task_create` refuses those keys in the model's `env`; `execute`'s `env` comes from chi's callers (`Builtins`, `!cmd`), not the model, and is set over the scrub. An `execute` handed over at its limit keeps the process it spawned, so the task runs with the same env.
46
46
  - `execute` and `task_create` export chi's own markers into the command, set over the model's `env` (which can't set them: `Error: env key is reserved`): `SAMAGOTCHI_PARENT_SESSION` (the session's id, `chi` without one; a chi started there answers as a parent agent), `SAMAGOTCHI_SESSION_MODEL` (the resolved ref the session runs on, read at each call so it follows `/model`; unset when unknown) `SAMAGOTCHI_SESSION_THINKING` (the session's own thinking level; unset when it has none) and `SAMAGOTCHI_SESSION_REASONING_HISTORY` (its own reasoning_history level, the same way). `chi self` reads the last three for its model, thinking level and reasoning history. The four are `ChildEnv::SESSION_MARKERS` (`TaskRuntime::MARKER_ENV_KEYS`), which a spawned worker unsets (`ChildEnv.unset`, merged into its spawn env). A session's worker never inherits them from whoever started it (a `chi send` run by another session's `execute`): its other commands (a user's `!cmd`, an MCP server, a context command) have none.
47
47
  - They also pass on a set `XDG_STATE_HOME` / `XDG_CONFIG_HOME` made absolute (`Builtins.xdg_absolute`, via `Paths`), so a chi the command runs in another `cwd` (`chi send`) reads this chi's state and config. These aren't reserved: an XDG dir the model sets in `task_create`'s `env` stays its own.
48
+ - Every runner starts from the session's project env, `ProjectEnv.tools_env`: the overlay a spawned worker computed once at boot (`SessionManager.apply_project_env` → `ProjectEnv.compute` → `ProjectEnv.install!`; its folder's `mise env`, and the chi shim's dir first on PATH so `chi` is the chi home's), less `ProjectEnv::TOOLS_DROP` (`RUBYOPT`, `RUBYLIB`, Bundler's: the scrub above sets those). Outside a worker (`ProjectEnv::NONE`) it is `{}`. The sites: `execute` and `task_create` (`Tools::Builtins#project_env`, under the markers and the model's `env`; a participant's turn gets `{}`), the user's `!cmd` (`SessionCommands#shell`) and a context source's command (`ContextFetch#env_for`). MCP servers take `ProjectEnv.current.env` whole under the server's own keys (`bundles/mcp/plugin.rb` `server_env`: no `TOOLS_DROP`, no participant rule). chi's own process env is never changed, and the project env never enters the prompt. User docs: [Project env](../sessions.md#project-env); the launch side: [How chi launches itself](../architecture.md#how-chi-launches-itself).
48
49
 
49
50
  ## Finish wake
50
51