agentic-dev-loop 1.0.0a26__tar.gz → 1.0.0a28__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 (89) hide show
  1. {agentic_dev_loop-1.0.0a26/src/agentic_dev_loop.egg-info → agentic_dev_loop-1.0.0a28}/PKG-INFO +21 -6
  2. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/README.md +20 -5
  3. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/pyproject.toml +1 -1
  4. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/core/config.py +2 -2
  5. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/engine/isolation.py +13 -5
  6. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/engine/proc.py +7 -35
  7. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/engine/stages.py +1 -1
  8. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28/src/agentic_dev_loop.egg-info}/PKG-INFO +21 -6
  9. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_actions.py +2 -3
  10. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_config.py +12 -8
  11. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_isolation.py +40 -0
  12. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_proc.py +7 -41
  13. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_stages.py +2 -2
  14. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_sync_back.py +3 -3
  15. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_trigger_script.py +4 -21
  16. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/LICENSE +0 -0
  17. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/setup.cfg +0 -0
  18. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/__init__.py +0 -0
  19. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/core/__init__.py +0 -0
  20. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/core/paths.py +0 -0
  21. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/core/results.py +0 -0
  22. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/core/verdict.py +0 -0
  23. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/engine/__init__.py +0 -0
  24. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/engine/actions.py +0 -0
  25. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/engine/agents.py +0 -0
  26. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/engine/cli.py +0 -0
  27. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/engine/deps.py +0 -0
  28. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/engine/dryrun.py +0 -0
  29. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/engine/egress_policy.py +0 -0
  30. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/engine/egress_proxy.py +0 -0
  31. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
  32. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
  33. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/engine/graph.py +0 -0
  34. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/engine/hostboundary.py +0 -0
  35. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/engine/llm.py +0 -0
  36. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/engine/loaders.py +0 -0
  37. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/engine/mounts.py +0 -0
  38. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/engine/outputs.py +0 -0
  39. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/engine/presets.py +0 -0
  40. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/engine/registry.py +0 -0
  41. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -0
  42. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
  43. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/engine/secrets.py +0 -0
  44. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/engine/step_workspace.py +0 -0
  45. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/engine/sync_back.py +0 -0
  46. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/py.typed +0 -0
  47. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/viz/__init__.py +0 -0
  48. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/viz/__main__.py +0 -0
  49. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop/viz/server.py +0 -0
  50. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop.egg-info/SOURCES.txt +0 -0
  51. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
  52. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
  53. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
  54. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
  55. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_028_declared_caches.py +0 -0
  56. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_029_merge_on_conflict.py +0 -0
  57. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_agents.py +0 -0
  58. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_aidlc.py +0 -0
  59. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_cli.py +0 -0
  60. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_deps.py +0 -0
  61. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_dry_run.py +0 -0
  62. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_e2e_fixtures.py +0 -0
  63. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_egress_proxy.py +0 -0
  64. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_graph.py +0 -0
  65. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_hostboundary.py +0 -0
  66. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_hostboundary_integration.py +0 -0
  67. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_isolation_integration.py +0 -0
  68. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_isolation_pbt.py +0 -0
  69. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_llm.py +0 -0
  70. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_loaders.py +0 -0
  71. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_module_contract.py +0 -0
  72. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_outputs.py +0 -0
  73. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_outputs_pbt.py +0 -0
  74. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_publish.py +0 -0
  75. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_qa_prompt.py +0 -0
  76. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_registry.py +0 -0
  77. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_repo_ascii.py +0 -0
  78. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_results.py +0 -0
  79. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_sdk_compat.py +0 -0
  80. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_secrets.py +0 -0
  81. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_secrets_integration.py +0 -0
  82. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_secrets_pipeline.py +0 -0
  83. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_source_resolution.py +0 -0
  84. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_step_workspace.py +0 -0
  85. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_unit5_config.py +0 -0
  86. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_unit5_pbt.py +0 -0
  87. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_unit5_pure.py +0 -0
  88. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/tests/test_verdict.py +0 -0
  89. {agentic_dev_loop-1.0.0a26 → agentic_dev_loop-1.0.0a28}/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.0a26
3
+ Version: 1.0.0a28
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
@@ -84,6 +84,20 @@ timeout (agent runs are long), and fail fast on a broken sandbox - daemon-side
84
84
  exec failures are classified and stop the run without an agent retry loop,
85
85
  while a startup reaper reclaims containers leaked by a crashed run.
86
86
 
87
+ ## Supported platforms
88
+
89
+ The engine is plain Linux under the hood and runs wherever a compliant
90
+ Docker engine does:
91
+
92
+ | Platform | Support |
93
+ | --- | --- |
94
+ | **Linux** | Native. Docker Engine, OrbStack, Colima, or podman with docker compat. |
95
+ | **macOS** | Native. Docker Desktop, OrbStack, or Colima. |
96
+ | **Windows** | Via **WSL2** (Ubuntu distro). Enable Docker Desktop's WSL integration (or run Docker Engine inside the distro) and keep the repo on the WSL filesystem (`~/dev/...`), not `/mnt/c` - bind-mounting a 9p path into containers breaks permissions and is slow. |
97
+
98
+ Native Windows (outside WSL2) is not supported: every Windows-specific code
99
+ path and test was removed, and CI runs on Ubuntu only.
100
+
87
101
  ## Installation
88
102
 
89
103
  **Prerequisites**
