samagotchi 0.32.0 → 0.34.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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +75 -1
  3. data/README.md +4 -1
  4. data/docs/context.md +8 -1
  5. data/docs/guardrails.md +31 -1
  6. data/docs/hooks.md +7 -4
  7. data/docs/memory.md +17 -1
  8. data/docs/plugins.md +79 -3
  9. data/docs/releasing.md +3 -2
  10. data/docs/sessions.md +9 -3
  11. data/lib/samagotchi/answer_command.rb +2 -1
  12. data/lib/samagotchi/archive_store.rb +1 -15
  13. data/lib/samagotchi/bridge.rb +8 -3
  14. data/lib/samagotchi/bundles/coordinator/manifest.yml +14 -0
  15. data/lib/samagotchi/bundles/coordinator/plugin.rb +114 -0
  16. data/lib/samagotchi/bundles/coordinator/skill_coordinator.md +60 -0
  17. data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
  18. data/lib/samagotchi/bundles/mcp/manifest.yml +3 -3
  19. data/lib/samagotchi/bundles/mcp/plugin.rb +10 -11
  20. data/lib/samagotchi/bundles/system/delegated.md +1 -1
  21. data/lib/samagotchi/bundles/system/manifest.yml +4 -4
  22. data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
  23. data/lib/samagotchi/bundles/system/self_map.md +4 -2
  24. data/lib/samagotchi/child_reports.rb +31 -10
  25. data/lib/samagotchi/children_status.rb +157 -0
  26. data/lib/samagotchi/client_id.rb +52 -0
  27. data/lib/samagotchi/context_absorber.rb +2 -1
  28. data/lib/samagotchi/context_fetch.rb +14 -45
  29. data/lib/samagotchi/context_sources.rb +20 -10
  30. data/lib/samagotchi/cut_policy.rb +58 -0
  31. data/lib/samagotchi/engine.rb +24 -108
  32. data/lib/samagotchi/engine_peers.rb +30 -0
  33. data/lib/samagotchi/guardrail_wiring.rb +27 -2
  34. data/lib/samagotchi/guardrails/approvals.rb +7 -0
  35. data/lib/samagotchi/guardrails/child_boundary.rb +103 -0
  36. data/lib/samagotchi/guardrails/context.rb +32 -0
  37. data/lib/samagotchi/guardrails/parent_approvals.rb +0 -2
  38. data/lib/samagotchi/guardrails/shared_git.rb +124 -0
  39. data/lib/samagotchi/guardrails.rb +1 -0
  40. data/lib/samagotchi/kernel_loop.rb +9 -22
  41. data/lib/samagotchi/llm/chat_loop.rb +11 -22
  42. data/lib/samagotchi/memory_bundle/builder.rb +35 -1
  43. data/lib/samagotchi/memory_bundle/installer.rb +6 -2
  44. data/lib/samagotchi/memory_bundle/manifest.rb +28 -5
  45. data/lib/samagotchi/memory_bundle/provenance.rb +2 -1
  46. data/lib/samagotchi/memory_paths.rb +10 -0
  47. data/lib/samagotchi/plugin/api.rb +2 -2
  48. data/lib/samagotchi/plugin/sessions.rb +47 -2
  49. data/lib/samagotchi/process_group.rb +157 -0
  50. data/lib/samagotchi/prompt_warmup.rb +20 -1
  51. data/lib/samagotchi/question_desk.rb +2 -1
  52. data/lib/samagotchi/relay_verifier.rb +2 -4
  53. data/lib/samagotchi/reply_wait.rb +9 -4
  54. data/lib/samagotchi/send_command.rb +2 -1
  55. data/lib/samagotchi/session_manager.rb +2 -4
  56. data/lib/samagotchi/steer.rb +27 -19
  57. data/lib/samagotchi/steer_cut.rb +125 -0
  58. data/lib/samagotchi/system_prompt.rb +20 -8
  59. data/lib/samagotchi/terminal_ui/attached_loop.rb +5 -4
  60. data/lib/samagotchi/terminal_ui/formatting.rb +21 -12
  61. data/lib/samagotchi/terminal_ui.rb +2 -1
  62. data/lib/samagotchi/tool_declarations.rb +9 -5
  63. data/lib/samagotchi/tools/builtins.rb +2 -2
  64. data/lib/samagotchi/tools/delegate.rb +105 -16
  65. data/lib/samagotchi/tools/delegate_wait.rb +55 -13
  66. data/lib/samagotchi/tools/execute.rb +10 -81
  67. data/lib/samagotchi/tools/task_runtime.rb +25 -38
  68. data/lib/samagotchi/turn_note.rb +9 -1
  69. data/lib/samagotchi/version.rb +1 -1
  70. data/lib/samagotchi/web/app.rb +2 -1
  71. data/lib/samagotchi/web/public/app.js +19 -20
  72. data/lib/samagotchi/web/public/data.js +3 -0
  73. data/lib/samagotchi/web/public/question_card.js +3 -4
  74. data/lib/samagotchi/web/public/shown_texts.js +13 -0
  75. data/lib/samagotchi/web/public/stable_html.js +18 -0
  76. data/lib/samagotchi/web/public/turn_events.js +9 -2
  77. data/lib/samagotchi/web/public/turn_view.js +1 -1
  78. data/lib/samagotchi/worker.rb +40 -13
  79. metadata +14 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d9af1ef94626f6ec7c44f8a7a0ad46421eca3c770b9b0bc6dab78664ec3a9cd9
4
- data.tar.gz: bb2db5c510efa064f5c377335d83b6df450a67f0bafa8c70e4160ef97dc2d874
3
+ metadata.gz: c6f812c6128ae3ffaf91d9cc04716a77d351fb57793a7d9a4863e675f1f2337f
4
+ data.tar.gz: 25e7600fe6dc335b7d28320795a03ea9e8527185f6a0dd918787777458402f39
5
5
  SHA512:
