harnex 0.8.0 → 0.9.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 +88 -0
- data/GUIDE.md +17 -10
- data/README.md +30 -23
- data/TECHNICAL.md +26 -5
- data/docs/codex-appserver.md +172 -0
- data/docs/configuration.md +111 -0
- data/docs/dispatch-telemetry.md +461 -0
- data/docs/events.md +133 -0
- data/guides/01_dispatch.md +6 -0
- data/guides/04_monitoring.md +6 -5
- 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/commands/doctor.rb +38 -4
- data/lib/harnex/commands/history.rb +7 -3
- data/lib/harnex/commands/run.rb +37 -10
- data/lib/harnex/config.rb +166 -0
- data/lib/harnex/core.rb +0 -7
- data/lib/harnex/dispatch_history.rb +13 -4
- data/lib/harnex/pricing.rb +135 -0
- data/lib/harnex/retention.rb +311 -0
- data/lib/harnex/runtime/session.rb +140 -12
- data/lib/harnex/terminal_status.rb +11 -1
- data/lib/harnex/version.rb +2 -2
- data/lib/harnex.rb +3 -0
- metadata +8 -1
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
# Dispatch Telemetry
|
|
2
|
+
|
|
3
|
+
`harnex run` captures predicted-vs-actual dispatch telemetry for a wrapped
|
|
4
|
+
agent session. Raw measurements remain on the v1 per-session events stream.
|
|
5
|
+
Durable summaries use one canonical v2 dispatch stream: every run appends one
|
|
6
|
+
`dispatch_start` row at registration and one rich `dispatch_end` row at
|
|
7
|
+
teardown.
|
|
8
|
+
|
|
9
|
+
Inside a git repo the stream is `<git-root>/.harnex/dispatch.jsonl`; outside a
|
|
10
|
+
git repo it is `~/.local/state/harnex/dispatch.jsonl`. `harnex history`,
|
|
11
|
+
`status --id`, and `wait` consume the same rows. Legacy v1 thin rows and
|
|
12
|
+
pre-v2 envelope-less summaries may coexist and remain readable/skippable.
|
|
13
|
+
|
|
14
|
+
## CLI flags
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
harnex run codex --meta '{"model":"gpt-5.3-codex","effort":"high","predicted":{"input_tokens":[200000,800000]}}'
|
|
18
|
+
harnex run codex --summary-out tmp/dispatch-summary.jsonl
|
|
19
|
+
harnex artifact-report init .harnex/reports/pi-i-61.json
|
|
20
|
+
harnex run pi --artifact-report .harnex/reports/pi-i-61.json --require-artifact-report --context 'Finalize $HARNEX_ARTIFACT_REPORT_PATH and validate it with --final'
|
|
21
|
+
harnex run pi --project-id harnex --queue-id queue-005 --entry-id SP-4 --phase implement --intent queue-work --require-attribution
|
|
22
|
+
harnex run pi --orchestration-run-id queue-005 --orchestration-generation-id gen-1 --orchestration-role worker
|
|
23
|
+
harnex orchestration report --dispatch .harnex/dispatch.jsonl --run-id queue-005 --json
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- `--meta JSON` must be a JSON object. The parsed object is echoed verbatim on
|
|
27
|
+
the `started.meta` event.
|
|
28
|
+
- `--summary-out PATH` is an explicit-only compatibility mirror. It appends
|
|
29
|
+
the identical v2 `dispatch_end` row to `PATH`; it has no default.
|
|
30
|
+
- `--artifact-report PATH` asks the worker to write a bounded
|
|
31
|
+
`harnex.artifact_report.v1` JSON sidecar. Harnex exposes the absolute path as
|
|
32
|
+
`HARNEX_ARTIFACT_REPORT_PATH` and ingests it at finalization.
|
|
33
|
+
- `--validation-report PATH` is an alias for `--artifact-report` and also makes
|
|
34
|
+
the same path available as `HARNEX_VALIDATION_REPORT_PATH` for worker prompts
|
|
35
|
+
that only need validation proof.
|
|
36
|
+
- `--require-artifact-report` requires one of those paths and turns report
|
|
37
|
+
acceptance into the run verdict. The worker also receives
|
|
38
|
+
`HARNEX_ARTIFACT_REPORT_REQUIRED=1`. Missing, malformed, unsupported,
|
|
39
|
+
oversized, schema-incomplete, rejected, or unchanged stale proof returns
|
|
40
|
+
non-zero; optional mode remains fail-soft.
|
|
41
|
+
- `harnex artifact-report init PATH` writes a schema-valid in-progress skeleton.
|
|
42
|
+
`harnex artifact-report validate PATH` checks field shapes, while `--final`
|
|
43
|
+
additionally requires accepted/no-change final proof. Both commands return
|
|
44
|
+
machine-readable diagnostics without echoing report payloads or transcripts.
|
|
45
|
+
- `--project-id`, `--queue-id`, `--entry-id`, `--entry-title`, `--phase`,
|
|
46
|
+
`--tier`, `--issue`, `--plan`, `--intent`, `--model`, `--effort`,
|
|
47
|
+
`--parent-dispatch-id`, `--parent-attempt-id`, and `--attempt-kind` are
|
|
48
|
+
first-class queue/agent telemetry flags. They are persisted as caller-provided
|
|
49
|
+
strings and override same-named `--meta` values. `--attempt-kind` is one of
|
|
50
|
+
`initial`, `retry`, `fix`, `review`, `fallback`, or `superseding`; linkage fields keep
|
|
51
|
+
independently-run follow-ups joinable without merging their raw usage.
|
|
52
|
+
- `--orchestration-run-id`, `--orchestration-generation-id`,
|
|
53
|
+
`--orchestration-role`, `--orchestration-session-id`, and
|
|
54
|
+
`--orchestration-rotation-reason` opt a dispatch row into logical
|
|
55
|
+
primary-orchestrator rollups. `--orchestration-role` is `primary` or
|
|
56
|
+
`worker`; Harnex-managed primaries should use `primary` instead of emitting a
|
|
57
|
+
duplicate external sample for the same usage row.
|
|
58
|
+
- `--require-attribution` fails before launch unless `project_id`, `phase`,
|
|
59
|
+
`intent`, and at least one of `queue_id` / `entry_id` / `issue` / `plan` are
|
|
60
|
+
present through first-class flags or `--meta`.
|
|
61
|
+
- Omitting `--summary-out` is the normal path: the canonical stream still gets
|
|
62
|
+
its start/end pair and no mirror is written.
|
|
63
|
+
- The v2 `dispatch_end` combines the history envelope (`schema_version`,
|
|
64
|
+
`record_type`, id/status/timing fields) with all rich telemetry sections.
|
|
65
|
+
A default dispatch therefore adds exactly two rows, not separate thin and
|
|
66
|
+
rich end rows.
|
|
67
|
+
|
|
68
|
+
Use `harnex history --json | jq .` for pipelines over the repo-local log.
|
|
69
|
+
|
|
70
|
+
## Metadata and prediction contract
|
|
71
|
+
|
|
72
|
+
The v2 `dispatch_end` always has `meta`, `predicted`, `actual`, `agent`,
|
|
73
|
+
`usage`, `context`, `attribution`, `outcome`, `attempt`, and `reliability`
|
|
74
|
+
blocks. When queue attribution fields are provided, harnex also adds a
|
|
75
|
+
top-level `queue` block. When orchestration fields are provided, harnex adds a
|
|
76
|
+
top-level `orchestration` block. When `--artifact-report` /
|
|
77
|
+
`--validation-report` is configured, harnex may also add `artifact_report`,
|
|
78
|
+
`validation`, and `artifacts` top-level blocks.
|
|
79
|
+
|
|
80
|
+
Harnex-owned `meta` fields are always populated when derivable: `id`,
|
|
81
|
+
`tmux_session`, `description`, `started_at`, `ended_at`, `harness`,
|
|
82
|
+
`harness_version`, `agent`, `agent_version`, `agent_provider`, `host`,
|
|
83
|
+
`platform`, `repo`, `branch`, `start_sha`, and `end_sha`.
|
|
84
|
+
|
|
85
|
+
These top-level `--meta` keys pass through into `meta` when provided:
|
|
86
|
+
`orchestrator`, `orchestrator_session`, `chain_id`, `parent_dispatch_id`,
|
|
87
|
+
`parent_attempt_id`, `attempt_kind`, `tier`, `phase`, `issue`, `plan`, and
|
|
88
|
+
`task_brief`. Queue-specific keys such as
|
|
89
|
+
`project_id`, `queue_id`, `entry_id`, `entry_title`, and `intent` are used for
|
|
90
|
+
the top-level `queue` block but are not duplicated into legacy `meta`. Unknown
|
|
91
|
+
top-level keys are kept on `started.meta` but are not copied into the v2 end
|
|
92
|
+
row.
|
|
93
|
+
|
|
94
|
+
`predicted` is copied verbatim from `--meta.predicted` when it is a JSON object;
|
|
95
|
+
otherwise it is `{}`. Harnex does no profile lookup or recommendation-table
|
|
96
|
+
resolution.
|
|
97
|
+
|
|
98
|
+
## Queue, agent, and reliability blocks
|
|
99
|
+
|
|
100
|
+
The top-level `queue` block is emitted only when at least one queue attribution
|
|
101
|
+
field is known. When present, it has a stable key set and preserves values as
|
|
102
|
+
strings:
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"queue": {
|
|
107
|
+
"project_id": "harnex",
|
|
108
|
+
"queue_id": "queue-005",
|
|
109
|
+
"entry_id": "SP-4",
|
|
110
|
+
"entry_title": "Implement sidecar ingestion",
|
|
111
|
+
"issue": "52",
|
|
112
|
+
"plan": "52",
|
|
113
|
+
"phase": "implement",
|
|
114
|
+
"tier": "B",
|
|
115
|
+
"intent": "queue-work"
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The top-level `agent` block is always emitted and is the preferred home for
|
|
121
|
+
routing details; legacy `meta.agent*` and `actual.model` stay for compatibility:
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"agent": {
|
|
126
|
+
"cli": "codex",
|
|
127
|
+
"provider": "openai",
|
|
128
|
+
"model_requested": "gpt-5.3-codex",
|
|
129
|
+
"model_effective": "gpt-5.3-codex",
|
|
130
|
+
"reasoning_effort": "high",
|
|
131
|
+
"service_tier": "flex",
|
|
132
|
+
"adapter_transport": "stdio_jsonrpc"
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The top-level `reliability` block is always emitted and should be preferred over
|
|
138
|
+
legacy `actual.disconnections` for reliability analytics:
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"reliability": {
|
|
143
|
+
"adapter_close": "normal",
|
|
144
|
+
"real_disconnections": 0,
|
|
145
|
+
"stream_interruptions": 0,
|
|
146
|
+
"stalls": 0,
|
|
147
|
+
"force_resumes": 0,
|
|
148
|
+
"compactions": 0,
|
|
149
|
+
"recovered": false
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`adapter_close` is `normal` for ordinary process/adapter completion,
|
|
155
|
+
`interrupted` for timeout/signal termination, `lost` for boot failure or real
|
|
156
|
+
transport loss, and `unknown` when harnex cannot classify it. Successful
|
|
157
|
+
structured runs that close normally after task completion should report
|
|
158
|
+
`real_disconnections: 0` even if old consumers still read the legacy counter.
|
|
159
|
+
|
|
160
|
+
Example grouping for queue analysis:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
jq -r 'select(.queue) | [.queue.project_id, .queue.queue_id, .queue.entry_id, .queue.phase, .agent.model_effective] | @tsv' .harnex/dispatch.jsonl
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Usage, context pressure, attribution, outcomes, and attempts
|
|
167
|
+
|
|
168
|
+
`usage` makes nullable legacy `actual` token and cost fields interpretable:
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"usage": {
|
|
173
|
+
"status": "observed",
|
|
174
|
+
"cost_usd": 1.42,
|
|
175
|
+
"cost_source": "price_table",
|
|
176
|
+
"cost_price_as_of": "2026-08-03",
|
|
177
|
+
"input_tokens": 120000,
|
|
178
|
+
"output_tokens": 8000,
|
|
179
|
+
"cached_input_tokens": 2000,
|
|
180
|
+
"reasoning_tokens": null,
|
|
181
|
+
"total_tokens": 130000
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`usage.status` is `observed` for an adapter measurement, `zero` for an explicit
|
|
187
|
+
all-zero adapter measurement, `estimated` for caller-supplied
|
|
188
|
+
`--meta '{"usage":{"status":"estimated",...}}'` values, `unsupported` when
|
|
189
|
+
the adapter has no supported usage source, or `missing` when a supported source
|
|
190
|
+
provided no observation. `cost_source` is `provider_reported` for a reliable
|
|
191
|
+
adapter value, `price_table` when Harnex computes exact maintained
|
|
192
|
+
provider/model/service-tier/context-band list pricing, and `caller_estimate` for
|
|
193
|
+
a declared estimate. Price-table rows carry `cost_price_as_of`; unknown models,
|
|
194
|
+
service/context tiers, missing context evidence, or required token components
|
|
195
|
+
remain null. Provider-reported values are never
|
|
196
|
+
overwritten, and all cost telemetry is operational estimation rather than a
|
|
197
|
+
billing invoice.
|
|
198
|
+
|
|
199
|
+
`context` is separate from cumulative `usage`: it describes how full the active
|
|
200
|
+
model context became, not how many tokens all requests accumulated:
|
|
201
|
+
|
|
202
|
+
```json
|
|
203
|
+
{
|
|
204
|
+
"context": {
|
|
205
|
+
"status": "observed",
|
|
206
|
+
"source": "pi_get_session_stats",
|
|
207
|
+
"terminal_tokens": 64000,
|
|
208
|
+
"window_tokens": 200000,
|
|
209
|
+
"terminal_percent": 32.0,
|
|
210
|
+
"peak_tokens": 118000,
|
|
211
|
+
"peak_percent": 59.0,
|
|
212
|
+
"samples": 7,
|
|
213
|
+
"missing_samples": 1,
|
|
214
|
+
"latest_sample_status": "missing"
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`terminal_*` is the final **valid** occupancy sample and `peak_*` is the
|
|
220
|
+
independent high-water mark across valid samples. `window_tokens` is the model
|
|
221
|
+
window paired with that terminal sample. `samples` counts bounded source
|
|
222
|
+
samples, including unavailable ones; `missing_samples` counts that unavailable
|
|
223
|
+
subset. Consequently, a null sample immediately after compaction leaves the
|
|
224
|
+
last valid terminal and peak values intact while setting
|
|
225
|
+
`latest_sample_status: "missing"`. Null never means zero.
|
|
226
|
+
|
|
227
|
+
`context.status` is `observed` for Pi's dedicated
|
|
228
|
+
`get_session_stats.contextUsage` signal, `estimated` for Codex app-server,
|
|
229
|
+
`missing` when a supported source yielded no valid occupancy, or `unsupported`
|
|
230
|
+
when the adapter has no active-context source. `source` is
|
|
231
|
+
`pi_get_session_stats` or `codex_thread_token_usage_last` for those structured
|
|
232
|
+
adapters and is null for unsupported adapters. Pi's percentage is adapter
|
|
233
|
+
reported. Codex's `tokenUsage.last.totalTokens` is the latest model-reported
|
|
234
|
+
active context size, but it excludes local items appended after that response;
|
|
235
|
+
Harnex therefore labels it estimated and derives
|
|
236
|
+
`terminal_percent = last.totalTokens / modelContextWindow * 100`. That is
|
|
237
|
+
full-window pressure, not Codex TUI's baseline-adjusted “context left” display.
|
|
238
|
+
No prompt, transcript, message, tool payload, or compaction summary is copied
|
|
239
|
+
into this block.
|
|
240
|
+
|
|
241
|
+
`attribution.status` is `complete` when `project_id`, `phase`, `intent`, and a
|
|
242
|
+
work id are present; `partial` when any attribution is known but that contract
|
|
243
|
+
is incomplete; otherwise `missing`. `outcome` keeps git observations separate
|
|
244
|
+
from semantic acceptance: its `status` is `accepted`, `rejected`, `no_change`,
|
|
245
|
+
or `unknown`; only a worker sidecar can assert accepted/rejected. Its additive
|
|
246
|
+
`class` records the proof verdict (`completed_with_proof`,
|
|
247
|
+
`completed_with_activity`, `completed_no_activity`, `report_missing`,
|
|
248
|
+
`report_invalid`, `report_rejected`, `task_failed`, or `unknown` in this slice),
|
|
249
|
+
and `report_status` records `accepted`, `missing`, `invalid`, `stale`,
|
|
250
|
+
`rejected`, or the underlying validator status when applicable. The block also
|
|
251
|
+
contains final commit/path/LOC observations and does **not** claim those changes
|
|
252
|
+
prove authorship or semantic quality.
|
|
253
|
+
|
|
254
|
+
Every end row has one Harnex-session `attempt`. Its random `id` is distinct
|
|
255
|
+
from operator-visible `run_id`; `parent_attempt_id` and `parent_dispatch_id`
|
|
256
|
+
link retries/fixes/reviews/fallbacks while each row keeps separate raw usage.
|
|
257
|
+
At finalization Harnex walks the canonical stream's parent chain once to derive
|
|
258
|
+
`actual.attempts_total`, succeeded/failed counts, `fallback_triggered`, and
|
|
259
|
+
`reliability.recovered`; missing parents and malformed/cyclic history degrade
|
|
260
|
+
safely to the resolvable chain. `retry_count` remains the separate in-run event
|
|
261
|
+
counter. The events JSONL also carries `attempt_started`, `attempt_finished`,
|
|
262
|
+
and adapter-reported retry/fallback events.
|
|
263
|
+
|
|
264
|
+
## Orchestration tax rollups
|
|
265
|
+
|
|
266
|
+
`harnex orchestration` joins one logical primary-orchestrator run across
|
|
267
|
+
primary generations and child dispatches. It is opt-in and bounded: the sample
|
|
268
|
+
path stores counters and lifecycle labels only.
|
|
269
|
+
|
|
270
|
+
Harnex-managed primaries can be represented directly by their dispatch row:
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
harnex run pi --orchestration-run-id queue-005 \
|
|
274
|
+
--orchestration-generation-id primary-1 --orchestration-role primary ...
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
External interactive primaries can emit bounded samples through an integration
|
|
278
|
+
or shell command:
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
harnex orchestration sample --out .harnex/orchestrator.jsonl \
|
|
282
|
+
--run-id queue-005 --generation-id primary-1 --project-id harnex \
|
|
283
|
+
--queue-id queue-005 --session-id pi-primary-1 \
|
|
284
|
+
--context-status observed --context-tokens 64000 \
|
|
285
|
+
--context-window-tokens 200000 --context-percent 32 \
|
|
286
|
+
--usage-status observed --usage-input-tokens 120000 \
|
|
287
|
+
--usage-output-tokens 9000 --usage-total-tokens 129000 \
|
|
288
|
+
--tool-calls 31 --compactions 1
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
The sample schema is `harnex.orchestrator_sample.v1`. Valid sample events are
|
|
292
|
+
`sample`, `generation_started`, `generation_finished`, `rotation`, `recovery`,
|
|
293
|
+
and `compaction`. Samples must never include prompts, transcripts, hidden
|
|
294
|
+
reasoning, tool arguments/results, secrets, or private payloads.
|
|
295
|
+
|
|
296
|
+
Reports join dispatch rows whose `orchestration.run_id` matches the requested
|
|
297
|
+
run and optional external samples with the same `orchestration_run_id`:
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
harnex orchestration report --dispatch .harnex/dispatch.jsonl \
|
|
301
|
+
--samples .harnex/orchestrator.jsonl --run-id queue-005 --json
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
The report schema is `harnex.orchestration_tax.v1`. It includes primary usage
|
|
305
|
+
and context coverage, per-generation peaks and rotation reasons, worker usage,
|
|
306
|
+
accepted/rejected/blocked/unknown child outcomes deduplicated by work id,
|
|
307
|
+
primary usage/tool calls per accepted entry, and explicit `missing` /
|
|
308
|
+
`unsupported` statuses instead of treating absent telemetry as zero.
|
|
309
|
+
|
|
310
|
+
## Artifact and validation sidecars
|
|
311
|
+
|
|
312
|
+
The artifact report sidecar is deliberately small and links machine-readable
|
|
313
|
+
proof to canonical human-readable artifacts (usually files under `koder/`). A
|
|
314
|
+
valid v1 report looks like:
|
|
315
|
+
|
|
316
|
+
```json
|
|
317
|
+
{
|
|
318
|
+
"schema": "harnex.artifact_report.v1",
|
|
319
|
+
"status": "pass",
|
|
320
|
+
"outcome": {
|
|
321
|
+
"status": "accepted",
|
|
322
|
+
"summary": "Queue gate accepted the implementation."
|
|
323
|
+
},
|
|
324
|
+
"canonical_artifacts": ["koder/issues/52_typed_artifact_validation_sidecars.md"],
|
|
325
|
+
"validation": {
|
|
326
|
+
"status": "pass",
|
|
327
|
+
"final_reported": true,
|
|
328
|
+
"commands": [
|
|
329
|
+
{ "cmd": "ruby -Ilib -Itest -e 'Dir[\"test/**/*_test.rb\"].each { |f| require_relative f }'", "exit_code": 0 }
|
|
330
|
+
]
|
|
331
|
+
},
|
|
332
|
+
"artifacts": [
|
|
333
|
+
{
|
|
334
|
+
"type": "gate",
|
|
335
|
+
"summary": "Full suite passed.",
|
|
336
|
+
"evidence": ["495 runs, 1708 assertions, 0 failures"],
|
|
337
|
+
"confidence": 1.0,
|
|
338
|
+
"canonical_ref": "koder/issues/52_typed_artifact_validation_sidecars.md"
|
|
339
|
+
}
|
|
340
|
+
]
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Create the bounded skeleton and validate it directly rather than reproducing
|
|
345
|
+
schema prose in a worker prompt:
|
|
346
|
+
|
|
347
|
+
```bash
|
|
348
|
+
harnex artifact-report init .harnex/reports/cx-i-61.json
|
|
349
|
+
harnex artifact-report validate .harnex/reports/cx-i-61.json
|
|
350
|
+
# After the worker sets status/outcome/validation and final_reported=true:
|
|
351
|
+
harnex artifact-report validate .harnex/reports/cx-i-61.json --final
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Normal validation requires the real schema and typed field shapes. In
|
|
355
|
+
particular, `outcome` is an object (not the string `"accepted"`) and every
|
|
356
|
+
listed validation command has a non-empty `cmd` plus integer `exit_code`.
|
|
357
|
+
Final validation additionally requires top-level `status: "pass"`, an
|
|
358
|
+
`accepted` or `no_change` outcome with a non-empty summary,
|
|
359
|
+
`validation.final_reported: true`, and successful command exit codes. A
|
|
360
|
+
`no_change` outcome may use `validation.status: "not_run"` and an empty command
|
|
361
|
+
list when no command is appropriate.
|
|
362
|
+
|
|
363
|
+
At finalization, harnex reads at most 256 KiB from the configured file. Valid
|
|
364
|
+
reports add compact `validation` and `artifacts` blocks to the dispatch row. A
|
|
365
|
+
valid optional `outcome.status` (`accepted`, `rejected`, `no_change`, or
|
|
366
|
+
`unknown`) is copied into top-level outcome evidence; it is the only source that
|
|
367
|
+
can assert semantic acceptance or rejection. The `artifact_report` block always
|
|
368
|
+
records sidecar `path`, `bytes`, `sha256`, `schema`, and `ingest_status` when a
|
|
369
|
+
path was configured.
|
|
370
|
+
|
|
371
|
+
Without `--require-artifact-report`, missing, malformed, unsupported-schema,
|
|
372
|
+
oversized, and shape-invalid reports remain fail-soft warning telemetry and do
|
|
373
|
+
not change the wrapped process exit code. Strict mode evaluates final validation
|
|
374
|
+
before successful auto-stop/terminal acceptance and returns non-zero for those
|
|
375
|
+
defects, rejected outcomes, or a valid final report that was already present
|
|
376
|
+
and unchanged when the session started. Only the configured path is read;
|
|
377
|
+
report-shaped JSON in final prose is not scraped. The typed failure is exposed
|
|
378
|
+
before auto-stop through `task_failed`, `outcome.class`, and
|
|
379
|
+
`outcome.report_status` so `harnex watch --until done` returns non-zero.
|
|
380
|
+
|
|
381
|
+
Harnex does not copy large transcripts or replace plain-text `koder/` docs. The
|
|
382
|
+
sidecar is an evidence index for queue tooling; the canonical explanation should
|
|
383
|
+
remain in the referenced files.
|
|
384
|
+
|
|
385
|
+
## Autonomous completion gate
|
|
386
|
+
|
|
387
|
+
For Codex app-server runs launched with `--context`, provider turn completion
|
|
388
|
+
is not by itself accepted work completion. Harnex emits `task_complete` only when
|
|
389
|
+
it has at least one structured command/tool/file-change item, a Git delta, or a
|
|
390
|
+
fresh final report accepted by the contract above. If all are absent, it emits
|
|
391
|
+
`task_failed` with `outcome_class=completed_no_activity` before teardown and
|
|
392
|
+
normalizes the auto-stop verdict to non-zero. This applies equally to Codex
|
|
393
|
+
`service_tier=flex` and `service_tier=fast` and deliberately ignores final
|
|
394
|
+
answer text. Intentional no-op work should use a valid fresh `no_change` report.
|
|
395
|
+
|
|
396
|
+
PTY transports do not expose equivalent item metadata, so their existing
|
|
397
|
+
prompt-return auto-stop behavior remains. `--require-artifact-report` is the
|
|
398
|
+
transport-independent way to require explicit proof on PTY or structured runs.
|
|
399
|
+
|
|
400
|
+
## Actuals
|
|
401
|
+
|
|
402
|
+
At process exit, harnex collects usage through the active adapter. JSON-RPC
|
|
403
|
+
Codex sessions read cumulative `thread/tokenUsage/updated` data, Pi RPC sessions
|
|
404
|
+
read `get_session_stats`, and PTY adapters parse the last 16 KB of transcript
|
|
405
|
+
when they support a parser. Adapters without a parser emit nullable usage
|
|
406
|
+
fields. Separately, Pi aggregates bounded `contextUsage` samples and Codex
|
|
407
|
+
aggregates `tokenUsage.last` plus `modelContextWindow`; neither source is
|
|
408
|
+
substituted with cumulative usage when active occupancy is unavailable.
|
|
409
|
+
|
|
410
|
+
Git actuals are captured with `git rev-parse`, `git diff --shortstat`, and
|
|
411
|
+
`git rev-list --count` between the start and end SHAs. Git failures leave the
|
|
412
|
+
corresponding consolidated fields `null` and omit `git` events.
|
|
413
|
+
|
|
414
|
+
The `actual` block includes model/effort hints from `--meta`, duration, token
|
|
415
|
+
counts, `agent_session_id`, compatibility `cost_usd`, adapter transport, git
|
|
416
|
+
deltas, exit reason, task completion state, signal/exit code, last error,
|
|
417
|
+
operational counters (`stalls`, `force_resumes`, `disconnections`,
|
|
418
|
+
`compactions`, `turn_count`, `tool_calls`, `commands_executed`), rate-limit
|
|
419
|
+
payloads, output/event volume measurements, and output/events log paths. New
|
|
420
|
+
additive attempt counters are `attempts_total`, `attempts_succeeded`,
|
|
421
|
+
`attempts_failed`, `retry_count`, `throttle_429_count`, `disconnect_count`, and
|
|
422
|
+
`fallback_triggered`. Throughput values are populated only for a
|
|
423
|
+
sidecar-accepted outcome: `throughput_tokens_per_s` and
|
|
424
|
+
`throughput_successes_per_h`; `retry_tax_pct` is `0.0` when no retry occurred
|
|
425
|
+
and `null` until a retry source can measure attributable wasted tokens.
|
|
426
|
+
|
|
427
|
+
Legacy `actual.cost_usd` reflects adapter/provider-reported cost only. Prefer
|
|
428
|
+
the top-level `usage` block: it also carries price-table-derived cost and its
|
|
429
|
+
`cost_source` / `cost_price_as_of` provenance. Claude PTY currently has no
|
|
430
|
+
bounded usage producer and reports `unsupported`; it is not silently priced.
|
|
431
|
+
|
|
432
|
+
Examples for downstream analysis (never treat missing usage as zero):
|
|
433
|
+
|
|
434
|
+
```bash
|
|
435
|
+
# Accepted successes per hour, grouped by project/phase/effective model.
|
|
436
|
+
jq -s 'map(select(.outcome.status == "accepted" and .attribution.status == "complete"))
|
|
437
|
+
| group_by([.attribution.project_id, .attribution.phase, .agent.model_effective])
|
|
438
|
+
| map({group: .[0].attribution.project_id + "/" + .[0].attribution.phase + "/" + .[0].agent.model_effective,
|
|
439
|
+
successes_per_hour: ((length * 3600) / (map(.actual.duration_s) | add))})' .harnex/dispatch.jsonl
|
|
440
|
+
|
|
441
|
+
# Retry and real-disconnect rates for completed rows.
|
|
442
|
+
jq -s 'map(select(.actual.attempts_total > 0))
|
|
443
|
+
| {retry_rate: ((map(.actual.retry_count) | add) / (map(.actual.attempts_total) | add)),
|
|
444
|
+
disconnect_rate: ((map(.actual.disconnect_count) | add) / (map(.actual.attempts_total) | add))}' .harnex/dispatch.jsonl
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
## Exit taxonomy
|
|
448
|
+
|
|
449
|
+
- `success`: wrapped process exited `0` with task completion, accepted strict
|
|
450
|
+
report proof, or an adapter session summary.
|
|
451
|
+
- `failure`: wrapped process exited non-zero or a completion/report proof gate
|
|
452
|
+
emitted `task_failed`.
|
|
453
|
+
- `timeout`: wrapped process exited with code `124`.
|
|
454
|
+
- `boot_failure`: JSON-RPC app-server exited within the startup window before a
|
|
455
|
+
turn was observed.
|
|
456
|
+
- `disconnected`: wrapped process exited `0` but no session summary was parsed.
|
|
457
|
+
|
|
458
|
+
Canonical dispatch-stream and explicit mirror writes are best-effort. Write
|
|
459
|
+
failures are printed as warnings and do not change the wrapped process exit
|
|
460
|
+
code. Repo phase allowlists and runtime-log retention are documented in
|
|
461
|
+
[configuration.md](configuration.md).
|
data/docs/events.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# `harnex events` (v1)
|
|
2
|
+
|
|
3
|
+
## 1. Purpose and non-goals
|
|
4
|
+
|
|
5
|
+
`harnex events` provides a per-session JSONL stream for orchestration and
|
|
6
|
+
monitoring tooling.
|
|
7
|
+
|
|
8
|
+
This layer is transport + contract only. It does not implement watcher policy
|
|
9
|
+
or preset logic.
|
|
10
|
+
|
|
11
|
+
## 2. CLI usage
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
harnex events --id ID [--repo PATH] [--cli CLI] [--from ISO8601]
|
|
15
|
+
harnex events --id ID --snapshot
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
- `--id ID` is required.
|
|
19
|
+
- `--follow` is enabled by default.
|
|
20
|
+
- `--snapshot` is non-blocking (`--no-follow` alias behavior).
|
|
21
|
+
- `--from` accepts ISO-8601 only (`ts >= from` replay filter).
|
|
22
|
+
|
|
23
|
+
Follow mode exits `0` when the target session emits `type: "exited"`.
|
|
24
|
+
|
|
25
|
+
## 3. Transport and file location
|
|
26
|
+
|
|
27
|
+
Events are append-only JSONL rows at:
|
|
28
|
+
|
|
29
|
+
```text
|
|
30
|
+
~/.local/state/harnex/events/<repo_key>--<id_key>.jsonl
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Writers append one event per line and flush after each append.
|
|
34
|
+
Readers can snapshot + tail the same file.
|
|
35
|
+
|
|
36
|
+
## 4. v1 schema reference
|
|
37
|
+
|
|
38
|
+
Every row includes this envelope:
|
|
39
|
+
|
|
40
|
+
- `schema_version` (Integer): always `1`
|
|
41
|
+
- `seq` (Integer): monotonic per session, starts at `1`
|
|
42
|
+
- `ts` (String): UTC ISO-8601 timestamp
|
|
43
|
+
- `id` (String): session ID
|
|
44
|
+
- `type` (String): event type
|
|
45
|
+
|
|
46
|
+
Emitted now (Layer 4):
|
|
47
|
+
|
|
48
|
+
- `started`: adds `pid` (Integer)
|
|
49
|
+
- `send`: adds
|
|
50
|
+
- `msg` (String): first 200 characters of the original text; if longer,
|
|
51
|
+
a trailing `…` is appended
|
|
52
|
+
- `msg_truncated` (Boolean): whether truncation occurred
|
|
53
|
+
- `forced` (Boolean): send force mode
|
|
54
|
+
- `exited`: adds
|
|
55
|
+
- `code` (Integer): synthesized numeric exit code
|
|
56
|
+
- `signal` (Integer, optional): present for signaled exits
|
|
57
|
+
|
|
58
|
+
## 5. Stability promise
|
|
59
|
+
|
|
60
|
+
Schema v1 is additive-only:
|
|
61
|
+
|
|
62
|
+
- existing fields will not be removed, renamed, or type-changed
|
|
63
|
+
- new fields and new event types may be added
|
|
64
|
+
|
|
65
|
+
Breaking changes require a major schema bump.
|
|
66
|
+
|
|
67
|
+
## 6. Consumer patterns
|
|
68
|
+
|
|
69
|
+
Snapshot:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
harnex events --id worker --snapshot
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Follow:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
harnex events --id worker | jq -c '.'
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Replay from a timestamp:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
harnex events --id worker --snapshot --from 2026-04-29T10:00:00Z
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## 7. Layer 2 integration note
|
|
88
|
+
|
|
89
|
+
Layer 4 defines the bus and schema. In a follow-up layer, watcher-owned
|
|
90
|
+
producers can publish `resume`, `log_active`, and `log_idle` events to this
|
|
91
|
+
same stream. `harnex events` remains a read-only consumer surface.
|
|
92
|
+
|
|
93
|
+
## 8. Layer 5: dispatch telemetry
|
|
94
|
+
|
|
95
|
+
Layer 5 adds dispatch telemetry events without changing schema version `1`.
|
|
96
|
+
The additions are optional for legacy consumers: existing event types keep
|
|
97
|
+
their fields, and new event types can be ignored by readers that do not need
|
|
98
|
+
telemetry.
|
|
99
|
+
|
|
100
|
+
New optional fields on existing event types:
|
|
101
|
+
|
|
102
|
+
- `started.meta` (Object, optional): parsed verbatim from `harnex run --meta`.
|
|
103
|
+
It is absent when `--meta` is not provided.
|
|
104
|
+
- `exited.reason` (String, optional): one of `success`, `failure`, `timeout`,
|
|
105
|
+
or `disconnected`.
|
|
106
|
+
|
|
107
|
+
New event types:
|
|
108
|
+
|
|
109
|
+
- `usage`: emitted once after the wrapped process exits and before `exited`.
|
|
110
|
+
It includes nullable `input_tokens`, `output_tokens`, `reasoning_tokens`,
|
|
111
|
+
`cached_tokens`, `total_tokens`, and `agent_session_id`.
|
|
112
|
+
- `git`: emitted when git metadata is available. `phase: "start"` includes
|
|
113
|
+
`sha` and `branch`; `phase: "end"` includes `sha`, `loc_added`,
|
|
114
|
+
`loc_removed`, `files_changed`, and `commits`.
|
|
115
|
+
- `summary`: emitted last before `exited`. `path` is the canonical tracked
|
|
116
|
+
dispatch stream. `mirror_path` is present only when explicit
|
|
117
|
+
`--summary-out PATH` was configured. `exit` is `success`, `failure`,
|
|
118
|
+
`timeout`, or `disconnected`.
|
|
119
|
+
|
|
120
|
+
Example telemetry sequence:
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{"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]}}}
|
|
124
|
+
{"schema_version":1,"seq":2,"ts":"2026-05-01T11:30:00Z","id":"cx-i-372","type":"git","phase":"start","sha":"a8114695c1f0","branch":"main"}
|
|
125
|
+
{"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"}
|
|
126
|
+
{"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}
|
|
127
|
+
{"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"}
|
|
128
|
+
{"schema_version":1,"seq":6,"ts":"2026-05-01T11:42:13Z","id":"cx-i-372","type":"exited","code":0,"reason":"success"}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Events files are runtime logs, not the durable dispatch summary. Their age and
|
|
132
|
+
size retention is configurable; current/live-session files are protected. See
|
|
133
|
+
[configuration.md](configuration.md).
|
data/guides/01_dispatch.md
CHANGED
|
@@ -145,6 +145,12 @@ harnex run pi --id pi-i-NN --tmux pi-i-NN \
|
|
|
145
145
|
`intent`, and at least one work id (`queue_id`, `entry_id`, `issue`, or `plan`)
|
|
146
146
|
are present.
|
|
147
147
|
|
|
148
|
+
Every dispatch writes one `dispatch_start` and one rich v2 `dispatch_end` row
|
|
149
|
+
to the canonical repo/global dispatch stream. Do not pass `--summary-out` for
|
|
150
|
+
normal telemetry; it is an explicit-only compatibility mirror of the same end
|
|
151
|
+
row. Repo `.harnex/config.json` can warn on or reject non-canonical phase names
|
|
152
|
+
before spawn.
|
|
153
|
+
|
|
148
154
|
Pi runs use structured RPC (`pi --mode rpc`). Pass Pi child flags after `--`
|
|
149
155
|
(e.g. `harnex run pi --context "..." -- --model anthropic/claude-sonnet-4-5 --thinking high`).
|
|
150
156
|
|
data/guides/04_monitoring.md
CHANGED
|
@@ -62,9 +62,9 @@ a coordination error (wrong id or wrong repo), and only `0` as success.
|
|
|
62
62
|
|
|
63
63
|
## Duplicate-Dispatch Guard
|
|
64
64
|
|
|
65
|
-
`harnex run --attempt-kind retry` requires
|
|
66
|
-
retry/fix/superseding dispatch whose
|
|
67
|
-
same repo is refused. Wait for the parent
|
|
65
|
+
`harnex run --attempt-kind retry|fallback` requires
|
|
66
|
+
`--parent-dispatch-id`, and any retry/fix/fallback/superseding dispatch whose
|
|
67
|
+
named parent is still running in the same repo is refused. Wait for the parent
|
|
68
68
|
(`harnex wait --id <parent> --until done`) or stop it first. Pass
|
|
69
69
|
`--allow-live-parent` only for intentional parallelism (e.g. isolated
|
|
70
70
|
worktrees). `--attempt-kind review` is exempt: a completed parent may still
|
|
@@ -91,8 +91,9 @@ harnex watch --id pi-i-NN --until done --max-wait 90m \
|
|
|
91
91
|
|
|
92
92
|
`harnex watch --until done` wraps the `harnex wait --until done` work fence:
|
|
93
93
|
it succeeds from `task_complete` or durable successful terminal telemetry
|
|
94
|
-
(
|
|
95
|
-
|
|
94
|
+
(the v2 `dispatch_end` in `.harnex/dispatch.jsonl`, an explicit mirror when
|
|
95
|
+
configured, or exit status), returns non-zero for `task_failed` / failed
|
|
96
|
+
terminal telemetry, returns `124` for `--max-wait`, and
|
|
96
97
|
only writes done/fail markers as compatibility outputs after harnex has seen a
|
|
97
98
|
terminal work signal.
|
|
98
99
|
|
data/guides/05_naming.md
CHANGED
|
@@ -66,17 +66,27 @@ If `--id` is missing, harnex generates a random session ID. The tmux window may
|
|
|
66
66
|
look right, but `harnex status`, `harnex pane --id`, and logs need the random
|
|
67
67
|
ID.
|
|
68
68
|
|
|
69
|
-
## Retry Suffixes
|
|
69
|
+
## Retry Suffixes And Linkage
|
|
70
70
|
|
|
71
|
-
If a session fails and you dispatch a fresh attempt,
|
|
71
|
+
If a session fails and you dispatch a fresh attempt, use a new ID and link it
|
|
72
|
+
to the completed parent so Harnex can derive chain counts and recovery:
|
|
72
73
|
|
|
73
74
|
```text
|
|
74
|
-
pi-i-42
|
|
75
|
-
pi-i-
|
|
76
|
-
pi-i-
|
|
75
|
+
pi-i-42 initial attempt
|
|
76
|
+
pi-i-42-r1 retry of pi-i-42
|
|
77
|
+
pi-i-42-r2 retry of pi-i-42-r1
|
|
77
78
|
```
|
|
78
79
|
|
|
79
|
-
|
|
80
|
+
```bash
|
|
81
|
+
harnex run pi --id pi-i-42-r1 --tmux pi-i-42-r1 \
|
|
82
|
+
--attempt-kind retry --parent-dispatch-id pi-i-42 \
|
|
83
|
+
--context "Retry the bounded task."
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Retry/fix/fallback/superseding work is refused while its named parent is still
|
|
87
|
+
running unless `--allow-live-parent` explicitly authorizes isolated parallelism.
|
|
88
|
+
Keep old logs for diagnosis; retention removes only expired/over-cap logs that
|
|
89
|
+
do not belong to a current or live session.
|
|
80
90
|
|
|
81
91
|
## Task Files
|
|
82
92
|
|
|
@@ -104,8 +114,8 @@ If a legacy workflow still expects a done marker, derive it from the session ID:
|
|
|
104
114
|
```
|
|
105
115
|
|
|
106
116
|
Treat done markers as compatibility hints only. Canonical completion should come
|
|
107
|
-
from harnex terminal telemetry (`harnex wait` / `harnex status --json` /
|
|
108
|
-
rows in `.harnex/dispatch.jsonl`).
|
|
117
|
+
from harnex terminal telemetry (`harnex wait` / `harnex status --json` / v2
|
|
118
|
+
`dispatch_end` rows in `.harnex/dispatch.jsonl`).
|
|
109
119
|
|
|
110
120
|
When a brief asks for a completion marker, make it one line and include the
|
|
111
121
|
highest-signal result: tests passed, review clean, or the blocking issue.
|