harnex 0.7.13 → 0.7.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +38 -0
- data/README.md +53 -11
- data/TECHNICAL.md +20 -1
- data/guides/01_dispatch.md +29 -9
- data/guides/04_monitoring.md +11 -5
- data/lib/harnex/artifact_report.rb +507 -35
- data/lib/harnex/cli.rb +17 -0
- data/lib/harnex/commands/artifact_report.rb +94 -0
- data/lib/harnex/commands/orchestration.rb +170 -0
- data/lib/harnex/commands/run.rb +49 -5
- data/lib/harnex/commands/wait.rb +6 -1
- data/lib/harnex/commands/watch.rb +2 -0
- data/lib/harnex/orchestration.rb +458 -0
- data/lib/harnex/runtime/session.rb +278 -13
- data/lib/harnex/terminal_status.rb +7 -0
- data/lib/harnex/version.rb +1 -1
- data/lib/harnex.rb +3 -0
- metadata +4 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "optparse"
|
|
3
|
+
|
|
4
|
+
module Harnex
|
|
5
|
+
class ArtifactReportCommand
|
|
6
|
+
def self.usage(program_name = "harnex artifact-report")
|
|
7
|
+
<<~TEXT
|
|
8
|
+
Usage:
|
|
9
|
+
#{program_name} init PATH [--force]
|
|
10
|
+
#{program_name} validate PATH [--final]
|
|
11
|
+
|
|
12
|
+
Commands:
|
|
13
|
+
init Write a bounded harnex.artifact_report.v1 skeleton
|
|
14
|
+
validate Validate schema and field shapes without printing report data
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
--final Require an accepted/no_change final outcome, passing proof,
|
|
18
|
+
and validation.final_reported=true
|
|
19
|
+
--force Replace an existing file during init
|
|
20
|
+
-h, --help Show this help
|
|
21
|
+
|
|
22
|
+
Both commands print machine-readable JSON. `validate` exits 0 only when
|
|
23
|
+
the requested contract is satisfied; diagnostics contain field paths and
|
|
24
|
+
shape errors, never report payloads or transcripts.
|
|
25
|
+
TEXT
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(argv)
|
|
29
|
+
@argv = argv.dup
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def run
|
|
33
|
+
command = @argv.shift
|
|
34
|
+
case command
|
|
35
|
+
when "init"
|
|
36
|
+
run_init(@argv)
|
|
37
|
+
when "validate"
|
|
38
|
+
run_validate(@argv)
|
|
39
|
+
when nil, "help", "-h", "--help"
|
|
40
|
+
puts self.class.usage
|
|
41
|
+
0
|
|
42
|
+
else
|
|
43
|
+
raise OptionParser::ParseError, "unknown artifact-report command #{command.inspect}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def run_init(argv)
|
|
50
|
+
options = { force: false, help: false }
|
|
51
|
+
parser = OptionParser.new do |opts|
|
|
52
|
+
opts.banner = "Usage: harnex artifact-report init PATH [--force]"
|
|
53
|
+
opts.on("--force", "Replace an existing report") { options[:force] = true }
|
|
54
|
+
opts.on("-h", "--help", "Show help") { options[:help] = true }
|
|
55
|
+
end
|
|
56
|
+
parser.parse!(argv)
|
|
57
|
+
return print_help if options[:help]
|
|
58
|
+
|
|
59
|
+
path = exactly_one_path!(argv, parser)
|
|
60
|
+
report_path = ArtifactReport.initialize_file(path, force: options[:force])
|
|
61
|
+
result = ArtifactReport.validate(report_path)
|
|
62
|
+
puts JSON.generate(result.public_payload(final: false).merge("created" => true))
|
|
63
|
+
result.ok ? 0 : 1
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def run_validate(argv)
|
|
67
|
+
options = { final: false, help: false }
|
|
68
|
+
parser = OptionParser.new do |opts|
|
|
69
|
+
opts.banner = "Usage: harnex artifact-report validate PATH [--final]"
|
|
70
|
+
opts.on("--final", "Require accepted final proof") { options[:final] = true }
|
|
71
|
+
opts.on("-h", "--help", "Show help") { options[:help] = true }
|
|
72
|
+
end
|
|
73
|
+
parser.parse!(argv)
|
|
74
|
+
return print_help if options[:help]
|
|
75
|
+
|
|
76
|
+
path = exactly_one_path!(argv, parser)
|
|
77
|
+
result = ArtifactReport.validate(path, final: options[:final])
|
|
78
|
+
puts JSON.generate(result.public_payload(final: options[:final]))
|
|
79
|
+
result.ok ? 0 : 1
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def exactly_one_path!(argv, parser)
|
|
83
|
+
raise OptionParser::MissingArgument, "PATH" if argv.empty?
|
|
84
|
+
raise OptionParser::InvalidArgument, "expected exactly one PATH\n#{parser}" unless argv.length == 1
|
|
85
|
+
|
|
86
|
+
argv.first
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def print_help
|
|
90
|
+
puts self.class.usage
|
|
91
|
+
0
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "optparse"
|
|
3
|
+
|
|
4
|
+
module Harnex
|
|
5
|
+
class OrchestrationCommand
|
|
6
|
+
def self.usage(program_name = "harnex orchestration")
|
|
7
|
+
<<~TEXT
|
|
8
|
+
Usage:
|
|
9
|
+
#{program_name} sample --out PATH --run-id ID --generation-id ID [options]
|
|
10
|
+
#{program_name} report --dispatch PATH --run-id ID [--samples PATH] [--json]
|
|
11
|
+
|
|
12
|
+
Sample options:
|
|
13
|
+
--project-id ID
|
|
14
|
+
--queue-id ID
|
|
15
|
+
--session-id ID
|
|
16
|
+
--event NAME sample, generation_started, generation_finished, rotation, recovery, or compaction
|
|
17
|
+
--ts ISO8601
|
|
18
|
+
--context-status STATUS observed, estimated, unsupported, missing, or zero
|
|
19
|
+
--context-tokens N
|
|
20
|
+
--context-window-tokens N
|
|
21
|
+
--context-percent N
|
|
22
|
+
--context-peak-tokens N
|
|
23
|
+
--context-peak-percent N
|
|
24
|
+
--usage-status STATUS observed, estimated, unsupported, missing, or zero
|
|
25
|
+
--usage-input-tokens N
|
|
26
|
+
--usage-output-tokens N
|
|
27
|
+
--usage-cached-input-tokens N
|
|
28
|
+
--usage-reasoning-tokens N
|
|
29
|
+
--usage-total-tokens N
|
|
30
|
+
--usage-cost-usd N
|
|
31
|
+
--usage-cost-source TEXT
|
|
32
|
+
--tool-calls N
|
|
33
|
+
--compactions N
|
|
34
|
+
--rotation-reason TEXT
|
|
35
|
+
|
|
36
|
+
Report options:
|
|
37
|
+
--dispatch PATH Dispatch summary JSONL
|
|
38
|
+
--samples PATH External primary sample JSONL
|
|
39
|
+
--run-id ID Logical orchestration run id
|
|
40
|
+
--json Output the full report JSON
|
|
41
|
+
-h, --help Show this help
|
|
42
|
+
TEXT
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def initialize(argv)
|
|
46
|
+
@argv = argv.dup
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def run
|
|
50
|
+
subcommand = @argv.shift
|
|
51
|
+
case subcommand
|
|
52
|
+
when "sample"
|
|
53
|
+
run_sample(@argv)
|
|
54
|
+
when "report"
|
|
55
|
+
run_report(@argv)
|
|
56
|
+
when "-h", "--help", nil
|
|
57
|
+
puts self.class.usage
|
|
58
|
+
0
|
|
59
|
+
else
|
|
60
|
+
raise OptionParser::ParseError, "unknown orchestration subcommand #{subcommand.inspect}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def run_sample(argv)
|
|
67
|
+
options = {
|
|
68
|
+
context: {},
|
|
69
|
+
usage: {}
|
|
70
|
+
}
|
|
71
|
+
sample_parser(options).parse!(argv)
|
|
72
|
+
if options.delete(:help)
|
|
73
|
+
puts self.class.usage
|
|
74
|
+
return 0
|
|
75
|
+
end
|
|
76
|
+
path = required_option(options.delete(:out), "--out")
|
|
77
|
+
options["orchestration_run_id"] = required_option(options.delete(:run_id), "--run-id")
|
|
78
|
+
options["generation_id"] = required_option(options.delete(:generation_id), "--generation-id")
|
|
79
|
+
sample = Harnex::Orchestration.append_sample(path, options)
|
|
80
|
+
puts JSON.generate("ok" => true, "path" => path, "sample" => sample)
|
|
81
|
+
0
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def run_report(argv)
|
|
85
|
+
options = { json: false }
|
|
86
|
+
report_parser(options).parse!(argv)
|
|
87
|
+
if options[:help]
|
|
88
|
+
puts self.class.usage
|
|
89
|
+
return 0
|
|
90
|
+
end
|
|
91
|
+
report = Harnex::Orchestration.report(
|
|
92
|
+
dispatch_path: required_option(options[:dispatch], "--dispatch"),
|
|
93
|
+
samples_path: options[:samples],
|
|
94
|
+
run_id: required_option(options[:run_id], "--run-id")
|
|
95
|
+
)
|
|
96
|
+
if options[:json]
|
|
97
|
+
puts JSON.generate(report)
|
|
98
|
+
else
|
|
99
|
+
puts render_report(report)
|
|
100
|
+
end
|
|
101
|
+
0
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def sample_parser(options)
|
|
105
|
+
OptionParser.new do |opts|
|
|
106
|
+
opts.banner = "Usage: harnex orchestration sample --out PATH --run-id ID --generation-id ID [options]"
|
|
107
|
+
opts.on("--out PATH") { |value| options[:out] = value }
|
|
108
|
+
opts.on("--run-id ID") { |value| options[:run_id] = value }
|
|
109
|
+
opts.on("--generation-id ID") { |value| options[:generation_id] = value }
|
|
110
|
+
opts.on("--project-id ID") { |value| options["project_id"] = value }
|
|
111
|
+
opts.on("--queue-id ID") { |value| options["queue_id"] = value }
|
|
112
|
+
opts.on("--session-id ID") { |value| options["session_id"] = value }
|
|
113
|
+
opts.on("--event NAME") { |value| options["event"] = value }
|
|
114
|
+
opts.on("--ts ISO8601") { |value| options["ts"] = value }
|
|
115
|
+
opts.on("--context-status STATUS") { |value| options[:context]["status"] = value }
|
|
116
|
+
opts.on("--context-tokens N") { |value| options[:context]["terminal_tokens"] = value }
|
|
117
|
+
opts.on("--context-window-tokens N") { |value| options[:context]["window_tokens"] = value }
|
|
118
|
+
opts.on("--context-percent N") { |value| options[:context]["terminal_percent"] = value }
|
|
119
|
+
opts.on("--context-peak-tokens N") { |value| options[:context]["peak_tokens"] = value }
|
|
120
|
+
opts.on("--context-peak-percent N") { |value| options[:context]["peak_percent"] = value }
|
|
121
|
+
opts.on("--usage-status STATUS") { |value| options[:usage]["status"] = value }
|
|
122
|
+
opts.on("--usage-input-tokens N") { |value| options[:usage]["input_tokens"] = value }
|
|
123
|
+
opts.on("--usage-output-tokens N") { |value| options[:usage]["output_tokens"] = value }
|
|
124
|
+
opts.on("--usage-cached-input-tokens N") { |value| options[:usage]["cached_input_tokens"] = value }
|
|
125
|
+
opts.on("--usage-reasoning-tokens N") { |value| options[:usage]["reasoning_tokens"] = value }
|
|
126
|
+
opts.on("--usage-total-tokens N") { |value| options[:usage]["total_tokens"] = value }
|
|
127
|
+
opts.on("--usage-cost-usd N") { |value| options[:usage]["cost_usd"] = value }
|
|
128
|
+
opts.on("--usage-cost-source TEXT") { |value| options[:usage]["cost_source"] = value }
|
|
129
|
+
opts.on("--tool-calls N") { |value| options["tool_calls"] = value }
|
|
130
|
+
opts.on("--compactions N") { |value| options["compactions"] = value }
|
|
131
|
+
opts.on("--rotation-reason TEXT") { |value| options["rotation_reason"] = value }
|
|
132
|
+
opts.on("-h", "--help") { options[:help] = true }
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def report_parser(options)
|
|
137
|
+
OptionParser.new do |opts|
|
|
138
|
+
opts.banner = "Usage: harnex orchestration report --dispatch PATH --run-id ID [options]"
|
|
139
|
+
opts.on("--dispatch PATH") { |value| options[:dispatch] = value }
|
|
140
|
+
opts.on("--samples PATH") { |value| options[:samples] = value }
|
|
141
|
+
opts.on("--run-id ID") { |value| options[:run_id] = value }
|
|
142
|
+
opts.on("--json") { options[:json] = true }
|
|
143
|
+
opts.on("-h", "--help") { options[:help] = true }
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def required_option(value, name)
|
|
148
|
+
text = value.to_s.strip
|
|
149
|
+
raise OptionParser::MissingArgument, name if text.empty?
|
|
150
|
+
|
|
151
|
+
text
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def render_report(report)
|
|
155
|
+
primary = report.fetch("primary")
|
|
156
|
+
workers = report.fetch("workers")
|
|
157
|
+
ratios = report.fetch("ratios")
|
|
158
|
+
[
|
|
159
|
+
"Run: #{report.fetch('orchestration_run_id')}",
|
|
160
|
+
"Primary: generations=#{primary.fetch('generation_count')} usage=#{primary.dig('usage', 'status')} total_tokens=#{format_value(primary.dig('usage', 'total_tokens'))} peak_context=#{format_value(primary.dig('context', 'peak_tokens'))} tool_calls=#{primary.fetch('tool_calls')}",
|
|
161
|
+
"Workers: dispatches=#{workers.fetch('dispatches')} usage=#{workers.dig('usage', 'status')} active_s=#{workers.fetch('active_s')} accepted=#{workers.dig('outcomes', 'accepted')} rejected=#{workers.dig('outcomes', 'rejected')} blocked=#{workers.dig('outcomes', 'blocked')} unknown=#{workers.dig('outcomes', 'unknown')}",
|
|
162
|
+
"Ratios: primary_tokens_per_accepted_entry=#{format_value(ratios.fetch('primary_total_tokens_per_accepted_entry'))} primary_tool_calls_per_accepted_entry=#{format_value(ratios.fetch('primary_tool_calls_per_accepted_entry'))}"
|
|
163
|
+
].join("\n")
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def format_value(value)
|
|
167
|
+
value.nil? ? "-" : value
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
data/lib/harnex/commands/run.rb
CHANGED
|
@@ -19,7 +19,12 @@ module Harnex
|
|
|
19
19
|
"--effort" => "effort",
|
|
20
20
|
"--parent-dispatch-id" => "parent_dispatch_id",
|
|
21
21
|
"--parent-attempt-id" => "parent_attempt_id",
|
|
22
|
-
"--attempt-kind" => "attempt_kind"
|
|
22
|
+
"--attempt-kind" => "attempt_kind",
|
|
23
|
+
"--orchestration-run-id" => "orchestration_run_id",
|
|
24
|
+
"--orchestration-generation-id" => "orchestration_generation_id",
|
|
25
|
+
"--orchestration-role" => "orchestration_role",
|
|
26
|
+
"--orchestration-session-id" => "orchestration_session_id",
|
|
27
|
+
"--orchestration-rotation-reason" => "orchestration_rotation_reason"
|
|
23
28
|
}.freeze
|
|
24
29
|
TELEMETRY_KEYS_TO_FLAGS = TELEMETRY_FLAGS.invert.freeze
|
|
25
30
|
TELEMETRY_EQUALS_PREFIXES = TELEMETRY_FLAGS.keys.map { |flag| "#{flag}=" }.freeze
|
|
@@ -28,7 +33,7 @@ module Harnex
|
|
|
28
33
|
--id --description --detach --tmux --host --port --watch --watch-file
|
|
29
34
|
--stall-after --max-resumes --preset --context --meta --summary-out
|
|
30
35
|
--artifact-report --validation-report --cwd --root --timeout --inbox-ttl
|
|
31
|
-
--require-attribution --auto-stop --fast --legacy-pty --help
|
|
36
|
+
--require-artifact-report --require-attribution --auto-stop --fast --legacy-pty --help
|
|
32
37
|
].concat(TELEMETRY_FLAGS.keys).freeze
|
|
33
38
|
VALUE_FLAGS = %w[
|
|
34
39
|
--id --description --host --port --watch --watch-file --stall-after
|
|
@@ -53,7 +58,7 @@ module Harnex
|
|
|
53
58
|
--preset NAME Watch preset: impl, plan, gate (requires --watch)
|
|
54
59
|
--watch-file PATH Auto-send a file-change hook on modification
|
|
55
60
|
--context TEXT Inject as the initial prompt (prepends session header)
|
|
56
|
-
--auto-stop Stop after the first task completion from --context
|
|
61
|
+
--auto-stop Stop after the first accepted task completion from --context
|
|
57
62
|
--fast (codex only) Use Codex service_tier="fast".
|
|
58
63
|
Default Codex runs force service_tier="flex".
|
|
59
64
|
--meta JSON Attach parsed JSON metadata to the started event
|
|
@@ -62,6 +67,9 @@ module Harnex
|
|
|
62
67
|
Worker-written harnex.artifact_report.v1 JSON sidecar to ingest at exit
|
|
63
68
|
--validation-report PATH
|
|
64
69
|
Alias for --artifact-report; also exposed as HARNEX_VALIDATION_REPORT_PATH
|
|
70
|
+
--require-artifact-report
|
|
71
|
+
Fail closed unless PATH contains accepted final proof;
|
|
72
|
+
requires --artifact-report or --validation-report
|
|
65
73
|
--project-id ID Queue telemetry project id (first-class flags override --meta)
|
|
66
74
|
--queue-id ID Queue telemetry queue id
|
|
67
75
|
--entry-id ID Queue telemetry entry id
|
|
@@ -79,6 +87,16 @@ module Harnex
|
|
|
79
87
|
Parent attempt id for retry/fix/review joins
|
|
80
88
|
--attempt-kind KIND
|
|
81
89
|
initial, retry, fix, review, or superseding (default: initial)
|
|
90
|
+
--orchestration-run-id ID
|
|
91
|
+
Logical primary-orchestrator run id for queue rollups
|
|
92
|
+
--orchestration-generation-id ID
|
|
93
|
+
Primary generation id after rotation/recovery boundaries
|
|
94
|
+
--orchestration-role ROLE
|
|
95
|
+
primary or worker; defaults to worker in rollups when omitted
|
|
96
|
+
--orchestration-session-id ID
|
|
97
|
+
External primary session id to preserve in rollups
|
|
98
|
+
--orchestration-rotation-reason TEXT
|
|
99
|
+
Clean rotation/recovery reason for the generation
|
|
82
100
|
--require-attribution
|
|
83
101
|
Fail before launch unless project/phase/intent and one work id are present
|
|
84
102
|
--cwd DIR Run the wrapped agent from DIR and use DIR as the session root
|
|
@@ -94,7 +112,9 @@ module Harnex
|
|
|
94
112
|
Notes:
|
|
95
113
|
Compatibility: `--watch PATH` and `--watch=PATH` still configure file-hook mode.
|
|
96
114
|
Bare `--watch` enables the babysitter.
|
|
97
|
-
--auto-stop requires --context
|
|
115
|
+
--auto-stop requires --context. Structured Codex turns only count as
|
|
116
|
+
accepted completion after activity, Git delta, or accepted sidecar proof.
|
|
117
|
+
--require-artifact-report makes sidecar validation part of the run verdict.
|
|
98
118
|
Explicit --stall-after/--max-resumes values override --preset defaults.
|
|
99
119
|
CLIs with smart prompt detection: #{Adapters.known.join(', ')}
|
|
100
120
|
Any other CLI name is launched with generic wrapping.
|
|
@@ -140,6 +160,7 @@ module Harnex
|
|
|
140
160
|
require_attribution: false,
|
|
141
161
|
summary_out: nil,
|
|
142
162
|
artifact_report: nil,
|
|
163
|
+
require_artifact_report: false,
|
|
143
164
|
cwd: nil,
|
|
144
165
|
root: nil,
|
|
145
166
|
auto_stop: false,
|
|
@@ -163,8 +184,10 @@ module Harnex
|
|
|
163
184
|
|
|
164
185
|
raise OptionParser::MissingArgument, "cli" if cli_name.nil?
|
|
165
186
|
validate_auto_stop_context!
|
|
187
|
+
validate_required_artifact_report!
|
|
166
188
|
apply_telemetry_options!
|
|
167
189
|
validate_attempt_metadata!
|
|
190
|
+
validate_orchestration_metadata!
|
|
168
191
|
validate_required_attribution!
|
|
169
192
|
|
|
170
193
|
repo_root = resolve_run_root(cli_name, child_args)
|
|
@@ -237,6 +260,7 @@ module Harnex
|
|
|
237
260
|
tmux_cmd << "--require-attribution" if @options[:require_attribution]
|
|
238
261
|
tmux_cmd += ["--summary-out", @options[:summary_out]] if @options[:summary_out]
|
|
239
262
|
tmux_cmd += ["--artifact-report", @options[:artifact_report]] if @options[:artifact_report]
|
|
263
|
+
tmux_cmd << "--require-artifact-report" if @options[:require_artifact_report]
|
|
240
264
|
tmux_cmd += ["--cwd", @options[:cwd]] if @options[:cwd]
|
|
241
265
|
tmux_cmd += ["--root", @options[:root]] if @options[:root]
|
|
242
266
|
tmux_cmd += ["--inbox-ttl", @options[:inbox_ttl].to_s]
|
|
@@ -347,6 +371,7 @@ module Harnex
|
|
|
347
371
|
meta: @options[:meta],
|
|
348
372
|
summary_out: @options[:summary_out],
|
|
349
373
|
artifact_report_path: @options[:artifact_report],
|
|
374
|
+
require_artifact_report: @options[:require_artifact_report],
|
|
350
375
|
inbox_ttl: @options[:inbox_ttl],
|
|
351
376
|
auto_stop: @options[:auto_stop],
|
|
352
377
|
launch_cwd: history_cwd,
|
|
@@ -539,6 +564,8 @@ module Harnex
|
|
|
539
564
|
when "--artifact-report", "--validation-report"
|
|
540
565
|
index += 1
|
|
541
566
|
@options[:artifact_report] = required_option_value(arg, argv[index])
|
|
567
|
+
when "--require-artifact-report"
|
|
568
|
+
@options[:require_artifact_report] = true
|
|
542
569
|
when /\A--artifact-report=(.+)\z/
|
|
543
570
|
@options[:artifact_report] = required_option_value("--artifact-report", Regexp.last_match(1))
|
|
544
571
|
when /\A--validation-report=(.+)\z/
|
|
@@ -619,7 +646,7 @@ module Harnex
|
|
|
619
646
|
case arg
|
|
620
647
|
when "--"
|
|
621
648
|
return false
|
|
622
|
-
when "-h", "--help", "--detach", "--tmux", "--auto-stop", "--require-attribution", "--fast", "--legacy-pty"
|
|
649
|
+
when "-h", "--help", "--detach", "--tmux", "--auto-stop", "--require-artifact-report", "--require-attribution", "--fast", "--legacy-pty"
|
|
623
650
|
nil
|
|
624
651
|
when /\A--tmux=/
|
|
625
652
|
nil
|
|
@@ -676,6 +703,14 @@ module Harnex
|
|
|
676
703
|
raise OptionParser::InvalidOption, "harnex run: --auto-stop requires --context"
|
|
677
704
|
end
|
|
678
705
|
|
|
706
|
+
def validate_required_artifact_report!
|
|
707
|
+
return unless @options[:require_artifact_report]
|
|
708
|
+
return unless @options[:artifact_report].to_s.strip.empty?
|
|
709
|
+
|
|
710
|
+
raise OptionParser::InvalidOption,
|
|
711
|
+
"harnex run: --require-artifact-report requires --artifact-report PATH"
|
|
712
|
+
end
|
|
713
|
+
|
|
679
714
|
def apply_telemetry_options!
|
|
680
715
|
explicit = @options[:telemetry]
|
|
681
716
|
return if explicit.empty? && @options[:meta].is_a?(Hash)
|
|
@@ -693,6 +728,15 @@ module Harnex
|
|
|
693
728
|
"harnex run: --attempt-kind must be one of #{Session::ATTEMPT_KINDS.join(', ')}"
|
|
694
729
|
end
|
|
695
730
|
|
|
731
|
+
def validate_orchestration_metadata!
|
|
732
|
+
metadata = @options[:meta].is_a?(Hash) ? @options[:meta] : {}
|
|
733
|
+
role = metadata["orchestration_role"].to_s
|
|
734
|
+
return if role.empty? || Orchestration::ROLES.include?(role)
|
|
735
|
+
|
|
736
|
+
raise OptionParser::InvalidOption,
|
|
737
|
+
"harnex run: --orchestration-role must be one of #{Orchestration::ROLES.join(', ')}"
|
|
738
|
+
end
|
|
739
|
+
|
|
696
740
|
def validate_required_attribution!
|
|
697
741
|
return unless @options[:require_attribution]
|
|
698
742
|
|
data/lib/harnex/commands/wait.rb
CHANGED
|
@@ -215,8 +215,11 @@ module Harnex
|
|
|
215
215
|
process_state: "running",
|
|
216
216
|
terminal: false,
|
|
217
217
|
task_complete: !failed,
|
|
218
|
+
task_failed: failed,
|
|
218
219
|
done: !failed,
|
|
219
|
-
work_state: failed ? "failed" : "completed"
|
|
220
|
+
work_state: failed ? "failed" : "completed",
|
|
221
|
+
outcome_class: event["outcome_class"],
|
|
222
|
+
artifact_report_status: event["artifact_report_status"]
|
|
220
223
|
)
|
|
221
224
|
payload[:last_error] = event["message"] || event["error"] if failed
|
|
222
225
|
end
|
|
@@ -520,6 +523,8 @@ module Harnex
|
|
|
520
523
|
task_failed: task_failed,
|
|
521
524
|
done: done,
|
|
522
525
|
work_state: work_state,
|
|
526
|
+
outcome_class: status["outcome_class"],
|
|
527
|
+
artifact_report_status: status["artifact_report_status"],
|
|
523
528
|
exit: status["exit"],
|
|
524
529
|
exit_code: status["exit_code"],
|
|
525
530
|
summary_out: status["summary_out"],
|
|
@@ -304,6 +304,8 @@ module Harnex
|
|
|
304
304
|
exit_code: exit_code,
|
|
305
305
|
status: payload["status"],
|
|
306
306
|
work_state: payload["work_state"],
|
|
307
|
+
outcome_class: payload["outcome_class"],
|
|
308
|
+
artifact_report_status: payload["artifact_report_status"],
|
|
307
309
|
task_complete: payload["task_complete"] || payload["event"] == "task_complete",
|
|
308
310
|
task_failed: payload["task_failed"] || payload["event"] == "task_failed",
|
|
309
311
|
done: payload["done"],
|