harnex 0.3.3 → 0.4.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/GUIDE.md +11 -0
- data/README.md +37 -5
- data/TECHNICAL.md +95 -42
- data/lib/harnex/cli.rb +7 -0
- data/lib/harnex/commands/events.rb +212 -0
- data/lib/harnex/commands/run.rb +128 -14
- data/lib/harnex/commands/skills.rb +30 -10
- data/lib/harnex/commands/status.rb +17 -3
- data/lib/harnex/commands/watch.rb +209 -0
- data/lib/harnex/commands/watch_presets.rb +17 -0
- data/lib/harnex/core.rb +33 -0
- data/lib/harnex/runtime/session.rb +75 -2
- data/lib/harnex/version.rb +2 -2
- data/lib/harnex.rb +3 -0
- data/skills/harnex/SKILL.md +9 -337
- data/skills/harnex-buddy/SKILL.md +20 -15
- data/skills/harnex-chain/SKILL.md +90 -192
- data/skills/harnex-dispatch/SKILL.md +115 -9
- metadata +5 -2
|
@@ -6,7 +6,7 @@ module Harnex
|
|
|
6
6
|
class Session
|
|
7
7
|
OUTPUT_BUFFER_LIMIT = 64 * 1024
|
|
8
8
|
|
|
9
|
-
attr_reader :repo_root, :host, :port, :session_id, :token, :command, :pid, :id, :adapter, :watch, :inbox, :description, :output_log_path
|
|
9
|
+
attr_reader :repo_root, :host, :port, :session_id, :token, :command, :pid, :id, :adapter, :watch, :inbox, :description, :output_log_path, :events_log_path
|
|
10
10
|
|
|
11
11
|
def initialize(adapter:, command:, repo_root:, host:, port: nil, id: DEFAULT_ID, watch: nil, description: nil, inbox_ttl: Inbox::DEFAULT_TTL)
|
|
12
12
|
@adapter = adapter
|
|
@@ -19,17 +19,21 @@ module Harnex
|
|
|
19
19
|
@description = nil if @description.empty?
|
|
20
20
|
@registry_path = Harnex.registry_path(repo_root, @id)
|
|
21
21
|
@output_log_path = Harnex.output_log_path(repo_root, @id)
|
|
22
|
+
@events_log_path = Harnex.events_log_path(repo_root, @id)
|
|
22
23
|
@session_id = SecureRandom.hex(8)
|
|
23
24
|
@token = SecureRandom.hex(16)
|
|
24
25
|
@port = Harnex.allocate_port(repo_root, @id, port, host: host)
|
|
25
26
|
@mutex = Mutex.new
|
|
26
27
|
@inject_mutex = Mutex.new
|
|
28
|
+
@events_mutex = Mutex.new
|
|
27
29
|
@injected_count = 0
|
|
28
30
|
@last_injected_at = nil
|
|
29
31
|
@started_at = Time.now
|
|
30
32
|
@server = nil
|
|
31
33
|
@reader = nil
|
|
32
34
|
@output_log = nil
|
|
35
|
+
@events_log = nil
|
|
36
|
+
@events_log_seq = 0
|
|
33
37
|
@writer = nil
|
|
34
38
|
@pid = nil
|
|
35
39
|
@term_signal = nil
|
|
@@ -60,8 +64,10 @@ module Harnex
|
|
|
60
64
|
def run(validate_binary: true)
|
|
61
65
|
validate_binary! if validate_binary
|
|
62
66
|
prepare_output_log
|
|
67
|
+
prepare_events_log
|
|
63
68
|
@reader, @writer, @pid = PTY.spawn(child_env, *command)
|
|
64
69
|
@writer.sync = true
|
|
70
|
+
emit_event("started", pid: @pid)
|
|
65
71
|
|
|
66
72
|
install_signal_handlers
|
|
67
73
|
sync_window_size
|
|
@@ -78,6 +84,7 @@ module Harnex
|
|
|
78
84
|
_, status = Process.wait2(pid)
|
|
79
85
|
@term_signal = status.signaled? ? status.termsig : nil
|
|
80
86
|
@exit_code = status.exited? ? status.exitstatus : 128 + status.termsig
|
|
87
|
+
emit_exit_event
|
|
81
88
|
|
|
82
89
|
output_thread.join(1)
|
|
83
90
|
input_thread&.kill
|
|
@@ -91,6 +98,7 @@ module Harnex
|
|
|
91
98
|
cleanup_registry
|
|
92
99
|
@reader&.close unless @reader&.closed?
|
|
93
100
|
@output_log&.close unless @output_log&.closed?
|
|
101
|
+
@events_log&.close unless @events_log&.closed?
|
|
94
102
|
@writer&.close unless @writer&.closed?
|
|
95
103
|
end
|
|
96
104
|
|
|
@@ -109,8 +117,10 @@ module Harnex
|
|
|
109
117
|
started_at: @started_at.iso8601,
|
|
110
118
|
last_injected_at: @last_injected_at&.iso8601,
|
|
111
119
|
injected_count: @injected_count,
|
|
112
|
-
output_log_path: output_log_path
|
|
120
|
+
output_log_path: output_log_path,
|
|
121
|
+
events_log_path: events_log_path
|
|
113
122
|
}
|
|
123
|
+
payload.merge!(log_activity_snapshot)
|
|
114
124
|
payload[:description] = description if description
|
|
115
125
|
|
|
116
126
|
if watch
|
|
@@ -168,6 +178,7 @@ module Harnex
|
|
|
168
178
|
input_state: payload[:input_state],
|
|
169
179
|
force: payload[:force]
|
|
170
180
|
)
|
|
181
|
+
.tap { emit_send_event(text, force: payload[:force]) }
|
|
171
182
|
end
|
|
172
183
|
|
|
173
184
|
def sync_window_size
|
|
@@ -327,6 +338,14 @@ module Harnex
|
|
|
327
338
|
@output_log_failed = false
|
|
328
339
|
end
|
|
329
340
|
|
|
341
|
+
def prepare_events_log
|
|
342
|
+
@events_log&.close unless @events_log&.closed?
|
|
343
|
+
@events_log = File.open(events_log_path, "ab")
|
|
344
|
+
@events_log.sync = true
|
|
345
|
+
@events_log_failed = false
|
|
346
|
+
@events_log_seq = 0
|
|
347
|
+
end
|
|
348
|
+
|
|
330
349
|
def install_signal_handlers
|
|
331
350
|
%w[INT TERM HUP QUIT].each do |signal_name|
|
|
332
351
|
Signal.trap(signal_name) { forward_signal(signal_name) }
|
|
@@ -364,6 +383,60 @@ module Harnex
|
|
|
364
383
|
warn("harnex: failed to write output log #{output_log_path}: #{e.message}")
|
|
365
384
|
end
|
|
366
385
|
|
|
386
|
+
def emit_send_event(text, force:)
|
|
387
|
+
compact = text.to_s
|
|
388
|
+
truncated = compact.length > 200
|
|
389
|
+
preview = truncated ? "#{compact[0, 200]}…" : compact
|
|
390
|
+
emit_event("send", msg: preview, msg_truncated: truncated, forced: !!force)
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def emit_exit_event
|
|
394
|
+
payload = { code: @exit_code }
|
|
395
|
+
payload[:signal] = @term_signal if @term_signal
|
|
396
|
+
emit_event("exited", **payload)
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def emit_event(type, **payload)
|
|
400
|
+
@events_mutex.synchronize do
|
|
401
|
+
return unless @events_log
|
|
402
|
+
|
|
403
|
+
@events_log_seq += 1
|
|
404
|
+
event = {
|
|
405
|
+
schema_version: 1,
|
|
406
|
+
seq: @events_log_seq,
|
|
407
|
+
ts: Time.now.utc.iso8601,
|
|
408
|
+
id: id,
|
|
409
|
+
type: type
|
|
410
|
+
}.merge(payload)
|
|
411
|
+
@events_log.write(JSON.generate(event))
|
|
412
|
+
@events_log.write("\n")
|
|
413
|
+
@events_log.flush
|
|
414
|
+
end
|
|
415
|
+
rescue StandardError => e
|
|
416
|
+
return if defined?(@events_log_failed) && @events_log_failed
|
|
417
|
+
|
|
418
|
+
@events_log_failed = true
|
|
419
|
+
warn("harnex: failed to write events log #{events_log_path}: #{e.message}")
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def log_activity_snapshot
|
|
423
|
+
return { log_mtime: nil, log_idle_s: nil } unless File.file?(output_log_path)
|
|
424
|
+
return { log_mtime: nil, log_idle_s: nil } if File.size?(output_log_path).nil?
|
|
425
|
+
|
|
426
|
+
mtime = File.mtime(output_log_path)
|
|
427
|
+
idle_seconds = (Time.now - mtime).to_i
|
|
428
|
+
idle_seconds = 0 if idle_seconds.negative?
|
|
429
|
+
{
|
|
430
|
+
log_mtime: mtime.iso8601,
|
|
431
|
+
log_idle_s: idle_seconds
|
|
432
|
+
}
|
|
433
|
+
rescue StandardError
|
|
434
|
+
{
|
|
435
|
+
log_mtime: nil,
|
|
436
|
+
log_idle_s: nil
|
|
437
|
+
}
|
|
438
|
+
end
|
|
439
|
+
|
|
367
440
|
def screen_snapshot
|
|
368
441
|
@mutex.synchronize { @output_buffer.dup }
|
|
369
442
|
end
|
data/lib/harnex/version.rb
CHANGED
data/lib/harnex.rb
CHANGED
|
@@ -12,12 +12,15 @@ require_relative "harnex/runtime/inbox"
|
|
|
12
12
|
require_relative "harnex/runtime/file_change_hook"
|
|
13
13
|
require_relative "harnex/runtime/api_server"
|
|
14
14
|
require_relative "harnex/runtime/session"
|
|
15
|
+
require_relative "harnex/commands/watch"
|
|
16
|
+
require_relative "harnex/commands/watch_presets"
|
|
15
17
|
require_relative "harnex/commands/run"
|
|
16
18
|
require_relative "harnex/commands/send"
|
|
17
19
|
require_relative "harnex/commands/wait"
|
|
18
20
|
require_relative "harnex/commands/stop"
|
|
19
21
|
require_relative "harnex/commands/status"
|
|
20
22
|
require_relative "harnex/commands/logs"
|
|
23
|
+
require_relative "harnex/commands/events"
|
|
21
24
|
require_relative "harnex/commands/pane"
|
|
22
25
|
require_relative "harnex/commands/recipes"
|
|
23
26
|
require_relative "harnex/commands/guide"
|
data/skills/harnex/SKILL.md
CHANGED
|
@@ -1,348 +1,20 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: harnex
|
|
3
|
-
description:
|
|
4
|
-
allowed-tools: Bash(harnex *)
|
|
3
|
+
description: Deprecated alias for harnex-dispatch. Use harnex-dispatch for active harnex collaboration guidance.
|
|
5
4
|
---
|
|
6
5
|
|
|
7
|
-
#
|
|
6
|
+
# Deprecated Skill Alias
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
getting started walkthrough, and `harnex recipes` for tested workflow patterns.
|
|
8
|
+
`harnex` is deprecated.
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
messages to a peer agent**, **check session status**, **spawn worker sessions**,
|
|
15
|
-
and **wait for them to finish**.
|
|
10
|
+
Use `harnex-dispatch` as the canonical skill for harnex collaboration,
|
|
11
|
+
Fire & Watch dispatching, relay handling, and return-channel discipline.
|
|
16
12
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
Check environment variables to understand your role:
|
|
20
|
-
|
|
21
|
-
| Variable | Meaning |
|
|
22
|
-
|----------|---------|
|
|
23
|
-
| `HARNEX_SESSION_CLI` | Which CLI you are (`claude` or `codex`) |
|
|
24
|
-
| `HARNEX_ID` | Your session ID |
|
|
25
|
-
| `HARNEX_SESSION_REPO_ROOT` | Repo root this session is scoped to |
|
|
26
|
-
| `HARNEX_SESSION_ID` | Internal instance identifier |
|
|
27
|
-
| `HARNEX_SPAWNER_PANE` | Tmux pane ID (`%N`) of whoever spawned this session |
|
|
28
|
-
|
|
29
|
-
If these are set, you are **inside a harnex session** and can send messages to
|
|
30
|
-
peer sessions or spawn new worker sessions.
|
|
31
|
-
|
|
32
|
-
`HARNEX_SPAWNER_PANE` is the stable tmux pane ID of the invoker — use it to
|
|
33
|
-
reach back to the session that launched you, even if that session is not
|
|
34
|
-
harnex-managed:
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
# Read the invoker's screen
|
|
38
|
-
tmux capture-pane -t "$HARNEX_SPAWNER_PANE" -p
|
|
39
|
-
|
|
40
|
-
# Type into the invoker
|
|
41
|
-
tmux send-keys -t "$HARNEX_SPAWNER_PANE" "done — results in /tmp/result.md" Enter
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
## Mode preference
|
|
45
|
-
|
|
46
|
-
When starting another agent session for the user, default to a visible tmux
|
|
47
|
-
session via `harnex run <cli> --tmux`. That is the preferred interactive mode
|
|
48
|
-
because the user can watch the peer's work live.
|
|
49
|
-
|
|
50
|
-
Use other modes only when the user asks for them or when visibility is not
|
|
51
|
-
wanted:
|
|
52
|
-
|
|
53
|
-
- prefer `--tmux` over a hidden foreground PTY for peer-agent work
|
|
54
|
-
- use plain foreground `harnex run` only when the current terminal is meant to
|
|
55
|
-
become that peer's UI
|
|
56
|
-
- use `--detach` only for explicitly headless/background workflows
|
|
57
|
-
|
|
58
|
-
## Return channel first
|
|
59
|
-
|
|
60
|
-
Before you start a peer session or send it work, decide how the result will get
|
|
61
|
-
back to you.
|
|
62
|
-
|
|
63
|
-
Preferred pattern when you are inside harnex:
|
|
64
|
-
- Use your own `HARNEX_ID` as the return address
|
|
65
|
-
- Tell the peer to send its final result back with `harnex send --id <YOUR_ID>`
|
|
66
|
-
- Wait for the peer's reply; do not rely on scraping logs or tmux panes as the
|
|
67
|
-
primary way to collect the answer
|
|
68
|
-
|
|
69
|
-
Fallback when you are not inside harnex:
|
|
70
|
-
- Define another explicit return channel before delegating, such as a known file
|
|
71
|
-
path in the repo
|
|
72
|
-
|
|
73
|
-
Do not launch a worker/reviewer without an explicit completion contract.
|
|
74
|
-
|
|
75
|
-
## Two rules for every send
|
|
76
|
-
|
|
77
|
-
**1. Keep messages short — use file references for long prompts.**
|
|
78
|
-
Long inline prompts can stall delivery (PTY buffer limits) and break shell
|
|
79
|
-
quoting. Instead, write the full task to a file and tell the peer to read it:
|
|
80
|
-
|
|
81
|
-
```bash
|
|
82
|
-
# Write the task to a file
|
|
83
|
-
cat > /tmp/task-impl1.md <<'EOF'
|
|
84
|
-
Implement phase 2 from koder/plans/03_output_streaming.md.
|
|
85
|
-
... detailed instructions ...
|
|
86
|
-
EOF
|
|
87
|
-
|
|
88
|
-
# Send a short message pointing to it
|
|
89
|
-
harnex send --id impl-1 --message "Read and execute /tmp/task-impl1.md. When done, send results back: harnex send --id $HARNEX_ID --message '<summary>'"
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
If the task is already written down (a plan file, issue, koder doc), just
|
|
93
|
-
reference it directly — no need for a temp file.
|
|
94
|
-
|
|
95
|
-
**2. Always tell the peer how to reply.**
|
|
96
|
-
Every delegated task must include a return path. Without it, the peer finishes
|
|
97
|
-
silently and you have no way to collect the result:
|
|
98
|
-
|
|
99
|
-
```bash
|
|
100
|
-
# Always end with the reply instruction
|
|
101
|
-
harnex send --id impl-1 --message "Review src/auth.rb. When done: harnex send --id $HARNEX_ID --message '<your findings>'"
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
## Core commands
|
|
105
|
-
|
|
106
|
-
### Send a message to a peer agent
|
|
107
|
-
|
|
108
|
-
```bash
|
|
109
|
-
harnex send --id <ID> --message "<text>"
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
- `--id` targets a specific session by its unique ID
|
|
113
|
-
- `--message` is the prompt text injected into the peer's terminal
|
|
114
|
-
- Message is auto-submitted (peer receives it as a prompt)
|
|
115
|
-
- `--no-submit` types without pressing Enter
|
|
116
|
-
- `--force` sends even if peer UI is not at a prompt (bypasses queue)
|
|
117
|
-
- `--submit-only` sends only Enter (submit what's already in the input box)
|
|
118
|
-
- `--wait-for-idle` blocks until the agent finishes processing (prompt→busy→prompt)
|
|
119
|
-
- `--no-wait` returns immediately with a message_id (don't wait for delivery)
|
|
120
|
-
- `--cli` filters by CLI type when multiple sessions share resolution scope
|
|
121
|
-
|
|
122
|
-
When the target agent is busy, the message is **queued** (HTTP 202) and
|
|
123
|
-
delivered automatically when the agent returns to a prompt. The sender polls
|
|
124
|
-
until delivery completes using one overall `--timeout` budget (default 120s).
|
|
125
|
-
|
|
126
|
-
### Atomic send+wait (recommended for orchestration)
|
|
127
|
-
|
|
128
|
-
Use `--wait-for-idle` instead of separate `send` + `sleep` + `wait` commands:
|
|
13
|
+
If you are installing skills, use:
|
|
129
14
|
|
|
130
15
|
```bash
|
|
131
|
-
|
|
132
|
-
harnex send --id cx-1 --message "implement the plan"
|
|
133
|
-
sleep 5
|
|
134
|
-
harnex wait --id cx-1 --until prompt --timeout 600
|
|
135
|
-
|
|
136
|
-
# Use:
|
|
137
|
-
harnex send --id cx-1 --message "implement the plan" --wait-for-idle --timeout 600
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
This eliminates the race condition where `wait --until prompt` sees the stale
|
|
141
|
-
prompt state before the agent starts working. The `--timeout` budget covers
|
|
142
|
-
the entire lifecycle (lookup + send + idle wait).
|
|
143
|
-
|
|
144
|
-
**Multi-line messages** (when a file reference isn't practical): use a heredoc:
|
|
145
|
-
|
|
146
|
-
```bash
|
|
147
|
-
harnex send --id worker-1 --message "$(cat <<'EOF'
|
|
148
|
-
Line one of the message.
|
|
149
|
-
Line two of the message.
|
|
150
|
-
EOF
|
|
151
|
-
)"
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
### Check session status
|
|
155
|
-
|
|
156
|
-
```bash
|
|
157
|
-
harnex status # sessions for current repo
|
|
158
|
-
harnex status --all # sessions across all repos
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
Shows live sessions with their ID, CLI, port, PID, age, and input state.
|
|
162
|
-
|
|
163
|
-
### Inspect a specific session
|
|
164
|
-
|
|
165
|
-
```bash
|
|
166
|
-
harnex status --id <ID> --json
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
Returns JSON with input state, agent state, inbox stats, description,
|
|
170
|
-
watch config, and timestamps.
|
|
171
|
-
|
|
172
|
-
### Only when explicitly requested: spawn a detached worker session
|
|
173
|
-
|
|
174
|
-
```bash
|
|
175
|
-
# Headless (no terminal)
|
|
176
|
-
harnex run codex --id impl-1 --detach -- --cd /path/to/worktree
|
|
177
|
-
|
|
178
|
-
# In a tmux window (observable)
|
|
179
|
-
harnex run codex --id impl-1 --tmux cx-p1 -- --cd /path/to/worktree
|
|
16
|
+
harnex skills install harnex-dispatch
|
|
180
17
|
```
|
|
181
18
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
- `--tmux NAME` sets a custom window title (keep names terse: `cx-p3`, `cl-r3`)
|
|
185
|
-
- `--context TEXT` sets an initial prompt with session ID auto-included
|
|
186
|
-
- `--description TEXT` stores a short session description in the registry/API
|
|
187
|
-
- Returns immediately; use `harnex send` to inject work, `harnex wait` to block
|
|
188
|
-
|
|
189
|
-
Do this only if the user explicitly asks for detached/background execution.
|
|
190
|
-
|
|
191
|
-
#### Using `--context` to orient spawned agents
|
|
192
|
-
|
|
193
|
-
`--context` prepends a context string as the agent's initial prompt, with the
|
|
194
|
-
session ID automatically included as `[harnex session id=<ID>]`. The spawner
|
|
195
|
-
decides what context to provide — harnex only adds the session ID.
|
|
196
|
-
|
|
197
|
-
```bash
|
|
198
|
-
# Fire-and-forget: give the task upfront
|
|
199
|
-
harnex run codex --id impl-1 --tmux cx-p1 \
|
|
200
|
-
--context "Implement the feature in koder/plans/03_auth.md. Commit when done." \
|
|
201
|
-
-- --cd /path/to/worktree
|
|
202
|
-
|
|
203
|
-
# Fire-and-wait: give context, then send work separately
|
|
204
|
-
harnex run codex --id reviewer --tmux cx-rv \
|
|
205
|
-
--context "You are a code reviewer. Wait for instructions via harnex relay messages." \
|
|
206
|
-
-- --cd /path/to/repo
|
|
207
|
-
harnex send --id reviewer --message "Review the changes in src/auth.rb"
|
|
208
|
-
harnex wait --id reviewer
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
The context string is the spawner's responsibility — tailor it to the use case.
|
|
212
|
-
|
|
213
|
-
### Wait for a session to exit
|
|
214
|
-
|
|
215
|
-
```bash
|
|
216
|
-
harnex wait --id impl-1
|
|
217
|
-
harnex wait --id impl-1 --timeout 300
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
Blocks until the session process exits. Returns JSON with exit code and timing.
|
|
221
|
-
Exit code 124 on timeout.
|
|
222
|
-
|
|
223
|
-
## Relay headers
|
|
224
|
-
|
|
225
|
-
When you send from inside a harnex session to a **different** session, harnex
|
|
226
|
-
automatically prepends a relay header:
|
|
227
|
-
|
|
228
|
-
```
|
|
229
|
-
[harnex relay from=claude id=supervisor at=2026-03-14T12:00:00+04:00]
|
|
230
|
-
<your message>
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
The peer sees this header and knows the message came from another agent. When
|
|
234
|
-
you **receive** a message with a `[harnex relay ...]` header, treat it as a
|
|
235
|
-
prompt from the peer agent — read the body and respond to it.
|
|
236
|
-
|
|
237
|
-
Control relay behavior:
|
|
238
|
-
- `--relay` forces the header even outside a session
|
|
239
|
-
- `--no-relay` suppresses the header
|
|
240
|
-
|
|
241
|
-
## Collaboration patterns
|
|
242
|
-
|
|
243
|
-
### Reply to a peer
|
|
244
|
-
|
|
245
|
-
When the user (or a relay message) asks you to reply to the other agent:
|
|
246
|
-
|
|
247
|
-
```bash
|
|
248
|
-
harnex send --id <TARGET_ID> --message "Your response here"
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
### Delegate work and get the answer back
|
|
252
|
-
|
|
253
|
-
When you ask a peer agent to do work, include the return path in the task
|
|
254
|
-
itself.
|
|
255
|
-
|
|
256
|
-
Preferred when you are inside harnex:
|
|
257
|
-
|
|
258
|
-
```bash
|
|
259
|
-
harnex send --id reviewer --message "$(cat <<EOF
|
|
260
|
-
Review the current working tree.
|
|
261
|
-
|
|
262
|
-
Return your final findings to me with:
|
|
263
|
-
harnex send --id $HARNEX_ID --message '<findings>'
|
|
264
|
-
|
|
265
|
-
Use findings-first format with file paths and line numbers where possible.
|
|
266
|
-
EOF
|
|
267
|
-
)"
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
Then wait for the peer to answer you. Do not assume you can reconstruct the
|
|
271
|
-
result later from detached logs or tmux capture.
|
|
272
|
-
|
|
273
|
-
### Supervisor pattern
|
|
274
|
-
|
|
275
|
-
Use this only when the user explicitly wants detached/background workers.
|
|
276
|
-
|
|
277
|
-
A supervisor session spawns workers, sends them tasks, and waits for completion:
|
|
278
|
-
|
|
279
|
-
```bash
|
|
280
|
-
# Spawn workers
|
|
281
|
-
harnex run codex --id impl-1 --tmux cx-p1 -- --cd ~/repo/wt-feature-a
|
|
282
|
-
harnex run codex --id impl-2 --tmux cx-p2 -- --cd ~/repo/wt-feature-b
|
|
283
|
-
|
|
284
|
-
# Send work and wait for completion (atomic)
|
|
285
|
-
harnex send --id impl-1 --message "implement plan 150" --wait-for-idle --timeout 600
|
|
286
|
-
harnex send --id impl-2 --message "implement plan 151" --wait-for-idle --timeout 600
|
|
287
|
-
|
|
288
|
-
# Review phase
|
|
289
|
-
harnex run claude --id review-1 --tmux cl-r1
|
|
290
|
-
harnex send --id review-1 --message "review changes in wt-feature-a"
|
|
291
|
-
harnex wait --id review-1
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
### File watch hook
|
|
295
|
-
|
|
296
|
-
Sessions can watch a shared file (e.g. `--watch ./tmp/tick.jsonl`). When the
|
|
297
|
-
file changes, harnex injects a `file-change-hook: read <path>` message. If you
|
|
298
|
-
receive this hook, read the file and act on its contents.
|
|
299
|
-
|
|
300
|
-
## Buddy pattern — accountability for long-running work
|
|
301
|
-
|
|
302
|
-
For any work that will take a long time (overnight pipelines, multi-hour
|
|
303
|
-
implementations, unattended batch jobs), spawn a **buddy** — a second harnex
|
|
304
|
-
session that watches the worker and nudges it if it stalls.
|
|
305
|
-
|
|
306
|
-
```bash
|
|
307
|
-
# Spawn the worker
|
|
308
|
-
harnex run codex --id worker-42 --tmux worker-42
|
|
309
|
-
harnex send --id worker-42 --message "Read and execute /tmp/task-42.md"
|
|
310
|
-
|
|
311
|
-
# Spawn a buddy to watch it
|
|
312
|
-
harnex run claude --id buddy-42 --tmux buddy-42
|
|
313
|
-
harnex send --id buddy-42 --message "Read and execute /tmp/buddy-42.md"
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
The buddy's prompt tells it: poll `harnex pane` and `harnex status` every N
|
|
317
|
-
minutes, nudge with `harnex send` if the worker stalls, report back to
|
|
318
|
-
`$HARNEX_SPAWNER_PANE` when done.
|
|
319
|
-
|
|
320
|
-
See `recipes/03_buddy.md` for the full pattern.
|
|
321
|
-
|
|
322
|
-
**When to spawn a buddy:**
|
|
323
|
-
- The user says "do this overnight" or "run this while I'm away"
|
|
324
|
-
- The task is expected to take more than 30 minutes unattended
|
|
325
|
-
- The user explicitly asks for a buddy or accountability partner
|
|
326
|
-
|
|
327
|
-
**When NOT to spawn a buddy:**
|
|
328
|
-
- Short tasks you're actively watching
|
|
329
|
-
- The user hasn't asked for long-running autonomy
|
|
330
|
-
|
|
331
|
-
## Important rules
|
|
332
|
-
|
|
333
|
-
1. **Always confirm with the user before sending** unless they explicitly asked
|
|
334
|
-
you to send a specific message. Sending injects a prompt into the peer's
|
|
335
|
-
terminal — it's an action visible to others.
|
|
336
|
-
2. **Never auto-loop** relay conversations. One send per user request unless
|
|
337
|
-
told otherwise.
|
|
338
|
-
3. **Check status first** if unsure whether a peer is running: `harnex status`
|
|
339
|
-
4. **Use `--force` sparingly** — it bypasses the inbox queue and adapter
|
|
340
|
-
readiness checks. Can corrupt peer input if it's mid-response.
|
|
341
|
-
5. **Relay headers are automatic** when sending from inside a session. Don't
|
|
342
|
-
manually prepend them.
|
|
343
|
-
6. When composing a message to send, be concise and actionable — the peer agent
|
|
344
|
-
receives it as a prompt and will act on it.
|
|
345
|
-
7. Do not spawn detached or tmux-backed sessions unless the user explicitly
|
|
346
|
-
asked for detached/background execution.
|
|
347
|
-
8. Before delegating work, define the result return path. Prefer a reply back to
|
|
348
|
-
your own `HARNEX_ID` over logs, pane capture, or other indirect collection.
|
|
19
|
+
Compatibility note: `harnex skills install harnex` remains supported and
|
|
20
|
+
installs `harnex-dispatch`.
|
|
@@ -8,9 +8,17 @@ description: Spawn an accountability partner for long-running harnex sessions. U
|
|
|
8
8
|
For any long-running or unattended work, spawn a **buddy** — a second harnex
|
|
9
9
|
agent that watches the worker and nudges it if it stalls.
|
|
10
10
|
|
|
11
|
+
For plain stall recovery (force-resume on inactivity), prefer
|
|
12
|
+
`harnex run --watch --preset impl`. Use a buddy when you need reasoning that
|
|
13
|
+
policy checks cannot provide (doc drift, semantic checks, multi-session
|
|
14
|
+
correlation).
|
|
15
|
+
|
|
11
16
|
The buddy is an LLM, so it has intelligence for free. It reads the worker's
|
|
12
17
|
screen, reasons about whether it's stuck, and composes a meaningful nudge.
|
|
13
18
|
|
|
19
|
+
Dispatch workers via `harnex-dispatch` first. This skill owns only buddy
|
|
20
|
+
behavior after the worker is already running.
|
|
21
|
+
|
|
14
22
|
## When to activate
|
|
15
23
|
|
|
16
24
|
- User says "do this overnight" or "run this while I'm away"
|
|
@@ -20,12 +28,11 @@ screen, reasons about whether it's stuck, and composes a meaningful nudge.
|
|
|
20
28
|
|
|
21
29
|
## Spawn the buddy
|
|
22
30
|
|
|
23
|
-
After dispatching the worker
|
|
31
|
+
After dispatching the worker with `harnex-dispatch`, spawn a buddy alongside
|
|
32
|
+
it. Keep ID/tmux naming consistent with `harnex-dispatch` (`--tmux` matches
|
|
33
|
+
`--id`):
|
|
24
34
|
|
|
25
35
|
```bash
|
|
26
|
-
# Worker already running
|
|
27
|
-
harnex run codex --id worker-42 --tmux worker-42
|
|
28
|
-
|
|
29
36
|
# Spawn its buddy
|
|
30
37
|
harnex run claude --id buddy-42 --tmux buddy-42
|
|
31
38
|
```
|
|
@@ -36,17 +43,17 @@ Write a task file with the watching instructions, then send it:
|
|
|
36
43
|
|
|
37
44
|
```bash
|
|
38
45
|
cat > /tmp/buddy-42.md <<'EOF'
|
|
39
|
-
You are an accountability partner for harnex session `
|
|
46
|
+
You are an accountability partner for harnex session `cx-impl-42`.
|
|
40
47
|
|
|
41
48
|
Your job:
|
|
42
49
|
1. Every 5 minutes, check on the worker:
|
|
43
|
-
- `harnex pane --id
|
|
44
|
-
- `harnex status --id
|
|
50
|
+
- `harnex pane --id cx-impl-42 --lines 20`
|
|
51
|
+
- `harnex status --id cx-impl-42 --json`
|
|
45
52
|
2. If the worker appears stuck at a prompt for more than 10 minutes
|
|
46
53
|
with no progress, nudge it:
|
|
47
|
-
- `harnex send --id
|
|
54
|
+
- `harnex send --id cx-impl-42 --message "You appear to have stalled. Continue with your current task."`
|
|
48
55
|
3. If the worker has exited, report back to the invoker:
|
|
49
|
-
- `tmux send-keys -t "$HARNEX_SPAWNER_PANE" "
|
|
56
|
+
- `tmux send-keys -t "$HARNEX_SPAWNER_PANE" "cx-impl-42 has exited. Check results." Enter`
|
|
50
57
|
4. Keep watching until the worker finishes or is stopped.
|
|
51
58
|
|
|
52
59
|
Do not interfere with work in progress. Only nudge when clearly stalled.
|
|
@@ -76,12 +83,8 @@ The invoker does NOT need to be a harnex session. It just needs to be in tmux.
|
|
|
76
83
|
|
|
77
84
|
## Naming convention
|
|
78
85
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
| Worker | `worker-NN` | `worker-42` |
|
|
82
|
-
| Buddy | `buddy-NN` | `buddy-42` |
|
|
83
|
-
|
|
84
|
-
Match the buddy ID to the worker it watches.
|
|
86
|
+
Use naming from `harnex-dispatch`: set `--tmux <same-as-id>` for every
|
|
87
|
+
session and keep the buddy ID paired with the worker step ID.
|
|
85
88
|
|
|
86
89
|
## Cleanup
|
|
87
90
|
|
|
@@ -93,6 +96,8 @@ harnex stop --id buddy-42
|
|
|
93
96
|
|
|
94
97
|
## Notes
|
|
95
98
|
|
|
99
|
+
- For chain orchestration, phase gates, and the 5-concurrent parallel planning
|
|
100
|
+
cap, see `harnex-chain`.
|
|
96
101
|
- One buddy per worker, or one buddy watching multiple sessions
|
|
97
102
|
- The buddy is a regular harnex session — stop, inspect, log it like any other
|
|
98
103
|
- Tune polling and thresholds in the buddy's prompt, not in harnex config
|