samagotchi 0.16.0 → 0.17.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +86 -1
  3. data/README.md +2 -0
  4. data/docs/architecture.md +13 -0
  5. data/docs/cli.md +4 -2
  6. data/docs/configuration.md +4 -4
  7. data/docs/internals/background-tasks.md +1 -0
  8. data/docs/memory.md +39 -0
  9. data/docs/plugins.md +2 -0
  10. data/docs/sessions.md +3 -2
  11. data/docs/sub-agent.md +1 -0
  12. data/docs/testing.md +47 -0
  13. data/lib/samagotchi/bridge/turn_accumulator.rb +20 -5
  14. data/lib/samagotchi/bridge.rb +38 -7
  15. data/lib/samagotchi/bundle_command.rb +13 -2
  16. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  17. data/lib/samagotchi/bundles/system/memory_guide.md +5 -5
  18. data/lib/samagotchi/bundles/system/self_map.md +1 -0
  19. data/lib/samagotchi/engine.rb +31 -5
  20. data/lib/samagotchi/memory_bundle/builder.rb +22 -0
  21. data/lib/samagotchi/memory_bundle/index_updater.rb +17 -6
  22. data/lib/samagotchi/memory_bundle/installer.rb +60 -21
  23. data/lib/samagotchi/memory_bundle/profile.rb +9 -4
  24. data/lib/samagotchi/memory_bundle/provenance.rb +14 -0
  25. data/lib/samagotchi/memory_bundle/shipped_update.rb +5 -3
  26. data/lib/samagotchi/memory_bundle/system_bundle.rb +5 -3
  27. data/lib/samagotchi/memory_bundle/trash.rb +57 -0
  28. data/lib/samagotchi/memory_bundle/uninstaller.rb +34 -5
  29. data/lib/samagotchi/plugin/context.rb +14 -2
  30. data/lib/samagotchi/self_report.rb +49 -3
  31. data/lib/samagotchi/send_command.rb +45 -6
  32. data/lib/samagotchi/session_metrics.rb +137 -48
  33. data/lib/samagotchi/system_prompt.rb +45 -4
  34. data/lib/samagotchi/terminal_ui/attached_loop.rb +16 -3
  35. data/lib/samagotchi/terminal_ui/event_renderer.rb +2 -1
  36. data/lib/samagotchi/terminal_ui.rb +24 -0
  37. data/lib/samagotchi/tools/builtins.rb +11 -3
  38. data/lib/samagotchi/tools/delegate_relay.rb +4 -0
  39. data/lib/samagotchi/tools/peers.rb +3 -2
  40. data/lib/samagotchi/tools/task_runtime.rb +1 -1
  41. data/lib/samagotchi/version.rb +1 -1
  42. data/lib/samagotchi/web/app.rb +2 -1
  43. data/lib/samagotchi/web/public/app.js +37 -28
  44. data/lib/samagotchi/web/public/notify.js +7 -3
  45. data/lib/samagotchi/web/public/stage_view.js +13 -1
  46. data/lib/samagotchi/web/public/strip.js +11 -0
  47. data/lib/samagotchi/web/public/timing.js +26 -12
  48. data/lib/samagotchi/web/public/turn_events.js +13 -3
  49. data/lib/samagotchi/worker.rb +28 -2
  50. data/lib/samagotchi/worker_sidecar.rb +4 -1
  51. metadata +3 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0dd06759f4b1d8b5c2d799aed0a4a57940c34bfd663599fcd1dbbad4f4421c99
4
- data.tar.gz: 040c8e4d3c4e3aee3f9605fded5676088c21a80b50426696a9cdee4a16f65d7a
3
+ metadata.gz: '056529144efb1765ab53c0ce9e88a9ea979ecd8c202d3e41a12a794827fee260'
4
+ data.tar.gz: 054d2a858289e543a8085f0ce774935ea164a41e98467e8fb099452a81eb4729
5
5
  SHA512:
