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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +592 -0
  3. data/GUIDE.md +222 -68
  4. data/README.md +78 -48
  5. data/TECHNICAL.md +235 -98
  6. data/agents/gpt.md +60 -0
  7. data/docs/agents.md +119 -0
  8. data/docs/dispatch-telemetry.md +184 -50
  9. data/docs/events.md +20 -7
  10. data/docs/lanes.md +202 -0
  11. data/docs/mcp.md +102 -0
  12. data/docs/pi-rpc.md +81 -7
  13. data/docs/upgrading-to-0.29.md +87 -0
  14. data/guides/01_dispatch.md +101 -34
  15. data/guides/04_monitoring.md +223 -25
  16. data/guides/05_naming.md +0 -1
  17. data/guides/06_claude.md +96 -0
  18. data/lib/harnex/adapters/base.rb +4 -8
  19. data/lib/harnex/adapters/pi.rb +92 -12
  20. data/lib/harnex/adapters.rb +1 -16
  21. data/lib/harnex/artifact_report.rb +206 -12
  22. data/lib/harnex/cli.rb +68 -50
  23. data/lib/harnex/commands/ask.rb +278 -0
  24. data/lib/harnex/commands/delegate.rb +202 -0
  25. data/lib/harnex/commands/doctor.rb +34 -36
  26. data/lib/harnex/commands/events.rb +3 -3
  27. data/lib/harnex/commands/history.rb +2 -8
  28. data/lib/harnex/commands/land.rb +150 -0
  29. data/lib/harnex/commands/lanes.rb +84 -0
  30. data/lib/harnex/commands/logs.rb +3 -3
  31. data/lib/harnex/commands/mcp.rb +208 -0
  32. data/lib/harnex/commands/orchestration.rb +88 -9
  33. data/lib/harnex/commands/pane.rb +3 -3
  34. data/lib/harnex/commands/preflight.rb +566 -0
  35. data/lib/harnex/commands/reply.rb +125 -0
  36. data/lib/harnex/commands/result.rb +108 -0
  37. data/lib/harnex/commands/run.rb +248 -64
  38. data/lib/harnex/commands/send.rb +121 -16
  39. data/lib/harnex/commands/status.rb +32 -4
  40. data/lib/harnex/commands/stop.rb +14 -4
  41. data/lib/harnex/commands/telemetry.rb +19 -1
  42. data/lib/harnex/commands/wait.rb +421 -56
  43. data/lib/harnex/commands/watch.rb +134 -21
  44. data/lib/harnex/context_file.rb +85 -0
  45. data/lib/harnex/core.rb +100 -15
  46. data/lib/harnex/dispatch_history.rb +157 -10
  47. data/lib/harnex/lanes.rb +513 -0
  48. data/lib/harnex/mcp.rb +362 -0
  49. data/lib/harnex/orchestration.rb +128 -5
  50. data/lib/harnex/owned_process_group.rb +50 -0
  51. data/lib/harnex/owner_stream.rb +399 -0
  52. data/lib/harnex/pricing.rb +4 -9
  53. data/lib/harnex/runtime/activity_tracker.rb +107 -0
  54. data/lib/harnex/runtime/api_server.rb +11 -1
  55. data/lib/harnex/runtime/completion_notifier.rb +90 -11
  56. data/lib/harnex/runtime/inbox.rb +26 -7
  57. data/lib/harnex/runtime/message.rb +5 -2
  58. data/lib/harnex/runtime/runtime_budget.rb +96 -0
  59. data/lib/harnex/runtime/session.rb +684 -294
  60. data/lib/harnex/runtime/stop_request.rb +39 -0
  61. data/lib/harnex/stall.rb +46 -0
  62. data/lib/harnex/telemetry_reconciler.rb +42 -20
  63. data/lib/harnex/terminal_status.rb +5 -5
  64. data/lib/harnex/version.rb +2 -2
  65. data/lib/harnex.rb +16 -0
  66. data/recipes/01_fire_and_watch.md +14 -14
  67. data/recipes/02_chain_implement.md +35 -31
  68. data/recipes/03_buddy.md +1 -1
  69. metadata +34 -12
  70. data/docs/codex-appserver.md +0 -175
  71. data/lib/harnex/adapters/codex.rb +0 -160
  72. data/lib/harnex/adapters/codex_appserver.rb +0 -407
  73. data/lib/harnex/codex/app_server/client.rb +0 -347
