samagotchi 0.7.0 → 0.8.1
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 +79 -1
- data/README.md +7 -3
- data/docs/architecture.md +4 -4
- data/docs/cli.md +19 -4
- data/docs/configuration.md +1 -0
- data/docs/guardrails.md +44 -4
- data/docs/hooks.md +2 -2
- data/docs/internals/context-telemetry.md +3 -1
- data/docs/memory.md +48 -1
- data/docs/plugins.md +7 -5
- data/docs/releasing.md +6 -2
- data/docs/sessions.md +2 -2
- data/lib/samagotchi/archive_store.rb +3 -3
- data/lib/samagotchi/atomic_file.rb +34 -0
- data/lib/samagotchi/bootstrap/bundles.rb +37 -0
- data/lib/samagotchi/bootstrap/config_writer.rb +4 -6
- data/lib/samagotchi/bootstrap/probe.rb +9 -5
- data/lib/samagotchi/bootstrap_command.rb +78 -8
- data/lib/samagotchi/bridge/pending_card.rb +2 -3
- data/lib/samagotchi/bridge.rb +56 -116
- data/lib/samagotchi/bridge_client.rb +7 -11
- data/lib/samagotchi/bundle_command.rb +81 -21
- data/lib/samagotchi/bundle_needs.rb +2 -11
- data/lib/samagotchi/bundles/core/manifest.yml +11 -0
- data/lib/samagotchi/bundles/dev/manifest.yml +13 -0
- data/lib/samagotchi/bundles/guardrails/guardrails/small-models.yml +27 -0
- data/lib/samagotchi/bundles/guardrails/manifest.yml +4 -2
- data/lib/samagotchi/bundles/system/identity.md +1 -0
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/memory_guide.md +3 -2
- data/lib/samagotchi/client.rb +3 -23
- data/lib/samagotchi/config.rb +9 -11
- data/lib/samagotchi/context_note.rb +1 -1
- data/lib/samagotchi/context_status.rb +255 -0
- data/lib/samagotchi/desktop/macos.rb +2 -3
- data/lib/samagotchi/engine.rb +19 -7
- data/lib/samagotchi/guardrail_wiring.rb +6 -6
- data/lib/samagotchi/guardrails/approvals.rb +2 -7
- data/lib/samagotchi/guardrails/gate.rb +7 -4
- data/lib/samagotchi/guardrails/model_size.rb +87 -0
- data/lib/samagotchi/guardrails/rules.rb +35 -3
- data/lib/samagotchi/guardrails/targets.rb +22 -4
- data/lib/samagotchi/guardrails.rb +1 -0
- data/lib/samagotchi/hooks/bundle_loader.rb +6 -3
- data/lib/samagotchi/hooks/loader.rb +2 -1
- data/lib/samagotchi/hooks/registry.rb +21 -0
- data/lib/samagotchi/image_store.rb +2 -3
- data/lib/samagotchi/kernel_loop.rb +21 -283
- data/lib/samagotchi/live_versions.rb +9 -13
- data/lib/samagotchi/llm/chat_loop.rb +1 -1
- data/lib/samagotchi/llm/native_backend.rb +8 -8
- data/lib/samagotchi/llm/openai_chat.rb +4 -17
- data/lib/samagotchi/llm/utf8_scrub.rb +32 -0
- data/lib/samagotchi/log_path.rb +2 -3
- data/lib/samagotchi/memory_bundle/index_updater.rb +2 -11
- data/lib/samagotchi/memory_bundle/installer.rb +5 -7
- data/lib/samagotchi/memory_bundle/listing.rb +27 -20
- data/lib/samagotchi/memory_bundle/manifest.rb +32 -1
- data/lib/samagotchi/memory_bundle/profile.rb +145 -0
- data/lib/samagotchi/memory_bundle/provenance.rb +42 -53
- data/lib/samagotchi/memory_bundle/shipped_update.rb +52 -27
- data/lib/samagotchi/memory_bundle/source.rb +2 -0
- data/lib/samagotchi/memory_bundle/status.rb +1 -2
- data/lib/samagotchi/memory_bundle.rb +1 -0
- data/lib/samagotchi/note_command.rb +5 -4
- data/lib/samagotchi/owner_lock.rb +0 -6
- data/lib/samagotchi/paths.rb +32 -0
- data/lib/samagotchi/plugin/api.rb +3 -6
- data/lib/samagotchi/plugin/context.rb +2 -3
- data/lib/samagotchi/plugin/loader.rb +1 -1
- data/lib/samagotchi/question_desk.rb +3 -2
- data/lib/samagotchi/recap_store.rb +2 -2
- data/lib/samagotchi/reply_wait.rb +2 -1
- data/lib/samagotchi/self_report.rb +2 -3
- data/lib/samagotchi/send_command.rb +3 -2
- data/lib/samagotchi/session.rb +41 -143
- data/lib/samagotchi/session_commands.rb +6 -1
- data/lib/samagotchi/session_inbox.rb +175 -0
- data/lib/samagotchi/session_manager.rb +62 -363
- data/lib/samagotchi/session_metrics.rb +2 -3
- data/lib/samagotchi/session_retention.rb +259 -0
- data/lib/samagotchi/sessions_command.rb +3 -2
- data/lib/samagotchi/terminal_ui/attached_loop.rb +1 -2
- data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -1
- data/lib/samagotchi/terminal_ui/input_support.rb +2 -9
- data/lib/samagotchi/terminal_ui.rb +10 -20
- data/lib/samagotchi/tool_runner.rb +12 -3
- data/lib/samagotchi/tools/memory.rb +3 -2
- data/lib/samagotchi/tools/send_note.rb +4 -3
- data/lib/samagotchi/tools/task_runtime.rb +2 -3
- data/lib/samagotchi/turn_note.rb +13 -2
- data/lib/samagotchi/update_command.rb +9 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +44 -76
- data/lib/samagotchi/web/info_probe.rb +33 -0
- data/lib/samagotchi/web/public/app.js +1 -1
- data/lib/samagotchi/web/public/index.html +1 -0
- data/lib/samagotchi/web/public/stage_model.js +22 -3
- data/lib/samagotchi/web/public/stage_view.js +25 -10
- data/lib/samagotchi/web/server.rb +8 -11
- data/lib/samagotchi/web/session_hub.rb +8 -7
- data/lib/samagotchi/web/session_summary.rb +4 -3
- data/lib/samagotchi/web/token.rb +2 -5
- data/lib/samagotchi/worker.rb +34 -20
- data/lib/samagotchi/worker_sidecar.rb +42 -0
- metadata +15 -1
|
@@ -94,6 +94,9 @@ module Samagotchi
|
|
|
94
94
|
return 1
|
|
95
95
|
end
|
|
96
96
|
expanded_source = expand_source(source)
|
|
97
|
+
if Samagotchi::MemoryBundle::Profile.shipped_meta?(expanded_source)
|
|
98
|
+
return install_profile(expanded_source, scope: scope, force: force, dry_run: false, word: "Install")
|
|
99
|
+
end
|
|
97
100
|
bundle_name = begin
|
|
98
101
|
bundle_name_for(expanded_source)
|
|
99
102
|
rescue Samagotchi::MemoryBundle::SourceNormalizer::UnknownSourceError => e
|
|
@@ -139,6 +142,9 @@ module Samagotchi
|
|
|
139
142
|
return 1
|
|
140
143
|
end
|
|
141
144
|
expanded_source = expand_source(source)
|
|
145
|
+
if Samagotchi::MemoryBundle::Profile.shipped_meta?(expanded_source)
|
|
146
|
+
return install_profile(expanded_source, scope: scope, force: force, dry_run: dry_run, word: "Upgrade")
|
|
147
|
+
end
|
|
142
148
|
bundle_name = begin
|
|
143
149
|
bundle_name_for(expanded_source)
|
|
144
150
|
rescue Samagotchi::MemoryBundle::SourceNormalizer::UnknownSourceError => e
|
|
@@ -238,6 +244,9 @@ module Samagotchi
|
|
|
238
244
|
# Propagate overrides
|
|
239
245
|
Samagotchi::MemoryBundle::Uninstaller.system_dir_override = Samagotchi::MemoryBundle::Installer.system_dir_override
|
|
240
246
|
Samagotchi::MemoryBundle::Uninstaller.project_dir_base_override = Samagotchi::MemoryBundle::Installer.project_dir_base_override
|
|
247
|
+
data = Samagotchi::MemoryBundle::Provenance.new(name: bundle_name).read
|
|
248
|
+
return uninstall_profile(bundle_name, force: force) if Samagotchi::MemoryBundle::Profile.installed_meta?(bundle_name, data)
|
|
249
|
+
|
|
241
250
|
uninstaller = Samagotchi::MemoryBundle::Uninstaller.new(name: bundle_name, scope: scope, force: force)
|
|
242
251
|
begin
|
|
243
252
|
uninstaller.run
|
|
@@ -252,6 +261,47 @@ module Samagotchi
|
|
|
252
261
|
end
|
|
253
262
|
end
|
|
254
263
|
|
|
264
|
+
# install/upgrade of a shipped profile: its new members (Profile).
|
|
265
|
+
def install_profile(dir, scope:, force:, dry_run:, word:)
|
|
266
|
+
name = File.basename(dir)
|
|
267
|
+
if scope.to_s.strip.downcase == "project"
|
|
268
|
+
@stderr.puts "#{word} failed: #{name} is a profile: its bundles install system-wide (drop --scope project)"
|
|
269
|
+
return 1
|
|
270
|
+
end
|
|
271
|
+
result = Samagotchi::MemoryBundle::Profile.install(dir, dry_run: dry_run, force: force)
|
|
272
|
+
@stdout.puts "#{result.name} v#{result.version} (profile)"
|
|
273
|
+
lines = []
|
|
274
|
+
lines << "#{dry_run ? "would install" : "installed"}: #{result.installed.join(", ")}" unless result.installed.empty?
|
|
275
|
+
lines << "already installed: #{result.already.join(", ")}" unless result.already.empty?
|
|
276
|
+
result.skipped.each { |member, why| lines << "skipped #{member}: #{why}" }
|
|
277
|
+
result.failed.each { |member, why| lines << "failed #{member}: #{why}" }
|
|
278
|
+
lines << "nothing new to install" if lines.empty?
|
|
279
|
+
lines.each { |line| @stdout.puts " #{line}" }
|
|
280
|
+
@stdout.puts "(dry-run: no changes written)" if dry_run
|
|
281
|
+
result.failed? ? 1 : 0
|
|
282
|
+
rescue Samagotchi::MemoryBundle::Manifest::ValidationError => e
|
|
283
|
+
@stderr.puts "#{word} failed: #{e.message}"
|
|
284
|
+
1
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# uninstall of a profile: its recorded members, then itself (Profile).
|
|
288
|
+
def uninstall_profile(name, force:)
|
|
289
|
+
result = Samagotchi::MemoryBundle::Profile.uninstall(name, force: force)
|
|
290
|
+
if result.gone
|
|
291
|
+
@stdout.puts "Uninstalled bundle '#{name}'"
|
|
292
|
+
@stdout.puts "Removed: #{result.removed.join(", ")}" unless result.removed.empty?
|
|
293
|
+
return 0
|
|
294
|
+
end
|
|
295
|
+
@stdout.puts "Removed from #{name}: #{result.removed.join(", ")}" unless result.removed.empty?
|
|
296
|
+
result.blocked.each { |member, why| @stdout.puts "Kept #{member}: #{why}" }
|
|
297
|
+
@stderr.puts "Uninstall failed: #{name} stays installed until #{result.blocked.keys.join(", ")} goes " \
|
|
298
|
+
"(chi bundle uninstall #{name} --force)"
|
|
299
|
+
1
|
|
300
|
+
rescue Samagotchi::MemoryBundle::Uninstaller::UninstallError => e
|
|
301
|
+
@stderr.puts "Uninstall failed: #{e.message}"
|
|
302
|
+
1
|
|
303
|
+
end
|
|
304
|
+
|
|
255
305
|
def status(rest)
|
|
256
306
|
name = rest.find { |a| !a.start_with?("--") }
|
|
257
307
|
require "json"
|
|
@@ -297,26 +347,26 @@ module Samagotchi
|
|
|
297
347
|
@stdout.puts " not loaded: #{plugin[:requires_failure]}" if plugin[:requires_failure]
|
|
298
348
|
end
|
|
299
349
|
st[:needs].each { |need| @stdout.puts " #{Samagotchi::MemoryBundle::Status.need_line(need)}" }
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
@stdout.puts "No installed bundles."; return 0
|
|
350
|
+
(st[:provenance][:includes] || []).each do |member|
|
|
351
|
+
state = Samagotchi::MemoryBundle::Provenance.new(name: member.to_s).installed? ? "installed" : "not installed: chi bundle install #{member}"
|
|
352
|
+
@stdout.puts " includes #{member} [#{state}]"
|
|
304
353
|
end
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
354
|
+
else
|
|
355
|
+
bundles = Samagotchi::MemoryBundle::Provenance.each_installed.to_a
|
|
356
|
+
@stdout.puts "No installed bundles." if bundles.empty?
|
|
357
|
+
bundles.each do |bname, data|
|
|
358
|
+
next @stdout.puts(" #{bname} (manifest.json unreadable)") if data[:error]
|
|
359
|
+
|
|
360
|
+
st = Samagotchi::MemoryBundle::Status.bundle_status(bname)
|
|
361
|
+
mods = st[:files].values.count { |v| v[:modified] || v[:missing] }
|
|
362
|
+
hooks_count = (st[:provenance][:hooks] || {}).size
|
|
363
|
+
hook_info = hooks_count > 0 ? " hooks=#{hooks_count}" : ""
|
|
364
|
+
plugin = st[:plugin]
|
|
365
|
+
mods += 1 if plugin && (plugin[:state] != "ok" || plugin[:requires_failure])
|
|
366
|
+
plugin_info = plugin ? " plugin=#{plugin[:file]}" : ""
|
|
367
|
+
includes = st[:provenance][:includes]
|
|
368
|
+
includes_info = includes ? " includes=#{includes.join(",")}" : ""
|
|
369
|
+
@stdout.puts " #{bname} v#{st[:provenance][:version]} scope=#{st[:scope]} files=#{st[:files].size}#{hook_info}#{plugin_info}#{includes_info} issues=#{mods}"
|
|
320
370
|
end
|
|
321
371
|
end
|
|
322
372
|
return 0
|
|
@@ -432,7 +482,8 @@ module Samagotchi
|
|
|
432
482
|
@stdout.puts "Installed:"
|
|
433
483
|
installed.each do |b|
|
|
434
484
|
next @stdout.puts(" #{b.name.ljust(name_w)} (#{b.error})") if b.error
|
|
435
|
-
line = " #{b.name.ljust(name_w)} #{"v#{b.version || "?"}".ljust(ver_w)} scope=#{b.scope || "?"}
|
|
485
|
+
line = " #{b.name.ljust(name_w)} #{"v#{b.version || "?"}".ljust(ver_w)} scope=#{b.scope || "?"} " \
|
|
486
|
+
"#{b.includes ? profile_members(b.includes, installed) : "files=#{b.files}"} installed=#{b.installed_at || "?"}"
|
|
436
487
|
line += " (shipped v#{b.upgrade.version}: chi bundle upgrade #{b.upgrade.source})" if b.upgrade
|
|
437
488
|
@stdout.puts line
|
|
438
489
|
end
|
|
@@ -442,12 +493,21 @@ module Samagotchi
|
|
|
442
493
|
@stdout.puts ""
|
|
443
494
|
@stdout.puts "Available (shipped with chi, install with: chi bundle install <name>):"
|
|
444
495
|
available.each do |s|
|
|
445
|
-
|
|
496
|
+
members = s.includes.empty? ? "" : " (#{s.includes.join(", ")})"
|
|
497
|
+
@stdout.puts " #{s.source.ljust(name_w)} #{"v#{s.version}".ljust(ver_w)} #{s.description}#{members}".rstrip
|
|
446
498
|
end
|
|
447
499
|
end
|
|
448
500
|
return 0
|
|
449
501
|
end
|
|
450
502
|
|
|
503
|
+
# An installed profile's list cell: its recorded members still
|
|
504
|
+
# installed, and the ones left out (uninstalled by the user).
|
|
505
|
+
def profile_members(members, installed)
|
|
506
|
+
names = installed.map(&:name)
|
|
507
|
+
here, gone = members.partition { |m| names.include?(m) }
|
|
508
|
+
"includes=#{here.join(",")}#{gone.empty? ? "" : " left out=#{gone.join(",")}"}"
|
|
509
|
+
end
|
|
510
|
+
|
|
451
511
|
def build(rest)
|
|
452
512
|
scope = nil
|
|
453
513
|
name = nil
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "json"
|
|
4
|
-
|
|
5
3
|
module Samagotchi
|
|
6
4
|
autoload :MutedMemories, File.expand_path("muted_memories", __dir__)
|
|
7
5
|
autoload :Log, File.expand_path("log", __dir__)
|
|
@@ -67,15 +65,8 @@ module Samagotchi
|
|
|
67
65
|
# entry name → marker, for the installed bundles of +scope+ with a
|
|
68
66
|
# missing need. A manifest.json or needs: that doesn't parse is skipped.
|
|
69
67
|
def self.entry_markers(scope, path:, bundles_dir:)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
Dir[File.join(bundles_dir, "*", "manifest.json")].sort.each_with_object({}) do |mjson, acc|
|
|
73
|
-
data = begin
|
|
74
|
-
JSON.parse(File.read(mjson), symbolize_names: true)
|
|
75
|
-
rescue JSON::ParserError, SystemCallError
|
|
76
|
-
next
|
|
77
|
-
end
|
|
78
|
-
next unless data.is_a?(Hash) && data[:scope].to_s == scope.to_s
|
|
68
|
+
MemoryBundle::Provenance.each_installed(dir: bundles_dir).each_with_object({}) do |(_name, data), acc|
|
|
69
|
+
next if data[:error] || data[:scope].to_s != scope.to_s
|
|
79
70
|
next unless data[:needs].is_a?(Array) && !data[:needs].empty?
|
|
80
71
|
|
|
81
72
|
needs = begin
|
|
@@ -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.
|
|
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
|
|
@@ -10,3 +10,4 @@
|
|
|
10
10
|
Before a task that a `skill_*` in the memory index matches, read it and follow it.
|
|
11
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.
|
|
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
|
+
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.8.1
|
|
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:d6f40d5e44096537d337c4ffda86823446f8b59eb8b8cc47014e97ec16f019c9
|
|
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:50c076fb6bda7ed010877ff664bb2e9e904418fe08b058ebaa911d4e82e9a683
|
|
11
11
|
delegated.md: sha256:0a71d5dfa7127df2d51cb5f0e7214240ed88777e36559430a8724c796d7ae561
|
|
@@ -66,7 +66,7 @@ Shape (plain Markdown, no frontmatter):
|
|
|
66
66
|
- **When to use it** is the `description:` of `memory_write`: one line starting with the task ("Release a new version of this repo: verify, tag, push"). It is what the index shows, so it is how you find the skill later.
|
|
67
67
|
- **Scope**: `project` by default; `system` when the user asks, or when the skill is clearly not about this project.
|
|
68
68
|
- **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
|
-
- **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.
|
|
69
|
+
- **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
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
71
|
- **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
72
|
|
|
@@ -78,6 +78,7 @@ Bundles are versioned directories/zips/tar.gz/git URLs with a `manifest.yml` and
|
|
|
78
78
|
- Integrity: each file's sha256 is recorded at install; a hook/plugin/rule file changed afterwards is not loaded (rules: every call denied) until reinstalled. It is an integrity check, not proof of authorship.
|
|
79
79
|
- Installing = trusting its Ruby code (hooks, plugin), like a gem. Install only copies; the code runs at the next session start (`Engine.new`), so a running worker needs a restart to pick it up.
|
|
80
80
|
- A bundle without `*.md` (btw, mcp, loop-guard) adds no line to the prompt's memory index.
|
|
81
|
+
- Profiles are shipped bundles with only `includes:` (bundle names): `core` (loop-guard, check-in, guardrails; `chi bootstrap` installs it) and `dev` (known-names, mcp, btw, skills, source-links). `chi bundle install core` installs the ones not installed and records them; a bundle the user uninstalled stays out on a later install, upgrade or `chi update`, which install only bundles new to the profile. `chi bundle uninstall core` removes its recorded bundles, then core (`docs/memory.md` "Bundle profiles").
|
|
81
82
|
|
|
82
83
|
### CLI — `chi bundle`
|
|
83
84
|
|
|
@@ -88,7 +89,7 @@ Bundles are versioned directories/zips/tar.gz/git URLs with a `manifest.yml` and
|
|
|
88
89
|
| `uninstall <bundle> [--force]` | Removes bundle files (skips locally edited files unless `--force`) and `index.md` lines, deletes provenance dir. |
|
|
89
90
|
| `status [<bundle>]` | Provenance + per-file `ok|modified|missing|no-index` vs stored checksum and base snapshot. |
|
|
90
91
|
| `diff <bundle> [file]` | Prints `base` (provenance snapshot) vs `current` (on-disk) for each file. |
|
|
91
|
-
| `list` | Lists installed bundles (`name v<version> scope files installed_at`, plus the shipped version when newer) and the bundles shipped with chi that are not installed (`install <name>` installs one)
|
|
92
|
+
| `list` | Lists installed bundles (`name v<version> scope files installed_at`, plus the shipped version when newer) and the bundles shipped with chi that are not installed (`install <name>` installs one), grouped under their profile; an installed profile shows `includes=` and `left out=`. |
|
|
92
93
|
| `build [--scope system\|project] [--name NAME] [--version VER] [--description DESC] [--out PATH] [FILES...]` | **Inverse of install** — builds a shareable bundle from local memories and installed hooks. Infers `zip` vs `dir`/`tar.gz` from `--out` extension; default `chi_system_memories.zip` (system) or `chi_<repo>_memories.zip` (project, named after the project root) v`1.0.0` in `Dir.pwd`. `FILES...` is an optional allowlist of memory basenames (`identity` or `identity.md`); if omitted, all `*.md` except `index.md`/hidden/non-md are included. Installed hooks are copied to `hooks/` with their manifest metadata. Computes `sha256:` checksums and writes `manifest.yml` via `Manifest.write`. No provenance write. |
|
|
93
94
|
|
|
94
95
|
**Scope resolution for install/build:**
|
data/lib/samagotchi/client.rb
CHANGED
|
@@ -6,6 +6,7 @@ require "uri"
|
|
|
6
6
|
require_relative "config"
|
|
7
7
|
require_relative "cancellation_controller"
|
|
8
8
|
require_relative "llm/http"
|
|
9
|
+
require_relative "llm/utf8_scrub"
|
|
9
10
|
require_relative "vision_context"
|
|
10
11
|
require_relative "vision_support"
|
|
11
12
|
require_relative "sampling_settings"
|
|
@@ -203,7 +204,7 @@ module Samagotchi
|
|
|
203
204
|
images: [], sampling: {})
|
|
204
205
|
images = Array(images)
|
|
205
206
|
request = { stop: stop, n_predict: n_predict, model: model, sampling: sampling }
|
|
206
|
-
return stream_completion(
|
|
207
|
+
return stream_completion(LLM::Utf8Scrub.call(prompt), request, on_chunk, cancel_controller, on_retry) if images.empty?
|
|
207
208
|
|
|
208
209
|
# The media marker is random per server process: a restart between the
|
|
209
210
|
# /props read and the request makes the prompt fail to tokenize, so
|
|
@@ -211,7 +212,7 @@ module Samagotchi
|
|
|
211
212
|
attempts = 0
|
|
212
213
|
begin
|
|
213
214
|
attempts += 1
|
|
214
|
-
payload_prompt = { prompt_string:
|
|
215
|
+
payload_prompt = { prompt_string: LLM::Utf8Scrub.call(prompt.gsub(ImagePlan::NATIVE_PLACEHOLDER, media_marker!(model))),
|
|
215
216
|
multimodal_data: images }
|
|
216
217
|
stream_completion(payload_prompt, request, on_chunk, cancel_controller, on_retry)
|
|
217
218
|
rescue LLM::BadRequest => e
|
|
@@ -403,27 +404,6 @@ module Samagotchi
|
|
|
403
404
|
(sampling || {}).reject { |key, value| value.nil? || ConfigFile::SAMPLING_RESERVED_KEYS.include?(key.to_s) }
|
|
404
405
|
end
|
|
405
406
|
|
|
406
|
-
# Conversation content (system prompt + tool responses + model output) can
|
|
407
|
-
# contain invalid UTF-8 — e.g. a shell/file op writes a garbled multibyte
|
|
408
|
-
# sequence (a truncated em-dash, a stray replacement byte). `JSON#to_json`
|
|
409
|
-
# raises `JSON::GeneratorError` on such input, which would abort the whole
|
|
410
|
-
# turn. Scrub the payload first: only offending bytes are replaced with "?",
|
|
411
|
-
# every valid UTF-8 string passes through untouched. Non-UTF-8 encodings are
|
|
412
|
-
# left alone because `#to_json` already handles them without raising.
|
|
413
|
-
def scrub_utf8(obj)
|
|
414
|
-
case obj
|
|
415
|
-
when String
|
|
416
|
-
str = obj.to_s
|
|
417
|
-
str.encoding == Encoding::UTF_8 && !str.valid_encoding? ? str.scrub("?") : str
|
|
418
|
-
when Array
|
|
419
|
-
obj.map { |element| scrub_utf8(element) }
|
|
420
|
-
when Hash
|
|
421
|
-
obj.each_with_object({}) { |(key, value), memo| memo[scrub_utf8(key)] = scrub_utf8(value) }
|
|
422
|
-
else
|
|
423
|
-
obj
|
|
424
|
-
end
|
|
425
|
-
end
|
|
426
|
-
|
|
427
407
|
# Resolve a user-facing SAMAGOTCHI_DEFAULT_MODEL selector to the exact id oMLX
|
|
428
408
|
# expects in the request body (an id from its `/v1/models` list).
|
|
429
409
|
#
|
data/lib/samagotchi/config.rb
CHANGED
|
@@ -5,7 +5,9 @@ require "json"
|
|
|
5
5
|
require "fileutils"
|
|
6
6
|
require "uri"
|
|
7
7
|
require "set"
|
|
8
|
+
require_relative "atomic_file"
|
|
8
9
|
require_relative "log"
|
|
10
|
+
require_relative "paths"
|
|
9
11
|
|
|
10
12
|
module Samagotchi
|
|
11
13
|
# Parses the thinking: levels of host and model entries (it needs Config).
|
|
@@ -88,7 +90,7 @@ module Samagotchi
|
|
|
88
90
|
Entry.new(key: "session.idle_exit_minutes", yaml_path: %w[session idle_exit_minutes], type: :float, default: 30.0, expose: %i[env config cli]),
|
|
89
91
|
# Plain `chi` runs like `chi --shared` (bin/chi, LaunchMode); false, or --no-shared per run, keeps the plain REPL. No CLI flag: that would duplicate --shared.
|
|
90
92
|
Entry.new(key: "session.shared", yaml_path: %w[session shared], type: :bool, default: true, expose: %i[env config]),
|
|
91
|
-
# false: a session nothing happened in is deleted when it is left (SessionManager.
|
|
93
|
+
# false: a session nothing happened in is deleted when it is left (SessionManager.discardable?).
|
|
92
94
|
Entry.new(key: "session.keep_empty", yaml_path: %w[session keep_empty], type: :bool, default: false, expose: %i[env config]),
|
|
93
95
|
# The most sessions one session may have delegated and still running (the delegate tool); a guard against a runaway model.
|
|
94
96
|
Entry.new(key: "session.max_children", yaml_path: %w[session max_children], type: :integer, default: 4, expose: %i[env config]),
|
|
@@ -101,6 +103,9 @@ module Samagotchi
|
|
|
101
103
|
Entry.new(key: "image.max_per_request", yaml_path: %w[image max_per_request], type: :integer, default: 20, expose: %i[env config]),
|
|
102
104
|
|
|
103
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]),
|
|
104
109
|
|
|
105
110
|
Entry.new(key: "log.file", yaml_path: %w[log file], type: :string, default: nil, expose: %i[env config cli]),
|
|
106
111
|
Entry.new(key: "log.disable", yaml_path: %w[log disable], type: :bool, default: false, expose: %i[env config cli]),
|
|
@@ -509,7 +514,6 @@ module Samagotchi
|
|
|
509
514
|
# (hosts, model_aliases) and the recap section resolution. All of it
|
|
510
515
|
# shares Config's precedence (CLI > ENV > file > default) for scalars.
|
|
511
516
|
module ConfigFile
|
|
512
|
-
XDG_CONFIG_HOME_ENV = "XDG_CONFIG_HOME"
|
|
513
517
|
CONFIG_DIR = "samagotchi"
|
|
514
518
|
CONFIG_FILE = "config.yml"
|
|
515
519
|
MODEL_ALIASES_KEY = "model_aliases"
|
|
@@ -635,9 +639,7 @@ module Samagotchi
|
|
|
635
639
|
end
|
|
636
640
|
|
|
637
641
|
def config_dir(env: ENV)
|
|
638
|
-
config_home
|
|
639
|
-
base_dir = config_home.empty? ? File.expand_path("~/.config") : config_home
|
|
640
|
-
File.join(base_dir, CONFIG_DIR)
|
|
642
|
+
File.join(Paths.config_home(env: env), CONFIG_DIR)
|
|
641
643
|
end
|
|
642
644
|
|
|
643
645
|
def global_path(env: ENV)
|
|
@@ -916,9 +918,7 @@ module Samagotchi
|
|
|
916
918
|
raw_data["default"] = { "model" => resolved }
|
|
917
919
|
end
|
|
918
920
|
FileUtils.mkdir_p(File.dirname(path))
|
|
919
|
-
|
|
920
|
-
File.write(tmp, YAML.dump(raw_data))
|
|
921
|
-
File.rename(tmp, path)
|
|
921
|
+
AtomicFile.write(path, YAML.dump(raw_data))
|
|
922
922
|
clear_yaml_cache!(path)
|
|
923
923
|
Samagotchi::Config.reload!(cli_overrides: Samagotchi::Config.cli_overrides)
|
|
924
924
|
true
|
|
@@ -1043,9 +1043,7 @@ module Samagotchi
|
|
|
1043
1043
|
normalized[lowered_key] = resolved_model
|
|
1044
1044
|
|
|
1045
1045
|
FileUtils.mkdir_p(File.dirname(path))
|
|
1046
|
-
|
|
1047
|
-
File.write(tmp, YAML.dump(raw_data))
|
|
1048
|
-
File.rename(tmp, path)
|
|
1046
|
+
AtomicFile.write(path, YAML.dump(raw_data))
|
|
1049
1047
|
clear_yaml_cache!(path)
|
|
1050
1048
|
previous
|
|
1051
1049
|
end
|
|
@@ -15,7 +15,7 @@ module Samagotchi
|
|
|
15
15
|
|
|
16
16
|
module_function
|
|
17
17
|
|
|
18
|
-
# Takes
|
|
18
|
+
# Takes SessionInbox.read_note's keys.
|
|
19
19
|
# @return [Hash] the conversation message
|
|
20
20
|
def message(note_id:, text:, source:, created_at: nil, from_session: nil, from_cwd: nil, **)
|
|
21
21
|
{ role: "system", kind: KIND, note_id: note_id, source: source, from_session: from_session,
|