processkit 0.10.0

Child-process management: kill-on-drop process trees and async run-and-capture
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
# Changelog

All notable changes to this project are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

Add entries to `[Unreleased]` as you work — manual bullets always win over the
git-cliff auto-fill (config: `cliff.toml`). On release, promote `[Unreleased]`
to a dated version section.

## [Unreleased]

### Added
-

### Changed
-

### Fixed
-

## [0.10.0] - 2026-06-14

### Added

- `OutputBufferPolicy::with_max_bytes(n)` (and a `max_bytes` field) — a retained-byte
  ceiling, independent of `max_lines`, so one enormous newline-free line can no longer
  evade the line cap and exhaust memory. Composes with `bounded`/`fail_loud`/`unbounded`;
  under `OverflowMode::Error` it is a fail-loud byte ceiling.
- `ScriptedRunner::on_sequence(prefix, replies)` — serve an ordered sequence of replies
  (each once in turn, then the last repeats forever), matching the cassette replay model
  so a fail-then-succeed retry scenario is scriptable declaratively.
- `Error::CassetteMiss { program }` — a cassette replay with no matching recording (a
  stale or incomplete cassette), kept distinct from a missing-program error so
  `is_not_found()` is `false` and a wrapper can't mistake it for an absent optional tool.
- `RunningProcess::shutdown(grace)` (D4) — gracefully stop a started handle's process tree:
  `SIGTERM`, wait up to `grace`, then `SIGKILL` survivors (atomic on Windows), returning the
  resulting `Outcome`. The "started a dev server, exercised it, now stop it cleanly" verb — the
  graceful counterpart to dropping the handle (hard kill) or `start_kill`. Own-group handles
  (`Command::start`/`JobRunner`) only; a shared-group handle (`ProcessGroup::start`) returns
  `Error::Unsupported` (use `ProcessGroup::shutdown`).
- `CliClient` verbs now take an **argument list directly** (D7): `git.run(["status"])`
  instead of the double-mention `git.run(git.command(["status"]))`. A new sealed
  `IntoCommand` trait lets every verb (`run`/`output`/`output_bytes`/`run_unit`/`exit_code`/
  `probe`/`parse`/`try_parse`) accept either an argument list (built for the client's program
  with its defaults) **or** a ready-made `Command` (for per-call customization) — so existing
  `git.run(git.command(…))` call sites keep compiling. Two missing verbs were added to
  `CliClient`: `checked` and `first_line`.
- `ProcessRunner::output_bytes` (with a default impl) and `CliClient::output_bytes` (D5) —
  raw-byte stdout capture is now part of the runner **seam**, not just `Command`, so a
  byte-producing tool (`git cat-file`, `tar -c`, an image transcoder) is testable through a
  `ScriptedRunner` / `&ProcessGroup` / `JobRunner` exactly like a text one. The default
  routes through `start`, so a runner that overrides `start` gets it for free; an
  `output`-only runner surfaces `Error::Unsupported`, matching `start`. (Adding a defaulted
  trait method is source-compatible — existing `impl ProcessRunner` blocks keep compiling.)

### Changed

- **Breaking:** `Error::OutputTooLarge` fields changed from `{ program, limit, total_lines }`
  to `{ program, line_limit: Option<usize>, byte_limit: Option<usize>, total_lines,
  total_bytes }` — the ceiling can now be a line cap, a byte cap, or both, so the error
  reports each configured cap and both totals. The `Display` message changed to match.
- **Breaking:** `Command::stdout_tee<W>` / `stderr_tee<W>` now take a
  `tokio::io::AsyncWrite` sink (was `std::io::Write`). The write is awaited on the capture
  pump, so a slow sink applies backpressure rather than blocking the runtime, and a write
  error disables the tee with a `tracing` warn instead of being silently swallowed. The
  tee now runs **independently** of `on_stdout_line` (it no longer replaces the handler).
- The fail-loud `OverflowMode::Error` ceiling now fires on the **cumulative** output the
  pump has seen (total lines / bytes), not the current backlog — so a streaming consumer
  draining lines as they arrive can no longer evade it.
- `ProcessGroup::terminate_all` / `shutdown` / `signal` now return `Err` when the pre-5.14
  Linux cgroup per-pid `SIGKILL` fallback cannot drain the tree (a fork bomb still
  out-spawning, or `D`-state zombies) — previously a false success. The atomic backends
  (cgroup `kill`, Windows Job Objects, the POSIX process-group fallback) never report this.
- `ProcessGroup::adopt` of a child that has exited but is **not yet reaped** is now a
  successful no-op (`Ok`) on the containment backends, instead of surfacing the backend's
  raw assign/write error. (An already-*reaped* child still errors — no pid/handle left.)
- **Breaking:** `ScriptedRunner::on(prefix, …)` now matches the **program name** as well
  as the arguments — the first prefix element is the program, so `.on(["git", "status"])`
  answers for `git status` but not `rm status` (aligning with the program-aware cassette
  key). Existing argument-only rules must prepend the program name.
- **Breaking:** a cassette replay that finds no matching recording now returns
  `Error::CassetteMiss` instead of `Error::Spawn` with a not-found source.
- **Breaking:** "program not found" now has a **single representation** (D11). Every
  launch failure where the program can't be located — a bare name absent from `PATH`, a
  path that doesn't resolve, a customized `PATH` — surfaces as `Error::NotFound`, and
  `Error::NotFound::searched` changed from `String` to `Option<String>` (`Some(dirs)` when a
  bare name was searched against `PATH`, `None` when no `PATH` search applied). As a result
  **`is_not_found()` is now true *only* for `Error::NotFound`**: a missing or invalid working
  directory (a `Spawn` carrying a `NotFound` io kind), a program that is installed but
  not directly executable (a Windows `.cmd`/`.bat`, surfaced as `Spawn`), and a missing
  cassette *file* (an `Io` not-found) are no longer reported as "not found", so the
  "command not installed?" hint can't misfire. `Error::NotFound`'s `Display` now says
  "not found on PATH" only when a `PATH` search happened (`searched` is `Some`); a path-form
  or customized-`PATH` program reads simply "not found".
- **Breaking:** run cancellation is now a **core feature, not opt-in** — the `cancellation`
  Cargo feature is removed. `Command::cancel_on`, `CliClient::default_cancel_on`,
  `Error::Cancelled`, `Reply::pending`, and the re-exported `CancellationToken` are always
  available, and `tokio-util` is now an unconditional dependency. Remove `"cancellation"`
  from your `features` list (a build that named it will fail with "unknown feature"); no code
  change is otherwise needed. Cancellation is core semantics, not an option — the feature gate
  bought little (`tokio-util`'s `sync` module is tiny and usually already in the graph) at the
  cost of the crate's largest `#[cfg]` surface.
- **Breaking:** `processkit` now supports **only Unix and Windows targets**. A bare target
  (e.g. `wasm32-unknown-unknown`) no longer compiles a containment-less fallback that
  couldn't honor kill-on-close or a graceful timeout — it now fails at compile time, via a
  `compile_error!` guard (or, since the crate needs `tokio::process`, earlier in tokio's own
  dependencies, which don't support such targets). The `Mechanism::None` variant (only ever
  produced on those targets) is removed; `Mechanism` stays `#[non_exhaustive]`, so a future
  fallback can re-add it.
- **Breaking:** `Error::Timeout` and `Error::Signalled` now carry `stdout` and `stderr`
  fields (D12) — whatever the run captured before the deadline or signal killed it. A
  hung-then-killed tool's partial stderr is frequently the explanation, and it was
  previously unreachable from `run()` / `checked()`: `diagnostic()` returned `None`. Now
  `Error::diagnostic()` covers both variants, and their one-line `Display` appends the same
  bounded last-line tail as `Error::Exit` (`` `db-migrate` timed out after 30s: waiting for
  lock held by pid 4123 ``). `Error::Cancelled` deliberately carries no streams —
  cancellation is a caller-initiated immediate stop; any output captured before the kill
  is intentionally discarded.
- **Breaking:** the blanket `impl From<std::io::Error> for Error` is removed (D13). An
  arbitrary `io::Error` no longer converts into `Error::Io` implicitly through `?`, so a
  caller's unrelated IO error can't silently fall into the crate's taxonomy (where
  `is_transient` / `is_permission_denied` would classify it). `Error::Io` is now produced
  only at the crate's own deliberate IO sites (driving a child, controlling a group,
  cassette files). Code that relied on `?`-converting an `io::Error` into `processkit::Error`
  should map explicitly (`.map_err(processkit::Error::Io)`) or use `Box<dyn Error>` /
  `anyhow`. `ProcessStdin`'s writer methods already returned `std::io::Result` and are
  unchanged.
- **Breaking (behavior):** the checking verbs that hand back stdout — `run`, `parse`,
  `try_parse` (on `Command`, `ProcessRunnerExt`, and `CliClient`) — now **fail loud** with
  `Error::OutputTooLarge` when a bounded `OutputBufferPolicy` silently dropped captured
  lines (B12), instead of returning a truncated tail as if complete (a parser would have
  parsed half a document). The lenient capture verbs (`output_string`/`output_bytes`) are
  unchanged — they still return the result with `truncated()` set for the caller to inspect.
  Only triggers under a non-default bounded *drop* policy.
