samagotchi 0.8.1 → 0.10.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 (122) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +89 -1
  3. data/docs/architecture.md +24 -9
  4. data/docs/cli.md +4 -3
  5. data/docs/guardrails.md +9 -2
  6. data/docs/hooks.md +44 -7
  7. data/docs/internals/gemma4-contract.md +1 -0
  8. data/docs/memory.md +5 -3
  9. data/docs/plugins.md +47 -17
  10. data/docs/sessions.md +1 -1
  11. data/lib/samagotchi/bootstrap_command.rb +29 -29
  12. data/lib/samagotchi/bridge/card_store.rb +4 -4
  13. data/lib/samagotchi/bridge/pending_card.rb +2 -2
  14. data/lib/samagotchi/bridge/sse_writer.rb +22 -28
  15. data/lib/samagotchi/bridge/turn_accumulator.rb +92 -5
  16. data/lib/samagotchi/bridge.rb +59 -112
  17. data/lib/samagotchi/bridge_client.rb +11 -28
  18. data/lib/samagotchi/bundle_command.rb +158 -212
  19. data/lib/samagotchi/bundles/check-in/manifest.yml +2 -2
  20. data/lib/samagotchi/bundles/check-in/plugin.rb +3 -4
  21. data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +7 -3
  22. data/lib/samagotchi/bundles/known-names/manifest.yml +2 -2
  23. data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
  24. data/lib/samagotchi/bundles/loop-guard/plugin.rb +10 -10
  25. data/lib/samagotchi/bundles/mcp/manifest.yml +3 -3
  26. data/lib/samagotchi/bundles/mcp/plugin.rb +2 -4
  27. data/lib/samagotchi/bundles/skills/manifest.yml +3 -3
  28. data/lib/samagotchi/bundles/skills/plugin.rb +65 -17
  29. data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +0 -1
  30. data/lib/samagotchi/bundles/source-links/manifest.yml +2 -2
  31. data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
  32. data/lib/samagotchi/bundles/system/identity.md +1 -1
  33. data/lib/samagotchi/bundles/system/manifest.yml +4 -4
  34. data/lib/samagotchi/bundles/system/memory_guide.md +5 -4
  35. data/lib/samagotchi/cli/command.rb +51 -0
  36. data/lib/samagotchi/cli/flags.rb +116 -0
  37. data/lib/samagotchi/config.rb +70 -55
  38. data/lib/samagotchi/config_text_edit.rb +107 -0
  39. data/lib/samagotchi/desktop_command.rb +24 -18
  40. data/lib/samagotchi/edit_preview.rb +2 -2
  41. data/lib/samagotchi/engine.rb +20 -75
  42. data/lib/samagotchi/events.rb +23 -0
  43. data/lib/samagotchi/guardrail_wiring.rb +2 -5
  44. data/lib/samagotchi/guardrails/targets.rb +7 -1
  45. data/lib/samagotchi/hooks/bundle_loader.rb +13 -30
  46. data/lib/samagotchi/hooks/loader.rb +23 -33
  47. data/lib/samagotchi/hooks/registry.rb +41 -0
  48. data/lib/samagotchi/hooks.rb +0 -3
  49. data/lib/samagotchi/host_registry.rb +15 -0
  50. data/lib/samagotchi/idle_reminders.rb +0 -7
  51. data/lib/samagotchi/image_store.rb +22 -0
  52. data/lib/samagotchi/kernel_loop.rb +12 -16
  53. data/lib/samagotchi/live_versions.rb +2 -13
  54. data/lib/samagotchi/llm/native_tool_normalizer.rb +10 -227
  55. data/lib/samagotchi/log_subscriber.rb +5 -1
  56. data/lib/samagotchi/memory_bundle/builder.rb +1 -14
  57. data/lib/samagotchi/memory_bundle/index_sync.rb +73 -0
  58. data/lib/samagotchi/memory_bundle/index_updater.rb +2 -28
  59. data/lib/samagotchi/memory_bundle/installer.rb +25 -30
  60. data/lib/samagotchi/memory_bundle/provenance.rb +2 -6
  61. data/lib/samagotchi/memory_bundle/status.rb +1 -14
  62. data/lib/samagotchi/memory_bundle/system_bundle.rb +2 -2
  63. data/lib/samagotchi/memory_bundle/uninstaller.rb +1 -24
  64. data/lib/samagotchi/memory_paths.rb +10 -0
  65. data/lib/samagotchi/note_command.rb +23 -37
  66. data/lib/samagotchi/owner_lock.rb +14 -3
  67. data/lib/samagotchi/plugin/api.rb +11 -17
  68. data/lib/samagotchi/plugin/context.rb +70 -15
  69. data/lib/samagotchi/plugin/loader.rb +2 -3
  70. data/lib/samagotchi/sampling_settings.rb +3 -5
  71. data/lib/samagotchi/self_report.rb +6 -9
  72. data/lib/samagotchi/send_command.rb +33 -52
  73. data/lib/samagotchi/session.rb +88 -92
  74. data/lib/samagotchi/session_archive_command.rb +8 -14
  75. data/lib/samagotchi/session_commands.rb +3 -21
  76. data/lib/samagotchi/session_delete_command.rb +15 -29
  77. data/lib/samagotchi/session_manager.rb +29 -15
  78. data/lib/samagotchi/session_metrics.rb +2 -1
  79. data/lib/samagotchi/sessions_command.rb +3 -2
  80. data/lib/samagotchi/system_prompt.rb +2 -1
  81. data/lib/samagotchi/terminal_ui/attached_loop.rb +30 -80
  82. data/lib/samagotchi/terminal_ui/attached_view.rb +1 -10
  83. data/lib/samagotchi/terminal_ui/between_turns.rb +133 -0
  84. data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -1
  85. data/lib/samagotchi/terminal_ui/formatting.rb +51 -1
  86. data/lib/samagotchi/terminal_ui/status_row.rb +32 -0
  87. data/lib/samagotchi/terminal_ui/thinking_line.rb +3 -3
  88. data/lib/samagotchi/terminal_ui.rb +156 -234
  89. data/lib/samagotchi/thinking.rb +2 -3
  90. data/lib/samagotchi/thought_stream_splitter.rb +10 -9
  91. data/lib/samagotchi/tool_call_parser.rb +56 -292
  92. data/lib/samagotchi/tool_declarations.rb +1 -1
  93. data/lib/samagotchi/tool_runner.rb +8 -0
  94. data/lib/samagotchi/tools/builtin_calls.rb +146 -0
  95. data/lib/samagotchi/tools/builtins.rb +2 -1
  96. data/lib/samagotchi/tools/edit.rb +21 -29
  97. data/lib/samagotchi/tools/memory.rb +2 -13
  98. data/lib/samagotchi/tools/send_note.rb +1 -1
  99. data/lib/samagotchi/update_command.rb +32 -22
  100. data/lib/samagotchi/version.rb +1 -1
  101. data/lib/samagotchi/vision_support.rb +4 -5
  102. data/lib/samagotchi/web/app.rb +89 -151
  103. data/lib/samagotchi/web/public/activity.js +1 -11
  104. data/lib/samagotchi/web/public/annotate_bar.js +120 -0
  105. data/lib/samagotchi/web/public/app.js +158 -785
  106. data/lib/samagotchi/web/public/chunk_router.js +7 -6
  107. data/lib/samagotchi/web/public/command_list.js +99 -0
  108. data/lib/samagotchi/web/public/data.js +22 -66
  109. data/lib/samagotchi/web/public/format.js +0 -23
  110. data/lib/samagotchi/web/public/index.html +0 -2
  111. data/lib/samagotchi/web/public/model_pick.js +0 -7
  112. data/lib/samagotchi/web/public/notifications.js +169 -0
  113. data/lib/samagotchi/web/public/question_cards.js +352 -0
  114. data/lib/samagotchi/web/public/session_state.js +34 -0
  115. data/lib/samagotchi/web/public/timing.js +1 -1
  116. data/lib/samagotchi/web/public/turn_events.js +11 -2
  117. data/lib/samagotchi/web/public/turn_model.js +0 -14
  118. data/lib/samagotchi/web/session_hub.rb +6 -4
  119. data/lib/samagotchi/web/session_summary.rb +5 -3
  120. data/lib/samagotchi/worker.rb +59 -53
  121. data/lib/samagotchi/worker_sidecar.rb +27 -1
  122. metadata +13 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 80a8fec85f3a253ce7f762f3c8881ee0c49d6e73117f205a53cb64482e7a9cc3
