samagotchi 0.33.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +47 -1
- data/README.md +4 -1
- data/docs/context.md +5 -0
- data/docs/guardrails.md +31 -1
- data/docs/hooks.md +1 -1
- data/docs/memory.md +17 -1
- data/docs/plugins.md +77 -2
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +9 -3
- data/lib/samagotchi/bundles/coordinator/manifest.yml +14 -0
- data/lib/samagotchi/bundles/coordinator/plugin.rb +114 -0
- data/lib/samagotchi/bundles/coordinator/skill_coordinator.md +60 -0
- data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
- data/lib/samagotchi/bundles/mcp/manifest.yml +3 -3
- data/lib/samagotchi/bundles/mcp/plugin.rb +10 -11
- data/lib/samagotchi/bundles/system/delegated.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/bundles/system/self_map.md +4 -2
- data/lib/samagotchi/children_status.rb +157 -0
- data/lib/samagotchi/cut_policy.rb +58 -0
- data/lib/samagotchi/engine.rb +16 -103
- data/lib/samagotchi/engine_peers.rb +30 -0
- data/lib/samagotchi/guardrail_wiring.rb +27 -2
- data/lib/samagotchi/guardrails/approvals.rb +7 -0
- data/lib/samagotchi/guardrails/child_boundary.rb +103 -0
- data/lib/samagotchi/guardrails/context.rb +32 -0
- data/lib/samagotchi/guardrails/shared_git.rb +124 -0
- data/lib/samagotchi/guardrails.rb +1 -0
- data/lib/samagotchi/kernel_loop.rb +9 -22
- data/lib/samagotchi/llm/chat_loop.rb +11 -22
- data/lib/samagotchi/memory_bundle/builder.rb +14 -1
- data/lib/samagotchi/memory_bundle/installer.rb +6 -2
- data/lib/samagotchi/memory_bundle/manifest.rb +26 -5
- data/lib/samagotchi/memory_bundle/provenance.rb +2 -1
- data/lib/samagotchi/memory_paths.rb +10 -0
- data/lib/samagotchi/plugin/api.rb +2 -2
- data/lib/samagotchi/plugin/sessions.rb +45 -1
- data/lib/samagotchi/process_group.rb +2 -5
- data/lib/samagotchi/steer_cut.rb +125 -0
- data/lib/samagotchi/system_prompt.rb +20 -8
- data/lib/samagotchi/tool_declarations.rb +9 -5
- data/lib/samagotchi/tools/builtins.rb +2 -2
- data/lib/samagotchi/tools/delegate.rb +103 -14
- data/lib/samagotchi/tools/delegate_wait.rb +55 -13
- data/lib/samagotchi/turn_note.rb +9 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/worker.rb +7 -2
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c6f812c6128ae3ffaf91d9cc04716a77d351fb57793a7d9a4863e675f1f2337f
|
|
4
|
+
data.tar.gz: 25e7600fe6dc335b7d28320795a03ea9e8527185f6a0dd918787777458402f39
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 322e9f5a5b553f7ed1f2955307b8d5bd8c901a355ac3ffcdff6b3fcb5013b28011c7c330d9496486338c77d2c0f58e2854393d4cb40ec33d00ed2516a38bd066
|
|
7
|
+
data.tar.gz: f6048763f53d9b865858842acaf60587d5e57a365aaba759c3c85fcf69532227c4b201fe0f878da3e48191dd43856637dad29537203a3dfd7159756caac6f778
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,51 @@ 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
|
+
|
|
11
56
|
## [0.33.0] - 2026-10-06
|
|
12
57
|
|
|
13
58
|
### Changed
|
|
@@ -1663,7 +1708,8 @@ and long-lived sessions.
|
|
|
1663
1708
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
1664
1709
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
1665
1710
|
|
|
1666
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
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
|
|
1667
1713
|
[0.33.0]: https://github.com/dm1try/samagotchi/compare/v0.32.0...v0.33.0
|
|
1668
1714
|
[0.32.0]: https://github.com/dm1try/samagotchi/compare/v0.31.0...v0.32.0
|
|
1669
1715
|
[0.31.0]: https://github.com/dm1try/samagotchi/compare/v0.30.0...v0.31.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
|
@@ -154,6 +154,11 @@ update note, whose last line becomes:
|
|
|
154
154
|
Every UI labels the turn `context <name> changed`, and the web's
|
|
155
155
|
notifications count it as a finished turn however short.
|
|
156
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
|
+
|
|
157
162
|
## GitHub PRs: the github-pr bundle
|
|
158
163
|
|
|
159
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
|
@@ -420,7 +420,7 @@ stripped of surrounding whitespace, except file text, an edit's
|
|
|
420
420
|
| `list_sessions` | — | — | `cwd` |
|
|
421
421
|
| `send_note` | `text` | — | `session` |
|
|
422
422
|
| `context_read` | `name` | — | `offset`, `limit` |
|
|
423
|
-
| `delegate` | `task` | — | `model`, `session`, `wait`, `timeout` |
|
|
423
|
+
| `delegate` | `task` | — | `model`, `session`, `cwd`, `wait`, `timeout` |
|
|
424
424
|
| `delegate_result` | — | — | `session`, `timeout` |
|
|
425
425
|
| `ask_user_question` | `question` | — | `question`, `options`, `header`, `multi_select`, `allow_freeform` |
|
|
426
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
|
|
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)
|
|
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
|
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:`
|
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
|
|
185
|
-
- `
|
|
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.
|
|
@@ -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"
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The coordinator bundle (docs/plugins.md, The coordinator bundle): chi as a
|
|
4
|
+
# coordinator of parallel work. Its skill_coordinator memory holds the steps
|
|
5
|
+
# (split the work, one worktree and a wait: false delegate per task, check
|
|
6
|
+
# each report, ask before each merge); this plugin adds two commands.
|
|
7
|
+
#
|
|
8
|
+
# /children [all] shows this session's children as a card, newest first:
|
|
9
|
+
# state, branch, the last reply and whether it was reported (ChildrenStatus
|
|
10
|
+
# through ctx.sessions.children), with a Stop button per running or waiting
|
|
11
|
+
# child and Refresh. `all` adds archived children. The card has one id, so
|
|
12
|
+
# Refresh and Stop replace it. It is read on demand: no thread, no polling.
|
|
13
|
+
#
|
|
14
|
+
# /children stop <id> stops one of this session's own children
|
|
15
|
+
# (ctx.sessions.stop, by session id) and shows the card again.
|
|
16
|
+
#
|
|
17
|
+
# /coordinate <goal> asks the model, in this session, to follow the skill
|
|
18
|
+
# for the goal. A REPL session (--no-shared) takes no messages: the request
|
|
19
|
+
# is shown for the user to send.
|
|
20
|
+
#
|
|
21
|
+
# A child is asked before it changes anything outside its worktree by chi
|
|
22
|
+
# itself (the core child-boundary guardrail), in every mode: nothing to set up.
|
|
23
|
+
class Plugin
|
|
24
|
+
CARD_ID = "children"
|
|
25
|
+
# A card takes 6 actions: Refresh and up to 5 Stops.
|
|
26
|
+
MAX_STOPS = 5
|
|
27
|
+
STOPPABLE = %w[running waiting].freeze
|
|
28
|
+
TEXT_CHARS = 80
|
|
29
|
+
USAGE = "usage: /children [all] | /children stop <id>"
|
|
30
|
+
COORDINATE_USAGE = "usage: /coordinate <goal> — chi splits it into tasks, one child session and worktree each"
|
|
31
|
+
|
|
32
|
+
def initialize(_settings = {}); end
|
|
33
|
+
|
|
34
|
+
def register(chi)
|
|
35
|
+
chi.command "/children", "this session's child sessions: state, branch, last reply; all, stop <id>",
|
|
36
|
+
anytime: true do |args, ctx|
|
|
37
|
+
children_command(args.to_s.strip, ctx)
|
|
38
|
+
end
|
|
39
|
+
chi.command "/coordinate", "run work in parallel: chi splits <goal> into tasks, each in a child session and worktree",
|
|
40
|
+
anytime: true do |args, ctx|
|
|
41
|
+
coordinate(args.to_s.strip, ctx)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def children_command(args, ctx)
|
|
48
|
+
verb, rest = args.split(/\s+/, 2)
|
|
49
|
+
case verb
|
|
50
|
+
when nil then show(ctx, all: false)
|
|
51
|
+
when "all" then rest.to_s.empty? ? show(ctx, all: true) : USAGE
|
|
52
|
+
when "stop" then rest.to_s.strip.empty? ? USAGE : stop(rest.strip, ctx)
|
|
53
|
+
else USAGE
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The card, made or replaced; nil, so the command adds no text of its own.
|
|
58
|
+
def show(ctx, all:, note: nil)
|
|
59
|
+
children = ctx.sessions.children(all: all)
|
|
60
|
+
title = "children of #{ctx.session_id.to_s[0, 8]}"
|
|
61
|
+
title += " (#{children.size})" unless children.empty?
|
|
62
|
+
body = children.empty? ? "no children" : children.map { |child| line(child) }.join("\n")
|
|
63
|
+
body = "#{note}\n\n#{body}" if note
|
|
64
|
+
refresh = all ? "/children all" : "/children"
|
|
65
|
+
stops = children.select { |child| STOPPABLE.include?(child[:state]) }.first(MAX_STOPS)
|
|
66
|
+
.map { |child| { label: "Stop #{child[:short_id]}", command: "/children stop #{child[:short_id]}" } }
|
|
67
|
+
ctx.card(id: CARD_ID, title: title, body: body, actions: [{ label: "Refresh", command: refresh }, *stops])
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# - `ab12cd34` · running · fix/flaky · "fix the flaky spec…"
|
|
72
|
+
# - `9a8b7c6d` · done · feat/x · reported · "All 12 specs pass…"
|
|
73
|
+
# - `77aa66bb` · waiting (approval) · open it: chi --attach 77aa66bb
|
|
74
|
+
def line(child)
|
|
75
|
+
parts = ["`#{child[:short_id]}`", child[:waiting] ? "waiting (#{child[:waiting]})" : child[:state]]
|
|
76
|
+
parts << child[:branch] if child[:branch]
|
|
77
|
+
parts << "fork" unless child[:delegate]
|
|
78
|
+
parts << "archived" if child[:archived]
|
|
79
|
+
if child[:waiting]
|
|
80
|
+
parts << "open it: chi --attach #{child[:short_id]}"
|
|
81
|
+
elsif child[:last_reply]
|
|
82
|
+
parts << (child[:reported] ? "reported" : "not reported yet")
|
|
83
|
+
parts << quote(child[:last_reply])
|
|
84
|
+
elsif !child[:title].to_s.strip.empty?
|
|
85
|
+
parts << quote(child[:title])
|
|
86
|
+
end
|
|
87
|
+
"- #{parts.join(" · ")}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def stop(id, ctx)
|
|
91
|
+
stopped = ctx.sessions.stop(id)
|
|
92
|
+
show(ctx, all: false, note: "stopped #{stopped[0, 8]}")
|
|
93
|
+
rescue Samagotchi::Plugin::Sessions::Error => e
|
|
94
|
+
"/children stop: #{e.message}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def coordinate(goal, ctx)
|
|
98
|
+
return COORDINATE_USAGE if goal.empty?
|
|
99
|
+
|
|
100
|
+
request = "Read the skill_coordinator memory and follow it for this goal:\n\n#{goal}"
|
|
101
|
+
begin
|
|
102
|
+
ctx.sessions.send(ctx.session_id, request)
|
|
103
|
+
rescue Samagotchi::Plugin::Sessions::Error => e
|
|
104
|
+
return "/coordinate: #{e.message}. Send this yourself:\n\n#{request}"
|
|
105
|
+
end
|
|
106
|
+
"asked chi to coordinate it; a running turn gets it at its next step"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def quote(text)
|
|
110
|
+
line = text.to_s.gsub(/\s+/, " ").strip
|
|
111
|
+
line = "#{line[0, TEXT_CHARS - 1]}…" if line.length > TEXT_CHARS
|
|
112
|
+
"\"#{line}\""
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Skill: coordinator
|
|
2
|
+
|
|
3
|
+
Use when the user hands over work with several independent parts, or asks to "run it in parallel",
|
|
4
|
+
"delegate", "coordinate". The user decides; you do the legwork and keep them in the loop briefly.
|
|
5
|
+
|
|
6
|
+
## Steps
|
|
7
|
+
1. Find the repository's default branch, <default> below: `git symbolic-ref --short
|
|
8
|
+
refs/remotes/origin/HEAD` (drop the `origin/`), else `main` or `master`, whichever exists; ask the
|
|
9
|
+
user if unsure. Split the work into independent tasks (at most session.max_children at once,
|
|
10
|
+
default 4). Tell the user the split in a few lines and go on unless they object.
|
|
11
|
+
2. For each task: from your own folder, `git worktree add ../<repo>-<task> -b <type>/<task>` with
|
|
12
|
+
execute; then `delegate` with `wait: false`, `cwd:` that worktree's absolute path, and a
|
|
13
|
+
self-contained task. The task starts with "Work only in <absolute worktree path>: every cd, read,
|
|
14
|
+
write, test and commit goes there, never the project root" and says what to report (the result,
|
|
15
|
+
evidence: commands and what they printed, what isn't done) and "commit on the branch, don't merge,
|
|
16
|
+
don't push".
|
|
17
|
+
3. Tell the user what started (one line per child: its id, branch and task) and end your turn or keep
|
|
18
|
+
talking. Don't call delegate_result: chi brings each child's reply as a delegate report, also when
|
|
19
|
+
you are idle.
|
|
20
|
+
4. On each report, check it before telling the user: `git -C <worktree> log --oneline <default>..` and
|
|
21
|
+
`git diff --stat <default>...<branch>`; run the tests the child names (execute with cwd: the worktree).
|
|
22
|
+
Say the result in one or two lines: done and verified / done with doubts / failed.
|
|
23
|
+
5. Then one of: a follow-up (`delegate session: <id>`, narrower, `wait: false`; then end your turn:
|
|
24
|
+
its report comes by itself, so don't wait for it with delegate_result or task_wait), stop it
|
|
25
|
+
(`chi sessions stop <session id>` with execute; task_stop is for task ids, not children; the user
|
|
26
|
+
can use /children), or tell the user it is ready to merge.
|
|
27
|
+
6. Merging is the user's call: ask (ask_user_question) per branch, "Merge <branch> into <default>?".
|
|
28
|
+
Before merging, in your own folder: `git branch --show-current` must print <default> (if it
|
|
29
|
+
doesn't, stop and ask the user; don't switch branches yourself); then check <default>:
|
|
30
|
+
`git log --oneline -3 <default>` and `git merge-base --is-ancestor <branch> <default>`; if the branch is already in, say so and don't merge
|
|
31
|
+
again. Merge with `git merge --ff-only <branch>` yourself, in your own folder. If that fails, ask the
|
|
32
|
+
child to rebase its branch on <default> (a follow-up that says "rebase only, don't merge") or ask the
|
|
33
|
+
user; don't rebase it from here (the branch is checked out in the child's worktree). Children
|
|
34
|
+
never merge. Never push unless asked.
|
|
35
|
+
7. Clean up only a branch that was merged or that the user said to drop: stop its child
|
|
36
|
+
(`chi sessions stop <session id>`), then `git worktree remove ../<repo>-<task>`, then
|
|
37
|
+
`git branch -d <branch>` (in that order: a branch checked out in a worktree can't be deleted). If
|
|
38
|
+
`-d` refuses, say why to the user; never `-D` unless they say so. Remove only worktrees and
|
|
39
|
+
branches you created in this session. Every other branch stays as it is, its child, worktree and
|
|
40
|
+
branch too (the user said "keep it", or didn't say): leave them and tell the user they remain.
|
|
41
|
+
8. When all are done, report: what merged, what didn't and why, what you cleaned up and what remains,
|
|
42
|
+
follow-ups found but not fixed. Report only what the commands' output showed: a step you planned
|
|
43
|
+
but didn't run, or whose output you didn't see, is not done; say so.
|
|
44
|
+
|
|
45
|
+
## Gotchas
|
|
46
|
+
- Children are asked before changing anything outside their worktree (your checkout, a sibling's):
|
|
47
|
+
the user approves it on the child's card. Still check where commits landed (step 4).
|
|
48
|
+
- Never kill processes by a pid from a file or an old note; stop children with `chi sessions stop
|
|
49
|
+
<session id>` (execute) and background tasks with task_stop (task ids only: it doesn't stop a child).
|
|
50
|
+
- A child's tool approvals go to the user on the child's card; tell the user when a report says a
|
|
51
|
+
child waits for one.
|
|
52
|
+
- A report is the child's claim: verify it (step 4) before telling the user it is done, and check
|
|
53
|
+
where its commits landed: `git log --oneline <default>..<branch>` has them and `git status` in your own
|
|
54
|
+
folder is clean. A child that worked in your folder instead of its worktree: tell the user.
|
|
55
|
+
- Don't send a follow-up to a child that is still running; wait for its report.
|
|
56
|
+
- Don't start more children than the user asked for.
|
|
57
|
+
|
|
58
|
+
## Changelog
|
|
59
|
+
- 2026-10-06 created (coordinator bundle 0.1.0)
|
|
60
|
+
- 2026-10-06 clean up only merged or dropped branches; report only what commands showed (0.1.2)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: dev
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.3.0
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Developer tools"
|
|
6
6
|
trust_level: reviewed
|
|
7
|
-
requires_chi: ">= 0.
|
|
7
|
+
requires_chi: ">= 0.34.0"
|
|
8
8
|
includes:
|
|
9
9
|
- known-names
|
|
10
10
|
- mcp
|
|
@@ -12,3 +12,4 @@ includes:
|
|
|
12
12
|
- skills
|
|
13
13
|
- source-links
|
|
14
14
|
- github-pr
|
|
15
|
+
- coordinator
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mcp
|
|
3
|
-
version: 0.4.
|
|
3
|
+
version: 0.4.5
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Tools from MCP servers (stdio): each server in config.yml bundles: mcp: servers: runs for the session, its tools are mcp_<server>_<tool>; /mcp lists them"
|
|
6
6
|
trust_level: reviewed
|
|
7
7
|
files: {}
|
|
8
8
|
plugin:
|
|
9
9
|
file: plugin.rb
|
|
10
|
-
sha256: sha256:
|
|
11
|
-
requires_chi: ">= 0.
|
|
10
|
+
sha256: sha256:1ebedc86cd21c918766c794015b6bd16c397c0f7e2734d939d8310d446b8a9a1
|
|
11
|
+
requires_chi: ">= 0.33.0"
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
require "digest"
|
|
32
32
|
require "json"
|
|
33
33
|
require "open3"
|
|
34
|
+
require "samagotchi/process_group"
|
|
34
35
|
require "time"
|
|
35
36
|
require "shellwords"
|
|
36
37
|
require "tmpdir"
|
|
@@ -59,6 +60,9 @@ class Plugin
|
|
|
59
60
|
class Dead < Error; end
|
|
60
61
|
|
|
61
62
|
POLL_SECONDS = 0.1
|
|
63
|
+
# How long close waits for the server after closing its stdin, after
|
|
64
|
+
# TERM, and after KILL.
|
|
65
|
+
STOP_GRACE_SECONDS = 1
|
|
62
66
|
|
|
63
67
|
attr_reader :pid
|
|
64
68
|
|
|
@@ -136,14 +140,15 @@ class Plugin
|
|
|
136
140
|
end
|
|
137
141
|
|
|
138
142
|
# End the process: stdin closed first (most servers leave then), then
|
|
139
|
-
# TERM and KILL to its process group
|
|
143
|
+
# TERM and KILL to its process group (chi's ProcessGroup), a second's
|
|
144
|
+
# grace each.
|
|
140
145
|
def close
|
|
141
146
|
@mutex.synchronize { @closing = true }
|
|
142
147
|
[@stdin].each { |io| io.close unless io.closed? }
|
|
143
|
-
unless @wait.join(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
@wait.join(
|
|
148
|
+
unless @wait.join(STOP_GRACE_SECONDS)
|
|
149
|
+
Samagotchi::ProcessGroup.stop(@pid, grace: STOP_GRACE_SECONDS, poll: STOP_GRACE_SECONDS,
|
|
150
|
+
stopped: -> { !@wait.alive? }, wait: ->(seconds) { @wait.join(seconds) })
|
|
151
|
+
@wait.join(STOP_GRACE_SECONDS)
|
|
147
152
|
end
|
|
148
153
|
[@stdout, @stderr].each { |io| io.close unless io.closed? }
|
|
149
154
|
[@reader, @err_reader].each { |t| t.join(1) }
|
|
@@ -234,12 +239,6 @@ class Plugin
|
|
|
234
239
|
@on_exit&.call(reason) unless closing
|
|
235
240
|
end
|
|
236
241
|
|
|
237
|
-
def signal(name)
|
|
238
|
-
Process.kill(name, -@pid)
|
|
239
|
-
rescue Errno::ESRCH, Errno::EPERM
|
|
240
|
-
nil
|
|
241
|
-
end
|
|
242
|
-
|
|
243
242
|
def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
244
243
|
end
|
|
245
244
|
|