harnex 0.7.8 → 0.7.9

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: 64a1ddf83ef070cc2b418c1a70ae3e1fc2c8b5fd671e8fa3e5a30536184728ca
4
- data.tar.gz: 6a076faed04db3eddbaf4bf2fefbee95426cd8ee8008499e3474fbfa1b5c62ed
3
+ metadata.gz: 56d9111b2ab4f12716444ecef721331d9878a4485a4b305ecd4a1ad7546d4b77
4
+ data.tar.gz: 3f727b473455245ba916cabe1202a6abc2d6c5a07f4ae257b04898a296d9149c
5
5
  SHA512:
6
- metadata.gz: 3508afcbddc0e9afaf17372ca98cb3146d5edc128fb54c6217f8b80be7f81baae0ab7f50feba4b513a9ba46165323f8fb95ddd243df5bd19921b198beb89bbc6
7
- data.tar.gz: 25419186825ac14d2f6cd844497d6dc356e88785f23784f30492284b80c384781ab834011f394d27c93078821fc394fed3ac97b8bff818f7b60fc280be83a560
6
+ metadata.gz: a2a87ae57e9ddc34bb85805ea9e07f3523691cf979361f687f4c3c68534c0b9f7c9e276b83d8d28a91a15bc52913ef41e1e36b1993127c97c68c87aa75af5766
7
+ data.tar.gz: 92084ab180df22e16316226adaaa8170b0ab71ceeda1e4de3f097873ed6ecb8e22f8bca7b06b3c78b0a1a0c36d8b18c4b13dcebb7126aecd4523034fead38399
data/CHANGELOG.md CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.7.9] - 2026-07-10 | 10:33 PM | IST
6
+
7
+ ### Added
8
+
9
+ - `harnex run` now accepts wrapper-level `--cwd DIR` and `--root DIR` options.
10
+ `--cwd` starts the wrapped agent in `DIR` and makes that directory the
11
+ harnex session root for registry/session metadata and default dispatch
12
+ summaries. `--root` changes harnex root attribution without changing the
13
+ child process cwd.
14
+
15
+ ### Changed
16
+
17
+ - `harnex run --help`, README, and the dispatch guide now document
18
+ public-bundle / temporary-workdir dispatches with `--cwd`.
19
+ - Refreshed pinned Codex app-server schema fixtures against
20
+ `codex-cli 0.144.1` so the release suite is green with the current local
21
+ Codex CLI.
22
+
23
+ ### Fixed
24
+
25
+ - Non-git temporary work directories no longer leak raw `git rev-parse` fatal
26
+ messages during harnex root probing.
27
+
5
28
  ## [0.7.8] - 2026-06-13 | 08:45 PM | IST
6
29
 
7
30
  ### Added
data/README.md CHANGED
@@ -57,6 +57,24 @@ harnex stop --id planner
57
57
  That's the core loop. Start a fresh agent for each step, hand it one
58
58
  job, watch it work, stop it when done.
59
59
 
60
+ ### Run from a temporary/public bundle
61
+
62
+ Use `--cwd DIR` when the worker should see a specific directory rather than
63
+ the orchestrator's current repo. Harnex starts the wrapped agent in `DIR`, sets
64
+ that directory as the session root, and resolves default telemetry such as
65
+ `.harnex/dispatch.jsonl` there:
66
+
67
+ ```bash
68
+ harnex run codex --cwd /tmp/leximaze_eval_run_001 \
69
+ --id lm-run-001 \
70
+ --context "Read README.md and write RESPONSES.jsonl and OUTPUT.md" \
71
+ --auto-stop
72
+ ```
73
+
74
+ Use `--root DIR` only when you need to override harnex's root attribution
75
+ without changing the child process cwd. `--cwd` is not a security sandbox; it is
76
+ an explicit working-directory/root selector for automation.
77
+
60
78
  ## Why use this
61
79
 
62
80
  - **You want agents to plan, implement, review, and fix — in sequence.**
@@ -228,9 +246,11 @@ Schema details and compatibility policy are documented in
228
246
 
229
247
  ## Dispatch history
230
248
 
231
- Every finished `harnex run` writes dispatch records. In a git repo, the
232
- default path is `<repo>/.harnex/dispatch.jsonl`; outside a git repo, the
233
- compact history record falls back to `~/.local/state/harnex/dispatch.jsonl`.
249
+ Every finished `harnex run` writes dispatch records. By default, the terminal
250
+ summary JSONL path is `<session-root>/.harnex/dispatch.jsonl`; `--cwd DIR` makes
251
+ `DIR` the session root, including for non-git temporary bundles. The compact
252
+ history record is repo-local in a git tree and falls back to
253
+ `~/.local/state/harnex/dispatch.jsonl` outside git.
234
254
  `harnex history` reads the compact records from that location, and
