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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +82 -1
- data/README.md +53 -11
- data/TECHNICAL.md +20 -1
- data/guides/01_dispatch.md +29 -9
- data/guides/04_monitoring.md +58 -6
- 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/history.rb +52 -2
- data/lib/harnex/commands/orchestration.rb +170 -0
- data/lib/harnex/commands/run.rb +93 -6
- data/lib/harnex/commands/status.rb +43 -4
- data/lib/harnex/commands/wait.rb +164 -62
- data/lib/harnex/commands/watch.rb +5 -2
- data/lib/harnex/core.rb +11 -1
- data/lib/harnex/dispatch_history.rb +95 -0
- data/lib/harnex/orchestration.rb +458 -0
- data/lib/harnex/runtime/session.rb +295 -15
- data/lib/harnex/terminal_status.rb +8 -0
- data/lib/harnex/version.rb +2 -2
- data/lib/harnex.rb +3 -0
- metadata +5 -2
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1429
|
-
|
|
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
|
|
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
|
-
"
|
|
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
|
-
|
|
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"]),
|
data/lib/harnex/version.rb
CHANGED
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.
|
|
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-
|
|
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
|