- **Breaking (behavior):** re-running or retrying a command whose stdin is a **one-shot**
  streaming source (`Stdin::from_reader`/`from_lines`) now fails loud at launch with an
  `Error::Io` (`InvalidInput`) once the source has been consumed (D10), instead of silently
  feeding the re-run empty stdin. Use a re-runnable source
  (`from_bytes`/`from_string`/`from_file`/`from_iter_lines`) to retry or re-run.
- **Breaking:** the streaming verbs are now **fallible** (D2): `RunningProcess::stdout_lines`
  and `output_events` return `Result<StdoutLines>` / `Result<OutputEvents>` instead of the
  bare stream. They `Err` (an `Error::Io` `InvalidInput`) on a non-piped stdout
  (`StdioMode::Inherit`/`Null`) or a second streaming call (stdout streams once) rather than
  handing back a silently-empty stream — mirroring the bulk verbs' loudness and making a
  second `wait_for_line` a clear error instead of a forever-`NotReady` probe. Add `?` /
  `.expect(..)` at the call site.
- **Breaking:** the streaming finishers are **unified** (D3): `finish_streamed()` and
  `finish_events()` collapse into a single `RunningProcess::finish() -> Result<Finished>`,
  and the `StreamedFinish` struct is renamed `Finished` (`{ outcome, stderr }`). After
  `output_events`, `finish().stderr` is empty (stderr was delivered to you as events). `wait()`
  (the discard finisher) is unchanged. Rename `finish_streamed`/`finish_events``finish` and
  `StreamedFinish``Finished`.
- **Breaking:** `RunningProcess::standard_input()` is renamed `take_stdin()` (D17) — the new
  name signals that it *takes* (consumes) the stdin writer on the first call (returning `None`
  after), and aligns the stdin family's spelling (`stdin` / `keep_stdin_open` / `take_stdin`).
