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.
@@ -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
@@ -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 and fires once after the first completion.
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
 
@@ -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"],