harnex 0.7.10 → 0.7.11

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2af8afe53ccea3fb668e920809da3fdbcd523ee5eef0179bcdd84f026077cff1
4
- data.tar.gz: f675d4a401f703bf52adc88ce352fec04d2090533c3249bd0ac05e6828d030f0
3
+ metadata.gz: 104afb9f09fe59bd24191fde1122294a35ae47069a929022228b4365d653da54
4
+ data.tar.gz: ac8032b88f3c0001718ffc053923000abbba33743e6de4b68459108ca481cbcf
5
5
  SHA512:
6
- metadata.gz: cda7d84fd1f80c9526529b03293623eb16f76a78872e5e29640896be76f79a448ab3bf8f4c92c74c56b8f88b814739b7fbd201a6c91ba749d22ecdc5cf8a720e
7
- data.tar.gz: bc6704fc2a86a01e61c1a431fd96781bbaf178029679bc1b5c64be699f82c49bbf8b27d4f921c0d7547752eadc2dc61cffc1bc946b99d0cd5d79dd46a0de77af
6
+ metadata.gz: f76c4a9847aebd46cca3bada6c0af0e622bb3840fa53160cb4811c2beac2a007c6a01faadf042ef22f73e55c2ddde5571adc56d774e5b444ece412ad656242e4
7
+ data.tar.gz: 1d9843aa631eb411fa31c772cf5c09b92cc5d5d67ed95583abf041fd250e31343c76215a8949f268563e14609416724fd681bad6bfc5c6b7c270a4f9639e6838
data/CHANGELOG.md CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.7.11] - 2026-07-10 | 11:14 PM | IST
6
+
7
+ ### Added
8
+
9
+ - Dispatch summaries now include top-level additive `agent` and `reliability`
10
+ blocks for every run, and a top-level `queue` block when queue attribution is
11
+ provided. New consumers should prefer these blocks over legacy flat fields for
12
+ queue economics and reliability analytics.
13
+ - `harnex run` now accepts first-class queue/agent telemetry flags:
14
+ `--project-id`, `--queue-id`, `--entry-id`, `--entry-title`, `--phase`,
15
+ `--tier`, `--issue`, `--plan`, `--intent`, `--model`, and `--effort`.
16
+ Explicit flags override same-named `--meta` values and are forwarded through
17
+ tmux re-exec.
18
+ - `harnex run --require-attribution` fails before launching unless required
19
+ queue attribution is present (`project_id`, `phase`, `intent`, and at least
20
+ one work id).
21
+
22
+ ### Changed
23
+
24
+ - Dispatch telemetry docs, README, and the packaged dispatch agents-guide now
25
+ document queue attribution, effective agent routing fields, reliability split,
26
+ and a sample queue grouping snippet.
27
+
5
28
  ## [0.7.10] - 2026-07-10 | 10:57 PM | IST
6
29
 
7
30
  ### Added
data/README.md CHANGED
@@ -284,9 +284,18 @@ compact `artifact_report`, `validation`, and `artifacts` summary in the dispatch
284
284
  row. Missing or malformed reports are warning telemetry, not wrapped-process
285
285
  crashes.
286
286
 
287
- Those declared values are copied into summary `meta`. Terminal summary
288
- `actual` records timing, exit classification, token usage when the adapter can
289
- capture it, adapter-reported `cost_usd` when reliably available, git deltas,
287
+ Queue runners can pass first-class attribution without hiding it in prose:
288
+
289
+ ```bash
290
+ harnex run pi --project-id harnex --queue-id queue-005 --entry-id SP-4 \
291
+ --phase implement --intent queue-work --require-attribution ...
292
+ ```
293
+
294
+ Soft budget metadata is copied into summary `meta`; queue/agent/reliability
295
+ metadata is copied into top-level `queue`, `agent`, and `reliability` summary
296
+ blocks. Terminal summary `actual` records timing, exit classification, token
297
+ usage when the adapter can capture it, adapter-reported `cost_usd` when
298
+ reliably available, git deltas,
290
299
  task-completion state, operational counters (`stalls`, `force_resumes`,
291
300
  `disconnections`, `tool_calls`, `commands_executed`), output/event log paths,
292
301
  and rough volume measurements such as `lines_changed`, `output_lines`,
@@ -111,6 +111,20 @@ artifact summaries (`finding`, `review`, `gate`, `blocker`, etc.), evidence,
111
111
  confidence, and canonical refs. Harnex records missing/malformed sidecars as
