coding-cli-runtime 0.3.0__tar.gz → 0.4.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 (45) hide show
  1. coding_cli_runtime-0.4.0/CHANGELOG.md +96 -0
  2. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/PKG-INFO +78 -6
  3. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/README.md +77 -5
  4. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/pyproject.toml +2 -2
  5. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/src/coding_cli_runtime/__init__.py +20 -2
  6. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/src/coding_cli_runtime/provider_contracts.py +176 -1
  7. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/src/coding_cli_runtime.egg-info/PKG-INFO +78 -6
  8. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/src/coding_cli_runtime.egg-info/SOURCES.txt +3 -1
  9. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/tests/test_stage2_tier1.py +1 -1
  10. coding_cli_runtime-0.4.0/tests/test_stage3_io_contracts.py +119 -0
  11. coding_cli_runtime-0.4.0/tests/test_stage4_helpers.py +105 -0
  12. coding_cli_runtime-0.3.0/CHANGELOG.md +0 -101
  13. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/CONTRIBUTING.md +0 -0
  14. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/LICENSE +0 -0
  15. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/MANIFEST.in +0 -0
  16. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/setup.cfg +0 -0
  17. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/src/coding_cli_runtime/auth.py +0 -0
  18. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/src/coding_cli_runtime/codex_cli.py +0 -0
  19. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/src/coding_cli_runtime/contracts.py +0 -0
  20. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/src/coding_cli_runtime/copilot_reasoning_baseline.json +0 -0
  21. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/src/coding_cli_runtime/copilot_reasoning_logs.py +0 -0
  22. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/src/coding_cli_runtime/failure_classification.py +0 -0
  23. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/src/coding_cli_runtime/headless.py +0 -0
  24. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/src/coding_cli_runtime/json_io.py +0 -0
  25. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/src/coding_cli_runtime/provider_controls.py +0 -0
  26. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/src/coding_cli_runtime/provider_specs.py +0 -0
  27. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/src/coding_cli_runtime/py.typed +0 -0
  28. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/src/coding_cli_runtime/reasoning.py +0 -0
  29. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/src/coding_cli_runtime/redaction.py +0 -0
  30. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/src/coding_cli_runtime/schema_validation.py +0 -0
  31. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/src/coding_cli_runtime/schemas/normalized_run_result.v1.json +0 -0
  32. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/src/coding_cli_runtime/schemas/reasoning_metadata.v1.json +0 -0
  33. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/src/coding_cli_runtime/session_execution.py +0 -0
  34. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/src/coding_cli_runtime/session_logs.py +0 -0
  35. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/src/coding_cli_runtime/subprocess_runner.py +0 -0
  36. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/src/coding_cli_runtime.egg-info/dependency_links.txt +0 -0
  37. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/src/coding_cli_runtime.egg-info/top_level.txt +0 -0
  38. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/tests/test_copilot_reasoning_logs.py +0 -0
  39. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/tests/test_coverage_gaps.py +0 -0
  40. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/tests/test_package_resources.py +0 -0
  41. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/tests/test_packaging.py +0 -0
  42. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/tests/test_playground_probe_smoke.py +0 -0
  43. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/tests/test_provider_catalog_resolution.py +0 -0
  44. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/tests/test_provider_contracts.py +0 -0
  45. {coding_cli_runtime-0.3.0 → coding_cli_runtime-0.4.0}/tests/test_runtime_parity.py +0 -0
@@ -0,0 +1,96 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.4.0] - 2026-04-09
10
+
11
+ ### Added
12
+ - `OutputContract`, `IoContract`, `SessionDiscoveryContract`,
13
+ `DiagnosticsContract` sub-contracts on `ProviderContract`, with data
14
+ populated for all four providers.
15
+ - `WorkspaceEnvVar` structured type with `name` + `value_source` semantics
16
+ (replaces bare env-var name strings in `IoContract.workspace_env_vars`).
17
+ - `WorkspaceEnvValueSource` — closed vocabulary (`"execution_dir"` /
18
+ `"workspace_root"`) for `WorkspaceEnvVar.value_source`.
19
+ - `resolve_workspace_env()` — turns `IoContract.workspace_env_vars` into a
20
+ concrete env overlay from an execution directory.
21
+ - `resolve_session_search_paths()` — expands `SessionDiscoveryContract`
22
+ roots into concrete host paths.
23
+ - `is_provider_installed()` — checks whether a provider CLI binary is on
24
+ PATH.
25
+ - README sections: "Query provider I/O conventions" and "Common integration
26
+ tasks" with copy-pasteable examples.
27
+ - `WorkspaceEnvVar` added to key-types table in README.
28
+
29
+ ### Changed
30
+ - Gemini `session_glob` tightened from `"*.json"` to `"*/chats/session-*.json"`
31
+ to match the real `tmp/{hash}/chats/session-*.json` layout.
32
+ - Claude `session_glob` tightened from `"*.jsonl"` to
33
+ `"*/conversation.jsonl"` to match per-project subdirectory structure.
34
+
35
+ ## [0.3.0] - 2026-04-09
36
+
37
+ ### Added
38
+ - Per-provider headless launch helpers: `build_claude_headless_core()`,
39
+ `build_codex_headless_core()`, `build_copilot_headless_core()`,
40
+ `build_gemini_headless_core()`. These emit the standard non-interactive
41
+ flags for each provider; callers append app-specific tails.
42
+ - Session log discovery section in README.
43
+ - API summary table in README.
44
+
45
+ ### Changed
46
+ - `build_codex_exec_spec()` now delegates to `build_codex_headless_core()`.
47
+ `full_auto` and `skip_git_repo_check` params preserved.
48
+ - README rewritten with task-oriented examples, `run_interactive_session`
49
+ usage, `uv add` install, and API summary.
50
+
51
+ ## [0.2.0] - 2026-04-08
52
+
53
+ ### Added
54
+ - `ProviderContract` API — structured, nested metadata for all four provider
55
+ CLIs (Claude, Codex, Gemini, Copilot). Composed of `AuthContract`,
56
+ `PathContract`, `HeadlessContract`, `PromptTransport`, `ApprovalContract`,
57
+ `SandboxContract`.
58
+ - `get_provider_contract(provider_id)` — returns structured contract for a
59
+ provider.
60
+ - `build_env_overlay(contract, api_key, base_url)` — builds provider-specific
61
+ env var overlay from contract metadata.
62
+ - `resolve_config_paths(contract, containerized)` — resolves host and container
63
+ config directory paths.
64
+ - `render_prompt(transport, prompt)` — resolves prompt delivery into argv args +
65
+ stdin text based on provider transport mode.
66
+ - `PromptPayload` dataclass for resolved prompt delivery.
67
+ - `resolve_auth()` — resolves provider auth status from environment.
68
+ - `__version__` attribute.
69
+ - `CONTRIBUTING.md` with development setup and quality checks.
70
+
71
+ ### Changed
72
+ - `run_interactive_session()` observability kwargs (`job_name`, `phase_tag`)
73
+ now have sensible defaults so callers don't need to supply them.
74
+ - `CliRunResult.command` type widened from `tuple[str, ...]` to `Sequence[str]`.
75
+ - Provider model catalogs resolved with three-tier fallback: user override
76
+ file > live CLI discovery > hardcoded fallback.
77
+
78
+ ### Fixed
79
+ - Copilot BYOK (`COPILOT_PROVIDER_API_KEY`) now discoverable via contract
80
+ but not reported as "required" in `resolve_auth()` — BYOK is opt-in.
81
+
82
+ ## [0.1.0] - 2026-04-07
83
+
84
+ ### Added
85
+ - Provider metadata and controls for Claude, Codex, Copilot, and Gemini CLIs.
86
+ - Shared request/result contracts (`CliRunRequest`, `CliRunResult`, `CliLaunchSpec`).
87
+ - Schema loading and payload validation (`load_schema`, `validate_payload`).
88
+ - Synchronous and asynchronous subprocess execution helpers.
89
+ - Interactive session execution with transcript mirroring.
90
+ - Session log discovery and parsing utilities.
91
+ - Claude reasoning policy resolution.
92
+ - Log redaction helpers.
93
+ - Copilot reasoning log parsing and classification.
94
+ - PEP 561 `py.typed` markers for both `coding_cli_runtime` and `shared_cli_runtime`.
95
+ - Packaged JSON schemas and Copilot reasoning baseline data.
96
+ - Playground knowledge base with probing guides and experiment templates.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coding-cli-runtime
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Reusable CLI runtime primitives for provider-backed automation workflows
5
5
  Author-email: LLM Eval maintainers <llm-eval-maintainers@users.noreply.github.com>