6
- metadata.gz: af61e4ce113e2dea9a3ecf9bedc4d7335aa35f7aca81e37734a952e150f79930e9a6451e1c22c507762caafa76c29fd324fe3544e901cf70fa6783547cf5924e
7
- data.tar.gz: fd9ca3f931a144e48e8a120202000cf845806aaaedb676d3dde3a094e03432bc19f79eadac054363f7248c257bec7fe74ba3ae99f8b682492215dae64a4e7392
6
+ metadata.gz: 322e9f5a5b553f7ed1f2955307b8d5bd8c901a355ac3ffcdff6b3fcb5013b28011c7c330d9496486338c77d2c0f58e2854393d4cb40ec33d00ed2516a38bd066
7
+ data.tar.gz: f6048763f53d9b865858842acaf60587d5e57a365aaba759c3c85fcf69532227c4b201fe0f878da3e48191dd43856637dad29537203a3dfd7159756caac6f778
data/CHANGELOG.md CHANGED
@@ -8,6 +8,78 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.34.0] - 2026-10-06
12
+
13
+ ### Added
14
+
15
+ - `delegate cwd:` starts a child in another folder of the session's repository: a worktree the model made with
16
+ `git worktree add`, or a subfolder. Another repository is refused.
17
+ - Plugins: `ctx.sessions.children` lists the session's children (state, branch, last reply and whether the parent
18
+ was given it), and `ctx.sessions.stop(id)` stops one of its own children.
19
+ - Bundles: a `files:` entry in `manifest.yml` may carry a `description:`, which the install writes into the
20
+ memory's `index.md` line.
21
+ - The `coordinator` bundle: chi coordinates parallel work. `/coordinate <goal>` (or asking for it) has chi split
22
+ the work into tasks, each in a child session in its own git worktree, check each report and ask you before each
23
+ merge. `/children` shows the session's children: state, branch, last reply and whether it was reported, with a
24
+ Stop button each.
25
+ - Guardrails: a delegate child asks before it changes anything outside its folder (its worktree): a write there,
26
+ git that changes the main checkout or a sibling worktree, a command run from or naming one of them. It's built in,
27
+ in every mode and without the guardrails bundle, and answered once or for the child's session.
28
+
29
+ ### Changed
30
+
31
+ - The `delegate` and `delegate_result` tools tell the model to start a child with `wait: false` unless its turn
32
+ can't go on without the reply, then end its turn or keep talking with you: the reply comes back by itself as a
33
+ delegate report. `delegate_result` is only for a reply the turn needs now.
34
+
35
+ ### Fixed
36
+
37
+ - A wake turn (a context source's change, a delegate's report) that fails no longer tells the model the user's
38
+ message went unanswered: it says the wake wasn't answered, and that no other wake turn starts until you write.
39
+ For a delegate's report it adds that the report comes again with your next message.
40
+ - A delegate wait that timed out or was canceled says the child's reply still comes as a delegate report, instead
41
+ of steering the model into waiting again with `delegate_result`; so does a question's result.
42
+ - `delegate` with `wait: false` in a session that gets no delegate reports (a `--no-shared` REPL, `-p`) says
43
+ `delegate_result` waits for the reply, instead of promising a report that never comes.
44
+ - In a linked git worktree the system prompt no longer names the repository's main checkout ("Project root"), which
45
+ led a model to work and commit there: it names this checkout's top and says to work only in it. Project memories
46
+ and session lists still follow the repository.
47
+ - `delegate` refused for too many running children tells a session that gets delegate reports to end its turn (a
48
+ report frees a slot), instead of to wait with `delegate_result`.
49
+ - At a repository's root the system prompt names the checkout the project's commands run in, and calls the project
50
+ memories folder your notes about the project, not its files: a model no longer goes looking for the repository
51
+ in the memories folder.
52
+
53
+ Update with `chi update`: it installs the new `coordinator` bundle for the `dev` profile (dev 0.3.0) and updates mcp
54
+ (0.4.5: stops an MCP server through chi's own process-group handling).
55
+
56
+ ## [0.33.0] - 2026-10-06
57
+
58
+ ### Changed
59
+
60
+ - Input from a client chi doesn't know (a script calling the web API with its own id, say) is no longer taken
61
+ for the user's: the model reads it as automatic input, not your message, and it never cuts in on a thinking
62
+ model. The TUI and the web label a prompt by its sender: "chi send", "chi answer", "plugin", or "automatic";
63
+ "user" is left for your own lines.
64
+
65
+ ### Fixed
66
+
67
+ - `task_stop` no longer signals a process group chi didn't start: a missing or reused pid in a task record could
68
+ stop chi or other processes.
69
+ - A delegate child that crashes still wakes its idle parent.
70
+ - `chi send <id> --wait` to a stopped session with attached context prints the answer instead of "no answer".
71
+ - `chi bundle build` keeps a bundle's `scripts:` and `context_providers:`, so a rebuilt bundle still attaches its URLs.
72
+ - A context source's wake is no longer lost when a later update that doesn't ask to wake comes in before the
73
+ session reads it.
74
+ - A failed wake turn leaves a plain note in the conversation; a reload no longer draws an empty turn for it.
75
+ - Web: the info bar no longer swallows a click on "copy chi --attach" or Restart while a turn runs, and its
76
+ tooltips stay up.
77
+ - Web: an image the server refuses shows why; a 409 that isn't "open in a chi REPL" no longer switches the page to
78
+ the REPL notice.
79
+ - Web: an answer equal to an earlier turn's ("Done." twice) is shown.
80
+
81
+ Update with `chi update`.
82
+
11
83
  ## [0.32.0] - 2026-10-06
12
84
 
13
85
  ### Added
@@ -1636,7 +1708,9 @@ and long-lived sessions.
1636
1708
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
1637
1709
  hotkey panel that send selected text or the clipboard to your sessions.
1638
1710
 
1639
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.32.0...HEAD
1711
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.34.0...HEAD
1712
+ [0.34.0]: https://github.com/dm1try/samagotchi/compare/v0.33.0...v0.34.0
1713
+ [0.33.0]: https://github.com/dm1try/samagotchi/compare/v0.32.0...v0.33.0
1640
1714
  [0.32.0]: https://github.com/dm1try/samagotchi/compare/v0.31.0...v0.32.0
1641
1715
  [0.31.0]: https://github.com/dm1try/samagotchi/compare/v0.30.0...v0.31.0
1642
1716
  [0.30.0]: https://github.com/dm1try/samagotchi/compare/v0.29.0...v0.30.0
data/README.md CHANGED
@@ -145,7 +145,10 @@ never takes orders from. The web and the attached terminal show it as a dim
145
145
  a note reaches (`id<TAB>description`). Agents can do the same: `list_sessions`
146
146
  finds another session, `send_note` tells it something, and `delegate` hands a
147
147
  task to a child session that runs in parallel and reports back only its final
148
- reply (a normal session: `chi --attach <id>` steers it).
148
+ reply (a normal session: `chi --attach <id>` steers it). With the `coordinator`
149
+ bundle (in `dev`), `/coordinate <goal>` has chi split the work into tasks, each
150
+ in a child session in its own git worktree, check each report and ask you
151
+ before each merge; `/children` shows the children and their state.
149
152
 
150
153
  `chi send` is the other half: the text goes in as your message, the same as
151
154
  typing it in the attached terminal or the web composer, and a turn runs. Piped
data/docs/context.md CHANGED
@@ -135,7 +135,9 @@ attaches a URL; the web never adds a command.
135
135
  A change can start a turn by itself, so the agent tells you about it while
136
136
  you're away from the terminal. All of these must hold:
137
137
 
138
- - the source asked (`"wake": true`; a plain-text source never does);
138
+ - the source asked (`"wake": true`; a plain-text source never does), in
139
+ any change since the session last heard of it: a later change that doesn't
140
+ ask doesn't take the wake back;
139
141
  - `context.wake` is on (default `true`);
140
142
  - the session's worker is up and idle: no turn running, nothing queued, no
141
143
  question or continue offer waiting, not just started;
@@ -152,6 +154,11 @@ update note, whose last line becomes:
152
154
  Every UI labels the turn `context <name> changed`, and the web's
153
155
  notifications count it as a finished turn however short.
154
156
 
157
+ A wake turn that fails goes back to before it and keeps the update as a
158
+ plain note; the model reads `[SYSTEM: the previous turn failed before any
159
+ answer: … The wake turn for the change in attached context <name> was not
160
+ answered; chi starts no other wake turn until the user writes.]`.
161
+
155
162
  ## GitHub PRs: the github-pr bundle
156
163
 
157
164
  ```sh
data/docs/guardrails.md CHANGED
@@ -11,7 +11,8 @@ are offered.
11
11
  Who votes, in order:
12
12
 
13
13
  1. `before_tool_call` hooks (Ruby; see [Hooks](hooks.md#guardrails-from-a-hook)).
14
- 2. Core checks: a required guardrail that failed to load, then protected paths.
14
+ 2. Core checks: a required guardrail that failed to load, then protected paths, then (a delegate child only)
15
+ [the child boundary](#a-delegate-childs-boundary).
15
16
  3. YAML rules: `config.yml`'s `guardrails:` section, then installed bundles' rule files (by bundle name).
16
17
 
17
18
  The UI shows the tool line first, then the approval under it.
@@ -189,6 +190,35 @@ Built in, for `write`, `edit` and `memory_write` (symlinks resolved):
189
190
  `execute` can still reach them; the guardrails bundle asks about shell
190
191
  commands that name them.
191
192
 
193
+ ## A delegate child's boundary
194
+
195
+ Built in, for [delegate](sessions.md#delegating) children only, in every
196
+ mode (also with `guardrails.enabled: false`) and without the guardrails
197
+ bundle: a child asks before it changes anything outside its folder, the work
198
+ tree top of the folder it was started in (a worktree from `delegate cwd:`,
199
+ else the parent's checkout). Rule `child-boundary`, source `core`; it asks when
200
+
201
+ - `write` or `edit` names a path outside it (tmp dirs and memory files don't count);
202
+ - `execute` or `task_create` runs mutating git in another dir (`git -C`, `cd`,
203
+ `GIT_DIR`: what `git: outside_repo` matches), or a command that isn't
204
+ read-only starts outside it (its `cwd:`) or names a path in the repository's
205
+ other checkouts (the main one and sibling worktrees, read from
206
+ `<common git dir>/worktrees/*/gitdir`) or in its common git dir;
207
+ - `execute` or `task_create` runs git that changes what every checkout shares,
208
+ even from its own worktree: a `git config` write (not `--get`, `--list`,
209
+ `--worktree`), `update-ref`, `worktree add|remove|move|prune`, `stash
210
+ drop|clear|pop`, creating or deleting a tag, and deleting, forcing or
211
+ overwriting a branch other than its own.
212
+
213
+ Read-only commands (`git -C ../app log`, `cat ../app/x`) and paths elsewhere
214
+ (`/usr/bin`, `~/.gem`) never ask. The answer is once or for the child's
215
+ session only: a repo or rule answer would hold in every worktree, for every
216
+ child, so a stored repo or rule approval doesn't let it through either. Its
217
+ ask goes before the rules, so with the guardrails bundle in strict mode the
218
+ card still names `child-boundary`. The parent (never a delegate), forks and
219
+ ordinary sessions don't get it. Like the rules it reads command text: a
220
+ `sh -c` body, a script or `$(…)` isn't looked into.
221
+
192
222
  ## Rules in config.yml
193
223
 
194
224
  ```yaml
data/docs/hooks.md CHANGED
@@ -122,9 +122,12 @@ Its `source` is the hook's bundle (a config or turn hook's label otherwise).
122
122
  Lines typed into a running turn are saved as `{role: "user", kind: "input",
123
123
  content: "…"}`: part of that turn, not a turn of their own. They carry a
124
124
  `source` when they are not the user's own: `chi_send` (`chi send -m`),
125
- `parent_agent` (a delegate's follow-up) or `plugin_send` (`ctx.sessions.send`).
126
- The model reads them with the same kind of header (`[Steer from the user, …]`,
127
- `[Steer sent with chi send, …]`, and so on).
125
+ `parent_agent` (a delegate's follow-up), `plugin_send` (`ctx.sessions.send`),
126
+ or `automatic:<client id>` for a client id chi doesn't know (an unknown sender
127
+ is never taken for the user). The model reads them with the same kind of
128
+ header (`[Steer from the user, …]`, `[Steer sent with chi send, …]`, and so
129
+ on; `[Automatic input from <client id>, sent mid-task; not your user's
130
+ message. …]` for an unknown one).
128
131
 
129
132
  Timing: a notice from `:after_turn` or `:session_end` shows after the turn's
130
133
  end line. A question from `:before_tool_call` shows **before** the tool
@@ -417,7 +420,7 @@ stripped of surrounding whitespace, except file text, an edit's
417
420
  | `list_sessions` | — | — | `cwd` |
418
421
  | `send_note` | `text` | — | `session` |
419
422
  | `context_read` | `name` | — | `offset`, `limit` |
420
- | `delegate` | `task` | — | `model`, `session`, `wait`, `timeout` |
423
+ | `delegate` | `task` | — | `model`, `session`, `cwd`, `wait`, `timeout` |
421
424
  | `delegate_result` | — | — | `session`, `timeout` |
422
425
  | `ask_user_question` | `question` | — | `question`, `options`, `header`, `multi_select`, `allow_freeform` |
423
426
 
data/docs/memory.md CHANGED
@@ -102,6 +102,22 @@ for a file another bundle installed.
102
102
  `- **memory_guide** · system · 2026-10-03 · 5120 · from samagotchi-system —
103
103
  …`. `memory_write`, `write` and `edit` keep it. A bundle installed by chi
104
104
  0.16 or earlier gets it on its next install or upgrade.
105
+ - **A bundle can give a memory its index description.** A `files:` entry
106
+ may be a mapping instead of a sha line:
107
+
108
+ ```yaml
109
+ files:
110
+ identity.md: sha256:…
111
+ skill_coordinator.md:
112
+ sha256: sha256:…
113
+ description: "Coordinate parallel work: …"
114
+ ```
115
+
116
+ The install writes that description into the memory's `index.md` line
117
+ (after `— `), so the model can tell what a shipped skill is for from the
118
+ index alone. The bundle's record keeps it, `chi bundle build` writes the
119
+ mapping back, and `rake bundles:sha` refreshes the nested sha. A file
120
+ with a plain sha line keeps the description its line already has.
105
121
  - **Uninstall moves, it doesn't delete.** The bundle's memory files go to
106
122
  `$XDG_CONFIG_HOME/samagotchi/memories/.bundles/.trash/<bundle>-<YYYYmmdd-HHMMSS>/`,
107
123
  and the output names them and the dir:
@@ -133,7 +149,7 @@ profiles, meta bundles that install a set of them:
133
149
  | profile | bundles |
134
150
  |---|---|
135
151
  | `core` | loop-guard, check-in, guardrails: the recommended safety set |
136
- | `dev` | known-names, mcp, btw, skills, source-links |
152
+ | `dev` | known-names, mcp, btw, skills, source-links, github-pr, coordinator |
137
153
 
138
154
  `chi bootstrap` installs `core` and, on a terminal, offers `dev`. An existing
139
155
  install gets neither by itself: `chi bundle install core` (or `dev`).
data/docs/plugins.md CHANGED
@@ -496,7 +496,7 @@ full label, `plugin.rb (bundle my-bundle)`.
496
496
  | `ctx.stop_turn(reason)` | stop the running turn after a warn notice with the reason; true when it stopped one now. In a `:before_tool_call` block it also denies that call |
497
497
  | `ctx.stop_generation(reason)` | cut the generation that is streaming: the turn goes on and the model is asked again ([hooks.md](hooks.md#watching-the-stream)). Shows nothing: post your own notice. True when it cut one now |
498
498
  | `ctx.ask_model(messages:, prompt:, …)` | a side answer from the session's model: see [Side answers](#side-answers-ctxask_model) |
499
- | `ctx.sessions` | fork, send to and read other sessions: see [Other sessions](#other-sessions-ctxsessions) |
499
+ | `ctx.sessions` | fork, send to, read, list and stop other sessions: see [Other sessions](#other-sessions-ctxsessions) |
500
500
  | `ctx.context` | attach outside text to this session and list it: see [Attached context](#attached-context-ctxcontext) |
501
501
 
502
502
  The Engine itself is never handed to a plugin.
@@ -608,7 +608,9 @@ llama.cpp serves on a native host too.
608
608
  id = ctx.sessions.fork(messages: ctx.messages + [{ role: "user", content: q }, { role: "model", content: a }],
609
609
  title: "btw: #{q}")
610
610
  ctx.sessions.send(id, "go on from here")
611
- ctx.sessions.read(id) # => {id:, title:, status:, parent_id:, running:, messages:}
611
+ ctx.sessions.read(id) # => {id:, title:, status:, parent_id:, running:, messages:}
612
+ ctx.sessions.children # => [{id:, short_id:, state:, branch:, last_reply:, reported:, ...}]
613
+ ctx.sessions.stop(id) # one of this session's own children
612
614
  ```
613
615
 
614
616
  - `fork(messages:, title: nil, prompt: nil)` starts a child session in its
@@ -629,6 +631,23 @@ ctx.sessions.read(id) # => {id:, title:, status:, parent_id:, running:, message
629
631
  - `read(id)` gives a session now: from its worker when one runs (with a
630
632
  running turn so far, `running: true`), else as saved. `messages` has no
631
633
  system prompt.
634
+ - `children(all: false)` lists this session's children, newest first, as
635
+ frozen Hashes: `{id:, short_id:, title:, state:, waiting:, live:,
636
+ delegate:, cwd:, branch:, last_reply:, last_reply_at:, reported:,
637
+ updated_at:, archived:}`.
638
+ - `state` is `waiting` (a question or approval is open; `waiting` says
639
+ which), `running`, `failed` (the last turn failed or the worker
640
+ crashed), `stopped`, `done` (the last turn ended with a reply) or `idle`.
641
+ - `delegate` is true for a `delegate` child, false for a fork. Archived
642
+ children are listed only with `all: true`.
643
+ - `branch` is the branch checked out in the child's folder (read from its
644
+ `HEAD`, a short sha when detached; nil outside git). `last_reply` is the
645
+ first line of its newest reply, and `reported` says whether this session
646
+ was already given that reply (a delegate report or a wait).
647
+ - It reads every session's file: call it on demand, not in a loop.
648
+ - `stop(id)` stops one of this session's own children, as
649
+ `chi sessions stop` does, and waits up to 2 s for its worker to let go. It
650
+ returns the child's id. Any other session raises.
632
651
  - Each raises `Samagotchi::Plugin::Sessions::Error` with the reason.
633
652
 
634
653
  ## Attached context: `ctx.context`
@@ -1120,6 +1139,62 @@ and a summary of what changed in counts, authors and states only; it wakes
1120
1139
  the session for a review requesting changes, checks turning red, or the PR
1121
1140
  merged or closed. See [Attached context](context.md#github-prs-the-github-pr-bundle).
1122
1141
 
1142
+ ## The coordinator bundle
1143
+
1144
+ ```sh
1145
+ chi bundle install coordinator # in the dev profile
1146
+ ```
1147
+
1148
+ chi as a coordinator of parallel work, the way you would run several agents
1149
+ yourself: split the work, one git worktree and child session per task, keep
1150
+ talking while they run, check each report, and merge only with your OK. It
1151
+ works in a session with a worker (plain `chi`, the web). A `--no-shared` REPL
1152
+ or `-p` gets no delegate reports, so the children's replies wouldn't come back
1153
+ by themselves there.
1154
+
1155
+ - **The skill.** `skill_coordinator` (system scope; its index line says what
1156
+ it is for, from the manifest's `description:`) holds the steps. The model
1157
+ creates each worktree with `execute` (`git worktree add ../<repo>-<task> -b
1158
+ <branch>`), starts a child there with `delegate wait: false, cwd:`, tells
1159
+ you what started and ends its turn. When a child's report comes, it checks
1160
+ the branch (`git log`, `git diff --stat`, the tests the child names) before
1161
+ telling you. It asks before each merge (`ask_user_question`), checks that its
1162
+ own folder is on the default branch and that branch's log first (another
1163
+ agent may have merged the branch already), merges `--ff-only` and never
1164
+ pushes unless you ask. After a merge it stops the
1165
+ child and removes only the worktrees and branches it made.
1166
+ - **`/coordinate <goal>`** sends this session a turn asking the model to
1167
+ follow the skill for the goal (in a `--no-shared` REPL it shows the text to
1168
+ send yourself). Asking "do this in parallel" works too: the index line leads
1169
+ the model to the skill.
1170
+ - **`/children [all]`** shows the session's children as a card, newest first,
1171
+ one line each: `` `ab12cd34` · running · fix/flaky · "fix the flaky spec" ``,
1172
+ `` `9a8b7c6d` · done · feat/x · reported · "All 12 specs pass" `` (`not
1173
+ reported yet` when the parent wasn't given that reply), `` `77aa66bb` ·
1174
+ waiting (approval) · open it: chi --attach 77aa66bb ``. A fork is marked
1175
+ `fork`; `all` adds archived children. It runs during a turn too. The card
1176
+ has Refresh and a `Stop <id>` per running or waiting child (up to 5);
1177
+ `/children stop <id>` stops one of this session's own children (by session
1178
+ id, as `chi sessions stop` does) and shows the card again.
1179
+ - **Children stay in their worktree.** A child asks before it changes
1180
+ anything outside its worktree (your checkout, a sibling's), in every
1181
+ guardrails mode and without the guardrails bundle ([a delegate child's
1182
+ boundary](guardrails.md#a-delegate-childs-boundary)); you answer on its
1183
+ card, or on the parent's while it waits.
1184
+ - **Merges.** The parent's merge in its own checkout is a plain `git
1185
+ merge`; to be asked before every merge, add a rule ([Rules in
1186
+ config.yml](guardrails.md#rules-in-configyml)):
1187
+
1188
+ ```yaml
1189
+ guardrails:
1190
+ rules:
1191
+ - id: git-merge
1192
+ tool: shell
1193
+ command: '\bgit\s+merge\b'
1194
+ verdict: ask
1195
+ reason: merging a child's branch
1196
+ ```
1197
+
1123
1198
  ## Shutdown
1124
1199
 
1125
1200
  When the REPL exits, or a session's worker exits (an idle exit, `/exit`, a
@@ -1181,7 +1256,8 @@ gem.
1181
1256
  - `chi bundle diff <name> [plugin.rb]`: shows the installed base and the
1182
1257
  file on disk.
1183
1258
  - `chi bundle build --name <name>`: puts the installed plugin (and
1184
- `requires_chi`) into the built bundle.
1259
+ `requires_chi`, its `scripts/` and `context_providers:`) into the built
1260
+ bundle.
1185
1261
  - `chi bundle uninstall <name>`: removes the plugin with the bundle.
1186
1262
 
1187
1263
  ## Not yet
data/docs/releasing.md CHANGED
@@ -13,7 +13,7 @@ can run the whole release; the user approves the notes before the tag and the
13
13
  and `rake release:check` enforce it). The system bundle upgrades itself when
14
14
  chi starts.
15
15
  - **Every other shipped bundle** (btw, check-in, guardrails, known-names,
16
- loop-guard, mcp, skills, source-links, github-pr, and the profiles core and dev) has its own semver in its
16
+ loop-guard, mcp, skills, source-links, github-pr, coordinator, and the profiles core and dev) has its own semver in its
17
17
  `manifest.yml` and, when it needs a newer chi, a `requires_chi:` line. They never upgrade by themselves: users run
18
18
  `chi update` (all of them, with the gem) or `chi bundle upgrade NAME`. So:
19
19
  - a change to a bundle's files bumps that bundle's `version:`
@@ -49,7 +49,8 @@ as the GitHub release body.
49
49
  | Task | What it does |
50
50
  | --- | --- |
51
51
  | `rake bundles:sha` | Recomputes the sha256 lines of every shipped bundle manifest (`files:`, `hooks:`, `plugin:`). |
52
- | `rake bundles:check` | Sha lines match; a bundle changed since the last `v*` tag has a higher version (skipped, with a note, before the first tag). Warns (doesn't fail) when such a bundle's `requires_chi` still admits the tagged chi: raise it if the change uses anything new. |
52
+ | `rake bundles:check` | Sha lines match; a bundle changed since the last `v*` tag has a higher version (skipped, with a note, before the first tag). Warns (doesn't fail) when such a bundle's `requires_chi` still admits the tagged chi: raise it if the change uses anything new. The release workflow and `release:check` run it in full. |
53
+ | `rake 'bundles:check[ci]'` | The CI workflow's (every push and PR): the same, except the version check leaves out the system bundle, which is changed and unbumped between releases (`release:bump` moves its version with VERSION). A task argument, not `CI=1`: GitHub sets `CI` in the release workflow too. |
53
54
  | `rake release:draft_changelog` | A draft Unreleased section from the commit subjects since the last tag, grouped. Stdout only. |
54
55
  | `rake release:bump[X.Y.Z]` | VERSION, the system manifest, Gemfile.lock, CHANGELOG. No commit. |
55
56
  | `rake release:check` | Clean tracked tree, VERSION == system bundle, a CHANGELOG section, `bundles:check`, `rake lint` (RuboCop), rspec + npm test, `gem build`, then a clean install into a temp GEM_HOME that runs `chi --version`, `chi self` and `chi bundle list` with a temp HOME. Needs the network (gem dependencies). |
data/docs/sessions.md CHANGED
@@ -120,6 +120,9 @@ system message marked `kind: turn_note` (the UIs don't show it; they showed the
120
120
  before any answer: network error after 2 attempts (host main: Errno::ECONNREFUSED). The message went back to
121
121
  the user, who may send it again.]` in the REPL and attached mode, where the prompt is given back;
122
122
  `… The user's last message was not answered.]` after `-p`, where the prompt stays in the session;
123
+ `… The wake turn for the change in attached context pr-7 was not answered; chi starts no other wake turn until
124
+ the user writes.]` after a wake turn chi started (a context source's change, a delegate's report), not the user
125
+ (for a delegate's report it adds `chi keeps the report and brings it again with the user's next message.`);
123
126
  - cancelled (Ctrl-C, the web's stop): `[SYSTEM: the previous turn was cancelled (ctrl-c) after 12s; the reply
124
127
  above ends where it was cut off.]` (an answer or only narration had streamed), or `…; no answer had been shown.]` when only thinking had streamed;
125
128
  - ended with nothing visible: `[SYSTEM: the previous turn ended with no visible answer (thinking only, or
@@ -181,8 +184,10 @@ chi send --wait 3fa2 # sends nothing: waits fo
181
184
 
182
185
  The `delegate` tool hands a task to a **child session**: an ordinary chi session in a worker of its own, started in the parent's `working_directory` with the task, verbatim, as its first user message, on the parent's model unless the call names one (`model:` takes a name or an alias, resolved as `--model` is; an unknown host comes back as the tool's `Error:` line; an id the host's saved model list doesn't have starts the child with a `Warning: host 'h' doesn't list model 'x' (did you mean: …?)` line in the result). Children run in parallel and only their **final reply** comes back to the parent: the child's newest `output/<timestamp>.txt` file, which the worker writes at the end of each turn that produced visible text, cut head-and-tail like an `execute` result. Nothing of the child's trace enters the parent's context. Nothing is hidden: a child shows in `chi sessions list` (`↳ <parent>`), the web (a `↳` chip on its card, `delegated by` in its info bar, listed right after its parent in the all-sessions view) and the `list_sessions` tool (`child`; the parent shows as `parent` in the child's own list); the user can `chi --attach <child id>` and steer it mid-run, and `chi send` and `send_note` reach it like any session.
183
186
 
184
- - `delegate(task, model:, session:, wait:, timeout:)`: starts a child and, by default, waits for its reply (`wait: false` returns at once, so several children can run side by side, and the reply comes back by itself as a *delegate report*, below; `timeout`, default 600 s, leaves a slow child running; 0 checks once without waiting). With `session:` (a child's id or prefix) the task goes to that child as a follow-up instead, delivered like `chi send` (client id `delegate:<parent short id>`, shown as `delegate>` in the child's UIs); only this session's children take one.
185
- - `delegate_result(session:, timeout:)`: waits for a child's next reply: the one named, or the newest running child (`timeout` as for `delegate`: 0 checks once and returns at once). The reply comes back once; a later call waits for the next one.
187
+ - `delegate(task, model:, session:, cwd:, wait:, timeout:)`: starts a child and, by default, waits for its reply. `wait: false` is the usual way for work that takes a while, several children side by side, or to keep talking with the user: it returns at once, the parent ends its turn or works on, and the reply comes back by itself as a *delegate report* (below), in a turn of its own if the parent is idle. The tool descriptions tell the model so, and to wait (`wait: true`, `delegate_result`) only when its turn can't go on without the reply. `timeout`, default 600 s, leaves a slow child running; 0 checks once without waiting. With `session:` (a child's id or prefix) the task goes to that child as a follow-up instead, delivered like `chi send` (client id `delegate:<parent short id>`, shown as `delegate>` in the child's UIs); only this session's children take one.
188
+ - `cwd:` starts the child in another folder of the parent's repository: a linked worktree (`git worktree add ../<repo>-<task> -b <branch>`, which the model runs with `execute` first, under guardrails; chi runs no git here) or a subfolder; a relative path is taken from the parent's folder. The folder must exist, have the parent's project root (the same common git dir, `MemoryPaths.project_root`) and be in a work tree: a git dir (`.git`, `.git/objects`, a bare layout's `.bare`) or a bare layout's container is refused (`git rev-parse --is-inside-work-tree`, read-only, decides). Outside git only a subfolder of the parent's folder is allowed. Otherwise the tool returns `Error: cwd must be a folder of this session's repository …` or `… is not in a work tree …`. A follow-up (`session:`) refuses `cwd:`: the child keeps its folder. The child's guardrails measure from its own worktree, and project memories, `list_sessions` and the web's project are the same as the parent's.
189
+ - `delegate_result(session:, timeout:)`: waits for a child's next reply: the one named, or the newest running child (`timeout` as for `delegate`: 0 checks once and returns at once). The reply comes back once; a later call waits for the next one. After `wait: false` it isn't needed: the report brings the reply.
190
+ - A wait that timed out or was canceled still gets its delegate report: neither moves the parent's cursor, so the child's reply comes as a report when its turn ends. The result says so (`chi brings its reply here by itself as a delegate report …`; with `queue`, that it joins the next step or turn) and that `delegate_result` waits again only if the turn needs the reply now. A parent that gets no reports (`session.delegate_reports: off`, a `--no-shared` REPL, `--non-interactive`) is told `delegate_result <id> waits again` instead, and `wait: false`'s result says `delegate_result waits for its reply`.
186
191
  - What a wait returns, `session: <id>` then `status:` and the text, with the status words `chi send --wait --format json` uses: `answered` with the reply after a `---` line; `question` as soon as the child opens a question (the model's own, or a hook's), with the question's text, options and the `chi answer` command for it (nothing is answered for it: `chi answer`, attach or use the web, then wait again); `failed`, `canceled` or `no_answer` when the child's turn ended with nothing to say (its session shows what happened); `error` (its worker crashed) or `stopped` (`chi sessions stop`) at once; `worker_gone` when the child has had no worker for 15 seconds (it crashed without saying so, or idle-exited): `delegate` with `session:` starts it again; `running` on timeout, or when the parent's own turn is canceled (the child keeps running; the next turn can wait again). A reply saved before the child's worker idle-exited is returned at once.
187
192
  - **A child's step-limit question goes back to the parent model**, not to its user: `delegate_result` returns `status: question` with the `chi answer` commands for Continue and Stop, and says the model may instead send a narrower follow-up (`delegate session:`, which drops the question) or report back. A parent's `chi answer` follows `turn.parent_continue` (see [chi as a sub-agent](sub-agent.md#the-step-limit)).
188
193
  - **A child's approvals go to the parent's user** (the approval relay). While the parent waits (`delegate`, `delegate_result`), a guardrail approval the child opens reopens as the parent session's own approval: the same card in the parent's web page and attached terminal, with the same facts and diff, a line naming the delegate and its task (`delegate ab12 · fix the flaky spec…`, linked to its session) and the header `Approve delegate ab12's tool call?`. The options are the child's, by index; "for the session" says it is the delegate's session (the session scope keys on the child; repo and rule approvals are one shared store). The user answers once there; the parent's model never sees the approval, only one line per relayed approval in the tool's result, before the reply: `approval relayed to your user: execute: git push origin main → allowed once` (or `denied ("why")`, `dismissed (denied)`, `answered on the child`, `not delivered (the child's worker is gone)`, `refused by the child …`, `still open (your turn was stopped)`). The time the card is open doesn't count against `timeout`. While the parent waits on one child, its other running children's approvals are relayed too, oldest first, one card at a time (`+1 more delegate waiting`); each outcome comes in that child's next result.
@@ -194,9 +199,10 @@ The `delegate` tool hands a task to a **child session**: an ordinary chi session
194
199
  - Parent mid-turn: the report joins the turn at its next iteration boundary (a "delegate report" bubble in the web, `delegate report> ab12cd34 answered: …` in the attached terminal).
195
200
  - Parent idle: it runs a turn for the report (a *wake turn*, `last_turn.origin: "delegate_report"`) within the worker's 5 s tick, or wakes a worker for it when the parent's has idle-exited. Several reports that wait together are one turn and one message. A stopped, archived or scratch parent, or one open in a `--no-shared` REPL, is not woken: the rings wait on disk and join the next turn it runs (the user's message first). A worker just started (`chi send` to a stopped parent) runs no wake turn in its first 2 s, so the message it was started for goes first.
196
201
  - The parent's cursor per child (the newest reply it was given and the child as it was then) is in its `delegates.json`, shared with `delegate_result` and `wait: true`: a reply the model already had is never reported again, also after the parent's worker restarts. A ring is deleted only when the turn that took its report is kept; a turn that fails leaves it for the next one (and the report isn't handed back to the composer).
197
- - Limits: `session.max_wakes` (default `10`) wake turns in a row with no human input; past it the reports wait for the next message and the UIs show `N delegate reports waiting; they join your next message`. A wake turn that fails pauses wakes until the next human input. A continue offer waiting in the parent takes the reports into its continue turn instead.
202
+ - Limits: `session.max_wakes` (default `10`) wake turns in a row with no human input; past it the reports wait for the next message and the UIs show `N delegate reports waiting; they join your next message`. A wake turn that fails pauses wakes until the next human input; its reports' rings stay, so they join that input's turn, and the failure note tells the model so. A continue offer waiting in the parent takes the reports into its continue turn instead.
198
203
  - `session.delegate_reports`: `wake` (default), `queue` (no wake turns: reports join the parent's next turn) or `off` (no rings; only `delegate_result` asks, as before). Env `SAMAGOTCHI_SESSION_DELEGATE_REPORTS`, `SAMAGOTCHI_SESSION_MAX_WAKES`.
199
204
  - The web's bell rings at a wake turn's end however short (nobody started it, so nobody watched it). The text a model writes about a report mid-turn, before its next tool call, stays visible after the report's bubble once the turn ends.
200
205
  - Every child starts with the shipped `delegated` system memory preloaded (`preloaded_memory_names: ["system/delegated"]`; `--mute delegated` would hide it): put everything in the final reply, don't delegate further, don't write memories or change config, stay in the folder. Its system prompt also says `Delegated by session <parent id>`. The parent's own `--mute` list does not carry over.
206
+ - **A child asks before it changes anything outside its folder** (the work tree top of the folder it started in): a write or edit there, mutating git in another checkout, a command that isn't read-only run from outside it or naming the main checkout or a sibling worktree. It's a core guardrail, on in every mode and without the guardrails bundle, and answered once or for the child's session ([Guardrails](guardrails.md#a-delegate-childs-boundary)); the approval relays to the parent's user as any other.
201
207
  - Limits: a child cannot `delegate` (depth 1; the tool refuses), and one session may have at most `session.max_children` (default `4`, env `SAMAGOTCHI_SESSION_MAX_CHILDREN`) children running at a time, counted from the session files (a finished or idle-exited child does not count; one whose worker is still starting does, for 15 s). A refused call creates no session. Nothing stops children with the parent: `chi sessions stop ID` does.
202
208
  - The link is the session's `parent_id`, written before the worker starts, so a respawn keeps it (and `delegate: true`, which a fork's `parent_id` doesn't come with; a child saved before that field is known by its `system/delegated` memory); `GET /api/sessions` and `/api/sessions/:id` carry it, and `SessionManager.children_of` lists a parent's children.
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "client_id"
3
4
  require_relative "session"
4
5
  require_relative "session_manager"
5
6
  require_relative "bridge_client"
@@ -189,7 +190,7 @@ module Samagotchi
189
190
  client.dismiss_question(id: qid)
190
191
  else
191
192
  client.answer(id: qid, selected: selected, freeform: options[:text],
192
- client_id: Guardrails::ParentApprovals::CLIENT_ID)
193
+ client_id: ClientId::CLI_ANSWER)
193
194
  end
194
195
  break unless reply.status == 408
195
196
  end
@@ -17,12 +17,6 @@ module Samagotchi
17
17
  # file from memory (and bumps updated_at) at every turn.
18
18
  module ArchiveStore
19
19
  FILE = "archived"
20
- # Input a human typed: a web tab, a chi TUI, `chi send`, and nil (a
21
- # worker's initial prompt; a web client may send none). Delegates,
22
- # plugins and reminders are not (an allowlist, so a new automatic
23
- # origin stays out).
24
- USER_CLIENT_PREFIXES = %w[web: tui:].freeze
25
- USER_CLIENT_IDS = ["cli:send"].freeze
26
20
 
27
21
  # @return [Hash, nil] the marker (string keys), nil when none or unreadable
28
22
  def self.read(session_dir)
@@ -57,15 +51,7 @@ module Samagotchi
57
51
  write(session_id, { "unarchived_at" => Time.now.iso8601(3) }, state_dir: state_dir)
58
52
  end
59
53
 
60
- # Whether +client_id+ (a turn's origin) is a human's input.
61
- def self.user_input?(client_id)
62
- return true if client_id.nil?
63
-
64
- id = client_id.to_s
65
- USER_CLIENT_IDS.include?(id) || USER_CLIENT_PREFIXES.any? { |prefix| id.start_with?(prefix) }
66
- end
67
-
68
- # A human's input came into the session: it is back in the lists. Its
54
+ # A human's input (ClientId.human?) came into the session: it is back in the lists. Its
69
55
  # children stay archived. Never raises.
70
56
  # @return [Boolean] whether it was archived
71
57
  def self.user_input(session_id, state_dir:)
@@ -55,6 +55,8 @@ module Samagotchi
55
55
  CANCEL_REASONS = %w[manual user ctrl_c].freeze
56
56
  # How long #stop waits for requests it is answering (not open streams).
57
57
  REQUEST_GRACE_SECONDS = 1.0
58
+ # How long #stop waits for each killed RelayWatcher thread to end.
59
+ RELAY_WATCHER_JOIN_SECONDS = 1.0
58
60
  # The largest request body read (images travel as refs, never bytes).
59
61
  MAX_BODY_BYTES = 1_000_000
60
62
  # A request's deadline (see #handle_post_turn) that isn't epoch seconds.
@@ -198,7 +200,9 @@ module Samagotchi
198
200
  await_answers(REQUEST_GRACE_SECONDS)
199
201
  @connection_threads.each { |t| t.kill rescue nil }
200
202
  @connection_threads.clear
201
- @mutex.synchronize { @relay_watchers&.each { |t| t.kill rescue nil } }
203
+ # Its relay watchers (RelayWatcher) end with it.
204
+ watchers = @mutex.synchronize { (@relay_watchers || []).tap { @relay_watchers = [] } }
205
+ watchers.each { |thread| thread.kill.join(RELAY_WATCHER_JOIN_SECONDS) }
202
206
  @accept_thread&.join(2)
203
207
  remove_sidecar
204
208
  nil
@@ -839,8 +843,9 @@ module Samagotchi
839
843
  return [{}, 500, { error: "enqueue_failed", detail: "could not write turn input" }] unless enqueued
840
844
 
841
845
  # Queued (outside the event lock): a running generation that has only
842
- # been thinking for long is cut for it, unless a plugin sent it. An
843
- # image input never merges mid-turn, so it never cuts.
846
+ # been thinking for long is cut for it, unless a plugin or a client
847
+ # chi doesn't know sent it. An image input never merges mid-turn, so
848
+ # it never cuts.
844
849
  @engine.cut_for_steer(Steer.source_for_client(client_id), epoch: epoch) if images.empty?
845
850
  [{}, 202, { status: "accepted", enqueued_id: enqueued_id, session_id: @session_id }]
846
851
  end
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: coordinator
3
+ version: 0.1.2
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), /coordinate <goal> and /children"
6
+ trust_level: reviewed
7
+ files:
8
+ skill_coordinator.md:
9
+ sha256: sha256:b21067c3f152c8dfd1b1f70001c6654ad1911616543a36b625727fb13d4ce3f6
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"
11
+ plugin:
12
+ file: plugin.rb
13
+ sha256: sha256:5173faeb6311984ad4f2a40b9052dcc577a90790fc5c5a8409b95d4f114e66a7
14
+ requires_chi: ">= 0.34.0"