ace-overseer 0.20.0 → 0.20.2
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/.ace-defaults/nav/protocols/wfi-sources/ace-overseer.yml +1 -1
- data/CHANGELOG.md +27 -0
- data/README.md +14 -14
- data/docs/usage.md +66 -41
- data/handbook/workflow-instructions/overseer.wf.md +13 -9
- data/handbook/workflow-instructions/roles/lab-coordinator.wf.md +99 -0
- data/handbook/workflow-instructions/roles/project-overseer.wf.md +103 -0
- data/handbook/workflow-instructions/roles/second-commander.wf.md +93 -0
- data/lib/ace/overseer/cli/commands/agents.rb +12 -7
- data/lib/ace/overseer/cli/commands/projects.rb +11 -7
- data/lib/ace/overseer/cli/commands/prompt.rb +44 -16
- data/lib/ace/overseer/cli/commands/prune.rb +51 -123
- data/lib/ace/overseer/cli/commands/review.rb +28 -8
- data/lib/ace/overseer/cli/commands/status.rb +33 -19
- data/lib/ace/overseer/cli/commands/stop.rb +18 -8
- data/lib/ace/overseer/cli/commands/work_on.rb +52 -16
- data/lib/ace/overseer/cli.rb +6 -9
- data/lib/ace/overseer/molecules/launch_request.rb +216 -0
- data/lib/ace/overseer/molecules/original_launch_child.rb +165 -0
- data/lib/ace/overseer/molecules/proposal_tick.rb +37 -9
- data/lib/ace/overseer/molecules/protected_selection.rb +46 -0
- data/lib/ace/overseer/molecules/window_opener.rb +9 -2
- data/lib/ace/overseer/organisms/launch_recovery.rb +65 -0
- data/lib/ace/overseer/organisms/protected_prune.rb +116 -0
- data/lib/ace/overseer/organisms/protected_review.rb +167 -0
- data/lib/ace/overseer/organisms/protected_status.rb +157 -0
- data/lib/ace/overseer/organisms/protected_steering.rb +78 -0
- data/lib/ace/overseer/organisms/protected_work_on.rb +102 -0
- data/lib/ace/overseer/organisms/work_on_orchestrator.rb +4 -5
- data/lib/ace/overseer/version.rb +1 -1
- data/lib/ace/overseer.rb +1 -2
- metadata +50 -13
- data/lib/ace/overseer/cli/commands/prepare.rb +0 -38
- data/lib/ace/overseer/molecules/lab_client.rb +0 -56
- data/lib/ace/overseer/molecules/lab_prune_safety_checker.rb +0 -69
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require "json"
|
|
3
|
+
require "ace/lab"
|
|
3
4
|
module Ace
|
|
4
5
|
module Overseer
|
|
5
6
|
module CLI
|
|
6
7
|
module Commands
|
|
7
8
|
class Agents < Ace::Support::Cli::Command
|
|
8
9
|
include Ace::Support::Cli::Base
|
|
9
|
-
desc "List
|
|
10
|
-
|
|
10
|
+
desc "List agents visible through maintained public topology"
|
|
11
|
+
option :project, required: true, desc: "Visible installed project ID"
|
|
12
|
+
def initialize(topology: nil)
|
|
11
13
|
super()
|
|
12
|
-
@
|
|
14
|
+
@topology = topology || Ace::Lab::Organisms::TopologyService.from_config
|
|
13
15
|
end
|
|
14
|
-
def call(**)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
def call(project:, **extra)
|
|
17
|
+
raise Error, "Unsupported topology options" unless extra.empty?
|
|
18
|
+
result = @topology.agents(project: project)
|
|
19
|
+
puts JSON.generate(result.envelope)
|
|
20
|
+
raise Error, "#{result.error_code}: #{result.message}" unless result.ok?
|
|
21
|
+
rescue StandardError => error
|
|
22
|
+
raise Ace::Support::Cli::Error, error.message
|
|
18
23
|
end
|
|
19
24
|
end
|
|
20
25
|
end
|
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require "json"
|
|
3
|
+
require "ace/lab"
|
|
3
4
|
module Ace
|
|
4
5
|
module Overseer
|
|
5
6
|
module CLI
|
|
6
7
|
module Commands
|
|
7
8
|
class Projects < Ace::Support::Cli::Command
|
|
8
9
|
include Ace::Support::Cli::Base
|
|
9
|
-
desc "List
|
|
10
|
-
def initialize(
|
|
10
|
+
desc "List projects visible through maintained public topology"
|
|
11
|
+
def initialize(topology: nil)
|
|
11
12
|
super()
|
|
12
|
-
@
|
|
13
|
+
@topology = topology || Ace::Lab::Organisms::TopologyService.from_config
|
|
13
14
|
end
|
|
14
|
-
def call(**)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
def call(**extra)
|
|
16
|
+
raise Error, "Unsupported topology options" unless extra.empty?
|
|
17
|
+
result = @topology.projects
|
|
18
|
+
puts JSON.generate(result.envelope)
|
|
19
|
+
raise Error, "#{result.error_code}: #{result.message}" unless result.ok?
|
|
20
|
+
rescue StandardError => error
|
|
21
|
+
raise Ace::Support::Cli::Error, error.message
|
|
18
22
|
end
|
|
19
23
|
end
|
|
20
24
|
end
|
|
@@ -1,32 +1,60 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
require "json"
|
|
3
|
+
require_relative "../../organisms/protected_steering"
|
|
2
4
|
module Ace
|
|
3
5
|
module Overseer
|
|
4
6
|
module CLI
|
|
5
7
|
module Commands
|
|
6
8
|
class Prompt < Ace::Support::Cli::Command
|
|
7
9
|
include Ace::Support::Cli::Base
|
|
8
|
-
desc "Prompt
|
|
9
|
-
option :
|
|
10
|
-
option :
|
|
11
|
-
|
|
10
|
+
desc "Prompt an exact protected attempt or inspect its original prompt mutation"
|
|
11
|
+
option :project, required: true, desc: "Installed project ID"
|
|
12
|
+
option :agent, required: true, desc: "Literal installed mapping ID"
|
|
13
|
+
option :assignment, required: true, desc: "Original assignment ID"
|
|
14
|
+
option :attempt, required: true, desc: "Original attempt ID"
|
|
15
|
+
option :mutation, required: true, desc: "Stable original prompt mutation ID"
|
|
16
|
+
option :expected_generation, type: :integer, desc: "Observed original generation; never refreshed"
|
|
17
|
+
option :file, desc: "Read exact bounded prompt bytes from this regular file"
|
|
18
|
+
option :stdin, type: :boolean, default: false, desc: "Read exact bounded prompt bytes from redirected stdin"
|
|
19
|
+
option :status, type: :boolean, default: false, desc: "Read original prompt status without sending input"
|
|
20
|
+
def initialize(steering: nil, input: $stdin)
|
|
12
21
|
super()
|
|
13
|
-
@
|
|
22
|
+
@steering = steering || Organisms::ProtectedSteering.new
|
|
14
23
|
@input = input
|
|
15
24
|
end
|
|
16
|
-
def call(
|
|
17
|
-
|
|
18
|
-
|
|
25
|
+
def call(project:, agent:, assignment:, attempt:, mutation:, expected_generation: nil,
|
|
26
|
+
file: nil, stdin: false, status: false, **extra)
|
|
27
|
+
raise Error, "Unsupported prompt options" unless extra.empty?
|
|
28
|
+
text = if status
|
|
29
|
+
raise Error, "Prompt status forbids input selectors and expected generation" if file || stdin || expected_generation
|
|
30
|
+
nil
|
|
19
31
|
else
|
|
20
|
-
|
|
21
|
-
|
|
32
|
+
raise Error, "Select exactly one --file or --stdin" unless (!!file ^ !!stdin)
|
|
33
|
+
file ? file_bytes(file) : stdin_bytes
|
|
34
|
+
end
|
|
35
|
+
result = @steering.prompt(project: project, agent: agent, assignment: assignment, attempt: attempt,
|
|
36
|
+
mutation: mutation, expected_generation: expected_generation, text: text, status: status)
|
|
37
|
+
puts JSON.pretty_generate(result)
|
|
38
|
+
rescue StandardError => error
|
|
39
|
+
raise Ace::Support::Cli::Error, error.message
|
|
40
|
+
end
|
|
41
|
+
private
|
|
42
|
+
def stdin_bytes
|
|
43
|
+
raise Error, "--stdin requires redirected input" if @input.respond_to?(:tty?) && @input.tty?
|
|
44
|
+
@input.read(16_385) || ""
|
|
45
|
+
end
|
|
46
|
+
def file_bytes(path)
|
|
47
|
+
File.open(path, File::RDONLY | File::NOFOLLOW | File::NONBLOCK) do |file|
|
|
48
|
+
before = file.stat
|
|
49
|
+
raise Error, "Prompt file must be bounded and regular" unless before.file? && before.size.between?(1, 16_384)
|
|
50
|
+
bytes = file.read(16_385) || ""
|
|
51
|
+
after = file.stat
|
|
52
|
+
identity = ->(stat) { [stat.dev, stat.ino, stat.size, stat.mtime, stat.ctime] }
|
|
53
|
+
unless bytes.bytesize == before.size && identity.call(before) == identity.call(after)
|
|
54
|
+
raise Error, "Prompt file changed during held read"
|
|
22
55
|
end
|
|
23
|
-
|
|
56
|
+
bytes
|
|
24
57
|
end
|
|
25
|
-
raise Ace::Support::Cli::Error, "prompt text must contain non-whitespace characters" if prompt.strip.empty?
|
|
26
|
-
|
|
27
|
-
puts @client.call("work", "prompt", work, stdin_data: prompt, json: false)
|
|
28
|
-
rescue => error
|
|
29
|
-
raise Ace::Support::Cli::Error.new(error.message)
|
|
30
58
|
end
|
|
31
59
|
end
|
|
32
60
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../../organisms/protected_prune"
|
|
4
|
+
|
|
3
5
|
module Ace
|
|
4
6
|
module Overseer
|
|
5
7
|
module CLI
|
|
@@ -19,31 +21,68 @@ module Ace
|
|
|
19
21
|
option :quiet, aliases: ["-q"], type: :boolean, default: false,
|
|
20
22
|
desc: "Suppress progress output; failures still print and signal via exit code"
|
|
21
23
|
option :debug, aliases: ["-d"], type: :boolean, default: false, desc: "Show debug output"
|
|
22
|
-
option :runtime, default: "tmux", desc: "Runtime (tmux, lab)"
|
|
23
24
|
option :preservation, type: :string,
|
|
24
25
|
desc: "YAML manifest (version 1) declaring verified cross-repository destinations"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
option :project, desc: "Protected target project"
|
|
27
|
+
option :agent, desc: "Exact protected target mapping"
|
|
28
|
+
option :attempt, desc: "Exact protected target attempt"
|
|
29
|
+
option :request, desc: "Bounded protected prune JSON FILE"
|
|
30
|
+
option :status, type: :boolean, default: false, desc: "Observe the existing protected service request"
|
|
31
|
+
option :mutation, desc: "Stable explicit protected service request ID"
|
|
32
|
+
option :expected_generation, type: :integer, desc: "Expected current authority mutation generation"
|
|
33
|
+
option :authorization, desc: "Existing exact service authorization reference"
|
|
34
|
+
|
|
35
|
+
def initialize(orchestrator: nil, protected_prune: nil, input: $stdin, output: $stdout)
|
|
28
36
|
super()
|
|
29
|
-
@orchestrator = orchestrator
|
|
37
|
+
@orchestrator = orchestrator
|
|
30
38
|
@input = input
|
|
31
39
|
@output = output
|
|
32
|
-
@
|
|
33
|
-
@lab_safety_checker = lab_safety_checker || Molecules::LabPruneSafetyChecker.new
|
|
34
|
-
@preservation_checker = preservation_checker || Molecules::GitPreservationChecker.new
|
|
40
|
+
@protected_prune = protected_prune || Organisms::ProtectedPrune.new
|
|
35
41
|
end
|
|
36
42
|
|
|
37
43
|
def call(**options)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
if options[:status]
|
|
45
|
+
if options.values_at(:project, :agent, :assignment, :attempt).any?
|
|
46
|
+
raise Ace::Support::Cli::Error, "Protected prune status selects its target only from FILE"
|
|
47
|
+
end
|
|
48
|
+
if options[:dry_run] || options[:yes] || options[:force] || options[:authorization] || options[:expected_generation] ||
|
|
49
|
+
options[:preservation] || !Array(options[:targets]).empty? || options.key?(:runtime)
|
|
50
|
+
raise Ace::Support::Cli::Error, "Protected prune status forbids effect/local options"
|
|
51
|
+
end
|
|
52
|
+
@output.puts JSON.generate(@protected_prune.status(request: options[:request], mutation: options[:mutation]))
|
|
53
|
+
return
|
|
54
|
+
end
|
|
55
|
+
if options[:project]
|
|
56
|
+
unless options.values_at(:project, :agent, :assignment, :attempt, :request).all? { |value| value.is_a?(String) && !value.empty? }
|
|
57
|
+
raise Ace::Support::Cli::Error, "Protected prune requires exact project, agent, assignment, attempt and request FILE"
|
|
58
|
+
end
|
|
59
|
+
unless !options[:force] && !options[:preservation] &&
|
|
60
|
+
Array(options[:targets]).empty? && !options.key?(:runtime)
|
|
61
|
+
raise Ace::Support::Cli::Error, "Protected prune forbids local options"
|
|
62
|
+
end
|
|
63
|
+
selectors = options.slice(:project, :agent, :assignment, :attempt, :request)
|
|
64
|
+
value = if options[:dry_run]
|
|
65
|
+
if options[:yes] || options[:mutation] || options[:authorization] || options[:expected_generation]
|
|
66
|
+
raise Ace::Support::Cli::Error, "Protected preview forbids effect options"
|
|
67
|
+
end
|
|
68
|
+
@protected_prune.preview(**selectors)
|
|
69
|
+
else
|
|
70
|
+
raise Ace::Support::Cli::Error, "Protected apply requires --yes" unless options[:yes]
|
|
71
|
+
@protected_prune.apply(**selectors, mutation: options[:mutation],
|
|
72
|
+
expected_generation: options[:expected_generation], authorization: options[:authorization])
|
|
73
|
+
end
|
|
74
|
+
@output.puts JSON.generate(value)
|
|
41
75
|
return
|
|
42
76
|
end
|
|
43
|
-
|
|
77
|
+
if options.values_at(:agent, :attempt, :request, :mutation, :authorization, :expected_generation).any?
|
|
78
|
+
raise Ace::Support::Cli::Error, "Protected prune options require --project"
|
|
79
|
+
end
|
|
80
|
+
raise Ace::Support::Cli::Error, "Prune uses the configured local runtime; protected physical cleanup has its own owner" if options.key?(:runtime)
|
|
44
81
|
|
|
45
82
|
Atoms::RepoGuard.ensure_repo!
|
|
46
83
|
|
|
84
|
+
@orchestrator ||= Organisms::PruneOrchestrator.new
|
|
85
|
+
|
|
47
86
|
targets = Array(options[:targets] || [])
|
|
48
87
|
progress = options[:quiet] ? nil : ->(msg) { puts msg }
|
|
49
88
|
result = @orchestrator.call(
|
|
@@ -93,117 +132,6 @@ module Ace
|
|
|
93
132
|
|
|
94
133
|
private
|
|
95
134
|
|
|
96
|
-
def prune_lab(**options)
|
|
97
|
-
raise Ace::Support::Cli::Error, "--assignment is not supported with Lab runtime; provide exact Work IDs" if options[:assignment]
|
|
98
|
-
raise Ace::Support::Cli::Error, "--force is not supported with Lab runtime" if options[:force]
|
|
99
|
-
raise Ace::Support::Cli::Error, "--preservation is not supported with Lab runtime" if options[:preservation]
|
|
100
|
-
|
|
101
|
-
works = Array(options[:targets]).map(&:to_s)
|
|
102
|
-
raise Ace::Support::Cli::Error, "provide at least one exact Lab Work ID to prune" if works.empty?
|
|
103
|
-
|
|
104
|
-
classifications = if atomic_destroy_supported?
|
|
105
|
-
works.to_h do |work|
|
|
106
|
-
[work, @lab_safety_checker.check(
|
|
107
|
-
lab_client: @lab_client,
|
|
108
|
-
work_id: work,
|
|
109
|
-
preservation_proof: lab_preservation_proof(work)
|
|
110
|
-
)]
|
|
111
|
-
end
|
|
112
|
-
else
|
|
113
|
-
# The raw Lab surface cannot make the no-writer check and the
|
|
114
|
-
# destruction atomic; claiming a probe was safe would be a
|
|
115
|
-
# lie, so the whole path is unsupported and preserved.
|
|
116
|
-
unsupported = Molecules::LabPruneSafetyChecker::Classification.new(
|
|
117
|
-
safe?: false,
|
|
118
|
-
reason: "lab runtime cannot make the no-writer check and destruction atomic; " \
|
|
119
|
-
"preserve the Work instead"
|
|
120
|
-
)
|
|
121
|
-
works.to_h { |work| [work, unsupported] }
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
if options[:dry_run]
|
|
125
|
-
print_lab_dry_run(classifications)
|
|
126
|
-
return
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
raise Ace::Support::Cli::Error, "Lab prune requires --yes after reviewing --dry-run" unless options[:yes]
|
|
130
|
-
|
|
131
|
-
blocked = []
|
|
132
|
-
destroyed = []
|
|
133
|
-
works.each do |work|
|
|
134
|
-
classification = classifications[work]
|
|
135
|
-
unless classification.safe?
|
|
136
|
-
blocked << [work, classification.reason]
|
|
137
|
-
next
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
# Guarded delegation: re-read the authoritative state
|
|
141
|
-
# immediately before destroy; a changed state aborts.
|
|
142
|
-
recheck = @lab_safety_checker.check(
|
|
143
|
-
lab_client: @lab_client,
|
|
144
|
-
work_id: work,
|
|
145
|
-
preservation_proof: lab_preservation_proof(work)
|
|
146
|
-
)
|
|
147
|
-
unless recheck.safe?
|
|
148
|
-
blocked << [work, "state changed before destroy: #{recheck.reason}"]
|
|
149
|
-
next
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
result = @lab_client.call("work", "destroy", work, "--confirm", json: false)
|
|
153
|
-
destroyed << work
|
|
154
|
-
puts result unless options[:quiet]
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
blocked.each do |work, reason|
|
|
158
|
-
puts "Blocked: lab work #{work}: #{reason}"
|
|
159
|
-
end
|
|
160
|
-
puts "#{destroyed.length} lab work(s) destroyed." unless options[:quiet]
|
|
161
|
-
return if blocked.empty?
|
|
162
|
-
|
|
163
|
-
raise Ace::Support::Cli::Error, "#{blocked.length} lab work(s) blocked; nothing unsafe was destroyed"
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
# Delegation is only honest when the Lab surface itself can make
|
|
167
|
-
# the state check and the destruction atomic. The raw CLI adapter
|
|
168
|
-
# cannot, so it reports unsupported; adapters with an atomic
|
|
169
|
-
# guarded destroy opt in.
|
|
170
|
-
def atomic_destroy_supported?
|
|
171
|
-
@lab_client.respond_to?(:supports_atomic_destroy?) && @lab_client.supports_atomic_destroy?
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
# Preservation evidence for a Lab Work comes from the Work's own
|
|
175
|
-
# documented surviving identity (repo/head/branch). A surface that
|
|
176
|
-
# does not document it cannot prove preservation.
|
|
177
|
-
def lab_preservation_proof(work)
|
|
178
|
-
entry = @lab_client.work_entry(work)
|
|
179
|
-
return Models::PreservationProof.blocked("lab work #{work} not found in authoritative status") if entry.nil?
|
|
180
|
-
|
|
181
|
-
repo = entry["repo"].to_s
|
|
182
|
-
head = entry["head"].to_s
|
|
183
|
-
branch = entry["branch"].to_s
|
|
184
|
-
if repo.empty? || head.empty? || branch.empty? || !repo.start_with?("/")
|
|
185
|
-
return Models::PreservationProof.blocked(
|
|
186
|
-
"lab work #{work} does not document preservation data (repo/head/branch)"
|
|
187
|
-
)
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
accepted_base = @preservation_checker.accepted_base_for(repo)
|
|
191
|
-
@preservation_checker.ancestry_proof(repo: repo, head: head, accepted_base: accepted_base)
|
|
192
|
-
rescue Ace::Overseer::Error => e
|
|
193
|
-
Models::PreservationProof.blocked("lab preservation evidence unavailable: #{e.message}")
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
def print_lab_dry_run(classifications)
|
|
197
|
-
puts "Lab prune classification:"
|
|
198
|
-
classifications.each do |work, classification|
|
|
199
|
-
if classification.safe?
|
|
200
|
-
puts " #{work}: safe (terminal, preserved, no in-flight work)"
|
|
201
|
-
else
|
|
202
|
-
puts " #{work}: BLOCKED — #{classification.reason}"
|
|
203
|
-
end
|
|
204
|
-
end
|
|
205
|
-
end
|
|
206
|
-
|
|
207
135
|
def print_assignment_result(result)
|
|
208
136
|
candidate = result[:assignment_candidate]
|
|
209
137
|
if result[:dry_run]
|
|
@@ -1,20 +1,40 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
require "json"
|
|
3
|
+
require_relative "../../organisms/protected_review"
|
|
4
|
+
|
|
2
5
|
module Ace
|
|
3
6
|
module Overseer
|
|
4
7
|
module CLI
|
|
5
8
|
module Commands
|
|
6
9
|
class Review < Ace::Support::Cli::Command
|
|
7
10
|
include Ace::Support::Cli::Base
|
|
8
|
-
desc "
|
|
9
|
-
option :
|
|
10
|
-
option :
|
|
11
|
-
|
|
11
|
+
desc "Review an exact protected candidate, inspect its request, or cancel its reservation"
|
|
12
|
+
option :project, required: true, desc: "Installed project ID"
|
|
13
|
+
option :agent, required: true, desc: "Installed agent mapping ID"
|
|
14
|
+
option :assignment, required: true, desc: "Assignment ID"
|
|
15
|
+
option :attempt, required: true, desc: "Attempt ID"
|
|
16
|
+
option :mutation, required: true, desc: "Request or cancel mutation; original request ID for status"
|
|
17
|
+
option :status, type: :boolean, default: false, desc: "Observe the original review request without execution"
|
|
18
|
+
option :cancel, type: :boolean, default: false, desc: "Cancel the exact review reservation"
|
|
19
|
+
option :head, desc: "Exact candidate commit"
|
|
20
|
+
option :candidate_generation, type: :integer, desc: "Exact candidate generation"
|
|
21
|
+
option :expected_generation, type: :integer, desc: "Observed authority generation; never refreshed implicitly"
|
|
22
|
+
option :accept_mutation, desc: "Distinct acceptance mutation for a new review"
|
|
23
|
+
option :review_event, desc: "Exact reservation event to cancel"
|
|
24
|
+
|
|
25
|
+
def initialize(review: nil)
|
|
12
26
|
super()
|
|
13
|
-
@
|
|
27
|
+
@review = review || Organisms::ProtectedReview.new
|
|
14
28
|
end
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
29
|
+
|
|
30
|
+
def call(project:, agent:, assignment:, attempt:, mutation:, status: false, cancel: false,
|
|
31
|
+
head: nil, candidate_generation: nil, expected_generation: nil, accept_mutation: nil, review_event: nil, **extra)
|
|
32
|
+
raise Error, "Unsupported review options" unless extra.empty?
|
|
33
|
+
result = @review.call(project: project, agent: agent, assignment: assignment, attempt: attempt,
|
|
34
|
+
mutation: mutation, status: status, cancel: cancel, head: head, candidate_generation: candidate_generation,
|
|
35
|
+
expected_generation: expected_generation, accept_mutation: accept_mutation, review_event: review_event)
|
|
36
|
+
puts JSON.pretty_generate(result)
|
|
37
|
+
rescue StandardError => error
|
|
18
38
|
raise Ace::Support::Cli::Error.new(error.message)
|
|
19
39
|
end
|
|
20
40
|
end
|
|
@@ -16,39 +16,38 @@ module Ace
|
|
|
16
16
|
option :quiet, aliases: ["-q"], type: :boolean, default: false, desc: "Suppress non-essential output"
|
|
17
17
|
option :debug, aliases: ["-d"], type: :boolean, default: false, desc: "Show debug output"
|
|
18
18
|
option :watch, aliases: ["-w"], type: :boolean, default: false, desc: "Auto-refresh dashboard"
|
|
19
|
-
option :runtime,
|
|
20
|
-
option :project, desc: "
|
|
19
|
+
option :runtime, desc: "Runtime (auto, tmux, herdr)"
|
|
20
|
+
option :project, desc: "Read protected canonical status for this project"
|
|
21
|
+
option :agent, desc: "Exact visible protected agent ID (requires --project)"
|
|
21
22
|
|
|
22
|
-
def initialize(collector: nil, config: nil,
|
|
23
|
+
def initialize(collector: nil, config: nil, protected_status: nil, proposal_tick: nil)
|
|
23
24
|
super()
|
|
24
25
|
@collector = collector || Organisms::StatusCollector.new
|
|
25
26
|
@config = config
|
|
26
|
-
@
|
|
27
|
+
@protected_status = protected_status || Organisms::ProtectedStatus.new
|
|
27
28
|
@proposal_tick = proposal_tick || Molecules::ProposalTick.new
|
|
28
29
|
end
|
|
29
30
|
|
|
30
|
-
def call(format:, runtime:
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
def call(format:, runtime: nil, project: nil, agent: nil, **options)
|
|
32
|
+
raise Ace::Support::Cli::Error, "--agent requires --project" if agent && project.to_s.empty?
|
|
33
|
+
selected_runtime = runtime || (@config || Ace::Overseer.config)["runtime"] || "auto"
|
|
34
|
+
unless project.to_s.empty?
|
|
35
|
+
unless %w[auto tmux herdr].include?(selected_runtime)
|
|
36
|
+
raise Ace::Support::Cli::Error, "unsupported runtime for protected inventory"
|
|
37
|
+
end
|
|
33
38
|
if options[:watch]
|
|
34
|
-
raise Ace::Support::Cli::Error, "
|
|
39
|
+
raise Ace::Support::Cli::Error, "Protected status is a bounded snapshot; omit --watch"
|
|
35
40
|
end
|
|
36
|
-
|
|
37
|
-
arguments.concat(["--project", project]) unless project.to_s.empty?
|
|
38
|
-
arguments << "--json" if format == "json"
|
|
39
|
-
output = @lab_client.call(*arguments, json: false)
|
|
41
|
+
value = @protected_status.collect(project: project, agent: agent)
|
|
40
42
|
unless options[:quiet]
|
|
41
43
|
warn @proposal_error if @proposal_error
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
output = JSON.pretty_generate(value.merge("proposal_resolution" =>
|
|
45
|
-
{"status" => "deferred", "error" => @proposal_error})) if value.is_a?(Hash)
|
|
46
|
-
end
|
|
47
|
-
puts output
|
|
44
|
+
value = value.merge("proposal_resolution" => {"status" => "deferred", "error" => @proposal_error}) if @proposal_error
|
|
45
|
+
puts(format == "json" ? JSON.pretty_generate(value) : protected_table(value))
|
|
48
46
|
end
|
|
49
47
|
return
|
|
50
48
|
end
|
|
51
|
-
raise Ace::Support::Cli::Error, "unsupported runtime: #{
|
|
49
|
+
raise Ace::Support::Cli::Error, "unsupported local runtime: #{selected_runtime}" unless %w[auto tmux].include?(selected_runtime)
|
|
50
|
+
tick_proposals
|
|
52
51
|
|
|
53
52
|
Atoms::RepoGuard.ensure_repo!
|
|
54
53
|
return if options[:quiet]
|
|
@@ -66,6 +65,21 @@ module Ace
|
|
|
66
65
|
|
|
67
66
|
private
|
|
68
67
|
|
|
68
|
+
def protected_table(value)
|
|
69
|
+
lines = ["#{value.fetch('project_id')}: #{value.fetch('visible_capacity')}/#{value.fetch('provisioned_capacity')} visible mappings (#{value.fetch('visibility')})"]
|
|
70
|
+
value.fetch("agents").each do |agent|
|
|
71
|
+
if agent.fetch("status") != "ok"
|
|
72
|
+
lines << "#{agent.fetch('agent_id')}\tunavailable"
|
|
73
|
+
next
|
|
74
|
+
end
|
|
75
|
+
agent.fetch("inventory").fetch("items").each do |row|
|
|
76
|
+
lines << [agent.fetch("agent_id"), row.fetch("assignment_id"), row.fetch("attempt_id") || "registration",
|
|
77
|
+
row.fetch("canonical_state") || "registered", row.fetch("reservation_release_event_id") ? "released" : "retained"].join("\t")
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
lines.join("\n")
|
|
81
|
+
end
|
|
82
|
+
|
|
69
83
|
def tick_proposals
|
|
70
84
|
@proposal_tick.call
|
|
71
85
|
@proposal_error = nil
|
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
require "json"
|
|
3
|
+
require_relative "../../organisms/protected_steering"
|
|
2
4
|
module Ace
|
|
3
5
|
module Overseer
|
|
4
6
|
module CLI
|
|
5
7
|
module Commands
|
|
6
8
|
class Stop < Ace::Support::Cli::Command
|
|
7
9
|
include Ace::Support::Cli::Base
|
|
8
|
-
desc "
|
|
9
|
-
option :
|
|
10
|
-
|
|
10
|
+
desc "Request proof-authenticated stop of an exact protected attempt"
|
|
11
|
+
option :project, required: true, desc: "Installed project ID"
|
|
12
|
+
option :agent, required: true, desc: "Literal installed mapping ID"
|
|
13
|
+
option :assignment, required: true, desc: "Original assignment ID"
|
|
14
|
+
option :attempt, required: true, desc: "Original attempt ID"
|
|
15
|
+
option :mutation, required: true, desc: "Stable stop mutation ID"
|
|
16
|
+
option :expected_generation, type: :integer, required: true, desc: "Observed original generation; never refreshed"
|
|
17
|
+
def initialize(steering: nil)
|
|
11
18
|
super()
|
|
12
|
-
@
|
|
19
|
+
@steering = steering || Organisms::ProtectedSteering.new
|
|
13
20
|
end
|
|
14
|
-
def call(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
def call(project:, agent:, assignment:, attempt:, mutation:, expected_generation:, **extra)
|
|
22
|
+
raise Error, "Unsupported stop options" unless extra.empty?
|
|
23
|
+
result = @steering.stop(project: project, agent: agent, assignment: assignment, attempt: attempt,
|
|
24
|
+
mutation: mutation, expected_generation: expected_generation)
|
|
25
|
+
puts JSON.pretty_generate(result)
|
|
26
|
+
rescue StandardError => error
|
|
27
|
+
raise Ace::Support::Cli::Error, error.message
|
|
18
28
|
end
|
|
19
29
|
end
|
|
20
30
|
end
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../../organisms/launch_recovery"
|
|
4
|
+
require_relative "../../organisms/protected_work_on"
|
|
5
|
+
|
|
3
6
|
module Ace
|
|
4
7
|
module Overseer
|
|
5
8
|
module CLI
|
|
@@ -7,35 +10,68 @@ module Ace
|
|
|
7
10
|
class WorkOn < Ace::Support::Cli::Command
|
|
8
11
|
include Ace::Support::Cli::Base
|
|
9
12
|
|
|
10
|
-
desc "
|
|
13
|
+
desc "Start local task work or retain one original protected launch"
|
|
11
14
|
|
|
12
15
|
option :task, aliases: ["-t"], type: :array,
|
|
13
16
|
desc: "Task reference(s), repeatable and comma-separated (e.g., 230 --task 231,232)"
|
|
17
|
+
option :recover_request, desc: "Read retained protected invocation input and original canonical reservation"
|
|
14
18
|
option :preset, aliases: ["-p"], desc: "Assignment preset name"
|
|
15
|
-
option :runtime,
|
|
16
|
-
option :
|
|
17
|
-
option :
|
|
19
|
+
option :runtime, desc: "Terminal backend (auto, tmux, herdr)"
|
|
20
|
+
option :project, desc: "Explicit protected project ID"
|
|
21
|
+
option :base_head, desc: "Explicit reviewed code base (must match current source HEAD)"
|
|
22
|
+
option :mutation, desc: "Stable retained protected invocation ID"
|
|
23
|
+
option :dependency_report, type: :array, desc: "Selected task:assignment:step report identities"
|
|
24
|
+
option :agent, desc: "Exact visible protected mapping ID (requires project)"
|
|
18
25
|
option :quiet, aliases: ["-q"], type: :boolean, default: false, desc: "Suppress non-essential output"
|
|
19
26
|
option :debug, aliases: ["-d"], type: :boolean, default: false, desc: "Show debug output"
|
|
20
27
|
|
|
21
|
-
def initialize(orchestrator: nil,
|
|
28
|
+
def initialize(orchestrator: nil, recovery: nil, protected_work_on: nil, config: nil)
|
|
22
29
|
super()
|
|
30
|
+
@recovery = recovery || Organisms::LaunchRecovery.new
|
|
23
31
|
@orchestrator = orchestrator || Organisms::WorkOnOrchestrator.new
|
|
24
|
-
@
|
|
32
|
+
@protected_work_on = protected_work_on || Organisms::ProtectedWorkOn.new
|
|
33
|
+
@config = config
|
|
25
34
|
end
|
|
26
35
|
|
|
27
|
-
def call(task: nil, preset: nil, runtime:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
def call(task: nil, preset: nil, runtime: nil, project: nil, agent: nil, mutation: nil, base_head: nil, dependency_report: nil, recover_request: nil, **options)
|
|
37
|
+
raise Ace::Support::Cli::Error, "--work is obsolete; select task/project or retained request" if options.key?(:work)
|
|
38
|
+
if recover_request
|
|
39
|
+
if task || preset || project || agent || runtime || mutation || base_head || dependency_report
|
|
40
|
+
raise Ace::Support::Cli::Error, "--recover-request cannot be combined with launch mode overrides"
|
|
31
41
|
end
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
puts output unless options[:quiet]
|
|
42
|
+
# Retained identity/canonical attribution remains mandatory even
|
|
43
|
+
# under --quiet. This path never enters preparation or launch.
|
|
44
|
+
puts JSON.generate(@recovery.call(path: recover_request))
|
|
36
45
|
return
|
|
37
46
|
end
|
|
38
|
-
|
|
47
|
+
selected_runtime = runtime || (@config || Ace::Overseer.config)["runtime"] || "auto"
|
|
48
|
+
raise Ace::Support::Cli::Error, "unsupported runtime: #{selected_runtime}" unless %w[auto tmux herdr].include?(selected_runtime)
|
|
49
|
+
if project
|
|
50
|
+
refs = normalize_task_refs(task)
|
|
51
|
+
raise Ace::Support::Cli::Error, "Protected launch requires exactly one reviewed leaf task and no preset override" unless refs.one? && preset.nil?
|
|
52
|
+
reports = Array(dependency_report).map do |entry|
|
|
53
|
+
fields = entry.split(":", -1)
|
|
54
|
+
raise Ace::Support::Cli::Error, "Report identity must be task:assignment:step" unless fields.size == 3 && fields.none?(&:empty?)
|
|
55
|
+
%w[task_id assignment_id number].zip(fields).to_h
|
|
56
|
+
end
|
|
57
|
+
report = ->(identity) { $stdout.write(JSON.generate(identity) + "\n"); $stdout.flush }
|
|
58
|
+
result = @protected_work_on.start(task_ref: refs.first, project: project, agent: agent, runtime: selected_runtime,
|
|
59
|
+
dependency_reports: reports, mutation_id: mutation, base_head: base_head, report: report)
|
|
60
|
+
child = result.fetch("child")
|
|
61
|
+
begin
|
|
62
|
+
puts JSON.generate("type" => "original_launch_observation", "pid" => child.pid, "state" => child.state,
|
|
63
|
+
"ready" => child.ready, "error" => child.error)
|
|
64
|
+
$stdout.flush
|
|
65
|
+
ensure
|
|
66
|
+
# An output failure after creation cannot discard the original
|
|
67
|
+
# living child or turn local observation into canonical release.
|
|
68
|
+
@protected_work_on.serve(child)
|
|
69
|
+
end
|
|
70
|
+
return
|
|
71
|
+
end
|
|
72
|
+
if agent || mutation || base_head || dependency_report
|
|
73
|
+
raise Ace::Support::Cli::Error, "Protected mapping/invocation/report selection requires --project"
|
|
74
|
+
end
|
|
39
75
|
|
|
40
76
|
Atoms::RepoGuard.ensure_repo!
|
|
41
77
|
|
|
@@ -49,7 +85,7 @@ module Ace
|
|
|
49
85
|
task_ref: task_refs.first,
|
|
50
86
|
task_refs: task_refs,
|
|
51
87
|
cli_preset: preset,
|
|
52
|
-
on_progress: progress
|
|
88
|
+
on_progress: progress, runtime: selected_runtime
|
|
53
89
|
)
|
|
54
90
|
|
|
55
91
|
return if options[:quiet]
|