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.
@@ -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).
@@ -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
 
@@ -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 `--parent-dispatch-id`, and any
66
- retry/fix/superseding dispatch whose named parent is still running in the
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
- (`--summary-out` / `.harnex/dispatch.jsonl` / exit status), returns non-zero for
95
- `task_failed` / failed terminal telemetry, returns `124` for `--max-wait`, and
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, append a suffix:
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 first attempt
75
- pi-i-42b second attempt
76
- pi-i-42c third attempt
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
- Keep the old session's logs. They are useful for diagnosis.
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` / summary
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.