6
6
  License-Expression: MIT
@@ -152,6 +152,44 @@ else:
152
152
  Works for all four providers. Recognizes auth failures, rate limits,
153
153
  network transients, and other provider-specific error patterns.
154
154
 
155
+ ### Common integration tasks
156
+
157
+ #### Check whether a provider CLI is installed
158
+
159
+ ```python
160
+ from coding_cli_runtime import is_provider_installed
161
+
162
+ if not is_provider_installed("claude"):
163
+ raise RuntimeError("Claude Code is not available on PATH")
164
+ ```
165
+
166
+ This is intentionally minimal: it checks whether the provider binary exists on
167
+ PATH. Deeper CLI drift validation belongs in maintainer tooling, not the
168
+ runtime API.
169
+
170
+ #### Resolve workspace env vars and session search paths
171
+
172
+ ```python
173
+ from coding_cli_runtime import (
174
+ get_provider_contract,
175
+ resolve_session_search_paths,
176
+ resolve_workspace_env,
177
+ )
178
+
179
+ gemini = get_provider_contract("gemini")
180
+
181
+ # Derive provider-specific workspace env vars from contract metadata
182
+ env = resolve_workspace_env(gemini, "/tmp/run-dir")
183
+ # {"GEMINI_CLI_IDE_WORKSPACE_PATH": "/tmp/run-dir"}
184
+
185
+ # Expand concrete host paths for session log searches
186
+ paths = resolve_session_search_paths(gemini)
187
+ # (Path.home() / ".gemini" / "tmp",)
188
+ ```
189
+
190
+ Use these helpers when you want the contract facts turned into concrete
191
+ filesystem/env values without rebuilding the same glue logic in each consumer.
192
+
155
193
  ### Look up provider contract metadata
156
194
 
157
195
  ```python
@@ -179,11 +217,43 @@ payload = render_prompt(contract.headless.prompt, "Fix the bug")
179
217
  ```
180
218
 
181
219
  `ProviderContract` is structured as nested sub-contracts
182
- (`AuthContract`, `PathContract`, `HeadlessContract`) so consumers
220
+ (`AuthContract`, `PathContract`, `HeadlessContract`, `OutputContract`,
221
+ `IoContract`, `SessionDiscoveryContract`, `DiagnosticsContract`) so consumers
183
222
  can drill into whichever aspect they need. This is reference metadata,
184
- not a command-construction control plane — consumers keep their own
223
+ not a command-construction control plane — callers keep their own
185
224
  command assembly and adopt contract fields selectively.
186
225
 
226
+ ### Query provider I/O conventions
227
+
228
+ ```python
229
+ from coding_cli_runtime import get_provider_contract
230
+
231
+ gemini = get_provider_contract("gemini")
232
+
233
+ # Workspace env vars with value semantics
234
+ for wev in gemini.io.workspace_env_vars:
235
+ print(f"{wev.name} = {wev.value_source}")
236
+ # GEMINI_CLI_IDE_WORKSPACE_PATH = execution_dir
237
+
238
+ # Session discovery (where session logs live)
239
+ sd = gemini.session_discovery
240
+ print(sd.session_roots) # ("tmp",)
241
+ print(sd.session_glob) # "*/chats/session-*.json"
242
+
243
+ # Output format support
244
+ codex = get_provider_contract("codex")
245
+ print(codex.output.output_path_flag) # "-o"
246
+ print(codex.output.schema_path_flag) # "--output-schema"
247
+
248
+ # Diagnostics (Copilot only)
249
+ copilot = get_provider_contract("copilot")
250
+ if copilot.diagnostics:
251
+ print(copilot.diagnostics.log_glob) # "logs/process-*.log"
252
+ ```
253
+
254
+ `WorkspaceEnvVar.value_source` uses a closed vocabulary:
255
+ `"execution_dir"` or `"workspace_root"`.
256
+
187
257
  ### Build headless launch commands
188
258
 
189
259
  ```python
@@ -224,7 +294,8 @@ files matching the working directory and time window.
224
294
  | `CliRunResult` | Result: returncode, stdout/stderr, duration, error code |
225
295
  | `ErrorCode` | `none` · `spawn_failed` · `timed_out` · `non_zero_exit` |
226
296
  | `ProviderSpec` | Provider catalog entry with models, controls, defaults |
227
- | `ProviderContract` | Structured provider CLI metadata (auth, paths, headless launch) |
297
+ | `ProviderContract` | Structured provider CLI metadata (auth, paths, headless, I/O, sessions) |
298
+ | `WorkspaceEnvVar` | Env var with value-source semantics (`execution_dir`, `workspace_root`) |
228
299
  | `FailureClassification` | Classified error with retryable flag and category |
229
300
 
230
301
  ### Run long-lived CLI sessions
@@ -249,7 +320,7 @@ result = await run_interactive_session(
249
320
  ```
250
321
 
251
322
  Only `cmd_parts`, `cwd`, `stdin_text`, and `logger` are required.
252
- Observability labels (`job_name`, `phase_tag`) default to sensible values.
323
+ Other parameters have sensible defaults.
253
324
 
254
325
  ## API summary
255
326
 
@@ -260,10 +331,11 @@ Key function groups:
260
331
  |-------|-----------|
261
332
  | Execution | `run_cli_command`, `run_cli_command_sync`, `run_interactive_session` |
262
333
  | Provider metadata | `get_provider_contract`, `get_provider_spec`, `list_provider_specs` |