235
255
  `harnex status --id ID --json` / `harnex wait` can use the same durable
236
256
  terminal summaries when the live session registry is already gone.
@@ -81,6 +81,20 @@ Rule: when you use `--tmux`, pass the same name as `--id`. If you pass only
81
81
  `--tmux NAME`, harnex creates a random session ID and the pane name no longer
82
82
  matches `harnex status` or `harnex pane --id`.
83
83
 
84
+ For public-bundle or benchmark runs, use `--cwd DIR` to make harnex launch the
85
+ wrapped agent from that directory and associate session metadata/default
86
+ telemetry with it:
87
+
88
+ ```bash
89
+ harnex run codex --cwd /tmp/leximaze_eval_run_001 \
90
+ --id lm-run-001 \
91
+ --context "Read README.md and write RESPONSES.jsonl and OUTPUT.md" \
92
+ --auto-stop
93
+ ```
94
+
95
+ `--root DIR` only overrides harnex's root attribution; it does not change the
96
+ child process cwd. Neither flag is a sandbox.
97
+
84
98
  Pi runs use structured RPC (`pi --mode rpc`). Pass Pi child flags after `--`
85
99
  (e.g. `harnex run pi --context "..." -- --model anthropic/claude-sonnet-4-5 --thinking high`).
86
100
 
@@ -8,11 +8,11 @@ module Harnex
8
8
  KNOWN_FLAGS = %w[
9
9
  --id --description --detach --tmux --host --port --watch --watch-file
10
10
  --stall-after --max-resumes --preset --context --meta --summary-out
11
- --timeout --inbox-ttl --auto-stop --fast --legacy-pty --help
11
+ --cwd --root --timeout --inbox-ttl --auto-stop --fast --legacy-pty --help
12
12
  ].freeze
13
13
  VALUE_FLAGS = %w[
14
14
  --id --description --host --port --watch --watch-file --stall-after
15
- --max-resumes --preset --context --meta --summary-out --timeout --inbox-ttl
15
+ --max-resumes --preset --context --meta --summary-out --cwd --root --timeout --inbox-ttl
16
16
  ].freeze
17
17
 
18
18
  def self.usage(program_name = "harnex run")
@@ -37,6 +37,8 @@ module Harnex
37
37
  Default Codex runs force service_tier="flex".
38
38
  --meta JSON Attach parsed JSON metadata to the started event
39
39
  --summary-out PATH Append dispatch telemetry summary JSONL to PATH
