agentilda 2.0.1 → 2.0.4
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/lib/agentilda/agents/agent.rb +79 -0
- data/lib/agentilda/agents/registry.rb +121 -0
- data/lib/agentilda/agents/roster.rb +92 -0
- data/lib/agentilda/agents.rb +11 -113
- data/lib/agentilda/cli/agents/subcommands/describe.rb +3 -3
- data/lib/agentilda/cli/agents/subcommands/list.rb +1 -1
- data/lib/agentilda/cli/base.rb +3 -3
- data/lib/agentilda/cli/create/create.rb +17 -18
- data/lib/agentilda/cli/docs/docs.rb +1 -1
- data/lib/agentilda/cli/index/index.rb +1 -1
- data/lib/agentilda/cli/linear/subcommands/import.rb +1 -1
- data/lib/agentilda/cli/list_plans/list_plans.rb +1 -1
- data/lib/agentilda/cli/mail/mail.rb +4 -4
- data/lib/agentilda/cli/mail/subcommands/send.rb +1 -1
- data/lib/agentilda/cli/resync/subcommands/dirs.rb +2 -2
- data/lib/agentilda/cli/resync/subcommands/prs.rb +3 -3
- data/lib/agentilda/cli/run/run.rb +40 -40
- data/lib/agentilda/cli/states/states.rb +1 -1
- data/lib/agentilda/cli/unblock/unblock.rb +17 -17
- data/lib/agentilda/cli/worktree/worktree.rb +4 -4
- data/lib/agentilda/cli.rb +1 -1
- data/lib/agentilda/engine/board.rb +93 -0
- data/lib/agentilda/engine/dispatcher.rb +646 -0
- data/lib/agentilda/engine/progress_log.rb +125 -0
- data/lib/agentilda/engine/runner.rb +141 -0
- data/lib/agentilda/engine/state_file.rb +176 -0
- data/lib/agentilda/engine/tally.rb +177 -0
- data/lib/agentilda/engine.rb +19 -0
- data/lib/agentilda/execution/child.rb +94 -0
- data/lib/agentilda/execution/clock.rb +116 -0
- data/lib/agentilda/execution/control.rb +180 -0
- data/lib/agentilda/execution/executor.rb +699 -0
- data/lib/agentilda/execution/transcript.rb +468 -0
- data/lib/agentilda/execution.rb +18 -0
- data/lib/agentilda/lifecycle/adoption.rb +197 -0
- data/lib/agentilda/lifecycle/brief.rb +285 -0
- data/lib/agentilda/lifecycle/creator.rb +125 -0
- data/lib/agentilda/lifecycle/resync.rb +428 -0
- data/lib/agentilda/lifecycle/unblocker.rb +223 -0
- data/lib/agentilda/lifecycle.rb +28 -0
- data/lib/agentilda/linear/api.rb +1 -1
- data/lib/agentilda/linear/attribution.rb +7 -7
- data/lib/agentilda/linear/import.rb +18 -18
- data/lib/agentilda/linear/mapping.rb +6 -6
- data/lib/agentilda/linear/push.rb +5 -5
- data/lib/agentilda/linear/survey.rb +7 -7
- data/lib/agentilda/linear/unit.rb +5 -5
- data/lib/agentilda/plans/dev_work.rb +56 -0
- data/lib/agentilda/plans/feature.rb +131 -0
- data/lib/agentilda/plans/index.rb +208 -0
- data/lib/agentilda/plans/ledger.rb +235 -0
- data/lib/agentilda/plans/mailbox.rb +143 -0
- data/lib/agentilda/plans/ordinal.rb +92 -0
- data/lib/agentilda/plans/pull_request.rb +29 -0
- data/lib/agentilda/plans/pull_requests.rb +229 -0
- data/lib/agentilda/plans/state_machine.rb +383 -0
- data/lib/agentilda/plans/status.rb +365 -0
- data/lib/agentilda/plans/subject.rb +131 -0
- data/lib/agentilda/plans/tree.rb +79 -0
- data/lib/agentilda/plans.rb +39 -0
- data/lib/agentilda/presentation/console.rb +159 -0
- data/lib/agentilda/presentation/dashboard.rb +221 -0
- data/lib/agentilda/presentation/diagram.rb +149 -0
- data/lib/agentilda/presentation/documentation.rb +439 -0
- data/lib/agentilda/presentation/keyboard.rb +158 -0
- data/lib/agentilda/presentation/reporter.rb +181 -0
- data/lib/agentilda/presentation/screen/ratatui/bar.rb +59 -0
- data/lib/agentilda/presentation/screen/ratatui/key_translator.rb +31 -0
- data/lib/agentilda/presentation/screen/ratatui.rb +320 -0
- data/lib/agentilda/presentation/viewer.rb +62 -0
- data/lib/agentilda/presentation.rb +23 -0
- data/lib/agentilda/{ui.rb → support/ui.rb} +5 -5
- data/lib/agentilda/vcs/github.rb +170 -0
- data/lib/agentilda/vcs/publisher.rb +177 -0
- data/lib/agentilda/vcs/worktree.rb +209 -0
- data/lib/agentilda/vcs.rb +19 -0
- data/lib/agentilda/version.rb +1 -1
- data/lib/agentilda.rb +6 -5
- metadata +56 -49
- data/lib/agentilda/adoption.rb +0 -197
- data/lib/agentilda/agent.rb +0 -77
- data/lib/agentilda/board.rb +0 -91
- data/lib/agentilda/brief.rb +0 -283
- data/lib/agentilda/child.rb +0 -92
- data/lib/agentilda/clock.rb +0 -114
- data/lib/agentilda/console.rb +0 -157
- data/lib/agentilda/control.rb +0 -178
- data/lib/agentilda/creator.rb +0 -125
- data/lib/agentilda/dashboard.rb +0 -219
- data/lib/agentilda/dev_work.rb +0 -56
- data/lib/agentilda/diagram.rb +0 -149
- data/lib/agentilda/dispatcher.rb +0 -646
- data/lib/agentilda/documentation.rb +0 -437
- data/lib/agentilda/executor.rb +0 -697
- data/lib/agentilda/feature.rb +0 -132
- data/lib/agentilda/github.rb +0 -168
- data/lib/agentilda/index.rb +0 -206
- data/lib/agentilda/keyboard.rb +0 -156
- data/lib/agentilda/ledger.rb +0 -233
- data/lib/agentilda/mailbox.rb +0 -141
- data/lib/agentilda/ordinal.rb +0 -90
- data/lib/agentilda/progress_log.rb +0 -126
- data/lib/agentilda/publisher.rb +0 -175
- data/lib/agentilda/pull_request.rb +0 -27
- data/lib/agentilda/pull_requests.rb +0 -230
- data/lib/agentilda/reporter.rb +0 -181
- data/lib/agentilda/resync.rb +0 -426
- data/lib/agentilda/roster.rb +0 -93
- data/lib/agentilda/runner.rb +0 -139
- data/lib/agentilda/screen/ratatui/bar.rb +0 -57
- data/lib/agentilda/screen/ratatui/key_translator.rb +0 -29
- data/lib/agentilda/screen/ratatui.rb +0 -318
- data/lib/agentilda/state_file.rb +0 -174
- data/lib/agentilda/state_machine.rb +0 -383
- data/lib/agentilda/status.rb +0 -363
- data/lib/agentilda/subject.rb +0 -133
- data/lib/agentilda/tally.rb +0 -175
- data/lib/agentilda/transcript.rb +0 -466
- data/lib/agentilda/tree.rb +0 -81
- data/lib/agentilda/unblocker.rb +0 -221
- data/lib/agentilda/viewer.rb +0 -60
- data/lib/agentilda/worktree.rb +0 -207
- /data/lib/agentilda/{config.rb → support/config.rb} +0 -0
- /data/lib/agentilda/{frontmatter.rb → support/frontmatter.rb} +0 -0
- /data/lib/agentilda/{markdown.rb → support/markdown.rb} +0 -0
|
@@ -21,8 +21,8 @@ module Agentilda
|
|
|
21
21
|
# @param options [Hash]
|
|
22
22
|
# @return [void]
|
|
23
23
|
def call(**options)
|
|
24
|
-
changes = Agentilda::
|
|
25
|
-
|
|
24
|
+
changes = Agentilda::Lifecycle.resync_dirs(tree: tree_for(options))
|
|
25
|
+
.call(commit: commit?(options))
|
|
26
26
|
|
|
27
27
|
if changes.empty?
|
|
28
28
|
success("Every folder is already named NNN.MM-<emoji> → <slug> and the emoji matches.") unless quiet?(options)
|
|
@@ -31,13 +31,13 @@ module Agentilda
|
|
|
31
31
|
# @param options [Hash]
|
|
32
32
|
# @return [void]
|
|
33
33
|
def call(**options)
|
|
34
|
-
github =
|
|
34
|
+
github = Vcs.github
|
|
35
35
|
tree = tree_for(options)
|
|
36
|
-
changes = Agentilda::
|
|
36
|
+
changes = Agentilda::Lifecycle.resync_prs(tree:,
|
|
37
37
|
github:,
|
|
38
38
|
adopt: options.fetch(:adopt, true),
|
|
39
39
|
root: File.dirname(tree.dir))
|
|
40
|
-
|
|
40
|
+
.call(commit: commit?(options))
|
|
41
41
|
|
|
42
42
|
if changes.empty?
|
|
43
43
|
success("Every pull request title already carries a prefix.") unless quiet?(options)
|
|
@@ -54,7 +54,7 @@ module Agentilda
|
|
|
54
54
|
option :scroll_height,
|
|
55
55
|
aliases: ["-s"],
|
|
56
56
|
desc: "Lines of each agent's latest statuses shown under its row, newest first " \
|
|
57
|
-
"(default: #{Screen::Ratatui::SCROLL_HEIGHT})"
|
|
57
|
+
"(default: #{Presentation::Screen::Ratatui::SCROLL_HEIGHT})"
|
|
58
58
|
option :log, desc: "Append progress to this file (default: a per-project file under the system temp dir)"
|
|
59
59
|
|
|
60
60
|
example [
|
|
@@ -89,7 +89,7 @@ module Agentilda
|
|
|
89
89
|
|
|
90
90
|
tree = tree_for(options)
|
|
91
91
|
root = options[:root] || File.dirname(tree.dir)
|
|
92
|
-
agents = Agentilda::Agents.
|
|
92
|
+
agents = Agentilda::Agents.registry
|
|
93
93
|
agents = filtered(agents, options[:agent]) if options[:agent]
|
|
94
94
|
agents = skipped(agents, options[:skip], options[:agent]) if options[:skip]
|
|
95
95
|
plans = options[:plan] ? scoped(tree, options[:plan]) : nil
|
|
@@ -99,7 +99,7 @@ module Agentilda
|
|
|
99
99
|
jobs = (options[:jobs] || config[:jobs] || UI.default_jobs).to_i
|
|
100
100
|
timeout = (options[:timeout] || config[:timeout])&.to_i
|
|
101
101
|
|
|
102
|
-
if isolation == :worktree &&
|
|
102
|
+
if isolation == :worktree && !Agentilda::Vcs.worktree(root:).repository?
|
|
103
103
|
refuse("#{root} is not a git repository, so plans cannot be isolated.\n\n" \
|
|
104
104
|
"Run with --isolation shared to work in one tree, serially.",
|
|
105
105
|
66)
|
|
@@ -119,9 +119,9 @@ module Agentilda
|
|
|
119
119
|
# so it never rides along in a commit. Editing somebody's .gitignore
|
|
120
120
|
# is announced, the once it happens. The runner drops the file on a
|
|
121
121
|
# dry run, which is why it can be built unconditionally.
|
|
122
|
-
state =
|
|
123
|
-
if commit?(options) && StateFile.ensure_ignored!(root) && !quiet?(options)
|
|
124
|
-
info("Added #{StateFile::IGNORE.join(" and ")} to .gitignore: the run keeps its state there.")
|
|
122
|
+
state = Engine.state_file(tree)
|
|
123
|
+
if commit?(options) && Engine::StateFile.ensure_ignored!(root) && !quiet?(options)
|
|
124
|
+
info("Added #{Engine::StateFile::IGNORE.join(" and ")} to .gitignore: the run keeps its state there.")
|
|
125
125
|
end
|
|
126
126
|
|
|
127
127
|
# The screen only on a terminal that is actually running agents: a dry
|
|
@@ -129,33 +129,33 @@ module Agentilda
|
|
|
129
129
|
# screen the keys still work, so the one line says which.
|
|
130
130
|
# With a screen, ratatui owns the terminal's input, so the keyboard is
|
|
131
131
|
# fed from its loop rather than started on STDIN of its own.
|
|
132
|
-
Control.reset!
|
|
133
|
-
scroll_height = options.fetch(:scroll_height, Screen::Ratatui::SCROLL_HEIGHT)
|
|
132
|
+
Execution::Control.reset!
|
|
133
|
+
scroll_height = options.fetch(:scroll_height, Presentation::Screen::Ratatui::SCROLL_HEIGHT)
|
|
134
134
|
unless scroll_height.to_s.match?(/\A[1-9]\d*\z/)
|
|
135
135
|
refuse("--scroll-height must be a whole number of lines, 1 or more, not #{scroll_height.inspect}.", 64)
|
|
136
136
|
end
|
|
137
|
-
screen = (Screen::Ratatui.new(scroll_height: scroll_height.to_i) if UI.animate? && commit?(options))
|
|
138
|
-
console = (Console.new(screen:) if screen)
|
|
137
|
+
screen = (Presentation::Screen::Ratatui.new(scroll_height: scroll_height.to_i) if UI.animate? && commit?(options))
|
|
138
|
+
console = (Presentation::Console.new(screen:) if screen)
|
|
139
139
|
# A dry run gets no keyboard at all: every key it offers speaks to a
|
|
140
140
|
# running agent, and listening costs the terminal's raw mode, whose
|
|
141
141
|
# output post-processing the report below needs left alone.
|
|
142
142
|
keyboard = if screen
|
|
143
|
-
Keyboard.new(sink: console).tap { |kb| screen.attach_keyboard(kb) }
|
|
143
|
+
Presentation::Keyboard.new(sink: console).tap { |kb| screen.attach_keyboard(kb) }
|
|
144
144
|
elsif commit?(options)
|
|
145
|
-
Keyboard.listen(sink: console)
|
|
145
|
+
Presentation::Keyboard.listen(sink: console)
|
|
146
146
|
end
|
|
147
147
|
UI.line("keys: h for help - s select, k kill, x extend, w wrap up, n stop, q quit") if keyboard && console.nil? && !quiet?(options)
|
|
148
148
|
|
|
149
|
-
runner =
|
|
149
|
+
runner = Engine.runner(
|
|
150
150
|
tree:,
|
|
151
151
|
agents:,
|
|
152
152
|
isolation:,
|
|
153
153
|
jobs:,
|
|
154
154
|
plans:,
|
|
155
155
|
state:,
|
|
156
|
-
worktree: (
|
|
156
|
+
worktree: (Agentilda::Vcs.worktree(root:) if isolation == :worktree),
|
|
157
157
|
rounds: (options[:rounds] || config[:rounds])&.to_i,
|
|
158
|
-
executor:
|
|
158
|
+
executor: Execution.executor(root:,
|
|
159
159
|
timeout:,
|
|
160
160
|
dry_run: !commit?(options),
|
|
161
161
|
instructions: instructions_from(options[:prompt]),
|
|
@@ -172,7 +172,7 @@ module Agentilda
|
|
|
172
172
|
started = UI.monotonic
|
|
173
173
|
attempts = begin
|
|
174
174
|
screen&.open
|
|
175
|
-
|
|
175
|
+
Execution.on_interrupt { runner.call { |dispatcher| console&.attach(dispatcher) } }
|
|
176
176
|
ensure
|
|
177
177
|
screen&.close
|
|
178
178
|
keyboard&.stop
|
|
@@ -192,7 +192,7 @@ module Agentilda
|
|
|
192
192
|
# from the rows themselves, so the columns are as narrow as the run
|
|
193
193
|
# allows and still square.
|
|
194
194
|
#
|
|
195
|
-
# @param attempts [Array<Agentilda::Runner::Attempt>]
|
|
195
|
+
# @param attempts [Array<Agentilda::Engine::Runner::Attempt>]
|
|
196
196
|
# @return [Array<String>] one line per attempt, indented by two
|
|
197
197
|
def attempt_lines(attempts)
|
|
198
198
|
rows = attempts.map { |a| [a.ordinal.to_s, a.agent.to_s, "[R:#{a.round}]", mark(a), a.note.to_s] }
|
|
@@ -205,7 +205,7 @@ module Agentilda
|
|
|
205
205
|
end
|
|
206
206
|
end
|
|
207
207
|
|
|
208
|
-
# @param attempt [Agentilda::Runner::Attempt]
|
|
208
|
+
# @param attempt [Agentilda::Engine::Runner::Attempt]
|
|
209
209
|
# @return [String] what the attempt did to its plan's state
|
|
210
210
|
def mark(attempt)
|
|
211
211
|
return "FAIL" unless attempt.ok
|
|
@@ -228,9 +228,9 @@ module Agentilda
|
|
|
228
228
|
File.read(prompt)
|
|
229
229
|
end
|
|
230
230
|
|
|
231
|
-
# @param agents [Agentilda::Agents]
|
|
231
|
+
# @param agents [Agentilda::Agents::Registry]
|
|
232
232
|
# @param name [String]
|
|
233
|
-
# @return [Agentilda::Agents] holding only the agent named — the
|
|
233
|
+
# @return [Agentilda::Agents::Registry] holding only the agent named — the
|
|
234
234
|
# restriction has to reach assignments, not just chaining, or a flag
|
|
235
235
|
# like `--prompt` aimed at one agent leaks to the whole round
|
|
236
236
|
def filtered(agents, name)
|
|
@@ -243,12 +243,12 @@ module Agentilda
|
|
|
243
243
|
# already-restricted one, so `--skip` misspelled is a refusal rather
|
|
244
244
|
# than a silent no-op — the same reasoning `--plan` applies to numbers.
|
|
245
245
|
#
|
|
246
|
-
# @param agents [Agentilda::Agents]
|
|
246
|
+
# @param agents [Agentilda::Agents::Registry]
|
|
247
247
|
# @param text [String] comma-separated agent names
|
|
248
248
|
# @param restricted [String, nil] what `--agent` asked for, if anything
|
|
249
|
-
# @return [Agentilda::Agents] without the agents named
|
|
249
|
+
# @return [Agentilda::Agents::Registry] without the agents named
|
|
250
250
|
def skipped(agents, text, restricted)
|
|
251
|
-
roster = Agentilda::Agents.
|
|
251
|
+
roster = Agentilda::Agents.registry
|
|
252
252
|
names = text.split(",").map(&:strip).reject(&:empty?)
|
|
253
253
|
names.each do |name|
|
|
254
254
|
roster.find(name) or
|
|
@@ -268,13 +268,13 @@ module Agentilda
|
|
|
268
268
|
# this exists to prevent, so an unknown one is refused rather than
|
|
269
269
|
# dropped.
|
|
270
270
|
#
|
|
271
|
-
# @param tree [Agentilda::Tree]
|
|
271
|
+
# @param tree [Agentilda::Plans::Tree]
|
|
272
272
|
# @param text [String]
|
|
273
|
-
# @return [Array<Agentilda::Ordinal>]
|
|
273
|
+
# @return [Array<Agentilda::Plans::Ordinal>]
|
|
274
274
|
def scoped(tree, text)
|
|
275
275
|
known = tree.subjects.map { |s| s.feature.ordinal }
|
|
276
276
|
text.split(",").map(&:strip).reject(&:empty?).map { |token|
|
|
277
|
-
ordinal = Ordinal.parse(token)
|
|
277
|
+
ordinal = Plans::Ordinal.parse(token)
|
|
278
278
|
unless ordinal && known.include?(ordinal)
|
|
279
279
|
refuse("No plan #{token} in #{tree.dir}.\n\n" \
|
|
280
280
|
"Known: #{known.join(", ")}",
|
|
@@ -290,34 +290,34 @@ module Agentilda
|
|
|
290
290
|
# agent and a plan meant them to meet, so a pairing that cannot happen
|
|
291
291
|
# is refused up front with the agent that would actually take each plan.
|
|
292
292
|
#
|
|
293
|
-
# @param agents [Agentilda::Agents] already narrowed to the one agent
|
|
294
|
-
# @param tree [Agentilda::Tree]
|
|
295
|
-
# @param plans [Array<Agentilda::Ordinal>, nil]
|
|
293
|
+
# @param agents [Agentilda::Agents::Registry] already narrowed to the one agent
|
|
294
|
+
# @param tree [Agentilda::Plans::Tree]
|
|
295
|
+
# @param plans [Array<Agentilda::Plans::Ordinal>, nil]
|
|
296
296
|
# @param name [String]
|
|
297
297
|
# @return [void]
|
|
298
298
|
def assignable!(agents, tree, plans, name)
|
|
299
299
|
agent = agents.find(name)
|
|
300
300
|
active = tree.subjects.select { |s|
|
|
301
301
|
(plans.nil? || plans.include?(s.feature.ordinal)) &&
|
|
302
|
-
!StateMachine::SETTLED.include?(s.status.key)
|
|
302
|
+
!Plans::StateMachine::SETTLED.include?(s.status.key)
|
|
303
303
|
}
|
|
304
304
|
return if active.empty? || active.any? { |s| agent.handles?(s.status) }
|
|
305
305
|
|
|
306
|
-
roster = Agentilda::Agents.
|
|
306
|
+
roster = Agentilda::Agents.registry
|
|
307
307
|
lines = active.map { |s|
|
|
308
308
|
takers = roster.for_status(s.status).map(&:name)
|
|
309
309
|
verb = takers.size == 1 ? "takes" : "take"
|
|
310
310
|
" #{s.feature.ordinal} is #{s.status.emoji} #{s.status.label}" \
|
|
311
311
|
"#{" - #{takers.join(" and ")} #{verb} it" unless takers.empty?}"
|
|
312
312
|
}
|
|
313
|
-
handled = agent.handles.map { |k| Agentilda::STATUS_BY_KEY[k]&.then { |st| "#{st.emoji} #{st.label}" } || k }
|
|
313
|
+
handled = agent.handles.map { |k| Agentilda::Plans::STATUS_BY_KEY[k]&.then { |st| "#{st.emoji} #{st.label}" } || k }
|
|
314
314
|
refuse("#{name} handles #{handled.join(", ")}, and no plan in scope is there:\n\n" \
|
|
315
315
|
"#{lines.join("\n")}\n\nName the agent that takes these states, or drop --agent.",
|
|
316
316
|
65)
|
|
317
317
|
end
|
|
318
318
|
|
|
319
|
-
# @param attempts [Array<Agentilda::Runner::Attempt>]
|
|
320
|
-
# @return [Array<Agentilda::Runner::Attempt>]
|
|
319
|
+
# @param attempts [Array<Agentilda::Engine::Runner::Attempt>]
|
|
320
|
+
# @return [Array<Agentilda::Engine::Runner::Attempt>]
|
|
321
321
|
def failures(attempts) = attempts.reject(&:ok)
|
|
322
322
|
|
|
323
323
|
# The one thing this harness does that leaves the machine — pushing a
|
|
@@ -332,11 +332,11 @@ module Agentilda
|
|
|
332
332
|
# @param root [String]
|
|
333
333
|
# @param isolation [Symbol]
|
|
334
334
|
# @param options [Hash]
|
|
335
|
-
# @return [Agentilda::Publisher, nil]
|
|
335
|
+
# @return [Agentilda::Vcs::Publisher, nil]
|
|
336
336
|
def publisher_for(root, isolation, options)
|
|
337
337
|
return nil if isolation != :worktree || options[:git_push] == false
|
|
338
338
|
|
|
339
|
-
|
|
339
|
+
Vcs.publisher(root:, dry_run: !commit?(options))
|
|
340
340
|
end
|
|
341
341
|
|
|
342
342
|
# One line per attempt: the plan, who ran, which of that agent's rounds
|
|
@@ -344,8 +344,8 @@ module Agentilda
|
|
|
344
344
|
# because the loop has no rounds of its own: each agent counts its own
|
|
345
345
|
# per plan, so two agents on one plan can be on different numbers.
|
|
346
346
|
#
|
|
347
|
-
# @param runner [Agentilda::Runner]
|
|
348
|
-
# @param attempts [Array<Agentilda::Runner::Attempt>]
|
|
347
|
+
# @param runner [Agentilda::Engine::Runner]
|
|
348
|
+
# @param attempts [Array<Agentilda::Engine::Runner::Attempt>]
|
|
349
349
|
# @param options [Hash]
|
|
350
350
|
# @param seconds [Float] wall clock for the whole loop
|
|
351
351
|
# @return [void]
|
|
@@ -360,12 +360,12 @@ module Agentilda
|
|
|
360
360
|
# What the run cost, on STDOUT with the attempts it belongs to, so a run
|
|
361
361
|
# redirected to a file keeps its bill. A dry run spent nothing and gets
|
|
362
362
|
# none of this.
|
|
363
|
-
tally =
|
|
363
|
+
tally = Engine.tally(attempts:, seconds:)
|
|
364
364
|
puts("", tally.render) if commit?(options)
|
|
365
365
|
|
|
366
366
|
return if quiet?(options)
|
|
367
367
|
|
|
368
|
-
if Control.quit?
|
|
368
|
+
if Execution::Control.quit?
|
|
369
369
|
warn("Stopped from the keyboard — q. Unfinished plans keep their state; " \
|
|
370
370
|
"re-run to resume where they stand.")
|
|
371
371
|
end
|
|
@@ -47,11 +47,11 @@ module Agentilda
|
|
|
47
47
|
def call(plans:, **options)
|
|
48
48
|
tree = tree_for(options)
|
|
49
49
|
quiet?(options)
|
|
50
|
-
unblocker =
|
|
50
|
+
unblocker = Lifecycle.unblocker(tree:,
|
|
51
51
|
agent: agent_for(options),
|
|
52
52
|
root: options[:root],
|
|
53
53
|
commit: commit?(options),
|
|
54
|
-
executor:
|
|
54
|
+
executor: Execution.executor(root: options[:root] || File.dirname(tree.dir),
|
|
55
55
|
dry_run: !commit?(options)))
|
|
56
56
|
|
|
57
57
|
targets = unblocker.resolve(plans)
|
|
@@ -62,7 +62,7 @@ module Agentilda
|
|
|
62
62
|
credentials_warning if commit?(options) && !quiet?(options)
|
|
63
63
|
preflight(subjects, unblocker, options)
|
|
64
64
|
|
|
65
|
-
outcomes =
|
|
65
|
+
outcomes = Execution.on_interrupt { unblocker.call(subjects) }
|
|
66
66
|
outcomes.each { |outcome| report(outcome, options) }
|
|
67
67
|
footer(outcomes, options) unless quiet?(options)
|
|
68
68
|
exit(worst(targets, outcomes))
|
|
@@ -71,10 +71,10 @@ module Agentilda
|
|
|
71
71
|
private
|
|
72
72
|
|
|
73
73
|
# @param options [Hash]
|
|
74
|
-
# @return [Agentilda::Agent]
|
|
74
|
+
# @return [Agentilda::Agents::Agent]
|
|
75
75
|
def agent_for(options)
|
|
76
76
|
name = options.fetch(:agent, DEFAULT_AGENT)
|
|
77
|
-
agents = Agentilda::Agents.
|
|
77
|
+
agents = Agentilda::Agents.registry
|
|
78
78
|
agents.find(name) or
|
|
79
79
|
refuse("No agent called #{name}.\n\nKnown: #{agents.all.map(&:name).join(", ")}", 65)
|
|
80
80
|
end
|
|
@@ -87,8 +87,8 @@ module Agentilda
|
|
|
87
87
|
# Every bad token is reported, not just the first, because stopping at
|
|
88
88
|
# the first one hides the rest of the answer.
|
|
89
89
|
#
|
|
90
|
-
# @param targets [Array<Agentilda::Unblocker::Target>]
|
|
91
|
-
# @param tree [Agentilda::Tree]
|
|
90
|
+
# @param targets [Array<Agentilda::Lifecycle::Unblocker::Target>]
|
|
91
|
+
# @param tree [Agentilda::Plans::Tree]
|
|
92
92
|
# @param options [Hash]
|
|
93
93
|
# @return [void]
|
|
94
94
|
def refused(targets, tree, options)
|
|
@@ -113,15 +113,15 @@ module Agentilda
|
|
|
113
113
|
# terminal showed nothing at all — a run that had found nothing to do and
|
|
114
114
|
# a run still working looked exactly alike.
|
|
115
115
|
#
|
|
116
|
-
# @param subjects [Array<Agentilda::Subject>]
|
|
117
|
-
# @param unblocker [Agentilda::Unblocker]
|
|
116
|
+
# @param subjects [Array<Agentilda::Plans::Subject>]
|
|
117
|
+
# @param unblocker [Agentilda::Lifecycle::Unblocker]
|
|
118
118
|
# @param options [Hash]
|
|
119
119
|
# @return [void]
|
|
120
120
|
def preflight(subjects, _unblocker, options)
|
|
121
121
|
return if quiet?(options)
|
|
122
122
|
|
|
123
123
|
subjects.each do |subject|
|
|
124
|
-
questions = Unblocker.questions(subject)
|
|
124
|
+
questions = Lifecycle::Unblocker.questions(subject)
|
|
125
125
|
answered = questions.count(&:answered)
|
|
126
126
|
say("#{paint(subject.feature.ordinal.to_s, :bright_black)} #{subject.status.emoji} " \
|
|
127
127
|
"#{subject.feature.title} — #{summary(questions.size, answered)}")
|
|
@@ -138,8 +138,8 @@ module Agentilda
|
|
|
138
138
|
"#{open} open, #{answered.zero? ? "none answered yet" : "#{answered} with an answer waiting"}"
|
|
139
139
|
end
|
|
140
140
|
|
|
141
|
-
# @param subject [Agentilda::Subject]
|
|
142
|
-
# @param questions [Array<Agentilda::Unblocker::Question>]
|
|
141
|
+
# @param subject [Agentilda::Plans::Subject]
|
|
142
|
+
# @param questions [Array<Agentilda::Lifecycle::Unblocker::Question>]
|
|
143
143
|
# @return [void]
|
|
144
144
|
def detail(subject, questions)
|
|
145
145
|
if subject.unreadable_block?
|
|
@@ -157,7 +157,7 @@ module Agentilda
|
|
|
157
157
|
# One plan, after the fact: the deliverable line on STDOUT, and what
|
|
158
158
|
# changed on STDERR.
|
|
159
159
|
#
|
|
160
|
-
# @param outcome [Agentilda::Unblocker::Outcome]
|
|
160
|
+
# @param outcome [Agentilda::Lifecycle::Unblocker::Outcome]
|
|
161
161
|
# @param options [Hash]
|
|
162
162
|
# @return [void]
|
|
163
163
|
def report(outcome, options)
|
|
@@ -177,7 +177,7 @@ module Agentilda
|
|
|
177
177
|
# What the file says happened, rather than what the agent claims. The
|
|
178
178
|
# questions are counted off disk on both sides of the run.
|
|
179
179
|
#
|
|
180
|
-
# @param outcome [Agentilda::Unblocker::Outcome]
|
|
180
|
+
# @param outcome [Agentilda::Lifecycle::Unblocker::Outcome]
|
|
181
181
|
# @param commit [Boolean]
|
|
182
182
|
# @return [String]
|
|
183
183
|
def movement(outcome, commit)
|
|
@@ -189,7 +189,7 @@ module Agentilda
|
|
|
189
189
|
"folded #{outcome.folded.map { |n| "B#{n}" }.join(", ")}"
|
|
190
190
|
end
|
|
191
191
|
|
|
192
|
-
# @param outcomes [Array<Agentilda::Unblocker::Outcome>]
|
|
192
|
+
# @param outcomes [Array<Agentilda::Lifecycle::Unblocker::Outcome>]
|
|
193
193
|
# @param options [Hash]
|
|
194
194
|
# @return [void]
|
|
195
195
|
def footer(outcomes, options)
|
|
@@ -222,8 +222,8 @@ module Agentilda
|
|
|
222
222
|
# The most serious thing that happened, as an exit status. A tree that
|
|
223
223
|
# was partly drained still exits non-zero when part of it could not be.
|
|
224
224
|
#
|
|
225
|
-
# @param targets [Array<Agentilda::Unblocker::Target>]
|
|
226
|
-
# @param outcomes [Array<Agentilda::Unblocker::Outcome>]
|
|
225
|
+
# @param targets [Array<Agentilda::Lifecycle::Unblocker::Target>]
|
|
226
|
+
# @param outcomes [Array<Agentilda::Lifecycle::Unblocker::Outcome>]
|
|
227
227
|
# @return [Integer]
|
|
228
228
|
def worst(targets, outcomes)
|
|
229
229
|
problems = targets.map(&:problem)
|
|
@@ -35,7 +35,7 @@ module Agentilda
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
root = File.dirname(tree.dir)
|
|
38
|
-
worktree =
|
|
38
|
+
worktree = Agentilda::Vcs.worktree(root:)
|
|
39
39
|
|
|
40
40
|
checkout = worktree.checkout_for(feature)
|
|
41
41
|
|
|
@@ -66,12 +66,12 @@ module Agentilda
|
|
|
66
66
|
|
|
67
67
|
# Find the plan that matches the given identifier.
|
|
68
68
|
#
|
|
69
|
-
# @param tree [Agentilda::Tree] plans directory wrapper
|
|
69
|
+
# @param tree [Agentilda::Plans::Tree] plans directory wrapper
|
|
70
70
|
# @param plan [String] ordinal, folder name, or slug
|
|
71
|
-
# @return [Agentilda::Feature, nil]
|
|
71
|
+
# @return [Agentilda::Plans::Feature, nil]
|
|
72
72
|
def resolve_plan(tree, plan)
|
|
73
73
|
# Try parsing as an ordinal first
|
|
74
|
-
ordinal = ::Agentilda::Ordinal.parse(plan)
|
|
74
|
+
ordinal = ::Agentilda::Plans::Ordinal.parse(plan)
|
|
75
75
|
if ordinal
|
|
76
76
|
found = tree.features.find { |f| f.ordinal == ordinal }
|
|
77
77
|
return found if found
|
data/lib/agentilda/cli.rb
CHANGED
|
@@ -106,7 +106,7 @@ module Agentilda
|
|
|
106
106
|
prefix.register "projects", Linear::Projects
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
-
# What a paired agent shells out to between steps. See {Agentilda::Mailbox}.
|
|
109
|
+
# What a paired agent shells out to between steps. See {Agentilda::Plans::Mailbox}.
|
|
110
110
|
register "mail" do |prefix|
|
|
111
111
|
prefix.register "send", Mail::Send
|
|
112
112
|
prefix.register "read", Mail::Read
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Agentilda
|
|
4
|
+
module Engine
|
|
5
|
+
# What the screen draws: everything about the run, as values, refreshed
|
|
6
|
+
# once a second by the dispatcher. The screen paints it and knows nothing
|
|
7
|
+
# about threads; the dispatcher builds it and knows nothing about cells.
|
|
8
|
+
#
|
|
9
|
+
# @!attribute [r] rows
|
|
10
|
+
# @return [Array<Agentilda::Engine::Board::Row>] one per agent invocation still
|
|
11
|
+
# worth showing: running, or finished within the last few seconds
|
|
12
|
+
Board = Data.define(:started_at,
|
|
13
|
+
:status,
|
|
14
|
+
:plans,
|
|
15
|
+
:up,
|
|
16
|
+
:down,
|
|
17
|
+
:rows,
|
|
18
|
+
:root,
|
|
19
|
+
:running,
|
|
20
|
+
:live_up,
|
|
21
|
+
:live_down,
|
|
22
|
+
:selected,
|
|
23
|
+
:dialog,
|
|
24
|
+
:help,
|
|
25
|
+
:about,
|
|
26
|
+
:hidden) do
|
|
27
|
+
def initialize(selected: nil, dialog: nil, help: false, about: nil, hidden: 0, **rest) = super
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @!attribute [r] key
|
|
31
|
+
# @return [String] `"001.00/leah-researcher"`, what selection and the
|
|
32
|
+
# keyboard address a row by
|
|
33
|
+
# @!attribute [r] file
|
|
34
|
+
# @return [String] the document holding the agent's latest ledger line
|
|
35
|
+
# @!attribute [r] pr
|
|
36
|
+
# @return [Hash, nil] `{number:, url:, rejected:}` once the plan is at
|
|
37
|
+
# the review stage
|
|
38
|
+
# @!attribute [r] state
|
|
39
|
+
# @return [Symbol] :running, :done, :failed or :stuck
|
|
40
|
+
# @!attribute [r] phase
|
|
41
|
+
# @return [Symbol, nil] the clock's phase while running
|
|
42
|
+
# @!attribute [r] frame
|
|
43
|
+
# @return [Integer] spinner frame counter
|
|
44
|
+
# @!attribute [r] history
|
|
45
|
+
# @return [Array<String>] what the agent has said, newest first, for
|
|
46
|
+
# `run --scroll-height` to show more than the latest line of
|
|
47
|
+
Board::Row = Data.define(:key,
|
|
48
|
+
:at,
|
|
49
|
+
:ordinal,
|
|
50
|
+
:file,
|
|
51
|
+
:agent,
|
|
52
|
+
:role,
|
|
53
|
+
:round,
|
|
54
|
+
:rounds,
|
|
55
|
+
:model,
|
|
56
|
+
:remaining,
|
|
57
|
+
:phase,
|
|
58
|
+
:up,
|
|
59
|
+
:down,
|
|
60
|
+
:message,
|
|
61
|
+
:state,
|
|
62
|
+
:pr,
|
|
63
|
+
:frame,
|
|
64
|
+
:bold,
|
|
65
|
+
:elapsed,
|
|
66
|
+
:subagents,
|
|
67
|
+
:history) do
|
|
68
|
+
def initialize(pr: nil, phase: nil, remaining: nil, frame: 0, bold: false, message: nil,
|
|
69
|
+
elapsed: 0, subagents: 0, history: [], **rest)
|
|
70
|
+
super
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @param history [Array<String>] newest first
|
|
74
|
+
# @param text [String, nil]
|
|
75
|
+
# @return [Array<String>] +text+ on top, unless it is empty or repeats
|
|
76
|
+
# the newest line: an agent restating its activity is not news
|
|
77
|
+
def self.remember(history, text)
|
|
78
|
+
text = text.to_s.strip
|
|
79
|
+
return history if text.empty? || history.first == text
|
|
80
|
+
|
|
81
|
+
[text, *history].first(Board::Row::HISTORY)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# @return [Boolean]
|
|
85
|
+
def running? = state == :running
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Statuses a row keeps, whatever the screen shows of them. A cap, so a
|
|
89
|
+
# long-running agent's chatter cannot grow without bound. Outside the
|
|
90
|
+
# `Data.define` block, whose constants would land on Agentilda instead.
|
|
91
|
+
Board::Row::HISTORY = 50
|
|
92
|
+
end
|
|
93
|
+
end
|