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
data/docs/agents.md ADDED
@@ -0,0 +1,119 @@
1
+ # Coding agents
2
+
3
+ harnex is built to be driven by coding agents: Claude Code, Pi, or anything
4
+ that can run a shell command or call an MCP tool. A supervising agent hands a
5
+ question or a task to a Pi worker (a GPT model by default), keeps working, and
6
+ gets the worker's reply back as plain text it can act on.
7
+
8
+ There are three ways in, from smoothest to most portable:
9
+
10
+ 1. **MCP tools** (`harnex mcp install`): `ask`, `delegate`, `result`,
11
+ `reply`, `land`, `discard` and `lanes` as native tools. See
12
+ [MCP server](mcp.md).
13
+ 2. **The `gpt` subagent** for Claude Code: a background subagent that relays
14
+ one brief to a worker and returns the reply verbatim.
15
+ 3. **The CLI** from any agent's shell tool: the same verbs as
16
+ [lanes](lanes.md).
17
+
18
+ ## Claude Code
19
+
20
+ ### MCP tools (main conversation)
21
+
22
+ With the server registered, Claude calls the tools directly. A call that runs
23
+ longer than two minutes moves to the background on its own; Claude keeps
24
+ working and is notified when the result lands, so a `delegate` behaves like a
25
+ background subagent without one:
26
+
27
+ ```text
28
+ ❯ Use the harnex delegate tool to have a GPT worker run the slow benchmark and tell me the score.
29
+ ● Calling harnex…
30
+ ⎿ pi-slow-bench: working 59s · 3 tool calls · now bash 44s
31
+ ● The GPT worker is still running the benchmark. I'll report the score when it finishes.
32
+ ● MCP task (harnex/delegate) completed.
33
+ ● The score is 302926.
34
+ ```
35
+
36
+ ### The `gpt` subagent
37
+
38
+ `harnex mcp install` also installs a `gpt` agent definition at
39
+ `~/.claude/agents/gpt.md` when none exists. It is a thin Haiku relay with only
40
+ the harnex tools: it never does the task itself and returns the worker's reply
41
+ exactly, footer included. Use it for parallel work (several `Agent` calls in
42
+ one message) and for follow-ups (`SendMessage` to the same agent reaches the
43
+ same worker, with its context).
44
+
45
+ ```text
46
+ Agent(subagent_type: "gpt", prompt: """
47
+ worktree: word-count
48
+
49
+ Add TextKit.word_count(text) to lib/textkit.rb with a test; run bin/test.
50
+ """)
51
+ ```
52
+
53
+ Optional header lines, then a blank line, then the brief:
54
+
55
+ | Header | Effect |
56
+ | --- | --- |
57
+ | `worktree: NAME` | Edit in a new worktree on `harnex/NAME`; land it afterwards |
58
+ | `mode: write` | Run commands or edit the current checkout in place |
59
+ | `mode: read` | Read-only, even if the brief mentions commands |
60
+ | `model: provider/model`, `effort: low\|medium\|high` | Pick the worker's model |
61
+ | `fallback: provider/model` | Retry a read-only ask on another model |
62
+ | `cwd: DIR` | Work in another directory |
63
+
64
+ Without a header, questions and reviews go to a read-only worker, and a brief
65
+ that asks to run something or change files goes to a worker that can.
66
+ The definition ships with harnex as [`agents/gpt.md`](../agents/gpt.md).
67
+
68
+ ### The CLI from Claude's Bash tool
69
+
70
+ ```bash
71
+ harnex delegate --worktree word-count "Add TextKit.word_count(text) with a test; run bin/test."
72
+ ```
73
+
74
+ Give the Bash call `timeout: 600000`: commands wait up to 9 minutes, and
75
+ Claude Code's default Bash timeout is two minutes. On exit 124 run
76
+ `harnex result NAME`. In the main conversation, `run_in_background` works too.
77
+
78
+ ### Things worth knowing
79
+
80
+ - Claude Code loads agent definitions and MCP servers when a session starts;
81
+ restart after `harnex mcp install`.
82
+ - Background results reach the parent XML-escaped (`&`, `<`). The relay's
83
+ own text is exact.
84
+ - Headless `claude -p` stops background work ten idle minutes after the main
85
+ turn; set `CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=0` for long unattended runs.
86
+
87
+ ## Pi
88
+
89
+ `harnex mcp install` adds the server to `~/.pi/agent/mcp.json` with
90
+ `exposure: direct`, so a Pi supervisor sees the tools like built-ins. Without
91
+ MCP, Pi uses the CLI through its `bash` tool; `harnex agents-guide claude` is
92
+ the short guide it needs (it applies to any supervising agent). Workers
93
+ started by harnex never see harnex's own MCP tools, so a worker cannot start
94
+ more workers.
95
+
96
+ ## Habits that keep it smooth
97
+
98
+ - **Self-contained briefs.** The worker has not seen your conversation: name
99
+ the files, the expected behavior and the check (`bin/test`).
100
+ - **A worktree for anything that edits.** `--worktree NAME` keeps parallel
101
+ lanes apart and your checkout clean until you land.
102
+ - **Verify, then land.** `done` means the worker's turn ended without error,
103
+ not that the work is right. Read the diff and run the tests yourself.
104
+ - **Land, don't merge by hand.** `land` also removes the worktree and branch.
105
+ - **Check on a worker with `lanes`.** It shows the tool each worker is running
106
+ now.
107
+
108
+ ## Measuring it
109
+
110
+ `evals/agents/run` drives real supervisors (Claude Code headless and Pi print
111
+ mode, each with and without MCP) through seven scenarios against a fixture
112
+ repository and scores correctness, tool calls, harnex usage errors, doc
113
+ lookups and leftovers. harnex 0.29 passes all 28 runs with no harnex usage
114
+ errors, no doc lookups and nothing left behind.
115
+
116
+ ```bash
117
+ evals/agents/run --list
118
+ evals/agents/run --supervisor claude,pi S2 S3
119
+ ```
@@ -11,6 +11,26 @@ git repo it is `~/.local/state/harnex/dispatch.jsonl`. `harnex history`,
11
11
  `status --id`, and `wait` consume the same rows. Legacy v1 thin rows and
