samagotchi 0.70.0 → 0.72.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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +83 -1
  3. data/docs/architecture.md +16 -10
  4. data/docs/cli.md +7 -4
  5. data/docs/configuration.md +24 -14
  6. data/docs/hooks.md +1 -1
  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 +4 -2
  14. data/docs/testing.md +12 -3
  15. data/lib/samagotchi/bootstrap/config_writer.rb +2 -3
  16. data/lib/samagotchi/bridge/turn_accumulator.rb +4 -1
  17. data/lib/samagotchi/bridge.rb +5 -2
  18. data/lib/samagotchi/broadcast/scope_card.rb +2 -2
  19. data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -7
  20. data/lib/samagotchi/bundles/claude-peer/plugin.rb +2 -2
  21. data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -6
  22. data/lib/samagotchi/bundles/codex-peer/plugin.rb +2 -2
  23. data/lib/samagotchi/bundles/coordinator/manifest.yml +8 -6
  24. data/lib/samagotchi/bundles/coordinator/plugin.rb +10 -0
  25. data/lib/samagotchi/bundles/coordinator/skill_coordinator.md +2 -1
  26. data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
  27. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  28. data/lib/samagotchi/config.rb +7 -25
  29. data/lib/samagotchi/context_note.rb +2 -6
  30. data/lib/samagotchi/effective_model.rb +1 -1
  31. data/lib/samagotchi/engine.rb +91 -213
  32. data/lib/samagotchi/engine_peers.rb +2 -0
  33. data/lib/samagotchi/event_routes.rb +83 -0
  34. data/lib/samagotchi/hook_routes.rb +16 -37
  35. data/lib/samagotchi/host_config.rb +1 -1
  36. data/lib/samagotchi/host_keys.rb +0 -2
  37. data/lib/samagotchi/paths.rb +13 -0
  38. data/lib/samagotchi/plugin/api.rb +11 -5
  39. data/lib/samagotchi/plugin/peer.rb +5 -8
  40. data/lib/samagotchi/prompt_files.rb +2 -4
  41. data/lib/samagotchi/session.rb +2 -1
  42. data/lib/samagotchi/session_manager.rb +6 -16
  43. data/lib/samagotchi/session_model_settings.rb +227 -0
  44. data/lib/samagotchi/session_setup.rb +2 -1
  45. data/lib/samagotchi/sessions_command.rb +12 -3
  46. data/lib/samagotchi/system_prompt.rb +18 -13
  47. data/lib/samagotchi/terminal_ui/attached_loop.rb +36 -15
  48. data/lib/samagotchi/terminal_ui/formatting.rb +4 -0
  49. data/lib/samagotchi/tool_activity.rb +20 -1
  50. data/lib/samagotchi/tool_declarations.rb +4 -0
  51. data/lib/samagotchi/tool_output_cut.rb +11 -2
  52. data/lib/samagotchi/toolkit.rb +60 -22
  53. data/lib/samagotchi/toolkit_binding.rb +7 -5
  54. data/lib/samagotchi/toolkit_command.rb +28 -5
  55. data/lib/samagotchi/tools/builtins.rb +2 -1
  56. data/lib/samagotchi/tools/delegate.rb +32 -6
  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 +13 -2
  61. data/lib/samagotchi/tools/task_wait.rb +4 -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 +18 -17
  66. data/lib/samagotchi/web/message_parts.rb +1 -1
  67. data/lib/samagotchi/web/public/answer_refs.js +1 -0
  68. data/lib/samagotchi/web/public/app.js +20 -18
  69. data/lib/samagotchi/web/public/bubbles.js +1 -1
  70. data/lib/samagotchi/web/public/cards_place.js +6 -10
  71. data/lib/samagotchi/web/public/chains.js +1 -1
  72. data/lib/samagotchi/web/public/chip_popover.js +3 -1
  73. data/lib/samagotchi/web/public/composer.js +13 -6
  74. data/lib/samagotchi/web/public/context_chips.js +25 -12
  75. data/lib/samagotchi/web/public/data.js +5 -1
  76. data/lib/samagotchi/web/public/format.js +5 -0
  77. data/lib/samagotchi/web/public/index.html +1 -0
  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_view.js +8 -2
  81. data/lib/samagotchi/web/public/stream_handlers.js +6 -3
  82. data/lib/samagotchi/web/public/timing.js +1 -1
  83. data/lib/samagotchi/web/public/toolkit_chip.js +3 -1
  84. data/lib/samagotchi/web/public/turn_html.js +55 -3
  85. data/lib/samagotchi/web/session_hub.rb +8 -0
  86. metadata +3 -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
@@ -45,7 +45,7 @@ chi sessions archive ID... # hide from every list, ke
45
45
  chi sessions list --archived # archived sessions too, marked [archived]
46
46
  chi sessions delete [--force] ID... # for good; --force stops a live worker first
47
47
  chi sessions prune [--dry-run] [--days N] [--keep N] [--keep-status running,...] [--test-only]
48
- chi sessions clean [--dry-run] [--days N] # test sessions: all, or older than N days
48
+ chi sessions clean [--dry-run] [--days N] [--all] # test sessions: all, or older than N days; --all: every kind, as prune
49
49
  chi sessions stats ID [--format text|json] # cost, tokens and progress, without a model turn
50
50
  ```
51
51
 
@@ -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
@@ -56,8 +65,8 @@ script/affected-specs A..B # another range (A...B: from their merge-b
56
65
  A builder runs these while working, not the full suite: the specs the change touched, the specs named after the
57
66
  files it changed (`lib/samagotchi/foo/bar.rb` → `spec/foo/bar_spec.rb`, `bar_*_spec.rb`…), the specs that read
58
67
  docs or source as text (its `RULES`: add one when a full run catches what the names missed) and the golden of a changed
59
- file's layer (any `terminal_ui/*.rb` → `spec/terminal_ui_golden_spec.rb`), then the other name
60
- matches (a spec named after several changed files first, e.g. `engine_toolkit_spec.rb` for `engine.rb` and
68
+ file's layer (any `terminal_ui/*.rb` → `spec/terminal_ui_golden_spec.rb`; `turn_flow.rb` and `turn_note.rb`, whose
69
+ words the REPL prints, pick it by a rule), then the other name matches (a spec named after several changed files first, e.g. `engine_toolkit_spec.rb` for `engine.rb` and
61
70
  `toolkit.rb`; then one beside the file, so `plugin/context.rb` picks `spec/samagotchi/plugin/*context*` before
62
71
  `spec/guardrails/context_spec.rb`), then the specs that name a changed file's class. The default range is the merge-base with main (or origin/main) plus uncommitted and untracked files.
63
72
  The Ruby specs go to stdout; the node tests to run (`npm test` targets), "web changed: also run npm run e2e" and a
@@ -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
 
@@ -18,8 +18,11 @@ module Samagotchi
18
18
  # its terminal event, the same event that brings its messages into the
19
19
  # session, so a snapshot shows a turn in exactly one of the two.
20
20
  class TurnAccumulator
21
+ # @param max_output_chars [Integer] a tool part's output past this keeps
22
+ # its head and tail: a reload's cap, so a joined row is the row a
23
+ # reload draws
21
24
  # @param clock [#call] monotonic seconds: a tool call's duration
22
- def initialize(max_output_chars: KernelLoop::DEFAULT_MAX_TOOL_OUTPUT_CHARS,
25
+ def initialize(max_output_chars: ToolOutputCut::WEB_PREVIEW_CHARS,
23
26
  clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) })
24
27
  @max_output_chars = max_output_chars
25
28
  @clock = clock
@@ -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.70.0
3
+ version: 0.72.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]