4
- data.tar.gz: 6e21caec0f46b5a2831382a2c77842e5d3935e363b6b6ea96f5d136a24ffa1eb
3
+ metadata.gz: 38684442adc7d8b24e225a8219cd4834395744722f027bd5357b3632b2949641
4
+ data.tar.gz: f7d0537f004a6b2de38762bbf82d37af648e58b4d06e33e2d19f4c240da7899d
5
5
  SHA512:
6
- metadata.gz: ad7fd8882239bb39478cddc39d48e85c2dedfb782d73da193b7dfd401b40599aaf2385462c911d633ebdf78d2f2841d3c400367c1e6feefc300778e617afa425
7
- data.tar.gz: 2104f11e65bfdf5f9b82a92a2e1281cda2c6a1c6c28a58bd7eedc89e34492f47f34f8aec229aba66d6c875b465ae56f181a0d0a539b039c5ed79a4830c1e4cd0
6
+ metadata.gz: 695b4c2ab0898eb3a6e6ef2981258da778ec43780dbd8b96ebebeb37b3646522ba92df5851b55a10751a07ada0182392658f071c5be435776431f0dc8b85bfa9
7
+ data.tar.gz: 129ff9e9ca7bafb5d792fb6d578d8f85b935cc45d402bf820bedd69358fa9d064e2873053f4c38d42f85d8b4e4da86a067747ce5149c13b2cacfbb48c88ef9ee
data/CHANGELOG.md CHANGED
@@ -8,6 +8,92 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.10.0] - 2026-10-01
12
+
13
+ ### Changed
14
+
15
+ - Built-in tool calls are built the same way for every model format (Gemma and Qwen native, OpenAI-style chat), so
16
+ they no longer differ by format. As part of that:
17
+ - `edit` takes the old and new text as two fields (`old_text`, `new_text`), so old text that contains `</old>` is
18
+ no longer cut short; `edit` without `new_text` is an error (it deleted the matched text; pass `""` to delete).
19
+ - `write` without `content` is an error with every model format (Gemma and Qwen emptied the file).
20
+ - `register_reminder` from a Qwen model without an interval no longer always fails.
21
+ - Plugins: `ctx.notify`, `ctx.ask_user`, `ctx.steer`, `ctx.stop_turn` and `ctx.stop_generation` are the one way a plugin
22
+ acts; inside a `chi.on` block they act for that event as `event[:x]` does (`ctx.stop_turn` in `:before_tool_call`
23
+ denies the call, `ctx.steer` from `:after_turn` is false), and as before anywhere else (commands, your own threads).
24
+ Plain hook files keep `event[:x]`.
25
+ - Attaching to a session mid-turn (`chi --attach`) shows the turn's finished tool calls as they looked live, with
26
+ what each did and how long it took (`tool> running command (execute …): ok (1.3s)`), when the worker runs this chi.
27
+ - `chi bundle` usage errors (unknown subcommand or flag, a missing argument) exit 2 like every other command, not 1.
28
+ - A config hook with `on_error: log` warns as `[samagotchi:hooks] <file> (config) failed: <error>`, the same shape as a
29
+ bundle hook's warning; a fail-closed bundle guardrail's deny reason reads `... (bundle <name>) raised <error>`.
30
+ - Bundles (`chi update`):
31
+ - known-names 0.1.2: corrects a near-miss name only in paths, `cwd` and commands, no longer inside text being
32
+ written to a file (`write`/`memory_write` content, `edit`'s old and new text, where a correction could stop the
33
+ edit matching).
34
+ - loop-guard 0.2.2: tells repeated `edit` calls apart by their old and new text; acts through `ctx`.
35
+ - skills 0.1.3 and mcp 0.3.2 (mcp now needs chi 0.8.0): write a skill's history and the MCP tool cache through a
36
+ unique temporary file, so two workers saving at once no longer collide.
37
+ - check-in 0.1.3 acts through `ctx`; source-links 0.3.1 drops dead code. They behave as before.
38
+
39
+ ### Fixed
40
+
41
+ - Web: a Gemma model on a native llama.cpp host streams its answer into the reply as it writes (it showed only when the
42
+ turn ended), with its thoughts in the thinking block; the attached terminal shows them as thinking and the answer as
43
+ writing.
44
+ - `execute` with a `cwd` now runs in that directory with Gemma and Qwen native tool calls too (the directory was
45
+ dropped and the command ran in the project root).
46
+ - `/model … --default` and `/model … --alias` keep the comments and layout of `config.yml`: they change one line
47
+ (or add one) instead of rewriting the whole file.
48
+ - `chi sessions stop` marks the session with a `stopped` file in its folder instead of rewriting its session file, so
49
+ a worker saving at that moment can no longer undo the stop (or lose its own save); a resume removes the file.
50
+ - The web's answer, dismiss, command and cancel calls fail the same way: a worker that doesn't answer in time is
51
+ 504 ("did not answer, so ... was not ..."; cancel said "not running"), a worker running an older chi is 501 with
52
+ how to restart it (an answer said "not running").
53
+ - A web page joining (or re-syncing with) a live session gets its status and pending question as of the same moment
54
+ as the messages; they could be a step newer.
55
+ - The web's `POST /api/sessions/:id/turn` refuses more than 20 images in one message, as a live worker already did.
56
+ - Web: switching sessions no longer briefly shows the previous session's "delegated by" link in the info bar.
57
+ - The terminal's "retrying (1/3 …)" line counts retries as the web does (it said "1/4", counting the first try).
58
+ - `chi self`'s thinking line finds a `models:` entry under the model an alias points at, as a turn does.
59
+ - A `required: true` config hook that raises something other than a StandardError (e.g. `NotImplementedError`) denies
60
+ the tool call like any other raise, instead of escaping the guardrail check.
61
+
62
+ Update with `chi update` (bundles: known-names 0.1.2, loop-guard 0.2.2, skills 0.1.3, mcp 0.3.2, check-in 0.1.3,
63
+ source-links 0.3.1).
64
+
65
+ ## [0.9.0] - 2026-10-01
66
+
67
+ ### Changed
68
+
69
+ - A small change to a memory or skill can go through `edit` on its file instead of rewriting it all with
70
+ `memory_write`; the prompt, the memory guide and the nudge below say so. `write`/`edit` on a memory's file
71
+ refresh its line in `index.md` (date, size; the description is kept), and the default guardrails no longer ask
72
+ before them as "writes outside the repository".
73
+ - The system prompt no longer tells the model to keep `index.md` updated (`memory_write` does it); its result
74
+ says "Index line refreshed automatically." instead of naming the index file.
75
+ - `skills` bundle 0.1.1 (`chi update`; needs chi 0.9.0): a skill changed some other way (an `execute` running `sed`) counts as
76
+ updated: you get the usual "skill X updated" line, `/skill diff` has the old text, and the nudge and the turn-end
77
+ "wasn't updated" line no longer fire for it. The nudge asks to `edit` the changed step rather than rewrite the
78
+ whole skill.
79
+
80
+ ### Fixed
81
+
82
+ - `chi bundle install` of a bundle that's already installed no longer says "Skipped X (already exists; use --force
83
+ to overwrite)" for a file that's the same as the bundle's; it skips it quietly and says once that
84
+ `chi bundle upgrade NAME` updates the bundle and keeps local edits. A file skipped for a local edit no longer
85
+ also reports a checksum mismatch.
86
+ - `chi bundle install` and `upgrade` no longer list a bundle's unchanged guardrail rule files as "Installed"; they
87
+ are skipped as already up to date like its memory files.
88
+ - A memory file you edited, then `chi bundle install` of its bundle again (skipped for the edit), no longer gets
89
+ overwritten by the next `chi bundle upgrade`: the re-install kept your edit as the bundle's base, so the upgrade
90
+ took it as unedited.
91
+ - `chi sessions list --sort/--order` is no longer ignored with `--live`, `--cwd` or `--format`.
92
+ - A tool's time in the web's turn timing and the debug log's `tool_call_completed ms:` no longer counts the wait for
93
+ a guardrail approval; the log line shows the wait as `waited_ms:`.
94
+
95
+ Update with `chi update` (bundles: `skills` 0.1.1).
96
+
11
97
  ## [0.8.1] - 2026-10-01
