processkit-py 1.0.0__tar.gz → 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. {processkit_py-1.0.0 → processkit_py-1.1.0}/.gitignore +2 -9
  2. processkit_py-1.1.0/CHANGELOG.md +629 -0
  3. {processkit_py-1.0.0 → processkit_py-1.1.0}/CONTRIBUTING.md +7 -0
  4. {processkit_py-1.0.0 → processkit_py-1.1.0}/Cargo.lock +12 -12
  5. {processkit_py-1.0.0 → processkit_py-1.1.0}/Cargo.toml +15 -5
  6. {processkit_py-1.0.0 → processkit_py-1.1.0}/PKG-INFO +21 -17
  7. {processkit_py-1.0.0 → processkit_py-1.1.0}/README.md +19 -15
  8. {processkit_py-1.0.0 → processkit_py-1.1.0}/RELEASING.md +2 -1
  9. {processkit_py-1.0.0 → processkit_py-1.1.0}/ROADMAP.md +47 -8
  10. processkit_py-1.1.0/conftest.py +11 -0
  11. {processkit_py-1.0.0 → processkit_py-1.1.0}/docs/README.md +11 -7
  12. processkit_py-1.1.0/docs/api-reference.md +163 -0
  13. {processkit_py-1.0.0 → processkit_py-1.1.0}/docs/commands.md +78 -11
  14. {processkit_py-1.0.0 → processkit_py-1.1.0}/docs/cookbook.md +84 -11
  15. processkit_py-1.1.0/docs/event-loops.md +148 -0
  16. processkit_py-1.1.0/docs/internals.md +251 -0
  17. {processkit_py-1.0.0 → processkit_py-1.1.0}/docs/migrating.md +1 -1
  18. {processkit_py-1.0.0 → processkit_py-1.1.0}/docs/pipelines.md +19 -4
  19. {processkit_py-1.0.0 → processkit_py-1.1.0}/docs/platforms.md +14 -12
  20. {processkit_py-1.0.0 → processkit_py-1.1.0}/docs/process-groups.md +13 -3
  21. {processkit_py-1.0.0 → processkit_py-1.1.0}/docs/streaming.md +123 -38
  22. {processkit_py-1.0.0 → processkit_py-1.1.0}/docs/supervision.md +6 -2
  23. {processkit_py-1.0.0 → processkit_py-1.1.0}/docs/testing.md +158 -9
  24. {processkit_py-1.0.0 → processkit_py-1.1.0}/docs/timeouts-and-cancellation.md +53 -16
  25. {processkit_py-1.0.0 → processkit_py-1.1.0}/mkdocs.yml +33 -0
  26. processkit_py-1.1.0/pyproject.toml +243 -0
  27. processkit_py-1.1.0/scripts/ci-privileged-check.sh +83 -0
  28. processkit_py-1.1.0/scripts/ci-privileged-guard.py +79 -0
  29. processkit_py-1.1.0/scripts/gen_api_reference.py +294 -0
  30. processkit_py-1.1.0/scripts/release/__init__.py +7 -0
  31. processkit_py-1.1.0/scripts/release/cargo_lock.py +55 -0
  32. processkit_py-1.1.0/scripts/release/changelog.py +211 -0
  33. {processkit_py-1.0.0 → processkit_py-1.1.0}/src/batch.rs +63 -27
  34. processkit_py-1.1.0/src/cancellation.rs +67 -0
  35. processkit_py-1.1.0/src/cli.rs +324 -0
  36. {processkit_py-1.0.0 → processkit_py-1.1.0}/src/command.rs +211 -21
  37. processkit_py-1.1.0/src/convert.rs +248 -0
  38. processkit_py-1.1.0/src/errors.rs +281 -0
  39. {processkit_py-1.0.0 → processkit_py-1.1.0}/src/group.rs +112 -13
  40. processkit_py-1.1.0/src/lib.rs +53 -0
  41. {processkit_py-1.0.0 → processkit_py-1.1.0}/src/logging.rs +6 -0
  42. {processkit_py-1.0.0 → processkit_py-1.1.0}/src/processkit/__init__.py +37 -16
  43. processkit_py-1.1.0/src/processkit/_aio.py +309 -0
  44. {processkit_py-1.0.0 → processkit_py-1.1.0}/src/processkit/_processkit.pyi +268 -106
  45. processkit_py-1.1.0/src/processkit/_protocols.py +68 -0
  46. processkit_py-1.1.0/src/processkit/_types.py +33 -0
  47. processkit_py-1.1.0/src/processkit/pytest_plugin.py +296 -0
  48. {processkit_py-1.0.0 → processkit_py-1.1.0}/src/result.rs +49 -0
  49. {processkit_py-1.0.0 → processkit_py-1.1.0}/src/runner.rs +192 -17
  50. {processkit_py-1.0.0 → processkit_py-1.1.0}/src/running.rs +130 -46
  51. {processkit_py-1.0.0 → processkit_py-1.1.0}/src/runtime.rs +34 -0
  52. {processkit_py-1.0.0 → processkit_py-1.1.0}/src/supervisor.rs +81 -6
  53. {processkit_py-1.0.0 → processkit_py-1.1.0}/stubtest-allowlist.txt +6 -5
  54. processkit_py-1.1.0/tests/_docs_snippets.py +161 -0
  55. {processkit_py-1.0.0 → processkit_py-1.1.0}/tests/_liveness.py +19 -16
  56. processkit_py-1.1.0/tests/_programs.py +51 -0
  57. {processkit_py-1.0.0 → processkit_py-1.1.0}/tests/_typing_pins.py +17 -6
  58. processkit_py-1.1.0/tests/conftest.py +54 -0
  59. processkit_py-1.1.0/tests/property/__init__.py +3 -0
  60. processkit_py-1.1.0/tests/property/conftest.py +45 -0
  61. processkit_py-1.1.0/tests/property/test_argv_env_roundtrip.py +100 -0
  62. processkit_py-1.1.0/tests/property/test_numeric_validation.py +247 -0
  63. processkit_py-1.1.0/tests/property/test_output_limit.py +150 -0
  64. processkit_py-1.1.0/tests/property/test_signals.py +80 -0
  65. processkit_py-1.1.0/tests/property/test_wait_for_line.py +76 -0
  66. processkit_py-1.1.0/tests/test_api_reference.py +77 -0
  67. {processkit_py-1.0.0 → processkit_py-1.1.0}/tests/test_api_surface.py +186 -11
  68. {processkit_py-1.0.0 → processkit_py-1.1.0}/tests/test_async.py +9 -24
  69. processkit_py-1.1.0/tests/test_batch.py +164 -0
  70. processkit_py-1.1.0/tests/test_cli_client.py +287 -0
  71. processkit_py-1.1.0/tests/test_command.py +881 -0
  72. processkit_py-1.1.0/tests/test_docs_snippets.py +242 -0
  73. {processkit_py-1.0.0 → processkit_py-1.1.0}/tests/test_exceptions.py +63 -8
  74. {processkit_py-1.0.0 → processkit_py-1.1.0}/tests/test_hardening.py +36 -8
  75. {processkit_py-1.0.0 → processkit_py-1.1.0}/tests/test_logging.py +6 -2
  76. {processkit_py-1.0.0 → processkit_py-1.1.0}/tests/test_pipelines.py +16 -1
  77. processkit_py-1.1.0/tests/test_process_group.py +422 -0
  78. processkit_py-1.1.0/tests/test_pytest_plugin.py +301 -0
  79. processkit_py-1.1.0/tests/test_readiness.py +579 -0
  80. processkit_py-1.1.0/tests/test_release_scripts.py +197 -0
  81. {processkit_py-1.0.0 → processkit_py-1.1.0}/tests/test_runner_seam.py +192 -5
  82. processkit_py-1.1.0/tests/test_streaming.py +563 -0
  83. {processkit_py-1.0.0 → processkit_py-1.1.0}/tests/test_supervisor.py +105 -6
  84. processkit_py-1.1.0/uv.lock +1345 -0
  85. processkit_py-1.0.0/CHANGELOG.md +0 -291
  86. processkit_py-1.0.0/pyproject.toml +0 -138
  87. processkit_py-1.0.0/src/cli.rs +0 -121
  88. processkit_py-1.0.0/src/convert.rs +0 -139
  89. processkit_py-1.0.0/src/errors.rs +0 -202
  90. processkit_py-1.0.0/src/lib.rs +0 -104
  91. processkit_py-1.0.0/src/processkit/_aio.py +0 -175
  92. processkit_py-1.0.0/src/processkit/_runner.py +0 -46
  93. processkit_py-1.0.0/src/processkit/_types.py +0 -17
  94. processkit_py-1.0.0/tests/_programs.py +0 -28
  95. processkit_py-1.0.0/tests/test_batch.py +0 -72
  96. processkit_py-1.0.0/tests/test_cli_client.py +0 -69
  97. processkit_py-1.0.0/tests/test_command.py +0 -427
  98. processkit_py-1.0.0/tests/test_import.py +0 -10
  99. processkit_py-1.0.0/tests/test_process_group.py +0 -221
  100. processkit_py-1.0.0/tests/test_readiness.py +0 -316
  101. processkit_py-1.0.0/tests/test_streaming.py +0 -353
  102. processkit_py-1.0.0/uv.lock +0 -584
  103. {processkit_py-1.0.0 → processkit_py-1.1.0}/.editorconfig +0 -0
  104. {processkit_py-1.0.0 → processkit_py-1.1.0}/.gitattributes +0 -0
  105. {processkit_py-1.0.0 → processkit_py-1.1.0}/.pre-commit-config.yaml +0 -0
  106. {processkit_py-1.0.0 → processkit_py-1.1.0}/.python-version +0 -0
  107. {processkit_py-1.0.0 → processkit_py-1.1.0}/.yamllint.yml +0 -0
  108. {processkit_py-1.0.0 → processkit_py-1.1.0}/LICENSE +0 -0
  109. {processkit_py-1.0.0 → processkit_py-1.1.0}/SECURITY.md +0 -0
  110. {processkit_py-1.0.0 → processkit_py-1.1.0}/cliff.toml +0 -0
  111. {processkit_py-1.0.0 → processkit_py-1.1.0}/examples/01_no_orphan_guarantee.py +0 -0
  112. {processkit_py-1.0.0 → processkit_py-1.1.0}/examples/02_wait_for_server.py +0 -0
  113. {processkit_py-1.0.0 → processkit_py-1.1.0}/examples/03_supervise_until_healthy.py +0 -0
  114. {processkit_py-1.0.0 → processkit_py-1.1.0}/examples/04_sandbox_resource_limits.py +0 -0
  115. {processkit_py-1.0.0 → processkit_py-1.1.0}/examples/README.md +0 -0
  116. {processkit_py-1.0.0 → processkit_py-1.1.0}/rust-toolchain.toml +0 -0
  117. {processkit_py-1.0.0 → processkit_py-1.1.0}/scripts/check-env.ps1 +0 -0
  118. {processkit_py-1.0.0 → processkit_py-1.1.0}/scripts/check-env.sh +0 -0
  119. {processkit_py-1.0.0 → processkit_py-1.1.0}/scripts/smoke.py +0 -0
  120. {processkit_py-1.0.0 → processkit_py-1.1.0}/src/processkit/py.typed +0 -0
  121. {processkit_py-1.0.0 → processkit_py-1.1.0}/src/processkit/testing.py +0 -0
  122. {processkit_py-1.0.0 → processkit_py-1.1.0}/tests/__init__.py +0 -0
  123. {processkit_py-1.0.0 → processkit_py-1.1.0}/tests/test_examples.py +0 -0
