samagotchi 0.34.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +77 -1
  3. data/README.md +2 -0
  4. data/bin/chi +3 -0
  5. data/docs/broadcast.md +129 -0
  6. data/docs/cli.md +8 -0
  7. data/docs/configuration.md +2 -0
  8. data/docs/context.md +37 -0
  9. data/docs/guardrails.md +14 -4
  10. data/docs/hooks.md +25 -4
  11. data/docs/memory.md +3 -0
  12. data/docs/plugins.md +29 -3
  13. data/docs/sessions.md +6 -6
  14. data/lib/samagotchi/bridge/card_store.rb +1 -1
  15. data/lib/samagotchi/bridge/turn_notice.rb +1 -1
  16. data/lib/samagotchi/broadcast/recipients.rb +63 -0
  17. data/lib/samagotchi/broadcast/scope_card.rb +94 -0
  18. data/lib/samagotchi/broadcast/tags.rb +154 -0
  19. data/lib/samagotchi/broadcast_command.rb +228 -0
  20. data/lib/samagotchi/bundles/coordinator/manifest.yml +7 -6
  21. data/lib/samagotchi/bundles/coordinator/plugin.rb +72 -4
  22. data/lib/samagotchi/bundles/coordinator/skill_coordinator.md +59 -11
  23. data/lib/samagotchi/bundles/github-pr/manifest.yml +3 -3
  24. data/lib/samagotchi/bundles/github-pr/plugin.rb +444 -3
  25. data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +22 -0
  26. data/lib/samagotchi/bundles/guardrails/manifest.yml +5 -4
  27. data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
  28. data/lib/samagotchi/bundles/mcp/plugin.rb +44 -11
  29. data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +32 -12
  30. data/lib/samagotchi/bundles/source-links/manifest.yml +4 -3
  31. data/lib/samagotchi/bundles/source-links/source_links.md +1 -1
  32. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  33. data/lib/samagotchi/bundles/system/memory_guide.md +4 -2
  34. data/lib/samagotchi/bundles/system/self_map.md +4 -0
  35. data/lib/samagotchi/children_status.rb +2 -16
  36. data/lib/samagotchi/config.rb +6 -0
  37. data/lib/samagotchi/engine.rb +29 -5
  38. data/lib/samagotchi/git_head.rb +24 -0
  39. data/lib/samagotchi/guardrails/parent_approvals.rb +6 -0
  40. data/lib/samagotchi/guardrails/rules.rb +15 -4
  41. data/lib/samagotchi/guardrails/targets.rb +9 -2
  42. data/lib/samagotchi/guardrails/verdict.rb +4 -3
  43. data/lib/samagotchi/hooks/registry.rb +11 -5
  44. data/lib/samagotchi/log_subscriber.rb +1 -1
  45. data/lib/samagotchi/memory_bundle/index_updater.rb +6 -3
  46. data/lib/samagotchi/note_command.rb +13 -13
  47. data/lib/samagotchi/note_delivery.rb +42 -0
  48. data/lib/samagotchi/plugin/context.rb +4 -4
  49. data/lib/samagotchi/session_commands.rb +1 -0
  50. data/lib/samagotchi/session_manager.rb +4 -3
  51. data/lib/samagotchi/sessions_command.rb +3 -2
  52. data/lib/samagotchi/system_prompt.rb +1 -0
  53. data/lib/samagotchi/terminal_ui/attached_loop.rb +25 -2
  54. data/lib/samagotchi/terminal_ui/status_row.rb +23 -2
  55. data/lib/samagotchi/terminal_ui.rb +14 -0
  56. data/lib/samagotchi/tool_activity.rb +14 -3
  57. data/lib/samagotchi/tool_declarations.rb +8 -4
  58. data/lib/samagotchi/tools/builtins.rb +1 -1
  59. data/lib/samagotchi/tools/memory.rb +103 -2
  60. data/lib/samagotchi/tools/send_note.rb +5 -11
  61. data/lib/samagotchi/version.rb +1 -1
  62. data/lib/samagotchi/web/app.rb +3 -0
  63. data/lib/samagotchi/web/capabilities.rb +19 -0
  64. data/lib/samagotchi/web/public/activity.js +26 -6
  65. data/lib/samagotchi/web/public/app.js +515 -288
  66. data/lib/samagotchi/web/public/card.js +12 -3
  67. data/lib/samagotchi/web/public/command_view.js +2 -3
  68. data/lib/samagotchi/web/public/families_view.js +171 -0
  69. data/lib/samagotchi/web/public/format.js +52 -0
  70. data/lib/samagotchi/web/public/index.html +94 -21
  71. data/lib/samagotchi/web/public/live_timing.js +154 -0
  72. data/lib/samagotchi/web/public/live_turn.js +44 -0
  73. data/lib/samagotchi/web/public/sessions_list.js +228 -32
  74. data/lib/samagotchi/web/public/strip.js +5 -2
  75. data/lib/samagotchi/web/public/turn_html.js +107 -0
  76. data/lib/samagotchi/web/public/turn_model.js +5 -4
  77. data/lib/samagotchi/web/public/turn_view.js +43 -129
  78. data/lib/samagotchi/web/session_summary.rb +4 -0
  79. data/lib/samagotchi/worker.rb +166 -307
  80. data/lib/samagotchi/worker_inbound.rb +251 -0
  81. data/lib/samagotchi/worker_wakes.rb +120 -0
  82. metadata +15 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c6f812c6128ae3ffaf91d9cc04716a77d351fb57793a7d9a4863e675f1f2337f
4
- data.tar.gz: 25e7600fe6dc335b7d28320795a03ea9e8527185f6a0dd918787777458402f39
3
+ metadata.gz: 1a970676ddf9dacd7bd644f66dcdf28f68a49c6cb24caf2bc785656cb6a2d286
4
+ data.tar.gz: 8babf6d8562a213f07c3053b7efde79aae43ba4d51fef8ce16b933d17894acd0
5
5
  SHA512:
6
- metadata.gz: 322e9f5a5b553f7ed1f2955307b8d5bd8c901a355ac3ffcdff6b3fcb5013b28011c7c330d9496486338c77d2c0f58e2854393d4cb40ec33d00ed2516a38bd066
7
- data.tar.gz: f6048763f53d9b865858842acaf60587d5e57a365aaba759c3c85fcf69532227c4b201fe0f878da3e48191dd43856637dad29537203a3dfd7159756caac6f778
6
+ metadata.gz: 9727a14cff5153a0a061d9e787aebd0e0ea7494781e63386405b261fb4391d04bef56d4fefa346c9d0f2081d49fe7eddeadc3f6bb1b4ef8edc6478f5d273db6f
7
+ data.tar.gz: 3dc9840ef7eba85ea3160bb2ac84412e9be6219c4e9bdbabc89796d2ee677adeb7903d4ef1002d82cae12daefac3ca6545f1f6519466bcfda164e2c126fd699e
data/CHANGELOG.md CHANGED
@@ -8,6 +8,80 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.36.0] - 2026-10-07
12
+
13
+ ### Added
14
+
15
+ - `memory_write` with `name`, `scope` and `description` but no `content` changes only that memory's index line and
16
+ leaves its file as it is (see [docs/memory.md](docs/memory.md)).
17
+ - `memory_write` with `name`, `scope` and `remove: true` removes a memory: its file (and model overlays) move to the
18
+ bundle trash, which `chi bundle trash` lists and empties, and its index line goes; a line left after the file was
19
+ deleted by hand goes too. A memory a bundle installed is refused (`chi bundle uninstall` owns it).
20
+ - The coordinator bundle (0.2.0) keeps a project memory `handoff_<epic>` with what git and the session list can't
21
+ rebuild (the split, your decisions, its verdicts, follow-ups), its status in the index description, and resumes
22
+ from it in a new session after checking git and the session list; `/coordinate resume` picks up an open handoff.
23
+ When the work is done it marks the handoff DONE and asks whether to remove it. See
24
+ [docs/plugins.md](docs/plugins.md#the-coordinator-bundle).
25
+ - The guardrails bundle (0.8.0) asks before every memory removal (`memory-remove`, once at a time). Guardrail rules
26
+ take `memory: remove`.
27
+ - In the web, a file reference in an answer (`lib/foo.rb:28`, `lib/foo.rb:28-34`, in inline code too) links to that
28
+ line of the pull request the session reviews: the PR attached to it or named in your messages (a delegate child's
29
+ task too). Inside a changed hunk it opens the PR's Files changed view at the line, highlighted; elsewhere the file
30
+ at the PR's head. Display only; `bundles: github-pr: line_links: false` turns it off (github-pr 0.2.0; run
31
+ `chi update`). See [docs/context.md](docs/context.md#line-links).
32
+ - `/mcp` shows roughly how many tokens each MCP server's tool definitions take in every request, and the total
33
+ (mcp 0.5.0; run `chi update`). See [docs/plugins.md](docs/plugins.md#the-mcp-bundle).
34
+
35
+ ### Fixed
36
+
37
+ - `memory_write` with a blank `description` keeps the stored one instead of wiping it; a multi-line description is
38
+ written on one line (it left orphan lines in `index.md`), and one longer than 200 characters is refused.
39
+
40
+ Update with `chi update` (coordinator 0.2.0 and guardrails 0.8.0, which need chi 0.36.0; github-pr 0.2.0, mcp 0.5.0).
41
+
42
+ ## [0.35.0] - 2026-10-06
43
+
44
+ ### Added
45
+
46
+ - `chi broadcast -m TEXT` (or stdin) shares a note with every session it may concern, without picking them: your
47
+ own sessions a worker or a chi REPL runs, or that ended a turn in the last 8 hours (`broadcast.active_hours`),
48
+ delegate children and scratch sessions left out. One gets it when the note shares a tag with it: a ticket id
49
+ (`broadcast.ticket_pattern`) in its branch or your prompts there, a pull request attached to it, a link. The rest
50
+ are listed as skipped; `--all` reaches every one, `--dry-run` shows who would get it and why. The model mentions a
51
+ broadcast briefly when it affects its current work and doesn't act on it unless asked. For you, not for an agent:
52
+ refused inside a session, and the guardrails bundle's `chi-broadcast` rule asks. See
53
+ [docs/broadcast.md](docs/broadcast.md).
54
+ - `chi sessions list --format json` has `delegate`: true for a child the `delegate` tool started, false for a fork.
55
+ - A parent session shows its delegate children at a glance: a `⑂ 3` chip in its web info bar (how many run, wait,
56
+ are done or failed in the tooltip; the waiting ones in the attention colour; a click opens all sessions), and
57
+ `⑂ 2 running · 1 waiting` in the terminal's status row.
58
+ - A hook's `event[:notify]` and a plugin's `ctx.notify` take `fallback_for: :display`: the line repeats what the
59
+ answer's display (`event[:present]`) shows, so a UI that renders the display leaves it out. The web with markdown
60
+ on does; the REPL, the attached TUI, `chi -p` and the web with markdown off still show it. See
61
+ [docs/hooks.md](docs/hooks.md#what-a-hook-can-do-the-runtime).
62
+ - Web, all sessions: each card has an archive button (unarchive on an archived one), and Select picks many cards
63
+ (click, Shift-click a range, "Select all shown") for Archive N / Unarchive M. A session with a running turn is
64
+ skipped with the reason on its card; one toast at the end has Undo.
65
+
66
+ ### Changed
67
+
68
+ - source-links 0.4.0 (needs chi 0.35.0; run `chi update`): with markdown on, the web no longer shows the
69
+ `sources:` line under an answer that links every ref it names; it still shows when a ref is only in code or
70
+ markdown is off, and the terminals print it as before.
71
+ - `chi note --source broadcast` is refused: that source is `chi broadcast`'s.
72
+ - Web: a parent's delegates fold into its card, on the strip and in all sessions, instead of being cards of their
73
+ own. A `▸ 3 delegates · 1 live · 1 waiting` chip opens their list (in the card in all sessions, a popover on the
74
+ strip); it opens by itself while one of them waits on you, and the card then wears the warning colour. A family
75
+ takes one strip slot, a search finds a parent by its delegates, and select mode picks cards. A fork and a delegate
76
+ whose parent isn't listed stay cards of their own.
77
+ - guardrails 0.7.0: the `chi-broadcast` rule asks before an agent runs `chi broadcast`.
78
+
79
+ ### Fixed
80
+
81
+ - Web: a tool row shows how long the call took while you watch the turn, as it does after a reload.
82
+
83
+ Update with `chi update` (guardrails 0.7.0, source-links 0.4.0).
84
+
11
85
  ## [0.34.0] - 2026-10-06
12
86
 
13
87
  ### Added
@@ -1708,7 +1782,9 @@ and long-lived sessions.
1708
1782
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
1709
1783
  hotkey panel that send selected text or the clipboard to your sessions.
1710
1784
 
1711
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.34.0...HEAD
1785
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.36.0...HEAD
1786
+ [0.36.0]: https://github.com/dm1try/samagotchi/compare/v0.35.0...v0.36.0
1787
+ [0.35.0]: https://github.com/dm1try/samagotchi/compare/v0.34.0...v0.35.0
1712
1788
  [0.34.0]: https://github.com/dm1try/samagotchi/compare/v0.33.0...v0.34.0
1713
1789
  [0.33.0]: https://github.com/dm1try/samagotchi/compare/v0.32.0...v0.33.0
1714
1790
  [0.32.0]: https://github.com/dm1try/samagotchi/compare/v0.31.0...v0.32.0
data/README.md CHANGED
@@ -120,6 +120,7 @@ chi web --open # web UI: this project's sessions (--s
120
120
  chi web --web-host lan # the web UI on your phone too: scan the QR code it prints
121
121
  chi sessions list # this project's saved sessions (--scope=all: every one)
122
122
  pbpaste | chi note --source slack <id> # background context for a session (no turn)
123
+ chi broadcast -m "payments API is down" # the same for every session it concerns (a shared ticket, PR or link)
123
124
  chi context add <PR URL> <id> # attached context: chi keeps it fresh, the agent hears when it changes
124
125
  pbpaste | chi send -m "same bug?" <id> # a message to a session, the clipboard quoted above it
125
126
  chi send --new --wait -m "review feat/x" # a new session you can watch in the web; prints the answer
@@ -202,6 +203,7 @@ once, for the session, for the repo, or for the whole rule in the repo.
202
203
  - [Memory](docs/memory.md): scopes and model-specific overlays
203
204
  - [Sessions](docs/sessions.md): storage, retention, `chi sessions`
204
205
  - [Attached context](docs/context.md): `chi context`, sources, scopes, waking, the github-pr bundle, safety
206
+ - [Broadcast](docs/broadcast.md): `chi broadcast`, who a note reaches, tags, what a session gets
205
207
  - [chi as a sub-agent](docs/sub-agent.md): `chi send --wait --format json`, `chi answer`, instructions for a parent agent
206
208
  - [Desktop helper](docs/desktop.md): `chi desktop`, the macOS "Send to chi" Service and hotkey
207
209
  - [Guardrails](docs/guardrails.md): allow / ask / deny for tool calls, rules, approvals
data/bin/chi CHANGED
@@ -40,6 +40,8 @@ SUBCOMMANDS = {
40
40
  "sessions" => ["samagotchi/sessions_command", "SessionsCommand"],
41
41
  # a context note for sessions (-m, --source, --all)
42
42
  "note" => ["samagotchi/note_command", "NoteCommand"],
43
+ # a note for every session it may concern (-m, --all, --dry-run)
44
+ "broadcast" => ["samagotchi/broadcast_command", "BroadcastCommand"],
43
45
  # live external text attached to sessions (add, push, ls, show, rm, mute)
44
46
  "context" => ["samagotchi/context_command", "ContextCommand"],
45
47
  # a message into sessions as the user's (-m)
@@ -95,6 +97,7 @@ parser = OptionParser.new do |opts|
95
97
  chi web [--port PORT] [--open] [--scope=all] the web UI
96
98
  chi sessions <list|stop|archive|unarchive|delete|prune|clean>
97
99
  chi note [--source NAME] [-m TEXT] ID...|--all background context for sessions
100
+ chi broadcast [-m TEXT] [--all] [--dry-run] a note for every session it may concern
98
101
  chi context <add|push|ls|show|refresh|rm|mute|unmute> live external text attached to sessions
99
102
  chi send [-m TEXT] ID...|--new a message into sessions, as if typed there
100
103
  chi answer ID --question QID --option N answer a question a session waits on
data/docs/broadcast.md ADDED
@@ -0,0 +1,129 @@
1
+ # Broadcast
2
+
3
+ `chi broadcast` shares one piece of information with every session it may
4
+ concern, without picking them: "payments API returns 500 since 14:00 (PAY-123)",
5
+ "the PRD for checkout v2 changed: no guest checkout". Each session it reaches
6
+ gets it as a [context note](sessions.md#context-notes) from `broadcast`, and
7
+ that session's agent decides whether it affects its work. Sessions it doesn't
8
+ concern never see it.
9
+
10
+ ```sh
11
+ chi broadcast -m "payments API returns 500 since 14:00 (PAY-123)"
12
+ pbpaste | chi broadcast # the note from stdin
13
+ chi broadcast --dry-run -m "…" # who would get it and why; nothing is delivered
14
+ chi broadcast --all -m "deploy freeze until 18:00" # every recipient, tag or not
15
+ ```
16
+
17
+ To tell one session or a few you picked, use `chi note ID...` instead.
18
+
19
+ ## Who it reaches
20
+
21
+ The recipients are your own sessions, in every project:
22
+
23
+ - a worker or a chi REPL runs them now, or their last turn ended within
24
+ `broadcast.active_hours` (default 8). A session with no turn yet counts
25
+ by its last save;
26
+ - not a delegate child: its parent relays what concerns it (the
27
+ `coordinator`'s builders included). A fork is your session and counts;
28
+ - not a `chi scratch` session, not an archived one, and not a test run
29
+ (unless `chi broadcast` itself runs as one, `SAMAGOTCHI_ENV=test`).
30
+
31
+ A session open in a plain chi REPL (`--no-shared`) can't take notes: it is
32
+ listed as skipped, "open in a chi REPL".
33
+
34
+ ## Tags: which sessions it concerns
35
+
36
+ A recipient gets the note when the note and the session share a tag:
37
+
38
+ | Tag | In the note | About the session |
39
+ |---|---|---|
40
+ | ticket | an id like `PAY-123` (`broadcast.ticket_pattern`) | its branch (any case: `feat/pay-123-retry`), your prompts there |
41
+ | pr | `PR #42`, a GitHub pull request URL | an attached `pr-42` source (the github-pr bundle), a PR URL in your prompts there |
42
+ | link | a URL, by host and path | a URL in your prompts there or in an attached context source's hint |
43
+
44
+ A link is compared without its scheme, `www.`, query, fragment or trailing
45
+ slash; a bare host (`https://github.com/`) is no tag. A PR URL names its repo,
46
+ so `acme/shop#42` doesn't match `acme/other#42`; `PR #42` with no repo matches
47
+ pull request 42 of any repo. Words the default ticket pattern would take that
48
+ are no ticket (`UTF-8`, `SHA-256`, `ISO-8601`, `GPT-4`, …) are left out.
49
+
50
+ A session that shares no tag is skipped for now, listed as "no tag match (not
51
+ checked: no triage yet)": a small triage model that reads the note and each
52
+ session's scope card is the next step. Until then, `--all` reaches them all.
53
+
54
+ ## What it prints
55
+
56
+ One line per recipient, the ones that got it first, then a summary:
57
+
58
+ ```
59
+ broadcast "payments API returns 500 since 14:00 (PAY-123)"
60
+ 3f2a1c9e delivered ticket PAY-123 matches (branch)
61
+ 91ab02c4 delivered link notion.so/team/checkout-v2 matches (messages); waits for its next start
62
+ c0ffee12 skipped no tag match (not checked: no triage yet)
63
+ d00dad00 skipped open in a chi REPL
64
+ delivered 2 · skipped 2
65
+ ```
66
+
67
+ A session a worker runs adds the note within a few seconds (after a running
68
+ turn); one with no worker gets it at its next start ("waits for its next
69
+ start"). Nothing starts a turn anywhere.
70
+
71
+ `--dry-run` prints the note's tags, each recipient's verdict ("would get it" or
72
+ "skipped") and its scope card: what chi knows about it from local state, and
73
+ what a triage model will read.
74
+
75
+ ```
76
+ 3f2a1c9e would get it ticket PAY-123 matches (branch)
77
+ project: shop (folder ../shop-pay, branch feat/pay-123-retry)
78
+ title: shop-pay · now the retry spec
79
+ tags: ticket PAY-123 · pr acme/shop#42
80
+ started: Fix the payments retry …
81
+ recap: Fixing the payments retry. Specs next.
82
+ recent: now the retry spec
83
+ ```
84
+
85
+ Exit status: 0 when it ran (skipped sessions included), 1 when it was refused,
86
+ no session is active or a delivery failed, 2 for a usage error.
87
+
88
+ ## What a session gets
89
+
90
+ The note is your text, then one line chi adds:
91
+
92
+ ```
93
+ [CONTEXT NOTE from broadcast, 14:20]
94
+ payments API returns 500 since 14:00 (PAY-123)
95
+ (Shared by your user on 2026-10-06 14:20 with the sessions it may concern; it reached you because ticket PAY-123 matches your branch.)
96
+ [END NOTE]
97
+ ```
98
+
99
+ With `--all` the line says "with every active session". The date is there
100
+ because a session with no worker may read it days later. The system prompt
101
+ tells the model what a note from `broadcast` is: if it affects its current
102
+ work, it says so briefly in its next answer, otherwise it ignores it, and it
103
+ doesn't act on it unless you ask. Other notes keep their rule (background,
104
+ not mentioned on their own). `chi note --source broadcast` is refused, so a
105
+ plain note can't pass for a broadcast.
106
+
107
+ The note and its line together are capped at 16 KiB like any note; the text
108
+ may take up to 15872 bytes.
109
+
110
+ ## For you, not for an agent
111
+
112
+ `chi broadcast` refuses to run inside a chi session (where
113
+ `SAMAGOTCHI_PARENT_SESSION` is set): "chi broadcast is for your user, not an
114
+ agent". With the guardrails bundle, the `chi-broadcast` rule also asks before
115
+ any shell command that runs it, once at a time, and only you may allow it
116
+ (a parent agent's `chi answer` can't). See [Guardrails](guardrails.md).
117
+
118
+ ## Settings
119
+
120
+ | Setting | Default | What it does |
121
+ |---|---|---|
122
+ | `broadcast.active_hours` | `8` | A session with no worker or REPL counts when its last turn ended within this many hours. |
123
+ | `broadcast.ticket_pattern` | `\b[A-Z][A-Z0-9]+-\d+\b` | A Ruby regex for ticket ids; an invalid one warns and the default is used. |
124
+
125
+ ```yaml
126
+ broadcast:
127
+ active_hours: 12
128
+ ticket_pattern: '\b(?:PAY|OPS)-\d+\b'
129
+ ```
data/docs/cli.md CHANGED
@@ -790,11 +790,19 @@ changes:
790
790
 
791
791
  ```
792
792
  status> model=Qwen3.6-35B | ↳ 3f2a1c9e | ctx=12.3% (under20) | mem: notes, cli_usage | muted: gh-helper
793
+ status> model=Qwen3.6-35B | ⑂ 2 running · 1 waiting | ctx=8.1% (under20) | mem: skill_coordinator
793
794
  ```
794
795
 
795
796
  - `model=`: the model in use, `(default: …)` beside it when it isn't the config's default, and
796
797
  `model=<served> (served; asked <name>)` when the server said it served another model.
797
798
  - `↳ <id>`: the session that delegated this one.
799
+ - `⑂ …`: this session's delegate children (forks aren't counted): how many run and how many wait
800
+ for an answer; only the counts there are, nothing when none does (`/children` has the rest:
801
+ done, failed, whether a reply was reported). The terminal reads them from the children's files
802
+ (the delegates in the session's `delegates.json`): an attached one when it joins, when a turn
803
+ ends and when a delegate's report merges into a turn, the REPL whenever it draws the row. A
804
+ child that starts waiting on an approval (which doesn't wake the parent) shows at the next of
805
+ those.
798
806
  - `ctx=`: the kernel's context estimate and its bucket, updated during a turn (on hosts that
799
807
  report none, `api: openai`, at the turn's end).
800
808
  - `mem:`: the memories the session read, with its `--memory` list; `muted:` its `--mute` list.
@@ -882,6 +882,8 @@ described in their own sections.
882
882
  | `recap.timeout` | `30` | yes | Seconds a recap request may take. |
883
883
  | `recap.min_user_turns` | `2` | yes | Prompts a session needs before it gets a recap. |
884
884
  | `recap.sentences` | `2-4` | yes | Recap length, `N` or `N-M` (1–10). |
885
+ | `broadcast.active_hours` | `8` | | `chi broadcast` reaches a session a worker or a chi REPL runs, or whose last turn ended within this many hours. See [Broadcast](broadcast.md). |
886
+ | `broadcast.ticket_pattern` | `\b[A-Z][A-Z0-9]+-\d+\b` | | A Ruby regex for ticket ids; one in the note and in a session's branch (any case) or prompts delivers the broadcast there. See [Broadcast](broadcast.md). |
885
887
  | `session.shared` | `true` | | Plain `chi` runs its session in a worker and attaches; `--no-shared` per run. |
886
888
  | `session.idle_exit_minutes` | `30` | yes | An unused worker exits after this; `0` = never. |
887
889
  | `session.keep_empty` | `false` | | Keep sessions nothing happened in. |
data/docs/context.md CHANGED
@@ -184,6 +184,43 @@ It needs [`gh`](https://cli.github.com), logged in (`gh auth login`).
184
184
  - It wakes for a review requesting changes, checks turning red, and the PR
185
185
  merged or closed. Comments alone never wake.
186
186
 
187
+ ### Line links
188
+
189
+ In the web, a file reference in an answer links to that line of the pull
190
+ request the session reviews: `lib/foo.rb:28` and `lib/foo.rb:28-34`, also in
191
+ inline code (`` `lib/foo.rb:28` ``). The model writes plain references; the
192
+ links are display only (the model's text stays as it was), and terminals
193
+ show the answer as it is.
194
+
195
+ - **Which PR**: the session's attached PRs (the branch's, `chi context add
196
+ <PR URL>`, "+ URL") and the PR URLs in your messages, newest first (a
197
+ delegate child's task counts: a parent that names the PR there gets links
198
+ in the child's answers). PR URLs in tool output or answers don't count.
199
+ - **Which file**: a file the PR changes, by its path, a renamed file's old
200
+ path, a bare name only one PR file has (`source_links.rb:28`), or an
201
+ absolute path into a checkout (a child's worktree). Any other file isn't
202
+ linked, nor is a path several of the session's PRs change.
203
+ - **Where to**: the line in the PR's **Files changed**, highlighted, when it
204
+ is inside a changed hunk (a removed file: on its old side); else the file
205
+ at the PR's head commit (a removed one: at its base), which has every
206
+ line.
207
+ - Code blocks, markdown links and URLs are left alone.
208
+ - The PR's files are read with `gh api` in the background when a turn
209
+ starts (and when a PR is attached while it runs), at most once a minute,
210
+ and read again only when the PR's head moved; the answer is linked from
211
+ what was read.
212
+ So the first answer of a very short turn may come without links, and
213
+ after a push links may be a few lines off until the next read. Without
214
+ `gh` or its login, nothing is linked.
215
+
216
+ Turn it off:
217
+
218
+ ```yaml
219
+ bundles:
220
+ github-pr:
221
+ line_links: false
222
+ ```
223
+
187
224
  ## Safety
188
225
 
189
226
  A command source runs **outside the guardrails gate**, every few minutes,
data/docs/guardrails.md CHANGED
@@ -5,7 +5,7 @@ loops (native and chat hosts). The verdict is **allow**, **ask** or
5
5
  **deny**; the strictest vote wins, and a deny can't be undone by a later
6
6
  voter. Of two asks the first one names the rule, except a rule only the
7
7
  user may allow (`chi-config`, `chi-hooks`, `shell-touches-chi`,
8
- `chi-context-cmd`): it takes over, and only the scopes both asks offer
8
+ `chi-context-cmd`, `chi-broadcast`): it takes over, and only the scopes both asks offer
9
9
  are offered.
10
10
 
11
11
  Who votes, in order:
@@ -244,7 +244,12 @@ guardrails:
244
244
  scopes: [once, session, rule]
245
245
  ```
246
246
 
247
- A rule gives at least one of `tool`, `command`, `path`, `git`, `touches` and `rm`.
247
+ A rule gives at least one of `tool`, `command`, `path`, `git`, `touches`, `rm` and `memory`.
248
+
249
+ `memory: remove` matches a `memory_write` with `remove: true` (it moves a
250
+ memory into the bundle trash). Nothing else asks about it: a memory's file is
251
+ never outside the repo (below), so without such a rule any session, a delegate
252
+ child included, removes a memory unasked.
248
253
 
249
254
  `rm: outside_tmp` (shell tools) matches a command whose `rm -r -f` reaches
250
255
  outside the tmp folders (`$TMPDIR`, `/tmp`, `/var/tmp`): every such `rm` in a
@@ -408,7 +413,7 @@ rules (`models: small`) are untagged, so they ask a small model in both modes.
408
413
  leaves out `(strict only)`.
409
414
 
410
415
  A rule in config.yml that uses a key this chi doesn't know yet (`modes:`,
411
- `touches:`, `rm:`, `skip_read_only:` before 0.20) doesn't parse, and a rule
416
+ `touches:`, `rm:`, `skip_read_only:` before 0.20, `memory:` before 0.36) doesn't parse, and a rule
412
417
  that doesn't parse denies every tool call (see Failing closed). Bundles say
413
418
  which chi they need (`requires_chi`), so their rules don't hit this.
414
419
 
@@ -427,7 +432,12 @@ hooks, approvals, bundles or attached context sources, or a `.git/hooks`
427
432
  (`shell-touches-chi`, `touches: chi_dirs`), unless they only read
428
433
  (`skip_read_only`); `chi context add … --cmd` (`chi-context-cmd`, once at a
429
434
  time: a command chi will run every few minutes outside the guardrails; see
430
- [Attached context](context.md#safety)); and answering chi's
435
+ [Attached context](context.md#safety)); `chi broadcast` (`chi-broadcast`,
436
+ once at a time and only by the user: it is the user's command, which also
437
+ refuses to run inside a session; see [Broadcast](broadcast.md)); removing a
438
+ memory with `memory_write remove: true` (`memory-remove`, once at a time:
439
+ a delegate child could otherwise remove its parent's handoff unasked; see
440
+ [Memory](memory.md)); and answering chi's
431
441
  questions around `chi answer`: `chi --attach` or `chi -p` with stdin from a
432
442
  pipe, a here-string or a file (`chi-answer-piped`), and `curl`/`wget` to a
433
443
  session's `/answer` route (`chi-answer-http`), both once or for the session.
data/docs/hooks.md CHANGED
@@ -82,6 +82,8 @@ class Watchful
82
82
  when :after_generation
83
83
  # One line in the REPL, the attached TUI and the web ("<bundle>> text",
84
84
  # or "hook> text" for a config hook); level: :warn colours it.
85
+ # fallback_for: :display marks a line a UI that renders the answer's
86
+ # display may leave out (below).
85
87
  event[:notify].call("the model repeated itself", level: :warn)
86
88
  when :before_tool_call
87
89
  # A single-select question through the question flow (REPL, attached
@@ -114,6 +116,21 @@ end
114
116
  known-names)` for a bundle hook, `audit.rb (config)` for a config hook,
115
117
  `turn hook` for one registered at runtime.
116
118
 
119
+ `event[:notify]` also takes `fallback_for:`, what the line stands in for, so
120
+ that a UI that already shows it can leave the line out. The one value is
121
+ `:display`: the line repeats what the hook's
122
+ [`event[:present]`](#presenting-the-answer-display-only) display shows (its
123
+ links), for a UI that doesn't render that display. The hook never names a UI;
124
+ each decides by what it renders. The web with markdown on (`web.markdown` and
125
+ the commonmarker gem) leaves such a line out, live and after a reload; with
126
+ markdown off it shows, as it does in the REPL, the attached TUI and
127
+ `chi -p`, which print the answer as text. The log keeps the line with
128
+ `fallback_for=display`. Another value, or none, shows the line everywhere.
129
+ Mark a line only when the display really shows all of it (source-links marks
130
+ its note only when the answer links every URL the note names). It needs chi
131
+ 0.35.0: an older chi's `event[:notify]` raises `ArgumentError` on the keyword,
132
+ so a bundle that passes it sets `requires_chi: ">= 0.35.0"`.
133
+
117
134
  A steer is saved in the session as `{role: "user", kind: "steer", source:
118
135
  "<bundle>", content: "…"}`, its text raw. The model reads it as a user turn
119
136
  led by one line naming the sender: `[Steer from the <bundle> plugin,
@@ -407,7 +424,7 @@ stripped of surrounding whitespace, except file text, an edit's
407
424
  | `write` | — | `path` | `content` |
408
425
  | `edit` | — | `path` | `old_text`, `new_text`, `start_line`, `end_line` |
409
426
  | `memory_read` | `name` | — | `scope` |
410
- | `memory_write` | — | `name` | `content`, `scope`, `description`, `current_model_only` |
427
+ | `memory_write` | — | `name` | `content`, `scope`, `description`, `current_model_only`, `remove` |
411
428
  | `task_create` | `command` | — | `cwd`, `env` |
412
429
  | `task_get` | `id` or `task_id` | — | — |
413
430
  | `task_list` | — | — | — |
@@ -504,8 +521,8 @@ sources: JIRA JIRA-123 → https://myjira.com/browse/JIRA-123, JIRA JIRA-10 →
504
521
  ```
505
522
 
506
523
  The note is **not part of the conversation**: it is an event shown to the
507
- user, never stored in the session file. A UI replays it while the session's
508
- worker lives (a page reload keeps it; a stopped worker loses it). Only the
524
+ user, never stored in the session file. A UI replays it with the session's
525
+ cards (a page reload keeps it, after the worker stopped too). Only the
509
526
  model's final answer is scanned (the last `role: "model"` message), and only
510
527
  the first 20 000 characters of it. A ref that is already a link is skipped —
511
528
  inside a bare URL (`https://x.com/JIRA-123`), in a markdown link's target, or
@@ -543,7 +560,11 @@ text stays as it was, and the links survive a reload and a stopped worker
543
560
  apply, and a ref in code (a `` `span` `` or a fenced block) or anywhere in a
544
561
  markdown link is left as it is; past the first 20 000 characters the answer
545
562
  is unchanged. The terminals see only the line; `note: false` drops it and
546
- keeps the web links.
563
+ keeps the web links. With markdown on, the web leaves the line out when the
564
+ answer links every URL it names, since it would only repeat them as plain
565
+ text ([`fallback_for: :display`](#what-a-hook-can-do-the-runtime)); it shows
566
+ when it names one the answer doesn't link (a ref only in code, two sources on
567
+ one ref) and whenever markdown is off. Bundle 0.4.0 needs chi 0.35.0 for that.
547
568
 
548
569
  The `prefix:` form compiles to `\b<prefix>-(\d+)\b` and the URL is
549
570
  `base_url` + the full ref text (`JIRA-123`). The `pattern:` form takes a
data/docs/memory.md CHANGED
@@ -15,6 +15,9 @@ Tool behavior:
15
15
  - If `scope` is provided (`project` or `system`), only that scope is read.
16
16
  - If `scope` is omitted, read falls back from project to system.
17
17
  - `memory_write`: `scope` is required (`project` or `system`). The entry name is passed via the `name` parameter (not `path` — the file tools use `path`). On success, the return value includes the full file path. For a small change to an existing memory (a step, a line) the model is told to `edit` that file rather than rewrite it all with `memory_write`.
18
+ - `memory_write`'s `description` is the managed `index.md` line's text: one line (whitespace and newlines collapse to single spaces), at most 200 characters (longer is refused, since every session's prompt for the repo carries it). A missing or blank one keeps the line's description.
19
+ - `memory_write` with `name`, `scope` and `description` but no `content` changes only that entry's index line (its size and date are refreshed; the date becomes today) and leaves the file as it is: a skill that keeps its status in a memory's description changes it without resending the body. The memory must exist in that scope; the form is refused for `index` and with `current_model_only`.
20
+ - `memory_write` with `name`, `scope` and `remove: true` (no content, description or `current_model_only`) removes a memory: its file and its model overlays (`<name>.<key>.md`, not a dotted memory with an index line of its own) move into one dir of the bundle trash, `memory-<name>-<time>` under `memories/.bundles/.trash/`, which `chi bundle trash` lists and empties, and its index line is dropped. When the file is already gone (deleted by hand), the dangling line is dropped alone. A memory a bundle owns (an installed bundle's record lists it, or its line says `· from <bundle>`) is refused: `chi bundle uninstall` removes it. `index` can't be removed. The guardrails bundle's `memory-remove` rule asks before every removal, once at a time (see [Guardrails](guardrails.md#the-guardrails-bundle)).
18
21
  - `write`/`edit` that change a `*.md` right in a scope dir refresh its managed `index.md` line (date, bytes; the description is kept), as `memory_write` does. A memory created with `write` gets a line with no description; any other `.md` written there gets a line too. Model overlays (`<name>.<key>.md` next to `<name>.md`), files in another project's folder and files changed by `execute` are left out.
19
22
 
20
23
  ## Model-Specific Memory Overlays
data/docs/plugins.md CHANGED
@@ -488,7 +488,7 @@ full label, `plugin.rb (bundle my-bundle)`.
488
488
  | `ctx.log` | `ctx.log.info(:event, key: value)`: debug-log records tagged `plugins`, with `bundle=<bundle>` |
489
489
  | `ctx.messages` | the conversation, as a frozen copy, without the system prompt. While a turn runs, a session worker's (attached, web) adds that turn so far: its prompt, the model's text and the lines merged into it (no tool calls or thinking); the REPL's is the conversation before that turn |
490
490
  | `ctx.messages_partial?` | whether `ctx.messages` leaves out a running turn (the REPL mid-turn), so a plugin can say what its answer is about |
491
- | `ctx.notify(text, level: :info)` | one line to the user, labelled by the bundle (`my-bundle> …`). Every UI shows it, during a turn (a tool, a hook) or between turns (a command) |
491
+ | `ctx.notify(text, level: :info, fallback_for: nil)` | one line to the user, labelled by the bundle (`my-bundle> …`). Every UI shows it, during a turn (a tool, a hook) or between turns (a command). `fallback_for: :display` marks a line that repeats the answer's display (`event[:present]`), which a UI that renders the display leaves out: see [Hooks](hooks.md#what-a-hook-can-do-the-runtime). The keyword needs chi 0.35.0 (it raises on an older chi) |
492
492
  | `ctx.card(title:, body: "", actions: [], level: :info, id: nil)` | a card in every UI, returning its id: see [Cards](#cards) |
493
493
  | `ctx.ask_user(question:, options:, header: nil, allow_freeform: false)` | a single-select question through the question flow: `{selected:, freeform:, selected_indices:}`, or nil (no one to ask, cancelled, bad options) |
494
494
  | `ctx.cancelled?` | whether the running turn was cancelled (a long tool should stop) |
@@ -836,6 +836,13 @@ bundles:
836
836
  closed, then TERM and KILL go to the server's process group.
837
837
  - **`/mcp`** (anytime) shows a card with the servers, their state (cached
838
838
  (not started), running with its pid, failed, stopped) and their tools.
839
+ A cached or running server's line says roughly how many tokens its tool
840
+ definitions take (`~4,232 tokens`), and the last line totals the servers
841
+ whose tools the model has: what every request carries. It is an estimate:
842
+ the definitions' JSON as the chat API gets it (`api: openai`), divided by
843
+ 4. The native prompts (Gemma, Qwen) render a flatter schema, so there they
844
+ take less. The log records each server's estimate when it changes
845
+ (`mcp_tools_estimated`).
839
846
  - The server's stderr goes to the debug log (`plugins` records, bundle=mcp).
840
847
  - **Guardrails.** A rule's `tool:` can be a glob, so one rule covers every
841
848
  MCP tool:
@@ -1137,7 +1144,9 @@ nothing without `gh`, a repository or an open PR), and resolves PR URLs for
1137
1144
  `chi context add` and the web's "+ URL". Its script prints the PR as text
1138
1145
  and a summary of what changed in counts, authors and states only; it wakes
1139
1146
  the session for a review requesting changes, checks turning red, or the PR
1140
- merged or closed. See [Attached context](context.md#github-prs-the-github-pr-bundle).
1147
+ merged or closed. In the web, a `path:line` in an answer links to that line
1148
+ of the session's PR ([Line links](context.md#line-links)). See [Attached
1149
+ context](context.md#github-prs-the-github-pr-bundle).
1141
1150
 
1142
1151
  ## The coordinator bundle
1143
1152
 
@@ -1163,10 +1172,27 @@ by themselves there.
1163
1172
  agent may have merged the branch already), merges `--ff-only` and never
1164
1173
  pushes unless you ask. After a merge it stops the
1165
1174
  child and removes only the worktrees and branches it made.
1175
+ - **A handoff memory.** The conversation is lost when the session ends, so
1176
+ the skill keeps a project memory `handoff_<epic-slug>` with only what git
1177
+ and the session list can't rebuild: the split (branch, worktree, child id
1178
+ per task), your decisions, its verdicts and the follow-ups. Its index
1179
+ description is the status, descriptive only and naming the session that
1180
+ owns it ("OPEN coordinator handoff calc-v2 (session ab12cd34): 2/3 merged",
1181
+ later "DONE: …"): every session in the repo sees that line. The model saves
1182
+ a decision there before acting on it, and changes the status with
1183
+ `memory_write`'s description-only form. When everything is done it marks
1184
+ the handoff DONE and asks whether to remove it (`memory_write remove:
1185
+ true`, which the guardrails bundle asks you to confirm). Children are told
1186
+ not to touch it.
1187
+ - **Resume.** A new coordinator session (or one asked "where were we?")
1188
+ reads the open handoff, checks it against git and the session list (git
1189
+ wins; mismatches are reported) and goes on from the first open step.
1166
1190
  - **`/coordinate <goal>`** sends this session a turn asking the model to
1167
1191
  follow the skill for the goal (in a `--no-shared` REPL it shows the text to
1168
1192
  send yourself). Asking "do this in parallel" works too: the index line leads
1169
- the model to the skill.
1193
+ the model to the skill. **`/coordinate resume`** asks it to resume this
1194
+ project's open handoff; with several open it shows a card with a Resume
1195
+ action each (`/coordinate resume <name>` picks one), with none it says so.
1170
1196
  - **`/children [all]`** shows the session's children as a card, newest first,
1171
1197
  one line each: `` `ab12cd34` · running · fix/flaky · "fix the flaky spec" ``,
1172
1198
  `` `9a8b7c6d` · done · feat/x · reported · "All 12 specs pass" `` (`not
data/docs/sessions.md CHANGED
@@ -60,14 +60,14 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
60
60
 
61
61
  **Projects:** a session belongs to the git project it was started in: the repository, whichever worktree or subfolder of it (the same project root memories use). `chi sessions list` (with `--live` and `--format` too) and `chi web` show the current folder's project; `--scope=all` shows every session, and so does a folder in no repo (`~`). `--cwd PATH` is a folder filter instead of the project. The project is stored with the session (`project_root` in its JSON, `project` in `--format json`), so a session keeps it after its worktree is deleted; sessions older than that are placed by their folder, and one whose folder is gone shows in `--scope=all` only. Retention, `--resume`/`--attach ID`, `chi send`/`chi note ID` and `chi note --all` (every live session) are not scoped. The agent's `list_sessions` lists its own project's sessions; `cwd: "/"` lists every one.
62
62
 
63
- `--live`, `--cwd` and `--format` make `list` a picker for scripts (`SessionManager.session_summaries`): `--live` keeps the sessions a worker runs now (the owner lock, not the saved status; a session open in a plain REPL is left out), `--cwd PATH` those in PATH or below, and test runs are left out unless the list is itself run as one (`SAMAGOTCHI_ENV=test`, `RACK_ENV=test` or `CI` set: then they show, marked `[test]`; `chi note --all` likewise). `--live` shows 10 unless `--limit` says otherwise; filters apply before the limit. `--format json` prints `[{id, short_id, desc, cwd, project, updated_at, live, busy, owner, recap, parent_id, archived, scratch, ctx_pct}]` (`owner`: `"worker"`, `"tui"` for a plain REPL, which takes no notes or messages, or null; `recap`: the first sentence of the session's recap, or null), `--format tsv` one `id<TAB>desc` line per session, where `desc` is `<folder> · <last prompt>` cut to 60 characters (the text form of `--live`/`--cwd` shows `<folder> · <recap>` when there is one; tsv and json keep `desc`).
63
+ `--live`, `--cwd` and `--format` make `list` a picker for scripts (`SessionManager.session_summaries`): `--live` keeps the sessions a worker runs now (the owner lock, not the saved status; a session open in a plain REPL is left out), `--cwd PATH` those in PATH or below, and test runs are left out unless the list is itself run as one (`SAMAGOTCHI_ENV=test`, `RACK_ENV=test` or `CI` set: then they show, marked `[test]`; `chi note --all` likewise). `--live` shows 10 unless `--limit` says otherwise; filters apply before the limit. `--format json` prints `[{id, short_id, desc, cwd, project, updated_at, live, busy, owner, recap, parent_id, delegate, archived, scratch, ctx_pct}]` (`owner`: `"worker"`, `"tui"` for a plain REPL, which takes no notes or messages, or null; `recap`: the first sentence of the session's recap, or null; `delegate`: true for a child the `delegate` tool started, false for a fork, which has a `parent_id` too), `--format tsv` one `id<TAB>desc` line per session, where `desc` is `<folder> · <last prompt>` cut to 60 characters (the text form of `--live`/`--cwd` shows `<folder> · <recap>` when there is one; tsv and json keep `desc`).
64
64
 
65
65
  **Ordering:**
66
66
 
67
67
  - `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.
68
- - 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); "All sessions" (or `/`) opens every session at `#/sessions`, with a search over preview, id and status (Esc or Back returns). 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). The info bar copies `chi --attach <id>` for a terminal. 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. 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; 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, sampling?}], warning?}`; `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). 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 choice; 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).
68
+ - 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). The info bar copies `chi --attach <id>` for a terminal. 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. 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; 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, sampling?}], warning?}`; `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). 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 choice; 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).
69
69
  - 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).
