harnex 0.7.13 → 0.8.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.
@@ -31,6 +31,9 @@ module Harnex
31
31
  BUDGET_META_FIELDS = %w[read_budget_lines output_ceiling_lines].freeze
32
32
  QUEUE_FIELDS = %w[project_id queue_id entry_id entry_title issue plan phase tier intent].freeze
33
33
  AGENT_FIELDS = %w[cli provider model_requested model_effective reasoning_effort service_tier adapter_transport].freeze
34
+ ORCHESTRATION_FIELDS = %w[
35
+ run_id generation_id role project_id queue_id session_id rotation_reason
36
+ ].freeze
34
37
  RELIABILITY_FIELDS = %w[
35
38
  adapter_close real_disconnections stream_interruptions stalls force_resumes compactions recovered
36
39
  ].freeze
@@ -70,7 +73,7 @@ module Harnex
70
73
  return unless item.is_a?(Hash)
71
74
 
72
75
  case item["type"]
73
- when "mcpToolCall", "dynamicToolCall"
76
+ when "mcpToolCall", "dynamicToolCall", "fileChange", "webSearch"
74
77
  @counts[:tool_calls] += 1
75
78
  when "commandExecution"
76
79
  @counts[:commands_executed] += 1
@@ -84,9 +87,9 @@ module Harnex
84
87
 
85
88
  attr_reader :repo_root, :launch_cwd, :child_cwd, :host, :port, :session_id, :token, :command, :pid, :id, :adapter, :watch,
86
89
  :inbox, :description, :meta, :summary_out, :artifact_report_path, :output_log_path, :events_log_path,
87
- :started_at, :ended_at, :exit_code, :term_signal
90
+ :started_at, :ended_at, :exit_code, :term_signal, :require_artifact_report
88
91
 
89
- def initialize(adapter:, command:, repo_root:, host:, port: nil, id: DEFAULT_ID, watch: nil, description: nil, meta: nil, summary_out: nil, artifact_report_path: nil, inbox_ttl: Inbox::DEFAULT_TTL, auto_stop: false, launch_cwd: nil, child_cwd: nil)
92
+ def initialize(adapter:, command:, repo_root:, host:, port: nil, id: DEFAULT_ID, watch: nil, description: nil, meta: nil, summary_out: nil, artifact_report_path: nil, require_artifact_report: false, inbox_ttl: Inbox::DEFAULT_TTL, auto_stop: false, launch_cwd: nil, child_cwd: nil)
90
93
  @adapter = adapter
91
94
  @command = command
92
95
  @repo_root = repo_root
@@ -103,6 +106,9 @@ module Harnex
103
106
  @artifact_report_path = artifact_report_path.to_s.strip
104
107
  @artifact_report_path = nil if @artifact_report_path.empty?
105
108
  @artifact_report_path = File.expand_path(@artifact_report_path, repo_root) if @artifact_report_path
109
+ @require_artifact_report = !!require_artifact_report
110
+ raise ArgumentError, "require_artifact_report requires artifact_report_path" if @require_artifact_report && !@artifact_report_path
111
+ @artifact_report_start_fingerprint = Harnex::ArtifactReport.fingerprint(@artifact_report_path) if @artifact_report_path
106
112
  @registry_path = Harnex.registry_path(repo_root, @id)
107
113
  @output_log_path = Harnex.output_log_path(repo_root, @id)
108
114
  @events_log_path = Harnex.events_log_path(repo_root, @id)
@@ -136,6 +142,9 @@ module Harnex
136
142
  @last_completed_at = nil
137
143
  @last_failed_at = nil
138
144
  @last_failed_status = nil
145
+ @completion_outcome_class = nil
146
+ @completion_report_status = nil
147
+ @completion_diagnostics = []
139
148
  @pi_streamed_text_by_message = {}
140
149
  @auto_stop = !!auto_stop
141
150
  @auto_stop_fired = false
@@ -196,6 +205,7 @@ module Harnex
196
205
  @server = ApiServer.new(self)
197
206
  @server.start
198
207
  persist_registry
208
+ append_dispatch_start_record
199
209
 
200
210
  stdin_state = STDIN.tty? ? STDIN.raw! : nil
201
211
  watch_thread = start_watch_thread
