samagotchi 0.19.0 → 0.20.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 +83 -1
- data/docs/architecture.md +6 -0
- data/docs/cli.md +14 -3
- data/docs/configuration.md +4 -1
- data/docs/guardrails.md +92 -14
- data/docs/plugins.md +5 -0
- data/docs/sessions.md +1 -1
- data/lib/samagotchi/bootstrap/config_writer.rb +14 -3
- data/lib/samagotchi/bootstrap_command.rb +26 -5
- data/lib/samagotchi/bundle_command.rb +22 -4
- data/lib/samagotchi/bundles/check-in/manifest.yml +2 -2
- data/lib/samagotchi/bundles/check-in/plugin.rb +14 -5
- data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +24 -5
- data/lib/samagotchi/bundles/guardrails/manifest.yml +5 -4
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/plugin.rb +18 -4
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/commands/registry.rb +4 -3
- data/lib/samagotchi/config.rb +3 -0
- data/lib/samagotchi/context_usage.rb +3 -3
- data/lib/samagotchi/engine.rb +16 -2
- data/lib/samagotchi/guardrail_wiring.rb +2 -1
- data/lib/samagotchi/guardrails/approval.rb +4 -3
- data/lib/samagotchi/guardrails/approvals.rb +11 -5
- data/lib/samagotchi/guardrails/context.rb +16 -9
- data/lib/samagotchi/guardrails/parent_approvals.rb +10 -4
- data/lib/samagotchi/guardrails/read_only_shell.rb +322 -0
- data/lib/samagotchi/guardrails/rm_targets.rb +86 -0
- data/lib/samagotchi/guardrails/rules.rb +69 -5
- data/lib/samagotchi/guardrails/shell_git_dirs.rb +5 -137
- data/lib/samagotchi/guardrails/shell_lex.rb +164 -0
- data/lib/samagotchi/guardrails/shell_paths.rb +145 -0
- data/lib/samagotchi/guardrails/targets.rb +65 -4
- data/lib/samagotchi/guardrails.rb +2 -0
- data/lib/samagotchi/hooks/registry.rb +6 -2
- data/lib/samagotchi/idle_recap.rb +7 -2
- data/lib/samagotchi/llm/usage.rb +1 -1
- data/lib/samagotchi/memory_bundle/builder.rb +1 -5
- data/lib/samagotchi/memory_bundle/installer.rb +46 -13
- data/lib/samagotchi/memory_bundle/shipped_update.rb +3 -1
- data/lib/samagotchi/memory_bundle/status.rb +12 -4
- data/lib/samagotchi/memory_bundle/uninstaller.rb +1 -5
- data/lib/samagotchi/memory_paths.rb +6 -0
- data/lib/samagotchi/self_report.rb +38 -2
- data/lib/samagotchi/session.rb +35 -6
- data/lib/samagotchi/session_commands.rb +13 -2
- data/lib/samagotchi/session_metrics.rb +224 -19
- data/lib/samagotchi/terminal_ui/formatting.rb +52 -1
- data/lib/samagotchi/token_usage.rb +57 -41
- data/lib/samagotchi/tool_runner.rb +21 -2
- data/lib/samagotchi/tools/execute.rb +9 -8
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +19 -0
- data/lib/samagotchi/web/public/app.js +34 -15
- data/lib/samagotchi/web/public/card.js +34 -2
- data/lib/samagotchi/web/public/ctx.js +59 -0
- data/lib/samagotchi/web/public/format.js +11 -0
- data/lib/samagotchi/web/public/index.html +26 -2
- data/lib/samagotchi/web/public/model_pick.js +6 -2
- data/lib/samagotchi/web/public/model_picker.js +2 -1
- data/lib/samagotchi/web/public/question_card.js +2 -1
- data/lib/samagotchi/web/public/timing.js +6 -2
- data/lib/samagotchi/web/public/turn_events.js +1 -1
- data/lib/samagotchi/web/session_summary.rb +6 -2
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c3b1a6860a8c22ed75259438c755633283385c12ffbc28a8fcb4c817f59114c5
|
|
4
|
+
data.tar.gz: 24f7c45ebe336077bc2d9a1e670d3313b364c48d5b28aef3624593705236f29e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9f1d9dec24344b223170728f80629a76d1db18697f752b2ce7135cf3569428a902f1cc18589a928184f7809bcbe3a20289281cd4cae6324bdd3f5bb1192cf860
|
|
7
|
+
data.tar.gz: 8bf9a7cf6dbaac98a7583987be61d98e06ddd3ab6a0b6f36423ae41a6aa530ef42479fa28139ad89b8253e61edce1a57013dd27d0a9bcbf5b31c080cea6f8801
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,87 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.20.0] - 2026-10-03
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `guardrails.mode` (config.yml only): `auto`, the new default, or `strict`. Rules can be tagged `modes: [strict]`
|
|
16
|
+
to vote only in strict mode; a rule without `modes:` (every rule you wrote in config.yml) votes in both.
|
|
17
|
+
`/guardrails` shows the mode and marks the rules it leaves out `(strict only)`. See docs/guardrails.md#modes.
|
|
18
|
+
- New guardrail rule keys: `skip_read_only: true` (a shell command that only reads, such as `ls`, `cat`, `rg`,
|
|
19
|
+
`sed -n` or `git log`, doesn't match), `touches: chi_dirs` (a shell command names a path in chi's real config,
|
|
20
|
+
hooks, approvals or bundles dir, or a `.git/hooks`) and `rm: outside_tmp` (an `rm -rf` reaches outside the tmp
|
|
21
|
+
dirs). A config.yml rule that uses one fails closed on an older chi.
|
|
22
|
+
- Model speed and token stats. `/stats` has a speed line (the last generation's decode speed and the
|
|
23
|
+
session's average: exact from llama.cpp, estimated and marked `~` elsewhere), cached and reasoning
|
|
24
|
+
tokens, and the cost when the provider reports it (OpenRouter). The web's info bar shows the speed
|
|
25
|
+
next to `ctx`, updated after each generation, and the `ctx` tooltip there and on session cards lists
|
|
26
|
+
tokens in / cached / out (+ reasoning) and the cost, for that session only.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- [BEHAVIOUR] Guardrails ask less by default (auto mode). The guardrails bundle (0.5.0) no longer asks before
|
|
31
|
+
`git rebase`, writes outside the session's repo (`write-outside-repo`) or git in another checkout
|
|
32
|
+
(`git-outside-repo`); `guardrails: { mode: strict }` in config.yml brings those back. Everything else still asks
|
|
33
|
+
in both modes, as do the small-model rules and chi's own protected paths.
|
|
34
|
+
- [BEHAVIOUR] `shell-touches-chi` matches chi's real folders instead of text, and lets read-only commands through:
|
|
35
|
+
`ls ~/.config/samagotchi`, `rg samagotchi/hooks lib` and a scratch copy under `/tmp` no longer ask; `echo x >>
|
|
36
|
+
<config dir>/config.yml` and `cp hook .git/hooks/pre-commit` still do. A word it can't resolve falls back to the
|
|
37
|
+
old text match.
|
|
38
|
+
- [BEHAVIOUR] `rm-rf-wide` no longer asks when every `rm -rf` target is inside a tmp dir (`rm -rf /tmp/x`);
|
|
39
|
+
`/tmp` itself, `/tmp/*`, a `$VAR` and a link out of tmp still ask.
|
|
40
|
+
- [BEHAVIOUR] "Allow … in this repo" approvals hold for the whole repository, in every worktree of it, and the
|
|
41
|
+
ask names the repository (`in this repo (samagotchi)`) instead of the worktree folder. Approvals stored
|
|
42
|
+
before keep matching their exact folder.
|
|
43
|
+
- `chi bundle status` shows a bundle whose recorded scope this chi doesn't know (one a newer chi wrote, or a hand
|
|
44
|
+
edit) as `scope=team (unknown)`, counted as one issue, instead of checking its files in the system memories;
|
|
45
|
+
the shipped-bundle update skips it (upgrade chi or reinstall), and `chi bundle diff` refuses it with a one-line
|
|
46
|
+
error instead of a stack trace.
|
|
47
|
+
- The web model picker shows a model's configured sampling (`temperature=0.6 (hosts.work)`) as its row's tooltip;
|
|
48
|
+
`GET /api/models` has it as `sampling` on the models that have some.
|
|
49
|
+
- The web shows a plugin card that is only a notice (info level, no buttons, one short line, such as check-in's
|
|
50
|
+
"Nudged the model at 105 tool calls.") as a one-line row, `▸ check-in: Nudged …`, that opens on a click, like the
|
|
51
|
+
tool rows and resolved questions; warn cards, cards with buttons and longer ones stay framed cards.
|
|
52
|
+
- On a phone (a window under 900 px wide) the sessions strip's `hide` is in the top bar, where the `▾ N sessions`
|
|
53
|
+
pill comes back, with a finger-sized target, instead of the last column of the sideways-scrolling strip.
|
|
54
|
+
- On a phone the web info bar shows `mem N` (the memories the session used; their names in the tooltip) after the
|
|
55
|
+
session's status, since the `mem: …` line is hidden there.
|
|
56
|
+
- The web info bar's chips (model, ctx, speed, session time) are tinted over the card's glass instead of solid, and
|
|
57
|
+
the empty-history cover is see-through like the session panel.
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
|
|
61
|
+
- An MCP server that exits without a wait status no longer raises `NoMethodError`; its waiting calls fail with
|
|
62
|
+
"the server exited" instead of hanging.
|
|
63
|
+
- A hook that raises is now logged (`hook_failed`) instead of being swallowed silently; the turn still goes on.
|
|
64
|
+
- A hook's card (check-in, skills) now prints under its tool call's row: `tool_call_completed` is emitted before
|
|
65
|
+
`after_tool_call` fires.
|
|
66
|
+
- When a hook corrects a tool call (known-names), the model is told what actually ran with a leading
|
|
67
|
+
`ran as: <tool> <args>` line.
|
|
68
|
+
- MCP image content is described with a neutral `[image N: …]` line instead of claiming it was attached, which
|
|
69
|
+
was wrong for a model that can't see images.
|
|
70
|
+
- An MCP `resource` block carrying an image `blob` is now attached like an `image` block instead of being
|
|
71
|
+
dropped to its URI text.
|
|
72
|
+
- check-in's `nudge` mode no longer says "nudged" when the steer was dropped later; it says "nudge not sent" at
|
|
73
|
+
the turn's end.
|
|
74
|
+
- A session started with an annotation (a web quote) is previewed by the note typed under the quote, not
|
|
75
|
+
`From your thinking: > …`; a quote with no note previews by its words.
|
|
76
|
+
- A session that has only context notes so far (`chi note`, `send_note`) is previewed on its card and in the
|
|
77
|
+
lists as `note: …` instead of `—`, until a message is typed.
|
|
78
|
+
- A web turn of images alone names each image once in the transcript a recap is written from, not twice.
|
|
79
|
+
- `chi bootstrap` no longer says "loading the model?" for a slow test on a remote provider (OpenRouter and the
|
|
80
|
+
like); the hint is for a local server, where a slow answer is a model loading.
|
|
81
|
+
- `chi bootstrap` says why it saved a host as `name-2` when the derived name is already taken (or a model id
|
|
82
|
+
starts with it), instead of silently picking another name.
|
|
83
|
+
- `chi bundle upgrade` reports a hook or plugin file that is byte-identical to the installed one as "already up
|
|
84
|
+
to date" instead of "Updated".
|
|
85
|
+
- `chi bundle upgrade --agent` from a git/zip/tar source no longer hands the agent an `incoming:` path that was
|
|
86
|
+
already deleted: the extracted source is kept until the agent step is done, then cleaned up.
|
|
87
|
+
- `chi self` now checks a local `api: openai` host's reachability (a `GET <base>/models` with the same short
|
|
88
|
+
probe timeouts) and reports up/down with the ids it serves, instead of always saying "reported per turn".
|
|
89
|
+
|
|
90
|
+
Update with `chi update`: guardrails moves to 0.5.0 (auto mode and the new rule keys; it needs chi 0.20.0), mcp to 0.4.1 (exit status, image lines, image blobs) and check-in to 0.2.1 (a dropped nudge says so). Restart `chi web` and running sessions afterwards (`chi sessions restart ID`).
|
|
91
|
+
|
|
11
92
|
## [0.19.0] - 2026-10-03
|
|
12
93
|
|
|
13
94
|
### Changed
|
|
@@ -1118,7 +1199,8 @@ and long-lived sessions.
|
|
|
1118
1199
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
1119
1200
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
1120
1201
|
|
|
1121
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
1202
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.20.0...HEAD
|
|
1203
|
+
[0.20.0]: https://github.com/dm1try/samagotchi/compare/v0.19.0...v0.20.0
|
|
1122
1204
|
[0.19.0]: https://github.com/dm1try/samagotchi/compare/v0.18.1...v0.19.0
|
|
1123
1205
|
[0.18.1]: https://github.com/dm1try/samagotchi/compare/v0.18.0...v0.18.1
|
|
1124
1206
|
[0.18.0]: https://github.com/dm1try/samagotchi/compare/v0.17.0...v0.18.0
|
data/docs/architecture.md
CHANGED
|
@@ -274,6 +274,12 @@ The metrics in `/state`, `/snapshot`, `/stats` and the SSE `snapshot`/`reset` fr
|
|
|
274
274
|
newest turn the worker finished and any it hasn't saved yet (at most 20 turns), with their tool
|
|
275
275
|
calls and the running turn's finished ones. The full history is the session's `analytics.json`,
|
|
276
276
|
which `SessionMetrics#persist` rewrites after each turn; the web server merges the two by id.
|
|
277
|
+
The totals' `tokens` block adds the cached and reasoning sums, the cost, the decode time and tokens
|
|
278
|
+
behind the average speed (`avg_decode_tps`) and the last speeds (`last_decode_tps`,
|
|
279
|
+
`last_prefill_tps`, `tps_source`: `server` or `estimate`). Each `generation_completed` event
|
|
280
|
+
carries the generation's `speed` (`{decode_tps, source}`, null without one) and those running
|
|
281
|
+
`tokens`: the Engine closes the generation in `SessionMetrics#finish_generation` before relaying
|
|
282
|
+
it, so the web's info bar updates per generation without parsing chunk payloads.
|
|
277
283
|
|
|
278
284
|
The page's frequent reads of `GET /api/sessions/:id` stay light too:
|
|
279
285
|
- `?tail=1&recent=1` (each turn's end, a cancel, an answer display): `{tail, session: {id, status,
|
data/docs/cli.md
CHANGED
|
@@ -53,7 +53,9 @@ chi bootstrap # try localhost 8080, 11434, 1234, 8000
|
|
|
53
53
|
- **The file.** With no config.yml it writes a small commented one:
|
|
54
54
|
`default.model` as `<host>:<model>` and one `hosts:` entry named `local`
|
|
55
55
|
(localhost), `lan` (an IP) or after the domain (`openrouter`); `--name`
|
|
56
|
-
sets it.
|
|
56
|
+
sets it. When that name is taken (or a model id starts with it), chi picks
|
|
57
|
+
`name-2` and says why, with `--name` to choose another. An existing file is
|
|
58
|
+
left as it is apart from the new entry, added
|
|
57
59
|
at the end of its `hosts:` block (it gets a `hosts:` block, with a
|
|
58
60
|
`default` entry for its `server:` first, when it has none), after a backup
|
|
59
61
|
to `config.yml.bak-<time>`. `default.model` is set only when the file has
|
|
@@ -413,8 +415,17 @@ file and index line stay as they are; only this session doesn't see it.
|
|
|
413
415
|
`/help` lists the commands this session takes, the installed bundles' too, each
|
|
414
416
|
with a line on what it does. `/stats` shows the session's numbers: turns, tool
|
|
415
417
|
calls (by tool, with errors), iterations, tokens in/out summed over every
|
|
416
|
-
request
|
|
417
|
-
the
|
|
418
|
+
request (with the cached and reasoning tokens when the server reports them),
|
|
419
|
+
the decode speed, generation latency, cancellations, retries, context used
|
|
420
|
+
and window, the prompt profile and the model the server says it ran, and the
|
|
421
|
+
cost when the provider reports one (OpenRouter's `usage.cost`; no prices are
|
|
422
|
+
guessed). The speed line is the last generation's and the session's average
|
|
423
|
+
(weighted by tokens): llama.cpp reports it exactly (`87 tok/s out, 1.9k tok/s
|
|
424
|
+
prompt (last, server)`); for other servers it is estimated from the first
|
|
425
|
+
streamed chunk to the end and marked `~` (`~64 tok/s out (last, estimate)`).
|
|
426
|
+
Generations under 8 tokens or 100 ms get no speed of their own, nor does a
|
|
427
|
+
reply without server token counts or a non-streamed one. A delegated child
|
|
428
|
+
keeps its own numbers: a parent's tokens and cost leave them out. `/recap` is in
|
|
418
429
|
[Session recap](#session-recap), `/model` and `/models` in [Runtime Model
|
|
419
430
|
Switch](#runtime-model-switch-assist-mode).
|
|
420
431
|
|
data/docs/configuration.md
CHANGED
|
@@ -243,7 +243,9 @@ or ending in `localhost`, `.local`, `.lan`, `.home`, `.home.arpa`, `.internal`,
|
|
|
243
243
|
or `.ts.net` (Tailscale) is local: chi doesn't look names up. A remote
|
|
244
244
|
host keeps its model list for 10 minutes, gets a 120-second first-token limit,
|
|
245
245
|
and isn't asked for llama.cpp's `/props` (its context window and served model
|
|
246
|
-
come from its model list and each turn).
|
|
246
|
+
come from its model list and each turn). A local `api: openai` host has no
|
|
247
|
+
`/props`, so `chi self` GETs its `/models` with the same short probe timeouts
|
|
248
|
+
and reports up/down with the ids it serves. `remote: true` or `remote: false` on a
|
|
247
249
|
host decides it instead, e.g. for a llama.cpp behind an https proxy on the LAN:
|
|
248
250
|
|
|
249
251
|
```yaml
|
|
@@ -852,6 +854,7 @@ described in their own sections.
|
|
|
852
854
|
| `image.max_bytes` | `3750000` | | See "Images". |
|
|
853
855
|
| `image.max_per_request` | `20` | | See "Images". |
|
|
854
856
|
| `guardrails.enabled` | `true` | | `false`: no rules, and hooks' asks are dropped (a deny still applies). config.yml only: a worker unsets any `SAMAGOTCHI_GUARDRAILS_*` it inherits. See [Guardrails](guardrails.md). |
|
|
857
|
+
| `guardrails.mode` | `auto` | | Which rules vote: `auto` leaves out the rules tagged `modes: [strict]` (in the guardrails bundle: `git rebase`, writes and git outside the session's repo); `strict` runs them all. An unknown value warns and is `auto`. config.yml only. See [Guardrails](guardrails.md#modes). |
|
|
855
858
|
| `guardrails.small_models` | `auto` | | Which models get the `models: small` rules: `auto` (32B or less by the size in the name, an MoE's active size), a list of globs, or `[]`. config.yml only. See [Guardrails](guardrails.md#rules-for-some-models). |
|
|
856
859
|
| `guardrails.parent_approvals` | `off` | | What `chi answer` lets a parent agent allow on an approval: `off` (deny only) or `once` ("Allow once", never a wider scope). config.yml only: no environment variable (a parent can still pick the config dir with `XDG_CONFIG_HOME`). A convention for an honest parent, not a security boundary. See [Guardrails](guardrails.md#approvals-from-a-parent-agent). |
|
|
857
860
|
| `log.file` | state dir | yes | See "Debug Log File". |
|
data/docs/guardrails.md
CHANGED
|
@@ -81,6 +81,13 @@ Allowing beyond "once" is stored in `$XDG_STATE_HOME/samagotchi/guardrails/appro
|
|
|
81
81
|
| repo | this exact call in this repo (the cwd outside a repo), any session |
|
|
82
82
|
| rule | anything this rule asks about in this repo |
|
|
83
83
|
|
|
84
|
+
"This repo" is the git repository, not the folder: an approval given in one
|
|
85
|
+
worktree holds in the main checkout and every other worktree of it (it is
|
|
86
|
+
stored with the repository's common git dir as `repo`, and the worktree as
|
|
87
|
+
`repo_root`). The ask names the repository (`Allow this call in this repo
|
|
88
|
+
(samagotchi)`). An entry stored before chi 0.20 has no `repo` and still only
|
|
89
|
+
matches its exact folder.
|
|
90
|
+
|
|
84
91
|
A stored approval only relaxes an ask; a deny rule is never approvable.
|
|
85
92
|
A file that doesn't parse is moved aside to `approvals.json.corrupt-<UTC time>`
|
|
86
93
|
with one warning, and chi starts with no stored approvals (more asks, nothing lost).
|
|
@@ -137,7 +144,7 @@ grants no permission, so a parent may answer it either way by default.
|
|
|
137
144
|
`turn.parent_continue: false` (config.yml only, like `parent_approvals`)
|
|
138
145
|
makes parents stop-only. See [chi as a sub-agent](sub-agent.md#the-step-limit).
|
|
139
146
|
|
|
140
|
-
The guardrails settings (`enabled`, `small_models`, `parent_approvals`) are
|
|
147
|
+
The guardrails settings (`enabled`, `mode`, `small_models`, `parent_approvals`) are
|
|
141
148
|
read from config.yml only: they have no environment variable, and a worker
|
|
142
149
|
chi starts unsets every `SAMAGOTCHI_GUARDRAILS_*` it would inherit. What a
|
|
143
150
|
parent's environment does choose is the config dir: `XDG_CONFIG_HOME` picks
|
|
@@ -182,6 +189,7 @@ commands that name them.
|
|
|
182
189
|
```yaml
|
|
183
190
|
guardrails:
|
|
184
191
|
enabled: true # false: no rules, and hooks' asks are dropped (a deny still applies); config.yml only
|
|
192
|
+
mode: auto # or strict: also the rules tagged modes: [strict]; config.yml only
|
|
185
193
|
rules:
|
|
186
194
|
- id: git-push
|
|
187
195
|
tool: shell # execute + task_create; or a tool name, a glob, or a list
|
|
@@ -201,7 +209,41 @@ guardrails:
|
|
|
201
209
|
scopes: [once, session, rule]
|
|
202
210
|
```
|
|
203
211
|
|
|
204
|
-
A rule gives at least one of `tool`, `command`, `path` and `
|
|
212
|
+
A rule gives at least one of `tool`, `command`, `path`, `git`, `touches` and `rm`.
|
|
213
|
+
|
|
214
|
+
`rm: outside_tmp` (shell tools) matches a command whose `rm -r -f` reaches
|
|
215
|
+
outside the tmp folders (`$TMPDIR`, `/tmp`, `/var/tmp`): every such `rm` in a
|
|
216
|
+
chain must name only paths inside one, resolved like `touches` (so
|
|
217
|
+
`/tmp/../etc` and a link from `/tmp` to `/` are outside). The tmp folder
|
|
218
|
+
itself, a glob (`/tmp/*`), a `$VAR` other than `$HOME`/`$TMPDIR`, an `rm`
|
|
219
|
+
behind `sudo`/`xargs`/`sh -c` or in `$(…)`, and the tmp folder the session's
|
|
220
|
+
repo is in count as outside.
|
|
221
|
+
|
|
222
|
+
`touches: chi_dirs` (shell tools) matches a command that names a path in chi's
|
|
223
|
+
own folders: the config folder (`config.yml`, `memories/`), the hooks folder,
|
|
224
|
+
the approval store, installed bundles (`memories/.bundles`), the session repo's
|
|
225
|
+
git hooks (`core.hooksPath` honoured) or any `.git/hooks` outside a tmp folder.
|
|
226
|
+
Each word is read as a path against the folder the command is in by then
|
|
227
|
+
(`cd`, `pushd`/`popd` and `( … )` followed), with `~`, `$HOME` and
|
|
228
|
+
`$XDG_CONFIG_HOME`/`$XDG_STATE_HOME` expanded and symlinks resolved. So
|
|
229
|
+
`/tmp/x/config/samagotchi/config.yml` and chi's own source
|
|
230
|
+
(`lib/samagotchi/hooks/…`) don't match. A word it can't resolve (another
|
|
231
|
+
`$VAR`, `$(…)`, a glob, a relative path after `cd $X`) or one that reads as a
|
|
232
|
+
script (`sh -c '…'`, `ruby -e '…'`) is matched as text instead (`.config/samagotchi`,
|
|
233
|
+
`samagotchi/config.yml`, `samagotchi/hooks`, `samagotchi/guardrails`,
|
|
234
|
+
`memories/.bundles`, `.git/hooks`).
|
|
235
|
+
|
|
236
|
+
`skip_read_only: true` lets a shell command through when it only reads:
|
|
237
|
+
every command in it (split at `;`, `&&`, `||`, `|`) is `ls`, `cat`, `head`,
|
|
238
|
+
`tail`, `wc`, `stat`, `du`, `grep`, `rg`, `find`, `tree`, `file`, `sort`,
|
|
239
|
+
`uniq`, `cut`, `diff`, `echo`, `cd`, `sed`, `awk` or a reading `git`
|
|
240
|
+
(`log`, `show`, `diff`, `status`, `branch` listing, …), with no option that
|
|
241
|
+
writes or runs something (`sed -i`, `sed 's/a/b/w f'`, `find -delete`/`-exec`,
|
|
242
|
+
`rg --pre`, `sort -o`, `awk` with `>`/`|`/`system(`, `git -c`, `git diff
|
|
243
|
+
--ext-diff`, …) and no redirection other than to `/dev/null` or another fd.
|
|
244
|
+
`$(…)`, backticks, `$VAR` (other than `$HOME`, `$XDG_*`, `$TMPDIR` and `$PWD`
|
|
245
|
+
at a word's start), an env prefix, `sh -c`, `xargs`, `eval`, `sudo` and the
|
|
246
|
+
like make a command not read-only. It errs towards asking.
|
|
205
247
|
|
|
206
248
|
A tool name may be a glob, so one rule covers a plugin's tools (an MCP server's,
|
|
207
249
|
say): `tool: "mcp_*"` or `tool: ["mcp_{git,gh}_*", web_fetch]` (`*`, `?`, `[…]` and
|
|
@@ -303,6 +345,38 @@ once per bundle version to restart the session (`chi sessions stop ID`, then
|
|
|
303
345
|
older than the bundle) denies every tool call, as for a rule that doesn't
|
|
304
346
|
parse, until chi is updated or the bundle uninstalled.
|
|
305
347
|
|
|
348
|
+
### Modes
|
|
349
|
+
|
|
350
|
+
`guardrails.mode` (config.yml only) picks how much is asked:
|
|
351
|
+
|
|
352
|
+
- `auto`, the default: the rules tagged `modes: [strict]` don't vote. Asks
|
|
353
|
+
are kept for what is hard to undo or reaches chi itself.
|
|
354
|
+
- `strict`: every rule votes.
|
|
355
|
+
|
|
356
|
+
```yaml
|
|
357
|
+
guardrails:
|
|
358
|
+
mode: strict
|
|
359
|
+
rules:
|
|
360
|
+
- id: no-docker
|
|
361
|
+
tool: shell
|
|
362
|
+
command: '\bdocker\b'
|
|
363
|
+
modes: [strict] # or auto, or [auto, strict] (the same as leaving it out)
|
|
364
|
+
verdict: ask
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
A rule without `modes:` votes in every mode, so a rule you add to config.yml
|
|
368
|
+
always applies unless you tag it. The core checks (protected paths, hooks)
|
|
369
|
+
vote in both modes. In the guardrails bundle, `git-rebase`,
|
|
370
|
+
`write-outside-repo` and `git-outside-repo` are strict only; its small-model
|
|
371
|
+
rules (`models: small`) are untagged, so they ask a small model in both modes.
|
|
372
|
+
`/guardrails` shows the mode on its first lines and marks the rules this mode
|
|
373
|
+
leaves out `(strict only)`.
|
|
374
|
+
|
|
375
|
+
A rule in config.yml that uses a key this chi doesn't know yet (`modes:`,
|
|
376
|
+
`touches:`, `rm:`, `skip_read_only:` before 0.20) doesn't parse, and a rule
|
|
377
|
+
that doesn't parse denies every tool call (see Failing closed). Bundles say
|
|
378
|
+
which chi they need (`requires_chi`), so their rules don't hit this.
|
|
379
|
+
|
|
306
380
|
## The guardrails bundle
|
|
307
381
|
|
|
308
382
|
```sh
|
|
@@ -310,18 +384,22 @@ chi bundle install guardrails # chi bootstrap installs it, with the core prof
|
|
|
310
384
|
```
|
|
311
385
|
|
|
312
386
|
installs a default rule set plus a short memory telling the model not to
|
|
313
|
-
route around a deny.
|
|
314
|
-
`
|
|
315
|
-
`$HOME` or `..` paths
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
387
|
+
route around a deny. In both modes it asks before `git push`, `reset --hard`,
|
|
388
|
+
`clean -f`, `branch -D`, `filter-branch`/`filter-repo`; `rm -rf` on `/`, `~`,
|
|
389
|
+
`$HOME` or `..` paths, unless every target is inside a tmp folder; `curl … |
|
|
390
|
+
sh` and `base64 -d … | sh`; shell commands that name a path in chi's config,
|
|
391
|
+
hooks, approvals or bundles or a `.git/hooks` (`shell-touches-chi`, `touches:
|
|
392
|
+
chi_dirs`), unless they only read (`skip_read_only`); and answering chi's
|
|
393
|
+
questions around `chi answer`: `chi --attach` or `chi -p` with stdin from a
|
|
394
|
+
pipe, a here-string or a file (`chi-answer-piped`), and `curl`/`wget` to a
|
|
395
|
+
session's `/answer` route (`chi-answer-http`), both once or for the session.
|
|
396
|
+
`chi answer` itself isn't asked about. It denies writes into `.git/hooks`.
|
|
397
|
+
|
|
398
|
+
In strict mode (`guardrails: { mode: strict }`) it also asks before `git
|
|
399
|
+
rebase`, writes outside the session's repo (`write-outside-repo`) and git that
|
|
400
|
+
changes another checkout (`git-outside-repo`: `cd ../main && git commit`, `git
|
|
401
|
+
-C ../main add .`; it offers once, this session and "rule in this repo", the
|
|
402
|
+
last stored for later sessions in that repo). The rules are in
|
|
325
403
|
`lib/samagotchi/bundles/guardrails/guardrails/rules.yml`.
|
|
326
404
|
|
|
327
405
|
Small models (`models: small`, see above) get two more asks, in
|
data/docs/plugins.md
CHANGED
|
@@ -524,6 +524,11 @@ ctx.card(id: id, title: "Build finished", body: "no warnings left") # replaces
|
|
|
524
524
|
its line; the web shows a bubble only when the command answers with
|
|
525
525
|
text or fails.
|
|
526
526
|
- `level:` is `:info` or `:warn` (the warning colour).
|
|
527
|
+
- An `:info` card with no actions and a body of one short line (up to 160
|
|
528
|
+
characters) is a notice: the web shows it as a one-line row,
|
|
529
|
+
`▸ check-in: Nudged the model at 105 tool calls.`, that opens on a click,
|
|
530
|
+
like a resolved question. A warn card, one with actions and one with more
|
|
531
|
+
to read stay framed cards.
|
|
527
532
|
- `id:` names an earlier card to replace. Without one a new id is made. The
|
|
528
533
|
web updates the card in place; the terminal prints it again, marked
|
|
529
534
|
`(updated)`. A card that waits for something (a model's answer) shows
|
data/docs/sessions.md
CHANGED
|
@@ -65,7 +65,7 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
|
|
|
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; 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. 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 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); "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).
|
|
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
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`).
|
|
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).
|
|
@@ -95,20 +95,31 @@ module Samagotchi
|
|
|
95
95
|
# +base+, with -2, -3… when the name is taken or is the prefix of a
|
|
96
96
|
# model id (chi would read "qwen3:8b" as host qwen3, model 8b).
|
|
97
97
|
def host_name(base, requested: nil, model_ids: [])
|
|
98
|
+
host_name_with_reason(base, requested: requested, model_ids: model_ids).first
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# [name, reason]: reason is :taken when the name was suffixed because
|
|
102
|
+
# config.yml already has it, :model_prefix when a model id's prefix
|
|
103
|
+
# would read as a host, nil when the name is free as given.
|
|
104
|
+
def host_name_with_reason(base, requested: nil, model_ids: [])
|
|
98
105
|
taken = current_hosts.keys
|
|
99
106
|
if requested
|
|
100
107
|
name = requested.to_s.strip
|
|
101
108
|
raise Error, "--name must match #{ConfigFile::HOST_NAME_RE.source}" unless name.match?(ConfigFile::HOST_NAME_RE)
|
|
102
109
|
raise Error, "config.yml already has a host named '#{name}'; pick another --name" if taken.include?(name.downcase)
|
|
103
110
|
|
|
104
|
-
return name
|
|
111
|
+
return [name, nil]
|
|
105
112
|
end
|
|
106
113
|
|
|
107
114
|
prefixes = [*model_ids, configured_default_model].compact.map { |id| id.to_s.split(":", 2).first.downcase }
|
|
108
115
|
name = base
|
|
109
116
|
n = 1
|
|
110
|
-
|
|
111
|
-
name
|
|
117
|
+
reason = nil
|
|
118
|
+
while taken.include?(name) || prefixes.include?(name)
|
|
119
|
+
reason ||= taken.include?(name) ? :taken : :model_prefix
|
|
120
|
+
name = "#{base}-#{n += 1}"
|
|
121
|
+
end
|
|
122
|
+
[name, reason]
|
|
112
123
|
end
|
|
113
124
|
|
|
114
125
|
# Write the entry. @param fields [Hash] the hosts entry (string keys,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "rbconfig"
|
|
4
4
|
require_relative "config"
|
|
5
5
|
require_relative "model_profile"
|
|
6
|
+
require_relative "host_registry"
|
|
6
7
|
require_relative "bootstrap/probe"
|
|
7
8
|
require_relative "bootstrap/config_writer"
|
|
8
9
|
require_relative "bootstrap/bundles"
|
|
@@ -235,9 +236,11 @@ module Samagotchi
|
|
|
235
236
|
|
|
236
237
|
def test(result, model, key_env)
|
|
237
238
|
done = Queue.new
|
|
238
|
-
waiting =
|
|
239
|
-
|
|
240
|
-
|
|
239
|
+
waiting = if loading_hint?(result)
|
|
240
|
+
Thread.new do
|
|
241
|
+
@stdout.puts("test: waiting for an answer (loading the model?)…") if done.pop(timeout: LOADING_AFTER).nil?
|
|
242
|
+
end
|
|
243
|
+
end
|
|
241
244
|
seconds = @probe.test_turn(result.candidate, model, key_env: key_env)
|
|
242
245
|
@stdout.puts(format("test: answered in %.1f s", seconds))
|
|
243
246
|
true
|
|
@@ -249,17 +252,25 @@ module Samagotchi
|
|
|
249
252
|
waiting&.join
|
|
250
253
|
end
|
|
251
254
|
|
|
255
|
+
# The "loading the model?" hint fits a local server (a llama.cpp loading
|
|
256
|
+
# weights); a remote provider's slow answer is the network, not a load.
|
|
257
|
+
def loading_hint?(result)
|
|
258
|
+
!HostRegistry.remote_address?(result.candidate.scheme, result.candidate.host)
|
|
259
|
+
end
|
|
260
|
+
|
|
252
261
|
# 0 written, 1 written but not as planned, nil with @exit on a refusal.
|
|
253
262
|
def write(result, model, key_env, options)
|
|
254
263
|
writer = Bootstrap::ConfigWriter.new(path: @config_path, env: @env)
|
|
255
264
|
fields = host_fields(result.candidate, result.native?, key_env)
|
|
256
265
|
ids = result.models.map(&:id)
|
|
257
|
-
|
|
258
|
-
|
|
266
|
+
base = Bootstrap::ConfigWriter.derived_name(result.candidate.host)
|
|
267
|
+
name, reason = begin
|
|
268
|
+
writer.host_name_with_reason(base, requested: options[:name], model_ids: ids)
|
|
259
269
|
rescue Bootstrap::ConfigWriter::Error => e
|
|
260
270
|
@exit = usage_error(e.message)
|
|
261
271
|
return nil
|
|
262
272
|
end
|
|
273
|
+
explain_name(base, name, reason)
|
|
263
274
|
outcome = writer.write(name: name, fields: fields, model: model, dry_run: options[:dry_run])
|
|
264
275
|
code = report(outcome, model)
|
|
265
276
|
return code unless INSTALLS_BUNDLES.include?(outcome.kind)
|
|
@@ -269,6 +280,16 @@ module Samagotchi
|
|
|
269
280
|
code
|
|
270
281
|
end
|
|
271
282
|
|
|
283
|
+
# One line when the name wasn't free as derived: why chi picked another.
|
|
284
|
+
def explain_name(base, name, reason)
|
|
285
|
+
case reason
|
|
286
|
+
when :taken
|
|
287
|
+
@stdout.puts("a host named #{base} already exists; saved as #{name}, use --name to choose")
|
|
288
|
+
when :model_prefix
|
|
289
|
+
@stdout.puts("a model id starts with #{base} (chi would read it as a host); saved as #{name}, use --name to choose")
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
272
293
|
# The system bundle, core, and dev when a terminal says yes: one line
|
|
273
294
|
# each. 1 when something failed.
|
|
274
295
|
def install_bundles(dry_run: false)
|
|
@@ -263,6 +263,10 @@ module Samagotchi
|
|
|
263
263
|
@stdout.puts "Provenance updated after interactive resolution."
|
|
264
264
|
@stdout.puts "Upgrade resolved interactively."
|
|
265
265
|
0
|
|
266
|
+
ensure
|
|
267
|
+
# An owned source (git/zip/tar) was kept for the incoming files above;
|
|
268
|
+
# the agent step is done with them now.
|
|
269
|
+
installer.cleanup_source!
|
|
266
270
|
end
|
|
267
271
|
|
|
268
272
|
def uninstall(rest)
|
|
@@ -353,9 +357,15 @@ module Samagotchi
|
|
|
353
357
|
@stdout.puts "Bundle '#{name}' not installed."
|
|
354
358
|
return 0
|
|
355
359
|
end
|
|
356
|
-
@stdout.puts "Bundle: #{name} v#{st[:provenance][:version]} scope=#{st
|
|
357
|
-
|
|
360
|
+
@stdout.puts "Bundle: #{name} v#{st[:provenance][:version]} scope=#{status_scope(st)} installed=#{st[:provenance][:installed_at]}"
|
|
361
|
+
if st[:scope_error]
|
|
362
|
+
@stdout.puts "Target: (unknown scope: this chi can't resolve it; upgrade chi or reinstall the bundle)"
|
|
363
|
+
else
|
|
364
|
+
@stdout.puts "Target: #{st[:target_dir]}"
|
|
365
|
+
end
|
|
358
366
|
st[:files].each do |k, info|
|
|
367
|
+
next @stdout.puts(" #{k}: unchecked") if info[:unchecked]
|
|
368
|
+
|
|
359
369
|
mods = []
|
|
360
370
|
mods << "conflict (kept your edits over v#{st[:provenance][:version]}: chi bundle diff #{name} #{k})" if info[:conflict]
|
|
361
371
|
mods << "modified" if info[:modified]
|
|
@@ -409,15 +419,19 @@ module Samagotchi
|
|
|
409
419
|
plugin = st[:plugin]
|
|
410
420
|
mods += 1 if plugin && (plugin[:state] != "ok" || plugin[:requires_failure])
|
|
411
421
|
mods += 1 if st[:hooks_requires_failure]
|
|
422
|
+
mods += 1 if st[:scope_error]
|
|
412
423
|
plugin_info = plugin ? " plugin=#{plugin[:file]}" : ""
|
|
413
424
|
includes = st[:provenance][:includes]
|
|
414
425
|
includes_info = includes ? " includes=#{includes.join(",")}" : ""
|
|
415
|
-
@stdout.puts " #{bname} v#{st[:provenance][:version]} scope=#{st
|
|
426
|
+
@stdout.puts " #{bname} v#{st[:provenance][:version]} scope=#{status_scope(st)} files=#{st[:files].size}#{hook_info}#{plugin_info}#{includes_info} issues=#{mods}"
|
|
416
427
|
end
|
|
417
428
|
end
|
|
418
429
|
return 0
|
|
419
430
|
end
|
|
420
431
|
|
|
432
|
+
# "team (unknown)" for a scope this chi can't resolve (Status#bundle_status).
|
|
433
|
+
def status_scope(st) = st[:scope_error] ? "#{st[:scope]} (unknown)" : st[:scope]
|
|
434
|
+
|
|
421
435
|
def diff(rest)
|
|
422
436
|
args = rest.reject { |a| a.start_with?("--") }
|
|
423
437
|
bname = args[0]
|
|
@@ -431,7 +445,11 @@ module Samagotchi
|
|
|
431
445
|
@stderr.puts "Bundle '#{bname}' not installed"; return 1
|
|
432
446
|
end
|
|
433
447
|
scope = data[:scope]&.to_s || "system"
|
|
434
|
-
|
|
448
|
+
begin
|
|
449
|
+
target_dir = Samagotchi::MemoryPaths.scope_dir!(scope)
|
|
450
|
+
rescue ArgumentError => e
|
|
451
|
+
@stderr.puts "Bundle '#{bname}': #{e.message}"; return 1
|
|
452
|
+
end
|
|
435
453
|
files = data[:files] || {}
|
|
436
454
|
hooks = data[:hooks] || {}
|
|
437
455
|
plugin_path = prov.plugin_path(data)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: check-in
|
|
3
|
-
version: 0.2.
|
|
3
|
+
version: 0.2.1
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Checks on a long turn: after N tool calls with no answer, a card asks the user (Nudge / Keep going / Stop), or the model is nudged to say what it has found"
|
|
6
6
|
trust_level: reviewed
|
|
7
7
|
plugin:
|
|
8
8
|
file: plugin.rb
|
|
9
|
-
sha256: sha256:
|
|
9
|
+
sha256: sha256:afbd0ecf5a41eefe79786e089aa918728ebb0fed0e3b75f84d754a47ec6ade18
|
|
10
10
|
requires_chi: ">= 0.3.0"
|
|
@@ -103,7 +103,11 @@ class Plugin
|
|
|
103
103
|
def check_in(ctx, count, mode)
|
|
104
104
|
case mode
|
|
105
105
|
when "nudge"
|
|
106
|
-
|
|
106
|
+
text = message(count)
|
|
107
|
+
if ctx.steer(text)
|
|
108
|
+
@mutex.synchronize { @nudge = { text: text, count: count } }
|
|
109
|
+
ctx.notify("nudged the model after #{count} tool calls")
|
|
110
|
+
end
|
|
107
111
|
when "notify"
|
|
108
112
|
ctx.notify("#{count} tool calls in this turn, no answer yet")
|
|
109
113
|
else
|
|
@@ -117,15 +121,20 @@ class Plugin
|
|
|
117
121
|
nudge_not_sent(event, ctx)
|
|
118
122
|
end
|
|
119
123
|
|
|
120
|
-
#
|
|
121
|
-
#
|
|
124
|
+
# A nudge that was accepted but dropped before it joined the turn (the
|
|
125
|
+
# model answered first, or the turn was stopped): the card says so, or,
|
|
126
|
+
# for a mode nudge (no card), a notice does.
|
|
122
127
|
def nudge_not_sent(event, ctx)
|
|
123
128
|
id, nudge = @mutex.synchronize { [@card_id, @nudge] }
|
|
124
|
-
return unless
|
|
129
|
+
return unless nudge
|
|
125
130
|
return if steered?(event[:messages], nudge[:text])
|
|
126
131
|
|
|
127
132
|
first = event[:status].to_s == "canceled" ? "The turn ended first" : "The answer came first"
|
|
128
|
-
|
|
133
|
+
if id
|
|
134
|
+
ctx.card(id: id, title: "check-in", body: "#{first}; nudge not sent.")
|
|
135
|
+
else
|
|
136
|
+
ctx.notify("#{first}; nudge not sent.", level: :warn)
|
|
137
|
+
end
|
|
129
138
|
end
|
|
130
139
|
|
|
131
140
|
# Whether this turn's messages (after its last prompt) have the steer.
|