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 +4 -4
- data/CHANGELOG.md +23 -0
- data/README.md +23 -3
- data/guides/01_dispatch.md +14 -0
- data/lib/harnex/commands/run.rb +52 -8
- data/lib/harnex/core.rb +1 -1
- data/lib/harnex/runtime/session.rb +8 -5
- data/lib/harnex/version.rb +2 -2
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 56d9111b2ab4f12716444ecef721331d9878a4485a4b305ecd4a1ad7546d4b77
|
|
4
|
+
data.tar.gz: 3f727b473455245ba916cabe1202a6abc2d6c5a07f4ae257b04898a296d9149c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
232
|
-
|
|
233
|
-
|
|
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.
|
data/guides/01_dispatch.md
CHANGED
|
@@ -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
|
|
data/lib/harnex/commands/run.rb
CHANGED
|
@@ -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 =
|
|
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",
|
|
194
|
+
system("tmux", "new-window", "-c", tmux_start_cwd, "-n", window_name, "-d", shell_cmd)
|
|
187
195
|
else
|
|
188
|
-
system("tmux", "new-session", "-c",
|
|
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:
|
|
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
|
-
|
|
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
|
|
data/lib/harnex/version.rb
CHANGED
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.
|
|
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-
|
|
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.
|