harnex 0.7.4 → 0.7.5
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 +20 -0
- data/guides/03_buddy.md +2 -2
- data/guides/04_monitoring.md +25 -12
- data/guides/05_naming.md +6 -2
- data/lib/harnex/commands/status.rb +35 -6
- data/lib/harnex/commands/wait.rb +45 -1
- data/lib/harnex/terminal_status.rb +202 -0
- data/lib/harnex/version.rb +2 -2
- data/lib/harnex.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4c6173057b1fe4fa547979d3d9803f345acb4e658dd942aefa096ef886727c33
|
|
4
|
+
data.tar.gz: da1662c9dce791ae644aa4a0ea57d2124a1698593e6db98b2ac737d9f42816f6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e2758cfa10fc9b221235efea20ff21be32b5558c1ab06eb4515cea5485be96e1371d663735c826d9587833caa46aed65486b5c3cedfe8ce3be04e7ac1c328f7f
|
|
7
|
+
data.tar.gz: 765cf4ef020f3a7cfa8bbd1aad517ac1b4ec8901579b3720ea618ec246b20009c07c088d4d86e354ead78194844bba771790df0271d54fccecad6f6058874d9f
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.7.5] - 2026-05-26 | 05:18 PM | IST
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `Harnex::TerminalStatus` now resolves durable terminal dispatch state from
|
|
10
|
+
summary/history rows so commands can classify inactive sessions without
|
|
11
|
+
relying on tmp done markers.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- `harnex status --json --id <id>` now returns a machine-readable row even when
|
|
16
|
+
the live session is gone, with `state` in `running|completed|failed|unknown`
|
|
17
|
+
and terminal metadata (`terminal`, `exit`, `exit_code`, `summary_out`).
|
|
18
|
+
- `harnex wait --id <id>` now falls back to terminal summary/history telemetry
|
|
19
|
+
when registry and exit-status files are missing, returning `completed` on
|
|
20
|
+
summary success and `unknown` when no durable terminal signal exists.
|
|
21
|
+
- Monitoring guides now treat `/tmp/*-done.txt` as legacy compatibility hints;
|
|
22
|
+
canonical completion is `harnex wait` / `harnex status --json` / dispatch
|
|
23
|
+
summary rows.
|
|
24
|
+
|
|
5
25
|
## [0.7.4] - 2026-05-25 | 08:45 AM | IST
|
|
6
26
|
|
|
7
27
|
### Added
|
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
|
|
58
|
-
- `tmux send-keys -t "$HARNEX_SPAWNER_PANE" "pi-i-42
|
|
57
|
+
If `harnex status --id pi-i-42 --json` reports `state=completed` or `state=failed`, report back:
|
|
58
|
+
- `tmux send-keys -t "$HARNEX_SPAWNER_PANE" "pi-i-42 terminal 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
|
@@ -24,16 +24,20 @@ afterward.
|
|
|
24
24
|
|
|
25
25
|
## Completion Test
|
|
26
26
|
|
|
27
|
-
For unattended work,
|
|
27
|
+
For unattended work, first gate on harnex terminal state, then verify the task
|
|
28
|
+
artifact and repo health:
|
|
28
29
|
|
|
29
30
|
```bash
|
|
30
|
-
|
|
31
|
-
test -
|
|
32
|
-
test "$(git
|
|
31
|
+
harnex wait --id pi-i-NN --timeout 5400 &&
|
|
32
|
+
test -f path/to/expected-artifact &&
|
|
33
|
+
test -z "$(git status --short)"
|
|
33
34
|
```
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
`harnex wait` succeeds from durable terminal telemetry (`--summary-out` /
|
|
37
|
+
`.harnex/dispatch.jsonl` / exit status), not from tmp done markers.
|
|
38
|
+
|
|
39
|
+
Adjust the artifact path to the task. The point is to avoid declaring done while
|
|
40
|
+
a worker is between edits or between commits.
|
|
37
41
|
|
|
38
42
|
## Why Pane State Alone Is Not Enough
|
|
39
43
|
|
|
@@ -73,22 +77,30 @@ harnex wait --id pi-i-NN --until task_complete --timeout 900
|
|
|
73
77
|
|
|
74
78
|
## Background Sweeper
|
|
75
79
|
|
|
76
|
-
Consumers often run a small shell loop that checks
|
|
77
|
-
|
|
78
|
-
pipeline cannot wait forever:
|
|
80
|
+
Consumers often run a small shell loop that checks terminal state, then drops
|
|
81
|
+
to pane diagnostics only while work is still running. Keep a hard wall-clock cap
|
|
82
|
+
so an unattended pipeline cannot wait forever:
|
|
79
83
|
|
|
80
84
|
```bash
|
|
81
85
|
start=$(date +%s)
|
|
82
86
|
max_wait=5400
|
|
83
87
|
|
|
84
|
-
|
|
88
|
+
while :; do
|
|
85
89
|
if test "$(($(date +%s) - start))" -gt "$max_wait"; then
|
|
86
90
|
echo "wall-clock cap hit for pi-i-NN" >&2
|
|
87
91
|
exit 2
|
|
88
92
|
fi
|
|
89
93
|
|
|
90
|
-
harnex status --id pi-i-NN --json
|
|
91
|
-
|
|
94
|
+
row=$(harnex status --id pi-i-NN --json | ruby -rjson -e 'rows=JSON.parse(STDIN.read); print JSON.generate(rows.first || {})')
|
|
95
|
+
state=$(printf '%s' "$row" | ruby -rjson -e 'print(JSON.parse(STDIN.read)["state"].to_s)')
|
|
96
|
+
|
|
97
|
+
case "$state" in
|
|
98
|
+
completed) echo "pi-i-NN completed"; break ;;
|
|
99
|
+
failed|unknown) echo "pi-i-NN terminal state: $state" >&2; exit 1 ;;
|
|
100
|
+
running) harnex pane --id pi-i-NN --lines 20 ;;
|
|
101
|
+
*) harnex pane --id pi-i-NN --lines 20 ;;
|
|
102
|
+
esac
|
|
103
|
+
|
|
92
104
|
sleep 60
|
|
93
105
|
done
|
|
94
106
|
```
|
|
@@ -125,6 +137,7 @@ interpretation.
|
|
|
125
137
|
## Anti-Patterns
|
|
126
138
|
|
|
127
139
|
- Polling `state=prompt` alone and calling it done.
|
|
140
|
+
- Blocking orchestrators on `/tmp/*-done.txt` as the only completion signal.
|
|
128
141
|
- Letting an unattended loop run with no wall-clock cap.
|
|
129
142
|
- Reading raw tmux panes instead of `harnex pane`.
|
|
130
143
|
- Using `--wait-for-idle` as acceptance proof.
|
data/guides/05_naming.md
CHANGED
|
@@ -93,9 +93,9 @@ The task file name does not need to duplicate the exact short phase code. It
|
|
|
93
93
|
should be easy to scan in `/tmp` and should include the same task number as the
|
|
94
94
|
session ID.
|
|
95
95
|
|
|
96
|
-
## Done Markers
|
|
96
|
+
## Done Markers (Legacy Compatibility)
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
If a legacy workflow still expects a done marker, derive it from the session ID:
|
|
99
99
|
|
|
100
100
|
```text
|
|
101
101
|
/tmp/pi-p-42-done.txt
|
|
@@ -103,5 +103,9 @@ Derive done markers from the session ID:
|
|
|
103
103
|
/tmp/pi-cr-42-done.txt
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
+
Treat done markers as compatibility hints only. Canonical completion should come
|
|
107
|
+
from harnex terminal telemetry (`harnex wait` / `harnex status --json` / summary
|
|
108
|
+
rows in `.harnex/dispatch.jsonl`).
|
|
109
|
+
|
|
106
110
|
When a brief asks for a completion marker, make it one line and include the
|
|
107
111
|
highest-signal result: tests passed, review clean, or the blocking issue.
|
|
@@ -28,6 +28,8 @@ module Harnex
|
|
|
28
28
|
Gotchas:
|
|
29
29
|
By default, status filters to the current repo root.
|
|
30
30
|
Use --all when supervising workers launched from sibling worktrees.
|
|
31
|
+
With --id, terminal summaries can report completed/failed/unknown
|
|
32
|
+
even after the live session registry is gone.
|
|
31
33
|
A prompt-like state is not a completion signal by itself.
|
|
32
34
|
TEXT
|
|
33
35
|
end
|
|
@@ -83,12 +85,31 @@ module Harnex
|
|
|
83
85
|
end
|
|
84
86
|
|
|
85
87
|
def load_sessions
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
active_repo_root = @options[:all] ? nil : Harnex.resolve_repo_root(@options[:repo_path])
|
|
89
|
+
fallback_repo_root = Harnex.resolve_repo_root(@options[:repo_path])
|
|
90
|
+
sessions = Harnex.active_sessions(active_repo_root, id: @options[:id])
|
|
91
|
+
|
|
92
|
+
live = sessions.map { |session| normalize_live_status(load_live_status(session)) }
|
|
93
|
+
.sort_by { |session| [session["repo_root"].to_s, session["started_at"].to_s, session["id"].to_s] }
|
|
94
|
+
.reverse
|
|
95
|
+
return live unless @options[:id]
|
|
96
|
+
return [live.first] unless live.empty?
|
|
97
|
+
|
|
98
|
+
terminal = Harnex::TerminalStatus.resolve(id: @options[:id], repo_root: fallback_repo_root)
|
|
99
|
+
[terminal || Harnex::TerminalStatus.unknown(id: @options[:id], repo_root: fallback_repo_root)]
|
|
100
|
+
end
|
|
88
101
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
102
|
+
def normalize_live_status(session)
|
|
103
|
+
session.merge(
|
|
104
|
+
"state" => "running",
|
|
105
|
+
"terminal" => false,
|
|
106
|
+
"task_complete" => !session["last_completed_at"].to_s.empty?,
|
|
107
|
+
"exit" => nil,
|
|
108
|
+
"exit_code" => nil,
|
|
109
|
+
"summary_out" => nil,
|
|
110
|
+
"ended_at" => nil,
|
|
111
|
+
"source" => "live"
|
|
112
|
+
)
|
|
92
113
|
end
|
|
93
114
|
|
|
94
115
|
def load_live_status(session)
|
|
@@ -128,7 +149,7 @@ module Harnex
|
|
|
128
149
|
"PORT" => session["port"].to_s,
|
|
129
150
|
"AGE" => timeago(session["started_at"]),
|
|
130
151
|
"IDLE" => format_idle(session["log_idle_s"]),
|
|
131
|
-
"STATE" =>
|
|
152
|
+
"STATE" => table_state(session),
|
|
132
153
|
"DESC" => truncate(session["description"])
|
|
133
154
|
}
|
|
134
155
|
row["REPO"] = truncate_repo(session["repo_root"])
|
|
@@ -139,6 +160,14 @@ module Harnex
|
|
|
139
160
|
columns.map { |column| row.fetch(column).ljust(widths.fetch(column)) }.join(" ")
|
|
140
161
|
end
|
|
141
162
|
|
|
163
|
+
def table_state(session)
|
|
164
|
+
input_state = session.dig("input_state", "state").to_s
|
|
165
|
+
return input_state unless input_state.empty?
|
|
166
|
+
|
|
167
|
+
state = session["state"].to_s
|
|
168
|
+
state.empty? ? "-" : state
|
|
169
|
+
end
|
|
170
|
+
|
|
142
171
|
def timeago(timestamp)
|
|
143
172
|
return "-" if timestamp.to_s.empty?
|
|
144
173
|
|
data/lib/harnex/commands/wait.rb
CHANGED
|
@@ -38,6 +38,8 @@ module Harnex
|
|
|
38
38
|
Gotchas:
|
|
39
39
|
task_complete is an event predicate; prompt/busy are live state polls.
|
|
40
40
|
Prompt state alone does not prove work acceptance. Verify artifacts/tests.
|
|
41
|
+
Exit waits can resolve from terminal summary rows when live registry/
|
|
42
|
+
exit-status files are already gone.
|
|
41
43
|
Without --timeout, wait can block indefinitely.
|
|
42
44
|
TEXT
|
|
43
45
|
end
|
|
@@ -238,7 +240,11 @@ module Harnex
|
|
|
238
240
|
unless registry
|
|
239
241
|
return read_exit_status(exit_path, @options[:id]) if File.exist?(exit_path)
|
|
240
242
|
|
|
243
|
+
terminal = terminal_status(repo_root)
|
|
244
|
+
return emit_terminal_status(terminal) if terminal
|
|
245
|
+
|
|
241
246
|
warn("harnex wait: no session found with id #{@options[:id].inspect}")
|
|
247
|
+
puts JSON.generate(ok: false, id: @options[:id], state: "unknown", terminal: false, status: "unknown")
|
|
242
248
|
return 1
|
|
243
249
|
end
|
|
244
250
|
|
|
@@ -248,7 +254,13 @@ module Harnex
|
|
|
248
254
|
loop do
|
|
249
255
|
unless Harnex.alive_pid?(target_pid)
|
|
250
256
|
await_exit_status(exit_path)
|
|
251
|
-
return read_exit_status(exit_path, @options[:id])
|
|
257
|
+
return read_exit_status(exit_path, @options[:id]) if File.exist?(exit_path)
|
|
258
|
+
|
|
259
|
+
terminal = terminal_status(repo_root)
|
|
260
|
+
return emit_terminal_status(terminal) if terminal
|
|
261
|
+
|
|
262
|
+
puts JSON.generate(ok: false, id: @options[:id], state: "unknown", terminal: false, status: "unknown")
|
|
263
|
+
return 1
|
|
252
264
|
end
|
|
253
265
|
|
|
254
266
|
if deadline && Time.now >= deadline
|
|
@@ -308,6 +320,38 @@ module Harnex
|
|
|
308
320
|
end
|
|
309
321
|
end
|
|
310
322
|
|
|
323
|
+
def terminal_status(repo_root)
|
|
324
|
+
status = Harnex::TerminalStatus.resolve(id: @options[:id], repo_root: repo_root)
|
|
325
|
+
return nil unless status
|
|
326
|
+
return nil unless status["terminal"]
|
|
327
|
+
|
|
328
|
+
status
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def emit_terminal_status(status)
|
|
332
|
+
payload = {
|
|
333
|
+
ok: status["state"] == "completed",
|
|
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
|
+
}
|
|
344
|
+
puts JSON.generate(payload)
|
|
345
|
+
|
|
346
|
+
if payload[:ok]
|
|
347
|
+
0
|
|
348
|
+
elsif status["exit_code"].is_a?(Integer) && status["exit_code"] > 0
|
|
349
|
+
status["exit_code"]
|
|
350
|
+
else
|
|
351
|
+
1
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
311
355
|
def parser
|
|
312
356
|
@parser ||= OptionParser.new do |opts|
|
|
313
357
|
opts.banner = "Usage: harnex wait [options]"
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "time"
|
|
3
|
+
|
|
4
|
+
module Harnex
|
|
5
|
+
module TerminalStatus
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def resolve(id:, repo_root: Dir.pwd)
|
|
9
|
+
normalized_id = Harnex.normalize_id(id)
|
|
10
|
+
root = File.expand_path(repo_root.to_s.empty? ? Dir.pwd : repo_root)
|
|
11
|
+
|
|
12
|
+
latest_summary = nil
|
|
13
|
+
latest_summary_path = nil
|
|
14
|
+
latest_history = nil
|
|
15
|
+
|
|
16
|
+
history_paths(root).each do |path|
|
|
17
|
+
summary, history = scan_dispatch_path(path, normalized_id)
|
|
18
|
+
if newer_summary?(summary, latest_summary)
|
|
19
|
+
latest_summary = summary
|
|
20
|
+
latest_summary_path = path
|
|
21
|
+
end
|
|
22
|
+
latest_history = history if newer_history?(history, latest_history)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
summary_path = latest_history && latest_history["summary_out_path"].to_s.strip
|
|
26
|
+
if summary_path && !summary_path.empty? && File.file?(summary_path)
|
|
27
|
+
summary, = scan_dispatch_path(summary_path, normalized_id)
|
|
28
|
+
if newer_summary?(summary, latest_summary)
|
|
29
|
+
latest_summary = summary
|
|
30
|
+
latest_summary_path = summary_path
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
return build_from_summary(latest_summary, latest_summary_path, root) if latest_summary
|
|
35
|
+
return build_from_history(latest_history, root) if latest_history
|
|
36
|
+
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def unknown(id:, repo_root: Dir.pwd)
|
|
41
|
+
{
|
|
42
|
+
"id" => Harnex.normalize_id(id),
|
|
43
|
+
"repo_root" => File.expand_path(repo_root.to_s.empty? ? Dir.pwd : repo_root),
|
|
44
|
+
"state" => "unknown",
|
|
45
|
+
"terminal" => false,
|
|
46
|
+
"task_complete" => false,
|
|
47
|
+
"exit" => nil,
|
|
48
|
+
"exit_code" => nil,
|
|
49
|
+
"summary_out" => nil,
|
|
50
|
+
"started_at" => nil,
|
|
51
|
+
"ended_at" => nil,
|
|
52
|
+
"source" => "none"
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def history_paths(repo_root)
|
|
57
|
+
local_path = DispatchHistory.path_for(repo_root)
|
|
58
|
+
return [local_path] if File.file?(local_path)
|
|
59
|
+
|
|
60
|
+
global_path = DispatchHistory.global_path
|
|
61
|
+
return [global_path] if File.file?(global_path)
|
|
62
|
+
|
|
63
|
+
[]
|
|
64
|
+
rescue StandardError
|
|
65
|
+
[]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def scan_dispatch_path(path, id)
|
|
69
|
+
summary_record = nil
|
|
70
|
+
history_record = nil
|
|
71
|
+
|
|
72
|
+
File.foreach(path) do |line|
|
|
73
|
+
record = JSON.parse(line)
|
|
74
|
+
next unless record.is_a?(Hash)
|
|
75
|
+
|
|
76
|
+
if summary_record?(record) && record.dig("meta", "id").to_s == id
|
|
77
|
+
summary_record = record
|
|
78
|
+
elsif history_record?(record) && record["id"].to_s == id
|
|
79
|
+
history_record = record
|
|
80
|
+
end
|
|
81
|
+
rescue JSON::ParserError
|
|
82
|
+
next
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
[summary_record, history_record]
|
|
86
|
+
rescue Errno::ENOENT
|
|
87
|
+
[nil, nil]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def summary_record?(record)
|
|
91
|
+
record["meta"].is_a?(Hash) && record["actual"].is_a?(Hash)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def history_record?(record)
|
|
95
|
+
record["schema_version"] == 1 && record.key?("status")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def newer_summary?(candidate, current)
|
|
99
|
+
return false unless candidate
|
|
100
|
+
return true unless current
|
|
101
|
+
|
|
102
|
+
summary_time(candidate) >= summary_time(current)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def newer_history?(candidate, current)
|
|
106
|
+
return false unless candidate
|
|
107
|
+
return true unless current
|
|
108
|
+
|
|
109
|
+
history_time(candidate) >= history_time(current)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def summary_time(record)
|
|
113
|
+
parse_time(record.dig("meta", "ended_at")) || parse_time(record.dig("meta", "started_at")) || Time.at(0)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def history_time(record)
|
|
117
|
+
parse_time(record["ended_at"]) || parse_time(record["started_at"]) || Time.at(0)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def parse_time(value)
|
|
121
|
+
Time.iso8601(value.to_s)
|
|
122
|
+
rescue ArgumentError
|
|
123
|
+
nil
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def build_from_summary(record, summary_path, fallback_repo_root)
|
|
127
|
+
meta = record["meta"] || {}
|
|
128
|
+
actual = record["actual"] || {}
|
|
129
|
+
state = classify_summary_state(actual)
|
|
130
|
+
{
|
|
131
|
+
"id" => meta["id"].to_s,
|
|
132
|
+
"repo_root" => meta["repo"] || fallback_repo_root,
|
|
133
|
+
"state" => state,
|
|
134
|
+
"terminal" => state != "unknown",
|
|
135
|
+
"task_complete" => !!actual["task_complete"],
|
|
136
|
+
"exit" => blank_to_nil(actual["exit"]),
|
|
137
|
+
"exit_code" => actual["exit_code"],
|
|
138
|
+
"summary_out" => summary_path,
|
|
139
|
+
"started_at" => meta["started_at"],
|
|
140
|
+
"ended_at" => meta["ended_at"],
|
|
141
|
+
"source" => "summary_out"
|
|
142
|
+
}
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def classify_summary_state(actual)
|
|
146
|
+
exit = actual["exit"].to_s
|
|
147
|
+
exit_code = actual["exit_code"]
|
|
148
|
+
|
|
149
|
+
return "completed" if exit == "success"
|
|
150
|
+
return "completed" if exit.empty? && exit_code == 0
|
|
151
|
+
return "failed" unless exit.empty? && exit_code.nil?
|
|
152
|
+
|
|
153
|
+
"unknown"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def build_from_history(record, fallback_repo_root)
|
|
157
|
+
status = record["status"].to_s
|
|
158
|
+
state =
|
|
159
|
+
case status
|
|
160
|
+
when "completed"
|
|
161
|
+
"completed"
|
|
162
|
+
when "failed", "timeout", "killed"
|
|
163
|
+
"failed"
|
|
164
|
+
else
|
|
165
|
+
"unknown"
|
|
166
|
+
end
|
|
167
|
+
{
|
|
168
|
+
"id" => record["id"].to_s,
|
|
169
|
+
"repo_root" => fallback_repo_root,
|
|
170
|
+
"state" => state,
|
|
171
|
+
"terminal" => state != "unknown",
|
|
172
|
+
"task_complete" => record["terminal_event"].to_s == "task_complete",
|
|
173
|
+
"exit" => history_exit(status),
|
|
174
|
+
"exit_code" => nil,
|
|
175
|
+
"summary_out" => blank_to_nil(record["summary_out_path"]),
|
|
176
|
+
"started_at" => record["started_at"],
|
|
177
|
+
"ended_at" => record["ended_at"],
|
|
178
|
+
"source" => "dispatch_history"
|
|
179
|
+
}
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def history_exit(status)
|
|
183
|
+
case status
|
|
184
|
+
when "completed"
|
|
185
|
+
"success"
|
|
186
|
+
when "timeout"
|
|
187
|
+
"timeout"
|
|
188
|
+
when "killed"
|
|
189
|
+
"killed"
|
|
190
|
+
when "failed"
|
|
191
|
+
"failure"
|
|
192
|
+
else
|
|
193
|
+
nil
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def blank_to_nil(value)
|
|
198
|
+
text = value.to_s
|
|
199
|
+
text.empty? ? nil : text
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
data/lib/harnex/version.rb
CHANGED
data/lib/harnex.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "open3"
|
|
|
5
5
|
require_relative "harnex/version"
|
|
6
6
|
require_relative "harnex/core"
|
|
7
7
|
require_relative "harnex/dispatch_history"
|
|
8
|
+
require_relative "harnex/terminal_status"
|
|
8
9
|
require_relative "harnex/watcher"
|
|
9
10
|
require_relative "harnex/adapters"
|
|
10
11
|
require_relative "harnex/runtime/session_state"
|
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.5
|
|
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-05-
|
|
11
|
+
date: 2026-05-26 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.
|
|
@@ -65,6 +65,7 @@ files:
|
|
|
65
65
|
- lib/harnex/runtime/message.rb
|
|
66
66
|
- lib/harnex/runtime/session.rb
|
|
67
67
|
- lib/harnex/runtime/session_state.rb
|
|
68
|
+
- lib/harnex/terminal_status.rb
|
|
68
69
|
- lib/harnex/version.rb
|
|
69
70
|
- lib/harnex/watcher.rb
|
|
70
71
|
- lib/harnex/watcher/inotify.rb
|