6
- metadata.gz: 418ba106ef5c7f610fd831b93b3441ab1de00910b84a782d37bc3fe617dab833ce35073f4733dc2fe6bdca9d90dba5c7af6fba7b7df29edf60c72c08c99acff9
7
- data.tar.gz: 8c78e649bbadc097d5cb9b3de7adaac3bcebeda06d04ea1a34f7cbfab531fcaf15e0bce7bbc25480eb28e18c47867cbd0226d57ac3cece9452e7912471aa55b5
6
+ metadata.gz: 0a88b140031661b2dd2903df3ca74e50587971cba38029e65f8dcf8ec5e1e2a098fbfa4247c2a0954b5f249ca70d85a30600236f728e6131056f4defb6058a5b
7
+ data.tar.gz: 5a2d8a1fac55ad2d4a69ec0002a99a856e82de8b0a91a4c09e4a05c04a10a27d634febf0d77d5dfdfb94a7fd5b3c8f88397ed0affb13384e4394a33c7d21424e
data/CHANGELOG.md CHANGED
@@ -8,6 +8,90 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.17.0] - 2026-10-03
12
+
13
+ ### Added
14
+
15
+ - The system prompt names the model the session runs on: `Model: this session runs on box:gemma-small (host box,
16
+ box.test:8080; model key gemma-small).`, plus a line to answer "which model are you" from it, not from training
17
+ (a fine-tune often knows only its base model's name), and that model-only guidance goes in
18
+ `memory_write current_model_only: true` overlays. A local llama.cpp host whose `/props` names another model adds
19
+ `; the server says it serves <name>`. The line changes only with `/model`.
20
+ - `execute` and `task_create` export `SAMAGOTCHI_SESSION_MODEL`, the session's model ref (it follows `/model`; the
21
+ model's `env` can't set it).
22
+ - Plugins: `ctx.model` (the session's model ref, right after `/model` too) and `ctx.model_key` (its memory overlay
23
+ key).
24
+ - `chi self` has a `model key` row: the memory overlay key of the model it reports.
25
+
26
+ ### Changed
27
+
28
+ - `chi self` run by a session's `execute` reports **that session's** model, and its host, loop, profile, thinking and
29
+ served model: `model splash:… (this session abcd1234; default main:…)`. Before, it showed the config default
30
+ (or the `--model` the worker started with), wrong after `/model`, for web-picker and `chi send --new --model`
31
+ sessions and delegate children. Elsewhere the row reads `main:… (default)`. `chi self --model` is still the default.
32
+ - The identity memory and the preloaded memories (`--memory`, config `memories:`) get their model overlays
33
+ (`<name>.<key>.md`) in the system prompt, as `memory_read` gives them.
34
+ - A session's live state (what the web and an attached terminal read from its worker) no longer carries every turn's
35
+ and tool call's timing record, only the newest turn's: reads stay the same size however long the session runs. The
36
+ full history stays in the session's `analytics.json`.
37
+ - A memory a bundle installed says so in its `index.md` line, and so in the prompt's memory index:
38
+ `- **memory_guide** · system · 2026-10-03 · 5120 · from samagotchi-system — …`. `memory_write` and `write`/`edit`
39
+ keep the tag; a same-name memory the install skipped gets none. Bundles installed earlier get it on their next
40
+ install or upgrade (the system bundle: with the next chi version).
41
+ - `chi bundle uninstall` (and an upgrade dropping a file the new version no longer ships) moves the bundle's memory
42
+ files to `<memories>/.bundles/.trash/<bundle>-<time>/` instead of deleting them, and says where:
43
+ `Moved to the trash: notes.md (…/.trash/notes-bundle-20261003-120000)`. Nothing empties the trash; delete it by hand.
44
+ - `chi bundle build` leaves out the memories an installed bundle owns (the system bundle's `identity.md`, a shipped
45
+ bundle's memory), one line each: `Left out identity.md: installed by bundle samagotchi-system (name it to include
46
+ it)`; naming a file in `FILES...` includes it. It now prints the builder's warnings.
47
+ - An `index.md` line a bundle install, upgrade or uninstall couldn't write or remove is now a warning
48
+ (`index.md: line for notes not updated (…)`) instead of passing silently; `chi bundle uninstall` of a profile
49
+ prints its bundles' warnings too.
50
+ - Web: a composer line is a command only when it names one of the session's commands (or is `!…`); an unknown
51
+ `/word` (a path like `/usr/bin/env …`, a typo) goes to the model as a prompt, as in a terminal, instead of being
52
+ refused with `not a session command`.
53
+ - Web: a window under 700 px tall hides the latest-sessions strip by itself (the `▾ N sessions` pill shows it again
54
+ until the window is tall), so a question or approval card in the turn's stage has room for its choices and
55
+ buttons. Taller windows and phones are unchanged.
56
+
57
+ ### Fixed
58
+
59
+ - `chi self`'s `served model` row for a chat host (`api: openai`), or a server without `/props`, says
60
+ `reported per turn (the server has no /props)`; it said `unknown (no answer from the server's /props)`, which a
61
+ model took to mean the server never said what it served.
62
+ - `chi bundle uninstall` no longer deletes a memory the user had before the install. A bundle now records (and
63
+ so upgrades and removes) only the files it wrote; a same-name file that was already there is reported
64
+ `Skipped` and stays the user's, on install, re-install and upgrade. Before, it was recorded with the user's
65
+ content as its base, so uninstall removed it without `--force`: a third-party bundle shipping `notes.md`, or
66
+ `chi bundle build` then installing the result on the same machine. An upgrade no longer reports such a file as
67
+ a conflict (or `chi update --dry-run` as kept). Bundles installed before this fix may still list such a file;
68
+ uninstall now moves it to the trash (below) rather than deleting it.
69
+ - A memory file two installed bundles list (one an older install adopted, like `identity.md` under both
70
+ `samagotchi-system` and a `chi bundle build` of your memories) stays when one of them is uninstalled, or drops it
71
+ in an upgrade: `Kept identity.md: bundle samagotchi-system has it too`. It goes with the last one.
72
+ - Web: after a reload, a turn with an empty retry mid-turn shows each tool row's own duration; the rows took their
73
+ records by iteration, so every call after the retry showed the wrong one, or none.
74
+ - Attached TUI: `/detach` typed at a question's `?` prompt detaches (the question stays open), as Ctrl-D does; it
75
+ was read as an answer and refused as an unknown option.
76
+ - Attached TUI: joining a turn while a tool call runs (a long `delegate`, a slow command), its row's duration counts
77
+ from the call's start, not from the join.
78
+ - Web: in a short window a question or approval card that opens in the turn's stage is scrolled into view (its
79
+ choices and buttons); it sat below the prompt and headline, out of sight.
80
+ - A session command sent as a message runs as the command, as typed in a terminal: `chi send -m "/model x" ID`,
81
+ `chi send --new -m "/model x"`, `chi -p "/model x"` (attached and `--non-interactive`) and the web start page's
82
+ first message went to the model as a prompt. `chi send` says `sent as a session command`; with `--wait` there is
83
+ no reply to wait for (exit 0, `--format json` status `command`). An unknown `/word` still goes to the model.
84
+ - macOS 26: a dead worker's leftover `bridge.json` no longer counts as a live worker. Ruby's `Socket.tcp` with a
85
+ connect timeout returns the socket of a refused connect there, so the check said live: clients tried its port
86
+ first, `chi update` listed the dead worker as running, and an approval relay kept watching a parent that was gone.
87
+ - REPL with piped input: when the input ends at a question or approval, no bare `? ` line is left above its
88
+ `(denied)` / `(cancelled)` summary.
89
+ - Web: an approval from a delegate its parent isn't waiting on directly rings once (the parent's relayed card), not
90
+ twice: a child's approval now waits 3.5 s (was 1.5 s) for the relay before it notifies on its own. Only delegated
91
+ children's approvals wait; one nobody relays rings that much later.
92
+
93
+ Update with `chi update` (no bundle changes; the system bundle upgrades itself with chi).
94
+
11
95
  ## [0.16.0] - 2026-10-03
12
96
 
13
97
  ### Added
@@ -856,7 +940,8 @@ and long-lived sessions.
856
940
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
857
941
  hotkey panel that send selected text or the clipboard to your sessions.
858
942
 
859
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.16.0...HEAD
943
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.17.0...HEAD
944
+ [0.17.0]: https://github.com/dm1try/samagotchi/compare/v0.16.0...v0.17.0
860
945
  [0.16.0]: https://github.com/dm1try/samagotchi/compare/v0.15.0...v0.16.0
861
946
  [0.15.0]: https://github.com/dm1try/samagotchi/compare/v0.14.0...v0.15.0
862
947
  [0.14.0]: https://github.com/dm1try/samagotchi/compare/v0.13.0...v0.14.0
data/README.md CHANGED
@@ -201,6 +201,8 @@ npm test # web frontend specs
201
201
  npm run e2e # web UI happy paths in Chromium, fake model (once: npx playwright install chromium)
202
202
  ```
203
203
 
204
+ Integration specs against a live model server, and what the suite isolates: [docs/testing.md](docs/testing.md).
205
+
204
206
  ## License
205
207
 
206
208
  MIT, see [LICENSE](LICENSE).
data/docs/architecture.md CHANGED
@@ -183,6 +183,13 @@ engine.system_prompt # fully built system prompt string
183
183
  engine.session # current session (Engine owns create/resume)
184
184
  ```
185
185
 
186
+ The system prompt (`SystemPrompt#build`) is the base prompt, then rg guidance,
187
+ AGENT.md, the working directory, **the model** (`Model: this session runs on
188
+ <ref> (host …; model key …)` and a line telling the model to answer "which model
189
+ are you" from it, not from training), the session id and log, the memory indexes,
190
+ the identity and the preloaded memories. It is built once per loop and rebuilt only
191
+ by a model switch (or changed tools), so the model line costs no KV churn.
192
+
186
193
  #### Subscribing to the live stream (and the bridge)
187
194
 
188
195
  For an **always-on** consumer (an external SSE client, a second UI), use
@@ -252,6 +259,12 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
252
259
  the messages itself, then streams from its `event_seq`).
253
260
  - `OPTIONS *` — CORS preflight (`Access-Control-Allow-Origin: *`).
254
261
 
262
+ The metrics in `/state`, `/snapshot`, `/stats` and the SSE `snapshot`/`reset` frames
263
+ (`SessionMetrics#snapshot`) carry the session's totals and only the recent timing records: the
264
+ newest turn the worker finished and any it hasn't saved yet (at most 20 turns), with their tool
265
+ calls and the running turn's finished ones. The full history is the session's `analytics.json`,
266
+ which `SessionMetrics#persist` rewrites after each turn; the web server merges the two by id.
267
+
255
268
  Every route goes through `Bridge#dispatch`: an id other than the bridge's own session is
256
269
  `404 unknown_session`, and a handler that raises answers `500 bridge_error` (logged as
257
270
  `handler_failed`) rather than dropping the connection.