12
12
  pre-v2 envelope-less summaries may coexist and remain readable/skippable.
13
13
 
14
+ ## Portable home paths
15
+
16
+ From 0.14.1, new canonical records and reconciliation repair appends serialize
17
+ string values equal to the current user's home as `~`, and paths beneath it as
18
+ `~/...`. The home-directory match uses a complete path-component boundary;
19
+ sibling directories, other users' tilde forms and paths outside that home are
20
+ not rewritten. A path mentioned later inside a prose string is not scrubbed.
21
+ This is path serialization, not a general identity or secret redactor.
22
+
23
+ The writer serializes a copy. Runtime/session structures, the events stream,
24
+ registry and receipt files retain their normal paths. Existing ledger rows are
25
+ not rewritten automatically. Status API results still expose absolute repository
26
+ roots, and path-consuming retention/report operations resolve portable paths
27
+ against the reader's home.
28
+
29
+ Reconciliation compares absolute paths under the current home and their portable
30
+ forms consistently. Genuinely different paths, identities, values and outcomes
31
+ still conflict. Home-relative paths are interpreted in the current user's home
32
+ context; this does not infer another machine's home directory or migrate archives.
33
+
14
34
  ## Clean-Tree Checks And The Tracked Stream
15
35
 
16
36
  Some repos deliberately track `.harnex/dispatch.jsonl` as project telemetry.
@@ -24,7 +44,7 @@ alongside the work it describes.
24
44
  ## CLI flags
25
45
 