112
112
  warning telemetry instead of failing the wrapped process.
113
113
 
114
+ Queue runners should pass first-class attribution so dispatch rows can be grouped
115
+ without path/id heuristics:
116
+
117
+ ```bash
118
+ harnex run pi --id pi-i-NN --tmux pi-i-NN \
119
+ --project-id harnex --queue-id queue-005 --entry-id SP-4 \
120
+ --phase implement --intent queue-work --require-attribution \
121
+ --context "Read and execute /tmp/task-impl-NN.md"
122
+ ```
123
+
124
+ `--require-attribution` fails before launch unless `project_id`, `phase`,
125
+ `intent`, and at least one work id (`queue_id`, `entry_id`, `issue`, or `plan`)
126
+ are present.
127
+
114
128
  Pi runs use structured RPC (`pi --mode rpc`). Pass Pi child flags after `--`
115
129
  (e.g. `harnex run pi --context "..." -- --model anthropic/claude-sonnet-4-5 --thinking high`).
116
130
 
@@ -5,15 +5,33 @@ require "shellwords"
5
5
  module Harnex
6
6
  class Runner
7
7
  DEFAULT_TIMEOUT = 5.0
8
+ TELEMETRY_FLAGS = {
9
+ "--project-id" => "project_id",
10
+ "--queue-id" => "queue_id",
11
+ "--entry-id" => "entry_id",
12
+ "--entry-title" => "entry_title",
13
+ "--phase" => "phase",
14
+ "--tier" => "tier",
15
+ "--issue" => "issue",
16
+ "--plan" => "plan",
17
+ "--intent" => "intent",
18
+ "--model" => "model",
19
+ "--effort" => "effort"
20
+ }.freeze
21
+ TELEMETRY_KEYS_TO_FLAGS = TELEMETRY_FLAGS.invert.freeze
22
+ TELEMETRY_EQUALS_PREFIXES = TELEMETRY_FLAGS.keys.map { |flag| "#{flag}=" }.freeze
23
+
8
24
  KNOWN_FLAGS = %w[
9
25
  --id --description --detach --tmux --host --port --watch --watch-file
10
26
  --stall-after --max-resumes --preset --context --meta --summary-out
11
- --artifact-report --validation-report --cwd --root --timeout --inbox-ttl --auto-stop --fast --legacy-pty --help
12
- ].freeze
27
+ --artifact-report --validation-report --cwd --root --timeout --inbox-ttl
28
+ --require-attribution --auto-stop --fast --legacy-pty --help
29
+ ].concat(TELEMETRY_FLAGS.keys).freeze
13
30
  VALUE_FLAGS = %w[
14
31
  --id --description --host --port --watch --watch-file --stall-after
15
- --max-resumes --preset --context --meta --summary-out --artifact-report --validation-report --cwd --root --timeout --inbox-ttl
16
- ].freeze
32
+ --max-resumes --preset --context --meta --summary-out --artifact-report
33
+ --validation-report --cwd --root --timeout --inbox-ttl
34
+ ].concat(TELEMETRY_FLAGS.keys).freeze
17
35
 
18
36
  def self.usage(program_name = "harnex run")
19
37
  <<~TEXT
@@ -41,6 +59,19 @@ module Harnex
41
59
  Worker-written harnex.artifact_report.v1 JSON sidecar to ingest at exit
42
60
  --validation-report PATH
43
61
  Alias for --artifact-report; also exposed as HARNEX_VALIDATION_REPORT_PATH
62
+ --project-id ID Queue telemetry project id (first-class flags override --meta)
63
+ --queue-id ID Queue telemetry queue id
64
+ --entry-id ID Queue telemetry entry id
65
+ --entry-title TEXT Queue telemetry entry title
66
+ --phase TEXT Queue/work phase telemetry
67
+ --tier TEXT Queue/work tier telemetry
68
+ --issue ID Queue/work issue telemetry
69
+ --plan ID Queue/work plan telemetry
70
+ --intent TEXT Queue/work intent telemetry
71
+ --model NAME Requested model metadata (also used for structured dispatch)
72
+ --effort LEVEL Requested reasoning effort metadata (structured dispatch)
73
+ --require-attribution
74
+ Fail before launch unless project/phase/intent and one work id are present
44
75
  --cwd DIR Run the wrapped agent from DIR and use DIR as the session root
45
76
  --root DIR Override harnex session/root attribution without changing child cwd
