samagotchi 0.71.0 → 0.73.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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +70 -1
  3. data/docs/architecture.md +15 -9
  4. data/docs/cli.md +12 -5
  5. data/docs/configuration.md +23 -13
  6. data/docs/hooks.md +5 -3
  7. data/docs/internals/anthropic.md +1 -1
  8. data/docs/internals/background-tasks.md +2 -2
  9. data/docs/internals/engine.md +38 -13
  10. data/docs/internals/llm-context-live.md +1 -1
  11. data/docs/internals/prompt-caching.md +6 -0
  12. data/docs/plugins.md +18 -7
  13. data/docs/sessions.md +5 -3
  14. data/docs/testing.md +10 -1
  15. data/lib/samagotchi/bootstrap/config_writer.rb +2 -3
  16. data/lib/samagotchi/bridge.rb +5 -2
  17. data/lib/samagotchi/broadcast/scope_card.rb +2 -2
  18. data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -7
  19. data/lib/samagotchi/bundles/claude-peer/plugin.rb +2 -2
  20. data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -6
  21. data/lib/samagotchi/bundles/codex-peer/plugin.rb +2 -2
  22. data/lib/samagotchi/bundles/coordinator/manifest.yml +8 -6
  23. data/lib/samagotchi/bundles/coordinator/plugin.rb +10 -0
  24. data/lib/samagotchi/bundles/coordinator/skill_coordinator.md +2 -1
  25. data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
  26. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  27. data/lib/samagotchi/config.rb +7 -25
  28. data/lib/samagotchi/context_note.rb +2 -6
  29. data/lib/samagotchi/effective_model.rb +1 -1
  30. data/lib/samagotchi/engine.rb +91 -213
  31. data/lib/samagotchi/engine_peers.rb +2 -0
  32. data/lib/samagotchi/event_routes.rb +83 -0
  33. data/lib/samagotchi/hook_routes.rb +16 -37
  34. data/lib/samagotchi/host_config.rb +1 -1
  35. data/lib/samagotchi/host_keys.rb +0 -2
  36. data/lib/samagotchi/paths.rb +13 -0
  37. data/lib/samagotchi/plugin/api.rb +11 -5
  38. data/lib/samagotchi/plugin/peer.rb +5 -8
  39. data/lib/samagotchi/prompt_files.rb +2 -4
  40. data/lib/samagotchi/session.rb +7 -1
  41. data/lib/samagotchi/session_chain.rb +24 -0
  42. data/lib/samagotchi/session_continue.rb +3 -1
  43. data/lib/samagotchi/session_manager.rb +6 -16
  44. data/lib/samagotchi/session_model_settings.rb +227 -0
  45. data/lib/samagotchi/session_setup.rb +2 -1
  46. data/lib/samagotchi/system_prompt.rb +18 -13
  47. data/lib/samagotchi/terminal_ui/attached_loop.rb +30 -14
  48. data/lib/samagotchi/terminal_ui/formatting.rb +4 -0
  49. data/lib/samagotchi/tool_activity.rb +1 -0
  50. data/lib/samagotchi/tool_declarations.rb +4 -0
  51. data/lib/samagotchi/toolkit.rb +55 -24
  52. data/lib/samagotchi/toolkit_binding.rb +7 -5
  53. data/lib/samagotchi/toolkit_command.rb +28 -5
  54. data/lib/samagotchi/tools/builtins.rb +2 -1
  55. data/lib/samagotchi/tools/delegate.rb +32 -6
  56. data/lib/samagotchi/tools/execute.rb +1 -15
  57. data/lib/samagotchi/tools/list_sessions.rb +2 -2
  58. data/lib/samagotchi/tools/peers.rb +5 -2
  59. data/lib/samagotchi/tools/register_reminder.rb +1 -0
  60. data/lib/samagotchi/tools/registry.rb +1 -1
  61. data/lib/samagotchi/tools/task_runtime.rb +69 -6
  62. data/lib/samagotchi/tools/tool_index.rb +15 -4
  63. data/lib/samagotchi/turn_note.rb +4 -1
  64. data/lib/samagotchi/version.rb +1 -1
  65. data/lib/samagotchi/web/app.rb +40 -13
  66. data/lib/samagotchi/web/public/app.js +53 -28
  67. data/lib/samagotchi/web/public/bubbles.js +5 -3
  68. data/lib/samagotchi/web/public/cards_place.js +6 -10
  69. data/lib/samagotchi/web/public/chains.js +22 -2
  70. data/lib/samagotchi/web/public/chip_popover.js +3 -1
  71. data/lib/samagotchi/web/public/composer.js +13 -6
  72. data/lib/samagotchi/web/public/context_chips.js +25 -12
  73. data/lib/samagotchi/web/public/data.js +14 -1
  74. data/lib/samagotchi/web/public/format.js +5 -0
  75. data/lib/samagotchi/web/public/index.html +39 -2
  76. data/lib/samagotchi/web/public/notice_links.js +55 -0
  77. data/lib/samagotchi/web/public/question_cards.js +33 -1
  78. data/lib/samagotchi/web/public/sessions_list.js +2 -3
  79. data/lib/samagotchi/web/public/stage_frame.js +1 -1
  80. data/lib/samagotchi/web/public/stage_slots.js +35 -3
  81. data/lib/samagotchi/web/public/stage_view.js +17 -6
  82. data/lib/samagotchi/web/public/stream_handlers.js +6 -3
  83. data/lib/samagotchi/web/public/timing.js +1 -1
  84. data/lib/samagotchi/web/public/toolkit_chip.js +3 -1
  85. data/lib/samagotchi/web/public/turn_view.js +3 -2
  86. data/lib/samagotchi/web/session_hub.rb +8 -0
  87. metadata +4 -1
data/docs/plugins.md CHANGED
@@ -568,19 +568,25 @@ end
568
568
  - Each task runs once per session start. A `-p … --non-interactive` run
569
569
  starts them with its turn and shows nothing but the answer.
570
570
 
571
- ### `chi.toolkit(name, base:, add: [], remove: [])`
571
+ ### `chi.toolkit(name, base:, add: [], remove: [], prompt: nil)`
572
572
 
573
573
  A toolkit sessions can be put on: `base`'s tools (`full`, `lean`, `child`, a
