samagotchi 0.106.0 → 0.108.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 +157 -1
- data/bin/chi +2 -0
- data/docs/architecture.md +6 -1
- data/docs/cli.md +45 -5
- data/docs/guardrails.md +8 -2
- data/docs/internals/engine.md +17 -6
- data/docs/internals/participants.md +129 -30
- data/docs/internals/remote-sessions.md +12 -5
- data/docs/internals/working-set.md +2 -1
- data/docs/memory.md +20 -0
- data/docs/plugins.md +38 -3
- data/docs/releasing.md +13 -6
- data/docs/sessions.md +39 -4
- data/docs/sub-agent.md +3 -1
- data/docs/testing.md +7 -1
- data/lib/samagotchi/bridge/turn_accumulator.rb +8 -1
- data/lib/samagotchi/bridge.rb +7 -2
- data/lib/samagotchi/bridge_client/stream_request.rb +10 -3
- data/lib/samagotchi/bundles/coordinator/manifest.yml +6 -5
- data/lib/samagotchi/bundles/coordinator/plugin.rb +85 -5
- data/lib/samagotchi/bundles/coordinator/skill_coordinator.md +18 -0
- data/lib/samagotchi/bundles/cost-cap/manifest.yml +2 -2
- data/lib/samagotchi/bundles/cost-cap/plugin.rb +51 -9
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/bundles/work-cap/manifest.yml +2 -2
- data/lib/samagotchi/bundles/work-cap/plugin.rb +50 -14
- data/lib/samagotchi/child_reports.rb +2 -1
- data/lib/samagotchi/cli/parent_wait.rb +1 -1
- data/lib/samagotchi/context_sources.rb +1 -6
- data/lib/samagotchi/continue_offer.rb +56 -7
- data/lib/samagotchi/engine.rb +36 -25
- data/lib/samagotchi/guardrail_wiring.rb +9 -6
- data/lib/samagotchi/guardrails/review_boundary.rb +6 -57
- data/lib/samagotchi/guardrails/review_paths.rb +93 -0
- data/lib/samagotchi/idle_scheduler.rb +17 -2
- data/lib/samagotchi/last_turn.rb +7 -0
- data/lib/samagotchi/log_subscriber.rb +2 -1
- data/lib/samagotchi/memory_orphans.rb +74 -0
- data/lib/samagotchi/memory_paths.rb +4 -4
- data/lib/samagotchi/model_profile.rb +5 -3
- data/lib/samagotchi/parent_report.rb +34 -4
- data/lib/samagotchi/participant_messages.rb +11 -0
- data/lib/samagotchi/participants.rb +20 -6
- data/lib/samagotchi/plugin/api.rb +2 -1
- data/lib/samagotchi/plugin/sessions.rb +25 -0
- data/lib/samagotchi/remote_command.rb +1 -1
- data/lib/samagotchi/remotes.rb +23 -8
- data/lib/samagotchi/reply_wait.rb +7 -2
- data/lib/samagotchi/review_apply.rb +123 -0
- data/lib/samagotchi/review_command.rb +121 -0
- data/lib/samagotchi/review_start.rb +161 -0
- data/lib/samagotchi/run_options.rb +17 -2
- data/lib/samagotchi/send_command/busy.rb +5 -2
- data/lib/samagotchi/send_command/wait_target.rb +18 -8
- data/lib/samagotchi/send_command.rb +2 -0
- data/lib/samagotchi/session.rb +33 -5
- data/lib/samagotchi/session_access/contract.rb +10 -2
- data/lib/samagotchi/session_access/dispatch.rb +8 -2
- data/lib/samagotchi/session_access/http_transport.rb +10 -1
- data/lib/samagotchi/session_access/local.rb +19 -2
- data/lib/samagotchi/session_access/remote.rb +15 -1
- data/lib/samagotchi/session_access/web_bridge_client.rb +1 -1
- data/lib/samagotchi/session_access.rb +23 -10
- data/lib/samagotchi/session_chain.rb +10 -2
- data/lib/samagotchi/session_continue.rb +4 -3
- data/lib/samagotchi/session_inbox.rb +12 -8
- data/lib/samagotchi/session_manager.rb +30 -8
- data/lib/samagotchi/session_metrics.rb +7 -6
- data/lib/samagotchi/session_participants.rb +3 -1
- data/lib/samagotchi/session_review.rb +28 -0
- data/lib/samagotchi/share_command.rb +7 -1
- data/lib/samagotchi/steer.rb +11 -1
- data/lib/samagotchi/terminal_ui/attached_loop.rb +34 -27
- data/lib/samagotchi/terminal_ui/formatting.rb +2 -2
- data/lib/samagotchi/terminal_ui/one_shot_sink.rb +5 -1
- data/lib/samagotchi/testing/sessions.rb +10 -2
- data/lib/samagotchi/tools/memory.rb +16 -9
- data/lib/samagotchi/turn_checkpoint.rb +13 -0
- data/lib/samagotchi/turn_origin.rb +68 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +108 -21
- data/lib/samagotchi/web/grants.rb +92 -17
- data/lib/samagotchi/web/participant_access.rb +56 -110
- data/lib/samagotchi/web/participant_review.rb +45 -2
- data/lib/samagotchi/web/participant_stream_filter.rb +118 -17
- data/lib/samagotchi/web/participant_view.rb +19 -14
- data/lib/samagotchi/web/participants_admin.rb +129 -0
- data/lib/samagotchi/web/public/app.js +60 -5
- data/lib/samagotchi/web/public/chip_popover.js +10 -3
- data/lib/samagotchi/web/public/data.js +10 -2
- data/lib/samagotchi/web/public/delivery_line.js +79 -15
- data/lib/samagotchi/web/public/index.html +26 -0
- data/lib/samagotchi/web/public/info_bar.js +7 -3
- data/lib/samagotchi/web/public/info_bar_model.js +7 -1
- data/lib/samagotchi/web/public/live_turn.js +3 -0
- data/lib/samagotchi/web/public/participant.html +58 -5
- data/lib/samagotchi/web/public/participant.js +172 -25
- data/lib/samagotchi/web/public/participant_model.js +89 -24
- data/lib/samagotchi/web/public/participant_names.js +37 -5
- data/lib/samagotchi/web/public/participants_panel.js +86 -18
- data/lib/samagotchi/web/public/question_cards.js +1 -0
- data/lib/samagotchi/web/public/review_apply.js +34 -0
- data/lib/samagotchi/web/public/review_start.js +101 -0
- data/lib/samagotchi/web/public/send_turn.js +25 -1
- data/lib/samagotchi/web/public/session_actions.js +44 -3
- data/lib/samagotchi/web/public/session_state.js +3 -0
- data/lib/samagotchi/web/public/start_page.js +38 -2
- data/lib/samagotchi/web/public/stream_handlers.js +10 -0
- data/lib/samagotchi/web/public/turn_html.js +2 -1
- data/lib/samagotchi/web/public/turn_view.js +5 -0
- data/lib/samagotchi/web/session_summary.rb +3 -0
- data/lib/samagotchi/worker.rb +44 -27
- data/lib/samagotchi/worker_inbound.rb +12 -9
- data/lib/samagotchi/working_set.rb +3 -2
- metadata +11 -1
|
@@ -63,6 +63,7 @@ class Plugin
|
|
|
63
63
|
@resolved = nil # {session:, settings:, meters: {meter => {cap:, step:, key:}}, unreadable: {name => value}}
|
|
64
64
|
@warned = {} # meter => [session id, cap] the share warning was given for
|
|
65
65
|
@unread_warned = nil # the session id the "isn't a number" notice was given for
|
|
66
|
+
@corrupt_warned = nil # the session id the "state file doesn't read" notice was given for
|
|
66
67
|
end
|
|
67
68
|
|
|
68
69
|
def register(chi)
|
|
@@ -126,6 +127,7 @@ class Plugin
|
|
|
126
127
|
def check(ctx)
|
|
127
128
|
resolved = resolve(ctx)
|
|
128
129
|
unreadable(ctx, resolved)
|
|
130
|
+
corrupt(ctx, resolved)
|
|
129
131
|
capped = resolved[:meters].select { |_meter, meter_cap| meter_cap[:cap] }
|
|
130
132
|
return if capped.empty?
|
|
131
133
|
|
|
@@ -161,11 +163,26 @@ class Plugin
|
|
|
161
163
|
return if bad.empty? || !first_time?(:@unread_warned, resolved[:session])
|
|
162
164
|
|
|
163
165
|
named = bad.map { |key, value| "#{key}: #{value.inspect}" }.join(" and ")
|
|
166
|
+
ctx.notify("work-cap: #{named} #{bad.size == 1 ? "isn't" : "aren't"} a number or off; #{using(resolved)}",
|
|
167
|
+
level: :warn)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Once per session: a state file that is there but doesn't read (not
|
|
171
|
+
# JSON, or not a mapping) holds no /workcap caps, so the settings' are
|
|
172
|
+
# used; said rather than a /workcap cap silently gone.
|
|
173
|
+
def corrupt(ctx, resolved)
|
|
174
|
+
path = resolved[:corrupt]
|
|
175
|
+
return unless path && first_time?(:@corrupt_warned, resolved[:session])
|
|
176
|
+
|
|
177
|
+
ctx.notify("work-cap: #{path} doesn't read, its /workcap caps are ignored; #{using(resolved)}", level: :warn)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# "using tool_calls: (400), minutes: (60)", or "no cap".
|
|
181
|
+
def using(resolved)
|
|
164
182
|
used = resolved[:meters].values.filter_map do |meter_cap|
|
|
165
183
|
"#{meter_cap[:key]}: (#{count(meter_cap[:cap])})" if meter_cap[:cap]
|
|
166
184
|
end
|
|
167
|
-
|
|
168
|
-
ctx.notify("work-cap: #{named} #{bad.size == 1 ? "isn't" : "aren't"} a number or off; #{using}", level: :warn)
|
|
185
|
+
used.empty? ? "no cap" : "using #{used.join(", ")}"
|
|
169
186
|
end
|
|
170
187
|
|
|
171
188
|
def at_cap(ctx, resolved, reached, usage, used)
|
|
@@ -280,29 +297,37 @@ class Plugin
|
|
|
280
297
|
cached = @mutex.synchronize { @resolved }
|
|
281
298
|
return cached if cached && cached[:session] == session && cached[:settings] == settings
|
|
282
299
|
|
|
283
|
-
saved = state(ctx, session)
|
|
300
|
+
saved, corrupt = state(ctx, session)
|
|
284
301
|
configured = configured(ctx)
|
|
285
302
|
meters = METERS.to_h { |meter| [meter, saved[meter] || configured[:meters][meter]] }
|
|
286
|
-
resolved = { session: session, settings: settings, meters: meters, unreadable: configured[:unreadable]
|
|
303
|
+
resolved = { session: session, settings: settings, meters: meters, unreadable: configured[:unreadable],
|
|
304
|
+
corrupt: corrupt }
|
|
287
305
|
@mutex.synchronize { @resolved = resolved }
|
|
288
306
|
end
|
|
289
307
|
|
|
290
|
-
# The state file's meters ({meter => {cap:, step:}}), {} without one
|
|
308
|
+
# The state file's meters ({meter => {cap:, step:}}), {} without one,
|
|
309
|
+
# and the file's path when it is there but doesn't read (logged; the
|
|
310
|
+
# user is told by #corrupt), else nil.
|
|
291
311
|
def state(ctx, session)
|
|
292
|
-
return {} unless session
|
|
312
|
+
return [{}, nil] unless session
|
|
293
313
|
|
|
294
|
-
|
|
295
|
-
|
|
314
|
+
path = state_path(ctx, session)
|
|
315
|
+
saved = JSON.parse(File.read(path))
|
|
316
|
+
raise JSON::ParserError, "not a JSON object" unless saved.is_a?(Hash)
|
|
296
317
|
|
|
297
|
-
METERS.each_with_object({}) do |meter,
|
|
318
|
+
meters = METERS.each_with_object({}) do |meter, found|
|
|
298
319
|
entry = saved[meter]
|
|
299
320
|
next unless entry.is_a?(Hash) && entry.key?("cap")
|
|
300
321
|
|
|
301
322
|
cap = self.class.amount(entry["cap"], meter)
|
|
302
|
-
|
|
323
|
+
found[meter] = { cap: cap, step: self.class.amount(entry["step"], meter) || cap, key: "/workcap" }
|
|
303
324
|
end
|
|
304
|
-
|
|
305
|
-
|
|
325
|
+
[meters, nil]
|
|
326
|
+
rescue Errno::ENOENT
|
|
327
|
+
[{}, nil]
|
|
328
|
+
rescue JSON::ParserError => e
|
|
329
|
+
ctx.log.warn(:workcap_state_unreadable, path: path, error: e.message.to_s[0, 200])
|
|
330
|
+
[{}, path]
|
|
306
331
|
end
|
|
307
332
|
|
|
308
333
|
# Per meter the first of its own value, child:'s (a delegate's) and
|
|
@@ -358,21 +383,32 @@ class Plugin
|
|
|
358
383
|
end
|
|
359
384
|
|
|
360
385
|
# Write one meter's cap into the session's state file (the other kept).
|
|
386
|
+
# One there that doesn't read (#state) is written over: logged and said,
|
|
387
|
+
# never a silent replace.
|
|
361
388
|
def save_meter(ctx, session, meter, data)
|
|
362
389
|
path = state_path(ctx, session)
|
|
363
390
|
FileUtils.mkdir_p(File.dirname(path))
|
|
364
391
|
saved = begin
|
|
365
392
|
JSON.parse(File.read(path))
|
|
366
|
-
rescue Errno::ENOENT
|
|
393
|
+
rescue Errno::ENOENT
|
|
367
394
|
{}
|
|
395
|
+
rescue JSON::ParserError
|
|
396
|
+
nil
|
|
368
397
|
end
|
|
369
|
-
|
|
398
|
+
replacing = !saved.is_a?(Hash)
|
|
399
|
+
saved = {} if replacing
|
|
370
400
|
saved[meter] = data.transform_values { |value| whole(value) }
|
|
371
401
|
Samagotchi::AtomicFile.write(path, "#{JSON.generate(saved)}\n")
|
|
402
|
+
replaced(ctx, path) if replacing
|
|
372
403
|
# Resolved again at the next use, from the file now holding the meter.
|
|
373
404
|
@mutex.synchronize { @resolved = nil }
|
|
374
405
|
end
|
|
375
406
|
|
|
407
|
+
def replaced(ctx, path)
|
|
408
|
+
ctx.log.warn(:workcap_state_replaced, path: path)
|
|
409
|
+
ctx.notify("work-cap: #{path} didn't read; replaced with the new cap", level: :warn)
|
|
410
|
+
end
|
|
411
|
+
|
|
376
412
|
# 800.0 as 800 (a whole cap is saved as an Integer), 2.5 as it is.
|
|
377
413
|
def whole(value) = value.is_a?(Float) && value.finite? && value == value.to_i ? value.to_i : value
|
|
378
414
|
|
|
@@ -7,6 +7,7 @@ require_relative "session"
|
|
|
7
7
|
require_relative "session_inbox"
|
|
8
8
|
require_relative "reply_wait"
|
|
9
9
|
require_relative "steer"
|
|
10
|
+
require_relative "turn_origin"
|
|
10
11
|
require_relative "log"
|
|
11
12
|
require_relative "config"
|
|
12
13
|
require_relative "tools/delegate_wait"
|
|
@@ -138,7 +139,7 @@ module Samagotchi
|
|
|
138
139
|
def initialize(relay: nil, **) = super
|
|
139
140
|
|
|
140
141
|
def line(mark: nil) = Steer::Line.new(text: text, source: ChildReports::SOURCE, mark: mark)
|
|
141
|
-
def origin =
|
|
142
|
+
def origin = TurnOrigin.new(client_id: "#{ClientId::CHILD_PREFIX}#{child_id[0, 8]}")
|
|
142
143
|
end
|
|
143
144
|
|
|
144
145
|
# @param predecessor [String, nil] the link this session continues
|
|
@@ -96,7 +96,7 @@ module Samagotchi
|
|
|
96
96
|
@stdout.flush
|
|
97
97
|
@reported = true
|
|
98
98
|
end
|
|
99
|
-
error_line("#{command_name}: still running: #{@access.invocation
|
|
99
|
+
error_line("#{command_name}: still running: #{ParentReport.still_running_hint(id, report_turn_id, via: @access.invocation)}")
|
|
100
100
|
Exit::INTERRUPTED
|
|
101
101
|
rescue ArgumentError
|
|
102
102
|
error_line("#{command_name}: the session is gone (deleted while waiting)")
|
|
@@ -359,12 +359,7 @@ module Samagotchi
|
|
|
359
359
|
def project_location_for(project_root, state_dir: nil)
|
|
360
360
|
return nil if project_root.to_s.empty?
|
|
361
361
|
|
|
362
|
-
project_location(
|
|
363
|
-
end
|
|
364
|
-
|
|
365
|
-
# MemoryPaths.project_key for a root already resolved.
|
|
366
|
-
def project_key_of(root)
|
|
367
|
-
"#{File.basename(root)}_#{Digest::MD5.hexdigest(root)[0..7]}"
|
|
362
|
+
project_location(MemoryPaths.key_for_root(project_root), state_dir: state_dir)
|
|
368
363
|
end
|
|
369
364
|
|
|
370
365
|
# A session's sources: its own, then the project's it doesn't shadow
|
|
@@ -4,6 +4,7 @@ require "time"
|
|
|
4
4
|
require_relative "last_turn"
|
|
5
5
|
require_relative "log"
|
|
6
6
|
require_relative "steer"
|
|
7
|
+
require_relative "turn_origin"
|
|
7
8
|
|
|
8
9
|
module Samagotchi
|
|
9
10
|
# A session worker's continue offer: what it announces when a turn runs out
|
|
@@ -35,10 +36,13 @@ module Samagotchi
|
|
|
35
36
|
# @param max_iterations [#call] (no_interrupt) → Integer
|
|
36
37
|
# @param queue_command [#call] (line, client_id:) queues a session
|
|
37
38
|
# command as a card's (Bridge#queue_command, card: true)
|
|
38
|
-
# +participant_refused+ (author →
|
|
39
|
-
# turn may no longer run (revoked,
|
|
40
|
-
|
|
39
|
+
# +participant_refused+ (author → reason or nil): why a participant's
|
|
40
|
+
# turn may no longer run ("revoked", "cap_reached": Worker's check).
|
|
41
|
+
# @param save [#call] saves the session (a refused Continue's Stop)
|
|
42
|
+
def initialize(engine:, turn_flow:, run_turn:, max_iterations:, queue_command: nil, participant_refused: ->(_) {},
|
|
43
|
+
save: -> {})
|
|
41
44
|
@engine = engine
|
|
45
|
+
@save = save
|
|
42
46
|
@turn_flow = turn_flow
|
|
43
47
|
@run_turn = run_turn
|
|
44
48
|
@max_iterations = max_iterations
|
|
@@ -68,6 +72,7 @@ module Samagotchi
|
|
|
68
72
|
|
|
69
73
|
# A prompt (or a reminder turn) taken while a continue is offered
|
|
70
74
|
# replaces the answer (D2): the offer goes, the partial turn stays.
|
|
75
|
+
# @param origin [TurnOrigin] the taken turn's
|
|
71
76
|
def drop(origin)
|
|
72
77
|
return unless awaiting?
|
|
73
78
|
|
|
@@ -75,7 +80,7 @@ module Samagotchi
|
|
|
75
80
|
close("dropped")
|
|
76
81
|
@engine.synchronize_events do
|
|
77
82
|
@turn_flow.drop_offer!
|
|
78
|
-
@engine.announce(type: :continue_resolved, decision: "dropped", client_id: origin
|
|
83
|
+
@engine.announce(type: :continue_resolved, decision: "dropped", client_id: origin.client_id)
|
|
79
84
|
end
|
|
80
85
|
@engine.record_activity
|
|
81
86
|
end
|
|
@@ -91,7 +96,14 @@ module Samagotchi
|
|
|
91
96
|
# it ends (chi answer --option Stop).
|
|
92
97
|
# It carries the offer turn's enqueued_id, so a wait on that turn
|
|
93
98
|
# (ReplyWait's turn:) ends on it too.
|
|
99
|
+
# A Continue on the offer of a participant who may no longer run a turn
|
|
100
|
+
# (revoked, expired, at the cap since) is decided here, before anything
|
|
101
|
+
# says "resume": it ends the offer as a Stop does (#refuse_continue).
|
|
94
102
|
def announce_resolved(result, command)
|
|
103
|
+
if result.resume && (reason = refusal_of_offer_author)
|
|
104
|
+
return refuse_continue(reason, command)
|
|
105
|
+
end
|
|
106
|
+
|
|
95
107
|
@engine.announce(type: :continue_resolved, decision: result.decision.to_s, client_id: command.client_id)
|
|
96
108
|
return if result.resume
|
|
97
109
|
|
|
@@ -133,12 +145,18 @@ module Samagotchi
|
|
|
133
145
|
# review boundary and on their spend (never as the owner's). One whose
|
|
134
146
|
# participant may no longer run a turn isn't continued.
|
|
135
147
|
def run_continue_turn(command)
|
|
148
|
+
# Refused as it was answered (#announce_resolved): the offer is gone.
|
|
149
|
+
return @continue_steer = nil unless awaiting?
|
|
150
|
+
|
|
136
151
|
offer = @turn_flow.offer
|
|
137
152
|
resume_from = @turn_flow.resume_from
|
|
138
153
|
enqueued_id = offer_turn_id
|
|
139
|
-
author =
|
|
154
|
+
author = @engine.session&.last_turn&.participant_author
|
|
140
155
|
close("answered")
|
|
141
|
-
|
|
156
|
+
if author && (reason = @participant_refused.call(author))
|
|
157
|
+
@continue_steer = nil
|
|
158
|
+
return @engine.synchronize_events { refuse_continue(reason, command) }
|
|
159
|
+
end
|
|
142
160
|
|
|
143
161
|
steer = @continue_steer
|
|
144
162
|
@continue_steer = nil
|
|
@@ -150,7 +168,7 @@ module Samagotchi
|
|
|
150
168
|
Log.warn(:worker, "turn_not_begun", continue: true, error: e.class.name, msg: e.message)
|
|
151
169
|
return announce_offer(offer)
|
|
152
170
|
end
|
|
153
|
-
origin =
|
|
171
|
+
origin = TurnOrigin.new(client_id: command.client_id, enqueued_id: enqueued_id, author: author)
|
|
154
172
|
@run_turn.call(nil, continue: true, origin: origin, resumed: true, resume_from: resume_from,
|
|
155
173
|
max_iterations: @max_iterations.call(offer[:no_interrupt])) do |result, error|
|
|
156
174
|
if error
|
|
@@ -175,6 +193,37 @@ module Samagotchi
|
|
|
175
193
|
|
|
176
194
|
private
|
|
177
195
|
|
|
196
|
+
def refusal_of_offer_author
|
|
197
|
+
author = @engine.session&.last_turn&.participant_author
|
|
198
|
+
author && @participant_refused.call(author)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# The offer of a participant refused since (+reason+: revoked,
|
|
202
|
+
# cap_reached) ends as a Stop: announced as "refused" (with a notice
|
|
203
|
+
# for the owner), the model reads it wasn't continued, last_turn says
|
|
204
|
+
# not_continued with the offer turn's enqueued_id (a wait on it ends),
|
|
205
|
+
# and awaiting_continue? goes false (idle exit, /exit, the held
|
|
206
|
+
# participant turns go on). The question goes with the next close.
|
|
207
|
+
# Called with the event log held.
|
|
208
|
+
def refuse_continue(reason, command)
|
|
209
|
+
author = @engine.session&.last_turn&.participant_author
|
|
210
|
+
enqueued_id = offer_turn_id
|
|
211
|
+
@engine.announce(type: :continue_resolved, decision: "refused", reason: reason.to_s, client_id: command.client_id)
|
|
212
|
+
@engine.announce(type: :hook_notice, hook: "participants", level: :warn, between_turns: true,
|
|
213
|
+
text: format(REFUSED_NOTICE, who: Steer.participant_id(author), reason: REFUSED_WHY.fetch(reason.to_s, reason.to_s)))
|
|
214
|
+
@turn_flow.abort_continue!
|
|
215
|
+
session = @engine.session
|
|
216
|
+
session.last_turn = LastTurn.new(outcome: NOT_CONTINUED, ended_at: Time.now.iso8601(3), enqueued_id: enqueued_id) if session
|
|
217
|
+
@continue_steer = nil
|
|
218
|
+
close("answered")
|
|
219
|
+
@save.call
|
|
220
|
+
nil
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
REFUSED_NOTICE = "Not continued: participant %<who>s's turn %<reason>s."
|
|
224
|
+
REFUSED_WHY = { "revoked" => "is refused (their link was revoked or expired)",
|
|
225
|
+
"cap_reached" => "is refused (they reached their review cap)" }.freeze
|
|
226
|
+
|
|
178
227
|
# The enqueued_id of the turn the offer stands for: the exhausted
|
|
179
228
|
# turn's last_turn (a canceled continue turn's carries the same id);
|
|
180
229
|
# nil when it ran no input (a reminder turn, a delegate's first).
|
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -13,6 +13,7 @@ require_relative "context_note"
|
|
|
13
13
|
require_relative "context_status"
|
|
14
14
|
require_relative "steer"
|
|
15
15
|
require_relative "turn_note"
|
|
16
|
+
require_relative "turn_origin"
|
|
16
17
|
require_relative "model_profile"
|
|
17
18
|
require_relative "effective_model"
|
|
18
19
|
require_relative "session_setting"
|
|
@@ -57,6 +58,7 @@ require_relative "hook_routes"
|
|
|
57
58
|
require_relative "guardrails"
|
|
58
59
|
require_relative "reminder_store"
|
|
59
60
|
require_relative "reminder_queue"
|
|
61
|
+
require_relative "memory_orphans"
|
|
60
62
|
require_relative "tools/memory"
|
|
61
63
|
require_relative "muted_memories"
|
|
62
64
|
require_relative "used_memories"
|
|
@@ -623,7 +625,7 @@ module Samagotchi
|
|
|
623
625
|
def side_author(kind)
|
|
624
626
|
return nil unless turn_running? || kind.to_s == RECAP_SIDE_KIND
|
|
625
627
|
|
|
626
|
-
|
|
628
|
+
@guardrail_wiring.turn_author
|
|
627
629
|
end
|
|
628
630
|
|
|
629
631
|
RECAP_SIDE_KIND = "recap"
|
|
@@ -631,7 +633,7 @@ module Samagotchi
|
|
|
631
633
|
# The running turn's author when a participant wrote it, else nil
|
|
632
634
|
# (ctx.turn_author, the hooks' author).
|
|
633
635
|
# @return [String, nil]
|
|
634
|
-
def turn_author = turn_running? ?
|
|
636
|
+
def turn_author = turn_running? ? @guardrail_wiring.turn_author : nil
|
|
635
637
|
|
|
636
638
|
# @return [SessionMetrics] the per-session analytics collector
|
|
637
639
|
attr_reader :metrics
|
|
@@ -984,7 +986,8 @@ module Samagotchi
|
|
|
984
986
|
|
|
985
987
|
# Announce what failed to load and what plugins showed while loading,
|
|
986
988
|
# once, as soon as the owner can show it (the worker's Bridge is up, the
|
|
987
|
-
# REPL renders events): the guardrail and plugin warnings,
|
|
989
|
+
# REPL renders events): the guardrail and plugin warnings, an orphaned
|
|
990
|
+
# project memory folder (label: "memory", MemoryOrphans), then the
|
|
988
991
|
# held notices (between_turns) and cards (in_turn: false, so late
|
|
989
992
|
# joiners get them). Without this call the first turn announces them
|
|
990
993
|
# (#announce_guardrail_failures).
|
|
@@ -997,6 +1000,8 @@ module Samagotchi
|
|
|
997
1000
|
announce({ type: :guardrail_warning, message: message }) if message
|
|
998
1001
|
plugins = @plugin_failures.message
|
|
999
1002
|
announce({ type: :guardrail_warning, message: plugins, label: "plugins" }) if plugins
|
|
1003
|
+
memory = MemoryOrphans.message
|
|
1004
|
+
announce({ type: :guardrail_warning, message: memory, label: "memory" }) if memory
|
|
1000
1005
|
@extension_load.held_events.each do |event|
|
|
1001
1006
|
announce(event[:type] == :card ? event.merge(in_turn: false) : event.merge(between_turns: true))
|
|
1002
1007
|
end
|
|
@@ -1448,8 +1453,8 @@ module Samagotchi
|
|
|
1448
1453
|
|
|
1449
1454
|
# Once per Engine, on its first turn: what failed to load, so every UI
|
|
1450
1455
|
# (REPL, attached TUI, web) shows it: the guardrails, then the plugins
|
|
1451
|
-
# (label: "plugins"; the UIs say guardrails without one),
|
|
1452
|
-
# notices and cards plugins showed as they loaded.
|
|
1456
|
+
# (label: "plugins"; the UIs say guardrails without one), an orphaned
|
|
1457
|
+
# project memory folder (label: "memory"), then the notices and cards plugins showed as they loaded.
|
|
1453
1458
|
def announce_guardrail_failures(on_event)
|
|
1454
1459
|
return if @guardrail_failures_announced
|
|
1455
1460
|
|
|
@@ -1458,6 +1463,8 @@ module Samagotchi
|
|
|
1458
1463
|
emit_event(on_event, { type: :guardrail_warning, message: message }) if message
|
|
1459
1464
|
plugins = @plugin_failures.message
|
|
1460
1465
|
emit_event(on_event, { type: :guardrail_warning, message: plugins, label: "plugins" }) if plugins
|
|
1466
|
+
memory = MemoryOrphans.message
|
|
1467
|
+
emit_event(on_event, { type: :guardrail_warning, message: memory, label: "memory" }) if memory
|
|
1461
1468
|
@extension_load.held_events.each { |event| emit_event(on_event, event) }
|
|
1462
1469
|
end
|
|
1463
1470
|
private :announce_guardrail_failures
|
|
@@ -1494,14 +1501,14 @@ module Samagotchi
|
|
|
1494
1501
|
# @param limit [Integer, nil] the turn ran out of iterations at this
|
|
1495
1502
|
# limit: "exhausted" => true, "limit" => N, so a wait with nobody to
|
|
1496
1503
|
# answer the continue offer says so (ReplyWait, ParentReport "limit")
|
|
1504
|
+
# @param origin [TurnOrigin] the turn's (TurnOrigin::NONE for no sender)
|
|
1497
1505
|
# @param event [Hash] the turn's end event (#stop_facts)
|
|
1498
1506
|
def record_last_turn(session, outcome, seconds, origin, event, limit: nil)
|
|
1499
|
-
|
|
1500
|
-
enqueued_id = origin.is_a?(Hash) ? origin[:enqueued_id] : nil
|
|
1507
|
+
enqueued_id = origin.enqueued_id
|
|
1501
1508
|
session.last_turn = LastTurn.new(outcome: outcome, ended_at: Time.now.iso8601(3), seconds: seconds.round(1),
|
|
1502
|
-
origin: source, exhausted: limit ? true : nil, limit: limit,
|
|
1509
|
+
origin: origin.source, exhausted: limit ? true : nil, limit: limit,
|
|
1503
1510
|
enqueued_id: enqueued_id.to_s.empty? ? nil : enqueued_id.to_s,
|
|
1504
|
-
author:
|
|
1511
|
+
author: origin.participant_author,
|
|
1505
1512
|
**stop_facts(event))
|
|
1506
1513
|
end
|
|
1507
1514
|
|
|
@@ -1532,10 +1539,9 @@ module Samagotchi
|
|
|
1532
1539
|
# author:, and author_name: from participants.json), {} for the owner.
|
|
1533
1540
|
# The worker's drain names a participant's merged lines with it, and
|
|
1534
1541
|
# its running save a participant's prompt.
|
|
1535
|
-
# @param
|
|
1542
|
+
# @param author [String, nil] a turn's or a line's author (TurnOrigin#author)
|
|
1536
1543
|
# @param session [Session, nil] the session it is for (the turn's)
|
|
1537
|
-
def author_fields(
|
|
1538
|
-
author = origin.is_a?(Hash) ? origin[:author] : origin
|
|
1544
|
+
def author_fields(author, session: @session)
|
|
1539
1545
|
return {} unless Steer.participant_author?(author)
|
|
1540
1546
|
|
|
1541
1547
|
Steer.author_fields(author, @participants.name_for(author, session: session))
|
|
@@ -1995,9 +2001,11 @@ module Samagotchi
|
|
|
1995
2001
|
:settings, :id, :limit, :resumed, :resume_from) do
|
|
1996
2002
|
def resumed? = resumed ? true : false
|
|
1997
2003
|
|
|
1998
|
-
# The turn's boundary events carry the origin only when
|
|
1999
|
-
# so payloads stay unchanged for callers
|
|
2000
|
-
|
|
2004
|
+
# The turn's boundary events carry the origin only when it names a
|
|
2005
|
+
# sender (not TurnOrigin::NONE), so payloads stay unchanged for callers
|
|
2006
|
+
# that don't pass one; as its wire Hash (TurnOrigin#to_h), which the
|
|
2007
|
+
# Bridge and the UIs read.
|
|
2008
|
+
def tag(event) = origin.empty? ? event : event.merge(origin: origin.to_h)
|
|
2001
2009
|
|
|
2002
2010
|
# Seconds since the turn started (the cancel note says how long it ran).
|
|
2003
2011
|
def elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
|
|
@@ -2032,11 +2040,13 @@ module Samagotchi
|
|
|
2032
2040
|
# run it resumes started (ModelResult#run_start, kept in the offer by
|
|
2033
2041
|
# TurnFlow), so it keeps that run's reasoning; nil starts cold (a
|
|
2034
2042
|
# model change while the offer stood, every other turn).
|
|
2035
|
-
# @param origin [Hash, nil] who queued the turn (
|
|
2036
|
-
# and author
|
|
2037
|
-
# its input file;
|
|
2038
|
-
#
|
|
2039
|
-
#
|
|
2043
|
+
# @param origin [TurnOrigin, Hash, nil] who queued the turn (client_id,
|
|
2044
|
+
# enqueued_id, and author when someone other than the session's owner
|
|
2045
|
+
# wrote it, from its input file; none for the owner; a Hash is read as
|
|
2046
|
+
# one, TurnOrigin.of; TurnOrigin::NONE, the default, names no sender
|
|
2047
|
+
# and so does nil or {}); when it names one, the turn's boundary events
|
|
2048
|
+
# (:turn_started, :turn_completed, :turn_canceled, :turn_failed) carry
|
|
2049
|
+
# it as `origin:` (its wire Hash), and GuardrailWiring#begin_turn gets it
|
|
2040
2050
|
# @param images [Array<Hash>] the prompt's images: {path:} (a file on this
|
|
2041
2051
|
# machine; in-process and attached-TUI callers only) or {file:, name:}
|
|
2042
2052
|
# (already in the session's images/, e.g. a web upload). They are
|
|
@@ -2056,8 +2066,8 @@ module Samagotchi
|
|
|
2056
2066
|
# other error (e.g. an LLM::ProviderError) emits :turn_failed and
|
|
2057
2067
|
# re-raises; a provider error adds error_kind:, retryable:, host: and a
|
|
2058
2068
|
# one-line summary:.
|
|
2059
|
-
def run_turn(session, prompt, on_event: nil, max_iterations: nil, cancel_controller: nil, max_tool_output_chars: nil, pending_input: nil, continue: false,
|
|
2060
|
-
images: [], id: nil, resumed: false, resume_from: nil)
|
|
2069
|
+
def run_turn(session, prompt, on_event: nil, max_iterations: nil, cancel_controller: nil, max_tool_output_chars: nil, pending_input: nil, continue: false,
|
|
2070
|
+
origin: TurnOrigin::NONE, images: [], id: nil, resumed: false, resume_from: nil)
|
|
2061
2071
|
turn = begin_turn(session, prompt, on_event: on_event, cancel_controller: cancel_controller, origin: origin,
|
|
2062
2072
|
continue: continue, id: id)
|
|
2063
2073
|
if continue
|
|
@@ -2108,6 +2118,7 @@ module Samagotchi
|
|
|
2108
2118
|
# the turn flag, its cancel controller and sink. The cross-thread state
|
|
2109
2119
|
# is set here and cleared in #release_turn only.
|
|
2110
2120
|
def begin_turn(session, prompt, on_event:, cancel_controller:, origin:, continue:, id: nil)
|
|
2121
|
+
origin = TurnOrigin.of(origin)
|
|
2111
2122
|
# Track the active session for recap and status snapshot.
|
|
2112
2123
|
@session = session
|
|
2113
2124
|
# status is turn state: running now, idle again before the turn's end
|
|
@@ -2151,7 +2162,7 @@ module Samagotchi
|
|
|
2151
2162
|
turn_started[:cut_after] = steer_cut_after
|
|
2152
2163
|
# A participant's turn names them (author, author_name) for the UIs'
|
|
2153
2164
|
# badge; the owner's turn_started stays as it was.
|
|
2154
|
-
turn_started.merge!(author_fields(turn.origin, session: session))
|
|
2165
|
+
turn_started.merge!(author_fields(turn.origin.author, session: session))
|
|
2155
2166
|
emit_event(turn.on_event, turn.tag(turn_started))
|
|
2156
2167
|
raise image_error if image_error
|
|
2157
2168
|
|
|
@@ -2305,7 +2316,7 @@ module Samagotchi
|
|
|
2305
2316
|
# system prompt invalidates cache for the entire prefix.
|
|
2306
2317
|
# Not into a participant's turn: the owner's reminders are the owner's,
|
|
2307
2318
|
# and would be used up there under the review boundary.
|
|
2308
|
-
due_reminders =
|
|
2319
|
+
due_reminders = turn.origin.participant? ? [] : @reminder_queue.inject!(turn.messages)
|
|
2309
2320
|
if due_reminders.any?
|
|
2310
2321
|
emit_event(turn.on_event, {
|
|
2311
2322
|
type: :reminder_injected,
|
|
@@ -2314,7 +2325,7 @@ module Samagotchi
|
|
|
2314
2325
|
end
|
|
2315
2326
|
|
|
2316
2327
|
unless turn.continue
|
|
2317
|
-
user_message = { role: "user", content: turn.prompt, turn_id: turn.id, **author_fields(turn.origin) }
|
|
2328
|
+
user_message = { role: "user", content: turn.prompt, turn_id: turn.id, **author_fields(turn.origin.author) }
|
|
2318
2329
|
user_message[:images] = image_refs unless image_refs.empty?
|
|
2319
2330
|
turn.messages << user_message
|
|
2320
2331
|
session.last_prompt = turn.prompt
|
|
@@ -11,6 +11,7 @@ require_relative "version"
|
|
|
11
11
|
require_relative "hooks"
|
|
12
12
|
require_relative "edit_preview"
|
|
13
13
|
require_relative "tool_activity"
|
|
14
|
+
require_relative "turn_origin"
|
|
14
15
|
|
|
15
16
|
module Samagotchi
|
|
16
17
|
# An Engine's tool guardrails: who can approve (the interface), the YAML
|
|
@@ -55,6 +56,7 @@ module Samagotchi
|
|
|
55
56
|
@failures = Guardrails::LoadFailures.new
|
|
56
57
|
@rules_mutex = Mutex.new
|
|
57
58
|
@git = Guardrails::GitInfo.new
|
|
59
|
+
@origin = TurnOrigin::NONE
|
|
58
60
|
end
|
|
59
61
|
|
|
60
62
|
# Who can answer an approval: :repl, :worker or :non_interactive (the
|
|
@@ -83,17 +85,18 @@ module Samagotchi
|
|
|
83
85
|
@protected = nil
|
|
84
86
|
end
|
|
85
87
|
|
|
86
|
-
# A turn starts: its origin (Engine#run_turn's
|
|
87
|
-
#
|
|
88
|
+
# A turn starts: its origin (Engine#run_turn's TurnOrigin; nil or a
|
|
89
|
+
# Hash is read as one, TurnOrigin.of), and the git facts read afresh.
|
|
88
90
|
def begin_turn(origin)
|
|
89
|
-
@origin = origin
|
|
91
|
+
@origin = TurnOrigin.of(origin)
|
|
90
92
|
@git = Guardrails::GitInfo.new
|
|
91
93
|
end
|
|
92
94
|
|
|
93
95
|
# Who wrote the running turn when it isn't the session's owner (its
|
|
94
|
-
# origin's author); nil for the owner's turns and every
|
|
96
|
+
# origin's participant author); nil for the owner's turns and every
|
|
97
|
+
# other origin.
|
|
95
98
|
# @return [String, nil]
|
|
96
|
-
def turn_author = @origin.
|
|
99
|
+
def turn_author = @origin.participant_author
|
|
97
100
|
|
|
98
101
|
# The gate every tool call asks first.
|
|
99
102
|
# @return [Guardrails::Gate]
|
|
@@ -229,7 +232,7 @@ module Samagotchi
|
|
|
229
232
|
# @return [Guardrails::Context]
|
|
230
233
|
def context
|
|
231
234
|
Guardrails::Context.new(cwd: Dir.pwd, session_id: @session_lookup.call&.id, interface: interface,
|
|
232
|
-
origin: @origin, git: @git)
|
|
235
|
+
origin: @origin.wire, git: @git)
|
|
233
236
|
end
|
|
234
237
|
|
|
235
238
|
# Ask the user to approve a call the gate voted `ask` on, through the
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "
|
|
4
|
-
require_relative "shell_paths"
|
|
5
|
-
require_relative "../paths"
|
|
3
|
+
require_relative "review_paths"
|
|
6
4
|
require_relative "../toolkit"
|
|
7
5
|
require_relative "../steer"
|
|
8
6
|
|
|
@@ -18,7 +16,7 @@ module Samagotchi
|
|
|
18
16
|
# (Toolkit::REVIEW_TOOLS). Not forget_outputs: it would replace the
|
|
19
17
|
# owner's earlier tool outputs with a note of the participant's, which
|
|
20
18
|
# later owner turns read as their own output, with no review header.
|
|
21
|
-
# read: only under the session's project root,
|
|
19
|
+
# read: ReviewPaths: only under the session's project root, never a file of
|
|
22
20
|
# the read denylist (chi's state and config dirs, home secret dirs,
|
|
23
21
|
# project secret files), checked on the path as given and as it
|
|
24
22
|
# resolves (a symlink in the project to ~/.ssh is ~/.ssh);
|
|
@@ -35,19 +33,6 @@ module Samagotchi
|
|
|
35
33
|
ALLOWED = Toolkit::REVIEW_TOOLS
|
|
36
34
|
PROJECT_SCOPE = "project"
|
|
37
35
|
|
|
38
|
-
# Home folders that hold keys and tokens, never read in a review turn.
|
|
39
|
-
HOME_SECRET_DIRS = %w[.ssh .gnupg .aws].freeze
|
|
40
|
-
# Project files that hold secrets, by a path part's name: .env*,
|
|
41
|
-
# netrc, keys and certificates, anything named credentials, package
|
|
42
|
-
# registry tokens (.npmrc, .pypirc), ssh keys, terraform state, a
|
|
43
|
-
# secrets file; and a git config (.git/config, a submodule's
|
|
44
|
-
# .git/modules/x/config), checked apart.
|
|
45
|
-
SECRET_FILE = /\A(?:\.env.*|[._]netrc|.*\.(?:pem|key|p12|pfx|tfstate(?:\.backup)?)|.*credentials.*|
|
|
46
|
-
\.npmrc|\.pypirc|\.git-credentials|id_(?:rsa|dsa|ecdsa|ed25519)(?:\.pub)?|secrets?\.(?:ya?ml|json))\z/xi
|
|
47
|
-
# The .docker/config.json kind: a dot dir's config holding tokens.
|
|
48
|
-
SECRET_DIR_FILES = { ".docker" => "config.json", ".kube" => "config" }.freeze
|
|
49
|
-
GIT_DIR = ".git"
|
|
50
|
-
|
|
51
36
|
ADVICE = "This is a participant's review turn: answer from what you can read and propose any change as " \
|
|
52
37
|
"text or a diff for the owner to apply."
|
|
53
38
|
NOT_IN_KIT = "a participant's review turn may only read (read, memory_read): %s is not allowed"
|
|
@@ -55,6 +40,7 @@ module Samagotchi
|
|
|
55
40
|
SECRET = "a participant's review turn can't read secrets or chi's own files"
|
|
56
41
|
HOME_ROOT = "a participant's review turn reads only inside a project, and this session's folder is the " \
|
|
57
42
|
"home folder or above it"
|
|
43
|
+
READ_REFUSALS = { ReviewPaths::OUTSIDE => OUTSIDE, ReviewPaths::SECRET => SECRET, ReviewPaths::HOME_ROOT => HOME_ROOT }.freeze
|
|
58
44
|
MEMORY_SCOPE = "a participant's review turn reads project memories only: call memory_read with scope: project"
|
|
59
45
|
|
|
60
46
|
# @param author [#call] → String, nil: the running turn's author
|
|
@@ -66,16 +52,13 @@ module Samagotchi
|
|
|
66
52
|
# config: sessions, grants, the web token, memories and .bundles,
|
|
67
53
|
# config.yml, hooks)
|
|
68
54
|
# @param home [#call] → String, nil
|
|
69
|
-
def initialize(author:, root:, denied_dirs: -> {
|
|
55
|
+
def initialize(author:, root:, denied_dirs: -> { ReviewPaths.chi_dirs }, home: -> { Dir.home })
|
|
70
56
|
@author = author
|
|
71
57
|
@root = root
|
|
72
58
|
@denied_dirs = denied_dirs
|
|
73
59
|
@home = home
|
|
74
60
|
end
|
|
75
61
|
|
|
76
|
-
# chi's state dir and config dir, as Paths has them now.
|
|
77
|
-
def self.chi_dirs = [Paths.state_dir, File.join(Paths.config_home, Paths::APP_DIR)]
|
|
78
|
-
|
|
79
62
|
# Vote on +verdict+ (its call and targets).
|
|
80
63
|
def check(verdict)
|
|
81
64
|
return verdict unless Steer.participant_author?(@author.call)
|
|
@@ -106,42 +89,8 @@ module Samagotchi
|
|
|
106
89
|
end
|
|
107
90
|
|
|
108
91
|
def read_refusal(paths)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
root = @root.call.to_s
|
|
112
|
-
return OUTSIDE if root.empty?
|
|
113
|
-
|
|
114
|
-
real_root = ProtectedPaths.real(root)
|
|
115
|
-
home = @home.call.to_s
|
|
116
|
-
return HOME_ROOT if !home.empty? && ShellPaths.within?(ProtectedPaths.real(home), real_root)
|
|
117
|
-
|
|
118
|
-
paths.each do |path|
|
|
119
|
-
given = File.expand_path(path)
|
|
120
|
-
real = ProtectedPaths.real(given)
|
|
121
|
-
return OUTSIDE unless ShellPaths.within?(real, real_root)
|
|
122
|
-
return SECRET if secret?(given, home) || secret?(real, home)
|
|
123
|
-
end
|
|
124
|
-
nil
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def secret?(path, home)
|
|
128
|
-
dirs = @denied_dirs.call.compact.map { |dir| File.expand_path(dir) }
|
|
129
|
-
dirs += dirs.map { |dir| ProtectedPaths.real(dir) }
|
|
130
|
-
dirs += HOME_SECRET_DIRS.map { |dir| File.join(home, dir) } unless home.empty?
|
|
131
|
-
return true if dirs.any? { |dir| ShellPaths.within?(path, dir) }
|
|
132
|
-
|
|
133
|
-
parts = path.split(File::SEPARATOR)
|
|
134
|
-
return true if parts.any? { |part| SECRET_FILE.match?(part) }
|
|
135
|
-
return true if parts.each_cons(2).any? { |dir, file| SECRET_DIR_FILES[dir.downcase] == file.downcase }
|
|
136
|
-
|
|
137
|
-
git_config?(parts)
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
# A config file anywhere under a .git dir (.git/config,
|
|
141
|
-
# .git/modules/<sub>/config, a worktree's config.worktree).
|
|
142
|
-
def git_config?(parts)
|
|
143
|
-
git = parts.index { |part| part.casecmp?(GIT_DIR) } or return false
|
|
144
|
-
parts[(git + 1)..].any? { |part| part.downcase.start_with?("config") }
|
|
92
|
+
paths_check = ReviewPaths.new(root: @root.call, denied_dirs: @denied_dirs.call, home: @home.call)
|
|
93
|
+
READ_REFUSALS[paths_check.refusal(paths)]
|
|
145
94
|
end
|
|
146
95
|
end
|
|
147
96
|
end
|