12
98
 
13
99
  ### Added
@@ -481,7 +567,9 @@ and long-lived sessions.
481
567
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
482
568
  hotkey panel that send selected text or the clipboard to your sessions.
483
569
 
484
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.8.1...HEAD
570
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.10.0...HEAD
571
+ [0.10.0]: https://github.com/dm1try/samagotchi/compare/v0.9.0...v0.10.0
572
+ [0.9.0]: https://github.com/dm1try/samagotchi/compare/v0.8.1...v0.9.0
485
573
  [0.8.1]: https://github.com/dm1try/samagotchi/compare/v0.8.0...v0.8.1
486
574
  [0.8.0]: https://github.com/dm1try/samagotchi/compare/v0.7.0...v0.8.0
487
575
  [0.7.0]: https://github.com/dm1try/samagotchi/compare/v0.6.0...v0.7.0
data/docs/architecture.md CHANGED
@@ -115,9 +115,9 @@ bin/chi ─▶ TerminalUI ─▶ Engine#run_turn ─▶ KernelLoop ──┬─
115
115
 
116
116
  ## Session retention & ordering
117
117
 
118
- - **Files:** `~/.local/state/samagotchi/sessions/<uuid>.json` + `<uuid>/input|output|owner.lock|bridge.json` (XDG-aware).
118
+ - **Files:** `~/.local/state/samagotchi/sessions/<uuid>.json` + `<uuid>/input|output|owner.lock|bridge.json` (XDG-aware), and the `<uuid>/stopped` and `<uuid>/archived` marker files.
119
119
  - **Single owner:** the process running a session's Engine (worker or in-process TUI) holds a flock on `owner.lock` (`OwnerLock`); a second owner backs off, and the web answers 409 for a TUI-owned session.
120
- - **Status:** `status` is turn state (`idle`/`running`); liveness is the lock.
120
+ - **Status:** `status` is turn state (`idle`/`running`); liveness is the lock. A stop is the `<uuid>/stopped` marker (`Session.mark_stopped`; a resume removes it), which `Session.load` and `.summary_from_file` read as status `stopped`, so no other process rewrites a worker's session file.
121
121
  - **Retention:** 14 days / 500 cap (env `SAMAGOTCHI_SESSION_RETENTION_DAYS`/`MAX_COUNT`, optional `KEEP_STATUS`), live-owner guard, only when `*.json` present; lazy sweep ≤1/24h from the session hub's full-probe tick (and on `GET /api/sessions`, which the page no longer calls) & `Dashboard#render_list`, manual via `bin/chi sessions prune --dry-run`.
122
122
  - **Ordering:** `Session.list(sort:,order:,limit:,offset:)` and `GET /api/sessions?sort=&order=&limit=&offset=` default `updated_at desc`; Web UI sort/filter/pagination.
123
123
  - **Test hygiene:** `test_run` flag when `SAMAGOTCHI_ENV=test`/`RACK_ENV=test`/`CI`, targetable via `prune --test-only` / `clean`.
@@ -208,7 +208,8 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
208
208
  `?client_id=` names whose stream it is (`Bridge#open_streams_except`, used by `POST /exit`).
209
209
  - `POST /session/:id/turn` — fire-and-forget turn creation; returns `202` with an `enqueued_id`
210
210
  (delivery is at-least-once via the worker's file-IPC input path — it never calls `run_turn`
211
- across the HTTP boundary). Inspect results through the read surface, not the turn response.
211
+ across the HTTP boundary). Only the bridge's own session: another id, in the path or the body's
212
+ `session_id`, is `404 unknown_session`. Inspect results through the read surface, not the turn response.
212
213
  An optional `deadline` (epoch seconds; `BridgeClient` sends 5/6 of its read timeout ahead) makes
213
214
  a request read after it (a worker frozen by sleep or SIGSTOP) answer `408 deadline_passed` and
214
215
  not run: a client that timed out has said the message was not sent. `/answer`,
@@ -216,6 +217,10 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
216
217
  log held, as a turn is), and the Bridge logs `turn_expired`, `answer_expired`, `dismiss_expired`
217
218
  or `command_expired`. The web app answers either kind of timeout with `504 worker_timeout`
218
219
  ("… so the command was not run"). `/cancel`, `/recap` and `/exit` take none.
220
+ The web relays answer, dismiss, command and cancel the same way (`App#relay`): a 408 or a read
221
+ timeout is `504 worker_timeout`; a Bridge 404 for a route an older worker lacks is
222
+ `501 not_supported` with the restart hint; a Bridge `404 unknown_session`, no bridge or a
223
+ refused connection is `503 not_live`.
219
224
  - `POST /session/:id/cancel` — cancel the running turn; `202`, or `409` when none runs.
220
225
  - `POST /session/:id/answer` — answer the pending question; `200`, `409` when another client
221
226
  answered first or it is gone, `400` for an invalid selection.
@@ -235,6 +240,10 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
235
240
  the messages itself, then streams from its `event_seq`).
236
241
  - `OPTIONS *` — CORS preflight (`Access-Control-Allow-Origin: *`).
237
242
 
