agentic-dev-loop 1.0.0a45__tar.gz → 1.0.0a46__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.

Potentially problematic release.


This version of agentic-dev-loop might be problematic. Click here for more details.

Files changed (104) hide show
  1. {agentic_dev_loop-1.0.0a45/src/agentic_dev_loop.egg-info → agentic_dev_loop-1.0.0a46}/PKG-INFO +28 -1
  2. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/README.md +27 -0
  3. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/pyproject.toml +1 -1
  4. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/core/config.py +13 -1
  5. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/llm.py +14 -7
  6. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/stages.py +10 -2
  7. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46/src/agentic_dev_loop.egg-info}/PKG-INFO +28 -1
  8. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_config.py +34 -0
  9. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_llm.py +40 -0
  10. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_publish.py +39 -0
  11. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_trigger_script.py +49 -4
  12. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/LICENSE +0 -0
  13. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/setup.cfg +0 -0
  14. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/__init__.py +0 -0
  15. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/core/__init__.py +0 -0
  16. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/core/paths.py +0 -0
  17. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/core/results.py +0 -0
  18. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/core/verdict.py +0 -0
  19. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/__init__.py +0 -0
  20. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/actions.py +0 -0
  21. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/agent_error_guide.md +0 -0
  22. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/agent_error_guide.py +0 -0
  23. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/agents.py +0 -0
  24. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/allowlist.py +0 -0
  25. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/brief_coverage.py +0 -0
  26. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/brief_prompt.md +0 -0
  27. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/brief_prompt.py +0 -0
  28. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/cli.py +0 -0
  29. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/containers.py +0 -0
  30. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/deps.py +0 -0
  31. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/dryrun.py +0 -0
  32. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/egress_policy.py +0 -0
  33. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/egress_proxy.py +0 -0
  34. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
  35. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
  36. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/forbidden.py +0 -0
  37. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/graph.py +0 -0
  38. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/hostboundary.py +0 -0
  39. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/isolation.py +0 -0
  40. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/loaders.py +0 -0
  41. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/mounts.py +0 -0
  42. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/outputs.py +0 -0
  43. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/presets.py +0 -0
  44. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/proc.py +0 -0
  45. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/registry.py +0 -0
  46. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -0
  47. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
  48. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/secrets.py +0 -0
  49. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/step_workspace.py +0 -0
  50. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/engine/sync_back.py +0 -0
  51. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/py.typed +0 -0
  52. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/viz/__init__.py +0 -0
  53. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/viz/__main__.py +0 -0
  54. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop/viz/server.py +0 -0
  55. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop.egg-info/SOURCES.txt +0 -0
  56. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
  57. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
  58. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
  59. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
  60. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_028_declared_caches.py +0 -0
  61. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_029_merge_on_conflict.py +0 -0
  62. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_033_brief_step.py +0 -0
  63. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_035_finalize_nudge.py +0 -0
  64. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_036_banned_commands_and_lane_boundary.py +0 -0
  65. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_041_brief_coverage.py +0 -0
  66. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_046_brief_prompt.py +0 -0
  67. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_actions.py +0 -0
  68. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_agent_error_guide.py +0 -0
  69. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_agents.py +0 -0
  70. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_aidlc.py +0 -0
  71. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_cli.py +0 -0
  72. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_containers.py +0 -0
  73. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_deps.py +0 -0
  74. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_dry_run.py +0 -0
  75. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_e2e_fixtures.py +0 -0
  76. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_egress_proxy.py +0 -0
  77. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_graph.py +0 -0
  78. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_hostboundary.py +0 -0
  79. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_hostboundary_integration.py +0 -0
  80. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_isolation.py +0 -0
  81. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_isolation_integration.py +0 -0
  82. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_isolation_pbt.py +0 -0
  83. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_loaders.py +0 -0
  84. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_module_contract.py +0 -0
  85. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_outputs.py +0 -0
  86. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_outputs_pbt.py +0 -0
  87. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_proc.py +0 -0
  88. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_qa_prompt.py +0 -0
  89. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_registry.py +0 -0
  90. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_repo_ascii.py +0 -0
  91. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_results.py +0 -0
  92. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_sdk_compat.py +0 -0
  93. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_secrets.py +0 -0
  94. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_secrets_integration.py +0 -0
  95. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_secrets_pipeline.py +0 -0
  96. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_source_resolution.py +0 -0
  97. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_stages.py +0 -0
  98. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_step_workspace.py +0 -0
  99. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_sync_back.py +0 -0
  100. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_unit5_config.py +0 -0
  101. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_unit5_pbt.py +0 -0
  102. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_unit5_pure.py +0 -0
  103. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_verdict.py +0 -0
  104. {agentic_dev_loop-1.0.0a45 → agentic_dev_loop-1.0.0a46}/tests/test_viz.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-dev-loop
