harnex 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +188 -0
- data/GUIDE.md +17 -10
- data/README.md +61 -54
- data/TECHNICAL.md +38 -15
- data/docs/codex-appserver.md +175 -0
- data/docs/configuration.md +118 -0
- data/docs/dispatch-telemetry.md +502 -0
- data/docs/events.md +132 -0
- data/guides/01_dispatch.md +31 -28
- data/guides/04_monitoring.md +10 -9
- data/guides/05_naming.md +18 -8
- data/lib/harnex/adapters/base.rb +10 -0
- data/lib/harnex/adapters/codex_appserver.rb +25 -0
- data/lib/harnex/artifact_report.rb +455 -6
- data/lib/harnex/cli.rb +3 -3
- data/lib/harnex/commands/artifact_report.rb +8 -7
- data/lib/harnex/commands/doctor.rb +38 -4
- data/lib/harnex/commands/history.rb +7 -3
- data/lib/harnex/commands/run.rb +55 -43
- data/lib/harnex/commands/status.rb +0 -2
- data/lib/harnex/commands/wait.rb +0 -1
- data/lib/harnex/config.rb +170 -0
- data/lib/harnex/core.rb +200 -21
- data/lib/harnex/dispatch_history.rb +16 -7
- data/lib/harnex/pricing.rb +135 -0
- data/lib/harnex/retention.rb +320 -0
- data/lib/harnex/runtime/session.rb +451 -166
- data/lib/harnex/terminal_status.rb +18 -21
- data/lib/harnex/version.rb +2 -2
- data/lib/harnex.rb +3 -0
- metadata +9 -2
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Codex `app-server` adapter
|
|
2
|
+
|
|
3
|
+
harnex 0.6.0 talks to Codex over JSON-RPC 2.0 instead of scraping a
|
|
4
|
+
PTY pane. The adapter spawns `codex app-server` as a subprocess,
|
|
5
|
+
exchanges newline-delimited JSON-RPC messages on stdin/stdout, and
|
|
6
|
+
fans server notifications into the harnex events log.
|
|
7
|
+
|
|
8
|
+
## Transport
|
|
9
|
+
|
|
10
|
+
- Subprocess: `codex app-server` (CLI ≥ 0.128.0 — verify with
|
|
11
|
+
`harnex doctor`).
|
|
12
|
+
- Wire format: one JSON object per line.
|
|
13
|
+
- Encoding: UTF-8.
|
|
14
|
+
- One `Adapter#transport` value: `:stdio_jsonrpc`.
|
|
15
|
+
|
|
16
|
+
## Handshake
|
|
17
|
+
|
|
18
|
+
Mirrors `codex-plugin-cc/plugins/codex/scripts/lib/app-server.mjs`.
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
client.request("initialize", {
|
|
22
|
+
clientInfo: { title: "harnex", name: "harnex", version: Harnex::VERSION },
|
|
23
|
+
capabilities: {
|
|
24
|
+
experimentalApi: false,
|
|
25
|
+
optOutNotificationMethods: %w[
|
|
26
|
+
item/agentMessage/delta
|
|
27
|
+
item/reasoning/summaryTextDelta
|
|
28
|
+
item/reasoning/summaryPartAdded
|
|
29
|
+
item/reasoning/textDelta
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
client.notify("initialized", {})
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
After the handshake the client is ready to issue `thread/start` and
|
|
37
|
+
`turn/start` requests.
|
|
38
|
+
|
|
39
|
+
## Notification → event mapping
|
|
40
|
+
|
|
41
|
+
| Server notification | harnex event | Notes |
|
|
42
|
+
|-----------------------------|--------------------|-------|
|
|
43
|
+
| `thread/started` | (metadata) | Stashes `threadId` |
|
|
44
|
+
| `turn/started` | `turn_started` | Carries `turnId` |
|
|
45
|
+
| `turn/completed` | `task_complete` or `task_failed` | Failed/interrupted statuses emit `task_failed` with the Codex error. A completed `--context` turn emits `task_complete` only with structured command/tool activity or a Git delta; otherwise it emits typed `completed_no_activity`. Harnex writes the observed-state receipt before the successful event. |
|
|
46
|
+
| `item/started` | (silent) | Streaming deltas opted out |
|
|
47
|
+
| `item/completed` | `item_completed` + synthesized transcript | See "tmux/STDOUT" below |
|
|
48
|
+
| `error` | `error` | Turn-level Codex error notification; preserves nested `error.message` and does not count as a transport disconnect. |
|
|
49
|
+
| `thread/status/changed` | (state only) | Drives state machine |
|
|
50
|
+
| `thread/tokenUsage/updated` | (status field) | Surfaced via `harnex status --json` |
|
|
51
|
+
| `thread/compacted` | `compaction` | Increments `compactions` counter |
|
|
52
|
+
| `account/rateLimits/updated`| (silent, status) | Visible in `status --json` |
|
|
53
|
+
|
|
54
|
+
### How disconnects are detected
|
|
55
|
+
|
|
56
|
+
Disconnects are detected from subprocess exit / EOF on stdout and parse errors
|
|
57
|
+
when the server emits a malformed line. Request-level JSON-RPC error responses
|
|
58
|
+
reject only the in-flight request; they do not by themselves imply that the
|
|
59
|
+
transport disconnected. Schema-defined `error` notifications are turn-level
|
|
60
|
+
Codex errors and feed `last_error` / `task_failed` rather than the disconnect
|
|
61
|
+
counter.
|
|
62
|
+
|
|
63
|
+
Unexpected transport loss emits `disconnected` and increments
|
|
64
|
+
`auto_disconnects`. Normal auto-stop teardown after `task_complete` /
|
|
65
|
+
`task_failed` is treated as a clean structured close. There is no need for the
|
|
66
|
+
screen-text regex that the legacy adapter relied on.
|
|
67
|
+
|
|
68
|
+
## tmux / STDOUT — synthesized transcript
|
|
69
|
+
|
|
70
|
+
Without a PTY, `harnex run codex --tmux` and `harnex pane --id …`
|
|
71
|
+
would otherwise see an empty pane. The JSON-RPC path renders a
|
|
72
|
+
synthesized transcript built from `item/completed` notifications:
|
|
73
|
+
|
|
74
|
+
- `agent_message` items render their text payload
|
|
75
|
+
- `tool_call` items render as `tool: <name> <one-line summary>`
|
|
76
|
+
|
|
77
|
+
The synthesized transcript is written to BOTH the output log AND
|
|
78
|
+
STDOUT, so:
|
|
79
|
+
|
|
80
|
+
- `harnex run codex` (foreground) — user sees the transcript live
|
|
81
|
+
- `harnex run codex --tmux` — the tmux window shows the transcript
|
|
82
|
+
- `harnex pane --id <session>` — captures the synthesized text
|
|
83
|
+
- `harnex logs --id <session>` — replays the same transcript
|
|
84
|
+
|
|
85
|
+
For interactive debugging where the original Codex TUI is wanted,
|
|
86
|
+
`codex resume <thread-id>` opens the same thread in a real Codex
|
|
87
|
+
CLI.
|
|
88
|
+
|
|
89
|
+
## `harnex wait --until done` / `task_complete` / `task_failed`
|
|
90
|
+
|
|
91
|
+
For unattended monitors, block until Codex work completes, fails, or the session exits:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
harnex wait --id cx-i-242 --until done --timeout 300
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
When you need the exact successful structured turn event, wait for `task_complete`:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
harnex wait --id cx-i-242 --until task_complete --timeout 300
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`--until done` returns non-zero when it sees `task_failed` or failed terminal
|
|
104
|
+
telemetry. This includes acknowledgment-only autonomous `--context` turns
|
|
105
|
+
(`outcome_class=completed_no_activity`) and the rare receipt-write failure
|
|
106
|
+
(`report_invalid`). Harnex classifies completion from app-server item counters
|
|
107
|
+
and Git state; it does not inspect final-answer prose or trust worker claims.
|
|
108
|
+
The task-complete/task-failed waiters
|
|
109
|
+
tail the events JSONL — not the API socket — so they keep working across
|
|
110
|
+
restarts and are adapter-agnostic.
|
|
111
|
+
|
|
112
|
+
Every blind dispatch receives a harness-authored receipt. No worker report is
|
|
113
|
+
required: Harnex captures command exits, Git state, completion, and usage, then
|
|
114
|
+
writes `HARNEX_ARTIFACT_REPORT_PATH` before emitting `task_complete`. Use
|
|
115
|
+
`--artifact-report PATH` only to choose a fixed destination; otherwise the
|
|
116
|
+
status/end row points to the default state-directory path. Review workers may
|
|
117
|
+
write advisory summary/verdict/P1-P3 counts to `HARNEX_ARTIFACT_CLAIMS_PATH`.
|
|
118
|
+
Claims and report-shaped `agentMessage` text cannot satisfy the activity gate.
|
|
119
|
+
Consumers can run `harnex artifact-report validate PATH --final` afterward.
|
|
120
|
+
|
|
121
|
+
## `harnex doctor`
|
|
122
|
+
|
|
123
|
+
Verifies the Codex CLI is installed and at version ≥ 0.128.0. JSON
|
|
124
|
+
output, exit 0 if healthy.
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
$ harnex doctor
|
|
128
|
+
{"ok":true,"checks":[{"name":"codex","required":">= 0.128.0","ok":true,"found":"0.128.0"}]}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Long-term fallback: `--legacy-pty`
|
|
132
|
+
|
|
133
|
+
The pre-0.6.0 PTY adapter remains available as a long-term supported
|
|
134
|
+
fallback:
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
harnex run codex --legacy-pty
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
It's the right tool when you want the full Codex TUI live in tmux —
|
|
141
|
+
status bars, tool diffs, ANSI panels — that the headless `app-server`
|
|
142
|
+
backend doesn't render. JSON-RPC remains the default and is recommended
|
|
143
|
+
for autonomous worker dispatches; legacy-pty is for interactive/TUI use.
|
|
144
|
+
|
|
145
|
+
## Troubleshooting
|
|
146
|
+
|
|
147
|
+
- **`task_complete` never fires.** Check `harnex events --id <session>` first:
|
|
148
|
+
failed Codex turns emit `task_failed` with the provider/model error. If there
|
|
149
|
+
is neither `task_complete` nor `task_failed`, run `harnex doctor`; Codex <
|
|
150
|
+
0.128.0 is unsupported.
|
|
151
|
+
- **Empty tmux pane.** Codex hasn't emitted any `item/completed`
|
|
152
|
+
yet — the agent is reasoning. The pane fills as soon as the
|
|
153
|
+
first item completes.
|
|
154
|
+
- **`task_failed` immediately after dispatch.** Check
|
|
155
|
+
`harnex events --id <session>`. Provider/model failures retain their Codex
|
|
156
|
+
error message. `completed_no_activity` means the turn ended with no
|
|
157
|
+
command/tool or Git activity. `report_invalid` now primarily identifies a
|
|
158
|
+
harness receipt-write failure; old rows may still contain the legacy
|
|
159
|
+
`report_missing` / `report_rejected` classes. Common provider failures
|
|
160
|
+
include auth environment variables (for example `OPENAI_API_KEY` /
|
|
161
|
+
`AZURE_OPENAI_API_KEY`) and model unavailability.
|
|
162
|
+
|
|
163
|
+
## Schema fixtures
|
|
164
|
+
|
|
165
|
+
`test/fixtures/codex_appserver/schema/` holds hand-pruned subsets
|
|
166
|
+
of `ServerNotification` and `ClientRequest` for the methods harnex
|
|
167
|
+
issues / consumes. Regenerate via:
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
codex app-server generate-json-schema --out /tmp/codex-schema-X
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
then re-prune. The full bundle is ~3 MB; the pruned subsets are
|
|
174
|
+
< 50 KB and serve as a compact reference for what's actually wired
|
|
175
|
+
through the adapter.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Repository Configuration
|
|
2
|
+
|
|
3
|
+
Harnex has one optional repository configuration file:
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
<git-root>/.harnex/config.json
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
It is resolved with the same git-root rule as the canonical dispatch stream.
|
|
10
|
+
Non-git launches use the global dispatch stream and do not invent a global
|
|
11
|
+
configuration file. An absent file is valid. An explicitly present malformed
|
|
12
|
+
file fails before agent spawn so policy is never silently ignored.
|
|
13
|
+
|
|
14
|
+
## Phase allowlist
|
|
15
|
+
|
|
16
|
+
Repositories can normalize queue/work phase names:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"phase": {
|
|
21
|
+
"allowlist": [
|
|
22
|
+
"plan-write",
|
|
23
|
+
"plan-review",
|
|
24
|
+
"plan-fix",
|
|
25
|
+
"test-suite",
|
|
26
|
+
"implement",
|
|
27
|
+
"code-review",
|
|
28
|
+
"code-fix",
|
|
29
|
+
"mapping",
|
|
30
|
+
"triage",
|
|
31
|
+
"docs"
|
|
32
|
+
],
|
|
33
|
+
"policy": "reject"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The effective value is the first-class `harnex run --phase TEXT` value, or
|
|
39
|
+
`--meta.phase` when no first-class override is supplied.
|
|
40
|
+
|
|
41
|
+
- No `phase` configuration: any phase passes.
|
|
42
|
+
- Allowlisted phase: dispatches silently.
|
|
43
|
+
- `policy: "warn"`: prints a warning and dispatches.
|
|
44
|
+
- `policy: "reject"`: exits non-zero before spawn and writes no dispatch row.
|
|
45
|
+
|
|
46
|
+
The allowlist must be an array of non-empty strings. Policy must be `warn` or
|
|
47
|
+
`reject`.
|
|
48
|
+
|
|
49
|
+
## Events, output, and receipt retention
|
|
50
|
+
|
|
51
|
+
Per-session event JSONL, output transcripts, and generated proof receipts live
|
|
52
|
+
under Harnex's local state directory and are not the durable dispatch stream:
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
~/.local/state/harnex/events/
|
|
56
|
+
~/.local/state/harnex/output/
|
|
57
|
+
~/.local/state/harnex/receipts/
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Defaults apply independently to each directory:
|
|
61
|
+
|
|
62
|
+
- maximum age: 45 days;
|
|
63
|
+
- maximum total size: 1 GiB.
|
|
64
|
+
|
|
65
|
+
Override them in repo configuration:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"retention": {
|
|
70
|
+
"events": {
|
|
71
|
+
"max_age_days": 45,
|
|
72
|
+
"max_bytes": 1073741824
|
|
73
|
+
},
|
|
74
|
+
"output": {
|
|
75
|
+
"max_age_days": 45,
|
|
76
|
+
"max_bytes": 1073741824
|
|
77
|
+
},
|
|
78
|
+
"receipts": {
|
|
79
|
+
"max_age_days": 45,
|
|
80
|
+
"max_bytes": 1073741824
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Environment variables take precedence:
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
HARNEX_EVENTS_MAX_AGE_DAYS
|
|
90
|
+
HARNEX_EVENTS_MAX_BYTES
|
|
91
|
+
HARNEX_OUTPUT_MAX_AGE_DAYS
|
|
92
|
+
HARNEX_OUTPUT_MAX_BYTES
|
|
93
|
+
HARNEX_RECEIPTS_MAX_AGE_DAYS
|
|
94
|
+
HARNEX_RECEIPTS_MAX_BYTES
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Limits must be positive integers. Harnex deletes only regular files directly
|
|
98
|
+
owned by the `events`, `output`, and `receipts` directories: expired files first, then the
|
|
99
|
+
oldest unprotected files until the size cap is met. It never follows paths
|
|
100
|
+
outside those directories. Files for the current session, live registry PIDs,
|
|
101
|
+
and alive uncompleted dispatch-start rows are protected. If protected files
|
|
102
|
+
alone exceed a cap, Harnex reports the directory over cap rather than deleting
|
|
103
|
+
them.
|
|
104
|
+
|
|
105
|
+
A bounded prune runs opportunistically when a dispatch starts. Inspect or force
|
|
106
|
+
the same policy with `doctor`:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
harnex doctor # sizes, limits, and last-prune status
|
|
110
|
+
harnex doctor --prune --dry-run # preview bounded candidate paths, do not delete
|
|
111
|
+
harnex doctor --prune # apply now
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`--dry-run` is valid only with `--prune`. Manual prune bypasses the automatic
|
|
115
|
+
cadence but preserves the same live/current safety rules.
|
|
116
|
+
|
|
117
|
+
Set `HARNEX_STATE_DIR` before launching Harnex when tests or isolated automation
|
|
118
|
+
need a disposable local-state root.
|