samagotchi 0.62.0 → 0.63.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 +33 -1
- data/README.md +4 -1
- data/bin/chi +3 -2
- data/docs/cli.md +4 -1
- data/docs/configuration.md +1 -1
- data/docs/context.md +13 -3
- data/docs/hooks.md +6 -3
- data/docs/memory.md +1 -1
- data/docs/plugins.md +89 -2
- data/docs/releasing.md +1 -1
- data/docs/sub-agent.md +6 -0
- data/docs/testing.md +7 -4
- data/lib/samagotchi/answer_refs.rb +18 -26
- data/lib/samagotchi/bridge.rb +23 -1
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +22 -0
- data/lib/samagotchi/bundles/claude-peer/plugin.rb +406 -0
- data/lib/samagotchi/bundles/claude-peer/scripts/chi-peer-SKILL.md +104 -0
- data/lib/samagotchi/bundles/claude-peer/scripts/pair +66 -0
- data/lib/samagotchi/bundles/claude-peer/skill_claude_peer.md +60 -0
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -1
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/plugin.rb +17 -6
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/context_absorber.rb +25 -6
- data/lib/samagotchi/idle_recap.rb +2 -3
- data/lib/samagotchi/image_store.rb +8 -0
- data/lib/samagotchi/plugin/attached_context.rb +7 -4
- data/lib/samagotchi/recap_setup.rb +3 -3
- data/lib/samagotchi/row_refs.rb +3 -4
- data/lib/samagotchi/session.rb +12 -0
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +6 -3
- data/lib/samagotchi/terminal_ui/attached_loop.rb +26 -9
- data/lib/samagotchi/terminal_ui.rb +31 -11
- data/lib/samagotchi/tools/execute.rb +22 -4
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +6 -4
- data/lib/samagotchi/web/public/activity.js +2 -2
- data/lib/samagotchi/web/public/app.js +47 -91
- data/lib/samagotchi/web/public/card.js +73 -23
- data/lib/samagotchi/web/public/chains.js +6 -4
- data/lib/samagotchi/web/public/command_view.js +39 -35
- data/lib/samagotchi/web/public/copy.js +7 -15
- data/lib/samagotchi/web/public/diff_view.js +20 -23
- data/lib/samagotchi/web/public/format.js +0 -48
- data/lib/samagotchi/web/public/images.js +12 -16
- data/lib/samagotchi/web/public/question_cards.js +3 -2
- data/lib/samagotchi/web/public/read_view.js +10 -9
- data/lib/samagotchi/web/public/scope.js +23 -0
- data/lib/samagotchi/web/public/timing.js +0 -7
- data/lib/samagotchi/web/public/turn_html.js +119 -59
- data/lib/samagotchi/web/public/turn_view.js +73 -116
- data/lib/samagotchi/web/public/vendor/directive.js +8 -0
- data/lib/samagotchi/web/public/vendor/directives/unsafe-html.js +8 -0
- data/lib/samagotchi/worker.rb +46 -30
- data/lib/samagotchi/worker_inbound.rb +12 -11
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 20b05cd0da6dfdd621eba3b2add37f00b2abe96c2e3950d8ddd066bdf3078424
|
|
4
|
+
data.tar.gz: ecf5784f9f82c8a8c9e813185570d326a3f4d9ad3a404033f53abf1b62a20902
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 302c8ccf54cc947c9165fe8e30f4d32b4054ee02a1e2d62cbb7d7942a30574af58af84a7eafc5b2daf24a8f167e98024e2798fcacccf597b919435019d498613
|
|
7
|
+
data.tar.gz: b6ac96ff05fb4e52a5705b55de3fa505054860b154cd6622c4badc232c6284bc5e80d57ffc94b13a7eae1384557acd5b697b630a0ed21be1a8e85d99610b3d04
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,37 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.63.0] - 2026-10-13
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- The claude-peer bundle (in the dev profile): pair a chi session with a Claude Code session you work with, and
|
|
16
|
+
the `claude_send` tool sends Claude a part of the work (a plan step, a diff, a finding) to validate, as context or
|
|
17
|
+
with a question; Claude's answer comes back as the `claude-review` context. `/claude validate|context <text>` and
|
|
18
|
+
`/claude last` send without the model, `/claude` shows whether a Claude is paired, and `/claude install-skill`
|
|
19
|
+
installs Claude's `chi-peer` skill, which pairs ("pair with chi <id>"). See docs/plugins.md, "The claude-peer
|
|
20
|
+
bundle".
|
|
21
|
+
- Plugins: `ctx.context.list` gives each source's last `summary:` too. See docs/plugins.md, "Attached context".
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Attached context: a source added with `chi context add NAME --push` now wakes an idle session when its first
|
|
26
|
+
push asks to (`"wake": true`); before, only later pushes could.
|
|
27
|
+
- Plugins: `ctx.context.list` returns `[]` for a new session whose file isn't saved yet (a plugin command before
|
|
28
|
+
the first turn) instead of raising "Session not found".
|
|
29
|
+
- An after_turn hook's file refs on the answer (`event[:refs]`) must be under the session's folder, as a plugin
|
|
30
|
+
tool row's are: a file elsewhere is refused (and logged), and a stored one is dropped when the answer is shown.
|
|
31
|
+
- loop-guard (0.3.8): the card for a turn it stopped prints under the blocked call's row in the terminal, not
|
|
32
|
+
before it.
|
|
33
|
+
- Attached TUI: joining a session whose last turn was images alone (sent from the web) names each image once, not
|
|
34
|
+
twice.
|
|
35
|
+
- Attached TUI: text you had typed when the step-limit question opened comes back at the prompt once you answer
|
|
36
|
+
it, instead of being lost.
|
|
37
|
+
- `chi -p … --non-interactive` says so on stderr and exits 1 when the iteration limit cuts the turn short, instead
|
|
38
|
+
of exiting 0 with the partial text as if it were the answer.
|
|
39
|
+
- `chi --no-shared --resume` drops a question the session's last worker saved and died with, as an attached resume
|
|
40
|
+
does, instead of keeping it in the session file.
|
|
41
|
+
|
|
11
42
|
## [0.62.0] - 2026-10-13
|
|
12
43
|
|
|
13
44
|
### Added
|
|
@@ -2719,7 +2750,8 @@ and long-lived sessions.
|
|
|
2719
2750
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
2720
2751
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
2721
2752
|
|
|
2722
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
2753
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.63.0...HEAD
|
|
2754
|
+
[0.63.0]: https://github.com/dm1try/samagotchi/compare/v0.62.0...v0.63.0
|
|
2723
2755
|
[0.62.0]: https://github.com/dm1try/samagotchi/compare/v0.61.0...v0.62.0
|
|
2724
2756
|
[0.61.0]: https://github.com/dm1try/samagotchi/compare/v0.60.0...v0.61.0
|
|
2725
2757
|
[0.60.0]: https://github.com/dm1try/samagotchi/compare/v0.59.0...v0.60.0
|
data/README.md
CHANGED
|
@@ -150,7 +150,10 @@ task to a child session that runs in parallel and reports back only its final
|
|
|
150
150
|
reply (a normal session: `chi --attach <id>` steers it). With the `coordinator`
|
|
151
151
|
bundle (in `dev`), `/coordinate <goal>` has chi split the work into tasks, each
|
|
152
152
|
in a child session in its own git worktree, check each report and ask you
|
|
153
|
-
before each merge; `/children` shows the children and their state.
|
|
153
|
+
before each merge; `/children` shows the children and their state. With the
|
|
154
|
+
`claude-peer` bundle (in `dev`), chi sends a part of its work to a Claude Code
|
|
155
|
+
session you pair it with, to validate or as context, and Claude answers back
|
|
156
|
+
into chi (`/claude`).
|
|
154
157
|
|
|
155
158
|
`chi send` is the other half: the text goes in as your message, the same as
|
|
156
159
|
typing it in the attached terminal or the web composer, and a turn runs. Piped
|
data/bin/chi
CHANGED
|
@@ -386,8 +386,9 @@ begin
|
|
|
386
386
|
scratch: scratch
|
|
387
387
|
).run
|
|
388
388
|
# `chi -p X </dev/null`: the turn failed (nothing was sent again); -p
|
|
389
|
-
# --non-interactive: the turn ended with no answer
|
|
390
|
-
|
|
389
|
+
# --non-interactive: the turn ended with no answer or at the iteration
|
|
390
|
+
# limit, or Ctrl-C cancelled it.
|
|
391
|
+
exit 1 if %i[turn_failed empty_answer iteration_limit].include?(ended)
|
|
391
392
|
exit Samagotchi::CLI::Exit::INTERRUPTED if ended == :interrupted
|
|
392
393
|
rescue Samagotchi::TerminalUI::SessionBusy, Samagotchi::TerminalUI::SessionNotFound, Samagotchi::ModelProfile::MissingModel => e
|
|
393
394
|
warn "Error: #{e.message}"
|
data/docs/cli.md
CHANGED
|
@@ -250,7 +250,10 @@ Notes:
|
|
|
250
250
|
retry lines and hooks' notices as they come, the after_turn hooks' ones
|
|
251
251
|
(source-links' `sources:`) after the answer; and the error of a failed turn,
|
|
252
252
|
then `chi: the session is kept with your prompt; continue it with: chi
|
|
253
|
-
--resume ID` (exit 1).
|
|
253
|
+
--resume ID` (exit 1). A turn the iteration limit (`turn.max_iterations`)
|
|
254
|
+
cut short prints what the model wrote so far, then `chi: the iteration limit
|
|
255
|
+
(N) was reached before the turn finished; continue it with: chi --resume ID`
|
|
256
|
+
on stderr (exit 1). Ctrl-C saves the session with the prompt and a
|
|
254
257
|
cancel note and exits 130.
|
|
255
258
|
Nobody can answer there: an approval is denied, and a question the model
|
|
256
259
|
asks (`ask_user_question`) is dismissed at once (one line on stderr), never
|
data/docs/configuration.md
CHANGED
|
@@ -1350,7 +1350,7 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
|
|
|
1350
1350
|
| `memory.index_warn_tokens` | `2500` | yes | The tokens one scope's memory index (project or system; sent with every prompt) may hold before a `memory_write`, `write` or `edit` that takes it over gets a note asking the model to tighten long index descriptions; `0`: no note. Estimated as characters / `context.chars_per_token`. `memory:` is its own section, not the `memories:` preload list. See [Memory](memory.md). |
|
|
1351
1351
|
| `context.status_thresholds` | `20,40,60,80` | yes | Percentages that put the estimate into a bucket; the rise into a guidance bucket (from the second up) leaves the model its own `[CONTEXT: …]` line, and the bucket decides the guidance. |
|
|
1352
1352
|
| `context.every_seconds` | `300` | | How often an attached context source's command runs when it has no `--every` (seconds, at least 30). See [Attached context](context.md). |
|
|
1353
|
-
| `context.wake` | `true` | yes | A source whose update says `wake: true` may start a turn in a live, idle session (one per source per 10 minutes, within `session.max_wakes`); `false`: updates wait as notes for the next turn. See [Waking](context.md#waking). |
|
|
1353
|
+
| `context.wake` | `true` | yes | A source whose update says `wake: true` may start a turn in a live, idle session (one per source per 10 minutes, within `session.max_wakes`); so may a session's pushed source's first push. `false`: updates wait as notes for the next turn. See [Waking](context.md#waking). |
|
|
1354
1354
|
| `thinking.turn_preamble` | `true` | yes | Ask a `qwen36` model to open its thinking with a short `TURN:` line (the step label). |
|
|
1355
1355
|
| `reasoning_history` | `turn` | | Which of its reasoning a chat host's model gets back: `drop`, `turn`, `all`; environment only (`SAMAGOTCHI_REASONING_HISTORY`), below a session's own level (`/reasoning-history`), above the `models:`/`hosts:` entries. See "Reasoning history". |
|
|
1356
1356
|
| `thinking.level` | `default` | `--thinking` | `off`, `low`, `medium`, `high` or `default` for every model; the flag and env outrank the `models:`/`hosts:` entries, the file's value doesn't; a session's own level outranks them all, and `--thinking` sets it on a session run (on `chi web` it sets this). See "Thinking". |
|
data/docs/context.md
CHANGED
|
@@ -123,6 +123,11 @@ chi context add clip --push --project
|
|
|
123
123
|
pbpaste | chi context push clip --project
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
+
A Claude Code session can be a pushed source too: the
|
|
127
|
+
[claude-peer bundle](plugins.md#the-claude-peer-bundle)'s `pair` script attaches
|
|
128
|
+
`claude-inbox` (Claude's inbox socket as the hint) and `claude-review`, where
|
|
129
|
+
Claude pushes its answers as JSON with `wake: true`.
|
|
130
|
+
|
|
126
131
|
### A stream: chat, a log, a feed
|
|
127
132
|
|
|
128
133
|
A source's text is a snapshot, not a log: each run replaces it. For
|
|
@@ -233,6 +238,10 @@ you're away from the terminal. All of these must hold:
|
|
|
233
238
|
- the source asked (`"wake": true`; a plain-text source never does), in
|
|
234
239
|
any change since the session last heard of it: a later change that doesn't
|
|
235
240
|
ask doesn't take the wake back;
|
|
241
|
+
- it's a change, not the source's first text, except for a session's pushed
|
|
242
|
+
source (`chi context add NAME --push`, attached empty): its first push can
|
|
243
|
+
wake too. A command or URL source's first fetch runs as you attach it, and a
|
|
244
|
+
project source's first text may predate the session, so neither wakes;
|
|
236
245
|
- `context.wake` is on (default `true`);
|
|
237
246
|
- the session's worker is up and idle: no turn running, nothing queued, no
|
|
238
247
|
question or continue offer waiting, not just started;
|
|
@@ -242,15 +251,16 @@ you're away from the terminal. All of these must hold:
|
|
|
242
251
|
- the source hasn't woken the session in the last 10 minutes.
|
|
243
252
|
|
|
244
253
|
Otherwise the change arrives as a note, as above. The turn starts with the
|
|
245
|
-
update note, whose last
|
|
254
|
+
update note, or, for a session's pushed source, its attached note, whose last
|
|
255
|
+
line becomes:
|
|
246
256
|
|
|
247
257
|
> chi started this turn because the source changed; your user didn't write anything. Tell your user in a sentence or two what changed and what it may mean for their work. Don't run commands, change files or reply on the source for it: wait for your user to ask.
|
|
248
258
|
|
|
249
259
|
Every UI labels the turn `context <name> changed`, and the web's
|
|
250
260
|
notifications count it as a finished turn however short.
|
|
251
261
|
|
|
252
|
-
A wake turn that fails goes back to before it and keeps
|
|
253
|
-
plain
|
|
262
|
+
A wake turn that fails goes back to before it and keeps its note as a
|
|
263
|
+
plain one; the model reads `[SYSTEM: the previous turn failed before any
|
|
254
264
|
answer: … The wake turn for the change in attached context <name> was not
|
|
255
265
|
answered; chi starts no other wake turn until the user writes.]`.
|
|
256
266
|
|
data/docs/hooks.md
CHANGED
|
@@ -301,9 +301,12 @@ end
|
|
|
301
301
|
keywords only (String or Symbol keys inside `target` and `actions`).
|
|
302
302
|
- `match`: the text in the answer the ref stands for (at most 500 characters).
|
|
303
303
|
- `kind`: a short lowercase word. `"file"` takes `target: {path:, line:, end_line:}`
|
|
304
|
-
(the lines optional);
|
|
305
|
-
folder,
|
|
306
|
-
`
|
|
304
|
+
(the lines optional); the path is absolute or relative to the session's
|
|
305
|
+
folder, and must be **under** it (its own spelling or its real path:
|
|
306
|
+
`/tmp/x` or `/private/tmp/x`), as a plugin tool row's file refs
|
|
307
|
+
([plugins.md](plugins.md)): a file elsewhere is refused. It is kept under
|
|
308
|
+
the folder's real path. Any other kind (`"issue"`, `"commit"`, …) takes
|
|
309
|
+
`target: {id:}`.
|
|
307
310
|
- `label`: what a UI shows for it (optional, at most 200 characters).
|
|
308
311
|
- `actions`: links a UI offers on top of the kind's own, each
|
|
309
312
|
`{id:, label:, url:}`; http(s) URLs only, without userinfo (`user:pass@`),
|
data/docs/memory.md
CHANGED
|
@@ -270,7 +270,7 @@ profiles, meta bundles that install a set of them:
|
|
|
270
270
|
| profile | bundles |
|
|
271
271
|
|---|---|
|
|
272
272
|
| `core` | loop-guard, check-in, guardrails, slow-commands, agents-md: the recommended safety set |
|
|
273
|
-
| `dev` | known-names, mcp, btw, skills, source-links, github-pr, coordinator |
|
|
273
|
+
| `dev` | known-names, mcp, btw, skills, source-links, github-pr, coordinator, claude-peer |
|
|
274
274
|
|
|
275
275
|
`chi bootstrap` installs `core` and, on a terminal, offers `dev`. An existing
|
|
276
276
|
install gets neither by itself: `chi bundle install core` (or `dev`).
|
data/docs/plugins.md
CHANGED
|
@@ -722,7 +722,7 @@ ctx.sessions.stop(id) # one of this session's own children
|
|
|
722
722
|
```ruby
|
|
723
723
|
ctx.context.attach(url: "https://github.com/acme/app/pull/42", why: "branch feat/x has open PR #42")
|
|
724
724
|
ctx.context.attach(name: "ci", cmd: "bin/ci-status", why: "this branch's CI", every_seconds: 120)
|
|
725
|
-
ctx.context.list # => [{name:, scope:, why:, hint:, fetched_at:, error:, provider:}]
|
|
725
|
+
ctx.context.list # => [{name:, scope:, why:, hint:, fetched_at:, error:, provider:, summary:}]
|
|
726
726
|
ctx.context.decline(url: "https://github.com/acme/app/pull/42") # => "pr-42"
|
|
727
727
|
ctx.context.mark_offered("pr-42", "https://github.com/acme/app/pull/42", why: "branch feat/x has open PR #42")
|
|
728
728
|
ctx.context.offered("pr-42") # => #<data Offer name="pr-42", hint="https://…", why="branch…", at="2026-10-08T…Z">, or nil
|
|
@@ -756,7 +756,10 @@ gets a note when its text changes.
|
|
|
756
756
|
- The source is the session's (not the project's), marked
|
|
757
757
|
`added_by: plugin:<bundle>`. It is plugin code, so it isn't asked about as
|
|
758
758
|
the agent's `chi context add --cmd` is.
|
|
759
|
-
- `list` gives the session's sources, its own then its project's
|
|
759
|
+
- `list` gives the session's sources, its own then its project's (`[]` before
|
|
760
|
+
the session has an id or its file is saved: a command before the first turn). `hint` is the
|
|
761
|
+
one the last text gave (else the source's), `summary` that text's summary
|
|
762
|
+
(nil before any text; chi 0.63.0).
|
|
760
763
|
- Two attaches of one source at once (a double click) both return its
|
|
761
764
|
name.
|
|
762
765
|
- `attach`, `decline` and `mark_offered` raise
|
|
@@ -1504,6 +1507,90 @@ by themselves there.
|
|
|
1504
1507
|
reason: merging a child's branch
|
|
1505
1508
|
```
|
|
1506
1509
|
|
|
1510
|
+
## The claude-peer bundle
|
|
1511
|
+
|
|
1512
|
+
```sh
|
|
1513
|
+
chi bundle install claude-peer # in the dev profile
|
|
1514
|
+
```
|
|
1515
|
+
|
|
1516
|
+
A live link between a chi session and a Claude Code session you already work
|
|
1517
|
+
with: chi sends it a part of the work (a plan step, a diff, a finding) to
|
|
1518
|
+
validate or as context, and Claude's answer comes back into chi. This is
|
|
1519
|
+
Claude with the context it has, not a fresh review run. Nothing runs until a
|
|
1520
|
+
Claude pairs; without one the bundle costs one tool declaration.
|
|
1521
|
+
|
|
1522
|
+
- **Pairing is Claude's.** chi can't see Claude's inbox, so Claude attaches
|
|
1523
|
+
two pushed sources to the chi session: `claude-inbox`, whose hint is its
|
|
1524
|
+
per-session inbox socket (`CLAUDE_CODE_MESSAGING_SOCKET`, Claude Code
|
|
1525
|
+
2.1.224 or later), and `claude-review`, where its answers come. You never
|
|
1526
|
+
run that yourself:
|
|
1527
|
+
1. `/claude install-skill` (once) shows a card with the target,
|
|
1528
|
+
`$CLAUDE_CONFIG_DIR/skills/chi-peer/SKILL.md` (else `~/.claude/…`),
|
|
1529
|
+
whether one is there and, when it differs, a short diff; its Install (or
|
|
1530
|
+
Replace) button runs `/claude install-skill --yes`, which copies the
|
|
1531
|
+
bundle's `scripts/chi-peer-SKILL.md` there with the installed
|
|
1532
|
+
`scripts/pair` path written in. The copy happens inside chi, where no
|
|
1533
|
+
guardrail sees it: the button is your consent. The card also offers the
|
|
1534
|
+
Claude Code permission rule `Read(~/.local/state/samagotchi/plugins/claude-peer/**)`
|
|
1535
|
+
(your state folder), so Claude isn't asked before reading a long part's
|
|
1536
|
+
file; chi never writes Claude's settings.
|
|
1537
|
+
2. In Claude Code you say "pair with chi <id or chi web URL>". The skill runs
|
|
1538
|
+
`sh <bundle>/scripts/pair <id or URL>`, which attaches `claude-inbox` (the
|
|
1539
|
+
socket as its hint, pushed as JSON so a re-pair after a Claude restart
|
|
1540
|
+
refreshes it) and `claude-review`, and lists the session's context. A
|
|
1541
|
+
re-pair keeps Claude's last answer.
|
|
1542
|
+
3. Claude answers a validate or a question with a JSON push to
|
|
1543
|
+
`claude-review` (`text`, `summary`, `wake: true`), which wakes chi, the
|
|
1544
|
+
first answer too (on chi 0.62 the first one arrives as a note without
|
|
1545
|
+
waking); the model reads it with `context_read claude-review`.
|
|
1546
|
+
- **The tool `claude_send(kind, title, text)`**, `kind` being `validate`,
|
|
1547
|
+
`context` or `question`, writes one JSON line to that socket. The message
|
|
1548
|
+
starts with a header, `chi <id> · <kind> · <title>`. A text up to 4,000
|
|
1549
|
+
characters follows inline; a longer one goes to a file,
|
|
1550
|
+
`$XDG_STATE_HOME/samagotchi/plugins/claude-peer/sessions/<id>/<HHMMSS>-<slug>.md`
|
|
1551
|
+
(removed with the session), and the message names it (`payload: <path>`).
|
|
1552
|
+
The tool is declared whether or not a Claude is paired, so pairing doesn't
|
|
1553
|
+
change the prompt. Its errors say what to do: no `claude-inbox` (pair from
|
|
1554
|
+
Claude), the socket gone or refusing (that Claude session ended or
|
|
1555
|
+
restarted: pair again), a text over 200,000 characters.
|
|
1556
|
+
- **`/claude`** (an anytime command, so it works mid-turn) sends without
|
|
1557
|
+
the model:
|
|
1558
|
+
- `/claude` alone shows a card: paired or not, the socket and whether its
|
|
1559
|
+
file is there (it goes when that Claude session ends), the last send, and
|
|
1560
|
+
Claude's last answer (the `claude-review` summary).
|
|
1561
|
+
- `/claude validate <text>` and `/claude context <text>` send the text as it
|
|
1562
|
+
is, titled by its first line: paste a selection after them.
|
|
1563
|
+
- `/claude last` sends the model's last completed answer as context. Mid-turn
|
|
1564
|
+
it is the answer before the running turn, never the turn's partial text;
|
|
1565
|
+
a cancelled or failed turn has no answer, so the one before it goes.
|
|
1566
|
+
- **The skill.** `skill_claude_peer` (system scope) tells the model to send
|
|
1567
|
+
only when you ask, to pick the part rather than the session, not to wait
|
|
1568
|
+
or poll, and how to read the `claude-review` answer (a validate ends with
|
|
1569
|
+
`VERDICT: OK`, `CHANGES` or `UNSURE`).
|
|
1570
|
+
- **Asking first.** The tool has no `targets:`, so guardrails match it by name
|
|
1571
|
+
only: to be asked before each send, add a rule ([Rules in
|
|
1572
|
+
config.yml](guardrails.md#rules-in-configyml)):
|
|
1573
|
+
|
|
1574
|
+
```yaml
|
|
1575
|
+
guardrails:
|
|
1576
|
+
rules:
|
|
1577
|
+
- id: claude-send
|
|
1578
|
+
tool: claude_send
|
|
1579
|
+
verdict: ask
|
|
1580
|
+
reason: sends session content to Claude Code
|
|
1581
|
+
```
|
|
1582
|
+
|
|
1583
|
+
- **Limits.** The socket's format (`{"type":"user","message":{"role":"user","content":"…"}}`)
|
|
1584
|
+
is undocumented: it was found in, and checked on, Claude Code 2.1.296. A
|
|
1585
|
+
line Claude Code can't parse is dropped silently, so a Claude Code update
|
|
1586
|
+
could break this without an error. No auth line is sent: it is optional on
|
|
1587
|
+
macOS and Linux, and native Windows, which needs it, isn't supported.
|
|
1588
|
+
Claude's `crossSessionInbound` setting decides what it does with a peer
|
|
1589
|
+
message: an interactive auto-mode session delivers it; a session that
|
|
1590
|
+
bypasses permissions holds it for approval (in `-p` it expires after 5
|
|
1591
|
+
minutes). It is local only (a Unix socket), but it does hand session
|
|
1592
|
+
content to another agent: only what the model or you picked.
|
|
1593
|
+
|
|
1507
1594
|
## Shutdown
|
|
1508
1595
|
|
|
1509
1596
|
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, coordinator, and the profiles core and dev) has its own semver in its
|
|
16
|
+
loop-guard, mcp, skills, source-links, github-pr, coordinator, claude-peer, 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/sub-agent.md
CHANGED
|
@@ -13,6 +13,12 @@ worker keeps the turn paused. The parent answers with `chi answer`, from what it
|
|
|
13
13
|
knows or after asking its own user, and chi goes on from there. The model gets
|
|
14
14
|
the answer as its tool result, as if you had answered in the web.
|
|
15
15
|
|
|
16
|
+
The other direction, chi sending a part of its work to a Claude Code session
|
|
17
|
+
you already work with (to validate, as context, or with a question), is the
|
|
18
|
+
[claude-peer bundle](plugins.md#the-claude-peer-bundle): Claude pairs with the
|
|
19
|
+
chi session, chi's `claude_send` tool or `/claude` writes to Claude's inbox,
|
|
20
|
+
and Claude's answer comes back as attached context.
|
|
21
|
+
|
|
16
22
|
## The commands
|
|
17
23
|
|
|
18
24
|
```sh
|
data/docs/testing.md
CHANGED
|
@@ -15,10 +15,13 @@ bug) or a broken spec: fix it, or disable that line with the reason (`# rubocop:
|
|
|
15
15
|
No spec reads your own setup. The suite points `XDG_CONFIG_HOME` and `XDG_STATE_HOME` at temp folders,
|
|
16
16
|
clears your `SAMAGOTCHI_*` environment, and keeps every spec off the network (WebMock).
|
|
17
17
|
|
|
18
|
-
The web specs run in plain node. One that loads lit-html (`turn_view.js`,
|
|
19
|
-
`
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
The web specs run in plain node. One that loads lit-html, itself or through a module it imports (`turn_view.js`,
|
|
19
|
+
`turn_html.js`, `card.js`, `copy.js`, `images.js`, `scope.js`, `command_view.js`, `diff_view.js`, `read_view.js`,
|
|
20
|
+
`question_cards.js`, `stream_handlers.js`, the chips, `info_bar.js`, `lists_view.js`, `context_chips.js`, `families_view.js`,
|
|
21
|
+
`chains.js`, `vendor/lit-html.js`) needs a DOM: `import "./dom_shim.js";` as its first import (happy-dom, a devDependency only,
|
|
22
|
+
never shipped; about 0.3 s per spec file that loads it; lit reads `document` as it loads). The shim's `markupOf(template)`,
|
|
23
|
+
`htmlOf(nodes)` and `canon(html)` compare markup without lit's marker comments; the DOM writes text with only `&`, `<`, `>`
|
|
24
|
+
escaped (a `"` in text stays `"`) and an attribute with `&` and `"` escaped. happy-dom 20 picks the wrong option of a `<select>` whose options lit inserted with
|
|
22
25
|
`?selected`: read the option marked `selected` (`option[selected]`), not `select.value`; the browser follows the mark.
|
|
23
26
|
A select bound with `.value` instead reads right in both, but lit sets it before the options exist on a first render:
|
|
24
27
|
`llm_context_chip.js`'s `renderForm` draws twice then. Assert a kept node with `assert.ok(a === b)`: a failing
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require_relative "log"
|
|
5
5
|
require_relative "ref"
|
|
6
|
+
require_relative "row_refs"
|
|
6
7
|
|
|
7
8
|
module Samagotchi
|
|
8
9
|
# One ref on the answer: the text it stands for (+match+, what the page
|
|
@@ -27,30 +28,20 @@ module Samagotchi
|
|
|
27
28
|
MAX_MATCH = 500
|
|
28
29
|
|
|
29
30
|
# A hook's {match:, kind:, target:, label:, actions:}, or a stored
|
|
30
|
-
# entry (String or Symbol keys, nested ones too). A file ref
|
|
31
|
-
#
|
|
32
|
-
#
|
|
31
|
+
# entry (String or Symbol keys, nested ones too). A file ref is held to
|
|
32
|
+
# the session's folder as a plugin row's is (RowRefs.inside, plan D7):
|
|
33
|
+
# +roots+ is RowRefs.roots of it; a relative path is resolved against
|
|
34
|
+
# it (+relative+; a stored one was stored absolute, so it is dropped),
|
|
35
|
+
# a path outside it refused, and the path kept under its real
|
|
36
|
+
# spelling. With no folder (no +roots+), an absolute path stays.
|
|
33
37
|
# @raise [ArgumentError] anything invalid
|
|
34
|
-
def self.from(value,
|
|
38
|
+
def self.from(value, roots: [], relative: true)
|
|
35
39
|
raise ArgumentError, "a ref is a Hash {match:, kind:, target:, label:, actions:}" unless value.is_a?(Hash)
|
|
36
40
|
|
|
37
41
|
value = value.transform_keys(&:to_sym)
|
|
38
42
|
ref = Ref.new(kind: value[:kind], target: value[:target], label: value[:label], actions: value[:actions] || [])
|
|
39
|
-
new(match: value[:match], ref:
|
|
43
|
+
new(match: value[:match], ref: RowRefs.inside(ref, roots, relative))
|
|
40
44
|
end
|
|
41
|
-
|
|
42
|
-
def self.absolute(ref, cwd)
|
|
43
|
-
return ref unless ref.kind == Ref::FILE
|
|
44
|
-
|
|
45
|
-
path = ref.target[:path]
|
|
46
|
-
return ref if File.absolute_path?(path)
|
|
47
|
-
raise ArgumentError, "relative path, no session folder" if cwd.to_s.empty?
|
|
48
|
-
|
|
49
|
-
# absolute_path, not expand_path: a leading ~ is a name, never a home.
|
|
50
|
-
Ref.new(kind: ref.kind, target: ref.target.merge(path: File.absolute_path(path, cwd.to_s)), label: ref.label,
|
|
51
|
-
actions: ref.actions)
|
|
52
|
-
end
|
|
53
|
-
private_class_method :absolute
|
|
54
45
|
end
|
|
55
46
|
|
|
56
47
|
# The refs after_turn hooks attach to a turn's answer (event[:refs]),
|
|
@@ -71,12 +62,13 @@ module Samagotchi
|
|
|
71
62
|
# Stored refs (+raw+, as read from session.json or a snapshot), each
|
|
72
63
|
# rebuilt and checked again, the caps applied; what isn't valid any
|
|
73
64
|
# more is dropped. Never raises: a reload must show the conversation.
|
|
74
|
-
# A relative file path is dropped (
|
|
65
|
+
# A relative file path is dropped (it was stored absolute), and with
|
|
66
|
+
# +cwd+ (the session's folder) a file outside it.
|
|
75
67
|
# @return [Array<Hash>] the refs to send ({match:, kind:, target:, …})
|
|
76
|
-
def self.for_display(raw)
|
|
68
|
+
def self.for_display(raw, cwd: nil)
|
|
77
69
|
return [] if raw.nil?
|
|
78
70
|
|
|
79
|
-
kept = new([{ role: "model", KEY => raw }], cwd:
|
|
71
|
+
kept = new([{ role: "model", KEY => raw }], cwd: cwd).to_a
|
|
80
72
|
dropped = raw.is_a?(Array) ? raw.length - kept.length : 1
|
|
81
73
|
Log.debug(:web, "display_refs_dropped", count: dropped) if dropped.positive?
|
|
82
74
|
kept
|
|
@@ -92,7 +84,7 @@ module Samagotchi
|
|
|
92
84
|
def initialize(messages, cwd:)
|
|
93
85
|
last = Array(messages).last
|
|
94
86
|
@target = last if last.is_a?(Hash) && (last[:role] || last["role"]).to_s == "model"
|
|
95
|
-
@
|
|
87
|
+
@roots = RowRefs.roots(cwd)
|
|
96
88
|
@refs = []
|
|
97
89
|
@seen = {}
|
|
98
90
|
@bytes = 2 # the list's []
|
|
@@ -149,7 +141,7 @@ module Samagotchi
|
|
|
149
141
|
unknown = value.keys - KEYS
|
|
150
142
|
return reject(hook, "has unknown keys #{unknown.map(&:to_s).inspect}") unless unknown.empty?
|
|
151
143
|
|
|
152
|
-
entry = AnswerRef.from(value,
|
|
144
|
+
entry = AnswerRef.from(value, roots: @roots).to_h
|
|
153
145
|
return true if @seen.key?(entry)
|
|
154
146
|
return over_cap(hook, "#{MAX_REFS} refs") if @refs.length >= MAX_REFS
|
|
155
147
|
|
|
@@ -163,11 +155,11 @@ module Samagotchi
|
|
|
163
155
|
end
|
|
164
156
|
|
|
165
157
|
# A stored entry back in the list, checked again (a relative path is
|
|
166
|
-
# dropped: it was stored absolute
|
|
167
|
-
# caps go quietly.
|
|
158
|
+
# dropped: it was stored absolute; so is a file outside the folder).
|
|
159
|
+
# Duplicates and what is over the caps go quietly.
|
|
168
160
|
# @return [Boolean] kept
|
|
169
161
|
def keep(raw)
|
|
170
|
-
entry = AnswerRef.from(raw,
|
|
162
|
+
entry = AnswerRef.from(raw, roots: @roots, relative: false).to_h
|
|
171
163
|
return false if @seen.key?(entry) || @refs.length >= MAX_REFS
|
|
172
164
|
|
|
173
165
|
size = JSON.generate(entry).bytesize + (@refs.empty? ? 0 : 1)
|
data/lib/samagotchi/bridge.rb
CHANGED
|
@@ -180,6 +180,25 @@ module Samagotchi
|
|
|
180
180
|
raise e
|
|
181
181
|
end
|
|
182
182
|
|
|
183
|
+
# Raised on a connection thread whose request would queue something
|
|
184
|
+
# (a turn, a command, an exit) after #close_intake: the connection
|
|
185
|
+
# closes unanswered, as #stop's kill of a request still waiting did.
|
|
186
|
+
class IntakeClosed < StandardError; end
|
|
187
|
+
|
|
188
|
+
# With the event log held (the worker leaving: Worker#leave_idle,
|
|
189
|
+
# #leave_on_request): no connection is accepted and no request queues
|
|
190
|
+
# a turn, a command or an exit from here on. #stop, called once the log
|
|
191
|
+
# is let go, does the rest: a request that waited on the log then ends
|
|
192
|
+
# at once instead of holding #stop for its REQUEST_GRACE_SECONDS.
|
|
193
|
+
def close_intake
|
|
194
|
+
@mutex.synchronize { @stopped = true }
|
|
195
|
+
begin
|
|
196
|
+
@server&.close
|
|
197
|
+
rescue StandardError
|
|
198
|
+
nil
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
183
202
|
# Stop the acceptor, release the socket and remove the sidecar, so clients
|
|
184
203
|
# don't have to find it stale. Does not stop the owning turn. Joins the
|
|
185
204
|
# acceptor thread so the process can exit cleanly (Ruby waits for a thread
|
|
@@ -394,7 +413,7 @@ module Samagotchi
|
|
|
394
413
|
log_request(method, request[:path], started)
|
|
395
414
|
break if close_after_request?(headers)
|
|
396
415
|
end
|
|
397
|
-
rescue Errno::EPIPE, Errno::ECONNRESET, IOError
|
|
416
|
+
rescue Errno::EPIPE, Errno::ECONNRESET, IOError, IntakeClosed
|
|
398
417
|
nil
|
|
399
418
|
rescue StandardError => e
|
|
400
419
|
# The thread doesn't report (report_on_exception = false): log it.
|
|
@@ -728,6 +747,7 @@ module Samagotchi
|
|
|
728
747
|
# A card's action: the UIs leave its line out (the card is the echo).
|
|
729
748
|
command = QueuedCommand.mint(line, client_id: client_id, card: card)
|
|
730
749
|
queued = @engine.synchronize_events do
|
|
750
|
+
raise IntakeClosed if stopped?
|
|
731
751
|
next false if expired?("command_expired", deadline, sid: session_id, client_id: command.client_id)
|
|
732
752
|
|
|
733
753
|
queue_command_locked(command)
|
|
@@ -777,6 +797,7 @@ module Samagotchi
|
|
|
777
797
|
return BAD_DEADLINE unless deadline_valid?(deadline)
|
|
778
798
|
|
|
779
799
|
reason = @engine.synchronize_events do
|
|
800
|
+
raise IntakeClosed if stopped?
|
|
780
801
|
next :expired if expired?("exit_expired", deadline, sid: session_id, client_id: client_id)
|
|
781
802
|
|
|
782
803
|
@on_exit_request.call(client_id, **options)
|
|
@@ -851,6 +872,7 @@ module Samagotchi
|
|
|
851
872
|
# this turn's :turn_started (or merge it mid-turn) before
|
|
852
873
|
# :turn_enqueued, and a failed write announces nothing.
|
|
853
874
|
enqueued = @engine.synchronize_events do
|
|
875
|
+
raise IntakeClosed if stopped?
|
|
854
876
|
next :expired if expired?("turn_expired", deadline, sid: sid, client_id: client_id)
|
|
855
877
|
|
|
856
878
|
enqueue_turn(prompt: prompt, client_id: client_id, enqueued_id: enqueued_id,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: claude-peer
|
|
3
|
+
version: 0.1.0
|
|
4
|
+
scope: system
|
|
5
|
+
description: "Pair this session with a Claude Code session you work with: the claude_send tool and /claude send it a part of the work (a plan step, a diff, a finding) to validate or as context, and Claude's answer comes back as the claude-review context; /claude install-skill installs Claude's chi-peer skill, which pairs"
|
|
6
|
+
trust_level: reviewed
|
|
7
|
+
files:
|
|
8
|
+
skill_claude_peer.md:
|
|
9
|
+
sha256: sha256:dff0175d0370f43920daf5ecbb81315fdd2c7d8063a33fbefc75c7d0e7312993
|
|
10
|
+
description: "Send a part of the work to the paired Claude Code session (claude_send): only when the user asks; validate, context or question; Claude's answer comes as the claude-review context"
|
|
11
|
+
plugin:
|
|
12
|
+
file: plugin.rb
|
|
13
|
+
sha256: sha256:bf5a95eb125ba4831466e177295bdaba1f625e255b027cea1e59992310d8300e
|
|
14
|
+
scripts:
|
|
15
|
+
pair: sha256:033f1fc6a644950b57e71865253526a0a487bb4c9fd5a61bb89d031b65f40417
|
|
16
|
+
chi-peer-SKILL.md: sha256:f35f726b1a86091ff69d289e8dd58794ac6f44879a63630fcc88d69867c56e20
|
|
17
|
+
# Two things need chi 0.63.0, and the bundle works without them on 0.62:
|
|
18
|
+
# ctx.context.list's summary: (the /claude card's last answer; without it
|
|
19
|
+
# the card shows no summary), and a pushed source's first wake: true push
|
|
20
|
+
# waking the session (on 0.62 Claude's first answer arrives as a note
|
|
21
|
+
# without waking; later ones wake).
|
|
22
|
+
requires_chi: ">= 0.62.0"
|