@@ -208,6 +218,8 @@ module Harnex
208
218
  @exit_code = status.exited? ? status.exitstatus : 128 + status.termsig
209
219
  @ended_at = Time.now
210
220
 
221
+ enforce_required_artifact_report!
222
+ normalize_work_acceptance_exit_code!
211
223
  normalize_auto_stop_exit_code!
212
224
  drain_auto_stop_threads
213
225
  output_thread.join(1)
@@ -268,6 +280,8 @@ module Harnex
268
280
  payload[:task_failed] = task_failed
269
281
  payload[:done] = Harnex.work_done_for("running", task_complete: task_complete)
270
282
  payload[:work_state] = work_state
283
+ payload[:outcome_class] = @completion_outcome_class
284
+ payload[:artifact_report_status] = @completion_report_status
271
285
  payload[:last_error] = @last_error
272
286
  payload[:model] = summary_model
273
287
  payload[:effort] = meta_hash["effort"]
@@ -461,6 +475,7 @@ module Harnex
461
475
  @server = ApiServer.new(self)
462
476
  @server.start
463
477
  persist_registry
478
+ append_dispatch_start_record
464
479
 
465
480
  watch_thread = start_watch_thread
466
481
  @inbox.start
@@ -482,6 +497,8 @@ module Harnex
482
497
  end
483
498
  @ended_at = Time.now
484
499
 
500
+ enforce_required_artifact_report!
501
+ normalize_work_acceptance_exit_code!
485
502
  normalize_auto_stop_exit_code!
486
503
  drain_auto_stop_threads
487
504
  finalize_session!
@@ -540,8 +557,7 @@ module Harnex
540
557
  payload[:status] = status if status
541
558
  payload[:tokenUsage] = params["tokenUsage"] if params["tokenUsage"].is_a?(Hash)
542
559
  if successful_turn_status?(status)
543
- @last_completed_at = Time.now
544
- emit_event("task_complete", **payload)
560
+ record_successful_completion(payload)
545
561
  else
