ace-coworker 0.6.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/coworker/config.yml +6 -0
- data/.ace-defaults/coworker/roles/integrator.yml +15 -0
- data/.ace-defaults/coworker/roles/reviewer.yml +15 -0
- data/.ace-defaults/coworker/roles/spec.yml +14 -0
- data/.ace-defaults/coworker/roles/worker.yml +13 -0
- data/.ace-defaults/coworker/sequences/integrator.yml +20 -0
- data/.ace-defaults/coworker/sequences/reviewer.yml +20 -0
- data/.ace-defaults/coworker/sequences/spec.yml +16 -0
- data/.ace-defaults/coworker/sequences/worker.yml +21 -0
- data/.ace-defaults/nav/protocols/wfi-sources/ace-coworker.yml +19 -0
- data/CHANGELOG.md +255 -0
- data/README.md +98 -0
- data/Rakefile +13 -0
- data/docs/usage.md +564 -0
- data/exe/ace-coworker +16 -0
- data/handbook/workflow-instructions/coworker/integrator.wf.md +162 -0
- data/handbook/workflow-instructions/coworker/reviewer.wf.md +211 -0
- data/handbook/workflow-instructions/coworker/spec.wf.md +100 -0
- data/handbook/workflow-instructions/coworker/worker.wf.md +103 -0
- data/lib/ace/coworker/atoms/active_executor_rule.rb +71 -0
- data/lib/ace/coworker/atoms/brief_formatter.rb +33 -0
- data/lib/ace/coworker/atoms/config_schema.rb +121 -0
- data/lib/ace/coworker/atoms/integration_gate.rb +92 -0
- data/lib/ace/coworker/atoms/outcome_planner.rb +79 -0
- data/lib/ace/coworker/atoms/prepare_decision.rb +75 -0
- data/lib/ace/coworker/atoms/prompt_template.rb +28 -0
- data/lib/ace/coworker/atoms/report_record_formatter.rb +60 -0
- data/lib/ace/coworker/atoms/resume_brief_formatter.rb +84 -0
- data/lib/ace/coworker/atoms/resume_planner.rb +78 -0
- data/lib/ace/coworker/atoms/role_preset_schema.rb +91 -0
- data/lib/ace/coworker/atoms/round_record_schema.rb +62 -0
- data/lib/ace/coworker/atoms/round_rule.rb +105 -0
- data/lib/ace/coworker/cli/commands/commission.rb +100 -0
- data/lib/ace/coworker/cli/commands/gate.rb +91 -0
- data/lib/ace/coworker/cli/commands/prepare.rb +138 -0
- data/lib/ace/coworker/cli/commands/report.rb +79 -0
- data/lib/ace/coworker/cli/commands/resume.rb +57 -0
- data/lib/ace/coworker/cli/commands/roles.rb +60 -0
- data/lib/ace/coworker/cli/commands/rounds.rb +99 -0
- data/lib/ace/coworker/cli/commands/start.rb +39 -0
- data/lib/ace/coworker/cli/commands/status.rb +95 -0
- data/lib/ace/coworker/cli.rb +83 -0
- data/lib/ace/coworker/errors.rb +10 -0
- data/lib/ace/coworker/models/assignment_snapshot.rb +65 -0
- data/lib/ace/coworker/models/brief.rb +38 -0
- data/lib/ace/coworker/models/config.rb +54 -0
- data/lib/ace/coworker/models/outcome.rb +39 -0
- data/lib/ace/coworker/models/preparation.rb +67 -0
- data/lib/ace/coworker/models/resume_plan.rb +47 -0
- data/lib/ace/coworker/molecules/assignment_metadata.rb +57 -0
- data/lib/ace/coworker/molecules/assignment_probe.rb +57 -0
- data/lib/ace/coworker/molecules/config_loader.rb +119 -0
- data/lib/ace/coworker/molecules/git_state_probe.rb +63 -0
- data/lib/ace/coworker/molecules/hitl_reporter.rb +145 -0
- data/lib/ace/coworker/molecules/instruction_resolver.rb +82 -0
- data/lib/ace/coworker/molecules/project_root.rb +26 -0
- data/lib/ace/coworker/molecules/report_state_store.rb +53 -0
- data/lib/ace/coworker/molecules/review_round_store.rb +115 -0
- data/lib/ace/coworker/molecules/role_preset_loader.rb +95 -0
- data/lib/ace/coworker/molecules/role_sequence_composer.rb +245 -0
- data/lib/ace/coworker/molecules/step_evidence_reader.rb +60 -0
- data/lib/ace/coworker/molecules/step_report_reader.rb +39 -0
- data/lib/ace/coworker/molecules/task_probe.rb +40 -0
- data/lib/ace/coworker/molecules/task_record_writer.rb +44 -0
- data/lib/ace/coworker/molecules/worktree_executor_probe.rb +139 -0
- data/lib/ace/coworker/molecules/worktree_provision_probe.rb +137 -0
- data/lib/ace/coworker/organisms/commission_orchestrator.rb +148 -0
- data/lib/ace/coworker/organisms/prepare_orchestrator.rb +374 -0
- data/lib/ace/coworker/organisms/report_orchestrator.rb +192 -0
- data/lib/ace/coworker/organisms/resume_orchestrator.rb +91 -0
- data/lib/ace/coworker/organisms/start_config_writer.rb +127 -0
- data/lib/ace/coworker/organisms/start_orchestrator.rb +45 -0
- data/lib/ace/coworker/organisms/status_orchestrator.rb +62 -0
- data/lib/ace/coworker/version.rb +7 -0
- data/lib/ace/coworker.rb +86 -0
- metadata +319 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Coworker
|
|
7
|
+
module CLI
|
|
8
|
+
module Commands
|
|
9
|
+
class Resume < Ace::Support::Cli::Command
|
|
10
|
+
include Ace::Support::Cli::Base
|
|
11
|
+
|
|
12
|
+
desc "Assess interrupted work in this worktree and resume it"
|
|
13
|
+
|
|
14
|
+
option :config, desc: "Path to coworker config (default: .ace-local/coworker/config.yml)"
|
|
15
|
+
option :assignment, desc: "Assignment ID (default: config assignment)"
|
|
16
|
+
option :"dry-run", type: :boolean, desc: "Derive the resume plan without redelivering reports"
|
|
17
|
+
option :format, default: "table", desc: "Output format (table, json)"
|
|
18
|
+
option :quiet, aliases: ["-q"], type: :boolean, default: false, desc: "Suppress non-essential output"
|
|
19
|
+
option :debug, aliases: ["-d"], type: :boolean, default: false, desc: "Show debug output"
|
|
20
|
+
|
|
21
|
+
def call(**options)
|
|
22
|
+
orchestrator = Organisms::ResumeOrchestrator.new
|
|
23
|
+
result = orchestrator.call(
|
|
24
|
+
config_path: options[:config],
|
|
25
|
+
assignment: options[:assignment],
|
|
26
|
+
dry_run: options[:"dry-run"] == true
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Render the plan first: on a delegated report failure the
|
|
30
|
+
# resume guidance still matters; then keep the non-zero exit.
|
|
31
|
+
unless options[:quiet]
|
|
32
|
+
if options[:format] == "json"
|
|
33
|
+
puts JSON.pretty_generate(to_h(result))
|
|
34
|
+
else
|
|
35
|
+
puts Atoms::ResumeBriefFormatter.format_table(result.plan, result)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
if result.exit_code != 0
|
|
40
|
+
raise Ace::Support::Cli::Error, result.message
|
|
41
|
+
end
|
|
42
|
+
rescue Ace::Coworker::Error => e
|
|
43
|
+
raise Ace::Support::Cli::Error, e.message
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def to_h(result)
|
|
49
|
+
payload = result.plan.to_h
|
|
50
|
+
payload["message"] = result.message
|
|
51
|
+
payload
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Coworker
|
|
7
|
+
module CLI
|
|
8
|
+
module Commands
|
|
9
|
+
class Roles < Ace::Support::Cli::Command
|
|
10
|
+
include Ace::Support::Cli::Base
|
|
11
|
+
|
|
12
|
+
desc "List the role preset catalog (gem defaults + project overrides)"
|
|
13
|
+
|
|
14
|
+
option :format, default: "table", desc: "Output format (table, json)"
|
|
15
|
+
option :quiet, aliases: ["-q"], type: :boolean, default: false, desc: "Suppress non-essential output"
|
|
16
|
+
option :debug, aliases: ["-d"], type: :boolean, default: false, desc: "Show debug output"
|
|
17
|
+
|
|
18
|
+
def initialize(preset_loader: nil)
|
|
19
|
+
super()
|
|
20
|
+
@preset_loader = preset_loader
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Read-only catalog query; exit 0 for any valid catalog state (an
|
|
24
|
+
# empty catalog is a state, not an error). An invalid preset file
|
|
25
|
+
# surfaces as a CLI error (non-zero exit).
|
|
26
|
+
def call(**options)
|
|
27
|
+
loader = @preset_loader || begin
|
|
28
|
+
project_root = Molecules::ProjectRoot.new.resolve
|
|
29
|
+
Molecules::RolePresetLoader.new(project_root: project_root)
|
|
30
|
+
end
|
|
31
|
+
presets = loader.list
|
|
32
|
+
|
|
33
|
+
return if options[:quiet]
|
|
34
|
+
|
|
35
|
+
if options[:format] == "json"
|
|
36
|
+
puts JSON.pretty_generate("roles" => presets)
|
|
37
|
+
else
|
|
38
|
+
puts render_table(presets)
|
|
39
|
+
end
|
|
40
|
+
rescue Ace::Coworker::Error => e
|
|
41
|
+
raise Ace::Support::Cli::Error, e.message
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def render_table(presets)
|
|
47
|
+
return "Coworker roles: none found (gem .ace-defaults/coworker/roles/ and project .ace/coworker/roles/)" if presets.empty?
|
|
48
|
+
|
|
49
|
+
lines = ["Coworker roles:"]
|
|
50
|
+
presets.each do |preset|
|
|
51
|
+
lines << " #{preset['name'].ljust(10)} #{preset['source'].ljust(8)} #{preset['description']}"
|
|
52
|
+
lines << " #{" " * 10} instructions: #{preset['instructions']}"
|
|
53
|
+
end
|
|
54
|
+
lines.join("\n")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Coworker
|
|
7
|
+
module CLI
|
|
8
|
+
module Commands
|
|
9
|
+
class Rounds < Ace::Support::Cli::Command
|
|
10
|
+
include Ace::Support::Cli::Base
|
|
11
|
+
|
|
12
|
+
desc "Record a review round for an assignment and print the 3/2/10 verdict"
|
|
13
|
+
|
|
14
|
+
example [
|
|
15
|
+
"record --assignment asg01 --type full --status completed",
|
|
16
|
+
"record --assignment asg01 --type full --status completed \\",
|
|
17
|
+
" --confirmed f-1:high,f-2:low --resolved f-3",
|
|
18
|
+
"record --assignment asg01 --type delta --status aborted"
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
argument :action, required: true, desc: "Subcommand (record)"
|
|
22
|
+
|
|
23
|
+
option :assignment, required: true, desc: "Assignment ID"
|
|
24
|
+
option :type, required: true, desc: "Round type (full, delta)"
|
|
25
|
+
option :status, required: true, desc: "Round status (completed, aborted)"
|
|
26
|
+
option :confirmed, type: :array, default: [], desc: "Confirmed findings as id:severity pairs (e.g. f-1:high)"
|
|
27
|
+
option :resolved, type: :array, default: [], desc: "Finding ids resolved by this round"
|
|
28
|
+
option :format, default: "table", desc: "Output format (table, json)"
|
|
29
|
+
option :quiet, aliases: ["-q"], type: :boolean, default: false, desc: "Suppress non-essential output"
|
|
30
|
+
option :debug, aliases: ["-d"], type: :boolean, default: false, desc: "Show debug output"
|
|
31
|
+
|
|
32
|
+
def initialize(store: nil)
|
|
33
|
+
super()
|
|
34
|
+
@store = store
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def call(action:, assignment:, type:, status:, confirmed: [], resolved: [],
|
|
38
|
+
format: "table", **options)
|
|
39
|
+
raise Ace::Support::Cli::Error, "unknown subcommand '#{action}' (expected: record)" unless action == "record"
|
|
40
|
+
|
|
41
|
+
store = @store || begin
|
|
42
|
+
project_root = Molecules::ProjectRoot.new.resolve
|
|
43
|
+
Molecules::ReviewRoundStore.new(project_root: project_root)
|
|
44
|
+
end
|
|
45
|
+
verdict = store.record!(assignment, build_record(type, status, confirmed, resolved))
|
|
46
|
+
|
|
47
|
+
return if options[:quiet]
|
|
48
|
+
|
|
49
|
+
if format == "json"
|
|
50
|
+
puts JSON.pretty_generate(json_payload(assignment, verdict))
|
|
51
|
+
else
|
|
52
|
+
puts render(assignment, verdict)
|
|
53
|
+
end
|
|
54
|
+
rescue Ace::Coworker::Error => e
|
|
55
|
+
raise Ace::Support::Cli::Error, e.message
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def build_record(type, status, confirmed, resolved)
|
|
61
|
+
{
|
|
62
|
+
"type" => type,
|
|
63
|
+
"status" => status,
|
|
64
|
+
"confirmed" => Array(confirmed).map { |pair| parse_finding(pair) },
|
|
65
|
+
"resolved" => Array(resolved).map(&:to_s)
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def parse_finding(pair)
|
|
70
|
+
id, severity = pair.to_s.split(":", 2)
|
|
71
|
+
{"id" => id.to_s, "severity" => severity.to_s}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def json_payload(assignment, verdict)
|
|
75
|
+
{
|
|
76
|
+
"assignment" => assignment,
|
|
77
|
+
"verdict" => verdict[:verdict].to_s,
|
|
78
|
+
"full_rounds" => verdict[:full_rounds],
|
|
79
|
+
"completed_full_rounds" => verdict[:completed_full_rounds],
|
|
80
|
+
"consecutive_clean" => verdict[:consecutive_clean],
|
|
81
|
+
"unresolved_high_critical" => verdict[:unresolved_high_critical]
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def render(assignment, verdict)
|
|
86
|
+
lines = ["Coworker rounds record"]
|
|
87
|
+
lines << " assignment: #{assignment}"
|
|
88
|
+
lines << " verdict: #{verdict[:verdict]}"
|
|
89
|
+
lines << " full rounds: #{verdict[:full_rounds]}"
|
|
90
|
+
lines << " completed full rounds: #{verdict[:completed_full_rounds]}"
|
|
91
|
+
lines << " consecutive clean: #{verdict[:consecutive_clean]}"
|
|
92
|
+
lines << " unresolved High/Critical: #{verdict[:unresolved_high_critical].join(', ')}"
|
|
93
|
+
lines.join("\n")
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Coworker
|
|
5
|
+
module CLI
|
|
6
|
+
module Commands
|
|
7
|
+
class Start < Ace::Support::Cli::Command
|
|
8
|
+
include Ace::Support::Cli::Base
|
|
9
|
+
|
|
10
|
+
desc "Validate prepared run config and render the executor brief"
|
|
11
|
+
|
|
12
|
+
option :config, desc: "Path to coworker config (default: .ace-local/coworker/config.yml)"
|
|
13
|
+
option :format, default: "table", desc: "Output format (table, json)"
|
|
14
|
+
option :quiet, aliases: ["-q"], type: :boolean, default: false, desc: "Suppress non-essential output"
|
|
15
|
+
option :debug, aliases: ["-d"], type: :boolean, default: false, desc: "Show debug output"
|
|
16
|
+
|
|
17
|
+
def initialize(orchestrator: nil)
|
|
18
|
+
super()
|
|
19
|
+
@orchestrator = orchestrator
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call(config: nil, format: "table", **options)
|
|
23
|
+
orchestrator = @orchestrator || Organisms::StartOrchestrator.new
|
|
24
|
+
brief = orchestrator.call(config_path: config)
|
|
25
|
+
return if options[:quiet]
|
|
26
|
+
|
|
27
|
+
if format == "json"
|
|
28
|
+
puts Atoms::BriefFormatter.format_json(brief)
|
|
29
|
+
else
|
|
30
|
+
puts Atoms::BriefFormatter.format_table(brief)
|
|
31
|
+
end
|
|
32
|
+
rescue Ace::Coworker::Error, StandardError => e
|
|
33
|
+
raise Ace::Support::Cli::Error, e.message
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Coworker
|
|
7
|
+
module CLI
|
|
8
|
+
module Commands
|
|
9
|
+
class Status < Ace::Support::Cli::Command
|
|
10
|
+
include Ace::Support::Cli::Base
|
|
11
|
+
|
|
12
|
+
desc "Show readiness, identity, and assignment progress"
|
|
13
|
+
|
|
14
|
+
option :config, desc: "Path to coworker config (default: .ace-local/coworker/config.yml)"
|
|
15
|
+
option :assignment, desc: "Show status for a specific assignment ID (default: config assignment)"
|
|
16
|
+
option :format, default: "table", desc: "Output format (table, json)"
|
|
17
|
+
option :quiet, aliases: ["-q"], type: :boolean, default: false, desc: "Suppress non-essential output"
|
|
18
|
+
option :debug, aliases: ["-d"], type: :boolean, default: false, desc: "Show debug output"
|
|
19
|
+
|
|
20
|
+
def initialize(orchestrator: nil)
|
|
21
|
+
super()
|
|
22
|
+
@orchestrator = orchestrator
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def call(config: nil, assignment: nil, format: "table", **options)
|
|
26
|
+
orchestrator = @orchestrator || Organisms::StatusOrchestrator.new
|
|
27
|
+
result = orchestrator.call(config_path: config, assignment: assignment)
|
|
28
|
+
return if options[:quiet]
|
|
29
|
+
|
|
30
|
+
if format == "json"
|
|
31
|
+
puts JSON.pretty_generate(to_h(result))
|
|
32
|
+
else
|
|
33
|
+
puts render_table(result)
|
|
34
|
+
end
|
|
35
|
+
rescue Ace::Coworker::Error, StandardError => e
|
|
36
|
+
raise Ace::Support::Cli::Error, e.message
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def effective_assignment(result)
|
|
42
|
+
result.assignment || result.config.assignment
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def to_h(result)
|
|
46
|
+
if result.is_a?(Organisms::StatusOrchestrator::NotReady)
|
|
47
|
+
{"ready" => false, "message" => result.message}
|
|
48
|
+
else
|
|
49
|
+
cfg = result.config
|
|
50
|
+
payload = {
|
|
51
|
+
"ready" => true,
|
|
52
|
+
"role" => cfg.role,
|
|
53
|
+
"task" => cfg.task,
|
|
54
|
+
"assignment" => effective_assignment(result),
|
|
55
|
+
"overseer" => cfg.overseer,
|
|
56
|
+
"on_failure" => cfg.on_failure
|
|
57
|
+
}
|
|
58
|
+
if result.snapshot
|
|
59
|
+
payload["assignment_status"] = result.snapshot.to_h
|
|
60
|
+
elsif result.probe_error
|
|
61
|
+
payload["assignment_error"] = result.probe_error
|
|
62
|
+
end
|
|
63
|
+
payload
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def render_table(result)
|
|
68
|
+
if result.is_a?(Organisms::StatusOrchestrator::NotReady)
|
|
69
|
+
return result.message
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
cfg = result.config
|
|
73
|
+
lines = ["Coworker status"]
|
|
74
|
+
lines << " role: #{cfg.role}"
|
|
75
|
+
lines << " task: #{cfg.task}"
|
|
76
|
+
lines << " assignment: #{effective_assignment(result)}"
|
|
77
|
+
lines << " overseer: #{cfg.overseer}"
|
|
78
|
+
if result.snapshot
|
|
79
|
+
snapshot = result.snapshot
|
|
80
|
+
verdict = snapshot.completion_verdict == :complete ? "complete" : "incomplete"
|
|
81
|
+
lines << " state: #{snapshot.state} (#{snapshot.progress}) — #{verdict}"
|
|
82
|
+
unless verdict == "complete"
|
|
83
|
+
reasons = snapshot.incomplete_reasons
|
|
84
|
+
lines.concat(reasons.map { |r| " remaining: #{r}" })
|
|
85
|
+
end
|
|
86
|
+
elsif result.probe_error
|
|
87
|
+
lines << " assignment error: #{result.probe_error}"
|
|
88
|
+
end
|
|
89
|
+
lines.join("\n")
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/support/cli"
|
|
4
|
+
require "ace/core"
|
|
5
|
+
require_relative "cli/commands/start"
|
|
6
|
+
require_relative "cli/commands/status"
|
|
7
|
+
require_relative "cli/commands/report"
|
|
8
|
+
require_relative "cli/commands/resume"
|
|
9
|
+
require_relative "cli/commands/prepare"
|
|
10
|
+
require_relative "cli/commands/commission"
|
|
11
|
+
require_relative "cli/commands/roles"
|
|
12
|
+
require_relative "cli/commands/rounds"
|
|
13
|
+
require_relative "cli/commands/gate"
|
|
14
|
+
|
|
15
|
+
module Ace
|
|
16
|
+
module Coworker
|
|
17
|
+
module CLI
|
|
18
|
+
extend Ace::Support::Cli::RegistryDsl
|
|
19
|
+
|
|
20
|
+
PROGRAM_NAME = "ace-coworker"
|
|
21
|
+
|
|
22
|
+
REGISTERED_COMMANDS = [
|
|
23
|
+
["start", "Validate prepared run config and render the executor brief"],
|
|
24
|
+
["status", "Show readiness, identity, and assignment progress"],
|
|
25
|
+
["report", "Record the assignment outcome and report it through ace-hitl"],
|
|
26
|
+
["resume", "Assess interrupted work in this worktree and resume it"],
|
|
27
|
+
["prepare", "Accept a preparation commission and ensure the task worktree"],
|
|
28
|
+
["commission", "Instantiate a role preset into the coworker run config"],
|
|
29
|
+
["roles", "List the role preset catalog (gem defaults + project overrides)"],
|
|
30
|
+
["rounds", "Record a review round for an assignment and print the 3/2/10 verdict"],
|
|
31
|
+
["gate", "Evaluate the pre-merge integration gate (:ready | :blocked with reasons)"]
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
HELP_EXAMPLES = [
|
|
35
|
+
"ace-coworker start # Read .ace-local/coworker/config.yml",
|
|
36
|
+
"ace-coworker start --config PATH # Use an explicit config",
|
|
37
|
+
"ace-coworker status --format json # Readiness + assignment verdict",
|
|
38
|
+
"ace-coworker report # Record outcome + HITL report",
|
|
39
|
+
"ace-coworker report --result error --message 'cause'",
|
|
40
|
+
"ace-coworker resume # Resume interrupted work",
|
|
41
|
+
"ace-coworker resume --dry-run # Plan only, no redelivery",
|
|
42
|
+
"ace-coworker prepare --task 148 --roles worker,reviewer",
|
|
43
|
+
"ace-coworker prepare --task 148 --roles worker --dry-run",
|
|
44
|
+
"ace-coworker roles # List role presets",
|
|
45
|
+
"ace-coworker commission --role worker --task 148 --assignment abc123 \\",
|
|
46
|
+
" --overseer overseer@lab",
|
|
47
|
+
"ace-coworker rounds record --assignment abc123 --type full --status completed",
|
|
48
|
+
"ace-coworker rounds record --assignment abc123 --type full --status completed \\",
|
|
49
|
+
" --confirmed f-1:high --resolved f-2",
|
|
50
|
+
"ace-coworker gate --candidate-head 1a2b3c4 --review-head 1a2b3c4 \\",
|
|
51
|
+
" --review-verdict accept --tests executed,passed \\",
|
|
52
|
+
" --dep 8xg.t.sbz.4.2=integrated"
|
|
53
|
+
].freeze
|
|
54
|
+
|
|
55
|
+
register "start", Commands::Start
|
|
56
|
+
register "status", Commands::Status
|
|
57
|
+
register "report", Commands::Report
|
|
58
|
+
register "resume", Commands::Resume
|
|
59
|
+
register "prepare", Commands::Prepare
|
|
60
|
+
register "commission", Commands::Commission
|
|
61
|
+
register "roles", Commands::Roles
|
|
62
|
+
register "rounds", Commands::Rounds
|
|
63
|
+
register "gate", Commands::Gate
|
|
64
|
+
|
|
65
|
+
version_cmd = Ace::Support::Cli::VersionCommand.build(
|
|
66
|
+
gem_name: "ace-coworker",
|
|
67
|
+
version: Ace::Coworker::VERSION
|
|
68
|
+
)
|
|
69
|
+
register "version", version_cmd
|
|
70
|
+
register "--version", version_cmd
|
|
71
|
+
|
|
72
|
+
help_cmd = Ace::Support::Cli::HelpCommand.build(
|
|
73
|
+
program_name: PROGRAM_NAME,
|
|
74
|
+
version: Ace::Coworker::VERSION,
|
|
75
|
+
commands: REGISTERED_COMMANDS,
|
|
76
|
+
examples: HELP_EXAMPLES
|
|
77
|
+
)
|
|
78
|
+
register "help", help_cmd
|
|
79
|
+
register "--help", help_cmd
|
|
80
|
+
register "-h", help_cmd
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Coworker
|
|
5
|
+
module Models
|
|
6
|
+
# Pure view of `ace-assign status --format json` for one assignment.
|
|
7
|
+
class AssignmentSnapshot
|
|
8
|
+
Step = Struct.new(:number, :name, :status, keyword_init: true) do
|
|
9
|
+
def done?
|
|
10
|
+
status == "done"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
attr_reader :id, :name, :state, :steps, :progress
|
|
15
|
+
|
|
16
|
+
def initialize(id:, name:, state:, steps:, progress:)
|
|
17
|
+
@id = id.freeze
|
|
18
|
+
@name = name.freeze
|
|
19
|
+
@state = state.freeze
|
|
20
|
+
@steps = steps.map { |s| Step.new(number: s[:number], name: s[:name], status: s[:status]) }.freeze
|
|
21
|
+
@progress = progress.freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.from_status_json(payload)
|
|
25
|
+
assignment = payload["assignment"] || {}
|
|
26
|
+
steps = (payload["steps"] || []).map { |s| {number: s["number"], name: s["name"], status: s["status"]} }
|
|
27
|
+
new(
|
|
28
|
+
id: assignment["id"].to_s,
|
|
29
|
+
name: assignment["name"].to_s,
|
|
30
|
+
state: assignment["state"].to_s,
|
|
31
|
+
steps: steps,
|
|
32
|
+
progress: payload["progress"].to_s
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Deterministic completion verdict: complete only when the assignment
|
|
37
|
+
# state is completed, the plan is non-empty, and every step is done.
|
|
38
|
+
# Empty plans, failed steps, or pending/active steps are not complete.
|
|
39
|
+
def completion_verdict
|
|
40
|
+
return :complete if state == "completed" && !steps.empty? && steps.all?(&:done?)
|
|
41
|
+
|
|
42
|
+
:incomplete
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def incomplete_reasons
|
|
46
|
+
reasons = []
|
|
47
|
+
reasons << "assignment state is '#{state}' (not completed)" if state != "completed"
|
|
48
|
+
reasons << "assignment plan is empty" if steps.empty?
|
|
49
|
+
reasons << "#{steps.count { |s| s.status == 'failed' }} failed step(s)" if steps.any? { |s| s.status == "failed" }
|
|
50
|
+
pending = steps.count { |s| s.status == "pending" }
|
|
51
|
+
active = steps.count { |s| s.status == "active" }
|
|
52
|
+
reasons << "#{pending} pending step(s)" if pending.positive?
|
|
53
|
+
reasons << "#{active} active step(s)" if active.positive?
|
|
54
|
+
reasons
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def to_h
|
|
58
|
+
{"id" => id, "name" => name, "state" => state, "progress" => progress,
|
|
59
|
+
"steps" => steps.map { |s| {"number" => s.number, "name" => s.name, "status" => s.status} },
|
|
60
|
+
"complete" => completion_verdict == :complete}
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Coworker
|
|
5
|
+
module Models
|
|
6
|
+
# Executor brief rendered by `ace-coworker start`.
|
|
7
|
+
class Brief
|
|
8
|
+
attr_reader :config, :instructions_source, :task_path, :assignment_snapshot, :project_root
|
|
9
|
+
|
|
10
|
+
def initialize(config:, instructions_source:, task_path:, assignment_snapshot:, project_root:)
|
|
11
|
+
@config = config
|
|
12
|
+
@instructions_source = instructions_source.freeze
|
|
13
|
+
@task_path = task_path.freeze
|
|
14
|
+
@assignment_snapshot = assignment_snapshot
|
|
15
|
+
@project_root = project_root.freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_h
|
|
19
|
+
{
|
|
20
|
+
"role" => config.role,
|
|
21
|
+
"instructions" => config.instructions,
|
|
22
|
+
"instructions_source" => instructions_source,
|
|
23
|
+
"start_prompt" => config.start_prompt,
|
|
24
|
+
"task" => config.task,
|
|
25
|
+
"task_path" => task_path,
|
|
26
|
+
"assignment" => config.assignment,
|
|
27
|
+
"assignment_state" => assignment_snapshot&.state,
|
|
28
|
+
"assignment_progress" => assignment_snapshot&.progress,
|
|
29
|
+
"overseer" => config.overseer,
|
|
30
|
+
"reporting_channel" => config.reporting_channel,
|
|
31
|
+
"on_failure" => config.on_failure,
|
|
32
|
+
"project_root" => project_root
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Coworker
|
|
5
|
+
module Models
|
|
6
|
+
# Immutable validated coworker run configuration.
|
|
7
|
+
class Config
|
|
8
|
+
attr_reader :role, :instructions, :start_prompt, :start_prompt_file, :task, :assignment,
|
|
9
|
+
:overseer, :reporting_channel, :on_failure, :source_path
|
|
10
|
+
|
|
11
|
+
def initialize(role:, instructions:, start_prompt:, task:, assignment:, overseer:,
|
|
12
|
+
start_prompt_file: nil, reporting_channel: "hitl", on_failure: "report", source_path: nil)
|
|
13
|
+
@role = role.freeze
|
|
14
|
+
@instructions = instructions.freeze
|
|
15
|
+
@start_prompt = start_prompt.freeze
|
|
16
|
+
@start_prompt_file = start_prompt_file.freeze
|
|
17
|
+
@task = task.freeze
|
|
18
|
+
@assignment = assignment.freeze
|
|
19
|
+
@overseer = overseer.freeze
|
|
20
|
+
@reporting_channel = reporting_channel.freeze
|
|
21
|
+
@on_failure = on_failure.freeze
|
|
22
|
+
@source_path = source_path.freeze
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# A copy with a different assignment, e.g. a --assignment CLI
|
|
26
|
+
# override threaded through report/resume. Single place so new
|
|
27
|
+
# fields can never drift between the two identities.
|
|
28
|
+
def with(assignment:)
|
|
29
|
+
return self if assignment == @assignment
|
|
30
|
+
|
|
31
|
+
self.class.new(role: role, instructions: instructions,
|
|
32
|
+
start_prompt: start_prompt, start_prompt_file: start_prompt_file,
|
|
33
|
+
task: task, assignment: assignment, overseer: overseer,
|
|
34
|
+
reporting_channel: reporting_channel, on_failure: on_failure,
|
|
35
|
+
source_path: source_path)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def to_h
|
|
39
|
+
{
|
|
40
|
+
"role" => role,
|
|
41
|
+
"instructions" => instructions,
|
|
42
|
+
"start_prompt" => start_prompt,
|
|
43
|
+
"start_prompt_file" => start_prompt_file,
|
|
44
|
+
"task" => task,
|
|
45
|
+
"assignment" => assignment,
|
|
46
|
+
"overseer" => overseer,
|
|
47
|
+
"reporting" => {"channel" => reporting_channel},
|
|
48
|
+
"errors" => {"on_failure" => on_failure}
|
|
49
|
+
}.compact
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Coworker
|
|
5
|
+
module Models
|
|
6
|
+
# Immutable derived outcome of one reporting run.
|
|
7
|
+
class Outcome
|
|
8
|
+
StepRef = Struct.new(:number, :name, keyword_init: true)
|
|
9
|
+
|
|
10
|
+
attr_reader :result, :verdict, :cause, :step, :unfinished_steps, :evidence, :snapshot
|
|
11
|
+
|
|
12
|
+
def initialize(result:, verdict:, cause: nil, step: nil, unfinished_steps: [], evidence: {}, snapshot: nil)
|
|
13
|
+
@result = result.freeze
|
|
14
|
+
@verdict = verdict
|
|
15
|
+
@cause = cause.freeze
|
|
16
|
+
@step = step
|
|
17
|
+
@unfinished_steps = unfinished_steps.map(&:freeze).freeze
|
|
18
|
+
@evidence = evidence.freeze
|
|
19
|
+
@snapshot = snapshot
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def success?
|
|
23
|
+
result == "success"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_h
|
|
27
|
+
{
|
|
28
|
+
"result" => result,
|
|
29
|
+
"verdict" => verdict.to_s,
|
|
30
|
+
"cause" => cause,
|
|
31
|
+
"step" => step && {"number" => step.number, "name" => step.name},
|
|
32
|
+
"unfinished_steps" => unfinished_steps,
|
|
33
|
+
"evidence" => evidence
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|