samagotchi 0.8.1 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -1
- data/docs/guardrails.md +6 -0
- data/docs/memory.md +5 -3
- data/docs/plugins.md +13 -5
- data/lib/samagotchi/bundles/skills/manifest.yml +3 -3
- data/lib/samagotchi/bundles/skills/plugin.rb +61 -11
- data/lib/samagotchi/bundles/system/identity.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/memory_guide.md +5 -4
- data/lib/samagotchi/guardrails/targets.rb +7 -1
- data/lib/samagotchi/log_subscriber.rb +5 -1
- data/lib/samagotchi/memory_bundle/index_sync.rb +73 -0
- data/lib/samagotchi/memory_bundle/installer.rb +24 -3
- data/lib/samagotchi/session_manager.rb +2 -2
- data/lib/samagotchi/session_metrics.rb +2 -1
- data/lib/samagotchi/sessions_command.rb +3 -2
- data/lib/samagotchi/system_prompt.rb +2 -1
- data/lib/samagotchi/tool_declarations.rb +1 -1
- data/lib/samagotchi/tool_runner.rb +8 -0
- data/lib/samagotchi/tools/memory.rb +1 -2
- data/lib/samagotchi/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9290117f26040b725e12c5cd18bf2a8858a573f34e2323e5a56517c030f95d7f
|
|
4
|
+
data.tar.gz: 7e5d1e2e8a285424b907172038f6bab1045b1388129241cc1a32cb26132437ae
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d70bd257f5b82a2057077e2142adf5601f4bc652a0e820c695b5ecff305156c0270815374054e41d6403ecb0ee4579fd75fda84f670094e99a21f7cf73e34b5c
|
|
7
|
+
data.tar.gz: 961269d190e236ecbe3a3ce0dae6d18d86adf93f5c16aa8246dd91e10a43b73152c8595a4e6377491aaf86e74d8558b225893364dc6dd226634d6b94a7484504
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,38 @@ 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
|
+
|
|
11
43
|
## [0.8.1] - 2026-10-01
|
|
12
44
|
|
|
13
45
|
### Added
|
|
@@ -481,7 +513,8 @@ and long-lived sessions.
|
|
|
481
513
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
482
514
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
483
515
|
|
|
484
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
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
|
|
485
518
|
[0.8.1]: https://github.com/dm1try/samagotchi/compare/v0.8.0...v0.8.1
|
|
486
519
|
[0.8.0]: https://github.com/dm1try/samagotchi/compare/v0.7.0...v0.8.0
|
|
487
520
|
[0.7.0]: https://github.com/dm1try/samagotchi/compare/v0.6.0...v0.7.0
|
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:
|
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
|
|
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
|
|
55
|
-
|
|
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:
|
|
949
|
-
its
|
|
950
|
-
|
|
951
|
-
skill
|
|
952
|
-
failed, the skill wasn't updated`. A failure
|
|
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
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: skills
|
|
3
|
-
version: 0.1.
|
|
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:
|
|
10
|
-
requires_chi: ">= 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
|
|
22
|
-
# the
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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)
|
|
156
|
+
Array(event.dig(:targets, :paths)).filter_map { |path| (at = skill_at(path)) && [at.last, path] }
|
|
120
157
|
else []
|
|
121
158
|
end
|
|
122
|
-
|
|
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:
|
|
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.
|
|
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:
|
|
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:
|
|
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.
|
|
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
|
-
-
|
|
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,
|
|
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
|
|
@@ -4,6 +4,7 @@ require_relative "../tools/tool_path"
|
|
|
4
4
|
require_relative "../tools/memory"
|
|
5
5
|
require_relative "../log"
|
|
6
6
|
require_relative "model_size"
|
|
7
|
+
require_relative "../memory_bundle/index_sync"
|
|
7
8
|
|
|
8
9
|
module Samagotchi
|
|
9
10
|
module Guardrails
|
|
@@ -123,9 +124,14 @@ module Samagotchi
|
|
|
123
124
|
end
|
|
124
125
|
|
|
125
126
|
# Whether any path is outside the repo root (the cwd outside a repo).
|
|
127
|
+
# A memory's file doesn't count (MemoryBundle::IndexSync.memory_scope):
|
|
128
|
+
# write/edit there is what memory_write does, unasked; index.md, the
|
|
129
|
+
# bundles dir and the rest of chi's config dir still count.
|
|
126
130
|
def outside_repo?
|
|
127
131
|
root = @repo_root || @cwd
|
|
128
|
-
@paths.any?
|
|
132
|
+
@paths.any? do |p|
|
|
133
|
+
p != root && !p.start_with?(File.join(root, "")) && !MemoryBundle::IndexSync.memory_scope(p)
|
|
134
|
+
end
|
|
129
135
|
end
|
|
130
136
|
|
|
131
137
|
# The hook event's targets: hash.
|
|
@@ -123,8 +123,12 @@ module Samagotchi
|
|
|
123
123
|
def on_tool_call_completed(event)
|
|
124
124
|
# A tool's output can be any bytes (invalid UTF-8 would fail the match).
|
|
125
125
|
output = event[:output].to_s.scrub
|
|
126
|
+
# ms leaves out a guardrail approval wait (waited_ms, logged apart).
|
|
127
|
+
waited = event[:waited_ms].to_i
|
|
128
|
+
ms = since(@tool_started_at.delete([event[:iteration], event[:call_index]]))
|
|
129
|
+
ms = [ms - waited, 0].max if ms
|
|
126
130
|
log(:info, :tool_call_completed, iteration: event[:iteration], tool: event[:tool],
|
|
127
|
-
ms:
|
|
131
|
+
ms: ms, waited_ms: (waited if waited.positive?),
|
|
128
132
|
output_chars: output.length, truncated: event[:output_truncated] || nil,
|
|
129
133
|
error: tool_error?(output) || nil)
|
|
130
134
|
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "index_updater"
|
|
4
|
+
require_relative "../tools/memory"
|
|
5
|
+
require_relative "../model_overlay"
|
|
6
|
+
require_relative "../log"
|
|
7
|
+
|
|
8
|
+
module Samagotchi
|
|
9
|
+
module MemoryBundle
|
|
10
|
+
# Keeps a memory's managed index.md line current when the file tools
|
|
11
|
+
# (write, edit) change it, as memory_write does: the ToolRunner calls
|
|
12
|
+
# .refresh after a write/edit that changed its file (one it could diff:
|
|
13
|
+
# a memory over 1 MB isn't refreshed). The line's description is kept.
|
|
14
|
+
#
|
|
15
|
+
# Only a *.md right in the project or system memories dir (the dirs
|
|
16
|
+
# memory_write writes to) counts: not index.md, not a hidden file, not a
|
|
17
|
+
# model overlay (<name>.<key>.md next to its <name>.md; a memory whose
|
|
18
|
+
# name has a dot and a sibling of the same stem reads as an overlay too,
|
|
19
|
+
# a leftover ambiguity). Writes into another project's folder and
|
|
20
|
+
# execute's writes aren't seen.
|
|
21
|
+
module IndexSync
|
|
22
|
+
SCOPES = %w[project system].freeze
|
|
23
|
+
|
|
24
|
+
# @param path [String] the file write/edit changed
|
|
25
|
+
# @return [Boolean] whether an index line was written
|
|
26
|
+
def self.refresh(path)
|
|
27
|
+
path = File.expand_path(path.to_s)
|
|
28
|
+
scope = memory_scope(path)
|
|
29
|
+
return false unless scope && File.file?(path)
|
|
30
|
+
|
|
31
|
+
name = File.basename(path, ".md")
|
|
32
|
+
return false if overlay?(path, name)
|
|
33
|
+
|
|
34
|
+
IndexUpdater.update_index(scope, name, File.size(path), nil)
|
|
35
|
+
rescue StandardError => e
|
|
36
|
+
Log.warn(:memory, "index_sync_failed", path: path, error: "#{e.class}: #{e.message}")
|
|
37
|
+
false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# "project" or "system" when +path+ is a memory's file (or an
|
|
41
|
+
# overlay's): a *.md right in that scope's memories dir, not index.md,
|
|
42
|
+
# not hidden, not a symlink. nil otherwise. The guardrails' outside_repo
|
|
43
|
+
# uses it too: write/edit there is what memory_write does. A symlink
|
|
44
|
+
# isn't: write/edit follow it to wherever it points, memory_write
|
|
45
|
+
# replaces the link.
|
|
46
|
+
def self.memory_scope(path)
|
|
47
|
+
path = File.expand_path(path.to_s)
|
|
48
|
+
name = File.basename(path, ".md")
|
|
49
|
+
return nil unless File.extname(path) == ".md"
|
|
50
|
+
return nil if name == IndexUpdater::MEMORY_INDEX || name.start_with?(".") || File.symlink?(path)
|
|
51
|
+
|
|
52
|
+
dir = real(File.dirname(path))
|
|
53
|
+
SCOPES.find { |s| real(Tools::MemoryRead.memories_dir(s)) == dir }
|
|
54
|
+
rescue StandardError
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.overlay?(path, name)
|
|
59
|
+
match = File.basename(path).match(ModelOverlay::OVERLAY_SUFFIX_PATTERN)
|
|
60
|
+
return false unless match
|
|
61
|
+
|
|
62
|
+
base = name.delete_suffix(".#{match[1]}")
|
|
63
|
+
File.file?(File.join(File.dirname(path), "#{base}.md"))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.real(dir)
|
|
67
|
+
File.realpath(dir)
|
|
68
|
+
rescue SystemCallError
|
|
69
|
+
File.expand_path(dir)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -86,6 +86,8 @@ module Samagotchi
|
|
|
86
86
|
all_files_in_bundle = []
|
|
87
87
|
provenance = Provenance.new(name: @name)
|
|
88
88
|
existing_provenance = provenance.read
|
|
89
|
+
# A plain install over an installed bundle: summary points at upgrade.
|
|
90
|
+
@reinstall = existing_provenance && !@upgrade && !@force && !@dry_run
|
|
89
91
|
|
|
90
92
|
Dir.glob(File.join(normalized_dir, "*.md")).each do |file_path|
|
|
91
93
|
file_key = File.basename(file_path)
|
|
@@ -125,12 +127,17 @@ module Samagotchi
|
|
|
125
127
|
classification = Merger.classify(base_path: base_path, current_path: target_path, incoming_path: file_path)
|
|
126
128
|
@results[file_key] = { status: classification.to_s }
|
|
127
129
|
@conflicts[file_key] = { base: base_path, current: target_path, incoming: file_path } if classification == :conflict
|
|
130
|
+
elsif FileUtils.identical?(file_path, target_path)
|
|
131
|
+
@results[file_key] = { status: "skipped", reason: "already up to date" }
|
|
128
132
|
else
|
|
129
133
|
@results[file_key] = { status: "would_skip" }
|
|
130
134
|
end
|
|
131
135
|
else
|
|
132
136
|
@results[file_key] = { status: "would_install" }
|
|
133
137
|
end
|
|
138
|
+
elsif File.exist?(target_path) && !@force && FileUtils.identical?(file_path, target_path)
|
|
139
|
+
@results[file_key] = { status: "skipped", reason: "already up to date" }
|
|
140
|
+
update_target_index(target_scope, target_path, file_key)
|
|
134
141
|
elsif File.exist?(target_path) && !@force
|
|
135
142
|
@results[file_key] = { status: "skipped", reason: "already exists" }
|
|
136
143
|
@warnings << "Skipped #{file_key} (already exists; use --force to overwrite)"
|
|
@@ -252,13 +259,19 @@ module Samagotchi
|
|
|
252
259
|
if @dry_run
|
|
253
260
|
incoming_rules.each { |src| @results["guardrails/#{File.basename(src)}"] = { status: "would_install" } }
|
|
254
261
|
else
|
|
262
|
+
# Same bytes as installed: reported as up to date, not "Installed".
|
|
263
|
+
unchanged = incoming_rules.select do |src|
|
|
264
|
+
dest = File.join(guardrails_target, File.basename(src))
|
|
265
|
+
File.file?(dest) && FileUtils.identical?(src, dest)
|
|
266
|
+
end
|
|
255
267
|
FileUtils.rm_rf(guardrails_target)
|
|
256
268
|
unless incoming_rules.empty?
|
|
257
269
|
FileUtils.mkdir_p(guardrails_target)
|
|
258
270
|
incoming_rules.each do |src|
|
|
259
271
|
dest = File.join(guardrails_target, File.basename(src))
|
|
260
272
|
FileUtils.cp(src, dest)
|
|
261
|
-
@results["guardrails/#{File.basename(src)}"] =
|
|
273
|
+
@results["guardrails/#{File.basename(src)}"] =
|
|
274
|
+
unchanged.include?(src) ? { status: "skipped", reason: "already up to date" } : { status: "installed" }
|
|
262
275
|
guardrail_files_for_provenance[File.basename(src)] = dest
|
|
263
276
|
end
|
|
264
277
|
end
|
|
@@ -278,7 +291,8 @@ module Samagotchi
|
|
|
278
291
|
all_files_in_bundle.each do |file_key|
|
|
279
292
|
next if @conflicts.key?(file_key)
|
|
280
293
|
status = @results[file_key] ? @results[file_key][:status].to_s : nil
|
|
281
|
-
|
|
294
|
+
# skipped: not copied, so a local edit isn't a bad download.
|
|
295
|
+
next if %w[kept kept_pruned fast_forward updated skipped].include?(status)
|
|
282
296
|
# For dry-run, fast_forward would appear as "fast_forward", updated not yet
|
|
283
297
|
target_path = File.join(target_dir, file_key)
|
|
284
298
|
next unless File.exist?(target_path)
|
|
@@ -340,7 +354,13 @@ module Samagotchi
|
|
|
340
354
|
else
|
|
341
355
|
all_files_in_bundle.each do |file_key|
|
|
342
356
|
target_path = File.join(target_dir, file_key)
|
|
343
|
-
|
|
357
|
+
next unless File.exist?(target_path)
|
|
358
|
+
|
|
359
|
+
# Skipped for a local edit: keep the installed base, so the
|
|
360
|
+
# next upgrade still sees the edit (not a fast-forward over it).
|
|
361
|
+
base_path = provenance.base_path(file_key)
|
|
362
|
+
kept = existing_provenance && @results.dig(file_key, :reason) == "already exists" && File.exist?(base_path)
|
|
363
|
+
provenance_files[file_key] = kept ? base_path : target_path
|
|
344
364
|
end
|
|
345
365
|
end
|
|
346
366
|
# Build hooks metadata for provenance
|
|
@@ -409,6 +429,7 @@ module Samagotchi
|
|
|
409
429
|
lines << "Conflicts: #{conflicts.join(', ')}" unless conflicts.empty?
|
|
410
430
|
lines.concat(@warnings) unless @warnings.empty?
|
|
411
431
|
lines.concat(@placeholder_warnings) unless @placeholder_warnings.empty?
|
|
432
|
+
lines << "#{@name} is already installed; `chi bundle upgrade #{@name}` updates it and keeps local edits" if @reinstall
|
|
412
433
|
|
|
413
434
|
lines.any? ? lines.join("\n") : "No files to install."
|
|
414
435
|
end
|
|
@@ -224,11 +224,11 @@ module Samagotchi
|
|
|
224
224
|
# a turn running, recap = the saved recap's first sentence, project =
|
|
225
225
|
# Session#project_root
|
|
226
226
|
def self.session_summaries(live: false, cwd: nil, limit: nil, include_tests: true, exclude: nil, state_dir: nil,
|
|
227
|
-
project_root: nil, include_archived: false)
|
|
227
|
+
project_root: nil, include_archived: false, sort: nil, order: nil)
|
|
228
228
|
sd = state_dir || Session.default_state_dir
|
|
229
229
|
root = cwd && folder_path(cwd)
|
|
230
230
|
roots = {}
|
|
231
|
-
summaries = Session.list(state_dir: sd, sort: "updated_at", order: "desc", project_root: project_root,
|
|
231
|
+
summaries = Session.list(state_dir: sd, sort: sort || "updated_at", order: order || "desc", project_root: project_root,
|
|
232
232
|
include_archived: include_archived).lazy
|
|
233
233
|
.reject { |s| (!include_tests && s.test_run) || s.id == exclude }
|
|
234
234
|
.select { |s| root.nil? || in_folder?(s.working_directory, root) }
|
|
@@ -427,7 +427,8 @@ module Samagotchi
|
|
|
427
427
|
).merge(
|
|
428
428
|
status: status.empty? ? "ok" : status,
|
|
429
429
|
completed_at: finished_at.iso8601(3),
|
|
430
|
-
|
|
430
|
+
# Less a guardrail approval wait (ToolRunner's waited_ms).
|
|
431
|
+
duration_ms: [elapsed_ms(active[:started_monotonic]) - event[:waited_ms].to_i, 0].max
|
|
431
432
|
)
|
|
432
433
|
end
|
|
433
434
|
end
|
|
@@ -120,11 +120,12 @@ module Samagotchi
|
|
|
120
120
|
end
|
|
121
121
|
|
|
122
122
|
def list_summaries(project, scope_note)
|
|
123
|
-
live, cwd, limit, include_archived, format
|
|
123
|
+
live, cwd, limit, include_archived, format, sort, order =
|
|
124
|
+
@opts.values_at(:live, :cwd, :limit, :include_archived, :format, :sort, :order)
|
|
124
125
|
summaries = Samagotchi::SessionManager.session_summaries(
|
|
125
126
|
live: live, cwd: cwd, limit: limit || (live ? 10 : nil), include_tests: Samagotchi::Session.test_session_env?,
|
|
126
127
|
project_root: project,
|
|
127
|
-
include_archived: include_archived
|
|
128
|
+
include_archived: include_archived, sort: sort, order: order
|
|
128
129
|
)
|
|
129
130
|
case format
|
|
130
131
|
when "json"
|
|
@@ -175,11 +175,12 @@ module Samagotchi
|
|
|
175
175
|
System scope: ~/.config/samagotchi/memories/ (cross-project)
|
|
176
176
|
memory_read accepts optional scope (project|system).
|
|
177
177
|
memory_write requires explicit scope and entry name.
|
|
178
|
+
For a small change to an existing memory, `edit` its file (<scope dir>/<name>.md)
|
|
179
|
+
instead of rewriting it all with memory_write; its index line is refreshed either way.
|
|
178
180
|
User prompts may contain memory shorthand like #entry_name.
|
|
179
181
|
Treat #entry_name as a memory reference, not as a file path.
|
|
180
182
|
If shorthand includes a scope prefix, such as #project/entry_name or #system/entry_name,
|
|
181
183
|
preserve that scope when reading the memory.
|
|
182
|
-
Keep each scope's index.md updated when adding/updating entries.
|
|
183
184
|
Each scope's `index.md` is auto-maintained by `memory_write` (one
|
|
184
185
|
managed line per entry with name/scope/date/size); free-form sections
|
|
185
186
|
are preserved. The verbatim `index` write (`name: "index"`) is kept.
|
|
@@ -123,7 +123,7 @@ module Samagotchi
|
|
|
123
123
|
},
|
|
124
124
|
{
|
|
125
125
|
name: "memory_write",
|
|
126
|
-
description: "Write or update a memory entry in scoped memories. Scope is required: project or system. Use the `name` parameter for the entry name (use `name`, NOT `path` — the file tools use `path`); each scope's index.md is auto-maintained (one managed line per entry); use name \"index\" to write the index file verbatim. If the user asks to save guidance for the current model only, pass current_model_only: true to save a model-specific overlay.",
|
|
126
|
+
description: "Write or update a memory entry in scoped memories. Scope is required: project or system. Use the `name` parameter for the entry name (use `name`, NOT `path` — the file tools use `path`); each scope's index.md is auto-maintained (one managed line per entry); use name \"index\" to write the index file verbatim. For a small change to an existing memory (a step, a line), `edit` its file (<scope dir>/<name>.md) instead of rewriting it all; its index line is refreshed either way. If the user asks to save guidance for the current model only, pass current_model_only: true to save a model-specific overlay.",
|
|
127
127
|
parameters: {
|
|
128
128
|
type: "object",
|
|
129
129
|
properties: {
|
|
@@ -6,6 +6,7 @@ require_relative "vision_context"
|
|
|
6
6
|
require_relative "log"
|
|
7
7
|
require_relative "edit_preview"
|
|
8
8
|
require_relative "tools/tool_path"
|
|
9
|
+
require_relative "memory_bundle/index_sync"
|
|
9
10
|
|
|
10
11
|
module Samagotchi
|
|
11
12
|
# The single per-call path both loops use: the tool_call_started and
|
|
@@ -67,6 +68,7 @@ module Samagotchi
|
|
|
67
68
|
before = verdict.deny? ? NOT_AN_EDIT : file_before(call)
|
|
68
69
|
result = verdict.deny? ? denied(call, verdict) : dispatch(call)
|
|
69
70
|
diff = file_change(call, before)
|
|
71
|
+
refresh_memory_index(call) if diff
|
|
70
72
|
result = approved(result, verdict) if verdict.allow? && verdict.decided_by
|
|
71
73
|
result, images = attach_images(call, result) if result[:images]
|
|
72
74
|
|
|
@@ -117,6 +119,12 @@ module Samagotchi
|
|
|
117
119
|
nil
|
|
118
120
|
end
|
|
119
121
|
|
|
122
|
+
# A write/edit that changed a memory's file refreshes its index line,
|
|
123
|
+
# as memory_write does (MemoryBundle::IndexSync; a no-op elsewhere).
|
|
124
|
+
def refresh_memory_index(call)
|
|
125
|
+
MemoryBundle::IndexSync.refresh(Tools::ToolPath.normalize(call[:path]))
|
|
126
|
+
end
|
|
127
|
+
|
|
120
128
|
# A tool's output can hold bytes that aren't UTF-8 (`printf '\xff'`, a
|
|
121
129
|
# binary file). They become "?" here, before the output reaches the
|
|
122
130
|
# conversation, the events and the saved session: JSON.generate raises
|
|
@@ -204,9 +204,8 @@ module Samagotchi
|
|
|
204
204
|
# The verbatim "write to index.md" behavior (path: "index") must not
|
|
205
205
|
# trigger upsert logic.
|
|
206
206
|
unless entry_name == MEMORY_INDEX
|
|
207
|
-
index_path = self.index_path_for(resolved_scope)
|
|
208
207
|
if manage_index(resolved_scope, entry_name, bytes, description)
|
|
209
|
-
message += " Index
|
|
208
|
+
message += " Index line refreshed automatically."
|
|
210
209
|
end
|
|
211
210
|
end
|
|
212
211
|
|
data/lib/samagotchi/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: samagotchi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -256,6 +256,7 @@ files:
|
|
|
256
256
|
- lib/samagotchi/log_subscriber.rb
|
|
257
257
|
- lib/samagotchi/memory_bundle.rb
|
|
258
258
|
- lib/samagotchi/memory_bundle/builder.rb
|
|
259
|
+
- lib/samagotchi/memory_bundle/index_sync.rb
|
|
259
260
|
- lib/samagotchi/memory_bundle/index_updater.rb
|
|
260
261
|
- lib/samagotchi/memory_bundle/installer.rb
|
|
261
262
|
- lib/samagotchi/memory_bundle/listing.rb
|