574
- config or bundle toolkit) with `add` declared and `remove` left out. Users pick
574
+ config or bundle toolkit) with `add` declared and `remove` left out, and
575
+ `prompt`: text the sessions on it get in their system prompt, verbatim (nil
576
+ inherits the base's, `""` clears it; at most 2000 characters, and one that
577
+ isn't a String or is longer is a load error). Users pick
575
578
  it like chi's own (`chi --toolkit NAME`, `toolkit.default`, `/toolkit NAME`),
576
579
  and the plugin with `ctx.use_toolkit` (below). Names are `a-z`, `0-9`, `_` and
577
580
  `-`; one chi has (`full`, `lean`, `child`) or another bundle declared is a
578
581
  load error. A session saved on a toolkit whose bundle is gone declares every
579
582
  tool (full) until it comes back, with a warning at its first turn. See
580
- [Toolkits](configuration.md#toolkits).
583
+ [Toolkits](configuration.md#toolkits). `prompt:` needs chi 0.72.0 (an older
584
+ chi refuses the keyword, a load error: a bundle that passes it sets
585
+ `requires_chi: ">= 0.72.0"`).
581
586
 
582
587
  ```ruby
583
588
  chi.toolkit "review", base: "lean", add: %w[delegate delegate_result]
589
+ chi.toolkit "coding", base: "child", prompt: "- Be concise in your responses"
584
590
  ```
585
591
 
586
592
  ### `chi.tool_source(call:, acts_as:, label:, wait:, prefix:)`
@@ -645,9 +651,10 @@ index = Samagotchi::Tools::ToolIndex
645
651
  ([acts_as:](#guardrails)), even when another bundle has a tool of that
646
652
  name (only the core-tool drop applies). A call made before its tool is published is
647
653
  named by `acts_as` from the names alone; a bare name with `prefix` is
648
- taken as an `acts_as` as given. The `acts_as:` hook must give exactly the
654
+ taken as an `acts_as` as given (without `prefix`, a bare name is refused:
655
+ call it as `<group>/<tool>`). The `acts_as:` hook must give exactly the
649
656
  `acts_as` the source publishes for that tool: a call whose tool isn't the
650
- one guardrails were given is refused ("the MCP tools changed while the call
657
+ one guardrails were given is refused ("the tool list changed while the call
651
658
  waited"), so a hook that disagrees refuses every call.
652
659
  - **The row** shows `label` (default: the bundle's name) and
653
660
  `<group>/<tool> args…`. `@source.declared_names` and `declared_tokens` say
@@ -1672,6 +1679,11 @@ by themselves there.
1672
1679
  or a turn already there) and the session's toolkit has no `delegate`, the
1673
1680
  reply says to run `/toolkit coordinator`. A continue of a coordinator link
1674
1681
  keeps the toolkit.
1682
+ - **The coding toolkit.** The bundle (0.5.0) also declares `coding`: `child`
1683
+ with the prompt text `- Be concise in your responses` / `- Show file paths
1684
+ clearly when working with files`. The skill starts build and fix children
1685
+ on it (`delegate` with `toolkit: "coding"`); reviews, research and plans
1686
+ stay on `toolkit.child`, as they need the length.
1675
1687
  - **`/children [all]`** shows the session's children as a card, newest first,
1676
1688
  one line each: `` `ab12cd34` · running · fix/flaky · "fix the flaky spec" ``,
1677
1689
  `` `9a8b7c6d` · done · feat/x · reported · "All 12 specs pass" `` (`not
@@ -1761,8 +1773,7 @@ Claude pairs; without one the bundle costs one tool declaration.
1761
1773
  - In the web, selecting text offers **Confirm with Claude**
1762
1774
  ([`chi.annotation`](#chiannotationlabel-command-template-quote-send-false)):
1763
1775
  it fills the composer with `/claude validate` and the quote, for you to
1764
- add what needs confirming. On a chi without `chi.annotation` (0.62–0.65)
1765
- the bundle loads without the pill.
1776
+ add what needs confirming.
1766
1777
  - `/claude last` sends the model's last completed answer as context. Mid-turn
1767
1778
  it is the answer before the running turn, never the turn's partial text;
1768
1779
  a cancelled or failed turn has no answer, so the one before it goes.
data/docs/sessions.md CHANGED
@@ -215,8 +215,8 @@ A session can **continue** an earlier one: "the next day of the same routine" (a
215
215
  - **Its open delegates move to the new link** instead: each direct delegate that is running, waiting for an answer, has a live worker or a reply its parent wasn't given, or has such a delegate below it (checked again after the recap wait, so one delegated meanwhile moves too). It moves with its own delegates, stays unarchived, and from then on is the new link's: its card folds under the new link, its reports ring the new link (an idle one runs a wake turn for them, as for its own delegates), `delegate_result` and the `⑂` chip count it there, and what its parent was already told (`delegates.json`) and any report still waiting come along, so nothing is reported twice. A moved delegate gets a context note from `session chain`, `Your parent session is now 4b7e9a01 (…), the next link of its session chain: …`, as its system prompt keeps naming the parent it started with. The move writes `<child>/parent.json` (`{parent_id, from, at}`) beside the child's session file, never the file itself, so a live child worker saving its own copy changes nothing; every reader applies it (a later move overwrites it; it is never removed). A continue is **refused for an open delegate that can't move** (`SessionContinue::Refused`, naming it): one whose live worker runs an older chi (`older chi worker 0.46.1; restart it`: `chi sessions restart <id>`, or stop it) or that is open in a chi REPL (close it there). An older `chi web` or CLI still running shows a moved delegate under the archived link until it is restarted.
216
216
  - **If the continue fails part-way:** a new link that fails to start gives its moved delegates back before it is deleted (each gets a note saying so; its worker is stopped first, if one started, and a worker that won't stop is reported, the link left), with any report one of them rang it with meanwhile; the previous link, back in the lists, is woken for reports waiting. A continue that dies in the middle leaves a `move.json` in the new link's folder: the new link's next worker finishes that move (only the delegates it names, and without a second note to one already told) as it starts. Until the new link's own worker holds it, a ring from a moved delegate doesn't start another (a `starting` marker in its folder, which that worker removes; a stale one, over 60 s old or from a process that is gone, is ignored). An open delegate below an archived delegate refuses the continue (`under archived …`): unarchive that one, or archive this one.
217
217
  - **Refused too:** a link that is continued already (naming the link that continues it; continue that one, or `last:<id>`), a previous link whose folder is gone, and one whose model's host can't run it when no default model is configured to fall back to. A refusal starts nothing and archives nothing; a new link that fails to start puts back what was archived. Every refusal that can be known up front (the archive's ones included) comes before the recap wait. One continue of a session runs at a time (a lock in its folder; other chains don't wait), so a double click finds the first one's link and is refused, while a `last:<id>` continue that waited follows the chain to its new end.
218
- - **Web:** `POST /api/sessions` takes `continues: "<id, prefix or last:id>"`, with `idle: true` (a new link waiting for its first message, previewed by the previous link's, or `preview`) or a `prompt`, and `model` (or `model_name`) for the new link's model, but not `dir`, `llm_context` or `thinking` (400 `invalid_continues`: the previous link's are taken). 201 is the new session's summary, `continues` set, with `moved`: the ids of the delegates that moved to it (`[]` for none), and `model_warning`: the default-model fallback's warning, then any about the model id, joined by `; ` (`null` for none); 404 `not_found`; 400 `ambiguous_id` for a prefix of several sessions, `invalid_model` for a given model whose host config.yml doesn't have (or a fallback with no default model); 409 `continued` with `next_id` (the link that continues it already, which the page opens instead), `open_children` with `ids` (open delegates that can't move), `folder_gone`, `busy`, `scratch` or `owned_by_tui`. Every session summary carries `continues`.
219
- - **The page:** the info bar's `continue →` continues the open session (`continues: <id>, idle: true`; it reads `continuing…` through the recap wait) and opens the new link with `Continue where we left off.` in the composer, not sent (the same line for every chain; a draft already there stays instead); the previous link is archived, a toast says so (`Continued 3fa2c1d0 in 4b7e9a01; 3fa2c1d0 is archived; 1 open delegate moved`), and a moved delegate's card folds under the new link. When the previous link's model's host is gone the link starts on the default model and the toast carries the server's `model_warning` too, and then stays until closed. A fresh link (one with no turn yet, however it was made: `continue →`, `chi send --new --continues`, a reload) shows the model picker in its composer with the link's model, until its first turn starts (a failed or canceled first turn ends it too); a pick runs `/model <name>` in the link (a command bubble; the info bar follows) and changes neither the start page's remembered model nor its Recent list, and a Send right after it waits for the `/model` to be taken. A link a chi REPL owns, or one continued already, shows none. A link continued already has no `continue →`: its `→` link goes to the next link (a stale page's continue answered 409 `continued` opens `next_id`). A continue refused for open delegates that can't move shows a toast that names each one (its preview and what the server says to do: `older chi worker 0.46.1; restart it`, `open in a chi REPL; close it there`), each a link to its page; any other refusal shows the server's words. The info bar of a link carries `← 2026-10-08` (the previous link's day) and, on an earlier link, `2026-10-10 →`, each a link with its preview and id in the tooltip. A link whose title is the opener and nothing else (its first message once sent) shows its chain's first link's title instead, on its card, in the popover and in its info bar's title line, display only (its messages and data keep the opener; its own title when the first link is gone). The lists fold a chain into the card of its latest listed link, whatever the earlier links' archived state (typing in an archived link brings it back, folded there still), their delegates with them: the card's `↩ day N` chip (`day N of M` when later links aren't listed) opens a popover of the other links, newest first (`day N`, its date, its recap's first sentence or its preview, `archived`), a click opens one; the chip shows a live dot or `waiting` for a link that has a worker or waits on the user, and a waiting link puts its chain's card first. All sessions folds nothing while a search is typed or "include archived" is on: every link is a card of its own then, its chip `day N of M`, with its own delegates folded in, so a search finds an earlier link and its delegates.
218
+ - **Web:** `POST /api/sessions` takes `continues: "<id, prefix or last:id>"`, with `idle: true` (a new link waiting for its first message, previewed by the previous link's, or `preview`) or a `prompt`, and `model` (or `model_name`) for the new link's model, but not `dir`, `llm_context` or `thinking` (400 `invalid_continues`: the previous link's are taken). 201 is the new session's summary, `continues` set, with `moved`: the ids of the delegates that moved to it (`[]` for none), and `model_warning`: the default-model fallback's warning, then any about the model id, joined by `; ` (`null` for none), and `opener`: the previous link's first typed message, in full, when that link continues one itself (`null` otherwise: a chain's first link's first message is its task); 404 `not_found`; 400 `ambiguous_id` for a prefix of several sessions, `invalid_model` for a given model whose host config.yml doesn't have (or a fallback with no default model); 409 `continued` with `next_id` (the link that continues it already, which the page opens instead), `open_children` with `ids` (open delegates that can't move), `folder_gone`, `busy`, `scratch` or `owned_by_tui`. Every session summary carries `continues`.
219
+ - **The page:** the info bar's `continue →` continues the open session (`continues: <id>, idle: true`; it reads `continuing…` through the recap wait) and opens the new link with an opener in the composer, not sent (a draft already there stays instead): the previous link's own first typed message when that link continues one (the 201's `opener`; a `!shell` line isn't taken), else `Continue where we left off.`. So whatever you sent on the last link comes back on the next one, and editing it once changes it for every later link; the previous link is archived, a toast says so (`Continued 3fa2c1d0 in 4b7e9a01; 3fa2c1d0 is archived; 1 open delegate moved`), and a moved delegate's card folds under the new link. When the previous link's model's host is gone the link starts on the default model and the toast carries the server's `model_warning` too, and then stays until closed. A fresh link (one with no turn yet, however it was made: `continue →`, `chi send --new --continues`, a reload) shows the model picker in its composer with the link's model, until its first turn starts (a failed or canceled first turn ends it too); a pick runs `/model <name>` in the link (a command bubble; the info bar follows) and changes neither the start page's remembered model nor its Recent list, and a Send right after it waits for the `/model` to be taken. A link a chi REPL owns, or one continued already, shows none. A link continued already has no `continue →`: its `→` link goes to the next link (a stale page's continue answered 409 `continued` opens `next_id`). A continue refused for open delegates that can't move shows a toast that names each one (its preview and what the server says to do: `older chi worker 0.46.1; restart it`, `open in a chi REPL; close it there`), each a link to its page; any other refusal shows the server's words. The info bar of a link carries `← 2026-10-08` (the previous link's day) and, on an earlier link, `2026-10-10 →`, each a link with its preview and id in the tooltip. A link made by `continue →` inherits the previous link's title when it is created, so its card keeps its chain's title whatever opener it is sent, and its top bar and browser tab show its card's title. A link whose title is the opener and nothing else (a `chi send --new --continues` link whose first message is `Continue where we left off.`) shows its chain's first link's title instead, on its card, in the popover and in its info bar's title line, display only (its messages and data keep the opener; its own title when the first link is gone). The lists fold a chain into the card of its latest listed link, whatever the earlier links' archived state (typing in an archived link brings it back, folded there still), their delegates with them: the card's `↩ day N` chip (`day N of M` when later links aren't listed) opens a popover of the other links, newest first (`day N`, its date, its recap's first sentence or its preview, `archived`), a click opens one; the chip shows a live dot or `waiting` for a link that has a worker or waits on the user, and a waiting link puts its chain's card first. All sessions folds nothing while a search is typed or "include archived" is on: every link is a card of its own then, its chip `day N of M`, with its own delegates folded in, so a search finds an earlier link and its delegates.
220
220
  - **CLI:** `chi send --new --continues (ID|PREFIX|last:ID) [--model M] [-m TEXT]` (see *Starting a session*).
221
221
  - **Which link:** an id, a unique prefix, or `last:<id or prefix>`, the latest link of the chain that session is in. It walks the links that still exist: a deleted link in the middle (`chi sessions delete`, the retention sweep of an unarchived one) ends the walk before it, so `last:` of an earlier link then names that earlier link, and continuing it starts a second line beside the links after the gap; name a link after the gap instead. A trigger names the chain this way: `chi send` to an archived link would bring it back (a human's input), not continue the chain.
222
222
 
@@ -226,7 +226,9 @@ A session file's `toolkit` (`{"name", "add", "remove"}`) is the toolkit it was c
226
226
  `toolkit.default`, `toolkit.child` for a delegate child, or `chi --toolkit` / `chi send --new --toolkit` (with
227
227
  `--tool` / `--no-tool`). It is never resolved again from config: a respawned or woken worker declares the same
228
228
  tools. `null` (a session from before toolkits) is `full`. A continue (`continue →`, `chi send --new --continues`)
229
- and a fork (`ctx.sessions.fork`) keep it, as they keep the thinking level; a delegate child gets `toolkit.child`.
229
+ and a fork (`ctx.sessions.fork`) keep it, as they keep the thinking level; a delegate child gets the kit its
230
+ `delegate` call names (`toolkit:`, any kit the parent knows) or `toolkit.child`; one its worker can't resolve
231
+ declares every tool (full). The file stores only the name: a kit's tools and prompt text come from its definition.
230
232
  `/toolkit` changes it, saved at once, from the next turn's start. See
231
233
  [Toolkits](configuration.md#toolkits).
232
234
 
data/docs/testing.md CHANGED
@@ -36,7 +36,10 @@ A select bound with `.value` instead reads right in both, but lit sets it before
36
36
  `npm test` runs every file in one process (`--test-isolation=none`; CI and local runs use Node 24): the files share
37
37
  `globalThis`, the shim's one happy-dom window and every module's state: a global a spec sets (stage_view's
38
38
  `ResizeObserver` stand-in) is there for the files after it, so set only harmless stand-ins and put anything else
39
- back (`t.mock` restores itself). One file: `node --test spec/web/public/NAME.test.js`.
39
+ back (`t.mock` restores itself). A test that sets off a module's timer (a flash, a highlight's end) fakes it
40
+ (`t.mock.timers.enable({ apis: ["setTimeout"] })`, then `tick`): a real one still pending after the last test keeps
41
+ node running until it fires (a 4 s one made the run 6 s for 2 s of work). One file:
42
+ `node --test spec/web/public/NAME.test.js`.
40
43
 
41
44
  Where a composer line goes is decided twice, in Ruby (`ComposerRoute`, both terminals) and in the web
42
45
  (`composer.js#routeComposerLine`). `spec/fixtures/composer_routes.json` holds the cases both answer: rspec
@@ -44,6 +47,12 @@ Where a composer line goes is decided twice, in Ruby (`ComposerRoute`, both term
44
47
  either router adds its row there; a row where a UI differs on purpose (or a found divergence) carries a `web` or
45
48
  `repl` override and a `note`.
46
49
 
50
+ A spec that waits for something uses `wait_until(timeout:)`, or `wait_until!(what, timeout:)` where no check
51
+ follows (stubs, helper threads: it raises "timed out after N s waiting for <what>") — never a loop with no deadline.
52
+ Every example also runs under a 120 s limit (`spec_helper`'s `around`; `integration`, `macos_build`, `warmup` and
53
+ `recap` examples are exempt), so a hang fails its example instead of the run. `SPEC_EXAMPLE_TIMEOUT=N` changes the
54
+ limit, `0` turns it off (a debugger session).
55
+
47
56
  ## While you work: affected specs
48
57
 
49
58
  ```sh
@@ -60,12 +60,11 @@ module Samagotchi
60
60
  attr_reader :path
61
61
 
62
62
  # @param path [String] config.yml (a symlink is written through)
63
- # @param env [Hash] the environment for the check (SAMAGOTCHI_HOSTS_JSON
64
- # is dropped: it would replace the file's hosts)
63
+ # @param env [Hash] the environment for the check
65
64
  # @param now [Proc] the time for the backup's name and the header
66
65
  def initialize(path:, env: ENV, now: -> { Time.now })
67
66
  @path = path
68
- @env = env.to_h.except("SAMAGOTCHI_HOSTS_JSON")
67
+ @env = env.to_h
69
68
  @now = now
70
69
  end
71
70
 
@@ -289,13 +289,16 @@ module Samagotchi
289
289
  # session state, the last answer (Engine#last_answer_message: one
290
290
  # message, not the conversation) and the cards, as one step of the event
291
291
  # log (one event_seq). Light whatever the session's length: GET tail.
292
- # @return [Hash] {session_id:, session_state_snapshot:, answer:, cards:, event_seq:, event_id:}
292
+ # The session's folder (working_directory) rides along: what the web
293
+ # checks the answer's file refs against without reading the session file.
294
+ # @return [Hash] {session_id:, working_directory:, session_state_snapshot:, answer:, cards:, event_seq:, event_id:}
293
295
  # +turn_id+: that turn's answer, not the newest (Engine#last_answer_message).
294
296
  def tail_frame(turn_id: nil)
295
297
  @engine.synchronize_events do
296
298
  seq = @engine.event_count
297
299
  state = @engine.session_state_snapshot.merge(event_seq: seq, event_id: event_id(seq))
298
- { session_id: @session_id, session_state_snapshot: state, answer: @engine.last_answer_message(turn_id: turn_id),
300
+ { session_id: @session_id, working_directory: @engine.session&.working_directory,
301
+ session_state_snapshot: state, answer: @engine.last_answer_message(turn_id: turn_id),
299
302
  cards: @cards.list, event_seq: seq, event_id: event_id(seq) }
300
303
  end
301
304
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "pathname"
4
4
  require_relative "../session"
5
- require_relative "../context_note"
5
+ require_relative "../paths"
6
6
  require_relative "../context_sources"
7
7
  require_relative "../git_head"
8
8
  require_relative "../recap_store"
@@ -78,7 +78,7 @@ module Samagotchi
78
78
  # back resolved (/private/tmp/… on macOS) while the session's folder
79
79
  # may go through a symlink (/tmp/…).
80
80
  def folder(cwd, project)
81
- return ContextNote.home_relative(cwd) if project.nil?
81
+ return Paths.tilde(cwd) if project.nil?
82
82
 
83
83
  cwd = real(cwd)
84
84
  project = real(project)
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: claude-peer
3
- version: 0.4.0
3
+ version: 0.4.1
4
4
  scope: system
5
5
  description: "Pair this session with a Claude Code session you work with: the claude_send tool and /claude send it a part of the work (a plan step, a diff, a finding) to validate or as context, and Claude's answer comes back as the claude-outbox context; /claude install-skill installs Claude's chi-peer skill, which pairs"
6
6
  trust_level: reviewed
@@ -10,15 +10,10 @@ files:
10
10
  description: "Send a part of the work to the paired Claude Code session (claude_send): only when the user asks, or to reply to its answer in a peer turn; validate, context or question; Claude's answer comes as the claude-outbox context"
11
11
  plugin:
12
12
  file: plugin.rb
13
- sha256: sha256:08000c9ccd8672ecae38fb4f53694ad3220caa8ed06de70cc4661ce7e882eecb
13
+ sha256: sha256:50f427d3862ae9d0d15693b4d887c528162b96c0320cc663df89d59d830d4660
14
14
  scripts:
15
15
  pair: sha256:35f3be2b466a5ba07591d64040bc5780fdd843b9b08737f8ea8a2fccb2102832
16
16
  chi-peer-SKILL.md: sha256:e991a91139a127a60f9a216043eb5864945fb211952a1e3c8db2068c6cc254f1
17
- # Two things need chi 0.63.0, and the bundle works without them on 0.62:
18
- # ctx.context.list's summary: (the /claude card's last answer; without it
19
- # the card shows no summary), and a pushed source's first wake: true push
20
- # waking the session (on 0.62 Claude's first answer arrives as a note
21
- # without waking; later ones wake).
22
17
  # The plugin needs Samagotchi::Plugin::Peer and chi.peer_source (chi 0.67.0).
23
18
  # An agreeing answer's closing wake (the skills say an OK wakes chi once)
24
19
  # and the plugin's OUTBOX (Plugin::Peer's rename of REVIEW) need chi 0.70.0.
@@ -40,8 +40,8 @@ require "samagotchi/plugin/peer"
40
40
  # sees it. The skill is scripts/chi-peer-SKILL.md beside this file, with the
41
41
  # installed scripts/pair path and the payload folder written in.
42
42
  #
43
- # In the web, selecting text offers "Confirm with Claude" (chi.annotation;
44
- # skipped on a chi without it, 0.62–0.65): it fills the composer with
43
+ # In the web, selecting text offers "Confirm with Claude" (chi.annotation):
44
+ # it fills the composer with
45
45
  # `/claude validate` and the quote, for the user to add what to confirm.
46
46
  # Such a text is titled by its first quoted line, not the quote's
47
47
  # `From your thinking:` label.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: codex-peer
3
- version: 0.3.0
3
+ version: 0.3.1
4
4
  scope: system
5
5
  description: "Pair this session with an OpenAI Codex CLI session you work with: the codex_send tool and /codex send it a part of the work (a plan step, a diff, a finding) to validate or as context, with codex queue, and Codex's answer comes back as the codex-outbox context; /codex install-skill installs Codex's chi-peer skill, which pairs"
6
6
  trust_level: reviewed
@@ -10,14 +10,10 @@ files:
10
10
  description: "Send a part of the work to the paired Codex CLI session (codex_send): only when the user asks, or to reply to its answer in a peer turn; validate, context or question; Codex's answer comes as the codex-outbox context"
11
11
  plugin:
12
12
  file: plugin.rb
13
- sha256: sha256:c36cba9ab4c87c1a5ec9fbb8f907286bb04410b2a85b56269e29392173160689
13
+ sha256: sha256:eb6888e3611b8ed42498cfecab5b430e0ef7a4c2dc7b3c090d015336aa039b11
14
14
  scripts:
15
15
  pair: sha256:251448a31959b5c916400eff7909e85c696855d5c16b6b10973ada40915c8051
16
16
  chi-peer-SKILL.md: sha256:7c392ac04c115ea1e762ad562b5c4c44c3eca71558ba5d9dfeaeabdd533b6a82
17
- # Codex's answer is a pushed source's first wake: true push, which wakes the
18
- # session from chi 0.63.0 (before, the first answer arrives as a note
19
- # without waking); ctx.context.list's summary: (the /codex card's last
20
- # answer) is 0.63.0 too.
21
17
  # The plugin needs Samagotchi::Plugin::Peer and chi.peer_source (chi 0.67.0).
22
18
  # An agreeing answer's closing wake (the skills say an OK wakes chi once)
23
19
  # and the plugin's OUTBOX (Plugin::Peer's rename of REVIEW) need chi 0.70.0.
@@ -42,8 +42,8 @@ require "samagotchi/plugin/peer"
42
42
  # pairing and replies (its default sandbox can't write chi's state); chi
43
43
  # never edits Codex's config.
44
44
  #
45
- # In the web, selecting text offers "Confirm with Codex" (chi.annotation;
46
- # skipped on a chi without it, 0.63–0.65): it fills the composer with
45
+ # In the web, selecting text offers "Confirm with Codex" (chi.annotation):
46
+ # it fills the composer with
47
47
  # `/codex validate` and the quote, for the user to add what to confirm.
48
48
  # Such a text is titled by its first quoted line, not the quote's
49
49
  # `From your thinking:` label.
@@ -1,15 +1,17 @@
1
1
  ---
2
2
  name: coordinator
3
- version: 0.4.0
3
+ version: 0.5.0
4
4
  scope: system
5
- description: "Coordinate parallel work: the coordinator skill (split the work, one worktree and child session per task, check each report, ask before each merge, a handoff memory to resume from), /coordinate <goal>, /coordinate resume, /coordinate end and /children; the coordinator toolkit (lean + delegate, delegate_result, list_sessions)"
5
+ description: "Coordinate parallel work: the coordinator skill (split the work, one worktree and child session per task, check each report, ask before each merge, a handoff memory to resume from), /coordinate <goal>, /coordinate resume, /coordinate end and /children; the coordinator toolkit (lean + delegate, delegate_result, list_sessions) and the coding toolkit for build children (child + be concise)"
6
6
  trust_level: reviewed
7
7
  files:
8
8
  skill_coordinator.md:
9
- sha256: sha256:2ef3faefa89cbb23ce89c76bd5d913e29dde9817edf08cd29a063aeafa698643
9
+ sha256: sha256:718cb76da10f9610a7582eddf1441590566836ebc6a9cb127b7f7847b17ceb92
10
10
  description: "Coordinate parallel work: split it, delegate each task wait:false in its own worktree (cwd:), check each report, ask the user before each merge; resume from a handoff_* memory"
11
11
  plugin:
12
12
  file: plugin.rb
13
- sha256: sha256:caaddd24fbec2c66fa0e88937478b7ec7799bac7c98d5dc7f71ffc3fc227953a
14
- # the coordinator toolkit needs chi.toolkit and ctx.use_toolkit (0.66.0)
15
- requires_chi: ">= 0.66.0"
13
+ sha256: sha256:3b5f766ad198862243c494e4c5aec7c4a0972e7644a6388bed843c5651f5ca0d
14
+ # the coordinator toolkit needs chi.toolkit and ctx.use_toolkit (0.66.0); the
15
+ # coding toolkit (chi.toolkit prompt:) and the skill's delegate toolkit: need
16
+ # the next chi.
17
+ requires_chi: ">= 0.72.0"
@@ -38,6 +38,13 @@
38
38
  # before the session's first model reply and outside a turn: otherwise it
39
39
  # says to run /toolkit coordinator, unless the session declares delegate
40
40
  # anyway.
41
+ #
42
+ # The coding toolkit is for build and fix children (the skill's delegate
43
+ # toolkit: "coding"): child's tools plus a prompt text, pi's two plain
44
+ # lines. In the harness evals (r5, r7: Qwen3.6-35B-A3B, lean, N=3) they cut
45
+ # the median time by a third at the same quality. Plain wording only: a
46
+ # scoped wording ("in this task, …") backfired (+50% thinking). Reviews,
47
+ # research and plans stay on child (no toolkit:): they need the length.
41
48
  class Plugin
42
49
  CARD_ID = "children"
43
50
  # A card takes 6 actions: Refresh and up to 5 Stops.
@@ -56,9 +63,12 @@ class Plugin
56
63
 
57
64
  TOOLKIT = "coordinator"
58
65
  TOOLKIT_TOOLS = %w[delegate delegate_result list_sessions].freeze
66
+ CODING_TOOLKIT = "coding"
67
+ CODING_PROMPT = "- Be concise in your responses\n- Show file paths clearly when working with files"
59
68
 
60
69
  def register(chi)
61
70
  chi.toolkit TOOLKIT, base: "lean", add: TOOLKIT_TOOLS
71
+ chi.toolkit CODING_TOOLKIT, base: "child", prompt: CODING_PROMPT
62
72
  chi.command "/children", "this session's child sessions: state, branch, last reply; all, stop <id>",
63
73
  anytime: true do |args, ctx|
64
74
  children_command(args.to_s.strip, ctx)
@@ -46,7 +46,8 @@ If the request is a new goal, say which handoffs are open (one line each) and go
46
46
  user if unsure. Split the work into independent tasks (at most session.max_children at once,
47
47
  default 4). Tell the user the split in a few lines and go on unless they object.
48
48
  2. For each task: from your own folder, `git worktree add ../<repo>-<task> -b <type>/<task>` with
49
- execute; then `delegate` with `wait: false`, `cwd:` that worktree's absolute path, and a
49
+ execute; then `delegate` with `wait: false`, `cwd:` that worktree's absolute path, `toolkit:
50
+ "coding"` for a build or fix task (none for a review, research or a plan), and a
50
51
  self-contained task. The task starts with "Work only in <absolute worktree path>: every cd, read,
51
52
  write, test and commit goes there, never the project root" and says what to report (the result,
52
53
  evidence: commands and what they printed, what isn't done) and "commit on the branch, don't merge,
@@ -57,7 +57,7 @@ Only the nested form is read. An env name used as a top-level key (`SAMAGOTCHI_D
57
57
  **Excluded maps** (YAML-only, not part of the flat registry; skipped by scalar loader):
58
58
 
59
59
  - `model_aliases:` map of alias → model id (`ModelRef.parse`, through `HostRegistry#resolve`). Keys lowercased on write (`ConfigFile.write_model_alias!`). Values may be bare `model` or qualified `host:model` (hybrid; only `:` names a host, `org/model` is an id). Aliases apply once: never point an alias at another alias.
60
- - `hosts:` map of `name → {host, port | url, transport, api, api_key_env, auth, dialect, profile, first_token_timeout, vision, sampling, thinking, enabled, models}` (`ConfigFile.hosts_config`, `host_registry.rb` `HostEntry`). Names lowercased; `url:` (http/https, optional path) replaces host/port, never both; `api_key_env:` names the env var holding the API key (never write a key into config.yml); `auth:` (`bearer|x-api-key`) is the header the key goes in (default `x-api-key` on `api: anthropic`, Bearer elsewhere); `dialect:` (`fireworks`, or `standard` for the default; `api: anthropic` only) adjusts the Messages wire for that provider (Bearer by default, no thinking `display`, no `cache_control`, `x-session-affinity`; `LLM::Dialect`, docs/configuration.md "Anthropic Messages hosts"); `transport` overrides `server.transport`; `first_token_timeout` (seconds, `0` = off; a negative or non-number warns and is ignored) overrides `server.first_token_timeout` for that host; `models:` (a map of model ids, or a plain list) declares ids the host serves whatever its `/v1/models` lists: no "doesn't list" warning, and a bare id routes there as if listed (`HostModel`); an entry may hold `price: {input, output, cache_read?, cache_write?}` in USD per 1M tokens (`ModelPrice`; an unset cache rate is the input rate), which estimates a cost the provider doesn't report or reports as 0 (`cost_estimate`, shown as `~$`), and `served: [ids] | any`, the models a gateway may answer that id with (no served-model warning for them); model settings stay in the root `models:`; a worker reads `hosts:` from the file as it starts (`SAMAGOTCHI_HOSTS_JSON` replaces it for a process it is set on; `SessionManager.spawn_options` unsets it for workers).
60
+ - `hosts:` map of `name → {host, port | url, transport, api, api_key_env, auth, dialect, profile, first_token_timeout, vision, sampling, thinking, enabled, models}` (`ConfigFile.hosts_config`, `host_registry.rb` `HostEntry`). Names lowercased; `url:` (http/https, optional path) replaces host/port, never both; `api_key_env:` names the env var holding the API key (never write a key into config.yml); `auth:` (`bearer|x-api-key`) is the header the key goes in (default `x-api-key` on `api: anthropic`, Bearer elsewhere); `dialect:` (`fireworks`, or `standard` for the default; `api: anthropic` only) adjusts the Messages wire for that provider (Bearer by default, no thinking `display`, no `cache_control`, `x-session-affinity`; `LLM::Dialect`, docs/configuration.md "Anthropic Messages hosts"); `transport` overrides `server.transport`; `first_token_timeout` (seconds, `0` = off; a negative or non-number warns and is ignored) overrides `server.first_token_timeout` for that host; `models:` (a map of model ids, or a plain list) declares ids the host serves whatever its `/v1/models` lists: no "doesn't list" warning, and a bare id routes there as if listed (`HostModel`); an entry may hold `price: {input, output, cache_read?, cache_write?}` in USD per 1M tokens (`ModelPrice`; an unset cache rate is the input rate), which estimates a cost the provider doesn't report or reports as 0 (`cost_estimate`, shown as `~$`), and `served: [ids] | any`, the models a gateway may answer that id with (no served-model warning for them); model settings stay in the root `models:`; a worker reads `hosts:` from the file as it starts (no environment variable sets hosts).
61
61
  - `models:` map of model id or alias → `{profile, vision, sampling, thinking, reasoning_history}` (`ConfigFile.model_settings`). `thinking:` (here, on a host, or `thinking.level` for every model) is `off|low|medium|high|default` (`Thinking`; unquoted `off` works, `on` is not a level; `default` = send nothing). Precedence: the session's own level (`/thinking LEVEL`, saved in the session; `chi --thinking` on a session run, `chi send --new --thinking`) > `chi web --thinking`/`SAMAGOTCHI_THINKING_LEVEL` > `models:` > `hosts.<name>.thinking` > `thinking.level` in config.yml > `default`. A `sampling:` key wins over the fields a level sends (`null` drops one); `docs/configuration.md` "Thinking". `reasoning_history:` (here or on a host) is `drop|turn|all`, which of its reasoning a chat host's model gets back. Precedence: the session's own level (`/reasoning-history LEVEL`, saved in the session) > `SAMAGOTCHI_REASONING_HISTORY` > `models:` > `hosts.<name>.reasoning_history` > `turn`; `docs/configuration.md` "Reasoning history". `/thinking` and `/reasoning-history` ask before a change that restarts the prompt cache; `--yes` applies it (`chi send` commands apply directly). `sampling:` (here or on a host) is a map of request fields passed to the provider as written (`temperature`, `top_p`, `presence_penalty`, `repeat_penalty`, …; a model's fields win over its host's per field; `null` = don't send; chi's own fields like `max_tokens`/`stream` are refused with a warning; `docs/configuration.md` "Sampling"). Keys match case-insensitively. `profile` (here or on a host) is `qwen36|gemma4`: the raw prompt format for native hosts. Precedence: `--profile`/`SAMAGOTCHI_MODEL_PROFILE` > `models:` > `hosts.<name>.profile` > the llama.cpp server's chat template > the name (`qwen`/`gemma`) > `qwen36` (`ModelProfile.resolve`). Set one when a model's name hides its family (e.g. a Qwen fine-tune under another name on mlx, which has no template to read).
62
62
  - `hooks:` map of `hooks_dir` + per-event lists `{path, on_error}` (`Hooks::Loader.load`). `hooks_dir` may start with `~`.
63
63
  - `guardrails:` tool-call rules (`docs/guardrails.md`; read in `Engine#guardrail_rules`, parsed by `Guardrails::Rules.parse`): `enabled` (bool, default true; `false` drops rules and hooks' asks, a deny still applies; config.yml only, like every `guardrails` setting: no env variable), `parent_approvals` (`off`, the default: a parent agent's `chi answer` can only deny an approval; `once`: it may give "Allow once" too; a convention for an honest parent, not a security boundary, and the user's choice: set it only when the user asks), `rules:` (list), `disable:` (a rule id or a list of them, `id` or `bundle:id`, switching off a bundle's or config rule without editing it), `small_models` (`auto`, the default: small = 32B or less read from the name; a glob or list of globs; `[]` for none). A rule takes only `id`, `tool`, `command`, `path`, `git`, `models`, `verdict`, `reason`, `scopes`: `id` required; at least one of `tool` (a name, `shell` = execute + task_create, a `File.fnmatch` glob like `"mcp_*"`, or a list), `command` (a Ruby regex on the shell command), `path` (a glob, or `outside_repo`: outside the session's repo, tmp folders and memory files excepted), `git` (only `outside_repo`: a shell call runs commit/add/reset/… in another checkout); `models` (optional: `small`, a model-name glob, or a list; without it the rule is for every model); `verdict` `ask|deny`; `scopes` (for `ask`) a subset of `once, session, repo, rule`. **Any parse error (an unknown key, a bad regex, no verdict) makes chi deny every tool call** until fixed, so validate with `YAML.safe_load` and keep the list shape. Rules are read again on the next tool call after config.yml or a bundle's rules change: no restart needed. Installed bundles' rule files (`chi bundle install guardrails`) add to them; `/guardrails` lists what loaded.
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: samagotchi-system
3
- version: 0.71.0
3
+ version: 0.73.0
4
4
  scope: system
5
5
  description: Default system memories — identity, self map, config modification protocol, memory guide and the delegated-session rules
6
6
  files:
7
7
  identity.md: sha256:29c2f7df9f6c5acbc5ec17000a4490741e55e426af15ca0d81c8706edd9bd027
8
8
  self_map.md: sha256:b5b3166c91f6f2af2563dd9db5f9959cda66747d907017acd0776fcd2fe29049
9
- config_modification_protocol.md: sha256:c7decf27af999066195f83e71222a6ae294f13174ced9664a21cd1dabf8b854a
9
+ config_modification_protocol.md: sha256:66792f788cdbb8ee0252f2c387c96bd75266817bb75f721aaa181b602db97137
10
10
  memory_guide.md: sha256:c48eea744db69e4f3e9bb415e9a03a8c78bbcaa1205ad3b1fb157a079f92d22e
11
11
  delegated.md: sha256:a0d6ddcd4a8be06526d964898611775fd251cfd9d145a9d16afaccaa503597bd
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "yaml"
4
- require "json"
5
4
  require "fileutils"
6
5
  require "uri"
7
6
  require_relative "atomic_file"
@@ -877,13 +876,13 @@ module Samagotchi
877
876
  HOST_NAME_RE = /\A[a-z0-9][a-z0-9._-]*\z/i
878
877
  ENV_NAME_RE = /\A[A-Za-z_][A-Za-z0-9_]*\z/
879
878
 
880
- # config.yml's hosts: (or a SAMAGOTCHI_HOSTS_JSON override) by lowercased
881
- # name; an entry that is disabled or invalid (warned) is left out. No
882
- # entry left: "default", from server.host/port/transport.
879
+ # config.yml's hosts: by lowercased name; an entry that is disabled or
880
+ # invalid (warned) is left out. No entry left: "default", from
881
+ # server.host/port/transport.
883
882
  # @return [Hash{String => HostConfig}]
884
883
  def hosts_config(env: ENV, path: global_path(env: env))
885
884
  data = read_yaml(env: env, path: path)
886
- raw_hosts = hosts_source(data, env)
885
+ raw_hosts = data[HOSTS_KEY] if data.is_a?(Hash)
887
886
 
888
887
  normalized = {}
889
888
  if raw_hosts.is_a?(Hash)
@@ -933,11 +932,11 @@ module Samagotchi
933
932
  private_class_method :warn_duplicate_host_models
934
933
 
935
934
  # The names (lowercased) of the hosts entries with enabled: false, which
936
- # hosts_config leaves out. Read from the same source: config.yml's
937
- # hosts:, or a SAMAGOTCHI_HOSTS_JSON override.
935
+ # hosts_config leaves out, from config.yml's hosts:.
938
936
  # @return [Array<String>]
939
937
  def disabled_host_names(env: ENV, path: global_path(env: env))
940
- raw = hosts_source(read_yaml(env: env, path: path), env)
938
+ data = read_yaml(env: env, path: path)
939
+ raw = data[HOSTS_KEY] if data.is_a?(Hash)
941
940
  return [] unless raw.is_a?(Hash)
942
941
 
943
942
  raw.filter_map { |name, cfg| name.to_s.strip.downcase if cfg.is_a?(Hash) && HostConfig.disabled?(cfg) }
@@ -945,23 +944,6 @@ module Samagotchi
945
944
  []
946
945
  end
947
946
 
948
- # config.yml's hosts: as written, or the SAMAGOTCHI_HOSTS_JSON override
949
- # set on this process (specs; SessionManager.spawn_options unsets it for
950
- # a worker, which reads the file).
951
- def hosts_source(data, env)
952
- raw = data[HOSTS_KEY] if data.is_a?(Hash)
953
- env_json = env["SAMAGOTCHI_HOSTS_JSON"].to_s.strip
954
- return raw if env_json.empty?
955
-
956
- parsed = begin
957
- JSON.parse(env_json)
958
- rescue StandardError
959
- nil
960
- end
961
- parsed.is_a?(Hash) ? parsed : raw
962
- end
963
- private_class_method :hosts_source
964
-
965
947
  # Resolve the `recap:` section through the Config registry so scalar
966
948
  # recap settings share the single precedence path (CLI > ENV > file >
967
949
  # default). Returns false when explicitly disabled (file `recap: false`
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "time"
4
+ require_relative "paths"
4
5
 
5
6
  module Samagotchi
6
7
  # A context note: background text pushed into a session from outside (the
@@ -39,7 +40,7 @@ module Samagotchi
39
40
  def label(source:, from_session: nil, from_cwd: nil)
40
41
  return source.to_s unless from_session
41
42
 
42
- "session #{from_session.to_s[0, 6]}#{" (#{home_relative(from_cwd)})" if from_cwd}"
43
+ "session #{from_session.to_s[0, 6]}#{" (#{Paths.tilde(from_cwd)})" if from_cwd}"
43
44
  end
44
45
 
45
46
  def label_of(message)
@@ -78,10 +79,5 @@ module Samagotchi
78
79
  rescue ArgumentError
79
80
  nil
80
81
  end
81
-
82
- def home_relative(path)
83
- home = Dir.home
84
- path.to_s.start_with?("#{home}/") || path.to_s == home ? path.to_s.sub(home, "~") : path.to_s
85
- end
86
82
  end
87
83
  end
@@ -30,7 +30,7 @@ module Samagotchi
30
30
  #
31
31
  # Each setting reader rescues its own failure and gives the fallback the
32
32
  # Engine has always given (Unresolved); building one raises as
33
- # HostRegistry#resolve does (the Engine's #model_setting gives the
33
+ # HostRegistry#resolve does (SessionModelSettings#model_setting gives the
34
34
  # fallback then).
35
35
  EffectiveModel = Data.define(:registry, :name, :typed, :target, :names) do
36
36
  # @param registry [HostRegistry]