data/docs/cli.md CHANGED
@@ -21,7 +21,7 @@
21
21
  - `chi answer ID --question QID (--option N|LABEL)... [--text T] [--timeout S] [--format json]` (or `--dismiss`) — answer the question a session waits on (the one `chi send --wait` exited 3 with), then wait and print what comes next as `chi send --wait` does: the reply (0), the next question (3), still running after `--timeout` (4). `--option` is 1-based or the label, repeated on a multi-select question; `--text` is free text, or a Deny's reason; `--dismiss` leaves it unanswered and the model finishes its reply. A question no longer open (the web answered first) isn't answered again: it waits for that turn's reply. An option the question doesn't offer exits 2; a worker that is gone exits 1 (`send the task again: chi send --wait -m "…" ID`). An approval can be denied, not allowed, unless `guardrails.parent_approvals: once` (a convention, not a security boundary; see [Guardrails](guardrails.md#approvals-from-a-parent-agent))
22
22
  - `chi desktop install|upgrade|uninstall|status` — the macOS "Send to chi" helper: a Service and a ⌃⌥⌘N hotkey that send text or images to a session (live, stopped or new) as a message (⏎, a turn runs) or a context note (⌘⏎) (see [Desktop helper](desktop.md))
23
23
  - `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))
24
- - `chi self` — print version, source dir (checkout or installed gem), config/memory/session paths, model/host and bundles; `chi self --model` prints only the model a new session starts on, an alias resolved to its target (the desktop helper's hint)
24
+ - `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`). `chi self --model` prints only the model a new session starts on, an alias resolved to its target (the desktop helper's hint)
25
25
  - `chi update [--dry-run] [--no-gem] [--no-bundles] [--no-desktop]` — update an installed chi: the gem, the system bundle, the shipped bundles you installed and the desktop helper, in one table (see [Updating](#updating))
26
26
  - `chi bundle install|upgrade|uninstall|status|diff|list|build` — 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)). 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
27
27
 
@@ -283,7 +283,9 @@ In an attached terminal:
283
283
  - The prompt stays open while a turn runs; see [Typing during a turn](#typing-during-a-turn).
284
284
  - `/model`, `/models`, `/guardrails`, `/continue`, `!rollback` and `!commands` run in the
285
285
  worker, and every UI sees their output; `/stats` and `/recap` work too. The
286
- Web UI's composer takes the same commands.
286
+ Web UI's composer takes the same commands, and so do `-p "/model x"` and
287
+ `chi send -m "/model x"`. A `/word` the session doesn't know (a path, a
288
+ typo) goes to the model as a prompt.
287
289
  - `!commands` and the model's tools run in the session's directory (where it
288
290
  was started), whichever terminal you attach from.
289
291
  - `-p` sends its prompt once attached, `--model` switches the worker's model
@@ -279,14 +279,14 @@ spec. It requires the model to call `execute` and return the current UTC date:
279
279
 
280
280
  ```shell
281
281
  SAMAGOTCHI_INTEGRATION=1 \
282
- SAMAGOTCHI_SERVER_HOST=192.0.2.10 SAMAGOTCHI_SERVER_PORT=8000 \
283
- SAMAGOTCHI_DEFAULT_MODEL=incoai/Qwen3.8-27B-Splash \
284
- bundle exec rspec spec/integration/chat_loop_spec.rb -fd < /dev/null
282
+ SAMAGOTCHI_INTEGRATION_HOST=192.0.2.10 SAMAGOTCHI_INTEGRATION_PORT=8000 \
283
+ SAMAGOTCHI_INTEGRATION_MODEL=incoai/Qwen3.8-27B-Splash \
284
+ bundle exec rspec spec/integration/chat_loop_spec.rb
285
285
  ```
286
286
 
287
287
  The same test works against a llama.cpp OpenAI-compatible server by changing
288
288
  the host, port, and model values. The test is skipped unless
289
- `SAMAGOTCHI_INTEGRATION=1` is set.
289
+ `SAMAGOTCHI_INTEGRATION=1` is set; see [Testing](testing.md).
290
290
 
291
291
  oMLX's known tool-call limitation (a stream filter that strips markup) only
292
292
  affects its `/v1/chat/completions` endpoint, not the `/v1/completions` endpoint
@@ -22,5 +22,6 @@ Behavior:
22
22
  - `task_get` returns metadata and `output_path`; use `read` for output contents.
23
23
  - `task_wait` accepts `timeout`, `tail_lines` (maximum 100), and `done_pattern` (a regular expression string).
24
24
  - `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.
25
+ - `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) and `SAMAGOTCHI_SESSION_MODEL` (the resolved ref the session runs on, read at each call so it follows `/model`; unset when unknown). `chi self` reads the latter for its `model` row.
25
26
 
26
27
  Work that needs a model, not a shell command, goes to a child chi session instead: `delegate` / `delegate_result` have the same shape (start, then wait) and return only the child's final reply; see [Sessions: Delegating](../sessions.md#delegating).
data/docs/memory.md CHANGED
@@ -23,6 +23,8 @@ Each memory entry may have a companion file named `<name>.<model-key>.md` in the
23
23
 
24
24
  - **Key derivation**: The harness normalizes the model name sent to the server, without its host prefix (`box:qwen3.6-35b-a3b` keys as `qwen3.6-35b-a3b`; an alias keys as its target, and the alias's own key is read when the target has no overlay) (lowercase, replace non-alphanumeric with `-`, squeeze dashes) to derive the file key. For example, `qwen3.6-35b-a3b` → `qwen3-6-35b-a3b`.
25
25
  - **Saving overlays**: Pass `current_model_only: true` to `memory_write` (the harness resolves the model key automatically). This writes the content as `<name>.<model-key>.md` and skips index maintenance.
26
+ - **Which key is mine**: the system prompt's `Model:` line names the session's model key, and `chi self` (via `execute`) has a `model key` row; both follow `/model`.
27
+ - **In the system prompt**: the identity memory and the preloaded memories (`--memory`, config `memories:`) get their overlays too, as `memory_read` gives them; after `/model` the rebuilt prompt carries the new model's.
26
28
  - **Dormancy**: Overlays are only active under the matching model key; other models see the base entry only.
27
29
  - **Invariant**: The base entry is the contract. Overlays only add model-specific guidance and never contradict the base protocol.
28
30
 
@@ -80,6 +82,43 @@ skills`, [docs/plugins.md](plugins.md#the-skills-bundle)) adds `/skill save`,
80
82
  one-line diff after each update, and nudges a model that skips a failing
81
83
  step instead of fixing the skill.
82
84
 
85
+ ## What a bundle owns
86
+
87
+ A bundle owns the memory files it wrote, nothing else. `chi bundle install`
88
+ copies each of its `*.md` files into the scope dir; a file of the same name
89
+ that is already there is left as it is and reported `Skipped` (`--force`
90
+ overwrites it, and then the bundle owns it). A skipped file stays yours: the
91
+ bundle doesn't record it, so `chi bundle upgrade` never merges into it and
92
+ `chi bundle uninstall` never removes it. That holds for a file the user wrote,
93
+ for `chi bundle build` then installing the result on the same machine, and
94
+ for a file another bundle installed.
95
+
96
+ - **The index says where a memory came from.** The `index.md` line of a file
97
+ a bundle installed ends in `· from <bundle>`, before the description:
98
+ `- **memory_guide** · system · 2026-10-03 · 5120 · from samagotchi-system —
99
+ …`. `memory_write`, `write` and `edit` keep it. A bundle installed by chi
100
+ 0.16 or earlier gets it on its next install or upgrade.
101
+ - **Uninstall moves, it doesn't delete.** The bundle's memory files go to
102
+ `$XDG_CONFIG_HOME/samagotchi/memories/.bundles/.trash/<bundle>-<YYYYmmdd-HHMMSS>/`,
103
+ and the output names them and the dir:
104
+ `Moved to the trash: notes.md (…/.trash/team-notes-20261003-120000)`. So
105
+ does an upgrade's removal of a file the new version no longer ships. A
106
+ file you edited still needs `--force`. Nothing empties the trash: delete
107
+ the dirs when you're sure. Hooks, rules and the plugin live in the
108
+ bundle's own dir and are deleted with it.
109
+ - **A file two bundles list stays** until the last of them is uninstalled
110
+ (`Kept identity.md: bundle samagotchi-system has it too`); its index line
111
+ then names the bundle that still has it.
112
+ - **Installs by chi 0.16 or earlier** recorded skipped files too. Such a bundle may
113
+ still list a file that was yours; uninstalling it moves that file to the
114
+ trash rather than deleting it, so check the `Moved to the trash` line and
115
+ move a file back if it was yours (and its line in `index.md` comes back
116
+ with the next `memory_write`, `write` or `edit` of it).
117
+ - **`chi bundle build`** packs your memories, not other bundles': a memory an
118
+ installed bundle owns is left out with a line (`Left out identity.md:
119
+ installed by bundle samagotchi-system (name it to include it)`); naming it
120
+ in `FILES...` includes it.
121
+
83
122
  ## Bundle profiles: core and dev
84
123
 
85
124
  Every bundle chi ships (the system bundle aside) belongs to one of two
data/docs/plugins.md CHANGED
@@ -478,6 +478,8 @@ full label, `plugin.rb (bundle my-bundle)`.
478
478
  | `ctx.session_id` | the session's id (nil before there is one) |
479
479
  | `ctx.cwd` | the session's working directory |
480
480
  | `ctx.repo_root` | the git checkout holding `cwd`, or nil |
481
+ | `ctx.model` | the model the session runs on now: its resolved ref (`host:id`), right after `/model` too |
482
+ | `ctx.model_key` | that model's memory overlay key (`<name>.<key>.md`, what `memory_write current_model_only` writes) |
481
483
  | `ctx.settings` | the bundle's settings, frozen |
482
484
  | `ctx.data_dir` | `$XDG_STATE_HOME/samagotchi/plugins/<bundle>/`, created on first use. A session's own state goes in `sessions/<id>.json` or `sessions/<id>/` there: chi removes it when the session is deleted, discarded or pruned |
483
485
  | `ctx.log` | `ctx.log.info(:event, key: value)`: debug-log records tagged `plugins`, with `bundle=<bundle>` |
data/docs/sessions.md CHANGED
@@ -63,7 +63,7 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
63
63
  **Ordering:**
64
64
 
65
65
  - `Session.list` / `SessionManager.list_sessions` / `GET /api/sessions?sort=&order=&limit=&offset=` default to `updated_at desc` (newest activity first). Also supports `created_at`, `asc`. `X-Total-Count` header when paginated.
66
- - Web UI (`chi web`): the page's scope is in its URL. Started in a git repo, `chi web` opens `/?dir=<that folder>`: that project's sessions, and new chats start in that folder; the header chip says `<project> · all`, and `all` opens the same place without `?dir` (every session; a new chat there starts in the server's own folder, shown on the start page, and cards name their folder; `← <project>` goes back to the project view it came from, or to the server's own project). One server serves every project: a second `chi web` (from another repo) finds it through `GET /api/info` and prints (with `--open`, opens) its page for its own folder instead of starting another. The 3 latest sessions sit above the chat; "All sessions" (or `/`) opens every session at `#/sessions`, with a search over preview, id and status (Esc or Back returns). The open session is in the URL (`#/s/<id>`), so a reload or a copied link opens it again; the chi logo top left goes back to the empty start for a new chat. The message box grows with its text; drag its top edge to keep it taller (double-click resets). The info bar copies `chi --attach <id>` for a terminal. The start page's model picker (bottom left of the composer, the chosen name `▾`) chooses the model a new chat starts on; it opens a search list over the conversation (↓ or a click; downward when the start page leaves no room above): an empty search shows the last 5 picks (Recent) and then each host's model ids A–Z under the host, the default host first; typed words must each match the host or the id (a substring, else for 3+ characters the letters in order, so `deepseek4.1 fla` finds `openrouter · deepseek/deepseek-v4.1-flash`), ⏎ picks, Esc/Tab close, a pick puts the focus in the message box: `GET /api/models` lists the hosts' models as chi spells them (`{default, models: [{name, host, id}], warning?}`; bare for the default host, `host:model` for the others, from the same cached lists as `/models`, a bounded wait, a host that is down noted in `warning`), and `POST /api/sessions` takes `model` (blank means the default). The start page's first message goes as later ones do: `POST /api/sessions` with `idle: true` and `preview` (the message, which names the session until its turn is saved), then `POST /api/sessions/:id/turn`, so a failed first turn puts its prompt back in the composer too; a first command line (`/model …`) still starts the session as its `prompt`. The browser remembers the last choice; a running session's model is in the info bar and changes only with `/model`. ↑/↓ in the composer walk the prompt history the TUI's ↑ walks ([Persistent Prompt History](cli.md#persistent-prompt-history)): ↑ with the caret on the first line, ↓ from the last line while browsing, ↓ past the newest brings the draft back; `GET /api/history` returns `{entries: [...]}`, oldest first (the page fetches it on focus and after each send). The server records a sent line in the routes once the worker took it (`POST /turn` accepted, a `!command` through `POST /command` or as a session's first line); `"history": false` in the body keeps it out (an image-only send).
66
+ - Web UI (`chi web`): the page's scope is in its URL. Started in a git repo, `chi web` opens `/?dir=<that folder>`: that project's sessions, and new chats start in that folder; the header chip says `<project> · all`, and `all` opens the same place without `?dir` (every session; a new chat there starts in the server's own folder, shown on the start page, and cards name their folder; `← <project>` goes back to the project view it came from, or to the server's own project). One server serves every project: a second `chi web` (from another repo) finds it through `GET /api/info` and prints (with `--open`, opens) its page for its own folder instead of starting another. The 3 latest sessions sit above the chat (`hide ▴` puts them away behind a `▾ N sessions` pill, remembered; a window under 700 px tall hides them by itself, so a question or approval card in the stage keeps room, and the pill shows them until the window is tall again); "All sessions" (or `/`) opens every session at `#/sessions`, with a search over preview, id and status (Esc or Back returns). The open session is in the URL (`#/s/<id>`), so a reload or a copied link opens it again; the chi logo top left goes back to the empty start for a new chat. The message box grows with its text; drag its top edge to keep it taller (double-click resets). The info bar copies `chi --attach <id>` for a terminal. The start page's model picker (bottom left of the composer, the chosen name `▾`) chooses the model a new chat starts on; it opens a search list over the conversation (↓ or a click; downward when the start page leaves no room above): an empty search shows the last 5 picks (Recent) and then each host's model ids A–Z under the host, the default host first; typed words must each match the host or the id (a substring, else for 3+ characters the letters in order, so `deepseek4.1 fla` finds `openrouter · deepseek/deepseek-v4.1-flash`), ⏎ picks, Esc/Tab close, a pick puts the focus in the message box: `GET /api/models` lists the hosts' models as chi spells them (`{default, models: [{name, host, id}], warning?}`; bare for the default host, `host:model` for the others, from the same cached lists as `/models`, a bounded wait, a host that is down noted in `warning`), and `POST /api/sessions` takes `model` (blank means the default). The start page's first message goes as later ones do: `POST /api/sessions` with `idle: true` and `preview` (the message, which names the session until its turn is saved), then `POST /api/sessions/:id/turn`, so a failed first turn puts its prompt back in the composer too; a first line that is one of the new session's commands (`/model …`) goes to `POST /api/sessions/:id/command` instead, as later ones do. The browser remembers the last choice; a running session's model is in the info bar and changes only with `/model`. In the composer a line is a command when it is `!…` or starts with one of the session's command names (its `commands` list; `/stats`, `/exit` and the other terminal words get the page's own reply); an unknown `/word` (`/usr/bin/env is missing`) goes to the model as a prompt, as in a terminal. A `POST /turn` (or a session's first `prompt`) that is one of the worker's commands runs as that command too (`202 {status, command_id, session_id}`), so `chi send`, `chi -p` and a page whose command list lags all get the command. ↑/↓ in the composer walk the prompt history the TUI's ↑ walks ([Persistent Prompt History](cli.md#persistent-prompt-history)): ↑ with the caret on the first line, ↓ from the last line while browsing, ↓ past the newest brings the draft back; `GET /api/history` returns `{entries: [...]}`, oldest first (the page fetches it on focus and after each send). The server records a sent line in the routes once the worker took it (`POST /turn` accepted, a `!command` through `POST /command` or as a session's first line); `"history": false` in the body keeps it out (an image-only send).
67
67
  - Web notifications: when a session needs you while the tab is hidden or behind another window (a question or a guardrail approval, a plugin card with buttons during a turn such as check-in's, a failed turn, a turn done after 10 s or more; a delegated session only for its questions, a canceled turn never), the page title counts it, `(N) Chi`, until you come back to the tab. The bell at the right of the top bar also turns on OS notifications ("needs an answer", "needs approval", "needs you", "turn failed", "done in 42 s", under the session's first message; a click opens the session): its first click asks the browser for the permission, and the choice is kept per browser. Any session of the page's scope counts, not only the open one. What was already so when the page loaded never counts, and several chi tabs show one notification per event. A chi tab in front tells the others (a `BroadcastChannel`), so while you look at one, the ones behind neither notify nor count what it shows; coming back to a tab clears those events in the other tabs' titles too. The hub's `session` frames carry what this needs: `pending_question` (`{id, kind}`, kind `question` or `approval`), `pending_card` (`{id, bundle}`: the running turn's card with actions, from `pending_card.json` that the worker keeps in the session's folder while one is open; a card without actions, such as loop-guard's, never counts) and `last_turn` (`{outcome, ended_at, seconds, origin}`, saved in the session's JSON as the engine ends each turn).
68
68
  - The web frontend is a zero-build ES-module stack in `lib/samagotchi/web/public/`: `data.js` (retrieval, typed SSE `openStream` for a session and `openEvents` for the session list), `sessions_list.js` (the list as a pure reducer over `GET /api/events`: a snapshot replaces it, an upsert keeps a known card in place, a removal drops it), `notify.js` (which of those events needs the user, for the notifications), `app.js` (presentation/state; a session with no live stream gets one when its `session` event says `bridge_up`, after one re-read: `event_seq` starts over in each worker, so a dropped stream is never resumed with its old cursor), `format.js` (pure formatters such as `previewOf`), `prompt_history.js` (the composer's ↑/↓ over the shared prompt history). Unit-tested via `npm test` (`node --test spec/web/public/*.test.js`).
69
69
  - Selecting a session in the web UI is read-only: `GET /api/sessions/:id` never spawns a worker (it reads a live worker's snapshot when one runs, else the session file). A prompt (`POST /turn`) or a command (`POST /command`) wakes the worker, and `/stream` briefly waits for a freshly-spawned bridge before answering. A caught-up SSE reconnect holds the stream open; `reset` markers are only sent for reconnects behind the ring window, or with a cursor from another worker (event ids are `<event_seq>-<epoch>`, one epoch per worker).
@@ -149,7 +149,8 @@ pbpaste | chi send 3fa2 # the clipboard is the messa
149
149
  - A session with no worker gets one started (like `--attach` or the web composer). A session open in a plain REPL (`--no-shared`) refuses it. Only sessions on this machine: Bridges listen on 127.0.0.1.
150
150
  - Fire and forget: it returns once the message is queued and never prints the answer; that shows in whatever is attached. A guardrail "ask" waits for a UI to answer it, so with nothing attached the turn stalls there until one attaches (`chi --attach ID`).
151
151
  - `--image PATH` (repeatable, up to 20) sends images with the message, as the web composer's chips do. Each file is read once before anything is sent (bmp, tiff and heic converted to png, large ones downscaled, as for `@path`); a missing file or one that isn't an image stops the send (exit 1; with `--wait --format json`, an `error` line) and nothing is sent; more than 20 is a usage error (exit 2). Each session gets its own copy in `<session>/images/`. Text is still required (`-m` or stdin; context alone counts), also with `--wait`. The session's model must see images: one known not to (the host's model list says text-only, `vision: false` in `models:` or `hosts:`, a native llama.cpp without a vision model) is refused before anything is sent, with the reason (`<id> refused: gemma can't take images (…); send text only or switch the model (/model)`); the other sessions of the same send still get it, and the exit is 1. With `--new` nothing is started. When chi can't tell (a native host whose `/props` doesn't answer, a list that doesn't say), the message is sent as before and a text-only model fails the turn in the session. A running turn doesn't take images mid-turn: the message runs as the next turn, and the line says `(runs after the current turn)`. With `--new` the session starts idle with the message as its preview, then the message goes in as its first turn once its worker is up (`<id> started with 1 image`); if the worker doesn't come up in 5 s the session is kept, with its id on the `failed:` line.
152
- - One line per session: `sent`, `sent with 2 images`, `sent (the running turn picks it up)`, `sent (started its worker)`, `refused: …` or `failed: …`. Exit 0 when all were sent, 1 when any was refused, failed or not found, 2 for a usage error. There is no `--all`.
152
+ - A session command runs as the command, as typed in a terminal: `chi send -m "/model qwen" 3fa2` switches the model (`sent as a session command`), and the model never sees the line. What counts is what the session's worker knows (the built-ins, `!cmd`, its bundles' commands); an unknown `/word` (`/usr/bin/env is missing`) goes in as a message. With `--new` a line starting with `/` or `!` starts the session idle and goes to its worker once it is up, which decides. With `--wait` a command has no reply to wait for: exit 0 at once, `<id> sent as a session command: no reply to wait for …` on stderr (`{"status":"command",…,"detail":…}` with `--format json`); its output shows where the session is open.
153
+ - One line per session: `sent`, `sent as a session command`, `sent with 2 images`, `sent (the running turn picks it up)`, `sent (started its worker)`, `refused: …` or `failed: …`. Exit 0 when all were sent, 1 when any was refused, failed or not found, 2 for a usage error. There is no `--all`.
153
154
 
154
155
  The Automator action above works for messages too: swap its last line for `pbpaste | "$chi" send -m "what do you make of this?" $(print -r -- "$picked" | cut -f1)`.
155
156
 
data/docs/sub-agent.md CHANGED
@@ -33,6 +33,7 @@ Exit codes, both commands:
33
33
  | Exit | `status` | Meaning |
34
34
  |---|---|---|
35
35
  | 0 | `answered` | `text` is chi's reply |
36
+ | 0 | `command` | (`chi send`) the message was a session command (`/model x`): it ran as one, no reply comes |
36
37
  | 3 | `question` | chi waits for an answer: `question`, `answer_with` |
37
38
  | 4 | `running` | `--timeout` passed; the turn goes on, wait again |
38
39
  | 1 | `failed`, `canceled`, `no_answer`, `error`, `worker_gone`, `stopped` | `detail` says what happened |
data/docs/testing.md ADDED
@@ -0,0 +1,47 @@
1
+ # Testing
2
+
3
+ ```sh
4
+ bundle exec parallel_rspec -n 8 # Ruby specs, in parallel (or plain: bundle exec rspec)
5
+ npm test # web frontend specs
6
+ npm run e2e # web UI happy paths in Chromium, fake model (once: npx playwright install chromium)
7
+ ```
8
+
9
+ No spec reads your own setup. The suite points `XDG_CONFIG_HOME` and `XDG_STATE_HOME` at temp folders,
10
+ clears your `SAMAGOTCHI_*` environment, and keeps every spec off the network (WebMock).
11
+
12
+ ## Integration specs
13
+
14
+ Examples tagged `:integration` (`spec/integration/`, the "with LLM access" part of `spec/hooks/integration_spec.rb`)
15
+ run turns against a live model server. They are skipped unless `SAMAGOTCHI_INTEGRATION=1` is set, and they need the
16
+ server and the model it serves:
17
+
18
+ | Variable | Meaning | Default |
19
+ | --- | --- | --- |
20
+ | `SAMAGOTCHI_INTEGRATION` | `1` runs them | off |
21
+ | `SAMAGOTCHI_INTEGRATION_HOST` | model server host | `localhost` |
22
+ | `SAMAGOTCHI_INTEGRATION_PORT` | model server port | `8080` |
23
+ | `SAMAGOTCHI_INTEGRATION_MODEL` | the served model id | none: the examples skip |
24
+ | `SAMAGOTCHI_INTEGRATION_API` | `openai` for the chat loop (`hosts.<name>.api`) | none: the native loop |
25
+ | `SAMAGOTCHI_INTEGRATION_TRANSPORT` | `llama_cpp`, `mlx` or `omlx` (`server.transport`) | none: `llama_cpp` |
26
+
27
+ They run under a config built from these variables alone (`default.model`, `server.host`/`port`, and one
28
+ `hosts.integration` entry; `spec/support/integration_server.rb`), never your `~/.config/samagotchi/config.yml`: no
29
+ aliases, memories, hooks, guardrails or other hosts of yours. Sessions and logs go to the suite's temp state folder.
30
+
31
+ Run them one at a time (plain `rspec`, not `parallel_rspec`): each example is a real model turn, and the server may be
32
+ shared.
33
+
34
+ ```sh
35
+ SAMAGOTCHI_INTEGRATION=1 \
36
+ SAMAGOTCHI_INTEGRATION_HOST=192.0.2.10 SAMAGOTCHI_INTEGRATION_PORT=8081 \
37
+ SAMAGOTCHI_INTEGRATION_MODEL=org/Model-GGUF:Q4_K_M \
38
+ bundle exec rspec --tag integration spec/integration spec/hooks/integration_spec.rb spec/integration_config_spec.rb
39
+ ```
40
+
41
+ A run takes one to four minutes against a 35B MoE model on llama.cpp (25 examples). `ECONNREFUSED` means the server is down. The
42
+ examples assert what the model did (the tool it called, the file it edited), not its wording, but they still depend on
43
+ a model that follows tool instructions: a small model may fail some of them.
44
+
45
+ `spec/integration/omlx_spec.rb` tests oMLX's model-id resolution and runs only with
46
+ `SAMAGOTCHI_INTEGRATION_TRANSPORT=omlx`, against an oMLX server (a short model selector in
47
+ `SAMAGOTCHI_INTEGRATION_MODEL`).
@@ -41,11 +41,23 @@ module Samagotchi
41
41
  # continue, ordered parts (thinking / text / tool / input / steer /
42
42
  # reminder / notice), pending_question and the last event_seq folded
43
43
  # in. A finished tool part keeps the live row's action and duration_ms
44
- # too. A notice part holds the event of one of the turn's rows
45
- # (TurnNotice: a hook's notice, an empty-answer retry, a question and
46
- # its answer), which a UI replays through its live handler.
44
+ # too, a running one the time it has run so far (elapsed_ms), so a
45
+ # join times its row from its start. A notice part holds the event
46
+ # of one of the turn's rows (TurnNotice: a hook's notice, an
47
+ # empty-answer retry, a question and its answer), which a UI replays
48
+ # through its live handler.
47
49
  def current_turn
48
- @mutex.synchronize { @turn && Marshal.load(Marshal.dump(@turn)) }
50
+ @mutex.synchronize do
51
+ next nil unless @turn
52
+
53
+ turn = Marshal.load(Marshal.dump(@turn))
54
+ now = @clock.call
55
+ Array(turn[:parts]).each do |part|
56
+ started_at = part[:kind] == "tool" && part[:status] == "running" && @tool_started_at[[part[:iteration], part[:call_index]]]
57
+ part[:elapsed_ms] = ((now - started_at) * 1000).round if started_at
58
+ end
59
+ turn
60
+ end
49
61
  end
50
62
 
51
63
  # The turn in progress as conversation messages, for a plugin's
@@ -142,7 +154,10 @@ module Samagotchi
142
154
  call[:label] = part[:label] if part[:label]
143
155
  title = part[:title] ? { title: part[:title] } : {}
144
156
  events = [{ type: :tool_call_started, **call, params: part[:params], **title }]
145
- return events if part[:status] == "running"
157
+ if part[:status] == "running"
158
+ events.first[:elapsed_ms] = part[:elapsed_ms] unless part[:elapsed_ms].nil?
159
+ return events
160
+ end
146
161
 
147
162
  activity = { tool: part[:tool], status: part[:status], params: part[:params], **title }
148
163
  activity[:action] = part[:action] if part[:action]
@@ -244,6 +244,16 @@ module Samagotchi
244
244
  end
245
245
  end
246
246
 
247
+ # The worker's own way in for a session command that came as a message
248
+ # (its first prompt, an input file): queued and announced as a POST
249
+ # /command's is (#handle_command).
250
+ # @return [String] its command_id
251
+ def queue_command(line, client_id: nil)
252
+ command = { command_id: SecureRandom.uuid, client_id: client_id, line: line.to_s.strip }
253
+ @engine.synchronize_events { queue_command_locked(command) }
254
+ command[:command_id]
255
+ end
256
+
247
257
  private
248
258
 
249
259
  # The Engine's commands: the built-ins and its plugins'.
@@ -612,17 +622,21 @@ module Samagotchi
612
622
  deadline = fetched(parsed, "deadline")
613
623
  return BAD_DEADLINE unless deadline_valid?(deadline)
614
624
 
615
- command = { command_id: SecureRandom.uuid, client_id: fetched(parsed, "client_id"), line: line }
625
+ queue_command_request(session_id, line, client_id: fetched(parsed, "client_id"), deadline: deadline,
626
+ card: fetched(parsed, "card") == true)
627
+ end
628
+
629
+ # Queues +line+ (a session command) for the worker and announces its
630
+ # :command_queued, unless +deadline+ has passed. Returns [headers,
631
+ # status, body]: 202 with its command_id, or 408.
632
+ def queue_command_request(session_id, line, client_id:, deadline: nil, card: false)
633
+ command = { command_id: SecureRandom.uuid, client_id: client_id, line: line }
616
634
  # A card's action: the UIs leave its line out (the card is the echo).
617
- command[:card] = true if fetched(parsed, "card") == true
635
+ command[:card] = true if card
618
636
  queued = @engine.synchronize_events do
619
637
  next false if expired?("command_expired", deadline, sid: session_id, client_id: command[:client_id])
620
638
 
621
- @on_command.call(command)
622
- # An anytime command runs now, beside a turn (D8): the UIs show its
623
- # line here, so the cards it shows come after it.
624
- anytime = command_registry.lookup(line)&.anytime ? { anytime: true } : {}
625
- @engine.announce(type: :command_queued, **command, **anytime)
639
+ queue_command_locked(command)
626
640
  true
627
641
  end
628
642
  return deadline_passed("command") unless queued
@@ -630,6 +644,15 @@ module Samagotchi
630
644
  [{}, 202, { status: "accepted", command_id: command[:command_id], session_id: @session_id }]
631
645
  end
632
646
 
647
+ # With the event log held.
648
+ def queue_command_locked(command)
649
+ @on_command.call(command)
650
+ # An anytime command runs now, beside a turn (D8): the UIs show its
651
+ # line here, so the cards it shows come after it.
652
+ anytime = command_registry.lookup(command[:line])&.anytime ? { anytime: true } : {}
653
+ @engine.announce(type: :command_queued, **command, **anytime)
654
+ end
655
+
633
656
  # A client asks the worker to exit now (`/exit` in the attached TUI). The
634
657
  # worker decides with the event log held, so no POST /turn lands between
635
658
  # its check and its answer; it leaves from its loop after this reply.
@@ -688,6 +711,14 @@ module Samagotchi
688
711
  deadline = fetched(parsed, "deadline")
689
712
  return BAD_DEADLINE unless deadline_valid?(deadline)
690
713
 
714
+ # A session command sent as a message (chi send -m "/model x", chi -p,
715
+ # a UI whose command list lags) runs as the command, as typed in a
716
+ # TUI; an unknown /word stays a prompt. Not with images: those are
717
+ # for the model.
718
+ if images.empty? && @on_command && command_registry.command?(prompt.to_s)
719
+ return queue_command_request(sid, prompt.to_s.strip, client_id: client_id, deadline: deadline)
720
+ end
721
+
691
722
  enqueued_id = SecureRandom.uuid
692
723
  # Write and announce with the event log held: the worker can't emit
693
724
  # this turn's :turn_started (or merge it mid-turn) before
@@ -36,7 +36,7 @@ module Samagotchi
36
36
  build [--scope system|project] [--name NAME] [--version VER] [--description DESC] [--out PATH] [FILES...]
37
37
  Build local memories and installed hooks (and plugin) into a shareable bundle (dir or zip).
38
38
  --scope selects source dir (default: system). --out inferred from extension; default <name>.zip.
39
- FILES... optional allowlist of *.md basenames to include (default: all).
39
+ FILES... optional allowlist of *.md basenames to include (default: all but installed bundles' ones).
40
40
  TEXT
41
41
 
42
42
  # Each subcommand's --help (and only --help: -h and help are taken as
@@ -57,7 +57,7 @@ module Samagotchi
57
57
  build [--scope system|project] [--name NAME] [--version VER] [--description DESC] [--out PATH] [FILES...]
58
58
  Build local memories and installed hooks into a shareable bundle (dir or zip).
59
59
  --scope selects source dir (default: system). --out inferred from extension; default <name>.zip.
60
- FILES... optional allowlist of *.md basenames to include (default: all).
60
+ FILES... optional allowlist of *.md basenames to include (default: all but installed bundles' ones).
61
61
 
62
62
  Examples:
63
63
  chi bundle build --scope system
@@ -272,6 +272,7 @@ module Samagotchi
272
272
  begin
273
273
  uninstaller.run
274
274
  @stdout.puts "Uninstalled bundle '#{bundle_name}'"
275
+ puts_trashed(uninstaller.trashed_files, uninstaller.trash_dir)
275
276
  @stdout.puts "Removed: #{uninstaller.removed_files.join(', ')}" unless uninstaller.removed_files.empty?
276
277
  hook_removed = uninstaller.removed_files.count { |f| f.start_with?("hooks/") }
277
278
  @stdout.puts "Hooks removed: #{hook_removed}" if hook_removed > 0
@@ -311,9 +312,13 @@ module Samagotchi
311
312
  if result.gone
312
313
  @stdout.puts "Uninstalled bundle '#{name}'"
313
314
  @stdout.puts "Removed: #{result.removed.join(", ")}" unless result.removed.empty?
315
+ result.trash.each_value { |files, dir| puts_trashed(files, dir) }
316
+ result.warnings.each { |w| @stdout.puts w }
314
317
  return 0
315
318
  end
316
319
  @stdout.puts "Removed from #{name}: #{result.removed.join(", ")}" unless result.removed.empty?
320
+ result.trash.each_value { |files, dir| puts_trashed(files, dir) }
321
+ result.warnings.each { |w| @stdout.puts w }
317
322
  result.blocked.each { |member, why| @stdout.puts "Kept #{member}: #{why}" }
318
323
  @stderr.puts "Uninstall failed: #{name} stays installed until #{result.blocked.keys.join(", ")} goes " \
319
324
  "(chi bundle uninstall #{name} --force)"
@@ -323,6 +328,11 @@ module Samagotchi
323
328
  1
324
329
  end
325
330
 
331
+ # Memory files an uninstall moved to the trash (MemoryBundle::Trash).
332
+ def puts_trashed(files, dir)
333
+ @stdout.puts "Moved to the trash: #{files.join(", ")} (#{dir})" if dir && !files.empty?
334
+ end
335
+
326
336
  def status(rest)
327
337
  name = rest.find { |a| !a.start_with?("--") }
328
338
  require "json"
@@ -557,6 +567,7 @@ module Samagotchi
557
567
  @stdout.puts "Built #{result[:files].size} file(s) to #{result[:out_path]}"
558
568
  @stdout.puts "Bundle: #{result[:name]} v#{result[:version]} scope=#{result[:scope]}"
559
569
  @stdout.puts "Files: #{result[:files].join(', ')}" unless result[:files].empty?
570
+ builder.warnings.each { |w| @stdout.puts w }
560
571
  unless result[:placeholder_warnings].empty?
561
572
  @stdout.puts "Placeholders:"
562
573
  result[:placeholder_warnings].each { |w| @stdout.puts " #{w}" }
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: samagotchi-system
3
- version: 0.16.0
3
+ version: 0.17.0
4
4
  scope: system
5
5
  description: Default system memories — identity, self map, config modification protocol, memory guide and the delegated-session rules
6
6
  files:
7
7
  identity.md: sha256:29c2f7df9f6c5acbc5ec17000a4490741e55e426af15ca0d81c8706edd9bd027
8
- self_map.md: sha256:0410c2e62716e256a2a6aec73c7801def185cbae9c1db556efcedbbad22d908c
8
+ self_map.md: sha256:69bc0ec091fdaeedf6e3aa7365275461fe3eba32b2dedc9cf45044176c356af5
9
9
  config_modification_protocol.md: sha256:d0e9d4a84652bf54ec2fd664f61c536637af9decdd22fcd982f52195bf9f5fb4
10
- memory_guide.md: sha256:81b911b9a015f0924c48162b3dc4b6376309e743765074419a99fab46e480858
10
+ memory_guide.md: sha256:e535e31fcf29b4b32c270042851ecdd5f412ef2e3c56b0076963e3e1f1a08ef8
11
11
  delegated.md: sha256:0a71d5dfa7127df2d51cb5f0e7214240ed88777e36559430a8724c796d7ae561
@@ -16,7 +16,7 @@ This memory teaches you (the agent) how to use Samagotchi memories — persisten
16
16
  | `project` | `<system dir>/projects/<basename>_<hash>/` (`MemoryPaths.project_dir`: `basename(root)` + 8-char `MD5(root)`, root = `MemoryPaths.project_root`) | Repo-specific conventions, workflow, stack decisions |
17
17
 
18
18
  - The project root is the git repository: its common git dir, which every linked worktree shares. All worktrees and subdirectories of one repository share one project folder; the system prompt shows the resolved root and folder. Outside a repository the root is the working directory. A separate clone is a different project.
19
- - `index.md` lives in each scope dir and contains auto-managed lines like `- **name** · scope · date · bytes — description`. Your free-form sections in `index.md` are preserved but managed lines are owned by `memory_write` (and refreshed when `write`/`edit` change a memory's file).
19
+ - `index.md` lives in each scope dir and contains auto-managed lines like `- **name** · scope · date · bytes — description`; a memory a bundle installed adds `· from <bundle>` before the description. Your free-form sections in `index.md` are preserved but managed lines are owned by `memory_write` (and refreshed when `write`/`edit` change a memory's file).
20
20
 
21
21
  ## Tools you have
22
22
 
@@ -85,13 +85,13 @@ Bundles are versioned directories/zips/tar.gz/git URLs with a `manifest.yml` and
85
85
 
86
86
  | Command | Purpose |
87
87
  |---------|---------|
88
- | `install <source> [--scope system\|project] [--force]` | Install from dir/zip/tar.gz/git URL. `--force` overwrites existing entries, otherwise skips. Writes provenance to `.bundles/<name>/` (base snapshots + `manifest.json`) and updates `index.md`. Warns on double-brace placeholders and checksum mismatches (strict mode). |
88
+ | `install <source> [--scope system\|project] [--force]` | Install from dir/zip/tar.gz/git URL. `--force` overwrites existing entries, otherwise skips them; a skipped file stays the user's (not recorded, never upgraded or removed by the bundle). Writes provenance to `.bundles/<name>/` (base snapshots + `manifest.json`) and updates `index.md`. Warns on double-brace placeholders and checksum mismatches (strict mode). |
89
89
  | `upgrade <source> [--force] [--dry-run] [--agent]` | 3-way merge upgrade (base vs current vs incoming) per `Merger.classify`: `install` (new file), `noop` (current==incoming), `fast_forward` (current==base, not edited → auto-update), `keep` (incoming==base → preserve local edits), `conflict` (both edited → warn, needs `--force` or interactive `memory_write` resolution). Pruned files (removed from new bundle) are kept if locally edited, otherwise warned. |
90
- | `uninstall <bundle> [--force]` | Removes bundle files (skips locally edited files unless `--force`) and `index.md` lines, deletes provenance dir. |
90
+ | `uninstall <bundle> [--force]` | Moves the bundle's memory files to `.bundles/.trash/<bundle>-<time>/` (refuses locally edited ones unless `--force`; keeps a file another installed bundle also lists), removes their `index.md` lines, deletes the provenance dir. |
91
91
  | `status [<bundle>]` | Provenance + per-file `ok|modified|missing|no-index` vs stored checksum and base snapshot. |
92
92
  | `diff <bundle> [file]` | Prints `base` (provenance snapshot) vs `current` (on-disk) for each file. |
93
93
  | `list` | Lists installed bundles (`name v<version> scope files installed_at`, plus the shipped version when newer) and the bundles shipped with chi that are not installed (`install <name>` installs one), grouped under their profile; an installed profile shows `includes=` and `left out=`. |
94
- | `build [--scope system\|project] [--name NAME] [--version VER] [--description DESC] [--out PATH] [FILES...]` | **Inverse of install** — builds a shareable bundle from local memories and installed hooks. Infers `zip` vs `dir`/`tar.gz` from `--out` extension; default `chi_system_memories.zip` (system) or `chi_<repo>_memories.zip` (project, named after the project root) v`1.0.0` in `Dir.pwd`. `FILES...` is an optional allowlist of memory basenames (`identity` or `identity.md`); if omitted, all `*.md` except `index.md`/hidden/non-md are included. Installed hooks are copied to `hooks/` with their manifest metadata. Computes `sha256:` checksums and writes `manifest.yml` via `Manifest.write`. No provenance write. |
94
+ | `build [--scope system\|project] [--name NAME] [--version VER] [--description DESC] [--out PATH] [FILES...]` | **Inverse of install** — builds a shareable bundle from local memories and installed hooks. Infers `zip` vs `dir`/`tar.gz` from `--out` extension; default `chi_system_memories.zip` (system) or `chi_<repo>_memories.zip` (project, named after the project root) v`1.0.0` in `Dir.pwd`. `FILES...` is an optional allowlist of memory basenames (`identity` or `identity.md`); if omitted, all `*.md` except `index.md`/hidden/non-md and the memories other installed bundles own (each named in a `Left out` line) are included. Installed hooks are copied to `hooks/` with their manifest metadata. Computes `sha256:` checksums and writes `manifest.yml` via `Manifest.write`. No provenance write. |
95
95
 
96
96
  **Scope resolution for install/build:**
97
97
  - CLI `--scope` wins over `manifest.yml` `scope`. Default is `system` if none given (`Installer#run`). For `project`, target is the project folder `<system dir>/projects/<basename>_<hash>` of the project root (`Installer#resolve_target_dir`).
@@ -119,7 +119,7 @@ chi bundle build --scope system --out updated.zip
119
119
  ### Provenance internals (for debugging)
120
120
 
121
121
  - Each installed bundle is recorded at `<system dir>/.bundles/<name>/manifest.json` + `bases/<file>.md` snapshots (`Provenance`). Used only for upgrade `Merger` and `status`/`diff`. Build does **not** write provenance.
122
- - `index.md` is best-effort — failures are swallowed (`Installer#update_target_index`).
122
+ - A bundle records only the files it wrote. `index.md` updates aren't fatal: a failed one is a warning in the install/uninstall output.
123
123
 
124
124
  ## Best practices for the agent
125
125
 
@@ -3,6 +3,7 @@
3
3
  ## Start here
4
4
  - `chi self` (via `execute`) prints my version, **source dir**, config path, hooks dir,
5
5
  memory dirs, sessions dir, model/host and bundles. Use its source dir; don't hunt via `which`/`gem list`/`find /`.
6
+ Its `model` row is this session's model (live after `/model`), `model key` my overlay key.
6
7
  - My current session id is in the system prompt; resume with `chi --resume <id>`.
7
8
  - My debug log is the `log` line of `chi self` (default `$XDG_STATE_HOME/samagotchi/samagotchi.log`,
8
9
  next to the sessions dir; not `~/.local/state` when `XDG_STATE_HOME` is set). One record per line,