samagotchi 0.3.0 → 0.5.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 +162 -1
- data/README.md +29 -2
- data/bin/chi +60 -69
- data/docs/cli.md +211 -77
- data/docs/configuration.md +118 -21
- data/docs/desktop.md +39 -4
- data/docs/guardrails.md +11 -0
- data/docs/hooks.md +89 -7
- data/docs/memory.md +40 -0
- data/docs/plugins.md +50 -0
- data/docs/releasing.md +15 -12
- data/docs/sessions.md +20 -18
- data/lib/samagotchi/bootstrap/config_writer.rb +1 -2
- data/lib/samagotchi/bridge/sse_writer.rb +0 -3
- data/lib/samagotchi/bridge/turn_accumulator.rb +2 -0
- data/lib/samagotchi/bridge.rb +20 -12
- data/lib/samagotchi/bundles/skills/manifest.yml +10 -0
- data/lib/samagotchi/bundles/skills/plugin.rb +419 -0
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +178 -5
- data/lib/samagotchi/bundles/source-links/manifest.yml +3 -3
- data/lib/samagotchi/bundles/source-links/source_links.md +1 -1
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +9 -10
- data/lib/samagotchi/bundles/system/delegated.md +6 -7
- data/lib/samagotchi/bundles/system/identity.md +5 -0
- data/lib/samagotchi/bundles/system/manifest.yml +6 -6
- data/lib/samagotchi/bundles/system/memory_guide.md +26 -0
- data/lib/samagotchi/bundles/system/self_map.md +2 -1
- data/lib/samagotchi/client.rb +25 -26
- data/lib/samagotchi/commands/registry.rb +8 -0
- data/lib/samagotchi/config.rb +97 -113
- data/lib/samagotchi/desktop/macos/App.swift +12 -8
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +4 -2
- data/lib/samagotchi/desktop/macos/Images.swift +113 -0
- data/lib/samagotchi/desktop/macos/Info.plist.erb +6 -0
- data/lib/samagotchi/desktop/macos/Panel.swift +112 -9
- data/lib/samagotchi/desktop/macos.rb +59 -8
- data/lib/samagotchi/desktop_command.rb +6 -3
- data/lib/samagotchi/edit_preview.rb +82 -0
- data/lib/samagotchi/engine.rb +236 -443
- data/lib/samagotchi/gem_update.rb +89 -0
- data/lib/samagotchi/guardrails/approval.rb +26 -4
- data/lib/samagotchi/guardrails/load_failures.rb +9 -3
- data/lib/samagotchi/host_registry.rb +8 -12
- data/lib/samagotchi/idle_client.rb +24 -15
- data/lib/samagotchi/idle_reminders.rb +2 -2
- data/lib/samagotchi/image_store.rb +10 -6
- data/lib/samagotchi/kernel_loop.rb +59 -123
- data/lib/samagotchi/live_versions.rb +65 -0
- data/lib/samagotchi/llm/api_key.rb +41 -0
- data/lib/samagotchi/llm/chat_loop.rb +77 -13
- data/lib/samagotchi/llm/errors.rb +38 -7
- data/lib/samagotchi/llm/http.rb +19 -22
- data/lib/samagotchi/llm/openai_chat.rb +22 -26
- data/lib/samagotchi/memory_bundle/installer.rb +65 -63
- data/lib/samagotchi/memory_bundle/provenance.rb +51 -12
- data/lib/samagotchi/memory_bundle/shipped_update.rb +157 -0
- data/lib/samagotchi/memory_bundle/status.rb +4 -1
- data/lib/samagotchi/memory_bundle/system_bundle.rb +81 -53
- data/lib/samagotchi/model_profile.rb +27 -10
- data/lib/samagotchi/note_command.rb +2 -1
- data/lib/samagotchi/prompt.rb +4 -2
- data/lib/samagotchi/reminder_store.rb +1 -9
- data/lib/samagotchi/reply_wait.rb +48 -4
- data/lib/samagotchi/self_report.rb +37 -5
- data/lib/samagotchi/send_command.rb +190 -17
- data/lib/samagotchi/session.rb +4 -2
- data/lib/samagotchi/session_commands.rb +38 -8
- data/lib/samagotchi/session_manager.rb +19 -53
- data/lib/samagotchi/system_prompt.rb +403 -0
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +5 -3
- data/lib/samagotchi/terminal_ui/attached_loop.rb +141 -108
- data/lib/samagotchi/terminal_ui/attached_view.rb +27 -12
- data/lib/samagotchi/terminal_ui/event_renderer.rb +29 -8
- data/lib/samagotchi/terminal_ui/formatting.rb +41 -22
- data/lib/samagotchi/terminal_ui/input_support.rb +7 -23
- data/lib/samagotchi/terminal_ui/plain_surface.rb +13 -7
- data/lib/samagotchi/terminal_ui/question_prompt.rb +35 -0
- data/lib/samagotchi/terminal_ui/status_row.rb +81 -0
- data/lib/samagotchi/terminal_ui/surface.rb +1 -1
- data/lib/samagotchi/terminal_ui.rb +142 -923
- data/lib/samagotchi/text_diff.rb +181 -0
- data/lib/samagotchi/thinking.rb +126 -0
- data/lib/samagotchi/tool_activity.rb +52 -2
- data/lib/samagotchi/tool_runner.rb +37 -1
- data/lib/samagotchi/tools/ask_user_question.rb +41 -33
- data/lib/samagotchi/tools/edit.rb +23 -9
- data/lib/samagotchi/tools/execute.rb +3 -3
- data/lib/samagotchi/tools/output_guardrails.rb +8 -7
- data/lib/samagotchi/tools/read.rb +4 -4
- data/lib/samagotchi/tools/write.rb +4 -0
- data/lib/samagotchi/turn_flow.rb +12 -2
- data/lib/samagotchi/update_command.rb +309 -0
- data/lib/samagotchi/update_hint.rb +59 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +6 -4
- data/lib/samagotchi/web/app.rb +173 -38
- data/lib/samagotchi/web/lan.rb +99 -0
- data/lib/samagotchi/web/message_parts.rb +19 -10
- data/lib/samagotchi/web/public/activity.js +10 -0
- data/lib/samagotchi/web/public/app.js +135 -78
- data/lib/samagotchi/web/public/chat_view.js +8 -1
- data/lib/samagotchi/web/public/data.js +2 -0
- data/lib/samagotchi/web/public/diff_view.js +58 -0
- data/lib/samagotchi/web/public/index.html +185 -18
- data/lib/samagotchi/web/public/model_pick.js +136 -0
- data/lib/samagotchi/web/public/model_picker.js +224 -0
- data/lib/samagotchi/web/public/notify.js +10 -0
- data/lib/samagotchi/web/public/question_card.js +3 -1
- data/lib/samagotchi/web/public/stage_model.js +110 -0
- data/lib/samagotchi/web/public/stage_view.js +580 -0
- data/lib/samagotchi/web/public/timing.js +6 -2
- data/lib/samagotchi/web/public/turn_events.js +38 -10
- data/lib/samagotchi/web/public/turn_model.js +11 -3
- data/lib/samagotchi/web/public/turn_view.js +76 -20
- data/lib/samagotchi/web/qr.rb +40 -0
- data/lib/samagotchi/web/server.rb +101 -11
- data/lib/samagotchi/web/token.rb +97 -0
- data/lib/samagotchi/worker.rb +5 -4
- metadata +38 -3
- data/lib/samagotchi/terminal_ui/legacy_surface.rb +0 -111
data/docs/desktop.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Desktop helper (macOS)
|
|
2
2
|
|
|
3
|
-
`chi desktop` installs **Chi Helper**, a small native app. It sends text you selected in any app
|
|
3
|
+
`chi desktop` installs **Chi Helper**, a small native app. It sends text you selected in any app (or a screenshot,
|
|
4
|
+
an image: see [Images](#images)) to a chi session,
|
|
4
5
|
either with a question as [your message](sessions.md#sending-a-message) (a turn runs, and the answer shows in the
|
|
5
6
|
attached terminal or web page), or as a [context note](sessions.md#context-notes) (the model sees it on its next
|
|
6
7
|
turn, and no turn starts).
|
|
@@ -35,6 +36,32 @@ note needs a session. After the send the panel shows `started <id>…` for 3 s.
|
|
|
35
36
|
|
|
36
37
|
A session open in a `chi --no-shared` REPL isn't listed: it takes no notes or messages.
|
|
37
38
|
|
|
39
|
+
## Images
|
|
40
|
+
|
|
41
|
+
The panel also sends images, as attachments of the message (`chi send --image`, see
|
|
42
|
+
[Sending a message](sessions.md#sending-a-message)):
|
|
43
|
+
|
|
44
|
+
- **A screenshot:** ⌃⇧⌘4 (a region to the clipboard), then ⌃⌥⌘N. The panel shows it as a thumbnail, named
|
|
45
|
+
`clipboard.png`; the context box stays empty.
|
|
46
|
+
- **Finder:** right-click image files → Services → **Send to chi** (the same Service as for text, so its shortcut
|
|
47
|
+
works here too), or ⌘C on them and ⌃⌥⌘N. Up to 20 at once.
|
|
48
|
+
- **Other apps:** a picture selected in Preview, Safari's Copy Image, anything that puts image data on the clipboard.
|
|
49
|
+
- **Drop** image files or the floating screenshot thumbnail (⇧⌘4 to a file) anywhere on the open panel: they are
|
|
50
|
+
added to the ones there.
|
|
51
|
+
|
|
52
|
+
When the clipboard holds both text and a picture (cells copied in Numbers or Excel), the text wins, as before. The
|
|
53
|
+
thumbnails sit under the message line, 48 px high, the file name as a tooltip; hover one for its ✕.
|
|
54
|
+
|
|
55
|
+
- A message is required with images: the line says "Say something about the image…", and ⏎ does nothing until
|
|
56
|
+
there is text (the context box counts).
|
|
57
|
+
- Notes are text only: ⌘⏎ with images beeps and says "Notes are text only: ⏎ sends the image as a message".
|
|
58
|
+
- The session's model must see images. A text-only one fails the turn in the session (the attached terminal or the
|
|
59
|
+
web shows why); the panel has already said it was sent.
|
|
60
|
+
- A session busy with a turn runs the image message as its next turn: the line says `(runs after the current turn)`.
|
|
61
|
+
- Clipboard and dropped image data goes to temp files under `$TMPDIR/chi-helper`, deleted after the send or when
|
|
62
|
+
the panel closes; files from Finder are sent as they are, never touched. A send with images may take up to 30 s
|
|
63
|
+
(converting, a worker starting) before the panel gives up.
|
|
64
|
+
|
|
38
65
|
## Install
|
|
39
66
|
|
|
40
67
|
```sh
|
|
@@ -53,16 +80,22 @@ don't break it. From a checkout it runs **that** checkout's `bin/chi`; installin
|
|
|
53
80
|
warning, because the helper stops working once that worktree is removed. After switching between a checkout and a
|
|
54
81
|
gem install, run `chi desktop upgrade` from the one you now use.
|
|
55
82
|
|
|
83
|
+
`chi update` keeps it current: it rebuilds and restarts the helper only when its Swift sources changed since the
|
|
84
|
+
build (`launch.json` records their digest) or the Ruby it runs moved. A new chi that left the sources alone only
|
|
85
|
+
rewrites `launch.json`, which the app reads at each send, so the app keeps its older version number and that's fine.
|
|
86
|
+
|
|
56
87
|
## Commands
|
|
57
88
|
|
|
58
89
|
| Command | Does |
|
|
59
90
|
|---|---|
|
|
60
91
|
| `chi desktop install [--force] [--login]` | builds, installs and starts it; `--force` replaces an existing copy |
|
|
61
|
-
| `chi desktop upgrade` | rebuilds it for this chi and restarts it, keeping the login setting |
|
|
92
|
+
| `chi desktop upgrade` | rebuilds it for this chi and restarts it, keeping the login setting (always; `chi update` does it only when needed) |
|
|
62
93
|
| `chi desktop uninstall` | quits it and removes the app, its login item, launch file and settings |
|
|
63
94
|
| `chi desktop status` | version against chi's, how it runs chi, state dirs, Service, hotkey, process, login item |
|
|
64
95
|
|
|
65
|
-
`chi self` has a `desktop` line: `0.1.x (matches)`, `0.1.w (chi
|
|
96
|
+
`chi self` has a `desktop` line: `0.1.x (matches)`, `0.1.w (up to date for chi 0.1.x)` (an older build whose sources
|
|
97
|
+
haven't changed), `0.1.w (chi is 0.1.x: chi update)` (a rebuild is due) or `not installed`. `chi desktop status`
|
|
98
|
+
says the same.
|
|
66
99
|
|
|
67
100
|
## How it runs chi
|
|
68
101
|
|
|
@@ -92,7 +125,7 @@ Each call is stopped after 10 s. A stopped `chi note` says the note may be partl
|
|
|
92
125
|
## Troubleshooting
|
|
93
126
|
|
|
94
127
|
- **"chi not found at …, run `chi desktop upgrade`"**: the Ruby or checkout in `launch.json` moved (a Ruby upgrade,
|
|
95
|
-
a removed worktree). Run `chi desktop upgrade` from the chi you use now.
|
|
128
|
+
a removed worktree). Run `chi desktop upgrade` (or `chi update`) from the chi you use now.
|
|
96
129
|
- **No "Send to chi" in the Services menu:** check `chi desktop status` (service). Try
|
|
97
130
|
`/System/Library/CoreServices/pbs -update`, start the app again, or log out and back in. It must be ticked in
|
|
98
131
|
System Settings → Keyboard → Keyboard Shortcuts… → Services → Text.
|
|
@@ -100,4 +133,6 @@ Each call is stopped after 10 s. A stopped `chi note` says the note may be partl
|
|
|
100
133
|
restarts or you log out: macOS caches Services.
|
|
101
134
|
- **⌃⌥⌘N does nothing:** `status` says whether another app holds it. macOS doesn't report clashes with its own
|
|
102
135
|
shortcuts.
|
|
136
|
+
- **"Send to chi" missing on images in Finder or Preview** after an upgrade: the Services cache still has the old
|
|
137
|
+
(text-only) entry; `/System/Library/CoreServices/pbs -update`, restart the helper, or log out and back in.
|
|
103
138
|
- **"No live sessions":** start one with `chi` in a terminal; `chi sessions list --live --scope=all` shows the same list.
|
data/docs/guardrails.md
CHANGED
|
@@ -38,6 +38,17 @@ once it closes. On a short terminal the list shrinks (the hint row, then the `in
|
|
|
38
38
|
lines, then the header go, then the options fold onto fewer rows). Once answered, one
|
|
39
39
|
line stays in the scrollback: `! execute: git push origin main → Allow once`.
|
|
40
40
|
|
|
41
|
+
An `edit` or `write` also shows the change it would make, computed without
|
|
42
|
+
touching the file: a `change: +3 −1` line in the question (`new file, 12 lines`;
|
|
43
|
+
`would fail: old text not found in …` when the edit can't apply), and the
|
|
44
|
+
unified diff itself. The web card shows the diff under the path (20 lines,
|
|
45
|
+
then "show all"); the terminals print it above the question, green and red,
|
|
46
|
+
40 lines at most (the rest is on the web). Binary files and files over 1 MB
|
|
47
|
+
say so instead of a diff. After the call runs, its row shows what really
|
|
48
|
+
changed: `diff +3 −1` under the row on the web (closed, it survives a
|
|
49
|
+
reload) and ` +3 −1` at the end of the terminal's tool line. The model never
|
|
50
|
+
sees these diffs.
|
|
51
|
+
|
|
41
52
|
Who answers:
|
|
42
53
|
|
|
43
54
|
- REPL (`chi --no-shared`, `-p` without `--non-interactive`): at the `? ` prompt.
|
data/docs/hooks.md
CHANGED
|
@@ -351,7 +351,7 @@ Notes:
|
|
|
351
351
|
- Ordering: bundle hooks fire by `(priority, bundle_name, hook_name)` (lower priority first), then plain `config.yml` hooks in registration order.
|
|
352
352
|
- Settings: a hook class with `initialize(settings = {})` gets the bundle's section of `config.yml` `bundles:` (see [Settings](#settings)).
|
|
353
353
|
- A bundle can also ship a `plugin.rb` whose `chi.on(event)` blocks are bundle hooks too, next to commands and tools; see [Plugins](plugins.md).
|
|
354
|
-
- Shipped bundles: `chi bundle install guardrails` (rules, see [Guardrails](guardrails.md#the-guardrails-bundle)) and `chi bundle install known-names` (a hook, see [Guardrails](guardrails.md#the-known-names-bundle)), `chi bundle install source-links` (a hook: announces source refs, see [The source-links bundle](#the-source-links-bundle)), `chi bundle install btw` (a plugin: `/btw`, see [Plugins](plugins.md#the-btw-bundle)), `chi bundle install mcp` (a plugin: tools from MCP servers, see [Plugins](plugins.md#the-mcp-bundle)) `chi bundle install loop-guard` (a plugin: breaks tool-call loops, see [Plugins](plugins.md#the-loop-guard-bundle))
|
|
354
|
+
- Shipped bundles: `chi bundle install guardrails` (rules, see [Guardrails](guardrails.md#the-guardrails-bundle)) and `chi bundle install known-names` (a hook, see [Guardrails](guardrails.md#the-known-names-bundle)), `chi bundle install source-links` (a hook: announces source refs, see [The source-links bundle](#the-source-links-bundle)), `chi bundle install btw` (a plugin: `/btw`, see [Plugins](plugins.md#the-btw-bundle)), `chi bundle install mcp` (a plugin: tools from MCP servers, see [Plugins](plugins.md#the-mcp-bundle)) `chi bundle install loop-guard` (a plugin: breaks tool-call loops, see [Plugins](plugins.md#the-loop-guard-bundle)), `chi bundle install check-in` (a plugin: checks on a long turn, see [Plugins](plugins.md#the-check-in-bundle)) and `chi bundle install skills` (a plugin: `/skill`, versions of skills, see [Plugins](plugins.md#the-skills-bundle)).
|
|
355
355
|
- Installing a bundle executes its hook code at `Engine` startup. Only install bundles you trust, as you would a gem. Hooks are **not** executed at install time (copy-only); they are `module_eval`'d at `Engine.new` inside per-bundle `Samagotchi::Bundles::<name>` namespaces (no top-level `require` collisions). Keep hook files side-effect-free at load time; do work in `#call` — top-level side effects (require, IO, `at_exit`, global assignment) run once per `Engine.new` (class redefinition is idempotent).
|
|
356
356
|
|
|
357
357
|
Lifecycle:
|
|
@@ -386,9 +386,11 @@ in a markdown link's label when the target names the same ref
|
|
|
386
386
|
and `[fix for JIRA-123](https://github.com/o/r/pull/9)` still link. A ref
|
|
387
387
|
glued to URL punctuation (`/browse/JIRA-1`, `?key=JIRA-1`, `JIRA-1/foo`) is
|
|
388
388
|
skipped too; `Ticket:JIRA-5` and `#JIRA-123` are ordinary plain text and do
|
|
389
|
-
link.
|
|
390
|
-
|
|
391
|
-
|
|
389
|
+
link. The line names each URL once (compared case-insensitively: `#12` and
|
|
390
|
+
`dm1try/samagotchi#12` may be one issue, and with `case_insensitive: true`
|
|
391
|
+
`JIRA-1` and `jira-1` are one ticket), in first-occurrence order, whatever
|
|
392
|
+
order the sources are configured in. With no sources configured the hook is a
|
|
393
|
+
silent no-op.
|
|
392
394
|
|
|
393
395
|
```yaml
|
|
394
396
|
bundles:
|
|
@@ -417,9 +419,8 @@ keeps the web links.
|
|
|
417
419
|
|
|
418
420
|
The `prefix:` form compiles to `\b<prefix>-(\d+)\b` and the URL is
|
|
419
421
|
`base_url` + the full ref text (`JIRA-123`). The `pattern:` form takes a
|
|
420
|
-
regex
|
|
421
|
-
|
|
422
|
-
`/i` flag. Past `max` refs the line ends with `… +N more`.
|
|
422
|
+
regex and a `url:` template with placeholders (below). `case_insensitive:
|
|
423
|
+
true` adds the `/i` flag. Past `max` refs the line ends with `… +N more`.
|
|
423
424
|
|
|
424
425
|
Each regex is compiled with a per-regex timeout (0.5 s, per match attempt),
|
|
425
426
|
so a catastrophic pattern is abandoned instead of hanging the turn: that
|
|
@@ -428,3 +429,84 @@ and the others still report. An entry with neither `prefix:` nor `pattern:`,
|
|
|
428
429
|
or a pattern that does not compile, is skipped with a warning at load. The
|
|
429
430
|
hook is `on_error: log`: a bug in it warns and the turn is unaffected. As with
|
|
430
431
|
every bundle hook, a running worker picks it up after its next start.
|
|
432
|
+
|
|
433
|
+
### Placeholders, and the project's own repo
|
|
434
|
+
|
|
435
|
+
A `url:` template (the `pattern:` form only; `base_url:` is always
|
|
436
|
+
`base_url` + the ref) can use:
|
|
437
|
+
|
|
438
|
+
| placeholder | value | when it can't be filled |
|
|
439
|
+
|---|---|---|
|
|
440
|
+
| `{match}` | the first capture group, else the whole ref | never: it falls back to the ref |
|
|
441
|
+
| `{1}` … `{9}` | a numbered capture group | the group didn't take part: the ref is **not linked** |
|
|
442
|
+
| `{name}` | a named capture group `(?<name>…)` | the group didn't take part: **not linked** |
|
|
443
|
+
| `{repo}`, `{host}` | the named group `repo` / `host` when the pattern has one and it took part; else the project's git remote | neither: **not linked** |
|
|
444
|
+
|
|
445
|
+
A ref with a placeholder that can't be filled is left out of both the line
|
|
446
|
+
and the answer: no URL is built with a hole in it (another source on the same
|
|
447
|
+
ref can still link it). A `{word}` or `{N}` that is none of the above (not a
|
|
448
|
+
group of the pattern, or `{7}` in a two-group pattern) stays as literal text,
|
|
449
|
+
with one warning when the source is loaded. Every value is percent-encoded
|
|
450
|
+
(everything outside `A-Za-z0-9-._~`, `/` included), except that `{repo}`
|
|
451
|
+
keeps its `/` between segments (GitLab's `group/sub/proj`); a `{repo}` with an
|
|
452
|
+
empty, `.` or `..` segment counts as unfilled.
|
|
453
|
+
|
|
454
|
+
So one source links both `#12` in this project and a cross-repo ref:
|
|
455
|
+
|
|
456
|
+
```yaml
|
|
457
|
+
bundles:
|
|
458
|
+
source-links:
|
|
459
|
+
sources:
|
|
460
|
+
- name: GitHub
|
|
461
|
+
# `#12` → this project's repo; `owner/repo#12` → that repo
|
|
462
|
+
pattern: '(?<![\w/&])(?:(?<repo>[A-Za-z0-9][\w-]*/[\w.-]*\w))?#(?<num>\d+)\b'
|
|
463
|
+
url: 'https://github.com/{repo}/issues/{num}'
|
|
464
|
+
remote_host: github.com
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
```
|
|
468
|
+
sources: GitHub #12 → https://github.com/dm1try/samagotchi/issues/12, GitHub rails/rails#5 → https://github.com/rails/rails/issues/5
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
GitHub redirects `/issues/N` to `/pull/N` and back, so one URL covers issues
|
|
472
|
+
and pull requests. The lookbehind keeps `{`, `x/#1` and a partial
|
|
473
|
+
`b/c#1` inside `a/b/c#1` out; code and URLs are skipped as always. `PR #12`
|
|
474
|
+
links, `PR#12` doesn't (a `#` right after a letter). The pattern is loose on
|
|
475
|
+
purpose: a bare `#\d+` also matches "step #2", and `and/or#5` or `TCP/IP#3`
|
|
476
|
+
read as qualified refs. A stricter variant wants `PR #`, `issue #` or a
|
|
477
|
+
qualified ref:
|
|
478
|
+
|
|
479
|
+
```yaml
|
|
480
|
+
pattern: '(?<![\w/&])(?:(?<repo>[A-Za-z0-9][\w-]*/[\w.-]*\w)#|\b(?:PR|[Ii]ssue) #)(?<num>\d+)\b'
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
**Where `{repo}` and `{host}` come from.** Without a named group that took
|
|
484
|
+
part, they come from `git remote get-url <remote>` run in the session's
|
|
485
|
+
working directory (the worker's; the in-process REPL's is the terminal's
|
|
486
|
+
current directory). `remote:` picks the remote, default `origin` — a fork
|
|
487
|
+
sets `remote: upstream`. Git applies `insteadOf` rewrites and includes, and a
|
|
488
|
+
worktree reports its main repo's remote. The URL forms understood are
|
|
489
|
+
`https://`, `http://`, `ssh://`, `git://` (credentials and port dropped) and
|
|
490
|
+
scp-like `[user@]host:owner/repo`; the repo is the path without a trailing
|
|
491
|
+
`.git` or `/`. A local path, `file://`, no git, no repo or no such remote
|
|
492
|
+
leaves the ref unlinked, silently (a debug log line only). Git is asked only
|
|
493
|
+
when a hit needs it — a JIRA source or a qualified ref never runs it — and
|
|
494
|
+
the answer, even "none", is remembered for the worker's life: a remote
|
|
495
|
+
changed mid-session counts after the worker's next start.
|
|
496
|
+
|
|
497
|
+
`remote_host:` (a host or a list, compared case-insensitively) applies the
|
|
498
|
+
remote-derived links only when the remote's host is one of them, so a
|
|
499
|
+
`https://github.com/{repo}/…` template never points a GitLab project's `#12`
|
|
500
|
+
at github.com. A qualified ref is linked whatever the local remote is. An SSH
|
|
501
|
+
alias (`git@github-work:o/r.git` from a multi-account `~/.ssh/config`) or an
|
|
502
|
+
`insteadOf` mirror reports its own host; list it too:
|
|
503
|
+
`remote_host: [github.com, github-work]`.
|
|
504
|
+
|
|
505
|
+
Two Ruby regex notes. With named groups in a pattern, a plain `(…)` doesn't
|
|
506
|
+
capture and gets no number, so `{1}` is the first *named* group, and so is
|
|
507
|
+
`{match}` (in the example above `{match}` is the repo part): use either named
|
|
508
|
+
or numbered groups in one pattern, and named placeholders with named groups.
|
|
509
|
+
And a pattern with a `host` (or `repo`) group lets the model's text choose the
|
|
510
|
+
link's domain (or repo): the escaping rules out URL injection, but the choice
|
|
511
|
+
of the target is the model's.
|
|
512
|
+
|
data/docs/memory.md
CHANGED
|
@@ -34,6 +34,46 @@ names of a comma list are read). Its file and index line are untouched.
|
|
|
34
34
|
These startup index reads are harness-injected context assembly and are not
|
|
35
35
|
rendered as `tool>` activity lines.
|
|
36
36
|
|
|
37
|
+
## Skills
|
|
38
|
+
|
|
39
|
+
A **skill** is a memory named `skill_<name>` that holds the steps of a
|
|
40
|
+
repeatable task you and chi did together: a release, a deploy, a data fix.
|
|
41
|
+
chi is told about skills by the system bundle (`identity.md`, every turn, and
|
|
42
|
+
`memory_guide.md`), so this works without installing anything:
|
|
43
|
+
|
|
44
|
+
- **Saving.** Say "let's memorize this" or "save this as a skill" after the
|
|
45
|
+
task, and chi writes the skill with `memory_write` (project scope; system
|
|
46
|
+
when you ask, or when it isn't about this project) and shows it. On a vague
|
|
47
|
+
one ("I like how we did that") it may ask first.
|
|
48
|
+
- **Following.** The skill's index line (the `description:` of its
|
|
49
|
+
`memory_write`, "Release a new version of this repo: …") is in every
|
|
50
|
+
prompt, so next time chi reads the skill and follows it.
|
|
51
|
+
- **Updating.** When a step turned out different (a renamed script, an extra
|
|
52
|
+
step), chi fixes the skill in the same turn: those steps changed, the rest
|
|
53
|
+
kept, a dated Changelog line added. No confirmation.
|
|
54
|
+
|
|
55
|
+
A skill is plain Markdown, no frontmatter:
|
|
56
|
+
|
|
57
|
+
```markdown
|
|
58
|
+
# Skill: release
|
|
59
|
+
|
|
60
|
+
## Steps
|
|
61
|
+
1. Run `scripts/verify.sh`; stop if it fails.
|
|
62
|
+
2. …
|
|
63
|
+
## Gotchas
|
|
64
|
+
- …
|
|
65
|
+
## Changelog
|
|
66
|
+
- 2026-09-29 created
|
|
67
|
+
- 2026-09-30 step 1: check.sh was renamed to verify.sh
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
It is a memory like any other: `memory_read`, `chi --mute skill_release`,
|
|
71
|
+
`chi bundle build` share it. The optional `skills` bundle (`chi bundle install
|
|
72
|
+
skills`, [docs/plugins.md](plugins.md#the-skills-bundle)) adds `/skill save`,
|
|
73
|
+
`/skill list`, `/skill show`, `/skill diff`, keeps older versions with a
|
|
74
|
+
one-line diff after each update, and nudges a model that skips a failing
|
|
75
|
+
step instead of fixing the skill.
|
|
76
|
+
|
|
37
77
|
## Bundles that need outside commands
|
|
38
78
|
|
|
39
79
|
A bundle's memory can rely on a command chi doesn't ship, such as a GitHub
|
data/docs/plugins.md
CHANGED
|
@@ -814,6 +814,56 @@ with a line and carry on unchanged.
|
|
|
814
814
|
| `/checkin mode ask` / `nudge` / `notify` | the mode |
|
|
815
815
|
| `/checkin nudge` / `later` / `stop` | the card's actions, also by hand |
|
|
816
816
|
|
|
817
|
+
## The skills bundle
|
|
818
|
+
|
|
819
|
+
`chi bundle install skills` installs the bundle shipped with chi
|
|
820
|
+
(`lib/samagotchi/bundles/skills/plugin.rb`): one anytime command, `chi.on`
|
|
821
|
+
hooks, `ctx.sessions.send`, `ctx.notify` and `event[:steer]`. It has no memory
|
|
822
|
+
file; skills themselves work without it ([docs/memory.md](memory.md#skills)).
|
|
823
|
+
|
|
824
|
+
| | |
|
|
825
|
+
|---|---|
|
|
826
|
+
| `/skill save [name] [--system]` | sends this session a request to save what was just done as `skill_<name>` (chi picks a name when none is given), project scope unless `--system`. The request holds the skill's shape, so the result is the same with a model that never read the memory guide. It runs as a turn; sent while a turn runs, it joins that turn at its next step (the request says to finish the task first). An existing skill is updated. In a `--no-shared` REPL, which takes no messages, the command shows the request to send yourself |
|
|
827
|
+
| `/skill list` | the `skill_*` memories of both scopes, with the date and description from the index |
|
|
828
|
+
| `/skill show <name>` | one skill as saved (project first, as `memory_read` looks) |
|
|
829
|
+
| `/skill diff <name> [N]` | the skill now against its N-th newest older version (default 1: before the last change), unified |
|
|
830
|
+
|
|
831
|
+
**History.** Before `memory_write`, `write` or `edit` changes a
|
|
832
|
+
`skill_<name>.md` in a memories folder, the file as it was is kept under
|
|
833
|
+
`$XDG_STATE_HOME/samagotchi/plugins/skills/history/<scope>/<name>/` (`system`,
|
|
834
|
+
or `project-<project folder>`), the newest `history_keep`. It is state, not a
|
|
835
|
+
memory: `chi bundle build` and a synced `~/.config` never see it. After the
|
|
836
|
+
call a line says what happened:
|
|
837
|
+
|
|
838
|
+
```
|
|
839
|
+
skills> skill release saved (project, 14 lines)
|
|
840
|
+
skills> skill release updated (+2 −1): 1. Run `scripts/verify.sh`; stop if it fails. · /skill diff release
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
The line is the file on disk changing, whatever the tool answered; a denied
|
|
844
|
+
write shows nothing.
|
|
845
|
+
|
|
846
|
+
**The nudge** (`nudge: true`). Some models, finding a skill's step broken,
|
|
847
|
+
skip it and go on without fixing the skill. In a turn that read a skill
|
|
848
|
+
(`memory_read` of a `skill_*` name, or `read` of its file), the first failing
|
|
849
|
+
tool call after it (an `execute` that exited non-zero, a tool error) steers
|
|
850
|
+
the model once: *"A step of skill release failed. Find out why before skipping
|
|
851
|
+
it; if the skill is out of date, fix it now: memory_write the whole skill,
|
|
852
|
+
its title and every section as they were, that step fixed, a Changelog line
|
|
853
|
+
added."* If the turn ends with a failed step and the
|
|
854
|
+
skill not rewritten, one line says so: `skill release was followed, a step
|
|
855
|
+
failed, the skill wasn't updated`. A failure unrelated to the skill (a test
|
|
856
|
+
meant to fail) can set it off too: once per turn, and only after a skill was
|
|
857
|
+
read.
|
|
858
|
+
|
|
859
|
+
```yaml
|
|
860
|
+
# config.yml
|
|
861
|
+
bundles:
|
|
862
|
+
skills:
|
|
863
|
+
history_keep: 20 # older versions kept per skill
|
|
864
|
+
nudge: true # steer once when a followed skill's step fails
|
|
865
|
+
```
|
|
866
|
+
|
|
817
867
|
## Shutdown
|
|
818
868
|
|
|
819
869
|
When the REPL exits, or a session's worker exits (an idle exit, `/exit`, a
|
data/docs/releasing.md
CHANGED
|
@@ -12,16 +12,16 @@ can run the whole release; the user approves the notes before the tag and the
|
|
|
12
12
|
and `lib/samagotchi/bundles/system/manifest.yml` are bumped together (a spec
|
|
13
13
|
and `rake release:check` enforce it). The system bundle upgrades itself when
|
|
14
14
|
chi starts.
|
|
15
|
-
- **Every other shipped bundle** (btw, guardrails, known-names,
|
|
16
|
-
mcp) has its own semver in its
|
|
17
|
-
a `requires_chi:` line. They never upgrade by themselves: users run
|
|
18
|
-
`chi bundle upgrade NAME`. So:
|
|
15
|
+
- **Every other shipped bundle** (btw, check-in, guardrails, known-names,
|
|
16
|
+
loop-guard, mcp, skills, source-links) has its own semver in its
|
|
17
|
+
`manifest.yml` and, when it needs a newer chi, a `requires_chi:` line. They never upgrade by themselves: users run
|
|
18
|
+
`chi update` (all of them, with the gem) or `chi bundle upgrade NAME`. So:
|
|
19
19
|
- a change to a bundle's files bumps that bundle's `version:`
|
|
20
20
|
(`rake bundles:check` fails otherwise, once there's a tag to compare with);
|
|
21
21
|
- a bundle that uses something new in chi raises its `requires_chi` in the
|
|
22
22
|
same change; `release:bump` never touches `requires_chi`;
|
|
23
|
-
- the release notes
|
|
24
|
-
bundle
|
|
23
|
+
- the release notes say "run `chi update`" and name the bundles whose
|
|
24
|
+
version moved (what changed in each), not per-bundle upgrade steps.
|
|
25
25
|
- Pre-1.0: config and commands may change in a minor version (0.2 → 0.3); a
|
|
26
26
|
patch version (0.2.0 → 0.2.1) is fixes only.
|
|
27
27
|
|
|
@@ -58,9 +58,9 @@ The agent does each step and stops where the user has to say yes.
|
|
|
58
58
|
1. **Start from main, up to date and green.** `git checkout main && git pull`;
|
|
59
59
|
CI on main is green.
|
|
60
60
|
2. **Draft the notes.** `bundle exec rake release:draft_changelog`, then edit
|
|
61
|
-
`## [Unreleased]` in CHANGELOG.md into short user-facing lines.
|
|
62
|
-
`chi
|
|
63
|
-
last tag (`git diff vPREV -- lib/samagotchi/bundles/*/manifest.yml`).
|
|
61
|
+
`## [Unreleased]` in CHANGELOG.md into short user-facing lines. End with
|
|
62
|
+
"Update with `chi update`", naming the bundles whose version moved since
|
|
63
|
+
the last tag (`git diff vPREV -- lib/samagotchi/bundles/*/manifest.yml`).
|
|
64
64
|
Pick the version: fixes only → patch, anything else → minor.
|
|
65
65
|
3. **The user approves the notes and the version.** Show them the section.
|
|
66
66
|
4. **Bump.** `bundle exec rake "release:bump[X.Y.Z]"`, review `git diff`.
|
|
@@ -130,9 +130,12 @@ next patch version.
|
|
|
130
130
|
The suite runs the same on Linux CI as on macOS, with no CI-only skips. A few
|
|
131
131
|
things differ there, and a new spec that trips on them fails only on CI:
|
|
132
132
|
|
|
133
|
-
- `CI` set marks every new session a test run, and
|
|
134
|
-
|
|
135
|
-
`test_run: false`.
|
|
133
|
+
- `CI` set marks every new session a test run, and `list_sessions` and
|
|
134
|
+
friends leave test runs out. A spec that lists sessions makes them with
|
|
135
|
+
`test_run: false`. `chi sessions list --live`/`--cwd`/`--format` and
|
|
136
|
+
`chi note --all` show test runs when they run as one (`CI` set counts): a
|
|
137
|
+
spec checking that they are hidden unsets `CI` or stubs
|
|
138
|
+
`Session.test_session_env?`.
|
|
136
139
|
- The gems live under `vendor/bundle`: a child `ruby` started with a bare env
|
|
137
140
|
(`unsetenv_others: true`) needs `GEM_HOME`/`GEM_PATH` to find nokogiri.
|
|
138
141
|
- Ruby 3.3's zlib raises `Zlib::BufError` when a thread interrupt lands in a
|
data/docs/sessions.md
CHANGED
|
@@ -56,7 +56,22 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
|
|
|
56
56
|
|
|
57
57
|
**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.
|
|
58
58
|
|
|
59
|
-
`--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. `--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`).
|
|
59
|
+
`--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`).
|
|
60
|
+
|
|
61
|
+
**Ordering:**
|
|
62
|
+
|
|
63
|
+
- `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.
|
|
64
|
+
- 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; "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. 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}], warning?}`; 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 command line (`/model …`) still starts the session as its `prompt`. The browser remembers the last choice; a running session's model is in the info bar and changes only with `/model`.
|
|
65
|
+
- 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).
|
|
66
|
+
- 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`). Unit-tested via `npm test` (`node --test spec/web/public/*.test.js`).
|
|
67
|
+
- 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).
|
|
68
|
+
|
|
69
|
+
**Test-session hygiene:**
|
|
70
|
+
|
|
71
|
+
- New sessions set `test_run:true` when `SAMAGOTCHI_ENV=test` or `RACK_ENV=test` or `CI` is set (explicit flag, `metadata_version` 2). Old sessions without the flag load as `test_run:false`.
|
|
72
|
+
- Test runs are tagged and obey the same retention. `chi sessions clean` deletes every test session whatever its age (`--days N`: only those older than N days); a live worker or a `keep_status` status still keeps one. `chi sessions prune --test-only` applies the usual age and count rules to test sessions only.
|
|
73
|
+
- A `chi scratch` session (`"scratch": true`) is deleted when its REPL ends; one a killed process left behind is deleted by the next sweep, `prune` or `clean`, whatever its age or status, once nobody owns it. `list` shows it as `[scratch]` (every text form; `scratch: true` in `--format json`); `chi web` never shows one. See [CLI: Scratch sessions](cli.md#scratch-sessions).
|
|
74
|
+
- For ad-hoc manual QA use `SAMAGOTCHI_ENV=test XDG_STATE_HOME=/tmp/chi-test-$USER chi ...` to isolate from real state; the flag also marks sessions that `chi web` or an attached `chi` spawn (their workers inherit the environment), so `clean` finds them if they land in the real state.
|
|
60
75
|
|
|
61
76
|
## Context notes
|
|
62
77
|
|
|
@@ -120,8 +135,9 @@ cancel during its thinking, it said it had never been asked.
|
|
|
120
135
|
`chi send` is the other half of `chi note`: the text goes in as your message, the same as typing it in the attached terminal or the web composer, so a turn runs.
|
|
121
136
|
|
|
122
137
|
```sh
|
|
123
|
-
chi send [-m TEXT] (ID|PREFIX)...
|
|
138
|
+
chi send [-m TEXT] [--image PATH]... (ID|PREFIX)...
|
|
124
139
|
chi send -m "is this the same bug?" 3fa2 # a message
|
|
140
|
+
chi send --image shot.png -m "why is this red?" 3fa2 # with an image
|
|
125
141
|
pbpaste | chi send -m "is this the same bug?" 3fa2 # the clipboard quoted above the message
|
|
126
142
|
pbpaste | chi send 3fa2 # the clipboard is the message
|
|
127
143
|
```
|
|
@@ -130,7 +146,8 @@ pbpaste | chi send 3fa2 # the clipboard is the messa
|
|
|
130
146
|
- It goes through the same path as the web composer (`SessionManager.deliver_turn`): the worker's Bridge when it is up, so every attached UI shows it as your message (client id `cli:send`, shown like any user message); the input file when the worker is on its way out. During a running turn it is merged into that turn at the next step, like a message typed then.
|
|
131
147
|
- A session with no worker gets one started (like `--attach` or the web composer). A session open in a plain REPL (`--no-shared`) refuses it. Only sessions on this machine: Bridges listen on 127.0.0.1.
|
|
132
148
|
- Fire and forget: it returns once the message is queued and never prints the answer; that shows in whatever is attached. A guardrail "ask" waits for a UI to answer it, so with nothing attached the turn stalls there until one attaches (`chi --attach ID`).
|
|
133
|
-
-
|
|
149
|
+
- `--image PATH` (repeatable, up to 20) sends images with the message, as the web composer's chips do. Each file is read once before anything is sent (bmp, tiff and heic converted to png, large ones downscaled, as for `@path`); a missing file or one that isn't an image is a usage error (exit 2) and nothing is sent. Each session gets its own copy in `<session>/images/`. Text is still required (`-m` or stdin; context alone counts), also with `--wait`. The session's model must see images: one known not to (the host's model list says text-only, `vision: false` in `models:` or `hosts:`, a native llama.cpp without a vision model) is refused before anything is sent, with the reason (`<id> refused: gemma can't take images (…); send text only or switch the model (/model)`); the other sessions of the same send still get it, and the exit is 1. With `--new` nothing is started. When chi can't tell (a native host whose `/props` doesn't answer, a list that doesn't say), the message is sent as before and a text-only model fails the turn in the session. A running turn doesn't take images mid-turn: the message runs as the next turn, and the line says `(runs after the current turn)`. With `--new` the session starts idle with the message as its preview, then the message goes in as its first turn once its worker is up (`<id> started with 1 image`); if the worker doesn't come up in 5 s the session is kept, with its id on the `failed:` line.
|
|
150
|
+
- One line per session: `sent`, `sent with 2 images`, `sent (the running turn picks it up)`, `sent (started its worker)`, `refused: …` or `failed: …`. Exit 0 when all were sent, 1 when any was refused, failed or not found, 2 for a usage error. There is no `--all`.
|
|
134
151
|
|
|
135
152
|
The Automator action above works for messages too: swap its last line for `pbpaste | "$chi" send -m "what do you make of this?" $(print -r -- "$picked" | cut -f1)`.
|
|
136
153
|
|
|
@@ -164,18 +181,3 @@ The `delegate` tool hands a task to a **child session**: an ordinary chi session
|
|
|
164
181
|
- 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.
|
|
165
182
|
- 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.
|
|
166
183
|
- The link is the session's `parent_id`, written before the worker starts, so a respawn keeps it; `GET /api/sessions` and `/api/sessions/:id` carry it, and `SessionManager.children_of` lists a parent's children.
|
|
167
|
-
|
|
168
|
-
**Ordering:**
|
|
169
|
-
|
|
170
|
-
- `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.
|
|
171
|
-
- 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; "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. The start page's model picker (bottom left of the composer, `host:model ▾`) chooses the model a new chat starts on: `GET /api/models` lists the hosts' models as chi spells them (`{default, models: [{name, host, id}], warning?}`; 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 command line (`/model …`) still starts the session as its `prompt`. The browser remembers the last choice; a running session's model is in the info bar and changes only with `/model`.
|
|
172
|
-
- 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).
|
|
173
|
-
- 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`). Unit-tested via `npm test` (`node --test spec/web/public/*.test.js`).
|
|
174
|
-
- 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).
|
|
175
|
-
|
|
176
|
-
**Test-session hygiene:**
|
|
177
|
-
|
|
178
|
-
- New sessions set `test_run:true` when `SAMAGOTCHI_ENV=test` or `RACK_ENV=test` or `CI` is set (explicit flag, `metadata_version` 2). Old sessions without the flag load as `test_run:false`.
|
|
179
|
-
- Test runs are tagged and obey the same retention. `chi sessions clean` deletes every test session whatever its age (`--days N`: only those older than N days); a live worker or a `keep_status` status still keeps one. `chi sessions prune --test-only` applies the usual age and count rules to test sessions only.
|
|
180
|
-
- A `chi scratch` session (`"scratch": true`) is deleted when its REPL ends; one a killed process left behind is deleted by the next sweep, `prune` or `clean`, whatever its age or status, once nobody owns it. `list` shows it as `[scratch]` (every text form; `scratch: true` in `--format json`); `chi web` never shows one. See [CLI: Scratch sessions](cli.md#scratch-sessions).
|
|
181
|
-
- For ad-hoc manual QA use `SAMAGOTCHI_ENV=test XDG_STATE_HOME=/tmp/chi-test-$USER chi ...` to isolate from real state; the flag also marks sessions that `chi web` or an attached `chi` spawn (their workers inherit the environment), so `clean` finds them if they land in the real state.
|
|
@@ -243,14 +243,13 @@ module Samagotchi
|
|
|
243
243
|
|
|
244
244
|
section = data["default"]
|
|
245
245
|
value = section.is_a?(Hash) ? section["model"] : nil
|
|
246
|
-
value ||= data[ConfigFile::DEFAULT_MODEL_KEY]
|
|
247
246
|
value.to_s.strip.empty? ? nil : value.to_s.strip
|
|
248
247
|
end
|
|
249
248
|
|
|
250
249
|
# Whether bare model names go somewhere today: a default.model or a
|
|
251
250
|
# server: section. Then a new hosts: block keeps that route as `default`.
|
|
252
251
|
def routed?
|
|
253
|
-
configured_default_model || data.key?("server")
|
|
252
|
+
configured_default_model || data.key?("server")
|
|
254
253
|
end
|
|
255
254
|
|
|
256
255
|
# The `default` hosts entry chi derives from server.* when the file has
|
|
@@ -187,9 +187,6 @@ module Samagotchi
|
|
|
187
187
|
"Cache-Control: no-cache\r\n" \
|
|
188
188
|
"Connection: keep-alive\r\n" \
|
|
189
189
|
"X-Accel-Buffering: no\r\n" \
|
|
190
|
-
"Access-Control-Allow-Origin: *\r\n" \
|
|
191
|
-
"Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n" \
|
|
192
|
-
"Access-Control-Allow-Headers: Content-Type, Last-Event-ID\r\n" \
|
|
193
190
|
"\r\n"
|
|
194
191
|
)
|
|
195
192
|
io.flush
|
|
@@ -146,6 +146,7 @@ module Samagotchi
|
|
|
146
146
|
part = { kind: "tool", iteration: event[:iteration], call_index: event[:call_index],
|
|
147
147
|
tool: event[:tool], params: event[:params], status: "running" }
|
|
148
148
|
part[:label] = event[:label] if event[:label]
|
|
149
|
+
part[:title] = event[:title] if event[:title]
|
|
149
150
|
parts << part
|
|
150
151
|
when :tool_call_completed
|
|
151
152
|
tool = parts.reverse_each.find do |part|
|
|
@@ -159,6 +160,7 @@ module Samagotchi
|
|
|
159
160
|
tool[:output] = capped ? output[0, @max_output_chars] : output.dup
|
|
160
161
|
tool[:output_truncated] = capped || !!event[:output_truncated]
|
|
161
162
|
tool[:images] = event[:images] if event[:images]
|
|
163
|
+
tool[:diff] = event[:diff] if event[:diff]
|
|
162
164
|
when :pending_input_merged
|
|
163
165
|
# A steer-only merge (count 0) has no user part: the origins stay
|
|
164
166
|
# for the user lines' own merge.
|
data/lib/samagotchi/bridge.rb
CHANGED
|
@@ -19,6 +19,7 @@ require_relative "engine"
|
|
|
19
19
|
require_relative "session_commands"
|
|
20
20
|
require_relative "image_store"
|
|
21
21
|
require_relative "log"
|
|
22
|
+
require_relative "version"
|
|
22
23
|
|
|
23
24
|
module Samagotchi
|
|
24
25
|
# Bridge is an optional HTTP transport that lets an external web / desktop
|
|
@@ -304,8 +305,8 @@ module Samagotchi
|
|
|
304
305
|
if request[:too_large]
|
|
305
306
|
write_json(io, 413, { "Connection" => "close" }, { error: "too_large", detail: "request body over #{MAX_BODY_BYTES} bytes" })
|
|
306
307
|
break
|
|
307
|
-
elsif
|
|
308
|
-
write_json(io,
|
|
308
|
+
elsif browser_request?(headers)
|
|
309
|
+
write_json(io, 403, nil, { error: "cross_origin", detail: "the bridge answers chi's own clients only" })
|
|
309
310
|
elsif (m = stream_match(request[:path])) && method == "GET"
|
|
310
311
|
cursor = reconnect_cursor(headers, request[:query])
|
|
311
312
|
Log.debug(:bridge, "stream", method: method, path: request[:path], client_id: stream_client_id(request[:query]))
|
|
@@ -343,7 +344,7 @@ module Samagotchi
|
|
|
343
344
|
payload, status, body = handle_snapshot(m[1])
|
|
344
345
|
write_json(io, status, payload, body)
|
|
345
346
|
else
|
|
346
|
-
write_json(io, 404, { "Allow" => "GET, POST
|
|
347
|
+
write_json(io, 404, { "Allow" => "GET, POST" },
|
|
347
348
|
{ error: "not_found", path: request[:path] })
|
|
348
349
|
end
|
|
349
350
|
|
|
@@ -413,12 +414,18 @@ module Samagotchi
|
|
|
413
414
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
414
415
|
end
|
|
415
416
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
417
|
+
LOOPBACK_NAMES = %w[127.0.0.1 [::1] localhost].freeze
|
|
418
|
+
|
|
419
|
+
# Only chi's Ruby clients talk to the Bridge, and they send no Origin and
|
|
420
|
+
# no Sec-Fetch-Site. A browser page always sends one of them (another
|
|
421
|
+
# website's text/plain POST needs no preflight), and a DNS-rebound page
|
|
422
|
+
# also has a foreign Host. Headers are the raw ones, lowercased.
|
|
423
|
+
def browser_request?(headers)
|
|
424
|
+
return true if headers.key?("origin")
|
|
425
|
+
return true if headers.key?("sec-fetch-site") && headers["sec-fetch-site"].downcase != "none"
|
|
426
|
+
|
|
427
|
+
host = headers["host"].to_s
|
|
428
|
+
!host.empty? && !LOOPBACK_NAMES.include?(host.downcase.sub(/:\d*\z/, ""))
|
|
422
429
|
end
|
|
423
430
|
|
|
424
431
|
def stream_match(path)
|
|
@@ -892,8 +899,7 @@ module Samagotchi
|
|
|
892
899
|
"Content-Type" => "application/json",
|
|
893
900
|
"Content-Length" => data.bytesize.to_s,
|
|
894
901
|
"Connection" => "close",
|
|
895
|
-
"Cache-Control" => "no-store"
|
|
896
|
-
"Access-Control-Allow-Origin" => "*"
|
|
902
|
+
"Cache-Control" => "no-store"
|
|
897
903
|
}.merge(extra_headers)
|
|
898
904
|
|
|
899
905
|
io.write("HTTP/1.1 #{status} #{reason}\r\n")
|
|
@@ -961,7 +967,9 @@ module Samagotchi
|
|
|
961
967
|
"port" => @port,
|
|
962
968
|
"bind" => @bind,
|
|
963
969
|
"session_id" => @session_id,
|
|
964
|
-
"started_at" => Time.now.iso8601(3)
|
|
970
|
+
"started_at" => Time.now.iso8601(3),
|
|
971
|
+
# The chi this worker runs (chi update reports older ones).
|
|
972
|
+
"version" => Samagotchi::VERSION
|
|
965
973
|
}
|
|
966
974
|
record["input_format"] = @input_format if @input_format
|
|
967
975
|
path = File.join(session_dir, SIDECAR_FILE)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: skills
|
|
3
|
+
version: 0.1.0
|
|
4
|
+
scope: system
|
|
5
|
+
description: "Skills (skill_<name> memories, the steps of a task done together): /skill save, list, show and diff; older versions kept with a short diff line on every update; a nudge when a followed skill's step fails"
|
|
6
|
+
trust_level: reviewed
|
|
7
|
+
plugin:
|
|
8
|
+
file: plugin.rb
|
|
9
|
+
sha256: sha256:76758d41e75db107e0a6f7ce35b9eac4944052746af16aeba30f38210cc436dc
|
|
10
|
+
requires_chi: ">= 0.4.0"
|