ace-herdr 0.1.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 +7 -0
- data/.ace-defaults/herdr/config.yml +26 -0
- data/.ace-defaults/herdr/tabs/agent.yml +11 -0
- data/.ace-defaults/herdr/workspaces/development.yml +23 -0
- data/CHANGELOG.md +22 -0
- data/LICENSE +21 -0
- data/README.md +54 -0
- data/Rakefile +12 -0
- data/docs/usage.md +306 -0
- data/exe/ace-herdr +17 -0
- data/lib/ace/herdr/atoms/answer_digest.rb +18 -0
- data/lib/ace/herdr/cli/commands/capture.rb +55 -0
- data/lib/ace/herdr/cli/commands/close.rb +53 -0
- data/lib/ace/herdr/cli/commands/deliver.rb +67 -0
- data/lib/ace/herdr/cli/commands/dispatch.rb +62 -0
- data/lib/ace/herdr/cli/commands/list.rb +71 -0
- data/lib/ace/herdr/cli/commands/list_presets.rb +47 -0
- data/lib/ace/herdr/cli/commands/send.rb +103 -0
- data/lib/ace/herdr/cli/commands/support.rb +68 -0
- data/lib/ace/herdr/cli/commands/tab.rb +54 -0
- data/lib/ace/herdr/cli/commands/tidy.rb +65 -0
- data/lib/ace/herdr/cli/commands/wait.rb +88 -0
- data/lib/ace/herdr/cli/commands/workspace.rb +52 -0
- data/lib/ace/herdr/cli.rb +98 -0
- data/lib/ace/herdr/errors.rb +51 -0
- data/lib/ace/herdr/models/delivery_record.rb +109 -0
- data/lib/ace/herdr/models/dispatch_outcome.rb +29 -0
- data/lib/ace/herdr/molecules/delivery_record_store.rb +150 -0
- data/lib/ace/herdr/molecules/herdr_executor.rb +264 -0
- data/lib/ace/herdr/molecules/pane_tidy_probe.rb +80 -0
- data/lib/ace/herdr/molecules/preset_loader.rb +70 -0
- data/lib/ace/herdr/molecules/preset_resolver.rb +88 -0
- data/lib/ace/herdr/organisms/control_surface.rb +575 -0
- data/lib/ace/herdr/organisms/deliverer.rb +358 -0
- data/lib/ace/herdr/organisms/dispatcher.rb +130 -0
- data/lib/ace/herdr/organisms/tidy.rb +188 -0
- data/lib/ace/herdr/version.rb +7 -0
- data/lib/ace/herdr.rb +103 -0
- metadata +224 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/support/cli"
|
|
4
|
+
require "ace/core"
|
|
5
|
+
require_relative "support"
|
|
6
|
+
|
|
7
|
+
module Ace
|
|
8
|
+
module Herdr
|
|
9
|
+
module CLI
|
|
10
|
+
module Commands
|
|
11
|
+
class Deliver < Ace::Support::Cli::Command
|
|
12
|
+
include Ace::Support::Cli::Base
|
|
13
|
+
include Runtime
|
|
14
|
+
|
|
15
|
+
desc <<~DESC.strip
|
|
16
|
+
Push an answer to an agent pane (ace-hitl delivery contract)
|
|
17
|
+
|
|
18
|
+
Delivers are idempotent per event id and never lose the answer:
|
|
19
|
+
a write-ahead record survives crashes, missing agents are
|
|
20
|
+
bootstrapped, and transient failures retry with fixed backoff.
|
|
21
|
+
DESC
|
|
22
|
+
|
|
23
|
+
example [
|
|
24
|
+
"--session ws-1 --pane p5 --event-id evt-1 --answer-file answer.md",
|
|
25
|
+
"echo 'the answer' | ace-herdr deliver --pane p5",
|
|
26
|
+
"--session ws-1 --pane p5 --kind codex --label 8wm.t.vs0 (bootstraps if missing)"
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
option :session, type: :string, desc: "Ref session (default: HERDR_SESSION)"
|
|
30
|
+
option :pane, type: :string, desc: "Ref pane (default: HERDR_PANE)"
|
|
31
|
+
option :event_id, type: :string, desc: "Idempotency key (derived from ref and content when omitted)"
|
|
32
|
+
option :kind, type: :string, desc: "Agent kind used when bootstrapping (default: config default_agent_kind)"
|
|
33
|
+
option :label, type: :string, desc: "Agent name used when bootstrapping (default: event id)"
|
|
34
|
+
option :answer_file, type: :string, desc: "Answer file (default: stdin)"
|
|
35
|
+
option :resume, type: :string, desc: "Event id to re-deliver from the stored record (crash recovery)"
|
|
36
|
+
|
|
37
|
+
def initialize(executor: nil)
|
|
38
|
+
@executor = executor
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def call(**options)
|
|
42
|
+
translate_errors do
|
|
43
|
+
deliverer = Organisms::Deliverer.from_config(
|
|
44
|
+
executor: executor, deliveries_dir: deliveries_dir, config: config
|
|
45
|
+
)
|
|
46
|
+
if options[:resume]
|
|
47
|
+
result = deliverer.resume(options[:resume], kind: options[:kind], label: options[:label])
|
|
48
|
+
puts JSON.generate(ref: result.ref.to_h, state: result.state.to_s, resumed: true)
|
|
49
|
+
cli_error("delivery did not complete (state: #{result.state})") unless result.state == :delivered
|
|
50
|
+
return
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
ref = resolve_ref(options[:session], options[:pane])
|
|
54
|
+
answer = read_content(options[:answer_file], what: "answer")
|
|
55
|
+
result = deliverer.deliver(
|
|
56
|
+
ref, answer,
|
|
57
|
+
event_id: options[:event_id], kind: options[:kind], label: options[:label]
|
|
58
|
+
)
|
|
59
|
+
puts JSON.generate(ref: result.ref.to_h, state: result.state.to_s)
|
|
60
|
+
cli_error("delivery did not complete (state: #{result.state})") unless result.state == :delivered
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/support/cli"
|
|
4
|
+
require "ace/core"
|
|
5
|
+
require_relative "support"
|
|
6
|
+
|
|
7
|
+
module Ace
|
|
8
|
+
module Herdr
|
|
9
|
+
module CLI
|
|
10
|
+
module Commands
|
|
11
|
+
class Dispatch < Ace::Support::Cli::Command
|
|
12
|
+
include Ace::Support::Cli::Base
|
|
13
|
+
include Runtime
|
|
14
|
+
|
|
15
|
+
desc <<~DESC.strip
|
|
16
|
+
Start an agent in one command: tab + agent start + prompt
|
|
17
|
+
|
|
18
|
+
Defaults are deterministic: the caller's workspace, the label as
|
|
19
|
+
agent and pane name, the prompt from a file or stdin. Zero-token.
|
|
20
|
+
DESC
|
|
21
|
+
|
|
22
|
+
example [
|
|
23
|
+
"--label 8wm.t.vs0 --kind pi --prompt-file prompt.md",
|
|
24
|
+
"--label review --pane p7 --cwd /Users/me/project",
|
|
25
|
+
"--label 8wm.t.vs0 --no-prompt"
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
option :label, type: :string, desc: "Agent and pane label (typically the task id)"
|
|
29
|
+
option :kind, type: :string, desc: "Agent kind (default: config default_agent_kind)"
|
|
30
|
+
option :workspace, type: :string, desc: "Target workspace (default: caller's workspace)"
|
|
31
|
+
option :pane, type: :string, desc: "Existing pane (default: create a new tab)"
|
|
32
|
+
option :cwd, type: :string, desc: "Working directory for a created tab"
|
|
33
|
+
option :prompt_file, type: :string, desc: "Prompt file (default: stdin)"
|
|
34
|
+
option :no_prompt, type: :boolean, desc: "Start the agent without submitting a prompt"
|
|
35
|
+
|
|
36
|
+
def initialize(executor: nil)
|
|
37
|
+
@executor = executor
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def call(**options)
|
|
41
|
+
translate_errors do
|
|
42
|
+
cli_error("--label is required") if options[:label].to_s.empty?
|
|
43
|
+
prompt =
|
|
44
|
+
if options[:no_prompt]
|
|
45
|
+
""
|
|
46
|
+
else
|
|
47
|
+
read_content(options[:prompt_file], what: "prompt")
|
|
48
|
+
end
|
|
49
|
+
dispatcher = Organisms::Dispatcher.from_config(executor: executor, config: config)
|
|
50
|
+
outcome = dispatcher.dispatch(
|
|
51
|
+
label: options.fetch(:label), prompt: prompt,
|
|
52
|
+
kind: options[:kind], workspace_id: options[:workspace],
|
|
53
|
+
pane: options[:pane], cwd: options[:cwd]
|
|
54
|
+
)
|
|
55
|
+
puts JSON.generate(outcome.to_h)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "ace/support/cli"
|
|
5
|
+
require "ace/core"
|
|
6
|
+
require_relative "support"
|
|
7
|
+
|
|
8
|
+
module Ace
|
|
9
|
+
module Herdr
|
|
10
|
+
module CLI
|
|
11
|
+
module Commands
|
|
12
|
+
class List < Ace::Support::Cli::Command
|
|
13
|
+
include Ace::Support::Cli::Base
|
|
14
|
+
include Runtime
|
|
15
|
+
|
|
16
|
+
desc <<~DESC.strip
|
|
17
|
+
List live herdr panes, tabs, or workspaces as one JSON line
|
|
18
|
+
|
|
19
|
+
Panes are the default scope; --workspace scopes panes and tabs.
|
|
20
|
+
DESC
|
|
21
|
+
|
|
22
|
+
example [
|
|
23
|
+
" # Panes across all workspaces",
|
|
24
|
+
"--workspace w1 # Panes in one workspace",
|
|
25
|
+
"--tabs --workspace w1 # Tabs in one workspace",
|
|
26
|
+
"--workspaces # Workspaces"
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
option :panes, type: :boolean, desc: "List panes (default)"
|
|
30
|
+
option :tabs, type: :boolean, desc: "List tabs"
|
|
31
|
+
option :workspaces, type: :boolean, desc: "List workspaces"
|
|
32
|
+
option :workspace, type: :string, desc: "Scope panes/tabs to a workspace id"
|
|
33
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress output"
|
|
34
|
+
|
|
35
|
+
def initialize(executor: nil)
|
|
36
|
+
@executor = executor
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def call(**options)
|
|
40
|
+
translate_errors do
|
|
41
|
+
scope = selected_scope(options)
|
|
42
|
+
cli_error("Use only one of --panes, --tabs, or --workspaces") if scope == :ambiguous
|
|
43
|
+
cli_error("--workspaces does not accept --workspace") if scope == :workspaces && options[:workspace]
|
|
44
|
+
|
|
45
|
+
control = Organisms::ControlSurface.new(executor: executor)
|
|
46
|
+
payload =
|
|
47
|
+
case scope
|
|
48
|
+
when :workspaces
|
|
49
|
+
{workspaces: control.list_workspaces}
|
|
50
|
+
when :tabs
|
|
51
|
+
{tabs: control.list_tabs(workspace_id: options[:workspace])}
|
|
52
|
+
else
|
|
53
|
+
{panes: control.list_panes(workspace_id: options[:workspace])}
|
|
54
|
+
end
|
|
55
|
+
puts JSON.generate(payload) unless options[:quiet]
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def selected_scope(options)
|
|
62
|
+
scopes = %i[panes tabs workspaces].select { |key| options[key] }
|
|
63
|
+
return :ambiguous if scopes.length > 1
|
|
64
|
+
|
|
65
|
+
scopes.first || :panes
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "ace/support/cli"
|
|
5
|
+
require "ace/core"
|
|
6
|
+
require_relative "support"
|
|
7
|
+
|
|
8
|
+
module Ace
|
|
9
|
+
module Herdr
|
|
10
|
+
module CLI
|
|
11
|
+
module Commands
|
|
12
|
+
class ListPresets < Ace::Support::Cli::Command
|
|
13
|
+
include Ace::Support::Cli::Base
|
|
14
|
+
include Runtime
|
|
15
|
+
|
|
16
|
+
desc <<~DESC.strip
|
|
17
|
+
List available workspace/tab presets as one JSON line
|
|
18
|
+
|
|
19
|
+
The inventory reflects the merged ADR-022 cascade: gem defaults
|
|
20
|
+
under .ace-defaults/herdr/, overridden by ~/.ace/herdr/ and the
|
|
21
|
+
project .ace/herdr/.
|
|
22
|
+
DESC
|
|
23
|
+
|
|
24
|
+
example [
|
|
25
|
+
"",
|
|
26
|
+
"workspaces",
|
|
27
|
+
"tabs"
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
argument :type, required: false, desc: "Scope: workspaces or tabs (default: both)"
|
|
31
|
+
|
|
32
|
+
def initialize(executor: nil, control: nil)
|
|
33
|
+
@executor = executor
|
|
34
|
+
@control = control
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def call(type: nil, **options)
|
|
38
|
+
translate_errors do
|
|
39
|
+
control = @control || Organisms::ControlSurface.new(executor: executor)
|
|
40
|
+
puts JSON.generate(control.list_presets(type: type))
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "ace/support/cli"
|
|
5
|
+
require "ace/core"
|
|
6
|
+
require_relative "support"
|
|
7
|
+
|
|
8
|
+
module Ace
|
|
9
|
+
module Herdr
|
|
10
|
+
module CLI
|
|
11
|
+
module Commands
|
|
12
|
+
class Send < Ace::Support::Cli::Command
|
|
13
|
+
include Ace::Support::Cli::Base
|
|
14
|
+
include Runtime
|
|
15
|
+
|
|
16
|
+
desc <<~DESC.strip
|
|
17
|
+
Send a command, raw text, or named keys to a pane
|
|
18
|
+
|
|
19
|
+
Input reaches the pane in declaration order. Panes hosting a
|
|
20
|
+
live agent get prompt semantics: text becomes one self-submitting
|
|
21
|
+
agent prompt (agent_blocked is rejected pre-send) and a single
|
|
22
|
+
trailing --key Enter is dropped and reported.
|
|
23
|
+
DESC
|
|
24
|
+
|
|
25
|
+
example [
|
|
26
|
+
"--pane p5 --cmd 'bundle exec rake test'",
|
|
27
|
+
"--pane p5 --msg 'continue with option 2' --key Enter",
|
|
28
|
+
"--pane p5 --key Esc --cmd 'reset'",
|
|
29
|
+
"--pane p5 --key Enter",
|
|
30
|
+
"--pane p5 --cmd 'y' --quiet"
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
option :pane, type: :string, desc: "Target pane id"
|
|
34
|
+
option :cmd, type: :string, aliases: %w[-c], desc: "Command text to submit (at most one, before any --key)"
|
|
35
|
+
option :msg, type: :string, aliases: %w[-m], repeat: true, desc: "Literal text to type without submitting"
|
|
36
|
+
option :key, type: :string, aliases: %w[-k], repeat: true, desc: "Named key to send (for example enter, esc, ctrl+c)"
|
|
37
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress output"
|
|
38
|
+
|
|
39
|
+
def initialize(executor: nil, argv_source: nil)
|
|
40
|
+
@executor = executor
|
|
41
|
+
@argv_source = argv_source
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def call(**options)
|
|
45
|
+
translate_errors do
|
|
46
|
+
cli_error("--pane is required") if options[:pane].to_s.empty?
|
|
47
|
+
control = Organisms::ControlSurface.new(executor: executor)
|
|
48
|
+
result = control.send_input(pane: options.fetch(:pane), tokens: input_tokens(options))
|
|
49
|
+
puts JSON.generate(result) unless options[:quiet]
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Declaration order carries meaning (spec 8wq.t.k84), so the argv
|
|
56
|
+
# token stream is the source of truth for real invocations. Parsed
|
|
57
|
+
# option arrays only back programmatic calls with no input flags on
|
|
58
|
+
# the command line (canonical order: cmd, msgs, keys).
|
|
59
|
+
def input_tokens(options)
|
|
60
|
+
argv_tokens(@argv_source || ARGV) || option_tokens(options)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def argv_tokens(argv)
|
|
64
|
+
tokens = []
|
|
65
|
+
seen_input_flag = false
|
|
66
|
+
index = 0
|
|
67
|
+
while index < argv.length
|
|
68
|
+
token = argv[index]
|
|
69
|
+
if (match = token.match(/\A(--cmd|-c|--msg|-m|--key|-k)=(.*)\z/m))
|
|
70
|
+
seen_input_flag = true
|
|
71
|
+
tokens << {type: flag_type(match[1]), value: match[2]}
|
|
72
|
+
index += 1
|
|
73
|
+
elsif (flag = %w[--cmd -c --msg -m --key -k].include?(token) && token)
|
|
74
|
+
seen_input_flag = true
|
|
75
|
+
tokens << {type: flag_type(flag), value: argv[index + 1]}
|
|
76
|
+
index += 2
|
|
77
|
+
else
|
|
78
|
+
index += 1
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
seen_input_flag ? tokens : nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def flag_type(flag)
|
|
85
|
+
case flag
|
|
86
|
+
when "--cmd", "-c" then :cmd
|
|
87
|
+
when "--msg", "-m" then :msg
|
|
88
|
+
else :key
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def option_tokens(options)
|
|
93
|
+
tokens = []
|
|
94
|
+
tokens << {type: :cmd, value: options[:cmd]} if options[:cmd]
|
|
95
|
+
Array(options[:msg]).each { |value| tokens << {type: :msg, value: value} }
|
|
96
|
+
Array(options[:key]).each { |value| tokens << {type: :key, value: value} }
|
|
97
|
+
tokens
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "ace/support/cli"
|
|
5
|
+
require "ace/core"
|
|
6
|
+
|
|
7
|
+
module Ace
|
|
8
|
+
module Herdr
|
|
9
|
+
module CLI
|
|
10
|
+
module Commands
|
|
11
|
+
# Shared collaborator construction for ace-herdr commands. The
|
|
12
|
+
# executor is injectable so tests can substitute a fake; commands
|
|
13
|
+
# registered in the CLI use the real herdr binary.
|
|
14
|
+
module Runtime
|
|
15
|
+
def executor
|
|
16
|
+
@executor ||= Ace::Herdr::Molecules::HerdrExecutor.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def config
|
|
20
|
+
@config ||= Ace::Herdr.config
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def deliveries_dir
|
|
24
|
+
File.expand_path(config["deliveries_dir"] || ".ace-local/herdr/deliveries", Dir.pwd)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Reverse address from flags or the caller's herdr environment,
|
|
28
|
+
# fail closed (spec 8wm.t.vrz §3)
|
|
29
|
+
def resolve_ref(session, pane)
|
|
30
|
+
Ace::Hitl::Providers::Ref.new(
|
|
31
|
+
session: Ace::Hitl::Providers::Ref.validate!(
|
|
32
|
+
session || ENV["HERDR_SESSION"], "HERDR_SESSION"
|
|
33
|
+
),
|
|
34
|
+
pane: Ace::Hitl::Providers::Ref.validate!(
|
|
35
|
+
pane || ENV["HERDR_PANE"], "HERDR_PANE"
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Answer/prompt content from a file or stdin
|
|
41
|
+
def read_content(path, what:)
|
|
42
|
+
return $stdin.read if path.nil? || path.empty?
|
|
43
|
+
|
|
44
|
+
File.read(path)
|
|
45
|
+
rescue Errno::ENOENT
|
|
46
|
+
raise Ace::Support::Cli::Error, "#{what} file not found: #{path}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def cli_error(message)
|
|
50
|
+
raise Ace::Support::Cli::Error, message
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Contract and executor failures become CLI errors (ADR-023)
|
|
54
|
+
def translate_errors
|
|
55
|
+
yield
|
|
56
|
+
rescue Ace::Hitl::Providers::InvalidRefError,
|
|
57
|
+
Ace::Hitl::Providers::ProviderUnavailableError,
|
|
58
|
+
Ace::Herdr::ValidationError,
|
|
59
|
+
Ace::Herdr::TargetResolutionError,
|
|
60
|
+
Ace::Herdr::WaitTimeoutError,
|
|
61
|
+
Ace::Herdr::ExecutorError => e
|
|
62
|
+
raise Ace::Support::Cli::Error, e.message
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "ace/support/cli"
|
|
5
|
+
require "ace/core"
|
|
6
|
+
require_relative "support"
|
|
7
|
+
|
|
8
|
+
module Ace
|
|
9
|
+
module Herdr
|
|
10
|
+
module CLI
|
|
11
|
+
module Commands
|
|
12
|
+
class Tab < Ace::Support::Cli::Command
|
|
13
|
+
include Ace::Support::Cli::Base
|
|
14
|
+
include Runtime
|
|
15
|
+
|
|
16
|
+
desc <<~DESC.strip
|
|
17
|
+
Create a tab from a preset (≈ ace-tmux window)
|
|
18
|
+
|
|
19
|
+
The tab lands in the caller's workspace unless --workspace is
|
|
20
|
+
given. Presets resolve through the ADR-022 cascade and may
|
|
21
|
+
compose via `preset:` refs; --cwd overrides the resolved tab cwd.
|
|
22
|
+
DESC
|
|
23
|
+
|
|
24
|
+
example [
|
|
25
|
+
"agent",
|
|
26
|
+
"agent --workspace w1",
|
|
27
|
+
"agent --cwd /path/to/project"
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
argument :preset, desc: "Tab preset name (list with ace-herdr --list-presets tabs)"
|
|
31
|
+
option :workspace, type: :string, desc: "Target workspace (default: caller's workspace)"
|
|
32
|
+
option :cwd, type: :string, desc: "Working directory overriding the resolved tab/pane cwd"
|
|
33
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress output"
|
|
34
|
+
|
|
35
|
+
def initialize(executor: nil, control: nil)
|
|
36
|
+
@executor = executor
|
|
37
|
+
@control = control
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def call(preset: nil, **options)
|
|
41
|
+
translate_errors do
|
|
42
|
+
cli_error("preset name is required") if preset.to_s.empty?
|
|
43
|
+
control = @control || Organisms::ControlSurface.from_config(executor: executor, config: config)
|
|
44
|
+
result = control.create_tab(
|
|
45
|
+
preset, workspace_id: options[:workspace], cwd: options[:cwd]
|
|
46
|
+
)
|
|
47
|
+
puts JSON.generate(result) unless options[:quiet]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "ace/support/cli"
|
|
5
|
+
require "ace/core"
|
|
6
|
+
require_relative "support"
|
|
7
|
+
|
|
8
|
+
module Ace
|
|
9
|
+
module Herdr
|
|
10
|
+
module CLI
|
|
11
|
+
module Commands
|
|
12
|
+
class Tidy < Ace::Support::Cli::Command
|
|
13
|
+
include Ace::Support::Cli::Base
|
|
14
|
+
include Runtime
|
|
15
|
+
|
|
16
|
+
desc <<~DESC.strip
|
|
17
|
+
Report cleanable agent panes and delivery records as one JSON line
|
|
18
|
+
|
|
19
|
+
Dry run by default: nothing is closed or removed. With --apply,
|
|
20
|
+
panes close only on positive completion evidence (observed done
|
|
21
|
+
state or dead pane process) re-confirmed right before closing;
|
|
22
|
+
delivered records older than tidy.delivered_retention_days are
|
|
23
|
+
archived. Unknown, unreadable, pending, retryable, and failed
|
|
24
|
+
resources are never touched.
|
|
25
|
+
DESC
|
|
26
|
+
|
|
27
|
+
example [
|
|
28
|
+
" # Dry run: report candidates only",
|
|
29
|
+
"--apply # Close eligible panes, archive old delivered records",
|
|
30
|
+
"--quiet # Suppress the report"
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
option :apply, type: :boolean, desc: "Close eligible panes and archive old delivered records (default: dry run)"
|
|
34
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress output"
|
|
35
|
+
|
|
36
|
+
def initialize(executor: nil, config: nil)
|
|
37
|
+
@executor = executor
|
|
38
|
+
@injected_config = config
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def call(**options)
|
|
42
|
+
translate_errors do
|
|
43
|
+
report = tidy.run(apply: options[:apply] || false)
|
|
44
|
+
puts JSON.generate(report) unless options[:quiet]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def config
|
|
51
|
+
@injected_config || super
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def tidy
|
|
55
|
+
Organisms::Tidy.new(
|
|
56
|
+
executor: executor,
|
|
57
|
+
deliveries_dir: deliveries_dir,
|
|
58
|
+
retention_days: (config["tidy"] || {})["delivered_retention_days"]
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "ace/support/cli"
|
|
5
|
+
require "ace/core"
|
|
6
|
+
require_relative "support"
|
|
7
|
+
|
|
8
|
+
module Ace
|
|
9
|
+
module Herdr
|
|
10
|
+
module CLI
|
|
11
|
+
module Commands
|
|
12
|
+
class Wait < Ace::Support::Cli::Command
|
|
13
|
+
include Ace::Support::Cli::Base
|
|
14
|
+
include Runtime
|
|
15
|
+
|
|
16
|
+
desc <<~DESC.strip
|
|
17
|
+
Wait for an agent state or matching pane output
|
|
18
|
+
|
|
19
|
+
--for agent (default) waits for an agent state without scraping
|
|
20
|
+
pane output. --for output waits until the pane content contains
|
|
21
|
+
the literal --pattern; existing content is checked immediately,
|
|
22
|
+
then polled.
|
|
23
|
+
DESC
|
|
24
|
+
|
|
25
|
+
example [
|
|
26
|
+
"--pane p5",
|
|
27
|
+
"--pane p5 --until done --timeout 120",
|
|
28
|
+
"--pane p5 --for output --pattern 'build succeeded'",
|
|
29
|
+
"--pane p5 --for output --pattern done --timeout 30"
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
option :pane, type: :string, desc: "Target pane id"
|
|
33
|
+
option :for, type: :string, desc: "What to wait for: agent (default) or output"
|
|
34
|
+
option :until, type: :string, desc: "Agent states, comma-separated: idle,working,blocked,done,unknown (default: idle,done,blocked)"
|
|
35
|
+
option :pattern, type: :string, desc: "Literal output pattern to wait for (with --for output)"
|
|
36
|
+
option :timeout, type: :integer, desc: "Timeout in seconds (default: config timeouts.wait)"
|
|
37
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress output"
|
|
38
|
+
|
|
39
|
+
def initialize(executor: nil)
|
|
40
|
+
@executor = executor
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def call(**options)
|
|
44
|
+
translate_errors do
|
|
45
|
+
cli_error("--pane is required") if options[:pane].to_s.empty?
|
|
46
|
+
mode = options[:for] || "agent"
|
|
47
|
+
unless %w[agent output].include?(mode)
|
|
48
|
+
cli_error("--for must be agent or output")
|
|
49
|
+
end
|
|
50
|
+
timeout_seconds =
|
|
51
|
+
options[:timeout] || (config.dig("timeouts", "wait") || 30)
|
|
52
|
+
|
|
53
|
+
if mode == "output"
|
|
54
|
+
wait_for_output(options, timeout_seconds)
|
|
55
|
+
else
|
|
56
|
+
wait_for_agent(options, timeout_seconds)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def wait_for_agent(options, timeout_seconds)
|
|
64
|
+
cli_error("--pattern requires --for output") if options[:pattern]
|
|
65
|
+
until_states = (options[:until] || "idle,done,blocked")
|
|
66
|
+
.split(",").map(&:strip).reject(&:empty?)
|
|
67
|
+
executor.agent_wait(
|
|
68
|
+
pane: options.fetch(:pane), until_states: until_states,
|
|
69
|
+
timeout_ms: timeout_seconds * 1000
|
|
70
|
+
)
|
|
71
|
+
puts JSON.generate(pane: options.fetch(:pane), state: "ready") unless options[:quiet]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def wait_for_output(options, timeout_seconds)
|
|
75
|
+
cli_error("--for output requires --pattern") if options[:pattern].to_s.empty?
|
|
76
|
+
cli_error("--until requires --for agent") if options[:until]
|
|
77
|
+
control = Organisms::ControlSurface.new(executor: executor)
|
|
78
|
+
control.wait_output(
|
|
79
|
+
pane: options.fetch(:pane), pattern: options.fetch(:pattern),
|
|
80
|
+
timeout_ms: timeout_seconds * 1000
|
|
81
|
+
)
|
|
82
|
+
puts JSON.generate(pane: options.fetch(:pane), matched: true) unless options[:quiet]
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "ace/support/cli"
|
|
5
|
+
require "ace/core"
|
|
6
|
+
require_relative "support"
|
|
7
|
+
|
|
8
|
+
module Ace
|
|
9
|
+
module Herdr
|
|
10
|
+
module CLI
|
|
11
|
+
module Commands
|
|
12
|
+
class Workspace < Ace::Support::Cli::Command
|
|
13
|
+
include Ace::Support::Cli::Base
|
|
14
|
+
include Runtime
|
|
15
|
+
|
|
16
|
+
desc <<~DESC.strip
|
|
17
|
+
Create a workspace from a preset (≈ ace-tmux start)
|
|
18
|
+
|
|
19
|
+
Presets resolve through the ADR-022 cascade (.ace/herdr/ >
|
|
20
|
+
~/.ace/herdr/ > gem defaults) and may compose via `preset:` refs.
|
|
21
|
+
Tabs, panes, splits, commands, and agents are created in declared
|
|
22
|
+
order; --cwd overrides the resolved root/tab cwd.
|
|
23
|
+
DESC
|
|
24
|
+
|
|
25
|
+
example [
|
|
26
|
+
"development",
|
|
27
|
+
"development --cwd /path/to/project",
|
|
28
|
+
"development --quiet"
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
argument :preset, desc: "Workspace preset name (list with ace-herdr --list-presets workspaces)"
|
|
32
|
+
option :cwd, type: :string, desc: "Working directory overriding the resolved root/tab cwd"
|
|
33
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress output"
|
|
34
|
+
|
|
35
|
+
def initialize(executor: nil, control: nil)
|
|
36
|
+
@executor = executor
|
|
37
|
+
@control = control
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def call(preset: nil, **options)
|
|
41
|
+
translate_errors do
|
|
42
|
+
cli_error("preset name is required") if preset.to_s.empty?
|
|
43
|
+
control = @control || Organisms::ControlSurface.from_config(executor: executor, config: config)
|
|
44
|
+
result = control.create_workspace(preset, cwd: options[:cwd])
|
|
45
|
+
puts JSON.generate(result) unless options[:quiet]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|