@@ -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`, `codex`, or `claude` |
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 the session alive for
73
- later reuse. On Codex app-server, a turn launched from `--context` is accepted
74
- only after structured command/tool activity or a Git delta. Optional claims and
75
- final prose cannot satisfy that observed-activity gate. A prose-only acknowledgment emits
76
- `completed_no_activity`, is
77
- visible as `task_failed` before teardown, and exits non-zero. This keeps
78
- parallel orchestration compact without converting agent turn completion into
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
- For unattended work, add a session-owned push signal rather than depending on
91
- one long blocking watcher call:
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 run pi --id pi-i-NN --tmux pi-i-NN \
95
- --context "Read and execute /tmp/task-NN.md" --auto-stop \
96
- --on-done 'printf "%s %s\n" "$HARNEX_ID" "$HARNEX_OUTCOME" >> koder/scratch/HARNEX_WAKE.txt'
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
- Every registered session writes
100
- `$HARNEX_STATE_DIR/done/<repo-key>--<normalized-id>.<outcome>`, even without a
101
- hook. `--on-done CMD` receives `HARNEX_ID`, `HARNEX_OUTCOME`,
102
- `HARNEX_WORK_STATE`, `HARNEX_RECEIPT_PATH`, `HARNEX_END_SHA`, and
103
- `HARNEX_ELAPSED_S`. Use a gitignored wake file. `CMD` is trusted `/bin/sh -c`
104
- input; never embed secrets in the command line.
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
- A push only wakes the consumer. Even `completed` requires report, artifact,
107
- test, and Git verification plus bounded `harnex watch --until done` calls.
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 codex --cwd /tmp/leximaze_eval_run_001 \
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. Pass other Pi startup
173
- flags after `--` (e.g. `harnex run pi --context "..." -- --model
174
- anthropic/claude-sonnet-4-5 --thinking high`).
175
- Since RPC cannot display Pi's trust prompt, explicitly pass child `--approve`
176
- for reviewed project-local resources or `--no-approve` to ignore them.
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
@@ -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 bounded watcher calls with the runner-owned
21
- push path. Every session writes
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. `harnex run --on-done CMD` can also launch one non-blocking local hook,
24
- even without a watcher and while the agent remains alive at a prompt:
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 --context "Read the task brief" \
28
- --auto-stop \
29
- --on-done 'printf "%s %s\n" "$HARNEX_ID" "$HARNEX_OUTCOME" >> koder/scratch/HARNEX_WAKE.txt'
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
- Use a gitignored wake destination. `CMD` is trusted shell input; never embed
34
- secrets. It receives `HARNEX_ID`, `HARNEX_OUTCOME`, `HARNEX_WORK_STATE`,
35
- `HARNEX_RECEIPT_PATH`, `HARNEX_END_SHA`, and `HARNEX_ELAPSED_S`. `completed`
36
- wakes the consumer but does not certify the prose; verify the receipt and
37
- artifact before acting.
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 wall-clock caps exit `124`. For
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
- | `124` | `timeout` | `--timeout` elapsed while the session was still running |
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 sessions (Pi
92
- RPC and Codex app-server), `harnex wait --until task_complete` remains the exact
93
- accepted-turn fence. Pi reaches that fence only at `agent_settled`; an earlier
94
- `agent_end` can still be followed by retry, compaction recovery, or queued work.
95
- Final Pi `error`, `aborted`, and `length` stop reasons emit `task_failed`.
96
- Codex acknowledgment-only auto-stop turns are typed
97
- `completed_no_activity` and fail this fence without transcript parsing. Harnex
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 `--on-done` push path.
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
@@ -16,7 +16,6 @@ Common prefixes:
16
16
  | Prefix | Meaning |
17
17
  | --- | --- |
18
18
  | `pi` | Pi worker (default) |
19
- | `cx` | Codex worker |
20
19
  | `cl` | Claude worker |
21
20
  | `buddy` | Buddy monitor |
22
21
 
@@ -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.
@@ -24,25 +24,21 @@ module Harnex
24
24
  @extra_args = extra_args.dup
25
25
  end
26
26
 
27
- # Default transport. Structured adapters override to :stdio_jsonrpc
28
- # (Codex app-server) or :stdio_jsonl_rpc (Pi RPC); Session#run uses
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", codex → "openai").
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: codex app-server JSON reports cached as a subset of
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