40
+ --cwd DIR Run the wrapped agent from DIR and use DIR as the session root
41
+ --root DIR Override harnex session/root attribution without changing child cwd
40
42
  --timeout SECS Max seconds to wait for detached registration (default: #{DEFAULT_TIMEOUT})
41
43
  --inbox-ttl SECS Expire queued inbox messages after SECS (default: #{Inbox::DEFAULT_TTL})
42
44
  --legacy-pty (codex only) Use the legacy PTY adapter instead of
@@ -58,6 +60,7 @@ module Harnex
58
60
  #{program_name} pi --id pi-i-42 --tmux pi-i-42 --context "Read /tmp/task-impl-42.md"
59
61
  #{program_name} pi --id pi-i-42 --tmux pi-i-42 --context "Read /tmp/task-impl-42.md" --auto-stop
60
62
  #{program_name} pi --id pi-i-42 --watch --preset impl --context "Read /tmp/task-impl-42.md"
63
+ #{program_name} codex --cwd /tmp/public-bundle --id eval-001 --context "Read README.md and write OUTPUT.md" --auto-stop
61
64
  #{program_name} claude --id cl-r-42 --tmux cl-r-42 --description "Review task 42"
62
65
 
63
66
  Gotchas:
@@ -88,6 +91,8 @@ module Harnex
88
91
  context: nil,
89
92
  meta: nil,
90
93
  summary_out: nil,
94
+ cwd: nil,
95
+ root: nil,
91
96
  auto_stop: false,
92
97
  detach: false,
93
98
  tmux: false,
@@ -110,7 +115,7 @@ module Harnex
110
115
  raise OptionParser::MissingArgument, "cli" if cli_name.nil?
111
116
  validate_auto_stop_context!
112
117
 
113
- repo_root = Harnex.resolve_repo_root(adapter_repo_path(cli_name, child_args))
118
+ repo_root = resolve_run_root(cli_name, child_args)
114
119
  @options[:summary_out] = resolve_summary_out(repo_root)
115
120
  @options[:id] ||= Harnex.generate_id(repo_root)
116
121
  validate_unique_id!(repo_root)
@@ -173,6 +178,8 @@ module Harnex
173
178
  tmux_cmd << "--auto-stop" if @options[:auto_stop]
174
179
  tmux_cmd += ["--meta", JSON.generate(@options[:meta])] if @options[:meta]
175
180
  tmux_cmd += ["--summary-out", @options[:summary_out]] if @options[:summary_out]
181
+ tmux_cmd += ["--cwd", @options[:cwd]] if @options[:cwd]
182
+ tmux_cmd += ["--root", @options[:root]] if @options[:root]
176
183
  tmux_cmd += ["--inbox-ttl", @options[:inbox_ttl].to_s]
177
184
  tmux_cmd << "--fast" if @options[:fast]
178
185
  tmux_cmd += ["--legacy-pty"] if @options[:legacy_pty]
@@ -181,11 +188,12 @@ module Harnex
181
188
  window_name = @options[:tmux_name] || @options[:id]
182
189
  shell_cmd = tmux_cmd.map { |arg| Shellwords.shellescape(arg) }.join(" ")
183
190
 
191
+ tmux_start_cwd = @options[:cwd] || @launch_cwd
184
192
  started =
185
193
  if ENV["TMUX"]
186
- system("tmux", "new-window", "-c", @launch_cwd, "-n", window_name, "-d", shell_cmd)
194
+ system("tmux", "new-window", "-c", tmux_start_cwd, "-n", window_name, "-d", shell_cmd)
187
195
  else
188
- system("tmux", "new-session", "-c", @launch_cwd, "-d", "-s", "harnex", "-n", window_name, shell_cmd)
196
+ system("tmux", "new-session", "-c", tmux_start_cwd, "-d", "-s", "harnex", "-n", window_name, shell_cmd)
189
197
  end
190
198
 
191
199
  raise "tmux failed to start #{cli_name.inspect}" unless started
@@ -281,7 +289,8 @@ module Harnex
281
289
  summary_out: @options[:summary_out],
282
290
  inbox_ttl: @options[:inbox_ttl],
283
291
  auto_stop: @options[:auto_stop],
284
- launch_cwd: @launch_cwd
292
+ launch_cwd: history_cwd,
293
+ child_cwd: session_child_cwd
285
294
  )
286
295
  end
287
296
 
@@ -289,6 +298,24 @@ module Harnex
289
298
  Harnex.build_adapter(cli_name, child_args, legacy_pty: @options[:legacy_pty]).infer_repo_path(child_args)
290
299
  end
291
300
 
301
+ def resolve_run_root(cli_name, child_args)
302
+ return @options[:root] if @options[:root]
303
+ return @options[:cwd] if @options[:cwd]
304
+
305
+ Harnex.resolve_repo_root(adapter_repo_path(cli_name, child_args))
306
+ end
307
+
308
+ def history_cwd
309
+ @options[:root] || @options[:cwd] || @launch_cwd
310
+ end
311
+
312
+ def session_child_cwd
313
+ return @options[:cwd] if @options[:cwd]
314
+ return @launch_cwd if @options[:root]
315
+
316
+ nil
317
+ end
318
+
292
319
  def apply_context(child_args)
293
320
  return child_args unless @options[:context]
294
321
 
@@ -441,6 +468,16 @@ module Harnex
441
468
  @options[:summary_out] = required_option_value(arg, argv[index])
442
469
  when /\A--summary-out=(.+)\z/
443
470
  @options[:summary_out] = required_option_value("--summary-out", Regexp.last_match(1))
471
+ when "--cwd"
472
+ index += 1
473
+ @options[:cwd] = expand_existing_directory(required_option_value(arg, argv[index]), option_name: arg)
474
+ when /\A--cwd=(.+)\z/
475
+ @options[:cwd] = expand_existing_directory(required_option_value("--cwd", Regexp.last_match(1)), option_name: "--cwd")
476
+ when "--root"
477
+ index += 1
478
+ @options[:root] = expand_existing_directory(required_option_value(arg, argv[index]), option_name: arg)
479
+ when /\A--root=(.+)\z/
480
+ @options[:root] = expand_existing_directory(required_option_value("--root", Regexp.last_match(1)), option_name: "--root")
444
481
  when "--timeout"
445
482
  index += 1
446
483
  @options[:timeout] = Float(required_option_value(arg, argv[index]))
@@ -509,7 +546,7 @@ module Harnex
509
546
  nil
510
547
  when *VALUE_FLAGS
511
548
  index += 1
512
- when /\A--(?:id|description|host|port|watch|watch-file|stall-after|max-resumes|context|meta|summary-out|timeout|inbox-ttl)=/
549
+ when /\A--(?:id|description|host|port|watch|watch-file|stall-after|max-resumes|context|meta|summary-out|cwd|root|timeout|inbox-ttl)=/
513
550
  nil
514
551
  when /\A--preset=/
515
552
  nil
@@ -528,7 +565,7 @@ module Harnex
528
565
  arg.start_with?(
529
566
  "--id=", "--description=", "--tmux=", "--host=", "--port=", "--watch=", "--watch-file=",
530
567
  "--stall-after=", "--max-resumes=", "--preset=", "--context=", "--meta=", "--summary-out=",
531
- "--timeout=", "--inbox-ttl="
568
+ "--cwd=", "--root=", "--timeout=", "--inbox-ttl="
532
569
  )
533
570
  end
534
571
 
@@ -585,6 +622,13 @@ module Harnex
585
622
  raise OptionParser::InvalidArgument, "#{option_name} must be an integer"
586
623
  end
587
624
 
625
+ def expand_existing_directory(value, option_name:)
626
+ path = File.expand_path(value.to_s, @launch_cwd)
627
+ return path if File.directory?(path)
628
+
629
+ raise OptionParser::InvalidArgument, "#{option_name} must be an existing directory: #{value}"
630
+ end
631
+
588
632
  def parse_meta(value)
589
633
  parsed = JSON.parse(value)
590
634
  return parsed if parsed.is_a?(Hash)
data/lib/harnex/core.rb CHANGED
@@ -32,7 +32,7 @@ module Harnex
32
32
  ].freeze