@@ -91,7 +105,7 @@ while a startup reaper reclaims containers leaked by a crashed run.
91
105
  | Requirement | Detail |
92
106
  | --- | --- |
93
107
  | Python | ≥ 3.12 |
94
- | Docker engine | Engine API ≥ 1.43 (Engine 24). Any compliant engine works - Docker Desktop, OrbStack, Colima, Rancher Desktop, podman docker-compat. On Windows, Linux-containers mode is required. |
108
+ | Docker engine | Engine API ≥ 1.43 (Engine 24). Any compliant engine works - Docker Desktop, OrbStack, Colima, Rancher Desktop, podman docker-compat. On Windows, run under WSL2: the repo must live on the WSL filesystem, not /mnt/c. |
95
109
  | LLM API key | The env var named by your pipeline's `api_key_env` (e.g. `OPENAI_API_KEY`) must be set - a missing key fails fast, naming it. |
96
110
 
97
111
  ```bash
@@ -214,7 +228,7 @@ steps:
214
228
  | `agent:` / `command:` / `action:` | Exactly one per step. Agents live in `agents_dir` as `<name>.agent.md`; commands run sandboxed (mechanical, never an LLM call). `commit` is the sole host-side built-in action; consumer actions are sandbox-executed, command-shaped scripts (see [Host boundary](#host-boundary-security-model)). |
215
229
  | `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