243
+ Every route goes through `Bridge#dispatch`: an id other than the bridge's own session is
244
+ `404 unknown_session`, and a handler that raises answers `500 bridge_error` (logged as
245
+ `handler_failed`) rather than dropping the connection.
246
+
238
247
  The per-session port is OS-assigned (bound to `0`) and published to a `bridge.json` sidecar
239
248
  for client discovery. `chi web`'s `GET /api/sessions/:id/stream` proxies this bridge
240
249
  (503 `not_live` when the worker is not running; full history of any session is served by
@@ -248,15 +257,15 @@ changes to every open tab over `GET /api/events` (SSE: a `snapshot` frame on eve
248
257
  then `session` for an upsert and `session_gone` for a removal, `: ping` while idle, no replay).
249
258
  Files stay the source of truth and workers don't know the hub. Its watcher is a 1 s tick that
250
259
  stats the sessions dir (every session writer goes tmp + rename, which bumps the dir's mtime) and
251
- each `<id>/` folder (recap.json, bridge.json), re-parsing only the files whose mtime or size
260
+ each `<id>/` folder (recap.json, bridge.json, the stopped and archived markers), re-parsing only the files whose mtime or size
252
261
  moved through `Session.summary_from_file`. Liveness is probed, since a killed owner leaves no
253
262
  file trace: the owner lock every tick for the sessions the projection believes owned, and every
254
263
  session every 10 s, so a `kill -9` shows within a second. The summary (`Web::SessionSummary`,
255
264
  shared with `/api/sessions` and the session view) carries `owner`, `project_root` and
256
265
  `bridge_up` (the sidecar is there *and* the lock is held: the page attaches its stream on it).
257
266
  `POST/DELETE /api/sessions` and `/stop` rescan the session before answering (`SessionHub#touch`).
258
- Without a hub (`App.new` alone) `/api/events` answers `503 no_hub` and the page falls back to
259
- fetching the list.
267
+ `GET /api/sessions` is the hub's projection too (its sort, paging and total). `Server` always builds
268
+ a hub; an `App.new` without one answers both routes `503 no_hub`.
260
269
 
261
270
  ## Model loops and adapters
262
271
 
@@ -269,9 +278,15 @@ Engine picks the loop from the effective model's host (`HostRegistry#resolve`):
269
278
 
270
279
  Both return an `LLM::ModelResult` and emit the same stream events; tool calls in
271
280
  both go through `ToolRunner` (events, hooks, veto, output cap) and
272
- `KernelLoop#dispatch_tool_call`. The chat loop's `generation_chunk` carries
273
- `thinking:` (the server's `reasoning_content`), `text:` and `content:` (both);
274
- its model turns keep their `tool_calls` and tool results their `tool_call_id`,
281
+ `KernelLoop#dispatch_tool_call`. Every format's built-in calls (Gemma, Qwen,
282
+ chat `tool_calls`) are built by one table, `Tools::BuiltinCalls`, from the
283
+ parsed `{name, args}` (derived from `TOOL_SCHEMAS`), so a call looks the same to
284
+ hooks and tools whichever model made it. Both loops' `generation_chunk` carries
285
+ `thinking:`, `text:` and `content:` (both): the chat loop's thinking is the
286
+ server's `reasoning_content`; the raw-prompt loop splits each generation's
287
+ stream with a fresh `ThoughtStreamSplitter` (Qwen's `<think>` and Gemma's
288
+ `<|channel>thought … <channel|>` are thinking, tool-call bodies are dropped
289
+ from `text:`). The chat loop's model turns keep their `tool_calls` and tool results their `tool_call_id`,
275
290
  so later requests and resumed sessions pair them. It has its own system prompt
276
291
  (no raw-prompt tool declarations; the tools go as JSON schemas).
277
292
 
data/docs/cli.md CHANGED
@@ -21,7 +21,7 @@
21
21
  - `chi desktop install|upgrade|uninstall|status` — the macOS "Send to chi" helper: a Service and a ⌃⌥⌘N hotkey that send text to live sessions as context notes (see [Desktop helper](desktop.md))
22
22
  - `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 (the desktop helper's hint)
23
23
  - `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))
24
- - `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))
24
+ - `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 every other command
25
25
 
26
26
  ### First setup
27
27
 
@@ -586,9 +586,10 @@ In interactive assist mode, you can switch the request model without restarting:
586
586
 
587
587
  Notes:
588
588
 