263
- | Contract helpers | `build_env_overlay`, `resolve_config_paths`, `render_prompt`, `resolve_auth` |
334
+ | Contract helpers | `build_env_overlay`, `resolve_config_paths`, `render_prompt`, `resolve_auth`, `resolve_workspace_env`, `resolve_session_search_paths` |
264
335
  | Headless launch | `build_claude_headless_core`, `build_codex_headless_core`, `build_copilot_headless_core`, `build_gemini_headless_core` |
265
336
  | Codex batch | `build_codex_exec_spec` |
266
337
  | Failure handling | `classify_provider_failure` |
338
+ | Installation check | `is_provider_installed` |
267
339
  | Session logs | `find_codex_session`, `find_claude_session` |
268
340
  | Schema | `load_schema`, `validate_payload` |
269
341
  | Utilities | `redact_text`, `build_model_id`, `normalize_path_str` |
@@ -126,6 +126,44 @@ else:
126
126
  Works for all four providers. Recognizes auth failures, rate limits,
127
127
  network transients, and other provider-specific error patterns.
128
128
 
129
+ ### Common integration tasks
130
+
131
+ #### Check whether a provider CLI is installed
132
+
133
+ ```python
134
+ from coding_cli_runtime import is_provider_installed
135
+
136
+ if not is_provider_installed("claude"):
137
+ raise RuntimeError("Claude Code is not available on PATH")
138
+ ```
139
+
140
+ This is intentionally minimal: it checks whether the provider binary exists on
141
+ PATH. Deeper CLI drift validation belongs in maintainer tooling, not the
142
+ runtime API.
143
+
144
+ #### Resolve workspace env vars and session search paths
145
+
146
+ ```python
147
+ from coding_cli_runtime import (
148
+ get_provider_contract,
149
+ resolve_session_search_paths,
150
+ resolve_workspace_env,
151
+ )
152
+
153
+ gemini = get_provider_contract("gemini")
154
+
155
+ # Derive provider-specific workspace env vars from contract metadata
156
+ env = resolve_workspace_env(gemini, "/tmp/run-dir")
157
+ # {"GEMINI_CLI_IDE_WORKSPACE_PATH": "/tmp/run-dir"}
158
+
159
+ # Expand concrete host paths for session log searches
160
+ paths = resolve_session_search_paths(gemini)
161
+ # (Path.home() / ".gemini" / "tmp",)
162
+ ```
163
+
164
+ Use these helpers when you want the contract facts turned into concrete
165
+ filesystem/env values without rebuilding the same glue logic in each consumer.
166
+
129
167
  ### Look up provider contract metadata
130
168
 
131
169
  ```python
@@ -153,11 +191,43 @@ payload = render_prompt(contract.headless.prompt, "Fix the bug")
153
191
  ```
154
192
 
155
193
  `ProviderContract` is structured as nested sub-contracts
156
- (`AuthContract`, `PathContract`, `HeadlessContract`) so consumers
194
+ (`AuthContract`, `PathContract`, `HeadlessContract`, `OutputContract`,
195
+ `IoContract`, `SessionDiscoveryContract`, `DiagnosticsContract`) so consumers
157
196
  can drill into whichever aspect they need. This is reference metadata,
158
- not a command-construction control plane — consumers keep their own
197
+ not a command-construction control plane — callers keep their own
159
198
  command assembly and adopt contract fields selectively.
160
199
 
200
+ ### Query provider I/O conventions
201
+
202
+ ```python
203
+ from coding_cli_runtime import get_provider_contract
204
+
205
+ gemini = get_provider_contract("gemini")
206
+
207
+ # Workspace env vars with value semantics
208
+ for wev in gemini.io.workspace_env_vars:
209
+ print(f"{wev.name} = {wev.value_source}")
210
+ # GEMINI_CLI_IDE_WORKSPACE_PATH = execution_dir
211
+
212
+ # Session discovery (where session logs live)
213
+ sd = gemini.session_discovery
214
+ print(sd.session_roots) # ("tmp",)
215
+ print(sd.session_glob) # "*/chats/session-*.json"
216
+
217
+ # Output format support
218
+ codex = get_provider_contract("codex")
219
+ print(codex.output.output_path_flag) # "-o"
220
+ print(codex.output.schema_path_flag) # "--output-schema"
221
+
222
+ # Diagnostics (Copilot only)
223
+ copilot = get_provider_contract("copilot")
224
+ if copilot.diagnostics:
225
+ print(copilot.diagnostics.log_glob) # "logs/process-*.log"
226
+ ```
227
+
228
+ `WorkspaceEnvVar.value_source` uses a closed vocabulary:
229
+ `"execution_dir"` or `"workspace_root"`.
230
+
161
231
  ### Build headless launch commands
162
232
 
163
233
  ```python
@@ -198,7 +268,8 @@ files matching the working directory and time window.
198
268
  | `CliRunResult` | Result: returncode, stdout/stderr, duration, error code |
199
269
  | `ErrorCode` | `none` · `spawn_failed` · `timed_out` · `non_zero_exit` |
200
270
  | `ProviderSpec` | Provider catalog entry with models, controls, defaults |
201
- | `ProviderContract` | Structured provider CLI metadata (auth, paths, headless launch) |
271
+ | `ProviderContract` | Structured provider CLI metadata (auth, paths, headless, I/O, sessions) |
272
+ | `WorkspaceEnvVar` | Env var with value-source semantics (`execution_dir`, `workspace_root`) |
202
273
  | `FailureClassification` | Classified error with retryable flag and category |
203
274
 
204
275
  ### Run long-lived CLI sessions
@@ -223,7 +294,7 @@ result = await run_interactive_session(
223
294
  ```
224
295
 
225
296
  Only `cmd_parts`, `cwd`, `stdin_text`, and `logger` are required.
226
- Observability labels (`job_name`, `phase_tag`) default to sensible values.
297
+ Other parameters have sensible defaults.
227
298
 
228
299
  ## API summary
229
300
 
@@ -234,10 +305,11 @@ Key function groups:
234
305
  |-------|-----------|
235
306
  | Execution | `run_cli_command`, `run_cli_command_sync`, `run_interactive_session` |
236
307
  | Provider metadata | `get_provider_contract`, `get_provider_spec`, `list_provider_specs` |
237
- | Contract helpers | `build_env_overlay`, `resolve_config_paths`, `render_prompt`, `resolve_auth` |
308
+ | Contract helpers | `build_env_overlay`, `resolve_config_paths`, `render_prompt`, `resolve_auth`, `resolve_workspace_env`, `resolve_session_search_paths` |
238
309
  | Headless launch | `build_claude_headless_core`, `build_codex_headless_core`, `build_copilot_headless_core`, `build_gemini_headless_core` |
239
310
  | Codex batch | `build_codex_exec_spec` |
240
311
  | Failure handling | `classify_provider_failure` |
312
+ | Installation check | `is_provider_installed` |
241
313
  | Session logs | `find_codex_session`, `find_claude_session` |
242
314
  | Schema | `load_schema`, `validate_payload` |
243
315
  | Utilities | `redact_text`, `build_model_id`, `normalize_path_str` |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "coding-cli-runtime"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "Reusable CLI runtime primitives for provider-backed automation workflows"
9
9
  readme = {file = "README.md", content-type = "text/markdown"}
10
10
  license = "MIT"
