samagotchi 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +54 -1
  3. data/docs/configuration.md +1 -0
  4. data/docs/guardrails.md +48 -2
  5. data/docs/memory.md +5 -3
  6. data/docs/plugins.md +13 -5
  7. data/docs/releasing.md +1 -1
  8. data/lib/samagotchi/bundles/guardrails/guardrails/small-models.yml +27 -0
  9. data/lib/samagotchi/bundles/guardrails/manifest.yml +4 -2
  10. data/lib/samagotchi/bundles/skills/manifest.yml +3 -3
  11. data/lib/samagotchi/bundles/skills/plugin.rb +61 -11
  12. data/lib/samagotchi/bundles/system/identity.md +1 -1
  13. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  14. data/lib/samagotchi/bundles/system/memory_guide.md +5 -4
  15. data/lib/samagotchi/config.rb +3 -0
  16. data/lib/samagotchi/engine.rb +13 -0
  17. data/lib/samagotchi/guardrail_wiring.rb +4 -1
  18. data/lib/samagotchi/guardrails/gate.rb +7 -4
  19. data/lib/samagotchi/guardrails/model_size.rb +87 -0
  20. data/lib/samagotchi/guardrails/rules.rb +35 -3
  21. data/lib/samagotchi/guardrails/targets.rb +29 -5
  22. data/lib/samagotchi/guardrails.rb +1 -0
  23. data/lib/samagotchi/log_subscriber.rb +5 -1
  24. data/lib/samagotchi/memory_bundle/index_sync.rb +73 -0
  25. data/lib/samagotchi/memory_bundle/installer.rb +24 -3
  26. data/lib/samagotchi/session_commands.rb +6 -1
  27. data/lib/samagotchi/session_manager.rb +2 -2
  28. data/lib/samagotchi/session_metrics.rb +2 -1
  29. data/lib/samagotchi/sessions_command.rb +3 -2
  30. data/lib/samagotchi/system_prompt.rb +2 -1
  31. data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -1
  32. data/lib/samagotchi/tool_declarations.rb +1 -1
  33. data/lib/samagotchi/tool_runner.rb +20 -3
  34. data/lib/samagotchi/tools/memory.rb +1 -2
  35. data/lib/samagotchi/version.rb +1 -1
  36. metadata +4 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4db526cc3c79a4e5aa8061f124ff09e65e289415074fcb51c71c25ccee1ab0c9
4
- data.tar.gz: c30c2094cb357df9fe4e5618956e0c1a559ea0a2765709605488d03c5febbb87
3
+ metadata.gz: 9290117f26040b725e12c5cd18bf2a8858a573f34e2323e5a56517c030f95d7f
4
+ data.tar.gz: 7e5d1e2e8a285424b907172038f6bab1045b1388129241cc1a32cb26132437ae
5
5
  SHA512:
6
- metadata.gz: 2ad7fd71ecfee430c9dec6d9b51971b1e7fdc38c099c5f4a868ad07add50a21ac293f6c686e5f8aca9b0d3bd919ee49e1d79336ac1fe1ac4926d6ace2de24adb
7
- data.tar.gz: 5856a9fe2fcb23b4802626ee918e7015cc718d74b4d17828265377a606cb76fdf80badb3f5d0e314c76bcd3ceb4629fc3f79ab735275fd102c1a2895b54b4f3d
6
+ metadata.gz: d70bd257f5b82a2057077e2142adf5601f4bc652a0e820c695b5ecff305156c0270815374054e41d6403ecb0ee4579fd75fda84f670094e99a21f7cf73e34b5c
7
+ data.tar.gz: 961269d190e236ecbe3a3ce0dae6d18d86adf93f5c16aa8246dd91e10a43b73152c8595a4e6377491aaf86e74d8558b225893364dc6dd226634d6b94a7484504
data/CHANGELOG.md CHANGED
@@ -8,6 +8,57 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.9.0] - 2026-10-01
12
+
13
+ ### Changed
14
+
15
+ - A small change to a memory or skill can go through `edit` on its file instead of rewriting it all with
16
+ `memory_write`; the prompt, the memory guide and the nudge below say so. `write`/`edit` on a memory's file
17
+ refresh its line in `index.md` (date, size; the description is kept), and the default guardrails no longer ask
18
+ before them as "writes outside the repository".
19
+ - The system prompt no longer tells the model to keep `index.md` updated (`memory_write` does it); its result
20
+ says "Index line refreshed automatically." instead of naming the index file.
21
+ - `skills` bundle 0.1.1 (`chi update`; needs chi 0.9.0): a skill changed some other way (an `execute` running `sed`) counts as
22
+ updated: you get the usual "skill X updated" line, `/skill diff` has the old text, and the nudge and the turn-end
23
+ "wasn't updated" line no longer fire for it. The nudge asks to `edit` the changed step rather than rewrite the
24
+ whole skill.
25
+
26
+ ### Fixed
27
+
28
+ - `chi bundle install` of a bundle that's already installed no longer says "Skipped X (already exists; use --force
29
+ to overwrite)" for a file that's the same as the bundle's; it skips it quietly and says once that
30
+ `chi bundle upgrade NAME` updates the bundle and keeps local edits. A file skipped for a local edit no longer
31
+ also reports a checksum mismatch.
32
+ - `chi bundle install` and `upgrade` no longer list a bundle's unchanged guardrail rule files as "Installed"; they
33
+ are skipped as already up to date like its memory files.
34
+ - A memory file you edited, then `chi bundle install` of its bundle again (skipped for the edit), no longer gets
35
+ overwritten by the next `chi bundle upgrade`: the re-install kept your edit as the bundle's base, so the upgrade
36
+ took it as unedited.
37
+ - `chi sessions list --sort/--order` is no longer ignored with `--live`, `--cwd` or `--format`.
38
+ - A tool's time in the web's turn timing and the debug log's `tool_call_completed ms:` no longer counts the wait for
39
+ a guardrail approval; the log line shows the wait as `waited_ms:`.
40
+
41
+ Update with `chi update` (bundles: `skills` 0.1.1).
42
+
43
+ ## [0.8.1] - 2026-10-01
44
+
45
+ ### Added
46
+
47
+ - Guardrail rules can name the models they apply to (`models: small`, or globs on the model name). The guardrails
48
+ bundle (0.2.0, `chi update`; needs chi 0.8.1) adds two asks only small models get: `git checkout -- <file>` /
49
+ `git restore` (discards uncommitted changes) and `git stash drop`/`clear`. Small is `guardrails.small_models`:
50
+ `auto` (the default: 32B or less by the size in the model's name, an MoE's active size counting), a list of
51
+ globs, or `[]` for none. A chi older than 0.8.1 that shares the bundles dir (a checkout next to the gem, or a
52
+ worker still running after a gem update) doesn't know `models:` and denies every tool call until it is
53
+ restarted on the new chi.
54
+
55
+ ### Fixed
56
+
57
+ - A tool row's time in the REPL and the attached TUI no longer counts the wait for a guardrail approval
58
+ (`blocked (2m 54s)` for a call denied at once).
59
+ - docs/guardrails.md: rules are read again on the next tool call after config.yml or a bundle's rules change, with
60
+ no restart.
61
+
11
62
  ## [0.8.0] - 2026-10-01
