samagotchi 0.159.0 → 0.161.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 +125 -1
- data/docs/architecture.md +12 -9
- data/docs/cli.md +7 -5
- data/docs/context.md +10 -1
- data/docs/guardrails.md +33 -5
- data/docs/internals/attached-context.md +4 -2
- data/docs/internals/bridge.md +28 -6
- data/docs/internals/bundles.md +24 -5
- data/docs/internals/engine.md +7 -1
- data/docs/internals/objectives.md +8 -2
- data/docs/internals/remote-sessions.md +2 -1
- data/docs/memory.md +43 -13
- data/docs/plugins.md +13 -4
- data/docs/releasing.md +7 -4
- data/docs/scheduling.md +53 -8
- data/docs/sessions.md +17 -12
- data/docs/testing.md +9 -1
- data/lib/samagotchi/bridge/card_store.rb +31 -3
- data/lib/samagotchi/bridge/ring_buffer.rb +11 -1
- data/lib/samagotchi/bridge/sse_writer.rb +8 -5
- data/lib/samagotchi/bridge.rb +103 -44
- data/lib/samagotchi/bridge_client.rb +9 -6
- data/lib/samagotchi/broadcast/scope_card.rb +2 -1
- data/lib/samagotchi/broadcast_command.rb +24 -8
- data/lib/samagotchi/bundle_command.rb +57 -6
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/memory_guide.md +2 -2
- data/lib/samagotchi/child_reports.rb +29 -23
- data/lib/samagotchi/client_id.rb +6 -0
- data/lib/samagotchi/command_intake.rb +56 -0
- data/lib/samagotchi/compat.rb +32 -0
- data/lib/samagotchi/context_absorber.rb +6 -3
- data/lib/samagotchi/context_candidate_choice.rb +8 -5
- data/lib/samagotchi/context_candidates.rb +11 -8
- data/lib/samagotchi/context_candidates_log.rb +10 -1
- data/lib/samagotchi/context_command.rb +10 -4
- data/lib/samagotchi/context_move.rb +6 -1
- data/lib/samagotchi/context_poller.rb +5 -2
- data/lib/samagotchi/context_sources.rb +6 -4
- data/lib/samagotchi/engine/events.rb +25 -2
- data/lib/samagotchi/engine/guardrails.rb +3 -4
- data/lib/samagotchi/engine/plugins.rb +1 -1
- data/lib/samagotchi/engine/steering.rb +2 -2
- data/lib/samagotchi/engine.rb +1 -0
- data/lib/samagotchi/engine_peers.rb +1 -0
- data/lib/samagotchi/events.rb +71 -0
- data/lib/samagotchi/guardrails/approvals.rb +27 -3
- data/lib/samagotchi/guardrails/child_boundary.rb +37 -4
- data/lib/samagotchi/guardrails/protected_paths.rb +8 -5
- data/lib/samagotchi/guardrails/targets.rb +25 -6
- data/lib/samagotchi/hook_routes.rb +0 -1
- data/lib/samagotchi/live_versions.rb +7 -2
- data/lib/samagotchi/memory_bundle/asset_installer.rb +86 -18
- data/lib/samagotchi/memory_bundle/bundle_hook.rb +7 -27
- data/lib/samagotchi/memory_bundle/file_actions.rb +3 -4
- data/lib/samagotchi/memory_bundle/file_state.rb +11 -0
- data/lib/samagotchi/memory_bundle/index_updater.rb +7 -4
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +1 -1
- data/lib/samagotchi/memory_bundle/installer.rb +93 -23
- data/lib/samagotchi/memory_bundle/manifest.rb +6 -0
- data/lib/samagotchi/memory_bundle/provenance.rb +158 -6
- data/lib/samagotchi/memory_bundle/shipped_update.rb +7 -4
- data/lib/samagotchi/memory_bundle/status.rb +3 -3
- data/lib/samagotchi/memory_bundle/system_bundle.rb +2 -0
- data/lib/samagotchi/memory_bundle/uninstaller.rb +29 -13
- data/lib/samagotchi/memory_orphans.rb +54 -13
- data/lib/samagotchi/memory_paths.rb +29 -6
- data/lib/samagotchi/memory_usage.rb +23 -12
- data/lib/samagotchi/note_command.rb +9 -1
- data/lib/samagotchi/note_delivery.rb +34 -24
- data/lib/samagotchi/note_move.rb +205 -0
- data/lib/samagotchi/objectives_command.rb +3 -1
- data/lib/samagotchi/owner_lock.rb +7 -1
- data/lib/samagotchi/plugin/attached_context.rb +3 -1
- data/lib/samagotchi/plugin/sessions.rb +4 -1
- data/lib/samagotchi/project_command.rb +58 -20
- data/lib/samagotchi/project_id.rb +102 -0
- data/lib/samagotchi/project_id_clash.rb +62 -0
- data/lib/samagotchi/project_init.rb +214 -10
- data/lib/samagotchi/project_scope.rb +11 -2
- data/lib/samagotchi/review_start.rb +5 -3
- data/lib/samagotchi/schedule_command.rb +12 -0
- data/lib/samagotchi/schedules.rb +374 -76
- data/lib/samagotchi/send_command.rb +2 -15
- data/lib/samagotchi/session.rb +59 -7
- data/lib/samagotchi/session_access/local.rb +53 -22
- data/lib/samagotchi/session_access/remote.rb +13 -5
- data/lib/samagotchi/session_access/web_bridge_client.rb +2 -2
- data/lib/samagotchi/session_access.rb +20 -4
- data/lib/samagotchi/session_commands.rb +7 -3
- data/lib/samagotchi/session_continue.rb +162 -24
- data/lib/samagotchi/session_delete_command.rb +3 -1
- data/lib/samagotchi/session_inbox.rb +41 -1
- data/lib/samagotchi/session_inbox_delivery.rb +254 -0
- data/lib/samagotchi/session_manager/delivery.rb +66 -12
- data/lib/samagotchi/session_manager/emptiness.rb +7 -2
- data/lib/samagotchi/session_manager/lifecycle.rb +2 -1
- data/lib/samagotchi/session_manager/listing.rb +4 -1
- data/lib/samagotchi/session_manager/removal.rb +130 -26
- data/lib/samagotchi/session_manager/spawn.rb +167 -100
- data/lib/samagotchi/session_manager/worker_command.rb +7 -19
- data/lib/samagotchi/session_manager.rb +1 -0
- data/lib/samagotchi/session_retention.rb +53 -23
- data/lib/samagotchi/session_setup.rb +27 -7
- data/lib/samagotchi/starting_marker.rb +93 -24
- data/lib/samagotchi/state_format.rb +129 -0
- data/lib/samagotchi/system_one/runs.rb +6 -1
- data/lib/samagotchi/system_one/runs_store.rb +18 -14
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +7 -4
- data/lib/samagotchi/terminal_ui/attached_loop/composer.rb +5 -2
- data/lib/samagotchi/terminal_ui/attached_loop.rb +5 -0
- data/lib/samagotchi/terminal_ui/version_lines.rb +4 -2
- data/lib/samagotchi/terminal_ui.rb +2 -3
- data/lib/samagotchi/tools/context_read.rb +7 -2
- data/lib/samagotchi/tools/delegate.rb +19 -6
- data/lib/samagotchi/tools/schedule_task.rb +5 -3
- data/lib/samagotchi/tools/send_note.rb +10 -4
- data/lib/samagotchi/turn_stages/context_judge.rb +7 -7
- data/lib/samagotchi/turn_state.rb +108 -4
- data/lib/samagotchi/update_command.rb +9 -4
- data/lib/samagotchi/verify_fresh.rb +27 -9
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +6 -5
- data/lib/samagotchi/web/public/app.js +2 -7
- data/lib/samagotchi/web/public/chains.js +4 -4
- data/lib/samagotchi/web/public/data.js +18 -2
- data/lib/samagotchi/web/public/info_bar_model.js +1 -1
- data/lib/samagotchi/web/public/session_fields.js +3 -2
- data/lib/samagotchi/web/public/session_state.js +2 -0
- data/lib/samagotchi/web/public/update_notice.js +2 -2
- data/lib/samagotchi/web/session_access_routes.rb +15 -11
- data/lib/samagotchi/web/session_control.rb +9 -3
- data/lib/samagotchi/web/session_create.rb +8 -4
- data/lib/samagotchi/web/session_hub.rb +50 -5
- data/lib/samagotchi/web/session_summary.rb +5 -2
- data/lib/samagotchi/worker.rb +61 -11
- data/lib/samagotchi/worker_boot.rb +22 -11
- data/lib/samagotchi/worker_inbound.rb +62 -10
- data/lib/samagotchi/worker_link.rb +4 -1
- data/lib/samagotchi/worker_presence.rb +106 -0
- data/lib/samagotchi/worker_sidecar.rb +3 -4
- metadata +9 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "digest"
|
|
3
4
|
require "json"
|
|
4
5
|
require "fileutils"
|
|
5
6
|
require "time"
|
|
@@ -13,7 +14,9 @@ module Samagotchi
|
|
|
13
14
|
# written aside and renamed under an exclusive flock (several chi
|
|
14
15
|
# processes share it). An entry relaxes an ask, never a deny:
|
|
15
16
|
# session — this exact call (tool + normalized command / paths) in
|
|
16
|
-
# this session;
|
|
17
|
+
# this session; a command's in its place too (the
|
|
18
|
+
# worktree or folder it ran in, and a digest of its env:),
|
|
19
|
+
# since the same text acts on whatever they point at;
|
|
17
20
|
# repo — this exact call in this repo (the cwd outside a repo);
|
|
18
21
|
# rule — anything this rule (from this source) asks about here.
|
|
19
22
|
# "This repo" is the repository (repo: its common git dir), so an
|
|
@@ -74,11 +77,14 @@ module Samagotchi
|
|
|
74
77
|
here = ->(e) { e["repo"] ? e["repo"] == repo : e["repo_root"] == place }
|
|
75
78
|
session_id = verdict.context&.session_id
|
|
76
79
|
session_only = SESSION_ONLY_RULES.include?(verdict.rule.to_s)
|
|
80
|
+
command_place = session_place(verdict)
|
|
77
81
|
list.find do |e|
|
|
78
82
|
next false if session_only && e["scope"] != "session"
|
|
79
83
|
|
|
80
84
|
case e["scope"]
|
|
81
|
-
when "session"
|
|
85
|
+
when "session"
|
|
86
|
+
session_id && e["session_id"] == session_id && e["key"] == key &&
|
|
87
|
+
(command_place.nil? || e.slice("repo_root", "env") == command_place)
|
|
82
88
|
when "repo" then here.call(e) && e["key"] == key
|
|
83
89
|
when "rule"
|
|
84
90
|
# One rule's grant can't answer for a second rule's ask on the call.
|
|
@@ -94,7 +100,9 @@ module Samagotchi
|
|
|
94
100
|
entry = { "scope" => scope, "tool" => verdict.targets&.tool || verdict.call[:name].to_s,
|
|
95
101
|
"acts_as" => verdict.targets&.acts_as, "rule" => verdict.rule, "source" => verdict.source, "created_at" => Time.now.utc.iso8601 }
|
|
96
102
|
case scope
|
|
97
|
-
when "session"
|
|
103
|
+
when "session"
|
|
104
|
+
entry.merge!("session_id" => verdict.context&.session_id, "key" => key_for(verdict))
|
|
105
|
+
entry.merge!(session_place(verdict) || {})
|
|
98
106
|
when "repo"
|
|
99
107
|
entry.merge!("repo_root" => place_for(verdict), "repo" => verdict.targets&.repo, "key" => key_for(verdict))
|
|
100
108
|
when "rule" then entry.merge!("repo_root" => place_for(verdict), "repo" => verdict.targets&.repo)
|
|
@@ -131,6 +139,22 @@ module Samagotchi
|
|
|
131
139
|
t&.repo_root || t&.cwd || verdict.context&.cwd
|
|
132
140
|
end
|
|
133
141
|
|
|
142
|
+
# Where a session entry of a command holds: the call's worktree root
|
|
143
|
+
# (or folder: #place_for; never the common git dir, which every
|
|
144
|
+
# worktree shares) and, when it sets one, a sha256 of its env: (never
|
|
145
|
+
# the env: it may hold a secret). nil for a call with no command:
|
|
146
|
+
# its paths are its place. An entry without them (stored before) no
|
|
147
|
+
# longer matches: one more ask.
|
|
148
|
+
# @return [Hash, nil]
|
|
149
|
+
def self.session_place(verdict)
|
|
150
|
+
t = verdict.targets
|
|
151
|
+
return nil unless t&.command
|
|
152
|
+
|
|
153
|
+
env = t.env.to_h
|
|
154
|
+
digest = env.empty? ? nil : Digest::SHA256.hexdigest(JSON.generate(env.map { |k, v| [k.to_s, v.to_s] }.sort))
|
|
155
|
+
{ "repo_root" => place_for(verdict), "env" => digest }.compact
|
|
156
|
+
end
|
|
157
|
+
|
|
134
158
|
private
|
|
135
159
|
|
|
136
160
|
# @param locked [Boolean] the caller holds the store's lock
|
|
@@ -17,14 +17,20 @@ module Samagotchi
|
|
|
17
17
|
# (a repo or rule answer would hold in every worktree, for every child),
|
|
18
18
|
# when:
|
|
19
19
|
# write, edit: a path is outside root (Outside: tmp dirs and memory
|
|
20
|
-
# files never are);
|
|
20
|
+
# files never are); so is a path a plugin tool's targets: declare
|
|
21
|
+
# the call writes (writes: true), the same declaration
|
|
22
|
+
# ProtectedPaths reads — a call is judged by what its targets say it
|
|
23
|
+
# acts on, not by its tool's name;
|
|
21
24
|
# execute, task_create: mutating git runs in a dir outside root
|
|
22
25
|
# (ShellGitDirs), or a command that isn't read-only starts outside
|
|
23
26
|
# root (its cwd:, else Dir.pwd) or names a path in the repository's
|
|
24
27
|
# other checkouts (the main one and sibling worktrees) or its
|
|
25
28
|
# common git dir; or it runs git that changes what all checkouts
|
|
26
29
|
# share, from anywhere (SharedGit: config, update-ref, worktree add,
|
|
27
|
-
# stash clear, tags, another branch's delete).
|
|
30
|
+
# stash clear, tags, another branch's delete). A plugin tool's
|
|
31
|
+
# declared command: is checked for what chi can read for sure only:
|
|
32
|
+
# SharedGit, and whether the call's cwd: is outside root (its text
|
|
33
|
+
# may not be shell; Targets#rule_command).
|
|
28
34
|
# Paths elsewhere in a command (/usr/bin, ~/.gem) don't count, and
|
|
29
35
|
# read-only commands never ask, nor do ones that only write into a tmp
|
|
30
36
|
# dir (> / >>, cp's destination, tee; Targets#read_only_but_tmp_writes?). Paths resolve under chi's env and the
|
|
@@ -67,12 +73,39 @@ module Samagotchi
|
|
|
67
73
|
|
|
68
74
|
def crosses?(targets, root)
|
|
69
75
|
case targets.tool
|
|
70
|
-
when "write", "edit" then
|
|
76
|
+
when "write", "edit" then paths_cross?(targets, root)
|
|
71
77
|
when *SHELL_TOOLS then shell_crosses?(targets, root)
|
|
72
|
-
else
|
|
78
|
+
else
|
|
79
|
+
# chi's other own tools (read, memory_write, …) are unchanged:
|
|
80
|
+
# memory_write carries writes: for ProtectedPaths, but its paths
|
|
81
|
+
# are memory files (Outside exempts them) and its `..` entry names
|
|
82
|
+
# asked for nothing here before.
|
|
83
|
+
return false if Targets::CORE_TOOLS.include?(targets.tool)
|
|
84
|
+
|
|
85
|
+
plugin_crosses?(targets, root)
|
|
73
86
|
end
|
|
74
87
|
end
|
|
75
88
|
|
|
89
|
+
def paths_cross?(targets, root)
|
|
90
|
+
targets.paths.any? { |path| Outside.outside?(path, root: root) }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# A plugin tool, by what its targets: say it acts on (Targets#writes?,
|
|
94
|
+
# #command, #cwd), not by its name: paths its targets say it writes
|
|
95
|
+
# like a write's, and a declared command like a shell's — but for the
|
|
96
|
+
# command only what chi can read for sure: git that changes what every
|
|
97
|
+
# checkout shares (SharedGit), and where the call runs (its cwd:). A
|
|
98
|
+
# plugin's command text may not be shell (Targets#rule_command), so
|
|
99
|
+
# the shell half's git-dir, cd and redirect reads are left out; a
|
|
100
|
+
# plugin that wants more can bring a rule of its own.
|
|
101
|
+
def plugin_crosses?(targets, root)
|
|
102
|
+
return true if targets.writes? && paths_cross?(targets, root)
|
|
103
|
+
return :shared if targets.command && SharedGit.changes?(targets.command, own_branch: @branch.call)
|
|
104
|
+
return true if targets.command && targets.cwd && Outside.outside?(targets.cwd, root: root)
|
|
105
|
+
|
|
106
|
+
false
|
|
107
|
+
end
|
|
108
|
+
|
|
76
109
|
def shell_crosses?(targets, root)
|
|
77
110
|
return true if targets.git_dirs.any? { |dir| dir.is_a?(String) && Outside.outside?(dir, root: root) }
|
|
78
111
|
return :shared if SharedGit.changes?(targets.command, own_branch: @branch.call)
|
|
@@ -4,7 +4,8 @@ require_relative "path_resolve"
|
|
|
4
4
|
|
|
5
5
|
module Samagotchi
|
|
6
6
|
module Guardrails
|
|
7
|
-
# Core checks on what
|
|
7
|
+
# Core checks on what a call writes (Targets#writes?: write, edit,
|
|
8
|
+
# memory_write, and a plugin tool whose targets: say writes: true),
|
|
8
9
|
# after the hooks and before the rules. Paths are compared with
|
|
9
10
|
# symlinks resolved (on the longest existing parent, `..` taken
|
|
10
11
|
# physically, a dangling symlink followed: PathResolve.reals, the
|
|
@@ -14,7 +15,8 @@ module Samagotchi
|
|
|
14
15
|
# hooks, rules, manifests), attached context (a source's
|
|
15
16
|
# command runs later, outside the gate; chi context writes
|
|
16
17
|
# it) and System 1's saved runs (a label there may train a
|
|
17
|
-
# model); nothing legitimate writes there through
|
|
18
|
+
# model); nothing legitimate writes there through a tool
|
|
19
|
+
# that says it writes.
|
|
18
20
|
# ask (once / session): config.yml, home.json (the chi home: the
|
|
19
21
|
# Ruby every chi launch boots), the chi shim (ChiShim: the
|
|
20
22
|
# `chi` every tool runs) and the plain hooks dir. The
|
|
@@ -24,7 +26,6 @@ module Samagotchi
|
|
|
24
26
|
# execute can still write all of them (the guardrails bundle adds a
|
|
25
27
|
# text match for that).
|
|
26
28
|
class ProtectedPaths
|
|
27
|
-
FILE_TOOLS = %w[write edit memory_write].freeze
|
|
28
29
|
SOURCE = "core"
|
|
29
30
|
|
|
30
31
|
# @param store_dir [String] <state dir>/guardrails
|
|
@@ -50,10 +51,12 @@ module Samagotchi
|
|
|
50
51
|
].select { |_, root, _| root }
|
|
51
52
|
end
|
|
52
53
|
|
|
53
|
-
# Vote on +verdict+ (its targets)
|
|
54
|
+
# Vote on +verdict+ (its targets): a call whose targets say it writes
|
|
55
|
+
# (Targets#writes?: write, edit, memory_write, or a plugin tool's
|
|
56
|
+
# targets: writes: true) is judged on its paths.
|
|
54
57
|
def check(verdict)
|
|
55
58
|
targets = verdict.targets
|
|
56
|
-
return verdict unless targets
|
|
59
|
+
return verdict unless targets&.writes?
|
|
57
60
|
|
|
58
61
|
paths = targets.paths.flat_map { |p| PathResolve.reals(p) }
|
|
59
62
|
@deny.each do |rule, root, reason|
|
|
@@ -90,6 +90,15 @@ module Samagotchi
|
|
|
90
90
|
# reads it)
|
|
91
91
|
attr_reader :label
|
|
92
92
|
|
|
93
|
+
# @return [Boolean] whether the call writes the paths its targets name:
|
|
94
|
+
# chi's own write, edit and memory_write, and a plugin tool whose
|
|
95
|
+
# targets: say writes: true (a literal true only). The checks that
|
|
96
|
+
# guard a write (ProtectedPaths, ChildBoundary) ask this rather than
|
|
97
|
+
# the tool's name, so a plugin tool that acts like one is judged
|
|
98
|
+
# like one.
|
|
99
|
+
attr_reader :writes
|
|
100
|
+
alias writes? writes
|
|
101
|
+
|
|
93
102
|
# @return [Boolean] a memory_write call that removes a memory (remove:
|
|
94
103
|
# true), for a rule's memory: remove
|
|
95
104
|
attr_reader :memory_remove
|
|
@@ -126,6 +135,7 @@ module Samagotchi
|
|
|
126
135
|
args = nil
|
|
127
136
|
acts_as = nil
|
|
128
137
|
label = nil
|
|
138
|
+
writes = false
|
|
129
139
|
env = {}
|
|
130
140
|
case tool
|
|
131
141
|
when *SHELL_TOOLS
|
|
@@ -134,14 +144,17 @@ module Samagotchi
|
|
|
134
144
|
given = call[:cwd].to_s.strip
|
|
135
145
|
cwd = File.expand_path(given, base) unless given.empty?
|
|
136
146
|
when "write", "edit"
|
|
147
|
+
writes = true
|
|
137
148
|
paths.concat(absolutes(call[:path], base))
|
|
138
149
|
when "read"
|
|
139
150
|
paths.concat(absolutes(call[:content], base))
|
|
140
151
|
when "memory_write"
|
|
152
|
+
writes = true
|
|
141
153
|
paths << memory_path(call, model_key)
|
|
142
154
|
else
|
|
143
155
|
given = plugin_targets(call, registry, surface)
|
|
144
156
|
command = given[:command]
|
|
157
|
+
writes = given[:writes]
|
|
145
158
|
given_cwd = given[:cwd].to_s.strip
|
|
146
159
|
cwd = File.expand_path(given_cwd, base) unless given_cwd.empty?
|
|
147
160
|
paths.concat(given[:paths].flat_map { |path| absolutes(path, cwd) })
|
|
@@ -154,25 +167,29 @@ module Samagotchi
|
|
|
154
167
|
memory_overlay = tool == "memory_write" && !memory_remove && call[:current_model_only].to_s.strip.downcase == "true"
|
|
155
168
|
memory = tool == "memory_write" ? memory_facts(call) : nil
|
|
156
169
|
new(tool: tool, command: command, paths: paths.compact, cwd: cwd, env: env, repo_root: context.repo_root(cwd),
|
|
157
|
-
repo: context.repo(cwd), args: args, acts_as: acts_as, label: label,
|
|
170
|
+
repo: context.repo(cwd), args: args, acts_as: acts_as, label: label, writes: writes,
|
|
171
|
+
memory_remove: memory_remove,
|
|
158
172
|
memory_overlay: memory_overlay, memory: memory,
|
|
159
173
|
model_name: model_name, model_key: model_key, session_root: context.repo_root(base) || base,
|
|
160
174
|
chi_dirs: -> { chi_dirs(context, base) })
|
|
161
175
|
end
|
|
162
176
|
|
|
163
177
|
# What a plugin tool's targets: callable says the call acts on:
|
|
164
|
-
# {command:, paths:, cwd:, acts_as:, args:, label:}, each
|
|
178
|
+
# {command:, paths:, writes:, cwd:, acts_as:, args:, label:}, each
|
|
179
|
+
# optional; a callable
|
|
165
180
|
# that raises or gives something else counts as nothing (and is
|
|
166
181
|
# logged). args: counts when it is a Hash; acts_as: is dropped (and
|
|
167
182
|
# logged) when it names a core tool or another bundle's tool, so a
|
|
168
183
|
# call can't pass for one and match (or reuse the approvals of) its
|
|
169
|
-
# rules. label: counts when it is one short line (.label_of).
|
|
184
|
+
# rules. label: counts when it is one short line (.label_of). writes:
|
|
185
|
+
# counts only for a literal true: the tool says its paths are written,
|
|
186
|
+
# and the checks that guard write/edit judge them (.writes?). A
|
|
170
187
|
# missing surface (Tools::MissingSurface) is raised, never taken as
|
|
171
188
|
# nothing: call_tool's acts_as would go missing, and with it the
|
|
172
189
|
# mcp_* rules on the call (fail-open).
|
|
173
190
|
def self.plugin_targets(call, registry, surface = nil)
|
|
174
191
|
entry = entry_of(call[:name], registry, surface)
|
|
175
|
-
none = { command: nil, paths: [], cwd: nil, acts_as: nil, args: nil, label: nil }
|
|
192
|
+
none = { command: nil, paths: [], writes: false, cwd: nil, acts_as: nil, args: nil, label: nil }
|
|
176
193
|
return none unless entry && !entry.core? && entry.targets
|
|
177
194
|
|
|
178
195
|
given = entry.targets_of(call, surface)
|
|
@@ -180,7 +197,8 @@ module Samagotchi
|
|
|
180
197
|
|
|
181
198
|
given = given.transform_keys(&:to_sym)
|
|
182
199
|
command = given[:command].to_s
|
|
183
|
-
{ command: command.empty? ? nil : command, paths: Array(given[:paths]).map(&:to_s),
|
|
200
|
+
{ command: command.empty? ? nil : command, paths: Array(given[:paths]).map(&:to_s), writes: given[:writes] == true,
|
|
201
|
+
cwd: given[:cwd],
|
|
184
202
|
acts_as: acts_as_of(given[:acts_as], entry, registry, surface), args: given[:args].is_a?(Hash) ? given[:args] : nil,
|
|
185
203
|
label: label_of(given[:label], entry) }
|
|
186
204
|
rescue Tools::MissingSurface
|
|
@@ -318,10 +336,11 @@ module Samagotchi
|
|
|
318
336
|
# @param env [Hash{String=>String}] the call's env: (.env_of)
|
|
319
337
|
def initialize(tool:, command:, paths:, cwd:, repo_root:, env: {}, repo: nil, args: nil, acts_as: nil,
|
|
320
338
|
label: nil, model_name: nil, model_key: nil, session_root: nil, chi_dirs: nil,
|
|
321
|
-
memory_remove: false, memory_overlay: false, memory: nil)
|
|
339
|
+
memory_remove: false, memory_overlay: false, memory: nil, writes: false)
|
|
322
340
|
@env = env
|
|
323
341
|
@acts_as = acts_as
|
|
324
342
|
@label = label
|
|
343
|
+
@writes = writes
|
|
325
344
|
@memory_remove = memory_remove
|
|
326
345
|
@memory_overlay = memory_overlay
|
|
327
346
|
@memory = memory
|
|
@@ -24,7 +24,6 @@ module Samagotchi
|
|
|
24
24
|
# (a chi.on handler :log, or :deny for an on_error: :fail_closed one).
|
|
25
25
|
HOOK_FAILURE_EFFECTS = {
|
|
26
26
|
log: "it's skipped, and only the log has its next failures",
|
|
27
|
-
skip: "it's skipped, and its next failures are silent",
|
|
28
27
|
deny: "it denies every tool call it guards, and its next failures are in each denial's reason"
|
|
29
28
|
}.freeze
|
|
30
29
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rubygems/version"
|
|
3
4
|
require_relative "config"
|
|
4
5
|
require_relative "session"
|
|
5
6
|
require_relative "version"
|
|
@@ -38,9 +39,13 @@ module Samagotchi
|
|
|
38
39
|
end
|
|
39
40
|
end
|
|
40
41
|
|
|
41
|
-
# The live workers
|
|
42
|
+
# The live workers older than the target chi (unknown versions count).
|
|
43
|
+
# Compat's N-1 verdict is for hints; these callers are adopting the new code.
|
|
42
44
|
def stale_workers(version = VERSION, state_dir: Session.default_state_dir)
|
|
43
|
-
|
|
45
|
+
target = Gem::Version.new(version)
|
|
46
|
+
workers(state_dir: state_dir).select do |worker|
|
|
47
|
+
worker.version.nil? || !Gem::Version.correct?(worker.version) || Gem::Version.new(worker.version) < target
|
|
48
|
+
end
|
|
44
49
|
end
|
|
45
50
|
|
|
46
51
|
# The /api/info of a chi web on host:port, or nil when nothing (or not
|
|
@@ -50,7 +50,8 @@ module Samagotchi
|
|
|
50
50
|
# @param chi_version [String] the chi this install is for (the Installer's;
|
|
51
51
|
# a caller that checks its own bundles against another version passes it)
|
|
52
52
|
def initialize(name:, source_dir:, manifest:, provenance:, existing:, results:, warnings:, conflicts:, trash:,
|
|
53
|
-
force: false, strict: true, upgrade: false, dry_run: false, dev: false, chi_version: Samagotchi::VERSION
|
|
53
|
+
force: false, strict: true, upgrade: false, dry_run: false, dev: false, chi_version: Samagotchi::VERSION,
|
|
54
|
+
refuse_mismatch: false)
|
|
54
55
|
@name = name
|
|
55
56
|
@bundle_dir = source_dir
|
|
56
57
|
@manifest = manifest
|
|
@@ -66,11 +67,42 @@ module Samagotchi
|
|
|
66
67
|
@dry_run = dry_run
|
|
67
68
|
@dev = dev
|
|
68
69
|
@chi_version = chi_version
|
|
70
|
+
@refuse_mismatch = refuse_mismatch
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Raises for the first plugin or script the manifest names that the
|
|
74
|
+
# bundle doesn't have (or ships as a link), writing nothing: the
|
|
75
|
+
# Installer runs it before its first write.
|
|
76
|
+
# @raise [Installer::InstallError]
|
|
77
|
+
def check_named_files
|
|
78
|
+
plugin_source
|
|
79
|
+
scripts_sources
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# The same early pass on the shas: the plugin and each script against the
|
|
83
|
+
# bundle's own bytes, before anything is written. Only when the caller
|
|
84
|
+
# refuses a mismatch (`chi bundle install/upgrade`; chi update, a profile
|
|
85
|
+
# and a dev install keep their warning), and not with --force, which
|
|
86
|
+
# installs a mismatch anyway.
|
|
87
|
+
# @raise [Installer::InstallError] one line per mismatched file
|
|
88
|
+
def check_named_checksums
|
|
89
|
+
return unless @refuse_mismatch && @strict && !@dev && !@force && @manifest
|
|
90
|
+
|
|
91
|
+
lines = []
|
|
92
|
+
if @manifest.plugin
|
|
93
|
+
lines << Installer.mismatch_refusal("plugin #{@manifest.plugin[:file]}", plugin_source,
|
|
94
|
+
@manifest.checksum_for_plugin, upgrade: @upgrade)
|
|
95
|
+
end
|
|
96
|
+
scripts_sources.each do |name, src|
|
|
97
|
+
lines << Installer.mismatch_refusal("script #{name}", src, declared_sha(:script, name), upgrade: @upgrade)
|
|
98
|
+
end
|
|
99
|
+
lines = lines.compact
|
|
100
|
+
raise Installer::InstallError, lines.join("\n") unless lines.empty?
|
|
69
101
|
end
|
|
70
102
|
|
|
71
103
|
# @return [Installed]
|
|
72
|
-
# @raise [Installer::InstallError] the
|
|
73
|
-
#
|
|
104
|
+
# @raise [Installer::InstallError] the same, when #check_named_files
|
|
105
|
+
# wasn't run first
|
|
74
106
|
def run
|
|
75
107
|
incoming_rules = Dir.glob(File.join(@bundle_dir, "guardrails", "*.yml")).sort
|
|
76
108
|
.reject { |src| linked_out?(src, "guardrails/#{File.basename(src)}") }
|
|
@@ -115,7 +147,11 @@ module Samagotchi
|
|
|
115
147
|
files.each do |file|
|
|
116
148
|
@results["hooks/#{file}"] = @dry_run ? retired.with(word: :would_remove) : retired
|
|
117
149
|
end
|
|
118
|
-
|
|
150
|
+
if @dry_run
|
|
151
|
+
edits = Trash.edits(dir, "hooks", @existing&.hooks&.transform_values(&:sha256))
|
|
152
|
+
@warnings << "Would copy to the trash first: #{edits.join(", ")}" unless edits.empty?
|
|
153
|
+
return
|
|
154
|
+
end
|
|
119
155
|
|
|
120
156
|
@trash.copy_edits(dir, "hooks", @existing&.hooks&.transform_values(&:sha256))
|
|
121
157
|
FileUtils.rm_rf(dir)
|
|
@@ -133,38 +169,53 @@ module Samagotchi
|
|
|
133
169
|
plugin = @manifest&.plugin
|
|
134
170
|
return {} unless plugin
|
|
135
171
|
|
|
136
|
-
src =
|
|
137
|
-
raise Installer::InstallError, "the manifest names plugin #{plugin[:file]}, which the bundle doesn't have" unless File.file?(src)
|
|
138
|
-
raise Installer::InstallError, "plugin #{plugin[:file]} is a symlink (not installed)" if linked?(src)
|
|
139
|
-
|
|
172
|
+
src = plugin_source
|
|
140
173
|
if (failure = Manifest.requires_chi_failure(@manifest.requires_chi, @chi_version))
|
|
141
174
|
@warnings << "Plugin #{plugin[:file]} won't load: #{failure}"
|
|
142
175
|
end
|
|
143
176
|
{ plugin[:file] => src }
|
|
144
177
|
end
|
|
145
178
|
|
|
146
|
-
# The manifest's
|
|
179
|
+
# The manifest's plugin file in the source, checked; nil without one.
|
|
180
|
+
def plugin_source
|
|
181
|
+
plugin = @manifest&.plugin
|
|
182
|
+
return unless plugin
|
|
183
|
+
|
|
184
|
+
src = File.join(@bundle_dir, plugin[:file])
|
|
185
|
+
raise Installer::InstallError, "the manifest names plugin #{plugin[:file]}, which the bundle doesn't have" unless File.file?(src)
|
|
186
|
+
raise Installer::InstallError, "plugin #{plugin[:file]} is a symlink (not installed)" if linked?(src)
|
|
187
|
+
|
|
188
|
+
src
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# The manifest's scripts in the source, checked.
|
|
147
192
|
# @return [Hash{String => String}] file name => the source file
|
|
148
|
-
def
|
|
149
|
-
|
|
150
|
-
scripts.keys.to_h do |file|
|
|
193
|
+
def scripts_sources
|
|
194
|
+
(@manifest&.scripts || {}).keys.to_h do |file|
|
|
151
195
|
src = File.join(@bundle_dir, "scripts", file)
|
|
152
196
|
raise Installer::InstallError, "the manifest names scripts/#{file}, which the bundle doesn't have" unless File.file?(src)
|
|
153
197
|
raise Installer::InstallError, "scripts/#{file} is a symlink (not installed)" if linked?(src)
|
|
154
198
|
|
|
155
|
-
@warnings << "scripts/#{file} is not executable; chmod +x #{src}" if @dev && !File.executable?(src) && !page_only?(file)
|
|
156
199
|
[file, src]
|
|
157
200
|
end
|
|
158
201
|
end
|
|
159
202
|
|
|
203
|
+
# The manifest's scripts (a dev install warns of one it can't run).
|
|
204
|
+
# @return [Hash{String => String}] file name => the source file
|
|
205
|
+
def scripts_incoming
|
|
206
|
+
scripts_sources.each do |file, src|
|
|
207
|
+
@warnings << "scripts/#{file} is not executable; chmod +x #{src}" if @dev && !File.executable?(src) && !page_only?(file)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
160
211
|
# One folder of the bundle's own dir (guardrails/, plugin/, scripts/),
|
|
161
212
|
# replaced whole by +incoming+: each file's AssetState and its action
|
|
162
213
|
# (FileActions' ASSET_INSTALL, or ASSET_UPGRADE over an installed
|
|
163
214
|
# bundle), one result per incoming file (a dry run's DRY_WORDS of it,
|
|
164
215
|
# and nothing written), an edit copied to the trash first (save_first),
|
|
165
|
-
#
|
|
166
|
-
#
|
|
167
|
-
#
|
|
216
|
+
# then the folder replaced. A removed file is named in the summary.
|
|
217
|
+
# A plugin or script whose sha256 differs from the declared one warns
|
|
218
|
+
# (not a dev link: it is the source).
|
|
168
219
|
# @param incoming [Hash{String => String}] name => the source file, in
|
|
169
220
|
# the order the results list them
|
|
170
221
|
# @return [Hash{String => String}] name => installed path ({} on a dry run)
|
|
@@ -173,21 +224,36 @@ module Samagotchi
|
|
|
173
224
|
command = @upgrade && @provenance.installed? ? :upgrade : :install
|
|
174
225
|
states = AssetState.in(dir, kind_of_file: kind_of_file, recorded: recorded, incoming: incoming)
|
|
175
226
|
.to_h { |asset| [asset.name, [asset, FileActions.for_asset(command, asset)]] }
|
|
227
|
+
if @dry_run
|
|
228
|
+
copies = states.filter_map { |_, (asset, action)| asset.label if action&.save_first }
|
|
229
|
+
@warnings << "Would copy to the trash first: #{copies.join(", ")}" unless copies.empty?
|
|
230
|
+
end
|
|
176
231
|
results = incoming.keys.to_h do |name|
|
|
177
232
|
asset, action = states.fetch(name)
|
|
178
233
|
[name, Installer::FileResult.new(word: @dry_run ? DRY_WORDS.fetch(action.word) : action.word, state: asset, action: action)]
|
|
179
234
|
end
|
|
235
|
+
dropped = states.filter_map do |name, (asset, action)|
|
|
236
|
+
next unless action&.do == :remove
|
|
237
|
+
|
|
238
|
+
[result_key(kind_of_file, name),
|
|
239
|
+
Installer::FileResult.new(word: @dry_run ? :would_remove : :removed, state: asset, action: action,
|
|
240
|
+
note: "no longer in the bundle")]
|
|
241
|
+
end
|
|
180
242
|
if @dry_run
|
|
181
243
|
results.each { |name, result| @results[result_key(kind_of_file, name)] = result }
|
|
244
|
+
dropped.each { |key, result| @results[key] = result }
|
|
182
245
|
return {}
|
|
183
246
|
end
|
|
184
247
|
|
|
185
248
|
states.each_value { |asset, action| @trash.copy(asset.path, as: asset.label) if action.save_first }
|
|
186
249
|
FileUtils.rm_rf(dir)
|
|
187
|
-
|
|
250
|
+
if incoming.empty?
|
|
251
|
+
dropped.each { |key, result| @results[key] = result }
|
|
252
|
+
return {}
|
|
253
|
+
end
|
|
188
254
|
|
|
189
255
|
FileUtils.mkdir_p(dir)
|
|
190
|
-
incoming.to_h do |name, src|
|
|
256
|
+
installed = incoming.to_h do |name, src|
|
|
191
257
|
dest = File.join(dir, name)
|
|
192
258
|
place(src, dest)
|
|
193
259
|
@results[result_key(kind_of_file, name)] = results[name]
|
|
@@ -198,6 +264,8 @@ module Samagotchi
|
|
|
198
264
|
end
|
|
199
265
|
[name, dest]
|
|
200
266
|
end
|
|
267
|
+
dropped.each { |key, result| @results[key] = result }
|
|
268
|
+
installed
|
|
201
269
|
end
|
|
202
270
|
|
|
203
271
|
# The kind's folder and the record's shas for it.
|
|
@@ -2,38 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module Samagotchi
|
|
4
4
|
module MemoryBundle
|
|
5
|
-
# One class-file hook's
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
# ("sha256:<hex>", "" when none is recorded), the events it registers
|
|
10
|
-
# for (manifest `event:` for one, `events:` a list; none: it isn't
|
|
11
|
-
# loaded), on_error (skip, log or fail_closed) and priority. The one
|
|
12
|
-
# place the defaults live.
|
|
13
|
-
class BundleHook < Data.define(:sha256, :events, :on_error, :priority)
|
|
14
|
-
DEFAULT_ON_ERROR = "skip"
|
|
15
|
-
DEFAULT_PRIORITY = 100
|
|
5
|
+
# One class-file hook's sha256 in an older chi's installed record; its
|
|
6
|
+
# other fields are ignored now that class-file hooks are retired.
|
|
7
|
+
BundleHook = Data.define(:sha256) do
|
|
8
|
+
def initialize(sha256: "") = super
|
|
16
9
|
|
|
17
|
-
# @param
|
|
18
|
-
#
|
|
19
|
-
# `events:`); `event:` and `events:` together are joined, blanks
|
|
20
|
-
# and repeats dropped
|
|
21
|
-
def initialize(sha256: "", event: nil, events: nil, on_error: DEFAULT_ON_ERROR, priority: DEFAULT_PRIORITY)
|
|
22
|
-
names = [*event, *events].map { |e| e.to_s.strip }.reject(&:empty?).uniq.freeze
|
|
23
|
-
super(sha256: sha256, events: names, on_error: on_error, priority: priority)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# @param raw [Hash, BundleHook, nil] string keys (manifest.yml) or
|
|
27
|
-
# symbol keys (a read record); missing or blank values get the
|
|
28
|
-
# defaults
|
|
10
|
+
# @param raw [Hash, BundleHook, nil] string or symbol keys; legacy
|
|
11
|
+
# metadata beyond sha256 is ignored
|
|
29
12
|
def self.parse(raw)
|
|
30
13
|
return raw if raw.is_a?(BundleHook)
|
|
31
14
|
|
|
32
15
|
h = raw.is_a?(Hash) ? raw.transform_keys(&:to_s) : {}
|
|
33
|
-
|
|
34
|
-
new(sha256: prefixed(h["sha256"]), event: h["event"], events: h["events"],
|
|
35
|
-
on_error: on_error.empty? ? DEFAULT_ON_ERROR : on_error,
|
|
36
|
-
priority: (h["priority"] || DEFAULT_PRIORITY).to_i)
|
|
16
|
+
new(sha256: prefixed(h["sha256"]))
|
|
37
17
|
end
|
|
38
18
|
|
|
39
19
|
# "sha256:<hex>" from either form, "" for none.
|
|
@@ -180,14 +180,13 @@ module Samagotchi
|
|
|
180
180
|
%i[missing any] => ASSET_GONE
|
|
181
181
|
}.freeze
|
|
182
182
|
|
|
183
|
-
# An upgrade (--force too) over the record: a file it replaces is
|
|
184
|
-
# updated, except a rule (an upgrade lists a replaced rule as installed:
|
|
185
|
-
# the corpus's rule-installed).
|
|
183
|
+
# An upgrade (--force too) over the record: a file it replaces is updated.
|
|
186
184
|
ASSET_UPGRADE = ASSET_INSTALL.merge(
|
|
187
185
|
%i[pristine any] => ASSET_WRITE.with(word: :updated),
|
|
188
186
|
%i[edited any] => ASSET_WRITE.with(word: :updated, save_first: true),
|
|
189
187
|
%i[linked any] => ASSET_WRITE.with(word: :updated),
|
|
190
|
-
%i[pristine rule] => ASSET_WRITE
|
|
188
|
+
%i[pristine rule] => ASSET_WRITE.with(word: :updated),
|
|
189
|
+
%i[edited rule] => ASSET_WRITE.with(word: :updated, save_first: true),
|
|
191
190
|
%i[linked rule] => ASSET_WRITE
|
|
192
191
|
).freeze
|
|
193
192
|
|
|
@@ -11,6 +11,10 @@ module Samagotchi
|
|
|
11
11
|
# the record's entry (R). Derived, never stored. FileActions says what each
|
|
12
12
|
# acting command does in each state.
|
|
13
13
|
#
|
|
14
|
+
# A file whose disk bytes are the text an interrupted run journaled
|
|
15
|
+
# (Provenance#write_pending) reads as the bundle's, at that run's
|
|
16
|
+
# version: owned (an entry made up if the record has none), its own base.
|
|
17
|
+
#
|
|
14
18
|
# kind, with a source (install, upgrade, remerge, restore, plan):
|
|
15
19
|
# :new not the bundle's, not on disk
|
|
16
20
|
# :user_same not the bundle's, on disk with I's bytes
|
|
@@ -82,6 +86,7 @@ module Samagotchi
|
|
|
82
86
|
# @return [FileState]
|
|
83
87
|
def of(key, record:, provenance:, target_dir:, source_dir:, claimants: nil, shipped: true)
|
|
84
88
|
entry = record&.files&.fetch(key, nil)
|
|
89
|
+
pending = provenance.pending_shas(key, target_dir: target_dir)
|
|
85
90
|
paths = { base: provenance.base_path(key), current: File.join(target_dir, key),
|
|
86
91
|
incoming: source_dir && File.join(source_dir, key) }
|
|
87
92
|
mark = mark_of(entry)
|
|
@@ -90,6 +95,12 @@ module Samagotchi
|
|
|
90
95
|
incoming: source_dir && shipped ? digest(paths[:incoming]) : nil, recorded: entry && Provenance.recorded_sha(entry[:checksum]),
|
|
91
96
|
marked_incoming: mark == :none ? nil : entry[:incoming_sha]
|
|
92
97
|
)
|
|
98
|
+
if !pending.empty? && pending.include?(digests.disk)
|
|
99
|
+
# An interrupted run put this text here (Provenance#write_pending): it is the bundle's at
|
|
100
|
+
# that version, owned and its own base, not the user's file or an edit of the recorded one.
|
|
101
|
+
entry = (entry || {}).merge(checksum: digests.disk)
|
|
102
|
+
digests = digests.with(base: digests.disk, recorded: digests.disk)
|
|
103
|
+
end
|
|
93
104
|
shared = nil
|
|
94
105
|
lookup = -> { shared ||= claimants ? claimants.call(key) : [] }
|
|
95
106
|
state = FileState.new(key: key, kind: nil, mark: mark, digests: digests, entry: entry, paths: paths, claimants: lookup)
|
|
@@ -14,8 +14,11 @@ module Samagotchi
|
|
|
14
14
|
# @param source [String, false, nil] the bundle the memory came from
|
|
15
15
|
# ("· from <bundle>"): a name sets it, false drops it, nil keeps the
|
|
16
16
|
# line's (memory_write and write/edit pass nil)
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
# @param target_dir [String, nil] the folder whose index.md to edit, when
|
|
18
|
+
# it is not the one +scope+ resolves from the current folder (an
|
|
19
|
+
# uninstall of a bundle journaled into another project)
|
|
20
|
+
def self.update_index(scope, entry_name, byte_count, description = nil, source: nil, target_dir: nil)
|
|
21
|
+
index_path = target_dir ? File.join(target_dir, "#{MEMORY_INDEX}.md") : index_path_for(scope)
|
|
19
22
|
return true unless index_path
|
|
20
23
|
|
|
21
24
|
locked_write(File.dirname(index_path)) do |content|
|
|
@@ -39,8 +42,8 @@ module Samagotchi
|
|
|
39
42
|
/^- \*\*#{Regexp.escape(name)}\*\*[ \t]*(?:[·•].*|:.*)?(\r?\n|\z)/
|
|
40
43
|
end
|
|
41
44
|
|
|
42
|
-
def self.remove_index(scope, entry_name)
|
|
43
|
-
index_path = index_path_for(scope)
|
|
45
|
+
def self.remove_index(scope, entry_name, target_dir: nil)
|
|
46
|
+
index_path = target_dir ? File.join(target_dir, "#{MEMORY_INDEX}.md") : index_path_for(scope)
|
|
44
47
|
return true unless index_path && File.exist?(index_path)
|
|
45
48
|
|
|
46
49
|
locked_write(File.dirname(index_path)) do |content|
|
|
@@ -26,7 +26,7 @@ module Samagotchi
|
|
|
26
26
|
# legacy_base: true on an older mark that has since got its incoming_sha (.legacy_mark?);
|
|
27
27
|
# why, missed, missed_places: FileStanding::Behind's, nil on an older mark;
|
|
28
28
|
# merged_version, merged_at, merged_trash: a merge no report has shown (Provenance#note_merged)
|
|
29
|
-
# hooks basename => BundleHook
|
|
29
|
+
# hooks basename => BundleHook (sha256; other legacy fields ignored)
|
|
30
30
|
# guardrails basename => recorded sha256 ("" when none)
|
|
31
31
|
# scripts file name => recorded sha256 ("" when none)
|
|
32
32
|
# plugin PluginRef, nil without one
|