@@ -19,8 +19,10 @@ venv/
19
19
  .ruff_cache/
20
20
  .coverage
21
21
  .coverage.*
22
+ coverage.xml
22
23
  htmlcov/
23
24
  .tox/
25
+ .hypothesis/
24
26
 
25
27
  # Release pipeline scratch file — written by .github/workflows/release.yml
26
28
  release-notes.md
@@ -68,15 +70,6 @@ Desktop.ini
68
70
  $RECYCLE.BIN/
69
71
  *.lnk
70
72
 
71
- ## -------
72
- ## Agent-instruction files — kept on disk for tooling but never pushed to remote.
73
- ## CLAUDE.md, CLAUDE.local.md, AGENTS.md, and .claude/ are local-only here.
74
- ## -------
75
- /CLAUDE.md
76
- /CLAUDE.local.md
77
- /AGENTS.md
78
- .claude/
79
-
80
73
  ## -------
81
74
  ## Rust build artifacts
82
75
  ## -------
@@ -0,0 +1,629 @@
1
+ # Changelog
2
+
3
+ All notable changes to **processkit** are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+ -
12
+
13
+ ### Changed
14
+ -
15
+
16
+ ### Fixed
17
+ -
18
+
19
+ ## [1.1.0] - 2026-07-06
20
+
21
+ ### Breaking
22
+ - `RunningProcess`'s consuming verbs now come in a sync/async pair, like
23
+ everywhere else in this library, instead of being coroutine-only. Migration:
24
+ `await proc.wait()` → `await proc.aoutcome()` (renamed — `await` is a
25
+ reserved word, so the async twin of the new sync `outcome()` couldn't be
26
+ called `await()`); `await proc.finish()` → `await proc.afinish()`;
27
+ `await proc.output()` → `await proc.aoutput()`; `await proc.output_bytes()`
28
+ → `await proc.aoutput_bytes()`; `await proc.profile(...)` →
29
+ `await proc.aprofile(...)`; `await proc.shutdown(...)` →
30
+ `await proc.ashutdown(...)`. Each bare name is now a new **synchronous**
31
+ method (`proc.outcome()`, `proc.finish()`, `proc.output()`,
32
+ `proc.output_bytes()`, `proc.profile(...)`, `proc.shutdown(...)`), making a
33
+ handle from the synchronous `Command.start()` / `Runner.start()` genuinely
34
+ usable end-to-end with no event loop at all — not just for the
35
+ monitor-and-`kill()` pattern. No aliasing was possible (the old bare names
36
+ now mean something different — synchronous — so keeping them pointing at the
37
+ old async behavior would be actively misleading, not merely redundant).
38
+ `RunningProcess.shutdown()`/`ashutdown()` also now match
39
+ `ProcessGroup.shutdown()`/`ashutdown()`'s naming exactly, closing a trap
40
+ where the same verb name meant "call it" on one class but "await it" on the
41
+ other.
42
+ - `ProcessRunner` no longer includes `start`/`astart` — it is now the
43
+ capture/check verb surface only (`output`/`run`/`exit_code`/`probe` and
44
+ their `a`-prefixed twins). A new `StreamingRunner(ProcessRunner)` protocol
45
+ adds `start`/`astart` back for code that also needs a live `RunningProcess`
46
+ handle. Migration: annotate an injection point that only calls the
47
+ capture/check verbs as `ProcessRunner` (now narrower, easier for a custom
48
+ double to satisfy); annotate one that also calls `start`/`astart` as
49
+ `StreamingRunner`. Every built-in runner (`Runner`, `ScriptedRunner`,
50
+ `RecordingRunner`, `RecordReplayRunner`) satisfies `StreamingRunner` (and
51
+ therefore `ProcessRunner` too), so existing injected-runner call sites are
52
+ unaffected — only code that annotated *against* `ProcessRunner` expecting
53
+ `start`/`astart` to be part of it needs to switch to `StreamingRunner`. The
54
+ internal `_runner.py` module (never part of the public import path) is
55
+ renamed `_protocols.py` to reflect holding two protocols now, not one.
56
+ - `wait_for()` is renamed `wait_until()` — the old name collided with
57
+ `asyncio.wait_for`, which bounds one *awaitable*, not a *polled predicate*
58
+ (different semantics entirely). Migration: `await wait_for(...)` →
59
+ `await wait_until(...)`, same arguments. No alias was kept — a `wait_for`
60
+ alias sitting next to `asyncio.wait_for` in the same import line would
61
+ perpetuate exactly the confusion this rename fixes. All three readiness
62
+ helpers (`wait_until`, `wait_for_port`, `wait_for_line`) now raise
63
+ `WaitTimeout` (`ProcessError`, `TimeoutError`) instead of a bare
64
+ `TimeoutError` on their own deadline — still catchable as `except
65
+ TimeoutError`, but now carrying `timeout_seconds` (and, for
66
+ `wait_for_port`, `host`/`port`) as structured fields instead of only a
67
+ message string.
68
+
69
+ ### Added
70
+ - A **pytest plugin**, autoloaded via a `pytest11` entry point in every pytest
71
+ session where processkit is installed (nothing to add to `conftest.py`; the
72
+ plugin module is pure Python and import-safe). It exposes the
73
+ `processkit.testing` doubles as ready-made fixtures — `scripted_runner` (a fresh
74
+ `ScriptedRunner`), `recording_runner` (a `RecordingRunner` spy replying
75
+ `Reply.ok("")`, the neutral default), and `record_replay_runner` (a
76
+ `RecordReplayRunner` bound to a per-test cassette) — so injecting a test double
77
+ is a single fixture parameter. The cassette fixture is replay-by-default with a
78
+ vcr-style switch to record (`--processkit-record` CLI flag, then the
79
+ `PROCESSKIT_RECORD` env var, then the `processkit_record` ini option, in that
80
+ precedence); its file lives under the test's `tmp_path` unless the
81
+ `processkit_cassette_dir` ini option points at a kept directory, and its name is
82
+ derived deterministically from the test's node id. A `@pytest.mark.no_real_spawn`
83
+ marker (registered so it passes `--strict-markers`) makes any real spawn through
84
+ `Command`/`Pipeline`/`Runner`/`ProcessGroup` inside the marked test fail loudly,
85
+ while injected doubles keep working. Documented in `docs/testing.md` and the
86
+ cookbook.
87
+ - `Args` and `ReadableBuffer` type aliases (`from processkit import Args,
88
+ ReadableBuffer`). `Args` (`list[StrPath] | tuple[StrPath, ...]`) replaces
89
+ `Sequence[str]`/`Sequence[StrPath]` on every argv-like parameter
90
+ (`Command`'s `args`, `ScriptedRunner.on()`/`on_sequence()`'s `prefix`,
91
+ `CliClient.command()`/its verbs) — deliberately **not** `Sequence[StrPath]`,
92
+ since `str` is itself structurally a `Sequence[str]` (each character is a
93
+ `str`), so that spelling let a bare string slip through everywhere an argv
94
+ list was expected (`cmd.args("--flag")` type-checked, then exploded into
95
+ one argument *per character* at runtime). This is a static-typing-only
96
+ tightening — runtime behavior (and any caller not using mypy) is
97
+ unaffected; a mypy-strict caller passing something other than a `list`/
98
+ `tuple` (an arbitrary custom `Sequence`) at one of these call sites may
99
+ need to wrap it in `list(...)`. `ReadableBuffer` (`bytes | bytearray |
100
+ memoryview`) replaces the too-narrow `bytes` on `Command.stdin_bytes()` /
101
+ `ProcessStdin.write()` — both already accepted `bytearray`/`memoryview` at
102
+ runtime (PyO3's buffer-protocol extraction), so this only catches up the
103
+ stub to reality, no runtime change.
104
+ - `CliClient`'s `command()` and every verb (`run`/`output`/`output_bytes`/
105
+ `exit_code`/`probe`, `a`-prefixed twins) now accept a `str` or any
106
+ `os.PathLike[str]` for each argv element, unified with `Command`'s own
107
+ `arg`/`args` typing — previously `CliClient` was `str`-only, so a
108
+ `pathlib.Path` argument needed a manual `str()` there but not on `Command`.
109
+ - Documented explicitly: `Timeout`, `ProcessNotFound`, and `PermissionDenied`
110
+ are transitively `OSError` subclasses too (since their builtin second base
111
+ — `TimeoutError`/`FileNotFoundError`/`PermissionError` — has itself been an
112
+ `OSError` subclass since Python 3.3), so `except OSError` catches all
113
+ three alongside `except ProcessError`. No behavior change — this was
114
+ already true; it just wasn't written down anywhere.
115
+ - Fixed: `PermissionDenied.program` is now typed `str | None` (was `str`) and
116
+ reliably reads `None` — not a missing-attribute `AttributeError` — on the
117
+ broader OS-refusal path with no program to name (`is_permission_denied()`
118
+ also classifies a program-less `Io` failure, e.g. a group signal the OS
119
+ refused, alongside the ordinary spawn-time denial that does name one).
120
+ Mirrors the class-level default already used for `Timeout.timeout_seconds`.
121
+ - `CancellationToken` — a portable cancel switch: `Command.cancel_on(token)`
122
+ (replaces any prior token — last write wins), `Pipeline.cancel_on(token)`
123
+ (gap-fill — a stage with its own explicit token keeps it), and `CliClient`'s
124
+ `default_cancel_on=` (also gap-fill) tear the run/chain down when `token`
125
+ fires, surfacing the new `Cancelled` exception. `token.cancel()` is
126
+ idempotent; `token.child_token()` derives a token cancelled automatically
127
+ with its parent but cancellable independently, for scoping a broader
128
+ shutdown token down to one operation.
129
+ - `Cancelled` exception — a run deliberately cancelled via a
130
+ `CancellationToken`. Previously such a cancellation surfaced only as a
131
+ plain `ProcessError` (no dedicated subclass existed since `cancel_on` had
132
+ no binding yet); now a distinct, terminal exception — never retried by
133
+ `Command.retry()` or restarted by `Supervisor`, matching the crate's own
134
+ contract (a cancelled token stays cancelled forever, so a replay could only
135
+ fail the same way).
136
+ - `ScriptedRunner.when(predicate, reply)` — reply with `reply` when
137
+ `predicate(command)` accepts it, for a match that isn't a plain argv
138
+ prefix (`on()`) — e.g. inspecting `cwd`/`arguments`/flags via `Command`'s
139
+ own inspection accessors. `predicate` is infallible from the crate's
140
+ perspective, like `Supervisor.stop_when`: a raising or non-`bool` predicate
141
+ reads as "does not match", surfaced via the unraisable hook.
142
+ - `Reply.with_line_delay(seconds)` — sleep `seconds` before each scripted
143
+ stdout line on a `start()`/`astart()` run, so a hermetic streaming test can
144
+ observe genuinely incremental delivery instead of every line arriving at
145
+ once.
146
+ - `RecordingRunner.new(inner)` — wrap any of `Runner`, `ScriptedRunner`,
147
+ `RecordReplayRunner`, or another `RecordingRunner`, recording every call
148
+ made through it. The general form behind the existing `replying(reply)`
149
+ (a recorder whose inner runner is always a fresh `ScriptedRunner` replying
150
+ with one canned `Reply`) — `new()` lets a test combine recording with a
151
+ double it already built (e.g. a `RecordReplayRunner` cassette) or with the
152
+ real `Runner`.
153
+ - `ProcessGroup` is now itself a runner: `group.output(cmd)` / `.run(cmd)` /
154
+ `.exit_code(cmd)` / `.probe(cmd)` / `.output_bytes(cmd)` (+ `a`-prefixed
155
+ twins) run `cmd` as a *shared* member of the group (not a standalone
156
+ private tree) — the same verb surface `Runner`/`ScriptedRunner`/… expose,
157
+ for code written against that seam that should route every spawn through
158
+ one shared group. (Not registered as a `runner=` injection target — a
159
+ `ProcessGroup` carries real OS resources and is injected directly by
160
+ callers who already hold one, not through that kwarg seam.)
161
+ - `output_all()` / `aoutput_all()` / `output_all_bytes()` / `aoutput_all_bytes()`
162
+ now reject `concurrency=0` with `ValueError` instead of silently clamping it
163
+ to `1` (a confusing "asked for none, got some anyway").
164
+ - `Command.no_timeout()` — run without a timeout, and (unlike simply leaving
165
+ it unset) opt out of a client-wide `CliClient` `default_timeout` gap-fill.
166
+ Clears a prior `.timeout()`; the last of the two wins.
167
+ - `Command.stdout_tee(path, *, append=False)` / `stderr_tee(path, *,
168
+ append=False)` — tee every decoded line of the stream to a file *as it is
169
+ produced* (the line plus a `\n`, CRLF normalized) while the run **also** keeps
170
+ capturing the full output: the one-line way to "stream a log to a file and
171
+ still get the captured `ProcessResult`", without a manual loop over
172
+ `stdout_lines()`. The sink is a **file path** (`str` / `os.PathLike[str]`);
173
+ teeing to an arbitrary Python object as a live async writer is deliberately
174
+ **not** supported yet (a separate, deferred feature — dispatching each line to
175
+ a thread, re-acquiring the GIL, honoring backpressure across the FFI boundary
176
+ is its own scope). The file is opened **at build time** — the crate takes a
177
+ concrete sink, not a lazy factory — so an unopenable path (missing parent
178
+ directory, a directory, a permission denial) raises the matching `OSError`
179
+ subclass right at the builder call, not at run; it is created/truncated by
180
+ default, or appended to with `append=True`. Inherited crate semantics: a slow
181
+ sink applies backpressure (it does not block the runtime); a tee write error
182
+ disables the tee for the rest of the run without breaking the run or its
183
+ captured result (warned under `enable_logging()`); and the tee is inert unless
184
+ the line pump runs — a no-op under `stdout("inherit")` / `stdout("null")` and
185
+ under `output_bytes()` (raw capture), working with the line verbs (`output()`
186
+ / `aoutput()` / `run()`, `start()` + `stdout_lines()` / `output_events()`). A
187
+ reused command's shared sink handle **appends** across sequential re-runs
188
+ (retries, `Supervisor` incarnations) and **interleaves** across concurrent
189
+ pipeline stages.
190
+ - `Command.command_line()` — render the command as a single shell-quoted line
191
+ for display (logs, error messages, a dry-run echo); includes argv, unlike
192
+ the redacted `repr()`. Never used to actually execute anything. Plus
193
+ `Command.program` / `Command.arguments` read-only properties (named
194
+ `arguments`, not `args` — that name is already the builder method that
195
+ appends args).
196
+ - `Command.unchecked_in_pipe()` — exempt a command, as a `Pipeline` stage,
197
+ from pipefail attribution (its unclean exit, including a `SIGPIPE`, is
198
+ skipped when the chain decides what to report); a no-op outside a
199
+ `Pipeline`.
200
+ - `ProcessResult.ensure_success()` / `BytesResult.ensure_success()` — raise
201
+ the same exception a checking verb would if the result's exit isn't in
202
+ `success_codes`, for turning an already-captured `output()`/`output_bytes()`
203
+ result into an error after the fact. Returns `self` unchanged on success, so
204
+ it composes: `cmd.output().ensure_success().stdout`.
205
+ - `.diagnostic: str | None` on `NonZeroExit`, `Timeout`, and `Signalled` — the
206
+ best human-facing message (captured stderr if it carries text, otherwise
207
+ captured stdout; `None` if both streams are blank), so a generic `except
208
+ ProcessError` handler can log/report something useful without knowing which
209
+ of the three stream-bearing exceptions it caught.
210
+ - `Command.timeout_signal()` / `ProcessGroup.signal()` now also accept a raw
211
+ platform signal number (an `int`), not just a portable name — the crate's
212
+ `Signal::Other` escape hatch (Unix only; a raw number is `Unsupported` on
213
+ Windows like every non-`Kill` signal, same as the named variants).
214
+ - `CliClient.command(args)` — a `Command` for `program <args>` with the
215
+ client's defaults (timeout/env/retry/cancel) pre-applied; chain more
216
+ builders for a customized one-off call, then pass the result to `run()` /
217
+ `output()` / … (which now accept either a plain arg list or such a
218
+ `Command` — the `IntoCommand` path). An explicit setting on the returned
219
+ `Command` always wins over the client's default; only the gaps get filled.
220
+ - `CliClient`'s `default_env_fn={key: resolver, ...}` — a per-key zero-arg
221
+ resolver called fresh each time a command is *built* (not each retry
222
+ attempt) to fill an environment variable, for a credential that should be
223
+ read freshly rather than baked in once at client-construction time (a
224
+ static `default_env` value). An explicit per-call `env`/`default_env` at
225
+ the same key still wins — this only fills the gap.
226
+ - `Supervisor`'s `capture_max_bytes=`/`capture_max_lines=`/
227
+ `capture_on_overflow=` — bound (or widen) the output captured from each
228
+ supervised incarnation; the default is already a sensible bounded tail
229
+ (`Command.output_limit`'s own kwargs, applied here as constructor kwargs
230
+ instead of a builder method, per the config-struct convention). Setting any
231
+ of the three requires at least one of the two cap sizes, mirroring
232
+ `output_limit`'s own validation.
233
+ - `Command.retry(retry_if, *, max_retries=, initial_backoff=, multiplier=,
234
+ max_backoff=, jitter=)` and `CliClient`'s `default_retry_if=` (+
235
+ `default_max_retries=`/`default_initial_backoff=`/`default_multiplier=`/
236
+ `default_max_backoff=`/`default_jitter=`) — retry a run with exponential
237
+ backoff, a cap, and jitter, while `retry_if` accepts the resulting error.
238
+ Honored only by the success-checking verbs (`run`/`exit_code`/`probe`, and
239
+ `CliClient`'s equivalents); ignored by `Supervisor` (its own `RestartPolicy`
240
+ governs keep-alive restarts — a different concern), `output_all`, and
241
+ `Pipeline`. Bound as kwargs over the crate's `RetryPolicy`, not a mirrored
242
+ pyclass (the established config-struct convention — see `AGENTS.md`).
243
+ `retry_if` is a named preset over the crate's own error-classification
244
+ accessors, not an arbitrary Python callable crossing the FFI boundary:
245
+ `"transient"` (a bare-retry-clears spawn/IO condition — interrupted,
246
+ would-block, a busy resource) or `"transient_or_timeout"` (also retries a
247
+ `.timeout()` expiry). `CliClient`'s tuning knobs require
248
+ `default_retry_if=` to be set (raises `ValueError` otherwise) — the same
249
+ explicit opt-in `Command.retry()`'s required `retry_if` already enforces.
250
+ - `wait_for_line(lines, predicate, *, timeout)` is generalized over the
251
+ iterator's item type (previously hardcoded to `AsyncIterator[str]`) — it now
252
+ works over any async iterator (e.g. `RunningProcess.output_events()`'s
253
+ `OutputEvent` items), not just stdout lines, given a callable predicate.
254
+ `predicate` also accepts a plain `str` as a substring-match shorthand
255
+ (`wait_for_line(lines, "listening on", timeout=10)`) when the iterator
256
+ yields `str`. Purely additive: an existing callable-predicate,
257
+ `str`-iterator call site is unaffected.
258
+ - `Invocation.env_is(name, value)` / `has_env(name)` — the platform-correct
259
+ (case-insensitive on Windows, last write wins) effective-override check. The
260
+ existing `env` dict is plain Python dict semantics, not platform env-key
261
+ rules: a same-case duplicate key collapses to its last value, but a
262
+ differently-cased Windows duplicate (`"Path"`/`"PATH"`) survives as two
263
+ separate entries — use `env_is()`/`has_env()` for the correct answer either
264
+ way.
265
+ - `runner=` keyword on `output_all` / `aoutput_all` / `output_all_bytes` /
266
+ `aoutput_all_bytes`, `Supervisor(...)`, and `CliClient(...)` — drives the
267
+ batch/supervision/client through an injected runner (`Runner`,
268
+ `ScriptedRunner`, `RecordingRunner`, or `RecordReplayRunner`) instead of the
269
+ real one, so a test double stands in with no real process spawned. Defaults
270
+ to the real `Runner` when omitted (no behavior change). `CliClient` was
271
+ previously locked to the real runner; it is now just as testable as raw
272
+ `Command` code.
273
+ - `ScriptedRunner.on_sequence(prefix, replies)` — reply with each of `replies`
274
+ in turn on successive matching calls (fail a few times, then succeed), then
275
+ repeat the last reply once exhausted. The declarative form for retry/
276
+ supervision test scenarios.
277
+ - Prebuilt wheels for **Intel macOS** (x86_64), cross-compiled from the arm64
278
+ (Apple Silicon) runner. Previously Intel Mac users installed from the sdist
279
+ (needing a Rust toolchain); both macOS architectures are now covered.
280
+ - Prebuilt wheels for **Windows on ARM (arm64)**, built natively on GitHub's
281
+ free-for-public-repos `windows-11-arm` runner. Both families ship — the abi3
282
+ GIL wheel (CPython 3.10+) and the free-threaded cp314t wheel — so ARM64
283
+ Windows users (a growing laptop segment) get a binary `pip install` instead
284
+ of a from-source build needing a Rust toolchain. No cibuildwheel override was
285
+ needed: it already provides a native ARM64 CPython 3.10 (for the abi3 wheel)
286
+ and a native ARM64 cp314t, so the existing `build`/`skip` selectors cover
287
+ win_arm64 unchanged.
288
+ - An **API reference** section on the documentation site — a complete,
289
+ per-symbol index of the public surface (every class, function, protocol, type
290
+ alias, and exception, plus the `processkit.testing` submodule), reachable from
291
+ the site navigation. It is rendered by `mkdocstrings` straight from the type
292
+ stub (`_processkit.pyi`) and docstrings via griffe's *static* analysis (no
293
+ compiled extension needed, so it builds in the extension-free Docs CI), and a
294
+ drift guard (`scripts/gen_api_reference.py --check` plus
295
+ `tests/test_api_reference.py`) fails if the page ever omits — or invents — a
296
+ public symbol, so the reference cannot silently diverge from the real API.
297
+
298
+ ### Changed
299
+ - `[project.urls] Homepage` in `pyproject.toml` now points at the project
300
+ overview site (https://zelanton.github.io/processkit/) instead of the
301
+ GitHub repository, which is still linked separately as `Repository`.
302
+
303
+ ### Fixed
304
+ - Fixed the macOS x86_64 release wheel build: `delocate-wheel` was rejecting
305
+ the cross-compiled Intel wheel because the compiled extension's embedded
306
+ minimum macOS target (10.12, the current Rust default for
307
+ `x86_64-apple-darwin`) didn't match the wheel's `macosx_10_9` tag. The
308
+ x86_64 cibuildwheel build now sets `MACOSX_DEPLOYMENT_TARGET=10.12`
309
+ explicitly so the tag matches the binary.
310
+ - `wait_for()`'s deadline handling no longer swallows the *caller's* own
311
+ cancellation (turning it into a misleading `TimeoutError`) if that cancellation
312
+ lands while the timed-out predicate is being cancelled and drained; it also no
313
+ longer cancels a pre-existing `asyncio.Future`/`Task` passed in as the
314
+ predicate's own awaitable (only a task it created itself), no longer discards a
315
+ condition that turns out true in the same tick as the deadline, and no longer
316
+ swallows a `SystemExit`/`KeyboardInterrupt` raised by the predicate.
317
+ - `wait_for_line()` no longer masks a builtin-`TimeoutError`-family exception
318
+ raised by the predicate or the stream itself behind the generic timeout
319
+ message; it now shares `wait_for()`'s bounding, so `timeout=0` reliably
320
+ evaluates once instead of sometimes short-circuiting first.
321
+ - `wait_for()`, `wait_for_line()`, and `wait_for_port()` now reject a NaN
322
+ `timeout` with `ValueError` instead of polling forever; `wait_for()` and
323
+ `wait_for_port()` reject a NaN `interval` the same way (`wait_for_line()` has
324
+ no `interval` parameter).
325
+ - `wait_for_port()` now chains the last connection attempt's exception (e.g. a
326
+ DNS failure) as the raised `TimeoutError`'s `__cause__` instead of discarding
327
+ it.
328
+ - A consuming verb called without the context it needs — an async verb
329
+ (`RunningProcess.wait`/`finish`/`output`/`output_bytes`/`profile`/`shutdown`/
330
+ `__aexit__`, `Supervisor.arun`, `ProcessGroup.ashutdown`/`__aexit__`) called
331
+ with no running `asyncio` event loop, or a sync verb (`Supervisor.run`,
332
+ `ProcessGroup.shutdown`/`__exit__`) called from inside an already-running
333
+ async context — now raises a clear error and leaves the handle intact and
334
+ reusable. Previously the same misuse destroyed the live process (or spent the
335
+ handle) as a side effect of the error path.
336
+ - `Timeout.timeout_seconds` is now `None` (not a misleading `0.0`) when the
337
+ deadline wasn't known to the checking verb (a scripted/cassette-replayed
338
+ timeout with no `timeout()` configured).
339
+ - `ProcessStdin.write()` / `write_line()` / `flush()` / `close()` now raise the
340
+ matching stdlib `OSError` subclass (e.g. `BrokenPipeError` for a closed
341
+ child), not a bare `OSError`.
342
+ - `ProcessGroup.signal()`'s docstring no longer claims Windows "emulates" the
343
+ POSIX signals — a Job Object only delivers `kill` there; every other name
344
+ raises `Unsupported`, as it always has.
345
+ - Error mapping now uses the `processkit` 1.2.0 crate's `Error` accessors
346
+ instead of hand-matching each variant, closing two gaps: a cancelled run's
347
+ exception now carries `.program` (previously missing); and a spawn/IO
348
+ failure refused for a permission reason is now consistently `PermissionDenied`
349
+ (previously only a spawn-time refusal was — e.g. an OS-refused
350
+ `ProcessGroup.signal()` used to surface as a plain `ProcessError`).
351
+ - `docs/testing.md`/`docs/cookbook.md` no longer claim an unmatched
352
+ `ScriptedRunner` call with no fallback raises `ProcessNotFound` (it raises a
353
+ plain `ProcessError` — that was always the actual behavior, the docs were
354
+ wrong) or that `CliClient` is un-injectable (see `runner=` above).
355
+
356
+ ## [1.0.0] - 2026-07-04
357
+
358
+ ### Added
359
+ - Synchronous `Command` builder over the `processkit` Rust crate (pinned at
360
+ `=1.2.0`): `output()` (captures a non-zero exit, timeout, and signal-kill as
361
+ data), `output_bytes()` (raw-bytes stdout → `BytesResult`), `run()` (returns
362
+ trimmed stdout, raises on failure), `exit_code()`, and `probe()`, configured
363
+ with `arg`/`args`/`cwd`/`env`/`envs`/`env_remove`/`env_clear`/`timeout`/
364
+ `output_limit`. The program and working directory accept any `os.PathLike`, not
365
+ only `str`.
366
+ - Full environment control on `Command`: `envs(mapping)` (set many at once),
367
+ `env_remove(key)`, and `env_clear()` (start from an empty environment) — for
368
+ reproducible or locked-down (sandboxed) children.
369
+ - Output caps on `Command`: `output_limit(max_bytes=…, max_lines=…,
370
+ on_overflow="drop_oldest"|"drop_newest"|"error")` bounds how much captured
371
+ output is retained (cap `max_bytes` to bound the parent's memory against an
372
+ untrusted child; a `max_lines`-only cap does not); on `"error"` overflow the
373
+ run raises `OutputTooLarge`.
374
+ - More `Command` knobs: `success_codes([…])` (treat the given exit codes as
375
+ success, replacing the default `{0}` — for `grep`/`diff`-style tools),
376
+ `inherit_env([…])`
377
+ (allowlist inheritance), `timeout_grace()` / `timeout_signal()` (graceful
378
+ timeout), `stdout("inherit"|"null")` / `stderr(…)` redirection, `encoding(…)` /
379
+ `stdout_encoding` / `stderr_encoding` (decode non-UTF-8 output),
380
+ `kill_on_parent_death()`, `create_no_window()` (Windows), and POSIX
381
+ `uid` / `gid` / `groups` / `setsid`.
382
+ - Concurrent batch execution: `output_all` / `aoutput_all` (and `…_bytes`
383
+ variants) run many commands with bounded `concurrency`, returning each
384
+ `ProcessResult` — or a `ProcessError` for a spawn/I/O failure — in input order.
385
+ - `CliClient(program, *, default_timeout=…, default_env=…, default_env_remove=…)`
386
+ — a typed wrapper for a tool you call repeatedly, with `run` / `output` /
387
+ `output_bytes` / `exit_code` / `probe` (+ async) taking just the per-call args.
388
+ - `enable_logging()` — opt-in observability: forwards the core's per-run events to
389
+ Python's `logging` (a `processkit` logger; DEBUG for a run, WARNING for an edge
390
+ case). Idempotent; off by default; `argv`/`env` are never logged (secrets). Use
391
+ `logging.basicConfig(level=…)` and filter the `processkit` logger as usual.
392
+ - `RunningProcess` live introspection (`elapsed_seconds`, `cpu_time_seconds`,
393
+ `peak_memory_bytes`, `stdout_line_count` / `stderr_line_count`, `owns_group`),
394
+ plus `output_bytes()` and `profile(every_seconds)` → `RunProfile`. A `RunProfile`
395
+ carries the run's full `outcome` (`code` / `signal` / `timed_out` — a superset of
396
+ `wait()`) alongside the CPU/memory samples (`cpu_time_seconds`,
397
+ `peak_memory_bytes`, `avg_cpu_cores`, `samples`).
398
+ - Synchronous `Command.start()` — a blocking twin of `astart()` returning a live
399
+ `RunningProcess` for streaming a child from synchronous code (its consuming
400
+ methods `wait` / `finish` / `output` / … remain coroutines, awaited from an
401
+ event loop).
402
+ - `RecordReplayRunner` test double — `record(path)` real runs then `save()`, and
403
+ `replay(path)` offline; plus `output_bytes` on `Runner` / `ScriptedRunner`. It
404
+ records and replays the streaming `start()` verb too (record is capture-whole;
405
+ interactive mid-stream stdin can't be cassette-recorded — script those with
406
+ `ScriptedRunner`); `output_bytes` through a cassette raises `Unsupported` (a
407
+ text fixture can't reproduce exact bytes).
408
+ - `RecordingRunner` spy test double — `RecordingRunner.replying(reply)` answers
409
+ every command with one canned `Reply` and records each call, so a test can
410
+ assert on *what* its code ran: `calls()` returns every `Invocation` (in order)
411
+ and `only_call()` the single one. Each `Invocation` exposes `program`, `args`,
412
+ `cwd`, `env`, `has_stdin`, and `has_flag(flag)`; its `repr` is redacted (program
413
+ + arg count + env names, never values). Completes the test-double set.
414
+ - `ProcessResult` with `stdout`, `stderr`, `code`, `is_success`, `timed_out`,
415
+ `signal`, `program`, `duration_seconds`, `truncated`, and `combined`; plus a
416
+ `BytesResult` (raw-bytes `stdout`, text `stderr`) from `output_bytes()` /
417
+ `aoutput_bytes()`.
418
+ - `ProcessGroup` context manager — a kill-on-drop container for a process tree;
419
+ `start()` a command into it, inspect `mechanism` / `members()`, and the whole
420
+ tree (grandchildren included) is reaped on `with`-exit or `shutdown()`.
421
+ - `RunningProcess` handle exposing the child `pid`.
422
+ - Exception hierarchy rooted at `ProcessError`: `NonZeroExit`, `Timeout`,
423
+ `Signalled`, `ProcessNotFound`, `PermissionDenied`, `Unsupported`,
424
+ `OutputTooLarge`. `Timeout` is also a builtin `TimeoutError`, `ProcessNotFound`
425
+ is also a `FileNotFoundError`, and `PermissionDenied` is also a
426
+ `PermissionError` (matching `asyncio` / `subprocess`), so the stdlib `except`
427
+ clauses catch them. The data-carrying ones expose structured fields — e.g.
428
+ `NonZeroExit.code` / `.stdout` / `.stderr` / `.program`,
429
+ `Timeout.timeout_seconds`, `Signalled.signal`, `OutputTooLarge.max_bytes` /
430
+ `.total_bytes`, `Unsupported.operation` — so a failure can be inspected
431
+ programmatically, not just read as a message. (`ResourceLimit` carries no extra
432
+ field; its reason is `str(exc)`.)
433
+ - Blocking synchronous calls are interruptible: `Ctrl+C` (SIGINT) raises
434
+ `KeyboardInterrupt` promptly and tears down the run's process tree, instead of
435
+ hanging until the child exits.
436
+ - Asyncio-native surface (tokio ↔ asyncio bridge). Cancelling an awaited run —
437
+ directly, or via `asyncio.wait_for` / `asyncio.timeout` — tears down the whole
438
+ process tree and raises `asyncio.CancelledError`.
439
+ - `Command`: `aoutput()`, `aoutput_bytes()`, `arun()`, `aexit_code()`,
440
+ `aprobe()`, and `astart()` (returns a `RunningProcess` for
441
+ streaming/interactive I/O).
442
+ - `RunningProcess`: `async for line in proc.stdout_lines()`, `output_events()`
443
+ (stdout+stderr as `OutputEvent`s), interactive `take_stdin()` →
444
+ `ProcessStdin` (`write`/`write_line`/`flush`/`close`), and `await`able
445
+ `wait()` → `Outcome`, `finish()` → `Finished`, `output()` → `ProcessResult`,
446
+ plus `kill()` / `shutdown(grace_seconds)`. It is also a context manager
447
+ (`with` / `async with`): exiting the block tears the process down
448
+ deterministically — a hard kill of the whole private tree for a standalone
449
+ `start()`/`astart()` handle — without relying on Python's GC.
450
+ - `ProcessGroup`: `async with`, `astart()`, `ashutdown()`.
451
+ - `Command` stdin configuration: `stdin_bytes()` / `stdin_text()` (feed input
452
+ upfront) and `keep_stdin_open()` (write interactively after start).
453
+ - New result types: `Outcome`, `Finished`, `OutputEvent`.
454
+ - Higher-level features:
455
+ - **Resource limits** on `ProcessGroup`: keyword-only `max_memory`,
456
+ `max_processes`, `cpu_quota`, `shutdown_grace`, `escalate_to_kill`
457
+ (enforced via the Windows Job Object or a Linux cgroup-v2 *root*).
458
+ - **Signals & observability** on `ProcessGroup`: `signal("term"|…)`,
459
+ `suspend()`, `resume()`, `kill_all()`, and `stats()` →
460
+ `ProcessGroupStats`.
461
+ - **Pipelines**: `Command | Command` (or `.pipe()`) → `Pipeline`, with the
462
+ sync/async run verbs (incl. `output_bytes()` / `aoutput_bytes()` for a binary
463
+ tail) and `timeout()`.
464
+ - **Supervision**: `Supervisor(cmd, restart=…, max_restarts=…, backoff_initial=…,
465
+ backoff_factor=…, max_backoff=…, jitter=…, stop_when=…, storm_pause=…,
466
+ failure_threshold=…, failure_decay=…)` with `run()` / `arun()` →
467
+ `SupervisionOutcome`. Setting `storm_pause` enables the failure-storm guard
468
+ (crash-loop circuit-breaker), reported via `SupervisionOutcome.storm_pauses`.
469
+ - **Readiness probes**: `await wait_for_port(host, port, *, timeout)`,
470
+ `await wait_for_line(lines, predicate, *, timeout)`, and
471
+ `await wait_for(predicate, *, timeout)` (poll any sync-or-async condition).
472
+ - New types/exception: `Pipeline`, `ProcessGroupStats`, `Supervisor`,
473
+ `SupervisionOutcome`, `ResourceLimit`.
474
+ - Testing seam: a `Runner` (real) and a `ScriptedRunner` (test double) with a
475
+ uniform sync + async (`a`-prefixed) `output`/`run`/`exit_code`/`probe`/`start`
476
+ interface, plus `Reply`
477
+ (`ok`/`fail`/`timeout`/`signalled`/`lines`/`pending`). Inject a `Runner` in
478
+ production and a `ScriptedRunner` in tests — no real processes spawned; the
479
+ results returned are genuine `ProcessResult` / `RunningProcess` objects. The
480
+ injected runner is typed by the `ProcessRunner` `typing.Protocol`, which
481
+ `Runner` / `ScriptedRunner` / `RecordReplayRunner` / `RecordingRunner` all
482
+ satisfy structurally. The test doubles (`ScriptedRunner`, `RecordReplayRunner`,
483
+ `RecordingRunner`) plus `Reply` and `Invocation` live in the **`processkit.testing`**
484
+ submodule; `Runner` and `ProcessRunner` are top-level (production).
485
+ - A full [documentation guide set](docs/README.md): a task-oriented
486
+ [cookbook](docs/cookbook.md) plus deep guides for
487
+ [running commands](docs/commands.md), [process groups](docs/process-groups.md),
488
+ [streaming & interactive I/O](docs/streaming.md), [pipelines](docs/pipelines.md),
489
+ [timeouts & cancellation](docs/timeouts-and-cancellation.md),
490
+ [supervision](docs/supervision.md), and [testing](docs/testing.md), tied
491
+ together by a progressively-disclosed README with a cover illustration.
492
+ - Type stubs (`_processkit.pyi`) for the compiled extension.
493
+ - A [platform support & caveats](docs/platforms.md) matrix documenting per-OS
494
+ teardown, resource-limit, signal, and stats behaviour.
495
+ - **Stability commitment:** as of 1.0 the public API follows SemVer — breaking
496
+ changes land only in a new major version.
497
+ - **Free-threaded CPython (PEP 703):** the extension declares `gil_used = false`,
498
+ so importing it on a free-threaded build (CPython 3.14t) does **not** re-enable
499
+ the GIL. Shipped as a version-specific free-threaded wheel alongside the
500
+ abi3 (GIL) wheel, and the full test suite runs on the free-threaded interpreter
501
+ in CI. Also adds CPython **3.14** to the supported set (the abi3 wheel already
502
+ runs there).
503
+ - **musllinux (Alpine/musl) wheels** for x86_64 and aarch64, alongside the
504
+ existing manylinux (glibc) wheels — so `pip install` gets a binary wheel on
505
+ Alpine-based images instead of building from the sdist. Both the abi3 GIL wheel
506
+ and the free-threaded cp314t wheel ship per libc. CI builds and smoke-tests the
507
+ x86_64 musllinux wheels on every push (aarch64 builds natively at release).
508
+ - Packaging metadata for the PyPI page: Trove classifiers (CPython 3.10–3.14, the
509
+ supported operating systems, topics) and project URLs (Documentation, Issues).
510
+ - Runnable [`examples/`](examples/) — self-contained, cross-platform programs, one
511
+ per target niche (whole-tree no-orphan teardown, a readiness-gated server,
512
+ supervision-until-healthy, a resource-limited sandbox). Each is exercised in CI.
513
+ - Docs: a **"Coming from subprocess"** guide that maps `subprocess` /
514
+ `asyncio.subprocess` patterns onto their processkit equivalents (verbs, flags,
515
+ pipelines, the exception mapping) and shows the whole-tree containment the stdlib
516
+ can't express.
517
+
518
+ ### Changed
519
+ - Renamed `Command.ok_codes()` → **`success_codes()`** (clearer that it is the
520
+ whole success set, not an addition), and an empty sequence now raises
521
+ `ValueError` instead of being silently ignored.
522
+ - Renamed `RunProfile.exit_code` → **`code`**, matching the exit-code field on
523
+ every other result type (`ProcessResult`, `Outcome`, …).
524
+ - `Command.encoding()` / `stdout_encoding` / `stderr_encoding` now also accept
525
+ common **Python codec aliases** (`latin_1`, `utf_8`, `euc_jp`, …) in addition to
526
+ WHATWG labels, normalized to the WHATWG form; an unmappable label raises
527
+ `ValueError` naming the WHATWG equivalent. (WHATWG `iso-8859-1` / Python
528
+ `latin_1` decode as windows-1252.)
529
+ - `Command.arg()` / `args()` and the `Command(...)` constructor's args accept any
530
+ `os.PathLike[str]` (e.g. `pathlib.Path`), not only `str`, so a `Path` argument
531
+ needs no `str()`. (`bytes` paths are not accepted; `StrPath` was narrowed to
532
+ `str | os.PathLike[str]` to match.)
533
+ - Closed-set string parameters and return values are typed as `Literal` in the
534
+ stubs (signal names, `restart`, `mechanism`, `SupervisionOutcome.stopped`,
535
+ `OutputEvent.stream`) for editor autocomplete and `mypy` typo-catching.
536
+ - Exported the `StrPath` (`str | os.PathLike[str]`) and `SignalName` (the signal-name
537
+ `Literal`) type aliases from the package, so your own wrappers can annotate against
538
+ the same types the API accepts.
539
+ - Renamed `ProcessGroup(memory_max=…)` → **`max_memory`**, so every ceiling on the
540
+ surface follows the `max_*` convention (`max_processes`, `output_limit(max_bytes=…,
541
+ max_lines=…)`, `Supervisor(max_restarts=…, max_backoff=…)`). The crate builder
542
+ remains `memory_max()`.
543
+ - Renamed `RunProfile.avg_cpu` → **`avg_cpu_cores`** (self-documenting: the value is
544
+ CPU-cores, e.g. `1.7` ≈ 1.7 cores busy).
545
+ - Renamed `RunningProcess.start_kill()` → **`kill()`**, matching
546
+ `subprocess.Popen.kill()` (fire-and-forget; does not wait for exit).
547
+ - Renamed `ProcessGroup.terminate_all()` → **`kill_all()`** and the
548
+ `ProcessGroup(shutdown_timeout=…)` ceiling → **`shutdown_grace`**, so the group's
549
+ teardown surface reads as what it does — a hard kill of the whole tree, after an
550
+ optional grace period — and lines up with `RunningProcess.kill()` and
551
+ `Command.timeout_grace()`. The crate keeps `terminate_all()` / `shutdown_timeout()`.
552
+ - Renamed the `OutputTooLarge` overflow fields `line_limit` / `byte_limit` →
553
+ **`max_lines`** / **`max_bytes`**, so the caps reported on overflow match the
554
+ `output_limit(max_bytes=…, max_lines=…)` kwargs that set them.
555
+ - Moved the runner test doubles — `ScriptedRunner`, `RecordReplayRunner`,
556
+ `RecordingRunner`, the `Reply` builder, and the `Invocation` record — into a new
557
+ **`processkit.testing`** submodule (mirroring the crate's `processkit::testing`
558
+ split), so the top-level `processkit` namespace is the production surface and the
559
+ test scaffolding is one explicit import away (`from processkit.testing import
560
+ ScriptedRunner`). `Runner` and the `ProcessRunner` protocol stay top-level.
561
+ - `ProcessResult.combined` is now a **property** (was `combined()`), matching the
562
+ other read accessors (`stdout`, `code`, …).
563
+ - Renamed `Outcome.is_success` / `Finished.is_success` → **`exited_zero`**. These
564
+ test literal "exit code 0" and — unlike `ProcessResult.is_success` — carry no
565
+ `success_codes` context, so the new name no longer implies the command's own
566
+ success verdict. Use `ProcessResult.is_success`, or test `code` against your set.
567
+ - `RunningProcess.take_stdin()` now **raises** `ProcessError` (instead of returning
568
+ `None`) when stdin was not kept open or was already taken — so a missing
569
+ `keep_stdin_open()` fails at the call, not later with an `AttributeError`. Its
570
+ return type is now `ProcessStdin` (no longer `... | None`).
571
+ - The readiness helpers `wait_for()` / `wait_for_port()` / `wait_for_line()` now
572
+ take `timeout` as a **keyword-only** argument, for uniformity.
573
+
574
+ ### Removed
575
+ - `Cancelled` exception. It was never raised from the Python surface (the binding
576
+ exposes no cancellation token; cancelling an awaited run surfaces as
577
+ `asyncio.CancelledError`), so it was pure catch-list clutter. Re-addable
578
+ (additive) if a token-style cancellation API is ever exposed.
579
+ - `CliClient.run_unit()` / `arun_unit()`. The success-only `-> None` verb existed
580
+ nowhere else on the surface; use `run()` / `arun()` and ignore the returned
581
+ stdout for the same "run, raise on failure" behavior.
582
+ - `ResourceLimit.message`. It duplicated `str(exc)` — idiomatic Python 3 exceptions
583
+ carry no separate `.message` attribute. Read the reason via `str(exc)`.
584
+
585
+ ### Fixed
586
+ - A synchronous verb called from inside a `Supervisor` `stop_when` predicate no
587
+ longer re-enters the tokio runtime and panics (the panic was previously
588
+ swallowed, so the predicate silently never fired); it now raises a clear
589
+ `ProcessError`. Documented that the predicate must read the result handed to it
590
+ rather than run new verbs.
591
+ - `Supervisor(backoff_factor=…)` is now applied (and validated) independently of
592
+ `backoff_initial` — previously the factor was silently dropped unless
593
+ `backoff_initial` was also passed.
594
+ - A `RecordReplayRunner.replay()` cassette miss now carries the `.program` field,
595
+ matching every other program-bearing `ProcessError`.
596
+ - `wait_for_port()` no longer leaks the probe socket if the awaiting task is
597
+ cancelled just after the connection is accepted.
598
+ - `wait_for()` now bounds its predicate by `timeout` — an async predicate that
599
+ hangs no longer ignores the deadline — while propagating the predicate's own
600
+ exception unchanged and cancelling the in-flight predicate (rather than orphaning
601
+ it) when the awaiting task is cancelled.
602
+
603
+ ### Security
604
+ - `repr(Command(...))` no longer renders argv (or env *values*): it now uses the
605
+ crate's redacted form — program, argument *count*, and env *names* only. A repr
606
+ is emitted everywhere (logging `%r`, f-strings, tracebacks, test diffs), so this
607
+ prevents a secret passed as an argument from leaking through any of them. (The
608
+ Python surface exposes no way to recover the full command line; argv remains
609
+ visible to the OS via `ps` / `/proc` while the child runs.)
610
+ - Documentation hardening: the sandbox/privilege-drop guidance now sets all of
611
+ `gid` / `groups` / `uid` (dropping `uid` alone leaves the child holding the
612
+ parent's supplementary groups — a sandbox-escape footgun); documents that
613
+ record/replay cassettes are written owner-only (`0600`, no symlink follow) on
614
+ Unix; and warns that exception `stdout`/`stderr` still carry raw values — pass
615
+ secrets via `env(...)`, not flags.
616
+
617
+ ### Notes
618
+
619
+ - This is the **1.0** release: the public API is frozen.
620
+ - Distributed as abi3 wheels for CPython 3.10+ (standard/GIL builds), **plus a
621
+ version-specific free-threaded wheel** for CPython 3.14t (PEP 703).
622
+ - The `RecordReplayRunner` test double enables the crate's `record` feature,
623
+ which pulls `serde` / `serde_json` into the compiled wheel.
624
+ - `enable_logging()` enables the crate's `tracing` feature; the bridge pulls
625
+ `tracing` / `tracing-subscriber` (registry only) into the compiled wheel.
626
+
627
+ [Unreleased]: https://github.com/ZelAnton/processkit-py/compare/v1.1.0...HEAD
628
+ [1.1.0]: https://github.com/ZelAnton/processkit-py/compare/v1.0.0...v1.1.0
629
+ [1.0.0]: https://github.com/ZelAnton/processkit-py/releases/tag/v1.0.0