samagotchi 0.149.0 → 0.150.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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -1
  3. data/README.md +4 -2
  4. data/docs/architecture.md +2 -2
  5. data/docs/cli.md +59 -24
  6. data/docs/configuration.md +5 -5
  7. data/docs/internals/background-tasks.md +1 -1
  8. data/docs/internals/bridge.md +8 -0
  9. data/docs/internals/bundles.md +42 -14
  10. data/docs/internals/objectives.md +7 -2
  11. data/docs/internals/web-frontend.md +1 -1
  12. data/docs/internals/web-push.md +2 -2
  13. data/docs/internals/working-set.md +5 -2
  14. data/docs/memory.md +47 -9
  15. data/docs/plugins.md +7 -1
  16. data/docs/sessions.md +5 -4
  17. data/docs/testing.md +5 -0
  18. data/lib/samagotchi/atomic_file.rb +3 -0
  19. data/lib/samagotchi/bootstrap_command.rb +5 -2
  20. data/lib/samagotchi/bridge/card_store.rb +52 -7
  21. data/lib/samagotchi/bridge.rb +1 -1
  22. data/lib/samagotchi/bundle_command.rb +91 -68
  23. data/lib/samagotchi/bundler_env.rb +24 -0
  24. data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
  25. data/lib/samagotchi/bundles/github-pr/manifest.yml +3 -3
  26. data/lib/samagotchi/bundles/github-pr/plugin.rb +2 -1
  27. data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +3 -1
  28. data/lib/samagotchi/bundles/known-names/manifest.yml +3 -2
  29. data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +2 -2
  30. data/lib/samagotchi/bundles/source-links/manifest.yml +3 -3
  31. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  32. data/lib/samagotchi/bundles/system/memory_guide.md +5 -5
  33. data/lib/samagotchi/bundles/system/self_map.md +1 -1
  34. data/lib/samagotchi/chi_home.rb +4 -2
  35. data/lib/samagotchi/chi_shim.rb +2 -1
  36. data/lib/samagotchi/child_env.rb +10 -3
  37. data/lib/samagotchi/engine.rb +1 -1
  38. data/lib/samagotchi/git_env.rb +43 -0
  39. data/lib/samagotchi/guardrails/context.rb +2 -0
  40. data/lib/samagotchi/guardrails/targets.rb +4 -1
  41. data/lib/samagotchi/memory_bundle/asset_installer.rb +16 -2
  42. data/lib/samagotchi/memory_bundle/file_standing.rb +167 -0
  43. data/lib/samagotchi/memory_bundle/installed_bundle.rb +62 -1
  44. data/lib/samagotchi/memory_bundle/installer.rb +161 -52
  45. data/lib/samagotchi/memory_bundle/listing.rb +4 -3
  46. data/lib/samagotchi/memory_bundle/merger.rb +209 -8
  47. data/lib/samagotchi/memory_bundle/placeholder.rb +2 -1
  48. data/lib/samagotchi/memory_bundle/profile.rb +7 -3
  49. data/lib/samagotchi/memory_bundle/provenance.rb +178 -20
  50. data/lib/samagotchi/memory_bundle/remerge.rb +167 -0
  51. data/lib/samagotchi/memory_bundle/shipped_history.rb +79 -0
  52. data/lib/samagotchi/memory_bundle/shipped_update.rb +89 -24
  53. data/lib/samagotchi/memory_bundle/source.rb +3 -3
  54. data/lib/samagotchi/memory_bundle/status.rb +21 -3
  55. data/lib/samagotchi/memory_bundle/system_bundle.rb +98 -33
  56. data/lib/samagotchi/memory_bundle/text_merge.rb +399 -0
  57. data/lib/samagotchi/memory_bundle/trash.rb +58 -2
  58. data/lib/samagotchi/memory_bundle/uninstaller.rb +26 -6
  59. data/lib/samagotchi/objectives_check.rb +37 -7
  60. data/lib/samagotchi/project_env.rb +3 -2
  61. data/lib/samagotchi/self_report.rb +2 -0
  62. data/lib/samagotchi/testing/boundary.rb +1 -0
  63. data/lib/samagotchi/testing/minitest.rb +3 -0
  64. data/lib/samagotchi/testing/rspec.rb +3 -0
  65. data/lib/samagotchi/testing/sandbox.rb +3 -1
  66. data/lib/samagotchi/tools/delegate.rb +2 -1
  67. data/lib/samagotchi/tools/task_runtime.rb +13 -10
  68. data/lib/samagotchi/turn_flow.rb +3 -1
  69. data/lib/samagotchi/update_command.rb +76 -28
  70. data/lib/samagotchi/update_hint.rb +4 -1
  71. data/lib/samagotchi/version.rb +1 -1
  72. data/lib/samagotchi/web/auto_update.rb +8 -8
  73. data/lib/samagotchi/web/auto_update_record.rb +6 -4
  74. data/lib/samagotchi/web/participant_view.rb +1 -1
  75. data/lib/samagotchi/web/public/app.js +5 -3
  76. data/lib/samagotchi/web/public/block_open.js +13 -0
  77. data/lib/samagotchi/web/public/bubbles.js +4 -2
  78. data/lib/samagotchi/web/public/cards_place.js +10 -5
  79. data/lib/samagotchi/web/public/details_motion.js +2 -1
  80. data/lib/samagotchi/web/public/index.html +18 -14
  81. data/lib/samagotchi/web/public/stage_model.js +5 -6
  82. data/lib/samagotchi/web/public/stage_view.js +8 -7
  83. data/lib/samagotchi/web/public/turn_block.js +34 -19
  84. data/lib/samagotchi/web/public/turn_model.js +1 -1
  85. data/lib/samagotchi/web/public/update_sheet.js +8 -3
  86. data/lib/samagotchi/web/public/working_set_view.js +10 -7
  87. data/lib/samagotchi/web/push/notifier.rb +7 -9
  88. data/lib/samagotchi/web/session_hub.rb +2 -1
  89. data/lib/samagotchi/web/update_ops.rb +2 -1
  90. metadata +8 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c93a3995e0d6192e52b21e25127bedc26e2442b9b45002ed6d3b758fc214c837
4
- data.tar.gz: 303ef3878f850b90a9f506fb8a09c710a25e18aca6836e8aa59ee27498258146
3
+ metadata.gz: caeb209690eb9b78ab3977c53ed4c7bbe7388b0a15e51b59b70da653ac9a9bdc
4
+ data.tar.gz: 43cd03b5039cc16b588323f82ca0e063053d4b78de83b9785e66dd7dbf8d4a71
5
5
  SHA512:
