samagotchi 0.90.0 → 0.92.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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +79 -1
  3. data/README.md +5 -2
  4. data/bin/chi +4 -4
  5. data/docs/architecture.md +17 -13
  6. data/docs/bundle-authoring.md +6 -11
  7. data/docs/cli.md +7 -4
  8. data/docs/configuration.md +8 -4
  9. data/docs/context.md +5 -3
  10. data/docs/hooks.md +3 -3
  11. data/docs/internals/background-tasks.md +27 -2
  12. data/docs/internals/bundles.md +4 -4
  13. data/docs/internals/engine.md +21 -8
  14. data/docs/internals/prompt-caching.md +4 -0
  15. data/docs/internals/tool-guardrails.md +10 -2
  16. data/docs/memory.md +6 -0
  17. data/docs/plugins.md +30 -13
  18. data/docs/releasing.md +3 -0
  19. data/docs/sessions.md +4 -4
  20. data/lib/samagotchi/bridge.rb +11 -7
  21. data/lib/samagotchi/bundle_command/check.rb +43 -12
  22. data/lib/samagotchi/bundle_command/scaffold.rb +12 -0
  23. data/lib/samagotchi/bundle_command/test_run.rb +12 -0
  24. data/lib/samagotchi/bundle_command/try/command.rb +164 -0
  25. data/lib/samagotchi/bundle_command/try/render.rb +80 -0
  26. data/lib/samagotchi/bundle_command/try/run.rb +35 -17
  27. data/lib/samagotchi/bundle_command/try/sandbox.rb +113 -34
  28. data/lib/samagotchi/bundle_command/try/trace.rb +26 -15
  29. data/lib/samagotchi/bundle_command.rb +11 -213
  30. data/lib/samagotchi/bundle_session_settings.rb +82 -45
  31. data/lib/samagotchi/bundles/core/manifest.yml +2 -2
  32. data/lib/samagotchi/bundles/cost-cap/manifest.yml +6 -5
  33. data/lib/samagotchi/bundles/cost-cap/plugin.rb +57 -12
  34. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  35. data/lib/samagotchi/bundles/system/self_map.md +5 -0
  36. data/lib/samagotchi/child_env.rb +50 -0
  37. data/lib/samagotchi/client_id.rb +3 -0
  38. data/lib/samagotchi/config.rb +3 -0
  39. data/lib/samagotchi/context_note.rb +13 -6
  40. data/lib/samagotchi/engine.rb +31 -15
  41. data/lib/samagotchi/hooks/registry.rb +2 -0
  42. data/lib/samagotchi/kernel_loop.rb +14 -3
  43. data/lib/samagotchi/last_turn.rb +3 -2
  44. data/lib/samagotchi/llm/chat_loop.rb +3 -1
  45. data/lib/samagotchi/llm/errors.rb +4 -4
  46. data/lib/samagotchi/memory_bundle/builder.rb +3 -2
  47. data/lib/samagotchi/memory_bundle/installed_bundle.rb +7 -2
  48. data/lib/samagotchi/memory_bundle/installer.rb +1 -0
  49. data/lib/samagotchi/memory_bundle/manifest.rb +42 -8
  50. data/lib/samagotchi/memory_bundle/provenance.rb +2 -1
  51. data/lib/samagotchi/parent_report.rb +12 -0
  52. data/lib/samagotchi/reply_wait.rb +6 -2
  53. data/lib/samagotchi/sandbox_layout.rb +66 -0
  54. data/lib/samagotchi/send_command.rb +3 -3
  55. data/lib/samagotchi/session_manager.rb +12 -20
  56. data/lib/samagotchi/task_finish_note.rb +90 -0
  57. data/lib/samagotchi/task_watch.rb +228 -0
  58. data/lib/samagotchi/terminal_ui/attached_loop.rb +19 -4
  59. data/lib/samagotchi/terminal_ui/formatting.rb +7 -2
  60. data/lib/samagotchi/testing/bundle.rb +32 -19
  61. data/lib/samagotchi/testing/install.rb +26 -17
  62. data/lib/samagotchi/testing/sandbox.rb +45 -37
  63. data/lib/samagotchi/testing/waiting.rb +5 -7
  64. data/lib/samagotchi/tool_declarations.rb +16 -0
  65. data/lib/samagotchi/tool_response.rb +28 -6
  66. data/lib/samagotchi/tools/builtins.rb +5 -1
  67. data/lib/samagotchi/tools/task_record.rb +9 -0
  68. data/lib/samagotchi/tools/task_runtime.rb +2 -2
  69. data/lib/samagotchi/tools/task_wait.rb +10 -3
  70. data/lib/samagotchi/turn_checkpoint.rb +23 -10
  71. data/lib/samagotchi/version.rb +1 -1
  72. data/lib/samagotchi/web/public/app.js +24 -14
  73. data/lib/samagotchi/web/public/cards_place.js +4 -1
  74. data/lib/samagotchi/web/public/images.js +6 -0
  75. data/lib/samagotchi/web/public/model_pick.js +10 -0
  76. data/lib/samagotchi/web/public/notify.js +4 -4
  77. data/lib/samagotchi/web/public/toasts.js +17 -9
  78. data/lib/samagotchi/web/public/turn_events.js +13 -1
  79. data/lib/samagotchi/web/public/unqueue.js +20 -4
  80. data/lib/samagotchi/worker.rb +104 -42
  81. data/lib/samagotchi/worker_idle_exit.rb +26 -3
  82. data/lib/samagotchi/worker_inbound.rb +18 -4
  83. data/lib/samagotchi/worker_task_wakes.rb +160 -0
  84. data/lib/samagotchi/worker_wakes.rb +11 -4
  85. metadata +8 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 06240ded5e15bc55c7c3467bc7d5c0e79daecd5822291feebf72bbab6e6e6cb7
4
- data.tar.gz: 52adfdbb519ba9bed3575d55abe737dbca654d81c3c213706a556b56742628af
3
+ metadata.gz: c6797c1728cd7acf0fc6d82e0aa5a1e6f6b65d6af6b5c69869f34c722d2176b0
4
+ data.tar.gz: a140559d9438ab5f16a6096b1f949280d67bfc062026c334544b9b312f9b9656
5
5
  SHA512:
