samagotchi 0.33.0 → 0.35.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 +91 -1
- data/README.md +6 -1
- data/bin/chi +3 -0
- data/docs/broadcast.md +129 -0
- data/docs/cli.md +8 -0
- data/docs/configuration.md +2 -0
- data/docs/context.md +5 -0
- data/docs/guardrails.md +35 -3
- data/docs/hooks.md +25 -4
- data/docs/memory.md +17 -1
- data/docs/plugins.md +78 -3
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +15 -9
- data/lib/samagotchi/bridge/card_store.rb +1 -1
- data/lib/samagotchi/bridge/turn_notice.rb +1 -1
- data/lib/samagotchi/broadcast/recipients.rb +63 -0
- data/lib/samagotchi/broadcast/scope_card.rb +94 -0
- data/lib/samagotchi/broadcast/tags.rb +154 -0
- data/lib/samagotchi/broadcast_command.rb +228 -0
- data/lib/samagotchi/bundles/coordinator/manifest.yml +14 -0
- data/lib/samagotchi/bundles/coordinator/plugin.rb +114 -0
- data/lib/samagotchi/bundles/coordinator/skill_coordinator.md +60 -0
- data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
- data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +11 -0
- data/lib/samagotchi/bundles/guardrails/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/manifest.yml +3 -3
- data/lib/samagotchi/bundles/mcp/plugin.rb +10 -11
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +32 -12
- data/lib/samagotchi/bundles/source-links/manifest.yml +4 -3
- data/lib/samagotchi/bundles/source-links/source_links.md +1 -1
- data/lib/samagotchi/bundles/system/delegated.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/bundles/system/self_map.md +8 -2
- data/lib/samagotchi/children_status.rb +143 -0
- data/lib/samagotchi/config.rb +6 -0
- data/lib/samagotchi/cut_policy.rb +58 -0
- data/lib/samagotchi/engine.rb +45 -108
- data/lib/samagotchi/engine_peers.rb +30 -0
- data/lib/samagotchi/git_head.rb +24 -0
- data/lib/samagotchi/guardrail_wiring.rb +27 -2
- data/lib/samagotchi/guardrails/approvals.rb +7 -0
- data/lib/samagotchi/guardrails/child_boundary.rb +103 -0
- data/lib/samagotchi/guardrails/context.rb +32 -0
- data/lib/samagotchi/guardrails/shared_git.rb +124 -0
- data/lib/samagotchi/guardrails/verdict.rb +4 -3
- data/lib/samagotchi/guardrails.rb +1 -0
- data/lib/samagotchi/hooks/registry.rb +11 -5
- data/lib/samagotchi/kernel_loop.rb +9 -22
- data/lib/samagotchi/llm/chat_loop.rb +11 -22
- data/lib/samagotchi/log_subscriber.rb +1 -1
- data/lib/samagotchi/memory_bundle/builder.rb +14 -1
- data/lib/samagotchi/memory_bundle/installer.rb +6 -2
- data/lib/samagotchi/memory_bundle/manifest.rb +26 -5
- data/lib/samagotchi/memory_bundle/provenance.rb +2 -1
- data/lib/samagotchi/memory_paths.rb +10 -0
- data/lib/samagotchi/note_command.rb +13 -13
- data/lib/samagotchi/note_delivery.rb +42 -0
- data/lib/samagotchi/plugin/api.rb +2 -2
- data/lib/samagotchi/plugin/context.rb +4 -4
- data/lib/samagotchi/plugin/sessions.rb +45 -1
- data/lib/samagotchi/process_group.rb +2 -5
- data/lib/samagotchi/session_manager.rb +4 -3
- data/lib/samagotchi/sessions_command.rb +3 -2
- data/lib/samagotchi/steer_cut.rb +125 -0
- data/lib/samagotchi/system_prompt.rb +21 -8
- data/lib/samagotchi/terminal_ui/attached_loop.rb +25 -2
- data/lib/samagotchi/terminal_ui/status_row.rb +23 -2
- data/lib/samagotchi/terminal_ui.rb +14 -0
- data/lib/samagotchi/tool_declarations.rb +9 -5
- data/lib/samagotchi/tools/builtins.rb +2 -2
- data/lib/samagotchi/tools/delegate.rb +103 -14
- data/lib/samagotchi/tools/delegate_wait.rb +55 -13
- data/lib/samagotchi/tools/send_note.rb +5 -11
- data/lib/samagotchi/turn_note.rb +9 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +3 -0
- data/lib/samagotchi/web/capabilities.rb +19 -0
- data/lib/samagotchi/web/public/activity.js +26 -6
- data/lib/samagotchi/web/public/app.js +515 -288
- data/lib/samagotchi/web/public/card.js +12 -3
- data/lib/samagotchi/web/public/command_view.js +2 -3
- data/lib/samagotchi/web/public/families_view.js +171 -0
- data/lib/samagotchi/web/public/format.js +52 -0
- data/lib/samagotchi/web/public/index.html +94 -21
- data/lib/samagotchi/web/public/live_timing.js +154 -0
- data/lib/samagotchi/web/public/live_turn.js +44 -0
- data/lib/samagotchi/web/public/sessions_list.js +228 -32
- data/lib/samagotchi/web/public/strip.js +5 -2
- data/lib/samagotchi/web/public/turn_html.js +107 -0
- data/lib/samagotchi/web/public/turn_model.js +5 -4
- data/lib/samagotchi/web/public/turn_view.js +43 -129
- data/lib/samagotchi/web/session_summary.rb +4 -0
- data/lib/samagotchi/worker.rb +173 -309
- data/lib/samagotchi/worker_inbound.rb +251 -0
- data/lib/samagotchi/worker_wakes.rb +120 -0
- metadata +24 -1
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "config"
|
|
4
|
+
require_relative "session"
|
|
5
|
+
require_relative "session_inbox"
|
|
6
|
+
require_relative "note_delivery"
|
|
7
|
+
require_relative "broadcast/recipients"
|
|
8
|
+
require_relative "broadcast/scope_card"
|
|
9
|
+
require_relative "broadcast/tags"
|
|
10
|
+
require_relative "cli/command"
|
|
11
|
+
require_relative "cli/flags"
|
|
12
|
+
|
|
13
|
+
module Samagotchi
|
|
14
|
+
# `chi broadcast`: share a note with every session it may concern,
|
|
15
|
+
# without picking them. Each recipient (Broadcast::Recipients) that shares
|
|
16
|
+
# a tag with the note (Broadcast::Tags) gets it as a context note from
|
|
17
|
+
# "broadcast"; --all gives it to every recipient. The rest are listed as
|
|
18
|
+
# skipped, with why. For the user only: refused inside a chi session.
|
|
19
|
+
class BroadcastCommand
|
|
20
|
+
include CLI::Command
|
|
21
|
+
|
|
22
|
+
SOURCE = "broadcast"
|
|
23
|
+
# Bytes kept free under SessionInbox's 16 KiB for the line each note
|
|
24
|
+
# ends with (#body).
|
|
25
|
+
BODY_ROOM = 512
|
|
26
|
+
# The longest "because" #body quotes (a link tag can be long).
|
|
27
|
+
BECAUSE_CHARS = 200
|
|
28
|
+
# The env every execute/task call exports (Tools::Builtins): set, the
|
|
29
|
+
# command runs inside a chi session, i.e. an agent runs it.
|
|
30
|
+
PARENT_ENV = "SAMAGOTCHI_PARENT_SESSION"
|
|
31
|
+
|
|
32
|
+
USAGE = <<~TEXT
|
|
33
|
+
Usage: chi broadcast [-m TEXT] [--all] [--dry-run]
|
|
34
|
+
Shares TEXT (or stdin) with the sessions it may concern, as a context
|
|
35
|
+
note from "broadcast": background the model sees on its next turn,
|
|
36
|
+
not a prompt. It starts no turn.
|
|
37
|
+
Recipients: your own sessions (not delegate children, not scratch
|
|
38
|
+
ones) that a worker or a chi REPL runs now, or that ended a turn in
|
|
39
|
+
the last broadcast.active_hours (8). One gets the note when it shares
|
|
40
|
+
a tag with it: a ticket id (PAY-123) in its branch or your prompts
|
|
41
|
+
there, a pull request (PR #42, its URL) attached to it, a link in
|
|
42
|
+
your prompts there or its attached context. The rest are skipped.
|
|
43
|
+
-m TEXT the note; without it, stdin is read
|
|
44
|
+
--all every recipient, tag or not
|
|
45
|
+
--dry-run show each recipient's scope card, tags and verdict; deliver nothing
|
|
46
|
+
For you, not for an agent: refused inside a chi session.
|
|
47
|
+
One session or a few by id: chi note. See docs/broadcast.md.
|
|
48
|
+
TEXT
|
|
49
|
+
|
|
50
|
+
FLAGS = CLI::Flags.new(help: CLI::Command::HELP_WORDS, args: false) do |f|
|
|
51
|
+
f.switch "--all"
|
|
52
|
+
f.switch "--dry-run"
|
|
53
|
+
f.value "-m", "--message", key: :text
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# One recipient's verdict. +match+: the tag that delivers it (nil with
|
|
57
|
+
# --all or none); +skip+: why it is skipped, else nil.
|
|
58
|
+
Verdict = Data.define(:recipient, :match, :skip) do
|
|
59
|
+
def deliver? = skip.nil?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @param argv [Array<String>] the arguments after "broadcast"
|
|
63
|
+
# @param active_hours [Numeric, nil] broadcast.active_hours (config)
|
|
64
|
+
# @param ticket_pattern [String, nil] broadcast.ticket_pattern (config)
|
|
65
|
+
def initialize(argv, stdin: $stdin, stdout: $stdout, stderr: $stderr, state_dir: nil, env: ENV, now: Time.now,
|
|
66
|
+
active_hours: Config.get("broadcast.active_hours"), ticket_pattern: Config.get("broadcast.ticket_pattern"))
|
|
67
|
+
@argv = argv.dup
|
|
68
|
+
@stdin = stdin
|
|
69
|
+
@stdout = stdout
|
|
70
|
+
@stderr = stderr
|
|
71
|
+
@state_dir = state_dir || Session.default_state_dir
|
|
72
|
+
@env = env
|
|
73
|
+
@now = now
|
|
74
|
+
@active_hours = active_hours || 8
|
|
75
|
+
@ticket = Broadcast::Tags.ticket_regexp(ticket_pattern, warn: ->(line) { @stderr.puts("chi broadcast: #{line}") })
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @return [Integer] exit status: 0 done (skipped sessions included),
|
|
79
|
+
# 1 refused, nothing to send to or a delivery failed, 2 usage
|
|
80
|
+
def run
|
|
81
|
+
options = parse
|
|
82
|
+
return options if options.is_a?(Integer)
|
|
83
|
+
|
|
84
|
+
unless @env[PARENT_ENV].to_s.empty?
|
|
85
|
+
error_line("chi broadcast: chi broadcast is for your user, not an agent")
|
|
86
|
+
return 1
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
text = utf8(options[:text] || read_stdin)
|
|
90
|
+
return usage_error("no note text: pass -m TEXT or pipe it in") unless text
|
|
91
|
+
|
|
92
|
+
begin
|
|
93
|
+
text = SessionInbox.checked_text(text)
|
|
94
|
+
limit = SessionInbox::NOTE_MAX_BYTES - BODY_ROOM
|
|
95
|
+
if text.bytesize > limit
|
|
96
|
+
raise SessionInbox::NoteRejected, "the note is #{text.bytesize} bytes; a broadcast takes up to #{limit} " \
|
|
97
|
+
"(16 KiB less room for the line chi adds)"
|
|
98
|
+
end
|
|
99
|
+
rescue SessionInbox::NoteRejected => e
|
|
100
|
+
error_line("chi broadcast: #{e.message}")
|
|
101
|
+
return 1
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
broadcast(text, all: options[:all], dry_run: options[:dry_run])
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def command_name = "chi broadcast"
|
|
110
|
+
|
|
111
|
+
def parse
|
|
112
|
+
parsed = parse_flags(FLAGS, @argv)
|
|
113
|
+
parsed.is_a?(Integer) ? parsed : parsed.options
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# @return [Integer] exit status
|
|
117
|
+
def broadcast(text, all:, dry_run:)
|
|
118
|
+
recipients = Broadcast::Recipients.list(state_dir: @state_dir, active_hours: @active_hours, now: @now)
|
|
119
|
+
if recipients.empty?
|
|
120
|
+
error_line("chi broadcast: no sessions to send to: none runs now or ended a turn in the last " \
|
|
121
|
+
"#{format_hours(@active_hours)} (chi sessions list --scope=all)")
|
|
122
|
+
return 1
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
note_tags = Broadcast::Tags.of_text(text, from: "note", ticket: @ticket)
|
|
126
|
+
cards = recipients.to_h { |r| [r.id, Broadcast::ScopeCards.build(r, state_dir: @state_dir, ticket: @ticket)] }
|
|
127
|
+
verdicts = recipients.map { |r| verdict(r, cards.fetch(r.id), note_tags, all: all) }
|
|
128
|
+
verdicts = verdicts.select(&:deliver?) + verdicts.reject(&:deliver?)
|
|
129
|
+
|
|
130
|
+
@stdout.puts("broadcast#{" (dry run: nothing is delivered)" if dry_run} #{headline(text)}")
|
|
131
|
+
@stdout.puts("note tags: #{note_tags.empty? ? "none" : note_tags.map(&:label).join(" · ")}") if dry_run
|
|
132
|
+
return dry_run(verdicts, cards) if dry_run
|
|
133
|
+
|
|
134
|
+
deliver_all(verdicts, text)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def verdict(recipient, card, note_tags, all:)
|
|
138
|
+
return Verdict.new(recipient: recipient, match: nil, skip: "open in a chi REPL") if recipient.repl?
|
|
139
|
+
|
|
140
|
+
match = Broadcast::Tags.match(note_tags, card.tags)
|
|
141
|
+
skip = "no tag match (not checked: no triage yet)" unless all || match
|
|
142
|
+
Verdict.new(recipient: recipient, match: match, skip: skip)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def dry_run(verdicts, cards)
|
|
146
|
+
verdicts.each do |v|
|
|
147
|
+
@stdout.puts(line(v.recipient, v.deliver? ? "would get it" : "skipped", reason(v), width: 12))
|
|
148
|
+
@stdout.puts(cards.fetch(v.recipient.id).to_s.gsub(/^/, " "))
|
|
149
|
+
end
|
|
150
|
+
delivered = verdicts.count(&:deliver?)
|
|
151
|
+
@stdout.puts("would deliver #{delivered} · skipped #{verdicts.size - delivered}")
|
|
152
|
+
0
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def deliver_all(verdicts, text)
|
|
156
|
+
ok = true
|
|
157
|
+
lines = verdicts.map do |v|
|
|
158
|
+
next [v.recipient, false, "skipped", v.skip] unless v.deliver?
|
|
159
|
+
|
|
160
|
+
result = NoteDelivery.deliver(v.recipient.id, text: body(text, v.match), source: SOURCE, state_dir: @state_dir)
|
|
161
|
+
next [v.recipient, false, "skipped", "open in a chi REPL"] unless result.delivered?
|
|
162
|
+
|
|
163
|
+
[v.recipient, true, "delivered", "#{reason(v)}#{"; waits for its next start" if result.status == :waits}"]
|
|
164
|
+
rescue SessionInbox::NoteRejected, SystemCallError => e
|
|
165
|
+
ok = false
|
|
166
|
+
[v.recipient, false, "failed", e.message]
|
|
167
|
+
end
|
|
168
|
+
lines.sort_by.with_index { |(_, delivered), i| [delivered ? 0 : 1, i] }.each do |recipient, _, word, why|
|
|
169
|
+
@stdout.puts(line(recipient, word, why))
|
|
170
|
+
end
|
|
171
|
+
delivered = lines.count { |_, d| d }
|
|
172
|
+
@stdout.puts("delivered #{delivered} · skipped #{lines.size - delivered}")
|
|
173
|
+
ok ? 0 : 1
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# What a recipient gets: the user's text first (the terminal's "note
|
|
177
|
+
# from broadcast: …" line shows its start), then a line saying when it
|
|
178
|
+
# was shared (a session with no worker may read it days later; the
|
|
179
|
+
# note's header has the time only) and why it reached this session. No
|
|
180
|
+
# instruction: the system prompt says what a broadcast note asks.
|
|
181
|
+
def body(text, match)
|
|
182
|
+
shared = "Shared by your user on #{@now.localtime.strftime("%Y-%m-%d %H:%M")}"
|
|
183
|
+
return "#{text}\n(#{shared} with every active session.)" unless match
|
|
184
|
+
|
|
185
|
+
because = match.because
|
|
186
|
+
because = "#{because[0, BECAUSE_CHARS - 1]}…" if because.length > BECAUSE_CHARS
|
|
187
|
+
"#{text}\n(#{shared} with the sessions it may concern; it reached you because #{because}.)"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def reason(verdict)
|
|
191
|
+
verdict.skip || verdict.match&.reason || "--all"
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def line(recipient, word, why, width: 9) = "#{recipient.short_id} #{word.ljust(width)} #{why}"
|
|
195
|
+
|
|
196
|
+
# The note's first line, quoted and cut, for the output's head.
|
|
197
|
+
def headline(text)
|
|
198
|
+
lines = text.lines
|
|
199
|
+
first = lines.first.to_s.strip
|
|
200
|
+
first = "#{first[0, 59]}…" if first.length > 60
|
|
201
|
+
first = "#{first} …" if lines.size > 1 && !first.end_with?("…")
|
|
202
|
+
"\"#{first}\""
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def format_hours(hours)
|
|
206
|
+
value = hours.to_f
|
|
207
|
+
"#{value == value.round ? value.round : value} #{value == 1 ? "hour" : "hours"}"
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Only a pipe or a file is read, as chi note does: a terminal or a
|
|
211
|
+
# socket a launcher passes down may never close.
|
|
212
|
+
def read_stdin
|
|
213
|
+
return nil if @stdin.respond_to?(:tty?) && @stdin.tty?
|
|
214
|
+
|
|
215
|
+
if @stdin.respond_to?(:stat)
|
|
216
|
+
stat = @stdin.stat
|
|
217
|
+
return nil unless stat.pipe? || stat.file?
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
@stdin.read
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# The note as UTF-8 whatever the locale says (see NoteCommand#utf8).
|
|
224
|
+
def utf8(text)
|
|
225
|
+
text&.dup&.force_encoding(Encoding::UTF_8)&.scrub
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: coordinator
|
|
3
|
+
version: 0.1.2
|
|
4
|
+
scope: system
|
|
5
|
+
description: "Coordinate parallel work: the coordinator skill (split the work, one worktree and child session per task, check each report, ask before each merge), /coordinate <goal> and /children"
|
|
6
|
+
trust_level: reviewed
|
|
7
|
+
files:
|
|
8
|
+
skill_coordinator.md:
|
|
9
|
+
sha256: sha256:b21067c3f152c8dfd1b1f70001c6654ad1911616543a36b625727fb13d4ce3f6
|
|
10
|
+
description: "Coordinate parallel work: split it, delegate each task wait:false in its own worktree (cwd:), check each report, ask the user before each merge"
|
|
11
|
+
plugin:
|
|
12
|
+
file: plugin.rb
|
|
13
|
+
sha256: sha256:5173faeb6311984ad4f2a40b9052dcc577a90790fc5c5a8409b95d4f114e66a7
|
|
14
|
+
requires_chi: ">= 0.34.0"
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The coordinator bundle (docs/plugins.md, The coordinator bundle): chi as a
|
|
4
|
+
# coordinator of parallel work. Its skill_coordinator memory holds the steps
|
|
5
|
+
# (split the work, one worktree and a wait: false delegate per task, check
|
|
6
|
+
# each report, ask before each merge); this plugin adds two commands.
|
|
7
|
+
#
|
|
8
|
+
# /children [all] shows this session's children as a card, newest first:
|
|
9
|
+
# state, branch, the last reply and whether it was reported (ChildrenStatus
|
|
10
|
+
# through ctx.sessions.children), with a Stop button per running or waiting
|
|
11
|
+
# child and Refresh. `all` adds archived children. The card has one id, so
|
|
12
|
+
# Refresh and Stop replace it. It is read on demand: no thread, no polling.
|
|
13
|
+
#
|
|
14
|
+
# /children stop <id> stops one of this session's own children
|
|
15
|
+
# (ctx.sessions.stop, by session id) and shows the card again.
|
|
16
|
+
#
|
|
17
|
+
# /coordinate <goal> asks the model, in this session, to follow the skill
|
|
18
|
+
# for the goal. A REPL session (--no-shared) takes no messages: the request
|
|
19
|
+
# is shown for the user to send.
|
|
20
|
+
#
|
|
21
|
+
# A child is asked before it changes anything outside its worktree by chi
|
|
22
|
+
# itself (the core child-boundary guardrail), in every mode: nothing to set up.
|
|
23
|
+
class Plugin
|
|
24
|
+
CARD_ID = "children"
|
|
25
|
+
# A card takes 6 actions: Refresh and up to 5 Stops.
|
|
26
|
+
MAX_STOPS = 5
|
|
27
|
+
STOPPABLE = %w[running waiting].freeze
|
|
28
|
+
TEXT_CHARS = 80
|
|
29
|
+
USAGE = "usage: /children [all] | /children stop <id>"
|
|
30
|
+
COORDINATE_USAGE = "usage: /coordinate <goal> — chi splits it into tasks, one child session and worktree each"
|
|
31
|
+
|
|
32
|
+
def initialize(_settings = {}); end
|
|
33
|
+
|
|
34
|
+
def register(chi)
|
|
35
|
+
chi.command "/children", "this session's child sessions: state, branch, last reply; all, stop <id>",
|
|
36
|
+
anytime: true do |args, ctx|
|
|
37
|
+
children_command(args.to_s.strip, ctx)
|
|
38
|
+
end
|
|
39
|
+
chi.command "/coordinate", "run work in parallel: chi splits <goal> into tasks, each in a child session and worktree",
|
|
40
|
+
anytime: true do |args, ctx|
|
|
41
|
+
coordinate(args.to_s.strip, ctx)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def children_command(args, ctx)
|
|
48
|
+
verb, rest = args.split(/\s+/, 2)
|
|
49
|
+
case verb
|
|
50
|
+
when nil then show(ctx, all: false)
|
|
51
|
+
when "all" then rest.to_s.empty? ? show(ctx, all: true) : USAGE
|
|
52
|
+
when "stop" then rest.to_s.strip.empty? ? USAGE : stop(rest.strip, ctx)
|
|
53
|
+
else USAGE
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The card, made or replaced; nil, so the command adds no text of its own.
|
|
58
|
+
def show(ctx, all:, note: nil)
|
|
59
|
+
children = ctx.sessions.children(all: all)
|
|
60
|
+
title = "children of #{ctx.session_id.to_s[0, 8]}"
|
|
61
|
+
title += " (#{children.size})" unless children.empty?
|
|
62
|
+
body = children.empty? ? "no children" : children.map { |child| line(child) }.join("\n")
|
|
63
|
+
body = "#{note}\n\n#{body}" if note
|
|
64
|
+
refresh = all ? "/children all" : "/children"
|
|
65
|
+
stops = children.select { |child| STOPPABLE.include?(child[:state]) }.first(MAX_STOPS)
|
|
66
|
+
.map { |child| { label: "Stop #{child[:short_id]}", command: "/children stop #{child[:short_id]}" } }
|
|
67
|
+
ctx.card(id: CARD_ID, title: title, body: body, actions: [{ label: "Refresh", command: refresh }, *stops])
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# - `ab12cd34` · running · fix/flaky · "fix the flaky spec…"
|
|
72
|
+
# - `9a8b7c6d` · done · feat/x · reported · "All 12 specs pass…"
|
|
73
|
+
# - `77aa66bb` · waiting (approval) · open it: chi --attach 77aa66bb
|
|
74
|
+
def line(child)
|
|
75
|
+
parts = ["`#{child[:short_id]}`", child[:waiting] ? "waiting (#{child[:waiting]})" : child[:state]]
|
|
76
|
+
parts << child[:branch] if child[:branch]
|
|
77
|
+
parts << "fork" unless child[:delegate]
|
|
78
|
+
parts << "archived" if child[:archived]
|
|
79
|
+
if child[:waiting]
|
|
80
|
+
parts << "open it: chi --attach #{child[:short_id]}"
|
|
81
|
+
elsif child[:last_reply]
|
|
82
|
+
parts << (child[:reported] ? "reported" : "not reported yet")
|
|
83
|
+
parts << quote(child[:last_reply])
|
|
84
|
+
elsif !child[:title].to_s.strip.empty?
|
|
85
|
+
parts << quote(child[:title])
|
|
86
|
+
end
|
|
87
|
+
"- #{parts.join(" · ")}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def stop(id, ctx)
|
|
91
|
+
stopped = ctx.sessions.stop(id)
|
|
92
|
+
show(ctx, all: false, note: "stopped #{stopped[0, 8]}")
|
|
93
|
+
rescue Samagotchi::Plugin::Sessions::Error => e
|
|
94
|
+
"/children stop: #{e.message}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def coordinate(goal, ctx)
|
|
98
|
+
return COORDINATE_USAGE if goal.empty?
|
|
99
|
+
|
|
100
|
+
request = "Read the skill_coordinator memory and follow it for this goal:\n\n#{goal}"
|
|
101
|
+
begin
|
|
102
|
+
ctx.sessions.send(ctx.session_id, request)
|
|
103
|
+
rescue Samagotchi::Plugin::Sessions::Error => e
|
|
104
|
+
return "/coordinate: #{e.message}. Send this yourself:\n\n#{request}"
|
|
105
|
+
end
|
|
106
|
+
"asked chi to coordinate it; a running turn gets it at its next step"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def quote(text)
|
|
110
|
+
line = text.to_s.gsub(/\s+/, " ").strip
|
|
111
|
+
line = "#{line[0, TEXT_CHARS - 1]}…" if line.length > TEXT_CHARS
|
|
112
|
+
"\"#{line}\""
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Skill: coordinator
|
|
2
|
+
|
|
3
|
+
Use when the user hands over work with several independent parts, or asks to "run it in parallel",
|
|
4
|
+
"delegate", "coordinate". The user decides; you do the legwork and keep them in the loop briefly.
|
|
5
|
+
|
|
6
|
+
## Steps
|
|
7
|
+
1. Find the repository's default branch, <default> below: `git symbolic-ref --short
|
|
8
|
+
refs/remotes/origin/HEAD` (drop the `origin/`), else `main` or `master`, whichever exists; ask the
|
|
9
|
+
user if unsure. Split the work into independent tasks (at most session.max_children at once,
|
|
10
|
+
default 4). Tell the user the split in a few lines and go on unless they object.
|
|
11
|
+
2. For each task: from your own folder, `git worktree add ../<repo>-<task> -b <type>/<task>` with
|
|
12
|
+
execute; then `delegate` with `wait: false`, `cwd:` that worktree's absolute path, and a
|
|
13
|
+
self-contained task. The task starts with "Work only in <absolute worktree path>: every cd, read,
|
|
14
|
+
write, test and commit goes there, never the project root" and says what to report (the result,
|
|
15
|
+
evidence: commands and what they printed, what isn't done) and "commit on the branch, don't merge,
|
|
16
|
+
don't push".
|
|
17
|
+
3. Tell the user what started (one line per child: its id, branch and task) and end your turn or keep
|
|
18
|
+
talking. Don't call delegate_result: chi brings each child's reply as a delegate report, also when
|
|
19
|
+
you are idle.
|
|
20
|
+
4. On each report, check it before telling the user: `git -C <worktree> log --oneline <default>..` and
|
|
21
|
+
`git diff --stat <default>...<branch>`; run the tests the child names (execute with cwd: the worktree).
|
|
22
|
+
Say the result in one or two lines: done and verified / done with doubts / failed.
|
|
23
|
+
5. Then one of: a follow-up (`delegate session: <id>`, narrower, `wait: false`; then end your turn:
|
|
24
|
+
its report comes by itself, so don't wait for it with delegate_result or task_wait), stop it
|
|
25
|
+
(`chi sessions stop <session id>` with execute; task_stop is for task ids, not children; the user
|
|
26
|
+
can use /children), or tell the user it is ready to merge.
|
|
27
|
+
6. Merging is the user's call: ask (ask_user_question) per branch, "Merge <branch> into <default>?".
|
|
28
|
+
Before merging, in your own folder: `git branch --show-current` must print <default> (if it
|
|
29
|
+
doesn't, stop and ask the user; don't switch branches yourself); then check <default>:
|
|
30
|
+
`git log --oneline -3 <default>` and `git merge-base --is-ancestor <branch> <default>`; if the branch is already in, say so and don't merge
|
|
31
|
+
again. Merge with `git merge --ff-only <branch>` yourself, in your own folder. If that fails, ask the
|
|
32
|
+
child to rebase its branch on <default> (a follow-up that says "rebase only, don't merge") or ask the
|
|
33
|
+
user; don't rebase it from here (the branch is checked out in the child's worktree). Children
|
|
34
|
+
never merge. Never push unless asked.
|
|
35
|
+
7. Clean up only a branch that was merged or that the user said to drop: stop its child
|
|
36
|
+
(`chi sessions stop <session id>`), then `git worktree remove ../<repo>-<task>`, then
|
|
37
|
+
`git branch -d <branch>` (in that order: a branch checked out in a worktree can't be deleted). If
|
|
38
|
+
`-d` refuses, say why to the user; never `-D` unless they say so. Remove only worktrees and
|
|
39
|
+
branches you created in this session. Every other branch stays as it is, its child, worktree and
|
|
40
|
+
branch too (the user said "keep it", or didn't say): leave them and tell the user they remain.
|
|
41
|
+
8. When all are done, report: what merged, what didn't and why, what you cleaned up and what remains,
|
|
42
|
+
follow-ups found but not fixed. Report only what the commands' output showed: a step you planned
|
|
43
|
+
but didn't run, or whose output you didn't see, is not done; say so.
|
|
44
|
+
|
|
45
|
+
## Gotchas
|
|
46
|
+
- Children are asked before changing anything outside their worktree (your checkout, a sibling's):
|
|
47
|
+
the user approves it on the child's card. Still check where commits landed (step 4).
|
|
48
|
+
- Never kill processes by a pid from a file or an old note; stop children with `chi sessions stop
|
|
49
|
+
<session id>` (execute) and background tasks with task_stop (task ids only: it doesn't stop a child).
|
|
50
|
+
- A child's tool approvals go to the user on the child's card; tell the user when a report says a
|
|
51
|
+
child waits for one.
|
|
52
|
+
- A report is the child's claim: verify it (step 4) before telling the user it is done, and check
|
|
53
|
+
where its commits landed: `git log --oneline <default>..<branch>` has them and `git status` in your own
|
|
54
|
+
folder is clean. A child that worked in your folder instead of its worktree: tell the user.
|
|
55
|
+
- Don't send a follow-up to a child that is still running; wait for its report.
|
|
56
|
+
- Don't start more children than the user asked for.
|
|
57
|
+
|
|
58
|
+
## Changelog
|
|
59
|
+
- 2026-10-06 created (coordinator bundle 0.1.0)
|
|
60
|
+
- 2026-10-06 clean up only merged or dropped branches; report only what commands showed (0.1.2)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: dev
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.3.0
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Developer tools"
|
|
6
6
|
trust_level: reviewed
|
|
7
|
-
requires_chi: ">= 0.
|
|
7
|
+
requires_chi: ">= 0.34.0"
|
|
8
8
|
includes:
|
|
9
9
|
- known-names
|
|
10
10
|
- mcp
|
|
@@ -12,3 +12,4 @@ includes:
|
|
|
12
12
|
- skills
|
|
13
13
|
- source-links
|
|
14
14
|
- github-pr
|
|
15
|
+
- coordinator
|
|
@@ -128,6 +128,17 @@ rules:
|
|
|
128
128
|
scopes: [once]
|
|
129
129
|
reason: attaches a command chi will run every few minutes, outside the guardrails
|
|
130
130
|
|
|
131
|
+
# ── chi broadcast: for the user, not an agent ─────────────────────────
|
|
132
|
+
# chi broadcast refuses to run inside a session (SAMAGOTCHI_PARENT_SESSION
|
|
133
|
+
# is set there), but an agent can unset that; this asks every time, and
|
|
134
|
+
# only the user may allow it. Quoted words and continuations still match.
|
|
135
|
+
- id: chi-broadcast
|
|
136
|
+
tool: shell
|
|
137
|
+
command: '\bchi["'']?[\s\\]+["'']?broadcast\b'
|
|
138
|
+
verdict: ask
|
|
139
|
+
scopes: [once]
|
|
140
|
+
reason: shares a note with every session it may concern; chi broadcast is for the user, not an agent
|
|
141
|
+
|
|
131
142
|
# ── answering chi's own questions around chi answer ───────────────────
|
|
132
143
|
# A parent agent answers a child's approval with chi answer, which holds it
|
|
133
144
|
# to guardrails.parent_approvals. These shapes go around that: chi --attach
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: guardrails
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.7.0
|
|
4
4
|
scope: system
|
|
5
|
-
description: Optional default guardrail rules (git push/rewrites, rm -rf wide, curl | sh, git hooks, chi's own config and attached context from the shell, chi context add --cmd, answering chi around chi answer; in strict mode also git rebase and writes and git in another checkout outside the session's repo), asks only small models get (git checkout --/restore, git stash drop), and a note for the model
|
|
5
|
+
description: Optional default guardrail rules (git push/rewrites, rm -rf wide, curl | sh, git hooks, chi's own config and attached context from the shell, chi context add --cmd, chi broadcast, answering chi around chi answer; in strict mode also git rebase and writes and git in another checkout outside the session's repo), asks only small models get (git checkout --/restore, git stash drop), and a note for the model
|
|
6
6
|
trust_level: reviewed
|
|
7
7
|
# models: in small-models.yml (guardrails.small_models) needs 0.8.1; git: in rules.yml needs 0.17.0;
|
|
8
8
|
# skip_read_only:, touches:, rm: and modes: need 0.20.0
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mcp
|
|
3
|
-
version: 0.4.
|
|
3
|
+
version: 0.4.5
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Tools from MCP servers (stdio): each server in config.yml bundles: mcp: servers: runs for the session, its tools are mcp_<server>_<tool>; /mcp lists them"
|
|
6
6
|
trust_level: reviewed
|
|
7
7
|
files: {}
|
|
8
8
|
plugin:
|
|
9
9
|
file: plugin.rb
|
|
10
|
-
sha256: sha256:
|
|
11
|
-
requires_chi: ">= 0.
|
|
10
|
+
sha256: sha256:1ebedc86cd21c918766c794015b6bd16c397c0f7e2734d939d8310d446b8a9a1
|
|
11
|
+
requires_chi: ">= 0.33.0"
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
require "digest"
|
|
32
32
|
require "json"
|
|
33
33
|
require "open3"
|
|
34
|
+
require "samagotchi/process_group"
|
|
34
35
|
require "time"
|
|
35
36
|
require "shellwords"
|
|
36
37
|
require "tmpdir"
|
|
@@ -59,6 +60,9 @@ class Plugin
|
|
|
59
60
|
class Dead < Error; end
|
|
60
61
|
|
|
61
62
|
POLL_SECONDS = 0.1
|
|
63
|
+
# How long close waits for the server after closing its stdin, after
|
|
64
|
+
# TERM, and after KILL.
|
|
65
|
+
STOP_GRACE_SECONDS = 1
|
|
62
66
|
|
|
63
67
|
attr_reader :pid
|
|
64
68
|
|
|
@@ -136,14 +140,15 @@ class Plugin
|
|
|
136
140
|
end
|
|
137
141
|
|
|
138
142
|
# End the process: stdin closed first (most servers leave then), then
|
|
139
|
-
# TERM and KILL to its process group
|
|
143
|
+
# TERM and KILL to its process group (chi's ProcessGroup), a second's
|
|
144
|
+
# grace each.
|
|
140
145
|
def close
|
|
141
146
|
@mutex.synchronize { @closing = true }
|
|
142
147
|
[@stdin].each { |io| io.close unless io.closed? }
|
|
143
|
-
unless @wait.join(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
@wait.join(
|
|
148
|
+
unless @wait.join(STOP_GRACE_SECONDS)
|
|
149
|
+
Samagotchi::ProcessGroup.stop(@pid, grace: STOP_GRACE_SECONDS, poll: STOP_GRACE_SECONDS,
|
|
150
|
+
stopped: -> { !@wait.alive? }, wait: ->(seconds) { @wait.join(seconds) })
|
|
151
|
+
@wait.join(STOP_GRACE_SECONDS)
|
|
147
152
|
end
|
|
148
153
|
[@stdout, @stderr].each { |io| io.close unless io.closed? }
|
|
149
154
|
[@reader, @err_reader].each { |t| t.join(1) }
|
|
@@ -234,12 +239,6 @@ class Plugin
|
|
|
234
239
|
@on_exit&.call(reason) unless closing
|
|
235
240
|
end
|
|
236
241
|
|
|
237
|
-
def signal(name)
|
|
238
|
-
Process.kill(name, -@pid)
|
|
239
|
-
rescue Errno::ESRCH, Errno::EPERM
|
|
240
|
-
nil
|
|
241
|
-
end
|
|
242
|
-
|
|
243
242
|
def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
244
243
|
end
|
|
245
244
|
|
|
@@ -8,7 +8,9 @@ require "open3"
|
|
|
8
8
|
# web, each ref in the answer becomes a link (`[JIRA-123](https://…)`,
|
|
9
9
|
# through event[:present]: display only, the model's text stays as it was,
|
|
10
10
|
# and it survives a reload). Every UI also gets one line right after the
|
|
11
|
-
# turn, the terminals' only view of the links:
|
|
11
|
+
# turn, the terminals' only view of the links (marked fallback_for:
|
|
12
|
+
# :display when the answer links every URL it names, so the web with
|
|
13
|
+
# markdown on leaves it out; needs chi 0.35.0):
|
|
12
14
|
#
|
|
13
15
|
# sources: JIRA JIRA-123 → https://myjira.com/browse/JIRA-123, JIRA JIRA-10 → https://myjira.com/browse/JIRA-10
|
|
14
16
|
#
|
|
@@ -48,8 +50,8 @@ require "open3"
|
|
|
48
50
|
# of these stays text (warned at load).
|
|
49
51
|
#
|
|
50
52
|
# The note is not stored in the conversation: it is an event, replayed by a
|
|
51
|
-
# UI
|
|
52
|
-
#
|
|
53
|
+
# UI with the session's cards (a reload keeps it, after the worker stopped
|
|
54
|
+
# too: cards.json). The links are stored as the answer's display. A ref in
|
|
53
55
|
# code (a `span` or a fenced block) or in a markdown link is not linked in
|
|
54
56
|
# the answer. The hook is inert until sources are configured.
|
|
55
57
|
class SourceLinks
|
|
@@ -114,17 +116,34 @@ class SourceLinks
|
|
|
114
116
|
return if text.nil? || text.empty?
|
|
115
117
|
|
|
116
118
|
hits = occurrences(text[0, MAX_SCAN])
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
119
|
+
linked = present_links(event[:present], text, hits)
|
|
120
|
+
return unless @note
|
|
121
|
+
|
|
122
|
+
found = collect(hits)
|
|
123
|
+
return if found.empty?
|
|
124
|
+
|
|
125
|
+
# The display links every URL the note names: the note only stands in
|
|
126
|
+
# for it, and a UI that renders the display's links leaves it out.
|
|
127
|
+
covered = linked && found.all? { |_name, _ref, url| linked.key?(url.downcase) }
|
|
128
|
+
event[:notify]&.call(line(found), level: :info, fallback_for: covered ? :display : nil)
|
|
124
129
|
end
|
|
125
130
|
|
|
126
131
|
private
|
|
127
132
|
|
|
133
|
+
# Links the refs in the answer as shown: the model's text unless an
|
|
134
|
+
# earlier hook changed it (then its offsets differ, so it is scanned
|
|
135
|
+
# again). The URLs it linked (downcased, as #collect dedups), or nil when
|
|
136
|
+
# the display didn't take them (no event[:present], nothing to present, or
|
|
137
|
+
# the presenter rejected the text).
|
|
138
|
+
def present_links(present, text, hits)
|
|
139
|
+
return nil unless present
|
|
140
|
+
|
|
141
|
+
linked = {}
|
|
142
|
+
ours = nil
|
|
143
|
+
shown = present.call { |display| ours = link(display, display == text ? hits : nil, linked) }
|
|
144
|
+
ours.nil? || shown != ours ? nil : linked
|
|
145
|
+
end
|
|
146
|
+
|
|
128
147
|
# The content of the last `role: "model"` message, or nil. Handles both
|
|
129
148
|
# string- and symbol-keyed messages. A turn that ended without a visible
|
|
130
149
|
# answer stores a `kind: turn_note` system message instead, so the role
|
|
@@ -198,8 +217,8 @@ class SourceLinks
|
|
|
198
217
|
|
|
199
218
|
# +text+ with each ref as a markdown link, every occurrence; the part
|
|
200
219
|
# beyond MAX_SCAN stays as it is. +hits+ are the scan of that text when
|
|
201
|
-
# the caller has it.
|
|
202
|
-
def link(text, hits = nil)
|
|
220
|
+
# the caller has it; +linked+ gets each linked URL, downcased.
|
|
221
|
+
def link(text, hits = nil, linked = {})
|
|
203
222
|
head = text[0, MAX_SCAN]
|
|
204
223
|
hits ||= occurrences(head)
|
|
205
224
|
out = +""
|
|
@@ -208,6 +227,7 @@ class SourceLinks
|
|
|
208
227
|
next if hit[:quiet] || hit[:start] < pos # two sources on one ref: the first wins
|
|
209
228
|
|
|
210
229
|
out << head[pos...hit[:start]] << "[#{hit[:ref].gsub(/[\[\]]/) { |c| "\\#{c}" }}](#{link_target(hit[:url])})"
|
|
230
|
+
linked[hit[:url].downcase] = true
|
|
211
231
|
pos = hit[:finish]
|
|
212
232
|
end
|
|
213
233
|
return text if pos.zero?
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: source-links
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.4.0
|
|
4
4
|
scope: system
|
|
5
5
|
description: Links source refs (JIRA tickets, GitHub issues, …) in the model's answer — inline links in the web, 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.35.0"
|
|
7
8
|
files:
|
|
8
|
-
source_links.md: sha256:
|
|
9
|
+
source_links.md: sha256:1670a3051b67610f41a680fc8e61bc1c99657a87f0f32121d040e05512896748
|
|
9
10
|
hooks:
|
|
10
11
|
source_links.rb:
|
|
11
|
-
sha256: sha256:
|
|
12
|
+
sha256: sha256:8ce4d11659f4bc151167f99e47299c0f6ef246797c9167c609c1699e3edea2e5
|
|
12
13
|
event: after_turn
|
|
13
14
|
on_error: log
|
|
14
15
|
priority: 90
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Source links
|
|
2
2
|
|
|
3
|
-
The `source-links` bundle's `after_turn` hook links the source refs (a JIRA ticket, a GitHub issue, …) in your answers. In the web, each ref in the answer becomes a link; that is how the answer is **shown**, not what you wrote: your own message keeps the plain ref. A `sources: NAME ref → url, …` line right after an answer is the same hook's note
|
|
3
|
+
The `source-links` bundle's `after_turn` hook links the source refs (a JIRA ticket, a GitHub issue, …) in your answers. In the web, each ref in the answer becomes a link; that is how the answer is **shown**, not what you wrote: your own message keeps the plain ref. A `sources: NAME ref → url, …` line right after an answer is the same hook's note (the terminals only see this line). The web leaves it out when markdown is on and the answer links every ref it names; otherwise every UI shows it. The line is **not part of the conversation**: it is an event, so it is not in the session file and you cannot refer back to it. A UI replays it with the session's cards (a page reload keeps it, after the worker stopped too).
|
|
4
4
|
|
|
5
5
|
The refs come from the `bundles: source-links:` section of config.yml: each source is a `prefix:` + `base_url:` pair (simple) or a `pattern:` regex + `url:` template (full form), plus an optional `max:` for refs per line and `note: false` to drop the line (the web links stay). A `url:` template can take the project's own repo from its git remote, so a bare `#12` links to this project's issue and `owner/repo#12` to that repo's. With no sources configured the hook does nothing. A ref already inside a URL is not linked again, and a ref in code or in a markdown link is not linked in the answer.
|
|
@@ -5,6 +5,6 @@ Another chi session (the parent) gave you this task. It reads only your last mes
|
|
|
5
5
|
- Finish the task in this turn if you can. End with one reply the parent can act on without asking back: the result first, then evidence (paths, commands run and what they printed), then anything you could not verify or finish.
|
|
6
6
|
- Do only what the task asks. A question ("why does this spec fail?", "what calls X?", "check whether…") gets a report, not file changes: say what you found and what change you would make, and leave making it to the parent. Edit files only when the task asks you to change, fix, add or write something, and then only the files that takes.
|
|
7
7
|
- The parent may send follow-up messages later; each one is a new turn in this same session, with what you did so far.
|
|
8
|
-
- Work in the directory you were started in. Don't change config, install bundles, edit managed files or write memories unless the task asks for it.
|
|
8
|
+
- Work in the directory you were started in: every cd, read, edit, test and commit stays in it (in a linked worktree, never the repository's other checkouts). Don't change config, install bundles, edit managed files or write memories unless the task asks for it.
|
|
9
9
|
- You can't delegate further. Don't start other chi sessions, and send notes only to the parent.
|
|
10
10
|
- If you are blocked, stop and say so in the reply with the exact question. Use ask_user_question only when the task says the user is watching.
|