26
46
  ```text
27
- harnex run codex --meta '{"model":"gpt-5.3-codex","effort":"high","predicted":{"input_tokens":[200000,800000]}}'
47
+ harnex run pi --meta '{"model":"openai/gpt-5.1","effort":"high","predicted":{"input_tokens":[200000,800000]}}'
28
48
  harnex run pi --context 'Implement the task; Harnex writes the final receipt'
29
49
  harnex run pi --artifact-report .harnex/receipts/pi-r-64.json --context 'Optionally write review claims to $HARNEX_ARTIFACT_CLAIMS_PATH'
30
50
  harnex run pi --project-id harnex --queue-id queue-005 --entry-id SP-4 --phase implement --intent queue-work --require-attribution
@@ -76,6 +96,31 @@ harnex orchestration report --dispatch .harnex/dispatch.jsonl --run-id queue-005
76
96
 
77
97
  Use `harnex history --json | jq .` for pipelines over the repo-local log.
78
98
 
99
+ ## Optional external archive bridge
100
+
101
+ Repositories may keep the active stream bounded while preserving one logical
102
+ history through two opt-in local executable hooks:
103
+
104
+ - `HARNEX_DISPATCH_ARCHIVER=/absolute/path/to/helper` runs immediately before
105
+ each append as `helper --canonical PATH --incoming-bytes N`. Harnex holds a
106
+ stable sidecar lock under `$HARNEX_STATE_DIR/locks/` before invoking the
107
+ helper and opens the active stream only afterward, so an atomic replacement
108
+ cannot strand a waiting writer on the old inode. A helper failure emits a
109
+ bounded warning and the new row is still appended to the active stream.
110
+ - `HARNEX_DISPATCH_HISTORY_READER=/absolute/path/to/helper` runs as
111
+ `helper --canonical PATH`. It must write the complete archive-plus-active
112
+ JSONL stream to stdout and exit successfully. Harnex captures that stream in
113
+ a private temporary file before exposing any rows; failure or malformed
114
+ output is reported as unavailable history rather than an empty result.
115
+
116
+ Both values must name an absolute, executable, non-symlink local file. Harnex
117
+ passes no telemetry bytes or credentials in arguments, does no network work of
118
+ its own, suppresses helper output on the write path, and retains its ordinary
119
+ active-file behavior when the hooks are unset. The repository-specific helper
120
+ owns encryption, storage credentials, archive verification, and retrieval.
121
+ `harnex history`, canonical assertion/reconciliation, latest-row lookup, and
122
+ attempt-chain accounting consume the complete reader when configured.
123
+
79
124
  ## Canonical assertion and reconciliation
80
125
 
81
126
  `harnex telemetry assert-canonical` is the read-only drift gate for the
@@ -130,15 +175,19 @@ Harnex-owned `meta` fields are always populated when derivable: `id`,
130
175
  These top-level `--meta` keys pass through into `meta` when provided:
131
176
  `orchestrator`, `orchestrator_session`, `chain_id`, `parent_dispatch_id`,
132
177
  `parent_attempt_id`, `attempt_kind`, `tier`, `phase`, `issue`, `plan`, and
133
- `task_brief`. Queue-specific keys such as
178
+ `task_brief`. `harnex run --context-file` fills `context_file` (absolute
179
+ path), `context_sha256` (SHA-256 of the text actually sent) and, on
180
+ `started.meta` only, `context_section`; it also defaults `task_brief` to the
181
+ file path. Queue-specific keys such as
134
182
  `project_id`, `queue_id`, `entry_id`, `entry_title`, and `intent` are used for
135
183
  the top-level `queue` block but are not duplicated into legacy `meta`. Unknown
136
184
  top-level keys are kept on `started.meta` but are not copied into the v2 end
137
185
  row.
138
186
 
139
- `predicted` is copied verbatim from `--meta.predicted` when it is a JSON object;
140
- otherwise it is `{}`. Harnex does no profile lookup or recommendation-table
141
- resolution.
187
+ `predicted` is copied from `--meta.predicted` when it is a JSON object;
188
+ otherwise it is `{}`. The canonical record applies the portable-path serialization
189
+ above; runtime values are unchanged. Harnex does no profile lookup or
190
+ recommendation-table resolution.
142
191
 
143
192
  ## Queue, agent, and reliability blocks
144
193
 
@@ -168,17 +217,20 @@ routing details; legacy `meta.agent*` and `actual.model` stay for compatibility:
168
217
  ```json
169
218
  {
170
219
  "agent": {
171
- "cli": "codex",
220
+ "cli": "pi",
172
221
  "provider": "openai",
173
- "model_requested": "gpt-5.3-codex",
174
- "model_effective": "gpt-5.3-codex",
222
+ "model_requested": "openai/gpt-5.1",
223
+ "model_effective": "gpt-5.1",
175
224
  "reasoning_effort": "high",
176
- "service_tier": "flex",
177
- "adapter_transport": "stdio_jsonrpc"
225
+ "service_tier": null,
226
+ "adapter_transport": "stdio_jsonl_rpc"
178
227
  }
179
228
  }