216
230
  | `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
217
- | `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` provisions the sandbox (command steps, network ON, stamp-cached); `network: true` lets a command/agent step reach the hosts its `egress:` allowlist names (default off) - see [Per-stage networking](#per-stage-networking). |
231
+ | `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). |
218
232
  | `mounts:` / `egress:` | Per-step workspace isolation. `mounts:` lists `{path, mode}` entries (`rw`/`ro`, default `ro`) and is **strict-by-default** - empty/missing mounts nothing; only declared-rw paths sync back to the repo. `egress:` is `{class, presets, targets}` (`class: browser` = broad-but-logged; otherwise a strict allowlist). See [Per-stage networking](#per-stage-networking) and [docs/setup.md](docs/setup.md). |
219
233
  | `secrets:` (top-level) / step `secrets:` | Declares secret **refs only - never values**; each step opts in to the refs it needs. Resolved host-side and injected into that step's exec env only. See [Secrets](#secrets--refs-only-never-values). |
220
234
  | `isolation:` (optional) | Configures the sandbox - image override, resource caps, `allow_git_mutations`, `seccomp_profile`. It can never disable it. See [browser sandbox](docs/browser-sandbox.md). |
@@ -222,7 +236,7 @@ steps:
222
236
  | `extensions_dir` | Where consumer `extensions:` modules resolve - defaults to `~/.agentic-dev-loop/extensions`, **outside** the workspace. Workspace-resident extension/tool modules are refused at load. |
223
237
  | `deps_policy` | `locked` (default) or `open`. `locked` = the human-owned lockfile is the source of truth and the agent may not add dependencies; `open` = the bootstrap protocol (the agent may add exact-pinned manifest entries). See [Dependency policy](#dependency-policy-locked-by-default). |
224
238
  | `lockfiles` | The pipeline's declared lockfile names (workspace-relative), injected into agent context and named by the missing-dependency fail-fast. Required (non-empty) when `deps_policy: locked` - a `locked` pipeline with no lockfiles fails fast at load. |
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); `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). |
239
+ | `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). |
226
240
  | `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). |
227
241
 
228
242
  ### Secrets (`secrets:`) - refs only, never values
@@ -418,7 +432,8 @@ Adaptation checklist for a new repo:
418
432
  only if a stage runs Chromium.
419
433
  3. `caches:` - toolchain cache dirs as rw bind-mounts shared across runs
420
434
  (`.nuget-packages`, `qa/node_modules`, `qa/.browsers`,
421
- `frontend-editor/node_modules`); caches are never synced back.
435
+ `frontend-editor/node_modules`); caches are never synced back, and a
436
+ step's `setup:` hook runs with them mounted so it can warm them.
422
437
  4. `sync_excludes:` - build output trees (`**/bin/**`, `**/obj/**`).
423
438
  5. `secrets:` - refs only, resolved from host env; each step opts in to the
424
439
  refs it needs.
@@ -579,7 +594,7 @@ agentic-dev-loop agentic-dev-loop/pipeline.yaml --dry-run # run the shipped pi
579
594
  - The shipped pipeline (`agentic-dev-loop/pipeline.yaml`) **dogfoods the tool on
580
595
  its own repo** - dev → test → review → qa → commit, with the engine's own
581
596
  three agent definitions in `sdk_agents/`.
582
- - CI: `ci.yml` (unit + integration suite, ubuntu + windows),
597
+ - CI: `ci.yml` (unit + integration suite, ubuntu),
583
598
  `publish-image.yml` (multi-arch dev image, amd64 + arm64), `release.yml`
584
599
  (PyPI publishing).
585
600
 
@@ -42,6 +42,20 @@ timeout (agent runs are long), and fail fast on a broken sandbox - daemon-side
42
42
  exec failures are classified and stop the run without an agent retry loop,
43
43
  while a startup reaper reclaims containers leaked by a crashed run.
44
44
 
45
+ ## Supported platforms
46
+
47
+ The engine is plain Linux under the hood and runs wherever a compliant
48
+ Docker engine does:
49
+
50
+ | Platform | Support |
51
+ | --- | --- |
52
+ | **Linux** | Native. Docker Engine, OrbStack, Colima, or podman with docker compat. |
53
+ | **macOS** | Native. Docker Desktop, OrbStack, or Colima. |
54
+ | **Windows** | Via **WSL2** (Ubuntu distro). Enable Docker Desktop's WSL integration (or run Docker Engine inside the distro) and keep the repo on the WSL filesystem (`~/dev/...`), not `/mnt/c` - bind-mounting a 9p path into containers breaks permissions and is slow. |
55
+
56
+ Native Windows (outside WSL2) is not supported: every Windows-specific code
57
+ path and test was removed, and CI runs on Ubuntu only.
58
+
45
59
  ## Installation
46
60
 
47
61
  **Prerequisites**
@@ -49,7 +63,7 @@ while a startup reaper reclaims containers leaked by a crashed run.
49
63
  | Requirement | Detail |
50
64
  | --- | --- |
51
65
  | Python | ≥ 3.12 |
52
- | Docker engine | Engine API ≥ 1.43 (Engine 24). Any compliant engine works - Docker Desktop, OrbStack, Colima, Rancher Desktop, podman docker-compat. On Windows, Linux-containers mode is required. |
66
+ | Docker engine | Engine API ≥ 1.43 (Engine 24). Any compliant engine works - Docker Desktop, OrbStack, Colima, Rancher Desktop, podman docker-compat. On Windows, run under WSL2: the repo must live on the WSL filesystem, not /mnt/c. |
53
67
  | LLM API key | The env var named by your pipeline's `api_key_env` (e.g. `OPENAI_API_KEY`) must be set - a missing key fails fast, naming it. |
54
68
 
55
69
  ```bash
@@ -172,7 +186,7 @@ steps:
172
186
  | `agent:` / `command:` / `action:` | Exactly one per step. Agents live in `agents_dir` as `<name>.agent.md`; commands run sandboxed (mechanical, never an LLM call). `commit` is the sole host-side built-in action; consumer actions are sandbox-executed, command-shaped scripts (see [Host boundary](#host-boundary-security-model)). |
173
187
  | `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
174
188
  | `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
175
- | `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` provisions the sandbox (command steps, network ON, stamp-cached); `network: true` lets a command/agent step reach the hosts its `egress:` allowlist names (default off) - see [Per-stage networking](#per-stage-networking). |
189
+ | `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). |
176
190
  | `mounts:` / `egress:` | Per-step workspace isolation. `mounts:` lists `{path, mode}` entries (`rw`/`ro`, default `ro`) and is **strict-by-default** - empty/missing mounts nothing; only declared-rw paths sync back to the repo. `egress:` is `{class, presets, targets}` (`class: browser` = broad-but-logged; otherwise a strict allowlist). See [Per-stage networking](#per-stage-networking) and [docs/setup.md](docs/setup.md). |
177
191
  | `secrets:` (top-level) / step `secrets:` | Declares secret **refs only - never values**; each step opts in to the refs it needs. Resolved host-side and injected into that step's exec env only. See [Secrets](#secrets--refs-only-never-values). |
178
192
  | `isolation:` (optional) | Configures the sandbox - image override, resource caps, `allow_git_mutations`, `seccomp_profile`. It can never disable it. See [browser sandbox](docs/browser-sandbox.md). |
@@ -180,7 +194,7 @@ steps:
180
194
  | `extensions_dir` | Where consumer `extensions:` modules resolve - defaults to `~/.agentic-dev-loop/extensions`, **outside** the workspace. Workspace-resident extension/tool modules are refused at load. |
181
195
  | `deps_policy` | `locked` (default) or `open`. `locked` = the human-owned lockfile is the source of truth and the agent may not add dependencies; `open` = the bootstrap protocol (the agent may add exact-pinned manifest entries). See [Dependency policy](#dependency-policy-locked-by-default). |
182
196
  | `lockfiles` | The pipeline's declared lockfile names (workspace-relative), injected into agent context and named by the missing-dependency fail-fast. Required (non-empty) when `deps_policy: locked` - a `locked` pipeline with no lockfiles fails fast at load. |
183
- | `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); `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). |
197
+ | `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). |
184
198
  | `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). |
185
199
 
186
200
  ### Secrets (`secrets:`) - refs only, never values
@@ -376,7 +390,8 @@ Adaptation checklist for a new repo:
376
390
  only if a stage runs Chromium.
377
391
  3. `caches:` - toolchain cache dirs as rw bind-mounts shared across runs
378
392
  (`.nuget-packages`, `qa/node_modules`, `qa/.browsers`,
379
- `frontend-editor/node_modules`); caches are never synced back.
393
+ `frontend-editor/node_modules`); caches are never synced back, and a
394
+ step's `setup:` hook runs with them mounted so it can warm them.
380
395
  4. `sync_excludes:` - build output trees (`**/bin/**`, `**/obj/**`).
381
396
  5. `secrets:` - refs only, resolved from host env; each step opts in to the
382
397
  refs it needs.
@@ -537,7 +552,7 @@ agentic-dev-loop agentic-dev-loop/pipeline.yaml --dry-run # run the shipped pi
537
552
  - The shipped pipeline (`agentic-dev-loop/pipeline.yaml`) **dogfoods the tool on
538
553
  its own repo** - dev → test → review → qa → commit, with the engine's own
539
554
  three agent definitions in `sdk_agents/`.
540
- - CI: `ci.yml` (unit + integration suite, ubuntu + windows),
555
+ - CI: `ci.yml` (unit + integration suite, ubuntu),
541
556
  `publish-image.yml` (multi-arch dev image, amd64 + arm64), `release.yml`
542
557
  (PyPI publishing).
543
558
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentic-dev-loop"
7
- version = "1.0.0a26"
7
+ version = "1.0.0a28"
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"
@@ -386,9 +386,9 @@ class StepConfig(BaseModel):
386
386
  raise ValueError(
387
387
  f"step {self.name!r}: 'timeout' requires 'command'"
388
388
  )
389
- if self.setup is not None and self.command is None:
389
+ if self.setup is not None and self.command is None and self.agent is None:
390
390
  raise ValueError(
391
- f"step {self.name!r}: 'setup' is only allowed on 'command' steps"
391
+ f"step {self.name!r}: 'setup' is only allowed on 'command' or 'agent' steps"
392
392
  )
393
393
  # `network` is NOT rejected here: the schema can't tell a host-side
394
394
  # built-in action (e.g. `commit`, which must never take egress) from
@@ -699,11 +699,14 @@ def docker_create_payload(*, run_id: str, image: str, profile: EffectiveProfile,
699
699
 
700
700
  def docker_provision_payload(*, run_id: str, image: str, workspace: str,
701
701
  setup_cmd: str,
702
- sandbox_venv_dir: str | None = None) -> list[str]:
702
+ sandbox_venv_dir: str | None = None,
703
+ cache_mounts: list[tuple[str, str, str]]
704
+ | None = None) -> list[str]:
703
705
  """Exact ``docker run --rm`` argv for the provision container (§3.4).
704
706
  Network ON (default bridge), rw rootfs, --cap-drop ALL, mount set
705
- {workspace} + host-only masks (022); --user host uid:gid (AE-SBX-009) with
706
- HOME=/tmp."""
707
+ {workspace} + host-only masks (022) + the declared cache paths (028, rw
708
+ bind mounts so a ``setup:`` hook can warm them); --user host uid:gid
709
+ (AE-SBX-009) with HOME=/tmp."""
707
710
  payload = [
708
711
  "docker", "run", "--rm", "--name", f"{run_id}-provision",
709
712
  "--network", "bridge", "--cap-drop", "ALL",
@@ -713,6 +716,8 @@ def docker_provision_payload(*, run_id: str, image: str, workspace: str,
713
716
  if user is not None:
714
717
  payload.extend(["--user", user, "--env", "HOME=/tmp"])
715
718
  payload.extend(_sandbox_mounts(workspace, sandbox_venv_dir))
719
+ for source, target, _mode in (cache_mounts or []):
720
+ payload += ["-v", f"{source}:{target}"]
716
721
  payload.extend([
717
722
  "--workdir", workspace,
718
723
  image, "sh", "-c", setup_cmd,
@@ -995,7 +1000,8 @@ class DockerBackend(Sandbox):
995
1000
  return
996
1001
  payload = docker_provision_payload(
997
1002
  run_id=self._run_id, image=self._image, workspace=self._workspace,
998
- setup_cmd=setup_cmd, sandbox_venv_dir=self._sandbox_venv_dir)
1003
+ setup_cmd=setup_cmd, sandbox_venv_dir=self._sandbox_venv_dir,
1004
+ cache_mounts=self._cache_mounts)
999
1005
  res = self._runner.run(payload, timeout=PROVISION_TIMEOUT_SECONDS)
1000
1006
  if res.exit_code != 0:
1001
1007
  tail = (res.stdout or res.stderr or "")[-4000:]
@@ -1541,7 +1547,9 @@ def _file_digest(p: Path) -> str:
1541
1547
  def _declared_setups(cfg: PipelineConfig, workspace: Path):
1542
1548
  hashes = _lockfile_hashes(workspace)
1543
1549
  for step in cfg.steps:
1544
- if step.command is not None and step.setup is not None:
1550
+ if step.setup is not None and (
1551
+ step.command is not None or step.agent is not None
1552
+ ):
1545
1553
  yield step.setup, setup_stamp_key(step.setup, hashes)
1546
1554
 
1547
1555
 
@@ -9,9 +9,7 @@ from __future__ import annotations
9
9
 
10
10
  import os
11
11
  import shlex
12
- import shutil
13
12
  import signal
14
- import subprocess
15
13
  from collections.abc import Mapping
16
14
 
17
15
  _SHELL_METACHARS = frozenset("&|;><$`~*?[]")
@@ -33,47 +31,21 @@ def split_command(command: str) -> list[str]:
33
31
  return shlex.split(command)
34
32
 
35
33
 
36
- def host_shell_execution(
37
- command: str, *, nt: bool = os.name == "nt",
38
- which=shutil.which) -> tuple[str | list[str], bool]:
39
- """How the HOST (non-sandbox) path executes a shell-needing command.
40
-
41
- POSIX: the raw string with ``shell=True`` (unchanged unit-2 behaviour).
42
- Windows: ``shell=True`` is cmd.exe, which mangles ``$VAR`` expansion and
43
- ``|`` pipelines (the CI-red hybrid failures) - prefer the discovered
44
- POSIX ``sh`` (git-bash) via argv instead; fall back to ``shell=True``
45
- only when no ``sh`` exists anywhere on PATH.
46
- """
47
- if not nt:
48
- return command, True
49
- sh = which("sh")
50
- if sh:
51
- return [sh, "-c", command], False
34
+ def host_shell_execution(command: str) -> tuple[str, bool]:
35
+ """How the HOST (non-sandbox) path executes a shell-needing command:
36
+ the raw string with ``shell=True``. The engine targets Linux and WSL2
37
+ (which is plain Linux), so ``sh`` is always present."""
52
38
  return command, True
53
39
 
54
40
 
55
- def kill_process_tree(proc, *, nt: bool | None = None,
56
- run=subprocess.run) -> None:
41
+ def kill_process_tree(proc) -> None:
57
42
  """Terminate `proc` and its descendants after a timeout.
58
43
 
59
- POSIX: ``start_new_session`` made ``proc.pid`` the process-group id -
44
+ ``start_new_session`` made ``proc.pid`` the process-group id, so
60
45
  ``killpg`` reaps the whole group (a shell wrapper's children survive a
61
46
  plain ``kill``).
62
-
63
- Windows: ``proc.kill()`` terminates only the direct child - a git-bash
64
- ``sh -c`` wrapper's python child survives and keeps the pipes open, so
65
- the follow-up ``communicate()`` blocks out the child's full lifetime.
66
- ``taskkill /T`` walks the process tree while the parent is still alive,
67
- then ``kill`` sweeps up anything left (CI-red: 9.5s timeout test).
68
47
  """
69
- if nt is None:
70
- nt = os.name == "nt"
71
- if nt:
72
- run(["taskkill", "/F", "/T", "/PID", str(proc.pid)],
73
- capture_output=True, check=False)
74
- proc.kill()
75
- else:
76
- os.killpg(proc.pid, signal.SIGKILL)
48
+ os.killpg(proc.pid, signal.SIGKILL)
77
49
 
78
50
 
79
51
  def is_pid_token(token: str) -> bool:
@@ -409,7 +409,7 @@ def make_agent_node(runtime: Runtime, step: StepConfig):
409
409
  update["artifacts"] = {step.name: result.response}
410
410
  return update
411
411
  if verdict == "unverifiable":
412
- print(f"[{subject}] {step.name} qa verdict: UNVERIFIABLE "
412
+ print(f"[{subject}] {step.name} verdict: UNVERIFIABLE "
413
413
  f"(moves on).", flush=True)
414
414
  update = {"step_verdicts": merged, "failed": False}
415
415
  if captures and result.response is not None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-dev-loop
3
- Version: 1.0.0a26
3
+ Version: 1.0.0a28
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
@@ -84,6 +84,20 @@ timeout (agent runs are long), and fail fast on a broken sandbox - daemon-side
84
84
  exec failures are classified and stop the run without an agent retry loop,
85
85
  while a startup reaper reclaims containers leaked by a crashed run.
86
86
 
87
+ ## Supported platforms
88
+
89
+ The engine is plain Linux under the hood and runs wherever a compliant
90
+ Docker engine does:
91
+
92
+ | Platform | Support |
93
+ | --- | --- |
94
+ | **Linux** | Native. Docker Engine, OrbStack, Colima, or podman with docker compat. |
95
+ | **macOS** | Native. Docker Desktop, OrbStack, or Colima. |
96
+ | **Windows** | Via **WSL2** (Ubuntu distro). Enable Docker Desktop's WSL integration (or run Docker Engine inside the distro) and keep the repo on the WSL filesystem (`~/dev/...`), not `/mnt/c` - bind-mounting a 9p path into containers breaks permissions and is slow. |
97
+
98
+ Native Windows (outside WSL2) is not supported: every Windows-specific code
99
+ path and test was removed, and CI runs on Ubuntu only.
100
+
87
101
  ## Installation
88
102
 
89
103
  **Prerequisites**
@@ -91,7 +105,7 @@ while a startup reaper reclaims containers leaked by a crashed run.
91
105
  | Requirement | Detail |
92
106
  | --- | --- |
93
107
  | Python | ≥ 3.12 |
94
- | Docker engine | Engine API ≥ 1.43 (Engine 24). Any compliant engine works - Docker Desktop, OrbStack, Colima, Rancher Desktop, podman docker-compat. On Windows, Linux-containers mode is required. |
108
+ | Docker engine | Engine API ≥ 1.43 (Engine 24). Any compliant engine works - Docker Desktop, OrbStack, Colima, Rancher Desktop, podman docker-compat. On Windows, run under WSL2: the repo must live on the WSL filesystem, not /mnt/c. |
95
109
  | LLM API key | The env var named by your pipeline's `api_key_env` (e.g. `OPENAI_API_KEY`) must be set - a missing key fails fast, naming it. |
96
110
 
97
111
  ```bash
@@ -214,7 +228,7 @@ steps:
214
228
  | `agent:` / `command:` / `action:` | Exactly one per step. Agents live in `agents_dir` as `<name>.agent.md`; commands run sandboxed (mechanical, never an LLM call). `commit` is the sole host-side built-in action; consumer actions are sandbox-executed, command-shaped scripts (see [Host boundary](#host-boundary-security-model)). |
215
229
  | `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
216
230
  | `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
217
- | `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` provisions the sandbox (command steps, network ON, stamp-cached); `network: true` lets a command/agent step reach the hosts its `egress:` allowlist names (default off) - see [Per-stage networking](#per-stage-networking). |
231
+ | `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). |
218
232
  | `mounts:` / `egress:` | Per-step workspace isolation. `mounts:` lists `{path, mode}` entries (`rw`/`ro`, default `ro`) and is **strict-by-default** - empty/missing mounts nothing; only declared-rw paths sync back to the repo. `egress:` is `{class, presets, targets}` (`class: browser` = broad-but-logged; otherwise a strict allowlist). See [Per-stage networking](#per-stage-networking) and [docs/setup.md](docs/setup.md). |
219
233
  | `secrets:` (top-level) / step `secrets:` | Declares secret **refs only - never values**; each step opts in to the refs it needs. Resolved host-side and injected into that step's exec env only. See [Secrets](#secrets--refs-only-never-values). |
220
234
  | `isolation:` (optional) | Configures the sandbox - image override, resource caps, `allow_git_mutations`, `seccomp_profile`. It can never disable it. See [browser sandbox](docs/browser-sandbox.md). |
@@ -222,7 +236,7 @@ steps:
222
236
  | `extensions_dir` | Where consumer `extensions:` modules resolve - defaults to `~/.agentic-dev-loop/extensions`, **outside** the workspace. Workspace-resident extension/tool modules are refused at load. |
223
237
  | `deps_policy` | `locked` (default) or `open`. `locked` = the human-owned lockfile is the source of truth and the agent may not add dependencies; `open` = the bootstrap protocol (the agent may add exact-pinned manifest entries). See [Dependency policy](#dependency-policy-locked-by-default). |
224
238
  | `lockfiles` | The pipeline's declared lockfile names (workspace-relative), injected into agent context and named by the missing-dependency fail-fast. Required (non-empty) when `deps_policy: locked` - a `locked` pipeline with no lockfiles fails fast at load. |
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); `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). |
239
+ | `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). |
226
240
  | `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). |
227
241
 
228
242
  ### Secrets (`secrets:`) - refs only, never values
@@ -418,7 +432,8 @@ Adaptation checklist for a new repo:
418
432
  only if a stage runs Chromium.
419
433
  3. `caches:` - toolchain cache dirs as rw bind-mounts shared across runs
420
434
  (`.nuget-packages`, `qa/node_modules`, `qa/.browsers`,
421
- `frontend-editor/node_modules`); caches are never synced back.
435
+ `frontend-editor/node_modules`); caches are never synced back, and a
436
+ step's `setup:` hook runs with them mounted so it can warm them.
422
437
  4. `sync_excludes:` - build output trees (`**/bin/**`, `**/obj/**`).
423
438
  5. `secrets:` - refs only, resolved from host env; each step opts in to the
424
439
  refs it needs.
@@ -579,7 +594,7 @@ agentic-dev-loop agentic-dev-loop/pipeline.yaml --dry-run # run the shipped pi
579
594
  - The shipped pipeline (`agentic-dev-loop/pipeline.yaml`) **dogfoods the tool on
580
595
  its own repo** - dev → test → review → qa → commit, with the engine's own
581
596
  three agent definitions in `sdk_agents/`.
582
- - CI: `ci.yml` (unit + integration suite, ubuntu + windows),
597
+ - CI: `ci.yml` (unit + integration suite, ubuntu),
583
598
  `publish-image.yml` (multi-arch dev image, amd64 + arm64), `release.yml`
584
599
  (PyPI publishing).
585
600
 
@@ -375,9 +375,8 @@ class TestCommitNode:
375
375
 
376
376
  def test_add_excludes_live_checkpoint_db(self, tmp_path, monkeypatch):
377
377
  """The live checkpointer DB under state_dir stays open for the whole
378
- run; on Windows an open SQLite file blocks other readers and
379
- `git add -A` fails to hash it (CI-red gate failure). The add must
380
- exclude sqlite files under the state dir on every OS."""
378
+ run, so `git add -A` cannot hash it. The add must exclude sqlite
379
+ files under the state dir."""
381
380
  repo = tmp_path / "repo"
382
381
  init_git_repo(repo)
383
382
  (repo / "feature.cs").write_text("// constructed by the pipeline")
@@ -489,15 +489,19 @@ class TestStepEnvAndSetup:
489
489
  "on_pass": "success"}]
490
490
  assert _load(tmp_path, data).steps[0].setup is not None
491
491
 
492
- @pytest.mark.parametrize("step", [
493
- {"name": "x", "agent": "dev", "setup": "npm i"},
494
- {"name": "x", "action": "commit", "setup": "npm i"},
495
- ])
496
- def test_setup_rejected_on_non_command_steps(self, tmp_path, step):
492
+ def test_setup_allowed_on_agent_step(self, tmp_path):
497
493
  data = _base()
498
- data["steps"] = [step | {"on_pass": "success"}]
499
- with pytest.raises(ValidationError,
500
- match="'setup' is only allowed on 'command' steps"):
494
+ data["steps"] = [{"name": "dev", "agent": "dev", "setup": "npm i",
495
+ "on_pass": "success"}]
496
+ assert _load(tmp_path, data).steps[0].setup == "npm i"
497
+
498
+ def test_setup_rejected_on_action_step(self, tmp_path):
499
+ data = _base()
500
+ data["steps"] = [{"name": "x", "action": "commit", "setup": "npm i",
501
+ "on_pass": "success"}]
502
+ with pytest.raises(
503
+ ValidationError,
504
+ match="'setup' is only allowed on 'command' or 'agent' steps"):
501
505
  _load(tmp_path, data)
502
506
 
503
507
  def test_env_round_trip(self, tmp_path):
@@ -2109,3 +2109,43 @@ class TestReaper:
2109
2109
  self._write(tmp_path / "state", [own, other])
2110
2110
  backend.close()
2111
2111
  assert self._read(tmp_path / "state") == [other]
2112
+
2113
+
2114
+ def test_declared_setups_includes_agent_steps(tmp_path):
2115
+ import yaml
2116
+
2117
+ from agentic_dev_loop.core.config import load_config
2118
+ from agentic_dev_loop.engine.isolation import _declared_setups
2119
+
2120
+ (tmp_path / "uv.lock").write_text("")
2121
+ data = {
2122
+ "name": "t",
2123
+ "llm": {"model": "m", "api_key_env": "K"},
2124
+ "agents_dir": "agents",
2125
+ "lockfiles": ["uv.lock"],
2126
+ "steps": [
2127
+ {"name": "dev", "agent": "dev",
2128
+ "setup": "cd frontend-editor && npm ci", "on_pass": "success"},
2129
+ {"name": "t", "command": "make", "setup": "echo warm",
2130
+ "on_pass": "success"},
2131
+ ],
2132
+ }
2133
+ p = tmp_path / "pipeline.yaml"
2134
+ p.write_text(yaml.safe_dump(data))
2135
+ cfg = load_config(p)
2136
+ assert set(dict(_declared_setups(cfg, tmp_path))) == {
2137
+ "cd frontend-editor && npm ci", "echo warm"}
2138
+
2139
+
2140
+ def test_provision_payload_mounts_declared_caches():
2141
+ payload = docker_provision_payload(
2142
+ run_id="run1",
2143
+ image="img",
2144
+ workspace="/ws",
2145
+ setup_cmd="cd frontend-editor && npm ci",
2146
+ cache_mounts=[
2147
+ ("/host/fe/node_modules", "/ws/frontend-editor/node_modules", "rw"),
2148
+ ],
2149
+ )
2150
+ assert "-v" in payload
2151
+ assert "/host/fe/node_modules:/ws/frontend-editor/node_modules" in payload
@@ -5,11 +5,8 @@ stateless pure functions shared by the command-step and action-step
5
5
  surfaces; property tests exercise the invariants that keep the subprocess
6
6
  boundary fail-safe (BR-4, BR-5)."""
7
7
 
8
- import os
9
- import pytest
10
8
  import signal
11
9
  import string
12
- import subprocess
13
10
 
14
11
  from hypothesis import given, settings
15
12
  from hypothesis import strategies as st
@@ -45,23 +42,10 @@ class TestNeedsShell:
45
42
 
46
43
 
47
44
  class TestHostShellExecution:
48
- """How the HOST (non-sandbox) path executes a shell-needing command.
49
- Windows cannot rely on shell=True - cmd.exe mangles $VAR and pipelines
50
- (the CI-red hybrid failures) - so it prefers the discovered POSIX sh."""
45
+ """How the HOST (non-sandbox) path executes a shell-needing command."""
51
46
 
52
- def test_posix_uses_shell_true(self):
53
- argv, shell = host_shell_execution("a && b", nt=False)
54
- assert (argv, shell) == ("a && b", True)
55
-
56
- def test_windows_prefers_discovered_sh(self):
57
- which = lambda name: "C:/Program Files/Git/bin/sh.exe" # noqa: E731
58
- argv, shell = host_shell_execution("a && b", nt=True, which=which)
59
- assert argv == ["C:/Program Files/Git/bin/sh.exe", "-c", "a && b"]
60
- assert shell is False
61
-
62
- def test_windows_without_sh_falls_back_to_shell_true(self):
63
- argv, shell = host_shell_execution("a && b", nt=True,
64
- which=lambda name: None)
47
+ def test_uses_shell_true(self):
48
+ argv, shell = host_shell_execution("a && b")
65
49
  assert (argv, shell) == ("a && b", True)
66
50
 
67
51
 
@@ -144,10 +128,8 @@ class TestChildEnv:
144
128
 
145
129
 
146
130
  class TestKillProcessTree:
147
- """The timeout reap path. CI-red on Windows: `proc.kill()` terminates
148
- only the direct child - a git-bash `sh -c` wrapper's python child
149
- survives, keeps the pipes open, and the follow-up communicate() blocks
150
- out the full sleep. The tree must be reaped root-to-leaves."""
131
+ """The timeout reap path: ``start_new_session`` made ``proc.pid`` the
132
+ process-group id, so ``killpg`` reaps the whole tree."""
151
133
 
152
134
  class _Proc:
153
135
  def __init__(self, log):
@@ -157,30 +139,14 @@ class TestKillProcessTree:
157
139
  def kill(self):
158
140
  self.log.append("kill")
159
141
 
160
- @pytest.mark.skipif(not hasattr(os, "killpg"), reason="POSIX-only: os.killpg does not exist on Windows")
161
- def test_posix_reaps_the_whole_process_group(self, monkeypatch):
142
+ def test_reaps_the_whole_process_group(self, monkeypatch):
162
143
  killed = []
163
144
  monkeypatch.setattr(
164
145
  "agentic_dev_loop.engine.proc.os.killpg",
165
146
  lambda pid, sig: killed.append((pid, sig)))
166
147
  proc = self._Proc([])
167
148
 
168
- kill_process_tree(proc, nt=False)
149
+ kill_process_tree(proc)
169
150
 
170
151
  assert killed == [(99, signal.SIGKILL)]
171
152
  assert proc.log == [] # the wrapper needs no separate kill
172
-
173
- def test_windows_taskkills_the_tree_then_kills_the_wrapper(self):
174
- log = []
175
- proc = self._Proc(log)
176
-
177
- def fake_run(argv, **kwargs):
178
- log.append("taskkill")
179
- assert argv == ["taskkill", "/F", "/T", "/PID", "99"]
180
- return subprocess.CompletedProcess(argv, 0, "", "")
181
-
182
- kill_process_tree(proc, nt=True, run=fake_run)
183
-
184
- # taskkill /T needs the parent alive to walk the tree - then the
185
- # wrapper itself is swept up.
186
- assert log == ["taskkill", "kill"]
@@ -393,8 +393,8 @@ class TestCommandNode:
393
393
  """The host-path timeout must cut the WHOLE tree short, not just the
394
394
  direct child - a shell wrapper's grandchild that survives keeps the
395
395
  pipes open and the follow-up communicate() blocks out the full sleep
396
- (CI-red 9.5s). kill_process_tree's per-OS branches are unit-pinned
397
- in tests/test_proc.py::TestKillProcessTree."""
396
+ (CI-red 9.5s). kill_process_tree is unit-pinned in
397
+ tests/test_proc.py::TestKillProcessTree."""
398
398
  code, out, timed_out = _run_command(
399
399
  f'"{sys.executable}" -c "import time; time.sleep(30)"',
400
400
  tmp_path, 0.2)
@@ -338,9 +338,9 @@ class TestMergeOnConflictPartition:
338
338
  exp = _export(tmp_path, ws)
339
339
  (ws / "src" / "audit.md").write_text("base\n")
340
340
  (exp / "src" / "audit.md").write_text("base\nexport\n")
341
- # Baseline comes from the real workspace bytes: write_text translates
342
- # \n to os.linesep on Windows, so a fabricated sha256(b"base\n") would
343
- # never match and would misclassify the copy as a merge conflict.
341
+ # Baseline comes from the real workspace bytes, so a fabricated
342
+ # sha256(b"base\n") would never match and would misclassify the
343
+ # copy as a merge conflict.
344
344
  baseline = {"src/audit.md": _digest(ws / "src" / "audit.md")}
345
345
  report = sync_back(exp, ws, ["src"], baseline=baseline,
346
346
  merge_on_conflict=["src/audit.md"])
@@ -6,11 +6,7 @@ a stale install rejects newer pipeline configs at load time (e.g.
6
6
  is upgraded on every run. Tests drive the real script with stub
7
7
  executables on PATH - no network, no mocks of the script's logic.
8
8
 
9
- The script is bash, so it is executed through `bash` on every platform:
10
- Windows cannot CreateProcess a `.sh` file (shebangs are a POSIX concept)
11
- and its MSYS bash (git-bash, preinstalled on windows-latest runners)
12
- understands only POSIX-style paths (`/c/...`), so PATH entries and
13
- redirect targets inside stub bodies are converted accordingly.
9
+ The script is bash, so it is executed through `bash`.
14
10
  """
15
11
 
16
12
  import os
@@ -24,19 +20,6 @@ REPO_ROOT = Path(__file__).resolve().parents[1]
24
20
  SCRIPT = REPO_ROOT / "trigger-agentic-dev-loop.sh"
25
21
 
26
22
 
27
- def _msys(path: Path) -> str:
28
- """POSIX-style form of a Windows path (`C:\\x` -> `/c/x`).
29
-
30
- MSYS bash splits PATH on `:` and treats backslashes as escapes, so a
31
- native `C:\\...` entry would fragment into `C` + `\\...`. Pass the
32
- mount-style form instead. On POSIX the path is returned unchanged.
33
- """
34
- if os.name != "nt":
35
- return str(path)
36
- drive, rest = os.path.splitdrive(str(path))
37
- return "/" + drive[0].lower() + rest.replace("\\", "/")
38
-
39
-
40
23
  def _bash() -> str:
41
24
  bash = shutil.which("bash")
42
25
  if bash is None:
@@ -46,7 +29,7 @@ def _bash() -> str:
46
29
 
47
30
  def _run_script(*args: str, env: dict) -> subprocess.CompletedProcess:
48
31
  return subprocess.run(
49
- [_bash(), _msys(SCRIPT), *args],
32
+ [_bash(), str(SCRIPT), *args],
50
33
  capture_output=True,
51
34
  text=True,
52
35
  env=env,
@@ -64,7 +47,7 @@ def _make_stub(bin_dir: Path, name: str, body: str) -> None:
64
47
  def _env_with(bin_dir: Path | None = None) -> dict:
65
48
  entries = ["/usr/bin", "/bin"]
66
49
  if bin_dir is not None:
67
- entries.insert(0, _msys(bin_dir))
50
+ entries.insert(0, str(bin_dir))
68
51
  return {**os.environ, "PATH": ":".join(entries)}
69
52
 
70
53
 
@@ -80,7 +63,7 @@ def test_trigger_upgrades_existing_install(tmp_path):
80
63
  _make_stub(
81
64
  bin_dir,
82
65
  "pipx",
83
- f'echo "$*" >> "{_msys(pipx_log)}"',
66
+ f'echo "$*" >> "{str(pipx_log)}"',
84
67
  )
85
68
 
86
69
  result = _run_script("--plan", "plan.md", env=_env_with(bin_dir))