samagotchi 0.8.1 → 0.10.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 +89 -1
- data/docs/architecture.md +24 -9
- data/docs/cli.md +4 -3
- data/docs/guardrails.md +9 -2
- data/docs/hooks.md +44 -7
- data/docs/internals/gemma4-contract.md +1 -0
- data/docs/memory.md +5 -3
- data/docs/plugins.md +47 -17
- data/docs/sessions.md +1 -1
- data/lib/samagotchi/bootstrap_command.rb +29 -29
- data/lib/samagotchi/bridge/card_store.rb +4 -4
- data/lib/samagotchi/bridge/pending_card.rb +2 -2
- data/lib/samagotchi/bridge/sse_writer.rb +22 -28
- data/lib/samagotchi/bridge/turn_accumulator.rb +92 -5
- data/lib/samagotchi/bridge.rb +59 -112
- data/lib/samagotchi/bridge_client.rb +11 -28
- data/lib/samagotchi/bundle_command.rb +158 -212
- data/lib/samagotchi/bundles/check-in/manifest.yml +2 -2
- data/lib/samagotchi/bundles/check-in/plugin.rb +3 -4
- data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +7 -3
- data/lib/samagotchi/bundles/known-names/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/plugin.rb +10 -10
- data/lib/samagotchi/bundles/mcp/manifest.yml +3 -3
- data/lib/samagotchi/bundles/mcp/plugin.rb +2 -4
- data/lib/samagotchi/bundles/skills/manifest.yml +3 -3
- data/lib/samagotchi/bundles/skills/plugin.rb +65 -17
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +0 -1
- data/lib/samagotchi/bundles/source-links/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
- data/lib/samagotchi/bundles/system/identity.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/memory_guide.md +5 -4
- data/lib/samagotchi/cli/command.rb +51 -0
- data/lib/samagotchi/cli/flags.rb +116 -0
- data/lib/samagotchi/config.rb +70 -55
- data/lib/samagotchi/config_text_edit.rb +107 -0
- data/lib/samagotchi/desktop_command.rb +24 -18
- data/lib/samagotchi/edit_preview.rb +2 -2
- data/lib/samagotchi/engine.rb +20 -75
- data/lib/samagotchi/events.rb +23 -0
- data/lib/samagotchi/guardrail_wiring.rb +2 -5
- data/lib/samagotchi/guardrails/targets.rb +7 -1
- data/lib/samagotchi/hooks/bundle_loader.rb +13 -30
- data/lib/samagotchi/hooks/loader.rb +23 -33
- data/lib/samagotchi/hooks/registry.rb +41 -0
- data/lib/samagotchi/hooks.rb +0 -3
- data/lib/samagotchi/host_registry.rb +15 -0
- data/lib/samagotchi/idle_reminders.rb +0 -7
- data/lib/samagotchi/image_store.rb +22 -0
- data/lib/samagotchi/kernel_loop.rb +12 -16
- data/lib/samagotchi/live_versions.rb +2 -13
- data/lib/samagotchi/llm/native_tool_normalizer.rb +10 -227
- data/lib/samagotchi/log_subscriber.rb +5 -1
- data/lib/samagotchi/memory_bundle/builder.rb +1 -14
- data/lib/samagotchi/memory_bundle/index_sync.rb +73 -0
- data/lib/samagotchi/memory_bundle/index_updater.rb +2 -28
- data/lib/samagotchi/memory_bundle/installer.rb +25 -30
- data/lib/samagotchi/memory_bundle/provenance.rb +2 -6
- data/lib/samagotchi/memory_bundle/status.rb +1 -14
- data/lib/samagotchi/memory_bundle/system_bundle.rb +2 -2
- data/lib/samagotchi/memory_bundle/uninstaller.rb +1 -24
- data/lib/samagotchi/memory_paths.rb +10 -0
- data/lib/samagotchi/note_command.rb +23 -37
- data/lib/samagotchi/owner_lock.rb +14 -3
- data/lib/samagotchi/plugin/api.rb +11 -17
- data/lib/samagotchi/plugin/context.rb +70 -15
- data/lib/samagotchi/plugin/loader.rb +2 -3
- data/lib/samagotchi/sampling_settings.rb +3 -5
- data/lib/samagotchi/self_report.rb +6 -9
- data/lib/samagotchi/send_command.rb +33 -52
- data/lib/samagotchi/session.rb +88 -92
- data/lib/samagotchi/session_archive_command.rb +8 -14
- data/lib/samagotchi/session_commands.rb +3 -21
- data/lib/samagotchi/session_delete_command.rb +15 -29
- data/lib/samagotchi/session_manager.rb +29 -15
- 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/terminal_ui/attached_loop.rb +30 -80
- data/lib/samagotchi/terminal_ui/attached_view.rb +1 -10
- data/lib/samagotchi/terminal_ui/between_turns.rb +133 -0
- data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -1
- data/lib/samagotchi/terminal_ui/formatting.rb +51 -1
- data/lib/samagotchi/terminal_ui/status_row.rb +32 -0
- data/lib/samagotchi/terminal_ui/thinking_line.rb +3 -3
- data/lib/samagotchi/terminal_ui.rb +156 -234
- data/lib/samagotchi/thinking.rb +2 -3
- data/lib/samagotchi/thought_stream_splitter.rb +10 -9
- data/lib/samagotchi/tool_call_parser.rb +56 -292
- data/lib/samagotchi/tool_declarations.rb +1 -1
- data/lib/samagotchi/tool_runner.rb +8 -0
- data/lib/samagotchi/tools/builtin_calls.rb +146 -0
- data/lib/samagotchi/tools/builtins.rb +2 -1
- data/lib/samagotchi/tools/edit.rb +21 -29
- data/lib/samagotchi/tools/memory.rb +2 -13
- data/lib/samagotchi/tools/send_note.rb +1 -1
- data/lib/samagotchi/update_command.rb +32 -22
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +4 -5
- data/lib/samagotchi/web/app.rb +89 -151
- data/lib/samagotchi/web/public/activity.js +1 -11
- data/lib/samagotchi/web/public/annotate_bar.js +120 -0
- data/lib/samagotchi/web/public/app.js +158 -785
- data/lib/samagotchi/web/public/chunk_router.js +7 -6
- data/lib/samagotchi/web/public/command_list.js +99 -0
- data/lib/samagotchi/web/public/data.js +22 -66
- data/lib/samagotchi/web/public/format.js +0 -23
- data/lib/samagotchi/web/public/index.html +0 -2
- data/lib/samagotchi/web/public/model_pick.js +0 -7
- data/lib/samagotchi/web/public/notifications.js +169 -0
- data/lib/samagotchi/web/public/question_cards.js +352 -0
- data/lib/samagotchi/web/public/session_state.js +34 -0
- data/lib/samagotchi/web/public/timing.js +1 -1
- data/lib/samagotchi/web/public/turn_events.js +11 -2
- data/lib/samagotchi/web/public/turn_model.js +0 -14
- data/lib/samagotchi/web/session_hub.rb +6 -4
- data/lib/samagotchi/web/session_summary.rb +5 -3
- data/lib/samagotchi/worker.rb +59 -53
- data/lib/samagotchi/worker_sidecar.rb +27 -1
- metadata +13 -1
|
@@ -187,13 +187,13 @@ end
|
|
|
187
187
|
class Plugin
|
|
188
188
|
DEFAULT_IGNORE = %w[task_wait task_get delegate_result list_sessions list_reminders].freeze
|
|
189
189
|
# Built-in tools carry flat fields; plugin and unknown tools carry args:.
|
|
190
|
-
|
|
190
|
+
# An edit's text is its old_text/new_text.
|
|
191
|
+
KEY_FIELDS = %i[content path start_line end_line cwd env scope old_text new_text].freeze
|
|
191
192
|
SHORT_CHARS = 60
|
|
192
193
|
THOUGHT_CHARS = 80
|
|
193
194
|
SOURCE = "bundle loop-guard"
|
|
194
195
|
|
|
195
196
|
def initialize(settings = {})
|
|
196
|
-
settings = {} unless settings.is_a?(Hash)
|
|
197
197
|
@deny_after = positive(settings["deny_after"]) || 2
|
|
198
198
|
@stop_after = positive(settings["stop_after"]) || 4
|
|
199
199
|
@ignore = settings.key?("ignore_tools") ? Array(settings["ignore_tools"]).map(&:to_s) : DEFAULT_IGNORE
|
|
@@ -234,16 +234,16 @@ class Plugin
|
|
|
234
234
|
ctx.notify("thinking repeats itself (#{what})", level: :warn)
|
|
235
235
|
when "retry"
|
|
236
236
|
@thinking_loops += 1
|
|
237
|
-
return stop_thinking(
|
|
237
|
+
return stop_thinking(ctx, found, what) if @thinking_loops > 1
|
|
238
238
|
|
|
239
|
-
ctx.notify("thinking repeats itself (#{what}): cut", level: :warn) if
|
|
239
|
+
ctx.notify("thinking repeats itself (#{what}): cut", level: :warn) if ctx.stop_generation("its thinking kept repeating itself")
|
|
240
240
|
else
|
|
241
|
-
stop_thinking(
|
|
241
|
+
stop_thinking(ctx, found, what)
|
|
242
242
|
end
|
|
243
243
|
end
|
|
244
244
|
|
|
245
|
-
def stop_thinking(
|
|
246
|
-
return unless
|
|
245
|
+
def stop_thinking(ctx, found, what)
|
|
246
|
+
return unless ctx.stop_turn("the model's thinking kept repeating itself (#{what})")
|
|
247
247
|
|
|
248
248
|
lines = found.sentences.first(3).map { |sentence| "- \"#{cut(sentence, THOUGHT_CHARS)}\"" }
|
|
249
249
|
ctx.card(title: "loop-guard stopped the turn",
|
|
@@ -288,7 +288,7 @@ class Plugin
|
|
|
288
288
|
verdict.deny!("repeated call", source: SOURCE, advice: advice(key, times))
|
|
289
289
|
warn(ctx, key, "repeated, denied")
|
|
290
290
|
@denials += 1
|
|
291
|
-
stop(
|
|
291
|
+
stop(ctx) if @denials == @stop_after
|
|
292
292
|
end
|
|
293
293
|
|
|
294
294
|
# A call's result, paired with the before_tool_call that keyed it. A call
|
|
@@ -308,8 +308,8 @@ class Plugin
|
|
|
308
308
|
@previews[key] = preview(output)
|
|
309
309
|
end
|
|
310
310
|
|
|
311
|
-
def stop(
|
|
312
|
-
|
|
311
|
+
def stop(ctx)
|
|
312
|
+
ctx.stop_turn("the model kept repeating the same calls")
|
|
313
313
|
repeated = @attempts.select { |key, n| n > 1 && @last_result[key] && @results[[key, @last_result[key]]] >= @deny_after }
|
|
314
314
|
lines = repeated.map do |key, n|
|
|
315
315
|
"- `#{short_key(key)}`: #{n} times, the same result each time (#{@previews[key]})"
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mcp
|
|
3
|
-
version: 0.3.
|
|
3
|
+
version: 0.3.2
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Tools from MCP servers (stdio): each server in config.yml bundles: mcp: servers: runs for the session, its tools are mcp_<server>_<tool>; /mcp lists them"
|
|
6
6
|
trust_level: reviewed
|
|
7
7
|
files: {}
|
|
8
8
|
plugin:
|
|
9
9
|
file: plugin.rb
|
|
10
|
-
sha256: sha256:
|
|
11
|
-
requires_chi: ">= 0.
|
|
10
|
+
sha256: sha256:61b195a5621ddf57ba69a505aff06d620d6459e836d736205343a251b0fecb71
|
|
11
|
+
requires_chi: ">= 0.8.0"
|
|
@@ -421,10 +421,8 @@ class Plugin
|
|
|
421
421
|
|
|
422
422
|
# Written aside and renamed: workers share the dir.
|
|
423
423
|
def save_cache(server, ctx)
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
File.write(tmp, JSON.generate({ digest: server.digest, saved_at: Time.now.utc.iso8601, tools: server.listed }))
|
|
427
|
-
File.rename(tmp, path)
|
|
424
|
+
Samagotchi::AtomicFile.write(cache_path(server, ctx),
|
|
425
|
+
JSON.generate({ digest: server.digest, saved_at: Time.now.utc.iso8601, tools: server.listed }))
|
|
428
426
|
rescue SystemCallError => e
|
|
429
427
|
ctx.log.warn("mcp_cache_not_written", server: server.name, msg: e.message)
|
|
430
428
|
end
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: skills
|
|
3
|
-
version: 0.1.
|
|
3
|
+
version: 0.1.3
|
|
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:4f32b3457ada3392009e4a32b690e577ef0bb05d46c6cfed9ae02640119620d8
|
|
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,13 +38,11 @@ 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
|
|
|
39
45
|
def initialize(settings = {})
|
|
40
|
-
settings = {} unless settings.is_a?(Hash)
|
|
41
46
|
@history_keep = positive(settings["history_keep"]) || 20
|
|
42
47
|
@nudge = settings.key?("nudge") ? settings["nudge"] != false : true
|
|
43
48
|
@stash = nil
|
|
@@ -79,16 +84,38 @@ class Plugin
|
|
|
79
84
|
def after_tool_call(event, ctx)
|
|
80
85
|
stash = @stash
|
|
81
86
|
@stash = nil
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
stash = nil unless stash && stash[:tool] == event[:tool].to_s
|
|
88
|
+
changed_elsewhere(ctx) unless stash
|
|
89
|
+
step_failed(event, ctx) if @nudge && !@read.empty?
|
|
90
|
+
return unless stash
|
|
84
91
|
|
|
85
92
|
now = File.file?(stash[:path]) ? File.read(stash[:path]) : nil
|
|
86
93
|
return if now.nil? || now == stash[:old]
|
|
87
94
|
|
|
88
|
-
@written
|
|
95
|
+
@written |= [stash[:name]]
|
|
96
|
+
@seen[stash[:name]] = { path: stash[:path], scope: stash[:scope], content: now }
|
|
89
97
|
ctx.notify(change_notice(stash, now))
|
|
90
98
|
end
|
|
91
99
|
|
|
100
|
+
# A skill read this turn whose file differs from the content last seen was
|
|
101
|
+
# changed by something other than the write tools (an execute's sed, say):
|
|
102
|
+
# keep what was there as a version, show the change, count the skill as
|
|
103
|
+
# updated (any change counts, the user's own edit too). A file gone since
|
|
104
|
+
# is left alone.
|
|
105
|
+
def changed_elsewhere(ctx)
|
|
106
|
+
@seen.each do |name, seen|
|
|
107
|
+
now = File.file?(seen[:path]) ? File.read(seen[:path]) : nil
|
|
108
|
+
next if now.nil? || now == seen[:content]
|
|
109
|
+
|
|
110
|
+
keep_version(ctx, seen[:scope], name, seen[:content]) if seen[:content]
|
|
111
|
+
ctx.notify(change_notice({ name: name, scope: seen[:scope], old: seen[:content] }, now))
|
|
112
|
+
seen[:content] = now
|
|
113
|
+
@written |= [name]
|
|
114
|
+
end
|
|
115
|
+
rescue SystemCallError => e
|
|
116
|
+
ctx.log.warn(:watch_failed, error: e.class.name, msg: e.message)
|
|
117
|
+
end
|
|
118
|
+
|
|
92
119
|
def change_notice(stash, now)
|
|
93
120
|
name = stash[:name]
|
|
94
121
|
return "skill #{name} saved (#{stash[:scope]}, #{now.lines.size} lines)" unless stash[:old]
|
|
@@ -105,31 +132,52 @@ class Plugin
|
|
|
105
132
|
|
|
106
133
|
def reset_turn
|
|
107
134
|
@read = [] # skills read this turn, in order
|
|
135
|
+
@seen = {} # of those, name => {path:, scope:, content:} last seen
|
|
108
136
|
@written = [] # skills changed this turn
|
|
109
137
|
@failed = false # a step failed after a skill was read
|
|
110
138
|
@nudged = false
|
|
111
139
|
end
|
|
112
140
|
|
|
113
141
|
def note_read(tool, event)
|
|
114
|
-
|
|
142
|
+
found = case tool
|
|
115
143
|
when "memory_read"
|
|
144
|
+
scope = event.dig(:call, :scope).to_s.strip
|
|
145
|
+
scope = nil unless memory_dirs.key?(scope)
|
|
146
|
+
# A skill not written yet is watched where memory_write would
|
|
147
|
+
# put it by default: the read scope, else project.
|
|
116
148
|
event.dig(:call, :content).to_s.split(",").map(&:strip).select { |entry| entry.start_with?("skill_") }
|
|
117
|
-
.filter_map
|
|
149
|
+
.filter_map do |entry|
|
|
150
|
+
name = skill_name(entry) or next
|
|
151
|
+
unwritten = File.join(memory_dirs[scope || "project"], "skill_#{name}.md")
|
|
152
|
+
[name, skill_path(name, scope: scope) || unwritten]
|
|
153
|
+
end
|
|
118
154
|
when "read"
|
|
119
|
-
Array(event.dig(:targets, :paths)).filter_map { |path| skill_at(path)
|
|
155
|
+
Array(event.dig(:targets, :paths)).filter_map { |path| (at = skill_at(path)) && [at.last, path] }
|
|
120
156
|
else []
|
|
121
157
|
end
|
|
122
|
-
|
|
158
|
+
found.each { |name, path| watch(name, path) }
|
|
159
|
+
@read |= found.map(&:first)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Remember a read skill's content (nil: not written yet) the first time in
|
|
163
|
+
# a turn: a later read mustn't hide a change made in between.
|
|
164
|
+
def watch(name, path)
|
|
165
|
+
return if @seen.key?(name) || path.nil?
|
|
166
|
+
|
|
167
|
+
content = File.file?(path) ? File.read(path) : nil
|
|
168
|
+
@seen[name] = { path: File.expand_path(path), scope: skill_at(path)&.first, content: content }
|
|
169
|
+
rescue SystemCallError
|
|
170
|
+
nil
|
|
123
171
|
end
|
|
124
172
|
|
|
125
|
-
def step_failed(event)
|
|
173
|
+
def step_failed(event, ctx)
|
|
126
174
|
tool = event[:tool].to_s
|
|
127
175
|
return if NOT_FAILURES.include?(tool) || !failure?(tool, event[:output].to_s)
|
|
128
176
|
|
|
129
177
|
@failed = true
|
|
130
|
-
return if @nudged
|
|
178
|
+
return if @nudged || @read.any? { |name| @written.include?(name) }
|
|
131
179
|
|
|
132
|
-
@nudged =
|
|
180
|
+
@nudged = ctx.steer(format(NUDGE, @read.join(", ")))
|
|
133
181
|
end
|
|
134
182
|
|
|
135
183
|
# A tool error ("[x] Error: …" raised, "[x]\nError: …" returned), or an
|
|
@@ -146,6 +194,7 @@ class Plugin
|
|
|
146
194
|
end
|
|
147
195
|
|
|
148
196
|
def after_turn(ctx)
|
|
197
|
+
changed_elsewhere(ctx)
|
|
149
198
|
return unless @nudge && @failed
|
|
150
199
|
|
|
151
200
|
missed = @read - @written
|
|
@@ -166,8 +215,7 @@ class Plugin
|
|
|
166
215
|
|
|
167
216
|
stamp = Time.now.utc.strftime("%Y%m%dT%H%M%S.%6NZ")
|
|
168
217
|
path = File.join(dir, "#{stamp}.md")
|
|
169
|
-
|
|
170
|
-
File.rename("#{path}.tmp", path)
|
|
218
|
+
Samagotchi::AtomicFile.write(path, content)
|
|
171
219
|
versions(dir).drop(@history_keep).each { |old| File.delete(old) }
|
|
172
220
|
rescue SystemCallError => e
|
|
173
221
|
ctx.log.warn(:history_failed, skill: name, error: e.class.name, msg: e.message)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: source-links
|
|
3
|
-
version: 0.3.
|
|
3
|
+
version: 0.3.1
|
|
4
4
|
scope: system
|
|
5
5
|
description: Links source refs (JIRA tickets, GitHub issues, …) in the model's answer — inline links in the web, and a one-line note after the turn (note false turns it off); sources are configured patterns
|
|
6
6
|
trust_level: reviewed
|
|
@@ -8,7 +8,7 @@ files:
|
|
|
8
8
|
source_links.md: sha256:aebd37f1e25491780994ecc4afc20ed6e8db32b724dc038302bb050177efada8
|
|
9
9
|
hooks:
|
|
10
10
|
source_links.rb:
|
|
11
|
-
sha256: sha256:
|
|
11
|
+
sha256: sha256:574ea0c2b36f80dde410c4251753d70cbeea081d129f3f3547f41370e3990993
|
|
12
12
|
event: after_turn
|
|
13
13
|
on_error: log
|
|
14
14
|
priority: 90
|
|
@@ -113,7 +113,7 @@ bundles:
|
|
|
113
113
|
|
|
114
114
|
A guardrail rule's `tool:` may be a glob (`tool: "mcp_*"`, verdict `ask`) to cover every MCP tool; see `docs/guardrails.md`. The mcp bundle saves each server's tool list in `$XDG_STATE_HOME/samagotchi/plugins/mcp/tools-<server>.json` (keyed by a digest of command/env/cwd): a changed server config is picked up by the next session start, which shows "Starting MCP server x (config changed, …)".
|
|
115
115
|
|
|
116
|
-
**Preservation rule**: `ConfigFile.write_default_model!` and `ConfigFile.write_model_alias!`
|
|
116
|
+
**Preservation rule**: `ConfigFile.write_default_model!` and `ConfigFile.write_model_alias!` (`/model --default`, `/model --alias`) change one key on its own line of the text (`ConfigTextEdit`: the user's comments and layout stay; a new key goes at the end of its section, a missing section at the end of the file), check that the result parses to the old data plus that key (else they dump the whole file from it), and write through `AtomicFile` (a unique temp file renamed over config.yml, through a symlink, keeping its mode). Never overwrite the file with only scalar keys — that would clobber `hooks:` / `model_aliases:` / `hosts:` / `recap:` / `guardrails:` / `bundles:`.
|
|
117
117
|
|
|
118
118
|
## Workflow for any config edit
|
|
119
119
|
|
|
@@ -121,7 +121,7 @@ A guardrail rule's `tool:` may be a glob (`tool: "mcp_*"`, verdict `ask`) to cov
|
|
|
121
121
|
2. `YAML.safe_load` (permitted_classes: [], aliases: false). If data nil or not Hash, treat as `{}` or raise with path.
|
|
122
122
|
3. Mutate the intended **nested** key in the raw hash. Preserve all other keys byte-for-byte where possible. Example for default model: `raw_data["default"] ||= {}; raw_data["default"]["model"] = "new-model"`.
|
|
123
123
|
4. **Validate** (see below) before writing. Also run `Samagotchi::Config.validate_yaml_sections` — it returns one `config: unknown key '…' (did you mean '…'?)` per key chi doesn't read (every config-exposed `Config::ENTRIES` key is known as written, including the section-less `max_tool_output_chars` and `skip_agent_md`; names under `hosts:`/`models:`/`model_aliases:`/`hooks:`/`bundles:`/`memories:` are free-form, host and model entries are checked against `Config::MAP_ENTRY_KEYS`). An empty list means no warning at start.
|
|
124
|
-
5. **Write
|
|
124
|
+
5. **Write**: change only the lines you mean to, with the edit tool, so the user's comments and layout stay; `YAML.dump` of the whole hash drops every comment, so rewrite the file only when it has none worth keeping. From Ruby, write atomically with `Samagotchi::AtomicFile.write(path, text)` (a unique temp file renamed over it), never through a fixed `"#{path}.tmp"` that another chi may be writing at the same time.
|
|
125
125
|
6. Nothing else to update: config.yml values are never copied into `ENV`, and every `Config.get` reads the file again when it changed, so the next read (and every worker started after the write) sees the new value with origin `:file`. What a running worker set up at its start (`hosts:`, guardrail rules, bundle settings) waits for its restart. CLI overrides (`--model`, `--recap-model`, …) win over the file until the process exits; a worker gets its spawner's CLI settings through its env (`Config.cli_env`).
|
|
126
126
|
|
|
127
127
|
## Validations
|
|
@@ -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.10.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
|
-
config_modification_protocol.md: sha256:
|
|
10
|
-
memory_guide.md: sha256:
|
|
9
|
+
config_modification_protocol.md: sha256:d8bdf062d5afce7430ead7f49439c1d1ed197a6261dc69a7c884f4b814266b12
|
|
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
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
module CLI
|
|
5
|
+
# The usage side of a subcommand: its error lines, usage errors (exit 2)
|
|
6
|
+
# and help. The including class sets @stdout and @stderr and defines
|
|
7
|
+
# #command_name ("chi send"); its usage text is USAGE unless it
|
|
8
|
+
# overrides #usage_text.
|
|
9
|
+
module Command
|
|
10
|
+
USAGE_EXIT = 2
|
|
11
|
+
HELP_WORDS = %w[-h --help help].freeze
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def usage_text = self.class::USAGE
|
|
16
|
+
|
|
17
|
+
# What follows a usage error's line: the usage, or nil for a short
|
|
18
|
+
# "(see chi … --help)" at the end of the line instead.
|
|
19
|
+
def usage_on_error = usage_text
|
|
20
|
+
|
|
21
|
+
# stderr isn't buffered, stdout is when it's a pipe: flush the lines
|
|
22
|
+
# already printed, so the output keeps the order of the ids given.
|
|
23
|
+
def error_line(text)
|
|
24
|
+
@stdout.flush
|
|
25
|
+
@stderr.puts(text)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @return [Integer] the usage exit status
|
|
29
|
+
def usage_error(message)
|
|
30
|
+
usage = usage_on_error
|
|
31
|
+
error_line("#{command_name}: #{message}#{usage ? "" : " (see #{command_name} --help)"}")
|
|
32
|
+
@stderr.puts(usage) if usage
|
|
33
|
+
USAGE_EXIT
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# +argv+ through +flags+ (a Flags), help and usage errors done.
|
|
37
|
+
# @return [Flags::Result, Integer] the result, or the exit status
|
|
38
|
+
# after the help (0) or a usage error (2)
|
|
39
|
+
def parse_flags(flags, argv, defaults = {})
|
|
40
|
+
parsed = flags.parse(argv, defaults)
|
|
41
|
+
if parsed.help
|
|
42
|
+
@stdout.puts(usage_text)
|
|
43
|
+
return 0
|
|
44
|
+
end
|
|
45
|
+
return usage_error(parsed.error.message) if parsed.error
|
|
46
|
+
|
|
47
|
+
parsed
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
module CLI
|
|
5
|
+
# A subcommand's flags as a table (the subcommands bin/chi dispatches
|
|
6
|
+
# before OptionParser): each flag's names, whether it takes a value
|
|
7
|
+
# ("--flag V", or "--flag=V" for a long name) and whether it repeats.
|
|
8
|
+
# #parse walks argv once, in order, and stops at the first help word or
|
|
9
|
+
# error; what isn't a flag is a positional argument.
|
|
10
|
+
#
|
|
11
|
+
# FLAGS = CLI::Flags.new(help: %w[-h --help help]) do |f|
|
|
12
|
+
# f.value "-m", "--message"
|
|
13
|
+
# f.value "--image", key: :images, repeat: true
|
|
14
|
+
# f.switch "--new"
|
|
15
|
+
# end
|
|
16
|
+
# FLAGS.parse(argv, images: []) # => Result
|
|
17
|
+
class Flags
|
|
18
|
+
Flag = Struct.new(:key, :value, :repeat, :set, :refusal, keyword_init: true)
|
|
19
|
+
|
|
20
|
+
# kind :unknown (an undeclared flag, a switch given =V, or a
|
|
21
|
+
# positional where none are taken), :missing (a value flag with no
|
|
22
|
+
# value after it) or :refused (a flag declared with #refuse); +arg+ is
|
|
23
|
+
# the argument as given.
|
|
24
|
+
Error = Data.define(:kind, :arg, :refusal) do
|
|
25
|
+
def message
|
|
26
|
+
case kind
|
|
27
|
+
when :unknown then "unknown option #{arg}"
|
|
28
|
+
when :missing then "#{arg} needs a value"
|
|
29
|
+
else refusal
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# +options+ by key, +args+ the positionals in order; +help+ true when
|
|
35
|
+
# a help word stopped the parse; +error+ nil or an Error.
|
|
36
|
+
Result = Data.define(:options, :args, :help, :error)
|
|
37
|
+
|
|
38
|
+
# @param help [Array<String>] words that stop the parse with help
|
|
39
|
+
# (not when they are a flag's value)
|
|
40
|
+
# @param flag_pattern [Regexp] what an undeclared flag looks like (an
|
|
41
|
+
# unknown option); other arguments are positionals
|
|
42
|
+
# @param dash_values [Boolean] false: a value flag followed by a
|
|
43
|
+
# "--…" argument has no value
|
|
44
|
+
# @param args [Boolean] false: a positional is an unknown option
|
|
45
|
+
def initialize(help: [], flag_pattern: /\A-/, dash_values: true, args: true)
|
|
46
|
+
@help = help
|
|
47
|
+
@flag_pattern = flag_pattern
|
|
48
|
+
@dash_values = dash_values
|
|
49
|
+
@args = args
|
|
50
|
+
@flags = {}
|
|
51
|
+
yield self if block_given?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# A flag without a value: options[key] = +set+. The key defaults to
|
|
55
|
+
# the last name without its dashes, snake_cased.
|
|
56
|
+
def switch(*names, key: nil, set: true)
|
|
57
|
+
add(names, Flag.new(key: key, value: false, set: set))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# A flag with a value; +repeat+: options[key] collects them in an array.
|
|
61
|
+
def value(*names, key: nil, repeat: false)
|
|
62
|
+
add(names, Flag.new(key: key, value: true, repeat: repeat))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# A flag that is an error, with +message+ (a flag that doesn't exist
|
|
66
|
+
# on purpose).
|
|
67
|
+
def refuse(*names, message)
|
|
68
|
+
add(names, Flag.new(refusal: message))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @param defaults [Hash] the options before any flag (not changed)
|
|
72
|
+
# @return [Result]
|
|
73
|
+
def parse(argv, defaults = {})
|
|
74
|
+
options = defaults.dup
|
|
75
|
+
args = []
|
|
76
|
+
rest = argv.dup
|
|
77
|
+
until rest.empty?
|
|
78
|
+
arg = rest.shift
|
|
79
|
+
return Result.new(options: options, args: args, help: true, error: nil) if @help.include?(arg)
|
|
80
|
+
|
|
81
|
+
name, inline = arg.start_with?("--") && arg.include?("=") ? arg.split("=", 2) : [arg, nil]
|
|
82
|
+
flag = @flags[name]
|
|
83
|
+
flag = nil if inline && !flag&.value
|
|
84
|
+
unless flag
|
|
85
|
+
return failure(options, args, :unknown, arg) if !@args || arg.match?(@flag_pattern)
|
|
86
|
+
|
|
87
|
+
args << arg
|
|
88
|
+
next
|
|
89
|
+
end
|
|
90
|
+
return failure(options, args, :refused, arg, flag.refusal) if flag.refusal
|
|
91
|
+
next options[flag.key] = flag.set unless flag.value
|
|
92
|
+
|
|
93
|
+
unless inline
|
|
94
|
+
return failure(options, args, :missing, arg) if rest.empty? || (!@dash_values && rest.first.start_with?("--"))
|
|
95
|
+
|
|
96
|
+
inline = rest.shift
|
|
97
|
+
end
|
|
98
|
+
options[flag.key] = flag.repeat ? [*options[flag.key], inline] : inline
|
|
99
|
+
end
|
|
100
|
+
Result.new(options: options, args: args, help: false, error: nil)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def add(names, flag)
|
|
106
|
+
flag.key ||= names.last.sub(/\A-+/, "").tr("-", "_").to_sym unless flag.refusal
|
|
107
|
+
names.each { |name| @flags[name] = flag }
|
|
108
|
+
self
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def failure(options, args, kind, arg, refusal = nil)
|
|
112
|
+
Result.new(options: options, args: args, help: false, error: Error.new(kind: kind, arg: arg, refusal: refusal))
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|