12
63
 
13
64
  ### Added
@@ -462,7 +513,9 @@ and long-lived sessions.
462
513
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
463
514
  hotkey panel that send selected text or the clipboard to your sessions.
464
515
 
465
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.8.0...HEAD
516
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.9.0...HEAD
517
+ [0.9.0]: https://github.com/dm1try/samagotchi/compare/v0.8.1...v0.9.0
518
+ [0.8.1]: https://github.com/dm1try/samagotchi/compare/v0.8.0...v0.8.1
466
519
  [0.8.0]: https://github.com/dm1try/samagotchi/compare/v0.7.0...v0.8.0
467
520
  [0.7.0]: https://github.com/dm1try/samagotchi/compare/v0.6.0...v0.7.0
468
521
  [0.6.0]: https://github.com/dm1try/samagotchi/compare/v0.5.1...v0.6.0
@@ -753,6 +753,7 @@ described in their own sections.
753
753
  | `image.max_bytes` | `3750000` | | See "Images". |
754
754
  | `image.max_per_request` | `20` | | See "Images". |
755
755
  | `guardrails.enabled` | `true` | | See [Guardrails](guardrails.md). |
756
+ | `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 `[]`. See [Guardrails](guardrails.md#rules-for-some-models). |
756
757
  | `log.file` | state dir | yes | See "Debug Log File". |
757
758
  | `log.disable` | `false` | yes | No file logging. |
758
759
  | `log.level` | `info` | yes | `debug`, `info`, `warn`, `error`. |
data/docs/guardrails.md CHANGED
@@ -126,6 +126,12 @@ All the fields a rule gives must match. Absolute and `**/` globs match the
126
126
  resolved path; other globs match the path relative to the repo root. Paths
127
127
  resolve the way the tools resolve them (against the cwd; `~` expanded).
128
128
 
129
+ `outside_repo` leaves out a memory's file: a `*.md` right in the project or
130
+ system memories folder (not `index.md`, not a hidden file). `write`/`edit`
131
+ there is what `memory_write` does unasked, and the model is told to `edit` a
132
+ memory for a small change. The memories folder's `index.md`, `.bundles/`, and
133
+ the rest of chi's config folder still count as outside.
134
+
129
135
  To switch off single rules (a bundle's, say) without editing its files, list
130
136
  them under `disable:`. A plain id switches off every rule with that id; `bundle:id`
131
137
  only that bundle's:
@@ -138,8 +144,40 @@ guardrails:
138
144
  `/guardrails` marks them `disabled (guardrails.disable)` and names entries that
139
145
  match no rule. `disable:` only removes rules; hooks and the core checks still vote.
140
146
 
141
- Rules load when chi starts (a long-running worker picks up changes after its
142
- next start). A rule that doesn't parse (an unknown key, a bad regex, no
147
+ ### Rules for some models
148
+
149
+ `models:` makes a rule vote only for some models: `small`, or a glob on the
150
+ model's name (`Qwen3.6-*`, matched on the bare name without the host prefix and
151
+ on the model key `qwen3-6-27b`, case-insensitively), or a list of them. Without
152
+ `models:` a rule is for every model. With no model name set, a `models:` rule
153
+ doesn't vote.
154
+
155
+ ```yaml
156
+ guardrails:
157
+ small_models: auto # the default; or a list of globs, or [] for none
158
+ rules:
159
+ - id: no-force-push-small
160
+ tool: shell
161
+ command: '\bgit\s+push\b.*--force'
162
+ models: small # or "gemma-*", or [small, "deepseek-*"]
163
+ verdict: deny
164
+ ```
165
+
166
+ Which models are small is `guardrails.small_models`. Nothing reports a model's
167
+ size, so `auto` reads it from the name: `Qwen3.6-27B` is 27B, `gemma-4-E4B-it`
168
+ 4B, `Mixtral-8x7B` 56B, and for an MoE the active size counts
169
+ (`Ornith-1.5-35B-A3B` is 3B, `Qwen3-235B-A22B` 22B). 32B or less is small. A name
170
+ without a size (`deepseek-v4.1-flash`) is not small. A list of globs names the
171
+ small models yourself (`auto` may be one of them), and `[]` means no model is
172
+ small. It is read on every check, so `/model` and config edits apply at once.
173
+
174
+ `/guardrails` names the model and whether it is small (`model: Qwen3.6-27B —
175
+ small (auto, 27B)`), shows each rule's `models`, and marks a rule that doesn't
176
+ vote for the current model `off for this model`.
177
+
178
+ Rules are read again on the next tool call after `config.yml` or an installed
179
+ bundle's rules change, so a long-running worker follows edits without a
180
+ restart. A rule that doesn't parse (an unknown key, a bad regex, no
143
181
  verdict, a `disable:` that isn't a list of ids) makes chi **deny every tool call** and say why, rather than run
144
182
  without it.
145
183
 
@@ -157,6 +195,14 @@ repo; and shell commands that name chi's config, hooks or guardrails or
157
195
  `.git/hooks`. It denies writes into `.git/hooks`. The rules are in
158
196
  `lib/samagotchi/bundles/guardrails/guardrails/rules.yml`.
159
197
 
198
+ Small models (`models: small`, see above) get two more asks, in
199
+ `guardrails/small-models.yml`: `git checkout -- <path>`, `git checkout <rev> --
200
+ <path>`, `git checkout .` and `git restore <path>` (not `--staged` alone), which
201
+ discard uncommitted changes, and `git stash drop`/`clear`. They offer only
202
+ "once" and "this session", so an approval doesn't silence them for good. To get
203
+ them on every model, set `small_models` to `"*"`; to drop them, `[]` (or
204
+ `disable:` the ids).
205
+
160
206
  A bundle ships rules as `guardrails/*.yml` (the same `rules:` shape). Install
161
207
  records each file's sha256; a file changed afterwards, missing, or not parsing
162
208
  denies every call until the bundle is reinstalled.
data/docs/memory.md CHANGED
@@ -12,7 +12,8 @@ Tool behavior:
12
12
  - `memory_read`: `scope` is optional.
13
13
  - If `scope` is provided (`project` or `system`), only that scope is read.
14
14
  - If `scope` is omitted, read falls back from project to system.
15
- - `memory_write`: `scope` is required (`project` or `system`). The entry name is passed via the `name` parameter (not `path` — the file tools use `path`). On success, the return value includes the full file path, so you can use the `edit` tool directly for targeted updates.
15
+ - `memory_write`: `scope` is required (`project` or `system`). The entry name is passed via the `name` parameter (not `path` — the file tools use `path`). On success, the return value includes the full file path. For a small change to an existing memory (a step, a line) the model is told to `edit` that file rather than rewrite it all with `memory_write`.
16
+ - `write`/`edit` that change a `*.md` right in a scope dir refresh its managed `index.md` line (date, bytes; the description is kept), as `memory_write` does. A memory created with `write` gets a line with no description; any other `.md` written there gets a line too. Model overlays (`<name>.<key>.md` next to `<name>.md`), files in another project's folder and files changed by `execute` are left out.
16
17
 
17
18
  ## Model-Specific Memory Overlays
18
19
 
@@ -51,8 +52,9 @@ chi is told about skills by the system bundle (`identity.md`, every turn, and
51
52
  looks unexpected on the way (a failing check, a warning the skill doesn't
52
53
  mention), chi stops, says what it saw and asks how to go on.
53
54
  - **Updating.** When a step turned out different (a renamed script, an extra
54
- step), chi fixes the skill in the same turn: those steps changed, the rest
55
- kept, a dated Changelog line added. No confirmation.
55
+ step), chi fixes the skill in the same turn: those steps changed (with `edit`
56
+ on its file, so a small fix doesn't resend the whole skill), the rest kept, a
57
+ dated Changelog line added. No confirmation.
56
58
 
57
59
  A skill is plain Markdown, no frontmatter:
58
60
 
data/docs/plugins.md CHANGED
@@ -940,16 +940,24 @@ skills> skill release updated (+2 −1): 1. Run `scripts/verify.sh`; stop if it
940
940
  The line is the file on disk changing, whatever the tool answered; a denied
941
941
  write shows nothing.
942
942
 
943
+ A skill read in the turn is also watched for changes made another way (an
944
+ `execute` running `sed`, a script): after each other tool call, and at the
945
+ turn's end, its file is compared with the content last seen (at the read, or
946
+ after the last write). A change shows the same line, keeps the content last
947
+ seen as a version (so `/skill diff` has it) and counts as the skill updated.
948
+ A plain read keeps no version.
949
+
943
950
  **The nudge** (`nudge: true`). Some models, finding a skill's step broken,
944
951
  skip it and go on without fixing the skill. In a turn that read a skill
945
952
  (`memory_read` of a `skill_*` name, or `read` of its file), the first failing
946
953
  tool call after it (an `execute` that exited non-zero, a tool error) steers
947
954
  the model once: *"A step of skill release failed. Find out why before skipping
948
- it; if the skill is out of date, fix it now: memory_write the whole skill,
949
- its title and every section as they were, that step fixed, a Changelog line
950
- added."* If the turn ends with a failed step and the
951
- skill not rewritten, one line says so: `skill release was followed, a step
952
- failed, the skill wasn't updated`. A failure unrelated to the skill (a test
955
+ it; if the skill is out of date, fix it now: edit the step that changed in
956
+ its file (or memory_write the whole skill) and add a Changelog line."* No
957
+ steer once a skill read this turn has changed, by any tool. If the turn ends
958
+ with a failed step and no skill read changed, one line says so: `skill
959
+ release was followed, a step failed, the skill wasn't updated`. A failure
960
+ unrelated to the skill (a test
953
961
  meant to fail) can set it off too: once per turn, and only after a skill was
954
962
  read.
955
963
 
data/docs/releasing.md CHANGED
@@ -49,7 +49,7 @@ as the GitHub release body.
49
49
  | Task | What it does |
50
50
  | --- | --- |
51
51
  | `rake bundles:sha` | Recomputes the sha256 lines of every shipped bundle manifest (`files:`, `hooks:`, `plugin:`). |
52
- | `rake bundles:check` | Sha lines match; a bundle changed since the last `v*` tag has a higher version (skipped, with a note, before the first tag). |
52
+ | `rake bundles:check` | Sha lines match; a bundle changed since the last `v*` tag has a higher version (skipped, with a note, before the first tag). Warns (doesn't fail) when such a bundle's `requires_chi` still admits the tagged chi: raise it if the change uses anything new. |
53
53
  | `rake release:draft_changelog` | A draft Unreleased section from the commit subjects since the last tag, grouped. Stdout only. |
54
54
  | `rake release:bump[X.Y.Z]` | VERSION, the system manifest, Gemfile.lock, CHANGELOG. No commit. |
55
55
  | `rake release:check` | Clean tracked tree, VERSION == system bundle, a CHANGELOG section, `bundles:check`, rspec + npm test, `gem build`, then a clean install into a temp GEM_HOME that runs `chi --version`, `chi self` and `chi bundle list` with a temp HOME. Needs the network (gem dependencies). |
@@ -0,0 +1,27 @@
1
+ # Guardrail rules only small models get (models: small): asks that a strong
2
+ # model would find noise. Which models are small is guardrails.small_models:
3
+ # auto (the default) is 32B or less by the size in the model's name, an
4
+ # MoE's active size counting (35B-A3B is small); a list of globs; or [] for
5
+ # none. `/guardrails` says whether the current model is small.
6
+ #
7
+ # These asks offer only once and session: an approval for the repo or the
8
+ # whole rule would silence them for good, on any model.
9
+ # Same fields and git prefix as rules.yml.
10
+ rules:
11
+ # ── losing uncommitted work ────────────────────────────────────────────
12
+ # git checkout -- <path>, git checkout <rev> -- <path>, git checkout .,
13
+ # and git restore of the worktree (not git restore --staged alone).
14
+ - id: git-discard-worktree
15
+ tool: shell
16
+ command: '\bgit(?:\s+(?:-C\s+\S+|-c\s+\S+|--?[A-Za-z][\w-]*(?:=\S+)?))*\s+(?:checkout\b[^;&|\n]*\s(?:--\s+\S|\.(?:\s|$|/))|restore\b(?![^;&|\n]*\s--staged\b)(?=[^;&|\n]*\s[^-\s])|restore\b(?=[^;&|\n]*\s(?:--worktree|-W)\b))'
17
+ models: small
18
+ verdict: ask
19
+ reason: git checkout -- / git restore discards uncommitted changes
20
+ scopes: [once, session]
21
+ - id: git-stash-drop
22
+ tool: shell
23
+ command: '\bgit(?:\s+(?:-C\s+\S+|-c\s+\S+|--?[A-Za-z][\w-]*(?:=\S+)?))*\s+stash\s+(drop|clear)\b'
24
+ models: small
25
+ verdict: ask
26
+ reason: git stash drop/clear deletes stashed work
27
+ scopes: [once, session]
@@ -1,8 +1,10 @@
1
1
  ---
2
2
  name: guardrails
3
- version: 0.1.1
3
+ version: 0.2.0
4
4
  scope: system
5
- description: Optional default guardrail rules (git push/rewrites, rm -rf wide, curl | sh, writes outside the repo, git hooks) and a note for the model
5
+ description: Optional default guardrail rules (git push/rewrites, rm -rf wide, curl | sh, writes outside the repo, git hooks), asks only small models get (git checkout --/restore, git stash drop), and a note for the model
6
6
  trust_level: reviewed
7
+ # models: in small-models.yml (guardrails.small_models)
8
+ requires_chi: ">= 0.8.1"
7
9
  files:
8
10
  guardrails.md: sha256:04207fd1738aeeeef3b7a8053fc57597565bddd7862f01acaba784419cbd909b
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  name: skills
3
- version: 0.1.0
3
+ version: 0.1.1
4
4
  scope: system
5
5
  description: "Skills (skill_<name> memories, the steps of a task done together): /skill save, list, show and diff; older versions kept with a short diff line on every update; a nudge when a followed skill's step fails"
6
6
  trust_level: reviewed
7
7
  plugin:
8
8
  file: plugin.rb
9
- sha256: sha256:76758d41e75db107e0a6f7ce35b9eac4944052746af16aeba30f38210cc436dc
10
- requires_chi: ">= 0.4.0"
9
+ sha256: sha256:ecf76de5d0f4d07a0e87b96cdc13b0bbc22369f8eea843bfd22c6fade469dfca
10
+ requires_chi: ">= 0.9.0"
@@ -13,13 +13,20 @@
13
13
  # shows a line: "skill release updated (+2 −1): …" or "skill release saved
14
14
  # (project, 14 lines)".
15
15
  #
16
+ # A skill read in this turn is also watched for changes made some other way
17
+ # (an execute running sed, a script): after each other tool call, and at the
18
+ # turn's end, its file is compared with the content last seen (at the read,
19
+ # or after the last write above); a change keeps that content as a version,
20
+ # shows the same line and counts as the skill updated.
21
+ #
16
22
  # The nudge (nudge: true), for models that skip a failing step instead of
17
23
  # fixing the skill: in a turn that read a skill (memory_read of a skill_*
18
24
  # name, or a read of its file), the first failing tool call after it (an
19
25
  # execute with "exit: N", N ≠ 0, or with no exit line an Error: line near
20
26
  # the top; any tool's "[tool] Error: …") steers the model once to find out
21
- # why and fix the skill. At the turn's end, a failed step with no rewrite of
22
- # the skill gets a notice line.
27
+ # why and fix the skill, unless a skill read was changed already (by any
28
+ # tool, see above). At the turn's end, a failed step with no change to a
29
+ # skill read gets a notice line.
23
30
  #
24
31
  # Settings (config.yml, bundles: skills:):
25
32
  # history_keep: 20 older versions kept per skill
@@ -31,8 +38,7 @@ class Plugin
31
38
  WRITE_TOOLS = %w[memory_write write edit].freeze
32
39
  NOTICE_WIDTH = 60 # the changed line in an update's notice
33
40
  NUDGE = "A step of skill %s failed. Find out why before skipping it; if the skill is out of date, fix it now: " \
34
- "memory_write the whole skill, its title and every section as they were, that step fixed, a Changelog " \
35
- "line added."
41
+ "edit the step that changed in its file (or memory_write the whole skill) and add a Changelog line."
36
42
  NOT_FAILURES = %w[memory_read memory_write].freeze
37
43
  USAGE = "usage: /skill save [name] [--system] | list | show <name> | diff <name> [N]"
38
44
 
@@ -79,16 +85,38 @@ class Plugin
79
85
  def after_tool_call(event, ctx)
80
86
  stash = @stash
81
87
  @stash = nil
88
+ stash = nil unless stash && stash[:tool] == event[:tool].to_s
89
+ changed_elsewhere(ctx) unless stash
82
90
  step_failed(event) if @nudge && !@read.empty?
83
- return unless stash && stash[:tool] == event[:tool].to_s
91
+ return unless stash
84
92
 
85
93
  now = File.file?(stash[:path]) ? File.read(stash[:path]) : nil
86
94
  return if now.nil? || now == stash[:old]
87
95
 
88
- @written << stash[:name]
96
+ @written |= [stash[:name]]
97
+ @seen[stash[:name]] = { path: stash[:path], scope: stash[:scope], content: now }
89
98
  ctx.notify(change_notice(stash, now))
90
99
  end
91
100
 
101
+ # A skill read this turn whose file differs from the content last seen was
102
+ # changed by something other than the write tools (an execute's sed, say):
103
+ # keep what was there as a version, show the change, count the skill as
104
+ # updated (any change counts, the user's own edit too). A file gone since
105
+ # is left alone.
106
+ def changed_elsewhere(ctx)
107
+ @seen.each do |name, seen|
108
+ now = File.file?(seen[:path]) ? File.read(seen[:path]) : nil
109
+ next if now.nil? || now == seen[:content]
110
+
111
+ keep_version(ctx, seen[:scope], name, seen[:content]) if seen[:content]
112
+ ctx.notify(change_notice({ name: name, scope: seen[:scope], old: seen[:content] }, now))
113
+ seen[:content] = now
114
+ @written |= [name]
115
+ end
116
+ rescue SystemCallError => e
117
+ ctx.log.warn(:watch_failed, error: e.class.name, msg: e.message)
118
+ end
119
+
92
120
  def change_notice(stash, now)
93
121
  name = stash[:name]
94
122
  return "skill #{name} saved (#{stash[:scope]}, #{now.lines.size} lines)" unless stash[:old]
@@ -105,21 +133,42 @@ class Plugin
105
133
 
106
134
  def reset_turn
107
135
  @read = [] # skills read this turn, in order
136
+ @seen = {} # of those, name => {path:, scope:, content:} last seen
108
137
  @written = [] # skills changed this turn
109
138
  @failed = false # a step failed after a skill was read
110
139
  @nudged = false
111
140
  end
112
141
 
113
142
  def note_read(tool, event)
114
- names = case tool
143
+ found = case tool
115
144
  when "memory_read"
145
+ scope = event.dig(:call, :scope).to_s.strip
146
+ scope = nil unless memory_dirs.key?(scope)
147
+ # A skill not written yet is watched where memory_write would
148
+ # put it by default: the read scope, else project.
116
149
  event.dig(:call, :content).to_s.split(",").map(&:strip).select { |entry| entry.start_with?("skill_") }
117
- .filter_map { |entry| skill_name(entry) }
150
+ .filter_map do |entry|
151
+ name = skill_name(entry) or next
152
+ unwritten = File.join(memory_dirs[scope || "project"], "skill_#{name}.md")
153
+ [name, skill_path(name, scope: scope) || unwritten]
154
+ end
118
155
  when "read"
119
- Array(event.dig(:targets, :paths)).filter_map { |path| skill_at(path)&.last }
156
+ Array(event.dig(:targets, :paths)).filter_map { |path| (at = skill_at(path)) && [at.last, path] }
120
157
  else []
121
158
  end
122
- @read |= names
159
+ found.each { |name, path| watch(name, path) }
160
+ @read |= found.map(&:first)
161
+ end
162
+
163
+ # Remember a read skill's content (nil: not written yet) the first time in
164
+ # a turn: a later read mustn't hide a change made in between.
165
+ def watch(name, path)
166
+ return if @seen.key?(name) || path.nil?
167
+
168
+ content = File.file?(path) ? File.read(path) : nil
169
+ @seen[name] = { path: File.expand_path(path), scope: skill_at(path)&.first, content: content }
170
+ rescue SystemCallError
171
+ nil
123
172
  end
124
173
 
125
174
  def step_failed(event)
@@ -127,7 +176,7 @@ class Plugin
127
176
  return if NOT_FAILURES.include?(tool) || !failure?(tool, event[:output].to_s)
128
177
 
129
178
  @failed = true
130
- return if @nudged
179
+ return if @nudged || @read.any? { |name| @written.include?(name) }
131
180
 
132
181
  @nudged = !!event[:steer]&.call(format(NUDGE, @read.join(", ")))
133
182
  end
@@ -146,6 +195,7 @@ class Plugin
146
195
  end
147
196
 
148
197
  def after_turn(ctx)
198
+ changed_elsewhere(ctx)
149
199
  return unless @nudge && @failed
150
200
 
151
201
  missed = @read - @written
@@ -8,6 +8,6 @@
8
8
  - **Skills**: a skill is a memory named `skill_<name>` holding the steps of a repeatable task we did together (sections Steps, Gotchas, Changelog; `memory_write description:` says when to use it).
9
9
  When the user asks to keep how we did something ("let's memorize this", "save this as a skill", `/skill save`), write it right away with `memory_write` (project scope; system only when the user asks or it isn't about this project), then show it briefly.
10
10
  Before a task that a `skill_*` in the memory index matches, read it and follow it.
11
- When a step turned out different (a renamed command, an extra step, a gotcha), update the skill in the same turn: fix those steps, keep the rest as it was, add a dated Changelog line.
11
+ When a step turned out different (a renamed command, an extra step, a gotcha), update the skill in the same turn: `edit` those steps in its file (memory_write only to rewrite it all), keep the rest as it was, add a dated Changelog line.
12
12
  When a skill's step fails or its file/command is missing, find out why (look around, read nearby READMEs) before skipping it; a step that says stop means stop and ask.
13
13
  While following a skill, when anything looks unexpected (a check fails, output differs from what a step says, a warning the skill doesn't mention): stop, don't improvise a fix, tell the user what you saw and ask how to go on. Read each command's whole output, including warnings, before the next step.
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: samagotchi-system
3
- version: 0.8.0
3
+ version: 0.9.0
4
4
  scope: system
5
5
  description: Default system memories — identity, self map, config modification protocol, memory guide and the delegated-session rules
6
6
  files:
7
- identity.md: sha256:d6f40d5e44096537d337c4ffda86823446f8b59eb8b8cc47014e97ec16f019c9
7
+ identity.md: sha256:29c2f7df9f6c5acbc5ec17000a4490741e55e426af15ca0d81c8706edd9bd027
8
8
  self_map.md: sha256:93c788fadaf98adeb8946b4a357d2372bd6ebb439042d7cef8dda8fb44187315
9
9
  config_modification_protocol.md: sha256:24e95727488daa80ac3008e020d8be098c7a1882153b7a61aba35450f72b86b7
10
- memory_guide.md: sha256:50c076fb6bda7ed010877ff664bb2e9e904418fe08b058ebaa911d4e82e9a683
10
+ memory_guide.md: sha256:81b911b9a015f0924c48162b3dc4b6376309e743765074419a99fab46e480858
11
11
  delegated.md: sha256:0a71d5dfa7127df2d51cb5f0e7214240ed88777e36559430a8724c796d7ae561
@@ -6,7 +6,7 @@ This memory teaches you (the agent) how to use Samagotchi memories — persisten
6
6
 
7
7
  - Plain Markdown files (`*.md`) stored outside the repo so they persist.
8
8
  - Loaded at startup into your system prompt as **Project memories** and **System memories** indexes (blank-name `memory_read`).
9
- - You manage them via tools, not shell file ops. On write, `index.md` is auto-maintained — do not edit it manually.
9
+ - You manage them via tools (`memory_write`, or `edit` on a memory's file for a small change), not shell file ops. `index.md` is auto-maintained on every write — do not edit it manually.
10
10
 
11
11
  ## Scopes — where files live
12
12
 
@@ -16,7 +16,7 @@ This memory teaches you (the agent) how to use Samagotchi memories — persisten
16
16
  | `project` | `<system dir>/projects/<basename>_<hash>/` (`MemoryPaths.project_dir`: `basename(root)` + 8-char `MD5(root)`, root = `MemoryPaths.project_root`) | Repo-specific conventions, workflow, stack decisions |
17
17
 
18
18
  - The project root is the git repository: its common git dir, which every linked worktree shares. All worktrees and subdirectories of one repository share one project folder; the system prompt shows the resolved root and folder. Outside a repository the root is the working directory. A separate clone is a different project.
19
- - `index.md` lives in each scope dir and contains auto-managed lines like `- **name** · scope · date · bytes — description`. Your free-form sections in `index.md` are preserved but managed lines are owned by `memory_write`.
19
+ - `index.md` lives in each scope dir and contains auto-managed lines like `- **name** · scope · date · bytes — description`. Your free-form sections in `index.md` are preserved but managed lines are owned by `memory_write` (and refreshed when `write`/`edit` change a memory's file).
20
20
 
21
21
  ## Tools you have
22
22
 
@@ -34,7 +34,8 @@ This memory teaches you (the agent) how to use Samagotchi memories — persisten
34
34
  - `name` is the entry name **without** `.md` (the tool adds it). Use `name`, **not** `path` — `path` belongs to the file tools and is ignored here. `name: "index"` is a verbatim write to `index.md` (no auto-index update) — rarely needed.
35
35
  - `scope` is **required** — never omit. Prefer `project` for repo conventions, `system` for user preferences.
36
36
  - `description` is appended to the managed `index.md` line (`— description`). Replaces previous description if given; otherwise preserves existing one.
37
- - On success returns `Memory 'name' saved to <scope> scope (N bytes). Index updated: …` — confirm `bytes` and `scope`.
37
+ - For a small change to an existing memory (a step, a line), `edit` its file instead of rewriting it all: the path is in `memory_write`'s result and the scope dirs are in the prompt. Its index line is refreshed either way.
38
+ - On success returns `Memory 'name' saved to <scope> scope (N bytes). File written: <path> Index line refreshed automatically.` — confirm `bytes` and `scope`; the index needs nothing from you.
38
39
 
39
40
  **When to write:**
40
41
  - After learning a durable preference (e.g. commit style, test command, coding guideline) that the user confirmed or you observed repeatedly — ask before overwriting existing entries where appropriate.
@@ -67,7 +68,7 @@ Shape (plain Markdown, no frontmatter):
67
68
  - **Scope**: `project` by default; `system` when the user asks, or when the skill is clearly not about this project.
68
69
  - **Saving**: on a request to keep how something was done ("let's memorize this", "save this as a skill", `/skill save`), write it at once, then show it briefly. On an ambiguous one ("I like how we did that") you may ask whether to save it.
69
70
  - **Following**: before a task a `skill_*` index line matches, `memory_read` it and follow its steps. A step that fails or names a missing file or command: find out why (look around, read nearby READMEs) before skipping it; a step that says stop means stop and ask. When anything looks unexpected (a check fails, output differs from what a step says, a warning the skill doesn't mention): stop, don't improvise a fix, tell the user what you saw and ask. Read each command's whole output, including warnings, before the next step.
70
- - **Updating**: when a step turned out different, rewrite the skill with `memory_write` in the same turn: fix those steps, keep the rest as it was, add a dated Changelog line. No confirmation needed.
71
+ - **Updating**: when a step turned out different, update the skill in the same turn: `edit` those steps in its file (`<scope dir>/skill_<name>.md`), or rewrite it with `memory_write`; keep the rest as it was, add a dated Changelog line. No confirmation needed.
71
72
  - **The `skills` bundle** (`chi bundle install skills`, optional) adds `/skill save [name] [--system]`, `/skill list`, `/skill show <name>`, `/skill diff <name> [N]`; it keeps older versions and shows a short diff line after each update.
72
73
 
73
74
  ## Memory Bundles — shareable packs
@@ -103,6 +103,9 @@ module Samagotchi
103
103
  Entry.new(key: "image.max_per_request", yaml_path: %w[image max_per_request], type: :integer, default: 20, expose: %i[env config]),
104
104
 
105
105
  Entry.new(key: "guardrails.enabled", yaml_path: %w[guardrails enabled], type: :bool, default: true, expose: %i[env config]),
106
+ # Which models get the rules with `models: small` (Guardrails::ModelSize): auto (32B or less by the name's size, an MoE's
107
+ # active size counting), globs on the bare model name or its key, "|"-separated (a YAML list works too), or "" ([]) for none.
108
+ Entry.new(key: "guardrails.small_models", yaml_path: %w[guardrails small_models], type: :string, default: "auto", expose: %i[env config]),
106
109
 
107
110
  Entry.new(key: "log.file", yaml_path: %w[log file], type: :string, default: nil, expose: %i[env config cli]),
108
111
  Entry.new(key: "log.disable", yaml_path: %w[log disable], type: :bool, default: false, expose: %i[env config cli]),
@@ -126,6 +126,7 @@ module Samagotchi
126
126
  tools: -> { @tools },
127
127
  session: -> { @session },
128
128
  model_key: -> { @model_key },
129
+ model_name: -> { guardrail_model_name },
129
130
  cancelled: -> { !!active_cancel_controller&.cancelled? },
130
131
  ask: ->(fields) { open_question(fields) }
131
132
  )
@@ -428,6 +429,11 @@ module Samagotchi
428
429
  attr_reader :metrics
429
430
  attr_reader :default_model_name, :effective_model_name
430
431
 
432
+ # The effective model's key (ModelOverlay.key_for its bare name): memory
433
+ # overlays, guardrail rules' models:.
434
+ # @return [String, nil]
435
+ attr_reader :model_key
436
+
431
437
  # The prompt profile for the effective model (see #profile_resolution).
432
438
  # @return [ModelProfile]
433
439
  def profile = profile_resolution.profile
@@ -452,6 +458,13 @@ module Samagotchi
452
458
  @host_registry.bare_name(full_ref)
453
459
  end
454
460
 
461
+ # The effective model's bare name for the guardrails' models: rules;
462
+ # nil without a model name.
463
+ def guardrail_model_name
464
+ name = @effective_model_name && bare_model_name(@effective_model_name)
465
+ name.to_s.strip.empty? ? nil : name
466
+ end
467
+
455
468
  # Point the kernel (and a chat backend) at the effective model's host
456
469
  # (after /model, --model, resume).
457
470
  def sync_kernel_client!
@@ -29,14 +29,16 @@ module Samagotchi
29
29
  # @param tools [#call] → Tools::Registry
30
30
  # @param session [#call] → Session, nil
31
31
  # @param model_key [#call] → String, the effective model's overlay key
32
+ # @param model_name [#call] → String, nil: the effective model's bare name
32
33
  # @param cancelled [#call] → Boolean, whether the running turn is cancelled
33
34
  # @param ask [#call] (fields) → the answer; the Engine's question flow
34
- def initialize(scratch:, hooks:, tools:, session:, model_key:, cancelled:, ask:)
35
+ def initialize(scratch:, hooks:, tools:, session:, model_key:, cancelled:, ask:, model_name: -> {})
35
36
  @scratch = scratch
36
37
  @hooks_lookup = hooks
37
38
  @tools_lookup = tools
38
39
  @session_lookup = session
39
40
  @model_key_lookup = model_key
41
+ @model_name_lookup = model_name
40
42
  @cancelled_lookup = cancelled
41
43
  @ask = ask
42
44
  @failures = Guardrails::LoadFailures.new
@@ -78,6 +80,7 @@ module Samagotchi
78
80
  -> { @hooks_lookup.call },
79
81
  context_lookup: -> { context },
80
82
  model_key_lookup: -> { @model_key_lookup.call },
83
+ model_name_lookup: -> { @model_name_lookup.call },
81
84
  approver: ->(verdict) { request_approval(verdict) },
82
85
  approvals_lookup: -> { @approvals },
83
86
  checks_lookup: -> { checks },
@@ -21,7 +21,8 @@ module Samagotchi
21
21
  # the kernel is built.
22
22
  # @param context_lookup [#call] returns the Context for this call
23
23
  # (the Engine's: session, interface, origin, per-turn git cache)
24
- # @param model_key_lookup [#call] the model key (memory overlays)
24
+ # @param model_key_lookup [#call] the model key (memory overlays, rules' models:)
25
+ # @param model_name_lookup [#call] the bare model name (rules' models:)
25
26
  # @param approver [#call, nil] settles an ask (Engine#request_approval);
26
27
  # without one an ask is denied
27
28
  # @param approvals_lookup [#call] returns the Approvals store (or nil)
@@ -31,12 +32,13 @@ module Samagotchi
31
32
  # hook's stop_turn): the rest of a batch is denied without a vote
32
33
  # @param tools_lookup [#call] returns the Tools::Registry (or nil): a
33
34
  # plugin tool's targets: say what its call acts on
34
- def initialize(hooks_lookup, context_lookup: -> { Context.new }, model_key_lookup: -> {}, approver: nil,
35
- approvals_lookup: -> {}, checks_lookup: -> { [] }, cancelled_lookup: -> { false },
35
+ def initialize(hooks_lookup, context_lookup: -> { Context.new }, model_key_lookup: -> {}, model_name_lookup: -> {},
36
+ approver: nil, approvals_lookup: -> {}, checks_lookup: -> { [] }, cancelled_lookup: -> { false },
36
37
  tools_lookup: -> {})
37
38
  @hooks_lookup = hooks_lookup
38
39
  @context_lookup = context_lookup
39
40
  @model_key_lookup = model_key_lookup
41
+ @model_name_lookup = model_name_lookup
40
42
  @approver = approver
41
43
  @approvals_lookup = approvals_lookup
42
44
  @checks_lookup = checks_lookup
@@ -113,7 +115,8 @@ module Samagotchi
113
115
  end
114
116
 
115
117
  def targets_for(call, context)
116
- Targets.for(call, context, model_key: @model_key_lookup.call, registry: @tools_lookup.call)
118
+ Targets.for(call, context, model_key: @model_key_lookup.call, model_name: @model_name_lookup.call,
119
+ registry: @tools_lookup.call)
117
120
  end
118
121
 
119
122
  # Registry#fire_each rescues a raising hook itself.