agentic-dev-loop 0.9.0__tar.gz → 0.9.2__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.
- {agentic_dev_loop-0.9.0/src/agentic_dev_loop.egg-info → agentic_dev_loop-0.9.2}/PKG-INFO +3 -6
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/README.md +2 -5
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/pyproject.toml +1 -1
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/core/config.py +40 -79
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/cli.py +2 -14
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/graph.py +3 -4
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/isolation.py +142 -408
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/stages.py +24 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/step_workspace.py +51 -39
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/sync_back.py +42 -16
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/viz/server.py +52 -15
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2/src/agentic_dev_loop.egg-info}/PKG-INFO +3 -6
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop.egg-info/SOURCES.txt +0 -1
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_029_merge_on_conflict.py +0 -6
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_cli.py +0 -48
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_config.py +26 -37
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_dry_run.py +1 -1
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_hostboundary_integration.py +6 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_isolation.py +173 -553
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_isolation_integration.py +8 -152
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_isolation_pbt.py +1 -11
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_stages.py +98 -1
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_step_workspace.py +10 -19
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_unit5_config.py +0 -7
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_unit5_pure.py +17 -5
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_viz.py +138 -4
- agentic_dev_loop-0.9.0/tests/test_028_declared_caches.py +0 -259
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/LICENSE +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/setup.cfg +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/__init__.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/core/__init__.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/core/paths.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/core/results.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/core/verdict.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/__init__.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/actions.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/agent_error_guide.md +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/agent_error_guide.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/agents.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/allowlist.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/brief_coverage.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/brief_prompt.md +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/brief_prompt.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/containers.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/deps.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/dryrun.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/egress_policy.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/egress_proxy.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/forbidden.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/hostboundary.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/llm.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/loaders.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/mounts.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/outputs.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/presets.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/proc.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/registry.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/engine/secrets.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/py.typed +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/viz/__init__.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop/viz/__main__.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_033_brief_step.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_035_finalize_nudge.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_036_banned_commands_and_lane_boundary.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_041_brief_coverage.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_046_brief_prompt.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_actions.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_agent_error_guide.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_agents.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_aidlc.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_containers.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_deps.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_e2e_fixtures.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_egress_proxy.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_graph.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_hostboundary.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_llm.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_loaders.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_module_contract.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_outputs.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_outputs_pbt.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_proc.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_publish.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_qa_prompt.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_registry.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_repo_ascii.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_results.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_sdk_compat.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_secrets.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_secrets_integration.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_secrets_pipeline.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_source_resolution.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_sync_back.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_trigger_script.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_unit5_pbt.py +0 -0
- {agentic_dev_loop-0.9.0 → agentic_dev_loop-0.9.2}/tests/test_verdict.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentic-dev-loop
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.2
|
|
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
|
|
@@ -155,8 +155,6 @@ agentic-dev-loop pipeline.yaml --resume <thread-id> --max-attempts 5
|
|
|
155
155
|
| `--dry-run` | Full load-and-validate pass, then print the resolved plan. No LLM, no Docker, no state writes, no API key. Exit `0` on a clean load. |
|
|
156
156
|
| `--resume <thread-id>` | Continue the latest checkpoint of a previous run (crash, Ctrl-C, exhaustion) with all accumulated notes intact. |
|
|
157
157
|
| `--max-attempts <n>` | Override the pipeline's `max_attempts`. |
|
|
158
|
-
| `--refresh` | Force re-provision of every `setup:` hook (bypasses the stamp cache). |
|
|
159
|
-
| `--clean-stamps` | Delete cached provision stamps and exit. |
|
|
160
158
|
| `--no-viz` / `--viz-port <port>` | Suppress the live graph view / set a preferred port (default 8321; falls back to a free port when busy). |
|
|
161
159
|
| extension flags | Document-loader flags (e.g. `--plan`, `--aidlc-unit`) appear once the pipeline's extensions are loaded - `agentic-dev-loop PIPELINE --help` shows them. |
|
|
162
160
|
|
|
@@ -261,8 +259,7 @@ steps:
|
|
|
261
259
|
| `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. |
|
|
262
260
|
| `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). |
|
|
263
261
|
| `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
|
|
264
|
-
| `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:`
|
|
265
|
-
| `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`. |
|
|
262
|
+
| `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` runs fresh at every step boundary in a one-shot provision container targeting the export (command or agent steps; network ON; no stamping); `network: true` lets a command/agent step reach the hosts its `egress:` allowlist names (default off) - see [Per-stage networking](#per-stage-networking). |
|
|
266
263
|
| `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). |
|
|
267
264
|
| `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). |
|
|
268
265
|
| `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). |
|
|
@@ -270,7 +267,7 @@ steps:
|
|
|
270
267
|
| `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. |
|
|
271
268
|
| `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). |
|
|
272
269
|
| `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. |
|
|
273
|
-
| `caches:` / `sync_excludes:` | Top-level run-wide lists. `caches:` names repo-relative toolchain cache dirs bind-mounted rw from
|
|
270
|
+
| `caches:` / `sync_excludes:` | Top-level run-wide lists. `caches:` names repo-relative toolchain cache dirs bind-mounted rw from host-side stores under `{state_dir}/caches/<name>` (auto-created); a `setup:` hook can warm them (the provision container carries the same cache mounts); `sync_excludes:` names non-gitignored 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). |
|
|
274
271
|
| `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). |
|
|
275
272
|
|
|
276
273
|
### Reasoning effort (opt-in)
|
|
@@ -111,8 +111,6 @@ agentic-dev-loop pipeline.yaml --resume <thread-id> --max-attempts 5
|
|
|
111
111
|
| `--dry-run` | Full load-and-validate pass, then print the resolved plan. No LLM, no Docker, no state writes, no API key. Exit `0` on a clean load. |
|
|
112
112
|
| `--resume <thread-id>` | Continue the latest checkpoint of a previous run (crash, Ctrl-C, exhaustion) with all accumulated notes intact. |
|
|
113
113
|
| `--max-attempts <n>` | Override the pipeline's `max_attempts`. |
|
|
114
|
-
| `--refresh` | Force re-provision of every `setup:` hook (bypasses the stamp cache). |
|
|
115
|
-
| `--clean-stamps` | Delete cached provision stamps and exit. |
|
|
116
114
|
| `--no-viz` / `--viz-port <port>` | Suppress the live graph view / set a preferred port (default 8321; falls back to a free port when busy). |
|
|
117
115
|
| extension flags | Document-loader flags (e.g. `--plan`, `--aidlc-unit`) appear once the pipeline's extensions are loaded - `agentic-dev-loop PIPELINE --help` shows them. |
|
|
118
116
|
|
|
@@ -217,8 +215,7 @@ steps:
|
|
|
217
215
|
| `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. |
|
|
218
216
|
| `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). |
|
|
219
217
|
| `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
|
|
220
|
-
| `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:`
|
|
221
|
-
| `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`. |
|
|
218
|
+
| `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` runs fresh at every step boundary in a one-shot provision container targeting the export (command or agent steps; network ON; no stamping); `network: true` lets a command/agent step reach the hosts its `egress:` allowlist names (default off) - see [Per-stage networking](#per-stage-networking). |
|
|
222
219
|
| `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). |
|
|
223
220
|
| `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). |
|
|
224
221
|
| `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). |
|
|
@@ -226,7 +223,7 @@ steps:
|
|
|
226
223
|
| `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. |
|
|
227
224
|
| `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). |
|
|
228
225
|
| `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. |
|
|
229
|
-
| `caches:` / `sync_excludes:` | Top-level run-wide lists. `caches:` names repo-relative toolchain cache dirs bind-mounted rw from
|
|
226
|
+
| `caches:` / `sync_excludes:` | Top-level run-wide lists. `caches:` names repo-relative toolchain cache dirs bind-mounted rw from host-side stores under `{state_dir}/caches/<name>` (auto-created); a `setup:` hook can warm them (the provision container carries the same cache mounts); `sync_excludes:` names non-gitignored 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). |
|
|
230
227
|
| `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). |
|
|
231
228
|
|
|
232
229
|
### Reasoning effort (opt-in)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agentic-dev-loop"
|
|
7
|
-
version = "0.9.
|
|
7
|
+
version = "0.9.2"
|
|
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"
|
|
@@ -16,9 +16,6 @@ from agentic_dev_loop.core.paths import _has_wildcards, path_excluded, relative_
|
|
|
16
16
|
TERMINALS = frozenset({"success", "failure"})
|
|
17
17
|
KNOWN_SECRET_PROVIDERS = frozenset({"env"})
|
|
18
18
|
MOUNT_MODES = frozenset({"rw", "ro"})
|
|
19
|
-
# Host-only dirs a mount path may never name (022): they are masked in
|
|
20
|
-
# the sandbox with a read-only tmpfs, so declaring them would be a decoy.
|
|
21
|
-
HOST_ONLY_MOUNT_DIRS = frozenset({".venv"})
|
|
22
19
|
# The egress preset names the engine ships (BR-9). Config validates the
|
|
23
20
|
# name here; the target data lives in engine/presets.py (preset data,
|
|
24
21
|
# never mechanism - D-2).
|
|
@@ -52,8 +49,8 @@ def _validate_mount_path(path: str) -> str:
|
|
|
52
49
|
"""Validate one repo-relative mount path (BR-1.3).
|
|
53
50
|
|
|
54
51
|
Rejects (loudly) an empty path, an absolute path, ``..`` traversal,
|
|
55
|
-
any ``.git`` component
|
|
56
|
-
|
|
52
|
+
and any ``.git`` component. The path is normalised to forward-slash
|
|
53
|
+
form and returned.
|
|
57
54
|
"""
|
|
58
55
|
if not isinstance(path, str) or not path.strip():
|
|
59
56
|
raise ValueError("mount path must be a non-empty string")
|
|
@@ -70,9 +67,6 @@ def _validate_mount_path(path: str) -> str:
|
|
|
70
67
|
if seg == ".git":
|
|
71
68
|
raise ValueError(
|
|
72
69
|
f"mount path {path!r} must never name '.git'")
|
|
73
|
-
if seg in HOST_ONLY_MOUNT_DIRS:
|
|
74
|
-
raise ValueError(
|
|
75
|
-
f"mount path {path!r} names the host-only dir {seg!r}")
|
|
76
70
|
return "/".join(parts) if parts else "."
|
|
77
71
|
|
|
78
72
|
|
|
@@ -80,7 +74,7 @@ def _validate_declared_path(path: str, *, allow_wildcards: bool) -> str:
|
|
|
80
74
|
"""Validate one declared ``caches:``/``sync_excludes:`` entry (028).
|
|
81
75
|
|
|
82
76
|
Same repo-relative contract as mount paths: non-empty, no absolute paths,
|
|
83
|
-
no ``..`` traversal, no ``.git`` component
|
|
77
|
+
no ``..`` traversal, no ``.git`` component. Wildcard
|
|
84
78
|
entries (``sync_excludes:`` only) skip wildcard segments in the literal
|
|
85
79
|
checks; the literal segments still obey the same rules. The normalised
|
|
86
80
|
forward-slash form is returned.
|
|
@@ -102,9 +96,6 @@ def _validate_declared_path(path: str, *, allow_wildcards: bool) -> str:
|
|
|
102
96
|
if seg == ".git":
|
|
103
97
|
raise ValueError(
|
|
104
98
|
f"declared path {path!r} must never name '.git'")
|
|
105
|
-
if seg in HOST_ONLY_MOUNT_DIRS:
|
|
106
|
-
raise ValueError(
|
|
107
|
-
f"declared path {path!r} names the host-only dir {seg!r}")
|
|
108
99
|
return "/".join(parts) if parts else "."
|
|
109
100
|
|
|
110
101
|
|
|
@@ -220,24 +211,32 @@ class SecretSpec(BaseModel):
|
|
|
220
211
|
class IsolationConfig(BaseModel):
|
|
221
212
|
"""Optional `isolation:` section - configures the always-on sandbox (BR-2).
|
|
222
213
|
Absence means opinionated defaults, never unsandboxed behaviour. Unknown
|
|
223
|
-
keys fail at load (BR-5). `state_dir` is HOST-ONLY (
|
|
224
|
-
never mounted into the sandbox (Q1)."""
|
|
214
|
+
keys fail at load (BR-5). `state_dir` is HOST-ONLY (host-side caches +
|
|
215
|
+
audit) and is never mounted into the sandbox (Q1)."""
|
|
225
216
|
|
|
226
217
|
model_config = ConfigDict(extra="forbid")
|
|
227
218
|
|
|
219
|
+
@model_validator(mode="before")
|
|
220
|
+
@classmethod
|
|
221
|
+
def _reject_removed_keys(cls, data: Any) -> Any:
|
|
222
|
+
"""052: removed isolation keys fail loud with a migration message."""
|
|
223
|
+
if isinstance(data, dict):
|
|
224
|
+
if "sandbox_venv_dir" in data:
|
|
225
|
+
raise ValueError(
|
|
226
|
+
"isolation.sandbox_venv_dir was removed: venvs are "
|
|
227
|
+
"per-run, built by a setup hook into the export; remove "
|
|
228
|
+
"this key")
|
|
229
|
+
if "refresh" in data:
|
|
230
|
+
raise ValueError(
|
|
231
|
+
"isolation.refresh was removed: setup now runs fresh at "
|
|
232
|
+
"every step entry with no stamping; remove this key")
|
|
233
|
+
return data
|
|
234
|
+
|
|
228
235
|
image: str | None = None # per-pipeline override (KD-2 ref rules)
|
|
229
|
-
refresh: bool = False # force re-provision (NFR-5)
|
|
230
236
|
memory: str | None = None # override exec container memory
|
|
231
237
|
cpus: float | None = None # override exec container cpus
|
|
232
238
|
allow_git_mutations: bool = False # BR-6 knob
|
|
233
|
-
state_dir: str = "~/.agentic-dev-loop/state" #
|
|
234
|
-
# 022 relocation opt-in: an out-of-workspace host dir that is bind-mounted
|
|
235
|
-
# at ``{ws}/.venv-sandbox`` (rw) in both the exec and provision containers,
|
|
236
|
-
# keeping the workspace free of container venv artifacts. Unset keeps the
|
|
237
|
-
# legacy workspace-local ``.venv-sandbox`` unchanged. Must resolve outside
|
|
238
|
-
# the workspace - validated at startup, not load (the workspace is unknown
|
|
239
|
-
# here).
|
|
240
|
-
sandbox_venv_dir: str | None = None
|
|
239
|
+
state_dir: str = "~/.agentic-dev-loop/state" # host-side caches + audit
|
|
241
240
|
# 023/025: optional host path to a Docker seccomp profile JSON, or a
|
|
242
241
|
# ``bundled:<name>`` sentinel resolved at load to a packaged profile
|
|
243
242
|
# (e.g. ``bundled:chromium-userns``). When set, the exec container runs
|
|
@@ -402,8 +401,7 @@ class StepConfig(BaseModel):
|
|
|
402
401
|
reasoning_effort: ReasoningEffort | None = None
|
|
403
402
|
custom_prompt: str | None = None # agent steps only (040); file path resolved at load
|
|
404
403
|
env: dict[str, str | None] | None = None # NFR-1: null = inherit host value
|
|
405
|
-
setup: str | None = None #
|
|
406
|
-
setup_probe: str | None = None # 045: consumer probe replacing the venv probe on a warm stamp; requires `setup`; probe text is NOT part of the stamp key (a probe-only change needs --refresh/--clean-stamps)
|
|
404
|
+
setup: str | None = None # command or agent steps; runs fresh at each step entry (052)
|
|
407
405
|
forbidden_commands: list[str] = [] # banned-command regex deny-list (036 D1)
|
|
408
406
|
network: bool = False # per-stage egress (command/agent + sandbox actions; host-side actions reject at registry time)
|
|
409
407
|
# unit-5: per-step mount granularity + egress policy. Empty mounts =
|
|
@@ -432,6 +430,16 @@ class StepConfig(BaseModel):
|
|
|
432
430
|
return "command"
|
|
433
431
|
return "action"
|
|
434
432
|
|
|
433
|
+
@model_validator(mode="before")
|
|
434
|
+
@classmethod
|
|
435
|
+
def _reject_removed_keys(cls, data: Any) -> Any:
|
|
436
|
+
"""052: removed step keys fail loud with a migration message."""
|
|
437
|
+
if isinstance(data, dict) and "setup_probe" in data:
|
|
438
|
+
raise ValueError(
|
|
439
|
+
"step setup_probe was removed: setup now runs fresh at every "
|
|
440
|
+
"step entry with no stamping; remove this key")
|
|
441
|
+
return data
|
|
442
|
+
|
|
435
443
|
@field_validator("produces", mode="before")
|
|
436
444
|
@classmethod
|
|
437
445
|
def _coerce_produces(cls, v: Any) -> Any:
|
|
@@ -464,13 +472,6 @@ class StepConfig(BaseModel):
|
|
|
464
472
|
raise ValueError("custom_prompt must be a non-empty string")
|
|
465
473
|
return v
|
|
466
474
|
|
|
467
|
-
@field_validator("setup_probe")
|
|
468
|
-
@classmethod
|
|
469
|
-
def _nonempty_setup_probe(cls, v: str | None) -> str | None:
|
|
470
|
-
if v is not None and not v.strip():
|
|
471
|
-
raise ValueError("setup_probe must be a non-empty string")
|
|
472
|
-
return v
|
|
473
|
-
|
|
474
475
|
@model_validator(mode="after")
|
|
475
476
|
def _check_shape(self) -> "StepConfig":
|
|
476
477
|
for key in ("agent", "action", "command"):
|
|
@@ -566,15 +567,10 @@ class StepConfig(BaseModel):
|
|
|
566
567
|
raise ValueError(
|
|
567
568
|
f"step {self.name!r}: 'custom_prompt' requires 'agent'"
|
|
568
569
|
)
|
|
569
|
-
if
|
|
570
|
-
and self.agent is None and self.brief_inputs is None):
|
|
570
|
+
if self.setup is not None and self.command is None and self.agent is None:
|
|
571
571
|
raise ValueError(
|
|
572
|
-
f"step {self.name!r}: 'setup' is only allowed on 'command'
|
|
573
|
-
"'agent'
|
|
574
|
-
)
|
|
575
|
-
if self.setup_probe is not None and self.setup is None:
|
|
576
|
-
raise ValueError(
|
|
577
|
-
f"step {self.name!r}: 'setup_probe' requires 'setup'"
|
|
572
|
+
f"step {self.name!r}: 'setup' is only allowed on 'command' or "
|
|
573
|
+
"'agent' steps (052)"
|
|
578
574
|
)
|
|
579
575
|
if self.secrets and self.action is not None:
|
|
580
576
|
raise ValueError(
|
|
@@ -624,11 +620,10 @@ class PipelineConfig(BaseModel):
|
|
|
624
620
|
# missing-dependency fail-fast.
|
|
625
621
|
deps_policy: str = "locked"
|
|
626
622
|
lockfiles: list[str] = []
|
|
627
|
-
# 028: pipeline-declared toolchain caches (rw bind-mounts from the
|
|
628
|
-
#
|
|
629
|
-
#
|
|
630
|
-
#
|
|
631
|
-
# "no sync-back excludes".
|
|
623
|
+
# 028/052: pipeline-declared toolchain caches (rw bind-mounts from the
|
|
624
|
+
# host-side cache dir ``{isolation.state_dir}/caches/<name>``) and
|
|
625
|
+
# run-wide sync-back excludes (scratch semantics: skipped from both the
|
|
626
|
+
# export copy and sync-back). Both default to [].
|
|
632
627
|
caches: list[str] = []
|
|
633
628
|
sync_excludes: list[str] = []
|
|
634
629
|
# 029: append-style logs that sync-back union-merges on a concurrent
|
|
@@ -715,25 +710,6 @@ class PipelineConfig(BaseModel):
|
|
|
715
710
|
f"the host-only state_dir {self.state_dir!r}")
|
|
716
711
|
return self
|
|
717
712
|
|
|
718
|
-
@model_validator(mode="after")
|
|
719
|
-
def _caches_avoid_state_dir(self) -> "PipelineConfig":
|
|
720
|
-
"""028: a declared cache may never name (or sit under) the host-only
|
|
721
|
-
run-state dir - it would bind-mount the engine's own state into the
|
|
722
|
-
sandbox as a rw cache."""
|
|
723
|
-
sd = self.state_dir.strip().rstrip("/\\")
|
|
724
|
-
if not sd or sd in ("/", "."):
|
|
725
|
-
return self
|
|
726
|
-
sd_parts = relative_parts(sd)
|
|
727
|
-
if not sd_parts:
|
|
728
|
-
return self
|
|
729
|
-
for cache in self.caches:
|
|
730
|
-
cp = relative_parts(cache)
|
|
731
|
-
if cp[:len(sd_parts)] == sd_parts:
|
|
732
|
-
raise ValueError(
|
|
733
|
-
f"cache path {cache!r} names the host-only state_dir "
|
|
734
|
-
f"{self.state_dir!r}")
|
|
735
|
-
return self
|
|
736
|
-
|
|
737
713
|
@model_validator(mode="after")
|
|
738
714
|
def _mounts_avoid_caches(self) -> "PipelineConfig":
|
|
739
715
|
"""028/BR-1.3: a step mount equal to or under a declared cache is
|
|
@@ -799,21 +775,6 @@ class PipelineConfig(BaseModel):
|
|
|
799
775
|
f"{('/'.join(ep))!r}")
|
|
800
776
|
return self
|
|
801
777
|
|
|
802
|
-
@model_validator(mode="after")
|
|
803
|
-
def _sandbox_venv_not_declared_cache(self) -> "PipelineConfig":
|
|
804
|
-
"""028: the sandbox venv is engine infrastructure, never author data.
|
|
805
|
-
Declaring ``.venv-sandbox`` in ``caches:`` while the 022 relocation
|
|
806
|
-
(``isolation.sandbox_venv_dir``) is set is a contradiction."""
|
|
807
|
-
if self.isolation is None or self.isolation.sandbox_venv_dir is None:
|
|
808
|
-
return self
|
|
809
|
-
for cache in self.caches:
|
|
810
|
-
if relative_parts(cache) == (".venv-sandbox",):
|
|
811
|
-
raise ValueError(
|
|
812
|
-
"declaring '.venv-sandbox' in 'caches:' contradicts "
|
|
813
|
-
"'isolation.sandbox_venv_dir' - the sandbox venv is "
|
|
814
|
-
"relocated outside the workspace (022)")
|
|
815
|
-
return self
|
|
816
|
-
|
|
817
778
|
@model_validator(mode="after")
|
|
818
779
|
def _warn_shadowed_artifacts(self) -> "PipelineConfig":
|
|
819
780
|
"""028: an artifact/produces path matching a sync-exclude would never
|
|
@@ -14,8 +14,7 @@ from pathlib import Path
|
|
|
14
14
|
|
|
15
15
|
from agentic_dev_loop.core.config import PipelineConfig, load_config
|
|
16
16
|
from .containers import containers_report
|
|
17
|
-
from .isolation import
|
|
18
|
-
reap_orphaned_containers)
|
|
17
|
+
from .isolation import isolation_state_dir, reap_orphaned_containers
|
|
19
18
|
from .loaders import mark_plan_completed
|
|
20
19
|
from .registry import Registry, default_registry, load_extensions
|
|
21
20
|
|
|
@@ -82,15 +81,10 @@ def _core_parser() -> argparse.ArgumentParser:
|
|
|
82
81
|
help="preferred port for the live graph view "
|
|
83
82
|
"(default: 8321; falls back to a free port when "
|
|
84
83
|
"busy)")
|
|
85
|
-
parser.add_argument("--refresh", action="store_true",
|
|
86
|
-
help="force re-provision of every setup: hook "
|
|
87
|
-
"(bypasses the stamp cache)")
|
|
88
84
|
parser.add_argument("--dry-run", action="store_true",
|
|
89
85
|
help="validate the pipeline and print a summary plan "
|
|
90
86
|
"without executing anything (no LLM, no "
|
|
91
87
|
"sandbox, no state writes)")
|
|
92
|
-
parser.add_argument("--clean-stamps", action="store_true",
|
|
93
|
-
help="delete cached provision stamps and exit")
|
|
94
88
|
_add_version_flag(parser)
|
|
95
89
|
return parser
|
|
96
90
|
|
|
@@ -185,18 +179,12 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
185
179
|
for loader in registry.document_loaders:
|
|
186
180
|
loader.add_cli_args(parser)
|
|
187
181
|
args = parser.parse_args(argv)
|
|
188
|
-
if args.clean_stamps:
|
|
189
|
-
state = isolation_state_dir(cfg)
|
|
190
|
-
removed = clean_stamps(state)
|
|
191
|
-
print(f"Removed {removed} cached provision stamp(s) from "
|
|
192
|
-
f"{state / 'stamps'}", flush=True)
|
|
193
|
-
return 0
|
|
194
182
|
if args.dry_run:
|
|
195
183
|
from .dryrun import dry_run
|
|
196
184
|
return dry_run(args.pipeline, args, registry)
|
|
197
185
|
# The run path alone needs the heavy graph/SDK imports, and --dry-run
|
|
198
186
|
# needs the dryrun import chain in-branch; defer both so `containers`,
|
|
199
|
-
# --
|
|
187
|
+
# --help, and --version stay side-effect free. 042 D4: a
|
|
200
188
|
# SIGTERM here raises SystemExit(143) so graph.run_pipeline's finally
|
|
201
189
|
# still closes the sandbox; a second SIGTERM hard-exits.
|
|
202
190
|
previous = _install_sigterm_handler()
|
|
@@ -160,15 +160,14 @@ def run_pipeline(cfg_path, args, *, viz_bus=None,
|
|
|
160
160
|
if not state_dir.is_absolute():
|
|
161
161
|
state_dir = base / state_dir
|
|
162
162
|
|
|
163
|
-
#
|
|
164
|
-
#
|
|
165
|
-
#
|
|
163
|
+
# Startup (052): probe -> image -> create+start exec container. No setup
|
|
164
|
+
# runs here: each step entry provisions its own setup hook, fresh, into
|
|
165
|
+
# the export.
|
|
166
166
|
# 042 D3: startup is close-protected - a post-create provision failure
|
|
167
167
|
# must still reach close() and reap the container it created.
|
|
168
168
|
sandbox = None
|
|
169
169
|
try:
|
|
170
170
|
sandbox = startup_sandbox(cfg, workspace=Path.cwd(),
|
|
171
|
-
refresh=getattr(args, "refresh", False),
|
|
172
171
|
state_dir=state_dir)
|
|
173
172
|
return _run_pipeline(cfg, base, registry, state_dir, max_attempts,
|
|
174
173
|
args, viz_bus, sandbox, resolved_secrets,
|