33
33
 
34
34
  def resolve_repo_root(path = Dir.pwd)
35
- output, status = Open3.capture2("git", "rev-parse", "--show-toplevel", chdir: path)
35
+ output, status = Open3.capture2("git", "rev-parse", "--show-toplevel", chdir: path, err: File::NULL)
36
36
  status.success? ? output.strip : File.expand_path(path)
37
37
  rescue StandardError
38
38
  File.expand_path(path)
@@ -58,15 +58,16 @@ module Harnex
58
58
  end
59
59
  end
60
60
 
61
- attr_reader :repo_root, :launch_cwd, :host, :port, :session_id, :token, :command, :pid, :id, :adapter, :watch,
61
+ attr_reader :repo_root, :launch_cwd, :child_cwd, :host, :port, :session_id, :token, :command, :pid, :id, :adapter, :watch,
62
62
  :inbox, :description, :meta, :summary_out, :output_log_path, :events_log_path,
63
63
  :started_at, :ended_at, :exit_code, :term_signal
64
64
 
65
- def initialize(adapter:, command:, repo_root:, host:, port: nil, id: DEFAULT_ID, watch: nil, description: nil, meta: nil, summary_out: nil, inbox_ttl: Inbox::DEFAULT_TTL, auto_stop: false, launch_cwd: nil)
65
+ def initialize(adapter:, command:, repo_root:, host:, port: nil, id: DEFAULT_ID, watch: nil, description: nil, meta: nil, summary_out: nil, inbox_ttl: Inbox::DEFAULT_TTL, auto_stop: false, launch_cwd: nil, child_cwd: nil)
66
66
  @adapter = adapter
67
67
  @command = command
68
68
  @repo_root = repo_root
69
69
  @launch_cwd = File.expand_path(launch_cwd.to_s.empty? ? repo_root : launch_cwd)
70
+ @child_cwd = child_cwd.to_s.empty? ? nil : File.expand_path(child_cwd)
70
71
  @host = host
71
72
  @id = Harnex.normalize_id(id)
72
73
  @watch = watch
@@ -153,7 +154,9 @@ module Harnex
153
154
  end
154
155
 
155
156
  def run_pty
156
- @reader, @writer, @pid = PTY.spawn(child_env, *command)
157
+ spawn_args = [child_env, *command]
158
+ spawn_args << { chdir: child_cwd } if child_cwd
159
+ @reader, @writer, @pid = PTY.spawn(*spawn_args)
157
160
  @writer.sync = true
158
161
  arm_auto_stop_after_initial_context
159
162
  emit_started_event
@@ -402,7 +405,7 @@ module Harnex
402
405
  adapter.on_notification { |msg| handle_structured_notification(msg) }
403
406
  adapter.on_disconnect { |err| handle_structured_disconnect(err) }
404
407
 
405
- adapter.start_rpc(env: child_env, cwd: repo_root)
408
+ adapter.start_rpc(env: child_env, cwd: child_cwd || repo_root)
406
409
  @pid = adapter.pid
407
410
  @state_machine.force_prompt!
408
411
  emit_started_event
@@ -821,7 +824,7 @@ module Harnex
821
824
  def registry_payload
822
825
  status_payload(include_input_state: false).merge(
823
826
  token: token,
824
- cwd: Dir.pwd
827
+ cwd: child_cwd || Dir.pwd
825
828
  )
826
829
  end
827
830
 
@@ -1,4 +1,4 @@
1
1
  module Harnex
2
- VERSION = "0.7.8"
3
- RELEASE_DATE = "2026-06-13"
2
+ VERSION = "0.7.9"
3
+ RELEASE_DATE = "2026-07-10"
4
4
  end
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.8
4
+ version: 0.7.9
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-06-13 00:00:00.000000000 Z
11
+ date: 2026-07-10 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.