harnex 0.7.7 → 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 +39 -0
- data/README.md +51 -16
- data/guides/01_dispatch.md +26 -6
- data/guides/03_buddy.md +8 -1
- data/guides/04_monitoring.md +36 -40
- data/lib/harnex/cli.rb +7 -0
- data/lib/harnex/commands/run.rb +52 -8
- data/lib/harnex/commands/watch.rb +204 -0
- 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,45 @@
|
|
|
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
|
+
|
|
28
|
+
## [0.7.8] - 2026-06-13 | 08:45 PM | IST
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- `harnex watch --id <id> --until done` now provides a native work-terminal
|
|
33
|
+
watcher for existing visible/detached sessions. It exits `0` for successful
|
|
34
|
+
work, non-zero for `task_failed` / failed terminal telemetry, `124` for
|
|
35
|
+
wall-clock timeout, and can write optional done/fail marker files for legacy
|
|
36
|
+
queue integrations.
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- Monitoring docs now recommend native `harnex watch` for unattended
|
|
41
|
+
single-dispatch monitoring and reserve `harnex run --watch` for foreground
|
|
42
|
+
launch-and-stall-recovery.
|
|
43
|
+
|
|
5
44
|
## [0.7.7] - 2026-06-12 | 10:48 AM | IST
|
|
6
45
|
|
|
7
46
|
### Fixed
|
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.**
|
|
@@ -68,8 +86,9 @@ job, watch it work, stop it when done.
|
|
|
68
86
|
`harnex events` streams structured JSONL lifecycle events.
|
|
69
87
|
|
|
70
88
|
- **You don't want to babysit.** Use `--context --auto-stop` for
|
|
71
|
-
one-shot work,
|
|
72
|
-
`--wait-for-idle`
|
|
89
|
+
one-shot work, `harnex watch` for existing visible/detached dispatches,
|
|
90
|
+
`run --watch` for bounded foreground stall recovery, or `--wait-for-idle`
|
|
91
|
+
as a send fence.
|
|
73
92
|
|
|
74
93
|
- **You want local-only orchestration.** Everything runs on your
|
|
75
94
|
machine. No cloud services, no API keys beyond what the agents need.
|
|
@@ -150,18 +169,27 @@ harnex agents-guide monitoring
|
|
|
150
169
|
|
|
151
170
|
## Built-in dispatch monitoring
|
|
152
171
|
|
|
153
|
-
For unattended dispatches, use
|
|
172
|
+
For unattended visible/background dispatches, use `harnex watch` instead of
|
|
173
|
+
writing a bash poll loop around `harnex wait`:
|
|
154
174
|
|
|
155
175
|
```bash
|
|
156
|
-
harnex run pi --id pi-impl-42 --
|
|
176
|
+
harnex run pi --id pi-impl-42 --tmux pi-impl-42 \
|
|
157
177
|
--context "Implement koder/plans/42_plan.md. Run tests and commit when done."
|
|
178
|
+
harnex watch --id pi-impl-42 --until done --max-wait 90m \
|
|
179
|
+
--done-marker /tmp/pi-impl-42-done.json \
|
|
180
|
+
--fail-marker /tmp/pi-impl-42-failed.json
|
|
158
181
|
```
|
|
159
182
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
183
|
+
`harnex watch --until done` is the safe work-terminal watcher for existing
|
|
184
|
+
`--tmux` or detached sessions. It exits `0` for `task_complete`/done, non-zero
|
|
185
|
+
for `task_failed` or failed terminal summaries, and `124` for `--max-wait`
|
|
186
|
+
timeouts. It does not keep pane/status polling after a terminal failure signal.
|
|
187
|
+
|
|
188
|
+
`harnex run --watch` is a separate foreground babysitter that checks session
|
|
189
|
+
activity every 60s, force-resumes on stall up to a cap, and exits when the
|
|
190
|
+
target session exits or the resume cap is reached. It is foreground-only; use
|
|
191
|
+
`--tmux` or `--detach` for visible/background sessions, and `run --watch` when
|
|
192
|
+
the current command should launch and monitor one worker.
|
|
165
193
|
|
|
166
194
|
Presets map to stall policy defaults:
|
|
167
195
|
|
|
@@ -180,11 +208,15 @@ and stops the session after the first task completion or PTY prompt return.
|
|
|
180
208
|
|
|
181
209
|
## Completion and waiting
|
|
182
210
|
|
|
183
|
-
Choose the wait predicate that matches how you launched the worker:
|
|
211
|
+
Choose the wait/watch predicate that matches how you launched the worker:
|
|
184
212
|
|
|
185
|
-
- `harnex
|
|
186
|
-
|
|
187
|
-
|
|
213
|
+
- `harnex watch --id ID --until done --max-wait DUR` is the safest unattended
|
|
214
|
+
monitor for an existing visible or detached dispatch. It wraps the work-level
|
|
215
|
+
fence, preserves the timeout/failure distinction, and can write done/fail
|
|
216
|
+
marker files for legacy queue integrations.
|
|
217
|
+
- `harnex wait --id ID --until done --timeout SECS` is the primitive work fence.
|
|
218
|
+
It returns when Harnex sees `task_complete`, `task_failed`, or a terminal
|
|
219
|
+
exit, whichever comes first; failed work returns non-zero.
|
|
188
220
|
- `harnex wait --id ID` waits for the wrapped process to exit. This is right
|
|
189
221
|
for already-exited sessions and terminal-summary recovery, but interactive
|
|
190
222
|
agents can stay open after finishing a turn.
|
|
@@ -214,9 +246,11 @@ Schema details and compatibility policy are documented in
|
|
|
214
246
|
|
|
215
247
|
## Dispatch history
|
|
216
248
|
|
|
217
|
-
Every finished `harnex run` writes dispatch records.
|
|
218
|
-
|
|
219
|
-
|
|
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.
|
|
220
254
|
`harnex history` reads the compact records from that location, and
|
|
221
255
|
`harnex status --id ID --json` / `harnex wait` can use the same durable
|
|
222
256
|
terminal summaries when the live session registry is already gone.
|
|
@@ -313,6 +347,7 @@ See [recipes/03_buddy.md](recipes/03_buddy.md) for the full pattern.
|
|
|
313
347
|
| `harnex send --id <id>` | Send a message (queues if busy, `--wait-for-idle` to block until the turn returns idle) |
|
|
314
348
|
| `harnex stop --id <id>` | Send the agent's native exit sequence |
|
|
315
349
|
| `harnex status` | List running sessions; with `--id ID --json`, terminal summaries can classify completed/failed sessions after exit |
|
|
350
|
+
| `harnex watch --id <id>` | Safely monitor existing visible/detached work until `done`, `task_failed`, or timeout; optional done/fail markers |
|
|
316
351
|
| `harnex pane --id <id>` | Capture a tmux-backed session's screen (`--follow` for live) |
|
|
317
352
|
| `harnex logs --id <id>` | Read session transcript (`--follow` to tail) |
|
|
318
353
|
| `harnex events --id <id>` | Stream structured session events (`--snapshot` for non-blocking dump) |
|
data/guides/01_dispatch.md
CHANGED
|
@@ -73,7 +73,7 @@ for i in 1 2 3; do
|
|
|
73
73
|
harnex run pi --id w-$i --tmux w-$i --detach \
|
|
74
74
|
--context "Read and execute /tmp/task-$i.md" --auto-stop &
|
|
75
75
|
done
|
|
76
|
-
for i in 1 2 3; do harnex
|
|
76
|
+
for i in 1 2 3; do harnex watch --id w-$i --until done --max-wait 90m & done
|
|
77
77
|
wait
|
|
78
78
|
```
|
|
79
79
|
|
|
@@ -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
|
|
|
@@ -125,18 +139,24 @@ Use the lightest primitive that gives the signal you need:
|
|
|
125
139
|
| Continuous pane view | `harnex pane --id pi-i-NN --follow` |
|
|
126
140
|
| Transcript tail | `harnex logs --id pi-i-NN --lines 80` |
|
|
127
141
|
| Structured events | `harnex events --id pi-i-NN --snapshot` |
|
|
128
|
-
|
|
|
142
|
+
| Existing-session work monitor | `harnex watch --id pi-i-NN --until done --max-wait 90m` |
|
|
143
|
+
| Primitive work completion/failure fence | `harnex wait --id pi-i-NN --until done` |
|
|
129
144
|
| Native successful-turn completion | `harnex wait --id pi-i-NN --until task_complete` |
|
|
130
145
|
|
|
131
|
-
For
|
|
146
|
+
For visible `--tmux` or detached dispatches, prefer `harnex watch --id`: it
|
|
147
|
+
returns `0` on done, non-zero on `task_failed`/failed terminal summaries, and
|
|
148
|
+
`124` on `--max-wait` timeout. Use `--done-marker` / `--fail-marker` only as
|
|
149
|
+
compatibility outputs for older queue scripts.
|
|
150
|
+
|
|
151
|
+
For foreground launch-and-stall-recovery, use `harnex run --watch`:
|
|
132
152
|
|
|
133
153
|
```bash
|
|
134
154
|
harnex run pi --id pi-i-NN --watch --preset impl --context "Read /tmp/task-impl-NN.md"
|
|
135
155
|
```
|
|
136
156
|
|
|
137
|
-
|
|
138
|
-
and monitor the worker. Use pane/log/event polling or a buddy when you
|
|
139
|
-
interpretation
|
|
157
|
+
`run --watch` is foreground-blocking. Use it when a single process should
|
|
158
|
+
launch and monitor the worker. Use pane/log/event polling or a buddy when you
|
|
159
|
+
need interpretation across multiple sessions.
|
|
140
160
|
|
|
141
161
|
## Verify And Stop
|
|
142
162
|
|
data/guides/03_buddy.md
CHANGED
|
@@ -4,7 +4,14 @@ A buddy is a second harnex session that watches one or more workers and nudges
|
|
|
4
4
|
them if they stall. Use a buddy when the work is long-running, unattended, or
|
|
5
5
|
needs interpretation that simple stall policy cannot provide.
|
|
6
6
|
|
|
7
|
-
For simple
|
|
7
|
+
For simple work-terminal monitoring of an existing visible/detached session,
|
|
8
|
+
prefer the native watcher:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
harnex watch --id pi-i-NN --until done --max-wait 90m
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
For foreground launch-and-inactivity recovery, use built-in run watch mode:
|
|
8
15
|
|
|
9
16
|
```bash
|
|
10
17
|
harnex run pi --id pi-i-NN --watch --preset impl --context "Read /tmp/task-impl-NN.md"
|
data/guides/04_monitoring.md
CHANGED
|
@@ -17,12 +17,15 @@ Prefer signals in this order:
|
|
|
17
17
|
| `harnex pane` | Live UI interpretation and prompt/error diagnosis |
|
|
18
18
|
| `harnex status` | Session liveness and coarse state |
|
|
19
19
|
|
|
20
|
-
For unattended monitors
|
|
21
|
-
work-level `task_complete` or
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
For unattended monitors on existing visible/detached sessions, prefer
|
|
21
|
+
`harnex watch --until done`: it returns on the work-level `task_complete` or
|
|
22
|
+
`task_failed` signal, or terminal exit, whichever comes first. Successful work
|
|
23
|
+
exits `0`, failed work exits non-zero, and wall-clock caps exit `124`. For
|
|
24
|
+
callers that need the lower-level primitive, `harnex wait --until done` exposes
|
|
25
|
+
the same work fence. For structured sessions (Pi RPC and Codex app-server),
|
|
26
|
+
`harnex wait --until task_complete` remains the exact successful-turn fence.
|
|
27
|
+
None of these know your acceptance criteria; verify the expected artifact or
|
|
28
|
+
tests afterward.
|
|
26
29
|
|
|
27
30
|
## Completion Test
|
|
28
31
|
|
|
@@ -30,15 +33,19 @@ For unattended work, first gate on harnex work completion, then verify the task
|
|
|
30
33
|
artifact and repo health:
|
|
31
34
|
|
|
32
35
|
```bash
|
|
33
|
-
harnex
|
|
36
|
+
harnex watch --id pi-i-NN --until done --max-wait 90m \
|
|
37
|
+
--done-marker /tmp/pi-i-NN-done.json \
|
|
38
|
+
--fail-marker /tmp/pi-i-NN-failed.json &&
|
|
34
39
|
test -f path/to/expected-artifact &&
|
|
35
40
|
test -z "$(git status --short)"
|
|
36
41
|
```
|
|
37
42
|
|
|
38
|
-
`harnex
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
`harnex watch --until done` wraps the `harnex wait --until done` work fence:
|
|
44
|
+
it succeeds from `task_complete` or durable successful terminal telemetry
|
|
45
|
+
(`--summary-out` / `.harnex/dispatch.jsonl` / exit status), returns non-zero for
|
|
46
|
+
`task_failed` / failed terminal telemetry, returns `124` for `--max-wait`, and
|
|
47
|
+
only writes done/fail markers as compatibility outputs after harnex has seen a
|
|
48
|
+
terminal work signal.
|
|
42
49
|
|
|
43
50
|
Adjust the artifact path to the task. The point is to avoid declaring done while
|
|
44
51
|
a worker is between edits or between commits.
|
|
@@ -76,42 +83,29 @@ harnex events --id pi-i-NN
|
|
|
76
83
|
For task completion:
|
|
77
84
|
|
|
78
85
|
```bash
|
|
86
|
+
harnex watch --id pi-i-NN --until done --max-wait 15m
|
|
87
|
+
# Primitive equivalent when a script wants raw wait semantics:
|
|
79
88
|
harnex wait --id pi-i-NN --until done --timeout 900
|
|
80
|
-
# Or, when you specifically need the structured turn event:
|
|
89
|
+
# Or, when you specifically need the structured successful-turn event:
|
|
81
90
|
harnex wait --id pi-i-NN --until task_complete --timeout 900
|
|
82
91
|
```
|
|
83
92
|
|
|
84
93
|
## Background Sweeper
|
|
85
94
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
95
|
+
Avoid custom shell loops that repeatedly call `harnex wait`/`harnex status` and
|
|
96
|
+
then accidentally swallow a failed work result. For a single unattended
|
|
97
|
+
visible/detached dispatch, use the native watcher with a hard wall-clock cap:
|
|
89
98
|
|
|
90
99
|
```bash
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
while :; do
|
|
95
|
-
if test "$(($(date +%s) - start))" -gt "$max_wait"; then
|
|
96
|
-
echo "wall-clock cap hit for pi-i-NN" >&2
|
|
97
|
-
exit 2
|
|
98
|
-
fi
|
|
99
|
-
|
|
100
|
-
row=$(harnex status --id pi-i-NN --json | ruby -rjson -e 'rows=JSON.parse(STDIN.read); print JSON.generate(rows.first || {})')
|
|
101
|
-
done=$(printf '%s' "$row" | ruby -rjson -e 'print(JSON.parse(STDIN.read)["done"] ? "true" : "false")')
|
|
102
|
-
work_state=$(printf '%s' "$row" | ruby -rjson -e 'print(JSON.parse(STDIN.read)["work_state"].to_s)')
|
|
103
|
-
state=$(printf '%s' "$row" | ruby -rjson -e 'print(JSON.parse(STDIN.read)["state"].to_s)')
|
|
104
|
-
|
|
105
|
-
case "$done:$work_state" in
|
|
106
|
-
true:*) echo "pi-i-NN work completed"; break ;;
|
|
107
|
-
false:failed) echo "pi-i-NN work failed; process state: $state" >&2; exit 1 ;;
|
|
108
|
-
*) harnex pane --id pi-i-NN --lines 20 ;;
|
|
109
|
-
esac
|
|
110
|
-
|
|
111
|
-
sleep 60
|
|
112
|
-
done
|
|
100
|
+
harnex watch --id pi-i-NN --until done --max-wait 90m \
|
|
101
|
+
--done-marker /tmp/pi-i-NN-done.json \
|
|
102
|
+
--fail-marker /tmp/pi-i-NN-failed.json
|
|
113
103
|
```
|
|
114
104
|
|
|
105
|
+
If that exits `124`, inspect the pane/logs/events and decide whether to nudge,
|
|
106
|
+
stop, or continue. If it exits any other non-zero code, treat the work as
|
|
107
|
+
failed; do not continue polling the same task as though it were still running.
|
|
108
|
+
|
|
115
109
|
Recommended caps:
|
|
116
110
|
|
|
117
111
|
| Work type | Cap |
|
|
@@ -120,17 +114,18 @@ Recommended caps:
|
|
|
120
114
|
| Medium implementation | 90 minutes |
|
|
121
115
|
| Large unattended phase | 3 hours |
|
|
122
116
|
|
|
123
|
-
## Built-In
|
|
117
|
+
## Built-In Stall Babysitter
|
|
124
118
|
|
|
125
119
|
Use `harnex run --watch` when one foreground process should launch the worker
|
|
126
|
-
and apply bounded stall recovery
|
|
120
|
+
and apply bounded stall recovery. This is different from `harnex watch --id`,
|
|
121
|
+
which watches an existing session's work-terminal state:
|
|
127
122
|
|
|
128
123
|
```bash
|
|
129
124
|
harnex run pi --id pi-i-NN --watch --preset impl \
|
|
130
125
|
--context "Read /tmp/task-impl-NN.md"
|
|
131
126
|
```
|
|
132
127
|
|
|
133
|
-
|
|
128
|
+
`run --watch` exits with:
|
|
134
129
|
|
|
135
130
|
| Code | Meaning |
|
|
136
131
|
| --- | --- |
|
|
@@ -145,6 +140,7 @@ interpretation.
|
|
|
145
140
|
|
|
146
141
|
- Polling `state=completed` alone and missing live sessions with `task_complete=true`.
|
|
147
142
|
- Polling `state=prompt` alone and calling it done.
|
|
143
|
+
- Wrapping `harnex wait` in loops that swallow non-zero `task_failed` results.
|
|
148
144
|
- Blocking orchestrators on `/tmp/*-done.txt` as the only completion signal.
|
|
149
145
|
- Letting an unattended loop run with no wall-clock cap.
|
|
150
146
|
- Reading raw tmux panes instead of `harnex pane`.
|
data/lib/harnex/cli.rb
CHANGED
|
@@ -15,6 +15,8 @@ module Harnex
|
|
|
15
15
|
Sender.new(@argv.drop(1)).run
|
|
16
16
|
when "wait"
|
|
17
17
|
Waiter.new(@argv.drop(1)).run
|
|
18
|
+
when "watch"
|
|
19
|
+
WatchCommand.new(@argv.drop(1)).run
|
|
18
20
|
when "stop"
|
|
19
21
|
Stopper.new(@argv.drop(1)).run
|
|
20
22
|
when "status"
|
|
@@ -59,6 +61,8 @@ module Harnex
|
|
|
59
61
|
Sender.usage
|
|
60
62
|
when "wait"
|
|
61
63
|
Waiter.usage
|
|
64
|
+
when "watch"
|
|
65
|
+
WatchCommand.usage
|
|
62
66
|
when "stop"
|
|
63
67
|
Stopper.usage
|
|
64
68
|
when "status"
|
|
@@ -90,6 +94,7 @@ module Harnex
|
|
|
90
94
|
harnex run <cli> [options] [--] [cli-args...]
|
|
91
95
|
harnex send --id ID [options] [text...]
|
|
92
96
|
harnex wait --id ID [options]
|
|
97
|
+
harnex watch --id ID [options]
|
|
93
98
|
harnex stop --id ID [options]
|
|
94
99
|
harnex status [options]
|
|
95
100
|
harnex logs --id ID [options]
|
|
@@ -104,6 +109,7 @@ module Harnex
|
|
|
104
109
|
run Start a wrapped interactive session and local API
|
|
105
110
|
send Send text to an active session
|
|
106
111
|
wait Block until a session exits or reaches a state
|
|
112
|
+
watch Safely watch existing work until done/task_failed/timeout
|
|
107
113
|
stop Send the adapter stop sequence to a session
|
|
108
114
|
status List live sessions
|
|
109
115
|
logs Read session output transcripts
|
|
@@ -129,6 +135,7 @@ module Harnex
|
|
|
129
135
|
harnex run aider --id blue-cat
|
|
130
136
|
harnex run codex -- --cd /path/to/repo
|
|
131
137
|
harnex status
|
|
138
|
+
harnex watch --id main --until done --max-wait 15m
|
|
132
139
|
harnex logs --id main --follow
|
|
133
140
|
harnex events --id main --snapshot
|
|
134
141
|
harnex history --limit 20
|
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)
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
require "fileutils"
|
|
1
2
|
require "json"
|
|
2
3
|
require "net/http"
|
|
4
|
+
require "optparse"
|
|
5
|
+
require "stringio"
|
|
3
6
|
require "uri"
|
|
4
7
|
|
|
5
8
|
module Harnex
|
|
@@ -206,4 +209,205 @@ module Harnex
|
|
|
206
209
|
@monotonic_clock.call
|
|
207
210
|
end
|
|
208
211
|
end
|
|
212
|
+
|
|
213
|
+
class TerminalWatcher
|
|
214
|
+
TIMEOUT_EXIT_CODE = 124
|
|
215
|
+
|
|
216
|
+
def initialize(
|
|
217
|
+
id:,
|
|
218
|
+
repo_path: Dir.pwd,
|
|
219
|
+
until_state: "done",
|
|
220
|
+
max_wait: nil,
|
|
221
|
+
done_marker: nil,
|
|
222
|
+
fail_marker: nil,
|
|
223
|
+
stop_on_terminal: false,
|
|
224
|
+
out: $stdout,
|
|
225
|
+
err: $stderr
|
|
226
|
+
)
|
|
227
|
+
@id = Harnex.normalize_id(id)
|
|
228
|
+
@repo_path = repo_path
|
|
229
|
+
@until_state = until_state.to_s.strip.empty? ? "done" : until_state.to_s
|
|
230
|
+
@max_wait = max_wait
|
|
231
|
+
@done_marker = done_marker
|
|
232
|
+
@fail_marker = fail_marker
|
|
233
|
+
@stop_on_terminal = stop_on_terminal
|
|
234
|
+
@out = out
|
|
235
|
+
@err = err
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def run
|
|
239
|
+
raise "harnex watch: only --until done is supported" unless @until_state == "done"
|
|
240
|
+
|
|
241
|
+
output, warnings, exit_code = capture_wait
|
|
242
|
+
@err.write(warnings) unless warnings.empty?
|
|
243
|
+
@out.write(output) unless output.empty?
|
|
244
|
+
|
|
245
|
+
payload = parse_payload(output)
|
|
246
|
+
outcome = classify(exit_code, payload)
|
|
247
|
+
case outcome
|
|
248
|
+
when :success
|
|
249
|
+
write_marker(@done_marker, payload, outcome: outcome, exit_code: exit_code)
|
|
250
|
+
when :failed
|
|
251
|
+
write_marker(@fail_marker, payload, outcome: outcome, exit_code: exit_code)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
stop_session if @stop_on_terminal && outcome != :timeout
|
|
255
|
+
exit_code
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
private
|
|
259
|
+
|
|
260
|
+
def capture_wait
|
|
261
|
+
argv = ["--id", @id, "--repo", @repo_path, "--until", @until_state]
|
|
262
|
+
argv += ["--timeout", @max_wait.to_s] if @max_wait
|
|
263
|
+
|
|
264
|
+
out_buffer = StringIO.new
|
|
265
|
+
err_buffer = StringIO.new
|
|
266
|
+
original_stdout = $stdout
|
|
267
|
+
original_stderr = $stderr
|
|
268
|
+
$stdout = out_buffer
|
|
269
|
+
$stderr = err_buffer
|
|
270
|
+
exit_code = Waiter.new(argv).run
|
|
271
|
+
[out_buffer.string, err_buffer.string, exit_code]
|
|
272
|
+
ensure
|
|
273
|
+
$stdout = original_stdout
|
|
274
|
+
$stderr = original_stderr
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def parse_payload(output)
|
|
278
|
+
line = output.to_s.lines.reverse.find { |candidate| !candidate.strip.empty? }
|
|
279
|
+
return {} unless line
|
|
280
|
+
|
|
281
|
+
parsed = JSON.parse(line)
|
|
282
|
+
parsed.is_a?(Hash) ? parsed : {}
|
|
283
|
+
rescue JSON::ParserError
|
|
284
|
+
{}
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def classify(exit_code, payload)
|
|
288
|
+
return :timeout if exit_code == TIMEOUT_EXIT_CODE || payload["status"].to_s == "timeout"
|
|
289
|
+
return :success if exit_code.to_i.zero? && (payload.empty? || payload["ok"] != false)
|
|
290
|
+
|
|
291
|
+
:failed
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def write_marker(path, payload, outcome:, exit_code:)
|
|
295
|
+
marker_path = path.to_s.strip
|
|
296
|
+
return if marker_path.empty?
|
|
297
|
+
|
|
298
|
+
expanded_path = File.expand_path(marker_path)
|
|
299
|
+
FileUtils.mkdir_p(File.dirname(expanded_path))
|
|
300
|
+
marker_payload = {
|
|
301
|
+
ok: outcome == :success,
|
|
302
|
+
id: @id,
|
|
303
|
+
outcome: outcome.to_s,
|
|
304
|
+
exit_code: exit_code,
|
|
305
|
+
status: payload["status"],
|
|
306
|
+
work_state: payload["work_state"],
|
|
307
|
+
task_complete: payload["task_complete"] || payload["event"] == "task_complete",
|
|
308
|
+
task_failed: payload["task_failed"] || payload["event"] == "task_failed",
|
|
309
|
+
done: payload["done"],
|
|
310
|
+
terminal: payload["terminal"],
|
|
311
|
+
source: "harnex watch"
|
|
312
|
+
}.compact
|
|
313
|
+
File.write(expanded_path, JSON.generate(marker_payload) + "\n")
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def stop_session
|
|
317
|
+
repo_root = Harnex.resolve_repo_root(@repo_path)
|
|
318
|
+
registry = Harnex.read_registry(repo_root, @id)
|
|
319
|
+
return unless registry
|
|
320
|
+
|
|
321
|
+
uri = URI("http://#{registry.fetch('host')}:#{registry.fetch('port')}/stop")
|
|
322
|
+
request = Net::HTTP::Post.new(uri)
|
|
323
|
+
request["Authorization"] = "Bearer #{registry['token']}" if registry["token"]
|
|
324
|
+
|
|
325
|
+
response = Net::HTTP.start(uri.host, uri.port, open_timeout: 1, read_timeout: 2) do |http|
|
|
326
|
+
http.request(request)
|
|
327
|
+
end
|
|
328
|
+
@err.puts("harnex watch: stop-on-terminal failed with HTTP #{response.code}") unless response.is_a?(Net::HTTPSuccess)
|
|
329
|
+
rescue StandardError => e
|
|
330
|
+
@err.puts("harnex watch: stop-on-terminal failed: #{e.message}")
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
class WatchCommand
|
|
335
|
+
def self.usage(program_name = "harnex watch")
|
|
336
|
+
<<~TEXT
|
|
337
|
+
Usage: #{program_name} --id ID [options]
|
|
338
|
+
|
|
339
|
+
Options:
|
|
340
|
+
--id ID Existing session ID to watch (required)
|
|
341
|
+
--until done Watch work-level terminal state (default: done)
|
|
342
|
+
--repo PATH Resolve session using PATH's repo root (default: current repo)
|
|
343
|
+
--max-wait DUR Wall-clock cap before returning timeout (examples: 900, 15m, 2h)
|
|
344
|
+
--timeout DUR Alias for --max-wait
|
|
345
|
+
--done-marker PATH Write a JSON marker when work completes successfully
|
|
346
|
+
--fail-marker PATH Write a JSON marker when work fails
|
|
347
|
+
--stop-on-terminal Stop the live session after success/failure (not on timeout)
|
|
348
|
+
-h, --help Show this help
|
|
349
|
+
|
|
350
|
+
`harnex watch` is the safe watcher for existing --tmux or detached
|
|
351
|
+
dispatches. It exits 0 for task_complete/done, non-zero for task_failed
|
|
352
|
+
or failed terminal summaries, and 124 for --max-wait timeouts.
|
|
353
|
+
|
|
354
|
+
For launch-and-babysit stall recovery, use `harnex run --watch`.
|
|
355
|
+
TEXT
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def initialize(argv)
|
|
359
|
+
@argv = argv.dup
|
|
360
|
+
@options = {
|
|
361
|
+
id: nil,
|
|
362
|
+
repo_path: Dir.pwd,
|
|
363
|
+
until_state: "done",
|
|
364
|
+
max_wait: nil,
|
|
365
|
+
done_marker: nil,
|
|
366
|
+
fail_marker: nil,
|
|
367
|
+
stop_on_terminal: false,
|
|
368
|
+
help: false
|
|
369
|
+
}
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def run
|
|
373
|
+
parser.parse!(@argv)
|
|
374
|
+
if @options[:help]
|
|
375
|
+
puts self.class.usage
|
|
376
|
+
return 0
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
raise "--id is required for harnex watch" unless @options[:id]
|
|
380
|
+
|
|
381
|
+
TerminalWatcher.new(
|
|
382
|
+
id: @options[:id],
|
|
383
|
+
repo_path: @options[:repo_path],
|
|
384
|
+
until_state: @options[:until_state],
|
|
385
|
+
max_wait: @options[:max_wait],
|
|
386
|
+
done_marker: @options[:done_marker],
|
|
387
|
+
fail_marker: @options[:fail_marker],
|
|
388
|
+
stop_on_terminal: @options[:stop_on_terminal]
|
|
389
|
+
).run
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
private
|
|
393
|
+
|
|
394
|
+
def parser
|
|
395
|
+
@parser ||= OptionParser.new do |opts|
|
|
396
|
+
opts.banner = "Usage: harnex watch --id ID [options]"
|
|
397
|
+
opts.on("--id ID", "Existing session ID to watch") { |value| @options[:id] = Harnex.normalize_id(value) }
|
|
398
|
+
opts.on("--until STATE", "Watch until terminal state") { |value| @options[:until_state] = value }
|
|
399
|
+
opts.on("--repo PATH", "Resolve session using PATH's repo root") { |value| @options[:repo_path] = value }
|
|
400
|
+
opts.on("--max-wait DUR", "Wall-clock cap") do |value|
|
|
401
|
+
@options[:max_wait] = Harnex.parse_duration_seconds(value, option_name: "--max-wait")
|
|
402
|
+
end
|
|
403
|
+
opts.on("--timeout DUR", "Alias for --max-wait") do |value|
|
|
404
|
+
@options[:max_wait] = Harnex.parse_duration_seconds(value, option_name: "--timeout")
|
|
405
|
+
end
|
|
406
|
+
opts.on("--done-marker PATH", "Write marker on successful completion") { |value| @options[:done_marker] = value }
|
|
407
|
+
opts.on("--fail-marker PATH", "Write marker on failed completion") { |value| @options[:fail_marker] = value }
|
|
408
|
+
opts.on("--stop-on-terminal", "Stop live session after success/failure") { @options[:stop_on_terminal] = true }
|
|
409
|
+
opts.on("-h", "--help", "Show help") { @options[:help] = true }
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
end
|
|
209
413
|
end
|
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.
|