180
229
  ```
181
230
 
231
+ `service_tier` is copied from `--meta` and is otherwise null; older rows may
232
+ carry an adapter-set value.
233
+
182
234
  The top-level `reliability` block is always emitted and should be preferred over
183
235
  legacy `actual.disconnections` for reliability analytics:
184
236
 
@@ -270,18 +322,12 @@ last valid terminal and peak values intact while setting
270
322
  `latest_sample_status: "missing"`. Null never means zero.
271
323
 
272
324
  `context.status` is `observed` for Pi's dedicated
273
- `get_session_stats.contextUsage` signal, `estimated` for Codex app-server,
274
- `missing` when a supported source yielded no valid occupancy, or `unsupported`
275
- when the adapter has no active-context source. `source` is
276
- `pi_get_session_stats` or `codex_thread_token_usage_last` for those structured
277
- adapters and is null for unsupported adapters. Pi's percentage is adapter
278
- reported. Codex's `tokenUsage.last.totalTokens` is the latest model-reported
279
- active context size, but it excludes local items appended after that response;
280
- Harnex therefore labels it estimated and derives
281
- `terminal_percent = last.totalTokens / modelContextWindow * 100`. That is
282
- full-window pressure, not Codex TUI's baseline-adjusted “context left” display.
283
- No prompt, transcript, message, tool payload, or compaction summary is copied
284
- into this block.
325
+ `get_session_stats.contextUsage` signal, `missing` when a supported source
326
+ yielded no valid occupancy, or `unsupported` when the adapter has no
327
+ active-context source (`estimated` appears only in older rows). `source` is
328
+ `pi_get_session_stats` for Pi and null for unsupported adapters. Pi's
329
+ percentage is adapter reported. No prompt, transcript, message, tool payload,
330
+ or compaction summary is copied into this block.
285
331
 
286
332
  `attribution.status` is `complete` when `project_id`, `phase`, `intent`, and a
287
333
  work id are present; `partial` when any attribution is known but that contract
@@ -294,6 +340,45 @@ is normally `accepted` or `rejected`. `source=harnex_observed_state` identifies
294
340
  new receipts. The block also contains final commit/path/LOC observations; those
295
341
  facts prove the delta, not semantic quality or human authorship.
296
342
 
343
+ `outcome.class` is one value from a closed vocabulary (#57):
344
+
345
+ - **Completion:** `completed_with_proof`, `completed_with_activity`.
346
+ - **Proof defects:** `completed_no_activity`, `report_missing`,
347
+ `report_invalid`, `report_rejected`.
348
+ - **Process:** `task_failed` (the turn failed), `provider_capacity` (the turn
349
+ failed after the adapter's own provider retries ran out, from Pi's
350
+ structured `auto_retry_end success:false`; capacity or rate limits rather
351
+ than the work), `boot_failure` (structured
352
+ transport died before its first turn), `permission_prompt` (ended on a
353
+ trust/confirmation prompt the adapter recognizes), `timeout` (`--max-runtime`
354
+ stopped running work, or exit 124), `stopped_by_caller` (a manual stop
355
+ while work was running), `process_failed` (nonzero exit with no turn
356
+ verdict).
357
+ - **Fallback:** `unknown`.
358
+
359
+ Completion and proof classes are never overridden. Process classes refine an
360
+ `unknown` or `task_failed` terminal, or a `report_rejected` receipt for a
361
+ process that never signalled completion (finalization always writes a receipt,
362
+ so that rejection records the failed process, not a proof verdict). Signals:
363
+ the exit path, stop provenance, and final input state; never transcript text.
364
+ A stop or deadline outranks `task_failed` only when the work was still running
365
+ when it was stopped. A clean exit 0 without completion keeps its accepted
366
+ receipt (`completed_with_proof`), as before. Older rows are not reclassified.
367
+
368
+ `harnex orchestration report --run-id ID` (or `--queue-id ID`) counts
369
+ `workers.outcome_classes` per raw attempt and per retry family. A family joins
370
+ attempts by `parent_attempt_id`, by `parent_dispatch_id` for non-initial
371
+ attempts (retry/fix/fallback/superseding/review), or by a reused dispatch id,
372
+ so correlated retries count as one root cause. Spawner-derived parents of
373
+ initial workers do not join families. Start rows are not counted. An optional
374
+ `--failure-budget N [--budget-unit attempts|families] [--fatal-classes A,B]`
375
+ adds a `failure_budget` verdict and exits 3 once N fatal failures are counted
376
+ (1 is an error, 2 a usage error; a missing `--dispatch` file is an error).
377
+ By default the fatal classes are `completed_no_activity`, `task_failed`,
378
+ `provider_capacity`, `boot_failure`, `permission_prompt`, `timeout`, and `process_failed`; proof
379
+ defects, caller stops and `unknown` are not fatal.
380
+ Harnex supplies the ledger only: the caller decides what to do on a breach.
381
+
297
382
  Every end row has one Harnex-session `attempt`. Its random `id` is distinct
298
383
  from operator-visible `run_id`; `parent_attempt_id` and `parent_dispatch_id`
299
384
  link retries/fixes/reviews/fallbacks while each row keeps separate raw usage.
@@ -368,7 +453,7 @@ A generated receipt contains:
368
453
  "version": 1,
369
454
  "author": "harnex",
370
455
  "generated_at": "2026-08-03T05:00:00Z",
371
- "id": "cx-r-64",
456
+ "id": "pi-r-64",
372
457
  "session_id": "8d8f3f07c8fc343d"
373
458
  },
374
459
  "outcome": {
@@ -415,23 +500,64 @@ A generated receipt contains:
415
500
  (`start_sha != end_sha`, commit count/path/LOC evidence) from an observed
416
501
  `no_change` result. `start_dirty`, `end_dirty`, and `worktree_changed` expose
417
502
  worktree caveats while unchanged pre-session dirt is excluded from the delta.
418
- Codex app-server `commandExecution` items contribute
419
- bounded command text, integer exit code, status, and optional duration. Other
420
- transports currently report `command_observation: "unsupported"` rather than
421
- guessing from prose or generic tool events. If the 256-KiB receipt cap requires
503
+ Pi RPC built-in `bash` tool calls contribute bounded command text, integer
504
+ exit code, status, and optional duration. Pi commands require an integer `result.structuredContent.exit_code`
505
+ correlated by `toolCallId`; Pi bash calls without one (abort, timeout,
506
+ extension override, older Pi, still running at teardown) are counted in
507
+ `observed.commands_unobserved` and mark `command_observation` as `partial`
508
+ when some exits were observed, or `unsupported` when none were. PTY transports
509
+ report `command_observation: "unsupported"` rather than guessing from prose or
510
+ generic tool events. If the 256-KiB receipt cap requires
422
511
  trimming, `commands_truncated` / `changed_paths_truncated` make that explicit;
423
512
  full aggregate counts remain in the dispatch row. Usage carries the same
424
513
  measured/missing/unsupported semantics as the dispatch
425
514
  row and is refreshed at teardown when final adapter usage becomes available.
426
515
 
427
516
  `validation.commands` mirrors all bounded observed command exits for legacy
428
- readers. Its aggregate status is `not_run`, `pass`, or `fail`; a failed
517
+ readers. Its aggregate status is `not_run`, `pass`, or `fail`, or `unknown`
518
+ when no observed exit failed but `commands_unobserved` is positive; a failed
429
519
  exploratory command may precede a successful turn. For harness receipts,
430
520
  `validate --final` therefore validates receipt authorship/shape, accepted turn,
431
521
  and accepted/no-change outcome instead of letting an intermediate command exit
432
522
  rewrite the harness verdict. Queue policy may impose a stricter command gate by
433
523
  inspecting `observed.commands`.
434
524
 
525
+ Final gates (#68) separate acceptance from command history. Declare them with
526
+ repeatable `harnex run --final-gate '<cmd>'` (or meta `"final_gates": [...]`,
527
+ which `--final-gate` appends to). The receipt then carries:
528
+
529
+ ```json
530
+ "validation": {
531
+ "status": "pass",
532
+ "commands_had_failures": true,
533
+ "final_gates": {
534
+ "declared": 1, "observed_pass": 1, "observed_fail": 0, "missing": 0,
535
+ "status": "pass",
536
+ "gates": [{ "cmd": "rake test", "exit_code": 0 }]
537
+ }
538
+ }
539
+ ```
540
+
541
+ - A gate matches an observed command by exact string. A structured adapter's
542
+ single-shell transport wrapper (`bash -lc '<cmd>'`, `sh -c '<cmd>'`) is
543
+ unwrapped first; the inner script must still equal the gate exactly.
544
+ - Each gate is decided by its **last** observed run, so an intermediate
545
+ failure followed by a green final run passes.
546
+ - A gate that never ran is `fail` when command observation was complete, and
547
+ `unknown` when it was `partial` or `unsupported` (PTY transports), since it
548
+ may be among the unobserved calls.
549
+ - With gates declared, `validation.status` is the gate status. Without them,
550
+ `final_gates.status` is `undeclared` and `validation.status` keeps the
551
+ any-failure semantics above.
552
+ - `commands_had_failures` covers the session's whole command history, not only
553
+ the retained 50-command window.
554
+ - Dispatch rows keep `commands_had_failures` and the final-gate counts and
555
+ status, without the gate command text.
556
+
557
+ This is receipt ergonomics, not proof: `validate --final` and outcome classes
558
+ do not change, and supervisors should still replay declared final commands
559
+ verbatim before accepting work.
560
+
435
561
  Reviewers can write one optional input file at
436
562
  `HARNEX_ARTIFACT_CLAIMS_PATH`:
437
563
 
@@ -467,30 +593,28 @@ not a required worker step for new dispatches.
467
593
 
468
594
  ## Autonomous completion gate
469
595
 
470
- For Codex app-server runs launched with `--context`, provider turn completion
471
- is not by itself accepted work completion. Harnex emits `task_complete` only
472
- when it has at least one structured command/tool/file-change item or a Git
473
- delta. If both are absent, it emits `task_failed` with
596
+ A Pi turn counts as accepted work completion once it settles
597
+ (`agent_settled`) without a final `error`, `aborted`, or `length` stop reason.
598
+ With `--require-artifact-report`, Harnex also requires at least one structured
599
+ command/tool call or a Git delta (PTY adapters can satisfy it only with a Git
600
+ delta). If both are absent, it emits `task_failed` with
474
601
  `outcome_class=completed_no_activity`, writes a rejected observed receipt, and
475
- normalizes auto-stop to non-zero. This applies equally to
476
- `service_tier=flex` and `service_tier=fast` and deliberately ignores final
477
- answer text and claims. Intentional no-op work must still perform observable
602
+ normalizes auto-stop to non-zero. The gate deliberately ignores final answer
603
+ text and claims. Intentional no-op work must still perform observable
478
604
  inspection/validation; a model assertion of `no_change` cannot self-approve.
479
605
 
480
606
  PTY transports do not expose equivalent completion-item metadata, so their
481
- existing prompt-return auto-stop behavior remains. Non-Codex transports label
607
+ existing prompt-return auto-stop behavior remains. PTY transports label
482
608
  command observation `unsupported`; Git and terminal state are still recorded
483
609
  without guessing.
484
610
 
485
611
  ## Actuals
486
612
 
487
- At process exit, harnex collects usage through the active adapter. JSON-RPC
488
- Codex sessions read cumulative `thread/tokenUsage/updated` data, Pi RPC sessions
489
- read `get_session_stats`, and PTY adapters parse the last 16 KB of transcript
490
- when they support a parser. Adapters without a parser emit nullable usage
491
- fields. Separately, Pi aggregates bounded `contextUsage` samples and Codex
492
- aggregates `tokenUsage.last` plus `modelContextWindow`; neither source is
493
- substituted with cumulative usage when active occupancy is unavailable.
613
+ At process exit, harnex collects usage through the active adapter. Pi RPC
614
+ sessions read `get_session_stats`, and PTY adapters parse the last 16 KB of
615
+ transcript when they support a parser. Adapters without a parser emit nullable
616
+ usage fields. Separately, Pi aggregates bounded `contextUsage` samples; they are
617
+ never substituted with cumulative usage when active occupancy is unavailable.
494
618
 
495
619
  Git actuals capture the start/end SHA plus committed, staged, unstaged, and
496
620
  untracked changes relative to the worktree state observed at session start.
@@ -498,15 +622,25 @@ Unchanged pre-existing dirt is not credited to the worker, and harness-owned
498
622
  dispatch/receipt paths are excluded. Git failures leave the corresponding
499
623
  consolidated fields `null` and omit `git` events.
500
624
 
625
+ Git is observed in the repository the agent works in. A lane launched with
626
+ `--cwd <worktree> --root <checkout>`, where the two resolve to different Git
627
+ toplevels, is observed in its `--cwd` worktree: `commit_sha`, `branch`,
628
+ `start_sha`/`end_sha`, `observed.git`, activity and acceptance all describe
629
+ the lane, so supervisor commits on the root never count as the lane's work.
630
+ `--root` still decides where the rows are written. The `git` start event then
631
+ carries `git_root`, and the end row's `root_commit_sha` records the `--root`
632
+ repository's head (it is `null` for every other dispatch).
633
+
501
634
  The `actual` block includes model/effort hints from `--meta`, duration, token
502
635
  counts, `agent_session_id`, compatibility `cost_usd`, adapter transport, git
503
636
  deltas, exit reason, task completion state, signal/exit code, last error,
504
637
  operational counters (`stalls`, `force_resumes`, `disconnections`,
505
- `compactions`, `turn_count`, `tool_calls`, `commands_executed`), rate-limit
506
- payloads, output/event volume measurements, and output/events log paths. New
507
- additive attempt counters are `attempts_total`, `attempts_succeeded`,
508
- `attempts_failed`, `retry_count`, `throttle_429_count`, `disconnect_count`, and
509
- `fallback_triggered`. Throughput values are populated only for a
638
+ `compactions`, `turn_count`, `tool_calls`, `commands_executed`), `rate_limits`
639
+ (always null now; only older rows carry payloads), output/event volume
640
+ measurements, and output/events log paths. New additive attempt counters are
641
+ `attempts_total`, `attempts_succeeded`, `attempts_failed`, `retry_count`,
642
+ `throttle_429_count` (no current producer, so 0 in new rows), `disconnect_count`,
643
+ and `fallback_triggered`. Throughput values are populated only for a
510
644
  harness-accepted outcome: `throughput_tokens_per_s` and
511
645
  `throughput_successes_per_h`; `retry_tax_pct` is `0.0` when no retry occurred
512
646
  and `null` until a retry source can measure attributable wasted tokens.
@@ -538,8 +672,8 @@ jq -s 'map(select(.actual.attempts_total > 0))
538
672
  - `failure`: wrapped process exited non-zero, the observed-activity gate emitted
539
673
  `task_failed`, or Harnex could not write/validate the receipt.
540
674
  - `timeout`: wrapped process exited with code `124`.
541
- - `boot_failure`: JSON-RPC app-server exited within the startup window before a
542
- turn was observed.
675
+ - `boot_failure`: the structured Pi RPC transport exited within the startup
676
+ window (5 s) before a turn was observed.
543
677
  - `disconnected`: wrapped process exited `0` but no session summary was parsed.
544
678
 
545
679
  Canonical dispatch-stream writes remain best-effort. Receipt
data/docs/events.md CHANGED
@@ -39,7 +39,8 @@ Every row includes this envelope:
39
39
 
40
40
  - `schema_version` (Integer): always `1`
41
41
  - `seq` (Integer): monotonic per session, starts at `1`
42
- - `ts` (String): UTC ISO-8601 timestamp
42
+ - `ts` (String): UTC ISO-8601 timestamp with millisecond precision
43
+ (`2026-05-01T11:30:00.123Z`; rows before 0.16.0 have whole seconds)
43
44
  - `id` (String): session ID
44
45
  - `type` (String): event type
45
46
 
@@ -54,6 +55,18 @@ Emitted now (Layer 4):
54
55
  - `exited`: adds
55
56
  - `code` (Integer): synthesized numeric exit code
56
57
  - `signal` (Integer, optional): present for signaled exits
58
+ - `task_complete` / `task_failed` (structured adapters): add `turn` (Integer),
59
+ the session's work turn. Turn 1 is the first prompt; a message injected
60
+ after a published result starts the next turn, announced by a `work_turn`
61
+ row (`turn`). `harnex send` reports `after_seq`, the newest `seq` before
62
+ its message, so `harnex wait --until done --after-seq N` waits for the turn
63
+ that message started or joined. They also carry, when known:
64
+ `final_message` (the worker's last reply this turn, at most 4,000
65
+ characters), `final_message_path` (the full text),
66
+ `final_message_truncated`, `follow_ups` (`{sent, pending}`), and the lane's
67
+ `end_sha` / `commits` at the turn's end.
68
+ - `queue_update` (Pi): `follow_up` and `steering`, the sizes of Pi's pending
69
+ queues (counts only, never message text).
57
70
 
58
71
  ## 5. Stability promise
59
72
 
@@ -119,12 +132,12 @@ New event types:
119
132
  Example telemetry sequence:
120
133
 
121
134
  ```json
