samagotchi 0.156.0 → 0.158.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 +70 -1
- data/README.md +1 -1
- data/bin/chi +4 -1
- data/docs/architecture.md +18 -12
- data/docs/bundle-authoring.md +5 -5
- data/docs/bundles.md +4 -1
- data/docs/cli.md +29 -24
- data/docs/configuration.md +43 -9
- data/docs/context.md +5 -3
- data/docs/guardrails.md +12 -7
- data/docs/hooks.md +53 -83
- data/docs/internals/bridge.md +84 -7
- data/docs/internals/bundles.md +69 -46
- data/docs/internals/context-telemetry.md +4 -3
- data/docs/internals/engine.md +23 -7
- data/docs/internals/participants.md +3 -2
- data/docs/internals/remote-sessions.md +5 -2
- data/docs/internals/web-frontend.md +3 -2
- data/docs/internals/working-set.md +1 -1
- data/docs/memory.md +51 -10
- data/docs/plugins.md +48 -11
- data/docs/releasing.md +6 -5
- data/docs/sessions.md +13 -13
- data/docs/sub-agent.md +17 -2
- data/docs/testing.md +56 -5
- data/lib/samagotchi/answer_command.rb +2 -1
- data/lib/samagotchi/archive_store.rb +4 -3
- data/lib/samagotchi/bootstrap_command.rb +3 -2
- data/lib/samagotchi/bridge/card_places.rb +5 -7
- data/lib/samagotchi/bridge/card_store.rb +3 -0
- data/lib/samagotchi/bridge/turn_accumulator.rb +113 -10
- data/lib/samagotchi/bridge/turn_drawings.rb +271 -0
- data/lib/samagotchi/bridge.rb +31 -6
- data/lib/samagotchi/broadcast/recipients.rb +8 -8
- data/lib/samagotchi/broadcast/triage_model.rb +1 -1
- data/lib/samagotchi/bundle_command/check.rb +41 -10
- data/lib/samagotchi/bundle_command/installed_check.rb +13 -2
- data/lib/samagotchi/bundle_command/try/run.rb +4 -1
- data/lib/samagotchi/bundle_command/try/sandbox.rb +1 -1
- data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +20 -45
- data/lib/samagotchi/bundles/known-names/manifest.yml +4 -7
- data/lib/samagotchi/bundles/known-names/{hooks/known_names.rb → plugin.rb} +17 -0
- data/lib/samagotchi/bundles/source-links/manifest.yml +4 -7
- data/lib/samagotchi/bundles/source-links/{hooks/source_links.rb → plugin.rb} +23 -4
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/memory_guide.md +7 -7
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/child_move.rb +6 -6
- data/lib/samagotchi/child_reports.rb +2 -2
- data/lib/samagotchi/children_status.rb +15 -21
- data/lib/samagotchi/cli/parent_wait.rb +8 -22
- data/lib/samagotchi/config.rb +3 -0
- data/lib/samagotchi/context_command.rb +7 -5
- data/lib/samagotchi/context_sources.rb +9 -6
- data/lib/samagotchi/context_window.rb +9 -12
- data/lib/samagotchi/effective_model.rb +57 -22
- data/lib/samagotchi/engine/plugins.rb +1 -1
- data/lib/samagotchi/engine/recap.rb +8 -4
- data/lib/samagotchi/engine.rb +30 -9
- data/lib/samagotchi/events.rb +3 -2
- data/lib/samagotchi/extension_load.rb +26 -44
- data/lib/samagotchi/guardrails/load_failures.rb +3 -2
- data/lib/samagotchi/guardrails/path_resolve.rb +11 -0
- data/lib/samagotchi/guardrails/review_paths.rb +1 -5
- data/lib/samagotchi/guardrails_command.rb +1 -1
- data/lib/samagotchi/hook_routes.rb +2 -2
- data/lib/samagotchi/hooks.rb +2 -3
- data/lib/samagotchi/host_address.rb +52 -0
- data/lib/samagotchi/host_registry.rb +57 -45
- data/lib/samagotchi/kernel_loop.rb +7 -3
- data/lib/samagotchi/llm/chat_loop.rb +6 -3
- data/lib/samagotchi/llm_context_strategy.rb +27 -63
- data/lib/samagotchi/memory_bundle/asset_installer.rb +92 -177
- data/lib/samagotchi/memory_bundle/builder.rb +12 -35
- data/lib/samagotchi/memory_bundle/bundle_hook.rb +6 -31
- data/lib/samagotchi/memory_bundle/file_actions.rb +193 -0
- data/lib/samagotchi/memory_bundle/file_digest.rb +13 -0
- data/lib/samagotchi/memory_bundle/file_state.rb +152 -0
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +1 -1
- data/lib/samagotchi/memory_bundle/installed_copy.rb +13 -19
- data/lib/samagotchi/memory_bundle/installed_manifest.rb +10 -17
- data/lib/samagotchi/memory_bundle/installer.rb +212 -178
- data/lib/samagotchi/memory_bundle/manifest.rb +51 -58
- data/lib/samagotchi/memory_bundle/merger.rb +69 -65
- data/lib/samagotchi/memory_bundle/plugin_ref.rb +6 -5
- data/lib/samagotchi/memory_bundle/profile.rb +5 -5
- data/lib/samagotchi/memory_bundle/provenance.rb +26 -52
- data/lib/samagotchi/memory_bundle/remerge.rb +31 -14
- data/lib/samagotchi/memory_bundle/shas.rb +5 -8
- data/lib/samagotchi/memory_bundle/shipped_update.rb +51 -11
- data/lib/samagotchi/memory_bundle/status.rb +26 -30
- data/lib/samagotchi/memory_bundle/system_bundle.rb +39 -10
- data/lib/samagotchi/memory_bundle/trash.rb +28 -18
- data/lib/samagotchi/memory_bundle/uninstaller.rb +16 -11
- data/lib/samagotchi/memory_orphans.rb +4 -4
- data/lib/samagotchi/memory_paths.rb +102 -13
- data/lib/samagotchi/memory_usage.rb +1 -1
- data/lib/samagotchi/model_lookup.rb +75 -0
- data/lib/samagotchi/model_profile.rb +1 -0
- data/lib/samagotchi/models_command.rb +1 -1
- data/lib/samagotchi/note_command.rb +1 -1
- data/lib/samagotchi/participant_mode.rb +2 -1
- data/lib/samagotchi/plugin/api.rb +42 -10
- data/lib/samagotchi/plugin/loader.rb +75 -15
- data/lib/samagotchi/plugin/sessions.rb +1 -1
- data/lib/samagotchi/project_command.rb +167 -0
- data/lib/samagotchi/project_init.rb +69 -0
- data/lib/samagotchi/project_scope.rb +6 -4
- data/lib/samagotchi/prompt_files.rb +10 -7
- data/lib/samagotchi/reasoning_history.rb +14 -20
- data/lib/samagotchi/recap_place.rb +26 -0
- data/lib/samagotchi/reply_wait/liveness.rb +38 -0
- data/lib/samagotchi/reply_wait/poll.rb +44 -15
- data/lib/samagotchi/reply_wait/target.rb +113 -0
- data/lib/samagotchi/reply_wait.rb +22 -2
- data/lib/samagotchi/run_options.rb +1 -0
- data/lib/samagotchi/sampling_settings.rb +9 -16
- data/lib/samagotchi/self_report.rb +34 -35
- data/lib/samagotchi/send_command.rb +17 -23
- data/lib/samagotchi/session.rb +349 -441
- data/lib/samagotchi/session_access/contract.rb +1 -1
- data/lib/samagotchi/session_access/local.rb +5 -6
- data/lib/samagotchi/session_access/remote.rb +5 -3
- data/lib/samagotchi/session_field.rb +134 -0
- data/lib/samagotchi/session_inbox.rb +49 -1
- data/lib/samagotchi/session_manager/emptiness.rb +2 -1
- data/lib/samagotchi/session_manager/listing.rb +29 -100
- data/lib/samagotchi/session_manager.rb +2 -1
- data/lib/samagotchi/session_metrics.rb +7 -4
- data/lib/samagotchi/session_paths.rb +57 -0
- data/lib/samagotchi/session_retention.rb +2 -2
- data/lib/samagotchi/session_row.rb +200 -0
- data/lib/samagotchi/session_setup.rb +15 -12
- data/lib/samagotchi/sessions_command.rb +11 -129
- data/lib/samagotchi/steer.rb +10 -0
- data/lib/samagotchi/steer_cut_after.rb +7 -14
- data/lib/samagotchi/system_prompt.rb +20 -11
- data/lib/samagotchi/terminal_ui/attached_loop.rb +6 -26
- data/lib/samagotchi/terminal_ui/join_replay.rb +91 -8
- data/lib/samagotchi/terminal_ui/local_link.rb +2 -2
- data/lib/samagotchi/terminal_ui.rb +3 -1
- data/lib/samagotchi/testing/bundle.rb +5 -12
- data/lib/samagotchi/testing/install.rb +5 -4
- data/lib/samagotchi/testing/recorder.rb +1 -1
- data/lib/samagotchi/testing.rb +1 -1
- data/lib/samagotchi/thinking.rb +16 -21
- data/lib/samagotchi/tool_declarations.rb +1 -1
- data/lib/samagotchi/tool_response.rb +5 -3
- data/lib/samagotchi/tool_runner.rb +18 -1
- data/lib/samagotchi/toolkit_binding.rb +1 -4
- data/lib/samagotchi/toolkit_command.rb +1 -4
- data/lib/samagotchi/tools/delegate.rb +60 -51
- data/lib/samagotchi/tools/delegate_relay.rb +5 -5
- data/lib/samagotchi/tools/delegate_result.rb +1 -1
- data/lib/samagotchi/tools/delegate_wait.rb +41 -17
- data/lib/samagotchi/tools/list_sessions.rb +7 -7
- data/lib/samagotchi/update_command.rb +11 -7
- data/lib/samagotchi/verify_fresh.rb +7 -11
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +11 -14
- data/lib/samagotchi/web/app.rb +66 -21
- data/lib/samagotchi/web/message_display.rb +57 -7
- data/lib/samagotchi/web/participant_access.rb +1 -1
- data/lib/samagotchi/web/project_list.rb +3 -2
- data/lib/samagotchi/web/public/activity.js +16 -7
- data/lib/samagotchi/web/public/app.js +29 -8
- data/lib/samagotchi/web/public/bubbles.js +8 -3
- data/lib/samagotchi/web/public/cards_place.js +15 -0
- data/lib/samagotchi/web/public/commands.js +5 -1
- data/lib/samagotchi/web/public/data.js +4 -2
- data/lib/samagotchi/web/public/history_shape.js +220 -0
- data/lib/samagotchi/web/public/index.html +6 -1
- data/lib/samagotchi/web/public/project_picker.js +86 -4
- data/lib/samagotchi/web/public/recap_stamp.js +17 -0
- data/lib/samagotchi/web/public/reload_check.js +71 -0
- data/lib/samagotchi/web/public/stage_model.js +7 -5
- data/lib/samagotchi/web/public/stage_view.js +7 -3
- data/lib/samagotchi/web/public/stream_handlers.js +8 -2
- data/lib/samagotchi/web/public/timing.js +43 -2
- data/lib/samagotchi/web/public/turn_block.js +215 -30
- data/lib/samagotchi/web/public/turn_drawing.js +43 -0
- data/lib/samagotchi/web/public/turn_events.js +10 -1
- data/lib/samagotchi/web/public/turn_pieces.js +29 -0
- data/lib/samagotchi/web/reload_check_route.rb +95 -0
- data/lib/samagotchi/web/server.rb +4 -2
- data/lib/samagotchi/web/session_access_routes.rb +3 -3
- data/lib/samagotchi/web/session_control.rb +1 -1
- data/lib/samagotchi/web/session_hub.rb +9 -17
- data/lib/samagotchi/web/session_show.rb +64 -2
- data/lib/samagotchi/web/session_summary.rb +36 -24
- data/lib/samagotchi/worker.rb +28 -5
- data/lib/samagotchi/worker_link.rb +3 -0
- metadata +22 -5
- data/lib/samagotchi/hooks/bundle_loader.rb +0 -191
- data/lib/samagotchi/send_command/wait_target.rb +0 -85
|
@@ -99,28 +99,38 @@ module Samagotchi
|
|
|
99
99
|
dest
|
|
100
100
|
end
|
|
101
101
|
|
|
102
|
-
# Copies
|
|
103
|
-
# that would lose something: its bytes are neither what
|
|
104
|
-
# was installed (no sha recorded: unknown, so it counts
|
|
105
|
-
#
|
|
106
|
-
#
|
|
107
|
-
|
|
108
|
-
return if File.symlink?(path) || !File.file?(path)
|
|
109
|
-
return if incoming && File.file?(incoming) && FileUtils.identical?(incoming, path)
|
|
110
|
-
return if !Provenance.recorded_sha(recorded).empty? && Provenance.sha_matches?(path, recorded)
|
|
111
|
-
|
|
112
|
-
copy(path, as: as)
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# copy_edit for every file under +dir+, which is about to go, at any depth
|
|
116
|
-
# (kept as <label>/<its path in dir>). A linked dir isn't followed.
|
|
102
|
+
# Copies every file under +dir+, which is about to be replaced or
|
|
103
|
+
# removed, when that would lose something: its bytes are neither what
|
|
104
|
+
# the record says was installed (no sha recorded: unknown, so it counts
|
|
105
|
+
# as edited) nor the incoming source's, at any depth (kept as
|
|
106
|
+
# <label>/<its path in dir>). A link (a dev install's) points at its
|
|
107
|
+
# source, which nothing here touches; a linked dir isn't followed.
|
|
117
108
|
# @param recorded [Hash, nil] path in dir => its recorded sha256
|
|
118
109
|
# @param incoming [Hash] path in dir => the source replacing it
|
|
119
110
|
def copy_edits(dir, label, recorded = nil, incoming = {})
|
|
120
|
-
|
|
111
|
+
self.class.edited_in(dir, recorded, incoming).each { |file| copy(File.join(dir, file), as: "#{label}/#{file}") }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Whether #copy_edits would copy the file at +path+ (read-only): the rule
|
|
115
|
+
# ShippedUpdate's plan names its replaced files by.
|
|
116
|
+
def self.edit?(path, recorded: nil, incoming: nil)
|
|
117
|
+
return false if File.symlink?(path) || !File.file?(path)
|
|
118
|
+
return false if incoming && File.file?(incoming) && FileUtils.identical?(incoming, path)
|
|
119
|
+
|
|
120
|
+
Provenance.recorded_sha(recorded).empty? || !Provenance.sha_matches?(path, recorded)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# What #copy_edits would copy (read-only), as <label>/<its path in dir>.
|
|
124
|
+
def self.edits(dir, label, recorded = nil, incoming = {})
|
|
125
|
+
edited_in(dir, recorded, incoming).map { |file| "#{label}/#{file}" }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# The files under +dir+ (paths in it) #copy_edits copies.
|
|
129
|
+
def self.edited_in(dir, recorded, incoming)
|
|
130
|
+
return [] if File.symlink?(dir) || !File.directory?(dir)
|
|
121
131
|
|
|
122
|
-
Dir.glob("**/*", File::FNM_DOTMATCH, base: dir).sort.
|
|
123
|
-
|
|
132
|
+
Dir.glob("**/*", File::FNM_DOTMATCH, base: dir).sort.select do |file|
|
|
133
|
+
edit?(File.join(dir, file), recorded: recorded&.[](file), incoming: incoming[file])
|
|
124
134
|
end
|
|
125
135
|
end
|
|
126
136
|
|
|
@@ -4,8 +4,8 @@ require "fileutils"
|
|
|
4
4
|
require "digest"
|
|
5
5
|
require_relative "../memory_paths"
|
|
6
6
|
require_relative "provenance"
|
|
7
|
+
require_relative "file_actions"
|
|
7
8
|
require_relative "index_updater"
|
|
8
|
-
require_relative "merger"
|
|
9
9
|
require_relative "trash"
|
|
10
10
|
require_relative "../model_overlay"
|
|
11
11
|
|
|
@@ -64,17 +64,22 @@ module Samagotchi
|
|
|
64
64
|
[key, Provenance.claimants(key, scope: target_scope, except: @name)]
|
|
65
65
|
end.reject { |_, others| others.empty? }
|
|
66
66
|
|
|
67
|
+
# Each file's UNINSTALL row (its source-less FileState): an edit
|
|
68
|
+
# refuses, unless --force (which moves it to the trash with the rest).
|
|
67
69
|
blocked = []
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
70
|
+
unless @force
|
|
71
|
+
files.each_key do |file_key|
|
|
72
|
+
key = file_key.to_s
|
|
73
|
+
state = FileState.of(key, record: bundle, provenance: provenance, target_dir: target_dir, source_dir: nil,
|
|
74
|
+
claimants: ->(k) { shared.fetch(k, []) })
|
|
75
|
+
next unless FileActions.for(:uninstall, state)&.do == :refuse
|
|
76
|
+
# Kept for another bundle, read or not.
|
|
77
|
+
next if state.shared_with.any?
|
|
78
|
+
|
|
79
|
+
# Whether it was edited can't be told: the read raises (nothing lost).
|
|
80
|
+
FileDigest.hex(state.paths[:current]) if state.kind == :unreadable
|
|
81
|
+
blocked << key
|
|
82
|
+
@warnings << "Skipped #{key}: local edits detected (#{FileStanding::FORCE_MOVES})"
|
|
78
83
|
end
|
|
79
84
|
end
|
|
80
85
|
|
|
@@ -19,14 +19,14 @@ module Samagotchi
|
|
|
19
19
|
|
|
20
20
|
module_function
|
|
21
21
|
|
|
22
|
-
# The orphan candidates for +cwd+'s project, or nil: only inside a
|
|
23
|
-
#
|
|
24
|
-
# match), and only while the computed folder holds no *.md (a
|
|
22
|
+
# The orphan candidates for +cwd+'s project, or nil: only inside a
|
|
23
|
+
# project, a git repository or a marked folder (outside both the root is
|
|
24
|
+
# cwd, and every src/ or tmp/ would match), and only while the computed folder holds no *.md (a
|
|
25
25
|
# memory_write's mkdir_p or a project-scope bundle install alone must
|
|
26
26
|
# not silence it).
|
|
27
27
|
# @return [Found, nil]
|
|
28
28
|
def find(env: ENV, cwd: Dir.pwd)
|
|
29
|
-
return nil unless MemoryPaths.
|
|
29
|
+
return nil unless MemoryPaths.in_project?(cwd)
|
|
30
30
|
|
|
31
31
|
projects = MemoryPaths.projects_dir(env: env)
|
|
32
32
|
computed = MemoryPaths.project_dir(env: env, cwd: cwd)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "digest"
|
|
4
4
|
require_relative "config"
|
|
5
|
+
require_relative "guardrails/path_resolve"
|
|
5
6
|
|
|
6
7
|
module Samagotchi
|
|
7
8
|
# Where memories live: <config dir>/memories, i.e. $XDG_CONFIG_HOME/samagotchi/memories
|
|
@@ -9,11 +10,61 @@ module Samagotchi
|
|
|
9
10
|
# ConfigFile.global_path, so an XDG_CONFIG_HOME set after load (specs, smoke runs,
|
|
10
11
|
# a fixture chi) moves memories together with config.yml.
|
|
11
12
|
#
|
|
12
|
-
# Project memories are keyed by the project root (see
|
|
13
|
-
#
|
|
13
|
+
# Project memories are keyed by the project root (see #lookup): one folder
|
|
14
|
+
# per project root, a git repository (every worktree and subdirectory of it
|
|
15
|
+
# shares one folder) or a folder marked by `chi project init` (and its
|
|
16
|
+
# subfolders).
|
|
14
17
|
module MemoryPaths
|
|
18
|
+
# The file that makes a folder in no git repository a chi project
|
|
19
|
+
# (`chi project init`). The file, not the bare .chi/ dir, is the marker.
|
|
20
|
+
MARKER = File.join(".chi", "project.yml").freeze
|
|
21
|
+
|
|
22
|
+
# How a folder's project was found (#lookup): +kind+ is :git, :marker or
|
|
23
|
+
# :none; +root+ is what memories are keyed on (#project_root: git's
|
|
24
|
+
# common-dir checkout, the marked folder's realpath, or the folder
|
|
25
|
+
# itself); +top+ is the folder a session reads its AGENTS.md chain from
|
|
26
|
+
# (the work tree's top, the marked root; nil for :none).
|
|
27
|
+
ProjectLookup = Data.define(:kind, :root, :top) do
|
|
28
|
+
def project? = kind != :none
|
|
29
|
+
def git? = kind == :git
|
|
30
|
+
def marker? = kind == :marker
|
|
31
|
+
end
|
|
32
|
+
|
|
15
33
|
module_function
|
|
16
34
|
|
|
35
|
+
# The project +cwd+ belongs to, looked up once: a .git above it wins
|
|
36
|
+
# (git is the identity wherever it exists); only when there is none, the
|
|
37
|
+
# nearest MARKER above it, ignoring one in $HOME or any folder above it
|
|
38
|
+
# (and /), so a marker in ~ never makes every folder under home one
|
|
39
|
+
# project. The marker walk starts from +cwd+'s real path and the marked
|
|
40
|
+
# root is a realpath, as git's root is, so every spelling of a folder
|
|
41
|
+
# (/tmp, /var on macOS, a symlink into or out of a marked tree) gets the
|
|
42
|
+
# answer the worker's physical Dir.pwd gets.
|
|
43
|
+
# @param home [String, nil] $HOME by default; nil or blank ignores only /
|
|
44
|
+
# @return [ProjectLookup]
|
|
45
|
+
def lookup(cwd = Dir.pwd, home: default_home)
|
|
46
|
+
dot_git = find_dot_git(cwd)
|
|
47
|
+
return ProjectLookup.new(kind: :git, root: git_root(dot_git), top: File.dirname(dot_git)) if dot_git
|
|
48
|
+
|
|
49
|
+
marked = nearest_marker(cwd, home: home)
|
|
50
|
+
return ProjectLookup.new(kind: :marker, root: marked, top: marked) if marked
|
|
51
|
+
|
|
52
|
+
ProjectLookup.new(kind: :none, root: cwd, top: nil)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Whether a marker at +dir+ is ignored: +dir+ is / or holds $HOME (both
|
|
56
|
+
# resolved; Guardrails::PathResolve.home_root?).
|
|
57
|
+
def above_home?(dir, home: default_home)
|
|
58
|
+
Guardrails::PathResolve.real(dir) == "/" || Guardrails::PathResolve.home_root?(dir, home)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# $HOME, or nil when it can't be found (no HOME, no passwd entry).
|
|
62
|
+
def default_home
|
|
63
|
+
Dir.home
|
|
64
|
+
rescue ArgumentError
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
17
68
|
def system_dir(env: ENV)
|
|
18
69
|
File.join(ConfigFile.config_dir(env: env), "memories")
|
|
19
70
|
end
|
|
@@ -28,16 +79,31 @@ module Samagotchi
|
|
|
28
79
|
# #project_key for a root already resolved (a session's project_root).
|
|
29
80
|
def key_for_root(root) = "#{File.basename(root)}_#{Digest::MD5.hexdigest(root)[0..7]}"
|
|
30
81
|
|
|
31
|
-
# The
|
|
32
|
-
# worktree shares it. Returns the
|
|
33
|
-
#
|
|
34
|
-
# --separate-git-dir, a submodule's
|
|
35
|
-
# the "project root" is a git
|
|
36
|
-
#
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
82
|
+
# The project root (#lookup): in a repository, its common git directory
|
|
83
|
+
# identifies the project; every linked worktree shares it. Returns the
|
|
84
|
+
# checkout holding it when it is named .git, otherwise the git dir itself
|
|
85
|
+
# (a bare repo, a proj/.bare layout, --separate-git-dir, a submodule's
|
|
86
|
+
# .git/modules/<name>), so for those layouts the "project root" is a git
|
|
87
|
+
# dir, not a checkout. Outside any repository, the marked folder holding
|
|
88
|
+
# +cwd+, else cwd. No git subprocess: at most two small files are read.
|
|
89
|
+
def project_root(cwd = Dir.pwd) = lookup(cwd).root
|
|
90
|
+
|
|
91
|
+
# Is +cwd+ in a project: a git repository or a marked folder?
|
|
92
|
+
def in_project?(cwd = Dir.pwd) = lookup(cwd).project?
|
|
93
|
+
|
|
94
|
+
# The marked folder (realpath) +cwd+'s project is, or nil (in a git
|
|
95
|
+
# repository, or in no project).
|
|
96
|
+
def marked_root(cwd = Dir.pwd)
|
|
97
|
+
found = lookup(cwd)
|
|
98
|
+
found.marker? ? found.root : nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# The top of +cwd+'s project: the git work tree's top (#work_tree_root) or
|
|
102
|
+
# the marked root; nil in no project.
|
|
103
|
+
def project_top(cwd = Dir.pwd) = lookup(cwd).top
|
|
40
104
|
|
|
105
|
+
# The project root of the checkout whose .git is +dot_git+ (#project_root).
|
|
106
|
+
def git_root(dot_git)
|
|
41
107
|
holder = File.dirname(dot_git)
|
|
42
108
|
gitdir = File.directory?(dot_git) ? dot_git : gitdir_from_file(dot_git)
|
|
43
109
|
return holder unless gitdir
|
|
@@ -78,7 +144,8 @@ module Samagotchi
|
|
|
78
144
|
File.directory?(dot_git) ? dot_git : gitdir_from_file(dot_git)
|
|
79
145
|
end
|
|
80
146
|
|
|
81
|
-
# Is +cwd+ inside a git repository (a .git somewhere above it)?
|
|
147
|
+
# Is +cwd+ inside a git repository (a .git somewhere above it)? Git only:
|
|
148
|
+
# a marked folder is not a repository (#in_project?).
|
|
82
149
|
def in_repo?(cwd = Dir.pwd)
|
|
83
150
|
!find_dot_git(cwd).nil?
|
|
84
151
|
end
|
|
@@ -96,6 +163,28 @@ module Samagotchi
|
|
|
96
163
|
end
|
|
97
164
|
end
|
|
98
165
|
|
|
166
|
+
# The realpath of the nearest folder at or above +start+ holding MARKER,
|
|
167
|
+
# git or not (#lookup asks only outside a repository; `chi project` tells
|
|
168
|
+
# of one a repository makes ignored), or nil. A marker at or above +home+
|
|
169
|
+
# (#above_home?) ends the walk with nil: every folder above it is above
|
|
170
|
+
# home too.
|
|
171
|
+
def nearest_marker(start, home: default_home)
|
|
172
|
+
dir = Guardrails::PathResolve.real(start)
|
|
173
|
+
loop do
|
|
174
|
+
if File.file?(File.join(dir, MARKER))
|
|
175
|
+
return nil if above_home?(dir, home: home)
|
|
176
|
+
|
|
177
|
+
return File.realpath(dir)
|
|
178
|
+
end
|
|
179
|
+
parent = File.dirname(dir)
|
|
180
|
+
return nil if parent == dir
|
|
181
|
+
|
|
182
|
+
dir = parent
|
|
183
|
+
end
|
|
184
|
+
rescue SystemCallError
|
|
185
|
+
nil
|
|
186
|
+
end
|
|
187
|
+
|
|
99
188
|
# A ".git" file holds "gitdir: <path>", relative to the file's directory.
|
|
100
189
|
def gitdir_from_file(path)
|
|
101
190
|
line = File.read(path, 4096).to_s[/\Agitdir:\s*(.+)$/, 1]&.strip
|
|
@@ -115,7 +204,7 @@ module Samagotchi
|
|
|
115
204
|
rescue SystemCallError
|
|
116
205
|
gitdir
|
|
117
206
|
end
|
|
118
|
-
private_class_method :find_dot_git, :gitdir_from_file, :common_dir
|
|
207
|
+
private_class_method :find_dot_git, :git_root, :gitdir_from_file, :common_dir
|
|
119
208
|
|
|
120
209
|
def project_dir(env: ENV, cwd: Dir.pwd)
|
|
121
210
|
File.join(projects_dir(env: env), project_key(cwd))
|
|
@@ -89,7 +89,7 @@ module Samagotchi
|
|
|
89
89
|
def self.load_sessions(state_dir: Session.default_state_dir)
|
|
90
90
|
sessions = []
|
|
91
91
|
unreadable = 0
|
|
92
|
-
Dir.glob(File.join(state_dir, "*#{
|
|
92
|
+
Dir.glob(File.join(state_dir, "*#{SessionPaths::FILE_EXT}")).sort.each do |path|
|
|
93
93
|
data = JSON.parse(File.read(path, encoding: "UTF-8"))
|
|
94
94
|
# A project_root that is not a path (a hand-edited file) makes every
|
|
95
95
|
# later step raise, so the file counts as unreadable rather than
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "config"
|
|
4
|
+
|
|
5
|
+
module Samagotchi
|
|
6
|
+
# The layered inputs a model's settings resolve from, in one value: the
|
|
7
|
+
# target (its host entry: the hosts: layer), the names its models: entry
|
|
8
|
+
# may be under (HostRegistry#lookup_names, most specific first), the
|
|
9
|
+
# models: map, and the env the CLI/env/file layers are read with.
|
|
10
|
+
# EffectiveModel builds one per read (with its registry's env); each
|
|
11
|
+
# setting module (Thinking, ReasoningHistory, SteerCutAfter,
|
|
12
|
+
# SamplingSettings, VisionSupport, ContextWindow, LLMContextStrategy) keeps
|
|
13
|
+
# its own ordered resolve over it, and ModelProfile takes its #models.
|
|
14
|
+
#
|
|
15
|
+
# The setting modules call it duck-typed and never name it, and it names
|
|
16
|
+
# none of them (only ModelSettings, ModelSetting, Config, ConfigFile): the
|
|
17
|
+
# modules' parsers are what ModelSettings calls, so either edge would close
|
|
18
|
+
# a cycle (spec/model_lookup_spec.rb checks this file's side).
|
|
19
|
+
#
|
|
20
|
+
# +given_models+ is a caller's models: map (one for a whole picker, or a
|
|
21
|
+
# spec's); without one, #models reads config.yml at each call, as every
|
|
22
|
+
# resolve always did.
|
|
23
|
+
ModelLookup = Data.define(:target, :names, :given_models, :env) do
|
|
24
|
+
# @param target [HostRegistry::ModelTarget, nil] nil: the global layers only
|
|
25
|
+
# @param names [Array<String>] HostRegistry#lookup_names
|
|
26
|
+
# @param models [Hash{String => ModelSettings}, nil] ModelSettings.all by default
|
|
27
|
+
# @param env [Hash] ENV, or another env whose config file and variables
|
|
28
|
+
# every layer reads (chi self's)
|
|
29
|
+
def self.for(target, names:, models: nil, env: ENV)
|
|
30
|
+
new(target: target, names: Array(names), given_models: models, env: env)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The models: map: the one given, else ModelSettings.all for #env; {}
|
|
34
|
+
# when it can't be read.
|
|
35
|
+
# @return [Hash{String => ModelSettings}]
|
|
36
|
+
def models
|
|
37
|
+
return given_models if given_models
|
|
38
|
+
|
|
39
|
+
env.equal?(ENV) ? ModelSettings.all : ModelSettings.all(env: env)
|
|
40
|
+
rescue StandardError # degrade-ok: the setting resolves from its other layers, as it always did
|
|
41
|
+
{}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def entry = target&.entry
|
|
45
|
+
|
|
46
|
+
# The host's name, as a "hosts.NAME" label names it.
|
|
47
|
+
def host_name = entry&.name
|
|
48
|
+
|
|
49
|
+
# The first lookup name whose models: entry sets +field+ (a
|
|
50
|
+
# ModelSettings member): a ModelSetting (key, value), or nil.
|
|
51
|
+
# @return [ModelSetting, nil]
|
|
52
|
+
def model(field) = ModelSettings.find(names, field, models: models)
|
|
53
|
+
|
|
54
|
+
# The host entry's +field+ (a HostEntry member), nil when unset or
|
|
55
|
+
# without a host.
|
|
56
|
+
def host(field) = entry&.public_send(field)
|
|
57
|
+
|
|
58
|
+
# [value, origin] of the global config key +key+ (:cli, :env, :file or
|
|
59
|
+
# :default), as Config.get_with_origin gives it; for another env its
|
|
60
|
+
# variables and config file, with this process's CLI settings. Raises as
|
|
61
|
+
# that does.
|
|
62
|
+
# @return [Array(Object, Symbol)]
|
|
63
|
+
def global(key)
|
|
64
|
+
return Config.get_with_origin(key) if env.equal?(ENV)
|
|
65
|
+
|
|
66
|
+
file_data = ConfigFile.read_yaml(env: env, path: ConfigFile.global_path(env: env))
|
|
67
|
+
Config.resolve_with_origin(key, file_data: file_data, env: env, cli_overrides: Config.cli_overrides)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The model and the names only: the default would print the env (it
|
|
71
|
+
# holds API keys) and the target's client.
|
|
72
|
+
def inspect = "#<Samagotchi::ModelLookup #{target&.model.inspect} host=#{host_name.inspect} names=#{names.inspect}>"
|
|
73
|
+
alias_method :to_s, :inspect
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -286,6 +286,7 @@ module Samagotchi
|
|
|
286
286
|
def self.default_relist(hosts)
|
|
287
287
|
lambda do |host_name, relist_env|
|
|
288
288
|
require_relative "host_registry"
|
|
289
|
+
# Not .current: the caller's hosts, and a fresh listing is the point.
|
|
289
290
|
registry = Samagotchi::HostRegistry.new(hosts_config: hosts, env: relist_env)
|
|
290
291
|
answer = nil
|
|
291
292
|
thread = Thread.new { answer = registry.list_models(host_name) }
|
|
@@ -73,7 +73,7 @@ module Samagotchi
|
|
|
73
73
|
|
|
74
74
|
# @return [Array(Hash, Boolean)] the payload and whether any host listed
|
|
75
75
|
def catalog(timeout)
|
|
76
|
-
registry = @registry || HostRegistry.
|
|
76
|
+
registry = @registry || HostRegistry.current
|
|
77
77
|
results = registry.list_all_models(force: true, wait: timeout)
|
|
78
78
|
listed = results.values.any? { |data| data.error.nil? }
|
|
79
79
|
[ModelCatalog.payload(results, registry: registry, default_name: default_name), listed]
|
|
@@ -97,7 +97,7 @@ module Samagotchi
|
|
|
97
97
|
|
|
98
98
|
# --all: every live session. @return [Integer] exit status
|
|
99
99
|
def deliver_to_live(text, source)
|
|
100
|
-
ids = @access.summaries(live: true, include_tests: Session.test_session_env?).map
|
|
100
|
+
ids = @access.summaries(live: true, include_tests: Session.test_session_env?).map(&:id)
|
|
101
101
|
if ids.empty?
|
|
102
102
|
error_line("chi note: no live sessions (chi sessions list --live --scope=all)")
|
|
103
103
|
return 1
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "delivery"
|
|
4
|
+
require_relative "session_paths"
|
|
4
5
|
|
|
5
6
|
module Samagotchi
|
|
6
7
|
# The rules for a participant's turn: the one place that decides them.
|
|
@@ -75,7 +76,7 @@ module Samagotchi
|
|
|
75
76
|
|
|
76
77
|
require_relative "session"
|
|
77
78
|
require_relative "worker_link"
|
|
78
|
-
dir =
|
|
79
|
+
dir = SessionPaths.session_dir(session.id, state_dir: state_dir || SessionPaths.default_state_dir)
|
|
79
80
|
link = WorkerLink.resolve(dir, unlink: false)
|
|
80
81
|
link.live? ? link.features : nil
|
|
81
82
|
rescue StandardError
|
|
@@ -32,10 +32,7 @@ module Samagotchi
|
|
|
32
32
|
# plugin writes it (ApiCalls#keywords: `ctx.sessions.send(…,
|
|
33
33
|
# delivery:)` is seen, a `sessions = ctx.sessions` alias isn't).
|
|
34
34
|
# Events aren't listed: chi.on takes any symbol and an older chi never
|
|
35
|
-
# fires one it doesn't know (harmless; chi bundle check warns on it)
|
|
36
|
-
# and a manifest hook's events: list (BundleHook) needs the bundle's
|
|
37
|
-
# requires_chi set by hand (docs/hooks.md, Bundle Hooks): no source
|
|
38
|
-
# scan covers manifest.yml.
|
|
35
|
+
# fires one it doesn't know (harmless; chi bundle check warns on it).
|
|
39
36
|
SINCE = {
|
|
40
37
|
"ctx.steer" => "0.3.0", "ctx.stop_turn" => "0.3.0", "ctx.stop_generation" => "0.6.0",
|
|
41
38
|
"Samagotchi::AtomicFile" => "0.8.0", "ctx.model" => "0.17.0", "ctx.model_key" => "0.17.0",
|
|
@@ -57,9 +54,14 @@ module Samagotchi
|
|
|
57
54
|
"ctx.continues" => "0.141.0", "ctx.context.left_behind" => "0.141.0", "ctx.context.attach(movable:)" => "0.141.0",
|
|
58
55
|
"ctx.session_dir" => "0.143.0", "ctx.session_dir(create:)" => "0.144.0",
|
|
59
56
|
"ctx.objectives" => "0.149.0", "ctx.turn_running?" => "0.149.0",
|
|
60
|
-
"ctx.last_answer" => "0.154.0"
|
|
57
|
+
"ctx.last_answer" => "0.154.0",
|
|
58
|
+
"chi.on(on_error:)" => "0.157.0"
|
|
61
59
|
}.freeze
|
|
62
60
|
|
|
61
|
+
# chi.on's on_error: values. :log (the default) logs a raise and goes
|
|
62
|
+
# on; :fail_closed (before_tool_call only) denies the call it raised on.
|
|
63
|
+
ON_ERROR = %i[log fail_closed].freeze
|
|
64
|
+
|
|
63
65
|
# @param bundle [String]
|
|
64
66
|
# @param label [String] "<file> (bundle <name>)": what hooks and
|
|
65
67
|
# failures are named by
|
|
@@ -222,14 +224,31 @@ module Samagotchi
|
|
|
222
224
|
# logged and skipped; its first failure in this worker also tells the
|
|
223
225
|
# user, once (Registries#hook_failed).
|
|
224
226
|
# @param priority [Integer] lower runs first among bundle hooks
|
|
225
|
-
|
|
227
|
+
# @param on_error [Symbol] :log, or :fail_closed for a before_tool_call
|
|
228
|
+
# guard: a raise denies that call. A plugin that registers one is a
|
|
229
|
+
# guardrail (#fail_closed?): if it then fails to load, every tool call
|
|
230
|
+
# is denied (Loader; the manifest's plugin: required: true says so
|
|
231
|
+
# before it registers anything).
|
|
232
|
+
def on(event, priority: 100, on_error: :log, &block)
|
|
226
233
|
raise ArgumentError, "on(#{event.inspect}) needs a block" unless block
|
|
227
234
|
raise ArgumentError, "on: the event must be a Symbol or String" unless event.is_a?(Symbol) || event.is_a?(String)
|
|
228
235
|
|
|
229
|
-
|
|
236
|
+
event = event.to_sym
|
|
237
|
+
policy = on_error.is_a?(String) ? on_error.to_sym : on_error
|
|
238
|
+
raise ArgumentError, "on(#{event.inspect}): on_error: must be :log or :fail_closed, not #{on_error.inspect}" unless ON_ERROR.include?(policy)
|
|
239
|
+
if policy == :fail_closed && event != :before_tool_call
|
|
240
|
+
raise ArgumentError, "on(#{event.inspect}): on_error: :fail_closed is for :before_tool_call only"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
@hooks << { event: event, priority: Integer(priority), on_error: policy, block: block }
|
|
230
244
|
nil
|
|
231
245
|
end
|
|
232
246
|
|
|
247
|
+
# Whether #register staged a fail-closed hook (chi.on(on_error:
|
|
248
|
+
# :fail_closed)): the plugin is a guardrail, so its failing to load
|
|
249
|
+
# denies every tool call (Loader).
|
|
250
|
+
def fail_closed? = @hooks.any? { |hook| hook[:on_error] == :fail_closed }
|
|
251
|
+
|
|
233
252
|
# A long-lived thing the plugin keeps (a server process): the block
|
|
234
253
|
# starts it and returns what #value gives; in it, svc.on_stop { }
|
|
235
254
|
# says how to stop it. It starts on first svc.value, or now with
|
|
@@ -451,10 +470,13 @@ module Samagotchi
|
|
|
451
470
|
@tools.each { |tool| @registries.tools.register(tool.name, source: @bundle, **tool.entry_fields(context)) }
|
|
452
471
|
@hooks.each do |hook|
|
|
453
472
|
block = hook[:block]
|
|
454
|
-
# Never stderr (echo: false): a turn's live region is on screen.
|
|
455
|
-
|
|
473
|
+
# Never stderr (echo: false): a turn's live region is on screen. A
|
|
474
|
+
# fail-closed one denies the call it raised on (Hooks.wrap's :deny).
|
|
475
|
+
policy = hook[:on_error] == :fail_closed ? :deny : :log
|
|
476
|
+
label = "#{"guardrail " if policy == :deny}#{@label} #{hook[:event]} hook"
|
|
477
|
+
handler = Hooks.wrap(label: label, event: hook[:event], policy: policy,
|
|
456
478
|
log: [:plugins, "plugin_hook_failed"], echo: false, fields: { bundle: @bundle },
|
|
457
|
-
on_failure:
|
|
479
|
+
on_failure: hook_failed(policy)) do |event|
|
|
458
480
|
# ctx's helpers act as this event's while the block runs.
|
|
459
481
|
context ? context.with_event(event) { block.call(event, context) } : block.call(event, context)
|
|
460
482
|
end
|
|
@@ -478,6 +500,16 @@ module Samagotchi
|
|
|
478
500
|
@committed = true
|
|
479
501
|
end
|
|
480
502
|
|
|
503
|
+
# Registries#hook_failed for a hook under +policy+: a fail-closed one's
|
|
504
|
+
# notice says it denies (effect: :deny).
|
|
505
|
+
def hook_failed(policy)
|
|
506
|
+
told = @registries.hook_failed
|
|
507
|
+
return told unless told && policy == :deny
|
|
508
|
+
|
|
509
|
+
->(label, failed) { told.call(label, failed, effect: :deny) }
|
|
510
|
+
end
|
|
511
|
+
private :hook_failed
|
|
512
|
+
|
|
481
513
|
# @return [Hash] how many of each it registered (for the log)
|
|
482
514
|
def counts = { commands: @commands.size, annotations: @annotations.size, tools: @tools.size, hooks: @hooks.size,
|
|
483
515
|
services: @services.size, inits: @inits.size, toolkits: @toolkits.size,
|