- **Breaking:** `Command::unchecked()` is renamed `unchecked_in_pipe()` (D9/D17) — the name now
  makes the **pipeline-only** scope explicit. It was always a no-op outside a `Pipeline` (a
  single run's status is already data in its `ProcessResult`); the clearer name removes that
  footgun. The `producer.unchecked_in_pipe().pipe(consumer)` shape (suppress a producer's
  `SIGPIPE` under pipefail) is otherwise unchanged.
- (Naming sweep, D17: `OutputBufferPolicy::fail_loud`, `RunningProcess::kills_tree_on_drop`, and
  the deadline lexicon `Error::Timeout` / `Outcome::TimedOut` / `timed_out` / `Error::NotReady`
  were reviewed and **kept** — already clear and well-differentiated (`NotReady` is intentionally
  distinct from `Timeout`), so a rename would be churn on a soon-frozen surface.)
- **Breaking:** `OutputEvent` (yielded by `output_events`) is now `#[non_exhaustive]` — a future
  release may add a third event kind (e.g. a lifecycle marker) without a breaking change, so a
  `match` on it now needs a `_` arm.
- The `mock` feature's `MockRunner` is documented as **semver-exempt**: its `mockall`-generated
  `expect_*` surface (and the opaque expectation types) tracks the `mockall` dependency, not this
  crate's frozen API. `ScriptedRunner` / `RecordingRunner` are the stable, recommended doubles.
- **Breaking:** the test doubles moved from the crate root into a `processkit::testing` module
  (D6): `ScriptedRunner`, `Reply`, `Invocation`, `RecordingRunner`, and (feature-gated)
  `RecordReplayRunner` / `MockRunner` are now `processkit::testing::*`. This keeps the production
  surface focused (they exist only to replace subprocesses in tests). Update imports:
  `use processkit::testing::{ScriptedRunner, Reply};`.

### Fixed

- `Supervisor` no longer panics when a backoff/storm delay approaches `Duration::MAX` with
  jitter enabled (the default): the jittered delay is clamped to the crate's `MAX_DEADLINE`
  ceiling instead of overflowing `Duration::mul_f64`. Reachable via `max_backoff(Duration::MAX)`
  or `storm_pause(Duration::MAX)`.
- `RunningProcess::start_kill` is **documented and guaranteed idempotent** (D20): killing a
  child that has already exited and been reaped (e.g. by a prior readiness probe or `wait_any`
  observation) is a successful no-op — like `kill` on a Unix zombie. (Current tokio/std
  already return `Ok` here; the crate also defensively treats a stray `InvalidInput` from a
  reaped handle as the no-op success it is, and a regression test pins the contract.)
- **Documented (D18):** `Outcome::Signalled` is **Unix-only**. On Windows a killed process
  reports `Outcome::Exited` with a platform code (no signal abstraction) —
  `TerminateJobObject(_, 1)` is `Exited(1)` (indistinguishable from `exit(1)`), `Ctrl-C` is
  `Exited(-1073741510)`. The crate reports the platform truth rather than guessing a
  `Signalled` from an NTSTATUS code; use a deadline or cancellation token when you must *know*
  a run was killed. (See `Outcome` docs and `docs/platform-support.md`.)
- **Pipeline status semantics are unified (D14).** The last stage is now evaluated by the
  same pipefail rule as the inner stages — one `is_clean`, one attribution — fixing two
  inconsistencies:
  - An inner stage that hit its **own** `Command::timeout` now reports **that stage's**
    deadline in the resulting `Error::Timeout`, instead of the chain's timeout or a
    misleading `timed out after 0ns` (B10).
  - The **last** stage's `ok_codes` are now honored (E24e): a last stage with
    `ok_codes([0, 1])` exiting `1` is a clean, successful chain — previously the last
    stage's `ok_codes` were reset to `[0]` while inner stages honored theirs.
  `unchecked_in_pipe` still forgives an exit (preserving the real code) but not a last-stage
  timeout/signal (the chain's output is then broken).
- A `wait_any` / `wait_all` **loser** with `keep_stdin_open` now keeps its stdin usable
  (B15): the race no longer closes an untaken stdin pipe out from under the caller (which
  left `take_stdin()` returning `None` and the child wedged on a premature EOF),
  honoring the documented "losers remain fully usable" guarantee. A `keep_stdin_open` child
  blocked reading stdin is the caller's responsibility, like the existing "no output
  pumping" non-feature — take its writer (or don't keep stdin open) before racing it.
- `output_all`'s cancel-on-drop documentation is corrected (B16): dropping the batch future
  tears down in-flight children only with an **own-group** runner (`JobRunner`); with a
  shared `&ProcessGroup` runner the children live until the caller tears the group down.
- **Non-ASCII-compatible encodings no longer corrupt output.** Bytes are fed through one
  persistent decoder and the *decoded* text is split on newlines, so UTF-16LE/BE (whose
  code units contain `0x0A` bytes that are not line breaks) and stateful encodings decode
  correctly instead of being mangled by a raw-byte split.
- A byte-order mark is handled once at the stream start (the chosen encoding's own BOM
  only), so a legacy line that merely begins with BOM-looking bytes is no longer silently
  re-decoded as UTF-16.
- A CRLF terminator now strips exactly one trailing `\r`, not every trailing `\r`, so
  `"data\r\r\n"` yields `"data\r"`.
- A mid-stream read error now flushes the partial final line instead of dropping it
  (matching the EOF path).
- Sys-layer safety hardening: the POSIX process-group fallback no longer risks signalling
  a **recycled PID**'s group (a latch gates the whole-group fallback) and recovers from a
  poisoned lock instead of panicking; on Windows, `suspend()`/`resume()` no longer return a
  false error when a member thread exits between the snapshot and the walk, and a `Drop`
  that skips the kill now clears the Job Object CPU-rate cap; on Linux, cgroup directory
  names carry a per-process salt so a recycled PID can't collide with a crashed run's
  leftover directory and silently downgrade to the process-group fallback.
- Deadline computations are clamped so a `Duration::MAX`-ish timeout/grace can no longer
  overflow `Instant` arithmetic and panic.
- `Error::Parse`'s `message` is now bounded to a 200-byte preview in both `Display` and
  `Debug` (B14) — a caller-built message that embeds the full unparsed output can no longer
  flood a log line or an `.unwrap()` panic message (the same protection the `Exit` streams
  already had). The complete text stays reachable on the `message` field.
- Test doubles now match the live runner on the contracts they exist to exercise: a
  panicking line handler is isolated on the bulk `ScriptedRunner::output` path (not only
  while streaming); a capture verb on a non-piped stdout errors instead of returning canned
  output; an already-cancelled token short-circuits to `Cancelled` before serving a reply;
  `wait_any`/`wait_all` honor cancellation mid-wait (a pending scripted handle no longer
  hangs forever); a kill landing after a scripted child's natural exit keeps the cached
  outcome (not `Signalled`); the scripted run lifetime accounts for a stderr longer
  than stdout (no truncation); and a scripted `stdout_lines`/`output_events` stream is now
  bounded by the command's `timeout` — the stream ends at the deadline and the run reports
  `TimedOut`, like a real child whose pipes close when its tree is killed (a scripted
  streamed run that previously ran to completion ignoring the timeout now ends early).
- Cassette replay now invokes `on_stdout_line`/`on_stderr_line` (as record mode does), and
  keys on the **stdin content** (hashed, never persisted) so concurrent calls differing
  only in their stdin no longer collide on replay. (A pre-existing cassette recorded *with*
  stdin must be re-recorded to match a stdin invocation again.)
- `ScriptedRunner` warns (under the `tracing` feature) when a rule is unreachable because an
  earlier, broader prefix rule shadows it.

## [0.9.2] - 2026-06-11

### Added

- `Error::Stdin { program, source }` — a non-broken-pipe stdin-writer failure surfaced on an
  otherwise-successful run (see the Phase H stdin fixes below).
- `StdioMode` enum (`Piped` / `Inherit` / `Null`) + `Command::stdout(mode)` /
  `Command::stderr(mode)` builders — control per-stream connection independently.
  `Piped` (the default) captures as before; `Inherit` lets the child share the parent's
  terminal/log; `Null` suppresses output entirely without tying up a pipe.
- `OutputEvent` enum (`Stdout(String)` / `Stderr(String)`) and `OutputEvents` stream —
  merge both stdout and stderr into a single ordered sequence of tagged lines.
  `RunningProcess::output_events()` starts both pumps and returns the stream;
  `RunningProcess::finish_events()` waits for exit and returns the run's `Outcome`.
  Lines interleave in arrival order (best-effort; no kernel timestamp).
- `OverflowMode::Error` variant and `OutputBufferPolicy::fail_loud(n)` builder — a
  fail-loud capture ceiling: once `n` lines are buffered, subsequent lines are counted
  but not retained, and the consuming verb errors with `Error::OutputTooLarge` after the
  run. The pipe is still fully drained so the child never blocks on a full pipe.
  Use this when unbounded output is a misbehavior rather than a policy choice.
- `Error::OutputTooLarge { program, limit, total_lines }` — produced by the fail-loud
  overflow path when the captured line count exceeds the configured ceiling.
- `Command::stdout_tee<W: Write + Send>(writer)` / `Command::stderr_tee<W>(writer)`  simultaneously capture *and* write each decoded line to `writer` (a `Vec<u8>`, a
  `File`, a locked stdout — any `std::io::Write + Send`). Replaces any previously set
  per-stream handler; compose inside `on_stdout_line` when multiple sinks are needed.
- `Error::NotFound { program, searched }` — a bare program name (no path separators)
  not found now surfaces a distinct, structured error: `` `git` not found on PATH ``.
  Enriched from the OS's opaque not-found error rather than a `PATH` pre-check, so a
  program the OS resolves by another route (e.g. the application directory on Windows)
  is never falsely reported missing. `Error::is_not_found()` returns `true` for this
  variant (as it does for the existing `Error::Spawn(NotFound)` / missing-cwd case).
  The `searched` field carries the `PATH` directories for programmatic diagnostics.
- `Command::envs([(key, val), …])` — set multiple environment variables in one call.
  Equivalent to chaining `env()` calls; order is preserved and a later entry for the
  same key wins.
- `Error::Signalled { program, signal }` — a process terminated by a signal now surfaces
  a distinct, structured error (was an opaque `Error::Io`). `signal` is the Unix signal
  number when the platform reports it, `None` otherwise (e.g. on Windows). The checking
  verbs (`run`, `exit_code`, `probe`, `ensure_success`, `require_code`) raise it.
- `StreamedFinish { outcome, stderr }` — the named return of
  `RunningProcess::finish_streamed()` (was a bare `(Option<i32>, String)` tuple).
  Derives `Debug`, `Clone`, `PartialEq`, `Eq`.
- `Reply::signalled(Option<i32>)` on the test-double seam — script a signal-killed reply
  so a hermetic test can exercise `Outcome::Signalled` / `Error::Signalled` handling
  without a real subprocess.

### Changed

- **Breaking:** `Outcome::Signalled` now carries the Unix signal number as
  `Signalled(Option<i32>)` (was a unit variant). `Some(n)` is the signal that killed the
  process when the platform reports it; `None` when unavailable (e.g. on Windows).
- **Breaking:** `RunningProcess::wait()`, `wait_any()`, and `wait_all()` now return the
  run's `Outcome` (`Outcome`, `(usize, Outcome)`, and `Vec<Outcome>` respectively) instead
  of the raw `Option<i32>` exit code — distinguishing a clean exit, a signal kill, and a
  timeout instead of collapsing the last two to `None`. A cancelled run raises
  `Error::Cancelled` on every one of these paths.
- **Breaking:** `RunningProcess::finish_streamed()` returns `StreamedFinish { outcome,
  stderr }` instead of `(Option<i32>, String)`; `finish_events()` returns `Outcome`
  instead of `Option<i32>`.
- `Command::current_dir` doc now explicitly calls out that a relative-path program
  (e.g. `"./tool"`) passed to `Command::new` resolves against the *caller's* cwd, not
  the directory set here — use an absolute path for the program when combining
  `current_dir` with a relative-path executable.

### Changed (Phase I — design block)

- `ProcessGroup::spawn` now takes its `tokio::process::Command` **by value** (D8) instead of
  `&mut`: reusing one command across spawns would stack `pre_exec` hooks / re-set creation
  flags, so by-value makes that a compile error rather than a silent footgun. The crate's own
  run helpers already rebuild the command per run, so only direct `spawn` callers are affected.
- `Command::to_tokio_command` is now `#[doc(hidden)]` (D8) — it remains public and callable as
  a raw-tokio bridge to `ProcessGroup::spawn`, but is no longer advertised as 1.0 surface.
- `Invocation::cwd` is now `Option<PathBuf>` instead of `Option<OsString>` (D9) — a working
  directory is a path.
- The bulk capture verbs (`output_string`/`output_bytes`) now **error loudly** when `stdout` was
  set to `StdioMode::Inherit`/`Null` (D5) — there is no pipe to read, so returning silently-empty
  output was a footgun; the streaming verbs document that the stream is empty instead. The
  discard verbs (`wait`/`profile`) are unaffected.
- `OutputBufferPolicy::Error` overflow on an **unbounded** buffer is no longer a silent no-op (D9c):
  `unbounded().with_overflow(Error)` is a misconfiguration (a ceiling with no ceiling), so it now
  fails loud on any **line-pumped** output (`Error::OutputTooLarge`). (`output_bytes` captures stdout
  raw, so its stdout is exempt — only its line-pumped stderr trips the ceiling.) Use `fail_loud(n)`
  for a real cap.
- `Supervisor` now defaults to a **bounded-tail** capture per incarnation (D3) instead of the
  unbounded one-shot default — a long-lived chatty supervised process no longer accumulates its
  entire output in memory. An explicit bounded/`fail_loud` command policy is respected; override
  via the new `Supervisor::capture`.
- `OutputEvents` (the merged stdout+stderr stream) now alternates which stream it polls first (D9d),
  so a continuously-ready stream can't starve the other.
- `Command::first_line`'s predicate now requires `F: Send` (D6) — it delegates through the new
  `ProcessRunnerExt::first_line` seam (see Added).

### Added (Phase I — design block)

- `RunningProcess::kills_tree_on_drop()` (D10) — reports whether dropping the handle tears down
  the process tree: `true` for a private-group handle (kill-on-close leak-safety), `false` for a
  shared-`ProcessGroup` handle (the group owner tears down). Lets a receiving function reason
  about whether dropping the handle is sufficient cleanup.
- `ProcessRunnerExt::first_line` (D6) — the streaming first-matching-line search, routed through
  the `start` seam so it is exercisable with any runner (a `ScriptedRunner` in tests), not just the
  real `JobRunner`. `Command::first_line` now delegates to it.
- `Supervisor::capture(policy)` (D3) — override the per-incarnation output-capture policy (the
  default is a bounded tail; see Changed).
- Documented the deliberate design choices the block confirmed: `ProcessRunner::start` stays a
  defaulted runtime capability (`Error::Unsupported`) rather than a compile-time `ProcessStarter`
  split (D4); the `cli_client!` macro is kept and documented as committed public API (D7); and
  `Command::timeout_signal` stays behind `process-control` because the `Signal` type does — the
  divergence is accepted rather than enlarging the always-on surface (D9b).

### Fixed (Phase H — stdin)

- A stdin-writer failure is no longer silently swallowed: a non-broken-pipe error feeding the
  child's standard input now surfaces as the new `Error::Stdin { program, source }`**but
  only when the run otherwise succeeded** (a non-zero exit, signal, or timeout is the "realer"
  failure and wins; a broken pipe, the child closing stdin early, never surfaces). Diagnoses a
  silently-truncated input the otherwise-successful child may have acted on.
- `Stdin::write_to` now releases the one-shot source mutex *before* the copy/stream (B17), so a
  concurrent second run on a cloned `Stdin` sees the consumed source and gets prompt EOF instead
  of blocking on the lock for the whole copy.
- `wait_any` / `wait_all` now close an untaken `keep_stdin_open` pipe (L5), matching the bulk
  verbs — a stdin-reading child joined via the race path sees EOF instead of blocking forever
  (the race path applies no timeout).
- Doc fixes (L12): `run` / `run_unit` document that `ok_codes` widens the accepted exit set;
  `Command::env`'s doc no longer falsely claims a `None` value removes a variable (use
  `env_remove`).

### Security (Phase G — security / hygiene)

- `Command`, `CliClient`, and `Invocation` now have a redacted `Debug`: it surfaces the
  argument *count* and the env variable *names* (sorted), never argv or env *values* — so a
  `{cmd:?}` log line or an `assert_eq!` failure can't leak a secret. `command_line()` stays
  the documented, explicitly-secret-bearing escape hatch for the real argv.
- `Error` now has a manual `Debug` (was derived): the `Exit` variant's captured streams are
  bounded to a 200-byte preview (mirroring the `Display` tail cap) so `{e:?}` / `.unwrap()`
  can't dump a multi-MiB stream, and `NotFound`'s `searched` (the `PATH` env value) is
  redacted to a directory count rather than logged. The size-bound is deliberately
  `Error`-only — the reflexive `{e:?}` / `.unwrap()` logging vector; `ProcessResult` keeps
  full streams in its `Debug` for test inspection (and its stdout/stderr are policy-verbatim
  regardless).
- Cassette (`RecordReplayRunner`) hardening: the file is written owner-only (`0600`) on Unix;
  the best-effort drop-flush is skipped while unwinding, so a panic mid-recording no longer
  persists a surprise cassette; and the docs now scope the "no secrets" guarantee to env
  *values* only — argv, cwd, stdout, and stderr are stored verbatim and may carry secrets.
- Documented the cassette's lossy-key limitation: two distinct non-UTF-8 invocations that
  differ only in their invalid bytes decode to the same match key and collide on replay
  (valid-UTF-8 invocations never collide).

### Fixed (Phase F — group / limits / sys layer)

- Linux cgroup resource limits (B13): made the `cgroup.subtree_control` controller-enable
  conditional (it now writes only the controllers not *already* enabled, skipping a redundant
  write) and corrected the previously **misleading** error/docs. The honest story: the crate
  creates the limit cgroup as a child of this process's own cgroup and enables the controllers
  there, which cgroup v2's "no internal processes" rule permits only at the **real cgroup-v2
  hierarchy root** (the one exempt cgroup) — so limits apply only when this process is a direct
  member of that real root, and fail fast (`Error::ResourceLimit`) under a systemd
  session/scope/service or an ordinary (private-cgroupns) container, both of which place it in a
  non-root cgroup. A cgroup *namespace* root does **not** count. The crate deliberately does not
  migrate your process into a sub-cgroup to work around the rule. (The previous error/docs
  recommended `Delegate=yes` / `systemd-run --scope` and a "delegated leaf", which all still
  `EBUSY` — that advice is removed.) Docs (`ResourceLimits`, README, platform-support,
  process-groups) corrected to match.
- Documented the Linux `max_processes` cross-platform divergence (B14): the kernel checks
  `pids.max` only for forks *inside* the cgroup, so on Linux the cap bounds a contained tree's own
  forks but does not reject additional `ProcessGroup::start` calls that each add a top-level child
  (Windows' `ActiveProcessLimit` does). `ResourceLimits::max_processes` now spells this out.
- Documented the POSIX process-group graceful-shutdown zombie caveat (B16): on the
  `ProcessGroup` mechanism (macOS/BSD, Linux fallback) an unreaped zombie still answers the
  liveness probe, so `ProcessGroup::shutdown` burns the full `shutdown_timeout` on a child that
  exited on `SIGTERM` but whose handle was never awaited — await each child you start into the
  group. The Job Object / cgroup mechanisms are immune.
- `ProcessGroup::shutdown` with `escalate_to_kill(false)` now actually preserves survivors:
  the `Drop` impls for all three backends (Linux cgroup, POSIX process-group, Windows Job
  Object) no longer hard-kill the tree when `graceful_shutdown` was invoked with
  `escalate=false`. Previously, the per-platform `Drop` backstop unconditionally killed
  regardless of the escalation setting. (The run-level `timeout_grace` path always escalates,
  so it is unaffected.)
- Fixed a provenance UB in the Windows `job_member_pids` helper: the flexible-array
  `ProcessIdList` field in `JOBOBJECT_BASIC_PROCESS_ID_LIST` is now addressed via
  `std::ptr::addr_of!((*list).ProcessIdList[0])` instead of `.as_ptr()` on the `[ULONG_PTR;1]`
  field, which previously created a reference with incorrect provenance over the out-of-bounds
  elements.
- `ProcessGroupStats::total_cpu_time` doc now explains the semantic divergence: the Windows
  Job Object accumulates CPU time historically (including terminated processes), while the
  Linux cgroup path sums only currently-live processes' `/proc` counters.
- POSIX process-group `exists()` probe no longer permanently prunes a just-spawned pid
  whose process group does not yet exist: `ESRCH` on the negative group-id probe now falls
  back to a direct pid probe, so a child between fork and its `setpgid(0,0)` call is not
  incorrectly evicted from the tracking set. The teardown sweep mirrors this — when
  `killpg` finds no group it falls back to a direct pid signal, so such an entry is actually
  delivered to and drains instead of being retained-but-never-signalled (which would have
  stalled `shutdown` to its full timeout).

### Fixed

- `ProcessResult::combined()` now inserts a `\n` separator between stdout and stderr when
  stdout is non-empty and does not already end with a newline, preventing the last stdout
  line from being glued to the first stderr line.
- Pipeline `pipefail` attribution now honors per-stage `ok_codes`: an inner stage that
  exits with a code in its `ok_codes` set is considered clean and does not trigger
  attribution, instead of checking only for `Exited(0)`.
- Pipeline `pipefail` now attributes to the first **non-SIGPIPE** checked failure rather
  than the first checked failure of any kind. A SIGPIPE-killed upstream stage is typically
  a victim of a downstream failure; the downstream culprit is now correctly attributed.
  When all failures are SIGPIPE, the leftmost is still attributed as before.
- Pipeline `pipefail` now preserves the real exit code of an `unchecked()` last stage
  instead of fabricating `Exited(0)`. `is_success()` remains `true` and `ensure_success()`
  still passes; `code()` now returns the actual exit code for callers that inspect it.
- `Error::NotFound` `Display` no longer includes the raw `PATH` environment value
  (e.g. `searched: /usr/bin:/usr/local/bin`). The `searched` field remains accessible for
  programmatic use. `PATH` is an environment value and must not appear in logs.
- When a bare program name is on `PATH` but the OS cannot execute it directly (e.g. a
  `.cmd`/`.bat` script on Windows that requires `cmd.exe`), the error is now the raw
  `Error::Spawn` rather than the misleading `Error::NotFound` — the program was found.
- `is_bare_name("git/")` now correctly returns `false`; a trailing path separator makes
  a name path-ish and it should not be looked up on `PATH` as a bare name.
- Windows `command_line()` display: a path argument ending with a backslash (e.g.
  `C:\my tools\`) now doubles the trailing backslash before the closing `"` so it does
  not escape the closing quote (was: `"C:\my tools\"`, now: `"C:\my tools\\"`).
- A signal-killed process is no longer reported as a generic `Error::Io("terminated by
  signal")`; the checking verbs now raise the structured `Error::Signalled` (carrying the
  signal number on Unix), and `Outcome::Signalled` preserves it for inspection.
- `finish_streamed` and `finish_events` previously drained an untaken stdout pipe into an
  unbounded `Vec`, bypassing any configured `OutputBufferPolicy`. They now route the pipe
  through the normal pumping path, respecting the buffer policy (including `fail_loud`).
- `wait` and `profile` previously accumulated all output in the user-configured buffer even
  though output is discarded on those paths, causing O(total-lines) peak heap use. Both now
  use a retain-nothing sink that keeps the pipe drained without buffering any lines.
  **Behavior note:** `OverflowMode::Error` (via `fail_loud`) no longer fires during `wait`
  or `profile` — it fires only on the capturing verbs (`output_string`, `output_bytes`,
  `finish_streamed`, `finish_events`). If you need the DoS guard on a run you don't capture,
  use a capturing verb.
- `output_string` / `output_bytes` called after `stdout_lines` previously returned empty
  output because they created fresh empty sinks and ignored the running streaming pump.
  They now reuse the existing pump's sink and join its handle, capturing all buffered and
  in-flight output correctly.
- Calling `stdout_lines` or `output_events` a second time on the same `RunningProcess` now
  returns an empty stream instead of silently replacing the first call's sink reference,
  which previously caused the overflow flag to be lost.
- A second `output_events` call no longer shares the same stderr `SharedLines` as the first;
  it receives a fresh already-closed sink, preventing a `notify_one` race that could leave
  the first consumer's internal task permanently parked.
- Pump task handles previously held in a frame-local `Vec` were leaked (left as detached
  tasks) if an early `?` exit occurred between the pump spawns and the explicit join. Handles
  are now stored on `RunningProcess` fields and aborted by `Drop`, bounding the leak to
  the process handle's lifetime.

## [0.9.1] - 2026-06-09

### Added

- `Command::ok_codes([..])` — treat the given exit codes (not just `0`) as success for
  the checking verbs (`run`/`run_unit` and `ProcessResult::is_success`/`ensure_success`),
  for tools whose non-zero exit is a normal result — `grep` (1 = no match), `diff`
  (1 = differs), rsync's code families. `exit_code` (raw code) and `probe` (0/1
  convention) are unchanged; an empty set is ignored.
- `ProcessResult::duration()` — the run's wall-clock time (spawn → exit/kill), carried
  on the result instead of making callers wrap each run in their own `Instant::now()`.
  `Duration::ZERO` for synthetic results (scripted/replayed bulk `output`).
- `ProcessResult::truncated()` — whether a bounded `OutputBufferPolicy` dropped captured
  output lines, so a caller that bounds the buffer can tell when output was lost
  (the unbounded default never truncates).
- `Command::command_line()` — render the command as a single shell-quoted line for
  logs, error messages, or a dry-run echo (per-platform quoting; **display only**  the crate never invokes a shell). It includes argv (which may carry secrets), so —
  unlike the `tracing` feature, which never logs argv — it is opt-in.
- A `current_dir` that does not exist now fails with a clear *"working directory does
  not exist"* error (`Error::is_not_found()` is `true`) instead of the opaque `ENOENT`
  that looked like the program itself was missing.
- `Command::timeout_grace(Duration)` + `Command::timeout_signal(Signal)` — a **graceful
  run-level timeout**: at the deadline the tree is signalled (`SIGTERM` by default, or
  the chosen signal), given up to the grace window to exit, then `SIGKILL`ed — instead
  of the immediate hard kill. Reuses the `ProcessGroup::shutdown` tier and reaps
  concurrently, so a signal-handling child ends the grace early. Applies to bulk and
  streaming runs, own- and shared-group; `timed_out()` stays `true`. Windows has no
  signal tier (atomic kill at the deadline). `timeout_signal` needs `process-control`.

### Changed

- **Breaking:** `RestartPolicy`, `OverflowMode`, `OutputBufferPolicy`, `ResourceLimits`,
  and `ProcessGroupOptions` are now `#[non_exhaustive]` — they may gain variants/fields
  later without another breaking change. Build the structs via their
  constructors/builders (`ProcessGroupOptions::default()`, `OutputBufferPolicy::bounded(..)`,
  …) instead of struct literals.
- `ProcessGroupOptions::shutdown_timeout(Duration)` / `escalate_to_kill(bool)` builders —
  the grace-window fields now have builders, matching the `limits` knobs.

### Fixed

- `Error::Exit` now carries the **full** captured `stdout`/`stderr` instead of truncating
  each to 4 KiB. Truncation happened before the caller could classify on the streams
  (grep for a marker, parse a sub-code), silently destroying the data they needed. The
  one-line `Display` message is still bounded, so logs stay tidy — only the fields grew.

## [0.9.0] - 2026-06-08

### Added

- `Error::is_not_found()` / `is_permission_denied()` / `is_transient()` — io-level
  classifiers over the `Spawn`/`Io` error: distinguish a missing binary (`ENOENT`),
  a permission denial (`EACCES`/`EPERM`), and a transient condition a bare retry can
  clear (`EINTR`/`EAGAIN`/busy, `ETXTBSY`, Windows sharing/lock violation) without
  matching raw `io::ErrorKind`. Pairs with `Command::retry(.., |e| e.is_transient())`.
  Scope is io/spawn-level only — exit-code retryability stays the caller's domain,
  and `Error::Timeout` is excluded (compose it explicitly if wanted).
- `Command::groups([gid, ..])` — set the child's supplementary groups (Unix
  privilege drop), the missing third leg beside `uid`/`gid`: dropping the uid alone
  leaves the child holding the parent's (often root's) supplementary groups. The OS
  applies `setgroups → setgid → setuid`. POSIX-only — non-Unix fails with
  `Error::Unsupported`, never a silent skip.