546
562
  mark_task_failed(
547
563
  turn_id: turn_id,
@@ -600,15 +616,170 @@ module Harnex
600
616
  SUCCESSFUL_TURN_STATUSES.include?(text)
601
617
  end
602
618
 
603
- def mark_task_failed(turn_id: nil, status: nil, error: nil, codex_error_info: nil)
619
+ def record_successful_completion(payload)
620
+ assessment = completion_gate_required? ? assess_completion_proof : { accepted: true }
621
+ unless assessment[:accepted]
622
+ mark_task_failed(
623
+ turn_id: payload[:turnId],
624
+ status: assessment.fetch(:outcome_class),
625
+ error: completion_failure_message(assessment.fetch(:outcome_class)),
626
+ outcome_class: assessment.fetch(:outcome_class),
627
+ artifact_report_status: assessment[:report_status],
628
+ diagnostics: assessment[:diagnostics]
629
+ )
630
+ return false
631
+ end
632
+
633
+ @last_failed_at = nil
634
+ @last_failed_status = nil
635
+ @last_error = nil
636
+ @last_completed_at = Time.now
637
+ if assessment[:outcome_class]
638
+ @completion_outcome_class = assessment[:outcome_class]
639
+ @completion_report_status = assessment[:report_status]
640
+ @completion_diagnostics = []
641
+ end
642
+
643
+ event_payload = payload.dup
644
+ event_payload[:outcome_class] = @completion_outcome_class if @completion_outcome_class
645
+ event_payload[:artifact_report_status] = @completion_report_status if @completion_report_status
646
+ emit_event("task_complete", **event_payload)
647
+ true
648
+ end
649
+
650
+ def completion_gate_required?
651
+ return true if require_artifact_report
652
+ return false unless adapter.transport == :stdio_jsonrpc
653
+
654
+ @auto_stop || (adapter.respond_to?(:initial_prompt) && !adapter.initial_prompt.to_s.empty?)
655
+ end
656
+
657
+ def assess_completion_proof
658
+ report_result = artifact_report_path ? Harnex::ArtifactReport.validate(artifact_report_path, final: true) : nil
659
+ report_fresh = report_result && report_result.status != "missing" && artifact_report_fresh?
660
+ report_stale = report_result && Harnex::ArtifactReport.accepted_final?(report_result) && !report_fresh
661
+ report_status = report_stale ? "stale" : report_result&.status
662
+ report_diagnostics = if report_stale
663
+ [artifact_report_stale_diagnostic]
664
+ else
665
+ report_result&.diagnostics || []
666
+ end
667
+
668
+ if report_result
669
+ if report_fresh && Harnex::ArtifactReport.accepted_final?(report_result)
670
+ return {
671
+ accepted: true,
672
+ outcome_class: "completed_with_proof",
673
+ report_status: "accepted",
674
+ diagnostics: []
675
+ }
676
+ end
677
+
678
+ report_outcome = Harnex::ArtifactReport.outcome_status(report_result)
679
+ if report_fresh && report_outcome == "rejected"
680
+ return {
681
+ accepted: false,
682
+ outcome_class: "report_rejected",
683
+ report_status: "rejected",
684
+ diagnostics: report_result.diagnostics
685
+ }
686
+ end
687
+
688
+ if require_artifact_report
689
+ outcome_class = report_result.status == "missing" ? "report_missing" : "report_invalid"
690
+ return {
691
+ accepted: false,
692
+ outcome_class: outcome_class,
693
+ report_status: report_status,
694
+ diagnostics: report_diagnostics
695
+ }
696
+ end
697
+ elsif require_artifact_report
698
+ return {
699
+ accepted: false,
700
+ outcome_class: "report_missing",
701
+ report_status: "missing",
702
+ diagnostics: []
703
+ }
704
+ end
705
+
706
+ if structured_activity_observed? || git_activity_observed?
707
+ {
708
+ accepted: true,
709
+ outcome_class: "completed_with_activity",
710
+ report_status: report_status,
711
+ diagnostics: []
712
+ }
713
+ else
714
+ {
715
+ accepted: false,
716
+ outcome_class: "completed_no_activity",
717
+ report_status: report_status,
718
+ diagnostics: report_diagnostics
719
+ }
720
+ end
721
+ end
722
+
723
+ def artifact_report_fresh?
724
+ current = Harnex::ArtifactReport.fingerprint(artifact_report_path)
725
+ return false unless current
726
+ return true unless @artifact_report_start_fingerprint
727
+
728
+ current != @artifact_report_start_fingerprint
729
+ end
730
+
731
+ def artifact_report_stale_diagnostic
732
+ {
733
+ "code" => "report_stale",
734
+ "path" => "$",
735
+ "message" => "artifact report was not created or updated during this session"
736
+ }
737
+ end
738
+
739
+ def structured_activity_observed?
740
+ counters = @event_counters.snapshot
741
+ counters[:commands_executed].to_i.positive? || counters[:tool_calls].to_i.positive?
742
+ end
743
+
744
+ def git_activity_observed?
745
+ return false if @git_start[:sha].to_s.empty?
746
+
747
+ snapshot = Harnex.git_capture_end(repo_root, @git_start[:sha])
748
+ snapshot[:commits].to_i.positive? || Array(snapshot[:changed_paths]).any? ||
749
+ (!snapshot[:sha].to_s.empty? && snapshot[:sha].to_s != @git_start[:sha].to_s)
750
+ end
751
+
752
+ def completion_failure_message(outcome_class)
753
+ case outcome_class
754
+ when "completed_no_activity"
755
+ "turn completed without command/tool execution, Git delta, or accepted artifact report"
756
+ when "report_missing"
757
+ "required artifact report is missing"
758
+ when "report_rejected"
759
+ "artifact report rejected work completion"
760
+ when "report_invalid"
761
+ "required artifact report is not valid final proof"
762
+ else
763
+ "work completion was not accepted"
764
+ end
765
+ end
766
+
767
+ def mark_task_failed(turn_id: nil, status: nil, error: nil, codex_error_info: nil, outcome_class: nil, artifact_report_status: nil, diagnostics: nil)
768
+ @last_completed_at = nil if outcome_class
604
769
  @last_failed_at = Time.now
605
770
  @last_failed_status = status.to_s.empty? ? "failed" : status.to_s
606
771
  @last_error = error.to_s unless error.to_s.empty?
772
+ @completion_outcome_class = outcome_class if outcome_class
773
+ @completion_report_status = artifact_report_status if artifact_report_status
774
+ @completion_diagnostics = Array(diagnostics).first(Harnex::ArtifactReport::MAX_DIAGNOSTICS) if diagnostics
607
775
 
608
776
  payload = { status: @last_failed_status }
609
777
  payload[:turnId] = turn_id if turn_id
610
778
  payload[:message] = error unless error.to_s.empty?
611
779
  payload[:codex_error_info] = codex_error_info if codex_error_info
780
+ payload[:outcome_class] = outcome_class if outcome_class
781
+ payload[:artifact_report_status] = artifact_report_status if artifact_report_status
782
+ payload[:diagnostics] = @completion_diagnostics unless @completion_diagnostics.empty?
612
783
  emit_event("task_failed", **payload)
613
784
  end
614
785
 
@@ -655,9 +826,8 @@ module Harnex
655
826
  @state_machine.force_busy!
656
827
  emit_event("turn_started") if event_type == "turn_start"
657
828
  when "agent_end"
658
- @last_completed_at = Time.now
659
829
  @state_machine.force_prompt!
660
- emit_event("task_complete")
830
+ record_successful_completion({})
661
831
  adapter.request_session_stats_async if adapter.respond_to?(:request_session_stats_async)
662
832
  schedule_auto_stop("task_complete", interrupt: false)
663
833
  when "message_start"
@@ -836,6 +1006,7 @@ module Harnex
836
1006
  env["HARNEX_ARTIFACT_REPORT_PATH"] = artifact_report_path
837
1007
  env["HARNEX_VALIDATION_REPORT_PATH"] = artifact_report_path
838
1008
  env["HARNEX_ARTIFACT_REPORT_SCHEMA"] = Harnex::ArtifactReport::SCHEMA
1009
+ env["HARNEX_ARTIFACT_REPORT_REQUIRED"] = "1" if require_artifact_report
839
1010
  end
840
1011
  env["HARNEX_SPAWNER_PANE"] = ENV["TMUX_PANE"] if ENV["TMUX_PANE"]
841
1012
  env
@@ -920,6 +1091,8 @@ module Harnex
920
1091
  task_failed: task_failed,
921
1092
  done: Harnex.work_done_for(state, task_complete: task_complete),
922
1093
  work_state: Harnex.work_state_for(state, task_complete: task_complete),
1094
+ outcome_class: @completion_outcome_class,
1095
+ artifact_report_status: @completion_report_status,
923
1096
  started_at: @started_at.iso8601,
924
1097
  exited_at: Time.now.iso8601,
925
1098
  injected_count: @injected_count
@@ -1143,7 +1316,10 @@ module Harnex
1143
1316
  seen_busy = @auto_stop_mutex.synchronize do
1144
1317
  @auto_stop_seen_busy ||= old_state == :busy || new_state == :busy
1145
1318
  end
1146
- schedule_auto_stop("prompt_after_busy") if seen_busy && new_state == :prompt
1319
+ return unless seen_busy && new_state == :prompt
1320
+
1321
+ record_successful_completion({}) if require_artifact_report
1322
+ schedule_auto_stop("prompt_after_busy")
1147
1323
  end
1148
1324
 
1149
1325
  def schedule_auto_stop(reason, turn_id: nil, interrupt: true)
@@ -1205,6 +1381,55 @@ module Harnex
1205
1381
  AUTOSTOP_TEARDOWN_GRACE_SECONDS_DEFAULT
1206
1382
  end
1207
1383
 
1384
+ def enforce_required_artifact_report!
1385
+ return unless require_artifact_report
1386
+ return if task_failed? && %w[report_missing report_invalid report_rejected].include?(@completion_outcome_class)
1387
+
1388
+ result = Harnex::ArtifactReport.validate(artifact_report_path, final: true)
1389
+ report_fresh = result.status != "missing" && artifact_report_fresh?
1390
+ report_accepted = Harnex::ArtifactReport.accepted_final?(result)
1391
+ report_stale = report_accepted && !report_fresh
1392
+ if report_fresh && report_accepted
1393
+ @completion_outcome_class = "completed_with_proof"
1394
+ @completion_report_status = "accepted"
1395
+ @completion_diagnostics = []
1396
+ return
1397
+ end
1398
+
1399
+ report_outcome = Harnex::ArtifactReport.outcome_status(result)
1400
+ outcome_class = if report_fresh && report_outcome == "rejected"
1401
+ "report_rejected"
1402
+ elsif result.status == "missing"
1403
+ "report_missing"
1404
+ else
1405
+ "report_invalid"
1406
+ end
1407
+ report_status = if report_stale
1408
+ "stale"
1409
+ elsif report_fresh && report_outcome == "rejected"
1410
+ "rejected"
1411
+ else
1412
+ result.status
1413
+ end
1414
+ diagnostics = report_stale ? [artifact_report_stale_diagnostic] : result.diagnostics
1415
+ mark_task_failed(
1416
+ status: outcome_class,
1417
+ error: completion_failure_message(outcome_class),
1418
+ outcome_class: outcome_class,
1419
+ artifact_report_status: report_status,
1420
+ diagnostics: diagnostics
1421
+ )
1422
+ @exit_code = 1 if @exit_code.nil? || @exit_code.zero? || @term_signal
1423
+ @term_signal = nil if @exit_code == 1
1424
+ end
1425
+
1426
+ def normalize_work_acceptance_exit_code!
1427
+ return unless task_failed? && @completion_outcome_class
1428
+
1429
+ @exit_code = 1 if @exit_code.nil? || @exit_code.zero? || @term_signal
1430
+ @term_signal = nil if @exit_code == 1
1431
+ end
1432
+
1208
1433
  def normalize_auto_stop_exit_code!
1209
1434
  return unless @auto_stop
1210
1435
  return unless @auto_stop_fired
@@ -1223,9 +1448,11 @@ module Harnex
1223
1448
 
1224
1449
  def classify_exit
1225
1450
  return "timeout" if @exit_code == 124
1451
+ return "failure" if task_failed? && @completion_outcome_class
1226
1452
  return "boot_failure" if boot_failure_exit?
1227
1453
  return "failure" if task_failed?
1228
1454
  return "success" if @exit_code == 0 && task_complete?
1455
+ return "success" if @exit_code == 0 && @completion_outcome_class == "completed_with_proof"
1229
1456
  return "success" if @exit_code == 0 && session_summary_present?
1230
1457
  return "failure" unless @exit_code == 0
1231
1458
 
@@ -1262,6 +1489,8 @@ module Harnex
1262
1489
  }