122
- {"schema_version":1,"seq":1,"ts":"2026-05-01T11:30:00Z","id":"cx-i-372","type":"started","pid":12345,"meta":{"issue":"23","plan":"27","predicted":{"input_tokens":[200000,800000]}}}
123
- {"schema_version":1,"seq":2,"ts":"2026-05-01T11:30:00Z","id":"cx-i-372","type":"git","phase":"start","sha":"a8114695c1f0","branch":"main"}
124
- {"schema_version":1,"seq":3,"ts":"2026-05-01T11:42:13Z","id":"cx-i-372","type":"usage","input_tokens":104158,"output_tokens":2709,"reasoning_tokens":870,"cached_tokens":250880,"total_tokens":106867,"agent_session_id":"019ddf05-0f03-7d70-904f-23db7f00640f"}
125
- {"schema_version":1,"seq":4,"ts":"2026-05-01T11:42:13Z","id":"cx-i-372","type":"git","phase":"end","sha":"abc1234567","loc_added":312,"loc_removed":65,"files_changed":7,"commits":1}
126
- {"schema_version":1,"seq":5,"ts":"2026-05-01T11:42:13Z","id":"cx-i-372","type":"summary","path":"/home/u/proj/.harnex/dispatch.jsonl","exit":"success"}
127
- {"schema_version":1,"seq":6,"ts":"2026-05-01T11:42:13Z","id":"cx-i-372","type":"exited","code":0,"reason":"success"}
135
+ {"schema_version":1,"seq":1,"ts":"2026-05-01T11:30:00Z","id":"pi-i-372","type":"started","pid":12345,"meta":{"issue":"23","plan":"27","predicted":{"input_tokens":[200000,800000]}}}
136
+ {"schema_version":1,"seq":2,"ts":"2026-05-01T11:30:00Z","id":"pi-i-372","type":"git","phase":"start","sha":"a8114695c1f0","branch":"main"}
137
+ {"schema_version":1,"seq":3,"ts":"2026-05-01T11:42:13Z","id":"pi-i-372","type":"usage","input_tokens":104158,"output_tokens":2709,"reasoning_tokens":870,"cached_tokens":250880,"total_tokens":106867,"agent_session_id":"019ddf05-0f03-7d70-904f-23db7f00640f"}
138
+ {"schema_version":1,"seq":4,"ts":"2026-05-01T11:42:13Z","id":"pi-i-372","type":"git","phase":"end","sha":"abc1234567","loc_added":312,"loc_removed":65,"files_changed":7,"commits":1}
139
+ {"schema_version":1,"seq":5,"ts":"2026-05-01T11:42:13Z","id":"pi-i-372","type":"summary","path":"/home/u/proj/.harnex/dispatch.jsonl","exit":"success"}
140
+ {"schema_version":1,"seq":6,"ts":"2026-05-01T11:42:13Z","id":"pi-i-372","type":"exited","code":0,"reason":"success"}
128
141
  ```
129
142
 
130
143
  Events files are runtime logs, not the durable dispatch summary. Their age and