### Changed
-

### Fixed
-

## [0.8.2] - 2026-06-08

### Added

- `wait_all(&mut [&mut RunningProcess])` — the join companion to `wait_any`:
  drives every handle to exit and returns the exit codes in input order (an
  empty slice resolves to an empty `Vec`). Cancel-safe and borrow-only, like
  `wait_any`.
- `output_all(commands, concurrency, runner)` — run a batch of commands with a
  concurrency cap, collecting every `Result<ProcessResult<String>>` in input
  order (collect-all: a non-zero exit is data, never a short-circuit). The
  back-pressure the one-shot verbs lack when fanning out many commands. Pass
  `&group` to share one kill-on-drop group, or `&JobRunner` for private groups.
  Not a pool/scheduler/retrier by design.

### Changed
-

### Fixed
-

## [0.8.1] - 2026-06-08

### Fixed

- fix(readme): use direct raw.githubusercontent URL for cover so crates.io stops generating a CSP-blocked github.com/raw redirect

## [0.8.0] - 2026-06-07

### Added

- `ProcessRunner::start` — the live-handle half of a run joins the seam (with
  an `Error::Unsupported` default, so `output`-only runners keep compiling).
  `ScriptedRunner::start` returns a **scripted `RunningProcess`** whose canned
  output flows through the same pump machinery as a real child: streaming
  (`stdout_lines`), readiness probes, and `finish_streamed` are now
  hermetically testable. `Reply::lines([...])` scripts the lines;
  `Reply::with_line_delay(d)` paces them (paused-clock friendly);
  `RecordingRunner` records `start` invocations. Scripted handles have no pid,
  don't compose into a real `Pipeline`, and don't model interactive stdin
  (documented). Cassette record/replay does not yet cover streaming runs.