3
- Version: 1.0.0a45
3
+ Version: 1.0.0a46
4
4
  Summary: Turn an LLM into a gated, sandboxed, resumable construction loop over your codebase.
5
5
  Author: Luke Wilson
6
6
  License-Expression: Apache-2.0
@@ -199,6 +199,7 @@ llm:
199
199
  model: openai/deepseek-v4-pro
200
200
  base_url: https://api.deepseek.com
201
201
  api_key_env: OPENAI_API_KEY
202
+ # reasoning_effort: medium # optional: low | medium | high | xhigh | none
202
203
 
203
204
  max_attempts: 10 # CLI --max-attempts overrides
204
205
  agents_dir: sdk_agents # your <name>.agent.md files
@@ -228,6 +229,7 @@ steps:
228
229
  agent: review
229
230
  artifact: review-findings.md
230
231
  verdicts: { pass: APPROVED, fail: NEEDS CHANGES }
232
+ # reasoning_effort: xhigh # per-step override; falls back to the llm block, then SDK default
231
233
  on_pass: qa
232
234
  on_fail: { goto: dev, retry: true }
233
235
 
@@ -253,6 +255,7 @@ steps:
253
255
  | `terminal_timeout:` | Optional agent-step per-action timeout in seconds (gt 0). `SandboxTerminalExecutor` uses it when the agent leaves the terminal action timeout unset, overriding the SDK tool's default. `None` = current behavior. |
254
256
  | `finalize_nudge_turns:` | Optional agent-step final-response nudge (gt 0). Requires `max_iterations:` and must be less than it. At `max_iterations - finalize_nudge_turns` completed turns the engine injects one advisory turn ("`N turns remain: produce your final response now`"), exactly once; the agent may still exhaust its turns and existing stuck/unclear handling applies unchanged. Absent = current behavior. Provisional pending one consumer run (revert if noisy or the raised cap inflates cost). |
255
257
  | `custom_prompt:` | Planned (planning-docs/040). Optional agent-step value: literal text, or a path whose file contents are read at load time, appended verbatim to the agent prompt at context assembly (after the prompt, before the task/artifacts). A value that looks like a path but names no file is a validation error. Agent steps only. |