589
- - The switch updates the request `model` field, routes to the matching host (`HostRegistry`, `lib/samagotchi/host_registry.rb:72`), and resolves the prompt profile again (config, the server's chat template, the name; see "Prompt profile" in configuration.md).
589
+ - The switch updates the request `model` field, routes to the matching host (`HostRegistry#resolve`), and resolves the prompt profile again (config, the server's chat template, the name; see "Prompt profile" in configuration.md).
590
590
  - Without `--default` the command is session-scoped and does not rewrite config files.
591
591
  - With `--default` the new default is written to `~/.config/samagotchi/config.yml` (honoring `XDG_CONFIG_HOME`) and takes effect for all new sessions; the current session's effective model is also updated immediately. Bare aliases and `host:model` are both valid.
592
+ - `--default` and `--alias` change only their one key's line in `config.yml` (`default.model`, or the alias under `model_aliases:`): comments and the rest of the file stay as written, a symlinked file is written through and keeps its mode. When the edited text wouldn't read back as the expected settings (a flow-style section, anchors), chi writes the whole file out from the parsed data instead, which drops its comments.
592
593
  - Worker sessions inherit `hosts:` via `SAMAGOTCHI_HOSTS_JSON`.
593
594
  - The idle recap uses the session's current model (a switch counts from the next recap), unless `recap: {host_ref, model}` pins one.
594
595
 
@@ -706,7 +707,7 @@ While a turn runs, one row above the prompt says what it is doing, the spinner f
706
707
  Qwen `TURN:` prefix and inline markdown are left out.
707
708
  - `| waiting for the first token… 5s` after 2 s with nothing streamed.
708
709
  - `| running execute…` while a tool runs (its `tool>` line prints when it ends).
709
- - `| retrying (1/4 in 0.5s): Errno::ECONNREFUSED` while a network error is retried.
710
+ - `| retrying (1/3 in 0.5s): Errno::ECONNREFUSED` while a network error is retried.
710
711
  - `| mcp: starting servers…` while a plugin's slow setup (an init task) runs, between turns too;
711
712
  `mcp> ✓ …` prints when it is done.
712
713
 
data/docs/guardrails.md CHANGED
@@ -126,6 +126,12 @@ All the fields a rule gives must match. Absolute and `**/` globs match the
126
126
  resolved path; other globs match the path relative to the repo root. Paths
127
127
  resolve the way the tools resolve them (against the cwd; `~` expanded).
128
128
 
129
+ `outside_repo` leaves out a memory's file: a `*.md` right in the project or
130
+ system memories folder (not `index.md`, not a hidden file). `write`/`edit`
131
+ there is what `memory_write` does unasked, and the model is told to `edit` a
132
+ memory for a small change. The memories folder's `index.md`, `.bundles/`, and
133
+ the rest of chi's config folder still count as outside.
134
+
129
135
  To switch off single rules (a bundle's, say) without editing its files, list
130
136
  them under `disable:`. A plain id switches off every rule with that id; `bundle:id`
131
137
  only that bundle's:
@@ -238,8 +244,9 @@ bundles:
238
244
  max_distance: 2 # default: 1 under 10 characters, else 2
239
245
  ```
240
246
 
241
- `mode: correct` rewrites the call (whole tokens, everywhere they appear) and
242
- says so; `mode: ask` shows the call with three choices, *Correct it and
247
+ `mode: correct` rewrites the call (whole tokens, everywhere they appear in
248
+ the command, the `cwd` and the paths; never in file text: a write's content
249
+ or an edit's `old_text`/`new_text`) and says so; `mode: ask` shows the call with three choices, *Correct it and
243
250
  run*, *Run as is*, *Deny*; with no one to ask (`--non-interactive`) or a
244
251
  dismissed question it rejects. A real near name (a folder `jondoe` next to
245
252
  user `johndoe`, a login one letter from another) is caught too: list it under
data/docs/hooks.md CHANGED
@@ -140,10 +140,10 @@ whole response), and none after the turn or the generation was cancelled.
140
140
  A 240k-char thinking gives about 120 fires.
141
141
 
142
142
  `thinking` holds what the model streamed as thinking: `reasoning_content`
143
- on the chat path, a Qwen `<think>` block on the raw-prompt path. Some
144
- thinking arrives as `text` instead: Gemma 4's on the raw-prompt path (it has
145
- no close marker), and a chat provider's that puts `<think>` inside the
146
- answer's content. Tool-call bodies are left out of `text`. With streaming off
143
+ on the chat path, a Qwen `<think>` block or a Gemma 4
144
+ `<|channel>thought … <channel|>` block on the raw-prompt path. Some
145
+ thinking arrives as `text` instead: a chat provider's that puts `<think>`
146
+ inside the answer's content. Tool-call bodies are left out of `text`. With streaming off
147
147
  (`stream: false` on a chat host) there are no chunks, so no fires.
148
148
 
149
149
  **Keep it fast.** The hook runs on the turn's thread, inside the HTTP read:
@@ -368,6 +368,43 @@ end
368
368
 
369
369
  Note: `:before_tool_call` can replace the `:call` hash to change what runs; `tool_call_started` (what the UIs show) and the rules see the final call.
370
370
 
371
+ ### The call a hook sees
372
+
373
+ `event[:call]` is the same hash for every model format (Gemma, Qwen, the
374
+ chat path): `name:`, `content:`, `path:`, `scope:`, plus the tool's other
375
+ parameters by their own names (symbol keys). `content:` holds the tool's
376
+ main argument (`""` when it has none), `path:` the parameter named in its
377
+ column, and an argument the model left out is `nil`. Text values are
378
+ stripped of surrounding whitespace, except file text, an edit's
379
+ `old_text`/`new_text`, `env` and `options`.
380
+
381
+ | Tool | `content:` from | `path:` from | Own fields |
382
+ |------|-----------------|--------------|------------|
383
+ | `execute` | `command` | — | `cwd` |
384
+ | `read` | `path` | — | `start_line`, `end_line` |
385
+ | `write` | — | `path` | `content` |
386
+ | `edit` | — | `path` | `old_text`, `new_text`, `start_line`, `end_line` |
387
+ | `memory_read` | `name` | — | `scope` |
388
+ | `memory_write` | — | `name` | `content`, `scope`, `description`, `current_model_only` |
389
+ | `task_create` | `command` | — | `cwd`, `env` |
390
+ | `task_get` | `id` or `task_id` | — | — |
391
+ | `task_list` | — | — | — |
392
+ | `task_stop` | `id` or `task_id` | — | — |
393
+ | `task_wait` | `id` or `task_id` | — | `timeout`, `tail_lines`, `done_pattern` |
394
+ | `web_fetch` | `url` | — | — |
395
+ | `register_reminder` | `name` | — | `description`, `interval_minutes` |
396
+ | `cancel_reminder` | `name` | — | — |
397
+ | `list_reminders` | — | — | — |
398
+ | `list_sessions` | — | — | `cwd` |
399
+ | `send_note` | `text` | — | `session` |
400
+ | `delegate` | `task` | — | `model`, `session`, `wait`, `timeout` |
401
+ | `delegate_result` | — | — | `session`, `timeout` |
402
+ | `ask_user_question` | `question` | — | `question`, `options`, `header`, `multi_select`, `allow_freeform` |
403
+
404
+ A `write`'s `content:` is the file text; an `edit` carries `old_text:` and
405
+ `new_text:` (its `content:` is `""`). A plugin or MCP tool's call has its
406
+ arguments whole on `args:` (string keys) instead.
407
+
371
408
  ## Bundle Hooks (unified workflow bundle)
372
409
 
373
410
  Bundles can ship executable guardrails alongside memories. A bundle with hooks lives as a directory with a `hooks/` subdirectory (flat, basename-keyed):
@@ -393,7 +430,7 @@ hooks:
393
430
  guardrails.rb:
394
431
  sha256: 1234...
395
432
  event: before_tool_call
396
- on_error: fail_closed # default for before_tool_call
433
+ on_error: fail_closed # skip (default) | log | fail_closed (before_tool_call only)
397
434
  priority: 10
398
435
  audit.rb:
399
436
  sha256: 5678...
@@ -410,12 +447,12 @@ Notes:
410
447
  - `event` is required for auto-registration; a hook with no event is skipped.
411
448
  - `sha256` is integrity (not authenticity). No signing in v1. Install records the sha256 of the copied file; at `Engine.new` a hook whose file differs is not loaded (reinstall the bundle after editing one by hand).
412
449
  - Hook code is the bundle author's source of truth: on upgrade, hooks are overwritten; if the installed file was locally modified, a warning is emitted (`was locally modified; overwriting`).
413
- - A raising `:before_tool_call` guardrail respects `on_error`: `fail_closed` denies the call (fail-closed), `log` warns, `skip` is silent.
450
+ - `on_error` defaults to `skip` for every event, `:before_tool_call` included: a raising hook is silent and the call goes ahead. `log` warns. `fail_closed` (only on `:before_tool_call`; on any other event it acts as `skip`) denies the call the hook raised on. Choose `fail_closed` for a hook that is a guardrail, whose failure should stop tool calls rather than let them through.
414
451
  - A `fail_closed` `:before_tool_call` hook is required: if it is missing, fails to load or its sha256 differs, chi denies every tool call until it is fixed.
415
452
  - A bundle can also ship YAML rules in `guardrails/*.yml`; see [Guardrails](guardrails.md#the-guardrails-bundle).
416
453
  - Ordering: bundle hooks fire by `(priority, bundle_name, hook_name)` (lower priority first), then plain `config.yml` hooks in registration order.
417
454
  - Settings: a hook class with `initialize(settings = {})` gets the bundle's section of `config.yml` `bundles:` (see [Settings](#settings)).
418
- - A bundle can also ship a `plugin.rb` whose `chi.on(event)` blocks are bundle hooks too, next to commands and tools; see [Plugins](plugins.md).
455
+ - A bundle can also ship a `plugin.rb` whose `chi.on(event)` blocks are bundle hooks too, next to commands and tools; see [Plugins](plugins.md). There `ctx.notify`, `ctx.steer` and the other `ctx` helpers act as this page's `event[:notify]`… do for the event the block runs for.
419
456
  - Shipped bundles (each in a profile: `core` or `dev`, see [Bundle profiles](memory.md#bundle-profiles-core-and-dev)): `chi bundle install guardrails` (rules, see [Guardrails](guardrails.md#the-guardrails-bundle)) and `chi bundle install known-names` (a hook, see [Guardrails](guardrails.md#the-known-names-bundle)), `chi bundle install source-links` (a hook: announces source refs, see [The source-links bundle](#the-source-links-bundle)), `chi bundle install btw` (a plugin: `/btw`, see [Plugins](plugins.md#the-btw-bundle)), `chi bundle install mcp` (a plugin: tools from MCP servers, see [Plugins](plugins.md#the-mcp-bundle)) `chi bundle install loop-guard` (a plugin: breaks tool-call loops, see [Plugins](plugins.md#the-loop-guard-bundle)), `chi bundle install check-in` (a plugin: checks on a long turn, see [Plugins](plugins.md#the-check-in-bundle)) and `chi bundle install skills` (a plugin: `/skill`, versions of skills, see [Plugins](plugins.md#the-skills-bundle)).
420
457
  - Installing a bundle executes its hook code at `Engine` startup. Only install bundles you trust, as you would a gem. Hooks are **not** executed at install time (copy-only); they are `module_eval`'d at `Engine.new` inside per-bundle `Samagotchi::Bundles::<name>` namespaces (no top-level `require` collisions). Keep hook files side-effect-free at load time; do work in `#call` — top-level side effects (require, IO, `at_exit`, global assignment) run once per `Engine.new` (class redefinition is idempotent).
421
458
 
@@ -19,5 +19,6 @@ Thought handling follows the Gemma guidance:
19
19
  - Standard multi-turn: prior model thoughts are stripped from conversation history before the next turn.
20
20
  - Function/tool-calling exception: during a single turn that includes tool calls, thoughts are not stripped between those tool-call rounds.
21
21
  - Final model output returned to the caller is thought-stripped.
22
+ - While a generation streams, the thought channel goes to the stream's `thinking:` lane, not `text:` (`ThoughtStreamSplitter.for_profile`, fresh per generation), so the UIs show it as thinking and `:generation_progress` hooks count it in `thinking_chars`.
22
23
 
23
24
  In short, raw thought blocks are treated as in-turn transient context, not durable history.
data/docs/memory.md CHANGED
@@ -12,7 +12,8 @@ Tool behavior:
12
12
  - `memory_read`: `scope` is optional.
13
13
  - If `scope` is provided (`project` or `system`), only that scope is read.
14
14
  - If `scope` is omitted, read falls back from project to system.
15
- - `memory_write`: `scope` is required (`project` or `system`). The entry name is passed via the `name` parameter (not `path` — the file tools use `path`). On success, the return value includes the full file path, so you can use the `edit` tool directly for targeted updates.
15
+ - `memory_write`: `scope` is required (`project` or `system`). The entry name is passed via the `name` parameter (not `path` — the file tools use `path`). On success, the return value includes the full file path. For a small change to an existing memory (a step, a line) the model is told to `edit` that file rather than rewrite it all with `memory_write`.
16
+ - `write`/`edit` that change a `*.md` right in a scope dir refresh its managed `index.md` line (date, bytes; the description is kept), as `memory_write` does. A memory created with `write` gets a line with no description; any other `.md` written there gets a line too. Model overlays (`<name>.<key>.md` next to `<name>.md`), files in another project's folder and files changed by `execute` are left out.
16
17
 
17
18
  ## Model-Specific Memory Overlays
18
19
 
@@ -51,8 +52,9 @@ chi is told about skills by the system bundle (`identity.md`, every turn, and
51
52
  looks unexpected on the way (a failing check, a warning the skill doesn't
52
53
  mention), chi stops, says what it saw and asks how to go on.
53
54
  - **Updating.** When a step turned out different (a renamed script, an extra
54
- step), chi fixes the skill in the same turn: those steps changed, the rest
55
- kept, a dated Changelog line added. No confirmation.
55
+ step), chi fixes the skill in the same turn: those steps changed (with `edit`
56
+ on its file, so a small fix doesn't resend the whole skill), the rest kept, a
57
+ dated Changelog line added. No confirmation.
56
58
 
57
59
  A skill is plain Markdown, no frontmatter:
58
60
 
data/docs/plugins.md CHANGED
@@ -304,18 +304,33 @@ end
304
304
  ### `chi.on(event, priority: 100) { |event, ctx| … }`
305
305
 
306
306
  This is a bundle hook, the same as a `hooks/*.rb` file. See
307
- [hooks.md](hooks.md#hook-events) for the events and for what `event[:notify]`,
308
- `event[:ask_user]`, `event[:stop_turn]` and `event[:steer]` do; on `:after_turn`,
307
+ [hooks.md](hooks.md#hook-events) for the events; on `:after_turn`,
309
308
  `event[:present]` sets how the answer is shown in the web
310
309
  ([Presenting the answer](hooks.md#presenting-the-answer-display-only)). Its label is
311
310
  `plugin.rb (bundle my-bundle)`. The block may take only the event. If it
312
311
  raises, the error is logged and the hook is skipped.
313
312
 
313
+ To act from the block, use `ctx`: `ctx.notify`, `ctx.ask_user`,
314
+ `ctx.steer`, `ctx.stop_turn`, `ctx.stop_generation`. Inside the block they
315
+ act for **this event** (the `event[:notify]`… helpers that plain hooks in
316
+ [hooks.md](hooks.md) get are the same thing, and a plugin doesn't need them):
317
+
318
+ - `ctx.stop_turn` in a `:before_tool_call` block also denies the call the
319
+ event is about.
320
+ - From `:after_turn` and `:session_end` there is no turn left:
321
+ `ctx.steer`, `ctx.stop_turn` and `ctx.stop_generation` do nothing and
322
+ return false.
323
+ - In a `:generation_progress` block `ctx.ask_user` asks no one (nil).
324
+
325
+ This holds on the block's own thread while it runs. A thread the block
326
+ starts doesn't inherit the event: there the helpers act as from a command,
327
+ on the session as it is when called ([The context](#the-context-ctx)), so
328
+ `ctx.steer` from it reaches whatever turn is running then.
329
+
314
330
  #### Watching the stream
315
331
 
316
332
  `chi.on(:generation_progress)` sees a response while it streams, in batches
317
- (2000 chars or a second), and `event[:stop_generation]` (or
318
- `ctx.stop_generation`) cuts it while the turn goes on: the model is asked
333
+ (2000 chars or a second), and `ctx.stop_generation` cuts it while the turn goes on: the model is asked
319
334
  again. The rules (the thread it runs on, keeping it fast, what a cut does)
320
335
  are in [hooks.md](hooks.md#watching-the-stream).
321
336
 
@@ -328,7 +343,7 @@ class Plugin
328
343
  next if event[:thinking_chars] < LIMIT
329
344
 
330
345
  # Once per generation: a cut generation fires no more.
331
- if event[:stop_generation].call("it thought for over #{LIMIT / 1000}k chars")
346
+ if ctx.stop_generation("it thought for over #{LIMIT / 1000}k chars")
332
347
  ctx.notify("thinking too long (#{event[:elapsed_ms] / 1000} s): cut", level: :warn)
333
348
  end
334
349
  end
@@ -449,7 +464,14 @@ name order, so the first bundle keeps the name.
449
464
  ## The context: `ctx`
450
465
 
451
466
  Every handler gets the plugin's context. There is one per plugin for the
452
- session's life, and each read gives the session as it is now.
467
+ session's life, and each read gives the session as it is now. It is a
468
+ plugin's one way to talk to the user and to the turn: the helpers below act
469
+ on the session now from a command, a tool, an init task or your own thread,
470
+ and for the event inside a `chi.on` block (see `chi.on` above).
471
+
472
+ Everything a plugin shows (notices, nudges, stop notices, questions) is
473
+ named by its bundle in every UI (`my-bundle> …`); the debug log keeps the
474
+ full label, `plugin.rb (bundle my-bundle)`.
453
475
 
454
476
  | | |
455
477
  |---|---|
@@ -461,13 +483,13 @@ session's life, and each read gives the session as it is now.
461
483
  | `ctx.log` | `ctx.log.info(:event, key: value)`: debug-log records tagged `plugins`, with `bundle=<bundle>` |
462
484
  | `ctx.messages` | the conversation, as a frozen copy, without the system prompt. While a turn runs, a session worker's (attached, web) adds that turn so far: its prompt, the model's text and the lines merged into it (no tool calls or thinking); the REPL's is the conversation before that turn |
463
485
  | `ctx.messages_partial?` | whether `ctx.messages` leaves out a running turn (the REPL mid-turn), so a plugin can say what its answer is about |
464
- | `ctx.notify(text, level: :info)` | one line to the user, like a hook's `event[:notify]`, labelled by the bundle (`my-bundle> …`). Every UI shows it, during a turn (a tool, a hook) or between turns (a command) |
486
+ | `ctx.notify(text, level: :info)` | one line to the user, labelled by the bundle (`my-bundle> …`). Every UI shows it, during a turn (a tool, a hook) or between turns (a command) |
465
487
  | `ctx.card(title:, body: "", actions: [], level: :info, id: nil)` | a card in every UI, returning its id: see [Cards](#cards) |
466
- | `ctx.ask_user(question:, options:, header: nil, allow_freeform: false)` | a question, like a hook's `event[:ask_user]` |
488
+ | `ctx.ask_user(question:, options:, header: nil, allow_freeform: false)` | a single-select question through the question flow: `{selected:, freeform:, selected_indices:}`, or nil (no one to ask, cancelled, bad options) |
467
489
  | `ctx.cancelled?` | whether the running turn was cancelled (a long tool should stop) |
468
- | `ctx.steer(text)` | put text into the running turn, like a hook's `event[:steer]`: its own user message at the loop's next boundary, shown as `my-bundle> nudged: …`. Returns true when queued, false with no turn running (it never starts one; that is `ctx.sessions`' send). Dropped (logged) if the model answers or the turn ends first. Safe from any thread: an anytime command, a hook, your own |
469
- | `ctx.stop_turn(reason)` | stop the running turn after a warn notice with the reason, like a hook's `event[:stop_turn]`; true when it stopped one now |
470
- | `ctx.stop_generation(reason)` | cut the generation that is streaming, like a hook's `event[:stop_generation]`: the turn goes on and the model is asked again ([hooks.md](hooks.md#watching-the-stream)). Shows nothing: post your own notice. True when it cut one now |
490
+ | `ctx.steer(text)` | put text into the running turn: its own user message at the loop's next boundary, shown as `my-bundle> nudged: …`. Returns true when queued, false with no turn running (it never starts one; that is `ctx.sessions`' send). Dropped (logged) if the model answers or the turn ends first. Safe from any thread: an anytime command, a hook (false from `:after_turn`), your own |
491
+ | `ctx.stop_turn(reason)` | stop the running turn after a warn notice with the reason; true when it stopped one now. In a `:before_tool_call` block it also denies that call |
492
+ | `ctx.stop_generation(reason)` | cut the generation that is streaming: the turn goes on and the model is asked again ([hooks.md](hooks.md#watching-the-stream)). Shows nothing: post your own notice. True when it cut one now |
471
493
  | `ctx.ask_model(messages:, prompt:, …)` | a side answer from the session's model: see [Side answers](#side-answers-ctxask_model) |
472
494
  | `ctx.sessions` | fork, send to and read other sessions: see [Other sessions](#other-sessions-ctxsessions) |
473
495
 
@@ -915,7 +937,7 @@ with a line and carry on unchanged.
915
937
 
916
938
  `chi bundle install skills` (or the `dev` profile) installs the bundle shipped with chi
917
939
  (`lib/samagotchi/bundles/skills/plugin.rb`): one anytime command, `chi.on`
918
- hooks, `ctx.sessions.send`, `ctx.notify` and `event[:steer]`. It has no memory
940
+ hooks, `ctx.sessions.send`, `ctx.notify` and `ctx.steer`. It has no memory
919
941
  file; skills themselves work without it ([docs/memory.md](memory.md#skills)).
920
942
 
921
943
  | | |
@@ -940,16 +962,24 @@ skills> skill release updated (+2 −1): 1. Run `scripts/verify.sh`; stop if it
940
962
  The line is the file on disk changing, whatever the tool answered; a denied
941
963
  write shows nothing.
942
964
 
965
+ A skill read in the turn is also watched for changes made another way (an
966
+ `execute` running `sed`, a script): after each other tool call, and at the
967
+ turn's end, its file is compared with the content last seen (at the read, or
968
+ after the last write). A change shows the same line, keeps the content last
969
+ seen as a version (so `/skill diff` has it) and counts as the skill updated.
970
+ A plain read keeps no version.
971
+
943
972
  **The nudge** (`nudge: true`). Some models, finding a skill's step broken,
944
973
  skip it and go on without fixing the skill. In a turn that read a skill
945
974
  (`memory_read` of a `skill_*` name, or `read` of its file), the first failing
946
975
  tool call after it (an `execute` that exited non-zero, a tool error) steers
947
976
  the model once: *"A step of skill release failed. Find out why before skipping
948
- it; if the skill is out of date, fix it now: memory_write the whole skill,
949
- its title and every section as they were, that step fixed, a Changelog line
950
- added."* If the turn ends with a failed step and the
951
- skill not rewritten, one line says so: `skill release was followed, a step
952
- failed, the skill wasn't updated`. A failure unrelated to the skill (a test
977
+ it; if the skill is out of date, fix it now: edit the step that changed in
978
+ its file (or memory_write the whole skill) and add a Changelog line."* No
979
+ steer once a skill read this turn has changed, by any tool. If the turn ends
980
+ with a failed step and no skill read changed, one line says so: `skill
981
+ release was followed, a step failed, the skill wasn't updated`. A failure
982
+ unrelated to the skill (a test
953
983
  meant to fail) can set it off too: once per turn, and only after a skill was
954
984
  read.
955
985
 
data/docs/sessions.md CHANGED
@@ -19,7 +19,7 @@ A session is deleted if **expired by age OR overflow by count** (unless `keep_st
19
19
 
20
20
  **Attached by default:** plain `chi` and `chi --resume ID` run their session in a background worker and attach to it (`session.shared`, default `true`; env `SAMAGOTCHI_SESSION_SHARED`). The worker runs in the session's `working_directory`, so `!commands` and tools see the directory the session was started in. `session.shared: false` keeps the in-process REPL, `--no-shared` does for one run; a session the REPL has open can't be attached ("close it there first"). See [CLI: Sharing a session](cli.md#sharing-a-session).
21
21
 
22
- **Stopping a worker:** `chi sessions stop ID` marks the session stopped, sends its worker TERM and waits (up to 10 s) until the worker has let go of `owner.lock`, so a `chi --resume ID` right after spawns a fresh worker. That is also how to restart a worker still running an older chi, which the attached terminal and the web report when a command or a question dismiss gets a 404 (`BridgeClient.stale_worker_message`). The web's `POST /api/sessions/:id/stop` waits the same way, for 2 s.
22
+ **Stopping a worker:** `chi sessions stop ID` marks the session stopped (an empty `<id>/stopped` file, not a session.json field, which a worker saving at that moment would write over; a resume removes it), sends its worker TERM and waits (up to 10 s) until the worker has let go of `owner.lock`, so a `chi --resume ID` right after spawns a fresh worker. That is also how to restart a worker still running an older chi, which the attached terminal reports when a command or a question dismiss gets a 404, and the web when an answer, a dismiss, a command or a cancel does (`501 not_supported`; `BridgeClient.stale_worker_message`). The web's `POST /api/sessions/:id/stop` waits the same way, for 2 s.
23
23
 
24
24
  **Deleting a session:** `chi sessions delete ID...`, `/exit --delete` in a terminal and the Web UI's delete all go through `SessionManager.delete_session`: it resolves a unique prefix, removes `<id>.json` and the whole `<id>/` dir, and returns what it removed. A session a plain REPL has open (`owner.lock` kind `tui`) is always refused. One a worker runs is refused unless the caller asks to stop it: then it stops the worker as `chi sessions stop` does and deletes once `owner.lock` is free (`--force` on the CLI, 10 s; the web always, 2 s; `/exit --delete` after the worker agreed to exit, 10 s). A worker that outlives the wait leaves the session in place. The web's route is `DELETE /api/sessions/:id` (200 `{status: "deleted", session_id, stopped}`; 409 `owned_by_tui` or `still_stopping`; 404).
25
25
 
@@ -6,6 +6,8 @@ require_relative "model_profile"
6
6
  require_relative "bootstrap/probe"
7
7
  require_relative "bootstrap/config_writer"
8
8
  require_relative "bootstrap/bundles"
9
+ require_relative "cli/command"
10
+ require_relative "cli/flags"
9
11
 
10
12
  module Samagotchi
11
13
  # `chi bootstrap [TARGET]`: the first setup. It names a model server, works
@@ -15,6 +17,8 @@ module Samagotchi
15
17
  # system bundle and the core profile, and on a terminal asks about dev.
16
18
  # Prompts only on a terminal; a script gets the list and exit 2 instead.
17
19
  class BootstrapCommand
20
+ include CLI::Command
21
+
18
22
  USAGE = <<~TEXT
19
23
  Usage: chi bootstrap [TARGET] [--name NAME] [--model ID] [--key-env VAR] [--no-test] [--dry-run]
20
24
  Finds the model server at TARGET and writes config.yml for it.
@@ -37,6 +41,14 @@ module Samagotchi
37
41
  check-in, guardrails), and on a terminal offers the dev bundles.
38
42
  TEXT
39
43
 
44
+ FLAGS = CLI::Flags.new(help: CLI::Command::HELP_WORDS) do |f|
45
+ f.value "--name"
46
+ f.value "--model"
47
+ f.value "--key-env"
48
+ f.switch "--no-test"
49
+ f.switch "--dry-run"
50
+ end
51
+
40
52
  PICK_SHOWN = 20
41
53
  # A test request slower than this says it is waiting (a model loading).
42
54
  LOADING_AFTER = 3
@@ -66,8 +78,8 @@ module Samagotchi
66
78
  # @return [Integer] 0 written (or already there), 1 failed (a failed
67
79
  # test still writes the config), 2 usage or a choice to make
68
80
  def run
69
- options = parse or return 2
70
- return 0 if options[:help]
81
+ options = parse
82
+ return options if options.is_a?(Integer)
71
83
 
72
84
  key_env = options[:key_env]
73
85
  return fail!("#{key_env} is not set; export it first (the API key; chi only writes its name)") if key_env && unset?(key_env)
@@ -85,26 +97,21 @@ module Samagotchi
85
97
 
86
98
  private
87
99
 
100
+ def command_name = "chi bootstrap"
101
+ # A usage error's line ends with "(see chi bootstrap --help)".
102
+ def usage_on_error = nil
103
+
104
+ # @return [Hash, Integer] the options, or the exit status after the
105
+ # help or a usage error
88
106
  def parse
89
- options = {}
90
- until @argv.empty?
91
- arg = @argv.shift
92
- case arg
93
- when "-h", "--help", "help"
94
- @stdout.puts(USAGE)
95
- return { help: true }
96
- when "--name", "--model", "--key-env"
97
- options[arg.delete_prefix("--").tr("-", "_").to_sym] = @argv.shift or return usage_error("#{arg} needs a value")
98
- when /\A--(name|model|key-env)=(.+)\z/ then options[Regexp.last_match(1).tr("-", "_").to_sym] = Regexp.last_match(2)
99
- when "--no-test" then options[:no_test] = true
100
- when "--dry-run" then options[:dry_run] = true
101
- when /\A-/ then return usage_error("unknown option #{arg}")
102
- else
103
- return usage_error("one TARGET only (got #{options[:target]} and #{arg})") if options[:target]
107
+ parsed = parse_flags(FLAGS, @argv)
108
+ return parsed if parsed.is_a?(Integer)
104
109
 
105
- options[:target] = arg
106
- end
107
- end
110
+ options = parsed.options
111
+ target, extra = parsed.args
112
+ return usage_error("one TARGET only (got #{target} and #{extra})") if extra
113
+
114
+ options[:target] = target if target
108
115
  if options[:key_env] && !options[:key_env].match?(ConfigFile::ENV_NAME_RE)
109
116
  return usage_error("--key-env takes the variable's name (e.g. OPENROUTER_API_KEY), not the key")
110
117
  end
@@ -119,8 +126,7 @@ module Samagotchi
119
126
  candidates = begin
120
127
  Bootstrap::Probe.candidates(options[:target])
121
128
  rescue ArgumentError => e
122
- usage_error(e.message)
123
- @exit = 2
129
+ @exit = usage_error(e.message)
124
130
  return nil
125
131
  end
126
132
  result = @probe.classify_target(candidates, key_env: key_env)
@@ -251,8 +257,7 @@ module Samagotchi
251
257
  name = begin
252
258
  writer.host_name(Bootstrap::ConfigWriter.derived_name(result.candidate.host), requested: options[:name], model_ids: ids)
253
259
  rescue Bootstrap::ConfigWriter::Error => e
254
- usage_error(e.message)
255
- @exit = 2
260
+ @exit = usage_error(e.message)
256
261
  return nil
257
262
  end
258
263
  outcome = writer.write(name: name, fields: fields, model: model, dry_run: options[:dry_run])
@@ -408,10 +413,5 @@ module Samagotchi
408
413
  @exit = 1
409
414
  nil
410
415
  end
411
-
412
- def usage_error(message)
413
- @stderr.puts("chi bootstrap: #{message} (see chi bootstrap --help)")
414
- nil
415
- end
416
416
  end
417
417
  end