samagotchi 0.133.0 → 0.134.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 +68 -1
- data/docs/cli.md +11 -0
- data/docs/configuration.md +8 -4
- data/docs/guardrails.md +102 -13
- data/docs/internals/background-tasks.md +1 -0
- data/docs/internals/participants.md +6 -5
- data/docs/internals/tool-guardrails.md +2 -0
- data/docs/plugins.md +2 -1
- data/docs/sessions.md +3 -2
- data/docs/testing.md +7 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/chi_home.rb +12 -2
- data/lib/samagotchi/desktop/macos.rb +19 -4
- data/lib/samagotchi/engine.rb +13 -0
- data/lib/samagotchi/guardrails/implicit_writers.rb +143 -0
- data/lib/samagotchi/guardrails/pair_boundary.rb +6 -4
- data/lib/samagotchi/guardrails/pair_command.rb +493 -60
- data/lib/samagotchi/guardrails/parent_approvals.rb +1 -1
- data/lib/samagotchi/guardrails/read_only_shell.rb +34 -2
- data/lib/samagotchi/guardrails/shell_git_dirs.rb +6 -1
- data/lib/samagotchi/guardrails/shell_lex.rb +23 -0
- data/lib/samagotchi/guardrails/shell_paths.rb +3 -2
- data/lib/samagotchi/guardrails/targets.rb +8 -3
- data/lib/samagotchi/home_command.rb +22 -6
- data/lib/samagotchi/home_doctor.rb +36 -3
- data/lib/samagotchi/home_notice.rb +8 -1
- data/lib/samagotchi/hooks/loader.rb +5 -0
- data/lib/samagotchi/log_line.rb +12 -0
- data/lib/samagotchi/project_env.rb +3 -11
- data/lib/samagotchi/self_report.rb +3 -1
- data/lib/samagotchi/system_one/self_protect.rb +58 -2
- data/lib/samagotchi/testing/waiting.rb +13 -0
- data/lib/samagotchi/toolkit_binding.rb +16 -0
- data/lib/samagotchi/update_command.rb +7 -3
- data/lib/samagotchi/update_hint.rb +6 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/public/answer_fold.js +262 -0
- data/lib/samagotchi/web/public/answer_refs.js +2 -0
- data/lib/samagotchi/web/public/app.js +18 -1
- data/lib/samagotchi/web/public/cards_place.js +5 -2
- data/lib/samagotchi/web/public/chip_popover.js +73 -11
- data/lib/samagotchi/web/public/claim_link.js +4 -2
- data/lib/samagotchi/web/public/composer.css +3 -1
- data/lib/samagotchi/web/public/history_reveal.js +23 -0
- data/lib/samagotchi/web/public/index.html +44 -28
- data/lib/samagotchi/web/public/llm_context_chip.js +5 -13
- data/lib/samagotchi/web/public/refs.js +6 -7
- data/lib/samagotchi/web/public/review_start.js +1 -1
- data/lib/samagotchi/web/public/start_page.js +4 -4
- data/lib/samagotchi/web/public/start_settings.js +29 -18
- data/lib/samagotchi/web/public/start_toolkit.js +5 -4
- data/lib/samagotchi/web/public/timing.js +5 -1
- data/lib/samagotchi/web/public/turn_html.js +12 -6
- data/lib/samagotchi/web/public/turn_range.js +32 -0
- data/lib/samagotchi/web/public/turn_view.js +21 -11
- data/lib/samagotchi/web/public/verdict_card.js +5 -13
- data/lib/samagotchi/web/public/verify_target.js +7 -10
- 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: 1400c2aa3cceb5a84b6aa0d5610021b5e5893415afa37f98e8c99efe291de436
|
|
4
|
+
data.tar.gz: eda23134194381fd10460ba0b026ae10aa5a8aaf674fbc9dc4504359b1acb86d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7029cb713c02b4c3099ade377bc57618ec6467c2a1f04a4b21c7ebd3fbb6e34705e1ad54fe547672becdc6553824b785c96222e310690fd707a12c0fb057e852
|
|
7
|
+
data.tar.gz: e7400f091254ba9ffdf04f14160e8d6816cbe264016f8be8558c9da091edcc33bf34ed93652c83b6647be728a349b31631db49fc26e483280deda75a7658e5e3
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,72 @@ and commands may change between minor versions. How releases are made:
|
|
|
7
7
|
[docs/releasing.md](docs/releasing.md).
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.134.0] - 2026-10-30
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Web: a long answer (over ~25 lines or ~2,000 characters) gets a fold row above it: one click folds it to its first
|
|
15
|
+
line and its length, another opens it. Your folds are kept per browser across reloads; a jump into a folded answer
|
|
16
|
+
(a claim link, the working set, "View check") opens it.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Web: the start page's ⚙ and 🧰 buttons say whether their panel is open (`aria-expanded`) to screen readers.
|
|
21
|
+
- Web: an open sheet or popover is placed again when the window resizes (a rotation, a phone's keyboard): its header
|
|
22
|
+
no longer ends up above the screen, and a sheet turns back into a popover on a wide window.
|
|
23
|
+
- Web: on a phone the ⚙ panel's sheet may take 85 % of the screen, so with "Apply and budget" open its closing note is
|
|
24
|
+
no longer cut off at the bottom edge.
|
|
25
|
+
- Web: the ⚙ and 🧰 panels keep the keyboard focus while open (Tab wraps inside, `aria-modal`), their override chips
|
|
26
|
+
say whether the panel is open (`aria-expanded`), and Esc or × gives focus back to the chip that opened it.
|
|
27
|
+
- Web: "default" on the ⚙ panel's Context row now clears a set apply rule and budget too: the chip read "ctx apply
|
|
28
|
+
payoff · budget 64000" while the row showed default, and the new session got them.
|
|
29
|
+
- Web: on a touch screen a tapped row or link in the ⚙ and 🧰 panels (and the ⚙ and 🧰 buttons) no longer keeps its
|
|
30
|
+
hover tint.
|
|
31
|
+
- Web: a sheet's and a popover's × is a 36 px tap target on a touch screen (it was 22x16 px).
|
|
32
|
+
- Web: the Stop sheet's "+N more" no longer sits 74 px to the right (it shared a class with the ⚙ panel's row).
|
|
33
|
+
- Pair turns: a read-only mention of mise's config (`find . -name mise.toml && make`) no longer asks the owner; a
|
|
34
|
+
delete, move or write of it still does, and `.tool-versions` (mise reads it too) is covered the same way.
|
|
35
|
+
- `chi home doctor` checks the desktop helper only when the helper's chi reads this chi's config dir: a run with
|
|
36
|
+
`XDG_CONFIG_HOME` elsewhere no longer reports the installed helper against a home it never boots.
|
|
37
|
+
- The new-version hint ("… the desktop helper can be updated: chi update") follows the same rule: a run with
|
|
38
|
+
`XDG_CONFIG_HOME` elsewhere no longer hints about the installed helper.
|
|
39
|
+
- `chi home set` under `bundle exec` refuses to record its own Ruby, whose gem path is the bundle's, instead of making
|
|
40
|
+
it the home every launch boots (`--ruby` another Ruby still works).
|
|
41
|
+
- `chi update` under `bundle exec`, run on another Ruby than the chi home's, hands over to the home's chi outside the
|
|
42
|
+
bundle instead of loading the bundle into it (which failed noisily).
|
|
43
|
+
- `chi self`'s home row says when this chi runs on the home's Ruby and gems but isn't the home's newest chi (`this chi
|
|
44
|
+
is X`), as the terminal line and `chi home doctor` do.
|
|
45
|
+
- `chi home doctor` reports a worker that couldn't boot the chi home's chi (the debug log's newest `boot_failed` line
|
|
46
|
+
since the home was set, unless a worker started after it), with what to run.
|
|
47
|
+
- Toolkit suggestions (`toolkit.suggest.mode: on`): a suggestion that times out or gets no answer from System 1 now
|
|
48
|
+
says so in a notice (the session runs its default kit) and a timeout logs a warning, instead of falling back silently.
|
|
49
|
+
- Pair turns: a participant can no longer write git's config at all, so no setting makes git run a command later in
|
|
50
|
+
your name (a hook, a filter or driver, a helper, an include): `git config` only reads, parsed strictly (an unknown or
|
|
51
|
+
abbreviated option counts as a write), and `git -c`, `--config-env`, a clone's `-c`/`--config`, `--template` and
|
|
52
|
+
`GIT_TEMPLATE_DIR` are denied, as are a submodule's hooks (`.git/modules/*/hooks`) and the hook installers
|
|
53
|
+
(`pre-commit install`, `husky`, `lefthook install`). A command git runs from its text (`rebase --exec`,
|
|
54
|
+
`submodule foreach`) is checked like any command. Reading git's config (`git config --get`, `-l`) is no longer
|
|
55
|
+
refused.
|
|
56
|
+
- Pair turns: a command that writes outside the project with no word naming where now asks you like any other
|
|
57
|
+
outside write: global installs (`npm -g`, `pip install`, `gem install`, `brew`, `cargo`/`go install`, `pipx`,
|
|
58
|
+
`uv tool`), user config (`npm config set` writes `~/.npmrc`, `gh config set`), `defaults write`, `launchctl`; and
|
|
59
|
+
so does a shell or interpreter reading its script from stdin (`… | sh`, `| bash -x`, `curl … | bash`,
|
|
60
|
+
`sh < run.sh`). Global installs are recognised in any spelling (`npm i --location global`, `python3.12 -m pip`,
|
|
61
|
+
`npm_config_global=true`, `npm link`), with more tools (`deno`, `dotnet tool`, `corepack`, `bundle config set`).
|
|
62
|
+
- System 1 no longer auto-allows a command that names the memories folder and then climbs out of it by a script's
|
|
63
|
+
own calls (`python3 -c '…os.chdir(os.pardir)…'`, `ruby -e '…File.dirname(Dir.pwd)…'`) or runs a script it can't
|
|
64
|
+
see (a file, stdin, `| sh -x`, `python3.12 x.py`, `make`, `awk -f`): it could write `config.yml`. You decide
|
|
65
|
+
those; plain memory edits stay eligible.
|
|
66
|
+
- Guardrails: a `task_create` whose `env:` points git at another config (`GIT_CONFIG_GLOBAL`, `GIT_CONFIG_SYSTEM`,
|
|
67
|
+
`GIT_CONFIG_KEY_n`/`VALUE_n`) is no longer read-only, and a pair turn's command that sets one is denied like
|
|
68
|
+
`git -c`.
|
|
69
|
+
- Pair turns: more spellings that hid a git config write or a command are caught: a git global option that takes a
|
|
70
|
+
value (`--attr-source`, `--shallow-file`, one git adds later), `git-config` and the other dashed builtins, a script
|
|
71
|
+
after a shell's options (`sh -c -- S`, `bash -c -x S`) or in an option cluster (`ruby -we`, `perl -le`, `node -pe`,
|
|
72
|
+
`python3 -Ic`), and a command handed through an environment variable (`GIT_EDITOR`, `GIT_SSH_COMMAND`,
|
|
73
|
+
`GIT_EXTERNAL_DIFF`, `GIT_PAGER`, `EDITOR`, …). `git maintenance register/start`, `git lfs install` and
|
|
74
|
+
`scalar register` now ask you.
|
|
75
|
+
|
|
10
76
|
## [0.133.0] - 2026-10-30
|
|
11
77
|
|
|
12
78
|
### Added
|
|
@@ -5077,7 +5143,8 @@ and long-lived sessions.
|
|
|
5077
5143
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
5078
5144
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
5079
5145
|
|
|
5080
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
5146
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.134.0...HEAD
|
|
5147
|
+
[0.134.0]: https://github.com/dm1try/samagotchi/compare/v0.133.0...v0.134.0
|
|
5081
5148
|
[0.133.0]: https://github.com/dm1try/samagotchi/compare/v0.132.0...v0.133.0
|
|
5082
5149
|
[0.132.0]: https://github.com/dm1try/samagotchi/compare/v0.131.0...v0.132.0
|
|
5083
5150
|
[0.131.0]: https://github.com/dm1try/samagotchi/compare/v0.130.1...v0.131.0
|
data/docs/cli.md
CHANGED
|
@@ -1026,6 +1026,17 @@ touch screen), and so does each code block of a rendered answer. An answer
|
|
|
1026
1026
|
copies its Markdown source, not the rendered text; a code block copies just
|
|
1027
1027
|
its code; a prompt copies the text as you typed it.
|
|
1028
1028
|
|
|
1029
|
+
A long answer (over 25 lines or 2,000 characters of its text) can be folded
|
|
1030
|
+
by hand: a slim "▾ N lines" row sits just above it once its turn has ended.
|
|
1031
|
+
Click it and the answer folds to one row, "▸ its first line · N lines" (the
|
|
1032
|
+
first line of what a hook presented in its place, else its first heading,
|
|
1033
|
+
else its first line); click again to unfold. A delegate report's reply
|
|
1034
|
+
folds the same way. Folding is this browser's choice, kept per session in
|
|
1035
|
+
localStorage (`chi_answer_fold:<session id>`, the last 200 folded answers;
|
|
1036
|
+
the keys of sessions gone from the list are dropped when the list loads);
|
|
1037
|
+
other viewers and the participant page are not affected. A jump into a
|
|
1038
|
+
folded answer (a claim link, say) unfolds it for that page load.
|
|
1039
|
+
|
|
1029
1040
|
### Web views
|
|
1030
1041
|
|
|
1031
1042
|
`web.view` picks how the page draws a turn: `stage` (the default: the
|
data/docs/configuration.md
CHANGED
|
@@ -1375,7 +1375,8 @@ launches it. It is recorded in `~/.config/samagotchi/home.json` (`$XDG_CONFIG_HO
|
|
|
1375
1375
|
- **Setting it.** `chi home set` records the Ruby your `chi` runs on and its gem path (a chruby shell's
|
|
1376
1376
|
`GEM_HOME`/`GEM_PATH` are part of it: that is where its chi lives); `chi home set --ruby PATH` records another
|
|
1377
1377
|
Ruby with its own gem dirs. Either needs samagotchi installed there (`gem install samagotchi` with that Ruby
|
|
1378
|
-
first).
|
|
1378
|
+
first). Under `bundle exec`, `chi home set` refuses to record its own Ruby (the bundle's gem path is no home);
|
|
1379
|
+
`--ruby` another Ruby still works. Without a home.json, the first `chi bootstrap` or `chi update` records one, once (from an installed chi,
|
|
1379
1380
|
never under `bundle exec`): of the desktop helper's Ruby (when its newest chi is at least the one running) and the
|
|
1380
1381
|
Ruby chi runs on, the one with the newer chi. Nothing else ever overwrites it.
|
|
1381
1382
|
`chi home` shows it; `chi home doctor` checks it (see [CLI](cli.md#commands)).
|
|
@@ -1396,7 +1397,7 @@ launches it. It is recorded in `~/.config/samagotchi/home.json` (`$XDG_CONFIG_HO
|
|
|
1396
1397
|
`home_older_than_spawner`), but chi web's restart runs the newest chi it knows outside the home instead
|
|
1397
1398
|
(`restart_home_older`): its flags may be ones the older chi doesn't know.
|
|
1398
1399
|
- **A home that still fails at boot** (a broken install): the worker writes one `boot_failed` line to the debug log
|
|
1399
|
-
(`log.file`) and exits, rather than leave the spawner waiting.
|
|
1400
|
+
(`log.file`) and exits, rather than leave the spawner waiting; `chi home doctor` reports the newest one.
|
|
1400
1401
|
- Each machine has its own home (a remote's is that machine's).
|
|
1401
1402
|
|
|
1402
1403
|
### Checking it: chi home doctor
|
|
@@ -1408,7 +1409,8 @@ fails (a launch can't boot what it should), 0 otherwise. Run it in a project fol
|
|
|
1408
1409
|
|---|---|---|
|
|
1409
1410
|
| the home | `fail`: its Ruby is gone (a Ruby upgrade) or has no samagotchi | `gem install samagotchi` with the new Ruby, then `chi home set` from it |
|
|
1410
1411
|
| this chi | `warn`: the chi you ran is another Ruby's, or another version, than the home's (`ok` for a source checkout, which starts its own code) | `chi home set` to make this one the home, or run the home's |
|
|
1411
|
-
| the
|
|
1412
|
+
| the last worker boot | `fail`: the newest worker boot in the debug log since the home was set is a `boot_failed` line (the home's Ruby couldn't load chi), not a worker's `start`; nothing without a log file | `gem install samagotchi` with the home's Ruby (or `chi home set` another), then `chi sessions restart ID` |
|
|
1413
|
+
| the desktop helper (when installed, and its chi reads this chi's config dir: none with `XDG_CONFIG_HOME` elsewhere) | `fail`: its launch file's Ruby or chi is gone; `warn`: it runs `[ruby, chi]` (installed before the home), boots the home as it was before a `chi home set`, or sets `GEM_HOME`/`GEM_PATH` | `chi desktop install --force` / `chi desktop upgrade` |
|
|
1412
1414
|
| sessions | `warn`: sessions last saved by a newer chi than the home's (their workers run without what it added) | `chi update` |
|
|
1413
1415
|
| the tools' `chi` | `warn`: `$XDG_STATE_HOME/samagotchi/bin/chi` boots another chi than the home | the next worker start rewrites it (`chi sessions restart ID` for a running session) |
|
|
1414
1416
|
| this project's Gemfile.lock | `warn`: the nearest Gemfile.lock up to the git root bundles samagotchi (`bundle exec chi` isn't supported) | `bundle remove samagotchi` there; run chi itself |
|
|
@@ -1530,7 +1532,9 @@ toolkit:
|
|
|
1530
1532
|
suggested`, and the turn's first request declares it; the notice reads
|
|
1531
1533
|
`toolkit: web (suggested, p=0.71) · /toolkit to change`. Anything else (the default, a low-confidence or unknown
|
|
1532
1534
|
pick, `undecided`, no answer, a failing System 1, which warns once as `[samagotchi:toolkit]`) keeps the default
|
|
1533
|
-
kit.
|
|
1535
|
+
kit. No answer in time or none at all says so in a warn notice, `toolkit: full (no suggestion: System 1 took over
|
|
1536
|
+
5000 ms) · /toolkit to change` (or `System 1 gave no answer`), and a timeout logs a `toolkit_suggest_timeout`
|
|
1537
|
+
warning: a slow System 1 host wants a larger `guardrails.system_one.timeout_ms`. A Stop during the wait (or before it, even with the answer in) ends the turn before its first request, and
|
|
1534
1538
|
nothing switches. After an `undecided`, a later prompt within the defer limits may switch the session; that notice
|
|
1535
1539
|
says `from this turn`, and that request re-reads the whole prompt (system prompt, tools and conversation, which
|
|
1536
1540
|
the defer limits keep small) without `/toolkit`'s cache confirmation. `/toolkit` or `ctx.use_toolkit` marks the
|
data/docs/guardrails.md
CHANGED
|
@@ -433,16 +433,46 @@ message:
|
|
|
433
433
|
project env ([Project env](sessions.md#project-env)); a command that runs
|
|
434
434
|
`mise` or `direnv` asks the owner (`ENV_LOADER`: they load the project's env
|
|
435
435
|
files, and `.envrc`, `mise.local.toml` and its kin are secrets like `.env`),
|
|
436
|
-
and so does a write (by a file tool or a command) to mise's config
|
|
437
|
-
(`MISE_CONFIG`: the owner's next worker start runs
|
|
436
|
+
and so does a write (by a file tool or a command) to mise's config, its
|
|
437
|
+
`.tool-versions` included (`MISE_CONFIG`: the owner's next worker start runs
|
|
438
|
+
its `[env]` and installs what it pins; a command asks when a part of it that
|
|
439
|
+
names one may write: `rm`, `mv`, `find … -delete` and `find … | xargs rm`
|
|
440
|
+
do, a read-only part such as `find . -name mise.toml && make` doesn't, the
|
|
441
|
+
list split at `;`, `&&`, `||` and newlines outside `( … )`; mise's own
|
|
442
|
+
writes, `mise use`/`mise set`, ask as runs of mise; a config that changes
|
|
443
|
+
without being named, by `git checkout`, `git stash pop`, `tar x`, `patch` or
|
|
444
|
+
`cp -r dir/. .`, isn't asked about: the owner's next restart runs it);
|
|
438
445
|
and a command still
|
|
439
446
|
running at the limit is killed, never handed over as a background task;
|
|
440
|
-
- git
|
|
441
|
-
|
|
442
|
-
|
|
447
|
+
- git config writes, whatever the key (any may make git run a command later
|
|
448
|
+
in the owner's name: hooks, `core.fsmonitor`, a filter, diff or merge
|
|
449
|
+
driver, a credential or gpg helper, `hook.<name>.command`, an include): a
|
|
450
|
+
pair turn's `git config` only reads, parsed strictly
|
|
451
|
+
(`PairCommand#git_config_read?`: `--get`, `--get-all`, `--get-regexp`,
|
|
452
|
+
`--get-urlmatch`, `--get-color(bool)`, `-l`/`--list`, `get`, `list` or a lone
|
|
453
|
+
key, with `--show-origin`, `--show-scope`, `--name-only`, `-z`, `--type=…`,
|
|
454
|
+
`--default`, `--file F` and the like, every word accounted for; any other
|
|
455
|
+
option, an abbreviation git would take (`--glob`, `--comm`), a short form
|
|
456
|
+
(`-t`) or a word too many is a write). Denied too: `git -c`, `--config-env`,
|
|
457
|
+
a clone's, init's or submodule's `-c`/`--config`, an assignment of
|
|
458
|
+
`GIT_CONFIG_*` or `GIT_TEMPLATE_DIR`, `git` under an alias, `git config` after
|
|
459
|
+
`xargs` or in `find -exec`, and an interpreter's script naming `git config`
|
|
460
|
+
or `git -c`, behind any of git's own options (`--attr-source X`,
|
|
461
|
+
`--shallow-file X`; one chi doesn't know is read both with and without the
|
|
462
|
+
next word as its value, `PairCommand.git_command_starts`), and by a
|
|
463
|
+
builtin's dashed name (`git-config`, `…/git-core/git-submodule foreach`:
|
|
464
|
+
judged as `git config`, `git submodule`). A command git runs now from its text (`git submodule foreach`,
|
|
465
|
+
`rebase --exec`, `bisect run`, a `filter-branch` filter, `--upload-pack`,
|
|
466
|
+
`--receive-pack`, `--extcmd`, an `ext::` URL) is read as a command of its
|
|
467
|
+
own and asks as an outside write. A `.gitattributes` write isn't asked
|
|
468
|
+
about: it only selects a driver, and defining one is what's denied;
|
|
443
469
|
and writing git's hooks: a `write` or `edit` of a path in a `.git/hooks`
|
|
444
|
-
folder (as given or as it
|
|
445
|
-
|
|
470
|
+
folder, a submodule's `.git/modules/<name>/hooks` too (as given or as it
|
|
471
|
+
resolves), or a command whose part that names one isn't read-only
|
|
472
|
+
(`printf … > .git/hooks/pre-commit`, `cp x .git/hooks/`), and installing
|
|
473
|
+
them: `git init`/`clone --template`, and the hook installers
|
|
474
|
+
(`PairCommand::HOOK_INSTALLERS`: `pre-commit install`, `husky`, `lefthook
|
|
475
|
+
install`, `simple-git-hooks`, `overcommit --install`);
|
|
446
476
|
- running chi, by any wrapper (`bin/chi`, `bundle exec chi`, `ruby … bin/chi`,
|
|
447
477
|
`env … chi`, `watch`, `npx`, `sh -c 'chi …'`, `$(chi …)`), in an assignment's
|
|
448
478
|
value or an alias, a function, after `-exec`/`-c`/`-e`, but `chi --version` and
|
|
@@ -482,7 +512,12 @@ message:
|
|
|
482
512
|
A script a command hands on is read too (`Guardrails::PairCommand`): one a
|
|
483
513
|
shell runs (`sh -c`, `eval`, a heredoc or here-string) as a command of its
|
|
484
514
|
own, one another interpreter runs (`ruby -e`, `python - <<EOF`) by its text
|
|
485
|
-
(chi, secrets, the environment, detaching).
|
|
515
|
+
(chi, secrets, the environment, detaching). So is a command handed through a
|
|
516
|
+
variable a program runs through a shell (`PairCommand::COMMAND_ENV`:
|
|
517
|
+
`GIT_EDITOR`, `GIT_SEQUENCE_EDITOR`, `EDITOR`, `VISUAL`, `GIT_EXTERNAL_DIFF`,
|
|
518
|
+
`GIT_SSH_COMMAND`, `GIT_SSH`, `GIT_PROXY_COMMAND`, `GIT_PAGER`, `PAGER`,
|
|
519
|
+
`MANPAGER`, `GIT_ASKPASS`, `SSH_ASKPASS`), as a prefix, `env`'s or `export`'s:
|
|
520
|
+
read as `sh -c` would run it, for the refusals and for an outside write. A heredoc written as data (`cat`,
|
|
486
521
|
`tee`, `git commit -F -`) is not looked into: where it goes is checked. A
|
|
487
522
|
command the checks can't read is refused, never let through on a text match.
|
|
488
523
|
|
|
@@ -502,7 +537,44 @@ rule lowers it), so it comes even in auto mode. A command fails closed
|
|
|
502
537
|
(`~/oth*`), `git -C`'s folder and the paths in a script it hands on (`sh -c`
|
|
503
538
|
read as a command, `python -c`/`ruby -e`/a heredoc by the paths in its text)
|
|
504
539
|
among them. The program's own word doesn't count (`/usr/bin/sed`), nor does
|
|
505
|
-
`PATH=…`, nor a message's heredoc (`git commit -m "$(cat <<'EOF' … EOF)"`)
|
|
540
|
+
`PATH=…`, nor a message's heredoc (`git commit -m "$(cat <<'EOF' … EOF)"`);
|
|
541
|
+
and it runs no program that writes to its own place outside with no word
|
|
542
|
+
naming it (`Guardrails::ImplicitWriters`, examples rather than a closed
|
|
543
|
+
set: `npm`/`pnpm`/`bun`/`deno` global in any spelling (`-g`, `--global=true`,
|
|
544
|
+
`--location global`, `npm_config_global=true`), `npm`/`yarn`/`bun link`,
|
|
545
|
+
`npm config set`/`login` (`~/.npmrc`), `yarn global`, `pip install`/`pip
|
|
546
|
+
config` by any interpreter (`python3.12 -m pip`, `pypy3 -mpip`; with or
|
|
547
|
+
without `--user`: its site-packages is wherever its Python is; a project
|
|
548
|
+
venv's `.venv/bin/pip` and `-t`/`--target`/`--prefix` don't count), `uv pip
|
|
549
|
+
install --system`, `gem install`, `bundle config set` without `--local`,
|
|
550
|
+
`brew` but its reading verbs, `defaults write`, `cargo install`, `go
|
|
551
|
+
install`/`go env -w`, `pipx`, `uv tool`/`uv python install`, `dotnet tool
|
|
552
|
+
install -g`, `corepack enable`, `rustup`/`pyenv`/`rbenv`/`asdf`/`volta`/`nvm`
|
|
553
|
+
installs and globals, `code --install-extension`, `gh auth`/`config
|
|
554
|
+
set`/`extension install`, `launchctl` but its reading verbs, `git maintenance register`/`start`, `git
|
|
555
|
+
lfs install`, `scalar register`/`clone`; a project
|
|
556
|
+
install such as `npm install` or `bundle install` and build caches don't
|
|
557
|
+
count), and no shell or interpreter that reads its script from stdin: one
|
|
558
|
+
on the receiving end of a pipe whatever its options (`… | sh`, `| bash -x`,
|
|
559
|
+
`| sh -e`, `| python3 -c 'exec(input())'`), `< file`, `bash -s`, `python3 -`,
|
|
560
|
+
or with no script at all: chi can't see that script. A shell's `-e`, `-x`
|
|
561
|
+
and `-o …` are flags, only `-c` (or a cluster holding it, `-ec`, `-cx`)
|
|
562
|
+
hands it a script: the first word after its options (`sh -c -- S`, `bash -c
|
|
563
|
+
-x S`); an interpreter's own code flag does (`python -c`, `ruby -e`,
|
|
564
|
+
`node -e`/`-p`/`--eval=`, `perl -e`/`-E`, `php -r`), alone or in a cluster
|
|
565
|
+
(`ruby -we`, `perl -le`, `node -pe`, `python3 -Ic`, `python3 -c'…'`;
|
|
566
|
+
`PairCommand.cluster_code_at`: a letter that takes the rest of the word,
|
|
567
|
+
`ruby -rset`, holds no code). A script file it names (`sh
|
|
568
|
+
run.sh`, `ruby x.rb`) is not read.
|
|
569
|
+
|
|
570
|
+
Accepted gaps (an accident guard for a trusted team): a script file the turn
|
|
571
|
+
wrote and then runs; inline scripts read only by their text for paths
|
|
572
|
+
(`find … -exec sh -c '…'`, `flock … sh -c`, `awk 'BEGIN{system(…)}'`,
|
|
573
|
+
`deno eval`); a `GIT_CONFIG_*` set by `printf -v` or `read` and exported
|
|
574
|
+
(it affects that command only); a variable outside `COMMAND_ENV` whose
|
|
575
|
+
value a program runs (`LESSOPEN`, a tool's own `*_EDITOR`); `npx -y`,
|
|
576
|
+
`npm exec`, `pnpm dlx`, `uvx`: they fetch into a cache, which doesn't count,
|
|
577
|
+
and run the package as in-project work.
|
|
506
578
|
|
|
507
579
|
Anything else asks: `rm -rf ~/x`, `sed -i … ~/x`, `curl -o ~/x`, `dd of=~/x`,
|
|
508
580
|
`touch`/`mkdir` outside, `perl -pi … ~/x`, `cd .. && make`, `cd && make`, and
|
|
@@ -629,7 +701,8 @@ repo is in count as outside.
|
|
|
629
701
|
`touches: chi_dirs` (shell tools) matches a command that names a path in chi's
|
|
630
702
|
own folders: the config folder (`config.yml`, `memories/`), the hooks folder,
|
|
631
703
|
the approval store, installed bundles (`memories/.bundles`), the session repo's
|
|
632
|
-
git hooks (`core.hooksPath` honoured) or any `.git/hooks`
|
|
704
|
+
git hooks (`core.hooksPath` honoured) or any `.git/hooks` (a submodule's `.git/modules/<name>/hooks` too)
|
|
705
|
+
outside a tmp folder.
|
|
633
706
|
Each word is read as a path against the folder the command is in by then
|
|
634
707
|
(`cd`, `pushd`/`popd` and `( … )` followed), with `~`, `$HOME` and
|
|
635
708
|
`$XDG_CONFIG_HOME`/`$XDG_STATE_HOME` expanded and symlinks resolved. So
|
|
@@ -657,7 +730,9 @@ only `run list|view`, `pr list|view|diff|checks`, `issue list|view` and
|
|
|
657
730
|
at a word's start), an env prefix, `sh -c`, `xargs`, `eval`, `sudo` and the
|
|
658
731
|
like make a command not read-only, as does a `task_create` `env:` that sets `BASH_ENV`, `ENV`, `CDPATH`, `IFS`,
|
|
659
732
|
`SHELLOPTS`, `BASHOPTS`, `BASH_FUNC_…`, `LD_PRELOAD`, `LD_AUDIT`, `DYLD_INSERT_LIBRARIES`, `GIT_EXTERNAL_DIFF`,
|
|
660
|
-
|
|
733
|
+
or a git config variable (`GIT_CONFIG`, `GIT_CONFIG_GLOBAL`, `GIT_CONFIG_SYSTEM`, `GIT_CONFIG_PARAMETERS`,
|
|
734
|
+
`GIT_CONFIG_COUNT`, `GIT_CONFIG_KEY_n`/`VALUE_n`: `ShellGitDirs::CONFIG_ENV`) (they change what its words run; `PATH`
|
|
735
|
+
doesn't count; a pair turn's command that assigns a git config variable is denied like `git -c`). It errs
|
|
661
736
|
towards asking.
|
|
662
737
|
|
|
663
738
|
A tool name may be a glob, so one rule covers a plugin's tools (an MCP server's,
|
|
@@ -1061,8 +1136,22 @@ gate below passes, in this order:
|
|
|
1061
1136
|
it knew the dir: a script or heredoc body (`python3 -c '…'`, `cat >> x.md
|
|
1062
1137
|
<<'EOF'`), a phrase (`echo "- a note"`) or a glob with no `/`. So
|
|
1063
1138
|
`cd memories; cd ..`, `cd "${PWD%/*}"`, `cd $(dirname $PWD)` or `cd -`
|
|
1064
|
-
loses the exemption, and the memories text then refuses it.
|
|
1065
|
-
|
|
1139
|
+
loses the exemption, and the memories text then refuses it. So does a
|
|
1140
|
+
script a shell or interpreter runs that climbs by its own calls
|
|
1141
|
+
(`SelfProtect::SCRIPT_CLIMB`: `os.pardir`, `dirname`, `chdir`, `Dir.pwd`,
|
|
1142
|
+
`Path.parent`, `rindex`, a slice from the end), and one chi can't see: a
|
|
1143
|
+
script file (`cd memories && python3 x.py`) or stdin (`… | python3`,
|
|
1144
|
+
`sh < f`, any interpreter or shell a pipe feeds, `| sh -x` too), by any
|
|
1145
|
+
version's name (`python3.12`, `pypy3`, `Rscript`, `tclsh`), or a program
|
|
1146
|
+
that runs a file (`SelfProtect::FILE_RUNNERS`: `make`, `rake`, `awk -f`,
|
|
1147
|
+
`deno run`, `npm run`). A heredoc only written (`cat >> x.md <<'EOF'`) is
|
|
1148
|
+
data whatever it says. A script body is not looked into beyond its text:
|
|
1149
|
+
an accident guard, not containment (a script can build a path some other
|
|
1150
|
+
way: `p.replace("memories", "config.yml")`, `p[0:len(p)-8]`, `.sub`,
|
|
1151
|
+
`.slice` aren't caught). The price is some harmless commands the user now
|
|
1152
|
+
answers: `python3 scripts/lint.py <memories>/notes.md`, `cat notes.md |
|
|
1153
|
+
python3 -c …` from the memories folder, a climb word written as data by a
|
|
1154
|
+
script (`File.write(".../notes.md", "dirname")`).
|
|
1066
1155
|
A protected rule
|
|
1067
1156
|
(`Verdict::PROTECTED_RULES`) may be listed by name and System 1 may answer
|
|
1068
1157
|
it, but a call that really writes config.yml or the hooks is refused here;
|
|
@@ -45,6 +45,7 @@ Behavior:
|
|
|
45
45
|
- `execute` and `task_create` both spawn with chi's own Ruby/Bundler setup scrubbed (`TaskRuntime.sanitized_spawn_env`): `RUBYOPT` loses its `-r…bundler/setup` flags (others stay) and `RUBYLIB`, `BUNDLE_GEMFILE`, `BUNDLE_BIN_PATH`, `BUNDLER_VERSION` are unset, so a project's `ruby -e` or `bundle exec` uses its own Gemfile when chi runs under `bundle exec`. `task_create` refuses those keys in the model's `env`; `execute`'s `env` comes from chi's callers (`Builtins`, `!cmd`), not the model, and is set over the scrub. An `execute` handed over at its limit keeps the process it spawned, so the task runs with the same env.
|
|
46
46
|
- `execute` and `task_create` export chi's own markers into the command, set over the model's `env` (which can't set them: `Error: env key is reserved`): `SAMAGOTCHI_PARENT_SESSION` (the session's id, `chi` without one; a chi started there answers as a parent agent), `SAMAGOTCHI_SESSION_MODEL` (the resolved ref the session runs on, read at each call so it follows `/model`; unset when unknown) `SAMAGOTCHI_SESSION_THINKING` (the session's own thinking level; unset when it has none) and `SAMAGOTCHI_SESSION_REASONING_HISTORY` (its own reasoning_history level, the same way). `chi self` reads the last three for its model, thinking level and reasoning history. The four are `ChildEnv::SESSION_MARKERS` (`TaskRuntime::MARKER_ENV_KEYS`), which a spawned worker unsets (`ChildEnv.unset`, merged into its spawn env). A session's worker never inherits them from whoever started it (a `chi send` run by another session's `execute`): its other commands (a user's `!cmd`, an MCP server, a context command) have none.
|
|
47
47
|
- They also pass on a set `XDG_STATE_HOME` / `XDG_CONFIG_HOME` made absolute (`Builtins.xdg_absolute`, via `Paths`), so a chi the command runs in another `cwd` (`chi send`) reads this chi's state and config. These aren't reserved: an XDG dir the model sets in `task_create`'s `env` stays its own.
|
|
48
|
+
- Every runner starts from the session's project env, `ProjectEnv.tools_env`: the overlay a spawned worker computed once at boot (`SessionManager.apply_project_env` → `ProjectEnv.compute` → `ProjectEnv.install!`; its folder's `mise env`, and the chi shim's dir first on PATH so `chi` is the chi home's), less `ProjectEnv::TOOLS_DROP` (`RUBYOPT`, `RUBYLIB`, Bundler's: the scrub above sets those). Outside a worker (`ProjectEnv::NONE`) it is `{}`. The sites: `execute` and `task_create` (`Tools::Builtins#project_env`, under the markers and the model's `env`; a participant's turn gets `{}`), the user's `!cmd` (`SessionCommands#shell`) and a context source's command (`ContextFetch#env_for`). MCP servers take `ProjectEnv.current.env` whole under the server's own keys (`bundles/mcp/plugin.rb` `server_env`: no `TOOLS_DROP`, no participant rule). chi's own process env is never changed, and the project env never enters the prompt. User docs: [Project env](../sessions.md#project-env); the launch side: [How chi launches itself](../architecture.md#how-chi-launches-itself).
|
|
48
49
|
|
|
49
50
|
## Finish wake
|
|
50
51
|
|
|
@@ -195,11 +195,12 @@ Everything else is `:owner`: `/answer`, `/command`, `/images` (an image a prompt
|
|
|
195
195
|
to act: the turn only reads (`Guardrails::ReviewBoundary`), and proposes what the owner applies ([Apply](#apply)).
|
|
196
196
|
- **`:pair`**, a working session's participant invited to act: the turn acts with the session's tools under the
|
|
197
197
|
owner's guardrails, its asks go to the owner and offer once only, and `Guardrails::PairBoundary` denies secrets,
|
|
198
|
-
chi itself and chi web on loopback, background and scheduled jobs, the environment and credential printers, git
|
|
199
|
-
hooks, owner-voiced tools and the owner's task outputs, and the model's own question, and asks before a command
|
|
200
|
-
that makes a link and before a write that may land outside the project, failing closed (a
|
|
201
|
-
|
|
202
|
-
|
|
198
|
+
chi itself and chi web on loopback, background and scheduled jobs, the environment and credential printers, git
|
|
199
|
+
config writes (reads only) and git's hooks, owner-voiced tools and the owner's task outputs, and the model's own question, and asks before a command
|
|
200
|
+
that makes a link and before a write that may land outside the project, failing closed (a global install or a
|
|
201
|
+
user config write that names no path, `Guardrails::ImplicitWriters`, and a script read from stdin count; a
|
|
202
|
+
session at or above home has none: every write asks) ([guardrails](../guardrails.md#a-participants-pair-boundary)).
|
|
203
|
+
No Apply: it acted itself.
|
|
203
204
|
|
|
204
205
|
`ParticipantMode.of(session, features:)` is the session's: what a participant invited now gets. No session is
|
|
205
206
|
`:review`. A worker names `pair_turns` (`Delivery::PAIR_FEATURE`) when it runs pair turns; one without it runs
|
|
@@ -50,6 +50,8 @@ Configuration (config.yml `execute.*` keys and CLI flags too, as for `read`):
|
|
|
50
50
|
- `SAMAGOTCHI_EXECUTE_TIMEOUT_SEC` (`execute.timeout_sec`, default `120`): what a command still running then gets is `execute.on_timeout`.
|
|
51
51
|
- `SAMAGOTCHI_EXECUTE_ON_TIMEOUT` (`execute.on_timeout`, default `task`): `task` hands the command over as a background task, not killed; the result starts `Still running at the 120s limit: moved to a background task, not killed.` (`Execute::HANDED_OVER`), then its `task_id:` and the tails of stdout and stderr so far, and `task_wait` waits for it. `kill` stops it and answers `Error: command timed out after …s` (`Execute::TIMEOUT_PREFIX`) with the hint to use `task_create`. The `!cmd` bang always kills. See [Background tasks](background-tasks.md).
|
|
52
52
|
|
|
53
|
+
The env a command runs in (the session's project env, `ProjectEnv.tools_env`, then the Ruby/Bundler scrub and chi's markers) is the same for `execute`, `task_create`, `!cmd` and context sources: see [Background tasks](background-tasks.md).
|
|
54
|
+
|
|
53
55
|
Implementation note:
|
|
54
56
|
|
|
55
57
|
- Shared logic lives in `lib/samagotchi/tools/output_guardrails.rb` and is used by `read` and `execute`, and by `edit` (its env switches) and `delegate_wait` (the head+tail preview of a child's long reply).
|
data/docs/plugins.md
CHANGED
|
@@ -1310,7 +1310,8 @@ bundle.shutdown
|
|
|
1310
1310
|
value, or raises `Samagotchi::Testing::Waiting::TimedOut` ("timed out after 5 s waiting for <what>", plus
|
|
1311
1311
|
`; last saw: <value>` from the `seen:` callable). `join!(thread, what)` and `pop!(queue, what)` are
|
|
1312
1312
|
`Thread#join` and `Queue#pop` with that deadline and message; `wait_until { }` returns the block's last value
|
|
1313
|
-
instead of raising, for a wait whose timeout is fine; `process_gone?(pid)
|
|
1313
|
+
instead of raising, for a wait whose timeout is fine; `process_gone?(pid)`; `time_bound(seconds, below: nil)`, a
|
|
1314
|
+
wall-clock upper bound scaled the same way and kept under `below:` (the unscaled deadline it tells apart from). Every deadline is multiplied by
|
|
1314
1315
|
`SPEC_TIME_SCALE` (default 1, at least 1; chi's CI sets 2), so a slow runner gets headroom.
|
|
1315
1316
|
- **Model notes:** `sandbox.model_notes_for(model_id, small: nil, cwd: nil)` lists the model notes chi would load
|
|
1316
1317
|
into the prompt for a bare model id (`deepseek/deepseek-v4.1-flash`, no host prefix), from the sandbox's
|
data/docs/sessions.md
CHANGED
|
@@ -396,8 +396,9 @@ mise.toml pins Ruby 3.4 gets that `ruby`, `gem` and `bundle` in its tools while
|
|
|
396
396
|
- **Pair turns** (a participant's, see [Participants](internals/participants.md)) run their `execute` without the
|
|
397
397
|
project env. MCP servers are the session's, started once in the project env, so a pair turn's MCP calls do use
|
|
398
398
|
it. A participant's command that runs `mise` or `direnv`, and any write to mise's config (`mise.toml`,
|
|
399
|
-
`.mise.toml`, `mise.<env>.toml`, `.config/mise.toml`, `.config/mise/…`, `mise/` or `.mise/config*.toml
|
|
400
|
-
next worker start runs it, and its `[env]` can run commands),
|
|
399
|
+
`.mise.toml`, `mise.<env>.toml`, `.config/mise.toml`, `.config/mise/…`, `mise/` or `.mise/config*.toml`, and
|
|
400
|
+
`.tool-versions`: your next worker start runs it, and its `[env]` can run commands), a delete or move of one
|
|
401
|
+
included, asks you first; a read-only mention (`find . -name mise.toml`) doesn't. `.envrc` and mise's local files
|
|
401
402
|
(`mise.local.toml`, `mise.<env>.local.toml`, dotted or not, and `config.local.toml` / `config.<env>.local.toml` in
|
|
402
403
|
`.config/mise/`, `mise/`, `.mise/`) are secrets to them, as `.env` is.
|
|
403
404
|
- **What tools still drop**: every `execute`, task and `!cmd` takes no `RUBYOPT`, `RUBYLIB`, `BUNDLE_GEMFILE`,
|
data/docs/testing.md
CHANGED
|
@@ -95,6 +95,9 @@ run, CI) and too long on a fast one. The contract a new spec follows:
|
|
|
95
95
|
4. **"Nothing happens" is shown by counting passes** (the worker's loop rounds, spec/support/worker_loop_rounds.rb;
|
|
96
96
|
a class's ticks), not by a pause.
|
|
97
97
|
5. **A wall-clock bound** only as "not the full N s", with at least 5× headroom measured on the slow (x86) laptop.
|
|
98
|
+
Write it `be < time_bound(3, below: 5)`: the bound scaled like a helper's deadline, and kept under the unscaled
|
|
99
|
+
deadline it tells apart from (here a 5 s read timeout), so the check still fails on the full wait at any scale. A
|
|
100
|
+
cost bound with nothing to tell apart from ("no quadratic blow-up") takes no `below:`.
|
|
98
101
|
|
|
99
102
|
The helpers are the bundle test kit's (`Samagotchi::Testing::Waiting`, [plugins.md](plugins.md#testing-a-bundle-experimental)),
|
|
100
103
|
so core and bundle specs share one copy; `SpecWaiting` (spec/support/waiting.rb) includes them in every example
|
|
@@ -107,6 +110,7 @@ group and also answers them as module calls (`SpecWaiting.pop!`, for a stubbed l
|
|
|
107
110
|
same deadline and message. A gate a stubbed lib thread waits on takes `timeout: 30`: it only ends a hang.
|
|
108
111
|
- `wait_until(timeout:) { … }` returns the block's last value (falsy at the deadline): for a value the example
|
|
109
112
|
checks after, or a `wait-ok` wait.
|
|
113
|
+
- `time_bound(seconds, below: nil)`: a wall-clock upper bound, scaled and kept under `below:` (rule 5).
|
|
110
114
|
- `eventually(timeout: 5) { expect(…) }` (spec only) retries until the expectations hold, then re-raises the last
|
|
111
115
|
failure prefixed `still failing after 5 s:`, so the matcher's own expected/got is in the message.
|
|
112
116
|
|
|
@@ -129,8 +133,8 @@ Every example also runs under a 120 s limit (`spec_helper`'s `around`; `integrat
|
|
|
129
133
|
limit, `0` turns it off (a debugger session). The limit is not scaled.
|
|
130
134
|
|
|
131
135
|
**The cops.** `Samagotchi/SpecSleep` (an untagged `sleep` in a spec) and `Samagotchi/SilentWait` (a `wait_until`
|
|
132
|
-
whose value is dropped, an example's last line included, `expect(wait_until { … }).to be(true)`,
|
|
133
|
-
`Timeout.timeout`) live in `rubocop/` and run in `rake lint`, so CI fails on them. RSpec counts `wait_for`,
|
|
136
|
+
whose value is dropped, an example's last line included, `expect(wait_until { … }).to be(true)`, an untagged
|
|
137
|
+
`Timeout.timeout`, or a statement `thread.join(N)`, whose N is unscaled and whose nil is dropped: use `join!`) live in `rubocop/` and run in `rake lint`, so CI fails on them. RSpec counts `wait_for`,
|
|
134
138
|
`eventually`, `join!` and `pop!` as an example's expectation (`RSpec/NoExpectationExample`), so a wait can be an
|
|
135
139
|
example's last line. What the cops can't see, `script/affected-specs` hints at (`timing:`, below).
|
|
136
140
|
|
|
@@ -184,6 +188,7 @@ any other file requires it, so a spec file run alone still finds its requires. E
|
|
|
184
188
|
| `fake_provider_server.rb` | `FakeProviderServer`: an in-process HTTP provider replaying spec/fixtures/providers, recording requests |
|
|
185
189
|
| `fake_tailscale.rb` | A fake `tailscale` for `Web::Tunnel::Tailscale` (status, serve, lock from JSON files; argv.log) |
|
|
186
190
|
| `git_repo.rb` | *helper*: `git_repo`, a temp folder with `git init`, removed after the example |
|
|
191
|
+
| `lock_holder.rb` | *helper*: `spawn_lock_holder(session_dir)`, a stand-in worker process holding a session's owner lock, which says so on a pipe |
|
|
187
192
|
| `integration_server.rb` | *helper*: the live model server `:integration` examples use, from `SAMAGOTCHI_INTEGRATION_*` ([Integration specs](#integration-specs)) |
|
|
188
193
|
| `mcp_bundle.rb` | The shipped mcp bundle's path and the fake MCP server its specs start (`MCP_FAKE_COMMAND`, no RubyGems) |
|
|
189
194
|
| `no_console.rb` | Loaded first (`ruby -r`) by spawned bin/chi children: `IO.console` is nil, so output never wraps at a terminal's width |
|
data/lib/samagotchi/chi_home.rb
CHANGED
|
@@ -42,6 +42,15 @@ module Samagotchi
|
|
|
42
42
|
class Invalid < StandardError
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
# A record of this process under Bundler (`bundle exec chi home set`)
|
|
46
|
+
# would name the bundle's Ruby and gems as the home every launch boots.
|
|
47
|
+
class UnderBundler < StandardError
|
|
48
|
+
def initialize(msg = "under Bundler (bundle exec) this chi's gem path is the bundle's, not a chi home: " \
|
|
49
|
+
"run chi itself, or name another Ruby with --ruby")
|
|
50
|
+
super
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
45
54
|
# home.json's fields. +gem_path+ is what `Gem.paths=` takes (dirs joined
|
|
46
55
|
# by File::PATH_SEPARATOR); +mise+ an absolute path or nil.
|
|
47
56
|
Record = Data.define(:ruby, :gem_home, :gem_path, :mise, :set_at, :set_by_version) do
|
|
@@ -274,8 +283,9 @@ module Samagotchi
|
|
|
274
283
|
nil
|
|
275
284
|
end
|
|
276
285
|
|
|
277
|
-
#
|
|
278
|
-
#
|
|
286
|
+
# Whether this process runs under Bundler (`bundle exec`), where a
|
|
287
|
+
# record of it would name the bundle's gems: record_first!, chi home
|
|
288
|
+
# set (UnderBundler) and chi update's @bundler.
|
|
279
289
|
def under_bundler?(env = ENV) = !!(defined?(::Bundler) || !env["BUNDLE_GEMFILE"].to_s.empty?)
|
|
280
290
|
|
|
281
291
|
# mise's absolute path: on +env+'s PATH, else in MISE_DIRS; nil without.
|
|
@@ -259,6 +259,24 @@ module Samagotchi
|
|
|
259
259
|
"the desktop helper's launch file sets #{baked.join(", ")} for its sessions' tools: chi desktop upgrade"
|
|
260
260
|
end
|
|
261
261
|
|
|
262
|
+
# The config dir the helper's chi reads (its home.json, config.yml):
|
|
263
|
+
# the launch file's XDG_CONFIG_HOME when baked, else ~/.config.
|
|
264
|
+
# @return [String]
|
|
265
|
+
def launch_config_home
|
|
266
|
+
baked = read_launch["env"]
|
|
267
|
+
dir = baked["XDG_CONFIG_HOME"] if baked.is_a?(Hash)
|
|
268
|
+
# Spelled out: Paths.config_home's ~ is this process's HOME, not @env's.
|
|
269
|
+
dir.to_s.empty? ? File.join(@env["HOME"] || Dir.home, ".config") : dir
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Whether the helper's chi reads this chi's config dir (home.json,
|
|
273
|
+
# config.yml): false for a run with XDG_CONFIG_HOME elsewhere (a spec,
|
|
274
|
+
# a smoke run), whose home and bundles the helper never sees, though
|
|
275
|
+
# its launch file sits under the real ~/Library.
|
|
276
|
+
def reads_this_config?
|
|
277
|
+
File.expand_path(launch_config_home) == Paths.config_home(env: @env)
|
|
278
|
+
end
|
|
279
|
+
|
|
262
280
|
# The chi version the launch file names, nil without one.
|
|
263
281
|
def launch_version = read_launch["version"]
|
|
264
282
|
|
|
@@ -316,10 +334,7 @@ module Samagotchi
|
|
|
316
334
|
# reads: the launch file's XDG_CONFIG_HOME when baked; disabled hosts
|
|
317
335
|
# are left out. @return [Array<Hash>] name:, hosts:, state:
|
|
318
336
|
def host_keys
|
|
319
|
-
|
|
320
|
-
dir = baked["XDG_CONFIG_HOME"] if baked.is_a?(Hash)
|
|
321
|
-
# Spelled out: Paths.config_home's ~ is this process's HOME, not @env's.
|
|
322
|
-
config_env = { "XDG_CONFIG_HOME" => dir.to_s.empty? ? File.join(@env["HOME"] || Dir.home, ".config") : dir }
|
|
337
|
+
config_env = { "XDG_CONFIG_HOME" => launch_config_home }
|
|
323
338
|
hosts = ConfigFile.hosts_config(env: config_env).values.select(&:api_key_env).group_by(&:api_key_env)
|
|
324
339
|
hosts.map do |name, entries|
|
|
325
340
|
state = if HostKeys.from_keychain(name, env: @env) then :keychain
|
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -3044,6 +3044,15 @@ module Samagotchi
|
|
|
3044
3044
|
return if pending.nil?
|
|
3045
3045
|
|
|
3046
3046
|
outcome, judged = pending.wait(turn.controller)
|
|
3047
|
+
if outcome == :timeout
|
|
3048
|
+
# The session starts on its default without a word otherwise: a
|
|
3049
|
+
# slow System 1 host (a local one) is the usual cause.
|
|
3050
|
+
timeout_ms = SystemOne::Client.configured_timeout_ms
|
|
3051
|
+
Log.warn(:toolkit, "toolkit_suggest_timeout", session_id: turn.session.id, timeout_ms: timeout_ms,
|
|
3052
|
+
reason: "System 1 gave no answer within guardrails.system_one.timeout_ms")
|
|
3053
|
+
@toolkit_binding.fell_back(turn.session, "System 1 took over #{timeout_ms} ms")
|
|
3054
|
+
return
|
|
3055
|
+
end
|
|
3047
3056
|
unless outcome == :answered
|
|
3048
3057
|
Log.info(:toolkit, "toolkit_suggest_#{outcome}", session_id: turn.session.id)
|
|
3049
3058
|
return
|
|
@@ -3070,6 +3079,10 @@ module Samagotchi
|
|
|
3070
3079
|
deferred: ToolkitSuggest.model_reply?(session.messages))
|
|
3071
3080
|
switched ? "suggested" : "chosen"
|
|
3072
3081
|
when "defer" then "defer"
|
|
3082
|
+
when "no_judgment"
|
|
3083
|
+
# System 1 failed (its own warn) or answered nothing usable.
|
|
3084
|
+
@toolkit_binding.fell_back(session, "System 1 gave no answer")
|
|
3085
|
+
"default"
|
|
3073
3086
|
else "default"
|
|
3074
3087
|
end
|
|
3075
3088
|
end
|