samagotchi 0.78.0 → 0.80.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 +44 -1
- data/README.md +7 -5
- data/docs/cli.md +24 -7
- data/docs/configuration.md +11 -6
- data/docs/internals/anthropic.md +12 -5
- data/docs/internals/bundles.md +4 -0
- data/docs/internals/engine.md +15 -5
- data/docs/plugins.md +144 -3
- data/docs/releasing.md +2 -2
- data/docs/sessions.md +1 -1
- data/docs/testing.md +32 -2
- data/lib/samagotchi/bootstrap/config_writer.rb +13 -3
- data/lib/samagotchi/bootstrap/probe.rb +153 -31
- data/lib/samagotchi/bootstrap_command.rb +92 -15
- data/lib/samagotchi/bridge.rb +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/continue_offer.rb +5 -5
- data/lib/samagotchi/engine.rb +40 -37
- data/lib/samagotchi/guardrails/approval.rb +6 -4
- data/lib/samagotchi/guardrails/gate.rb +7 -3
- data/lib/samagotchi/hooks/bundle_loader.rb +31 -16
- data/lib/samagotchi/hooks/registry.rb +44 -3
- data/lib/samagotchi/host_registry.rb +12 -4
- data/lib/samagotchi/llm/adapter_common.rb +8 -4
- data/lib/samagotchi/llm/anthropic_binding_recovery.rb +198 -0
- data/lib/samagotchi/llm/anthropic_messages.rb +39 -178
- data/lib/samagotchi/llm/anthropic_wire.rb +7 -1
- data/lib/samagotchi/llm/chat_loop.rb +21 -12
- data/lib/samagotchi/llm/http.rb +12 -7
- data/lib/samagotchi/llm/model_listing.rb +29 -1
- data/lib/samagotchi/llm/refusal_memo.rb +3 -2
- data/lib/samagotchi/plugin/card.rb +44 -0
- data/lib/samagotchi/plugin/context.rb +53 -8
- data/lib/samagotchi/plugin/http.rb +154 -0
- data/lib/samagotchi/plugin/http_transport.rb +77 -0
- data/lib/samagotchi/plugin/loader.rb +29 -10
- data/lib/samagotchi/question_answer.rb +29 -0
- data/lib/samagotchi/question_desk.rb +30 -22
- data/lib/samagotchi/reasoning_history.rb +2 -1
- data/lib/samagotchi/relay_verifier.rb +1 -1
- data/lib/samagotchi/testing/boundary.rb +156 -0
- data/lib/samagotchi/testing/bundle.rb +349 -0
- data/lib/samagotchi/testing/fake_executable.rb +42 -0
- data/lib/samagotchi/testing/fake_http.rb +137 -0
- data/lib/samagotchi/testing/helpers.rb +56 -0
- data/lib/samagotchi/testing/host.rb +76 -0
- data/lib/samagotchi/testing/minitest.rb +31 -0
- data/lib/samagotchi/testing/recorder.rb +138 -0
- data/lib/samagotchi/testing/recording_api.rb +27 -0
- data/lib/samagotchi/testing/rspec.rb +31 -0
- data/lib/samagotchi/testing/sandbox.rb +163 -0
- data/lib/samagotchi/testing/sessions.rb +77 -0
- data/lib/samagotchi/testing/verdict.rb +28 -0
- data/lib/samagotchi/testing/waiting.rb +36 -0
- data/lib/samagotchi/testing.rb +26 -0
- data/lib/samagotchi/tools/delegate_relay.rb +7 -6
- data/lib/samagotchi/tools/tool_index.rb +3 -0
- data/lib/samagotchi/turn_state.rb +28 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/public/app.js +79 -245
- data/lib/samagotchi/web/public/composer_grip.js +231 -0
- data/lib/samagotchi/web/public/images.js +33 -0
- data/lib/samagotchi/web/public/index.html +73 -40
- data/lib/samagotchi/web/public/level_chip.js +11 -11
- data/lib/samagotchi/web/public/lists_view.js +19 -14
- data/lib/samagotchi/web/public/llm_context_chip.js +15 -15
- data/lib/samagotchi/web/public/model_picker.js +5 -6
- data/lib/samagotchi/web/public/scope.js +3 -3
- data/lib/samagotchi/web/public/stage_view.js +3 -2
- data/lib/samagotchi/web/public/storage.js +23 -0
- data/lib/samagotchi/web/public/strip.js +35 -9
- data/lib/samagotchi/web/public/strip_grip.js +76 -0
- data/lib/samagotchi/web/public/thinking_chip.js +5 -5
- data/lib/samagotchi/web/public/toasts.js +5 -4
- metadata +24 -1
|
@@ -65,22 +65,41 @@ module Samagotchi
|
|
|
65
65
|
reason = bundle.error || unloadable_reason(file, bundle)
|
|
66
66
|
return failed(bundle_name, basename, reason, failures) if reason
|
|
67
67
|
|
|
68
|
-
api =
|
|
69
|
-
plugin = instantiate(bundle_name, file, settings)
|
|
70
|
-
plugin_label = label(bundle_name, basename)
|
|
71
|
-
api = Api.new(bundle: bundle_name, label: plugin_label, registries: registries,
|
|
72
|
-
context: registries.context_for&.call(bundle_name, settings, plugin_label))
|
|
73
|
-
plugin.register(api)
|
|
74
|
-
api.commit!
|
|
68
|
+
api = register_file(bundle_name, file, registries, settings: settings)
|
|
75
69
|
Log.info(TAG, "plugin_loaded", bundle: bundle_name, file: basename, **api.counts)
|
|
76
70
|
true
|
|
77
71
|
rescue Exception => e # rubocop:disable Lint/RescueException -- a plugin's SyntaxError or exit must not stop chi
|
|
78
|
-
api&.abort!
|
|
79
72
|
raise if e.is_a?(SignalException) # Ctrl-C and kill signals are the user's
|
|
80
73
|
|
|
81
74
|
failed(bundle_name, basename, "#{e.class}: #{e.message}", failures)
|
|
82
75
|
end
|
|
83
76
|
|
|
77
|
+
# Build +file+'s plugin with +settings+ and commit what its #register
|
|
78
|
+
# staged into +registries+ (no sha or requires_chi check: #load_bundle
|
|
79
|
+
# makes them; the test kit loads a bundle's source this way).
|
|
80
|
+
# @param namespace [Module, nil] the module the file is evaluated in
|
|
81
|
+
# (default: a fresh one, #fresh_namespace)
|
|
82
|
+
# @param api_class [Class] what #register gets (Api; the test kit's
|
|
83
|
+
# recording subclass)
|
|
84
|
+
# @yieldparam plugin [Object] the built plugin, before #register
|
|
85
|
+
# @return [Api] the committed Api (#counts)
|
|
86
|
+
# @raise [Exception] whatever the file, #register or #commit! raised,
|
|
87
|
+
# after stopping the services it started; nothing is registered
|
|
88
|
+
def register_file(bundle_name, file, registries, settings: {}, namespace: nil, api_class: Api)
|
|
89
|
+
api = nil
|
|
90
|
+
plugin = instantiate(bundle_name, file, settings, namespace: namespace)
|
|
91
|
+
yield plugin if block_given?
|
|
92
|
+
plugin_label = label(bundle_name, File.basename(file))
|
|
93
|
+
api = api_class.new(bundle: bundle_name, label: plugin_label, registries: registries,
|
|
94
|
+
context: registries.context_for&.call(bundle_name, settings, plugin_label))
|
|
95
|
+
plugin.register(api)
|
|
96
|
+
api.commit!
|
|
97
|
+
api
|
|
98
|
+
rescue Exception # rubocop:disable Lint/RescueException -- stop its services whatever it raised
|
|
99
|
+
api&.abort!
|
|
100
|
+
raise
|
|
101
|
+
end
|
|
102
|
+
|
|
84
103
|
# What a plugin's hooks, notices and failures are named by.
|
|
85
104
|
def label(bundle_name, basename) = "#{basename} (bundle #{bundle_name})"
|
|
86
105
|
|
|
@@ -98,8 +117,8 @@ module Samagotchi
|
|
|
98
117
|
# classes, not the last load's reopened) and build its class
|
|
99
118
|
# (plugin.rb → Plugin; my_plugin.rb → MyPlugin) as hooks are built:
|
|
100
119
|
# an initialize that takes an argument gets the settings.
|
|
101
|
-
def instantiate(bundle_name, file, settings)
|
|
102
|
-
namespace
|
|
120
|
+
def instantiate(bundle_name, file, settings, namespace: nil)
|
|
121
|
+
namespace ||= fresh_namespace(bundle_name)
|
|
103
122
|
namespace.module_eval(File.read(file), file, 1)
|
|
104
123
|
klass = Hooks.class_for(file, namespace)
|
|
105
124
|
plugin = Hooks.build_plugin(klass, settings)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
# An answer to the pending question (QuestionDesk#answer's value, what
|
|
5
|
+
# Engine#answer_question returns): id: the question's id; selected: the
|
|
6
|
+
# chosen option labels; freeform: the typed text or nil; selected_indices:
|
|
7
|
+
# the chosen options' positions; by: "parent_agent" for a parent agent's
|
|
8
|
+
# answer, else nil.
|
|
9
|
+
#
|
|
10
|
+
# Crosses a process or a bundle as a Hash (#to_h, symbol keys): the
|
|
11
|
+
# :question_answered event, the Bridge's and RelayVerifier's JSON
|
|
12
|
+
# responses, the ask_user_question tool result and the hook/plugin
|
|
13
|
+
# ask_user result (built field by field in QuestionDesk#ask_for_hook).
|
|
14
|
+
# JSON.generate of a Data value writes its inspect string, so every JSON
|
|
15
|
+
# boundary calls #to_h first.
|
|
16
|
+
QuestionAnswer = Data.define(:id, :selected, :freeform, :selected_indices, :by) do
|
|
17
|
+
def initialize(id:, selected:, freeform: nil, selected_indices: [], by: nil)
|
|
18
|
+
super
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Whether a parent agent gave it (not the user).
|
|
22
|
+
def parent_agent? = by == "parent_agent"
|
|
23
|
+
|
|
24
|
+
# The event / JSON shape: symbol keys in member order; +by+ left out
|
|
25
|
+
# when nil, every other key kept even when nil.
|
|
26
|
+
# @return [Hash{Symbol => Object}]
|
|
27
|
+
def to_h = super.tap { |h| h.delete(:by) if h[:by].nil? }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -8,6 +8,7 @@ require "time"
|
|
|
8
8
|
require_relative "client_id"
|
|
9
9
|
require_relative "log"
|
|
10
10
|
require_relative "pending_question"
|
|
11
|
+
require_relative "question_answer"
|
|
11
12
|
require_relative "tools/ask_user_question"
|
|
12
13
|
require_relative "guardrails/parent_approvals"
|
|
13
14
|
require_relative "guardrails/parent_continue"
|
|
@@ -124,7 +125,7 @@ module Samagotchi
|
|
|
124
125
|
result = { dismissed: true, id: result[:id], note: note }
|
|
125
126
|
end
|
|
126
127
|
# Who answered is for the relay, not the model's tool result.
|
|
127
|
-
result = result.except(:by) if result.is_a?(
|
|
128
|
+
result = result.to_h.except(:by) if result.is_a?(QuestionAnswer)
|
|
128
129
|
result.is_a?(String) ? result : JSON.generate(result)
|
|
129
130
|
end
|
|
130
131
|
|
|
@@ -139,8 +140,8 @@ module Samagotchi
|
|
|
139
140
|
# question waits (outside the lock): a String closes the question with
|
|
140
141
|
# that reason, announced and returned as a cancel; nil keeps waiting.
|
|
141
142
|
# An answer recorded first wins. Not on the sync-handler path.
|
|
142
|
-
# @return [Hash, String] the answer {
|
|
143
|
-
#
|
|
143
|
+
# @return [QuestionAnswer, Hash, String] the answer, or {error:, …}; a
|
|
144
|
+
# String when a sync handler returned text itself
|
|
144
145
|
def open_question(fields, watch: nil)
|
|
145
146
|
supersede_standing
|
|
146
147
|
ask(fields, watch)
|
|
@@ -156,7 +157,7 @@ module Samagotchi
|
|
|
156
157
|
# close (as a superseded one does). A standing one already up is
|
|
157
158
|
# replaced (withdrawn, reason "replaced").
|
|
158
159
|
# @param fields [Hash] as for #open_question
|
|
159
|
-
# @param on_answer [#call] (answer, client_id:)
|
|
160
|
+
# @param on_answer [#call] (answer [QuestionAnswer], client_id:)
|
|
160
161
|
# @param on_superseded_close [#call, nil] called when a question that
|
|
161
162
|
# superseded it closes (its poster posts it again if it still
|
|
162
163
|
# stands); nil: posted again as it was
|
|
@@ -220,14 +221,16 @@ module Samagotchi
|
|
|
220
221
|
@answer
|
|
221
222
|
elsif sync_res.is_a?(Hash) && sync_res[:selected]
|
|
222
223
|
# Treat returned hash as answer (handler rendered and parsed)
|
|
223
|
-
@answer = sync_res
|
|
224
|
+
@answer = QuestionAnswer.new(id: id.to_s, selected: Array(sync_res[:selected]),
|
|
225
|
+
freeform: sync_res[:freeform],
|
|
226
|
+
selected_indices: Array(sync_res[:selected_indices]))
|
|
224
227
|
@pending = nil
|
|
225
|
-
|
|
228
|
+
@answer
|
|
226
229
|
end
|
|
227
230
|
end
|
|
228
231
|
if ans
|
|
229
232
|
clear_saved_question
|
|
230
|
-
emit({ type: :question_answered, id: id, answer: ans })
|
|
233
|
+
emit({ type: :question_answered, id: id, answer: ans.to_h })
|
|
231
234
|
return ans
|
|
232
235
|
end
|
|
233
236
|
if sync_res.is_a?(String) && !sync_res.strip.empty?
|
|
@@ -274,7 +277,7 @@ module Samagotchi
|
|
|
274
277
|
end
|
|
275
278
|
|
|
276
279
|
if answer
|
|
277
|
-
emit({ type: :question_answered, id: id, answer: answer })
|
|
280
|
+
emit({ type: :question_answered, id: id, answer: answer.to_h })
|
|
278
281
|
answer
|
|
279
282
|
else
|
|
280
283
|
{ error: "no answer", id: id }
|
|
@@ -289,7 +292,7 @@ module Samagotchi
|
|
|
289
292
|
# @param parent_agent [Boolean, nil] the answer is a parent agent's, so
|
|
290
293
|
# it is held to guardrails.parent_approvals on an approval and doesn't
|
|
291
294
|
# bring the session back to the lists; nil: chi answer's client id says so
|
|
292
|
-
# @return [
|
|
295
|
+
# @return [QuestionAnswer] the answer; a parent agent's carries by: "parent_agent"
|
|
293
296
|
# @raise [NotPending, ArgumentError, Refused]
|
|
294
297
|
def answer(id:, selected:, freeform: nil, client_id: nil, parent_agent: nil)
|
|
295
298
|
sel = Array(selected).map { |v| v.to_s.strip }.reject(&:empty?)
|
|
@@ -349,7 +352,7 @@ module Samagotchi
|
|
|
349
352
|
@pending = pending = pending.with(relayed_to: relayed_to)
|
|
350
353
|
if session
|
|
351
354
|
session.pending_question = pending
|
|
352
|
-
|
|
355
|
+
save_session(session)
|
|
353
356
|
end
|
|
354
357
|
pending.id
|
|
355
358
|
end
|
|
@@ -398,7 +401,8 @@ module Samagotchi
|
|
|
398
401
|
# web), single-select, kind "hook". A --non-interactive run has no one
|
|
399
402
|
# to ask: nil at once. Anything but an answer (a sync handler's text, no
|
|
400
403
|
# answer, cancelled) is nil too.
|
|
401
|
-
# @return [Hash, nil] {selected:, freeform:, selected_indices:}
|
|
404
|
+
# @return [Hash, nil] {selected:, freeform:, selected_indices:} (a plain
|
|
405
|
+
# Hash: the bundle boundary)
|
|
402
406
|
def ask_for_hook(question, options, header, allow_freeform, hook)
|
|
403
407
|
return nil if @interface_lookup.call == :non_interactive
|
|
404
408
|
|
|
@@ -411,11 +415,9 @@ module Samagotchi
|
|
|
411
415
|
fields = { question: question.to_s, options: opts, header: header, multi_select: false,
|
|
412
416
|
allow_freeform: !!allow_freeform, kind: "hook", hook: hook.to_s }.compact
|
|
413
417
|
answer = open_question(fields)
|
|
414
|
-
return nil unless answer.is_a?(
|
|
418
|
+
return nil unless answer.is_a?(QuestionAnswer)
|
|
415
419
|
|
|
416
|
-
|
|
417
|
-
result[:selected_indices] = answer[:selected_indices] if answer.key?(:selected_indices)
|
|
418
|
-
result
|
|
420
|
+
{ selected: answer.selected, freeform: answer.freeform, selected_indices: answer.selected_indices }
|
|
419
421
|
end
|
|
420
422
|
|
|
421
423
|
private
|
|
@@ -439,7 +441,7 @@ module Samagotchi
|
|
|
439
441
|
end
|
|
440
442
|
if session
|
|
441
443
|
session.pending_question = pending
|
|
442
|
-
|
|
444
|
+
save_session(session)
|
|
443
445
|
end
|
|
444
446
|
event = { type: :question_requested, pending_question: pending.to_h }
|
|
445
447
|
event[:standing] = true if standing
|
|
@@ -483,7 +485,7 @@ module Samagotchi
|
|
|
483
485
|
# handed to its poster with who answered.
|
|
484
486
|
def settle_standing(standing, answer, client_id)
|
|
485
487
|
clear_saved_question
|
|
486
|
-
emit({ type: :question_answered, id: answer
|
|
488
|
+
emit({ type: :question_answered, id: answer.id, answer: answer.to_h })
|
|
487
489
|
standing[:on_answer].call(answer, client_id: client_id)
|
|
488
490
|
rescue StandardError => e
|
|
489
491
|
Log.warn(:turn, "standing_answer_failed", error: e.class.name, message: e.message)
|
|
@@ -493,7 +495,7 @@ module Samagotchi
|
|
|
493
495
|
# == label in v1), one for a single-select, one or a text when required,
|
|
494
496
|
# and a parent agent's within guardrails.parent_approvals. Under the
|
|
495
497
|
# question lock: checked against the question pending now.
|
|
496
|
-
# @return [
|
|
498
|
+
# @return [QuestionAnswer]
|
|
497
499
|
# @raise [ArgumentError, Refused]
|
|
498
500
|
def validated_answer(pending, sel, fm, parent_agent:, parent_setting:)
|
|
499
501
|
opts = Array(pending.options)
|
|
@@ -508,9 +510,8 @@ module Samagotchi
|
|
|
508
510
|
reason = Guardrails::ParentApprovals.refusal(pending, indices, setting: parent_setting)
|
|
509
511
|
raise Refused, reason if reason
|
|
510
512
|
end
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
answer
|
|
513
|
+
QuestionAnswer.new(id: pending.id.to_s, selected: sel, freeform: fm, selected_indices: indices.compact,
|
|
514
|
+
by: parent_agent ? "parent_agent" : nil)
|
|
514
515
|
end
|
|
515
516
|
|
|
516
517
|
# Wait until the question is answered, cancelled or closed by +watch+.
|
|
@@ -559,12 +560,19 @@ module Samagotchi
|
|
|
559
560
|
def cancel_controller = @cancel_controller_lookup.call
|
|
560
561
|
def emit(event) = @emit.call(event)
|
|
561
562
|
|
|
563
|
+
def save_session(session)
|
|
564
|
+
session.save(state_dir: state_dir)
|
|
565
|
+
rescue StandardError => e
|
|
566
|
+
Log.warn(:turn, "question_save_failed", sid: session.id, error: e.class.name, msg: e.message.to_s[0, 200])
|
|
567
|
+
nil
|
|
568
|
+
end
|
|
569
|
+
|
|
562
570
|
# The session file no longer holds a pending question.
|
|
563
571
|
def clear_saved_question
|
|
564
572
|
return unless session
|
|
565
573
|
|
|
566
574
|
session.pending_question = nil
|
|
567
|
-
|
|
575
|
+
save_session(session)
|
|
568
576
|
end
|
|
569
577
|
end
|
|
570
578
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "config"
|
|
4
4
|
require_relative "setting_explained"
|
|
5
|
+
require_relative "llm/anthropic_wire"
|
|
5
6
|
|
|
6
7
|
module Samagotchi
|
|
7
8
|
# Which of its own reasoning a chat host's model gets back
|
|
@@ -125,7 +126,7 @@ module Samagotchi
|
|
|
125
126
|
next false unless entry[:role].to_s == "model"
|
|
126
127
|
|
|
127
128
|
(entry[:thinking].is_a?(String) && !entry[:thinking].strip.empty?) ||
|
|
128
|
-
Array(entry[:anthropic_content]).any? { |block|
|
|
129
|
+
Array(entry[:anthropic_content]).any? { |block| LLM::AnthropicWire.thinking_block?(block) }
|
|
129
130
|
end
|
|
130
131
|
end
|
|
131
132
|
|
|
@@ -54,7 +54,7 @@ module Samagotchi
|
|
|
54
54
|
result = @engine.answer_question(id: question_id, selected: selected, freeform: answer["freeform"],
|
|
55
55
|
client_id: client_id, parent_agent: parent_agent)
|
|
56
56
|
log("relay_answered", relay_id, id: question_id, by: relay["by"])
|
|
57
|
-
[{}, 200, { status: "answered", question_id: question_id, answer: result }]
|
|
57
|
+
[{}, 200, { status: "answered", question_id: question_id, answer: result.to_h }]
|
|
58
58
|
rescue QuestionDesk::NotPending
|
|
59
59
|
not_pending(question_id)
|
|
60
60
|
rescue QuestionDesk::Refused => e
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
module Testing
|
|
5
|
+
# What a bundle's Ruby may reach of chi (docs/plugins.md, "Stable for
|
|
6
|
+
# bundles"): ALLOWED is the checked copy of that list. #scan finds a
|
|
7
|
+
# bundle's other references to Samagotchi:: and requires of chi's files;
|
|
8
|
+
# EXCEPTIONS is what the shipped bundles reach today anyway, each with
|
|
9
|
+
# why: the APIs to design before that bundle could leave chi's repo
|
|
10
|
+
# (spec/samagotchi/testing/boundary_spec.rb keeps both exact).
|
|
11
|
+
module Boundary
|
|
12
|
+
# A name a bundle may use, and the file that defines it (a require of
|
|
13
|
+
# it is fine too). A name allows itself, its methods (".x") and what
|
|
14
|
+
# it holds ("::X"); one ending in a method or constant allows only that.
|
|
15
|
+
Allowed = Data.define(:name, :file)
|
|
16
|
+
|
|
17
|
+
ALLOWED = [
|
|
18
|
+
Allowed.new(name: "Samagotchi::Plugin::Api", file: "samagotchi/plugin/api"),
|
|
19
|
+
Allowed.new(name: "Samagotchi::Plugin::ToolResult", file: "samagotchi/plugin/tool_result"),
|
|
20
|
+
Allowed.new(name: "Samagotchi::Plugin::ModelError", file: "samagotchi/plugin/context"),
|
|
21
|
+
Allowed.new(name: "Samagotchi::Plugin::ModelCancelled", file: "samagotchi/plugin/context"),
|
|
22
|
+
Allowed.new(name: "Samagotchi::Plugin::HttpError", file: "samagotchi/plugin/http"),
|
|
23
|
+
Allowed.new(name: "Samagotchi::Plugin::HttpCancelled", file: "samagotchi/plugin/http"),
|
|
24
|
+
Allowed.new(name: "Samagotchi::Plugin::Http::Response", file: "samagotchi/plugin/http"),
|
|
25
|
+
Allowed.new(name: "Samagotchi::Plugin::Service::Stopped", file: "samagotchi/plugin/service"),
|
|
26
|
+
Allowed.new(name: "Samagotchi::Plugin::Sessions::Error", file: "samagotchi/plugin/sessions"),
|
|
27
|
+
Allowed.new(name: "Samagotchi::Plugin::AttachedContext::Error", file: "samagotchi/plugin/attached_context"),
|
|
28
|
+
Allowed.new(name: "Samagotchi::Plugin::Peer", file: "samagotchi/plugin/peer"),
|
|
29
|
+
Allowed.new(name: "Samagotchi::Tools::ToolIndex::Tool", file: "samagotchi/tools/tool_index"),
|
|
30
|
+
Allowed.new(name: "Samagotchi::Tools::ToolIndex::Group", file: "samagotchi/tools/tool_index"),
|
|
31
|
+
Allowed.new(name: "Samagotchi::Tools::ToolIndex.schema_note", file: "samagotchi/tools/tool_index"),
|
|
32
|
+
Allowed.new(name: "Samagotchi::Tools::ToolIndex.spec_tokens", file: "samagotchi/tools/tool_index"),
|
|
33
|
+
Allowed.new(name: "Samagotchi::Tools::Execute::NAME", file: "samagotchi/tools/execute"),
|
|
34
|
+
Allowed.new(name: "Samagotchi::Tools::Execute::TIMEOUT_PREFIX", file: "samagotchi/tools/execute"),
|
|
35
|
+
Allowed.new(name: "Samagotchi::Tools::Execute::HANDED_OVER", file: "samagotchi/tools/execute"),
|
|
36
|
+
Allowed.new(name: "Samagotchi::Tools::Execute.timeout_seconds", file: "samagotchi/tools/execute"),
|
|
37
|
+
Allowed.new(name: "Samagotchi::Tools::TaskRuntime::TASKS_DIR", file: "samagotchi/tools/task_runtime"),
|
|
38
|
+
Allowed.new(name: "Samagotchi::Tools::TaskRuntime::EXIT_CODE_FILENAME", file: "samagotchi/tools/task_runtime"),
|
|
39
|
+
Allowed.new(name: "Samagotchi::AtomicFile.write", file: "samagotchi/atomic_file"),
|
|
40
|
+
Allowed.new(name: "Samagotchi::Log.warn", file: "samagotchi/log"),
|
|
41
|
+
Allowed.new(name: "Samagotchi::Log.debug", file: "samagotchi/log"),
|
|
42
|
+
Allowed.new(name: "Samagotchi::Config.get", file: "samagotchi/config"),
|
|
43
|
+
Allowed.new(name: "Samagotchi::VERSION", file: "samagotchi/version"),
|
|
44
|
+
# The kit itself, for a bundle's own specs.
|
|
45
|
+
Allowed.new(name: "Samagotchi::Testing", file: "samagotchi/testing")
|
|
46
|
+
].freeze
|
|
47
|
+
|
|
48
|
+
# The kit's adapters, required by a bundle's specs.
|
|
49
|
+
ALLOWED_REQUIRES = (ALLOWED.map(&:file) + %w[samagotchi/testing/rspec samagotchi/testing/minitest]).uniq.freeze
|
|
50
|
+
|
|
51
|
+
SESSION = "a saved session (context sources are keyed by its id): needs a kit helper that saves one"
|
|
52
|
+
SOURCES = "the session's context sources (the peer's inbox / outbox): needs a kit helper that attaches one"
|
|
53
|
+
REF_LIMITS = "a ref's kinds and limits (event[:refs] checks them too): could be stable constants"
|
|
54
|
+
# The peers' specs pair a session through its context sources.
|
|
55
|
+
PEER_SPEC = {
|
|
56
|
+
'require "samagotchi/context_sources"' => "spec: #{SOURCES}",
|
|
57
|
+
"Samagotchi::Session.new_session" => "spec: #{SESSION}",
|
|
58
|
+
"Samagotchi::ContextSources.session_location" => "spec: #{SOURCES}",
|
|
59
|
+
"Samagotchi::ContextSources::Source.new" => "spec: #{SOURCES}",
|
|
60
|
+
"Samagotchi::ContextSources::Fetched.new" => "spec: an outbox answer as the context poller records it"
|
|
61
|
+
}.freeze
|
|
62
|
+
|
|
63
|
+
# bundle => {reference => why}: what each shipped bundle reaches
|
|
64
|
+
# beyond ALLOWED today, its specs' (why "spec: …") included.
|
|
65
|
+
EXCEPTIONS = {
|
|
66
|
+
"agents-md" => {
|
|
67
|
+
'require "samagotchi/guardrails/context"' => "for GitInfo: a ctx helper for the checkout's files could replace it",
|
|
68
|
+
"Samagotchi::Guardrails::GitInfo.new" => "the work tree's top for the AGENTS.md chain (ctx.repo_root covers the top only)"
|
|
69
|
+
},
|
|
70
|
+
"claude-peer" => PEER_SPEC,
|
|
71
|
+
"codex-peer" => PEER_SPEC,
|
|
72
|
+
"coordinator" => {
|
|
73
|
+
"Samagotchi::MemoryPaths.scope_dir" => "where the coordinator's memories live: needs a ctx memory-path API",
|
|
74
|
+
"Samagotchi::MemoryPaths" => "spec: checks /coordinate end reads no memories (MemoryPaths.scope_dir isn't called)"
|
|
75
|
+
},
|
|
76
|
+
"github-pr" => {
|
|
77
|
+
"Samagotchi::Session.new_session" => "spec: #{SESSION} (ctx.delegate?, ctx.fork?, ctx.context read it)",
|
|
78
|
+
"Samagotchi::ContextSources.session_location" => "spec: reads and removes the session's sources (added_by), " \
|
|
79
|
+
"which ctx.context.list doesn't show",
|
|
80
|
+
'require "samagotchi/memory_bundle/installer"' => "spec: for Installer",
|
|
81
|
+
"Samagotchi::MemoryBundle::Installer.new" => "spec: installs the bundle so its context provider resolves: " \
|
|
82
|
+
"needs a kit install helper",
|
|
83
|
+
'require "samagotchi/context_providers"' => "spec: for ContextProviders",
|
|
84
|
+
"Samagotchi::ContextProviders.resolve" => "spec: the manifest's URL provider through chi's own resolver",
|
|
85
|
+
'require "samagotchi/answer_display"' => "spec: for AnswerDisplay",
|
|
86
|
+
"Samagotchi::AnswerDisplay.new" => "spec: after_turn's present: helper, as chi's fire site builds it: " \
|
|
87
|
+
"the kit could supply after_turn's fire-site keys",
|
|
88
|
+
'require "samagotchi/answer_refs"' => "spec: for AnswerRefs",
|
|
89
|
+
"Samagotchi::AnswerRefs.new" => "spec: after_turn's refs: helper, as chi's fire site builds it",
|
|
90
|
+
"Samagotchi::Log" => "spec: stubs Log to check what ctx.log recorded: a kit log recorder could replace it"
|
|
91
|
+
},
|
|
92
|
+
"loop-guard" => {
|
|
93
|
+
'require "samagotchi/llm/native_tool_normalizer"' => "spec: for NativeToolNormalizer",
|
|
94
|
+
"Samagotchi::LLM::NativeToolNormalizer.normalize" => "spec: replays recorded calls in the shape chi's parser gives",
|
|
95
|
+
"Samagotchi::Hooks::StreamWatch::EVERY_CHARS" => "spec: the stream watch's batch size (a ctx or kit value later)"
|
|
96
|
+
},
|
|
97
|
+
"skills" => {
|
|
98
|
+
"Samagotchi::Tools::MemoryRead.memories_dir" => "lists the skill memories: needs a ctx memory-path API"
|
|
99
|
+
},
|
|
100
|
+
"mcp" => {
|
|
101
|
+
'require "samagotchi/process_group"' => "for ProcessGroup.stop",
|
|
102
|
+
"Samagotchi::ProcessGroup.stop" => "stops a server's process group: needs a ctx process API",
|
|
103
|
+
'require "samagotchi/token_usage"' => "for CHARS_PER_TOKEN",
|
|
104
|
+
"Samagotchi::TokenUsage::CHARS_PER_TOKEN" => "estimates a tool's tokens: could be a stable constant",
|
|
105
|
+
'require "samagotchi/tool_declarations"' => "loaded for Api.tool_spec's schemas"
|
|
106
|
+
},
|
|
107
|
+
"slow-commands" => {
|
|
108
|
+
'require "samagotchi/guardrails/shell_lex"' => "for ShellLex",
|
|
109
|
+
"Samagotchi::Guardrails::ShellLex" => "splits a command into its parts: needs a stable shell-parsing helper",
|
|
110
|
+
"Samagotchi::Tools::Execute.const_defined?" => "reads execute's limits that older chis lack, by reflection",
|
|
111
|
+
"Samagotchi::Tools::Execute.const_get" => "reads execute's limits that older chis lack, by reflection"
|
|
112
|
+
},
|
|
113
|
+
"source-links" => {
|
|
114
|
+
'require "samagotchi/ref"' => "for the Ref / RefAction limits",
|
|
115
|
+
"Samagotchi::Ref::FILE" => REF_LIMITS,
|
|
116
|
+
"Samagotchi::Ref::KIND.match?" => REF_LIMITS,
|
|
117
|
+
"Samagotchi::Ref::MAX_LABEL" => REF_LIMITS,
|
|
118
|
+
"Samagotchi::RefAction::MAX_LABEL" => REF_LIMITS,
|
|
119
|
+
"Samagotchi::RefAction.http_url?" => "which URLs an action may open: could be a stable helper"
|
|
120
|
+
}
|
|
121
|
+
}.freeze
|
|
122
|
+
|
|
123
|
+
# A Samagotchi:: name with what follows it (::X, .method).
|
|
124
|
+
REFERENCE = /\bSamagotchi(?:::[A-Z]\w*)+(?:\.[a-z_]\w*[?!]?)?/
|
|
125
|
+
# A require of one of chi's files.
|
|
126
|
+
REQUIRE = %r{\brequire\s*\(?\s*["'](samagotchi(?:/[\w/]+)?)["']}
|
|
127
|
+
|
|
128
|
+
# One reference outside ALLOWED, in a file of the bundle.
|
|
129
|
+
Finding = Data.define(:reference, :file)
|
|
130
|
+
|
|
131
|
+
module_function
|
|
132
|
+
|
|
133
|
+
# The bundle's references to chi outside ALLOWED (its plugin, hooks,
|
|
134
|
+
# scripts and specs: every .rb under +dir+), one per reference and
|
|
135
|
+
# file, sorted.
|
|
136
|
+
# @param dir [String] the bundle's folder
|
|
137
|
+
# @return [Array<Finding>]
|
|
138
|
+
def scan(dir)
|
|
139
|
+
Dir.glob(File.join(dir, "**", "*.rb")).sort.flat_map do |path|
|
|
140
|
+
text = File.read(path)
|
|
141
|
+
refs = text.scan(REFERENCE).reject { |name| allowed?(name) }
|
|
142
|
+
refs += text.scan(REQUIRE).flatten.reject { |file| ALLOWED_REQUIRES.include?(file) }.map { |file| %(require "#{file}") }
|
|
143
|
+
file = path.delete_prefix("#{File.expand_path(dir)}/")
|
|
144
|
+
refs.uniq.map { |reference| Finding.new(reference: reference, file: file) }
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Whether a bundle may use +name+ ("Samagotchi::X::Y", maybe ".method").
|
|
149
|
+
def allowed?(name)
|
|
150
|
+
ALLOWED.any? do |entry|
|
|
151
|
+
name == entry.name || name.start_with?("#{entry.name}.", "#{entry.name}::")
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|