6
- metadata.gz: 33fdd1b34ce5e0840330451bb90675fe6fb9de6903d3569f3d8e6559e09cad004bcd40bfeec0e0468943d0b2d2abfce8a21d097152c86a53bd2cd332e0386437
7
- data.tar.gz: 5b19168272d7de67413c480c59bbee363b14a0ba106cdc146632cfb03be45cc646367454758fcbe95aba7562926ab700620e839b2526e818875312f5453d413d
6
+ metadata.gz: 28133e65ee4aed6e07f685a2b65045c0c157306e406eb02836c8e9549c80d9d99391edc087d6eb711e0f522c21be3d0c83276804a73d0f4f03e6d91c379b569c
7
+ data.tar.gz: fdc100fd5b8f528b3f2fbce1e4ca25c1817a811c1fb0d32369e7c0ceb36e426c546f0819e41fefea3b7ddfdebfe7e9de0d9209faefe2177421f41b4acb30c701
data/CHANGELOG.md CHANGED
@@ -8,6 +8,82 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.92.0] - 2026-10-19
12
+
13
+ ### Added
14
+
15
+ - A bundle's manifest can type its session settings (`session_settings: {cap: number}`: `number`, `boolean`,
16
+ `string` or `any`); `--set` and `POST /api/sessions` refuse a value of another type (exit 2 / 400), and a
17
+ `string` key keeps its text as given.
18
+
19
+ ### Changed
20
+
21
+ - `--set BUNDLE.KEY=VALUE` (and `bundle_settings` strings in `POST /api/sessions`) type values as config.yml does:
22
+ for a bundle that lists its keys plainly (or types them `any`), `yes`/`on` are now `true`, `no` is now `false`
23
+ (any case: `No`, `ON`), and `.5`, `5.`, `1e3` are now numbers; before they reached the plugin as text. A bundle
24
+ that wants the text types the key `string`.
25
+ - Test kit: `Samagotchi::Testing::Install.run(...).registered` is an `Install::Registered` value
26
+ (`registered.commands`, `.tools`, `.hooks`, `.inits`, `.toolkits`, `.tool_sources`, `.peer_sources`), its commands
27
+ and hooks `Install::Command` / `Install::Hook` values, not a Hash of arrays: a bundle spec reading
28
+ `registered[:commands]` needs `registered.commands`.
29
+ - Test kit: `Install::Result#ok?` is gone; `result.failures.empty?` says the same.
30
+
31
+ ### Fixed
32
+
33
+ - A Ctrl-C during a turn keeps what the turn did, as a Stop does: its finished tool calls and their results stay in
34
+ the session with the cancel note (a call it cut short reads as interrupted), so the next turn knows what already
35
+ ran and the note lists the background tasks the turn started. `chi bundle try --timeout` now shows that turn's
36
+ tool calls with their arguments.
37
+ - `chi --help` lists `chi sessions restart`, `chi bundle trash` and the bundle authoring subcommands (`new`, `test`,
38
+ `check`, `try`).
39
+ - Web: removing a queued message (its ×) and opening another session before the worker answered no longer puts the
40
+ removed text into that session's composer; a toast names the session it came from and stays until
41
+ dismissed, with Copy for the full text.
42
+ - Web: images added to the composer while a message with images was still uploading or sending stay after it is sent,
43
+ instead of vanishing with the sent ones.
44
+ - Web: when the model list can't be read, the start page says so in a toast (a new session starts on the default
45
+ model) instead of just leaving out the model picker.
46
+ - `--set cost-cap.cap=.5` and `=1e1` set a cap (they were read as text and turned the cap off); `--set
47
+ cost-cap.cap=five` is refused (exit 2) instead of silently running without a cap. A `cap:`, `child:` or `default:`
48
+ in config.yml that isn't a number (or is infinite, `1e400`) now warns once, and a bad `cap:`/`child:` falls back to
49
+ the next one. Run `chi update` (cost-cap 0.2.1).
50
+ - An attached-context wake turn stopped before the model call keeps its note as a plain one and pauses wakes until
51
+ you write, as delegate and task wake turns do; it left the note marked as a turn's start.
52
+ - A background task that finished just as the idle worker was about to exit still wakes the session (the worker
53
+ stays up for its wake); one that finished before the worker left for another reason is a note for the next turn.
54
+ A task whose turn you stopped no longer keeps the worker up or holds `/exit`.
55
+ - `chi send --wait` on a session command delivered as a file reads the command's real outcome (failed, or canceled
56
+ when it was refused mid-turn or dropped), not always completed, and says so as the command's ("the command
57
+ failed"), not a turn's.
58
+ - `chi bundle try` no longer signals its child's process group after the child exited (the group id may name
59
+ another process by then), and no longer waits 0.1 s after every run. A `--timeout` stops the background tasks the
60
+ turn started along with the child, and the delegate workers and tasks a run leaves behind are stopped before the
61
+ sandbox is deleted.
62
+ - `chi bundle check` warns (`clash`) about an installed bundle of the same name whose record can't be read, instead
63
+ of skipping the clash check without a word.
64
+
65
+ Update with `chi update` (the system bundle 0.92.0, cost-cap 0.2.1, core 0.3.4), then restart `chi web`.
66
+
67
+ ## [0.91.0] - 2026-10-19
68
+
69
+ ### Added
70
+
71
+ - A background task the model left running (`task_create`, an `execute` handed over at its limit) that finishes
72
+ unseen wakes the idle session for a short turn opened by a `TASK FINISHED` note (exit code, duration, log tail).
73
+ `session.task_wakes`: `wake` (default), `queue` (a note for the next turn) or `off`.
74
+
75
+ ### Changed
76
+
77
+ - The bundle-authoring doc says `chi bundle try` is the live check; its `chi send` recipe for checking the installed
78
+ copy is gone.
79
+
80
+ ### Fixed
81
+
82
+ - `chi bundle try --json` reports always carry `timed_out`; a run cut by `--timeout` lists the tool calls it made
83
+ (name, status and duration; their arguments aren't saved) instead of an empty `tools`.
84
+
85
+ Update with `chi update` (the system bundle 0.91.0), then restart `chi web`.
86
+
11
87
  ## [0.90.0] - 2026-10-19
12
88
 
13
89
  ### Changed
@@ -3529,7 +3605,9 @@ and long-lived sessions.
3529
3605
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
3530
3606
  hotkey panel that send selected text or the clipboard to your sessions.
3531
3607
 
3532
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.90.0...HEAD
3608
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.92.0...HEAD
3609
+ [0.92.0]: https://github.com/dm1try/samagotchi/compare/v0.91.0...v0.92.0
3610
+ [0.91.0]: https://github.com/dm1try/samagotchi/compare/v0.90.0...v0.91.0
3533
3611
  [0.90.0]: https://github.com/dm1try/samagotchi/compare/v0.89.0...v0.90.0
3534
3612
  [0.89.0]: https://github.com/dm1try/samagotchi/compare/v0.88.0...v0.89.0
3535
3613
  [0.88.0]: https://github.com/dm1try/samagotchi/compare/v0.87.0...v0.88.0
data/README.md CHANGED
@@ -80,7 +80,8 @@ the config. With a config already there, it adds a `hosts:` entry and keeps
80
80
  the rest of the file. Then it installs the `core` bundles (loop-guard,
81
81
  check-in, guardrails, slow-commands, agents-md, cost-cap: the safety set for a
82
82
  small local model, the cost cap inert until you set one) and, on a terminal, offers `dev` (known-names, mcp, btw, skills,
83
- source-links). Edit the
83
+ source-links, github-pr, coordinator, claude-peer, codex-peer, ruby; see
84
+ [Bundle profiles](docs/memory.md#bundle-profiles-core-and-dev)). Edit the
84
85
  file later as in [Configure](#configure); `chi bootstrap --help` has the
85
86
  options.
86
87
 
@@ -216,8 +217,10 @@ once, for the session, for the repo, or for the whole rule in the repo.
216
217
  - [Guardrails](docs/guardrails.md): allow / ask / deny for tool calls, rules, approvals
217
218
  - [Hooks](docs/hooks.md): plugin hooks and bundle hooks
218
219
  - [Plugins](docs/plugins.md): bundle plugins (commands, tools, hooks)
220
+ - [Writing a bundle](docs/bundle-authoring.md): `chi bundle new`, `test`, `check`, `try`, `install --dev`
221
+ - [Testing](docs/testing.md): specs, affected specs, lint, worktrees
219
222
  - [Architecture](docs/architecture.md): Engine, TerminalUI, bridge, web
220
- - Internals: [Engine](docs/internals/engine.md), [Gemma 4 contract](docs/internals/gemma4-contract.md), [prompt caching](docs/internals/prompt-caching.md), [context telemetry](docs/internals/context-telemetry.md), [LLM context: forget layer](docs/internals/llm-context-forget.md), [replay benchmark](docs/internals/llm-context-bench.md), [live runs](docs/internals/llm-context-live.md), [tool output limits](docs/internals/tool-guardrails.md), [background tasks](docs/internals/background-tasks.md)
223
+ - Internals: [Engine](docs/internals/engine.md), [Gemma 4 contract](docs/internals/gemma4-contract.md), [prompt caching](docs/internals/prompt-caching.md), [context telemetry](docs/internals/context-telemetry.md), [LLM context: forget layer](docs/internals/llm-context-forget.md), [replay benchmark](docs/internals/llm-context-bench.md), [live runs](docs/internals/llm-context-live.md), [tool output limits](docs/internals/tool-guardrails.md), [background tasks](docs/internals/background-tasks.md), [bundles](docs/internals/bundles.md), [Anthropic Messages API](docs/internals/anthropic.md)
221
224
 
222
225
  ## Development
223
226
 
data/bin/chi CHANGED
@@ -95,13 +95,13 @@ parser = OptionParser.new do |opts|
95
95
  chi bootstrap [HOST[:PORT]|URL] first setup: find the model server, write config.yml
96
96
  chi scratch [options] a one-time session in this terminal: nothing is kept
97
97
  chi web [--port PORT] [--open] [--scope=all] the web UI
98
- chi sessions <list|stop|archive|unarchive|delete|prune|clean|stats>
98
+ chi sessions <list|stop|restart|archive|unarchive|delete|prune|clean|stats>
99
99
  chi note [--source NAME] [-m TEXT] ID...|--all background context for sessions
100
100
  chi broadcast [-m TEXT] [--all] [--dry-run] | log | deliver a note for every session it may concern
101
101
  chi context <add|push|ls|show|refresh|rm|mute|unmute> live external text attached to sessions
102
102
  chi send [-m TEXT] ID...|--new a message into sessions, as if typed there
103
103
  chi answer ID --question QID --option N answer a question a session waits on
104
- chi bundle <install|upgrade|uninstall|status|diff|list|build>
104
+ chi bundle <install|upgrade|uninstall|status|diff|list|build|trash|new|test|check|try>
105
105
  chi desktop <install|upgrade|uninstall|status> macOS "Send to chi" helper
106
106
  chi models [--format json] [TEXT] the models --model takes, from every host
107
107
  chi self version, paths, model and bundles
@@ -126,8 +126,8 @@ parser = OptionParser.new do |opts|
126
126
  # as --tool's negation); this is its help line.
127
127
  opts.separator " --no-tool NAME Leave this tool out of the toolkit (repeatable; a comma list too)"
128
128
  opts.on("--set BUNDLE.KEY=VALUE", "A bundle setting for this session, over config.yml's bundles: (repeatable; a key the " \
129
- "bundle's manifest lists in session_settings:; numbers, true, false and off are typed, the rest is text; " \
130
- "saved in the session: --set cost-cap.cap=3)") do |v|
129
+ "bundle's manifest lists in session_settings:; numbers and booleans (true, off, no) are typed, the rest is " \
130
+ "text; a value of the wrong type is refused; saved in the session: --set cost-cap.cap=3)") do |v|
131
131
  (options[:sets] ||= []) << v
132
132
  end
133
133
  opts.on("--parent-answers LEVEL", "What a parent agent may answer on this session's questions: " \
data/docs/architecture.md CHANGED
@@ -301,10 +301,10 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
301
301
  An optional `deadline` (epoch seconds; `BridgeClient` sends 5/6 of its read timeout ahead) makes
302
302
  a request read after it (a worker frozen by sleep or SIGSTOP) answer `408 deadline_passed` and
303
303
  not run: a client that timed out has said the message was not sent. `/answer`,
304
- `/question/dismiss` and `/command` take the same `deadline` (a command is checked with the event
305
- log held, as a turn is), and the Bridge logs `turn_expired`, `answer_expired`, `dismiss_expired`
306
- or `command_expired`. The web app answers either kind of timeout with `504 worker_timeout`
307
- ("… so the command was not run"). `/cancel`, `/recap` and `/exit` take none.
304
+ `/question/dismiss`, `/command`, `/turn/dequeue` and `/exit` take the same `deadline` (a command is
305
+ checked with the event log held, as a turn is), and the Bridge logs `turn_expired`, `answer_expired`,
306
+ `dismiss_expired`, `command_expired`, `dequeue_expired` or `exit_expired`. The web app answers either
307
+ kind of timeout with `504 worker_timeout` ("… so the command was not run"). `/cancel` and `/recap` take none.
308
308
  The web relays answer, dismiss, command and cancel the same way (`App#relay`): a 408 or a read
309
309
  timeout is `504 worker_timeout`; a Bridge 404 for a route an older worker lacks is
310
310
  `501 not_supported` with the restart hint; a Bridge `404 unknown_session`, no bridge or a
@@ -330,7 +330,7 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
330
330
  saved messages, or the running turn's accumulator); answers once the
331
331
  task is gone: `200 {status, stop_reason, task_id}`, `404 task_not_found`, `409 not_running`.
332
332
  The worker names it in its sidecar features (`task_stop`).
333
- - `POST /session/:id/turn/dequeue` — `{enqueued_id, client_id?}`: take back a queued message
333
+ - `POST /session/:id/turn/dequeue` — `{enqueued_id, client_id?, deadline?}`: take back a queued message
334
334
  (one that waits for the running turn's end: `--queue`, Alt+Enter, `/queue TEXT`, or a message
335
335
  with images) before a turn takes it (web route `POST /api/sessions/:id/turn/:enqueued_id/dequeue`,
336
336
  the × on a queued bubble; `/unqueue` in the attached TUI). Under the event log, like its enqueue:
@@ -356,11 +356,13 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
356
356
  (the summaries the next turn runs under) when the command changed them or the model, and
357
357
  `confirm: {line, text}` when a `/thinking` or `/reasoning-history` change from a `web:` or `tui:`
358
358
  client asks first (`CacheConfirm`; nothing changed, `line` applies it).
359
- - `POST /session/:id/exit` — ask the worker to exit now (`{client_id:}`). The worker checks with
359
+ - `POST /session/:id/exit` — ask the worker to exit now (`{client_id:, delete?, restart?, deadline?}`; `delete`:
360
+ `/exit --delete`, no recap written; `restart: true`: hand the session to a new worker on the newest chi
361
+ installed, `200 {status: "restarting"}`; an empty session's `exiting` carries `discard: true`). The worker checks with
360
362
  the event log held (`WorkerIdleExit#hold_for_request`): `200 {status: "exiting"}` and it leaves
361
363
  like an idle exit, or `409 {status: "held", reason:}` with `turn_running`, `input_queued`,
362
- `continue_offered`, `client_connected` (a stream not named by the asker), `reminders` or
363
- `starting`.
364
+ `continue_offered`, `client_connected` (a stream not named by the asker), `reminders`,
365
+ `background_tasks` (with `tasks: [{id, command}]`: the running tasks a finish wake watches) or `starting`.
364
366
  - `GET /session/:id/state` — `session_state_snapshot` (JSON).
365
367
  - `GET /session/:id/stats` — `Engine#stats_snapshot` for attached `/stats`: the metrics, with the context window and prompt profile asked from the server before the first turn.
366
368
  - `GET /session/:id/snapshot` — the snapshot frame's content as one request (the web server renders
@@ -581,22 +583,24 @@ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/
581
583
 
582
584
  | Component | Main file(s) | What it is |
583
585
  |---|---|---|
584
- | Worker | `worker.rb` (`worker_idle_exit.rb`, `worker_inbound.rb`, `worker_signals.rb`) | A session's background process: builds the `Engine` and `Bridge`, runs queued turns, exits when idle. A spawned one traps TERM, HUP and INT (`WorkerSignals`, installed by `run_session_loop` when `SAMAGOTCHI_WORKER_PROCESS=1`): the running turn stops as a Stop with reason `signal`, is saved, and the worker leaves (`:signal`, exit 0; a new worker only for input already queued). A running turn is checkpointed at its iteration boundaries (`TurnCheckpoint`, the `interrupted_turn` mark), and the next worker settles a turn its predecessor died in (`TurnCheckpoint.settle`). The session is saved from the turn's thread, Bridge threads and a signal's thread; `Session#save` takes a per-session lock around its snapshot and write, so an older snapshot never lands over a newer one. |
586
+ | Worker | `worker.rb` (`worker_idle_exit.rb`, `worker_inbound.rb`, `worker_signals.rb`, `worker_wakes.rb`, `worker_task_wakes.rb`, `task_watch.rb`) | A session's background process: builds the `Engine` and `Bridge`, runs queued turns and the wake turns nobody typed (delegate reports, a background task that finished unseen: [Finish wake](internals/background-tasks.md#finish-wake), attached context), exits when idle. A spawned one traps TERM, HUP and INT (`WorkerSignals`, installed by `run_session_loop` when `SAMAGOTCHI_WORKER_PROCESS=1`): the running turn stops as a Stop with reason `signal`, is saved, and the worker leaves (`:signal`, exit 0; a new worker only for input already queued). A running turn is checkpointed at its iteration boundaries (`TurnCheckpoint`, the `interrupted_turn` mark), and the next worker settles a turn its predecessor died in (`TurnCheckpoint.settle`). The session is saved from the turn's thread, Bridge threads and a signal's thread; `Session#save` takes a per-session lock around its snapshot and write, so an older snapshot never lands over a newer one. |
585
587
  | Config | `config.rb` | Every setting as an `Entry`: env, config.yml and CLI layers ([configuration.md](configuration.md)). |
586
588
  | Paths | `paths.rb` (`Paths`) | The XDG base dirs chi's files live under, read from the env on every call: `Paths.state_home` (`$XDG_STATE_HOME`, else `~/.local/state`) and `Paths.config_home` (`$XDG_CONFIG_HOME`, else `~/.config`), both defaults under `Dir.home`; a blank variable counts as unset, a relative one is made absolute from the current directory. `Paths.tilde` shows a path with `~` for the home folder. `SessionManager` hands a spawned worker the spawner's `XDG_STATE_HOME` / `XDG_CONFIG_HOME`, when set, made absolute, so a relative one isn't read from the session's folder. |
587
589
  | MemoryBundle | `memory_bundle.rb`, `memory_bundle/{installer,builder,installed_bundle,plugin_ref,bundle_hook}.rb` (`Installer`, `Builder`, `InstalledBundle`, `PluginRef`, `BundleHook`) | Installing, building and reading bundles of memories, hooks and plugins. |
588
- | Plugin API | `plugin/api.rb` (`Plugin::Api`), `plugin/tool_spec.rb` (`Plugin::ToolSpec`: a checked tool declaration, how it is registered), `plugin/loader.rb` | What a bundle's plugin registers: tools, commands, annotation actions, hooks, services, toolkits, peer sources ([plugins.md](plugins.md)). |
590
+ | Bundle authoring | `bundle_command.rb` (`BundleCommand`), `bundle_command/{scaffold,check,test_run}.rb`, `bundle_command/try/{run,sandbox,trace}.rb`, `testing.rb` + `testing/` (the test kit) | `chi bundle new`, `check`, `test` and `try`, and the kit a bundle's tests load it with ([bundle-authoring.md](bundle-authoring.md), [internals/bundles.md](internals/bundles.md)). |
591
+ | Bundle session settings | `bundle_session_settings.rb` (`BundleSessionSettings`) | A session's own `--set BUNDLE.KEY=VALUE` values, checked against the manifest's `session_settings:`, laid over `bundles: <name>:` in `ctx.settings` ([plugins.md](plugins.md)). |
592
+ | Plugin API | `plugin/api.rb` (`Plugin::Api`), `plugin/tool_spec.rb` (`Plugin::ToolSpec`: a checked tool declaration, how it is registered), `plugin/loader.rb`, `plugin/context.rb` (`ctx`), `plugin/http.rb` + `plugin/http_transport.rb` (`ctx.http`), `plugin/card.rb` (`Plugin::Card`) | What a bundle's plugin registers: tools, commands, annotation actions, hooks, services, toolkits, peer sources; what its handlers get as `ctx` ([plugins.md](plugins.md)). |
589
593
  | Peer bundles' base | `plugin/peer.rb` (`Plugin::Peer`) | What claude-peer and codex-peer share (the send tool, the command, the cards, payloads); each bundle's subclass sets its strings and transport ([plugins.md](plugins.md#the-claude-peer-bundle)). |
590
594
  | Shipped bundles | `bundles/` (`mcp`, `github-pr`, `loop-guard`, `skills`, `source-links`, …) | Bundles that ship with chi. |
591
595
  | Hooks | `hooks/registry.rb` (`Hooks::Registry`), `hooks/loader.rb`, `hook_routes.rb` (`HookRoutes`), `event_routes.rb` (`EventRoutes`) | Hook registration and dispatch; what a hook or plugin does beyond reading its event (notice, question, stop, steer) routed to the UIs (cards and notices through one set of routes) ([hooks.md](hooks.md)). |
592
- | Guardrails | `guardrails.rb`, `guardrails/shell_lex.rb`, `guardrails/rules.rb`, `guardrail_wiring.rb` | The gate before every tool call: shell lexing, YAML rules, approvals ([guardrails.md](guardrails.md)). |
593
- | Delegation | `tools/delegate.rb`, `tools/delegate_result.rb`, `tools/delegate_wait.rb`, `child_reports.rb`, `relay_desk.rb` | Child sessions a session starts and waits on, and their approval relay ([sessions.md](sessions.md#delegating)). |
596
+ | Guardrails | `guardrails.rb`, `guardrails/shell_lex.rb`, `guardrails/data_readers.rb`, `guardrails/rules.rb`, `guardrails/protected_paths.rb`, `guardrails/parent_policy.rb` + `guardrails/parent_approvals.rb`, `guardrail_wiring.rb` | The gate before every tool call: shell lexing (and the commands that only read a heredoc as data), YAML rules, the core checks on chi's own files, approvals, what a parent agent may answer (`guardrails.parent_answers`) ([guardrails.md](guardrails.md)). |
597
+ | Delegation | `tools/delegate.rb`, `tools/delegate_result.rb`, `tools/delegate_wait.rb`, `child_reports.rb`, `tools/delegate_relay.rb`, `relay_desk.rb`, `relay_verifier.rb`, `relay_watcher.rb` | Child sessions a session starts and waits on, and their approval relay ([sessions.md](sessions.md#delegating)). |
594
598
  | send / broadcast | `send_command.rb` (`SendCommand`), `broadcast_command.rb` (`BroadcastCommand`), `broadcast/` | `chi send` and `chi broadcast` ([broadcast.md](broadcast.md), [sub-agent.md](sub-agent.md)). |
595
599
  | Idle work | `idle_scheduler.rb` (`IdleScheduler`), `idle_recap.rb` (`IdleRecap`), `idle_reminders.rb` | One poller for an idle session's recap and reminders. |
596
600
  | Attached context | `context_sources.rb` (`ContextSources`), `context_poller.rb`, `context_absorber.rb`, `peer_sources.rb` (`PeerSources`: the sources peer bundles declare) | External sources a session keeps fresh, and the peer loop's wakes ([context.md](context.md)). |
597
601
  | LLM context strategies | `llm_context_strategy.rb`, `llm_context_view.rb`, `llm_context_stale.rb`, `llm_context_forget.rb` | What a request sends of the history (none, stale, forget) ([internals/llm-context-forget.md](internals/llm-context-forget.md)). |
598
602
  | Session chains | `session_continue.rb` (`SessionContinue`), `session_chain.rb` (`SessionChain`) | A session that continues an earlier one. `SessionChain.opener_for` gives the text web `continue →` prefills in the new link: the previous link's own first typed message when that link continues one itself, else nil (the 201's `opener`; not saved) ([sessions.md](sessions.md)). |
599
- | Analytics | `analytics_file.rb` (`AnalyticsFile`), `session_metrics/totals.rb` (`SessionMetrics::Totals`), `session_metrics/model_segment.rb` (`SessionMetrics::ModelSegment`) | A session's `analytics.json` (`SessionMetrics#persist`); the sums over its turn records (`Totals`: tokens, cost, speeds, kept as running counters) and their split per model after a `/model` switch (`ModelSegment`: a run of turns on one model, the `model_segments` rows). `session_metrics/side_totals.rb` (`SessionMetrics::SideTotals`) and `side_usage.rb` (`SideUsage`): the side requests' sums, apart from the turns'. |
603
+ | Analytics | `analytics_file.rb` (`AnalyticsFile`), `session_metrics/totals.rb` (`SessionMetrics::Totals`), `session_metrics/model_segment.rb` (`SessionMetrics::ModelSegment`) | A session's `analytics.json` (`SessionMetrics#persist`); the sums over its turn records (`Totals`: tokens, cost, speeds, kept as running counters) and their split per model after a `/model` switch (`ModelSegment`: a run of turns on one model, the `model_segments` rows); `session_metrics/cost.rb` (`SessionMetrics::Cost`): the spend `ctx.cost` reads. `session_metrics/side_totals.rb` (`SessionMetrics::SideTotals`) and `side_usage.rb` (`SideUsage`): the side requests' sums, apart from the turns'. |
600
604
  | Images / vision | `image_store.rb` (`ImageStore`), `vision_support.rb`, `vision_context.rb` | Stored prompt images and whether a model takes them. |
601
605
  | Toolkits | `toolkit.rb` (`Toolkit`), `toolkit_binding.rb` (`ToolkitBinding`, the Engine's seam), `toolkit_command.rb` (`/toolkit`), `tools/registry.rb` (`Registry#toolkit`, `#left_out_names`), `web/public/toolkit_chip.js` | Which registered tools a session's prompts declare: saved in the session, resolved at a turn's start (`Engine#apply_toolkit!`), named in the prompt's not-loaded line, a kit's prompt text after the appended files; `chi.toolkit` and `ctx.use_toolkit` for plugins ([configuration.md](configuration.md#toolkits)). |
602
606
  | Tool index | `tools/tool_index.rb` (`Tools::ToolIndex`) | `find_tools` and `call_tool`: the tools a session knows but doesn't declare one by one (an mcp bundle's servers through `chi.tool_source`, the built-ins its toolkit leaves out), searched and called by name ([plugins.md](plugins.md)). |
@@ -57,7 +57,8 @@ plugin:
57
57
 
58
58
  `plugin.rb` defines `class Plugin` with `register(chi)`; `initialize(settings)` (optional) gets config.yml
59
59
  `bundles: my-bundle:` as a Hash with string keys, once, when the plugin loads. A value a session may set
60
- (`chi --set my-bundle.KEY=V`, listed in the manifest's `session_settings:`) is read from `ctx.settings` at call
60
+ (`chi --set my-bundle.KEY=V`, listed in the manifest's `session_settings:`, typed with `{KEY: number}` so a
61
+ value of another type is refused) is read from `ctx.settings` at call
61
62
  time. `chi bundle check --set KEY=VALUE` gives the sandboxed load a setting.
62
63
 
63
64
  ## What a hook sees
@@ -138,13 +139,7 @@ class, a hand-built `ctx`), that is a gap in the kit: say so to the user rather
138
139
 
139
140
  ## Verifying it live
140
141
 
141
- `chi bundle try . -p "Say hello"` is the live check (trace, plugin, notices); after the final install, check the copy:
142
-
143
- ```
144
- chi send --new --wait --format json --dir /tmp/try -m "/my-bundle chi" # the JSON names the session id
145
- grep plugin_loaded ~/.local/state/samagotchi/samagotchi.log | tail -1 # bundle=my-bundle commands=1 hooks=1
146
- chi sessions delete --force SESSION_ID # leave no test session behind
147
- ```
148
-
149
- (`$XDG_STATE_HOME/samagotchi/samagotchi.log` when `XDG_STATE_HOME` is set.) Then tell the user what it adds,
150
- how to use it, and that a new session picks it up.
142
+ `chi bundle try . -p "..."` **is** the live check: a real model turn on the installed bundle, with its trace
143
+ (tools, plugin, notices, cards). Don't repeat it with `chi send` sessions on the user's own install. After the
144
+ final `chi bundle install .`, `chi bundle list` showing the bundle and its version is enough. Then tell the user
145
+ what it adds, how to use it, and that a new session picks it up.
data/docs/cli.md CHANGED
@@ -83,9 +83,9 @@ chi bootstrap # try localhost 8080, 11434, 1234, 8000
83
83
  or with anchors gets the lines printed to paste instead. `--dry-run` shows
84
84
  what it would write.
85
85
  - **The bundles.** Unless writing the config failed, it installs the system
86
- bundle and the `core` profile (loop-guard, check-in, guardrails, slow-commands), one line
86
+ bundle and the `core` profile (loop-guard, check-in, guardrails, slow-commands, agents-md, cost-cap), one line
87
87
  each; on a terminal it then asks `Also install dev (known-names, mcp, btw,
88
- skills, source-links)? [y/N]`. Run again, it installs nothing new: a bundle
88
+ skills, source-links, github-pr, coordinator, claude-peer, codex-peer, ruby)? [y/N]`. Run again, it installs nothing new: a bundle
89
89
  you uninstalled stays out ([Bundle profiles](memory.md#bundle-profiles-core-and-dev)).
90
90
  A bundle that fails to install makes the exit 1. `--dry-run` says what it
91
91
  would install.
@@ -149,7 +149,10 @@ non-interactive mode. `--keep` retains the temporary sandbox and prints its path
149
149
  one experimental JSON object to stdout and human trace lines to stderr; its keys are `session_id`, `exit_code`,
150
150
  `answer`, `tools` (`index`, `name`, `args`, `status`, `duration_ms`), `notices`, `cards`, `questions`,
151
151
  `interactive_blocked`, `used_memories`, `tokens` (`prompt`, `completion`), `cost`, `cost_is_estimate`, `model`,
152
- `sandbox`, `denied` (1-based tool indexes) and `stderr_tail` (last 4,000 characters). Exit status: 2 for usage,
152
+ `sandbox`, `denied` (1-based tool indexes), `stderr_tail` (last 4,000 characters) and `timed_out`. A run past
153
+ `--timeout` is interrupted like a Ctrl-C and still prints the trace it got to, with `timed_out: true` (text output:
154
+ the trace lines, then `timed out after N seconds` on stderr). That turn keeps its tool calls, as a Ctrl-C does, so
155
+ they show with their `args`; a call the interrupt cut short has status `interrupted`. Exit status: 2 for usage,
153
156
  1 for install/turn failure or timeout, 130 for Ctrl-C, otherwise the child's status.
154
157
 
155
158
  `try` runs the bundle's code and the model's tools with your real HOME, account and keys. Its temporary working
@@ -277,7 +280,7 @@ controls exit behavior (`--non-interactive`); `--resume` composes with both.
277
280
  | `--thinking LEVEL` | This session's own thinking level: `off`, `low`, `medium` or `high`, saved in the session and first in the order (`default` unsets it). On `--resume` or `--attach` it is set at once, through a `/thinking` on an attached worker. With `chi web` it is instead the default for the sessions its workers run, below a session's own (as env `SAMAGOTCHI_THINKING_LEVEL` is). See "Thinking" in configuration.md. |
278
281
  | `--toolkit NAME`, `--tool NAME`, `--no-tool NAME` | A new session's toolkit, the tools its prompts declare: `full` (every built-in), `lean`, `child`, one of config's `toolkit.kits` or a bundle's (without `--toolkit`: config's `toolkit.default`), with tools declared on top of it (`--tool`) or left out (`--no-tool`); both repeatable, a comma list too. Saved in the session as given; a respawned worker, a fork and a continue keep it. Ignored with `--attach`/`--resume` (one line, as `--memory`); `/toolkit` changes it. Not on `chi web` (config's `toolkit.default` covers its sessions). See [Toolkits](configuration.md#toolkits). |
279
282
  | `--parent-answers LEVEL` | A new session's own `guardrails.parent_answers` (`hard`, `standard` or `soft`): what a parent agent may answer on its questions. It may only tighten config.yml's: the stricter of the two applies, and a looser one is saved as given with a warning (`--parent-answers soft is looser than config.yml's guardrails.parent_answers: standard; standard applies`). Saved in the session; a respawned worker, a fork and a continue keep it, a `delegate` child takes its parent's. Ignored with `--attach`/`--resume` (one line, as `--toolkit`); refused with `chi web`. Only you loosen it: `/parent-answers` in the terminal or the web. See [Guardrails](guardrails.md#answers-from-a-parent-agent). |
280
- | `--set BUNDLE.KEY=VALUE` | A bundle setting for a new session (repeatable; a later one of the same key wins), laid over config.yml's `bundles: <bundle>:` in what the bundle's plugin reads (`ctx.settings`): `--set cost-cap.cap=3`. The value is typed simply: an integer (`3`, `-1`) or a decimal (`2.5`) is a number, `true` is true, `false` and `off` are false, anything else stays the text as given (`010`, `0x10`, `3:30`, `No`, `null`). For cost-cap, `0`, a negative number or `off` is no cap. Only a key the installed bundle's manifest lists in `session_settings:` is taken: a bundle that isn't installed (or whose plugin doesn't load), a key it doesn't list (a record installed before the bundle listed it says to run `chi update` or `chi bundle upgrade NAME`), or another shape (`--set foo`, `--set a.b.c=1`) is an error naming what it takes, exit 2, nothing started. Saved in the session; a respawned worker, a fork and a continue keep it, a delegate child doesn't. Ignored with `--attach`/`--resume` (one line, as `--toolkit`); refused with `chi web`. See [Per-session settings](plugins.md#per-session-settings). |
283
+ | `--set BUNDLE.KEY=VALUE` | A bundle setting for a new session (repeatable; a later one of the same key wins), laid over config.yml's `bundles: <bundle>:` in what the bundle's plugin reads (`ctx.settings`): `--set cost-cap.cap=3`. The value is typed simply: an integer (`3`, `-1`) or a decimal (`2.5`, `.5`, `1e1`) is a number, `true`, `yes` and `on` are true, `false`, `no` and `off` are false (any case, as in config.yml), anything else stays the text as given (`010`, `0x10`, `3:30`, `y`, `null`), and so does every value of a key the manifest types `string`. For cost-cap, `0`, a negative number or `off` is no cap. Only a key the installed bundle's manifest lists in `session_settings:` is taken, with a value of the type it gives the key: a bundle that isn't installed (or whose plugin doesn't load), a key it doesn't list (a record installed before the bundle listed it says to run `chi update` or `chi bundle upgrade NAME`), a value of another type (`--set cost-cap.cap=five`), or another shape (`--set foo`, `--set a.b.c=1`) is an error naming what it takes, exit 2, nothing started. Saved in the session; a respawned worker, a fork and a continue keep it, a delegate child doesn't. Ignored with `--attach`/`--resume` (one line, as `--toolkit`); refused with `chi web`. See [Per-session settings](plugins.md#per-session-settings). |
281
284
  | `--profile NAME` | Prompt profile (`qwen36` or `gemma4`) for every model in this run, over config and the server's template (same as `--model-profile`, env `SAMAGOTCHI_MODEL_PROFILE`). See "Prompt profile" in configuration.md. |
282
285
  | `--memory NAME` | Preload a memory entry into the system prompt (repeatable; a comma list too). Merged under the config.yml `memories:` baseline. Works attached: the list is stored on the session, so its worker builds the same prompt on every respawn. |
283
286
  | `--append-system-prompt FILE` | Put `FILE`'s text into the system prompt, after AGENTS.md and before the memory indexes (repeatable; relative to the shell's directory). Added after the config.yml `append_system_prompt:` list. A missing or unreadable file is an error, exit 1. Saved in the session, so a respawned worker, a fork and a continue keep it; ignored with `--attach`/`--resume` (one line, as `--memory`). See [Appended prompt files](configuration.md#appended-prompt-files). |
@@ -73,7 +73,8 @@ session:
73
73
  # keep_empty: false # true keeps sessions nothing happened in (default: deleted when left)
74
74
  # max_children: 4 # running sessions one session may have delegated at a time (the delegate tool)
75
75
  # delegate_reports: wake # a child's reply reaches its parent by itself: wake (an idle parent runs a turn), queue, off
76
- # max_wakes: 10 # wake turns in a row (delegate reports, context, peers) with no human input
76
+ # task_wakes: wake # a background task finishing unseen: wake (an idle session runs a turn), queue, off
77
+ # max_wakes: 10 # wake turns in a row (delegate reports, finished tasks, context, peers) with no human input
77
78
 
78
79
  # Baseline memories preloaded into the system prompt (same shape as --memory);
79
80
  # name entries you have, or chi warns at each start.
@@ -117,7 +118,9 @@ Besides the settings sections, the file holds maps that are read by their
117
118
  own subsystems: `hosts:` (below), `models:` (see "Prompt profile" and
118
119
  "Images"), `model_aliases:`, `hooks:` (see [Hooks](hooks.md)),
119
120
  `guardrails:` (see [Guardrails](guardrails.md)), `bundles:` (a bundle's
120
- settings for its hooks, see [Hooks: Settings](hooks.md#settings)),
121
+ settings for its hooks, see [Hooks: Settings](hooks.md#settings); a session's own
122
+ `--set BUNDLE.KEY=VALUE`, for a key the bundle's manifest lists in `session_settings:`, lays over them, see
123
+ [Per-session settings](plugins.md#per-session-settings)),
121
124
  `memories:` and `append_system_prompt:` (see "Appended prompt files"). Their entry names (host names, model ids, aliases) are yours to
122
125
  choose. The `memories:` list is the persistent baseline for preloaded memory entries —
123
126
  the same name shape as `--memory` (bare name or `scope/name`), merged under
@@ -160,7 +163,7 @@ or `chi web` restart.
160
163
  | `hooks`, `bundles` | Worker start | Hooks, plugins and their bundle settings load with the engine. |
161
164
  | `recap.*` | Worker start | The engine builds its recap job once. |
162
165
  | `session.shared` | Next `chi` invocation | It selects attached-worker or in-process mode for that invocation. |
163
- | `session.idle_exit_minutes`, `session.keep_empty`, `session.max_children`, `session.delegate_reports`, `session.max_wakes` | Worker start or next matching lifecycle decision | Existing workers keep their idle-exit timer; the other values are read for the next corresponding decision. |
166
+ | `session.idle_exit_minutes`, `session.keep_empty`, `session.max_children`, `session.delegate_reports`, `session.task_wakes`, `session.max_wakes` | Worker start or next matching lifecycle decision | Existing workers keep their idle-exit timer; the other values are read for the next corresponding decision. |
164
167
  | `session.retention_*`, `session.max_count`, `session.keep_status`, `session.sweep_interval_hours` | Next retention sweep | `chi web` sweeps on its configured schedule and on `GET /api/sessions`; `chi sessions prune` uses the values for that invocation. The sweep interval is checked on the next hub tick. |
165
168
  | `web.*` | `chi web` start | Restart `chi web` to change its bind address, port or page settings. |
166
169
  | `log.*` | Process start | Each process holds its configured log sink and level after first use; restart that process. |
@@ -1622,7 +1625,8 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
1622
1625
  | `session.keep_empty` | `false` | | Keep sessions nothing happened in. |
1623
1626
  | `session.max_children` | `4` | | Running delegated sessions one session may have. |
1624
1627
  | `session.delegate_reports` | `wake` | | When a delegate child ends a turn, its reply reaches the parent: `wake` runs an idle parent's turn for it, `queue` waits for the parent's next turn, `off` leaves it to `delegate_result`. |
1625
- | `session.max_wakes` | `10` | | Wake turns a session runs in a row with no human input: for delegate reports, attached context and peer answers; past it reports wait for the next turn and context updates arrive as notes. |
1628
+ | `session.task_wakes` | `wake` | | When a background task the session started (`task_create`, an `execute` handed over at its limit) finishes and the model hasn't seen it finish: `wake` runs an idle session's turn opened by a `TASK FINISHED` note, `queue` leaves the note for the next turn, `off` leaves it to `task_wait`. See [Finish wake](internals/background-tasks.md#finish-wake). |
1629
+ | `session.max_wakes` | `10` | | Wake turns a session runs in a row with no human input: for delegate reports, finished background tasks, attached context and peer answers; past it reports wait for the next turn and task finishes and context updates arrive as notes. |
1626
1630
  | `session.retention_days` | `14` | yes | Delete sessions not updated for N days; `0` = forever. See [Sessions](sessions.md). |
1627
1631
  | `session.max_count` | `500` | yes | Keep the newest N; `0` = uncapped. |
1628
1632
  | `session.keep_status` | none | yes | Comma list of statuses never pruned (a session a worker or `chi` has open is never pruned anyway). |
data/docs/context.md CHANGED
@@ -249,8 +249,8 @@ you're away from the terminal. All of these must hold:
249
249
  - the session's worker is up and idle: no turn running, nothing queued, no
250
250
  question or continue offer waiting, not just started;
251
251
  - under `session.max_wakes` turns in a row with no message from you (the
252
- budget delegate reports use too; a turn that failed pauses wakes until you
253
- write);
252
+ budget delegate reports and finished background tasks use too; a turn that
253
+ failed, or was stopped before the model call, pauses wakes until you write);
254
254
  - the source hasn't woken the session in the last 10 minutes (a peer
255
255
  source has its own limit instead: below).
256
256
 
@@ -266,7 +266,9 @@ notifications count it as a finished turn however short.
266
266
  A wake turn that fails goes back to before it and keeps its note as a
267
267
  plain one; the model reads `[SYSTEM: the previous turn failed before any
268
268
  answer: … The wake turn for the change in attached context <name> was not
269
- answered; chi starts no other wake turn until the user writes.]`.
269
+ answered; chi starts no other wake turn until the user writes.]`. One
270
+ stopped before the model call (a Stop, a `before_turn` hook's cancel) goes
271
+ back the same way, its note plain, and pauses wakes too.
270
272
 
271
273
  ### Peer sources: the peer loop
272
274
 
data/docs/hooks.md CHANGED
@@ -590,7 +590,7 @@ hooks:
590
590
  event: after_tool_call
591
591
  on_error: log
592
592
  priority: 100
593
- trust_level: reviewed # reviewed | experimental (default)
593
+ trust_level: reviewed # reviewed | trusted | experimental (default)
594
594
  requires_chi: ">= 0.12.0" # optional: a gem-style requirement, as for plugins
595
595
  needs: [gh] # optional: outside commands the memories use (docs/memory.md#bundles-that-need-outside-commands)
596
596
  ```
@@ -608,12 +608,12 @@ Notes:
608
608
  - Ordering: bundle hooks fire by `(priority, bundle_name, hook_name)` (lower priority first), then plain `config.yml` hooks in registration order.
609
609
  - Settings: a hook class with `initialize(settings = {})` gets the bundle's section of `config.yml` `bundles:` (see [Settings](#settings)).
610
610
  - 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.
611
- - 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)), `chi bundle install slow-commands` (a plugin: commands that ran past the execute limit are run as tasks next time, see [Plugins](plugins.md#the-slow-commands-bundle)), `chi bundle install agents-md` (a plugin: a nested AGENTS.md is steered into the conversation on the first touch under its directory, see [Plugins](plugins.md#the-agents-md-bundle)), `chi bundle install cost-cap` (a plugin: a per-session $ cap, `/cap`, see [Plugins](plugins.md#the-cost-cap-bundle)), `chi bundle install ruby` (a plugin: an RSpec run with no examples at all, a hint, see [Plugins](plugins.md#the-ruby-bundle)) and `chi bundle install skills` (a plugin: `/skill`, versions of skills, see [Plugins](plugins.md#the-skills-bundle)).
611
+ - 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)), `chi bundle install slow-commands` (a plugin: commands that ran past the execute limit are run as tasks next time, see [Plugins](plugins.md#the-slow-commands-bundle)), `chi bundle install agents-md` (a plugin: a nested AGENTS.md is steered into the conversation on the first touch under its directory, see [Plugins](plugins.md#the-agents-md-bundle)), `chi bundle install cost-cap` (a plugin: a per-session $ cap, `/cap`, see [Plugins](plugins.md#the-cost-cap-bundle)), `chi bundle install ruby` (a plugin: an RSpec run with no examples at all, a hint, see [Plugins](plugins.md#the-ruby-bundle)) `chi bundle install skills` (a plugin: `/skill`, versions of skills, see [Plugins](plugins.md#the-skills-bundle)), `chi bundle install github-pr` (attached context: the branch's open PR, see [Plugins](plugins.md#the-github-pr-bundle)), `chi bundle install coordinator` (a plugin: parallel work in worktrees and child sessions, see [Plugins](plugins.md#the-coordinator-bundle)) and `chi bundle install claude-peer` / `codex-peer` (a plugin: talk to a Claude Code or Codex session, see [Plugins](plugins.md#the-claude-peer-bundle)).
612
612
  - 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).
613
613
 
614
614
  Lifecycle:
615
615
 
616
- - `chi bundle install <source>` copies the hook files its manifest's `hooks:` map lists (only those; with no `hooks:` map, every `hooks/*.rb`) to `$XDG_CONFIG_HOME/samagotchi/memories/.bundles/<name>/hooks/` (`$XDG_CONFIG_HOME` defaults to `~/.config`) and persists metadata + `trust_level` + `source_commit` (git HEAD) to provenance.
616
+ - `chi bundle install <source>` copies the hook files its manifest's `hooks:` map lists (only those; with no `hooks:` map, every `hooks/*.rb`) to `$XDG_CONFIG_HOME/samagotchi/memories/.bundles/<name>/hooks/` (`$XDG_CONFIG_HOME` defaults to `~/.config`) and persists metadata + `trust_level` + `source_commit` (git HEAD) to provenance. `install --dev <dir>` links them to `<dir>`'s files instead, with no sha256 check ([cli.md](cli.md#writing-a-bundle-new-test-check)).
617
617
  - `Engine.new` loads `config.yml` hooks first, then bundle hooks via `MemoryBundle::Provenance.each_installed(holding: :hooks)` → `Hooks::BundleLoader.load`. Bundle hooks are process-scoped (they survive the per-turn `clear_hooks`; only plain hooks are cleared). Experimental bundles emit a one-line startup warning.
618
618
  - `chi bundle status`, `diff`, `uninstall`, `build` are hook-aware (counts, metadata, removal).
619
619
 
@@ -19,7 +19,7 @@ Behavior:
19
19
 
20
20
  - `execute`, `task_create` and the `!cmd` shell bang run the command in a non-login shell: `/bin/sh -c` on Linux; on macOS, when `/bin/zsh` is there, `/bin/zsh --emulate sh +o ignore_braces +o sh_glob -o bash_rematch +o bsd_echo -c` (macOS's `/bin/sh` is bash 3.2, which can't parse an apostrophe in a heredoc inside `"$( )"`, the usual commit message shape). That mode reads no rc files and keeps POSIX word splitting, unmatched globs left as is, `{1..3}`, `[[ … =~ (…) ]]` with `BASH_REMATCH`, arrays, `$'…'`, `&>` and echo expanding `\t`. Bash-only builtins and variables (`shopt`, `BASH_VERSION`, `${!prefix*}`, `read -p`) are not there.
21
21
  - Task metadata and output are persisted under `tmp/tasks/`: each task's `tmp/tasks/<id>/task.json` is a `TaskRecord` (`lib/samagotchi/tools/task_record.rb`, a value object: `.from_file` reads it, `#to_file` is the JSON object written, an update is a `#with`). Its keys keep their order; `stderr_path` and `handed_over_by` (a handed-over execute) and `stop_requested_by` (a stop under way or done) are left out until set. `bundles/slow-commands` reads these files on its own, so the shape is a contract.
22
- - A task record keeps its process's pid and start time (`pid_started`). Before `task_stop` signals, the pid must still lead its own process group and must not have started at another time; otherwise the pid was reused by another process, nothing is signalled and the task ends as failed. A start time that can't be read at that moment (a failed `ps`) falls back to the group check, as does a record without `pid_started` (from an older chi). `task_get`, `task_list`, `task_wait`'s polls and the running-task lists (a cancelled turn's note, a worker's idle exit) make the same check, so a task whose pid was reused reads as exited (failed) at once, not as running until a `task_stop`; on macOS that is one `ps` per running task per check (~5 ms).
22
+ - A task record keeps its process's pid and start time (`pid_started`). Before `task_stop` signals, the pid must still lead its own process group and must not have started at another time; otherwise the pid was reused by another process, nothing is signalled and the task ends as failed. A start time that can't be read at that moment (a failed `ps`) falls back to the group check, as does a record without `pid_started` (from an older chi). `task_get`, `task_list`, `task_wait`'s polls and the running-task lists (a cancelled turn's note, a worker's restart hold, the finish wake's watch) make the same check, so a task whose pid was reused reads as exited (failed) at once, not as running until a `task_stop`; on macOS that is one `ps` per running task per check (~5 ms).
23
23
  - A stop (`task_stop`, the user's Stop of an `execute`, the execute limit in kill mode) TERMs the task's process group, then KILLs it after a grace. The groups its processes started are stopped too: a GNU `timeout N cmd` (without `--foreground`) moves itself and its command to a group of its own. `ProcessGroup.stop` finds them with one `ps -A -o pid=,ppid=,pgid=` (~40 ms on macOS) before the TERM and again before the KILL, walking down from the group's members. A process whose parent already exited (reparented to init, e.g. a setsid'd `cmd &`, or a backgrounded `timeout N cmd &` after its shell exited) is not found and runs on.
24
24
  - Task listing is workspace-scoped (current project only).
25
25
  - `execute` runs its command with stdout and stderr in two logs under `$XDG_STATE_HOME/samagotchi/execute/<id>/` (always an absolute path: a relative `XDG_STATE_HOME` is taken from the directory chi started in; outside the workspace: `git status` and `rm -rf tmp` never see them), stdin `/dev/null` and the same `exit_code` wrapper as a task. Its own copies of the logs hold a shared `flock` across the spawn: while a fresh exclusive `flock` fails, some process the command started still has them open. When the shell exits, a background child still holding them (`server &`) gets 1.5 s, then its group is stopped; one holding none (`nohup server >log 2>&1 &`) is left alone. A holder that left the group (setsid) is out of the stop's reach: it runs on, the result ends with the "left running" hint, and its dir stays for it to write to (the sweep removes it once no one holds the logs). Otherwise a finished command's dir is removed. Known gap on Linux: a command that writes through `/dev/stdout` or `/dev/stderr` (`echo b >/dev/stdout`) reopens the log file itself with `O_TRUNC`, so the output before it is lost (macOS dups the descriptor; the fix would be a relay pipe). The check needs the state dir on a local file system: over NFS, `flock` is emulated with `fcntl` locks, which children don't inherit.
@@ -27,11 +27,36 @@ Behavior:
27
27
  - A stop of an `execute` (the user's Stop, the limit in kill mode) is done once the shell is reaped and nothing holds the logs: a child that ignores TERM after its shell died gets the KILL.
28
28
  - A command still running at `execute.timeout_sec` is handed over as a task (`execute.on_timeout: task`, the default; `kill` stops it instead, and the `!cmd` bang always kills): the result starts `Still running at the 120s limit: moved to a background task, not killed.`, then `task_id: …` and the tails of both logs so far. A timeout during the 1.5 s background grace is a kill (the shell is gone, nothing to hand over). If the record can't be written, the command is killed. `TaskRuntime.adopt` writes a record for the process execute already started, with `handed_over_by: "execute"`. Its logs stay where execute wrote them, so the record's paths are absolute: `output_path` (stdout), `stderr_path` (stderr, a log of its own) and `exit_code_path` (a `task_create` record's `exit_code_path` is absolute too; its `output_path` is relative). `started_at` (and `created_at`) is the spawn time, not the hand-over's; `pid_started` is read at the hand-over, while execute's detach waiter is still running. From there it is an ordinary task: `task_wait`, `task_get`, `task_list` and `task_stop` treat it as any other. Its dir gets a `task` back-pointer file holding the record's absolute path. Once per process, the first `execute` sweeps `…/execute/`: a dir whose logs no process holds is removed when it has no back-pointer and is a day old (chi died mid-command), when its record is gone, or when it is 14 days old. The records live in each project's `tmp/tasks`, which the state dir can't list: hence the back-pointer.
29
29
  - `task_wait` and `task_get` show `stderr_path` when the record has one; `task_wait`'s output tail is followed by a `stderr_tail:` of at most 10 lines when that log isn't empty. A `task_create` task has one log (stdout and stderr together) and no `stderr_path`. A `task_wait` result longer than `max_tool_output_chars` keeps its header and the tail's end, with a gap line naming `output_path`, where the whole log already is (and `stderr_path`, when the record has one) (`ToolOutputCut`'s `:head_tail_ref`; nothing is copied; see [Tool output guardrails](tool-guardrails.md#the-runners-cut-max_tool_output_chars)).
30
- - Which tasks are a session's own (a cancelled turn's note, the turn-end note, a worker's idle exit, the web's stop-task button): the ids its `task_create` results and its handed-over executes name (`TaskRuntime.created_ids_in`, `created_id`). An execute's id is read only after its first line (`Execute::HANDED_OVER`, through `TaskRuntime.handed_over_id`; the web has a JS twin in `task_stop.js`), not from a `task_id:` line inside a run, which the command can print. (A command that prints the `---` separator chi joins a turn's results with, then a hand-over line, still fakes one: a known gap.) Either result may start with the `ran as: …` line of a gate-corrected call.
30
+ - Which tasks are a session's own (a cancelled turn's note, a worker's restart hold, the finish wake below, the web's stop-task button): the ids its `task_create` results and its handed-over executes name (`TaskRuntime.created_ids_in`, `created_id`). An execute's id is read only after its first line (`Execute::HANDED_OVER`, through `TaskRuntime.handed_over_id`; the web has a JS twin in `task_stop.js`), not from a `task_id:` line inside a run, which the command can print. (A command that prints the `---` separator chi joins a turn's results with, then a hand-over line, still fakes one: a known gap.) Either result may start with the `ran as: …` line of a gate-corrected call.
31
31
  - `task_get` returns metadata and `output_path`; use `read` for output contents.
32
32
  - `task_wait` accepts `timeout`, `tail_lines` (maximum 100), and `done_pattern` (a regular expression string).
33
33
  - `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.
34
34
  - `execute` and `task_create` export chi's own markers into the command, set over the model's `env` (which can't set them: `Error: env key is reserved`): `SAMAGOTCHI_PARENT_SESSION` (the session's id, `chi` without one; a chi started there answers as a parent agent), `SAMAGOTCHI_SESSION_MODEL` (the resolved ref the session runs on, read at each call so it follows `/model`; unset when unknown) `SAMAGOTCHI_SESSION_THINKING` (the session's own thinking level; unset when it has none) and `SAMAGOTCHI_SESSION_REASONING_HISTORY` (its own reasoning_history level, the same way). `chi self` reads the last three for its model, thinking level and reasoning history. The four are `TaskRuntime::MARKER_ENV_KEYS` (and `SessionManager::SESSION_MARKER_ENV`, which a spawned worker unsets). A session's worker never inherits them from whoever started it (a `chi send` run by another session's `execute`): its other commands (a user's `!cmd`, an MCP server, a context command) have none.
35
35
  - They also pass on a set `XDG_STATE_HOME` / `XDG_CONFIG_HOME` made absolute (`Builtins.xdg_absolute`, via `Paths`), so a chi the command runs in another `cwd` (`chi send`) reads this chi's state and config. These aren't reserved: an XDG dir the model sets in `task_create`'s `env` stays its own.
36
36
 
37
+ ## Finish wake
38
+
39
+ A task the model leaves running (it ends its turn: "I'll check when it's finished") is reported when it ends, so the result doesn't wait for the user's next message. `session.task_wakes` (env `SAMAGOTCHI_SESSION_TASK_WAKES`): `wake` (default), `queue` or `off` (no watch, as before).
40
+
41
+ - **The watch** (`TaskWatch`, `task_watch.rb`) is the Worker's (`WorkerTaskWakes`, `worker_task_wakes.rb`, which writes the notes; the Worker runs the wake turn), in memory: the in-process REPL (`--no-shared`) has none, and a crash loses it (those tasks behave as before). The Worker's Engine observer feeds it the raw `:tool_call_completed` events, the input `Bridge#own_task_ids` reads too. A `task_create` result and an `execute` hand-over add the task (`TaskRuntime.created_id`: a `task_id:` the command printed doesn't count), the hand-over's limit kept for the note. What shows the model the end removes it: a `task_wait` with a final status, or with `wait_result: pattern_matched` (the model chose its own "done" marker); a `task_get` with a final status; any `task_stop`. `task_list` and a `read` of `output.log` don't: a model that read the log still gets the wake once. Tasks a tool starts inside an anytime command (`/btw` and the like) reach the same observer, so they are watched too.
42
+ - **The idle tick**: with something watched, each idle pass (after delegate reports, before due reminders) refreshes the watched records (`TaskRuntime.get_record`, one `ps` per running task) and takes the finished ones, keeping only the ids the conversation still holds (`TaskRuntime.created_ids_in`: a `!rollback` or a restored turn may have dropped the result that started one; an LLM context strategy can't, since its stale and forget stubs are drawn in the request, never written over the saved result). A note that can't be written puts its task back for the next pass. An empty watch costs nothing. A task that finishes during a turn and is still unseen when the turn ends wakes on the next idle pass. One the user stopped with the web's stop-task button is dropped without a note.
43
+ - **The note** is a context note under its own header (`ContextNote::TASK_HEADER`; `TaskFinishNote`), `note_id: task-<id>`, `source: task <id8>`:
44
+
45
+ ```
46
+ [TASK FINISHED from task 3f9a2c1d, 14:20]
47
+ Background task 3f9a2c1d finished: exit 0 after 6m12s (handed over by execute at its 120s limit).
48
+ command: bundle exec rspec spec
49
+ <task_wait's own text (TaskWait.finished_text): status, the log's last 10 lines, a stderr_tail>
50
+ chi started this turn because a background task you started finished; your user didn't write anything.
51
+ Carry on with what you planned to do with its result. If you planned nothing, tell your user in a sentence or two how it went.
52
+ [END NOTE]
53
+ ```
54
+
55
+ The duration runs from `started_at` to the `exit_code` file's mtime (the shell's EXIT trap wrote it as it ended; `finished_at` is when a refresh first noticed the end), else `finished_at`. The body is cut to `max_tool_output_chars` as a `task_wait` result is (`ToolOutputCut`'s `:head_tail_ref`, naming the logs).
56
+ - **The wake turn**: the first finished task runs a continue turn from client `task:<id>` (`last_turn.origin: "task"`, labelled `task <id8> finished` in the TUI and the web; the web's bell rings at its end however short), whose first message is the note (`turn_start`, `turn_id`, written before the turn's checkpoint so a restore keeps it). It counts toward `session.max_wakes` with the other wakes (`WorkerWakes#task_open?`: no continue offer, not paused, past the 2 s start grace). Several tasks finishing together: one wake turn, the others plain notes before it.
57
+ - **A plain note instead** (the same text without the last two lines, no turn start), written at once, with `queue`, the budget spent (the UIs hear `N finished background tasks noted; the model reads them with your next message` once per human input, the flag shared with the other wakes), wakes paused, a continue offer waiting, input queued, an exit asked for, or a **note-only** task: one started or waited on in a turn the user stopped (reasons `user`, `manual`, `ctrl_c`), since a wake telling the model to carry on would undo that Stop. A prompt's turn takes the finished ones as plain notes first.
58
+ - **Failure**: a wake turn that fails and kept nothing, or is stopped and restored, turns its note back into the plain one (`WorkerInbound#unmark_wake`, as for a context wake) and pauses wakes until the user writes.
59
+ - **Idle exit**: with `wake`, a watched task holds the worker's idle exit (`WorkerIdleExit`'s `:background_tasks`) until its wake is taken: while it runs, and once it finished until the next idle pass takes it (`TaskWatch#holding`, in memory, no `ps`), so one that ends between the pass's look and the exit's check still wakes the session. A still-running one holds `/exit` too (`TaskWatch#running`; the 409 lists the tasks, `{id, command}`, with a `hint` on stopping them, and the attached terminal names them and says to use the web's stop-task button or ask for `task_stop`), as reminders do. A note-only task holds neither (no wake would come for it), nor does one whose note, or the look for it (the conversation's task ids unreadable), failed 3 times (`TaskWatch::MAX_FAILURES`: it is still tried at each look, but can't keep the worker up for good). With `queue` nothing holds: nothing would wake for the end, so keeping the worker up isn't worth it. As the worker leaves (any exit but a stop, a signal or an `/exit --delete`), the tasks that finished since the last look are written as plain notes; a task that ends after the worker left gets no note (the model sees its end at its next look, as before). A forgotten dev server keeps the worker up until it is stopped; a crash of that server wakes the session.
60
+ - **The tool descriptions**: a worker's `execute` and `task_create` say "if you end your turn before it finishes, chi tells you when it does" (left out under `off` and in the REPL), fixed when the worker builds its Engine.
61
+
37
62
  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).
@@ -22,9 +22,9 @@ user-facing side (what a bundle owns, profiles, needs) is in [memory.md](../memo
22
22
 
23
23
  | Piece | Role |
24
24
  |---|---|
25
- | `BundleCommand` (`lib/samagotchi/bundle_command.rb`) | `chi bundle install/upgrade/uninstall/status/diff/list/build/trash/new/test/check`; runs the Installer, prints its summary, runs the conflict agent step; refuses a source already installed under another name (`installed_under_another_name?`: a git URL bundle recorded under its repo's name before the manifest named it) |
25
+ | `BundleCommand` (`lib/samagotchi/bundle_command.rb`) | `chi bundle install/upgrade/uninstall/status/diff/list/build/trash/new/test/check/try` (`install --dev`: the Installer's `dev:`, links and `Provenance::DEV_SHA`); runs the Installer, prints its summary, runs the conflict agent step; refuses a source already installed under another name (`installed_under_another_name?`: a git URL bundle recorded under its repo's name before the manifest named it) |
26
26
  | `BundleCommand::Check` (bundle_command/check.rb) | `chi bundle check [PATH] [--fix] [--set K=V]`: a bundle being written, checked as an install takes it (docs/cli.md lists the codes); parses the manifest from `Shas.parseable` (a placeholder sha is a `sha` finding, not a YAML error), reads the user's installed bundles only for `clash`, and installs and loads through `Testing::Install` in a `Testing::Sandbox` with `force: true`, the log's echoes swallowed |
27
- | `BundleCommand::Try` (`bundle_command/try/`) | `chi bundle try PATH -p TEXT`: filtered-config `Sandbox` installs the tried bundle and shipped `guardrails` / `cost-cap` in a separate child, then `Run` starts one non-interactive chi process from its realpath work dir; `TraceReader` reads that session's analytics, cards and session JSON by id (never `Session.list`), joining timing and tool args for text or experimental JSON output. `Sandbox#child_env` is a Hash, never an ENV mutation; it keeps ordinary user model settings and Keychain access while dropping the spawner's session/guardrail/worker markers. |
27
+ | `BundleCommand::Try` (`bundle_command/try/`) | `chi bundle try PATH -p TEXT`: `Command` (its flags, help and checks) drives the rest and `Render` prints the trace; a filtered-config `Sandbox` (its folders a `SandboxLayout`, the layout `Testing::Sandbox` uses too) installs the tried bundle and shipped `guardrails` / `cost-cap` in a separate child, then `Run` starts one non-interactive chi process from its realpath work dir; `TraceReader` reads that session's analytics, cards (`Bridge::CardStore.saved`, as the web does) and session JSON by id (never `Session.list`), joining timing and tool args for text or experimental JSON output (a turn a `--timeout` interrupt cut keeps its calls like a Ctrl-C, the one cut short answered as interrupted, so its rows come from the saved calls too; `Run::Result#timed_out` becomes `timed_out`). `Sandbox#child_env` is a Hash, never an ENV mutation; it keeps ordinary user model settings and Keychain access while dropping the spawner's session/guardrail/worker markers (`ChildEnv`, the scrub a session's worker spawn uses too). The child runs in its own process group: a `--timeout` sends it INT, then `ProcessGroup.stop` (the groups its processes started too); after any exit the group is signalled only while a straggler is still in it, never a reaped group. `Sandbox#stop_leftovers` (before the trace, and in `cleanup`) stops what left the child's tree, found by the sandbox's own records: a delegate's worker by its session's `owner.lock`, a task by its running `task.json` whose pid still leads its group. |
28
28
  | `BundleCommand::Scaffold` (bundle_command/scaffold.rb) | `chi bundle new NAME`: the starting files (block-form `plugin:` entry, `requires_chi` at this chi, a kit test that passes as written); the manifest's shas are filled through `Shas.refresh` |
29
29
  | `BundleCommand::TestRun` (bundle_command/test_run.rb) | `chi bundle test`: `RbConfig.ruby -I <chi's lib>` on `test/**/*_test.rb` (Minitest) or `spec/` (RSpec, as `rake bundles:specs`), under `Bundler.with_unbundled_env` when Bundler's runtime is loaded |
30
30
  | `Shas` (shas.rb) | a manifest's `sha256:` lines: `.refresh(dir[, text])` rewrites them line by line (the layout kept; a `files:` entry it can't place raises `Shas::Error`), `.stale` → `[Stale(file, old, new)]` (`old` `""` for a placeholder, nil for a plugin / hook / script entry with no sha line), `.parseable` (a placeholder `sha256: sha256:` isn't YAML). Plain Ruby: `rake bundles:sha` / `bundles:check` (`script/release/release_tools.rb`) and `chi bundle check --fix` share it |
@@ -32,7 +32,7 @@ user-facing side (what a bundle owns, profiles, needs) is in [memory.md](../memo
32
32
  | `SourceIdentity` (source.rb) | what a source is, for comparing two installs of one name: `shipped:<dir>` (any gem or checkout), `git:<url>` (no `#ref`, trailing `/` or `.git`), `path:<realpath>`; nil when it can't place one |
33
33
  | `PreparedSource` (source.rb) | one source normalized once (dir, owned, git commit): `BundleCommand` reads the bundle's name from its `manifest.yml` (the name for every kind of source, a git URL too) and hands it to the Installer (`prepared:`), so a source is cloned or extracted once per command |
34
34
  | `MemoryFiles` | which top-level `*.md` files of a bundle are its memories (`.select`: the `files:` list, else all but README/LICENSE and the like), `.unshipped` (listed, not shipped) and `.symlinks` (skipped: a symlink is never a memory) |
35
- | `Manifest` | parses/validates `manifest.yml` (`name:` is the bundle's name and a directory under `.bundles/`, so a path such as `../x`, `a/b`, `.` or `..` is refused: `name: … is not a bundle name`; YAML that doesn't parse or isn't a mapping is a `ValidationError` too; files, hooks as `BundleHook`, plugin, scripts, needs, requires_chi, includes, context_providers, session_settings: the keys a session's `chi --set` may give, recorded at install in `manifest.json` for `BundleSessionSettings` to check) and writes it (`Manifest.write`, for Builder). It ignores `requires_chi_next`, a shipped bundle's release-time marker (`script/release/release_tools.rb`, docs/releasing.md) |
35
+ | `Manifest` | parses/validates `manifest.yml` (`name:` is the bundle's name and a directory under `.bundles/`, so a path such as `../x`, `a/b`, `.` or `..` is refused: `name: … is not a bundle name`; YAML that doesn't parse or isn't a mapping is a `ValidationError` too; files, hooks as `BundleHook`, plugin, scripts, needs, requires_chi, includes, context_providers, session_settings: the keys a session's `chi --set` may give, a list or a mapping of them to types (`SESSION_SETTING_TYPES`), recorded at install in `manifest.json` (`session_settings`, `session_setting_types`) for `BundleSessionSettings` to check) and writes it (`Manifest.write`, for Builder). It ignores `requires_chi_next`, a shipped bundle's release-time marker (`script/release/release_tools.rb`, docs/releasing.md) |
36
36
  | `Installer` | one install/upgrade/dry run of one bundle (below) |
37
37
  | `AssetInstaller` | the parts that live in `.bundles/<name>/`: hooks, guardrail rules, plugin, scripts; returns `AssetInstaller::Installed` (the paths the record keeps) |
38
38
  | `Merger` | 3-way classification per file: `install`, `fast_forward`, `keep`, `noop`, `conflict` |
@@ -63,7 +63,7 @@ The checks live in `Hooks::BundleLoader.load` and `Plugin::Loader.load_bundle`;
63
63
  plugin file: build, `register`, `commit!`), which raise instead of logging: the bundle test kit
64
64
  (`Samagotchi::Testing::Bundle`) loads a bundle's source through them, unchecked. `Samagotchi::Testing::Install` is the
65
65
  checked path in a sandbox: the Installer, then `ExtensionLoad` (bundle hooks, then `load_plugins`), returning the
66
- Installer's warnings, this bundle's load failures and what it registered (`Install.registered`, also the parity
66
+ Installer's warnings, this bundle's load failures and what it registered (`Install.registered`, an `Install::Registered` value, also the parity
67
67
  spec's shape). A load failure carries `LoadFailures::Failure#location` (the first backtrace frame inside the
68
68
  bundle, or a SyntaxError's own line), which `Install` maps from the installed copy (`.bundles/<name>/plugin/`,
69
69
  `hooks/`) back to the source dir.