samagotchi 0.66.0 → 0.68.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 +80 -1
- data/docs/architecture.md +55 -30
- data/docs/cli.md +3 -1
- data/docs/configuration.md +50 -9
- data/docs/context.md +35 -1
- data/docs/desktop.md +90 -22
- data/docs/guardrails.md +8 -5
- data/docs/hooks.md +2 -2
- data/docs/internals/anthropic.md +156 -0
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bundles.md +2 -0
- data/docs/internals/engine.md +129 -38
- data/docs/internals/gemma4-contract.md +1 -1
- data/docs/internals/llm-context-bench.md +5 -4
- data/docs/internals/prompt-caching.md +26 -11
- data/docs/plugins.md +126 -23
- data/docs/sessions.md +6 -6
- data/docs/sub-agent.md +4 -1
- data/docs/testing.md +18 -4
- data/lib/samagotchi/bootstrap/probe.rb +4 -3
- data/lib/samagotchi/bootstrap_command.rb +13 -3
- data/lib/samagotchi/bridge/ring_buffer.rb +0 -5
- data/lib/samagotchi/bridge.rb +6 -3
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +7 -6
- data/lib/samagotchi/bundles/claude-peer/plugin.rb +34 -313
- data/lib/samagotchi/bundles/claude-peer/scripts/chi-peer-SKILL.md +8 -3
- data/lib/samagotchi/bundles/claude-peer/skill_claude_peer.md +14 -6
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +7 -6
- data/lib/samagotchi/bundles/codex-peer/plugin.rb +37 -312
- data/lib/samagotchi/bundles/codex-peer/scripts/chi-peer-SKILL.md +8 -3
- data/lib/samagotchi/bundles/codex-peer/skill_codex_peer.md +14 -6
- data/lib/samagotchi/bundles/mcp/manifest.yml +5 -5
- data/lib/samagotchi/bundles/mcp/plugin.rb +82 -294
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +7 -3
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/config.rb +15 -7
- data/lib/samagotchi/context_absorber.rb +66 -13
- data/lib/samagotchi/desktop/macos/AgentTerminal.swift +260 -0
- data/lib/samagotchi/desktop/macos/App.swift +3 -3
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +5 -2
- data/lib/samagotchi/desktop/macos/Images.swift +2 -2
- data/lib/samagotchi/desktop/macos/Kitty.swift +33 -197
- data/lib/samagotchi/desktop/macos/Panel.swift +118 -82
- data/lib/samagotchi/desktop/macos/ProcessRunner.swift +1 -1
- data/lib/samagotchi/desktop/macos/Tmux.swift +239 -0
- data/lib/samagotchi/desktop/macos.rb +84 -13
- data/lib/samagotchi/desktop_command.rb +14 -3
- data/lib/samagotchi/effective_model.rb +283 -0
- data/lib/samagotchi/engine.rb +307 -549
- data/lib/samagotchi/engine_peers.rb +3 -0
- data/lib/samagotchi/guardrail_wiring.rb +1 -1
- data/lib/samagotchi/guardrails/approval.rb +1 -1
- data/lib/samagotchi/guardrails/approvals.rb +1 -1
- data/lib/samagotchi/guardrails/rules.rb +1 -1
- data/lib/samagotchi/guardrails/targets.rb +1 -1
- data/lib/samagotchi/hook_routes.rb +151 -0
- data/lib/samagotchi/hooks/registry.rb +2 -2
- data/lib/samagotchi/host_keys.rb +147 -0
- data/lib/samagotchi/level_setting_command.rb +99 -0
- data/lib/samagotchi/llm/adapter_common.rb +138 -0
- data/lib/samagotchi/llm/anthropic_messages.rb +28 -105
- data/lib/samagotchi/llm/api_key.rb +23 -7
- data/lib/samagotchi/llm/chat_loop.rb +10 -6
- data/lib/samagotchi/llm/openai_chat.rb +9 -92
- data/lib/samagotchi/llm/refusal_memo.rb +29 -0
- data/lib/samagotchi/llm_context_override.rb +1 -0
- data/lib/samagotchi/llm_context_strategy.rb +5 -10
- data/lib/samagotchi/peer_sources.rb +58 -0
- data/lib/samagotchi/plugin/api.rb +64 -155
- data/lib/samagotchi/plugin/loader.rb +4 -2
- data/lib/samagotchi/plugin/peer.rb +394 -0
- data/lib/samagotchi/plugin/tool_spec.rb +166 -0
- data/lib/samagotchi/plugin_tasks.rb +2 -2
- data/lib/samagotchi/reasoning_history_command.rb +29 -90
- data/lib/samagotchi/row_refs.rb +1 -1
- data/lib/samagotchi/self_report.rb +5 -3
- data/lib/samagotchi/send_command.rb +21 -11
- data/lib/samagotchi/session.rb +22 -8
- data/lib/samagotchi/session_continue.rb +58 -15
- data/lib/samagotchi/session_inbox.rb +0 -13
- data/lib/samagotchi/session_setting.rb +86 -0
- data/lib/samagotchi/thinking_command.rb +24 -76
- data/lib/samagotchi/thinking_notices.rb +169 -0
- data/lib/samagotchi/tool_call_parser.rb +1 -1
- data/lib/samagotchi/toolkit_binding.rb +138 -0
- data/lib/samagotchi/tools/context_read.rb +14 -0
- data/lib/samagotchi/tools/execute.rb +12 -12
- data/lib/samagotchi/tools/registry.rb +1 -1
- data/lib/samagotchi/tools/task_create.rb +6 -6
- data/lib/samagotchi/tools/task_get.rb +13 -13
- data/lib/samagotchi/tools/task_list.rb +6 -6
- data/lib/samagotchi/tools/task_record.rb +52 -0
- data/lib/samagotchi/tools/task_runtime.rb +55 -91
- data/lib/samagotchi/tools/task_stop.rb +6 -6
- data/lib/samagotchi/tools/task_wait.rb +12 -13
- data/lib/samagotchi/tools/tool_index.rb +454 -0
- data/lib/samagotchi/update_command.rb +9 -6
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +71 -93
- data/lib/samagotchi/web/capabilities.rb +1 -1
- data/lib/samagotchi/web/public/activity.js +2 -2
- data/lib/samagotchi/web/public/annotate_actions.js +1 -1
- data/lib/samagotchi/web/public/app.js +103 -539
- data/lib/samagotchi/web/public/bubbles.js +291 -0
- data/lib/samagotchi/web/public/cards_place.js +184 -0
- data/lib/samagotchi/web/public/chains.js +1 -1
- data/lib/samagotchi/web/public/chip_popover.js +111 -0
- data/lib/samagotchi/web/public/command_list.js +12 -10
- data/lib/samagotchi/web/public/commands.js +2 -2
- data/lib/samagotchi/web/public/composer_size.js +2 -2
- data/lib/samagotchi/web/public/copy.js +1 -1
- data/lib/samagotchi/web/public/ctx.js +1 -1
- data/lib/samagotchi/web/public/delivery_line.js +4 -4
- data/lib/samagotchi/web/public/families_view.js +1 -1
- data/lib/samagotchi/web/public/format.js +11 -7
- data/lib/samagotchi/web/public/images.js +25 -2
- data/lib/samagotchi/web/public/index.html +15 -7
- data/lib/samagotchi/web/public/info_bar.js +3 -3
- data/lib/samagotchi/web/public/info_bar_model.js +110 -0
- data/lib/samagotchi/web/public/init_row.js +21 -10
- data/lib/samagotchi/web/public/level_chip.js +29 -80
- data/lib/samagotchi/web/public/lists_view.js +64 -9
- data/lib/samagotchi/web/public/llm_context_chip.js +38 -107
- data/lib/samagotchi/web/public/model_picker.js +26 -22
- data/lib/samagotchi/web/public/notify.js +6 -4
- data/lib/samagotchi/web/public/question_card.js +1 -1
- data/lib/samagotchi/web/public/ref_scan.js +2 -8
- data/lib/samagotchi/web/public/refs.js +13 -0
- data/lib/samagotchi/web/public/session_state.js +14 -0
- data/lib/samagotchi/web/public/session_title.js +2 -3
- data/lib/samagotchi/web/public/sessions_list.js +0 -8
- data/lib/samagotchi/web/public/stage_frame.js +179 -0
- data/lib/samagotchi/web/public/stage_model.js +35 -2
- data/lib/samagotchi/web/public/stage_slots.js +130 -0
- data/lib/samagotchi/web/public/stage_view.js +56 -335
- data/lib/samagotchi/web/public/stage_watch.js +65 -0
- data/lib/samagotchi/web/public/strip.js +3 -3
- data/lib/samagotchi/web/public/task_stop.js +3 -2
- data/lib/samagotchi/web/public/turn_events.js +1 -1
- data/lib/samagotchi/web/public/turn_html.js +2 -2
- data/lib/samagotchi/web/public/turn_model.js +2 -2
- data/lib/samagotchi/web/public/turn_view.js +1 -1
- data/lib/samagotchi/web/public/vendor/directive-helpers.js +8 -0
- data/lib/samagotchi/web/public/vendor/directives/repeat.js +9 -0
- data/lib/samagotchi/web/server.rb +0 -4
- data/lib/samagotchi/worker.rb +22 -24
- data/lib/samagotchi/worker_inbound.rb +36 -4
- data/lib/samagotchi/worker_wakes.rb +67 -7
- metadata +27 -1
data/docs/plugins.md
CHANGED
|
@@ -291,7 +291,7 @@ is what `targets:` says:
|
|
|
291
291
|
- `command:`: a shell command the call runs; `command:` rules match it.
|
|
292
292
|
- `cwd:`: where it runs, for the repo root and relative paths.
|
|
293
293
|
- `acts_as:`: the tool a call stands for, when the tool runs other tools
|
|
294
|
-
(
|
|
294
|
+
(chi's `call_tool`, which calls a tool a [source](#chitool_sourcecall-acts_as-label-wait-prefix) published). Rules keyed by that tool's name match the
|
|
295
295
|
call too, as well as rules on the tool's own name. It can't be one of
|
|
296
296
|
chi's own tools or another bundle's tool: that is dropped (and logged).
|
|
297
297
|
- `args:`: the arguments it acts with (a Hash), when they aren't the call's
|
|
@@ -308,7 +308,7 @@ targets: ->(args) { { acts_as: "mcp_#{args["server"]}_#{args["tool"]}", args: ar
|
|
|
308
308
|
```
|
|
309
309
|
|
|
310
310
|
An approval of an `acts_as:` call keys on the tool's own name as well
|
|
311
|
-
(`
|
|
311
|
+
(`call_tool>mcp_github_x:…`), so it never stands in for an approval of the
|
|
312
312
|
tool it acts as, nor that one for it. Hooks see the call's own name, and so
|
|
313
313
|
does loop-guard's `ignore_tools`.
|
|
314
314
|
|
|
@@ -548,7 +548,7 @@ end
|
|
|
548
548
|
- **`provides_tools: true`**: the task provides what the plugin's tools
|
|
549
549
|
need: tools of its own (with `chi.replace_tools`), or what fixed tools
|
|
550
550
|
read (the mcp bundle's index of a first-run server's tools, which
|
|
551
|
-
`
|
|
551
|
+
`find_tools` searches). A turn sent while it runs starts at once (the user's
|
|
552
552
|
message shows), then waits for it **before its first model request**, so
|
|
553
553
|
the model sees the tools; the UIs keep showing the task meanwhile. The
|
|
554
554
|
wait lasts at most `timeout` seconds from the task's start (default 60).
|
|
@@ -579,6 +579,89 @@ tool (full) until it comes back, with a warning at its first turn. See
|
|
|
579
579
|
chi.toolkit "review", base: "lean", add: %w[delegate delegate_result]
|
|
580
580
|
```
|
|
581
581
|
|
|
582
|
+
### `chi.tool_source(call:, acts_as:, label:, wait:, prefix:)`
|
|
583
|
+
|
|
584
|
+
Tools the model reaches through a search instead of declaring them one by
|
|
585
|
+
one: the plugin publishes them into chi's **tool index**, and chi declares
|
|
586
|
+
two tools in all, whatever the number of sources: `find_tools` (a keyword
|
|
587
|
+
search that answers each match's `inputSchema`) and `call_tool` (calls one as
|
|
588
|
+
`<group>/<tool>`; its row label is the first source's `label`). They
|
|
589
|
+
are declared once, after the plugins load, and only when a plugin registered
|
|
590
|
+
a source: a session without one declares neither. Nothing a source publishes
|
|
591
|
+
later changes the request's tools, only what a search answers, so the prompt
|
|
592
|
+
cache keeps (the search tool's description lists the groups as they were at
|
|
593
|
+
load). The mcp bundle is the only source so far (a group per server), and the
|
|
594
|
+
texts speak of MCP servers.
|
|
595
|
+
|
|
596
|
+
```ruby
|
|
597
|
+
@source = chi.tool_source(label: "mcp", prefix: "mcp_",
|
|
598
|
+
call: ->(tool, args, ctx) { run(tool, args, ctx) }, # the result, as a tool block's
|
|
599
|
+
acts_as: ->(group, tool) { "mcp_#{group}_#{tool}" }, # what rules match
|
|
600
|
+
wait: ->(group) { start_of(group).value }) # a :starting group's start
|
|
601
|
+
index = Samagotchi::Tools::ToolIndex
|
|
602
|
+
@source.publish([index::Group.new(name: "fs", summary: "files in ~/scratch", tools: [
|
|
603
|
+
index::Tool.new(group: "fs", name: "read", description: "Read a file.", schema: read_schema,
|
|
604
|
+
acts_as: "mcp_fs_read")
|
|
605
|
+
]),
|
|
606
|
+
index::Group.new(name: "chrome", state: :starting)])
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
- **`publish(groups)`** swaps in the plugin's whole list, from any thread, at
|
|
610
|
+
any time (a server that started, `list_changed`): a search sees the old
|
|
611
|
+
list or the new one, never half. A `Group` has a `summary` (its line in
|
|
612
|
+
`find_tools`' description), a `state` (`:ready`; `:starting`, its tools may
|
|
613
|
+
still come: a call to it waits on `wait`, a search says "starting, try
|
|
614
|
+
again"; `:failed` with its `error`) and its tools. A group exists before its
|
|
615
|
+
tools do. A `Tool`'s `refusal` says why `call_tool` refuses it (nil: it can
|
|
616
|
+
be called), its `mark` what a search shows beside its name: the plugin
|
|
617
|
+
decides both (the mcp bundle: a bad schema, a name clash) and publishes
|
|
618
|
+
them again when they change.
|
|
619
|
+
- **`call`** gets a call chi looked up and checked: the `Tool`, its `args`
|
|
620
|
+
typed by its `inputSchema`, and the plugin's `ctx`. It returns what a tool's
|
|
621
|
+
block returns (a String, a `ToolResult` with images).
|
|
622
|
+
- **Guardrails** see a `call_tool` as the tool it calls: its `acts_as`, its
|
|
623
|
+
arguments and `"<group>: <tool>"` as the question's label
|
|
624
|
+
([acts_as:](#guardrails)). A call made before its tool is published is
|
|
625
|
+
named by `acts_as` from the names alone; a bare name with `prefix` is
|
|
626
|
+
taken as an `acts_as` as given. The `acts_as:` hook must give exactly the
|
|
627
|
+
`acts_as` the source publishes for that tool: a call whose tool isn't the
|
|
628
|
+
one guardrails were given is refused ("the MCP tools changed while the call
|
|
629
|
+
waited"), so a hook that disagrees refuses every call.
|
|
630
|
+
- **The row** shows `label` (default: the bundle's name) and
|
|
631
|
+
`<group>/<tool> args…`. `@source.declared_names` and `declared_tokens` say
|
|
632
|
+
what the two tools take in every request (none when other tools had both
|
|
633
|
+
names). A `Group` in a state other than the three, holding another group's
|
|
634
|
+
tool, or a `Tool` with a blank `acts_as` raises `ArgumentError`.
|
|
635
|
+
- `find_tools` and `call_tool` aren't chi's own tools: every toolkit declares
|
|
636
|
+
them (unless its `remove:` names them), as it does plugin tools. A bundle
|
|
637
|
+
that declares tools of those names itself (an mcp bundle before 0.7.0 has
|
|
638
|
+
`find_mcp_tools` and `mcp_call`) keeps them, and with no source chi
|
|
639
|
+
declares neither.
|
|
640
|
+
|
|
641
|
+
### `chi.peer_source(name, label:)`
|
|
642
|
+
|
|
643
|
+
Declares a context source as a peer agent's answers: the peer bundles'
|
|
644
|
+
`claude-review` and `codex-review`. Such a source, when it is a pushed source
|
|
645
|
+
of the session's own, wakes the session by the peer loop's rules instead of
|
|
646
|
+
the 10-minute window: up to `context.peer_turns` wake turns in a row, no wake
|
|
647
|
+
for an answer whose verdict is `VERDICT OK`, and a wake line that names the
|
|
648
|
+
peer and keeps the turn within the user's task (see [Waking: peer
|
|
649
|
+
sources](context.md#peer-sources-the-peer-loop)).
|
|
650
|
+
|
|
651
|
+
```ruby
|
|
652
|
+
chi.peer_source "codex-review", label: "Codex"
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
- `name`: a context source name (as `chi context add` takes it). Another
|
|
656
|
+
bundle's is a load error.
|
|
657
|
+
- `label:`: the peer's name, said in chi's own lines ("your peer agent
|
|
658
|
+
Codex"): a capital letter, then up to 23 letters and spaces.
|
|
659
|
+
|
|
660
|
+
The declaration lives in the session's process only, like a tool: a source
|
|
661
|
+
file can't make itself a peer's, and uninstalling the bundle ends it.
|
|
662
|
+
Subclasses of `Samagotchi::Plugin::Peer` (below) declare their `REVIEW`
|
|
663
|
+
source. Needs chi 0.67.0 (guard it with `chi.respond_to?(:peer_source)`).
|
|
664
|
+
|
|
582
665
|
### `chi.ctx`
|
|
583
666
|
|
|
584
667
|
The plugin's context (the `ctx` its handlers get), for `register` itself:
|
|
@@ -907,7 +990,9 @@ only against this API (`lib/samagotchi/bundles/btw/plugin.rb`).
|
|
|
907
990
|
only against this API (`lib/samagotchi/bundles/mcp/plugin.rb`), and gives
|
|
908
991
|
the model the tools of [MCP](https://modelcontextprotocol.io) servers. It
|
|
909
992
|
has no memory file, and the servers' tools aren't declared to the model one
|
|
910
|
-
by one:
|
|
993
|
+
by one: the bundle publishes them into chi's tool index
|
|
994
|
+
([chi.tool_source](#chitool_sourcecall-acts_as-label-wait-prefix), a group
|
|
995
|
+
per server), and the model has its two tools, `find_tools` and `call_tool` (~250 tokens in
|
|
911
996
|
every request plus a line per server, whatever the servers have; `/mcp`
|
|
912
997
|
says how many). Stdio servers only, for now.
|
|
913
998
|
|
|
@@ -925,7 +1010,7 @@ bundles:
|
|
|
925
1010
|
env: {NODE_OPTIONS: "--no-warnings"} # added to chi's environment
|
|
926
1011
|
cwd: ~/scratch # default: where chi runs
|
|
927
1012
|
tools: [read_*, list_directory] # optional: only these (globs)
|
|
928
|
-
description: files in ~/scratch # optional: its line in
|
|
1013
|
+
description: files in ~/scratch # optional: its line in find_tools
|
|
929
1014
|
timeout: 120 # optional: this server's per-call timeout
|
|
930
1015
|
chrome:
|
|
931
1016
|
command: [npx, -y, "chrome-devtools-mcp@latest", --slim, --headless]
|
|
@@ -944,7 +1029,7 @@ bundles:
|
|
|
944
1029
|
one is replaced, and so is what a search finds. The model's tools never
|
|
945
1030
|
change, so the prompt cache keeps. The saved file also keeps the first
|
|
946
1031
|
sentence of the server's `instructions` (from `initialize`), for its line
|
|
947
|
-
in `
|
|
1032
|
+
in `find_tools`.
|
|
948
1033
|
- **The first run** (no saved list, or the config changed) starts the
|
|
949
1034
|
server in an [init task](#chiinitlabel-provides_tools-false-quiet-false-timeout-nil--ctx--):
|
|
950
1035
|
every UI shows `Starting MCP server x (first run, saving its tools)`, and
|
|
@@ -977,9 +1062,9 @@ bundles:
|
|
|
977
1062
|
command: [chrome-devtools-mcp, --slim, --headless] # after npm i -g chrome-devtools-mcp
|
|
978
1063
|
```
|
|
979
1064
|
|
|
980
|
-
- **Tools: search, then call.** The servers' tools are kept in
|
|
1065
|
+
- **Tools: search, then call.** The servers' tools are kept in chi's tool index;
|
|
981
1066
|
the model has two fixed tools:
|
|
982
|
-
- `
|
|
1067
|
+
- `find_tools(query, server?)` searches it by keywords over the
|
|
983
1068
|
server's name, the tool's name and its description, and answers up to 5
|
|
984
1069
|
tools, each with its name `<server>/<tool>`, its description and its
|
|
985
1070
|
whole `inputSchema`. A word counts more the fewer tools have it (a word
|
|
@@ -995,7 +1080,7 @@ bundles:
|
|
|
995
1080
|
`instructions`, and how many tools it has (`- chrome (26 tools)` with
|
|
996
1081
|
neither: tool names there get called blind, without a search; a first
|
|
997
1082
|
run's server is just its name until the next session).
|
|
998
|
-
- `
|
|
1083
|
+
- `call_tool(tool, args)` calls one: `tool` is `<server>/<tool>`, and
|
|
999
1084
|
`mcp_<server>_<tool>` works too (looked up, not parsed). Two tools whose
|
|
1000
1085
|
`mcp_<server>_<tool>` comes out the same (servers `git-hub` and
|
|
1001
1086
|
`git_hub`, names alike up to the 48-character cut) are both refused,
|
|
@@ -1039,18 +1124,18 @@ bundles:
|
|
|
1039
1124
|
closed, then TERM and KILL go to the server's process group.
|
|
1040
1125
|
- **`/mcp`** (anytime) shows a card with the servers, their state (cached
|
|
1041
1126
|
(not started), running with its pid, failed with its count, stopped) and
|
|
1042
|
-
their tools as `<server>/<tool>`, the name `
|
|
1127
|
+
their tools as `<server>/<tool>`, the name `call_tool` takes.
|
|
1043
1128
|
A cached or running server's line says roughly how many tokens its tool
|
|
1044
1129
|
definitions take (`~4,232 tokens`), which only a search's answer carries
|
|
1045
|
-
now; the last line says what every request carries (`
|
|
1046
|
-
`
|
|
1130
|
+
now; the last line says what every request carries (`find_tools` and
|
|
1131
|
+
`call_tool`) and totals the servers'. It is an estimate:
|
|
1047
1132
|
the definitions' JSON as the chat API gets it (`api: openai`), divided by
|
|
1048
1133
|
4. The native prompts (Gemma, Qwen) render a flatter schema, so there they
|
|
1049
1134
|
take less. The log records each server's estimate when it changes
|
|
1050
1135
|
(`mcp_tools_estimated`).
|
|
1051
1136
|
- The server's stderr goes to the debug log (`plugins` records, bundle=mcp).
|
|
1052
1137
|
- **Guardrails.** A rule's `tool:` can be a glob, so one rule covers every
|
|
1053
|
-
MCP tool (a search is `
|
|
1138
|
+
MCP tool (a search is `find_tools`, outside `mcp_*`, so it isn't
|
|
1054
1139
|
asked about):
|
|
1055
1140
|
|
|
1056
1141
|
```yaml
|
|
@@ -1062,20 +1147,21 @@ bundles:
|
|
|
1062
1147
|
reason: an MCP server's tool
|
|
1063
1148
|
```
|
|
1064
1149
|
|
|
1065
|
-
`
|
|
1150
|
+
`call_tool`'s `targets:` act as the tool it calls, by its
|
|
1066
1151
|
`mcp_<server>_<tool>` name ([acts_as:](#guardrails)), so `tool:
|
|
1067
|
-
"mcp_github_*"` or `tool: mcp_github_merge_pull_request` match
|
|
1068
|
-
`
|
|
1152
|
+
"mcp_github_*"` or `tool: mcp_github_merge_pull_request` match a
|
|
1153
|
+
`call_tool` to it, as they matched the tool when it was declared by that
|
|
1069
1154
|
name. A call made before the server's tools are known (a first run
|
|
1070
1155
|
still starting) is matched by the name the model gave, so the rule fires
|
|
1071
1156
|
before the call waits; if the tool it then finds isn't that one, the
|
|
1072
1157
|
call is refused. The question shows the inner arguments under `<server>: <tool>`
|
|
1073
1158
|
(`everything: get_sum: a=20 b=22`), and "Allow this call for the
|
|
1074
1159
|
session" (or in this repo) allows that tool with those arguments only.
|
|
1075
|
-
(Approvals stored before 0.6.0, under the old name, don't carry over
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
`
|
|
1160
|
+
(Approvals stored before 0.6.0, under the old name, don't carry over, nor
|
|
1161
|
+
ones stored before 0.7.0 under `mcp_call`: each MCP tool is asked about once more.)
|
|
1162
|
+
Two things see `call_tool`, not the tool: hooks (a `before_tool_call`
|
|
1163
|
+
event's `tool:` is `"call_tool"`, the tool in its args), and loop-guard's
|
|
1164
|
+
`ignore_tools` (`call_tool` ignores every MCP call; calls to different
|
|
1079
1165
|
tools, or with different args, stay distinct, since the tool is in the
|
|
1080
1166
|
args).
|
|
1081
1167
|
|
|
@@ -1653,10 +1739,20 @@ Claude pairs; without one the bundle costs one tool declaration.
|
|
|
1653
1739
|
- `/claude last` sends the model's last completed answer as context. Mid-turn
|
|
1654
1740
|
it is the answer before the running turn, never the turn's partial text;
|
|
1655
1741
|
a cancelled or failed turn has no answer, so the one before it goes.
|
|
1742
|
+
- **The peer loop.** The bundle declares `claude-review` a peer's
|
|
1743
|
+
([`chi.peer_source`](#chipeer_sourcename-label)), so Claude's answers
|
|
1744
|
+
wake chi by the loop's rules ([Waking: peer
|
|
1745
|
+
sources](context.md#peer-sources-the-peer-loop)): chi fixes what Claude
|
|
1746
|
+
flagged or disagrees and sends again, up to `context.peer_turns` (3) wake
|
|
1747
|
+
turns in a row, and a `VERDICT OK` answer arrives as a note and ends the
|
|
1748
|
+
round. Only a message you type in the web or a TUI starts the count again.
|
|
1656
1749
|
- **The skill.** `skill_claude_peer` (system scope) tells the model to send
|
|
1657
|
-
only when you ask, to pick the part rather than
|
|
1658
|
-
or poll, and how to read the `claude-review`
|
|
1659
|
-
`VERDICT: OK`, `CHANGES` or `UNSURE`)
|
|
1750
|
+
only when you ask or to reply in a peer turn, to pick the part rather than
|
|
1751
|
+
the session, not to wait or poll, and how to read the `claude-review`
|
|
1752
|
+
answer (a validate ends with `VERDICT: OK`, `CHANGES` or `UNSURE`): act on
|
|
1753
|
+
it only within your task, and stop and tell you when Claude agrees or after
|
|
1754
|
+
the last turn. Claude's skill ends each answer's summary with its verdict
|
|
1755
|
+
and never answers with `chi send` (which counts as you typing).
|
|
1660
1756
|
- **Asking first.** The tool has no `targets:`, so guardrails match it by name
|
|
1661
1757
|
only: to be asked before each send, add a rule ([Rules in
|
|
1662
1758
|
config.yml](guardrails.md#rules-in-configyml)):
|
|
@@ -1680,6 +1776,10 @@ Claude pairs; without one the bundle costs one tool declaration.
|
|
|
1680
1776
|
bypasses permissions holds it for approval (in `-p` it expires after 5
|
|
1681
1777
|
minutes). It is local only (a Unix socket), but it does hand session
|
|
1682
1778
|
content to another agent: only what the model or you picked.
|
|
1779
|
+
- **Needs chi 0.67.0** for the loop (`chi update`): the plugin is built on
|
|
1780
|
+
`Samagotchi::Plugin::Peer` (lib/samagotchi/plugin/peer.rb), the base both
|
|
1781
|
+
peer bundles share (the tool, `/claude`, the cards, payloads), and each
|
|
1782
|
+
bundle sets its strings and transport.
|
|
1683
1783
|
|
|
1684
1784
|
## The codex-peer bundle
|
|
1685
1785
|
|
|
@@ -1758,6 +1858,9 @@ skill install card) works the same.
|
|
|
1758
1858
|
Codex's names, and on a missing answer points to Codex being mid-turn or
|
|
1759
1859
|
its sandbox (the `writable_roots` line). To be asked before each send, a
|
|
1760
1860
|
guardrails rule with `tool: codex_send` (see the claude-peer section).
|
|
1861
|
+
- **The peer loop** works as claude-peer's: `codex-review` is declared a
|
|
1862
|
+
peer's, Codex's answers wake chi up to `context.peer_turns` turns in a row,
|
|
1863
|
+
and `VERDICT OK` ends the round. Needs chi 0.67.0.
|
|
1761
1864
|
|
|
1762
1865
|
## Shutdown
|
|
1763
1866
|
|
data/docs/sessions.md
CHANGED
|
@@ -76,7 +76,7 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
|
|
|
76
76
|
- `Session.list` / `SessionManager.list_sessions` / `GET /api/sessions?sort=&order=&limit=&offset=` default to `updated_at desc` (newest activity first). Also supports `created_at`, `asc`. `X-Total-Count` header when paginated.
|
|
77
77
|
- Web UI (`chi web`): the page's scope is in its URL. Started in a git repo, `chi web` opens `/?dir=<that folder>`: that project's sessions, and new chats start in that folder; the header chip says `<project> · all`, and `all` opens the same place without `?dir` (every session; a new chat there starts in the server's own folder, shown on the start page, and cards name their folder; `← <project>` goes back to the project view it came from, or to the server's own project). One server serves every project: a second `chi web` (from another repo) finds it through `GET /api/info` and prints (with `--open`, opens) its page for its own folder instead of starting another. The 3 latest sessions sit above the chat (`hide ▴` puts them away behind a `▾ N sessions` pill, remembered; in a window under 900 px wide, where the strip scrolls sideways, `▴ hide` sits in the top bar where the pill comes back; a window under 700 px tall hides them by itself, so a question or approval card in the stage keeps room, and the pill shows them until the window is tall again). A parent and its delegates are one card there: its `▸ 2 delegates · 1 live · 1 waiting` chip (the waiting part in the attention colour, the card's border too) opens their rows in a popover under it (a row per delegate at any depth, indented; a click opens that one, Esc or a click outside closes it, and the strip's order holds while it is open). A fork is a card of its own, and so is a delegate whose parent isn't listed (archived, another project), with its `↳ <parent>` chip; the pill counts cards, and every live or waiting session. "All sessions" (or `/`) opens every session at `#/sessions`, with a search over preview, id and status (Esc or Back returns). There a parent's card holds its delegates the same way, the chip opening their rows inside the card (each with an archive button on hover), open by itself while one of them waits on you; the count says `14 (+7 delegates)`, a search that matches a delegate shows its parent's card with the list open and the matching rows marked, and select mode picks cards (archiving a parent takes its delegates). The open session is in the URL (`#/s/<id>`), so a reload or a copied link opens it again; the chi logo top left goes back to the empty start for a new chat. The message box grows with its text; drag its top edge to keep it taller (double-click resets). A file in a tool row (a read, edit or write's title) and its diff lines are refs: hover one and click to open the current file in your editor at that line, on this machine only (`web.editor`; the browser's "always allow" for the page's port and VS Code's "allow opening local files" are asked once; see [Opening files from chi web](configuration.md#opening-files-from-chi-web)). So are an edit or write approval card's paths (at the first line the dry run changes) and its diff lines, before you approve, and the numbered lines of a read's output: `lines 40–60` under it opens the text as read, numbered, when the output is the file as-is (not the head/tail preview of a big file, an error or an image; no numbers after a cut). A `path:line` (or `path:line-line`) in an answer or a step's text is one too once the turn has ended, and after a reload, when it names a file a row or card on the page touched: the path under the session's folder, else the one touched file it ends with (a bare `app.js:12` when only one touched file is named so; two such files make it none); a path without a line counts only when it is a touched file exactly. Code blocks, links, bare URLs and inline code with more than the ref in it are left alone, and a file the session didn't touch is never linked; a link whose text is such a ref alone (github-pr's link to the PR diff) becomes a ref too, and its plain click still goes to the PR. A plugin's refs on an answer (an `after_turn` hook's, see [Refs on the answer](hooks.md#refs-on-the-answer-display-only)) are paired with its links and text whatever the rows touched: they can link a file the session didn't touch, or an issue or a commit, and add their own actions to the ref's menu. A plugin tool's row carries the refs its `refs:` gives (see [Refs on a tool's row](plugins.md#refs-on-a-tools-row)): its title opens the first, and its `▾` menu adds the others as `<ref> · <action>` (the URL on hover); their URL actions work on a phone too. A ref with more than one target shows a small `▾` on hover: it opens a menu of them, in order **PR diff** (a link's), **Open in editor**, **Show in turn** and **Copy path**, and a plain click runs the first (a link's own, else the editor, else show in turn). **Show in turn** scrolls to the tool row that touched the file (looked for in the ref's own step, then its own turn, then the rows before it, then the nearest after it; there, the last one showing the ref's line, else the last one), opens the steps, the stage's steps and the row's diff (or the read showing the line) and flashes the row and the line; an answer's or a step's ref and an approval card's path have it, not a row's own title or lines. It needs no editor, so a viewer without one (a phone on the LAN) gets an answer's refs with **Show in turn** as their click; a row's title and diff lines stay plain text there. On a touch screen a tap opens the menu. A ref with a target is a tab stop: Enter runs its first target, ↓ opens the menu (↑/↓ move, Enter picks, Esc closes back to the ref). Diff and read lines have their click only. The info bar copies `chi --attach <id>` for a terminal. A session whose prompt carried model notes has a `notes: deepseek` chip there (the names without `model_notes_`; the tooltip lists each with its scope and size; `session.prompt_notes` in `GET /api/sessions/:id`, the live worker's, else the session file's; see [Memory](memory.md#model-notes)). Its `llm ctx` chip names the LLM context strategy the next turn runs under (`llm ctx stale,forget · session`, in the accent colour when the session set its own; the tooltip gives each value's source), and its popover sets the session's own strategy, apply rule and budget, or follows the model again, by running `/llm-context` in the worker (the reply shows as a command bubble); `GET /api/sessions/:id`'s `session.llm_context` carries the values (the live worker's, else resolved from the session file), and a `command_ran` that changed them carries the new ones. Its `think` chip does the same for the thinking level (`/thinking`), and on a chat host its `reasoning` chip for the reasoning history (`reasoning all · session`, `/reasoning-history`; `session.reasoning_history`, null on a native host). A `/thinking` or `/reasoning-history` change that restarts the prompt cache asks first: the reply bubble has an **Apply (restarts the cache)** button that sends its `--yes` line (the chips' Set sends that line itself; see [Changes that restart the prompt cache](configuration.md#changes-that-restart-the-prompt-cache)). It shows the context used (`ctx 42%`) and the last generation's decode speed (`87 tok/s`, `~64 tok/s` when estimated; see `/stats` in [cli.md](cli.md#session-commands)); hovering `ctx` there or on a session card lists the session's tokens in (summed over every request, with the cached share), out (with reasoning) and the cost when the provider reports one, for this session only (one estimated from a `hosts.<name>.models` price shows with `~`: `cost: ~$0.54 ($0.42 reported, ~$0.12 estimated)`), and the memory indexes its prompt holds (`memory index: ~3.4k tokens in this session's prompt (system 2.0k, project 1.4k)`; see [Memory](memory.md#the-indexs-size)). The start page's model picker (bottom left of the composer, the chosen name `▾`) chooses the model a new chat starts on; it opens a search list over the conversation (↓ or a click; downward when the start page leaves no room above): an empty search shows the last 5 picks (Recent) and then each host's model ids A–Z under the host, the default host first (an id the host declares under `hosts.<name>.models` first, noted `config`, its tooltip `served by config (hosts.<name>.models)`; a host that failed to list its models keeps those ids, greyed and noted `host down`, still pickable; a remembered or default model stays chosen even then, greyed on the button); typed words must each match the host or the id (a substring, else for 3+ characters the letters in order, so `deepseek4.1 fla` finds `openrouter · deepseek/deepseek-v4.1-flash`), ⏎ picks, Esc/Tab close, a pick puts the focus in the message box: `GET /api/models` lists the hosts' models as chi spells them (`{default, models: [{name, host, id, configured?, unavailable?, sampling?, llm_context?}], warning?}`; `configured: true` marks an id declared under `hosts.<name>.models`, listed first under its host, `unavailable: true` one whose host failed to list its models; `sampling` is the model's configured request parameters as `/model` words them, the row's tooltip in the list; bare for the default host, `host:model` for the others, from the same cached lists as `/models`, a bounded wait, a host that is down noted in `warning`), and `POST /api/sessions` takes `model` (blank means the default). Beside it, an `llm ctx` chip shows the LLM context the picked model starts under (each `GET /api/models` row's `llm_context`, as the info bar's); its form sets one for the new chat only, sent as `POST /api/sessions`'s `llm_context` and never remembered (see [configuration.md](configuration.md#llm-context-a-sessions-own-strategy)). The start page's first message goes as later ones do: `POST /api/sessions` with `idle: true` and `preview` (the message, which names the session until its turn is saved), then `POST /api/sessions/:id/turn`, so a failed first turn puts its prompt back in the composer too; a first line that is one of the new session's commands (`/model …`) goes to `POST /api/sessions/:id/command` instead, as later ones do. The browser remembers the last model picked; a running session's model is in the info bar and changes only with `/model`. In the composer a line is a command when it is `!…` or starts with one of the session's command names (its `commands` list; `/stats`, `/exit` and the other terminal words get the page's own reply); an unknown `/word` (`/usr/bin/env is missing`) goes to the model as a prompt, as in a terminal; a one-word `/word` no command answers (`/modle`) is not sent: the page shows `Unknown command /modle. Did you mean /model? /help lists the commands.` (the "Did you mean" part only when a name is close). A `POST /turn` (or a session's first `prompt`) that is one of the worker's commands runs as that command too (`202 {status, command_id, session_id}`), so `chi send`, `chi -p` and a page whose command list lags all get the command. ↑/↓ in the composer walk the prompt history the TUI's ↑ walks ([Persistent Prompt History](cli.md#persistent-prompt-history)): ↑ with the caret on the first line, ↓ from the last line while browsing, ↓ past the newest brings the draft back; `GET /api/history` returns `{entries: [...]}`, oldest first (the page fetches it on focus and after each send). The server records a sent line in the routes once the worker took it (`POST /turn` accepted, a `!command` through `POST /command` or as a session's first line); `"history": false` in the body keeps it out (an image-only send).
|
|
78
78
|
- Web notifications: when a session needs you while the tab is hidden or behind another window (a question or a guardrail approval, a plugin card with buttons during a turn such as check-in's, a failed turn, a turn done after 10 s or more; a delegated session only for its questions, a canceled turn never), the page title counts it, `(N) Chi`, until you come back to the tab. The bell at the right of the top bar also turns on OS notifications ("needs an answer", "needs approval", "needs you", "turn failed", "done in 42 s", under the session's first message; a click opens the session): its first click asks the browser for the permission, and the choice is kept per browser. Any session of the page's scope counts, not only the open one. What was already so when the page loaded never counts, and several chi tabs show one notification per event. A chi tab in front tells the others (a `BroadcastChannel`), so while you look at one, the ones behind neither notify nor count what it shows; coming back to a tab clears those events in the other tabs' titles too. The hub's `session` frames carry what this needs: `pending_question` (`{id, kind}`, kind `question` or `approval`), `pending_card` (`{id, bundle}`: the running turn's card with actions, from `pending_card.json` that the worker keeps in the session's folder while one is open; a card without actions, such as loop-guard's, never counts) and `last_turn` (`{outcome, ended_at, seconds, origin}`, saved in the session's JSON as the engine ends each turn).
|
|
79
|
-
- The web frontend is a zero-build ES-module stack in `lib/samagotchi/web/public/`: `data.js` (retrieval, typed SSE `openStream` for a session and `openEvents` for the session list), `sessions_list.js` (the list as a pure reducer over `GET /api/events`: a snapshot replaces it, an upsert keeps a known card in place, a removal drops it; `families()` groups delegates under their parent), `families_view.js` (a parent's family chip, its rows and the strip's popover; the chip and rows as lit-html templates), `keep_focus.js` (lit reuses a list's nodes by position: a redraw keeps the focus on the same item by its `data-id` / `data-name`), `chains.js` (session chains: the links from `continues`, the cards folded into the latest link's, its `day N` chip and rows as lit-html templates, the info bar's ← → links, a refused continue), `notify.js` (which of those events needs the user, for the notifications), `app.js` (presentation/state; a session with no live stream gets one when its `session` event says `bridge_up`, after one re-read: `event_seq` starts over in each worker, so a dropped stream is never resumed with its old cursor), `format.js` (pure formatters such as `previewOf`), `prompt_history.js` (the composer's ↑/↓ over the shared prompt history), `commands.js` (session commands: which composer lines are commands, the page's own replies, a `command_ran`'s bubble, the page's command bubbles kept across a re-read), `composer.js` (where a composer line goes, `routeComposerLine`: /cut and /queue, the start page's own replies, a session command or `!cmd`, a typo hint, else a turn; and a session command sent from the composer or a chip), `delivery_line.js` (the composer's row above the text field in a session, always there so the composer keeps its height: the key hint while idle, and the delivery line while a turn runs: where a message typed now goes in, from the step's phase, clock and size, whether "cut in now" can land, and the queue link for a worker with the `delivery` feature, as a pure `deliveryLine`, its lit-html template and `createDeliveryControl`, which ticks it and keeps a choice armed from a link; see docs/cli.md, "Typing during a turn"), `restore.js` (a failed turn's prompt handed back into this tab's composer, before or after its `/turn` ack), `lists_view.js` (the session lists: the strip and its pill, all sessions with its search, count and select mode, their cards, the family and chain popovers; the cards, tile and pill as lit-html templates, drawn as new nodes each time, and the lists' orders as pure functions), `stream_handlers.js` (the open session's stream event handlers, by event type, over the page's views and hooks; a snapshot replays through them too), `info_bar.js` (the session bar's info line as a lit-html template over a plain model: id, status, model, chain links, chips, ctx, speed, clock, worker badge), `session_state.js` (what the page knows about the open session: its empty state, and `sessionStateFrom`, the fields a `GET /sessions/:id` payload sets), `refs.js` (refs: a kind and a target on the element, a plugin's ref data (label, http(s) URL actions) bound in a WeakMap by `bindRef`, the targets per kind, show in turn, one delegated set of click, key and hover listeners), `ref_menu.js` (a ref's `▾` and its menu), `ref_scan.js` (the `path:line` refs in an answer's or a step's text that name a file the session's rows touched), `answer_refs.js` (the refs a plugin attached to an answer, its `display_refs`: bound to the answer's bubble, paired with its links and text, bound with `bindRef`). Unit-tested via `npm test` (`
|
|
79
|
+
- The web frontend is a zero-build ES-module stack in `lib/samagotchi/web/public/`: `data.js` (retrieval, typed SSE `openStream` for a session and `openEvents` for the session list), `sessions_list.js` (the list as a pure reducer over `GET /api/events`: a snapshot replaces it, an upsert keeps a known card in place, a removal drops it; `families()` groups delegates under their parent), `families_view.js` (a parent's family chip, its rows and the strip's popover; the chip and rows as lit-html templates), `keep_focus.js` (lit reuses a list's nodes by position: a redraw keeps the focus on the same item by its `data-id` / `data-name`), `chains.js` (session chains: the links from `continues`, the cards folded into the latest link's, its `day N` chip and rows as lit-html templates, the info bar's ← → links, a refused continue), `notify.js` (which of those events needs the user, for the notifications), `app.js` (presentation/state; a session with no live stream gets one when its `session` event says `bridge_up`, after one re-read: `event_seq` starts over in each worker, so a dropped stream is never resumed with its old cursor), `bubbles.js` (the history's bubbles between the turns' rows: output chunks, end lines, every client's prompt bubbles by `enqueued_id` with their queued/steered badges, the idle recap, guardrail warnings, hooks' notices, plugins' steers, command and note bubbles; lit-html templates, put in the history with the sticky follow by `createBubbles`), `cards_place.js` (where plugin cards and a snapshot's notices go in the history: a card added or updated in place by its id, a running turn's into its step, a snapshot's each above the first turn after it, a finished turn's notice in its step's rows and its question resolved where it was asked; a card's action buttons), `format.js` (pure formatters such as `previewOf`), `prompt_history.js` (the composer's ↑/↓ over the shared prompt history), `commands.js` (session commands: which composer lines are commands, the page's own replies, a `command_ran`'s bubble, the page's command bubbles kept across a re-read), `composer.js` (where a composer line goes, `routeComposerLine`: /cut and /queue, the start page's own replies, a session command or `!cmd`, a typo hint, else a turn; and a session command sent from the composer or a chip), `delivery_line.js` (the composer's row above the text field in a session, always there so the composer keeps its height: the key hint while idle, and the delivery line while a turn runs: where a message typed now goes in, from the step's phase, clock and size, whether "cut in now" can land, and the queue link for a worker with the `delivery` feature, as a pure `deliveryLine`, its lit-html template and `createDeliveryControl`, which ticks it and keeps a choice armed from a link; see docs/cli.md, "Typing during a turn"), `restore.js` (a failed turn's prompt handed back into this tab's composer, before or after its `/turn` ack), `lists_view.js` (the session lists: the strip and its pill, all sessions with its search, count and select mode, their cards, the family and chain popovers; the cards, tile and pill as lit-html templates, drawn as new nodes each time, and the lists' orders as pure functions), `stream_handlers.js` (the open session's stream event handlers, by event type, over the page's views and hooks; a snapshot replays through them too), `stage_view.js` (the stage view: the running turn pinned above the composer, then handed off into the history; its pure rules and texts in `stage_model.js`, its live slots (headline, now row, trail, ticks) in `stage_slots.js`, its cap and held height in `stage_frame.js`, the in-use watch that times the hand-off in `stage_watch.js`), `info_bar.js` (the session bar's info line as a lit-html template over a plain model: id, status, model, chain links, chips, ctx, speed, clock, worker badge), `info_bar_model.js` (that model and the bar's buttons' states, built from the page's session state, its list and its in-flight continues and restarts as pure functions; `app.js`'s `updateInfoBar` applies them every second while a turn runs), `session_state.js` (what the page knows about the open session: its empty state, and `sessionStateFrom`, the fields a `GET /sessions/:id` payload sets), `refs.js` (refs: a kind and a target on the element, a plugin's ref data (label, http(s) URL actions) bound in a WeakMap by `bindRef`, the targets per kind, show in turn, one delegated set of click, key and hover listeners), `ref_menu.js` (a ref's `▾` and its menu), `ref_scan.js` (the `path:line` refs in an answer's or a step's text that name a file the session's rows touched), `answer_refs.js` (the refs a plugin attached to an answer, its `display_refs`: bound to the answer's bubble, paired with its links and text, bound with `bindRef`). Unit-tested via `npm test` (`spec/web/public/*.test.js`).
|
|
80
80
|
- Selecting a session in the web UI is read-only: `GET /api/sessions/:id` never spawns a worker (it reads a live worker's snapshot when one runs, else the session file). A prompt (`POST /turn`) or a command (`POST /command`) wakes the worker, and `/stream` briefly waits for a freshly-spawned bridge before answering. A caught-up SSE reconnect holds the stream open; `reset` markers are only sent for reconnects behind the ring window, or with a cursor from another worker (event ids are `<event_seq>-<epoch>`, one epoch per worker).
|
|
81
81
|
|
|
82
82
|
**Test-session hygiene:**
|
|
@@ -198,7 +198,7 @@ chi send --new --continues last:3fa2 -m "new day" # the next link of 3fa2's
|
|
|
198
198
|
- Exit codes with `--wait`: 0 answered; 3 the turn waits for an answer from you (a question, a hook's question, a guardrail approval, or the step-limit question: see [chi as a sub-agent](sub-agent.md#the-step-limit)), and the session keeps waiting: stderr gets the whole question (`waiting for an answer (approval): …`, its text, the numbered options, whether free text is allowed) and how to answer it (`chi answer ID --question QID --option N`, or `chi --attach ID`, the web); 4 `--timeout S` passed with the turn still running (it goes on; wait again); 1 the turn ended without a reply (canceled, failed or empty), the worker failed or vanished, or the session was stopped; 130 on Ctrl-C, which leaves the turn running (`still running: chi --attach ID`). There is no default timeout.
|
|
199
199
|
- `--format json` (with `--wait`) prints one JSON object on stdout however the wait ends, for a script or a parent agent (see [chi as a sub-agent](sub-agent.md)): `{"status":"answered","session_id":…,"text":…}`; `{"status":"question",…,"question":{"id","kind","header","text","options","multi_select","allow_freeform","approval"},"answer_with":"chi answer ID --question QID --option N"}`, where `kind` is `question` (the model's), `hook` or `approval` (with the tool, command, cwd, rule, reason and offered scopes); `{"status":"running",…,"detail":…}` (exit 4, and on Ctrl-C); and `failed`, `canceled`, `no_answer`, `error`, `worker_gone` or `stopped` with a `detail` line (exit 1); `failed` adds `error_kind` and `retryable`, `canceled` adds `cancel_reason` and `stopped_by`, when known. A failure before the wait (no such session, a busy one) is `error` too. A usage error (exit 2) prints no JSON.
|
|
200
200
|
- `--wait ID` with no message (no `-m`, nothing piped) sends nothing: it waits for the session's next reply and prints and exits as above. With a turn running, that turn's reply (its `interrupted_turn` mark names it), not a turn's queued behind it; with the step-limit question standing, the reply of the turn its Continue runs. With neither (or a worker whose `bridge.json` lacks `turn_ids`), the next reply of any turn. A running session is fine (that is the point), so after exit 3, 4 or 130 an agent waits again this way while the user answers in the web or `chi --attach`; the question pending when it starts doesn't count again. An idle session with no worker waits until something wakes one. With `--new`, or two ids, it is a usage error (exit 2).
|
|
201
|
-
- `--continues ID` (with `--new`) starts the next link of that session's chain instead (see *Session chains*): in its folder, on its model, LLM context
|
|
201
|
+
- `--continues ID` (with `--new`) starts the next link of that session's chain instead (see *Session chains*): in its folder, on its model (or the one `--model` names), LLM context, thinking level and toolkit, so `--dir`, `--llm-context*`, `--thinking`, `--toolkit`, `--tool` and `--no-tool` are refused beside it; `ID` may be a prefix or `last:ID`, the chain's latest link. Without a message the new link starts idle; `--wait` then has nothing to wait for (a usage error). It prints `<full id> started (continues 3fa2c1d0)`, with `; 2 delegates moved` inside the parentheses when open delegates moved to it; a refusal (continued already, naming the next link; an open delegate that can't move; a turn running) is `chi send: refused: …`, exit 1, and a `--model` it can't start on is `chi send: could not start a session: …`, exit 1, nothing archived. A fallback to the default model (below) prints `chi send: warning: …`.
|
|
202
202
|
- The 16 KiB cap applies: `git diff | chi send --new …` on a big diff is refused; name the branch in the message instead and let the session read it.
|
|
203
203
|
- A session nobody attaches to stalls at its first question or guardrail ask until someone answers it (`chi answer`, the web, `chi --attach`), and its worker stays up meanwhile: it doesn't idle-exit while a question waits (`chi sessions list` shows it as `waiting`; `chi sessions stop ID` frees it). Otherwise it idle-exits after `session.idle_exit_minutes` like any worker. These are ordinary sessions: delete them like any other.
|
|
204
204
|
|
|
@@ -208,16 +208,16 @@ A session can **continue** an earlier one: "the next day of the same routine" (a
|
|
|
208
208
|
|
|
209
209
|
`SessionManager.continue_session` makes the next link:
|
|
210
210
|
|
|
211
|
-
- **What it copies:** the previous link's folder, its model (the name it was typed as, so an alias holds) and its own LLM context values. Its preview is the previous link's (until its first message is saved), unless it starts with a message.
|
|
211
|
+
- **What it copies:** the previous link's folder, its model (the name it was typed as, so an alias holds) unless the continue names one (`--model`, the web's `model`), and its own LLM context values. A model given is checked like a new session's (a host config.yml doesn't have refuses the continue, nothing archived). An inherited model whose host can't run it any more (gone from config.yml, `enabled: false`, an alias naming another host) **falls back to the default model** with a warning, `gone:org/model can't run (unknown host 'gone' in model …); started on <default>`, decided before the archive; with no default model configured the continue is refused. The carry note doesn't mention the model. The new link carries no transcript, so a different model, or another provider, starts clean there. Its preview is the previous link's (until its first message is saved), unless it starts with a message.
|
|
212
212
|
- **The carry note:** the new link starts with a context note from `session chain` (shown in its transcript as `note from session chain`), queued before its worker spawns, so its first turn sees it: `This session continues 3fa2c1d0 (2026-10-08), the previous link of its chain. Its recap, as of 2026-10-08 18:02:` and the previous link's recap (`recap.json`; cut at 8000 characters). No recap (fewer user turns than a recap needs, recaps off, none written in time): `… It left no recap.` Never more than the recap: no transcript. An idle new link holding only the note is kept, not discarded as empty.
|
|
213
213
|
- **The recap first:** a previous link with a live worker is asked for a recap (the Bridge's `POST /recap`) and given up to 30 s to write it; one with none wrote its recap when its worker left.
|
|
214
214
|
- **The previous link is archived** before the new one starts, by the archive's own rules (see *Archiving a session*): a turn running, a prompt queued or the step-limit question refuse it, a live idle worker is stopped, a plain REPL's session is refused. Its finished delegates are archived with it, as any archive does.
|
|
215
215
|
- **Its open delegates move to the new link** instead: each direct delegate that is running, waiting for an answer, has a live worker or a reply its parent wasn't given, or has such a delegate below it (checked again after the recap wait, so one delegated meanwhile moves too). It moves with its own delegates, stays unarchived, and from then on is the new link's: its card folds under the new link, its reports ring the new link (an idle one runs a wake turn for them, as for its own delegates), `delegate_result` and the `⑂` chip count it there, and what its parent was already told (`delegates.json`) and any report still waiting come along, so nothing is reported twice. A moved delegate gets a context note from `session chain`, `Your parent session is now 4b7e9a01 (…), the next link of its session chain: …`, as its system prompt keeps naming the parent it started with. The move writes `<child>/parent.json` (`{parent_id, from, at}`) beside the child's session file, never the file itself, so a live child worker saving its own copy changes nothing; every reader applies it (a later move overwrites it; it is never removed). A continue is **refused for an open delegate that can't move** (`SessionContinue::Refused`, naming it): one whose live worker runs an older chi (`older chi worker 0.46.1; restart it`: `chi sessions restart <id>`, or stop it) or that is open in a chi REPL (close it there). An older `chi web` or CLI still running shows a moved delegate under the archived link until it is restarted.
|
|
216
216
|
- **If the continue fails part-way:** a new link that fails to start gives its moved delegates back before it is deleted (each gets a note saying so; its worker is stopped first, if one started, and a worker that won't stop is reported, the link left), with any report one of them rang it with meanwhile; the previous link, back in the lists, is woken for reports waiting. A continue that dies in the middle leaves a `move.json` in the new link's folder: the new link's next worker finishes that move (only the delegates it names, and without a second note to one already told) as it starts. Until the new link's own worker holds it, a ring from a moved delegate doesn't start another (a `starting` marker in its folder, which that worker removes; a stale one, over 60 s old or from a process that is gone, is ignored). An open delegate below an archived delegate refuses the continue (`under archived …`): unarchive that one, or archive this one.
|
|
217
|
-
- **Refused too:** a link that is continued already (naming the link that continues it; continue that one, or `last:<id>`), a previous link whose folder is gone, and one whose model
|
|
218
|
-
- **Web:** `POST /api/sessions` takes `continues: "<id, prefix or last:id>"`, with `idle: true` (a new link waiting for its first message, previewed by the previous link's, or `preview`) or a `prompt`, and not `dir`, `
|
|
217
|
+
- **Refused too:** a link that is continued already (naming the link that continues it; continue that one, or `last:<id>`), a previous link whose folder is gone, and one whose model's host can't run it when no default model is configured to fall back to. A refusal starts nothing and archives nothing; a new link that fails to start puts back what was archived. Every refusal that can be known up front (the archive's ones included) comes before the recap wait. One continue of a session runs at a time (a lock in its folder; other chains don't wait), so a double click finds the first one's link and is refused, while a `last:<id>` continue that waited follows the chain to its new end.
|
|
218
|
+
- **Web:** `POST /api/sessions` takes `continues: "<id, prefix or last:id>"`, with `idle: true` (a new link waiting for its first message, previewed by the previous link's, or `preview`) or a `prompt`, and `model` (or `model_name`) for the new link's model, but not `dir`, `llm_context` or `thinking` (400 `invalid_continues`: the previous link's are taken). 201 is the new session's summary, `continues` set, with `moved`: the ids of the delegates that moved to it (`[]` for none), and `model_warning`: the default-model fallback's warning, then any about the model id, joined by `; ` (`null` for none); 404 `not_found`; 400 `ambiguous_id` for a prefix of several sessions, `invalid_model` for a given model whose host config.yml doesn't have (or a fallback with no default model); 409 `continued` with `next_id` (the link that continues it already, which the page opens instead), `open_children` with `ids` (open delegates that can't move), `folder_gone`, `busy`, `scratch` or `owned_by_tui`. Every session summary carries `continues`.
|
|
219
219
|
- **The page:** the info bar's `continue →` continues the open session (`continues: <id>, idle: true`; it reads `continuing…` through the recap wait) and opens the new link with `Continue where we left off.` in the composer, not sent (the same line for every chain; a draft already there stays instead); the previous link is archived, a toast says so (`Continued 3fa2c1d0 in 4b7e9a01; 3fa2c1d0 is archived; 1 open delegate moved`), and a moved delegate's card folds under the new link. A link continued already has no `continue →`: its `→` link goes to the next link (a stale page's continue answered 409 `continued` opens `next_id`). A continue refused for open delegates that can't move shows a toast that names each one (its preview and what the server says to do: `older chi worker 0.46.1; restart it`, `open in a chi REPL; close it there`), each a link to its page; any other refusal shows the server's words. The info bar of a link carries `← 2026-10-08` (the previous link's day) and, on an earlier link, `2026-10-10 →`, each a link with its preview and id in the tooltip. A link whose title is the opener and nothing else (its first message once sent) shows its chain's first link's title instead, on its card, in the popover and in its info bar's title line, display only (its messages and data keep the opener; its own title when the first link is gone). The lists fold a chain into the card of its latest listed link, whatever the earlier links' archived state (typing in an archived link brings it back, folded there still), their delegates with them: the card's `↩ day N` chip (`day N of M` when later links aren't listed) opens a popover of the other links, newest first (`day N`, its date, its recap's first sentence or its preview, `archived`), a click opens one; the chip shows a live dot or `waiting` for a link that has a worker or waits on the user, and a waiting link puts its chain's card first. All sessions folds nothing while a search is typed or "include archived" is on: every link is a card of its own then, its chip `day N of M`, with its own delegates folded in, so a search finds an earlier link and its delegates.
|
|
220
|
-
- **CLI:** `chi send --new --continues (ID|PREFIX|last:ID) [-m TEXT]` (see *Starting a session*).
|
|
220
|
+
- **CLI:** `chi send --new --continues (ID|PREFIX|last:ID) [--model M] [-m TEXT]` (see *Starting a session*).
|
|
221
221
|
- **Which link:** an id, a unique prefix, or `last:<id or prefix>`, the latest link of the chain that session is in. It walks the links that still exist: a deleted link in the middle (`chi sessions delete`, the retention sweep of an unarchived one) ends the walk before it, so `last:` of an earlier link then names that earlier link, and continuing it starts a second line beside the links after the gap; name a link after the gap instead. A trigger names the chain this way: `chi send` to an archived link would bring it back (a human's input), not continue the chain.
|
|
222
222
|
|
|
223
223
|
## Toolkits
|
data/docs/sub-agent.md
CHANGED
|
@@ -19,7 +19,10 @@ you already work with (to validate, as context, or with a question), is the
|
|
|
19
19
|
chi session, chi's `claude_send` tool or `/claude` writes to Claude's inbox,
|
|
20
20
|
and Claude's answer comes back as attached context. The
|
|
21
21
|
[codex-peer bundle](plugins.md#the-codex-peer-bundle) does the same with a
|
|
22
|
-
Codex CLI session (`codex_send`, `/codex`, through `codex queue`).
|
|
22
|
+
Codex CLI session (`codex_send`, `/codex`, through `codex queue`). The peer's
|
|
23
|
+
answers wake chi in a short loop (fix or disagree, send again) until it says
|
|
24
|
+
`VERDICT OK` or after `context.peer_turns` turns: [Waking: peer
|
|
25
|
+
sources](context.md#peer-sources-the-peer-loop).
|
|
23
26
|
|
|
24
27
|
## The commands
|
|
25
28
|
|
data/docs/testing.md
CHANGED
|
@@ -17,9 +17,9 @@ clears your `SAMAGOTCHI_*` environment, and keeps every spec off the network (We
|
|
|
17
17
|
|
|
18
18
|
The web specs run in plain node. One that loads lit-html, itself or through a module it imports (`turn_view.js`,
|
|
19
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`,
|
|
20
|
+
`question_cards.js`, `stream_handlers.js`, `stage_view.js`, `bubbles.js`, `cards_place.js`, the chips, `info_bar.js`, `info_bar_model.js`, `lists_view.js`, `context_chips.js`, `families_view.js`,
|
|
21
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;
|
|
22
|
+
never shipped; lit reads `document` as it loads). The shim's `markupOf(template)`,
|
|
23
23
|
`htmlOf(nodes)` and `canon(html)` compare markup without lit's marker comments; the DOM writes text with only `&`, `<`, `>`
|
|
24
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
|
|
25
25
|
`?selected`: read the option marked `selected` (`option[selected]`), not `select.value`; the browser follows the mark.
|
|
@@ -27,6 +27,11 @@ A select bound with `.value` instead reads right in both, but lit sets it before
|
|
|
27
27
|
`llm_context_chip.js`'s `renderForm` draws twice then. Assert a kept node with `assert.ok(a === b)`: a failing
|
|
28
28
|
`assert.equal` on two DOM nodes inspects the whole DOM (huge output, can stall the run).
|
|
29
29
|
|
|
30
|
+
`npm test` runs every file in one process (`--test-isolation=none`; CI and local runs use Node 24): the files share
|
|
31
|
+
`globalThis`, the shim's one happy-dom window and every module's state: a global a spec sets (stage_view's
|
|
32
|
+
`ResizeObserver` stand-in) is there for the files after it, so set only harmless stand-ins and put anything else
|
|
33
|
+
back (`t.mock` restores itself). One file: `node --test spec/web/public/NAME.test.js`.
|
|
34
|
+
|
|
30
35
|
Where a composer line goes is decided twice, in Ruby (`ComposerRoute`, both terminals) and in the web
|
|
31
36
|
(`composer.js#routeComposerLine`). `spec/fixtures/composer_routes.json` holds the cases both answer: rspec
|
|
32
37
|
(`spec/composer_route_spec.rb`) and `npm test` (`spec/web/public/composer.test.js`) run every row. A change to
|
|
@@ -44,8 +49,10 @@ script/affected-specs A..B # another range (A...B: from their merge-b
|
|
|
44
49
|
|
|
45
50
|
A builder runs these while working, not the full suite: the specs the change touched, the specs named after the
|
|
46
51
|
files it changed (`lib/samagotchi/foo/bar.rb` → `spec/foo/bar_spec.rb`, `bar_*_spec.rb`…), the specs that read
|
|
47
|
-
docs or source as text (its `RULES`: add one when a full run catches what the names missed), then the
|
|
48
|
-
|
|
52
|
+
docs or source as text (its `RULES`: add one when a full run catches what the names missed), then the other name
|
|
53
|
+
matches (a spec named after several changed files first, e.g. `engine_toolkit_spec.rb` for `engine.rb` and
|
|
54
|
+
`toolkit.rb`; then one beside the file, so `plugin/context.rb` picks `spec/samagotchi/plugin/*context*` before
|
|
55
|
+
`spec/guardrails/context_spec.rb`), then the specs that name a changed file's class. The default range is the merge-base with main (or origin/main) plus uncommitted and untracked files.
|
|
49
56
|
The Ruby specs go to stdout; the node tests to run (`npm test` targets), "web changed: also run npm run e2e" and a
|
|
50
57
|
hint for a hub class (named in too many specs to list) go to stderr. Nothing printed means nothing matched.
|
|
51
58
|
|
|
@@ -65,6 +72,13 @@ It runs `git worktree add ../samagotchi-NAME -b BRANCH main`, then `npm ci --pre
|
|
|
65
72
|
`npm test` and `npm run e2e` work there right away (Ruby gems and Playwright's browsers are shared: nothing else
|
|
66
73
|
to install). It refuses when the folder or the branch already exists.
|
|
67
74
|
|
|
75
|
+
## The desktop helper's build specs
|
|
76
|
+
|
|
77
|
+
`SAMAGOTCHI_MACOS_BUILD=1 bundle exec rspec spec/desktop` also compiles the macOS helper (`:macos_build`, needs the
|
|
78
|
+
Command Line Tools) and runs `ChiHelper` against a fake kitty and, when a `tmux` is on PATH, a real one (private
|
|
79
|
+
`-L` servers under a temp `TMUX_TMPDIR`, killed after). CI skips them, and `script/affected-specs` doesn't pick them
|
|
80
|
+
for a Swift change: run them by hand after editing `lib/samagotchi/desktop/macos/`.
|
|
81
|
+
|
|
68
82
|
## Integration specs
|
|
69
83
|
|
|
70
84
|
Examples tagged `:integration` (`spec/integration/`, the "with LLM access" part of `spec/hooks/integration_spec.rb`)
|
|
@@ -9,6 +9,7 @@ require_relative "../version"
|
|
|
9
9
|
require_relative "../llm/openai_chat"
|
|
10
10
|
require_relative "../llm/http"
|
|
11
11
|
require_relative "../llm/api_key"
|
|
12
|
+
require_relative "../host_keys"
|
|
12
13
|
require_relative "../redacted_inspect"
|
|
13
14
|
|
|
14
15
|
module Samagotchi
|
|
@@ -229,10 +230,10 @@ module Samagotchi
|
|
|
229
230
|
end
|
|
230
231
|
|
|
231
232
|
# One attempt through LLM::HTTP (User-Agent, the key's Bearer header),
|
|
232
|
-
# any status returned as is. A key
|
|
233
|
-
# header: the server's 401 then says one is needed.
|
|
233
|
+
# any status returned as is. A key neither its variable nor the
|
|
234
|
+
# Keychain holds sends no header: the server's 401 then says one is needed.
|
|
234
235
|
def send_request(uri, request, key_env, read_timeout:)
|
|
235
|
-
key = key_env &&
|
|
236
|
+
key = key_env && HostKeys.fetch(key_env, env: @env) ? LLM::ApiKey.for(key_env, host: uri.host, env: @env) : nil
|
|
236
237
|
LLM::HTTP.new(label: uri.host, open_timeout: @timeout, read_timeout: read_timeout, api_key: key,
|
|
237
238
|
retry_policy: LLM::HTTP::RetryPolicy.none)
|
|
238
239
|
.fetch(uri, request, retries: false, check_status: false, log_fields: { purpose: "probe" })
|
|
@@ -4,6 +4,7 @@ require "rbconfig"
|
|
|
4
4
|
require_relative "config"
|
|
5
5
|
require_relative "model_profile"
|
|
6
6
|
require_relative "host_registry"
|
|
7
|
+
require_relative "host_keys"
|
|
7
8
|
require_relative "bootstrap/probe"
|
|
8
9
|
require_relative "bootstrap/config_writer"
|
|
9
10
|
require_relative "bootstrap/bundles"
|
|
@@ -87,7 +88,7 @@ module Samagotchi
|
|
|
87
88
|
|
|
88
89
|
key_env = options[:key_env]
|
|
89
90
|
if key_env && unset?(key_env)
|
|
90
|
-
fail!("#{key_env} is not set;
|
|
91
|
+
fail!("#{key_env} is not set; #{export_hint(key_env, "first")} (the API key; chi only writes its name)")
|
|
91
92
|
return @exit
|
|
92
93
|
end
|
|
93
94
|
|
|
@@ -187,7 +188,7 @@ module Samagotchi
|
|
|
187
188
|
key_env = @stdin.gets.to_s.strip
|
|
188
189
|
return fail!("no variable given") if key_env.empty?
|
|
189
190
|
return fail!("#{key_env} isn't a variable name") unless key_env.match?(ConfigFile::ENV_NAME_RE)
|
|
190
|
-
return fail!("#{key_env} is not set;
|
|
191
|
+
return fail!("#{key_env} is not set; #{export_hint(key_env, "and run chi bootstrap again")}") if unset?(key_env)
|
|
191
192
|
|
|
192
193
|
result = @probe.classify(result.candidate, key_env: key_env)
|
|
193
194
|
return fail!("#{host} refused the key in #{key_env} (HTTP #{result.status})") if result.kind == :needs_key
|
|
@@ -433,7 +434,16 @@ module Samagotchi
|
|
|
433
434
|
@stdin.respond_to?(:tty?) && @stdin.tty?
|
|
434
435
|
end
|
|
435
436
|
|
|
436
|
-
|
|
437
|
+
# Neither the variable nor the Keychain (HostKeys) holds the key.
|
|
438
|
+
def unset?(name) = HostKeys.fetch(name, env: @env).nil?
|
|
439
|
+
|
|
440
|
+
# "export it first", or on macOS "export it or store it in the Keychain
|
|
441
|
+
# (security …) first".
|
|
442
|
+
def export_hint(name, tail)
|
|
443
|
+
return "export it #{tail}" unless HostKeys.available?
|
|
444
|
+
|
|
445
|
+
"export it or store it in the Keychain (#{HostKeys.store_command(name)}) #{tail}"
|
|
446
|
+
end
|
|
437
447
|
|
|
438
448
|
def fail!(message)
|
|
439
449
|
@stderr.puts("chi bootstrap: #{message}")
|
|
@@ -40,11 +40,6 @@ module Samagotchi
|
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
# @return [Integer, nil] the highest seq buffered so far.
|
|
44
|
-
def last_seq
|
|
45
|
-
@mutex.synchronize { @events.last&.[](:seq) }
|
|
46
|
-
end
|
|
47
|
-
|
|
48
43
|
# @return [Integer, nil] the lowest seq buffered so far.
|
|
49
44
|
def oldest_seq
|
|
50
45
|
@mutex.synchronize { @events.first&.[](:seq) }
|
data/lib/samagotchi/bridge.rb
CHANGED
|
@@ -374,7 +374,10 @@ module Samagotchi
|
|
|
374
374
|
|
|
375
375
|
Thread.new { handle_connection(client) }.tap do |t|
|
|
376
376
|
t.report_on_exception = false
|
|
377
|
-
|
|
377
|
+
# The ended connections' threads go as a new one comes, so a
|
|
378
|
+
# long-lived worker's list holds its open connections only. A new
|
|
379
|
+
# Array: #await_answers and #stop may be walking the old one.
|
|
380
|
+
@connection_threads = @connection_threads.select(&:alive?) << t
|
|
378
381
|
end
|
|
379
382
|
end
|
|
380
383
|
rescue StandardError => e
|
|
@@ -606,13 +609,13 @@ module Samagotchi
|
|
|
606
609
|
|
|
607
610
|
record, error = Tools::TaskRuntime.get_record(task_id)
|
|
608
611
|
return [{}, 404, { error: "task_not_found", task_id: task_id }] if error
|
|
609
|
-
return [{}, 409, { error: "not_running", status: record
|
|
612
|
+
return [{}, 409, { error: "not_running", status: record.status, task_id: task_id }] unless record.running?
|
|
610
613
|
|
|
611
614
|
Log.info(:bridge, "task_stop", task_id: task_id)
|
|
612
615
|
record, error = Tools::TaskRuntime.stop_task(task_id, by: "user")
|
|
613
616
|
return [{}, 500, { error: "bridge_error", detail: error }] if error
|
|
614
617
|
|
|
615
|
-
[{}, 200, { status: record
|
|
618
|
+
[{}, 200, { status: record.status, stop_reason: record.stop_reason, task_id: task_id }]
|
|
616
619
|
end
|
|
617
620
|
|
|
618
621
|
# The tasks this session started: its history's (created_ids_in) and
|
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: claude-peer
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.3.0
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Pair this session with a Claude Code session you work with: the claude_send tool and /claude send it a part of the work (a plan step, a diff, a finding) to validate or as context, and Claude's answer comes back as the claude-review context; /claude install-skill installs Claude's chi-peer skill, which pairs"
|
|
6
6
|
trust_level: reviewed
|
|
7
7
|
files:
|
|
8
8
|
skill_claude_peer.md:
|
|
9
|
-
sha256: sha256:
|
|
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"
|
|
9
|
+
sha256: sha256:9293740f3e1afa0fc2eff381f2c6406087598205df351abd22b4459a3bcb5d0e
|
|
10
|
+
description: "Send a part of the work to the paired Claude Code session (claude_send): only when the user asks, or to reply to its answer in a peer turn; validate, context or question; Claude's answer comes as the claude-review context"
|
|
11
11
|
plugin:
|
|
12
12
|
file: plugin.rb
|
|
13
|
-
sha256: sha256:
|
|
13
|
+
sha256: sha256:74960c46f4b98a453a6c6b16a53a4e9cb76752e6df1deecf55784fc7a0e616ce
|
|
14
14
|
scripts:
|
|
15
15
|
pair: sha256:033f1fc6a644950b57e71865253526a0a487bb4c9fd5a61bb89d031b65f40417
|
|
16
|
-
chi-peer-SKILL.md: sha256:
|
|
16
|
+
chi-peer-SKILL.md: sha256:753b609bca01877d46e6b1c3c5b74edba745433cbe33a86702d3ab75bc3f7107
|
|
17
17
|
# Two things need chi 0.63.0, and the bundle works without them on 0.62:
|
|
18
18
|
# ctx.context.list's summary: (the /claude card's last answer; without it
|
|
19
19
|
# the card shows no summary), and a pushed source's first wake: true push
|
|
20
20
|
# waking the session (on 0.62 Claude's first answer arrives as a note
|
|
21
21
|
# without waking; later ones wake).
|
|
22
|
-
|
|
22
|
+
# The plugin needs Samagotchi::Plugin::Peer and chi.peer_source (chi 0.67.0).
|
|
23
|
+
requires_chi: ">= 0.67.0"
|