harnex 0.7.12 → 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 +71 -0
- data/README.md +62 -17
- data/TECHNICAL.md +20 -1
- data/guides/01_dispatch.md +29 -9
- data/guides/04_monitoring.md +11 -5
- data/lib/harnex/adapters/base.rb +16 -0
- data/lib/harnex/adapters/codex.rb +4 -0
- data/lib/harnex/adapters/codex_appserver.rb +12 -0
- data/lib/harnex/adapters/pi.rb +46 -5
- data/lib/harnex/artifact_report.rb +522 -33
- 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 +68 -5
- data/lib/harnex/commands/wait.rb +6 -1
- data/lib/harnex/commands/watch.rb +2 -0
- data/lib/harnex/context_telemetry.rb +104 -0
- data/lib/harnex/core.rb +2 -0
- data/lib/harnex/orchestration.rb +458 -0
- data/lib/harnex/runtime/session.rb +620 -30
- data/lib/harnex/terminal_status.rb +7 -0
- data/lib/harnex/version.rb +2 -2
- data/lib/harnex.rb +4 -0
- metadata +6 -2
data/lib/harnex/cli.rb
CHANGED
|
@@ -37,6 +37,10 @@ module Harnex
|
|
|
37
37
|
AgentsGuide.new(@argv.drop(1)).run
|
|
38
38
|
when "doctor"
|
|
39
39
|
Doctor.new(@argv.drop(1)).run
|
|
40
|
+
when "orchestration"
|
|
41
|
+
OrchestrationCommand.new(@argv.drop(1)).run
|
|
42
|
+
when "artifact-report"
|
|
43
|
+
ArtifactReportCommand.new(@argv.drop(1)).run
|
|
40
44
|
when "help"
|
|
41
45
|
puts help(@argv[1])
|
|
42
46
|
0
|
|
@@ -83,6 +87,10 @@ module Harnex
|
|
|
83
87
|
AgentsGuide.usage
|
|
84
88
|
when "doctor"
|
|
85
89
|
Doctor.usage
|
|
90
|
+
when "orchestration"
|
|
91
|
+
OrchestrationCommand.usage
|
|
92
|
+
when "artifact-report"
|
|
93
|
+
ArtifactReportCommand.usage
|
|
86
94
|
else
|
|
87
95
|
usage
|
|
88
96
|
end
|
|
@@ -103,6 +111,8 @@ module Harnex
|
|
|
103
111
|
harnex pane --id ID [options]
|
|
104
112
|
harnex agents-guide [topic]
|
|
105
113
|
harnex doctor
|
|
114
|
+
harnex orchestration sample|report [options]
|
|
115
|
+
harnex artifact-report init|validate PATH [options]
|
|
106
116
|
harnex help [command]
|
|
107
117
|
|
|
108
118
|
Commands:
|
|
@@ -121,6 +131,10 @@ module Harnex
|
|
|
121
131
|
agents-guide
|
|
122
132
|
Show agent dispatch, chain, buddy, monitoring, and naming guides
|
|
123
133
|
doctor Run preflight checks and optional read-only session sweep
|
|
134
|
+
orchestration
|
|
135
|
+
Emit external primary samples and report orchestration tax
|
|
136
|
+
artifact-report
|
|
137
|
+
Initialize or validate harnex.artifact_report.v1 sidecars
|
|
124
138
|
help Show command help
|
|
125
139
|
|
|
126
140
|
New to harnex? Start with: harnex guide
|
|
@@ -142,6 +156,9 @@ module Harnex
|
|
|
142
156
|
harnex pane --id main --lines 40
|
|
143
157
|
harnex agents-guide dispatch
|
|
144
158
|
harnex doctor
|
|
159
|
+
harnex orchestration report --dispatch .harnex/dispatch.jsonl --run-id queue-005 --json
|
|
160
|
+
harnex artifact-report init .harnex/reports/cx-i-60.json
|
|
161
|
+
harnex artifact-report validate .harnex/reports/cx-i-60.json --final
|
|
145
162
|
harnex send --id main --message "Summarize current progress."
|
|
146
163
|
TEXT
|
|
147
164
|
end
|
|
@@ -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
|
@@ -16,7 +16,15 @@ module Harnex
|
|
|
16
16
|
"--plan" => "plan",
|
|
17
17
|
"--intent" => "intent",
|
|
18
18
|
"--model" => "model",
|
|
19
|
-
"--effort" => "effort"
|
|
19
|
+
"--effort" => "effort",
|
|
20
|
+
"--parent-dispatch-id" => "parent_dispatch_id",
|
|
21
|
+
"--parent-attempt-id" => "parent_attempt_id",
|
|
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"
|
|
20
28
|
}.freeze
|
|
21
29
|
TELEMETRY_KEYS_TO_FLAGS = TELEMETRY_FLAGS.invert.freeze
|
|
22
30
|
TELEMETRY_EQUALS_PREFIXES = TELEMETRY_FLAGS.keys.map { |flag| "#{flag}=" }.freeze
|
|
@@ -25,7 +33,7 @@ module Harnex
|
|
|
25
33
|
--id --description --detach --tmux --host --port --watch --watch-file
|
|
26
34
|
--stall-after --max-resumes --preset --context --meta --summary-out
|
|
27
35
|
--artifact-report --validation-report --cwd --root --timeout --inbox-ttl
|
|
28
|
-
--require-attribution --auto-stop --fast --legacy-pty --help
|
|
36
|
+
--require-artifact-report --require-attribution --auto-stop --fast --legacy-pty --help
|
|
29
37
|
].concat(TELEMETRY_FLAGS.keys).freeze
|
|
30
38
|
VALUE_FLAGS = %w[
|
|
31
39
|
--id --description --host --port --watch --watch-file --stall-after
|
|
@@ -50,7 +58,7 @@ module Harnex
|
|
|
50
58
|
--preset NAME Watch preset: impl, plan, gate (requires --watch)
|
|
51
59
|
--watch-file PATH Auto-send a file-change hook on modification
|
|
52
60
|
--context TEXT Inject as the initial prompt (prepends session header)
|
|
53
|
-
--auto-stop Stop after the first task completion from --context
|
|
61
|
+
--auto-stop Stop after the first accepted task completion from --context
|
|
54
62
|
--fast (codex only) Use Codex service_tier="fast".
|
|
55
63
|
Default Codex runs force service_tier="flex".
|
|
56
64
|
--meta JSON Attach parsed JSON metadata to the started event
|
|
@@ -59,6 +67,9 @@ module Harnex
|
|
|
59
67
|
Worker-written harnex.artifact_report.v1 JSON sidecar to ingest at exit
|
|
60
68
|
--validation-report PATH
|
|
61
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
|
|
62
73
|
--project-id ID Queue telemetry project id (first-class flags override --meta)
|
|
63
74
|
--queue-id ID Queue telemetry queue id
|
|
64
75
|
--entry-id ID Queue telemetry entry id
|
|
@@ -70,6 +81,22 @@ module Harnex
|
|
|
70
81
|
--intent TEXT Queue/work intent telemetry
|
|
71
82
|
--model NAME Requested model metadata (also used for structured dispatch)
|
|
72
83
|
--effort LEVEL Requested reasoning effort metadata (structured dispatch)
|
|
84
|
+
--parent-dispatch-id ID
|
|
85
|
+
Parent dispatch id for retry/fix/review joins
|
|
86
|
+
--parent-attempt-id ID
|
|
87
|
+
Parent attempt id for retry/fix/review joins
|
|
88
|
+
--attempt-kind KIND
|
|
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
|
|
73
100
|
--require-attribution
|
|
74
101
|
Fail before launch unless project/phase/intent and one work id are present
|
|
75
102
|
--cwd DIR Run the wrapped agent from DIR and use DIR as the session root
|
|
@@ -85,7 +112,9 @@ module Harnex
|
|
|
85
112
|
Notes:
|
|
86
113
|
Compatibility: `--watch PATH` and `--watch=PATH` still configure file-hook mode.
|
|
87
114
|
Bare `--watch` enables the babysitter.
|
|
88
|
-
--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.
|
|
89
118
|
Explicit --stall-after/--max-resumes values override --preset defaults.
|
|
90
119
|
CLIs with smart prompt detection: #{Adapters.known.join(', ')}
|
|
91
120
|
Any other CLI name is launched with generic wrapping.
|
|
@@ -131,6 +160,7 @@ module Harnex
|
|
|
131
160
|
require_attribution: false,
|
|
132
161
|
summary_out: nil,
|
|
133
162
|
artifact_report: nil,
|
|
163
|
+
require_artifact_report: false,
|
|
134
164
|
cwd: nil,
|
|
135
165
|
root: nil,
|
|
136
166
|
auto_stop: false,
|
|
@@ -154,7 +184,10 @@ module Harnex
|
|
|
154
184
|
|
|
155
185
|
raise OptionParser::MissingArgument, "cli" if cli_name.nil?
|
|
156
186
|
validate_auto_stop_context!
|
|
187
|
+
validate_required_artifact_report!
|
|
157
188
|
apply_telemetry_options!
|
|
189
|
+
validate_attempt_metadata!
|
|
190
|
+
validate_orchestration_metadata!
|
|
158
191
|
validate_required_attribution!
|
|
159
192
|
|
|
160
193
|
repo_root = resolve_run_root(cli_name, child_args)
|
|
@@ -227,6 +260,7 @@ module Harnex
|
|
|
227
260
|
tmux_cmd << "--require-attribution" if @options[:require_attribution]
|
|
228
261
|
tmux_cmd += ["--summary-out", @options[:summary_out]] if @options[:summary_out]
|
|
229
262
|
tmux_cmd += ["--artifact-report", @options[:artifact_report]] if @options[:artifact_report]
|
|
263
|
+
tmux_cmd << "--require-artifact-report" if @options[:require_artifact_report]
|
|
230
264
|
tmux_cmd += ["--cwd", @options[:cwd]] if @options[:cwd]
|
|
231
265
|
tmux_cmd += ["--root", @options[:root]] if @options[:root]
|
|
232
266
|
tmux_cmd += ["--inbox-ttl", @options[:inbox_ttl].to_s]
|
|
@@ -337,6 +371,7 @@ module Harnex
|
|
|
337
371
|
meta: @options[:meta],
|
|
338
372
|
summary_out: @options[:summary_out],
|
|
339
373
|
artifact_report_path: @options[:artifact_report],
|
|
374
|
+
require_artifact_report: @options[:require_artifact_report],
|
|
340
375
|
inbox_ttl: @options[:inbox_ttl],
|
|
341
376
|
auto_stop: @options[:auto_stop],
|
|
342
377
|
launch_cwd: history_cwd,
|
|
@@ -529,6 +564,8 @@ module Harnex
|
|
|
529
564
|
when "--artifact-report", "--validation-report"
|
|
530
565
|
index += 1
|
|
531
566
|
@options[:artifact_report] = required_option_value(arg, argv[index])
|
|
567
|
+
when "--require-artifact-report"
|
|
568
|
+
@options[:require_artifact_report] = true
|
|
532
569
|
when /\A--artifact-report=(.+)\z/
|
|
533
570
|
@options[:artifact_report] = required_option_value("--artifact-report", Regexp.last_match(1))
|
|
534
571
|
when /\A--validation-report=(.+)\z/
|
|
@@ -609,7 +646,7 @@ module Harnex
|
|
|
609
646
|
case arg
|
|
610
647
|
when "--"
|
|
611
648
|
return false
|
|
612
|
-
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"
|
|
613
650
|
nil
|
|
614
651
|
when /\A--tmux=/
|
|
615
652
|
nil
|
|
@@ -666,6 +703,14 @@ module Harnex
|
|
|
666
703
|
raise OptionParser::InvalidOption, "harnex run: --auto-stop requires --context"
|
|
667
704
|
end
|
|
668
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
|
+
|
|
669
714
|
def apply_telemetry_options!
|
|
670
715
|
explicit = @options[:telemetry]
|
|
671
716
|
return if explicit.empty? && @options[:meta].is_a?(Hash)
|
|
@@ -674,6 +719,24 @@ module Harnex
|
|
|
674
719
|
@options[:meta] = (@options[:meta].is_a?(Hash) ? @options[:meta].dup : {}).merge(explicit)
|
|
675
720
|
end
|
|
676
721
|
|
|
722
|
+
def validate_attempt_metadata!
|
|
723
|
+
metadata = @options[:meta].is_a?(Hash) ? @options[:meta] : {}
|
|
724
|
+
kind = metadata["attempt_kind"].to_s
|
|
725
|
+
return if kind.empty? || Session::ATTEMPT_KINDS.include?(kind)
|
|
726
|
+
|
|
727
|
+
raise OptionParser::InvalidOption,
|
|
728
|
+
"harnex run: --attempt-kind must be one of #{Session::ATTEMPT_KINDS.join(', ')}"
|
|
729
|
+
end
|
|
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
|
+
|
|
677
740
|
def validate_required_attribution!
|
|
678
741
|
return unless @options[:require_attribution]
|
|
679
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"],
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
module Harnex
|
|
2
|
+
# Bounded in-memory aggregate for active context-window samples.
|
|
3
|
+
#
|
|
4
|
+
# It retains only the final valid occupancy sample, independent high-water
|
|
5
|
+
# marks, and counters that make unavailable samples visible without storing
|
|
6
|
+
# raw protocol payloads.
|
|
7
|
+
class ContextTelemetry
|
|
8
|
+
MEASUREMENT_STATUSES = %w[observed estimated].freeze
|
|
9
|
+
|
|
10
|
+
def initialize(status:, source:)
|
|
11
|
+
measurement_status = status.to_s
|
|
12
|
+
unless MEASUREMENT_STATUSES.include?(measurement_status)
|
|
13
|
+
raise ArgumentError, "context telemetry status must be observed or estimated"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
@measurement_status = measurement_status
|
|
17
|
+
@source = source.to_s.empty? ? nil : source.to_s
|
|
18
|
+
@terminal_tokens = nil
|
|
19
|
+
@window_tokens = nil
|
|
20
|
+
@terminal_percent = nil
|
|
21
|
+
@peak_tokens = nil
|
|
22
|
+
@peak_percent = nil
|
|
23
|
+
@samples = 0
|
|
24
|
+
@missing_samples = 0
|
|
25
|
+
@latest_sample_status = nil
|
|
26
|
+
@valid_sample_seen = false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def record(tokens:, window_tokens:, percent: nil)
|
|
30
|
+
@samples += 1
|
|
31
|
+
token_count = nonnegative_integer(tokens)
|
|
32
|
+
window_size = positive_integer(window_tokens)
|
|
33
|
+
pressure_percent = nonnegative_float(percent)
|
|
34
|
+
pressure_percent ||= derived_percent(token_count, window_size)
|
|
35
|
+
|
|
36
|
+
if token_count.nil?
|
|
37
|
+
@missing_samples += 1
|
|
38
|
+
@latest_sample_status = "missing"
|
|
39
|
+
return snapshot
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
@valid_sample_seen = true
|
|
43
|
+
@terminal_tokens = token_count
|
|
44
|
+
@window_tokens = window_size
|
|
45
|
+
@terminal_percent = pressure_percent
|
|
46
|
+
@peak_tokens = [@peak_tokens, token_count].compact.max
|
|
47
|
+
@peak_percent = [@peak_percent, pressure_percent].compact.max
|
|
48
|
+
@latest_sample_status = @measurement_status
|
|
49
|
+
snapshot
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def snapshot
|
|
53
|
+
{
|
|
54
|
+
status: @valid_sample_seen ? @measurement_status : "missing",
|
|
55
|
+
source: @source,
|
|
56
|
+
terminal_tokens: @terminal_tokens,
|
|
57
|
+
window_tokens: @window_tokens,
|
|
58
|
+
terminal_percent: @terminal_percent,
|
|
59
|
+
peak_tokens: @peak_tokens,
|
|
60
|
+
peak_percent: @peak_percent,
|
|
61
|
+
samples: @samples,
|
|
62
|
+
missing_samples: @missing_samples,
|
|
63
|
+
latest_sample_status: @latest_sample_status
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def nonnegative_integer(value)
|
|
70
|
+
return nil if value.nil?
|
|
71
|
+
|
|
72
|
+
parsed = Integer(value)
|
|
73
|
+
return nil if value.is_a?(Numeric) && value.to_f != parsed.to_f
|
|
74
|
+
return nil if parsed.negative?
|
|
75
|
+
|
|
76
|
+
parsed
|
|
77
|
+
rescue ArgumentError, TypeError, FloatDomainError
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def positive_integer(value)
|
|
82
|
+
parsed = nonnegative_integer(value)
|
|
83
|
+
parsed&.positive? ? parsed : nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def nonnegative_float(value)
|
|
87
|
+
return nil if value.nil?
|
|
88
|
+
|
|
89
|
+
parsed = Float(value)
|
|
90
|
+
return nil unless parsed.finite?
|
|
91
|
+
return nil if parsed.negative?
|
|
92
|
+
|
|
93
|
+
parsed
|
|
94
|
+
rescue ArgumentError, TypeError
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def derived_percent(token_count, window_size)
|
|
99
|
+
return nil if token_count.nil? || window_size.nil?
|
|
100
|
+
|
|
101
|
+
((token_count.to_f / window_size) * 100).round(2)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
data/lib/harnex/core.rb
CHANGED
|
@@ -141,6 +141,7 @@ module Harnex
|
|
|
141
141
|
end_sha = git_output(repo_root, "rev-parse", "HEAD")
|
|
142
142
|
range = "#{start_sha}..#{end_sha}"
|
|
143
143
|
shortstat = git_output(repo_root, "diff", "--shortstat", range)
|
|
144
|
+
changed_paths = git_output(repo_root, "diff", "--name-only", range).lines.map(&:strip).reject(&:empty?).first(200)
|
|
144
145
|
commits = Integer(git_output(repo_root, "rev-list", "--count", range))
|
|
145
146
|
stats = parse_git_shortstat(shortstat)
|
|
146
147
|
|
|
@@ -149,6 +150,7 @@ module Harnex
|
|
|
149
150
|
loc_added: stats.fetch(:loc_added),
|
|
150
151
|
loc_removed: stats.fetch(:loc_removed),
|
|
151
152
|
files_changed: stats.fetch(:files_changed),
|
|
153
|
+
changed_paths: changed_paths,
|
|
152
154
|
commits: commits
|
|
153
155
|
}
|
|
154
156
|
rescue StandardError
|