46
77
  --timeout SECS Max seconds to wait for detached registration (default: #{DEFAULT_TIMEOUT})
@@ -66,6 +97,7 @@ module Harnex
66
97
  #{program_name} pi --id pi-i-42 --watch --preset impl --context "Read /tmp/task-impl-42.md"
67
98
  #{program_name} codex --cwd /tmp/public-bundle --id eval-001 --context "Read README.md and write OUTPUT.md" --auto-stop
68
99
  #{program_name} pi --id pi-i-52 --artifact-report .harnex/reports/pi-i-52.json --context "Write proof to $HARNEX_ARTIFACT_REPORT_PATH" --auto-stop
100
+ #{program_name} pi --project-id harnex --queue-id queue-005 --entry-id SP-4 --phase implement --intent queue-work --require-attribution --context "Implement SP-4"
69
101
  #{program_name} claude --id cl-r-42 --tmux cl-r-42 --description "Review task 42"
70
102
 
71
103
  Gotchas:
@@ -95,6 +127,8 @@ module Harnex
95
127
  watch: nil,
96
128
  context: nil,
97
129
  meta: nil,
130
+ telemetry: {},
131
+ require_attribution: false,
98
132
  summary_out: nil,
99
133
  artifact_report: nil,
100
134
  cwd: nil,
@@ -120,6 +154,8 @@ module Harnex
120
154
 
121
155
  raise OptionParser::MissingArgument, "cli" if cli_name.nil?
122
156
  validate_auto_stop_context!
157
+ apply_telemetry_options!
158
+ validate_required_attribution!
123
159
 
124
160
  repo_root = resolve_run_root(cli_name, child_args)
125
161
  @options[:summary_out] = resolve_summary_out(repo_root)
@@ -184,6 +220,11 @@ module Harnex
184
220
  tmux_cmd += ["--context", @options[:context]] if @options[:context]
185
221
  tmux_cmd << "--auto-stop" if @options[:auto_stop]
186
222
  tmux_cmd += ["--meta", JSON.generate(@options[:meta])] if @options[:meta]
223
+ @options[:telemetry].each do |key, value|
224
+ flag = TELEMETRY_KEYS_TO_FLAGS[key]
225
+ tmux_cmd += [flag, value] if flag && value
226
+ end
227
+ tmux_cmd << "--require-attribution" if @options[:require_attribution]
187
228
  tmux_cmd += ["--summary-out", @options[:summary_out]] if @options[:summary_out]
188
229
  tmux_cmd += ["--artifact-report", @options[:artifact_report]] if @options[:artifact_report]
189
230
  tmux_cmd += ["--cwd", @options[:cwd]] if @options[:cwd]
@@ -465,6 +506,8 @@ module Harnex
465
506
  @options[:context] = required_option_value("--context", Regexp.last_match(1))
466
507
  when "--auto-stop"
467
508
  @options[:auto_stop] = true
509
+ when "--require-attribution"
510
+ @options[:require_attribution] = true
468
511
  when "--fast"
469
512
  @options[:fast] = true
470
513
  when "--meta"
@@ -472,6 +515,12 @@ module Harnex
472
515
  @options[:meta] = parse_meta(required_option_value(arg, argv[index]))
473
516
  when /\A--meta=(.+)\z/
474
517
  @options[:meta] = parse_meta(required_option_value("--meta", Regexp.last_match(1)))
518
+ when *TELEMETRY_FLAGS.keys
519
+ index += 1
520
+ @options[:telemetry][TELEMETRY_FLAGS.fetch(arg)] = required_option_value(arg, argv[index])
521
+ when telemetry_equals_regex
522
+ flag = "--#{Regexp.last_match(1)}"
523
+ @options[:telemetry][TELEMETRY_FLAGS.fetch(flag)] = required_option_value(flag, Regexp.last_match(2))
475
524
  when "--summary-out"
476
525
  index += 1
477
526
  @options[:summary_out] = required_option_value(arg, argv[index])
@@ -520,6 +569,10 @@ module Harnex
520
569
  [cli_name, forwarded]
521
570
  end
522
571
 
572
+ def telemetry_equals_regex
573
+ @telemetry_equals_regex ||= /\A--(#{TELEMETRY_FLAGS.keys.map { |flag| Regexp.escape(flag.delete_prefix("--")) }.join("|")})=(.+)\z/
574
+ end
575
+
523
576
  def unknown_long_flag?(arg)
524
577
  arg.start_with?("--")
525
578
  end
@@ -556,7 +609,7 @@ module Harnex
556
609
  case arg
557
610
  when "--"
558
611
  return false
559
- when "-h", "--help", "--detach", "--tmux", "--auto-stop", "--fast", "--legacy-pty"
612
+ when "-h", "--help", "--detach", "--tmux", "--auto-stop", "--require-attribution", "--fast", "--legacy-pty"
560
613
  nil
561
614
  when /\A--tmux=/
562
615
  nil
@@ -564,6 +617,8 @@ module Harnex
564
617
  index += 1
565
618
  when /\A--(?:id|description|host|port|watch|watch-file|stall-after|max-resumes|context|meta|summary-out|artifact-report|validation-report|cwd|root|timeout|inbox-ttl)=/
566
619
  nil
620
+ when telemetry_equals_regex
621
+ nil
567
622
  when /\A--preset=/
568
623
  nil
569
624
  else
@@ -581,7 +636,8 @@ module Harnex
581
636
  arg.start_with?(
582
637
  "--id=", "--description=", "--tmux=", "--host=", "--port=", "--watch=", "--watch-file=",
583
638
  "--stall-after=", "--max-resumes=", "--preset=", "--context=", "--meta=", "--summary-out=",
584
- "--artifact-report=", "--validation-report=", "--cwd=", "--root=", "--timeout=", "--inbox-ttl="
639
+ "--artifact-report=", "--validation-report=", "--cwd=", "--root=", "--timeout=", "--inbox-ttl=",
640
+ *TELEMETRY_EQUALS_PREFIXES
585
641
  )
586
642
  end
587
643
 
@@ -610,6 +666,32 @@ module Harnex
610
666
  raise OptionParser::InvalidOption, "harnex run: --auto-stop requires --context"
611
667
  end
612
668
 
669
+ def apply_telemetry_options!
670
+ explicit = @options[:telemetry]
671
+ return if explicit.empty? && @options[:meta].is_a?(Hash)
672
+ return if explicit.empty?
673
+
674
+ @options[:meta] = (@options[:meta].is_a?(Hash) ? @options[:meta].dup : {}).merge(explicit)
675
+ end
676
+
677
+ def validate_required_attribution!
678
+ return unless @options[:require_attribution]
679
+
680
+ metadata = @options[:meta].is_a?(Hash) ? @options[:meta] : {}
681
+ missing = %w[project_id phase intent].select { |key| blank_value?(metadata[key]) }
682
+ unless %w[queue_id entry_id issue plan].any? { |key| !blank_value?(metadata[key]) }
683
+ missing << "one of queue_id/entry_id/issue/plan"
684
+ end
685
+ return if missing.empty?
686
+
687
+ raise OptionParser::InvalidOption,
688
+ "harnex run: --require-attribution missing #{missing.join(', ')}"
689
+ end
690
+
691
+ def blank_value?(value)
692
+ value.nil? || value.to_s.strip.empty?
693
+ end
694
+
613
695
  def apply_codex_service_tier(cli_name, child_args)
614
696
  return child_args unless cli_name.to_s == "codex"
615
697
  return child_args if child_service_tier_config?(child_args)
@@ -16,6 +16,11 @@ module Harnex
16
16
  agent_session_id cost_usd
17
17
  ].freeze
18
18
  BUDGET_META_FIELDS = %w[read_budget_lines output_ceiling_lines].freeze
19
+ QUEUE_FIELDS = %w[project_id queue_id entry_id entry_title issue plan phase tier intent].freeze
20
+ AGENT_FIELDS = %w[cli provider model_requested model_effective reasoning_effort service_tier adapter_transport].freeze
21
+ RELIABILITY_FIELDS = %w[
22
+ adapter_close real_disconnections stream_interruptions stalls force_resumes compactions recovered
23
+ ].freeze
19
24
  SUCCESSFUL_TURN_STATUSES = %w[completed success succeeded].freeze
20
25
  class EventCounters
21
26
  def initialize
@@ -1168,8 +1173,12 @@ module Harnex
1168
1173
  record = {
1169
1174
  meta: build_summary_meta,
1170
1175
  predicted: summary_predicted_payload,
1171
- actual: build_summary_actual
1176
+ actual: build_summary_actual,
1177
+ agent: build_summary_agent,
1178
+ reliability: build_summary_reliability
1172
1179
  }
1180
+ queue = build_summary_queue
1181
+ record[:queue] = queue if queue
1173
1182
  record.merge!(artifact_report_summary) if artifact_report_path
1174
1183
  record
1175
1184
  end
@@ -1207,12 +1216,42 @@ module Harnex
1207
1216
  }.merge(summary_budget_meta)