@@ -94,7 +94,7 @@ disallow_untyped_defs = false
94
94
  warn_return_any = false
95
95
 
96
96
  [tool.bumpversion]
97
- current_version = "0.3.0"
97
+ current_version = "0.4.0"
98
98
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
99
99
  serialize = ["{major}.{minor}.{patch}"]
100
100
  commit = true
@@ -2,8 +2,6 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.3.0"
6
-
7
5
  from .auth import AuthResolution, resolve_auth
8
6
  from .codex_cli import CodexExecSpec, build_codex_exec_spec
9
7
  from .contracts import (
@@ -24,16 +22,25 @@ from .headless import (
24
22
  from .provider_contracts import (
25
23
  ApprovalContract,
26
24
  AuthContract,
25
+ DiagnosticsContract,
27
26
  HeadlessContract,
27
+ IoContract,
28
+ OutputContract,
28
29
  PathContract,
29
30
  PromptPayload,
30
31
  PromptTransport,
31
32
  ProviderContract,
32
33
  SandboxContract,
34
+ SessionDiscoveryContract,
35
+ WorkspaceEnvValueSource,
36
+ WorkspaceEnvVar,
33
37
  build_env_overlay,
34
38
  get_provider_contract,
39
+ is_provider_installed,
35
40
  render_prompt,
36
41
  resolve_config_paths,
42
+ resolve_session_search_paths,
43
+ resolve_workspace_env,
37
44
  )
38
45
  from .provider_controls import build_model_id, resolve_provider_model_controls
39
46
  from .provider_specs import (
@@ -75,6 +82,8 @@ from .session_logs import (
75
82
  )
76
83
  from .subprocess_runner import run_cli_command, run_cli_command_sync
77
84
 
85
+ __version__ = "0.4.0"
86
+
78
87
  __all__ = [
79
88
  "ApprovalContract",
80
89
  "AuthContract",
@@ -87,10 +96,13 @@ __all__ = [
87
96
  "ClaudeReasoningPolicy",
88
97
  "CliLaunchSpec",
89
98
  "ControlSpec",
99
+ "DiagnosticsContract",
90
100
  "ErrorCode",
91
101
  "FailureClassification",
92
102
  "HeadlessContract",
103
+ "IoContract",
93
104
  "ModelSpec",
105
+ "OutputContract",
94
106
  "PathContract",
95
107
  "PromptPayload",
96
108
  "PromptTransport",
@@ -98,6 +110,9 @@ __all__ = [
98
110
  "ProviderSpec",
99
111
  "SandboxContract",
100
112
  "SchemaValidationError",
113
+ "SessionDiscoveryContract",
114
+ "WorkspaceEnvVar",
115
+ "WorkspaceEnvValueSource",
101
116
  "InteractiveCliRunResult",
102
117
  "SessionProgressEvent",
103
118
  "SessionRetryDecision",
@@ -121,6 +136,7 @@ __all__ = [
121
136
  "get_gemini_model_options",
122
137
  "get_provider_contract",
123
138
  "get_provider_spec",
139
+ "is_provider_installed",
124
140
  "list_provider_specs",
125
141
  "build_model_id",
126
142
  "classify_provider_failure",
@@ -130,6 +146,8 @@ __all__ = [
130
146
  "resolve_claude_reasoning_policy",
131
147
  "resolve_config_paths",
132
148
  "resolve_provider_model_controls",
149
+ "resolve_session_search_paths",
150
+ "resolve_workspace_env",
133
151
  "redact_text",
134
152
  "claude_project_key",
135
153
  "find_claude_session",
@@ -5,7 +5,8 @@ config paths, and headless launch conventions. It exposes frozen dataclasses
5
5
  that consumers can read selectively — no obligation to use the full structure.
6
6
 
7
7
  Public stable API:
8
- get_provider_contract, build_env_overlay, resolve_config_paths, render_prompt
8
+ get_provider_contract, build_env_overlay, resolve_config_paths, render_prompt,
9
+ resolve_workspace_env, resolve_session_search_paths, is_provider_installed
9
10
 
10
11
  Internal (not exported from __init__):
11
12
  _build_non_interactive_run
@@ -13,8 +14,10 @@ Internal (not exported from __init__):
13
14
 
14
15
  from __future__ import annotations
15
16
 
17
+ import shutil
16
18
  from dataclasses import dataclass
17
19
  from pathlib import Path
20
+ from typing import Literal, TypeAlias
18
21
 
19
22
  from .contracts import AuthMode
20
23
 
@@ -105,6 +108,51 @@ class HeadlessContract:
105
108
  default_stream_mode: str | None
106
109
 
107
110
 
111
+ @dataclass(frozen=True)
112
+ class OutputContract:
113
+ """How the CLI delivers structured output."""
114
+
115
+ format_flag: str | None
116
+ supported_formats: tuple[str, ...]
117
+ default_format: str | None
118
+ output_path_flag: str | None
119
+ schema_path_flag: str | None
120
+
121
+
122
+ WorkspaceEnvValueSource: TypeAlias = Literal["execution_dir", "workspace_root"]
123
+
124
+
125
+ @dataclass(frozen=True)
126
+ class WorkspaceEnvVar:
127
+ """An environment variable expected by the provider CLI."""
128
+
129
+ name: str
130
+ value_source: WorkspaceEnvValueSource
131
+
132
+
133
+ @dataclass(frozen=True)
134
+ class IoContract:
135
+ """Provider-specific I/O conventions beyond prompt transport."""
136
+
137
+ file_reference_prefix: str | None
138
+ workspace_env_vars: tuple[WorkspaceEnvVar, ...]
139
+
140
+
141
+ @dataclass(frozen=True)
142
+ class SessionDiscoveryContract:
143
+ """Where session logs live and how to find them."""
144
+
145
+ session_roots: tuple[str, ...]
146
+ session_glob: str
147
+
148
+
149
+ @dataclass(frozen=True)
150
+ class DiagnosticsContract:
151
+ """Where provider diagnostic logs live."""
152
+
153
+ log_glob: str
154
+
155
+
108
156
  @dataclass(frozen=True)
109
157
  class ProviderContract:
110
158
  """Structured metadata about a provider CLI.
@@ -118,6 +166,10 @@ class ProviderContract:
118
166
  auth: AuthContract
119
167
  paths: PathContract
120
168
  headless: HeadlessContract
169
+ output: OutputContract
170
+ io: IoContract
171
+ session_discovery: SessionDiscoveryContract | None
172
+ diagnostics: DiagnosticsContract | None
121
173
  notes: tuple[str, ...]
122
174
 
123
175
 
@@ -178,6 +230,22 @@ _CLAUDE_CONTRACT = ProviderContract(
178
230
  stream_modes=None,
179
231
  default_stream_mode=None,
180
232
  ),
233
+ output=OutputContract(
234
+ format_flag="--output-format",
235
+ supported_formats=("text", "json", "stream-json"),
236
+ default_format="text",
237
+ output_path_flag=None,
238
+ schema_path_flag=None,
239
+ ),
240
+ io=IoContract(
241
+ file_reference_prefix=None,
242
+ workspace_env_vars=(),
243
+ ),
244
+ session_discovery=SessionDiscoveryContract(
245
+ session_roots=("projects",),
246
+ session_glob="*/conversation.jsonl",
247
+ ),
248
+ diagnostics=None,
181
249
  notes=(),
182
250
  )
183
251
 
@@ -217,6 +285,22 @@ _CODEX_CONTRACT = ProviderContract(
217
285
  stream_modes=None,
218
286
  default_stream_mode=None,
219
287
  ),
288
+ output=OutputContract(
289
+ format_flag=None,
290
+ supported_formats=("json",),
291
+ default_format="json",
292
+ output_path_flag="-o",
293
+ schema_path_flag="--output-schema",
294
+ ),
295
+ io=IoContract(
296
+ file_reference_prefix=None,
297
+ workspace_env_vars=(),
298
+ ),
299
+ session_discovery=SessionDiscoveryContract(
300
+ session_roots=("sessions", "archived_sessions"),
301
+ session_glob="*.jsonl",
302
+ ),
303
+ diagnostics=None,
220
304
  notes=(
221
305
  "codex exec defaults to a read-only sandbox in non-interactive mode; "
222
306
  "use --sandbox danger-full-access for write access.",
@@ -258,9 +342,32 @@ _GEMINI_CONTRACT = ProviderContract(
258
342
  stream_modes=None,
259
343
  default_stream_mode=None,
260
344
  ),
345
+ output=OutputContract(
346
+ format_flag=None,
347
+ supported_formats=(),
348
+ default_format=None,
349
+ output_path_flag=None,
350
+ schema_path_flag=None,
351
+ ),
352
+ io=IoContract(
353
+ file_reference_prefix="@",
354
+ workspace_env_vars=(
355
+ WorkspaceEnvVar(
356
+ name="GEMINI_CLI_IDE_WORKSPACE_PATH",
357
+ value_source="execution_dir",
358
+ ),
359
+ ),
360
+ ),
361
+ session_discovery=SessionDiscoveryContract(
362
+ session_roots=("tmp",),
363
+ session_glob="*/chats/session-*.json",
364
+ ),
365
+ diagnostics=None,
261
366
  notes=(
262
367
  'Gemini requires --prompt "" to activate headless mode; '
263
368
  "the real prompt is delivered on stdin.",
369
+ "Gemini output format is prompt-directed, not CLI-flag-driven.",
370
+ "File references in prompts use @filename syntax.",
264
371
  ),
265
372
  )
266
373
 
@@ -295,6 +402,24 @@ _COPILOT_CONTRACT = ProviderContract(
295
402
  stream_modes=("on", "off"),
296
403
  default_stream_mode="on",
297
404
  ),
405
+ output=OutputContract(
406
+ format_flag=None,
407
+ supported_formats=("markdown",),
408
+ default_format="markdown",
409
+ output_path_flag="--share",
410
+ schema_path_flag=None,
411
+ ),
412
+ io=IoContract(
413
+ file_reference_prefix=None,
414
+ workspace_env_vars=(),
415
+ ),
416
+ session_discovery=SessionDiscoveryContract(
417
+ session_roots=("session-state",),
418
+ session_glob="*/events.jsonl",
419
+ ),
420
+ diagnostics=DiagnosticsContract(
421
+ log_glob="logs/process-*.log",
422
+ ),
298
423
  notes=(
299
424
  "Copilot default auth is CLI login (api_key_env_var is None). "
300
425
  "BYOK is available via COPILOT_PROVIDER_API_KEY.",
@@ -350,6 +475,33 @@ def build_env_overlay(
350
475
  return overlay
351
476
 
352
477
 
478
+ def resolve_workspace_env(
479
+ contract: ProviderContract,
480
+ execution_dir: str | Path,
481
+ *,
482
+ workspace_root: str | Path | None = None,
483
+ ) -> dict[str, str]:
484
+ """Resolve provider workspace env vars from contract metadata."""
485
+ resolved: dict[str, str] = {}
486
+ execution_dir_str = str(Path(execution_dir).expanduser())
487
+ workspace_root_str = None
488
+ if workspace_root is not None:
489
+ workspace_root_str = str(Path(workspace_root).expanduser())
490
+
491
+ for item in contract.io.workspace_env_vars:
492
+ if item.value_source == "execution_dir":
493
+ resolved[item.name] = execution_dir_str
494
+ continue
495
+ if item.value_source == "workspace_root":
496
+ if workspace_root_str is None:
497
+ raise ValueError(f"{item.name} requires workspace_root, but none was provided")
498
+ resolved[item.name] = workspace_root_str
499
+ continue
500
+ raise ValueError(f"Unknown workspace env value source: {item.value_source!r}")
501
+
502
+ return resolved
503
+
504
+
353
505
  def resolve_config_paths(
354
506
  contract: ProviderContract,
355
507
  *,
@@ -366,6 +518,23 @@ def resolve_config_paths(
366
518
  return host, host
367
519
 
368
520
 
521
+ def resolve_session_search_paths(
522
+ contract: ProviderContract,
523
+ *,
524
+ config_dir: str | Path | None = None,
525
+ ) -> tuple[Path, ...]:
526
+ """Expand contract session roots into concrete host paths."""
527
+ discovery = contract.session_discovery
528
+ if discovery is None:
529
+ return ()
530
+ base_dir = (
531
+ Path(config_dir).expanduser()
532
+ if config_dir is not None
533
+ else Path(contract.paths.config_dir).expanduser()
534
+ )
535
+ return tuple(base_dir / root for root in discovery.session_roots)
536
+
537
+
369
538
  def render_prompt(
370
539
  transport: PromptTransport,
371
540
  prompt: str,
@@ -387,6 +556,12 @@ def render_prompt(
387
556
  raise ValueError(f"Unknown prompt delivery mode: {transport.delivery!r}")
388
557
 
389
558
 
559
+ def is_provider_installed(provider_id: str) -> bool:
560
+ """Return whether the provider CLI binary is available on PATH."""
561
+ contract = get_provider_contract(provider_id)
562
+ return shutil.which(contract.binary) is not None
563
+
564
+
390
565
  # ---------------------------------------------------------------------------
391
566
  # Private builder (internal convenience, not public API)
392
567
  # ---------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coding-cli-runtime
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Reusable CLI runtime primitives for provider-backed automation workflows
5
5
  Author-email: LLM Eval maintainers <llm-eval-maintainers@users.noreply.github.com>
6
6
  License-Expression: MIT
@@ -152,6 +152,44 @@ else:
152
152
  Works for all four providers. Recognizes auth failures, rate limits,
153
153
  network transients, and other provider-specific error patterns.
154
154
 
155
+ ### Common integration tasks
156
+
157
+ #### Check whether a provider CLI is installed
158
+
159
+ ```python
160
+ from coding_cli_runtime import is_provider_installed
161
+
162
+ if not is_provider_installed("claude"):
163
+ raise RuntimeError("Claude Code is not available on PATH")
164
+ ```
165
+
166
+ This is intentionally minimal: it checks whether the provider binary exists on
167
+ PATH. Deeper CLI drift validation belongs in maintainer tooling, not the
168
+ runtime API.
169
+
170
+ #### Resolve workspace env vars and session search paths
171
+
172
+ ```python
173
+ from coding_cli_runtime import (
174
+ get_provider_contract,
175
+ resolve_session_search_paths,
176
+ resolve_workspace_env,
177
+ )
178
+
179
+ gemini = get_provider_contract("gemini")
180
+
181
+ # Derive provider-specific workspace env vars from contract metadata
182
+ env = resolve_workspace_env(gemini, "/tmp/run-dir")
183
+ # {"GEMINI_CLI_IDE_WORKSPACE_PATH": "/tmp/run-dir"}
184
+
185
+ # Expand concrete host paths for session log searches
186
+ paths = resolve_session_search_paths(gemini)
187
+ # (Path.home() / ".gemini" / "tmp",)
188
+ ```
189
+
190
+ Use these helpers when you want the contract facts turned into concrete
191
+ filesystem/env values without rebuilding the same glue logic in each consumer.
192
+
155
193
  ### Look up provider contract metadata
156
194
 
157
195
  ```python
@@ -179,11 +217,43 @@ payload = render_prompt(contract.headless.prompt, "Fix the bug")
179
217
  ```
180
218
 
181
219
  `ProviderContract` is structured as nested sub-contracts
182
- (`AuthContract`, `PathContract`, `HeadlessContract`) so consumers
220
+ (`AuthContract`, `PathContract`, `HeadlessContract`, `OutputContract`,
221
+ `IoContract`, `SessionDiscoveryContract`, `DiagnosticsContract`) so consumers
183
222
  can drill into whichever aspect they need. This is reference metadata,
184
- not a command-construction control plane — consumers keep their own
223
+ not a command-construction control plane — callers keep their own
185
224
  command assembly and adopt contract fields selectively.
186
225
 
226
+ ### Query provider I/O conventions
227
+
228
+ ```python
229
+ from coding_cli_runtime import get_provider_contract
230
+
231
+ gemini = get_provider_contract("gemini")
232
+
233
+ # Workspace env vars with value semantics
234
+ for wev in gemini.io.workspace_env_vars:
235
+ print(f"{wev.name} = {wev.value_source}")
236
+ # GEMINI_CLI_IDE_WORKSPACE_PATH = execution_dir
237
+
238
+ # Session discovery (where session logs live)
239
+ sd = gemini.session_discovery
240
+ print(sd.session_roots) # ("tmp",)
241
+ print(sd.session_glob) # "*/chats/session-*.json"
242
+
243
+ # Output format support
244
+ codex = get_provider_contract("codex")
245
+ print(codex.output.output_path_flag) # "-o"
246
+ print(codex.output.schema_path_flag) # "--output-schema"
247
+
248
+ # Diagnostics (Copilot only)
249
+ copilot = get_provider_contract("copilot")
250
+ if copilot.diagnostics:
251
+ print(copilot.diagnostics.log_glob) # "logs/process-*.log"
252
+ ```
253
+
254
+ `WorkspaceEnvVar.value_source` uses a closed vocabulary:
255
+ `"execution_dir"` or `"workspace_root"`.
256
+
187
257
  ### Build headless launch commands
188
258
 
189
259
  ```python
@@ -224,7 +294,8 @@ files matching the working directory and time window.
224
294
  | `CliRunResult` | Result: returncode, stdout/stderr, duration, error code |
225
295
  | `ErrorCode` | `none` · `spawn_failed` · `timed_out` · `non_zero_exit` |
226
296
  | `ProviderSpec` | Provider catalog entry with models, controls, defaults |
227
- | `ProviderContract` | Structured provider CLI metadata (auth, paths, headless launch) |
297
+ | `ProviderContract` | Structured provider CLI metadata (auth, paths, headless, I/O, sessions) |
298
+ | `WorkspaceEnvVar` | Env var with value-source semantics (`execution_dir`, `workspace_root`) |
228
299
  | `FailureClassification` | Classified error with retryable flag and category |
229
300
 
230
301
  ### Run long-lived CLI sessions
@@ -249,7 +320,7 @@ result = await run_interactive_session(
249
320
  ```
250
321
 
251
322
  Only `cmd_parts`, `cwd`, `stdin_text`, and `logger` are required.
252
- Observability labels (`job_name`, `phase_tag`) default to sensible values.
323
+ Other parameters have sensible defaults.
253
324
 
254
325
  ## API summary
255
326
 
@@ -260,10 +331,11 @@ Key function groups:
260
331
  |-------|-----------|
261
332
  | Execution | `run_cli_command`, `run_cli_command_sync`, `run_interactive_session` |
262
333
  | Provider metadata | `get_provider_contract`, `get_provider_spec`, `list_provider_specs` |
263
- | Contract helpers | `build_env_overlay`, `resolve_config_paths`, `render_prompt`, `resolve_auth` |
334
+ | Contract helpers | `build_env_overlay`, `resolve_config_paths`, `render_prompt`, `resolve_auth`, `resolve_workspace_env`, `resolve_session_search_paths` |
264
335
  | Headless launch | `build_claude_headless_core`, `build_codex_headless_core`, `build_copilot_headless_core`, `build_gemini_headless_core` |
265
336
  | Codex batch | `build_codex_exec_spec` |
266
337
  | Failure handling | `classify_provider_failure` |
338
+ | Installation check | `is_provider_installed` |
267
339
  | Session logs | `find_codex_session`, `find_claude_session` |
268
340
  | Schema | `load_schema`, `validate_payload` |
269
341
  | Utilities | `redact_text`, `build_model_id`, `normalize_path_str` |
@@ -37,4 +37,6 @@ tests/test_playground_probe_smoke.py
37
37
  tests/test_provider_catalog_resolution.py
38
38
  tests/test_provider_contracts.py
39
39
  tests/test_runtime_parity.py
40
- tests/test_stage2_tier1.py
40
+ tests/test_stage2_tier1.py
41
+ tests/test_stage3_io_contracts.py
42
+ tests/test_stage4_helpers.py
@@ -1,4 +1,4 @@
1
- """Tests for Stage 2 Tier 1 extractions: headless cores, scan_session_dir."""
1
+ """Tests for headless cores and scan_session_dir."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -0,0 +1,119 @@
1
+ """Tests for provider I/O contract types."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import get_args, get_type_hints
6
+
7
+ from coding_cli_runtime.provider_contracts import (
8
+ WorkspaceEnvValueSource,
9
+ WorkspaceEnvVar,
10
+ get_provider_contract,
11
+ )
12
+
13
+ ALL_PROVIDERS = ("claude", "codex", "gemini", "copilot")
14
+
15
+
16
+ # ── OutputContract ────────────────────────────────────────────────────
17
+
18
+
19
+ class TestOutputContract:
20
+ def test_claude_has_format_flag(self) -> None:
21
+ c = get_provider_contract("claude")
22
+ assert c.output.format_flag == "--output-format"
23
+ assert "text" in c.output.supported_formats
24
+ assert "json" in c.output.supported_formats
25
+ assert "stream-json" in c.output.supported_formats
26
+ assert c.output.default_format == "text"
27
+
28
+ def test_codex_has_output_path_flag(self) -> None:
29
+ c = get_provider_contract("codex")
30
+ assert c.output.output_path_flag == "-o"
31
+ assert c.output.schema_path_flag == "--output-schema"
32
+ assert c.output.format_flag is None
33
+
34
+ def test_gemini_has_no_output_flags(self) -> None:
35
+ c = get_provider_contract("gemini")
36
+ assert c.output.format_flag is None
37
+ assert c.output.output_path_flag is None
38
+ assert len(c.output.supported_formats) == 0
39
+
40
+ def test_copilot_has_share_flag(self) -> None:
41
+ c = get_provider_contract("copilot")
42
+ assert c.output.output_path_flag == "--share"
43
+ assert c.output.format_flag is None
44
+ assert "markdown" in c.output.supported_formats
45
+
46
+
47
+ # ── IoContract ────────────────────────────────────────────────────────
48
+
49
+
50
+ class TestIoContract:
51
+ def test_workspace_env_value_source_is_closed_vocabulary(self) -> None:
52
+ hints = get_type_hints(WorkspaceEnvVar)
53
+ assert hints["value_source"] == WorkspaceEnvValueSource
54
+ assert get_args(WorkspaceEnvValueSource) == ("execution_dir", "workspace_root")
55
+
56
+ def test_gemini_file_reference_prefix(self) -> None:
57
+ c = get_provider_contract("gemini")
58
+ assert c.io.file_reference_prefix == "@"
59
+
60
+ def test_other_providers_no_file_reference(self) -> None:
61
+ for pid in ("claude", "codex", "copilot"):
62
+ c = get_provider_contract(pid)
63
+ assert c.io.file_reference_prefix is None
64
+
65
+ def test_gemini_workspace_env_var(self) -> None:
66
+ c = get_provider_contract("gemini")
67
+ assert len(c.io.workspace_env_vars) == 1
68
+ wev = c.io.workspace_env_vars[0]
69
+ assert wev.name == "GEMINI_CLI_IDE_WORKSPACE_PATH"
70
+ assert wev.value_source == "execution_dir"
71
+
72
+ def test_other_providers_no_workspace_env_vars(self) -> None:
73
+ for pid in ("claude", "codex", "copilot"):
74
+ c = get_provider_contract(pid)
75
+ assert c.io.workspace_env_vars == ()
76
+
77
+
78
+ # ── SessionDiscoveryContract ──────────────────────────────────────────
79
+
80
+
81
+ class TestSessionDiscoveryContract:
82
+ def test_all_providers_have_session_discovery(self) -> None:
83
+ for pid in ALL_PROVIDERS:
84
+ c = get_provider_contract(pid)
85
+ assert c.session_discovery is not None
86
+
87
+ def test_codex_session_roots(self) -> None:
88
+ c = get_provider_contract("codex")
89
+ assert "sessions" in c.session_discovery.session_roots
90
+ assert "archived_sessions" in c.session_discovery.session_roots
91
+
92
+ def test_claude_session_glob(self) -> None:
93
+ c = get_provider_contract("claude")
94
+ assert c.session_discovery.session_glob == "*/conversation.jsonl"
95
+
96
+ def test_copilot_session_discovery(self) -> None:
97
+ c = get_provider_contract("copilot")
98
+ assert "session-state" in c.session_discovery.session_roots
99
+ assert "events.jsonl" in c.session_discovery.session_glob
100
+
101
+ def test_gemini_session_discovery(self) -> None:
102
+ c = get_provider_contract("gemini")
103
+ assert "tmp" in c.session_discovery.session_roots
104
+ assert c.session_discovery.session_glob == "*/chats/session-*.json"
105
+
106
+
107
+ # ── DiagnosticsContract ───────────────────────────────────────────────
108
+
109
+
110
+ class TestDiagnosticsContract:
111
+ def test_copilot_has_diagnostics(self) -> None:
112
+ c = get_provider_contract("copilot")
113
+ assert c.diagnostics is not None
114
+ assert "process-*.log" in c.diagnostics.log_glob
115
+
116
+ def test_other_providers_no_diagnostics(self) -> None:
117
+ for pid in ("claude", "codex", "gemini"):
118
+ c = get_provider_contract(pid)
119
+ assert c.diagnostics is None
@@ -0,0 +1,105 @@
1
+ """Tests for consumer-UX helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import replace
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+ from coding_cli_runtime import (
11
+ IoContract,
12
+ WorkspaceEnvVar,
13
+ get_provider_contract,
14
+ is_provider_installed,
15
+ resolve_session_search_paths,
16
+ resolve_workspace_env,
17
+ )
18
+ from coding_cli_runtime import provider_contracts as provider_contracts_mod
19
+
20
+
21
+ class TestResolveWorkspaceEnv:
22
+ def test_gemini_workspace_env_uses_execution_dir(self) -> None:
23
+ contract = get_provider_contract("gemini")
24
+
25
+ env = resolve_workspace_env(contract, "/tmp/run-dir")
26
+
27
+ assert env == {"GEMINI_CLI_IDE_WORKSPACE_PATH": "/tmp/run-dir"}
28
+
29
+ def test_provider_with_no_workspace_env_vars_returns_empty_dict(self) -> None:
30
+ contract = get_provider_contract("claude")
31
+
32
+ env = resolve_workspace_env(contract, "/tmp/run-dir")
33
+
34
+ assert env == {}
35
+
36
+ def test_workspace_root_value_source_requires_workspace_root(self) -> None:
37
+ base = get_provider_contract("claude")
38
+ contract = replace(
39
+ base,
40
+ io=IoContract(
41
+ file_reference_prefix=None,
42
+ workspace_env_vars=(
43
+ WorkspaceEnvVar(
44
+ name="TEST_WORKSPACE_ROOT",
45
+ value_source="workspace_root",
46
+ ),
47
+ ),
48
+ ),
49
+ )
50
+
51
+ with pytest.raises(ValueError, match="requires workspace_root"):
52
+ resolve_workspace_env(contract, "/tmp/execution-dir")
53
+
54
+ env = resolve_workspace_env(
55
+ contract,
56
+ "/tmp/execution-dir",
57
+ workspace_root="/tmp/workspace-root",
58
+ )
59
+ assert env == {"TEST_WORKSPACE_ROOT": "/tmp/workspace-root"}
60
+
61
+
62
+ class TestResolveSessionSearchPaths:
63
+ def test_gemini_defaults_to_config_dir_tmp_root(self) -> None:
64
+ contract = get_provider_contract("gemini")
65
+
66
+ paths = resolve_session_search_paths(contract)
67
+
68
+ assert paths == (Path("~/.gemini").expanduser() / "tmp",)
69
+
70
+ def test_defaults_to_contract_config_dir(self) -> None:
71
+ contract = get_provider_contract("claude")
72
+
73
+ paths = resolve_session_search_paths(contract)
74
+
75
+ assert paths == (Path("~/.claude").expanduser() / "projects",)
76
+
77
+ def test_honors_config_dir_override(self) -> None:
78
+ contract = get_provider_contract("codex")
79
+
80
+ paths = resolve_session_search_paths(contract, config_dir="/tmp/codex-config")
81
+
82
+ assert paths == (
83
+ Path("/tmp/codex-config") / "sessions",
84
+ Path("/tmp/codex-config") / "archived_sessions",
85
+ )
86
+
87
+ def test_returns_empty_tuple_when_session_discovery_is_none(self) -> None:
88
+ base = get_provider_contract("copilot")
89
+ contract = replace(base, session_discovery=None)
90
+
91
+ paths = resolve_session_search_paths(contract)
92
+
93
+ assert paths == ()
94
+
95
+
96
+ class TestIsProviderInstalled:
97
+ def test_returns_true_when_binary_is_found(self, monkeypatch: pytest.MonkeyPatch) -> None:
98
+ monkeypatch.setattr(provider_contracts_mod.shutil, "which", lambda _: "/usr/bin/claude")
99
+
100
+ assert is_provider_installed("claude") is True
101
+
102
+ def test_returns_false_when_binary_is_missing(self, monkeypatch: pytest.MonkeyPatch) -> None:
103
+ monkeypatch.setattr(provider_contracts_mod.shutil, "which", lambda _: None)
104
+
105
+ assert is_provider_installed("copilot") is False
@@ -1,101 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to this project will be documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/).
6
-
7
- ## [Unreleased]
8
-
9
- ## [0.3.0] - 2026-04-09
10
-
11
- ### Added
12
- - **Headless launch core helpers** — per-provider arg renderers derived from
13
- `ProviderContract.headless`: `build_claude_headless_core()`,
14
- `build_codex_headless_core()`, `build_copilot_headless_core()`,
15
- `build_gemini_headless_core()`. All consumers (app-generation, feather,
16
- codex_cli, provider_contracts builder) now delegate to these.
17
- - `scan_session_dir()` — generic directory-scanning primitive for session log
18
- discovery with `extract_fn` callback (internal, not in public `__all__`).
19
- - Session log discovery section in README.
20
- - API summary table in README.
21
- - 27 new Stage 2 tests for headless cores, builder delegation, and
22
- `scan_session_dir`.
23
-
24
- ### Changed
25
- - `build_codex_exec_spec()` now delegates to `build_codex_headless_core()`.
26
- `full_auto` and `skip_git_repo_check` params preserved and passed through.
27
- - `_build_non_interactive_run()` now delegates to per-provider headless core
28
- helpers instead of assembling flags inline.
29
- - Feather `report_data.py` and `report_sections.py` use headless core helpers
30
- with fallback for environments without `coding_cli_runtime`.
31
- - Feather `generate_report.py` Codex session discovery replaced with
32
- `find_codex_session()` from `coding_cli_runtime`.
33
- - App-generation `claude_impl.py`, `copilot_impl.py`, `gemini_impl.py`
34
- `build_command()` functions delegate to headless core helpers.
35
- - Dead headless opt-out flags removed from Copilot (`--allow-all`, `--ask-user`,
36
- `--use-custom-instructions`) and Gemini (`--auto-approve`) CLI specs —
37
- these were never used in batch runs and are now handled by the headless core.
38
- - README rewritten: user-action feature list, `run_interactive_session` example,
39
- `uv add` install, API summary, Contributing link, session log discovery.
40
-
41
- ## [0.2.0] - 2026-04-08
42
-
43
- ### Added
44
- - **ProviderContract API** — structured, nested metadata for all four provider CLIs
45
- (Claude, Codex, Gemini, Copilot). Composed of `AuthContract`, `PathContract`,
46
- `HeadlessContract`, `PromptTransport`, `ApprovalContract`, `SandboxContract`.
47
- - `get_provider_contract(provider_id)` — returns structured contract for a provider.
48
- - `build_env_overlay(contract, api_key, base_url)` — builds provider-specific env
49
- var overlay from contract metadata.
50
- - `resolve_config_paths(contract, containerized)` — resolves host and container
51
- config directory paths.
52
- - `render_prompt(transport, prompt)` — resolves prompt delivery into argv args +
53
- stdin text based on provider transport mode.
54
- - `PromptPayload` dataclass for resolved prompt delivery.
55
- - `__version__` attribute in `coding_cli_runtime`.
56
- - `CONTRIBUTING.md`, `MANIFEST.in`, `.pre-commit-config.yaml`.
57
- - PyPI / Python / Build / License badges in `README.md`.
58
- - `bump-my-version` configuration syncing `pyproject.toml` and `__init__.py`.
59
- - `ruff`, `mypy` (strict), and `pytest-cov` added to dev dependencies.
60
- - CI quality gates: ruff check, ruff format, mypy, pytest-cov.
61
- - README section documenting the new ProviderContract API with examples.
62
- - 75 new tests for provider contracts, helpers, internal builder, failure
63
- classification, codex_cli, schema validation (including nested), reasoning,
64
- redaction, json_io, provider_controls, and auth. Package coverage 47% → 62%.
65
-
66
- ### Changed
67
- - Consolidated `shared_cli_runtime` into `coding_cli_runtime`. The package now
68
- ships a single top-level package; the `shared_cli_runtime` directory is removed.
69
- - `MANIFEST.in` and docs updated to reference `coding_cli_runtime` paths.
70
- - `run_interactive_session()` observability kwargs (`provider_label`, `job_name`,
71
- `phase_tag`, `process_label`, `timeout_seconds`) now have sensible defaults so
72
- external callers don't need to supply internal batch-system labels.
73
- - Provider model catalogs are now resolved with a three-tier fallback:
74
- user override file > live CLI discovery > hardcoded fallback.
75
- - `auth.py`: `_PROVIDER_ENV_HINTS` now derived from `provider_contracts.py`
76
- (single source of truth for auth env var names).
77
- - `CliRunResult.command` type widened from `tuple[str, ...]` to `Sequence[str]`.
78
- - Publish workflow path corrected (`shared-cli-runtime` → `coding-cli-runtime`).
79
-
80
- ### Fixed
81
- - mypy strict compliance: return-type annotations, per-module overrides.
82
- - ruff lint and format compliance across all source and test files.
83
- - Copilot BYOK (`COPILOT_PROVIDER_API_KEY`) now discoverable via contract
84
- but not reported as "required" in `resolve_auth()` — BYOK is opt-in.
85
-
86
- ## [0.1.0] - 2026-04-07
87
-
88
- ### Added
89
- - Initial extraction from `llm-eval` monorepo.
90
- - Provider metadata and controls for Claude, Codex, Copilot, and Gemini CLIs.
91
- - Shared request/result contracts (`CliRunRequest`, `CliRunResult`, `CliLaunchSpec`).
92
- - Schema loading and payload validation (`load_schema`, `validate_payload`).
93
- - Synchronous and asynchronous subprocess execution helpers.
94
- - Interactive session execution with transcript mirroring.
95
- - Session log discovery and parsing utilities.
96
- - Claude reasoning policy resolution.
97
- - Log redaction helpers.
98
- - Copilot reasoning log parsing and classification.
99
- - PEP 561 `py.typed` markers for both `coding_cli_runtime` and `shared_cli_runtime`.
100
- - Packaged JSON schemas and Copilot reasoning baseline data.
101
- - Playground knowledge base with probing guides and experiment templates.