258
+ | `reasoning_effort:` | Optional `low` / `medium` / `high` / `xhigh` / `none`. Set on the `llm` block (pipeline-global) or per-step (overrides the block). Unset anywhere = the field is not passed and the SDK default (`high`) applies, so today's behavior is unchanged. Forwarded only for models the SDK/LiteLLM mark as supporting it; an unsupported value fails at the first LLM call. See [Reasoning effort](#reasoning-effort-opt-in). |
256
259
  | `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
257
260
  | `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` provisions the sandbox before the step runs (command or agent steps; network ON; stamp-cached, re-runs when the pipeline's lockfiles change); `network: true` lets a command/agent step reach the hosts its `egress:` allowlist names (default off) - see [Per-stage networking](#per-stage-networking). |
258
261
  | `setup_probe:` | Optional consumer-authored in-container probe run on a warm `setup:` stamp instead of the venv probe. Exit 0 trusts the stamp (skips); any nonzero re-provisions with network on (fail toward freshness). The probe text is NOT part of the stamp key, so a probe-only change needs `--refresh` or `--clean-stamps` to take effect. Example (asserting a warmed esbuild binary is Linux-shaped): `file frontend-editor/node_modules/esbuild/bin/esbuild | grep -q ELF`. |
@@ -266,6 +269,30 @@ steps:
266
269
  | `caches:` / `sync_excludes:` | Top-level run-wide lists. `caches:` names repo-relative toolchain cache dirs bind-mounted rw from the real workspace (auto-created if missing); a `setup:` hook can warm them (the provision container carries the same cache mounts); `sync_excludes:` names scratch paths skipped from both the export copy and sync-back (wildcard `*` spans separators, so `**/bin/**` matches a `/bin/` segment at any depth). Both default to `[]`. See [docs/setup.md](docs/setup.md). |
267
270
  | `merge_on_conflict:` | Top-level run-wide list of append-style logs (same path vocabulary as `sync_excludes:`, default `[]`). A concurrent host edit to a listed path is union-merged instead of raising: the host file wins and the sandbox's export-only lines are appended in order; both sides converge and the run continues. See [docs/setup.md](docs/setup.md). |
268
271
 
272
+ ### Reasoning effort (opt-in)
273
+
274
+ `reasoning_effort` is an opt-in latency dial on the pipeline `llm` block and
275
+ on individual agent steps. It takes one of `low`, `medium`, `high`, `xhigh`,
276
+ or `none`; unset means the field is never passed and the SDK default
277
+ (`high`) applies, so a pipeline that omits it behaves exactly as before. A
278
+ step's value overrides the `llm` block's value, so the review lane can hold
279
+ a higher effort than the rest.
280
+
281
+ The value is forwarded only for models the SDK/LiteLLM mark as supporting
282
+ it; an unsupported value fails at the first LLM call (the schema rejects
283
+ unknown names at load time, but cannot know provider capability). No
284
+ pipeline ships a value: adopt one only after an observed run proves lower
285
+ per-turn latency with a green suite and no provider 400s.
286
+
287
+ Recommended lane defaults (guidance only, not enforced):
288
+
289
+ | Lane | reasoning_effort | Why |
290
+ | --- | --- | --- |
291
+ | brief | medium | digest and classify, no design work |
292
+ | dev | high | hold the line: dev caught a real plan bug and a real sandbox triage this unit |
293
+ | qa | medium | classification discipline must stay reliable |
294
+ | review | xhigh | shortest lane, highest stakes: a missed detection sails past the last gate into commit |
295
+
269
296
  A `brief_inputs:` step is the per-run context-digestion pattern: it runs once at
270
297
  run start (or when its inputs change), its `produces:` artifact survives attempt
271
298
  starts, and the dev/review agents read that digest instead of re-deriving repo
@@ -155,6 +155,7 @@ llm:
155
155
  model: openai/deepseek-v4-pro
156
156
  base_url: https://api.deepseek.com
157
157
  api_key_env: OPENAI_API_KEY
158
+ # reasoning_effort: medium # optional: low | medium | high | xhigh | none
158
159
 
159
160
  max_attempts: 10 # CLI --max-attempts overrides
160
161
  agents_dir: sdk_agents # your <name>.agent.md files
@@ -184,6 +185,7 @@ steps:
184
185
  agent: review
185
186
  artifact: review-findings.md
186
187
  verdicts: { pass: APPROVED, fail: NEEDS CHANGES }
188
+ # reasoning_effort: xhigh # per-step override; falls back to the llm block, then SDK default
187
189
  on_pass: qa
188
190
  on_fail: { goto: dev, retry: true }
189
191
 
@@ -209,6 +211,7 @@ steps:
209
211
  | `terminal_timeout:` | Optional agent-step per-action timeout in seconds (gt 0). `SandboxTerminalExecutor` uses it when the agent leaves the terminal action timeout unset, overriding the SDK tool's default. `None` = current behavior. |
210
212
  | `finalize_nudge_turns:` | Optional agent-step final-response nudge (gt 0). Requires `max_iterations:` and must be less than it. At `max_iterations - finalize_nudge_turns` completed turns the engine injects one advisory turn ("`N turns remain: produce your final response now`"), exactly once; the agent may still exhaust its turns and existing stuck/unclear handling applies unchanged. Absent = current behavior. Provisional pending one consumer run (revert if noisy or the raised cap inflates cost). |
211
213
  | `custom_prompt:` | Planned (planning-docs/040). Optional agent-step value: literal text, or a path whose file contents are read at load time, appended verbatim to the agent prompt at context assembly (after the prompt, before the task/artifacts). A value that looks like a path but names no file is a validation error. Agent steps only. |
214
+ | `reasoning_effort:` | Optional `low` / `medium` / `high` / `xhigh` / `none`. Set on the `llm` block (pipeline-global) or per-step (overrides the block). Unset anywhere = the field is not passed and the SDK default (`high`) applies, so today's behavior is unchanged. Forwarded only for models the SDK/LiteLLM mark as supporting it; an unsupported value fails at the first LLM call. See [Reasoning effort](#reasoning-effort-opt-in). |
212
215
  | `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
213
216
  | `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` provisions the sandbox before the step runs (command or agent steps; network ON; stamp-cached, re-runs when the pipeline's lockfiles change); `network: true` lets a command/agent step reach the hosts its `egress:` allowlist names (default off) - see [Per-stage networking](#per-stage-networking). |
214
217
  | `setup_probe:` | Optional consumer-authored in-container probe run on a warm `setup:` stamp instead of the venv probe. Exit 0 trusts the stamp (skips); any nonzero re-provisions with network on (fail toward freshness). The probe text is NOT part of the stamp key, so a probe-only change needs `--refresh` or `--clean-stamps` to take effect. Example (asserting a warmed esbuild binary is Linux-shaped): `file frontend-editor/node_modules/esbuild/bin/esbuild | grep -q ELF`. |
@@ -222,6 +225,30 @@ steps:
222
225
  | `caches:` / `sync_excludes:` | Top-level run-wide lists. `caches:` names repo-relative toolchain cache dirs bind-mounted rw from the real workspace (auto-created if missing); a `setup:` hook can warm them (the provision container carries the same cache mounts); `sync_excludes:` names scratch paths skipped from both the export copy and sync-back (wildcard `*` spans separators, so `**/bin/**` matches a `/bin/` segment at any depth). Both default to `[]`. See [docs/setup.md](docs/setup.md). |
223
226
  | `merge_on_conflict:` | Top-level run-wide list of append-style logs (same path vocabulary as `sync_excludes:`, default `[]`). A concurrent host edit to a listed path is union-merged instead of raising: the host file wins and the sandbox's export-only lines are appended in order; both sides converge and the run continues. See [docs/setup.md](docs/setup.md). |
224
227
 
228
+ ### Reasoning effort (opt-in)
229
+
230
+ `reasoning_effort` is an opt-in latency dial on the pipeline `llm` block and
231
+ on individual agent steps. It takes one of `low`, `medium`, `high`, `xhigh`,
232
+ or `none`; unset means the field is never passed and the SDK default
233
+ (`high`) applies, so a pipeline that omits it behaves exactly as before. A
234
+ step's value overrides the `llm` block's value, so the review lane can hold
235
+ a higher effort than the rest.
236
+
237
+ The value is forwarded only for models the SDK/LiteLLM mark as supporting
238
+ it; an unsupported value fails at the first LLM call (the schema rejects
239
+ unknown names at load time, but cannot know provider capability). No
240
+ pipeline ships a value: adopt one only after an observed run proves lower
241
+ per-turn latency with a green suite and no provider 400s.
242
+
243
+ Recommended lane defaults (guidance only, not enforced):
244
+
245
+ | Lane | reasoning_effort | Why |
246
+ | --- | --- | --- |
247
+ | brief | medium | digest and classify, no design work |
248
+ | dev | high | hold the line: dev caught a real plan bug and a real sandbox triage this unit |
249
+ | qa | medium | classification discipline must stay reliable |
250
+ | review | xhigh | shortest lane, highest stakes: a missed detection sails past the last gate into commit |
251
+
225
252
  A `brief_inputs:` step is the per-run context-digestion pattern: it runs once at
226
253
  run start (or when its inputs change), its `produces:` artifact survives attempt
227
254
  starts, and the dev/review agents read that digest instead of re-deriving repo
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentic-dev-loop"
7
- version = "1.0.0a45"
7
+ version = "1.0.0a46"
8
8
  description = "Turn an LLM into a gated, sandboxed, resumable construction loop over your codebase."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -6,7 +6,7 @@ import hashlib
6
6
  import re
7
7
  import warnings
8
8
  from pathlib import Path
9
- from typing import Any
9
+ from typing import Any, Literal
10
10
 
11
11
  import yaml
12
12
  from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
@@ -35,6 +35,11 @@ DEFAULT_BRIEF_SECTIONS = [
35
35
  "Open risks",
36
36
  ]
37
37
 
38
+ # 047: the full SDK reasoning_effort literal, passed through verbatim. The
39
+ # engine is provider-generic, so it validates shape only - whether a given
40
+ # provider accepts every value (notably "none") is operator risk (D3).
41
+ ReasoningEffort = Literal["low", "medium", "high", "xhigh", "none"]
42
+
38
43
 
39
44
  def _validate_mount_path(path: str) -> str:
40
45
  """Validate one repo-relative mount path (BR-1.3).
@@ -385,6 +390,9 @@ class StepConfig(BaseModel):
385
390
  terminal_timeout: float | None = Field(default=None, gt=0) # seconds, agent steps only (032)
386
391
  finalize_nudge_turns: int | None = Field(default=None, gt=0) # agent steps only (035)
387
392
  brief_inputs: BriefInputsSpec | None = None # agent steps only (033)
393
+ # 047 (D7): per-step opt-in override; unset falls back to the llm block,
394
+ # then the SDK default. Full SDK literal, passthrough semantics.
395
+ reasoning_effort: ReasoningEffort | None = None
388
396
  custom_prompt: str | None = None # agent steps only (040); file path resolved at load
389
397
  env: dict[str, str | None] | None = None # NFR-1: null = inherit host value
390
398
  setup: str | None = None # BR-13: command steps only
@@ -552,6 +560,10 @@ class LlmConfig(BaseModel):
552
560
  base_url: str | None = None
553
561
  api_key_env: str
554
562
  timeout: float | None = None # omitted → opinionated no-timeout default
563
+ # 047 (D1): opt-in passthrough; unset means the field is not passed at
564
+ # all, so the SDK default ("high") applies and today's behavior is
565
+ # byte-identical. No default may move without an observed run (D6).
566
+ reasoning_effort: ReasoningEffort | None = None
555
567
 
556
568
 
557
569
  class PipelineConfig(BaseModel):
@@ -48,13 +48,20 @@ if not getattr(_ORIGINAL_FORMAT_MESSAGES, "_agentic_dev_loop_reasoning_repair",
48
48
  LLM.format_messages_for_llm = _repair_reasoning_passback
49
49
 
50
50
 
51
- def build_llm(cfg: LlmConfig) -> LLM:
51
+ def build_llm(cfg: LlmConfig, reasoning_effort: str | None = None) -> LLM:
52
52
  api_key = os.environ.get(cfg.api_key_env)
53
53
  if not api_key:
54
54
  raise RuntimeError(f"{cfg.api_key_env} environment variable is required")
55
- return LLM(
56
- model=cfg.model,
57
- api_key=api_key,
58
- base_url=cfg.base_url,
59
- timeout=cfg.timeout, # policy: no LLM timeout unless explicitly set
60
- )
55
+ # 047 (D2/D7): an explicit per-step override wins, else the llm block's
56
+ # value applies, else the field is omitted entirely and the SDK default
57
+ # ("high") is in force. The reasoning_content shims above stay untouched.
58
+ effort = reasoning_effort if reasoning_effort is not None else cfg.reasoning_effort
59
+ kwargs = {
60
+ "model": cfg.model,
61
+ "api_key": api_key,
62
+ "base_url": cfg.base_url,
63
+ "timeout": cfg.timeout, # policy: no LLM timeout unless explicitly set
64
+ }
65
+ if effort is not None:
66
+ kwargs["reasoning_effort"] = effort
67
+ return LLM(**kwargs)
@@ -42,6 +42,7 @@ from .egress_policy import assemble_egress_policy
42
42
  from .forbidden import resolve_forbidden_rules
43
43
  from .isolation import (SandboxError, SandboxExecutionRequest, command_argv,
44
44
  resolve_sandbox_env)
45
+ from .llm import build_llm
45
46
  from .mounts import resolve_mount_set
46
47
  from .outputs import CapturedOutput
47
48
  from .secrets import redact_values, step_secret_env
@@ -174,11 +175,18 @@ class Runtime:
174
175
  if step is not None else None),
175
176
  forbidden_commands=forbidden_commands,
176
177
  allowlist=allowlist)
178
+ # 047 (D7): a step's reasoning_effort overrides the llm block's;
179
+ # self.llm already carries the block value (or the SDK default),
180
+ # so only build a fresh LLM when the step overrides it.
181
+ llm = self.llm
182
+ if step is not None and step.reasoning_effort is not None:
183
+ llm = build_llm(self.cfg.llm,
184
+ reasoning_effort=step.reasoning_effort)
177
185
  if step is not None and step.brief_inputs is not None:
178
186
  agent = load_brief_agent(
179
- agent_name, step.brief_inputs.sections, self.llm)
187
+ agent_name, step.brief_inputs.sections, llm)
180
188
  else:
181
- agent = load_agent(self.agents_dir, agent_name, self.llm)
189
+ agent = load_agent(self.agents_dir, agent_name, llm)
182
190
  callbacks = []
183
191
  if finalize_controller is not None:
184
192
  callbacks.append(finalize_controller.on_event)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-dev-loop
3
- Version: 1.0.0a45
3
+ Version: 1.0.0a46
4
4
  Summary: Turn an LLM into a gated, sandboxed, resumable construction loop over your codebase.
5
5
  Author: Luke Wilson
6
6
  License-Expression: Apache-2.0
@@ -199,6 +199,7 @@ llm:
199
199
  model: openai/deepseek-v4-pro
200
200
  base_url: https://api.deepseek.com
201
201
  api_key_env: OPENAI_API_KEY
202
+ # reasoning_effort: medium # optional: low | medium | high | xhigh | none
202
203
 
203
204
  max_attempts: 10 # CLI --max-attempts overrides
204
205
  agents_dir: sdk_agents # your <name>.agent.md files
@@ -228,6 +229,7 @@ steps:
228
229
  agent: review
229
230
  artifact: review-findings.md
230
231
  verdicts: { pass: APPROVED, fail: NEEDS CHANGES }
232
+ # reasoning_effort: xhigh # per-step override; falls back to the llm block, then SDK default
231
233
  on_pass: qa
232
234
  on_fail: { goto: dev, retry: true }
233
235
 
@@ -253,6 +255,7 @@ steps:
253
255
  | `terminal_timeout:` | Optional agent-step per-action timeout in seconds (gt 0). `SandboxTerminalExecutor` uses it when the agent leaves the terminal action timeout unset, overriding the SDK tool's default. `None` = current behavior. |
254
256
  | `finalize_nudge_turns:` | Optional agent-step final-response nudge (gt 0). Requires `max_iterations:` and must be less than it. At `max_iterations - finalize_nudge_turns` completed turns the engine injects one advisory turn ("`N turns remain: produce your final response now`"), exactly once; the agent may still exhaust its turns and existing stuck/unclear handling applies unchanged. Absent = current behavior. Provisional pending one consumer run (revert if noisy or the raised cap inflates cost). |
255
257
  | `custom_prompt:` | Planned (planning-docs/040). Optional agent-step value: literal text, or a path whose file contents are read at load time, appended verbatim to the agent prompt at context assembly (after the prompt, before the task/artifacts). A value that looks like a path but names no file is a validation error. Agent steps only. |
258
+ | `reasoning_effort:` | Optional `low` / `medium` / `high` / `xhigh` / `none`. Set on the `llm` block (pipeline-global) or per-step (overrides the block). Unset anywhere = the field is not passed and the SDK default (`high`) applies, so today's behavior is unchanged. Forwarded only for models the SDK/LiteLLM mark as supporting it; an unsupported value fails at the first LLM call. See [Reasoning effort](#reasoning-effort-opt-in). |
256
259
  | `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
257
260
  | `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` provisions the sandbox before the step runs (command or agent steps; network ON; stamp-cached, re-runs when the pipeline's lockfiles change); `network: true` lets a command/agent step reach the hosts its `egress:` allowlist names (default off) - see [Per-stage networking](#per-stage-networking). |
258
261
  | `setup_probe:` | Optional consumer-authored in-container probe run on a warm `setup:` stamp instead of the venv probe. Exit 0 trusts the stamp (skips); any nonzero re-provisions with network on (fail toward freshness). The probe text is NOT part of the stamp key, so a probe-only change needs `--refresh` or `--clean-stamps` to take effect. Example (asserting a warmed esbuild binary is Linux-shaped): `file frontend-editor/node_modules/esbuild/bin/esbuild | grep -q ELF`. |
@@ -266,6 +269,30 @@ steps:
266
269
  | `caches:` / `sync_excludes:` | Top-level run-wide lists. `caches:` names repo-relative toolchain cache dirs bind-mounted rw from the real workspace (auto-created if missing); a `setup:` hook can warm them (the provision container carries the same cache mounts); `sync_excludes:` names scratch paths skipped from both the export copy and sync-back (wildcard `*` spans separators, so `**/bin/**` matches a `/bin/` segment at any depth). Both default to `[]`. See [docs/setup.md](docs/setup.md). |
267
270
  | `merge_on_conflict:` | Top-level run-wide list of append-style logs (same path vocabulary as `sync_excludes:`, default `[]`). A concurrent host edit to a listed path is union-merged instead of raising: the host file wins and the sandbox's export-only lines are appended in order; both sides converge and the run continues. See [docs/setup.md](docs/setup.md). |
268
271
 
272
+ ### Reasoning effort (opt-in)
273
+
274
+ `reasoning_effort` is an opt-in latency dial on the pipeline `llm` block and
275
+ on individual agent steps. It takes one of `low`, `medium`, `high`, `xhigh`,
276
+ or `none`; unset means the field is never passed and the SDK default
277
+ (`high`) applies, so a pipeline that omits it behaves exactly as before. A
278
+ step's value overrides the `llm` block's value, so the review lane can hold
279
+ a higher effort than the rest.
280
+
281
+ The value is forwarded only for models the SDK/LiteLLM mark as supporting
282
+ it; an unsupported value fails at the first LLM call (the schema rejects
283
+ unknown names at load time, but cannot know provider capability). No
284
+ pipeline ships a value: adopt one only after an observed run proves lower
285
+ per-turn latency with a green suite and no provider 400s.
286
+
287
+ Recommended lane defaults (guidance only, not enforced):
288
+
289
+ | Lane | reasoning_effort | Why |
290
+ | --- | --- | --- |
291
+ | brief | medium | digest and classify, no design work |
292
+ | dev | high | hold the line: dev caught a real plan bug and a real sandbox triage this unit |
293
+ | qa | medium | classification discipline must stay reliable |
294
+ | review | xhigh | shortest lane, highest stakes: a missed detection sails past the last gate into commit |
295
+
269
296
  A `brief_inputs:` step is the per-run context-digestion pattern: it runs once at
270
297
  run start (or when its inputs change), its `produces:` artifact survives attempt
271
298
  starts, and the dev/review agents read that digest instead of re-deriving repo
@@ -180,6 +180,40 @@ def test_extra_body_llm_key_rejected(tmp_path):
180
180
  _load(tmp_path, data)
181
181
 
182
182
 
183
+ def test_llm_reasoning_effort_round_trips(tmp_path):
184
+ data = _base()
185
+ data["llm"]["reasoning_effort"] = "medium"
186
+ assert _load(tmp_path, data).llm.reasoning_effort == "medium"
187
+
188
+
189
+ def test_llm_reasoning_effort_defaults_none(tmp_path):
190
+ assert _load(tmp_path, _base()).llm.reasoning_effort is None
191
+
192
+
193
+ def test_llm_reasoning_effort_unknown_value_rejected(tmp_path):
194
+ data = _base()
195
+ data["llm"]["reasoning_effort"] = "maximum"
196
+ with pytest.raises(ValidationError):
197
+ _load(tmp_path, data)
198
+
199
+
200
+ def test_step_reasoning_effort_round_trips(tmp_path):
201
+ data = _base()
202
+ data["steps"][0]["reasoning_effort"] = "xhigh"
203
+ assert _load(tmp_path, data).steps[0].reasoning_effort == "xhigh"
204
+
205
+
206
+ def test_step_reasoning_effort_defaults_none(tmp_path):
207
+ assert _load(tmp_path, _base()).steps[0].reasoning_effort is None
208
+
209
+
210
+ def test_step_reasoning_effort_unknown_value_rejected(tmp_path):
211
+ data = _base()
212
+ data["steps"][0]["reasoning_effort"] = "banana"
213
+ with pytest.raises(ValidationError):
214
+ _load(tmp_path, data)
215
+
216
+
183
217
  def test_unknown_route_key_rejected(tmp_path):
184
218
  data = _base()
185
219
  data["steps"][0]["on_pass"] = {"goto": "success", "bogus": 1}
@@ -43,6 +43,46 @@ class TestBuildLlm:
43
43
 
44
44
  assert mock_cls.call_args.kwargs["timeout"] == 30
45
45
 
46
+ def test_reasoning_effort_omitted_when_unset(self, monkeypatch):
47
+ monkeypatch.setenv(KEY, "sk-test")
48
+ with patch("agentic_dev_loop.engine.llm.LLM") as mock_cls:
49
+ build_llm(_cfg())
50
+
51
+ assert "reasoning_effort" not in mock_cls.call_args.kwargs
52
+
53
+ def test_reasoning_effort_passed_through_when_set(self, monkeypatch):
54
+ monkeypatch.setenv(KEY, "sk-test")
55
+ with patch("agentic_dev_loop.engine.llm.LLM") as mock_cls:
56
+ build_llm(_cfg(reasoning_effort="medium"))
57
+
58
+ assert mock_cls.call_args.kwargs["reasoning_effort"] == "medium"
59
+
60
+
61
+ class TestReasoningEffortResolution:
62
+ """D7: the step's value wins, else the llm block's value, else unset
63
+ (the SDK default "high" applies)."""
64
+
65
+ def test_step_override_wins_over_block(self, monkeypatch):
66
+ monkeypatch.setenv(KEY, "sk-test")
67
+ with patch("agentic_dev_loop.engine.llm.LLM") as mock_cls:
68
+ build_llm(_cfg(reasoning_effort="low"), reasoning_effort="xhigh")
69
+
70
+ assert mock_cls.call_args.kwargs["reasoning_effort"] == "xhigh"
71
+
72
+ def test_block_value_applies_without_step_override(self, monkeypatch):
73
+ monkeypatch.setenv(KEY, "sk-test")
74
+ with patch("agentic_dev_loop.engine.llm.LLM") as mock_cls:
75
+ build_llm(_cfg(reasoning_effort="medium"))
76
+
77
+ assert mock_cls.call_args.kwargs["reasoning_effort"] == "medium"
78
+
79
+ def test_unset_everywhere_omits_the_field(self, monkeypatch):
80
+ monkeypatch.setenv(KEY, "sk-test")
81
+ with patch("agentic_dev_loop.engine.llm.LLM") as mock_cls:
82
+ build_llm(_cfg())
83
+
84
+ assert "reasoning_effort" not in mock_cls.call_args.kwargs
85
+
46
86
 
47
87
  class TestReasoningContentShim:
48
88
  """DeepSeek thinking mode 400s on multi-turn requests unless prior
@@ -226,6 +226,45 @@ class TestChangelog:
226
226
  assert read_version(repo) == "0.2.0"
227
227
 
228
228
 
229
+ class TestConsumerAdoption:
230
+ def test_marker_without_adoption_section_fails(self, tmp_path):
231
+ repo, _ = make_repo(
232
+ tmp_path,
233
+ unreleased=(
234
+ "### Changed\n\n"
235
+ "- feat: construct plan 031-qa-environmental-verdict.md\n"
236
+ ),
237
+ )
238
+ proc = run_publish(repo, "--patch")
239
+ assert proc.returncode != 0
240
+ assert "Consumer adoption" in proc.stderr
241
+ assert read_version(repo) == "0.2.0"
242
+ assert tags(repo) == []
243
+
244
+ def test_marker_with_adoption_section_passes(self, tmp_path):
245
+ repo, _ = make_repo(
246
+ tmp_path,
247
+ unreleased=(
248
+ "### Changed\n\n"
249
+ "- feat: construct plan 031-qa-environmental-verdict.md\n\n"
250
+ "### Consumer adoption\n\n"
251
+ "- qa verdict steps must add `env: ENV` to their verdicts map\n"
252
+ ),
253
+ )
254
+ proc = run_publish(repo, "--patch")
255
+ assert proc.returncode == 0, proc.stderr
256
+ assert read_version(repo) == "0.2.1"
257
+
258
+ def test_no_marker_passes_without_section(self, tmp_path):
259
+ repo, _ = make_repo(
260
+ tmp_path,
261
+ unreleased="### Changed\n\n- feat: add kernel sandbox\n",
262
+ )
263
+ proc = run_publish(repo, "--patch")
264
+ assert proc.returncode == 0, proc.stderr
265
+ assert read_version(repo) == "0.2.1"
266
+
267
+
229
268
  class TestGitOps:
230
269
  def test_commit_tag_and_push(self, tmp_path):
231
270
  repo, bare = make_repo(tmp_path)
@@ -109,8 +109,8 @@ def test_trigger_rejects_both_flags(tmp_path):
109
109
  env=_env_with(bin_dir))
110
110
 
111
111
  assert result.returncode != 0
112
- assert ("pass exactly one of --plan <path> or --aidlc-unit <slug>, "
113
- "not both") in result.stderr
112
+ assert ("pass exactly one of --plan <path>, --aidlc-unit <slug>, or "
113
+ "--resume <thread-id>") in result.stderr
114
114
  assert "cli-ran:" not in result.stdout
115
115
 
116
116
 
@@ -120,8 +120,8 @@ def test_trigger_requires_exactly_one_flag(tmp_path):
120
120
  result = _run_script(env=_env_with(bin_dir))
121
121
 
122
122
  assert result.returncode != 0
123
- assert ("pass exactly one of --plan <path> or --aidlc-unit <slug>"
124
- in result.stderr)
123
+ assert ("pass exactly one of --plan <path>, --aidlc-unit <slug>, or "
124
+ "--resume <thread-id>") in result.stderr
125
125
  assert "cli-ran:" not in result.stdout
126
126
 
127
127
 
@@ -135,6 +135,51 @@ def test_trigger_forwards_aidlc_unit(tmp_path):
135
135
  assert "--aidlc-unit unit-3" in result.stdout
136
136
 
137
137
 
138
+ def test_trigger_forwards_resume(tmp_path):
139
+ bin_dir = _stub_env(tmp_path)
140
+
141
+ result = _run_script("--resume", "07a767a50de64529ac442759b3685a78",
142
+ env=_env_with(bin_dir))
143
+
144
+ assert result.returncode == 0, result.stderr
145
+ assert "cli-ran:" in result.stdout
146
+ assert "--resume 07a767a50de64529ac442759b3685a78" in result.stdout
147
+ assert "--plan" not in result.stdout
148
+ assert "--aidlc-unit" not in result.stdout
149
+
150
+
151
+ def test_trigger_resume_equals_form(tmp_path):
152
+ bin_dir = _stub_env(tmp_path)
153
+
154
+ result = _run_script("--resume=07a767a50de64529ac442759b3685a78",
155
+ env=_env_with(bin_dir))
156
+
157
+ assert result.returncode == 0, result.stderr
158
+ assert "--resume 07a767a50de64529ac442759b3685a78" in result.stdout
159
+
160
+
161
+ def test_trigger_resume_missing_value_is_usage_error(tmp_path):
162
+ bin_dir = _stub_env(tmp_path)
163
+
164
+ result = _run_script("--resume", env=_env_with(bin_dir))
165
+
166
+ assert result.returncode != 0
167
+ assert "--resume requires a thread id" in result.stderr
168
+ assert "cli-ran:" not in result.stdout
169
+
170
+
171
+ def test_trigger_rejects_resume_with_plan(tmp_path):
172
+ bin_dir = _stub_env(tmp_path)
173
+
174
+ result = _run_script("--resume", "abc123", "--plan", "plan.md",
175
+ env=_env_with(bin_dir))
176
+
177
+ assert result.returncode != 0
178
+ assert ("pass exactly one of --plan <path>, --aidlc-unit <slug>, or "
179
+ "--resume <thread-id>") in result.stderr
180
+ assert "cli-ran:" not in result.stdout
181
+
182
+
138
183
  def test_trigger_unit_positional_is_aidlc_shorthand(tmp_path):
139
184
  bin_dir = _stub_env(tmp_path)
140
185