samagotchi 0.148.0 → 0.150.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 +62 -1
- data/README.md +4 -2
- data/docs/architecture.md +3 -3
- data/docs/bundles.md +38 -1
- data/docs/cli.md +76 -24
- data/docs/configuration.md +8 -6
- data/docs/guardrails.md +2 -1
- data/docs/hooks.md +2 -1
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bridge.md +8 -0
- data/docs/internals/bundles.md +42 -14
- data/docs/internals/objectives.md +269 -0
- data/docs/internals/web-frontend.md +4 -1
- data/docs/internals/web-push.md +2 -2
- data/docs/internals/working-set.md +5 -2
- data/docs/memory.md +47 -9
- data/docs/plugins.md +10 -2
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +45 -2
- data/docs/system-one.md +17 -1
- data/docs/testing.md +5 -0
- data/lib/samagotchi/atomic_file.rb +3 -0
- data/lib/samagotchi/bootstrap_command.rb +5 -2
- data/lib/samagotchi/bridge/card_store.rb +52 -7
- data/lib/samagotchi/bridge.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +91 -68
- data/lib/samagotchi/bundler_env.rb +24 -0
- data/lib/samagotchi/bundles/coordinator/manifest.yml +6 -4
- data/lib/samagotchi/bundles/coordinator/plugin.rb +3 -2
- data/lib/samagotchi/bundles/coordinator/skill_coordinator.md +9 -0
- data/lib/samagotchi/bundles/decision-card/manifest.yml +13 -0
- data/lib/samagotchi/bundles/decision-card/plugin.rb +280 -0
- data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
- data/lib/samagotchi/bundles/github-pr/manifest.yml +3 -3
- data/lib/samagotchi/bundles/github-pr/plugin.rb +2 -1
- data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +3 -1
- data/lib/samagotchi/bundles/known-names/manifest.yml +3 -2
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +2 -2
- data/lib/samagotchi/bundles/source-links/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/memory_guide.md +5 -5
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/chi_home.rb +4 -2
- data/lib/samagotchi/chi_shim.rb +2 -1
- data/lib/samagotchi/child_env.rb +10 -3
- data/lib/samagotchi/config.rb +7 -1
- data/lib/samagotchi/context_note.rb +17 -5
- data/lib/samagotchi/engine.rb +113 -6
- data/lib/samagotchi/engine_peers.rb +11 -0
- data/lib/samagotchi/git_env.rb +43 -0
- data/lib/samagotchi/guardrails/context.rb +2 -0
- data/lib/samagotchi/guardrails/pair_boundary.rb +3 -2
- data/lib/samagotchi/guardrails/targets.rb +4 -1
- data/lib/samagotchi/llm/chat_loop.rb +1 -1
- data/lib/samagotchi/memory_bundle/asset_installer.rb +16 -2
- data/lib/samagotchi/memory_bundle/file_standing.rb +167 -0
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +62 -1
- data/lib/samagotchi/memory_bundle/installer.rb +161 -52
- data/lib/samagotchi/memory_bundle/listing.rb +4 -3
- data/lib/samagotchi/memory_bundle/merger.rb +209 -8
- data/lib/samagotchi/memory_bundle/placeholder.rb +2 -1
- data/lib/samagotchi/memory_bundle/profile.rb +7 -3
- data/lib/samagotchi/memory_bundle/provenance.rb +178 -20
- data/lib/samagotchi/memory_bundle/remerge.rb +167 -0
- data/lib/samagotchi/memory_bundle/shipped_history.rb +79 -0
- data/lib/samagotchi/memory_bundle/shipped_update.rb +89 -24
- data/lib/samagotchi/memory_bundle/source.rb +3 -3
- data/lib/samagotchi/memory_bundle/status.rb +21 -3
- data/lib/samagotchi/memory_bundle/system_bundle.rb +98 -33
- data/lib/samagotchi/memory_bundle/text_merge.rb +399 -0
- data/lib/samagotchi/memory_bundle/trash.rb +58 -2
- data/lib/samagotchi/memory_bundle/uninstaller.rb +26 -6
- data/lib/samagotchi/note_command.rb +1 -1
- data/lib/samagotchi/note_delivery.rb +6 -2
- data/lib/samagotchi/objectives.rb +395 -0
- data/lib/samagotchi/objectives_audit.rb +115 -0
- data/lib/samagotchi/objectives_check.rb +365 -0
- data/lib/samagotchi/objectives_command.rb +192 -0
- data/lib/samagotchi/plugin/api.rb +2 -1
- data/lib/samagotchi/plugin/context.rb +21 -1
- data/lib/samagotchi/project_env.rb +3 -2
- data/lib/samagotchi/self_report.rb +2 -0
- data/lib/samagotchi/session_commands.rb +15 -0
- data/lib/samagotchi/session_continue.rb +3 -0
- data/lib/samagotchi/session_inbox.rb +14 -6
- data/lib/samagotchi/session_manager.rb +1 -1
- data/lib/samagotchi/steer.rb +24 -4
- data/lib/samagotchi/testing/boundary.rb +2 -0
- data/lib/samagotchi/testing/host.rb +1 -0
- data/lib/samagotchi/testing/minitest.rb +3 -0
- data/lib/samagotchi/testing/rspec.rb +3 -0
- data/lib/samagotchi/testing/sandbox.rb +3 -1
- data/lib/samagotchi/tool_activity.rb +17 -0
- data/lib/samagotchi/tool_declarations.rb +34 -0
- data/lib/samagotchi/tools/builtin_calls.rb +2 -1
- data/lib/samagotchi/tools/builtins.rb +4 -1
- data/lib/samagotchi/tools/delegate.rb +2 -1
- data/lib/samagotchi/tools/objectives_tool.rb +75 -0
- data/lib/samagotchi/tools/peers.rb +3 -0
- data/lib/samagotchi/tools/task_runtime.rb +13 -10
- data/lib/samagotchi/turn_flow.rb +3 -1
- data/lib/samagotchi/update_command.rb +76 -28
- data/lib/samagotchi/update_hint.rb +4 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/auto_update.rb +8 -8
- data/lib/samagotchi/web/auto_update_record.rb +6 -4
- data/lib/samagotchi/web/message_display.rb +9 -0
- data/lib/samagotchi/web/participant_view.rb +2 -2
- data/lib/samagotchi/web/public/app.js +15 -3
- data/lib/samagotchi/web/public/block_open.js +13 -0
- data/lib/samagotchi/web/public/bubbles.js +4 -2
- data/lib/samagotchi/web/public/cards_place.js +30 -9
- data/lib/samagotchi/web/public/commands.js +4 -1
- data/lib/samagotchi/web/public/details_motion.js +2 -1
- data/lib/samagotchi/web/public/index.html +20 -14
- data/lib/samagotchi/web/public/objectives.css +80 -0
- data/lib/samagotchi/web/public/objectives_entry.js +22 -0
- data/lib/samagotchi/web/public/objectives_model.js +100 -0
- data/lib/samagotchi/web/public/objectives_moves.js +145 -0
- data/lib/samagotchi/web/public/objectives_strip.js +242 -0
- data/lib/samagotchi/web/public/objectives_wiring.js +62 -0
- data/lib/samagotchi/web/public/stage_model.js +5 -6
- data/lib/samagotchi/web/public/stage_view.js +8 -7
- data/lib/samagotchi/web/public/stream_handlers.js +6 -1
- data/lib/samagotchi/web/public/turn_block.js +34 -19
- data/lib/samagotchi/web/public/turn_html.js +4 -1
- data/lib/samagotchi/web/public/turn_model.js +1 -1
- data/lib/samagotchi/web/public/update_sheet.js +8 -3
- data/lib/samagotchi/web/public/working_set_view.js +10 -7
- data/lib/samagotchi/web/push/notifier.rb +7 -9
- data/lib/samagotchi/web/session_hub.rb +2 -1
- data/lib/samagotchi/web/session_show.rb +12 -1
- data/lib/samagotchi/web/update_ops.rb +2 -1
- data/lib/samagotchi/worker.rb +3 -0
- data/lib/samagotchi/worker_inbound.rb +16 -8
- metadata +22 -1
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
# itself (the core child-boundary guardrail), in every mode: nothing to set up.
|
|
43
43
|
#
|
|
44
44
|
# The coordinator toolkit is lean plus the tools that run children
|
|
45
|
-
# (delegate, delegate_result, list_sessions)
|
|
45
|
+
# (delegate, delegate_result, list_sessions) and objectives (the skill's
|
|
46
|
+
# moves). /coordinate puts the session
|
|
46
47
|
# on it before it asks the model (ctx.use_toolkit), which chi allows only
|
|
47
48
|
# before the session's first model reply and outside a turn: otherwise it
|
|
48
49
|
# says to run /toolkit coordinator, unless the session declares delegate
|
|
@@ -84,7 +85,7 @@ class Plugin
|
|
|
84
85
|
def initialize(_settings = {}); end
|
|
85
86
|
|
|
86
87
|
TOOLKIT = "coordinator"
|
|
87
|
-
TOOLKIT_TOOLS = %w[delegate delegate_result list_sessions].freeze
|
|
88
|
+
TOOLKIT_TOOLS = %w[delegate delegate_result list_sessions objectives].freeze
|
|
88
89
|
CODING_TOOLKIT = "coding"
|
|
89
90
|
CODING_PROMPT = "- Be concise in your responses\n- Show file paths clearly when working with files"
|
|
90
91
|
|
|
@@ -109,6 +109,13 @@ If the request is a new goal, say which handoffs are open (one line each) and go
|
|
|
109
109
|
description to "DONE: …" and ask the user whether to remove it (memory_write name, scope and
|
|
110
110
|
remove: true).
|
|
111
111
|
|
|
112
|
+
## Objectives
|
|
113
|
+
If this session has an OBJECTIVES note: when a branch's verdict, merge or blocker moves an objective, record it with
|
|
114
|
+
`objectives` (action: move, the id, a one-line note, evidence: a sha or a test count), not per command.
|
|
115
|
+
A move or a `done` is never approval to merge. A user's "not yet" on an objective a branch serves, or their changing
|
|
116
|
+
what it asks, counts as "the user said to hold" that branch (step 6), until the user says otherwise; their marking it
|
|
117
|
+
done isn't a hold. The user edits the list with /objectives; you don't.
|
|
118
|
+
|
|
112
119
|
## End of day
|
|
113
120
|
When the user ends the day (`/coordinate end`, "that's all for today"), before you answer:
|
|
114
121
|
1. Check where things stand: for each branch in your handoff `git log --oneline <default>..<branch>` and
|
|
@@ -167,3 +174,5 @@ participants: a manager or a teammate brings new context (a priority, a scope ch
|
|
|
167
174
|
owner's Apply updates the handoff and re-plans, nothing started or merged there (0.6.0)
|
|
168
175
|
- 2026-11-04 step 6: a branch verified with no doubts that can fast-forward merges with no question when chi's git-merge
|
|
169
176
|
rule asks: the merge is chi's approval to give or pass; anything else asks as before (0.7.0)
|
|
177
|
+
- 2026-11-04 Objectives: record a move with the objectives tool; a move or a done never approves a merge; a user's
|
|
178
|
+
"not yet" or changed objective is a hold until they say otherwise (0.8.0)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: decision-card
|
|
3
|
+
version: 0.2.0
|
|
4
|
+
scope: system
|
|
5
|
+
description: "In a session with objectives, a long answer ending in a question gets a card: a short summary and the question, answered in the composer"
|
|
6
|
+
trust_level: reviewed
|
|
7
|
+
plugin:
|
|
8
|
+
file: plugin.rb
|
|
9
|
+
sha256: sha256:e628d84ab9521f7e8e5f3bc69da97fe00e574082412ea24e9f3f1c6234ad31a9
|
|
10
|
+
# ctx.objectives and ctx.turn_running? are new after 0.148.0
|
|
11
|
+
requires_chi: ">= 0.149.0"
|
|
12
|
+
# config.yml bundles: decision-card: (another key warns; an older chi ignores this)
|
|
13
|
+
settings: [min_chars, tail_chars, model, timeout, max_tokens]
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The decision-card bundle (docs/bundles.md, The decision-card bundle): in a
|
|
4
|
+
# session with objectives, an answer that is long and ends in a question gets a
|
|
5
|
+
# card with a short summary and the question, so the question isn't lost at the
|
|
6
|
+
# bottom of a long read. The user answers in the composer: the card has no
|
|
7
|
+
# buttons, so it invents no options, and it asks nothing of anyone.
|
|
8
|
+
#
|
|
9
|
+
# The gate costs nothing: the answer's length and tail, read from the event
|
|
10
|
+
# (event[:answer]: the stored message may still hold the native loop's thought),
|
|
11
|
+
# then the list (ctx.objectives). Only then one side request (ctx.ask_model), on
|
|
12
|
+
# a thread: the turn's end isn't held, and the web's answer doesn't wait for it.
|
|
13
|
+
# Nothing goes in the conversation: the card is the Bridge's (cards.json, capped),
|
|
14
|
+
# and uninstalling the bundle leaves a session as it was.
|
|
15
|
+
#
|
|
16
|
+
# A card may be late: a one-slot local server answers a side request after the
|
|
17
|
+
# running one. A new turn means the user replied or moved on, so it cancels the
|
|
18
|
+
# request (which frees the slot for that turn), and a card still to come is
|
|
19
|
+
# dropped. One already shown folds to a line at the owner's next turn. The request
|
|
20
|
+
# starts at once, ahead of the turn-end warm-up, so that the warm-up is the last
|
|
21
|
+
# thing in a one-slot server's slot. The card waits for the turn to be over (the
|
|
22
|
+
# hook runs inside it): shown earlier, it would belong to that turn's block, which
|
|
23
|
+
# the web collapses.
|
|
24
|
+
#
|
|
25
|
+
# A failed request shows nothing: it is only logged.
|
|
26
|
+
#
|
|
27
|
+
# /short is the same seam on demand: the last answer's short version, in a card,
|
|
28
|
+
# for any long answer (the web's objectives offer it), its model and its limits
|
|
29
|
+
# the card's. Nothing enters the conversation here either.
|
|
30
|
+
#
|
|
31
|
+
# Settings (config.yml, bundles: decision-card:):
|
|
32
|
+
# min_chars: 1200 how long an answer is long
|
|
33
|
+
# tail_chars: 300 how far from the end the question may be
|
|
34
|
+
# model: nil a cheap configured model ("host:id", an alias); nil is the session's
|
|
35
|
+
# timeout: 120 seconds the side request may take
|
|
36
|
+
# max_tokens: 300 the side answer's limit
|
|
37
|
+
class Plugin
|
|
38
|
+
TITLE = "A question for you"
|
|
39
|
+
SYSTEM = "You read the end of a working session for a busy user. You only report what is written; you don't " \
|
|
40
|
+
"continue the task, and you don't answer the question."
|
|
41
|
+
# What the side request asks, in the three parts #parse reads.
|
|
42
|
+
REQUEST = <<~PROMPT
|
|
43
|
+
The assistant's last answer above may end by asking the user something. The session's objectives are:
|
|
44
|
+
|
|
45
|
+
%<list>s
|
|
46
|
+
|
|
47
|
+
Reply with exactly these three lines:
|
|
48
|
+
SUMMARY: what the answer says, in one or two plain sentences, at most 40 words
|
|
49
|
+
QUESTION: the question it asks the user, in one sentence, in its own words
|
|
50
|
+
OBJECTIVES: the ids of the objectives that question concerns, or none
|
|
51
|
+
|
|
52
|
+
If it asks the user nothing they have to decide, write QUESTION: none. Don't offer options or say which you would pick.
|
|
53
|
+
PROMPT
|
|
54
|
+
PART = /\A[\s*_#>-]*(summary|question|objectives)[\s*_]*:[\s*_]*(.*?)[\s*_]*\z/i
|
|
55
|
+
OBJECTIVE_ID = /\bO\d+\b/i
|
|
56
|
+
SUMMARY_CHARS = 500
|
|
57
|
+
QUESTION_CHARS = 300
|
|
58
|
+
# An :info card with no actions and one line up to 160 characters is a notice row (docs/plugins.md, Cards).
|
|
59
|
+
FOLDED_CHARS = 160
|
|
60
|
+
# A question mark that ends a word (not a URL's), with the marks that may close it.
|
|
61
|
+
SHORT_TITLE = "Short version"
|
|
62
|
+
SHORT_CUT_OFF = "No short version: the session stopped before it came."
|
|
63
|
+
SHORT_BUSY = "a turn is running: /short works on its answer, after this turn"
|
|
64
|
+
# Under the answer. Two paragraphs also keep it a card: a short body alone is a collapsed notice row.
|
|
65
|
+
SHORT_NOTE = "_Asked on demand, not added to the conversation._"
|
|
66
|
+
SHORT_REQUEST = "Give the assistant's last answer above in two or three plain sentences, at most 60 words. Say what it " \
|
|
67
|
+
"found or did, and anything it asks the user. Add nothing it doesn't say."
|
|
68
|
+
QUESTION_MARK = /[??؟](?=[\s"'”’»」』)*_\]…]|\.(?:\s|\z)|\z)/
|
|
69
|
+
CODE = /```.*?(?:```|\z)|~~~.*?(?:~~~|\z)|`[^`\n]*`|^(?: {4}|\t)[^\n]*/m
|
|
70
|
+
# What a side answer says for "nothing to decide", however it words it.
|
|
71
|
+
NO_QUESTION = %r{\A(?:none\b|n/a\b|no questions?\b|there (?:is|are) no questions?\b)}i
|
|
72
|
+
TURN_END_POLL = 0.02
|
|
73
|
+
|
|
74
|
+
# One answer's side request: the session it is for, and how to cancel it.
|
|
75
|
+
Ask = Data.define(:session_id, :cancel)
|
|
76
|
+
# A card on screen, to fold when the owner's next turn starts.
|
|
77
|
+
Shown = Data.define(:id, :question)
|
|
78
|
+
# What the side answer said: Item ids the list has, in its order.
|
|
79
|
+
Reading = Data.define(:summary, :question, :ids)
|
|
80
|
+
|
|
81
|
+
def initialize(settings = {})
|
|
82
|
+
@min_chars = positive(settings["min_chars"]) || 1200
|
|
83
|
+
@tail_chars = positive(settings["tail_chars"]) || 300
|
|
84
|
+
model = settings["model"].to_s.strip
|
|
85
|
+
@model = model.empty? ? nil : model
|
|
86
|
+
@timeout = positive_number(settings["timeout"]) || 120
|
|
87
|
+
@max_tokens = positive(settings["max_tokens"]) || 300
|
|
88
|
+
@mutex = Mutex.new
|
|
89
|
+
@ask = nil
|
|
90
|
+
@shown = []
|
|
91
|
+
@shorts = {}
|
|
92
|
+
@thread = nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def register(chi)
|
|
96
|
+
chi.on(:before_turn) { |event, ctx| turn_started(event, ctx) }
|
|
97
|
+
chi.on(:after_turn) { |event, ctx| turn_ended(event, ctx) }
|
|
98
|
+
chi.service(:short_requests, eager: true) do |svc|
|
|
99
|
+
svc.on_stop { @mutex.synchronize { @shorts.keys }.each { |id| end_short(id, SHORT_CUT_OFF) } }
|
|
100
|
+
true
|
|
101
|
+
end
|
|
102
|
+
chi.command("/short", "the last answer's short version, in a card; nothing enters the conversation", anytime: true) do |_args, ctx|
|
|
103
|
+
short(ctx)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# The side request's thread is done. Nothing in chi waits for it: it is for specs.
|
|
108
|
+
def settle = @thread&.join
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
# Any turn makes a request in flight stale, and the slot matters. Only the
|
|
113
|
+
# owner's answers a shown card: a reminder or a child's report has no prompt
|
|
114
|
+
# (a wake runs as a continue), and a participant's has an author.
|
|
115
|
+
def turn_started(event, ctx)
|
|
116
|
+
owner = !event[:author] && !event[:prompt].to_s.strip.empty?
|
|
117
|
+
ask, shown = @mutex.synchronize do
|
|
118
|
+
taken = [@ask, owner ? @shown : []]
|
|
119
|
+
@ask = nil
|
|
120
|
+
@shown = [] if owner
|
|
121
|
+
taken
|
|
122
|
+
end
|
|
123
|
+
ask&.cancel&.cancel!(:new_turn)
|
|
124
|
+
shown.each { |card| fold(ctx, card) }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def fold(ctx, shown)
|
|
128
|
+
ctx.card(id: shown.id, title: "Asked", body: cut(shown.question, FOLDED_CHARS))
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# On the command's own thread. Not beside a turn: ctx.messages holds that turn so
|
|
132
|
+
# far, so its "last answer" would be interim text.
|
|
133
|
+
# @return [String, nil] what the command answers: only why there is no card
|
|
134
|
+
def short(ctx)
|
|
135
|
+
return SHORT_BUSY if ctx.turn_running?
|
|
136
|
+
|
|
137
|
+
messages = ctx.messages
|
|
138
|
+
answer = last_answer(messages) or return "no answer to shorten yet"
|
|
139
|
+
|
|
140
|
+
id = ctx.card(title: SHORT_TITLE, body: "…")
|
|
141
|
+
@mutex.synchronize { @shorts[id] = ctx }
|
|
142
|
+
reply = ctx.ask_model(messages: exchange(last_prompt(messages), answer), prompt: SHORT_REQUEST, system: SYSTEM,
|
|
143
|
+
timeout: @timeout, max_tokens: @max_tokens, model: @model)
|
|
144
|
+
end_short(id, "#{reply.strip}\n\n#{SHORT_NOTE}")
|
|
145
|
+
nil
|
|
146
|
+
rescue StandardError => e
|
|
147
|
+
end_short(id, "No short version: #{e.message}") if id
|
|
148
|
+
nil
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Once per card, whichever comes first: its answer, or the worker leaving with
|
|
152
|
+
# the request still out (chi waits 3 s for a command, then this service stops,
|
|
153
|
+
# while a card can still reach the UIs). A worker killed outright runs neither:
|
|
154
|
+
# its "…" stays, a one-line row.
|
|
155
|
+
def end_short(id, body)
|
|
156
|
+
ctx = @mutex.synchronize { @shorts.delete(id) } or return
|
|
157
|
+
|
|
158
|
+
ctx.card(id: id, title: SHORT_TITLE, body: body)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# @return [String, nil] the newest non-empty answer's text
|
|
162
|
+
def last_answer(messages)
|
|
163
|
+
found = Array(messages).reverse_each.find do |message|
|
|
164
|
+
%w[model assistant].include?(field(message, :role).to_s) && !field(message, :content).to_s.strip.empty?
|
|
165
|
+
end
|
|
166
|
+
found && field(found, :content).to_s
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def turn_ended(event, ctx)
|
|
170
|
+
return unless event[:status].to_s == "completed"
|
|
171
|
+
# A participant's question is for the participant, on the owner's model; a
|
|
172
|
+
# one-shot run ends before any card is read.
|
|
173
|
+
return if event[:author] || ctx.frontend == :one_shot
|
|
174
|
+
|
|
175
|
+
answer = event[:answer]
|
|
176
|
+
return unless answer && answer.length >= @min_chars && question_at_tail?(answer)
|
|
177
|
+
|
|
178
|
+
prompt = last_prompt(event[:messages])
|
|
179
|
+
ask = Ask.new(session_id: ctx.session_id, cancel: Samagotchi::CancellationController.new)
|
|
180
|
+
@mutex.synchronize { @ask = ask }
|
|
181
|
+
@thread = Thread.new { ask_for_card(ctx, ask, answer, prompt) }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# @return [String, nil] the owner's text the turn answers
|
|
185
|
+
def last_prompt(messages)
|
|
186
|
+
prompt = Array(messages).reverse_each.find { |message| field(message, :role).to_s == "user" && field(message, :kind).to_s.empty? }
|
|
187
|
+
prompt && field(prompt, :content).to_s
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def field(message, key) = message[key] || message[key.to_s]
|
|
191
|
+
|
|
192
|
+
def question_at_tail?(answer)
|
|
193
|
+
prose = answer.gsub(CODE, "").rstrip
|
|
194
|
+
prose[-[@tail_chars, prose.length].min..].to_s.match?(QUESTION_MARK)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# On its own thread. Nothing here reaches the user but the card.
|
|
198
|
+
def ask_for_card(ctx, ask, answer, prompt)
|
|
199
|
+
list = ctx.objectives
|
|
200
|
+
return unless list.items.any? { |item| item.status == "active" }
|
|
201
|
+
|
|
202
|
+
reply = ctx.ask_model(messages: exchange(prompt, answer), prompt: format(REQUEST, list: list.text), system: SYSTEM,
|
|
203
|
+
timeout: @timeout, max_tokens: @max_tokens, cancel: ask.cancel, model: @model)
|
|
204
|
+
reading = parse(reply, list)
|
|
205
|
+
return ctx.log.info("no_card", reason: "the side answer named no question and summary") unless reading
|
|
206
|
+
|
|
207
|
+
sleep(TURN_END_POLL) while ctx.turn_running? && current?(ask)
|
|
208
|
+
show(ctx, ask, reading, list)
|
|
209
|
+
rescue Samagotchi::Plugin::ModelCancelled
|
|
210
|
+
ctx.log.info("no_card", reason: "a new turn started")
|
|
211
|
+
rescue StandardError => e
|
|
212
|
+
ctx.log.warn("no_card", reason: "#{e.class}: #{e.message}"[0, 300])
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def exchange(prompt, answer)
|
|
216
|
+
[prompt && { role: "user", content: prompt }, { role: "model", content: answer }].compact
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def current?(ask) = @mutex.synchronize { @ask.equal?(ask) }
|
|
220
|
+
|
|
221
|
+
# Under the mutex, as a new turn's start is: a card shows before that start
|
|
222
|
+
# takes it (and folds it), or not at all.
|
|
223
|
+
def show(ctx, ask, reading, list)
|
|
224
|
+
@mutex.synchronize do
|
|
225
|
+
next ctx.log.info("no_card", reason: "a new turn started") unless @ask.equal?(ask) && ctx.session_id == ask.session_id
|
|
226
|
+
|
|
227
|
+
@ask = nil
|
|
228
|
+
id = ctx.card(title: title(reading), body: body(reading, list))
|
|
229
|
+
@shown << Shown.new(id: id, question: reading.question)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def title(reading) = reading.ids.empty? ? TITLE : "#{TITLE} · #{reading.ids.join(", ")}"
|
|
234
|
+
|
|
235
|
+
def body(reading, list)
|
|
236
|
+
concerns = reading.ids.map { |id| "Concerns #{id} #{list.find(id).text}" }
|
|
237
|
+
[reading.summary, "**#{reading.question}**", *concerns].join("\n\n")
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# The reply's three parts, however a small model marked them up. Lines after a
|
|
241
|
+
# part's own belong to it.
|
|
242
|
+
# @return [Reading, nil] nil without both a summary and a question
|
|
243
|
+
def parse(reply, list)
|
|
244
|
+
parts = Hash.new { |hash, key| hash[key] = [] }
|
|
245
|
+
current = nil
|
|
246
|
+
reply.to_s.each_line do |line|
|
|
247
|
+
if (part = line.strip.match(PART))
|
|
248
|
+
current = part[1].downcase
|
|
249
|
+
parts[current] << part[2]
|
|
250
|
+
elsif current
|
|
251
|
+
parts[current] << line.strip
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
summary = clean(parts["summary"], SUMMARY_CHARS)
|
|
255
|
+
question = clean(parts["question"], QUESTION_CHARS)
|
|
256
|
+
return nil if summary.empty? || question.empty? || question.match?(NO_QUESTION)
|
|
257
|
+
|
|
258
|
+
Reading.new(summary: summary, question: question, ids: ids(parts["objectives"], list))
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def clean(lines, limit) = cut(lines.join(" ").gsub(/[[:space:]]+/, " ").strip, limit)
|
|
262
|
+
|
|
263
|
+
# Display only: a named id the list has and hasn't dropped.
|
|
264
|
+
def ids(lines, list)
|
|
265
|
+
named = lines.join(" ").scan(OBJECTIVE_ID).map(&:upcase)
|
|
266
|
+
list.items.reject { |item| item.status == "dropped" }.map(&:id) & named
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def cut(text, limit) = text.length > limit ? "#{text[0, limit - 1]}…" : text
|
|
270
|
+
|
|
271
|
+
def positive_number(value)
|
|
272
|
+
number = Float(value.to_s, exception: false)
|
|
273
|
+
number&.positive? ? number : nil
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def positive(value)
|
|
277
|
+
number = Integer(value.to_s, exception: false)
|
|
278
|
+
number&.positive? ? number : nil
|
|
279
|
+
end
|
|
280
|
+
end
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: dev
|
|
3
|
-
version: 0.7.
|
|
3
|
+
version: 0.7.6
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Developer tools"
|
|
6
6
|
trust_level: reviewed
|
|
7
|
-
requires_chi: ">= 0.
|
|
7
|
+
requires_chi: ">= 0.150.0"
|
|
8
8
|
includes:
|
|
9
9
|
- known-names
|
|
10
10
|
- mcp
|
|
@@ -17,3 +17,4 @@ includes:
|
|
|
17
17
|
- codex-peer
|
|
18
18
|
- ruby
|
|
19
19
|
- model-notes
|
|
20
|
+
- decision-card
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: github-pr
|
|
3
|
-
version: 0.6.
|
|
3
|
+
version: 0.6.3
|
|
4
4
|
scope: system
|
|
5
5
|
description: Proposes the branch's open GitHub pull request to a session, attached once you mention it (#N, the branch or its URL), and attaches any PR URL with chi context add or the web's "+ URL"; the agent gets a note when reviews, comments, checks or its state change; in the web, a path:line in an answer links to that line of the PR; the auto_attach setting can attach the PR at once instead, or do nothing
|
|
6
6
|
trust_level: reviewed
|
|
7
|
-
requires_chi: ">= 0.
|
|
7
|
+
requires_chi: ">= 0.150.0"
|
|
8
8
|
needs:
|
|
9
9
|
- command: gh
|
|
10
10
|
why: reads the pull request
|
|
11
11
|
hint: brew install gh, then gh auth login
|
|
12
12
|
plugin:
|
|
13
13
|
file: plugin.rb
|
|
14
|
-
sha256: sha256:
|
|
14
|
+
sha256: sha256:82f8c76d38f314c3503eef3a5de0f8c0b20d0bb5bf4a3daebca43b60d00f8c7a
|
|
15
15
|
scripts:
|
|
16
16
|
pr_context.rb: sha256:a883d2780e89c68c0d7b6d5e0fcb22c273f17bb167e79064e0e99db6bd2ee6f0
|
|
17
17
|
context_providers:
|
|
@@ -5,6 +5,7 @@ require "json"
|
|
|
5
5
|
require "open3"
|
|
6
6
|
require "time"
|
|
7
7
|
require "tmpdir"
|
|
8
|
+
require "samagotchi/git_env"
|
|
8
9
|
|
|
9
10
|
# github-pr: a session on a branch with an open GitHub PR gets the PR
|
|
10
11
|
# proposed as a candidate (docs/context.md, Candidates) when its worker
|
|
@@ -334,7 +335,7 @@ class Plugin
|
|
|
334
335
|
|
|
335
336
|
# stdout of a command that succeeded within COMMAND_SECONDS, else nil.
|
|
336
337
|
def run(ctx, *command, chdir: ctx.cwd)
|
|
337
|
-
Open3.popen2(*command, chdir: chdir, err: File::NULL, pgroup: true) do |stdin, stdout, wait|
|
|
338
|
+
Open3.popen2(Samagotchi::GitEnv::UNSET, *command, chdir: chdir, err: File::NULL, pgroup: true) do |stdin, stdout, wait|
|
|
338
339
|
stdin.close
|
|
339
340
|
reader = Thread.new { stdout.read }
|
|
340
341
|
unless wait.join(COMMAND_SECONDS)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "samagotchi/git_env"
|
|
4
|
+
|
|
3
5
|
# A before_tool_call guard against near-misses of protected identifiers in
|
|
4
6
|
# a tool call's command or paths: the user's home folder name, login, git
|
|
5
7
|
# name and email, the repo name, and any names from config. A local model
|
|
@@ -187,7 +189,7 @@ class KnownNames
|
|
|
187
189
|
end
|
|
188
190
|
|
|
189
191
|
def git_config(root, key)
|
|
190
|
-
IO.popen(["git", "-C", root, "config", "--get", key], err: File::NULL, &:read).to_s.strip
|
|
192
|
+
IO.popen(Samagotchi::GitEnv::UNSET, ["git", "-C", root, "config", "--get", key], err: File::NULL, &:read).to_s.strip
|
|
191
193
|
rescue StandardError
|
|
192
194
|
""
|
|
193
195
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: known-names
|
|
3
|
-
version: 0.1.
|
|
3
|
+
version: 0.1.9
|
|
4
|
+
requires_chi: ">= 0.150.0"
|
|
4
5
|
scope: system
|
|
5
6
|
description: Catches a near-miss of a protected name (home folder, login, git name, repo name, names from config) in a tool call and rejects it with the right spelling; mode ask / correct optional
|
|
6
7
|
trust_level: reviewed
|
|
@@ -8,7 +9,7 @@ files:
|
|
|
8
9
|
known_names.md: sha256:d5348b95b6c655017b541910b36e384220da280818160201f6ec9a50e1e4287c
|
|
9
10
|
hooks:
|
|
10
11
|
known_names.rb:
|
|
11
|
-
sha256: sha256:
|
|
12
|
+
sha256: sha256:d7eed0dd0f418d8099a147ed124c6e6062b8c752035dd6b5aa5e926e29941c8b
|
|
12
13
|
event: before_tool_call
|
|
13
14
|
on_error: log
|
|
14
15
|
priority: 50
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Loaded through module_eval: it requires what it uses.
|
|
4
4
|
require "open3"
|
|
5
|
+
require "samagotchi/git_env"
|
|
5
6
|
require "samagotchi/ref"
|
|
6
7
|
|
|
7
8
|
# An after_turn (and question_asked) hook that links the source refs the
|
|
@@ -621,8 +622,7 @@ class SourceLinks
|
|
|
621
622
|
end
|
|
622
623
|
|
|
623
624
|
def read_remote(dir, name)
|
|
624
|
-
out, status = Open3.capture2(
|
|
625
|
-
"git", "-C", dir, "remote", "get-url", name, err: File::NULL)
|
|
625
|
+
out, status = Open3.capture2(Samagotchi::GitEnv::UNSET, "git", "-C", dir, "remote", "get-url", name, err: File::NULL)
|
|
626
626
|
unless status.success?
|
|
627
627
|
Samagotchi::Log.debug(:hooks, "source_links_no_remote", remote: name, exit: status.exitstatus)
|
|
628
628
|
return nil
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: source-links
|
|
3
|
-
version: 0.6.
|
|
3
|
+
version: 0.6.3
|
|
4
4
|
scope: system
|
|
5
5
|
description: Links source refs (JIRA tickets, GitHub issues, …) in the model's answer — inline links in the web, refs in a waiting question's reply, and a one-line note after the turn (note false turns it off); sources are configured patterns
|
|
6
6
|
trust_level: reviewed
|
|
7
|
-
requires_chi: ">= 0.
|
|
7
|
+
requires_chi: ">= 0.150.0"
|
|
8
8
|
files:
|
|
9
9
|
source_links.md: sha256:ace01a0f7c308a0ae54ece714c31c0e6d7a49c372d98111030e6d268320bed3a
|
|
10
10
|
hooks:
|
|
11
11
|
source_links.rb:
|
|
12
|
-
sha256: sha256:
|
|
12
|
+
sha256: sha256:21d7a9f7c381c653b126b625feed0e0b6422eaa84c8dfe716b6c5b41644a33b3
|
|
13
13
|
events: [after_turn, question_asked]
|
|
14
14
|
on_error: log
|
|
15
15
|
priority: 90
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: samagotchi-system
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.150.0
|
|
4
4
|
scope: system
|
|
5
5
|
description: Default system memories — identity, self map, config modification protocol, memory guide and the delegated-session rules
|
|
6
6
|
files:
|
|
7
7
|
identity.md: sha256:29c2f7df9f6c5acbc5ec17000a4490741e55e426af15ca0d81c8706edd9bd027
|
|
8
|
-
self_map.md: sha256:
|
|
8
|
+
self_map.md: sha256:e695f78f858c29ccfef2ed6f9d6dd86d0ec067ed704a5a1956c01e53617948a1
|
|
9
9
|
config_modification_protocol.md: sha256:8896d1d364ce835040f69b504518d43dbdf959b93fdd9b10f87a8de61cae84a8
|
|
10
|
-
memory_guide.md: sha256:
|
|
10
|
+
memory_guide.md: sha256:bfe9f13e8a6c929056fe531c4c74c7e05d0f17f025191c1a111c8e7dfd224fd6
|
|
11
11
|
delegated.md: sha256:a0d6ddcd4a8be06526d964898611775fd251cfd9d145a9d16afaccaa503597bd
|
|
@@ -114,9 +114,9 @@ Bundles are versioned directories/zips/tar.gz/git URLs with a `manifest.yml` and
|
|
|
114
114
|
| Command | Purpose |
|
|
115
115
|
|---------|---------|
|
|
116
116
|
| `install <source> [--scope system\|project] [--force]` | Install from dir/zip/tar.gz/git URL. `--force` overwrites existing entries, otherwise skips them; a skipped file stays the user's (not recorded, never upgraded or removed by the bundle). Writes provenance to `.bundles/<name>/` (base snapshots + `manifest.json`) and updates `index.md`. Warns on double-brace placeholders and checksum mismatches (strict mode). |
|
|
117
|
-
| `upgrade <source> [--force] [--dry-run] [--agent]` | 3-way merge upgrade (base vs current vs incoming) per `Merger.classify
|
|
117
|
+
| `upgrade <source> [--force] [--dry-run] [--agent]` | 3-way merge upgrade (base vs current vs incoming) per `Merger::Upgrade#outcome` (`Merger.classify`, then a text merge where both sides changed): `install` (new file), `noop` (current==incoming), `fast_forward` (current==base, not edited → auto-update), `keep` (incoming==base → preserve local edits), `merged` (both edited, a text merge with `git` comes out clean → the file takes it, the old one goes to the bundle trash, and entries both sides added under a `Changelog` heading are kept), `conflict` (both edited, a real conflict, or no `git` or base → the file stays untouched and marked, reported as `X stays behind 0.8.0: …` with why and what it missed, needs a retry, `--force`, which copies it to the trash first, or interactive `memory_write` resolution). A marked file is retried at the same version: the system bundle's at each chi start and on `chi update`, another shipped bundle's on `chi update`, any other bundle's on `chi bundle upgrade`. A file marked by an older chi merges normally when its recorded base is a version chi shipped (an installed samagotchi gem still has it); otherwise it merges only if the bundle takes out or rewords no line it still holds, and if it does, it stays behind until resolved by hand, with `--agent` or with `--force`, and the retry can't fix that. Pruned files (removed from new bundle) are kept if locally edited, otherwise warned. |
|
|
118
118
|
| `uninstall <bundle> [--force]` | Moves the bundle's memory files to `.bundles/.trash/<bundle>-<time>/` (refuses locally edited ones unless `--force`; keeps a file another installed bundle also lists), removes their `index.md` lines, deletes the provenance dir. |
|
|
119
|
-
| `status [<bundle>]` | Provenance + per-file `ok|modified|missing|no-index` vs stored checksum and base snapshot; a model overlay (`<name>.<key>.md`) has no index line and reads `ok (model overlay)`. |
|
|
119
|
+
| `status [<bundle>]` | Provenance + per-file `ok|modified|missing|unreadable|no-index` vs stored checksum and base snapshot, and what stays behind or merged at a chi start; `issues=` counts files behind, missing or unreadable, not edits; a model overlay (`<name>.<key>.md`) has no index line and reads `ok (model overlay)`. |
|
|
120
120
|
| `diff <bundle> [file]` | Prints `base` (provenance snapshot) vs `current` (on-disk) for each file. |
|
|
121
121
|
| `list` | Lists installed bundles (`name v<version> scope files installed_at`, plus the shipped version when newer) and the bundles shipped with chi that are not installed (`install <name>` installs one), grouped under their profile; an installed profile shows `includes=` and `left out=`. |
|
|
122
122
|
| `build [--scope system\|project] [--name NAME] [--version VER] [--description DESC] [--out PATH] [FILES...]` | **Inverse of install** — builds a shareable bundle from local memories and installed hooks. Infers `zip` vs `dir`/`tar.gz` from `--out` extension; default `chi_system_memories.zip` (system) or `chi_<repo>_memories.zip` (project, named after the project root) v`1.0.0` in `Dir.pwd`. `FILES...` is an optional allowlist of memory basenames (`identity` or `identity.md`); if omitted, all `*.md` except `index.md`/hidden/non-md and the memories other installed bundles own (each named in a `Left out` line) are included. Installed hooks are copied to `hooks/` with their manifest metadata. Computes `sha256:` checksums and writes `manifest.yml` via `Manifest.write`. No provenance write. |
|
|
@@ -138,7 +138,7 @@ chi bundle install https://github.com/org/bundle.git#v1.2.0 --scope system --for
|
|
|
138
138
|
|
|
139
139
|
# check what would change and upgrade
|
|
140
140
|
chi bundle upgrade ./my-bundle --dry-run
|
|
141
|
-
chi bundle upgrade ./my-bundle #
|
|
141
|
+
chi bundle upgrade ./my-bundle # merges your edits, warns on a real conflict
|
|
142
142
|
|
|
143
143
|
# share again after editing
|
|
144
144
|
chi bundle build --scope system --out updated.zip
|
|
@@ -155,9 +155,9 @@ chi bundle build --scope system --out updated.zip
|
|
|
155
155
|
2. **Prefer project scope** for repo decisions; `system` for cross-project identity/preferences.
|
|
156
156
|
3. **One concept per file:** small files merge and share better than monoliths.
|
|
157
157
|
4. **Use bundles for sharing:** `build` → zip → share → `install`. Do not copy raw `~/.config` paths in docs — give `chi bundle install <url>` instructions.
|
|
158
|
-
5. **Respect local edits:** installs skip existing files by default; use `--force` only when the user explicitly wants overwrite. Upgrades preserve edits (`keep`) or report `conflict
|
|
158
|
+
5. **Respect local edits:** installs skip existing files by default; use `--force` only when the user explicitly wants overwrite. Upgrades preserve edits (`keep`, `merged`) or report `conflict`, leaving the file behind — guide the user to resolve via `memory_read`/`memory_write` or `chi bundle diff <bundle>` + `--force`.
|
|
159
159
|
6. **Keep secrets out:** never write tokens/keys to memories — they are plain files and go into bundles.
|
|
160
160
|
|
|
161
161
|
## Current system bundle
|
|
162
162
|
|
|
163
|
-
`samagotchi-system` (`lib/samagotchi/bundles/system/manifest.yml`) ships `identity.md` + `self_map.md` + `config_modification_protocol.md` + `delegated.md` + this guide itself. It is auto-installed/upgraded on first `Engine` creation (`SystemBundle.ensure!`) — no manual install needed. Its files are managed by chi: a newer chi upgrades them (a
|
|
163
|
+
`samagotchi-system` (`lib/samagotchi/bundles/system/manifest.yml`) ships `identity.md` + `self_map.md` + `config_modification_protocol.md` + `delegated.md` + this guide itself. It is auto-installed/upgraded on first `Engine` creation (`SystemBundle.ensure!`) — no manual install needed. Its files are managed by chi: a newer chi upgrades them (a text merge keeps local edits, and a real conflict leaves the file behind), so put your own preferences in separate memories rather than editing these.
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
`file:line`. Read the source `chi self` reports, not some other checkout on disk.
|
|
84
84
|
- Installed gem files and bundled memories (`identity`, `memory_guide`,
|
|
85
85
|
`config_modification_protocol`, `self_map`, `delegated`) are managed: don't edit them
|
|
86
|
-
(local edits conflict
|
|
86
|
+
(an upgrade merges local edits when it can, and a real conflict leaves the file behind).
|
|
87
87
|
Put notes in my own memories. Guidance for some models only is a model note
|
|
88
88
|
(`model_notes_<name>`, first line `models: <glob>`; memory_guide); a model-only
|
|
89
89
|
addition to one memory is an overlay on it (`current_model_only: true`), and it
|
data/lib/samagotchi/chi_home.rb
CHANGED
|
@@ -6,6 +6,7 @@ require "open3"
|
|
|
6
6
|
require "rbconfig"
|
|
7
7
|
require "time"
|
|
8
8
|
require_relative "atomic_file"
|
|
9
|
+
require_relative "bundler_env"
|
|
9
10
|
require_relative "installed_gem"
|
|
10
11
|
require_relative "log"
|
|
11
12
|
require_relative "paths"
|
|
@@ -33,8 +34,9 @@ module Samagotchi
|
|
|
33
34
|
# Where `mise` usually is when PATH doesn't have it (launchd's bare PATH).
|
|
34
35
|
MISE_DIRS = ["/opt/homebrew/bin", "/usr/local/bin", File.join(Dir.home, ".local", "bin")].freeze
|
|
35
36
|
# What a probe of another Ruby must not inherit: they would move it off
|
|
36
|
-
# its own gems.
|
|
37
|
-
|
|
37
|
+
# its own gems. BUNDLER_SETUP alone does: RubyGems requires what it names at
|
|
38
|
+
# boot, and a bundle with a path then replaces GEM_HOME and GEM_PATH.
|
|
39
|
+
PROBE_UNSET = (%w[GEM_HOME GEM_PATH] + BundlerEnv::CHILD_RUBY).freeze
|
|
38
40
|
PROBE = "require 'json'; print JSON.generate('ruby' => RbConfig.ruby, 'gem_home' => Gem.dir, " \
|
|
39
41
|
"'gem_path' => Gem.path.join(File::PATH_SEPARATOR))"
|
|
40
42
|
GEMSPEC = /\Asamagotchi-(#{Gem::Version::VERSION_PATTERN})\.gemspec\z/
|
data/lib/samagotchi/chi_shim.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require "shellwords"
|
|
5
5
|
require_relative "atomic_file"
|
|
6
|
+
require_relative "bundler_env"
|
|
6
7
|
require_relative "chi_home"
|
|
7
8
|
require_relative "log"
|
|
8
9
|
require_relative "paths"
|
|
@@ -25,7 +26,7 @@ module Samagotchi
|
|
|
25
26
|
# ruby: unset before the exec.
|
|
26
27
|
UNSET = "unset RUBYOPT RUBYLIB RUBYGEMS_GEMDEPS GEMRC LD_PRELOAD LD_LIBRARY_PATH DYLD_INSERT_LIBRARIES " \
|
|
27
28
|
"DYLD_LIBRARY_PATH\n" \
|
|
28
|
-
"for v in $(/usr/bin/env | /usr/bin/sed -n 's/^\\(
|
|
29
|
+
"for v in $(/usr/bin/env | /usr/bin/sed -n 's/^\\(#{BundlerEnv::PREFIX}[A-Za-z0-9_]*\\)=.*/\\1/p'); do unset \"$v\"; done\n"
|
|
29
30
|
|
|
30
31
|
module_function
|
|
31
32
|
|
data/lib/samagotchi/child_env.rb
CHANGED
|
@@ -20,16 +20,23 @@ module Samagotchi
|
|
|
20
20
|
# (Tools::Builtins.parent_env): that session's, not a child chi's.
|
|
21
21
|
SESSION_MARKERS = %w[SAMAGOTCHI_PARENT_SESSION SAMAGOTCHI_SESSION_MODEL SAMAGOTCHI_SESSION_THINKING
|
|
22
22
|
SAMAGOTCHI_SESSION_REASONING_HISTORY].freeze
|
|
23
|
+
# "1" in the chi update an automatic run starts (Web::UpdateOps): no person
|
|
24
|
+
# reads its rows. An env marker, so it reaches the chi a hand-over or the home
|
|
25
|
+
# execs, and an older chi that doesn't know it simply ignores it.
|
|
26
|
+
UNATTENDED_UPDATE = "SAMAGOTCHI_UPDATE_UNATTENDED"
|
|
23
27
|
|
|
24
28
|
module_function
|
|
25
29
|
|
|
26
30
|
# The keys of +env+ a child chi must not inherit: the guardrails
|
|
27
31
|
# settings, the session markers, a smoke's fake "newest installed"
|
|
28
|
-
# (InstalledVersions::ENV_KEY, for the spawner's notices only)
|
|
29
|
-
# worker's project env marker and record (a child worker applies its own)
|
|
32
|
+
# (InstalledVersions::ENV_KEY, for the spawner's notices only), the
|
|
33
|
+
# worker's project env marker and record (a child worker applies its own) and
|
|
34
|
+
# an automatic update's marker (a session someone starts from one isn't it; the
|
|
35
|
+
# update's own hand-over execs, which this doesn't touch).
|
|
30
36
|
# @return [Array<String>]
|
|
31
37
|
def spawner_keys(env = ENV)
|
|
32
|
-
env.each_key.select { |key| key.start_with?(GUARDRAILS_PREFIX) } + SESSION_MARKERS +
|
|
38
|
+
env.each_key.select { |key| key.start_with?(GUARDRAILS_PREFIX) } + SESSION_MARKERS +
|
|
39
|
+
[InstalledVersions::ENV_KEY, ProjectEnv::MARKER_ENV, ProjectEnv::ORIG_ENV, UNATTENDED_UPDATE]
|
|
33
40
|
end
|
|
34
41
|
|
|
35
42
|
# #spawner_keys mapped to nil: Process.spawn unsets a key mapped to nil.
|
data/lib/samagotchi/config.rb
CHANGED
|
@@ -241,6 +241,12 @@ module Samagotchi
|
|
|
241
241
|
Entry.new(key: "context.candidates.offer_threshold", yaml_path: %w[context candidates offer_threshold], type: :float, default: 0.6, expose: %i[config], lifecycle: :next_matching_turn_display_poll_write),
|
|
242
242
|
Entry.new(key: "context.candidates.reask_after_turns", yaml_path: %w[context candidates reask_after_turns], type: :integer, default: 3, expose: %i[config], lifecycle: :next_matching_turn_display_poll_write),
|
|
243
243
|
|
|
244
|
+
# Check this (ObjectivesCheck): a cheap configured model for its one side request; nil is the session's.
|
|
245
|
+
Entry.new(key: "objectives.check_model", yaml_path: %w[objectives check_model], type: :string, default: nil, expose: %i[config], lifecycle: :next_matching_turn_display_poll_write),
|
|
246
|
+
# System 1 on the objectives (ObjectivesAudit): audit saves a yes/no per active objective on what comes in and goes
|
|
247
|
+
# out, acted on by nothing; off (no conversation text goes to a System 1 host for it). Host and model: system_one.*.
|
|
248
|
+
Entry.new(key: "objectives.system_one", yaml_path: %w[objectives system_one], type: :enum, default: "off", expose: %i[config], enum_values: %w[off audit], lifecycle: :next_matching_turn_display_poll_write),
|
|
249
|
+
|
|
244
250
|
# What the model is sent of the conversation (LLMContextStrategy, LLMContextView): none, or a list of layers
|
|
245
251
|
# (stale, forget; "|"-separated or a YAML list); forget (the model's forget_outputs) is experimental.
|
|
246
252
|
# models.<key>.llm_context_strategy and hosts.<name>.llm_context_strategy come first.
|
|
@@ -416,7 +422,7 @@ module Samagotchi
|
|
|
416
422
|
# Names core keeps, as a judgment's first word: a bundle can't declare one and
|
|
417
423
|
# system_one.judgments can't set one (Record and Similar are model-blind, so a
|
|
418
424
|
# swapped model would inherit a clean record and could auto-allow).
|
|
419
|
-
RESERVED_JUDGMENTS = %w[approvals context guardrails toolkit].freeze
|
|
425
|
+
RESERVED_JUDGMENTS = %w[approvals context guardrails objectives toolkit].freeze
|
|
420
426
|
|
|
421
427
|
# What a :bool value reads as (Config.coerce!), YAML 1.1's words and 1/0.
|
|
422
428
|
BOOL_WORDS = { "1" => true, "true" => true, "yes" => true, "on" => true,
|