- `ScriptedRunner::output` now replays canned stdout/stderr through the
  command's `on_stdout_line`/`on_stderr_line` handlers, so progress-reporting
  wrappers test hermetically (requested by a downstream wrapper crate's
  streaming spec).
- `ProcessRunnerExt::run_unit` — run for the side effect, require a zero
  exit, discard the output (the verb `CliClient::run_unit` delegates to).
- More `tracing` events (behind the `tracing` feature, `processkit` target):
  child spawn (program/pid/mechanism), timeout and cancellation firing, group
  terminate/shutdown, retry attempts, stdin-writer failures, output-pump
  panics and teardown overruns, and `adopt`. Still never logs argv or
  environment values.
- `ProcessResult::outcome() -> Outcome` — how the run ended as an explicit
  `Exited(i32) | Signalled | TimedOut` enum, now the internal representation
  behind the `code()`/`timed_out()`/`is_success()` accessors (which are
  unchanged, derived, and remain the everyday surface). `Outcome` is
  `#[non_exhaustive]`. Cassette wire format is untouched.
- `CliClient::default_cancel_on(token)` (`cancellation` feature) — a
  client-level cancellation default, completing the run-control default set
  (`default_timeout`/`default_env`): every command the client builds carries
  the token, so cancelling it kills all of that client's in-flight runs. A
  per-command `cancel_on` *replaces* the default (explicit beats default).
  The `cli_client!` macro re-emits the builder on generated wrappers.
  Requested by a downstream wrapper crate.
- `Reply::pending()` (`cancellation` feature) — a `ScriptedRunner` reply that
  parks the call until the command's cancellation token fires, then resolves
  with `Error::Cancelled`, making cancellation *behaviour* (not just its
  aftermath) hermetically testable. With no token it parks forever, like a
  hung child.
