harnex 0.7.5 → 0.7.6
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 +18 -0
- data/README.md +48 -13
- data/guides/01_dispatch.md +2 -1
- data/guides/03_buddy.md +2 -2
- data/guides/04_monitoring.md +18 -12
- data/lib/harnex/commands/status.rb +12 -1
- data/lib/harnex/commands/wait.rb +173 -20
- data/lib/harnex/core.rb +30 -0
- data/lib/harnex/runtime/session.rb +12 -0
- data/lib/harnex/terminal_status.rb +17 -4
- data/lib/harnex/version.rb +2 -2
- data/recipes/01_fire_and_watch.md +5 -3
- data/recipes/03_buddy.md +3 -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: f29114f723ccfc61ade344c784cb6a11144c25e79392bf136f5b722473b47f0b
|
|
4
|
+
data.tar.gz: 810686487788509887bb8bfb5f9a4c45a8a1b8bca645530b1bf178f1435cee40
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 90c68832ab9218716fd2e6181f006a12b996a23c28b120a769411618aee35e507c38df43bcf9293a98fb3902923e70eb1be9e08a90795e9ec970cb5b51ca2b54
|
|
7
|
+
data.tar.gz: 64ebab38e3cf716bfe69fd7f8d1b28c0d114ef72e81f8028cb87f0b83f6a75cb555e1036a7831a41b3dddd156c0b9f71e51fa9550271f5be529dcea1fd23c9e1
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.7.6] - 2026-06-09 | 12:59 AM | IST
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `harnex status --json` now exposes work-level completion fields (`done`,
|
|
10
|
+
`work_state`, and `process_state`) so monitors can distinguish completed
|
|
11
|
+
work from a still-live interactive process.
|
|
12
|
+
- `harnex wait --until done` waits for `task_complete` or terminal exit,
|
|
13
|
+
whichever arrives first, giving queue monitors a safe default fence.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Refreshed README quick-start and monitoring guidance to emphasize
|
|
18
|
+
`--context --auto-stop`, `--until task_complete` for interactive structured
|
|
19
|
+
sessions, durable terminal summaries, and timeout/artifact verification.
|
|
20
|
+
- Monitoring, buddy, and recipe examples now gate unattended work on
|
|
21
|
+
`--until done` / `done` / `work_state` instead of `state=completed` alone.
|
|
22
|
+
|
|
5
23
|
## [0.7.5] - 2026-05-26 | 05:18 PM | IST
|
|
6
24
|
|
|
7
25
|
### Added
|
data/README.md
CHANGED
|
@@ -33,16 +33,24 @@ or project-local docs are required.
|
|
|
33
33
|
## What it does
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
|
-
# Start
|
|
37
|
-
harnex run pi --id planner --tmux planner
|
|
36
|
+
# Start a visible one-shot worker, give it a task, and stop on completion
|
|
37
|
+
harnex run pi --id planner --tmux planner \
|
|
38
|
+
--context "Write a plan to /tmp/plan.md" --auto-stop
|
|
38
39
|
|
|
39
|
-
#
|
|
40
|
-
harnex
|
|
40
|
+
# Wait for the work-level completion signal and terminal telemetry
|
|
41
|
+
harnex wait --id planner --until done --timeout 900
|
|
41
42
|
|
|
42
|
-
#
|
|
43
|
-
harnex
|
|
43
|
+
# Inspect the final state or the recent dispatch history
|
|
44
|
+
harnex status --id planner --json
|
|
45
|
+
harnex history --limit 5
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
For a long-lived interactive session, keep the worker open and send work to it:
|
|
44
49
|
|
|
45
|
-
|
|
50
|
+
```bash
|
|
51
|
+
harnex run pi --id planner --tmux planner
|
|
52
|
+
harnex send --id planner --message "Write a plan to /tmp/plan.md" --wait-for-idle
|
|
53
|
+
harnex pane --id planner --lines 30
|
|
46
54
|
harnex stop --id planner
|
|
47
55
|
```
|
|
48
56
|
|
|
@@ -59,8 +67,9 @@ job, watch it work, stop it when done.
|
|
|
59
67
|
tmux-backed terminal, `harnex logs` tails the persisted transcript, and
|
|
60
68
|
`harnex events` streams structured JSONL lifecycle events.
|
|
61
69
|
|
|
62
|
-
- **You don't want to babysit.**
|
|
63
|
-
|
|
70
|
+
- **You don't want to babysit.** Use `--context --auto-stop` for
|
|
71
|
+
one-shot work, `--watch` for bounded stall recovery, or
|
|
72
|
+
`--wait-for-idle` as a send fence.
|
|
64
73
|
|
|
65
74
|
- **You want local-only orchestration.** Everything runs on your
|
|
66
75
|
machine. No cloud services, no API keys beyond what the agents need.
|
|
@@ -169,6 +178,30 @@ bare `--watch` means babysitter mode.
|
|
|
169
178
|
For one-shot startup prompts, add `--auto-stop`. It requires `--context`
|
|
170
179
|
and stops the session after the first task completion or PTY prompt return.
|
|
171
180
|
|
|
181
|
+
## Completion and waiting
|
|
182
|
+
|
|
183
|
+
Choose the wait predicate that matches how you launched the worker:
|
|
184
|
+
|
|
185
|
+
- `harnex wait --id ID --until done --timeout SECS` is the safest unattended
|
|
186
|
+
work fence. It returns when Harnex sees `task_complete` or a terminal exit,
|
|
187
|
+
whichever comes first.
|
|
188
|
+
- `harnex wait --id ID` waits for the wrapped process to exit. This is right
|
|
189
|
+
for already-exited sessions and terminal-summary recovery, but interactive
|
|
190
|
+
agents can stay open after finishing a turn.
|
|
191
|
+
- For structured Pi RPC and Codex app-server sessions, use
|
|
192
|
+
`harnex wait --id ID --until task_complete --timeout SECS` when you need the
|
|
193
|
+
exact turn-completion event instead of terminal-exit fallback.
|
|
194
|
+
- `harnex send --wait-for-idle` is an atomic send fence for PTY-style
|
|
195
|
+
interactions. It proves the turn returned to an idle/prompt state, not that
|
|
196
|
+
your acceptance criteria passed.
|
|
197
|
+
- `harnex status --id ID --json` can report `running`, `completed`, `failed`,
|
|
198
|
+
or `unknown` from durable terminal summary rows even after the live registry
|
|
199
|
+
is gone. Treat `state` as process/session state; use `done` and `work_state`
|
|
200
|
+
as the work-level monitor contract.
|
|
201
|
+
|
|
202
|
+
Always set a timeout for unattended waits and verify the expected artifact,
|
|
203
|
+
tests, or git state after harnex reports completion.
|
|
204
|
+
|
|
172
205
|
For structured subscriptions, stream JSONL events:
|
|
173
206
|
|
|
174
207
|
```bash
|
|
@@ -183,7 +216,9 @@ Schema details and compatibility policy are documented in
|
|
|
183
216
|
Every finished `harnex run` writes dispatch records. In a git repo, the
|
|
184
217
|
default path is `<repo>/.harnex/dispatch.jsonl`; outside a git repo, the
|
|
185
218
|
compact history record falls back to `~/.local/state/harnex/dispatch.jsonl`.
|
|
186
|
-
`harnex history` reads the compact records from that location
|
|
219
|
+
`harnex history` reads the compact records from that location, and
|
|
220
|
+
`harnex status --id ID --json` / `harnex wait` can use the same durable
|
|
221
|
+
terminal summaries when the live session registry is already gone.
|
|
187
222
|
|
|
188
223
|
Use `harnex history` to inspect it:
|
|
189
224
|
|
|
@@ -274,14 +309,14 @@ See [recipes/03_buddy.md](recipes/03_buddy.md) for the full pattern.
|
|
|
274
309
|
| Command | What it does |
|
|
275
310
|
|---------|-------------|
|
|
276
311
|
| `harnex run <cli>` | Start an agent (`--tmux` visible, `--detach` background, `--watch` built-in monitoring) |
|
|
277
|
-
| `harnex send --id <id>` | Send a message (queues if busy, `--wait-for-idle` to block until
|
|
312
|
+
| `harnex send --id <id>` | Send a message (queues if busy, `--wait-for-idle` to block until the turn returns idle) |
|
|
278
313
|
| `harnex stop --id <id>` | Send the agent's native exit sequence |
|
|
279
|
-
| `harnex status` | List running sessions
|
|
314
|
+
| `harnex status` | List running sessions; with `--id ID --json`, terminal summaries can classify completed/failed sessions after exit |
|
|
280
315
|
| `harnex pane --id <id>` | Capture a tmux-backed session's screen (`--follow` for live) |
|
|
281
316
|
| `harnex logs --id <id>` | Read session transcript (`--follow` to tail) |
|
|
282
317
|
| `harnex events --id <id>` | Stream structured session events (`--snapshot` for non-blocking dump) |
|
|
283
318
|
| `harnex history` | List completed dispatches from `.harnex/dispatch.jsonl` |
|
|
284
|
-
| `harnex wait --id <id>` | Block until exit
|
|
319
|
+
| `harnex wait --id <id>` | Block until process exit by default; use `--until done` for unattended work completion or `--until task_complete` for exact structured turn completion |
|
|
285
320
|
| `harnex doctor` | Run adapter dependency preflight checks; add `--sweep` for read-only session drift diagnostics |
|
|
286
321
|
| `harnex guide` | Getting started walkthrough |
|
|
287
322
|
| `harnex agents-guide` | Agent-facing dispatch, chain, buddy, monitoring, and naming guides |
|
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 wait --id w-$i & done
|
|
76
|
+
for i in 1 2 3; do harnex wait --id w-$i --until done & done
|
|
77
77
|
wait
|
|
78
78
|
```
|
|
79
79
|
|
|
@@ -125,6 +125,7 @@ Use the lightest primitive that gives the signal you need:
|
|
|
125
125
|
| Continuous pane view | `harnex pane --id pi-i-NN --follow` |
|
|
126
126
|
| Transcript tail | `harnex logs --id pi-i-NN --lines 80` |
|
|
127
127
|
| Structured events | `harnex events --id pi-i-NN --snapshot` |
|
|
128
|
+
| Work completion fence | `harnex wait --id pi-i-NN --until done` |
|
|
128
129
|
| Native turn completion | `harnex wait --id pi-i-NN --until task_complete` |
|
|
129
130
|
|
|
130
131
|
For unattended policy-only stall recovery, use built-in watch mode:
|
data/guides/03_buddy.md
CHANGED
|
@@ -54,8 +54,8 @@ If the worker appears stuck at a prompt or permission dialog for more than
|
|
|
54
54
|
10 minutes with no progress, nudge it:
|
|
55
55
|
- `harnex send --id pi-i-42 --message "You appear to have stalled. Continue with your current task."`
|
|
56
56
|
|
|
57
|
-
If `harnex status --id pi-i-42 --json` reports `
|
|
58
|
-
- `tmux send-keys -t "$HARNEX_SPAWNER_PANE" "pi-i-42
|
|
57
|
+
If `harnex status --id pi-i-42 --json` reports `done=true` or `work_state=failed`, report back:
|
|
58
|
+
- `tmux send-keys -t "$HARNEX_SPAWNER_PANE" "pi-i-42 work-level state reached. Check harnex status --id pi-i-42 --json." Enter`
|
|
59
59
|
|
|
60
60
|
Do not interfere with active work. Stop yourself after reporting completion.
|
|
61
61
|
```
|
data/guides/04_monitoring.md
CHANGED
|
@@ -17,24 +17,26 @@ 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
|
|
21
|
-
`
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
For unattended monitors, prefer `harnex wait --until done`: it returns on the
|
|
21
|
+
work-level `task_complete` signal or terminal exit, whichever comes first. For
|
|
22
|
+
structured sessions (Pi RPC and Codex app-server), `harnex wait --until
|
|
23
|
+
task_complete` remains the exact turn-level fence. Neither knows your acceptance
|
|
24
|
+
criteria; verify the expected artifact or tests afterward.
|
|
24
25
|
|
|
25
26
|
## Completion Test
|
|
26
27
|
|
|
27
|
-
For unattended work, first gate on harnex
|
|
28
|
+
For unattended work, first gate on harnex work completion, then verify the task
|
|
28
29
|
artifact and repo health:
|
|
29
30
|
|
|
30
31
|
```bash
|
|
31
|
-
harnex wait --id pi-i-NN --timeout 5400 &&
|
|
32
|
+
harnex wait --id pi-i-NN --until done --timeout 5400 &&
|
|
32
33
|
test -f path/to/expected-artifact &&
|
|
33
34
|
test -z "$(git status --short)"
|
|
34
35
|
```
|
|
35
36
|
|
|
36
|
-
`harnex wait` succeeds from durable terminal
|
|
37
|
-
`.harnex/dispatch.jsonl` / exit status), not from
|
|
37
|
+
`harnex wait --until done` succeeds from `task_complete` or durable terminal
|
|
38
|
+
telemetry (`--summary-out` / `.harnex/dispatch.jsonl` / exit status), not from
|
|
39
|
+
tmp done markers.
|
|
38
40
|
|
|
39
41
|
Adjust the artifact path to the task. The point is to avoid declaring done while
|
|
40
42
|
a worker is between edits or between commits.
|
|
@@ -72,6 +74,8 @@ harnex events --id pi-i-NN
|
|
|
72
74
|
For task completion:
|
|
73
75
|
|
|
74
76
|
```bash
|
|
77
|
+
harnex wait --id pi-i-NN --until done --timeout 900
|
|
78
|
+
# Or, when you specifically need the structured turn event:
|
|
75
79
|
harnex wait --id pi-i-NN --until task_complete --timeout 900
|
|
76
80
|
```
|
|
77
81
|
|
|
@@ -92,12 +96,13 @@ while :; do
|
|
|
92
96
|
fi
|
|
93
97
|
|
|
94
98
|
row=$(harnex status --id pi-i-NN --json | ruby -rjson -e 'rows=JSON.parse(STDIN.read); print JSON.generate(rows.first || {})')
|
|
99
|
+
done=$(printf '%s' "$row" | ruby -rjson -e 'print(JSON.parse(STDIN.read)["done"] ? "true" : "false")')
|
|
100
|
+
work_state=$(printf '%s' "$row" | ruby -rjson -e 'print(JSON.parse(STDIN.read)["work_state"].to_s)')
|
|
95
101
|
state=$(printf '%s' "$row" | ruby -rjson -e 'print(JSON.parse(STDIN.read)["state"].to_s)')
|
|
96
102
|
|
|
97
|
-
case "$
|
|
98
|
-
|
|
99
|
-
failed
|
|
100
|
-
running) harnex pane --id pi-i-NN --lines 20 ;;
|
|
103
|
+
case "$done:$work_state" in
|
|
104
|
+
true:*) echo "pi-i-NN work completed"; break ;;
|
|
105
|
+
false:failed) echo "pi-i-NN work failed; process state: $state" >&2; exit 1 ;;
|
|
101
106
|
*) harnex pane --id pi-i-NN --lines 20 ;;
|
|
102
107
|
esac
|
|
103
108
|
|
|
@@ -136,6 +141,7 @@ interpretation.
|
|
|
136
141
|
|
|
137
142
|
## Anti-Patterns
|
|
138
143
|
|
|
144
|
+
- Polling `state=completed` alone and missing live sessions with `task_complete=true`.
|
|
139
145
|
- Polling `state=prompt` alone and calling it done.
|
|
140
146
|
- Blocking orchestrators on `/tmp/*-done.txt` as the only completion signal.
|
|
141
147
|
- Letting an unattended loop run with no wall-clock cap.
|
|
@@ -30,6 +30,8 @@ module Harnex
|
|
|
30
30
|
Use --all when supervising workers launched from sibling worktrees.
|
|
31
31
|
With --id, terminal summaries can report completed/failed/unknown
|
|
32
32
|
even after the live session registry is gone.
|
|
33
|
+
`state` is process/session state; use JSON `done`/`work_state`
|
|
34
|
+
or `harnex wait --until done` for work-level completion.
|
|
33
35
|
A prompt-like state is not a completion signal by itself.
|
|
34
36
|
TEXT
|
|
35
37
|
end
|
|
@@ -100,10 +102,14 @@ module Harnex
|
|
|
100
102
|
end
|
|
101
103
|
|
|
102
104
|
def normalize_live_status(session)
|
|
105
|
+
task_complete = task_complete?(session)
|
|
103
106
|
session.merge(
|
|
104
107
|
"state" => "running",
|
|
108
|
+
"process_state" => "running",
|
|
105
109
|
"terminal" => false,
|
|
106
|
-
"task_complete" =>
|
|
110
|
+
"task_complete" => task_complete,
|
|
111
|
+
"done" => Harnex.work_done_for("running", task_complete: task_complete),
|
|
112
|
+
"work_state" => Harnex.work_state_for("running", task_complete: task_complete),
|
|
107
113
|
"exit" => nil,
|
|
108
114
|
"exit_code" => nil,
|
|
109
115
|
"summary_out" => nil,
|
|
@@ -112,6 +118,11 @@ module Harnex
|
|
|
112
118
|
)
|
|
113
119
|
end
|
|
114
120
|
|
|
121
|
+
def task_complete?(session)
|
|
122
|
+
session["task_complete"] == true || session["task_complete"].to_s == "true" ||
|
|
123
|
+
!session["last_completed_at"].to_s.empty?
|
|
124
|
+
end
|
|
125
|
+
|
|
115
126
|
def load_live_status(session)
|
|
116
127
|
uri = URI("http://#{session.fetch('host')}:#{session.fetch('port')}/status")
|
|
117
128
|
request = Net::HTTP::Get.new(uri)
|
data/lib/harnex/commands/wait.rb
CHANGED
|
@@ -21,6 +21,8 @@ module Harnex
|
|
|
21
21
|
Options:
|
|
22
22
|
--id ID Session ID to wait for (required)
|
|
23
23
|
--until STATE Wait until session reaches STATE. Supported:
|
|
24
|
+
done (work fence — task_complete or
|
|
25
|
+
terminal exit, whichever comes first)
|
|
24
26
|
task_complete (events JSONL — fires on
|
|
25
27
|
turn/completed; adapter-agnostic)
|
|
26
28
|
<other> (agent_state HTTP poll, e.g.
|
|
@@ -31,11 +33,13 @@ module Harnex
|
|
|
31
33
|
-h, --help Show this help
|
|
32
34
|
|
|
33
35
|
Common patterns:
|
|
36
|
+
#{program_name} --id cx-i-42 --until done --timeout 900
|
|
34
37
|
#{program_name} --id cx-i-42 --until task_complete --timeout 900
|
|
35
38
|
#{program_name} --id cx-i-42 --until prompt --timeout 120
|
|
36
39
|
#{program_name} --id cx-i-42
|
|
37
40
|
|
|
38
41
|
Gotchas:
|
|
42
|
+
done is the safest work-level fence for monitors.
|
|
39
43
|
task_complete is an event predicate; prompt/busy are live state polls.
|
|
40
44
|
Prompt state alone does not prove work acceptance. Verify artifacts/tests.
|
|
41
45
|
Exit waits can resolve from terminal summary rows when live registry/
|
|
@@ -65,7 +69,10 @@ module Harnex
|
|
|
65
69
|
raise "--id is required for harnex wait" unless @options[:id]
|
|
66
70
|
|
|
67
71
|
if @options[:until_state]
|
|
68
|
-
|
|
72
|
+
case @options[:until_state]
|
|
73
|
+
when "done"
|
|
74
|
+
wait_until_done
|
|
75
|
+
when *EVENT_PREDICATES
|
|
69
76
|
wait_until_event(@options[:until_state])
|
|
70
77
|
else
|
|
71
78
|
wait_until_state
|
|
@@ -135,7 +142,7 @@ module Harnex
|
|
|
135
142
|
|
|
136
143
|
task_complete_seen = true if event_type(event) == "task_complete"
|
|
137
144
|
if matches?(event, predicate, task_complete_seen)
|
|
138
|
-
return [emit_event_match(event, start_time), f.pos, task_complete_seen]
|
|
145
|
+
return [emit_event_match(event, start_time, predicate), f.pos, task_complete_seen]
|
|
139
146
|
end
|
|
140
147
|
end
|
|
141
148
|
offset = f.pos
|
|
@@ -166,7 +173,7 @@ module Harnex
|
|
|
166
173
|
def matches?(event, predicate, task_complete_seen)
|
|
167
174
|
type = event_type(event)
|
|
168
175
|
case predicate
|
|
169
|
-
when "task_complete"
|
|
176
|
+
when "task_complete", "done"
|
|
170
177
|
type == "task_complete"
|
|
171
178
|
when "prompt"
|
|
172
179
|
type == "task_complete" ||
|
|
@@ -176,18 +183,100 @@ module Harnex
|
|
|
176
183
|
end
|
|
177
184
|
end
|
|
178
185
|
|
|
179
|
-
def emit_event_match(event, start_time)
|
|
186
|
+
def emit_event_match(event, start_time, predicate)
|
|
180
187
|
waited = (Time.now - start_time).round(1)
|
|
181
|
-
|
|
188
|
+
payload = {
|
|
182
189
|
ok: true,
|
|
183
190
|
id: @options[:id],
|
|
184
191
|
event: event_type(event),
|
|
185
192
|
seq: event["seq"],
|
|
186
193
|
waited_seconds: waited
|
|
187
|
-
|
|
194
|
+
}
|
|
195
|
+
if predicate == "done"
|
|
196
|
+
payload.merge!(
|
|
197
|
+
status: "done",
|
|
198
|
+
state: "running",
|
|
199
|
+
process_state: "running",
|
|
200
|
+
terminal: false,
|
|
201
|
+
task_complete: true,
|
|
202
|
+
done: true,
|
|
203
|
+
work_state: "completed"
|
|
204
|
+
)
|
|
205
|
+
end
|
|
206
|
+
puts JSON.generate(payload)
|
|
188
207
|
0
|
|
189
208
|
end
|
|
190
209
|
|
|
210
|
+
def wait_until_done
|
|
211
|
+
repo_root = Harnex.resolve_repo_root(@options[:repo_path])
|
|
212
|
+
events_path = Harnex.events_log_path(repo_root, @options[:id])
|
|
213
|
+
exit_path = Harnex.exit_status_path(repo_root, @options[:id])
|
|
214
|
+
registry = Harnex.read_registry(repo_root, @options[:id])
|
|
215
|
+
start_time = Time.now
|
|
216
|
+
deadline = @options[:timeout] ? start_time + @options[:timeout] : nil
|
|
217
|
+
|
|
218
|
+
offset = 0
|
|
219
|
+
task_complete_seen = false
|
|
220
|
+
final_event_deadline = nil
|
|
221
|
+
|
|
222
|
+
status, offset, task_complete_seen = scan_events(events_path, offset, "done", task_complete_seen, start_time)
|
|
223
|
+
return status if status
|
|
224
|
+
|
|
225
|
+
unless registry
|
|
226
|
+
terminal = done_status(repo_root)
|
|
227
|
+
return emit_done_terminal_status(terminal) if terminal
|
|
228
|
+
return emit_done_exit_status(exit_path, @options[:id]) if File.exist?(exit_path)
|
|
229
|
+
|
|
230
|
+
unless File.exist?(events_path)
|
|
231
|
+
warn("harnex wait: no session found with id #{@options[:id].inspect}")
|
|
232
|
+
puts JSON.generate(ok: false, id: @options[:id], state: "unknown", process_state: "unknown", terminal: false,
|
|
233
|
+
task_complete: false, done: false, work_state: "unknown", status: "unknown")
|
|
234
|
+
return 1
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
target_pid = registry && registry["pid"]
|
|
239
|
+
|
|
240
|
+
loop do
|
|
241
|
+
status, offset, task_complete_seen = scan_events(events_path, offset, "done", task_complete_seen, start_time)
|
|
242
|
+
return status if status
|
|
243
|
+
|
|
244
|
+
unless registry
|
|
245
|
+
terminal = done_status(repo_root)
|
|
246
|
+
return emit_done_terminal_status(terminal) if terminal
|
|
247
|
+
return emit_done_exit_status(exit_path, @options[:id]) if File.exist?(exit_path)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
if deadline && Time.now >= deadline
|
|
251
|
+
waited = (Time.now - start_time).round(1)
|
|
252
|
+
puts JSON.generate(ok: false, id: @options[:id], status: "timeout", waited_seconds: waited,
|
|
253
|
+
done: false, work_state: "running")
|
|
254
|
+
return 124
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
if target_pid && !Harnex.alive_pid?(target_pid)
|
|
258
|
+
final_event_deadline ||= Time.now + FINAL_EVENT_GRACE_SECONDS
|
|
259
|
+
if Time.now >= final_event_deadline
|
|
260
|
+
await_exit_status(exit_path)
|
|
261
|
+
return emit_done_exit_status(exit_path, @options[:id]) if File.exist?(exit_path)
|
|
262
|
+
|
|
263
|
+
terminal = done_status(repo_root)
|
|
264
|
+
return emit_done_terminal_status(terminal) if terminal
|
|
265
|
+
|
|
266
|
+
waited = (Time.now - start_time).round(1)
|
|
267
|
+
puts JSON.generate(ok: false, id: @options[:id], state: "exited", process_state: "exited",
|
|
268
|
+
terminal: true, task_complete: false, done: false, work_state: "unknown",
|
|
269
|
+
waited_seconds: waited)
|
|
270
|
+
return 1
|
|
271
|
+
end
|
|
272
|
+
else
|
|
273
|
+
final_event_deadline = nil
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
sleep EVENT_POLL_INTERVAL
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
191
280
|
def wait_until_state
|
|
192
281
|
repo_root = Harnex.resolve_repo_root(@options[:repo_path])
|
|
193
282
|
target_state = @options[:until_state]
|
|
@@ -244,7 +333,8 @@ module Harnex
|
|
|
244
333
|
return emit_terminal_status(terminal) if terminal
|
|
245
334
|
|
|
246
335
|
warn("harnex wait: no session found with id #{@options[:id].inspect}")
|
|
247
|
-
puts JSON.generate(ok: false, id: @options[:id], state: "unknown",
|
|
336
|
+
puts JSON.generate(ok: false, id: @options[:id], state: "unknown", process_state: "unknown",
|
|
337
|
+
terminal: false, task_complete: false, done: false, work_state: "unknown", status: "unknown")
|
|
248
338
|
return 1
|
|
249
339
|
end
|
|
250
340
|
|
|
@@ -259,7 +349,8 @@ module Harnex
|
|
|
259
349
|
terminal = terminal_status(repo_root)
|
|
260
350
|
return emit_terminal_status(terminal) if terminal
|
|
261
351
|
|
|
262
|
-
puts JSON.generate(ok: false, id: @options[:id], state: "unknown",
|
|
352
|
+
puts JSON.generate(ok: false, id: @options[:id], state: "unknown", process_state: "unknown",
|
|
353
|
+
terminal: false, task_complete: false, done: false, work_state: "unknown", status: "unknown")
|
|
263
354
|
return 1
|
|
264
355
|
end
|
|
265
356
|
|
|
@@ -328,19 +419,60 @@ module Harnex
|
|
|
328
419
|
status
|
|
329
420
|
end
|
|
330
421
|
|
|
422
|
+
def done_status(repo_root)
|
|
423
|
+
status = Harnex::TerminalStatus.resolve(id: @options[:id], repo_root: repo_root)
|
|
424
|
+
return nil unless status
|
|
425
|
+
return nil unless status["done"] || status["terminal"]
|
|
426
|
+
|
|
427
|
+
status
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def emit_done_exit_status(exit_path, id)
|
|
431
|
+
data = JSON.parse(File.read(exit_path))
|
|
432
|
+
exit_code = data["exit_code"]
|
|
433
|
+
task_complete = data["task_complete"] == true || data["task_complete"].to_s == "true"
|
|
434
|
+
exit_success = exit_code.nil? || exit_code.to_i == 0
|
|
435
|
+
state = exit_success ? "completed" : "failed"
|
|
436
|
+
done = task_complete || exit_success
|
|
437
|
+
payload = data.merge(
|
|
438
|
+
"ok" => done,
|
|
439
|
+
"id" => id,
|
|
440
|
+
"state" => state,
|
|
441
|
+
"process_state" => "exited",
|
|
442
|
+
"terminal" => true,
|
|
443
|
+
"task_complete" => task_complete,
|
|
444
|
+
"done" => done,
|
|
445
|
+
"work_state" => Harnex.work_state_for(state, task_complete: task_complete)
|
|
446
|
+
)
|
|
447
|
+
success = done
|
|
448
|
+
puts JSON.generate(payload)
|
|
449
|
+
return 0 if success
|
|
450
|
+
|
|
451
|
+
exit_code.is_a?(Integer) && exit_code.positive? ? exit_code : 1
|
|
452
|
+
rescue JSON::ParserError
|
|
453
|
+
puts JSON.generate(ok: false, id: id, state: "failed", process_state: "exited", terminal: true,
|
|
454
|
+
task_complete: false, done: false, work_state: "failed", status: "invalid_exit_status")
|
|
455
|
+
1
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def emit_done_terminal_status(status)
|
|
459
|
+
payload = terminal_payload(status)
|
|
460
|
+
payload[:ok] = !!payload[:done]
|
|
461
|
+
payload[:status] = payload[:done] ? "done" : status["state"]
|
|
462
|
+
puts JSON.generate(payload)
|
|
463
|
+
|
|
464
|
+
if payload[:ok]
|
|
465
|
+
0
|
|
466
|
+
elsif status["exit_code"].is_a?(Integer) && status["exit_code"] > 0
|
|
467
|
+
status["exit_code"]
|
|
468
|
+
else
|
|
469
|
+
1
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
331
473
|
def emit_terminal_status(status)
|
|
332
|
-
payload =
|
|
333
|
-
|
|
334
|
-
id: status["id"],
|
|
335
|
-
state: status["state"],
|
|
336
|
-
terminal: true,
|
|
337
|
-
task_complete: status["task_complete"],
|
|
338
|
-
exit: status["exit"],
|
|
339
|
-
exit_code: status["exit_code"],
|
|
340
|
-
summary_out: status["summary_out"],
|
|
341
|
-
ended_at: status["ended_at"],
|
|
342
|
-
source: status["source"]
|
|
343
|
-
}
|
|
474
|
+
payload = terminal_payload(status)
|
|
475
|
+
payload[:ok] = status["state"] == "completed"
|
|
344
476
|
puts JSON.generate(payload)
|
|
345
477
|
|
|
346
478
|
if payload[:ok]
|
|
@@ -352,6 +484,27 @@ module Harnex
|
|
|
352
484
|
end
|
|
353
485
|
end
|
|
354
486
|
|
|
487
|
+
def terminal_payload(status)
|
|
488
|
+
task_complete = !!status["task_complete"]
|
|
489
|
+
work_state = status["work_state"] || Harnex.work_state_for(status["state"], task_complete: task_complete)
|
|
490
|
+
done = status.key?("done") ? !!status["done"] : work_state == "completed"
|
|
491
|
+
{
|
|
492
|
+
ok: false,
|
|
493
|
+
id: status["id"],
|
|
494
|
+
state: status["state"],
|
|
495
|
+
process_state: status["process_state"] || Harnex.process_state_for(status["state"], terminal: true),
|
|
496
|
+
terminal: status.key?("terminal") ? !!status["terminal"] : true,
|
|
497
|
+
task_complete: task_complete,
|
|
498
|
+
done: done,
|
|
499
|
+
work_state: work_state,
|
|
500
|
+
exit: status["exit"],
|
|
501
|
+
exit_code: status["exit_code"],
|
|
502
|
+
summary_out: status["summary_out"],
|
|
503
|
+
ended_at: status["ended_at"],
|
|
504
|
+
source: status["source"]
|
|
505
|
+
}
|
|
506
|
+
end
|
|
507
|
+
|
|
355
508
|
def parser
|
|
356
509
|
@parser ||= OptionParser.new do |opts|
|
|
357
510
|
opts.banner = "Usage: harnex wait [options]"
|
data/lib/harnex/core.rb
CHANGED
|
@@ -68,6 +68,36 @@ module Harnex
|
|
|
68
68
|
VERSION
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
+
def work_state_for(session_state, task_complete: false)
|
|
72
|
+
return "completed" if task_complete
|
|
73
|
+
|
|
74
|
+
case session_state.to_s
|
|
75
|
+
when "completed"
|
|
76
|
+
"completed"
|
|
77
|
+
when "failed"
|
|
78
|
+
"failed"
|
|
79
|
+
when "running"
|
|
80
|
+
"running"
|
|
81
|
+
else
|
|
82
|
+
"unknown"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def work_done_for(session_state, task_complete: false)
|
|
87
|
+
work_state_for(session_state, task_complete: task_complete) == "completed"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def process_state_for(session_state, terminal: false)
|
|
91
|
+
return "running" unless terminal
|
|
92
|
+
|
|
93
|
+
case session_state.to_s
|
|
94
|
+
when "completed", "failed"
|
|
95
|
+
"exited"
|
|
96
|
+
else
|
|
97
|
+
"unknown"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
71
101
|
def host_info
|
|
72
102
|
{
|
|
73
103
|
host: Socket.gethostname,
|
|
@@ -221,9 +221,14 @@ module Harnex
|
|
|
221
221
|
end
|
|
222
222
|
|
|
223
223
|
payload[:input_state] = adapter.input_state(screen_snapshot) if include_input_state
|
|
224
|
+
task_complete = !!@last_completed_at
|
|
224
225
|
payload[:agent_state] = @state_machine.to_s
|
|
226
|
+
payload[:process_state] = "running"
|
|
225
227
|
payload[:inbox] = @inbox.stats
|
|
226
228
|
payload[:last_completed_at] = @last_completed_at&.iso8601
|
|
229
|
+
payload[:task_complete] = task_complete
|
|
230
|
+
payload[:done] = Harnex.work_done_for("running", task_complete: task_complete)
|
|
231
|
+
payload[:work_state] = Harnex.work_state_for("running", task_complete: task_complete)
|
|
227
232
|
payload[:model] = summary_model
|
|
228
233
|
payload[:effort] = meta_hash["effort"]
|
|
229
234
|
payload[:auto_disconnects] = @event_counters.snapshot[:disconnections]
|
|
@@ -733,6 +738,8 @@ module Harnex
|
|
|
733
738
|
return unless defined?(@exit_code) && !@exit_code.nil?
|
|
734
739
|
|
|
735
740
|
exit_path = Harnex.exit_status_path(repo_root, id)
|
|
741
|
+
task_complete = !!@last_completed_at
|
|
742
|
+
state = @exit_code.to_i == 0 ? "completed" : "failed"
|
|
736
743
|
payload = {
|
|
737
744
|
ok: true,
|
|
738
745
|
id: id,
|
|
@@ -740,6 +747,11 @@ module Harnex
|
|
|
740
747
|
session_id: session_id,
|
|
741
748
|
repo_root: repo_root,
|
|
742
749
|
exit_code: @exit_code,
|
|
750
|
+
state: state,
|
|
751
|
+
process_state: "exited",
|
|
752
|
+
task_complete: task_complete,
|
|
753
|
+
done: Harnex.work_done_for(state, task_complete: task_complete),
|
|
754
|
+
work_state: Harnex.work_state_for(state, task_complete: task_complete),
|
|
743
755
|
started_at: @started_at.iso8601,
|
|
744
756
|
exited_at: Time.now.iso8601,
|
|
745
757
|
injected_count: @injected_count
|
|
@@ -42,8 +42,11 @@ module Harnex
|
|
|
42
42
|
"id" => Harnex.normalize_id(id),
|
|
43
43
|
"repo_root" => File.expand_path(repo_root.to_s.empty? ? Dir.pwd : repo_root),
|
|
44
44
|
"state" => "unknown",
|
|
45
|
+
"process_state" => "unknown",
|
|
45
46
|
"terminal" => false,
|
|
46
47
|
"task_complete" => false,
|
|
48
|
+
"done" => false,
|
|
49
|
+
"work_state" => "unknown",
|
|
47
50
|
"exit" => nil,
|
|
48
51
|
"exit_code" => nil,
|
|
49
52
|
"summary_out" => nil,
|
|
@@ -127,12 +130,17 @@ module Harnex
|
|
|
127
130
|
meta = record["meta"] || {}
|
|
128
131
|
actual = record["actual"] || {}
|
|
129
132
|
state = classify_summary_state(actual)
|
|
133
|
+
task_complete = !!actual["task_complete"]
|
|
134
|
+
terminal = state != "unknown"
|
|
130
135
|
{
|
|
131
136
|
"id" => meta["id"].to_s,
|
|
132
137
|
"repo_root" => meta["repo"] || fallback_repo_root,
|
|
133
138
|
"state" => state,
|
|
134
|
-
"
|
|
135
|
-
"
|
|
139
|
+
"process_state" => Harnex.process_state_for(state, terminal: terminal),
|
|
140
|
+
"terminal" => terminal,
|
|
141
|
+
"task_complete" => task_complete,
|
|
142
|
+
"done" => Harnex.work_done_for(state, task_complete: task_complete),
|
|
143
|
+
"work_state" => Harnex.work_state_for(state, task_complete: task_complete),
|
|
136
144
|
"exit" => blank_to_nil(actual["exit"]),
|
|
137
145
|
"exit_code" => actual["exit_code"],
|
|
138
146
|
"summary_out" => summary_path,
|
|
@@ -164,12 +172,17 @@ module Harnex
|
|
|
164
172
|
else
|
|
165
173
|
"unknown"
|
|
166
174
|
end
|
|
175
|
+
task_complete = record["terminal_event"].to_s == "task_complete"
|
|
176
|
+
terminal = state != "unknown"
|
|
167
177
|
{
|
|
168
178
|
"id" => record["id"].to_s,
|
|
169
179
|
"repo_root" => fallback_repo_root,
|
|
170
180
|
"state" => state,
|
|
171
|
-
"
|
|
172
|
-
"
|
|
181
|
+
"process_state" => Harnex.process_state_for(state, terminal: terminal),
|
|
182
|
+
"terminal" => terminal,
|
|
183
|
+
"task_complete" => task_complete,
|
|
184
|
+
"done" => Harnex.work_done_for(state, task_complete: task_complete),
|
|
185
|
+
"work_state" => Harnex.work_state_for(state, task_complete: task_complete),
|
|
173
186
|
"exit" => history_exit(status),
|
|
174
187
|
"exit_code" => nil,
|
|
175
188
|
"summary_out" => blank_to_nil(record["summary_out_path"]),
|
data/lib/harnex/version.rb
CHANGED
|
@@ -19,7 +19,8 @@ harnex run codex --id cx-23 --tmux
|
|
|
19
19
|
If the plan file is self-contained, reference it directly:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
harnex send --id cx-23 --message "Implement koder/plans/plan_23.md. Run tests when done."
|
|
22
|
+
harnex send --id cx-23 --message "Implement koder/plans/plan_23.md. Run tests when done."
|
|
23
|
+
harnex wait --id cx-23 --until done --timeout 1200
|
|
23
24
|
```
|
|
24
25
|
|
|
25
26
|
For tasks that need structured output, tell the worker to write a
|
|
@@ -32,12 +33,13 @@ Run the full test suite when done.
|
|
|
32
33
|
Write a short summary to /tmp/impl-23.md.
|
|
33
34
|
EOF
|
|
34
35
|
|
|
35
|
-
harnex send --id cx-23 --message "Read and execute /tmp/task-cx-23.md"
|
|
36
|
+
harnex send --id cx-23 --message "Read and execute /tmp/task-cx-23.md"
|
|
37
|
+
harnex wait --id cx-23 --until done --timeout 1200
|
|
36
38
|
```
|
|
37
39
|
|
|
38
40
|
### 3. Watch until done
|
|
39
41
|
|
|
40
|
-
Use
|
|
42
|
+
Use `harnex wait --until done` as the work-level fence, then read the worker's screen:
|
|
41
43
|
|
|
42
44
|
```bash
|
|
43
45
|
harnex pane --id cx-23 --lines 25
|
data/recipes/03_buddy.md
CHANGED
|
@@ -45,8 +45,9 @@ Your job:
|
|
|
45
45
|
2. If the worker appears stuck at a prompt for more than 10 minutes
|
|
46
46
|
with no progress, nudge it:
|
|
47
47
|
- `harnex send --id worker-42 --message "You appear to have stalled. Continue with your current task."`
|
|
48
|
-
3. If
|
|
49
|
-
|
|
48
|
+
3. If `harnex status --id worker-42 --json` reports `done=true` or
|
|
49
|
+
`work_state=failed`, report back:
|
|
50
|
+
- `tmux send-keys -t "$HARNEX_SPAWNER_PANE" "worker-42 reached a work-level terminal state. Check results." Enter`
|
|
50
51
|
4. Keep watching until the worker finishes or is stopped.
|
|
51
52
|
|
|
52
53
|
Do not interfere with work in progress. Only nudge when clearly stalled.
|
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.6
|
|
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-06-08 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.
|