harnex 0.12.0 → 0.29.0
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 +592 -0
- data/GUIDE.md +222 -68
- data/README.md +78 -48
- data/TECHNICAL.md +235 -98
- data/agents/gpt.md +60 -0
- data/docs/agents.md +119 -0
- data/docs/dispatch-telemetry.md +184 -50
- data/docs/events.md +20 -7
- data/docs/lanes.md +202 -0
- data/docs/mcp.md +102 -0
- data/docs/pi-rpc.md +81 -7
- data/docs/upgrading-to-0.29.md +87 -0
- data/guides/01_dispatch.md +101 -34
- data/guides/04_monitoring.md +223 -25
- data/guides/05_naming.md +0 -1
- data/guides/06_claude.md +96 -0
- data/lib/harnex/adapters/base.rb +4 -8
- data/lib/harnex/adapters/pi.rb +92 -12
- data/lib/harnex/adapters.rb +1 -16
- data/lib/harnex/artifact_report.rb +206 -12
- data/lib/harnex/cli.rb +68 -50
- data/lib/harnex/commands/ask.rb +278 -0
- data/lib/harnex/commands/delegate.rb +202 -0
- data/lib/harnex/commands/doctor.rb +34 -36
- data/lib/harnex/commands/events.rb +3 -3
- data/lib/harnex/commands/history.rb +2 -8
- data/lib/harnex/commands/land.rb +150 -0
- data/lib/harnex/commands/lanes.rb +84 -0
- data/lib/harnex/commands/logs.rb +3 -3
- data/lib/harnex/commands/mcp.rb +208 -0
- data/lib/harnex/commands/orchestration.rb +88 -9
- data/lib/harnex/commands/pane.rb +3 -3
- data/lib/harnex/commands/preflight.rb +566 -0
- data/lib/harnex/commands/reply.rb +125 -0
- data/lib/harnex/commands/result.rb +108 -0
- data/lib/harnex/commands/run.rb +248 -64
- data/lib/harnex/commands/send.rb +121 -16
- data/lib/harnex/commands/status.rb +32 -4
- data/lib/harnex/commands/stop.rb +14 -4
- data/lib/harnex/commands/telemetry.rb +19 -1
- data/lib/harnex/commands/wait.rb +421 -56
- data/lib/harnex/commands/watch.rb +134 -21
- data/lib/harnex/context_file.rb +85 -0
- data/lib/harnex/core.rb +100 -15
- data/lib/harnex/dispatch_history.rb +157 -10
- data/lib/harnex/lanes.rb +513 -0
- data/lib/harnex/mcp.rb +362 -0
- data/lib/harnex/orchestration.rb +128 -5
- data/lib/harnex/owned_process_group.rb +50 -0
- data/lib/harnex/owner_stream.rb +399 -0
- data/lib/harnex/pricing.rb +4 -9
- data/lib/harnex/runtime/activity_tracker.rb +107 -0
- data/lib/harnex/runtime/api_server.rb +11 -1
- data/lib/harnex/runtime/completion_notifier.rb +90 -11
- data/lib/harnex/runtime/inbox.rb +26 -7
- data/lib/harnex/runtime/message.rb +5 -2
- data/lib/harnex/runtime/runtime_budget.rb +96 -0
- data/lib/harnex/runtime/session.rb +684 -294
- data/lib/harnex/runtime/stop_request.rb +39 -0
- data/lib/harnex/stall.rb +46 -0
- data/lib/harnex/telemetry_reconciler.rb +42 -20
- data/lib/harnex/terminal_status.rb +5 -5
- data/lib/harnex/version.rb +2 -2
- data/lib/harnex.rb +16 -0
- data/recipes/01_fire_and_watch.md +14 -14
- data/recipes/02_chain_implement.md +35 -31
- data/recipes/03_buddy.md +1 -1
- metadata +34 -12
- data/docs/codex-appserver.md +0 -175
- data/lib/harnex/adapters/codex.rb +0 -160
- data/lib/harnex/adapters/codex_appserver.rb +0 -407
- data/lib/harnex/codex/app_server/client.rb +0 -347
data/guides/01_dispatch.md
CHANGED
|
@@ -17,7 +17,7 @@ Inside a harnex-managed session, these environment variables are available:
|
|
|
17
17
|
|
|
18
18
|
| Variable | Meaning |
|
|
19
19
|
| --- | --- |
|
|
20
|
-
| `HARNEX_SESSION_CLI` | Wrapped CLI name, such as `pi
|
|
20
|
+
| `HARNEX_SESSION_CLI` | Wrapped CLI name, such as `pi` or `claude` |
|
|
21
21
|
| `HARNEX_ID` | Current harnex session ID |
|
|
22
22
|
| `HARNEX_SESSION_REPO_ROOT` | Repo root for the session |
|
|
23
23
|
| `HARNEX_SESSION_ID` | Internal harnex instance ID |
|
|
@@ -68,15 +68,31 @@ harnex run pi --id pi-i-NN --tmux pi-i-NN \
|
|
|
68
68
|
--context "Read and execute /tmp/task-impl-NN.md"
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
+
For a one-shot task, add `--auto-stop --wait-done --wait-timeout 9m`: the
|
|
72
|
+
same command blocks until the first turn's result and prints it with the
|
|
73
|
+
launch fields (`turn_result.final_message` is the worker's reply; on 124 run
|
|
74
|
+
the `resume` command it prints). `harnex ask "QUESTION"` does that for a
|
|
75
|
+
read-only question and prints only the answer. See `harnex agents-guide
|
|
76
|
+
claude` for the Claude Code patterns.
|
|
77
|
+
|
|
78
|
+
To send a brief file's text itself, use `--context-file PATH`. Add
|
|
79
|
+
`--context-section TEXT` to send only one part: after a Markdown heading that
|
|
80
|
+
starts with TEXT, up to the next heading of the same or a higher level; after a
|
|
81
|
+
plain marker line (e.g. `Sent verbatim as the harnex --context.`), to the end
|
|
82
|
+
of the file. An empty result is refused before launch. `--meta-file PATH`
|
|
83
|
+
reads `--meta` JSON from a file and refuses invalid JSON (inline `--meta`
|
|
84
|
+
keys win). The brief's path and the SHA-256 of the sent text are recorded as
|
|
85
|
+
`meta.context_file` / `meta.context_sha256` on the started event and dispatch
|
|
86
|
+
rows.
|
|
87
|
+
|
|
71
88
|
For one-shot context dispatches that should clean themselves up, add
|
|
72
|
-
`--auto-stop`. It requires `--context` and does not keep
|
|
73
|
-
later reuse.
|
|
74
|
-
|
|
75
|
-
final prose cannot satisfy that
|
|
76
|
-
`completed_no_activity`, is
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
accepted work completion:
|
|
89
|
+
`--auto-stop`. It requires `--context` (or `--context-file`) and does not keep
|
|
90
|
+
the session alive for later reuse. A Pi turn is accepted when it settles. With
|
|
91
|
+
`--require-artifact-report` it must also show command/tool activity or a Git
|
|
92
|
+
delta; optional claims and final prose cannot satisfy that gate, and a
|
|
93
|
+
prose-only acknowledgment emits `completed_no_activity`, is visible as
|
|
94
|
+
`task_failed` before teardown, and exits non-zero. This keeps parallel
|
|
95
|
+
orchestration compact:
|
|
80
96
|
|
|
81
97
|
```bash
|
|
82
98
|
for i in 1 2 3; do
|
|
@@ -87,24 +103,56 @@ for i in 1 2 3; do harnex watch --id w-$i --until done --max-wait 90m & done
|
|
|
87
103
|
wait
|
|
88
104
|
```
|
|
89
105
|
|
|
90
|
-
|
|
91
|
-
one
|
|
106
|
+
Before an unattended queue run, prove the plumbing with one tiny real
|
|
107
|
+
dispatch for the chosen worker and one declared fallback (with Pi, a fallback
|
|
108
|
+
`--model`). Both must be inside your dispatch-model policy; an out-of-policy
|
|
109
|
+
fallback is a launch blocker, not a fallback. `preflight` exits non-zero on any failed check
|
|
110
|
+
(boot, registration, trust prompt, task read, workspace write, command exit,
|
|
111
|
+
receipt, claims, completion, stop) and names the failure class:
|
|
92
112
|
|
|
93
113
|
```bash
|
|
94
|
-
harnex
|
|
95
|
-
|
|
96
|
-
--
|
|
114
|
+
harnex preflight pi --cwd "$REPO" \
|
|
115
|
+
&& harnex preflight pi --cwd "$REPO" --model "$FALLBACK_MODEL" \
|
|
116
|
+
&& harnex run pi --id pi-i-NN --tmux pi-i-NN --context "Read and execute /tmp/task-NN.md" --auto-stop
|
|
97
117
|
```
|
|
98
118
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
119
|
+
Add `--commit-check` when workers must commit (it commits only in a throwaway
|
|
120
|
+
scratch repo). Preflight rows land in history with `phase=preflight` and stay
|
|
121
|
+
out of `orchestration report` rollups unless `--include-preflight` is passed.
|
|
122
|
+
|
|
123
|
+
To separate final acceptance from exploratory command noise in the receipt,
|
|
124
|
+
declare the final commands: `--final-gate 'bundle exec rake test'` (repeatable).
|
|
125
|
+
`validation.status` then reflects each gate's last observed exit, and
|
|
126
|
+
`validation.commands_had_failures` keeps the history. Still replay the gates
|
|
127
|
+
yourself before accepting work.
|
|
128
|
+
|
|
129
|
+
For unattended work, pair each dispatch with an owner-addressed consumer and
|
|
130
|
+
bounded native monitoring rather than depending on one long blocking watcher:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
harnex run pi --id pi-i-NN --tmux pi-i-NN \
|
|
134
|
+
--context "Read and execute /tmp/task-NN.md" --auto-stop
|
|
135
|
+
```
|
|
105
136
|
|
|
106
|
-
|
|
107
|
-
|
|
137
|
+
When `harnex run` is invoked by a Pi shell tool, it captures `PI_SESSION_ID`
|
|
138
|
+
(from a Claude Code Bash shell, `CLAUDE_CODE_SESSION_ID`; Pi's wins if both
|
|
139
|
+
are present) before crossing detached/tmux process boundaries. The session registers a
|
|
140
|
+
per-owner, per-attempt record under `$HARNEX_STATE_DIR/notifications/` before
|
|
141
|
+
sending the structured initial prompt, then atomically settles that record at
|
|
142
|
+
the first typed outcome. A nested Harnex worker never implicitly nominates
|
|
143
|
+
itself; non-Pi or nested orchestrators can pass `--orchestration-session-id`
|
|
144
|
+
explicitly. Dispatch-name reuse does not erase these records.
|
|
145
|
+
|
|
146
|
+
Every registered session also writes the existing
|
|
147
|
+
`$HARNEX_STATE_DIR/done/<repo-key>--<normalized-id>.<outcome>` marker. Optional
|
|
148
|
+
`--on-done CMD` remains available for single-consumer integrations and receives
|
|
149
|
+
`HARNEX_ID`, `HARNEX_OUTCOME`, `HARNEX_WORK_STATE`, `HARNEX_RECEIPT_PATH`,
|
|
150
|
+
`HARNEX_END_SHA`, and `HARNEX_ELAPSED_S`; never use one destructive shared file
|
|
151
|
+
as a multi-session delivery queue. `CMD` is trusted `/bin/sh -c` input, so do
|
|
152
|
+
not embed secrets in the command line.
|
|
153
|
+
|
|
154
|
+
A delivery only wakes the consumer. Even `completed` requires exact-receipt,
|
|
155
|
+
artifact, test, and Git verification plus a durable consumer acknowledgment.
|
|
108
156
|
|
|
109
157
|
Rule: when you use `--tmux`, pass the same name as `--id`. If you pass only
|
|
110
158
|
`--tmux NAME`, harnex creates a random session ID and the pane name no longer
|
|
@@ -115,7 +163,7 @@ wrapped agent from that directory and associate session metadata/default
|
|
|
115
163
|
telemetry with it:
|
|
116
164
|
|
|
117
165
|
```bash
|
|
118
|
-
harnex run
|
|
166
|
+
harnex run pi --cwd /tmp/leximaze_eval_run_001 \
|
|
119
167
|
--id lm-run-001 \
|
|
120
168
|
--context "Read README.md and write RESPONSES.jsonl and OUTPUT.md" \
|
|
121
169
|
--auto-stop
|
|
@@ -169,17 +217,11 @@ phase names before spawn.
|
|
|
169
217
|
|
|
170
218
|
Pi runs use structured RPC (`pi --mode rpc`) and require Pi >= 0.80.4; gate
|
|
171
219
|
unattended work with `harnex doctor --adapter pi`. Harnex `--model` / `--effort`
|
|
172
|
-
apply verified Pi startup controls before the prompt.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
Since RPC cannot display Pi's trust prompt,
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
Codex flag forms differ between transports. The default JSON-RPC adapter
|
|
179
|
-
(`codex app-server`) does not accept `-m`/`--model`; pass the model as
|
|
180
|
-
`-c model="<name>"` instead. The legacy PTY adapter (`harnex run codex
|
|
181
|
-
--legacy-pty`) still accepts `-m`. Codex app-server runs also default to
|
|
182
|
-
`service_tier="flex"`; add `--fast` to use `service_tier="fast"`.
|
|
220
|
+
apply verified Pi startup controls before the prompt (e.g. `--model
|
|
221
|
+
openai/gpt-5.1 --effort high`). Pass other Pi startup flags after
|
|
222
|
+
`--` (e.g. `harnex run pi --context "..." -- --approve`).
|
|
223
|
+
Since RPC cannot display Pi's trust prompt, harnex starts Pi with
|
|
224
|
+
`--no-approve` (project-local resources ignored) unless you pass `--approve`.
|
|
183
225
|
|
|
184
226
|
## Send
|
|
185
227
|
|
|
@@ -196,6 +238,31 @@ an idle state; it is not a full work-completion signal.
|
|
|
196
238
|
harnex send --id pi-i-NN --message "Run the acceptance test." --wait-for-idle --timeout 900
|
|
197
239
|
```
|
|
198
240
|
|
|
241
|
+
Sending to a busy worker:
|
|
242
|
+
|
|
243
|
+
- **Pi:** the message is handed to Pi at once as a follow-up (`delivery:
|
|
244
|
+
"follow_up"`) and delivered when the current turn ends. It cannot expire.
|
|
245
|
+
`--force` steers it into the turn now instead.
|
|
246
|
+
- **Other adapters:** it queues until the agent is at a prompt. If it is not
|
|
247
|
+
delivered within `--timeout`, send withdraws it and exits 124; if it
|
|
248
|
+
expires in the inbox (`run --inbox-ttl`), send exits 1 with `status:
|
|
249
|
+
"expired"`. Either way the caller knows the note was not delivered.
|
|
250
|
+
|
|
251
|
+
Every delivery reports `after_seq`. To wait for the turn a message started
|
|
252
|
+
(for example after nudging a turn that failed on provider capacity), wait
|
|
253
|
+
after that point so the earlier failure cannot answer:
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
harnex send --id pi-i-NN --message "Continue the same task from where you stopped" \
|
|
257
|
+
--wait-done --timeout 540
|
|
258
|
+
# on exit 124, resume with the after_seq it printed:
|
|
259
|
+
harnex wait --id pi-i-NN --until done --after-seq 812 --timeout 540
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
`--wait-done` uses the `wait --until done` exit codes (0 done, 1 failed, 2
|
|
263
|
+
proof rejected, 4 stalled, 124 timeout). Without `--timeout` it waits for
|
|
264
|
+
the turn without a cap.
|
|
265
|
+
|
|
199
266
|
Messages sent from one harnex session to another include a relay header:
|
|
200
267
|
|
|
201
268
|
```text
|
data/guides/04_monitoring.md
CHANGED
|
@@ -17,31 +17,138 @@ 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 sessions, combine
|
|
21
|
-
|
|
20
|
+
For unattended sessions, combine owner-addressed delivery with bounded native
|
|
21
|
+
watching. Every session writes
|
|
22
22
|
`$HARNEX_STATE_DIR/done/<repo-key>--<id>.<outcome>` at its first work-terminal
|
|
23
|
-
result.
|
|
24
|
-
|
|
23
|
+
result. A Pi-launched dispatch also registers a durable per-owner, per-attempt
|
|
24
|
+
record under `$HARNEX_STATE_DIR/notifications/` before its initial structured
|
|
25
|
+
prompt, then atomically settles it at terminal work. The runner captures the
|
|
26
|
+
invoking Pi shell's `PI_SESSION_ID`; nested workers are excluded unless an
|
|
27
|
+
external owner is explicitly named with `--orchestration-session-id`.
|
|
25
28
|
|
|
26
29
|
```bash
|
|
27
|
-
harnex run pi --id pi-i-NN --tmux pi-i-NN
|
|
28
|
-
--auto-stop
|
|
29
|
-
|
|
30
|
-
harnex watch --id pi-i-NN --until done --max-wait 30m
|
|
30
|
+
harnex run pi --id pi-i-NN --tmux pi-i-NN \
|
|
31
|
+
--context "Read the task brief" --auto-stop
|
|
32
|
+
harnex watch --id pi-i-NN --until done --max-wait 15m --heartbeat 60s
|
|
31
33
|
```
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
The consumer must match owner and attempt identity, verify the exact Harnex
|
|
36
|
+
receipt, and persist acknowledgment before continuing. It must not erase a
|
|
37
|
+
shared signal before delivery. `harnex run --on-done CMD` remains an optional
|
|
38
|
+
non-blocking integration hook; `CMD` is trusted shell input, receives the typed
|
|
39
|
+
`HARNEX_*` completion values, and must not target a destructive shared queue.
|
|
40
|
+
`completed` is still only a wake: verify the artifact and acceptance gates.
|
|
38
41
|
|
|
39
42
|
`harnex watch --until done` returns on the work-level `task_complete` or
|
|
40
43
|
`task_failed` signal, or terminal exit, whichever comes first. Successful work
|
|
41
|
-
exits `0`, failed work exits non-zero, and
|
|
44
|
+
exits `0`, failed work exits non-zero, and observer caps exit `124`. For
|
|
42
45
|
callers that need the lower-level primitive, `harnex wait --until done` exposes
|
|
43
46
|
the same work fence. Do not park an orchestrator in one unbounded watcher call.
|
|
44
47
|
|
|
48
|
+
## Heartbeats and Hard Observer Caps
|
|
49
|
+
|
|
50
|
+
For LLM callers, opt in to `--heartbeat 60s` on `wait` or `watch`. While the
|
|
51
|
+
observer is blocked, it flushes a diagnostic line to **stderr** at each interval:
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
harnex wait: id=pi-i-NN waited=60.0s state=running last_event=started seq=1
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
These are the last observed state/event/sequence, not evidence of model or tool
|
|
58
|
+
progress. Unknown observations remain `unknown`; a slow probe does not prevent
|
|
59
|
+
heartbeats, but the snapshot can be stale. Neither UI repaint nor log freshness
|
|
60
|
+
is interpreted as work activity. Heartbeats are off by default, including for
|
|
61
|
+
JSON consumers. Even when enabled, stdout remains **one final JSON result**;
|
|
62
|
+
consume stderr live rather than combining/buffering the streams until exit.
|
|
63
|
+
|
|
64
|
+
`wait --timeout DUR` (alias `--max-wait`) and `watch --max-wait DUR` (alias
|
|
65
|
+
`--timeout`) accept finite positive durations in seconds or with `s`, `m`, `h`
|
|
66
|
+
suffixes. The cap uses a monotonic clock and includes repo resolution, registry
|
|
67
|
+
and event reads, status HTTP, and final-event/exit-status grace. Heartbeats and
|
|
68
|
+
new events never reset it. A late probe result cannot turn an expired observer
|
|
69
|
+
into success. Timeout exits `124`, without stopping the live worker or writing
|
|
70
|
+
compatibility done/fail markers, even with `--stop-on-terminal`.
|
|
71
|
+
|
|
72
|
+
This bounds the **observer**, not worker runtime. The timed/heartbeat observer
|
|
73
|
+
uses a short-lived, isolated read-side child; cancellation kills and reaps that
|
|
74
|
+
child, not the session. Slow Ruby probes, HTTP, and even slow Ruby cleanup are
|
|
75
|
+
bounded by the cap plus at most one poll and scheduling tolerance. This is not a
|
|
76
|
+
promise to preempt uninterruptible kernel I/O or OS scheduling stalls. Output
|
|
77
|
+
consumers must keep draining streams. Optional marker writes and
|
|
78
|
+
`--stop-on-terminal` are post-observation actions, not read-side probes.
|
|
79
|
+
|
|
80
|
+
## Worker Runtime Budgets, Activity, and Stops
|
|
81
|
+
|
|
82
|
+
A watcher timeout stops **waiting**, not working. To cap the worker itself,
|
|
83
|
+
first require `harnex doctor --adapter pi` to report `ok: true` and
|
|
84
|
+
`capabilities.runtime_budget: 1` (available in Harnex 0.14.0+), then opt in:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
harnex run pi --id pi-i-NN --tmux pi-i-NN \
|
|
88
|
+
--context "Read the task brief" --auto-stop --max-runtime 30m
|
|
89
|
+
harnex watch --id pi-i-NN --max-wait 5m --heartbeat 60s
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`--max-runtime` is a fixed monotonic budget in the session-owning runner. It is
|
|
93
|
+
armed before child launch/initial prompt, after binary validation, and never
|
|
94
|
+
reset by prompts, retries, UI chatter, or activity. Expiry requires no supervisor:
|
|
95
|
+
it terminates the owned worker process group with bounded TERM/KILL escalation
|
|
96
|
+
without waiting for an abort RPC, startup handshake, receipt lock, or runner
|
|
97
|
+
stdout. Normal descendant processes in that group are included; descendants
|
|
98
|
+
which deliberately create a different group/session are not contained. This is
|
|
99
|
+
not an OS sandbox. Child shutdown and receipt/telemetry finalization can take
|
|
100
|
+
additional time and still require working I/O/drained output; this is not a
|
|
101
|
+
promise to preempt OS scheduling or uninterruptible I/O.
|
|
102
|
+
Active work is failed with logical run exit `124`. Expiry used only to clean up
|
|
103
|
+
already accepted idle work preserves that proof and logical exit `0`. The stop
|
|
104
|
+
record says `runtime_budget` when expiry was the first requested stop. An earlier
|
|
105
|
+
stop keeps its original labels but cannot disable the cap: `runtime_budget.enforced`
|
|
106
|
+
and its timestamp plus `runtime_budget_expired` record actual enforcement
|
|
107
|
+
independently. Without this option there is no runtime cap.
|
|
108
|
+
|
|
109
|
+
`status --json` and dispatch-end records expose separate `activity` and
|
|
110
|
+
`runtime_budget` objects:
|
|
111
|
+
|
|
112
|
+
- `activity.turn_active`, `turn_started_at`, `turn_age_s` track the current
|
|
113
|
+
structured run, including quiet waits and retries.
|
|
114
|
+
- `model_active`, `model_started_at`, `model_age_s` describe the observed
|
|
115
|
+
assistant stream/item lifetime, not hidden provider internals.
|
|
116
|
+
- `last_model_activity_at` / `model_idle_s` and `last_tool_activity_at` /
|
|
117
|
+
`tool_idle_s` track their respective protocol events. Thinking deltas advance
|
|
118
|
+
the model clock without retaining their content in the activity tracker.
|
|
119
|
+
- UI requests, queue updates, log mtime, and status reads never advance work
|
|
120
|
+
clocks. A new run clears the prior run's activity clocks; steering an active
|
|
121
|
+
run does not. PTY/unobserved activity is `status: unknown` with null clocks.
|
|
122
|
+
- Existing `log_idle_s` and the status table's `IDLE` column remain **log age**,
|
|
123
|
+
not work progress. A degraded registry snapshot is stale, not a fresh sample.
|
|
124
|
+
|
|
125
|
+
Stops record bounded caller-declared labels, separately from work acceptance:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
harnex stop --id pi-i-NN --reason manual --origin cli
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Reasons are `manual`, `completion`, `runtime_budget`; origins are `api`, `cli`,
|
|
132
|
+
`auto_stop`, `watch`, `runtime`. These labels are not authenticated identities.
|
|
133
|
+
The first stop request wins. Inspect the `stop_requested` event, live/terminal
|
|
134
|
+
`stop` metadata, or receipt `observed.stop` for reason, origin, request time,
|
|
135
|
+
work state at request, and an applicable runtime limit. Harnex's own budget
|
|
136
|
+
expiry requests `runtime_budget`/`runtime` unless a prior stop already won;
|
|
137
|
+
auto-stop uses `completion`/`auto_stop`;
|
|
138
|
+
`watch --stop-on-terminal` uses `completion`/`watch`. Raw child status remains in
|
|
139
|
+
`process_exited` for Pi and budgeted runs when observed (startup failure may
|
|
140
|
+
have no raw wait status); the final run exit is logical status.
|
|
141
|
+
|
|
142
|
+
Explicit idle Pi cleanup preserves the latest settled accepted/no-change proof,
|
|
143
|
+
including after follow-up turns. Busy stop rejects unfinished work and cannot
|
|
144
|
+
reuse an older accepted turn. Unexpected EOF, malformed/truncated JSONL, and abnormal
|
|
145
|
+
idle teardown remain failures. Completion notifications remain once per session,
|
|
146
|
+
not once per reusable turn; verify the receipt appropriate to each turn.
|
|
147
|
+
|
|
148
|
+
Pi `send --force` while busy queues steering for a subsequent assistant turn.
|
|
149
|
+
Transport/queue acceptance is not proof the current model call consumed it.
|
|
150
|
+
Consumer wake deduplication/acknowledgment still belongs in the Pi bridge.
|
|
151
|
+
|
|
45
152
|
## Live-Run Visibility
|
|
46
153
|
|
|
47
154
|
Every dispatch appends a `dispatch_start` row to the repo's dispatch stream
|
|
@@ -60,6 +167,10 @@ signal, from any cwd in the same repo:
|
|
|
60
167
|
`dispatch_end` row.
|
|
61
168
|
- `harnex wait --until done` blocks while the session's pid is alive, up to
|
|
62
169
|
`--timeout`. "No signal yet" from a live worker is never terminal.
|
|
170
|
+
- A bare `harnex wait` (no `--until`) fences process exit. A session started
|
|
171
|
+
without `--auto-stop` stays alive at its prompt after its turn, so stderr
|
|
172
|
+
carries a `note:` on entry and again if the turn settles mid-wait; switch
|
|
173
|
+
to `--until done`. `status --json` exposes `auto_stop`.
|
|
63
174
|
|
|
64
175
|
A monitor consulting these signals can never classify a healthy mid-run
|
|
65
176
|
worker as dead. If `status` says running, do not dispatch a replacement.
|
|
@@ -72,7 +183,8 @@ worker as dead. If `status` says running, do not dispatch a replacement.
|
|
|
72
183
|
| `1` | `failed` | Work failed, process failed, or killed |
|
|
73
184
|
| `2` | `rejected_proof` | Completed but proof rejected (`completed_no_activity`, `report_missing`, `report_invalid`, `report_rejected`) |
|
|
74
185
|
| `3` | `no_such_session` | No live, start, event, or terminal signal for the id |
|
|
75
|
-
| `
|
|
186
|
+
| `4` | `stalled` | `--stall-after DUR`: no progress for DUR; worker is not stopped |
|
|
187
|
+
| `124` | `timeout` | Observer cap elapsed before a result was obtained; worker is not stopped |
|
|
76
188
|
|
|
77
189
|
The JSON payload always carries `wait_result` plus the work-state fields
|
|
78
190
|
(`done`, `work_state`, `outcome_class`, `artifact_report_status`). The child
|
|
@@ -80,6 +192,92 @@ process's own exit code is reported as data (`exit_code`), never passed
|
|
|
80
192
|
through as wait's exit status. Treat `2` as a work-acceptance failure, `3` as
|
|
81
193
|
a coordination error (wrong id or wrong repo), and only `0` as success.
|
|
82
194
|
|
|
195
|
+
### Stall detection (`--stall-after`)
|
|
196
|
+
|
|
197
|
+
`harnex watch --stall-after DUR` (and `wait --until done --stall-after DUR`)
|
|
198
|
+
exits `4` once the live session shows no progress for `DUR`. The worker is not
|
|
199
|
+
nudged or stopped, and no done/fail marker is written. The result carries
|
|
200
|
+
`stall: {clock, idle_s, threshold_s}`:
|
|
201
|
+
|
|
202
|
+
- `clock: activity` (Pi, the structured adapter): seconds since the
|
|
203
|
+
latest turn start, model event, or tool event. It counts only while a turn
|
|
204
|
+
is active, so a session idle at its prompt between turns never stalls. A
|
|
205
|
+
command that prints nothing for longer than `DUR` counts as a stall too,
|
|
206
|
+
which also catches hung commands, so choose `DUR` above your longest quiet
|
|
207
|
+
build or test.
|
|
208
|
+
- `clock: output_log` (PTY adapters): seconds since the transcript last
|
|
209
|
+
changed. This is conservative, because TUIs redraw spinners.
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
harnex watch --id pi-i-NN --until done --stall-after 25m --max-wait 4h
|
|
213
|
+
case $? in 0) ;; 4) echo "stalled: inspect, nudge or stop it" ;; *) echo "not done" ;; esac
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
This replaces polling `activity.model_idle_s` yourself. `harnex run --watch
|
|
217
|
+
--stall-after` is a separate babysitter that sends `resume` on output-log
|
|
218
|
+
idle. If both run, set watch's `DUR` above run's `stall-after × (max-resumes
|
|
219
|
+
+ 1)`.
|
|
220
|
+
|
|
221
|
+
## Owner Stream (Claude Code supervisors)
|
|
222
|
+
|
|
223
|
+
One long-running command reports every dispatch an owner started, so a
|
|
224
|
+
supervisor does not need a poller per worker:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
harnex watch --owner --stall-after 25m
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
- **Owner:** `harnex run` from a Claude Code Bash shell records
|
|
231
|
+
`CLAUDE_CODE_SESSION_ID` as the dispatch owner (from Pi: `PI_SESSION_ID`).
|
|
232
|
+
`--owner` with no value resolves the same id. Pass
|
|
233
|
+
`--orchestration-session-id ID` to `run` and `--owner ID` to the stream to
|
|
234
|
+
name an owner explicitly.
|
|
235
|
+
- **Output:** one JSON line per change, schema `harnex.owner_event.v1`. Events:
|
|
236
|
+
`started`, `done`, `rejected`, `failed`, `stalled` (with `--stall-after`;
|
|
237
|
+
once per episode), `stopped`, `timeout`. Each line carries `id`,
|
|
238
|
+
`session_id`, `repo`, `ts`, `work_state`, plus `outcome_class`,
|
|
239
|
+
`receipt_path`, `elapsed_s`, `stall`, `reason` where they apply, and a
|
|
240
|
+
readable `msg`. No heartbeats, so Claude Code's `Monitor` wakes only when
|
|
241
|
+
something needs a decision.
|
|
242
|
+
- **Restarts:** a restarted stream re-announces `started` for running
|
|
243
|
+
dispatches and delivers only results it has not delivered before (cursor
|
|
244
|
+
under the owner's notifications directory). Delivery is not acceptance:
|
|
245
|
+
still verify the receipt and artifacts.
|
|
246
|
+
- **Turns:** every turn reports its own result, once and in order, even when
|
|
247
|
+
several turns end between two polls. A lane nudged with `harnex send` after
|
|
248
|
+
it finished or failed reports again when that turn ends; the line carries
|
|
249
|
+
`turn` and the `msg` names it (`pi-i-NN turn 2 done ...`). Result lines
|
|
250
|
+
carry the worker's `final_message` (bounded; full text at
|
|
251
|
+
`final_message_path`).
|
|
252
|
+
- **Lifetime:** it runs until killed, or until `--max-wait` (exit 124).
|
|
253
|
+
|
|
254
|
+
### Claude Code subagents: `--once`
|
|
255
|
+
|
|
256
|
+
A subagent cannot be woken by a background notification after it ends its
|
|
257
|
+
turn, so it must block in a foreground Bash call (10-minute cap per call).
|
|
258
|
+
`--once` makes one call cover every lane the owner launched:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
harnex watch --owner --once --timeout 9m --cursor sub-a --stall-after 25m
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
It prints every undelivered result or stall that is ready (one JSON line
|
|
265
|
+
each), advances the cursor and exits 0; with nothing new before `--timeout` it
|
|
266
|
+
prints nothing and exits 124, so loop it. It never returns `started`, and a
|
|
267
|
+
stall episode is reported once across calls. A subagent shares its parent's
|
|
268
|
+
`CLAUDE_CODE_SESSION_ID`, so isolate it: launch its lanes with
|
|
269
|
+
`harnex run --owner-tag TAG` and watch `harnex watch --owner --owner-tag TAG`
|
|
270
|
+
(a separate owner, `<parent>/<TAG>`, with no other lanes and no backlog), or
|
|
271
|
+
keep the shared owner with its own `--cursor NAME` and `--match 'pi-TAG-*'`.
|
|
272
|
+
See `harnex agents-guide claude`.
|
|
273
|
+
|
|
274
|
+
A status line or panel can poll `harnex status --all --json`. Each row has
|
|
275
|
+
`id`, `cli`, `description`, `repo_root`, `started_at`, `state`, `work_state`,
|
|
276
|
+
`done`, `task_complete`, `task_failed`, and `degraded`/`live_status`/`source`.
|
|
277
|
+
A live session also has `agent_state`, `log_idle_s`, `auto_stop` and
|
|
278
|
+
`activity` (`turn_active`, `model_idle_s`, `tool_idle_s`, ...). These fields
|
|
279
|
+
are stable for consumers; new fields are additive.
|
|
280
|
+
|
|
83
281
|
## Duplicate-Dispatch Guard
|
|
84
282
|
|
|
85
283
|
`harnex run --attempt-kind retry|fallback` requires
|
|
@@ -88,13 +286,13 @@ named parent is still running in the same repo is refused. Wait for the parent
|
|
|
88
286
|
(`harnex wait --id <parent> --until done`) or stop it first. Pass
|
|
89
287
|
`--allow-live-parent` only for intentional parallelism (e.g. isolated
|
|
90
288
|
worktrees). `--attempt-kind review` is exempt: a completed parent may still
|
|
91
|
-
sit at a live prompt while its work is reviewed. For structured
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
`completed_no_activity` and
|
|
289
|
+
sit at a live prompt while its work is reviewed. For structured Pi RPC
|
|
290
|
+
sessions, `harnex wait --until task_complete` remains the exact accepted-turn
|
|
291
|
+
fence. Pi reaches that fence only at `agent_settled`; an earlier `agent_end` can
|
|
292
|
+
still be followed by retry, compaction recovery, or queued work. Final Pi
|
|
293
|
+
`error`, `aborted`, and `length` stop reasons emit `task_failed`. With
|
|
294
|
+
`--require-artifact-report`, an acknowledgment-only turn is typed
|
|
295
|
+
`completed_no_activity` and fails this fence without transcript parsing. Harnex
|
|
98
296
|
writes the observed-state receipt before publishing `task_complete`; optional
|
|
99
297
|
worker claims never make an inactive turn pass. Harnex still does not judge
|
|
100
298
|
semantic quality, so verify the expected artifact or tests afterward.
|
|
@@ -167,9 +365,9 @@ harnex events --id pi-i-NN
|
|
|
167
365
|
For task completion:
|
|
168
366
|
|
|
169
367
|
```bash
|
|
170
|
-
harnex watch --id pi-i-NN --until done --max-wait 15m
|
|
368
|
+
harnex watch --id pi-i-NN --until done --max-wait 15m --heartbeat 60s
|
|
171
369
|
# Primitive equivalent when a script wants raw wait semantics:
|
|
172
|
-
harnex wait --id pi-i-NN --until done --timeout 900
|
|
370
|
+
harnex wait --id pi-i-NN --until done --timeout 900 --heartbeat 60s
|
|
173
371
|
# Or, when you specifically need the structured successful-turn event:
|
|
174
372
|
harnex wait --id pi-i-NN --until task_complete --timeout 900
|
|
175
373
|
```
|
|
@@ -229,7 +427,7 @@ interpretation.
|
|
|
229
427
|
- Polling `state=prompt` alone and calling it done.
|
|
230
428
|
- Wrapping `harnex wait` in loops that swallow non-zero `task_failed` results.
|
|
231
429
|
- Blocking orchestrators on caller-owned `/tmp/*-done.txt` as the only completion signal.
|
|
232
|
-
- Using one long watcher call without the runner-owned marker or
|
|
430
|
+
- Using one long watcher call without the runner-owned marker or owner-addressed delivery path.
|
|
233
431
|
- Letting an unattended loop run with no wall-clock cap.
|
|
234
432
|
- Reading raw tmux panes instead of `harnex pane`.
|
|
235
433
|
- Using `--wait-for-idle` as acceptance proof.
|
data/guides/05_naming.md
CHANGED
data/guides/06_claude.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Claude Code: GPT Workers as Tools
|
|
2
|
+
|
|
3
|
+
Harnex lets a Claude Code session or subagent hand work to Pi workers (GPT or
|
|
4
|
+
other provider models) and get the result back the way it calls any tool. Each
|
|
5
|
+
worker runs in a visible tmux window. Every command prints plain text: the
|
|
6
|
+
worker's reply, then a `── NAME: …` footer with the lane's state, model, cost,
|
|
7
|
+
commits and the next commands.
|
|
8
|
+
|
|
9
|
+
## Pick The Command
|
|
10
|
+
|
|
11
|
+
| Need | Command |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| A second opinion, an explanation, a read-only review | `harnex ask "QUESTION"` |
|
|
14
|
+
| One task done by a worker | `harnex delegate --worktree NAME "TASK"`, then `harnex land NAME` |
|
|
15
|
+
| Several tasks in parallel | `harnex delegate --no-wait --worktree NAME "TASK"` per task, then `harnex result` |
|
|
16
|
+
| A follow-up to the same worker | `harnex reply NAME "MESSAGE"` |
|
|
17
|
+
| Drop a lane | `harnex discard NAME` |
|
|
18
|
+
| What is open | `harnex lanes` |
|
|
19
|
+
|
|
20
|
+
Exit codes: 0 done, 1 failed, 2 usage error, 124 still working (the worker
|
|
21
|
+
keeps going; `harnex result NAME` keeps waiting).
|
|
22
|
+
|
|
23
|
+
### Ask
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
harnex ask "Which module writes dispatch rows, and is the lane case tested?"
|
|
27
|
+
harnex ask --model openai/gpt-5.1 --file /tmp/review.md
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The worker reads the repository with read-only tools, answers, and stops.
|
|
31
|
+
stdout is the answer; stderr gets one line with the lane, its time and the
|
|
32
|
+
model. `--write` lets it edit and run commands; `--fallback-model M` asks once
|
|
33
|
+
more with another model after a provider-capacity failure.
|
|
34
|
+
|
|
35
|
+
### Delegate, Review, Land
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
harnex delegate --worktree word-count "Add TextKit.word_count(text) with a test; run bin/test"
|
|
39
|
+
git diff HEAD...harnex/word-count && bin/test # your own check
|
|
40
|
+
harnex land word-count
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`--worktree NAME` creates `.harnex/worktrees/NAME` on branch `harnex/NAME` from
|
|
44
|
+
`HEAD`. The worker is told to commit there. `harnex land NAME` merges the
|
|
45
|
+
branch into the main checkout's current branch, then removes the worktree and
|
|
46
|
+
branch; leftover uncommitted changes are committed first. On a merge conflict
|
|
47
|
+
the merge stays in progress: resolve, `git commit`, and run `harnex land NAME`
|
|
48
|
+
again to clean up. `harnex land` with no name lands every finished worktree
|
|
49
|
+
lane you started in this repo. `harnex discard NAME` drops the lane's work. Without
|
|
50
|
+
`--worktree`, the worker edits the current directory in place.
|
|
51
|
+
|
|
52
|
+
### Parallel Lanes
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
harnex delegate --no-wait --worktree slugify "Fix TextKit.slugify ..."
|
|
56
|
+
harnex delegate --no-wait --worktree truncate "Add TextKit.truncate ..."
|
|
57
|
+
harnex result # waits for all your lanes, printing each as it lands
|
|
58
|
+
harnex land slugify && harnex land truncate
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`harnex result` with no names covers every lane you started in this repo
|
|
62
|
+
whose result you have not collected; `harnex result NAME...` names them.
|
|
63
|
+
|
|
64
|
+
### Follow-Ups
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
harnex reply word-count "Also add TextKit.char_count(text) with a test."
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
A worker still on its turn takes the message as a follow-up. A finished lane
|
|
71
|
+
is relaunched on its own Pi session, so the worker keeps everything it knew.
|
|
72
|
+
After a provider-capacity failure, reply "Continue the same task from where
|
|
73
|
+
you stopped", optionally with `--model OTHER`.
|
|
74
|
+
|
|
75
|
+
## Rules For Claude Code
|
|
76
|
+
|
|
77
|
+
- **Long tasks.** Commands wait 9 minutes by default (`--timeout`), under the
|
|
78
|
+
10-minute Bash cap. Give the Bash call `timeout: 600000`; the default 2
|
|
79
|
+
minutes kills the wait. On 124 run `harnex result NAME` until it lands. In
|
|
80
|
+
the main session you can also run the command with `run_in_background` and
|
|
81
|
+
be notified.
|
|
82
|
+
- **Subagents** block in the foreground the same way; nothing needs a
|
|
83
|
+
background notification. A subagent shares its parent's
|
|
84
|
+
`CLAUDE_CODE_SESSION_ID`, so `harnex result` with no names also sees the
|
|
85
|
+
parent's lanes; name the lanes you wait for.
|
|
86
|
+
- **Briefs are self-contained.** The worker has not seen the conversation.
|
|
87
|
+
- **Verify before you report success.** `done` means the worker's turn ended
|
|
88
|
+
without error, not that the work is right. Check the diff and the tests.
|
|
89
|
+
- **Clean up.** Land or discard every worktree lane; `harnex lanes` lists what
|
|
90
|
+
is still open.
|
|
91
|
+
|
|
92
|
+
## Low-Level Commands
|
|
93
|
+
|
|
94
|
+
`harnex run`, `send`, `wait`, `watch --owner` and `stop` remain for queues and
|
|
95
|
+
custom orchestration (see `harnex agents-guide dispatch` and `monitoring`).
|
|
96
|
+
Lanes are ordinary harnex sessions named after the lane.
|
data/lib/harnex/adapters/base.rb
CHANGED
|
@@ -24,25 +24,21 @@ module Harnex
|
|
|
24
24
|
@extra_args = extra_args.dup
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
# Default transport.
|
|
28
|
-
#
|
|
29
|
-
# this to pick the I/O path.
|
|
27
|
+
# Default transport. The structured Pi adapter overrides it to
|
|
28
|
+
# :stdio_jsonl_rpc; Session#run uses this to pick the I/O path.
|
|
30
29
|
def transport
|
|
31
30
|
:pty
|
|
32
31
|
end
|
|
33
32
|
|
|
34
33
|
# Vendor of the underlying agent — populates DISPATCH meta.agent_provider.
|
|
35
|
-
# Subclasses override (claude → "anthropic"
|
|
34
|
+
# Subclasses override (claude → "anthropic").
|
|
36
35
|
def provider
|
|
37
36
|
nil
|
|
38
37
|
end
|
|
39
38
|
|
|
40
39
|
# Whether the usage this adapter captures reports input_tokens
|
|
41
40
|
# INCLUSIVE of cached_tokens. Depends on the capture path, not just
|
|
42
|
-
# the provider
|
|
43
|
-
# input, while the codex TUI line the PTY adapter scrapes shows
|
|
44
|
-
# non-cached input with cached as a separate additive count. Feeds
|
|
45
|
-
# Pricing.compute (plan 33 Phase 2).
|
|
41
|
+
# the provider. Feeds Pricing.compute (plan 33 Phase 2).
|
|
46
42
|
def usage_input_includes_cached?
|
|
47
43
|
false
|
|
48
44
|
end
|