1208
1217
  end
1209
1218
 
1219
+ def build_summary_queue
1220
+ queue = QUEUE_FIELDS.to_h { |field| [field, summary_string(meta_hash[field])] }
1221
+ return nil if queue.values.all?(&:nil?)
1222
+
1223
+ queue
1224
+ end
1225
+
1226
+ def build_summary_agent
1227
+ {
1228
+ "cli" => adapter.key,
1229
+ "provider" => summary_agent_provider,
1230
+ "model_requested" => summary_string(meta_hash["model"]),
1231
+ "model_effective" => summary_string(summary_model),
1232
+ "reasoning_effort" => summary_string(meta_hash["effort"]),
1233
+ "service_tier" => summary_service_tier,
1234
+ "adapter_transport" => adapter.transport.to_s
1235
+ }
1236
+ end
1237
+
1238
+ def build_summary_reliability
1239
+ counters = summary_event_counters
1240
+ real_disconnections = counters[:disconnections].to_i
1241
+ {
1242
+ "adapter_close" => summary_adapter_close(real_disconnections),
1243
+ "real_disconnections" => real_disconnections,
1244
+ "stream_interruptions" => real_disconnections,
1245
+ "stalls" => counters[:stalls].to_i,
1246
+ "force_resumes" => counters[:force_resumes].to_i,
1247
+ "compactions" => counters[:compactions].to_i,
1248
+ "recovered" => false
1249
+ }
1250
+ end
1251
+
1210
1252
  def build_summary_actual