- `Command::kill_on_parent_death()` — opt-in hardening so an abruptly-dying
  parent (`SIGKILL`, where `Drop` never runs) still takes its child down:
  Linux arms `PR_SET_PDEATHSIG(SIGKILL)` on the direct child (the
  parent-died-first race is closed by re-checking `getppid` against the
  spawner's pre-fork pid — PID-1-entrypoint-safe); Windows already
  guarantees the whole tree via the job handle closing; macOS/BSD have no
  equivalent (documented no-op). Idea borrowed from `execa`'s
  cleanup-on-exit, mapped to native primitives.
- `Command::unchecked()` — exempt a pipeline stage from pipefail attribution
  (design borrowed from `duct`): its unclean exit (non-zero, signal kill
  including SIGPIPE, or its per-stage-timeout kill) is skipped when blaming
  the chain, fixing the `producer | head -1` false failure. Checked failures
  always trump unchecked ones; a chain whose only failures are unchecked
  reports success. No-op outside a pipeline; never relaxes a whole-chain
  `Pipeline::timeout`.
- `|` operator on `Command`/`Pipeline``a | b | c` is sugar for
  `a.pipe(b).pipe(c)`: the same shell-free, one-group, pipefail pipeline.
  Parenthesize the chain before a terminal verb.
- `Supervisor::storm_pause` / `failure_decay` / `failure_threshold` — an
  opt-in failure-storm guard (design borrowed from Go's `suture`): each
  failure feeds a score that halves every `failure_decay`
  (`score = score × 0.5^(Δt/decay) + 1`); past `failure_threshold` the
  supervisor takes one jittered `storm_pause` and resets the score,
  distinguishing "fails rarely" from "crash-looping". Off by default;
  pauses taken are reported in `SupervisionOutcome::storm_pauses`.

### Changed

- A **panicking line handler no longer poisons the run**: the panic is caught,
  the handler is disabled for the rest of the run (surfaced as a `tracing`
  warn), and the child keeps being drained — the final result still carries
  every line. Previously the pump died with the panic and capture was cut at
  that point. The `on_stdout_line`/`on_stderr_line` docs now also state the
  ordering guarantees: FIFO within a stream, no cross-stream order, and all
  handler calls happen-before the consuming verb resolves (requested by a
  downstream wrapper crate's streaming spec).
- **Breaking**: `CliClient`'s run helpers renamed to the crate-wide verb
  vocabulary — `text → run`, `capture → output`, `unit → run_unit`,
  `code → exit_code` (`probe`/`parse`/`try_parse` unchanged). The same verb
  now means the same thing on `Command`, `ProcessRunnerExt`, and `CliClient`;
  `ProcessRunnerExt` gained `run_unit` for full symmetry. No deprecated
  aliases (pre-1.0). `ProcessResult::code()` — the plain accessor — is
  unrelated and unchanged.
- `Error::Exit`'s `Display` now appends a bounded diagnostic excerpt — the
  last non-empty line of stderr (or stdout as fallback), capped at 200
  bytes: `` `git` exited with code 2: fatal: boom `` (idea borrowed from
  `execa`'s error messages). Display text is not part of the semver
  contract; the carried `stdout`/`stderr` fields and `diagnostic()` are
  unchanged.
- `SupervisionOutcome` is now `#[non_exhaustive]` (it gained the
  `storm_pauses` field; like `ProcessGroupStats`/`RunProfile` it is a
  read-only report the crate produces, so future telemetry can be added
  without another breaking change). **Breaking** for exhaustive
  destructuring or struct-literal construction outside the crate.

### Fixed

- `keep_stdin_open` combined with a **bulk** verb (`output_string`/`run`/…)
  no longer hangs a stdin-reading child: a consuming verb now closes an
  **untaken** interactive stdin pipe (nothing could ever write to it again),
  so the child sees EOF instead of blocking to its timeout. A writer taken
  via `standard_input()` is unaffected. The `keep_stdin_open` docs previously
  claimed bulk helpers "always close stdin" — now they actually do.

## [0.7.1] - 2026-06-06

### Fixed

- fix: repair main after the v0.7.0 release commit was dropped (manifest, changelog, release guard)


### Added

- Add cover art to the project overview

## [0.6.2] - 2026-06-06 [YANKED]

- **Yanked on crates.io — use 0.7.0.** A force-push had dropped the
  `Release v0.7.0` commit from `main` before this patch release ran, so the
  release workflow computed the next version from the stale `0.6.1` manifest
  and published the **entire 0.7.0 content below under a `^0.6`-compatible
  patch version** — including the changes that are breaking for
  `default-features = false` consumers. The `v0.6.2` tag and GitHub Release
  remain for the record; the crates.io version is yanked. (The release
  workflow now refuses to run when the manifest is behind the latest release
  tag, so this failure mode is caught before publishing.)

## [0.7.0] - 2026-06-06

> **Release note:** this cycle contains a **breaking** change for
> `default-features = false` consumers (resource measurement moved behind the
> now-default `stats` feature — see *Changed*).

### Changed
- The tree-control surface is now behind a **default-on** `process-control`
  feature: `Signal` and
  `ProcessGroup::{signal, suspend, resume, members, adopt}`. The flag is
  additive and gates *visibility only* — the kill-on-drop tree guarantee
  (and `terminate_all`/`shutdown`) is unconditional in every configuration.
  **Migration note** for `default-features = false` consumers: previously
  that disabled only `stats`; now it also hides the surface above —
  re-enable it explicitly. (A broader visibility split — gating
  pipelines/supervisor/CliClient/test doubles too — was implemented and
  deliberately rolled back: those gates removed no dependencies while
  costing cfg noise and doc quality; see `ideas/three-layer-resource-split.md`
  for the full decision record.)
- `windows-sys` bumped 0.59 → 0.61 to dedup with the copy tokio/mio already
  ship — the lockfile now carries a single `windows-sys`.
- Every public type now implements `Debug` (enforced by a crate lint), and
  `Command` is `#[must_use]` — building one and dropping it unused now warns.
- Resource measurement (`ProcessGroupStats`, `ProcessGroup::stats`,
  `RunningProcess::cpu_time`/`peak_memory_bytes`) now sits behind a default-on
  `stats` Cargo feature: `default-features = false` compiles the accounting code
  (and its Windows ProcessStatus FFI) out. Consumers on default features see no
  change; consumers who already set `default-features = false` must add
  `features = ["stats"]` to keep that API.
- `ProcessGroupStats` and `RunProfile` are now `#[non_exhaustive]`: they are
  read-only outputs the crate produces, so future metrics can be added without
  a breaking change. Reading fields is unaffected; struct-literal construction
  and exhaustive destructuring outside the crate no longer compile.
  (`ProcessGroupOptions`, `ResourceLimits`, and `Invocation` deliberately stay
  exhaustive — constructing them is their intended use.)

### Fixed
- POSIX process-group liveness probes treated `EPERM` as "process gone": a
  live tree whose members the caller may no longer signal (e.g. after a
  third-party uid change) was silently pruned from tracking — and therefore
  never killed on drop. Probes now distinguish `ESRCH` (gone — prune) from
  `EPERM` (exists — keep and still attempt the best-effort signal).
- `output_bytes` awaited an **unbounded** raw stdout drain: on a shared-group
  handle whose timeout/cancel kills only the direct child, a surviving
  descendant holding the pipe could park the call forever. The drain is now
  bounded by the same pump-teardown grace as every other consumer, aborting
  the straggler and returning the partial bytes read so far.
- The streaming deadline/cancel watchdog tasks are now stopped as soon as the
  child's fate is settled (not only on handle drop), closing a narrow window
  where a late firing could signal an already-reaped pid.
- POSIX process-group `ProcessGroup::adopt` was a silent no-op for any child
  that had already `exec`'d (the normal case): POSIX refuses `setpgid` there
  (`EACCES`), and the pid was recorded as a process-*group* id that doesn't
  exist, so teardown never reached the child. Such children are now tracked
  and signalled individually — the adopted child is contained (killed with the
  group), though its future forks are not (unlike Windows/cgroup adoption).
  Adopting a child the group already tracks (a self-spawned leader, or a
  repeated adopt) is also de-duplicated now, so `members()`/`stats()` no
  longer over-report or grow per call.
- The streaming deadline/cancellation kill paths now also kill the **direct
  child by pid** after the group teardown — parity with the run-to-completion
  path's `start_kill` + `terminate_all` pairing, so a group-kill miss on the
  direct child can't leave a bounded stream running. Safe against pid reuse:
  the tasks are aborted when the handle drops, so they can only fire while
  the child is live or an unreaped zombie (its pid still held). (Note: this
  cannot rescue a *grandchild* forked mid-broadcast — the POSIX group
  broadcast is documented best-effort against a forking tree, which is what
  one macOS CI run actually hit.)

### Added
- `ProcessResult::program()` — the program a result is attributed to (for a
  `Pipeline` outcome, the pipefail-attributed stage). Previously the name was
  only recoverable by failing the result and matching the error.
- `docs/` guide set — eight cross-linked, per-topic guides (running commands,
  process groups, streaming & interactive I/O, pipelines, timeouts/retries/
  cancellation, supervision, testing, platform support) with richer examples
  and all capability matrices and platform caveats collected in one place;
  linked from the README's new Documentation section.
- Record/replay cassettes (`record` feature, off by default, pulls optional
  `serde` + `serde_json`): `RecordReplayRunner::record(path, inner)` captures
  real `Invocation → ProcessResult` pairs through any inner runner and writes
  a human-diffable JSON cassette (`save()`, or best-effort on drop);
  `RecordReplayRunner::replay(path)` serves them back hermetically — no
  subprocess. Matching is by program + args + cwd + has-stdin; env override
  values are never written (sorted names only — a committed fixture can't
  leak secrets) and env is not part of the match key. Duplicates of one
  invocation replay in capture order, then the last entry repeats. A miss in
  replay is a strict `Error::Spawn` (NotFound) — replay never spawns. The
  cassette carries a format `version` for forward evolution; non-UTF-8
  program/args/cwd are stored lossily (documented).
- Cancellation (`cancellation` feature, off by default, pulls optional
  `tokio-util`): `Command::cancel_on(token)` ties a run to a re-exported
  `CancellationToken` — cancelling it kills the process tree and every
  consuming path (`run`/`output_string`/`output_bytes`/`wait`/`profile`/
  `finish_streamed`) reports the new `Error::Cancelled`. Asymmetric with
  timeout by design: a timeout is *captured* in the result (`timed_out`), a
  cancellation is always an error; when both land, cancellation wins. A token
  cancelled before launch short-circuits without spawning. On a shared
  `ProcessGroup` handle, cancel kills the child only — siblings are untouched
  (same scope as timeout). A `stdout_lines` stream ends on cancel (own-group
  runs); the raw `wait_any`/`first_line` primitives don't synthesize the error
  for a mid-run cancel. A cancelled run is never re-attempted: `retry` policies
  and `Supervisor` restarts both treat it as terminal — no retry into a
  still-cancelled token.
- Environment and privilege builders on `Command`: `inherit_env([names])`
  (allow-list on a cleared environment, copied from the parent at each spawn;
  explicit `env`/`env_remove` still win), `uid(u32)`/`gid(u32)` (Unix privilege
  drop; gid applied before uid; on the Linux cgroup mechanism the spawn
  currently fails with a permission error — the cgroup join runs after the
  drop — while the process-group mechanism composes cleanly), `setsid()`
  (Unix new session — containment is
  preserved, the group tracks the new session's process group), and
  `create_no_window()` (Windows `CREATE_NO_WINDOW`, now OR'd with the group's
  `CREATE_SUSPENDED` on the Command-driven launch paths instead of being
  clobbered; harmless no-op elsewhere). On non-Unix targets `uid`/`gid`/
  `setsid` fail the run with `Error::Unsupported` — a requested privilege drop
  is never silently skipped.
- Shell-free pipelines: `Command::pipe(next)` starts a `Pipeline` (extend with
  `.pipe(...)`, bound with `.timeout(...)`, drive with `output_string()` /
  `run()`). Stages connect stdout→stdin through native pipes — no shell, no
  quoting/injection surface — and all run inside one shared kill-on-drop group.
  Pipefail outcome: stdout is the last stage's, while code/stderr/program are
  attributed to the first stage that didn't exit cleanly; `run()` requires
  every stage to succeed.
- Readiness probes on `RunningProcess` — wait until a started child is
  actually ready instead of sleeping: `wait_for_line(predicate, within)`
  (stream stdout until a line matches, returning it; consumes stdout up to the
  match), `wait_for_port(addr, within)` (until a TCP connect is accepted), and
  `wait_for(check, within)` (until any async predicate passes; ~50 ms cadence).
  All three fail with the new `Error::NotReady` when the deadline elapses — or
  immediately once readiness can no longer happen (the child exits; for
  `wait_for_line`, its stdout closes) — and never kill the child (a probe
  deadline is separate from `Command::timeout`).
- `Supervisor` — keep a child alive: restart per `RestartPolicy`
  (`Always`/`OnCrash`/`Never`, where a crash is any run without a clean exit —
  non-zero, timeout, signal, or spawn failure), bounded by `max_restarts`, with
  exponential backoff (`backoff(base, factor)`, capped by `max_backoff`,
  jittered ×[0.5, 1.5) by default — `jitter(false)` for determinism) and a
  `stop_when` predicate that ends supervision regardless of policy. `run()`
  reports a `SupervisionOutcome` (final result, restart count, `StopReason`).
  Platform-agnostic, built on the `ProcessRunner` seam: `with_runner(&group)`
  supervises inside one shared kill-on-drop group; doubles make it hermetic.
- Stats sampling over time (`stats` feature): `ProcessGroup::sample_stats(every)`
  yields a `Stream` of `ProcessGroupStats` snapshots (first sample immediate,
  missed ticks skipped, a zero interval clamped to 1 ms, series ends when the
  group can no longer report), and
  `RunningProcess::profile(every)` runs a child to completion while sampling it,
  returning a `RunProfile` summary (exit code, wall duration, last CPU reading,
  peak RSS, sample count, derived `avg_cpu()`).
- Tree inspection: `ProcessGroup::members()` snapshots the live member pids
  (whole tree via the Windows Job Object pid list / Linux `cgroup.procs`;
  tracked group leaders only on the POSIX process-group backends; always empty
  with no containment), and a free `wait_any` races several `RunningProcess`es
  and returns the index + exit code of whichever exits first — contenders are
  only borrowed (the race is cancel-safe), so losers stay fully usable.
- Whole-tree signals and suspend/resume: `ProcessGroup::signal(Signal)` broadcasts
  a signal to every member (new `Signal` enum — `Term`/`Kill`/`Int`/`Hup`/`Quit`/
  `Usr1`/`Usr2` plus an `Other(i32)` escape hatch), and
  `ProcessGroup::suspend`/`resume` freeze and thaw the tree. Per backend: Linux
  cgroup uses a single whole-subtree `cgroup.freeze` write (falling back to
  per-process `SIGSTOP`/`SIGCONT` on kernels without it), the POSIX process-group
  backends
  broadcast to each group, and Windows suspends/resumes every member thread
  (best-effort; suspend counts nest; the walks are mutually exclusive with a
  concurrent `spawn`'s assign-and-resume, so a mid-spawn child can't be
  stranded suspended). On Windows only `Signal::Kill` is
  deliverable (Job Object terminate); any other signal — and these operations on
  the no-containment target — return the new typed `Error::Unsupported`.
- `ProcessGroupOptions` resource limits (behind the new, off-by-default `limits`
  Cargo feature) — `memory_max`, `max_processes`, and `cpu_quota` cap a group's
  whole tree at creation, plus a public `limits:
  ResourceLimits` field. Enforced by the Windows Job Object (job memory limit,
  active-process limit, hard CPU-rate cap) and Linux cgroup v2 (`memory.max` /
  `pids.max` / `cpu.max`, enabling the matching controllers). `cpu_quota` is a
  fraction of one core (`0.5` = half a core); on Windows it is converted against the
  host CPU count and is approximate. Where no real container exists (macOS/BSD, the
  Linux process-group fallback, the no-containment target) — or a Linux cgroup lacks
  controller delegation — `ProcessGroup::with_options` fails fast with the new
  `Error::ResourceLimit` rather than handing back an unbounded group.

## [0.6.1] - 2026-06-03

### Added
-

### Changed
- Move the Testing and Releasing guides out of `README.md` into a dedicated
  `CONTRIBUTING.md`, keeping the README focused on usage.

### Fixed
-

## [0.6.0] - 2026-06-03

### Added
- `probe` — run a predicate command and read its exit code as a `bool`: exit `0`  `Ok(true)`, exit `1``Ok(false)`, anything else → `Err` (other code / timeout /
  signal-kill). On `Command`, `CliClient`, and `ProcessRunnerExt`. Collapses the
  `match code { 0 => …, 1 => …, _ => Err }` idiom (`git diff --quiet`, `grep -q`, …).
- `Command::retry(max_attempts, backoff, retry_if)` — replay the run while
  `retry_if(&Error)` accepts the failure, with fixed backoff. Honored by the
  success-checking helpers (`run`/`exit_code`/`probe` and the `CliClient`
  `text`/`unit`/`code`/`parse`/`try_parse` helpers); the non-erroring `output_string`/
  `output_bytes`/`capture` paths don't retry. One-shot stdin sources can't replay.

### Changed
- `RunningProcess::stdout_lines` now honors the command's `timeout`: at the deadline
  the process tree is killed and the stream ends, so a streamed run can no longer hang
  past its timeout (`finish_streamed` then reports the kill — `code` is `None` on a Unix
  signal-kill, a platform code on a Windows Job kill). Previously the timeout applied
  only to the run-to-completion helpers.

### Fixed
- Linux (cgroup backend): `Drop` no longer leaks the cgroup directory. `cgroup.kill`
  is asynchronous, so the immediate `rmdir` used to race the still-draining members
  and fail with `EBUSY`; `Drop` now waits (bounded) for the subtree to drain first.
- Linux (cgroup backend, pre-5.14 kernels): the per-pid SIGKILL fallback no longer
  busy-spins — it sleeps briefly between sweeps.
- Streaming: a panicking `on_stdout_line` / `on_stderr_line` handler no longer hangs a
  `stdout_lines` consumer. The pump now closes its sink on any exit (including a panic
  unwind), so the stream always ends instead of parking forever.
- Streaming: a second `stdout_lines()` call no longer silently discards the first call's
  stderr (it previously overwrote the stderr sink, so `finish_streamed` returned empty).
- Test double: `Reply::timeout()` now reports the command's real configured deadline in
  `Error::Timeout` (it previously surfaced a zero duration, diverging from the live runner).

## [0.5.2] - 2026-06-03

### Changed

- ci(release): push the release commit via a GitHub App token (App bypasses branch protection; no PAT expiry); attribute commit to owner (#1)

## [0.5.1] - 2026-06-02

### Added
-

### Changed
- `Error::diagnostic()` and `ProcessResult::diagnostic()` now return the message
  trimmed of surrounding whitespace (the trailing newline a tool leaves on its
  output is noise for a human-facing message). For the raw streams, match
  `Error::Exit`'s fields or use `ProcessResult::stdout`/`stderr`.

### Fixed
-

## [0.5.0] - 2026-06-02

### Added
- `Error::Exit` now carries `stdout` alongside `stderr` (each truncated to 4 KiB),
  so a failed `git`/`jj` run's stdout diagnostics (`CONFLICT (content): …`,
  `nothing to commit, working tree clean`) survive the typed error instead of
  being dropped.
- `Error::diagnostic()` and `ProcessResult::diagnostic()` — the best human message
  for a failed run: standard error if it has text, otherwise standard output.
- `CliClient::default_env` / `default_env_remove` (and matching `cli_client!`
  macro methods): set an environment variable on every command the client builds
  (e.g. `GIT_TERMINAL_PROMPT=0`) instead of repeating it per call.

### Changed
- `ProcessResult::exit_code() -> i32` is replaced by `code() -> Option<i32>`:
  a run that yields no code (killed by its timeout, or by a signal on Unix) is
  `None` — the synthetic `-1` sentinel is gone. `RunningProcess::wait` and
  `finish_streamed` likewise return `Option<i32>`. The `exit_code` convenience
  helpers (`Command`/`ProcessRunnerExt`/`CliClient`) still return `Result<i32>`,
  now surfacing a signal-kill as an IO error rather than `-1`.
- `CliClient::text` trims trailing whitespace only (`trim_end`), matching
  `run` — previously it trimmed both ends.

### Fixed
- Windows: closed the spawn→assign race in the kill-on-close guarantee. A child
  is now created `CREATE_SUSPENDED`, assigned to the Job Object, then resumed, so
  a fast-forking child can no longer escape containment in the window between
  spawn and assignment.

## [0.4.1] - 2026-06-02

### Changed

- review: harden macOS/BSD process-group containment

## [0.4.0] - 2026-06-01

### Added
- macOS and the BSDs now contain process trees with a POSIX process group
  (`killpg` on drop) instead of a plain, uncontained spawn — `mechanism()`
  reports `ProcessGroup` there rather than `None`. The shared backend is the same
  one Linux already uses when no cgroup is writable.

### Changed
-

### Fixed
-

## [0.3.4] - 2026-06-01

### Changed

- Release: reject dispatch from any ref other than main
- Stop tracking agent-instruction files (AGENTS.md, CLAUDE.md, .claude/) — keep them local only

## [0.3.3] - 2026-06-01

### Changed

- Release: always target main (check out + push main regardless of the dispatch ref)

## [0.3.2] - 2026-06-01

### Changed

- Release: publish to crates.io before tagging + retry/idempotent publish & GitHub Release, --locked

## [0.3.1] - 2026-06-01

### Added
- Async stdin/stdout usage examples on `RunningProcess::standard_input` and
  `RunningProcess::stdout_lines`, plus a `StreamExt` re-export so callers can
  consume the `stdout_lines` stream with `use processkit::StreamExt;` (no direct
  `tokio-stream` dependency).

### Changed
-

### Fixed
- `Command::first_line` now honors the command's `timeout` while streaming. It
  previously enforced the deadline only on the run-to-completion path, so a
  command that produced no matching line (e.g. a silent long-running process)
  could hang forever; it now returns `Error::Timeout` once the deadline elapses.

## [0.3.0] - 2026-06-01

### Changed
- **Timeouts are now a first-class `Error::Timeout`** on the success-checking
  helpers. `ProcessResult::ensure_success` (hence `ProcessRunnerExt::run`/`checked`,
  `CliClient::text`/`unit`/`parse`/`try_parse`, and `Command::run`) and
  `ProcessRunnerExt::exit_code` / `CliClient::code` / `Command::exit_code` now return
  `Error::Timeout` for a run killed by its deadline, instead of folding it into
  `Error::Exit { code: -1 }` / a synthetic `-1`.
  `capture`/`output` still expose the inspectable `ProcessResult::timed_out()`
  without erroring. **Breaking:** a timeout that previously surfaced as `Error::Exit`
  is now `Error::Timeout` (the variant was formerly unreachable).

### Added
- `Reply::timeout()` — a canned `ScriptedRunner` reply that drives the timeout
  path, so tests can assert that a command exceeding its deadline surfaces as
  `Error::Timeout`.

## [0.2.0] - 2026-06-01

### Changed
- Release workflow: pick the version bump from a menu, with auto-increment.
  (Release tooling only — no changes to the published library.)

## [0.1.2] - 2026-05-31

_No functional changes — republished to recover a failed crates.io upload; the
first version to actually reach crates.io._

## [0.1.1] - 2026-05-31

_No functional changes — republished to recover a failed crates.io upload._

## [0.1.0] - 2026-05-31

### Added
- `ProcessGroup` — a kill-on-drop container for a child-process tree, backed by
  Windows Job Objects, Linux cgroup v2 (with a POSIX process-group fallback), or
  no containment elsewhere. Async `shutdown` performs a graceful
  SIGTERM → wait → SIGKILL teardown on Unix; the mechanism in effect is
  observable via `Mechanism`.
- `Command` builder and async run-and-capture helpers: `output_string`,
  `output_bytes`, `exit_code`, `run`, `first_line`, and `start` (live handle).
- `RunningProcess` handle with incremental `stdout_lines` streaming (stderr
  drained in the background), `output_string`/`output_bytes`/`wait`, and process
  metadata.
- `ProcessResult<T>` with `is_success` / `ensure_success`, and a structured
  `Error` (`Spawn` / `Exit` / `Timeout` / `Io`).
- `Stdin` sources: `empty`, `from_string`, `from_bytes`, `from_file`,
  `from_iter_lines`, `from_reader`, and `from_lines` (async stream).
- `ProcessRunner` mock seam with `JobRunner`, `ScriptedRunner`,
  `RecordingRunner`, and a `mock`-feature `MockRunner`.
- Interactive stdin: `Command::keep_stdin_open` plus `RunningProcess::standard_input`
  returning a `ProcessStdin` writer (`write`/`write_line`/`flush`/`finish`).
- Push line-handlers: `Command::on_stdout_line` / `on_stderr_line`, invoked per
  decoded line as it is read.
- Output-buffer policy: `OutputBufferPolicy` (`bounded`/`unbounded`) with
  `OverflowMode::{DropOldest, DropNewest}`, plus exact `RunningProcess::stdout_line_count`
  / `stderr_line_count` (count survives dropped lines).
- Encoding overrides: `Command::stdout_encoding` / `stderr_encoding` / `encoding`
  to decode non-UTF-8 legacy output (via `encoding_rs`); default stays UTF-8.
- Diagnostics: `ProcessGroup::stats``ProcessGroupStats` (active count, and
  CPU/peak-memory where the platform reports them), and per-process
  `RunningProcess::cpu_time` / `peak_memory_bytes` / `elapsed`.
- `CliClient<R>` + the `cli_client!` macro — a reusable core for building typed
  wrappers around an external CLI tool (`command`/`command_in` builders;
  `text`/`capture`/`unit`/`code`/`parse`/`try_parse` run helpers), with the
  runner injectable for hermetic tests.
- Top-level `processkit::run` / `processkit::output` free functions.
- Public `Command` accessors (`program`/`arguments`/`working_dir`/
  `env_overrides`/`stdin_source`/`configured_timeout`) so external
  `ScriptedRunner::when` predicates can inspect a command; plus public
  `Command::to_tokio_command`.
- `ProcessRunnerExt::checked`, `ProcessResult::combined`, `Invocation::args_str`,
  `RunningProcess::finish_streamed` (exit code + collected stderr after
  streaming) and `RunningProcess::start_kill`.
- `Error::Parse { program, message }` for fallible output parsing.
- The `tracing` feature emits a per-run `debug` event (program, exit code,
  timed-out, elapsed) on the `processkit` target.

### Changed
- Output capture is line-oriented (pumped): captured text is normalized to
  `\n` line endings. `output_bytes` still returns exact raw stdout.

[Unreleased]: https://github.com/ZelAnton/ProcessKit-rs/compare/v0.10.0...HEAD
[0.10.0]: https://github.com/ZelAnton/ProcessKit-rs/compare/v0.9.2...v0.10.0
[0.9.2]: https://github.com/ZelAnton/ProcessKit-rs/compare/v0.9.1...v0.9.2
[0.9.1]: https://github.com/ZelAnton/ProcessKit-rs/compare/v0.9.0...v0.9.1
[0.9.0]: https://github.com/ZelAnton/ProcessKit-rs/compare/v0.8.2...v0.9.0
[0.8.2]: https://github.com/ZelAnton/ProcessKit-rs/compare/v0.8.1...v0.8.2
[0.8.1]: https://github.com/ZelAnton/ProcessKit-rs/compare/v0.8.0...v0.8.1
[0.8.0]: https://github.com/ZelAnton/ProcessKit-rs/compare/v0.7.1...v0.8.0
[0.7.1]: https://github.com/ZelAnton/ProcessKit-rs/compare/v0.7.0...v0.7.1
[0.6.2]: https://github.com/ZelAnton/ProcessKit-rs/releases/tag/v0.6.2
[0.7.0]: https://github.com/ZelAnton/ProcessKit-rs/compare/v0.6.1...v0.7.0
[0.6.1]: https://github.com/ZelAnton/ProcessKit-rs/compare/v0.6.0...v0.6.1
[0.6.0]: https://github.com/ZelAnton/ProcessKit-rs/compare/v0.5.2...v0.6.0
[0.5.2]: https://github.com/ZelAnton/ProcessKit-rs/compare/v0.5.1...v0.5.2
[0.5.1]: https://github.com/ZelAnton/ProcessKit-rs/compare/v0.5.0...v0.5.1
[0.5.0]: https://github.com/ZelAnton/ProcessKit-rs/compare/v0.4.1...v0.5.0
[0.4.1]: https://github.com/ZelAnton/ProcessKit-rs/compare/v0.4.0...v0.4.1
[0.4.0]: https://github.com/ZelAnton/ProcessKit-rs/compare/v0.3.4...v0.4.0
[0.3.4]: https://github.com/ZelAnton/ProcessKit-rs/compare/v0.3.3...v0.3.4
[0.3.3]: https://github.com/ZelAnton/ProcessKit-rs/compare/v0.3.2...v0.3.3
[0.3.2]: https://github.com/ZelAnton/ProcessKit-rs/compare/v0.3.1...v0.3.2
[0.3.1]: https://github.com/ZelAnton/ProcessKit-rs/compare/v0.3.0...v0.3.1
[0.3.0]: https://github.com/ZelAnton/ProcessKit-rs/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/ZelAnton/ProcessKit-rs/compare/v0.1.2...v0.2.0
[0.1.2]: https://github.com/ZelAnton/ProcessKit-rs/compare/v0.1.1...v0.1.2
[0.1.1]: https://github.com/ZelAnton/ProcessKit-rs/compare/v0.1.0...v0.1.1
[0.1.0]: https://github.com/ZelAnton/ProcessKit-rs/releases/tag/v0.1.0