harnex 0.2.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 +7 -0
- data/GUIDE.md +242 -0
- data/LICENSE +21 -0
- data/README.md +119 -0
- data/TECHNICAL.md +595 -0
- data/bin/harnex +18 -0
- data/lib/harnex/adapters/base.rb +134 -0
- data/lib/harnex/adapters/claude.rb +105 -0
- data/lib/harnex/adapters/codex.rb +112 -0
- data/lib/harnex/adapters/generic.rb +14 -0
- data/lib/harnex/adapters.rb +32 -0
- data/lib/harnex/cli.rb +115 -0
- data/lib/harnex/commands/guide.rb +23 -0
- data/lib/harnex/commands/logs.rb +184 -0
- data/lib/harnex/commands/pane.rb +251 -0
- data/lib/harnex/commands/recipes.rb +104 -0
- data/lib/harnex/commands/run.rb +384 -0
- data/lib/harnex/commands/send.rb +415 -0
- data/lib/harnex/commands/skills.rb +163 -0
- data/lib/harnex/commands/status.rb +171 -0
- data/lib/harnex/commands/stop.rb +127 -0
- data/lib/harnex/commands/wait.rb +165 -0
- data/lib/harnex/core.rb +286 -0
- data/lib/harnex/runtime/api_server.rb +187 -0
- data/lib/harnex/runtime/file_change_hook.rb +111 -0
- data/lib/harnex/runtime/inbox.rb +207 -0
- data/lib/harnex/runtime/message.rb +23 -0
- data/lib/harnex/runtime/session.rb +380 -0
- data/lib/harnex/runtime/session_state.rb +55 -0
- data/lib/harnex/version.rb +3 -0
- data/lib/harnex/watcher/inotify.rb +43 -0
- data/lib/harnex/watcher/polling.rb +92 -0
- data/lib/harnex/watcher.rb +24 -0
- data/lib/harnex.rb +25 -0
- data/recipes/01_fire_and_watch.md +82 -0
- data/recipes/02_chain_implement.md +115 -0
- data/skills/chain-implement/SKILL.md +234 -0
- data/skills/close/SKILL.md +47 -0
- data/skills/dispatch/SKILL.md +171 -0
- data/skills/harnex/SKILL.md +304 -0
- data/skills/open/SKILL.md +32 -0
- metadata +88 -0
data/TECHNICAL.md
ADDED
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
# Technical Reference
|
|
2
|
+
|
|
3
|
+
For what harnex is and whether you'd want it, see
|
|
4
|
+
[README.md](README.md). This document covers commands, patterns,
|
|
5
|
+
and internals.
|
|
6
|
+
|
|
7
|
+
## Commands
|
|
8
|
+
|
|
9
|
+
### `harnex run` — Start an agent
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
harnex run codex
|
|
13
|
+
harnex run claude --id review
|
|
14
|
+
harnex run codex -- --cd ~/other/repo
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
| Flag | What it does |
|
|
18
|
+
|-----------------|---------------------------------------|
|
|
19
|
+
| `--id ID` | Name this session (default: random) |
|
|
20
|
+
| `--description` | Store a short session description |
|
|
21
|
+
| `--detach` | Run in background, no terminal |
|
|
22
|
+
| `--tmux [NAME]` | Run in a tmux window you can watch |
|
|
23
|
+
| `--host HOST` | Bind a specific API host |
|
|
24
|
+
| `--port PORT` | Force a specific API port |
|
|
25
|
+
| `--context TXT` | Give the agent a task on startup |
|
|
26
|
+
| `--watch PATH` | Notify agent when a file changes |
|
|
27
|
+
| `--timeout SEC` | Wait budget for detached registration |
|
|
28
|
+
|
|
29
|
+
### `harnex send` — Talk to a running agent
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
harnex send --id worker --message "implement plan A"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
| Flag | What it does |
|
|
36
|
+
|-------------------|----------------------------------------|
|
|
37
|
+
| `--id ID` | Which agent to talk to |
|
|
38
|
+
| `--message` | The message text |
|
|
39
|
+
| `--wait-for-idle` | Block until agent finishes processing |
|
|
40
|
+
| `--repo PATH` | Resolve session from a repo root |
|
|
41
|
+
| `--cli CLI` | Filter by CLI type |
|
|
42
|
+
| `--submit-only` | Just press Enter (no new text) |
|
|
43
|
+
| `--no-submit` | Type the text but don't press Enter |
|
|
44
|
+
| `--force` | Send even if the agent looks busy |
|
|
45
|
+
| `--no-wait` | Don't wait for delivery confirmation |
|
|
46
|
+
| `--relay` | Force relay header formatting |
|
|
47
|
+
| `--no-relay` | Suppress automatic relay headers |
|
|
48
|
+
| `--port` / `--token` | Send directly to a known API port |
|
|
49
|
+
| `--timeout` | Overall wait budget |
|
|
50
|
+
|
|
51
|
+
### `harnex stop` — Ask an agent to stop
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
harnex stop --id worker
|
|
55
|
+
harnex stop --id worker --timeout 5
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Sends the adapter-specific stop sequence. Retries transient
|
|
59
|
+
failures for up to the given timeout.
|
|
60
|
+
|
|
61
|
+
### `harnex status` — See running agents
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
ID CLI AGE STATE
|
|
65
|
+
worker codex 36s ago prompt
|
|
66
|
+
review claude 8s ago busy
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Use `--json` for full payloads. Use `--all` for all repos.
|
|
70
|
+
|
|
71
|
+
### `harnex wait` — Wait for an agent to finish
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
harnex wait --id worker
|
|
75
|
+
harnex wait --id worker --until prompt --timeout 300
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### `harnex logs` — Read session transcripts
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
harnex logs --id worker --lines 50
|
|
82
|
+
harnex logs --id worker --follow
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### `harnex pane` — Capture a tmux screen snapshot
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
harnex pane --id worker --lines 40
|
|
89
|
+
harnex pane --id worker --follow
|
|
90
|
+
harnex pane --id worker --json
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Notes:
|
|
94
|
+
- Works only for tmux-backed sessions
|
|
95
|
+
- Resolves against the live tmux pane target, not just the harnex session ID
|
|
96
|
+
- If a session was started from another worktree, `pane` can fall back to a
|
|
97
|
+
unique cross-repo ID match; use `--repo` when the same ID exists in more
|
|
98
|
+
than one repo root
|
|
99
|
+
|
|
100
|
+
## Usage Patterns
|
|
101
|
+
|
|
102
|
+
### Atomic send+wait
|
|
103
|
+
|
|
104
|
+
Use `--wait-for-idle` instead of separate send + sleep + wait:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# Replaces: send → sleep 5 → wait --until prompt
|
|
108
|
+
harnex send --id cx-1 --message "implement the plan" --wait-for-idle --timeout 600
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Agents talking to each other
|
|
112
|
+
|
|
113
|
+
When one harnex agent sends to another, the receiver sees a
|
|
114
|
+
relay header automatically:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
[harnex relay from=claude id=supervisor at=2026-03-14T12:00]
|
|
118
|
+
implement A
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Messages queue when the agent is busy and auto-deliver when ready.
|
|
122
|
+
|
|
123
|
+
### Background agents
|
|
124
|
+
|
|
125
|
+
**tmux** (observable):
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
harnex run codex --id worker --tmux cx-w1
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Headless** (no terminal):
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
harnex run codex --id worker --detach
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Supervisor pattern
|
|
138
|
+
|
|
139
|
+
One agent manages others:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
harnex run codex --id impl-1 --tmux cx-p1 -- --cd ~/wt-a
|
|
143
|
+
harnex run codex --id impl-2 --tmux cx-p2 -- --cd ~/wt-b
|
|
144
|
+
|
|
145
|
+
harnex send --id impl-1 --message "implement feature A" --wait-for-idle --timeout 600
|
|
146
|
+
harnex send --id impl-2 --message "implement feature B" --wait-for-idle --timeout 600
|
|
147
|
+
|
|
148
|
+
harnex run claude --id review --tmux cl-r1
|
|
149
|
+
harnex send --id review --message "review changes" --wait-for-idle --timeout 300
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Context on startup
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
harnex run codex --id impl-1 --tmux cx-p1 \
|
|
156
|
+
--context "Implement the auth feature. Commit when done." \
|
|
157
|
+
-- --cd ~/repo/worktree
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### File watching
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
harnex run codex --id worker --watch ./tmp/status.jsonl
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Agent gets notified when the file changes. File doesn't need to
|
|
167
|
+
exist at startup.
|
|
168
|
+
|
|
169
|
+
## Architecture
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
bin/harnex
|
|
173
|
+
│
|
|
174
|
+
└── lib/harnex.rb (loader)
|
|
175
|
+
│
|
|
176
|
+
├── CLI dispatch: run / send / status / wait / stop
|
|
177
|
+
├── Runner spawn sessions (foreground/detach/tmux)
|
|
178
|
+
├── Sender resolve target, inject text
|
|
179
|
+
├── Status list live sessions
|
|
180
|
+
├── Waiter block until session exits
|
|
181
|
+
├── Stopper send stop sequence
|
|
182
|
+
│
|
|
183
|
+
├── Session PTY lifecycle, HTTP server, registry
|
|
184
|
+
├── SessionState state machine (prompt/busy/blocked)
|
|
185
|
+
├── Inbox per-session message queue
|
|
186
|
+
├── Message queued message struct
|
|
187
|
+
│
|
|
188
|
+
├── FileChangeHook inotify file watcher
|
|
189
|
+
└── LinuxInotify raw inotify via Fiddle
|
|
190
|
+
|
|
191
|
+
lib/harnex/adapters/
|
|
192
|
+
├── base.rb adapter interface
|
|
193
|
+
├── generic.rb fallback adapter for any CLI
|
|
194
|
+
├── codex.rb codex-specific behavior
|
|
195
|
+
└── claude.rb claude-specific behavior
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Session Lifecycle
|
|
199
|
+
|
|
200
|
+
Foreground execution is the default operating mode for a human directly using
|
|
201
|
+
`harnex run`. For agent-to-agent delegation, a visible tmux session is the
|
|
202
|
+
preferred interactive mode because it keeps the peer's work observable.
|
|
203
|
+
|
|
204
|
+
Headless/background execution (`--detach`) should still be treated as opt-in.
|
|
205
|
+
|
|
206
|
+
When you run `harnex run codex --id worker`:
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
1. Parse wrapper options, build adapter
|
|
210
|
+
2. Validate the target binary before spawn
|
|
211
|
+
3. Spawn the agent CLI under a PTY (pseudoterminal)
|
|
212
|
+
4. Generate a random bearer token for API auth
|
|
213
|
+
5. Pick a port:
|
|
214
|
+
hash(repo_root + id) % port_span + base_port
|
|
215
|
+
walk forward until a free port is found
|
|
216
|
+
6. Start HTTP server on 127.0.0.1:<port>
|
|
217
|
+
7. Write registry file:
|
|
218
|
+
~/.local/state/harnex/sessions/<repo_hash>--<id>.json
|
|
219
|
+
and open transcript file:
|
|
220
|
+
~/.local/state/harnex/output/<repo_hash>--<id>.log
|
|
221
|
+
8. Start background threads:
|
|
222
|
+
- PTY reader (screen buffer)
|
|
223
|
+
- State machine (adapter parses screen for state)
|
|
224
|
+
- Inbox delivery (queue -> inject when prompt)
|
|
225
|
+
- File watcher (if --watch was given)
|
|
226
|
+
9. Relay terminal I/O between user and agent
|
|
227
|
+
10. On exit: clean up registry, write exit status
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## PTY and Screen Parsing
|
|
231
|
+
|
|
232
|
+
Harnex spawns the agent inside a pseudoterminal (PTY). This
|
|
233
|
+
preserves the agent's full terminal UI while letting harnex:
|
|
234
|
+
|
|
235
|
+
- Read screen output into a ring buffer
|
|
236
|
+
- Feed the buffer to the adapter for state detection
|
|
237
|
+
- Inject text by writing to the PTY input
|
|
238
|
+
|
|
239
|
+
The screen text includes raw terminal escape sequences. The
|
|
240
|
+
adapter's `normalized_screen_text` strips ANSI and OSC codes
|
|
241
|
+
before parsing.
|
|
242
|
+
|
|
243
|
+
## Adapter Contract
|
|
244
|
+
|
|
245
|
+
Each adapter in `lib/harnex/adapters/` implements:
|
|
246
|
+
|
|
247
|
+
| Method | Purpose |
|
|
248
|
+
|---------------------------|---------------------------------|
|
|
249
|
+
| `base_command` | CLI args to launch the agent |
|
|
250
|
+
| `input_state(screen)` | Parse screen -> state hash |
|
|
251
|
+
| `build_send_payload(...)` | Build injection with submit |
|
|
252
|
+
| `inject_exit(writer)` | Send adapter-specific stop text |
|
|
253
|
+
| `infer_repo_path(argv)` | Extract repo path from CLI args |
|
|
254
|
+
| `wait_for_sendable(...)` | Wait strategy before sending |
|
|
255
|
+
|
|
256
|
+
### Input States
|
|
257
|
+
|
|
258
|
+
The adapter reads the screen and returns a state hash:
|
|
259
|
+
|
|
260
|
+
| State | `input_ready` | Meaning |
|
|
261
|
+
|--------------------------|---------------|-----------------------|
|
|
262
|
+
| `prompt` | `true` | Ready for input |
|
|
263
|
+
| `session` | `nil` | Agent is working |
|
|
264
|
+
| `workspace-trust-prompt` | `false` | Needs Enter to confirm|
|
|
265
|
+
| `confirmation` | `false` | Modal confirmation |
|
|
266
|
+
| `unknown` | `nil` | Can't determine |
|
|
267
|
+
|
|
268
|
+
### Codex Adapter
|
|
269
|
+
|
|
270
|
+
- Launches with `--no-alt-screen` for inline screen output
|
|
271
|
+
- Detects prompt by looking for `›` prefix in recent lines
|
|
272
|
+
- Multi-step submit: types text, then sends Enter after a
|
|
273
|
+
75ms delay (lets the UI process the input)
|
|
274
|
+
|
|
275
|
+
### Claude Adapter
|
|
276
|
+
|
|
277
|
+
- Detects workspace trust prompt ("Quick safety check")
|
|
278
|
+
- Allows `--submit-only` to clear the trust prompt
|
|
279
|
+
- Detects prompt via `--INSERT--` marker or `›` prefix
|
|
280
|
+
- Multi-step submit: types text, then sends Enter after a
|
|
281
|
+
short delay so pasted prompts are actually submitted
|
|
282
|
+
|
|
283
|
+
## State Machine
|
|
284
|
+
|
|
285
|
+
`SessionState` tracks the agent's readiness:
|
|
286
|
+
|
|
287
|
+
```
|
|
288
|
+
┌──────────┐
|
|
289
|
+
┌─────────│ unknown │──────────┐
|
|
290
|
+
│ └──────────┘ │
|
|
291
|
+
▼ ▼
|
|
292
|
+
┌──────────┐ screen change ┌──────────┐
|
|
293
|
+
│ prompt │ <───────────────── │ busy │
|
|
294
|
+
│ │ ────────────────> │ │
|
|
295
|
+
└──────────┘ screen change └──────────┘
|
|
296
|
+
│ │
|
|
297
|
+
▼ ▼
|
|
298
|
+
┌──────────┐ ┌──────────┐
|
|
299
|
+
│ blocked │ │ blocked │
|
|
300
|
+
└──────────┘ └──────────┘
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
- Uses a Mutex + ConditionVariable for thread-safe access
|
|
304
|
+
- `wait_for_prompt(timeout)` blocks until state == `:prompt`
|
|
305
|
+
- State is updated by the PTY reader thread calling the
|
|
306
|
+
adapter's `input_state` on each screen change
|
|
307
|
+
|
|
308
|
+
## Inbox and Message Queue
|
|
309
|
+
|
|
310
|
+
Each session has an `Inbox` with a background delivery thread:
|
|
311
|
+
|
|
312
|
+
```
|
|
313
|
+
harnex send ──> POST /send ──> Inbox#enqueue
|
|
314
|
+
│
|
|
315
|
+
┌─────────────┴──────────────┐
|
|
316
|
+
│ │
|
|
317
|
+
prompt + empty queue? otherwise
|
|
318
|
+
│ │
|
|
319
|
+
deliver_now() push to @queue
|
|
320
|
+
return 200 return 202
|
|
321
|
+
│
|
|
322
|
+
delivery_loop thread
|
|
323
|
+
wait_for_prompt()
|
|
324
|
+
deliver_now()
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Message Lifecycle
|
|
328
|
+
|
|
329
|
+
| Status | Meaning |
|
|
330
|
+
|-------------|--------------------------------------|
|
|
331
|
+
| `queued` | In the queue, waiting for delivery |
|
|
332
|
+
| `delivered` | Injected into agent's terminal |
|
|
333
|
+
| `failed` | Injection raised an error |
|
|
334
|
+
|
|
335
|
+
Poll status with `GET /messages/:id`.
|
|
336
|
+
|
|
337
|
+
## HTTP API
|
|
338
|
+
|
|
339
|
+
All endpoints are on `127.0.0.1:<port>`. Every request needs
|
|
340
|
+
the bearer token from the registry file.
|
|
341
|
+
|
|
342
|
+
### `GET /status`
|
|
343
|
+
|
|
344
|
+
Returns JSON:
|
|
345
|
+
|
|
346
|
+
```json
|
|
347
|
+
{
|
|
348
|
+
"ok": true,
|
|
349
|
+
"session_id": "abc123",
|
|
350
|
+
"repo_root": "/path/to/repo",
|
|
351
|
+
"cli": "codex",
|
|
352
|
+
"id": "worker",
|
|
353
|
+
"description": "implement auth module",
|
|
354
|
+
"pid": 12345,
|
|
355
|
+
"host": "127.0.0.1",
|
|
356
|
+
"port": 43123,
|
|
357
|
+
"command": ["codex", "--no-alt-screen"],
|
|
358
|
+
"started_at": "2026-03-13T20:45:00Z",
|
|
359
|
+
"last_injected_at": null,
|
|
360
|
+
"injected_count": 0,
|
|
361
|
+
"input_state": {
|
|
362
|
+
"state": "prompt",
|
|
363
|
+
"input_ready": true
|
|
364
|
+
},
|
|
365
|
+
"agent_state": "prompt",
|
|
366
|
+
"inbox": {
|
|
367
|
+
"pending": 0,
|
|
368
|
+
"delivered_total": 3
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### `POST /send`
|
|
374
|
+
|
|
375
|
+
Send JSON body:
|
|
376
|
+
|
|
377
|
+
```json
|
|
378
|
+
{
|
|
379
|
+
"text": "implement plan A",
|
|
380
|
+
"submit": true,
|
|
381
|
+
"enter_only": false,
|
|
382
|
+
"force": false
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
- **200** with `"status": "delivered"` — sent immediately
|
|
387
|
+
- **202** with `"status": "queued"` and `message_id` — agent
|
|
388
|
+
is busy, message will auto-deliver
|
|
389
|
+
- **400** — missing text or bad request
|
|
390
|
+
- **409** — agent not ready (use `--force` to override)
|
|
391
|
+
- **503** — inbox is full
|
|
392
|
+
|
|
393
|
+
### `POST /stop`
|
|
394
|
+
|
|
395
|
+
Tells the session adapter to inject its stop sequence.
|
|
396
|
+
The `harnex stop` CLI retries transient API failures for up
|
|
397
|
+
to its `--timeout` budget before returning exit code 124.
|
|
398
|
+
|
|
399
|
+
### `GET /messages/:id`
|
|
400
|
+
|
|
401
|
+
Check delivery status of a queued message.
|
|
402
|
+
|
|
403
|
+
### `GET /health`
|
|
404
|
+
|
|
405
|
+
Alias for `/status`.
|
|
406
|
+
|
|
407
|
+
## Session Registry
|
|
408
|
+
|
|
409
|
+
Registry files at `~/.local/state/harnex/sessions/`:
|
|
410
|
+
|
|
411
|
+
```
|
|
412
|
+
<repo_hash>--<normalized_id>.json
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
The repo hash is a hex digest of the git root path. The ID is
|
|
416
|
+
normalized: lowercased, non-alphanumeric chars replaced with
|
|
417
|
+
dashes, leading/trailing dashes stripped.
|
|
418
|
+
|
|
419
|
+
Contents: port, PID, token, CLI, repo root, timestamps,
|
|
420
|
+
injection counters, agent state, inbox stats.
|
|
421
|
+
|
|
422
|
+
Cleaned up on normal exit. Stale files (dead PID) are ignored
|
|
423
|
+
during lookups.
|
|
424
|
+
|
|
425
|
+
## Exit Status
|
|
426
|
+
|
|
427
|
+
When a session exits, harnex writes:
|
|
428
|
+
|
|
429
|
+
```
|
|
430
|
+
~/.local/state/harnex/exits/<repo_hash>--<normalized_id>.json
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
This lets `harnex wait` return exit info even if the session
|
|
434
|
+
registry entry is already gone.
|
|
435
|
+
|
|
436
|
+
## Output Transcript
|
|
437
|
+
|
|
438
|
+
Every session also writes a repo-keyed transcript:
|
|
439
|
+
|
|
440
|
+
```
|
|
441
|
+
~/.local/state/harnex/output/<repo_hash>--<normalized_id>.log
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
- PTY output is appended as bytes are read
|
|
445
|
+
- The transcript is opened in append mode, so reusing an ID
|
|
446
|
+
does not wipe prior output
|
|
447
|
+
- `output_log_path` is exposed via `status --json` and
|
|
448
|
+
detached `run` responses
|
|
449
|
+
- The transcript is the source of truth for the planned
|
|
450
|
+
`harnex logs` operator interface
|
|
451
|
+
|
|
452
|
+
## Relay Headers
|
|
453
|
+
|
|
454
|
+
When `harnex send` detects it is running inside a harnex
|
|
455
|
+
session (via `HARNEX_SESSION_ID` env) and the target is a
|
|
456
|
+
different session, it prepends:
|
|
457
|
+
|
|
458
|
+
```
|
|
459
|
+
[harnex relay from=<cli> id=<sender_id> at=<timestamp>]
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
Control with `--relay` (force) or `--no-relay` (suppress).
|
|
463
|
+
|
|
464
|
+
Already-wrapped messages (starting with `[harnex relay`) are
|
|
465
|
+
not double-wrapped.
|
|
466
|
+
|
|
467
|
+
## File Watching
|
|
468
|
+
|
|
469
|
+
`--watch PATH` creates a `FileChangeHook` using Linux inotify
|
|
470
|
+
(via Fiddle, no gem needed).
|
|
471
|
+
|
|
472
|
+
- Watches the file's parent directory for write/create events
|
|
473
|
+
- 1 second debounce: waits for quiet time before triggering
|
|
474
|
+
- Injects `file-change-hook: read <path>` when triggered
|
|
475
|
+
- Creates the parent directory if needed
|
|
476
|
+
- File does not need to exist at startup
|
|
477
|
+
|
|
478
|
+
## Port Selection
|
|
479
|
+
|
|
480
|
+
Ports are deterministic to keep things predictable:
|
|
481
|
+
|
|
482
|
+
```
|
|
483
|
+
hash = Digest::SHA256.hexdigest(repo_root + id)
|
|
484
|
+
start = (hash[0..7].to_i(16) % port_span) + base_port
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
Then walks forward from `start` until a free port is found.
|
|
488
|
+
|
|
489
|
+
Defaults: base `43000`, span `4000` (range 43000–46999).
|
|
490
|
+
|
|
491
|
+
## Concurrency
|
|
492
|
+
|
|
493
|
+
Harnex uses Ruby threads, not processes:
|
|
494
|
+
|
|
495
|
+
- **PTY reader** — reads agent output into screen buffer
|
|
496
|
+
- **State poller** — feeds screen to adapter for state
|
|
497
|
+
- **Inbox delivery** — dequeues messages when prompt detected
|
|
498
|
+
- **HTTP server** — one thread per connection
|
|
499
|
+
- **File watcher** — inotify read loop (if enabled)
|
|
500
|
+
|
|
501
|
+
All shared state is protected by `Mutex`. The `SessionState`
|
|
502
|
+
and `Inbox` classes use `ConditionVariable` for signaling.
|
|
503
|
+
|
|
504
|
+
## Skill Files
|
|
505
|
+
|
|
506
|
+
Harnex ships a skill file that teaches AI agents how to use
|
|
507
|
+
harnex commands. The file lives at:
|
|
508
|
+
|
|
509
|
+
```
|
|
510
|
+
skills/harnex/SKILL.md
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### What's in the skill
|
|
514
|
+
|
|
515
|
+
The skill tells agents:
|
|
516
|
+
|
|
517
|
+
- How to detect they're inside a harnex session (env vars)
|
|
518
|
+
- How to send messages, check status, spawn workers
|
|
519
|
+
- How to use `--context`, `--force`, `--no-wait`
|
|
520
|
+
- Relay header format and behavior
|
|
521
|
+
- Collaboration patterns (reply, supervisor, file watch)
|
|
522
|
+
- Safety rules (confirm before sending, no auto-loops)
|
|
523
|
+
|
|
524
|
+
### How agents load skills
|
|
525
|
+
|
|
526
|
+
Claude and Codex both support a `skills/` directory. When a
|
|
527
|
+
skill is present, the agent can use harnex commands without
|
|
528
|
+
being told how — the skill provides the instructions.
|
|
529
|
+
|
|
530
|
+
Skill files use YAML frontmatter:
|
|
531
|
+
|
|
532
|
+
```yaml
|
|
533
|
+
---
|
|
534
|
+
name: harnex
|
|
535
|
+
description: Collaborate with other AI agents...
|
|
536
|
+
allowed-tools: Bash(harnex *)
|
|
537
|
+
---
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
The `allowed-tools` field grants the agent permission to run
|
|
541
|
+
`harnex` commands without asking for approval each time.
|
|
542
|
+
|
|
543
|
+
### Symlinking the skill
|
|
544
|
+
|
|
545
|
+
To make the skill available globally (not just in the harnex
|
|
546
|
+
repo), symlink it into each agent's skill directory:
|
|
547
|
+
|
|
548
|
+
```bash
|
|
549
|
+
# For Claude Code
|
|
550
|
+
ln -s /path/to/harnex/skills/harnex \
|
|
551
|
+
~/.claude/skills/harnex
|
|
552
|
+
|
|
553
|
+
# For Codex
|
|
554
|
+
ln -s /path/to/harnex/skills/harnex \
|
|
555
|
+
~/.codex/skills/harnex
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
After symlinking, any Claude or Codex session — in any repo —
|
|
559
|
+
can use harnex commands. The skill activates automatically
|
|
560
|
+
when the user mentions agent collaboration or when a relay
|
|
561
|
+
message arrives.
|
|
562
|
+
|
|
563
|
+
### Skill directory structure
|
|
564
|
+
|
|
565
|
+
```
|
|
566
|
+
~/.claude/skills/
|
|
567
|
+
└── harnex -> /path/to/harnex/skills/harnex
|
|
568
|
+
└── SKILL.md
|
|
569
|
+
|
|
570
|
+
~/.codex/skills/
|
|
571
|
+
└── harnex -> /path/to/harnex/skills/harnex
|
|
572
|
+
└── SKILL.md
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
The symlink points to the `skills/harnex/` directory (not the
|
|
576
|
+
file directly), so updates to `SKILL.md` in the repo are
|
|
577
|
+
picked up immediately.
|
|
578
|
+
|
|
579
|
+
## Known Limitations
|
|
580
|
+
|
|
581
|
+
- Adapter prompt detection is heuristic-based. It works well
|
|
582
|
+
but can misread unusual screen output.
|
|
583
|
+
- No read-only HTTP output endpoint for local dashboards (use
|
|
584
|
+
`harnex pane --follow` or `harnex logs --follow` instead).
|
|
585
|
+
- File watching: inotify on Linux, stat-polling fallback on
|
|
586
|
+
macOS/other (works everywhere, inotify is just faster).
|
|
587
|
+
- The HTTP server is a simple socket server, not a full
|
|
588
|
+
framework. One thread per connection, no keep-alive.
|
|
589
|
+
|
|
590
|
+
## Dependencies
|
|
591
|
+
|
|
592
|
+
- Ruby 3.x standard library only
|
|
593
|
+
- No gems required
|
|
594
|
+
- Uses: `io/console`, `pty`, `socket`, `json`, `net/http`,
|
|
595
|
+
`digest`, `fileutils`, `shellwords`, `fiddle` (for inotify)
|
data/bin/harnex
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require_relative "../lib/harnex"
|
|
4
|
+
|
|
5
|
+
begin
|
|
6
|
+
exit(Harnex::CLI.new(ARGV).run)
|
|
7
|
+
rescue OptionParser::ParseError => e
|
|
8
|
+
trace = ENV["HARNEX_TRACE"] == "1"
|
|
9
|
+
warn "harnex: #{e.message}"
|
|
10
|
+
warn "Try: harnex --help" unless trace
|
|
11
|
+
warn e.backtrace.join("\n") if trace
|
|
12
|
+
exit 2
|
|
13
|
+
rescue StandardError => e
|
|
14
|
+
trace = ENV["HARNEX_TRACE"] == "1"
|
|
15
|
+
warn "harnex: #{e.message}"
|
|
16
|
+
warn e.backtrace.join("\n") if trace
|
|
17
|
+
exit 1
|
|
18
|
+
end
|