1263
1490
  queue = build_summary_queue
1264
1491
  record[:queue] = queue if queue
1492
+ orchestration = build_summary_orchestration
1493
+ record[:orchestration] = orchestration if orchestration
1265
1494
  record.merge!(artifact_payload.reject { |key, _value| key == "outcome" }) if artifact_payload
1266
1495
  record
1267
1496
  end
@@ -1306,6 +1535,14 @@ module Harnex
1306
1535
  queue
1307
1536
  end
1308
1537
 
1538
+ def build_summary_orchestration
1539
+ orchestration = Orchestration.normalize_metadata(meta_hash)
1540
+ return nil unless orchestration
1541
+
1542
+ orchestration["session_id"] ||= summary_agent_session_id || session_id
1543
+ orchestration
1544
+ end
1545
+
1309
1546
  def build_summary_agent
1310
1547
  {
1311
1548
  "cli" => adapter.key,
@@ -1425,14 +1662,25 @@ module Harnex
1425
1662
 
1426
1663
  def build_summary_outcome(artifact_payload)
1427
1664
  sidecar_outcome = artifact_payload&.dig("outcome") || {}
1428
- status = sidecar_outcome["status"]
1429
- status = "no_change" if status.nil? && @git_end.key?(:changed_paths) && @git_end[:changed_paths].empty?
1665
+ outcome_class, report_status = summary_proof_classification
1666
+ proof_failure = %w[completed_no_activity report_missing report_invalid task_failed].include?(outcome_class)
1667
+ status = proof_failure ? nil : sidecar_outcome["status"]
1668
+ status = "no_change" if status.nil? && !proof_failure && @git_end.key?(:changed_paths) && @git_end[:changed_paths].empty?
1430
1669
  status ||= "unknown"
1431
- status = "unknown" unless %w[accepted rejected no_change unknown].include?(status)
1670
+ status = "unknown" unless Harnex::ArtifactReport::OUTCOME_STATUSES.include?(status)
1671
+ source = if proof_failure
1672
+ "harnex_completion_gate"
1673
+ elsif sidecar_outcome["status"]
1674
+ "artifact_report"
1675
+ else
1676
+ "harnex_git_observation"
1677
+ end
1432
1678
 
1433
1679
  {
1434
1680
  "status" => status,
1435
- "source" => sidecar_outcome["status"] ? "artifact_report" : "harnex_git_observation",
1681
+ "class" => outcome_class,
1682
+ "source" => source,
1683
+ "report_status" => report_status,
1436
1684
  "commit_sha" => sidecar_outcome["commit_sha"] || summary_commit_sha,
1437
1685
  "changed_paths" => @git_end.key?(:changed_paths) ? @git_end[:changed_paths] : nil,
1438
1686
  "loc_added" => @git_end[:loc_added],
@@ -1443,6 +1691,25 @@ module Harnex
1443
1691
  }
1444
1692
  end
1445
1693
 
1694
+ def summary_proof_classification
1695
+ return [@completion_outcome_class, @completion_report_status] if @completion_outcome_class
1696
+ return ["task_failed", @completion_report_status] if task_failed?
1697
+
1698
+ if artifact_report_path
1699
+ result = Harnex::ArtifactReport.validate(artifact_report_path, final: true)
1700
+ report_fresh = result.status != "missing" && artifact_report_fresh?
1701
+ report_accepted = Harnex::ArtifactReport.accepted_final?(result)
1702
+ return ["completed_with_proof", "accepted"] if report_fresh && report_accepted
1703
+ return ["report_rejected", "rejected"] if report_fresh && Harnex::ArtifactReport.outcome_status(result) == "rejected"
1704
+ return ["unknown", "stale"] if report_accepted && !report_fresh
1705
+ return ["unknown", result.status]
1706
+ end
1707
+
1708
+ return ["completed_with_activity", @completion_report_status] if task_complete? && structured_activity_observed?
1709
+
1710
+ ["unknown", @completion_report_status]
1711
+ end
1712
+
1446
1713
  def build_summary_attempt
1447
1714
  {
1448
1715
  "run_id" => id,
@@ -1728,13 +1995,26 @@ module Harnex
1728
1995
  warn("harnex: failed to write dispatch summary #{summary_out}: #{e.message}")
1729
1996
  end
1730
1997
 
1998
+ def append_dispatch_start_record
1999
+ DispatchHistory.append(dispatch_history_path, DispatchHistory.build_start_record(self))
2000
+ rescue StandardError => e
2001
+ warn("harnex: failed to write dispatch start record: #{e.message}")
2002
+ end
2003
+
1731
2004
  def append_dispatch_history_record
1732
- path = DispatchHistory.path_for(launch_cwd)
1733
- DispatchHistory.append(path, DispatchHistory.build_record(self))
2005
+ DispatchHistory.append(dispatch_history_path, DispatchHistory.build_record(self))
1734
2006
  rescue StandardError => e
1735
2007
  warn("harnex: failed to write dispatch history: #{e.message}")
1736
2008
  end
1737
2009
 
2010
+ # One canonical stream per session: the start row and the end row must
2011
+ # land in the same file so readers can pair them. repo_root — not the
2012
+ # launch cwd — is the root that registry, events, and default summary
2013
+ # paths already key off.
2014
+ def dispatch_history_path
2015
+ @dispatch_history_path ||= DispatchHistory.path_for(repo_root)
2016
+ end
2017
+
1738
2018
  def normalized_usage_summary(summary)
1739
2019
  summary ||= {}
1740
2020
  USAGE_FIELDS.to_h { |field| [field, summary[field] || summary[field.to_s]] }
@@ -48,6 +48,8 @@ module Harnex
48
48
  "task_failed" => false,
49
49
  "done" => false,
50
50
  "work_state" => "unknown",
51
+ "outcome_class" => nil,
52
+ "artifact_report_status" => nil,
51
53
  "exit" => nil,
52
54
  "exit_code" => nil,
53
55
  "summary_out" => nil,
@@ -76,6 +78,7 @@ module Harnex
76
78
  File.foreach(path) do |line|
77
79
  record = JSON.parse(line)
78
80
  next unless record.is_a?(Hash)
81
+ next if DispatchHistory.start_record?(record)
79
82
 
80
83
  if summary_record?(record) && record.dig("meta", "id").to_s == id
81
84
  summary_record = record
@@ -130,6 +133,7 @@ module Harnex
130
133
  def build_from_summary(record, summary_path, fallback_repo_root)
131
134
  meta = record["meta"] || {}
132
135
  actual = record["actual"] || {}
136
+ outcome = record["outcome"] || {}
133
137
  state = classify_summary_state(actual)
134
138
  task_complete = !!actual["task_complete"]
135
139
  task_failed = state == "failed" && !task_complete
@@ -144,6 +148,8 @@ module Harnex
144
148
  "task_failed" => task_failed,
145
149
  "done" => Harnex.work_done_for(state, task_complete: task_complete),
146
150
  "work_state" => Harnex.work_state_for(state, task_complete: task_complete),
151
+ "outcome_class" => blank_to_nil(outcome["class"]),
152
+ "artifact_report_status" => blank_to_nil(outcome["report_status"]),
147
153
  "exit" => blank_to_nil(actual["exit"]),
148
154
  "exit_code" => actual["exit_code"],
149
155
  "summary_out" => summary_path,
@@ -188,6 +194,8 @@ module Harnex
188
194
  "task_failed" => task_failed,
189
195
  "done" => Harnex.work_done_for(state, task_complete: task_complete),
190
196
  "work_state" => Harnex.work_state_for(state, task_complete: task_complete),
197
+ "outcome_class" => nil,
198
+ "artifact_report_status" => nil,
191
199
  "exit" => history_exit(status),
192
200
  "exit_code" => nil,
193
201
  "summary_out" => blank_to_nil(record["summary_out_path"]),
@@ -1,4 +1,4 @@
1
1
  module Harnex
2
- VERSION = "0.7.13"
3
- RELEASE_DATE = "2026-07-15"
2
+ VERSION = "0.8.0"
3
+ RELEASE_DATE = "2026-08-02"
4
4
  end
data/lib/harnex.rb CHANGED
@@ -8,6 +8,7 @@ require_relative "harnex/dispatch_history"
8
8
  require_relative "harnex/terminal_status"
9
9
  require_relative "harnex/artifact_report"
10
10
  require_relative "harnex/context_telemetry"
11
+ require_relative "harnex/orchestration"
11
12
  require_relative "harnex/watcher"
12
13
  require_relative "harnex/adapters"
13
14
  require_relative "harnex/runtime/session_state"
@@ -31,4 +32,6 @@ require_relative "harnex/commands/recipes"
31
32
  require_relative "harnex/commands/guide"
32
33
  require_relative "harnex/commands/agents_guide"
33
34
  require_relative "harnex/commands/doctor"
35
+ require_relative "harnex/commands/orchestration"
36
+ require_relative "harnex/commands/artifact_report"
34
37
  require_relative "harnex/cli"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: harnex
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.13
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jikku Jose
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-15 00:00:00.000000000 Z
11
+ date: 2026-08-02 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A local PTY harness that wraps terminal AI agents (Claude, Codex, Pi)
14
14
  and adds a control plane for discovery, messaging, and coordination.
@@ -44,11 +44,13 @@ files:
44
44
  - lib/harnex/cli.rb
45
45
  - lib/harnex/codex/app_server/client.rb
46
46
  - lib/harnex/commands/agents_guide.rb
47
+ - lib/harnex/commands/artifact_report.rb
47
48
  - lib/harnex/commands/doctor.rb
48
49
  - lib/harnex/commands/events.rb
49
50
  - lib/harnex/commands/guide.rb
50
51
  - lib/harnex/commands/history.rb
51
52
  - lib/harnex/commands/logs.rb
53
+ - lib/harnex/commands/orchestration.rb
52
54
  - lib/harnex/commands/pane.rb
53
55
  - lib/harnex/commands/recipes.rb
54
56
  - lib/harnex/commands/run.rb
@@ -61,6 +63,7 @@ files:
61
63
  - lib/harnex/context_telemetry.rb
62
64
  - lib/harnex/core.rb
63
65
  - lib/harnex/dispatch_history.rb
66
+ - lib/harnex/orchestration.rb
64
67
  - lib/harnex/runtime/api_server.rb
65
68
  - lib/harnex/runtime/file_change_hook.rb
66
69
  - lib/harnex/runtime/inbox.rb