1211
- counters = @event_counters.snapshot
1253
+ counters = summary_event_counters
1212
1254
  output_measurements = summary_output_measurements
1213
- if %w[disconnected boot_failure].include?(@exit_reason)
1214
- counters[:disconnections] = [counters[:disconnections], 1].max
1215
- end
1216
1255
 
1217
1256
  actual = {
1218
1257
  model: summary_model,
@@ -1298,6 +1337,52 @@ module Harnex
1298
1337
  (adapter.current_model if adapter.respond_to?(:current_model))
1299
1338
  end
1300
1339
 
1340
+ def summary_service_tier
1341
+ summary_string(meta_hash["service_tier"]) || service_tier_from_command
1342
+ end
1343
+
1344
+ def service_tier_from_command
1345
+ command.each_with_index do |arg, index|
1346
+ text = arg.to_s
1347
+ if text == "-c" || text == "--config"
1348
+ parsed = parse_service_tier_config(command[index + 1])
1349
+ return parsed if parsed
1350
+ end
1351
+ parsed = parse_service_tier_config(text)
1352
+ return parsed if parsed
1353
+ end
1354
+ nil
1355
+ end
1356
+
1357
+ def parse_service_tier_config(value)
1358
+ text = value.to_s
1359
+ match = text.match(/(?:\A|=)service_tier=\\?"?([^"\s]+)\\?"?/)
1360
+ return nil unless match
1361
+
1362
+ summary_string(match[1])
1363
+ end
1364
+
1365
+ def summary_event_counters
1366
+ counters = @event_counters.snapshot
1367
+ if %w[disconnected boot_failure].include?(@exit_reason)
1368
+ counters[:disconnections] = [counters[:disconnections], 1].max
1369
+ end
1370
+ counters
1371
+ end
1372
+
1373
+ def summary_adapter_close(real_disconnections)
1374
+ return "interrupted" if @exit_code == 124 || @term_signal
1375
+ return "lost" if %w[disconnected boot_failure].include?(@exit_reason) || real_disconnections.to_i.positive?
1376
+ return "normal" if %w[success failure].include?(@exit_reason)
1377
+
1378
+ "unknown"
1379
+ end
1380
+
1381
+ def summary_string(value)
1382
+ text = value.to_s
1383
+ text.empty? ? nil : text
1384
+ end
1385
+
1301
1386
  def summary_tool_calls(counters)
1302
1387
  @usage_summary[:tool_calls] || counters[:tool_calls]
1303
1388
  end
@@ -1,4 +1,4 @@
1
1
  module Harnex
2
- VERSION = "0.7.10"
2
+ VERSION = "0.7.11"
3
3
  RELEASE_DATE = "2026-07-10"
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: harnex
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.10
4
+ version: 0.7.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jikku Jose