6
- metadata.gz: da19432429657d32a29891f5ac57d2a987d9f19fb7b8c027f7b8038660e51b76bc4a066331e2ad3a49fe62747b7f8dd85b5cd8f9b59d47282d1ca9b4ab7aaa74
7
- data.tar.gz: 33d8c08e66bf4eac732fabb7425a1ca00f3fabb8570418d5887c46db9ce0980466969437590908238861d67a267d139c9916e668bc546babf869cae3cc01f162
6
+ metadata.gz: 84a48999dd02619caa8900b8e2897614c93089519fa5e87d05e5f743b7073546e61b2c4e49a768798f1e612ef29eb768a86c7e69197e1880348ab5b767291ec4
7
+ data.tar.gz: 65ff02a943a5d58615976d531b22114c40d015b24098d230b5b389abff7bdacd33cabcb9b5622a3b57d054d2e156878a945a6e83895547ffe6dba8c929e0ab5b
data/CHANGELOG.md CHANGED
@@ -7,6 +7,52 @@ 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.150.0] - 2026-11-05
11
+
12
+ ### Changed
13
+
14
+ - The web's steps block closes to its summary line after a turn and after a reload, whatever its steps said; opened, it shows the outline as before. A canceled or failed turn with a narrated step stays open. [docs/cli.md](docs/cli.md#how-the-page-draws-a-turn)
15
+ - The web's working-set footprint is now the steps block's summary line, in body colour, the steps' count at its right (a phone shows the footprint alone); there is no separate line under the block. A click on its words opens the turn's list, the chevron or the count opens the steps. [docs/cli.md](docs/cli.md#the-working-set)
16
+ - Check this also reads the agent's visible answers, never its thinking, so a claim about what the agent said or asked can be confirmed. They settle only that: a claim of work with nothing but the agent's words behind it isn't `confirmed`. With `objectives.check_model` they go to that host too. [docs/sessions.md](docs/sessions.md#objectives)
17
+ - An upgrade now merges a memory you edited with the bundle's new version instead of leaving it behind. `chi bundle upgrade`, `chi update` and the automatic system bundle update run a text merge (needs `git`): a clean merge replaces the file, with the old one in the bundle trash and the report saying `merged 0.8.0 into your x.md`, and `--dry-run` says `would merge`. When a chi start updates the system bundle, a terminal launch says so in one line. Entries both sides added under a `Changelog` heading are kept, yours first. A real conflict, no `git`, or no recorded base leaves your file untouched, as before, and a file marked by an older chi can stay behind too (see the next line).
18
+ - A memory left behind as a conflict gets another try without waiting for the bundle's next version: the system bundle retries its files marked conflict at every chi start and on `chi update`, the other bundles chi ships on `chi update`, and any other bundle on `chi bundle upgrade`. It touches memory files only, and a row reads `0.8.0 merged 0.8.0 into your x.md`. A file marked by an older chi merges normally when its recorded base is a version chi shipped, checked against the samagotchi gems still installed. Otherwise that base may be your own text, so the file merges only if the bundle takes out or rewords no line it still holds. If it does, it stays behind until resolved by hand, with `--agent` or with `--force`, and the retry can't fix that.
19
+ - Everything that reports an edited bundle file now says what happened in one wording: `merged 0.8.0 into your x.md`, or `x.md stays behind 0.8.0: missed ## Steps, ## Rules and 2 more` with the reason when there is one (`no merge possible (git not found)`). The record keeps why and what was missed, and the bundle's text is saved at `.bundles/<name>/incoming/<file>` until the file settles. `kept your edits` is gone from `chi update`, whose `--json` rows gain `merged` next to `kept`, and from `chi bundle upgrade`, `chi bootstrap` and the system bundle's line at a start.
20
+
21
+ ### Fixed
22
+
23
+ - `bundle exec chi home set --ruby X` no longer records the bundle's vendor folder as X's gems when the bundle has a `path` and holds samagotchi: the probe of X still took `BUNDLER_SETUP`. A home set that way keeps its wrong `gem_home` until you run `chi home set` again.
24
+ - Under a `bundle exec chi`, a tool's plain `ruby` no longer loads Bundler: `BUNDLER_SETUP` still reached it, so in a folder whose Gemfile needs a missing gem `ruby -e` exited 1, and with Bundler 4 (`BUNDLE_LOCKFILE`) a folder with any other Gemfile had its lock written over chi's own `Gemfile.lock`. `execute`, tasks, `!cmd`, context sources and MCP servers all run without them, and `task_create` refuses them in `env`, saying to write `KEY=value cmd` in the command instead.
25
+ - `chi bundle install <git url>` run from a git hook or `git rebase --exec` no longer takes the repo git's variables name for its own: `GIT_WORK_TREE` failed the clone, `GIT_OBJECT_DIRECTORY` wrote the bundle's objects into that repo, and `GIT_DIR` alone recorded that repo's HEAD as the bundle's `source_commit`. Settings given around chi (`git -c url.….insteadOf=…`, `GIT_CONFIG_COUNT`) still apply to the clone. `delegate`'s `cwd:` check also asks about the folder it was given, as do the known-names (the git name), github-pr (the branch, `gh pr view`) and source-links (the remote) bundles.
26
+ - The web no longer loses a reply you steered: a line sent while a text-only step streamed left that step's whole text hidden, with no row or summary to open, live and after a reload. The step is now a row of the steps block (`1 step`), like any other text step.
27
+ - A web card updated just as a finished turn slid into the history no longer stays stuck on its first text until a reload (`/short` could stay on "Short version: …").
28
+ - A web card no longer lands a turn too early for good after `!rollback` of a canceled or failed turn: the rollback takes the turn out of the count. A rolled-back or failed turn's own cards stay where the turn was, without their buttons, instead of being read as the next turn's.
29
+ - A web card no longer lands one turn late for good after a worker was killed in the next turn (`kill -9`, a pod that didn't wait): the turn is counted, since its prompt stays in the session.
30
+ - A web card keeps its place after a reload when the turn after it failed with nothing kept: above the failed prompt, as it showed live, not below it, and an older card no longer lands one turn too late. A failed prompt isn't counted as a turn any more.
31
+ - `chi bundle list` and `chi self` say when a bundle has a file behind (`1 file behind`), and `chi bundle status` says it per file. `issues=` now counts files that stay behind, are missing or can't be read: an edit alone is no issue. A file `chi bundle status` can't read no longer crashes it.
32
+ - Every merge now leaves a note on the file's record, since a worker's reaches only the log and an automatic update's rows no one reads: `chi bundle status` shows it (with the old copy's folder), `chi update` names an earlier one in its row, and a row printed to you, or `chi bundle upgrade`'s output, ends the note. An automatic update names it without clearing it.
33
+ - `chi update` no longer crashes on an edited memory it can't read at a newer version: that bundle's row fails and says which file, and the others go on.
34
+ - The start-up hint `… can be updated: chi update` counts a bundle whose file would now merge at the same version.
35
+ - The update sheet shows `chi update`'s own words for an edited file, and the phone's push after an automatic update counts both sides (`Merged the update into 1 file you edited. 2 files stay behind.`): a merge used to go unmentioned, and every kept file read as `Kept your edits`.
36
+ - `chi update` no longer reads `up to date` for a file that stays behind at the same version: a real conflict, no `git`, a timeout, a file it can't read, or one left for the bundle's next version now name the file and why, and the summary counts them.
37
+ - A file marked by an older chi whose base chi can't vouch for no longer claims to hold your edits: it says chi can't tell, and why.
38
+ - `chi update`'s table no longer carries the trash's long path: the row says `old copies: chi bundle trash`, and `--json`'s `trash_copy` has the full line.
39
+ - The `--force` suggestions say what happens to your file: it is copied to the trash first.
40
+ - `chi bundle install --force` and `chi bundle upgrade --force` no longer destroy what they overwrite: a memory that differs from the bundle's, your own same-name file too, is copied to the bundle trash first, and the output names it (`Copied to the trash first: …`; `chi bundle trash` lists it).
41
+ - An install, upgrade or `chi update` that replaces or removes a hook, rule, plugin or script you edited copies it to the bundle trash first, under its own subfolder (`hooks/x`, `scripts/sub/x`) at any depth, and says so. A `--dev` install's links are left out.
42
+ - `chi bundle uninstall`, with `--force` or without, copies a hook, rule, plugin or script you edited to the bundle trash before removing it, and names it. Only memories reached the trash before.
43
+ - An install, upgrade or uninstall that can't read a file or write the bundle trash fails with a message naming the file, not a backtrace, and running it again after fixing that picks up where it stopped.
44
+ - A memory whose conflict was resolved through `chi bundle upgrade --agent` keeps its description in the bundle's record.
45
+ - `chi bundle upgrade --agent` no longer calls a conflict resolved when nothing merged it. A file the session didn't change, or one still holding conflict markers, stays a conflict, the output says so (`Nothing changed in X: still behind.`), and the command exits 2. It used to mark the file resolved, so later upgrades kept it without a word and the bundle's change never arrived.
46
+ - An edited memory with no recorded base keeps surviving upgrades as a conflict. Its own text used to become the base, so the following upgrade overwrote it with nothing in the trash. A record an older chi already left that way (marked conflict, the base being your text) stays behind when the update would take out a line it holds. Otherwise it takes the update, with your text copied to the bundle trash first and a line saying so.
47
+ - A plain `chi bundle install` keeps the conflict mark on a file it skipped, so `chi bundle status` still shows the file missed an update.
48
+ - `chi bundle upgrade --dry-run` no longer prints a false `Checksum mismatch` for a memory you edited, and its `Kept` line now names those files. This only changes a dry run's output.
49
+ - `chi bundle upgrade --agent` no longer takes your file as the new base when the bundle's copy disappears during the session. The next upgrade used to overwrite the edit with nothing in the trash; the file now stays a conflict and the command says so.
50
+ - An install or upgrade no longer fails at its last step on a memory that isn't valid UTF-8.
51
+ - A Changelog merge compares whole entries, nested items included: your `- a` / ` - note` beside the bundle's `- b` / ` - note` no longer loses the bundle's ` - note`.
52
+ - Putting back a deleted system memory at the same version now restores that file alone. It used to run a whole upgrade, replacing hooks, rules and the plugin under live sessions.
53
+
54
+ Update with `chi update` (github-pr 0.6.3, known-names 0.1.9, source-links 0.6.3: their git calls ask about the session's folder, and they need this chi; dev profile 0.7.6).
55
+
10
56
  ## [0.149.0] - 2026-11-04
11
57
 
12
58
  ### Added
@@ -5624,7 +5670,8 @@ and long-lived sessions.
5624
5670
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
5625
5671
  hotkey panel that send selected text or the clipboard to your sessions.
5626
5672
 
5627
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.149.0...HEAD
5673
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.150.0...HEAD
5674
+ [0.150.0]: https://github.com/dm1try/samagotchi/compare/v0.149.0...v0.150.0
5628
5675
  [0.149.0]: https://github.com/dm1try/samagotchi/compare/v0.148.0...v0.149.0
5629
5676
  [0.148.0]: https://github.com/dm1try/samagotchi/compare/v0.147.0...v0.148.0
5630
5677
  [0.147.0]: https://github.com/dm1try/samagotchi/compare/v0.146.0...v0.147.0
data/README.md CHANGED
@@ -53,8 +53,10 @@ chi update # the gem, then the bundles and the desktop helper
53
53
  `chi update` installs the newest samagotchi from rubygems.org (old versions
54
54
  stay installed: running sessions still use them), then brings the rest up to
55
55
  the new version: the system bundle, the bundles chi ships that you installed,
56
- and the macOS helper (rebuilt only when its sources changed). Memory files you
57
- edited are kept, and the table says which. Running sessions keep their chi
56
+ and the macOS helper (rebuilt only when its sources changed). A memory file you
57
+ edited takes the update in a text merge (the old file goes to the bundle trash),
58
+ or stays behind as it is when no merge is possible, and the table says which
59
+ and why. Running sessions keep their chi
58
60
  until restarted (`chi sessions restart ID`, or the Restart badge on the web);
59
61
  new and woken ones start on the new chi. A running `chi web` needs a restart,
60
62
  and its page says so. From a checkout, `git pull` instead. See
data/docs/architecture.md CHANGED
@@ -102,7 +102,7 @@ terminal renders the events it gets over the Bridge.
102
102
  | Model loops | `KernelLoop` (via `LLM::NativeBackend`), `LLM::ChatLoop` | The model↔tool loop: raw prompt, or a chat API (OpenAI chat, Anthropic Messages), chosen per host (see below). |
103
103
  | Adapters | `Samagotchi::Client`, `LLM::OpenAIChat`, `LLM::AnthropicMessages`, `LLM::HTTP` | Raw-prompt servers, the OpenAI chat API, Anthropic's Messages API, and the HTTP they share. |
104
104
  | Tools | `lib/samagotchi/tools/*` | Execute, read, edit, write, memory, task_*, web_fetch, delegate/delegate_result, ask_user_question, reminders, send_note, list_sessions, context_read, objectives, forget_outputs, find_tools/call_tool (the tool index), plus runtime/output-guardrails. |
105
- | Bundles | `lib/samagotchi/memory_bundle/*`, `BundleCommand` | `chi bundle`: install/upgrade (3-way merge), build, uninstall; the install record under `.bundles/<name>/` that the hook, guardrail and plugin loaders read ([internals/bundles.md](internals/bundles.md)). |
105
+ | Bundles | `lib/samagotchi/memory_bundle/*`, `BundleCommand` | `chi bundle`: install/upgrade (3-way, with a text merge of edited memories), build, uninstall; the install record under `.bundles/<name>/` that the hook, guardrail and plugin loaders read ([internals/bundles.md](internals/bundles.md)). |
106
106
  | Background | `Samagotchi::Worker`, `Samagotchi::SessionManager` | `SessionManager` forks/spawns one worker process per session; its `Worker` builds the `Engine` and `Bridge` (no terminal rendering) and runs queued turns. The spawned process (own process group) forks the worker, which `setsid`s and is reparented to init once its starter exits (it waits until the worker holds `owner.lock`, so the spawner's `Process.detach` waiter tells a starting worker): nothing that walks the spawner's process tree reaches it (`SAMAGOTCHI_WORKER_DETACH=1`, `detach_from_spawner`). The lock's pid is the worker's; `spawn`'s `child_pid` is its starter's. |
107
107
  | Web | `Samagotchi::Web::App`, `Samagotchi::Web::Server`, `Samagotchi::Web::SessionHub` | Rack+WEBrick on one port, `127.0.0.1:4567` (index.html + `/api/*` + SSE); with `web.host: lan` also on this machine's LAN address, behind an access token (`Web::Lan`, `Web::Token`). The hub is chi web's projection of the session list, pushed to every tab over `GET /api/events`. |
108
108
  | Sessions | `Samagotchi::Session`, `SessionManager` | File `sessions/<uuid>.json` + sidecar `input/`/`output/`; retention 14d/500, `updated_at desc`, lazy sweep. |
@@ -394,7 +394,7 @@ record starts at `phase: installed` and the run goes on as below. Ahead of every
394
394
  `SessionHub#auto_offer_restart` (`auto_offers`, in memory) when a run on record for the newest installed chi is closed with
395
395
  this chi web still older, or that chi has waited `HELD_FOR` (1 h): the trying goes on, and the page has its buttons back;
396
396
  `restart_failed` and a failed install call it at once. Its record is `update_check.json`'s `auto`:
397
- `{version, from, at, kept, phase: installed|workers|web|retrying|done|failed, workers: {restarted, left}, detail, pushed}`,
397
+ `{version, from, at, kept, merged, phase: installed|workers|web|retrying|done|failed, workers: {restarted, left}, detail, pushed}`,
398
398
  in Ruby the value object `Web::AutoUpdateRecord`, which owns that shape (`from_h`, `to_store`).
399
399
  With `update.restart: never` the run ends at the install (`chi_notice` `installed`). With `idle` it goes on
400
400
  (`phase` `installed` → `workers` → `web` → `retrying` → `done`): `UpdateOps#restart_workers(select:)` over the stale workers whose
data/docs/cli.md CHANGED
@@ -228,9 +228,11 @@ $ chi models qwen # only the names containing "qwen" (any case)
228
228
  ```
229
229
  component from to status
230
230
  chi (gem) 0.2.0 0.3.0 updated
231
- system bundle 0.2.0 0.3.0 updated (kept your edits in identity.md: chi bundle diff samagotchi-system identity.md)
231
+ system bundle 0.2.0 0.3.0 updated (identity.md stays behind 0.3.0: missed ## Who you are (chi bundle diff samagotchi-system identity.md))
232
232
  btw 0.1.1 up to date
233
- known-names 0.1.0 0.1.1 updated
233
+ known-names 0.1.0 0.1.1 updated (merged 0.1.1 into your known_names.md; old copies: chi bundle trash)
234
+ coordinator 0.8.0 merged 0.8.0 into your skill_coordinator.md; old copies: chi bundle trash
235
+ loop-guard 0.2.0 notes.md stays behind 0.2.0: no merge possible (git not found) (chi bundle diff loop-guard notes.md)
234
236
  core 0.1.0 0.2.0 updated (+ new-bundle)
235
237
  infra_tools 1.0.0 skipped (not from chi)
236
238
  infra_tools warn api plugin.rb:3: ctx.old_name isn't in chi 0.3.0's plugin API (fix: docs/plugins.md has the API)
@@ -238,15 +240,17 @@ Chi Helper 0.2.0 up to date (launch file refreshed)
238
240
  workers 2 live on 0.2.0: they keep it until restarted (chi sessions restart 2ea8c1f0 91b0d2aa); or they idle out after 30 min with nothing attached
239
241
  Bundles not from chi are left as they are: their author's next version, or chi bundle upgrade NAME, is what changes them
240
242
  Also shipped, not installed: dev (mcp, skills, source-links) (chi bundle install NAME)
241
- done
243
+ 2 files stay behind (see above)
242
244
  ```
243
245
 
244
246
  - `--to VERSION` installs exactly that gem version instead of checking for the
245
247
  newest release. It must be newer than the running chi; an equal or older
246
248
  version is refused.
247
249
  - `--json` writes the component rows as one JSON array on stdout. Each row has
248
- `component`, `from`, `to`, `status`, `failed`, `kept` and `level` fields;
249
- `kept` is an array of edited filenames, and `level` is `warn` or `error` on a
250
+ `component`, `from`, `to`, `status`, `failed`, `kept`, `merged`, `earlier`, `trash_copy` and `level` fields;
251
+ `kept` is an array of edited filenames left behind, `merged` of those this run
252
+ merged, `earlier` of those an earlier merge's note names, `trash_copy` the full line
253
+ saying where the old copies went (the table says `chi bundle trash`), and `level` is `warn` or `error` on a
250
254
  re-check's row (below), `null` on the others. Install progress is written to
251
255
  stderr.
252
256
 
@@ -304,11 +308,36 @@ done
304
308
  the row says what it did.
305
309
  - **Shipped bundles**: each one you installed from chi (`chi bundle install
306
310
  NAME`) is upgraded when chi ships a newer version. Memory files get the
307
- 3-way merge of `chi bundle upgrade`: an unedited file is updated, an edited
308
- one that the new version also changes is kept, and the row says so (`chi
309
- bundle diff NAME FILE` shows it; `chi bundle upgrade NAME --force` takes the
310
- bundle's). Hooks, rules and the plugin are replaced; an edited one didn't
311
- load anyway (its sha no longer matched) and the row says it was replaced.
311
+ 3-way merge of `chi bundle upgrade`: an unedited file is updated, and an
312
+ edited one that the new version also changes takes a text merge, with the old
313
+ file in the bundle trash and `merged 0.8.0 into your FILE` in the row. A real
314
+ conflict, no `git`, or no recorded base keeps your file untouched, and the row
315
+ says `FILE stays behind 0.8.0:` with why and what it missed (up to three headings
316
+ and a count, or `2 places`). `chi bundle diff NAME FILE` shows it; `chi bundle
317
+ upgrade NAME --force` takes the bundle's, with a copy of yours in the bundle
318
+ trash first. A later run retries such a file at the same version: its row reads
319
+ `0.8.0 merged 0.8.0 into your FILE` when it merges, and names it as behind when
320
+ it doesn't. So does a file left for the bundle's next version (it matches its old
321
+ base), or one the retry can't read, and the row never says `up to date` while a file
322
+ stays behind. Once a file has the bundle's bytes it isn't behind, however it got
323
+ them. A file marked by an older chi
324
+ merges normally when its recorded base is a version chi shipped (an installed
325
+ samagotchi gem still has it). Otherwise that base may be your own text, so it merges only if the bundle takes out or
326
+ rewords no line it still holds. If it does, chi can't tell whether the file holds
327
+ edits of yours, says so, and the file stays behind until resolved by
328
+ hand, with `--agent` or with `--force`; the retry can't fix that. Every merge
329
+ leaves a note on the file's record, since a worker's reaches only the log and an
330
+ automatic update's rows may be read by no one. `chi bundle status` shows it while it
331
+ exists. A `chi update` row names an earlier one (`up to date (merged 0.8.0 into your
332
+ FILE on 2026-11-05 14:03; …)`), and a row printed to you ends the note for what it
333
+ printed, that run's own merges included; so does `chi bundle upgrade`, which prints
334
+ the merge itself, and `--force` replacing the file. A dry run and an automatic update
335
+ (`update.auto`) show it and leave it, and so does a failed row or one with no place
336
+ for it (a skipped system bundle). A memory that can't be read at a
337
+ newer version fails that bundle's row with the file's name. Hooks, rules and the plugin are replaced; an edited one didn't
338
+ load anyway (its sha no longer matched), the row says it was replaced, and
339
+ a copy goes to the bundle trash first (`chi bundle trash`; `hooks/x`,
340
+ `guardrails/x`, `plugin/x`, `scripts/x` inside the dir).
312
341
  A bundle of the same name from elsewhere (a zip, git) is skipped ("not from
313
342
  chi"), a newer installed one is left, one whose new version needs a newer
314
343
  chi is skipped, and bundles you didn't install stay uninstalled.
@@ -367,9 +396,10 @@ this version's view: bundles that only a newer gem ships newer show up once
367
396
  that gem is installed (the real run installs it first and hands over).
368
397
  `--no-gem`, `--no-bundles` and `--no-desktop` leave a part alone for one run;
369
398
  `update.gem`, `update.bundles` and `update.desktop: false` in config.yml turn
370
- one off for good. It exits 0 when nothing failed (kept edits and skips are
399
+ one off for good. It exits 0 when nothing failed (merged edits, files that stay behind and skips are
371
400
  fine), 1 when a part failed, 2 on a usage error. A second run changes nothing
372
- and ends with "everything is up to date".
401
+ and ends with "everything is up to date", or with `N files stay behind (see above)`
402
+ when some do.
373
403
 
374
404
  From a checkout it refuses (`git pull`, or `chi bundle upgrade NAME` for one
375
405
  bundle). After a gem update, the first interactive start of the new version
@@ -1189,14 +1219,17 @@ again). When a step ends its thinking closes to a plain `thinking` line
1189
1219
  (unless you opened it); a step that only thought shows that line alone. The
1190
1220
  live narration is a box of about three lines that fills sentence by
1191
1221
  sentence (a sentence shows once it is complete) and scrolls to the newest
1192
- when full. When the turn ends the block shows its outline when any of its steps said
1193
- something: it stays open on one line per step that wrote text (that text's
1194
- first line, so a mid-turn note reads without a click), and each run of steps
1195
- that only called tools or thought sits behind one `· N steps` line that
1196
- shows them; old turns open the same way after a reload. A turn whose steps
1197
- said nothing collapses the block to its summary (`3 steps
1198
- · 8 tool calls · execute ×7 · read ×1`). Either way the answer is a normal
1199
- bubble under the block (Markdown, annotate). A plain answer
1222
+ when full. When the turn ends the block closes to its summary (`3 steps
1223
+ · 8 tool calls · execute ×7 · read ×1`, after the turn's
1224
+ [footprint](#the-working-set) when it has one). Opened, it shows its outline when any of
1225
+ its steps said something: one line per step that wrote text (that text's first
1226
+ line), and each run of steps that only called tools or thought sits behind one
1227
+ `· N steps` line that shows them. That includes a reply a line was steered into: its
1228
+ step is a row like any other, and the answer that follows is the bubble. A canceled or failed turn with such a step
1229
+ stays open, as its last narrated step is the only text you get. A reload draws
1230
+ the same from what was saved: a failed turn whose stream dropped mid-narration
1231
+ saved no such step, so it reloads closed. Either way the answer is a normal bubble under the block (Markdown,
1232
+ annotate). A plain answer
1200
1233
  without tools ends exactly as it does today. Rows the code collapses stay
1201
1234
  as you toggled them. A reloaded session shows the same block
1202
1235
  from the saved messages (each step's thinking, narration, tool parameters
@@ -1239,12 +1272,14 @@ session bar shows the `mem N` chip again.
1239
1272
  folds it to one line (`SET 17 + ◆ name`), kept per browser; a click opens the
1240
1273
  list. In a stage narrower than 760 px it is one line under the last calls;
1241
1274
  on a phone it isn't shown.
1242
- - **The footprint**, one line under each finished turn's block, live and after
1275
+ - **The footprint**, each finished turn's block's summary line, live and after
1243
1276
  a reload: `touched …/web/public/ · 4 files · ◆ 2 memories s2–10 · +1 url ·
1244
1277
  edited 1`, then `n stale`, `n forgotten` or `n dropped` while some are (a file
1245
- changed after the turn read it, even at its very end, says so here). A step
1246
- tag (`s2`) goes to that call's row; the rest opens the turn's list. A turn
1247
- that touched nothing has none.
1278
+ changed after the turn read it, even at its very end, says so here), with the
1279
+ steps' count at the right. A step tag (`s2`) goes to that call's row; the rest
1280
+ of its words opens the turn's list; the chevron or the count opens the steps.
1281
+ A phone shows the footprint alone, no count. A turn that touched nothing has
1282
+ none: its summary is the count.
1248
1283
  - **The `set N` chip** in the session bar, in place of the `mem N` chip (whose
1249
1284
  tooltip goes too): the session's whole set, preloaded (`--memory`) and muted
1250
1285
  (`--mute`) memories included; its dot lights when the set changed since you
@@ -2141,7 +2141,7 @@ Both are read when chi web starts (restart it to apply a change). What it does,
2141
2141
  install isn't blocked (a source checkout, Bundler, a RubyGems directory chi can't write: those stay a notice, with
2142
2142
  the command to run). `chi update` also merges the shipped bundles that live sessions read, hence "no turn anywhere".
2143
2143
  2. **Installs** exactly the release the notice named, as the **Update** button does (the system bundle and shipped
2144
- bundles are merged three ways; files you edited are kept and counted in the push). The page's sticky toast says
2144
+ bundles are merged three ways; a file you edited takes a text merge, one with a real conflict stays behind, and the push counts both). The page's sticky toast says
2145
2145
  "Automatic chi update in progress" and its sheet shows the rows; with `update.restart: idle` no "installed"
2146
2146
  toast with **Restart** buttons follows (the run restarts by itself). A failed install is tried **once per version**:
2147
2147
  it pushes the failed row's reason and stays quiet until a newer release (or you, with the button) tries again.
@@ -2158,7 +2158,7 @@ Both are read when chi web starts (restart it to apply a change). What it does,
2158
2158
  the install; after that they wait for their own idle exit or the session's **Restart**. A newer release in that day
2159
2159
  installs as usual and takes them over.
2160
2160
  4. **Says so.** The restarted chi web pushes "chi updated to X" ("chi web and 3 sessions restarted; 1 restarts when
2161
- idle. Kept your edits in 2 files."); the later restarts of those sessions push nothing more. With
2161
+ idle. Merged the update into 1 file you edited. 2 files stay behind."); the later restarts of those sessions push nothing more. With
2162
2162
  `update.restart: never` the push is "chi X is installed" and chi web and the workers move over when you restart them (the page's toast offers it). The push needs the phone set up
2163
2163
  ([Phone notifications](#phone-notifications-web-push)); without it the page's toasts and
2164
2164
  `$XDG_STATE_HOME/samagotchi/update_check.json` (`auto`) are the record.
@@ -2273,7 +2273,7 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
2273
2273
  | `context.candidates.threshold` | `0.8` | | The probability at which the judge attaches a candidate in on mode (a judged line's `would: attach`). config.yml only. |
2274
2274
  | `context.candidates.offer_threshold` | `0.6` | | The probability from which on mode shows the candidate's card, once (`would: offer`, below `threshold`). config.yml only. |
2275
2275
  | `context.candidates.reask_after_turns` | `3` | | Owner prompts after a judged ask before the judge's one re-ask of that candidate. config.yml only. |
2276
- | `objectives.check_model` | none | | The model [Check this](sessions.md#objectives) asks, its one side request (a host:id, an alias, a host's model id); none: the session's own. **Each Check this sends that model's host up to 24,000 characters of the session's tool output and messages, off the machine with a hosted model even from a local session;** known key shapes and `token=`-style values are redacted first, by pattern only. config.yml only. |
2276
+ | `objectives.check_model` | none | | The model [Check this](sessions.md#objectives) asks, its one side request (a host:id, an alias, a host's model id); none: the session's own. **Each Check this sends that model's host up to 24,000 characters of the session's tool output, messages and the agent's visible answers, off the machine with a hosted model even from a local session;** known key shapes and `token=`-style values are redacted first, by pattern only. config.yml only. |
2277
2277
  | `objectives.system_one` | `off` | | `audit`: System 1 answers a yes/no per active objective on what comes in and goes out, saved as runs and acted on by nothing; see [System 1's saved runs](system-one.md#objectives-in-audit). `off` (the default): no conversation text goes to a System 1 host for this. Its host and model are `system_one.host` / `.model`. config.yml only. |
2278
2278
  | `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". |
2279
2279
  | `toolkit.child` | `full` | | A delegate child's toolkit, unless its `delegate` call names one (`toolkit:`). See "Toolkits". |
@@ -2305,7 +2305,7 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
2305
2305
  | `update.bundles` | `true` | | `false`: `chi update` leaves the shipped bundles to `chi bundle upgrade` (`--no-bundles`). |
2306
2306
  | `update.desktop` | `true` | | `false`: `chi update` leaves the desktop helper alone (`--no-desktop`). |
2307
2307
  | `update.notify` | `on` | | `on`: chi web checks RubyGems once a day and shows aged stable releases newer than both this chi and the newest installed chi; `off`: no thread or network check. |
2308
- | `update.auto` | `off` | | `install`: chi web installs the release its checker found (aged by `update.min_age_hours`) once no turn runs and no question waits in any session, as the **Update** button would (shipped bundles merged, your edits kept and reported), then pushes how it went; `update.restart` says what follows, also for a chi installed some other way (`chi update` in a terminal). An install that fails is tried once per version (a newer release, or the **Update** button, tries again). The check runs even with `update.notify: off` (which then only silences the page's and the phone's "available"). An install that can't run here (a checkout, Bundler, an unwritable gem dir) stays a notice. `off`: notice only. See [Unattended updates](#unattended-updates-updateauto). |
2308
+ | `update.auto` | `off` | | `install`: chi web installs the release its checker found (aged by `update.min_age_hours`) once no turn runs and no question waits in any session, as the **Update** button would (shipped bundles merged, a file that can't merge left behind, both reported), then pushes how it went; `update.restart` says what follows, also for a chi installed some other way (`chi update` in a terminal). An install that fails is tried once per version (a newer release, or the **Update** button, tries again). The check runs even with `update.notify: off` (which then only silences the page's and the phone's "available"). An install that can't run here (a checkout, Bundler, an unwritable gem dir) stays a notice. `off`: notice only. See [Unattended updates](#unattended-updates-updateauto). |
2309
2309
  | `update.restart` | `idle` | | With `update.auto: install`: `idle` restarts the stale workers whose session is idle and untouched for 10 minutes, then chi web itself, once no turn runs anywhere and the newest chi has answered `chi --version`; it pushes "chi updated to X". `never`: the install only (the page's toast offers the restarts, workers also exit on their own when idle). |
2310
2310
  | `update.check_hours` | `24` | | Hours between RubyGems checks (at least 1). The first check starts about 30 seconds after chi web starts. |
2311
2311
  | `update.min_age_hours` | `24` | | A release must be this old before chi web offers it; `0` disables the age delay. |
@@ -2329,7 +2329,7 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
2329
2329
  | `web.port` | `4567` | `--port` | `chi web`'s port. See [CLI](cli.md). |
2330
2330
  | `web.host` | `127.0.0.1` | yes | `127.0.0.1`, `::1` or `localhost`; `lan` (this machine's private IPv4 address) or one of its IPv4 addresses also opens `chi web` to the network, with an access token, asked of this machine's 127.0.0.1 too (its `Local:` link carries a one-time code; `chi web --new-token` replaces the token). Anything else binds `127.0.0.1` with a warning. See [CLI: chi web on your phone](cli.md#chi-web-on-your-phone). |
2331
2331
  | `web.markdown` | `false` | yes | Render answers as Markdown in `chi web`. |
2332
- | `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. See [CLI: the working set](cli.md#the-working-set). |
2332
+ | `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 as each finished turn's block summary 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. See [CLI: the working set](cli.md#the-working-set). |
2333
2333
  | `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). |
2334
2334
  | `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). |
2335
2335
  | `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; the header's project picker switches projects. `~` 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). |
@@ -42,7 +42,7 @@ Behavior:
42
42
  again or stop it (`TaskWait::MESSAGE_WAITING_NOTE`), and `waited: Ns`; the task keeps running and stays watched
43
43
  (below). A task that finished meanwhile gives its finished result, as after a Stop. A `chi -p` turn has no probe. The same probe ends a blocking `delegate_result` (and `delegate`'s wait) early; see [Sessions: Delegating](../sessions.md).
44
44
  - `task_create` accepts `env` as a JSON object string for deterministic overrides such as `PATH`; use an absolute interpreter path when that is simpler. Ruby/Bundler isolation variables remain protected.
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.
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` and what `bundle exec` sets (`BundlerEnv::VARS`: `BUNDLE_GEMFILE`, `BUNDLE_LOCKFILE`, `BUNDLE_BIN_PATH`, `BUNDLER_VERSION`, `BUNDLER_SETUP`) are unset, so a project's `ruby -e` or `bundle exec` uses its own Gemfile, or none, when chi runs under `bundle exec`. RubyGems requires what `BUNDLER_SETUP` names at boot, whatever `RUBYOPT` says. A project's own settings (`BUNDLE_PATH`, `BUNDLE_WITHOUT`) stay. `task_create` refuses those keys in the model's `env`, its error saying to write `KEY=value cmd` in the command instead; `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
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 through `ProjectEnv.spawn_env` (the scrub above, then `tools_env`: what `execute` spawns with, for the runners that don't go through it) a context source's command (`ContextFetch#env_for`) and an MCP server, under the server's own keys (`bundles/mcp/plugin.rb` `server_env`, from mcp 0.7.4; no participant rule: the server is the session's). 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).
@@ -52,6 +52,9 @@ the "asking again" and steer-cut rows, ✂ rows and questions (with how they wer
52
52
  - **Placement is relative.** An entry carries `turns` and lists with `turns_since` (turns completed after it) and
53
53
  `current` (a card of the running turn). A card that comes mid-turn but isn't the turn's (an anytime command's, such as
54
54
  `/btw`) goes after that turn (`during`). A card with an earlier card's id replaces it in place.
55
+ - **A turn that leaves nothing.** `!rollback` (`TurnFlow#rollback!`, `:turn_rolled_back`) takes a kept turn's count
56
+ back. Of a rolled-back turn, and a failed one with nothing kept, the cards and hook notices become between-turns entries
57
+ at its place (the cards lose their actions); its other rows go, having no step to sit in.
55
58
  - **Caps.** `CAPACITY` (20) each for notices, ✂ rows (`LLM_CONTEXT_TYPES`) and cards, so one burst cannot push out
56
59
  another kind. An open question (an approval too) is never pushed out.
57
60
  - **Saved.** With a `path` (`cards.json` in the session's folder) the store rewrites the file after each event in
@@ -60,6 +63,11 @@ the "asking again" and steer-cut rows, ✂ rows and questions (with how they wer
60
63
  and an unanswered question waits for no one: neither is saved or seeded. An entry from an earlier worker lists with
61
64
  `earlier: true`; one shown during a turn comes back **without its actions** (their turn no longer runs), while a
62
65
  between-turns card keeps its buttons.
66
+ - **A killed turn counts.** A worker killed mid-turn never saw the turn end, but `TurnCheckpoint` keeps its prompt (the
67
+ next worker settles it as canceled). The Bridge's store (`killed_turn_stays:`) saves `"running": true` at
68
+ `:turn_started`, and a store started from that adds the one turn. An ordered end clears it, so a graceful stop's
69
+ cancel counts once; a file without the field counts none. A REPL's store never saves it: it keeps no running turn in
70
+ the session, so a kill takes the prompt too.
63
71
 
64
72
  ## SSEWriter
65
73
 
@@ -13,9 +13,14 @@ user-facing side (what a bundle owns, profiles, needs) is in [memory.md](../memo
13
13
  *.md, index.md the bundle's memories, one managed index line each ("· from <bundle>")
14
14
  <system memories>/.bundles/ MemoryPaths.bundles_dir
15
15
  <name>/manifest.json the install record (Provenance#write, read as InstalledBundle)
16
- <name>/bases/ each file as installed: the base of the next upgrade's 3-way merge
16
+ <name>/bases/ each file's base for the next upgrade's 3-way merge: as installed, or the
17
+ bundle's own text after a merge
18
+ <name>/incoming/ the bundle's text for each file marked conflict, until the file settles (Provenance:
19
+ #write, #refresh_marks, gone with #resolve_conflicts); nothing globs it for memories
20
+ (Builder takes the scope dir's top level), and an uninstall's rm_rf sweeps it
17
21
  <name>/hooks/ guardrails/ plugin/ scripts/
18
- .trash/<name>-<stamp>/ files an uninstall or upgrade took away (Trash), never deleted
22
+ .trash/<name>-<stamp>/ files an uninstall or upgrade took away, and copies of what an install
23
+ overwrote (Trash), never deleted
19
24
  ```
20
25
 
21
26
  ## Components
@@ -35,15 +40,19 @@ user-facing side (what a bundle owns, profiles, needs) is in [memory.md](../memo
35
40
  | `MemoryFiles` | which top-level `*.md` files of a bundle are its memories (`.select`: the `files:` list, else all but README/LICENSE and the like), `.unshipped` (listed, not shipped) and `.symlinks` (skipped: a symlink is never a memory) |
36
41
  | `Manifest` | parses/validates `manifest.yml` (`name:` is the bundle's name and a directory under `.bundles/`, so a path such as `../x`, `a/b`, `.` or `..` is refused: `name: … is not a bundle name`; YAML that doesn't parse or isn't a mapping is a `ValidationError` too; files, hooks as `BundleHook`, plugin, scripts, needs, requires_chi, includes, context_providers, session_settings: the keys a session's `chi --set` may give, a list or a mapping of them to types (`SESSION_SETTING_TYPES`), recorded at install in `manifest.json` (`session_settings`, `session_setting_types`) for `SessionConfig` to check; settings: the keys config.yml's `bundles: <name>:` may have, the same shape, recorded as `settings` (nil when the manifest has none) and `setting_types` for `BundleSettingsCheck`, whose warnings the Engine holds for the first turn and `SessionConfig`'s refusals share the words of) and writes it (`Manifest.write`, for Builder). It ignores `requires_chi_next`, a shipped bundle's release-time marker (`script/release/release_tools.rb`, docs/releasing.md) |
37
42
  | `Installer` | one install/upgrade/dry run of one bundle (below) |
38
- | `AssetInstaller` | the parts that live in `.bundles/<name>/`: hooks, guardrail rules, plugin, scripts; returns `AssetInstaller::Installed` (the paths the record keeps) |
39
- | `Merger` | 3-way classification per file: `install`, `fast_forward`, `keep`, `noop`, `conflict` |
40
- | `Provenance` | the `.bundles/<name>/` dir: `#write` the record + bases, `#record` → `InstalledBundle`, `.each_installed`, `.claimants` (which bundles own a file) |
43
+ | `AssetInstaller` | the parts that live in `.bundles/<name>/`: hooks, guardrail rules, plugin, scripts; returns `AssetInstaller::Installed` (the paths the record keeps). A file it replaces or removes whose bytes are neither the recorded sha nor the incoming file's is copied to the Installer's `Trash` first (`Trash#copy_edit`, `#copy_edits` for a folder at any depth, as `hooks/x`, `guardrails/x`, `plugin/x`, `scripts/sub/x`; no sha recorded counts as edited; a dev link never is) |
44
+ | `Merger` | 3-way classification per file: `.classify` → an `Outcome` (`status`: `install`, `fast_forward`, `keep`, `noop`, `conflict`, `merged`; `base`: where the file's next base comes from, `:on_disk`, `:recorded` or `:incoming`; a new outcome and its base go in its constants, and it needs a branch in `Installer#merge_memory`, a kind in `#next_base` if its base is new, and its status word in `#preview_memory`, `#verify_memory_checksums` and `#summary`). `.classify` never merges and answers `conflict` for two changed sides; `Merger::Upgrade` (provenance, target dir, source dir) is the one way to ask what an upgrade does to a file, nothing written: `#outcome` (`.classify`, then `TextMerge` where both sides changed, git once per call: `merged`, with its `TextMerge::Result` and the digest it read as the Outcome's `merge` and `current_sha`; `ALREADY_MERGED`, a `keep` with base `:incoming`, when the merge adds nothing; or `conflict` with that `merge`, whose `why` says why none was possible), `#classified`, `#forecast` (a `Forecast` of `merged` and `behind` in one pass), `#behind` (a `conflict` outcome as a `FileStanding::Behind`: why, and from `TextMerge.changed_sections` what it missed, nothing for an older mark, whose base may be the user's text), `#paths`, used by the Installer's merge and dry run, `ShippedUpdate.plan` and `SystemBundle.sync(dry_run:)`. `.write_merged` puts a merge in place: the digest checked just before the rename (`AtomicFile.write`'s block), through a symlink, the mode kept, the old file in the `Trash`; false when a live `memory_write` got in. A mark from an older chi (`InstalledBundle.legacy_mark?`: no `incoming_sha`, or `legacy_base`, which keeps that known once the digest is recorded, until the mark goes) may have the user's own text as its base, 0.149's doing, and a merge against it would undo that text with nothing to show, or a fast-forward replace all of it: `#outcome` makes a fast-forward that takes a line out of the file, and `.merge_outcome` a merge that does, a `conflict` (`FileStanding::LEGACY_BASE`), on the Installer's and `Remerge`'s path alike, unless `ShippedHistory` proves the base is a version chi shipped. The price, with no such gem left or for a bundle chi doesn't ship: a real base looks the same, so such a file whose bundle rewords or removes a line it still holds stays behind until it is resolved by hand, with `--agent` or with `--force`; no retry fixes it. `Installer#save_marked_file` serves a fast-forward that goes ahead over a mark: a file put back to its base under this chi's mark, or an older mark with a proved base or an update that only adds. `.markers?` (a file holding a merge's `<<<<<<<`/`>>>>>>>` lines; `=======` alone is a Markdown underline) |
45
+ | `ShippedHistory` | `.shipped?(bundle, file, path)`: whether those bytes are that file of that bundle (by manifest name, since a shipped dir can be renamed) in an installed `samagotchi-<version>` gem. Reads `<gem dir>/gems/` by hand, for `Gem.path` (set by a chi home's boot string) and the home's `gem_dirs` from home.json (a checkout or another Ruby runs on other gems), never `Gem::Specification`. Size, then bytes, then the manifest. Anything it can't read is no match, which keeps the guard. Asked only when `Merger`'s guard would otherwise fire: with 54 gems on the Studio a miss costs about 4 ms, a hit less |
46
+ | `TextMerge` | the text merge: `.merge(base:, ours:, theirs:)` → a `Result` (`status` `:clean` with `text`, `:conflict` with `hunks`, `:failed` with `why`). `git merge-file -p --diff3 --marker-size=20` on temp copies in a temp cwd, with every `GIT_*` that points at a repository or a config unset or at `/dev/null` (it can run inside another tool's hook). git isn't a runtime dependency: none on PATH, or macOS's `/usr/bin/git` with no developer tools behind it (`.apple_stub?`, by `xcode-select -p`; running it pops a dialog), is `:failed`. So is a git past `TIMEOUT` (10 s; its process group is killed: this runs under `SystemBundle`'s lock), and any doubt about the answer: `min(hunks, 127)` against the exit status, a status over 127, a NUL in a side, marker lines that don't pair up (a user's line equal to one), a line no input holds, or an empty answer to non-empty inputs. `Scan` reads the output: a hunk unions (ours, then the entries of theirs that ours lacks) only when the last heading before it, at level 1 or 2 and outside code fences, HTML comments and any hunk, is titled exactly `Changelog` or `Change log`, with nothing but list entries (an item and its indented lines) since that heading and no fence or comment open, and base, ours and theirs each hold only list entries, with every base entry still on both sides: both only added. Any other hunk is a real conflict, and markers never leave the module. An entry is an item and the lines indented deeper under it, nested items too, and the union compares whole entries. `.changed_sections(base, theirs, ours)`: the headings of the sections the bundle changed and the file hasn't got, and whether text under no heading did, which is what names a miss: a conflict's hunks can't, since the whole file stays and the one hunk may be a Changelog line beside a whole new section. `Hunk#heading` counts the places under no heading |
47
+ | `FileStanding` | the one owner of what chi says of an edited bundle file, used by every surface: `Behind` (`#state`, `#line`: `x.md stays behind 0.8.0: no merge possible (why); missed ## A, ## B, ## C and 2 more`, `would stay` in a dry run), `MergeNote`, `.merged_line`, `.no_merge` (for `LEGACY_BASE` it says chi can't tell whether the file holds edits), `.clauses` (a report's clauses for one bundle), `.behind_count`, `.stay_behind`, `.counts_sentence` (the push), `.still_behind`, `.text` (UTF-8, whatever a name or heading was tagged), `FORCE_TAKES`, `FORCE_MOVES`, `IN_THE_TRASH`. Behind means marked and bytes other than the mark's `incoming_sha` (`.behind?`, read by `InstalledBundle#behind_files` and `Remerge` alike): a file brought level by hand isn't, a missing one is a missing file, and one that can't be read is. An older mark no retry has looked at reads `may be behind` (`Behind#unsure`, and the counts say so), and isn't behind at all when level with its recorded source on disk (`InstalledBundle#behind_file?`). A new surface reads these and adds no wording |
48
+ | `Remerge` | the retry at the same version, memory files only, for a file marked `conflict`: `#run(dry_run:)` → a `Result` (`merged`, `settled`, `stamped`, `behind`, `warnings`, `trash_copy`, `trash_dir`). It merges what merges and settles those entries (`Provenance#resolve_conflicts`, the index line refreshed), leaving `version`, `installed_at` and the assets alone: the Installer would `rm_rf` guardrails, plugin and scripts under live sessions. Every file it leaves is in `behind`, with why: one not shipped now (`MemoryFiles.select`) keeps its mark, base and text; one whose `incoming_sha` isn't the shipped text (a checkout at one VERSION) is left alone, no git runs, and it reads as its mark says; a mark with no digest (older chi) is tried as well, and a real conflict then records it (`Provenance#refresh_marks`, which writes only when the mark would read differently, so a conflict at each start writes nothing). A mark whose digest matches is tried on every run, since the file or git may have changed. `noop` and `keep` settle quietly; `fast_forward` waits for the next upgrade, reading as behind until then; `install` is a missing file, which isn't behind, and a warning says so. A file it can't read or replace stays as it is, saying why. Called from `ShippedUpdate.plan_row`/`apply` (`chi update`) and `SystemBundle.sync`'s equal-version branch, under its lock (each chi start and `chi update`); a bundle chi doesn't ship retries through `chi bundle upgrade`. No marks: nothing is read beyond the record. One marked file costs one git run per Engine start, about 12 ms on the Studio (0.1 ms with none) |
49
+ | `Provenance` | the `.bundles/<name>/` dir: `#write` the record + bases (its `files:` is the full list: a key left out is dropped, a nil value keeps the entry with no base; `marks:` are the files left behind, each a `FileStanding::Behind`, and `incoming_dir:` the bundle text each speaks of, recorded as `incoming_sha` and saved at `#incoming_path`; an older mark that stays keeps `legacy_base`), `#resolve_conflicts` (base to the incoming file, mark and its fields (`MARK_FIELDS`) and saved text gone; a file whose incoming copy is gone keeps both), `#refresh_marks` (what a retry found: why, missed, the digest and `legacy_base`, the saved text), `#note_merged` and `#clear_merge_notes(notes)` (`MERGE_NOTE_FIELDS`, kept through `#write`; the saved text is best effort, after the manifest, so it can't leave a plugin under the old record's sha; any other failure writing the record still can, until a retry), `#record` → `InstalledBundle`, `.each_installed`, `.claimants` (which bundles own a file) |
41
50
  | `InstalledBundle`, `BundleHook`, `PluginRef` | value objects for the record; tolerant of older records (missing keys read as empty). `BundleHook#events` is the manifest's `event:` or its `events:` list (written back as `event:` for one, as an older chi reads it); `Hooks::BundleLoader#load_hook_file` builds the hook once and registers it under each |
42
51
  | `Builder` | `chi bundle build`: memories (+ the installed bundle's hooks, rules, plugin, scripts) → zip/tar/dir |
43
52
  | `Profile` | meta bundles (`includes:`): install the members, record which; `InstallResult#kept_own` names a new member's memory files the user already had, with other bytes (`kept_own_line`, printed by `chi bundle`, `chi update` and `chi bootstrap`); a dry run runs each member's Installer as a dry run too, so it names them as well |
44
- | `SystemBundle`, `ShippedUpdate` | the built-in system bundle (`ensure!` at Engine start, `sync` in `chi update`) and `chi update`'s upgrade of shipped bundles; both drive the Installer with `upgrade: true`. `ShippedUpdate.external` is the rest: what `chi update` only checks |
53
+ | `SystemBundle`, `ShippedUpdate` | the built-in system bundle (`ensure!` at Engine start, `sync` in `chi update`) and `chi update`'s upgrade of shipped bundles; both drive the Installer with `upgrade: true` and `Remerge` at an equal version (`ShippedUpdate::Row`'s `:would_merge`, `:merged`; `SystemBundle::Result`'s `:merged`). `SystemBundle` also puts back a memory missing at an equal version, that file alone, with no Installer (`verify_files_present`, `Provenance#record_restored`). `ensure!` echoes a merge in one line, in the row's words. That reaches a terminal launch; a worker has only its log (`Log.configure(stderr: false)`), so every merge leaves a note on the file's record (`Installer#note_merges`, a failed run's too, and `Remerge#note_merges`; `Provenance#note_merged`: version, time, trash dir). `chi bundle status` shows it, and `chi update` reads the notes before the run (`SystemBundle.merge_notes`, `ShippedUpdate::Row#earlier`), names them in the row and clears after it those exact ones and the notes of the files that run merged (a worker's meanwhile, on another file, stays), unless it was a dry run, the row failed or has no place for them (a skipped system row), or the run is unattended (`ChildEnv::UNATTENDED_UPDATE`, which `Web::UpdateOps` sets for `update.auto`, an env marker so a hand-over's exec keeps it and an older chi ignores it; `--json` alone says nothing, as a person or script may use it). `chi bundle upgrade` clears the notes of what it printed, and `--force` those of the files it replaced. `--json`'s `merged` is this run's, `earlier` the notes'. `ensure!` says what an upgrade left behind and nothing of a file already behind, which `Result#behind` still has for the row. A plan or sync that can't read a file is that bundle's failed row, in `Trash.failure_message`'s words (`ShippedUpdate.plan_row`, `SystemBundle.sync`). `ShippedUpdate.external` is the rest: what `chi update` only checks |
45
54
  | `InstalledCopy`, `BundleCommand::InstalledCheck` | `chi update`'s re-check of the bundles chi didn't ship ([docs/cli.md](../cli.md#updating)). `InstalledCopy` lays a record and its installed files out as a source folder (manifest.yml from `Manifest.write`, the files' own shas, memories from `bases/`); `InstalledCheck` forks per bundle (a plugin's `exit` or hang is that bundle's row; stdio to /dev/null, so `--json` stays parseable; `TIMEOUT`, then KILL of its group) and runs `Check.new(dir, installed: true)` there, which leaves out what only suits a source tree. Its `boundary` finding is narrower there than in a source tree: `Testing::Boundary.gone?` calls a reference gone only when its constant doesn't resolve or the object has no method the name names, so a bundle reaching `Samagotchi::Log` gets no row. lib loads lazily, so when a name doesn't resolve `gone?` requires every file of this chi's library in that same child and asks again (about 0.2 s, only for a bundle that reaches into chi at all); anything it can't read — a `const_get`, a require of a file that is there — is taken as still there. The fork is the point: `Sandbox` swaps ENV and the plugin's code is arbitrary. `UpdateCommand#external_rows` makes the rows (`level`, never `failed`) and holds the check back while a newer gem is yet to take over (`@not_checked`), since a fork is this chi's code |
46
- | `Uninstaller`, `Trash`, `Status`, `Listing` | remove, keep removed files, `chi bundle status`, `chi bundle list` |
55
+ | `Uninstaller`, `Trash`, `Status`, `Listing` | remove (the bundle's own folder too: an edited file there is copied to the trash first, `Uninstaller#trash_copy_line`); keep removed files (`Trash#move`) and a copy of overwritten ones (`Trash#copy`, its `as:` keeping a subfolder); `chi bundle status` (per file, `behind`, `merge_note`, `unreadable`; `issues=` counts behind, missing and unreadable, not edited); `chi bundle list` (`Installed#behind`) |
47
56
  | `IndexUpdater`, `IndexSync` | the managed `index.md` lines (Installer; and the write/edit tools for memories) |
48
57
  | `EntrySize` | one memory's size in tokens: `memory.entry_warn_tokens`' crossing note after a memory write, `chi self`'s `big memories` row |
49
58
  | `IndexSize` | one scope's memory index as the prompt carries it (tokens, lines, bytes): `memory.index_warn_tokens`' crossing note after a memory write, `chi self`'s summary, the prompt's figures (`SystemPrompt`) |
@@ -86,25 +95,44 @@ The order matters: warnings print in it, and a plugin/script the manifest names
86
95
  `#source_change` (the summary's first line, `Source changes: old → new`). A source `SourceIdentity` can't place
87
96
  (an older record's relative path) never refuses.
88
97
  3. `install_memories`, per memory (`MemoryFiles.select`: a non-empty `files:` list, else every top-level `*.md` but
89
- README, CHANGELOG, LICENSE and the like; a listed name not shipped warns; a symlink is skipped with a warning): an upgrade 3-way merges the files the bundle owns (`merge_memory`), a dry run only
90
- classifies (`preview_memory`), a plain install writes new files and skips existing ones (`skip_existing`). A
98
+ README, CHANGELOG, LICENSE and the like; a listed name not shipped warns; a symlink is skipped with a warning): an upgrade 3-way merges the files the bundle owns (`merge_memory`: a clean text merge replaces the file through `Merger.write_merged`, and a real conflict, no git or no base leaves it untouched, `note_conflict`, in `#behind`; a fast-forward over an entry this chi marked `conflict`, a file put back by hand, copies it to the trash first: `save_marked_file`), a dry run only
99
+ classifies (`preview_memory`, `would_merge` for a merge, git running on temp copies; both through `Merger::Upgrade`), a plain install writes new files and skips existing ones (`skip_existing`), and `--force` overwrites, a file that
100
+ differs copied to the trash first (`Trash#copy`, said by `#trash_copy_line`; a profile's in `InstallResult#trash_copies`). A
91
101
  same-name file the bundle didn't write is the user's: skipped and never recorded (`@owned`). Model overlays
92
102
  (`<name>.<key>.md`) get no index line (`note_overlay`).
93
103
  4. `AssetInstaller#run`: hooks (manifest-listed, else every `hooks/*.rb`), prune dropped hooks, hook checksums,
94
- guardrails, plugin, scripts; each set replaces the previous version's. requires_chi warnings for hooks/rules. A
104
+ guardrails, plugin, scripts; each set replaces the previous version's, an edited file copied to the trash first. requires_chi warnings for hooks/rules. A
95
105
  file that is a symlink or resolves outside the bundle dir (`AssetInstaller#linked?`, a linked `hooks/` too) is
96
106
  skipped with a warning (a hook or rule) or raises `InstallError` (the plugin, a script).
97
107
  5. An upgrade's `prune_dropped_files`: unchanged → trash; edited → kept (`kept_pruned`); owned by another bundle → kept.
98
108
  6. `verify_memory_checksums` (strict), `warn_missing_needs`.
99
- 7. `write_provenance` (manifest, not a dry run): bases come from the incoming file, except where the user's edit was
100
- kept (kept/noop/conflict/kept_pruned, or a re-install's skipped edit): those keep the old base, so the next
101
- upgrade still sees the edit. A plain install also keeps the record (the old base) of a file an earlier install
109
+ 7. `write_provenance` (manifest, not a dry run): an upgrade's bases come from each file's `Merger::Outcome`
110
+ (`Installer#next_base`): the file on disk, which has the bundle's bytes; the incoming file for a merge, since the merged
111
+ file holds the user's edit and taking it would make current == base, so the next upgrade would fast-forward over the
112
+ edit (the edited signal stays current != base, which `chi bundle status` and uninstall's refusal read); and where the user's edit was
113
+ kept (keep, conflict, whose `FileStanding::Behind` goes in `marks:`; a file the bundle dropped, kept_pruned, and a re-install's skipped edit take the same path by their
114
+ status and reason): those keep the old base, so the next upgrade still sees the edit. One with no base stays without one (a nil in `Provenance#write`'s `files:`, which keeps the
115
+ entry and its recorded checksum): taking the user's file would let the next upgrade fast-forward over it. A plain
116
+ install carries the `conflict` mark of a file it skipped (`held_conflict_marks`), a newer source moving `version`
117
+ included; it goes once the file has the incoming bytes. A plain install also keeps the record (the old base) of a file an earlier install
102
118
  owned and this version doesn't ship, while it is on disk (`dropped_owned_files`), so the next upgrade prunes it.
103
119
  8. `detect_placeholders`.
104
120
 
121
+ A file system error in steps 3 to 5 (`Installer#replace_files`; a `.trash` it can't write, an edited file it can't read) is an
122
+ `InstallError` in `Trash.failure_message`'s words, as it is an `UninstallError` in `Uninstaller#run`. Each save comes before
123
+ its removal, so no edit is lost, and the record isn't written, so a retry is the same upgrade. Not atomic: files the
124
+ failed run already placed stay.
125
+
126
+ Which files a plan asks `Merger::Upgrade` about is the caller's, and the lists differ, each less the files the bundle
127
+ doesn't own: the Installer's `MemoryFiles.select`; `ShippedUpdate.forecast`' every shipped `*.md`, so `chi update`'s plan
128
+ can name a file dropped from `files:` or now a symlink, which the upgrade doesn't merge: edited, it stays (`kept_pruned`, or `kept_shared` when another bundle has it too); `SystemBundle`'s dry
129
+ run the manifest's `files:`, so a manifest with none plans nothing (the shipped one lists its files).
130
+
105
131
  Results are `@results` (file → `{status:, reason:}`), summarised by `#summary`. An upgrade with conflicts keeps an
106
132
  owned source dir alive (`#source_dir`) until `#cleanup_source!`, because the conflict agent reads the incoming
107
- files; `Provenance#resolve_conflicts` then moves the resolved files' bases forward.
133
+ files; `BundleCommand#settle_conflicts` then runs `Provenance#resolve_conflicts`, which moves a base forward and keeps
134
+ the entry's description, only for a file the agent step changed that holds no markers (`Merger.markers?`), and whose incoming
135
+ copy is still there. Any other keeps its old base and mark, and the command exits `CONFLICTS_KEPT`.
108
136
 
109
137
  ## Builder#run, in order
110
138
 
@@ -220,8 +220,13 @@ the record settles is `unclear`, and a file changed since isn't seen. If that tu
220
220
  next step, and the file and view below can stay.
221
221
 
222
222
  **Independence.** The request (`ObjectivesCheck.request`) has its own system text and no tools. Its record is what people and
223
- notes said and what tools returned (`.record`, each entry cut at 1,500 characters, the newest 24,000 kept). It never holds the
224
- agent's prose, the system prompt, the list's notes, a continue's carry note, which holds the agent's last answer
223
+ notes said, what tools returned, and the agent's visible answers labelled `[agent's own words]` (`.record`, each entry cut at 1,500 characters, the
224
+ newest 24,000 kept). Without those answers a claim about what the agent said or asked could only come back `unclear`. So the request says they settle
225
+ what was said or asked and are never proof of work, which needs a tool's output or a person's word. That rule is words to a model, not a mechanism: a
226
+ probe on Qwen3.8 Flash Next held it 3 of 3 (only the agent's "all pass": `unclear`), and a verdict is information only either way.
227
+ The record never holds the agent's thinking (`own_words`: a chat entry's `thinking` and `anthropic_content` aren't read; a native
228
+ entry's content is the raw response, so its inline thoughts, a thought cut off unclosed, and its call markup come out), the system prompt, the list's
229
+ notes, a continue's carry note, an earlier session's answer that would read as outside news
225
230
  (`Objectives.outside_note?`, the rule the audit shares), or the objectives tool's calls, since a move isn't evidence for itself.
226
231
  Note and evidence are capped at 1,000 characters each. Mid-turn it reads the loop's conversation (`Engine#live_conversation`), the running turn's runs too.
227
232
 
@@ -23,7 +23,7 @@ page. A factory dep a new version stops passing is the same hazard the other way
23
23
  out of `send_turn.js`, `session_actions.js` and `lists_view.js`); a page loaded before keeps all its old modules
24
24
  until a reload, so the risk is a mixed cache, not an open tab. `answer_fold.js`, `details_motion.js`,
25
25
  `turn_range.js`, `history_reveal.js`, `latest.js`, `session_payload.js`, `method_api.js`, `session_store.js`,
26
- `session_fields.js`, `feature_gates.js`, `revise_stack.js`, `revise_switch.js`, `shell_command.js`, `stale_params.js`, `popover_modal.js`, `more_sheet.js`, `hash_intent.js` (what the hash asks for: session, all, start or keep), `worker_features.js` and the `objectives_*.js` modules are new modules for that reason.
26
+ `session_fields.js`, `feature_gates.js`, `revise_stack.js`, `revise_switch.js`, `shell_command.js`, `stale_params.js`, `popover_modal.js`, `more_sheet.js`, `hash_intent.js` (what the hash asks for: session, all, start or keep), `worker_features.js`, `block_open.js` and the `objectives_*.js` modules are new modules for that reason.
27
27
 
28
28
  ## Two pages, two models
29
29
 
@@ -181,9 +181,9 @@ from saying it twice across restarts). The key's `chi:` prefix keeps sw.js from
181
181
 
182
182
  | `kind` | key | title / body |
183
183
  |---|---|---|
184
- | `installed` (`update.restart: never`) | `chi:installed:<X>` | "chi X is installed" / "chi web and its sessions switch over when you restart them." + kept edits |
184
+ | `installed` (`update.restart: never`) | `chi:installed:<X>` | "chi X is installed" / "chi web and its sessions switch over when you restart them." + the edited files (`FileStanding.counts_sentence`) |
185
185
  | `failed` | `chi:update_failed:<X>` | "chi X didn't install" (or "installed with a problem" when the gem row was fine) / the first failed row's status, "Open chi web for details." |
186
- | `updated` | `chi:updated:<X>` | "chi updated to X" / "chi web and 3 sessions restarted; 1 restarts when idle." + kept edits |
186
+ | `updated` | `chi:updated:<X>` | "chi updated to X" / "chi web and 3 sessions restarted; 1 restarts when idle." + the edited files |
187
187
  | `restart_failed` | `chi:restart_failed:<X>` | "chi X is installed; chi web didn't restart" / why (a failed `--version` preflight, no wrapper), "Restart chi web in its terminal." |
188
188
 
189
189
  `updated` is sent by the **restarted** chi web (its AutoUpdate finds the run at `phase: web`): the exec ends the
@@ -242,7 +242,10 @@ session's set (load, resync) and when the list opens (a file changed while the s
242
242
  passes `working_set_updated` to it, and does nothing when the flag is off. A new snapshot redraws:
243
243
  - the session bar's chip and the open list;
244
244
  - the footprints, in place (`refreshFootprints`, the same node; a `data-ws-sig` of the items' states skips an
245
- unchanged one);
245
+ unchanged one). A footprint is a `<span>` in its block's summary line, `.turn-work > summary > .tw-sum`, before the
246
+ steps' `.turn-steps`: drawn in the reload's template (`turnHistoryNodes`), prepended at the hand-off
247
+ (`stage_view.js handOff`, while the stage still hides the summary). A click there must not toggle the block:
248
+ `details_motion.js` skips `.ws-foot`, and `footClick` cancels the click;
246
249
  - the rail (`onServer`, the stage's `scheduleRefresh`).
247
250
 
248
251
  A state is the item's state now, not when its turn ran. A state change never refolds and never moves a line: the
@@ -287,7 +290,7 @@ leaves nothing behind. The rail adds a dashed `+ name · next turn` line per pen
287
290
  (`working_set_rail.js addLabels`, inside the rail's budget), and `read again next turn` on a held line's title.
288
291
 
289
292
  **Looks** (design spec §3, §5): a stale line is faded (`--ws-stale-opacity`: `.55` on the light palette, `.4` on the
290
- dark one, where a lighter grey reads as plain secondary text; a footprint label `.55` on both, its text already faint); a forgotten one is struck in `--text-faint`
293
+ dark one, where a lighter grey reads as plain secondary text; a footprint label `.55` on both); a forgotten one is struck in `--text-faint`
291
294
  at `opacity: .5`. A group shows `✂n` for its forgotten members. It is struck only when all its members are
292
295
  forgotten, and faded when all are stale or forgotten (`groupState`). The list's ops text says `stale` or `forgotten`
293
296
  in words (colour is never alone) and shows `~1.2k` tokens per row (`tokensText`). A footprint adds `n stale` (plain; a file the turn's