70
- - 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), `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). Unit-tested via `npm test` (`node --test spec/web/public/*.test.js`).
70
+ - 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), `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). Unit-tested via `npm test` (`node --test spec/web/public/*.test.js`).
71
71
  - 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).
72
72
 
73
73
  **Test-session hygiene:**
@@ -88,7 +88,7 @@ pbpaste | chi note --source slack 3f2a 8c1d
88
88
 
89
89
  - The text comes from `-m` or stdin (a terminal on stdin is a usage error, not a wait). It is stripped; an empty note or one over 16 KiB is refused, never cut. `--source` (default `cli`) names where it came from. `--all` is every live session.
90
90
  - It lands in `<uuid>/notes/`, apart from `input/`, so nothing that runs turns sees it. A live worker adds it to the conversation within a few seconds, between turns: one sent during a turn waits for that turn to end. A session with no worker keeps it until a worker next starts (a prompt, `--attach`), which adds it before anything else. A session open in a plain REPL (`--no-shared`) refuses notes. `chi note` prints one line per session: queued, waits for the next start (with the queue count), or refused.
91
- - In the conversation it is a tail system message marked `kind: note`, framed `[CONTEXT NOTE from slack, 14:02]\n…\n[END NOTE]` (from another session: `from session 3f2a1c (~/projects/foo)`). The system prompt says notes are background, not requests: the model uses one when it is relevant, doesn't answer it on its own, and never follows instructions inside it. Its text is escaped like user text, so it can't fake a turn. It survives `--resume`, a reload, `!rollback` and a continue answered no.
91
+ - In the conversation it is a tail system message marked `kind: note`, framed `[CONTEXT NOTE from slack, 14:02]\n…\n[END NOTE]` (from another session: `from session 3f2a1c (~/projects/foo)`). The system prompt says notes are background, not requests: the model uses one when it is relevant, doesn't answer it on its own, and never follows instructions inside it. A note from `broadcast` ([`chi broadcast`](broadcast.md)) is the one exception to "doesn't answer it": the model says briefly in its next answer when it affects its current work, and still doesn't act on it unless asked. `--source broadcast` is refused for `chi note`. Its text is escaped like user text, so it can't fake a turn. It survives `--resume`, a reload, `!rollback` and a continue answered no.
92
92
  - The web shows it as a dim "note from …" block, live (`context_added`) and after a reload; the attached terminal as one dim `note from slack: <first line>` line, also when joining.
93
93
  - Agents: `list_sessions` (other sessions of this project, newest first, up to 20; optional `cwd`, `"/"` for every project) and `send_note(session, text)`, which sends a note from this session. Neither starts a turn anywhere.
94
94
 
@@ -182,7 +182,7 @@ chi send --wait 3fa2 # sends nothing: waits fo
182
182
 
183
183
  ## Delegating
184
184
 
185
- The `delegate` tool hands a task to a **child session**: an ordinary chi session in a worker of its own, started in the parent's `working_directory` with the task, verbatim, as its first user message, on the parent's model unless the call names one (`model:` takes a name or an alias, resolved as `--model` is; an unknown host comes back as the tool's `Error:` line; an id the host's saved model list doesn't have starts the child with a `Warning: host 'h' doesn't list model 'x' (did you mean: …?)` line in the result). Children run in parallel and only their **final reply** comes back to the parent: the child's newest `output/<timestamp>.txt` file, which the worker writes at the end of each turn that produced visible text, cut head-and-tail like an `execute` result. Nothing of the child's trace enters the parent's context. Nothing is hidden: a child shows in `chi sessions list` (`↳ <parent>`), the web (a `↳` chip on its card, `delegated by` in its info bar, listed right after its parent in the all-sessions view) and the `list_sessions` tool (`child`; the parent shows as `parent` in the child's own list); the user can `chi --attach <child id>` and steer it mid-run, and `chi send` and `send_note` reach it like any session.
185
+ The `delegate` tool hands a task to a **child session**: an ordinary chi session in a worker of its own, started in the parent's `working_directory` with the task, verbatim, as its first user message, on the parent's model unless the call names one (`model:` takes a name or an alias, resolved as `--model` is; an unknown host comes back as the tool's `Error:` line; an id the host's saved model list doesn't have starts the child with a `Warning: host 'h' doesn't list model 'x' (did you mean: …?)` line in the result). Children run in parallel and only their **final reply** comes back to the parent: the child's newest `output/<timestamp>.txt` file, which the worker writes at the end of each turn that produced visible text, cut head-and-tail like an `execute` result. Nothing of the child's trace enters the parent's context. Nothing is hidden: a child shows in `chi sessions list` (`↳ <parent>`), the web (folded into its parent's card on the strip and in all sessions, whose `▸ N delegates` chip lists the delegates at any depth, see Web UI above; a card of its own with a `↳` chip when its parent isn't listed; `delegated by` in its info bar; the parent's info bar carries a `⑂ N` chip counting its delegates, with how many run, wait, are done, failed, stopped or idle in the tooltip and the waiting ones named in the attention colour; a click opens all sessions), the terminal's status row (a parent's `⑂ 2 running · 1 waiting`, see [Status Line](cli.md#status-line)) and the `list_sessions` tool (`child`; the parent shows as `parent` in the child's own list); the user can `chi --attach <child id>` and steer it mid-run, and `chi send` and `send_note` reach it like any session.
186
186
 
187
187
  - `delegate(task, model:, session:, cwd:, wait:, timeout:)`: starts a child and, by default, waits for its reply. `wait: false` is the usual way for work that takes a while, several children side by side, or to keep talking with the user: it returns at once, the parent ends its turn or works on, and the reply comes back by itself as a *delegate report* (below), in a turn of its own if the parent is idle. The tool descriptions tell the model so, and to wait (`wait: true`, `delegate_result`) only when its turn can't go on without the reply. `timeout`, default 600 s, leaves a slow child running; 0 checks once without waiting. With `session:` (a child's id or prefix) the task goes to that child as a follow-up instead, delivered like `chi send` (client id `delegate:<parent short id>`, shown as `delegate>` in the child's UIs); only this session's children take one.
188
188
  - `cwd:` starts the child in another folder of the parent's repository: a linked worktree (`git worktree add ../<repo>-<task> -b <branch>`, which the model runs with `execute` first, under guardrails; chi runs no git here) or a subfolder; a relative path is taken from the parent's folder. The folder must exist, have the parent's project root (the same common git dir, `MemoryPaths.project_root`) and be in a work tree: a git dir (`.git`, `.git/objects`, a bare layout's `.bare`) or a bare layout's container is refused (`git rev-parse --is-inside-work-tree`, read-only, decides). Outside git only a subfolder of the parent's folder is allowed. Otherwise the tool returns `Error: cwd must be a folder of this session's repository …` or `… is not in a work tree …`. A follow-up (`session:`) refuses `cwd:`: the child keeps its folder. The child's guardrails measure from its own worktree, and project memories, `list_sessions` and the web's project are the same as the parent's.
@@ -205,4 +205,4 @@ The `delegate` tool hands a task to a **child session**: an ordinary chi session
205
205
  - Every child starts with the shipped `delegated` system memory preloaded (`preloaded_memory_names: ["system/delegated"]`; `--mute delegated` would hide it): put everything in the final reply, don't delegate further, don't write memories or change config, stay in the folder. Its system prompt also says `Delegated by session <parent id>`. The parent's own `--mute` list does not carry over.
206
206
  - **A child asks before it changes anything outside its folder** (the work tree top of the folder it started in): a write or edit there, mutating git in another checkout, a command that isn't read-only run from outside it or naming the main checkout or a sibling worktree. It's a core guardrail, on in every mode and without the guardrails bundle, and answered once or for the child's session ([Guardrails](guardrails.md#a-delegate-childs-boundary)); the approval relays to the parent's user as any other.
207
207
  - Limits: a child cannot `delegate` (depth 1; the tool refuses), and one session may have at most `session.max_children` (default `4`, env `SAMAGOTCHI_SESSION_MAX_CHILDREN`) children running at a time, counted from the session files (a finished or idle-exited child does not count; one whose worker is still starting does, for 15 s). A refused call creates no session. Nothing stops children with the parent: `chi sessions stop ID` does.
208
- - The link is the session's `parent_id`, written before the worker starts, so a respawn keeps it (and `delegate: true`, which a fork's `parent_id` doesn't come with; a child saved before that field is known by its `system/delegated` memory); `GET /api/sessions` and `/api/sessions/:id` carry it, and `SessionManager.children_of` lists a parent's children.
208
+ - The link is the session's `parent_id`, written before the worker starts, so a respawn keeps it (and `delegate: true`, which a fork's `parent_id` doesn't come with; a child saved before that field is known by its `system/delegated` memory); `GET /api/sessions`, `/api/sessions/:id` and the web hub's `session` frames carry it and `delegate` (a fork is `false`; the web parent's children chip counts delegates only); `SessionManager.children_of` lists a parent's children.