agentic-dev-loop 1.0.0a10__tar.gz → 1.0.0a11__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.
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/PKG-INFO +74 -1
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/README.md +72 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/pyproject.toml +2 -1
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/core/config.py +30 -0
- agentic_dev_loop-1.0.0a11/src/agentic_dev_loop/engine/deps.py +77 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/stages.py +21 -2
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop.egg-info/PKG-INFO +74 -1
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop.egg-info/SOURCES.txt +2 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop.egg-info/requires.txt +1 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/tests/test_cli.py +4 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/tests/test_config.py +35 -0
- agentic_dev_loop-1.0.0a11/tests/test_deps.py +76 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/tests/test_dry_run.py +1 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/tests/test_graph.py +38 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/tests/test_hostboundary.py +1 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/tests/test_hostboundary_integration.py +1 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/tests/test_isolation.py +2 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/tests/test_secrets.py +1 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/tests/test_secrets_pipeline.py +2 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/tests/test_stages.py +52 -1
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/tests/test_viz.py +1 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/LICENSE +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/setup.cfg +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/core/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/core/results.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/core/verdict.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/actions.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/agents.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/cli.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/dryrun.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/graph.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/hostboundary.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/isolation.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/llm.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/loaders.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/outputs.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/proc.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/registry.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/secrets.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/py.typed +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/viz/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/viz/__main__.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/viz/server.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/tests/test_actions.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/tests/test_agents.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/tests/test_aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/tests/test_e2e_fixtures.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/tests/test_isolation_integration.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/tests/test_isolation_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/tests/test_llm.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/tests/test_loaders.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/tests/test_module_contract.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/tests/test_outputs.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/tests/test_outputs_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/tests/test_proc.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/tests/test_publish.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/tests/test_registry.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/tests/test_results.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/tests/test_sdk_compat.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/tests/test_secrets_integration.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/tests/test_source_resolution.py +0 -0
- {agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/tests/test_verdict.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentic-dev-loop
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0a11
|
|
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
|
|
@@ -35,6 +35,7 @@ Requires-Dist: langgraph-checkpoint-sqlite>=3.0
|
|
|
35
35
|
Provides-Extra: dev
|
|
36
36
|
Requires-Dist: hypothesis>=6.0; extra == "dev"
|
|
37
37
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
38
|
+
Requires-Dist: pip-audit>=2.0; extra == "dev"
|
|
38
39
|
Dynamic: license-file
|
|
39
40
|
|
|
40
41
|
# agentic-dev-loop
|
|
@@ -213,6 +214,8 @@ steps:
|
|
|
213
214
|
| `isolation:` (optional) | Configures the sandbox — image override, resource caps, `allow_git_mutations`. It can never disable it. |
|
|
214
215
|
| `state_dir` | Defaults to `~/.agentic-dev-loop/runs/<pipeline-hash>/` — artifacts and checkpoints live outside the sandbox mount. |
|
|
215
216
|
| `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. |
|
|
217
|
+
| `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). |
|
|
218
|
+
| `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. |
|
|
216
219
|
|
|
217
220
|
### Secrets (`secrets:`) — refs only, never values
|
|
218
221
|
|
|
@@ -278,6 +281,76 @@ steps:
|
|
|
278
281
|
for stages that need it (QA, integration). The residual risk is documented
|
|
279
282
|
in [docs/setup.md](docs/setup.md).
|
|
280
283
|
|
|
284
|
+
## Dependency policy (locked by default)
|
|
285
|
+
|
|
286
|
+
The supply-chain dial is a pipeline-level `deps_policy:` field, **locked by
|
|
287
|
+
default**. The engine injects the policy into every agent step's context —
|
|
288
|
+
the rule lives in the engine, not in hand-authored `dev.agent.md` prose (the
|
|
289
|
+
class of rule that deadlocked the NuGet cache).
|
|
290
|
+
|
|
291
|
+
```yaml
|
|
292
|
+
deps_policy: locked # or: open (default: locked)
|
|
293
|
+
lockfiles: [uv.lock] # or: [packages.lock.json, package-lock.json]
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
- **`locked`** — the human-owned lockfile is the source of truth; the agent
|
|
297
|
+
may not add dependencies. The injected rule says *"You may only use
|
|
298
|
+
dependencies already present in `<lockfile>`. If the design requires a
|
|
299
|
+
dependency that is not present, declare it in your final response under the
|
|
300
|
+
fixed header `## Missing dependencies` (one `name==version` per line) and
|
|
301
|
+
finish what you can. Do not add it to the manifest."* A non-empty
|
|
302
|
+
`## Missing dependencies` declaration **fails the run fast** with
|
|
303
|
+
`Extend <lockfile> with these pins and re-run: …` — one human lockfile edit,
|
|
304
|
+
not a retry loop. The declaration is advisory to the human; the engine never
|
|
305
|
+
auto-fetches the declared packages.
|
|
306
|
+
- **`open`** — the bootstrap protocol (018): the agent may add exact-pinned
|
|
307
|
+
manifest entries directly (never a package-manager `add` command); the
|
|
308
|
+
network-on restore step fetches them. The engine injects the same rule
|
|
309
|
+
canonically. Nothing about `open` is default.
|
|
310
|
+
|
|
311
|
+
The **restore step** runs the toolchain's locked mode and fails when the
|
|
312
|
+
manifest and lockfile disagree:
|
|
313
|
+
|
|
314
|
+
| Toolchain | Locked restore | Lockfile |
|
|
315
|
+
| --- | --- | --- |
|
|
316
|
+
| .NET | `dotnet restore --locked-mode` | `packages.lock.json` (set `RestorePackagesWithLockFile=true`) |
|
|
317
|
+
| node | `npm ci` | `package-lock.json` |
|
|
318
|
+
| python | `uv sync --locked` (or pip-tools `pip-sync requirements.lock`) | `uv.lock` — pip has no native lockfile; `uv` is the canonical locked-install tool |
|
|
319
|
+
|
|
320
|
+
### Audit escalation in the restore step
|
|
321
|
+
|
|
322
|
+
The restore step also fails on known-CVE findings, at a per-toolchain
|
|
323
|
+
threshold:
|
|
324
|
+
|
|
325
|
+
| Toolchain | Audit (fails the step) |
|
|
326
|
+
| --- | --- |
|
|
327
|
+
| .NET | `NuGetAudit` at `error` — any audit finding fails the restore |
|
|
328
|
+
| node | `npm audit --audit-level=high` |
|
|
329
|
+
| python | `pip-audit` — any finding fails |
|
|
330
|
+
|
|
331
|
+
Under `locked`, a failing audit means the lockfile pins a vulnerable version:
|
|
332
|
+
the run fails and the human regenerates the lockfile at a patched version.
|
|
333
|
+
Under `open`, the failure routes to `dev` like any restore failure — the dev
|
|
334
|
+
agent can bump the pin, but that pin is audited again on the next pass, so
|
|
335
|
+
the loop self-corrects.
|
|
336
|
+
|
|
337
|
+
### Generating a lockfile
|
|
338
|
+
|
|
339
|
+
A human generates the lockfile once, on a network machine, and commits it —
|
|
340
|
+
the engine never fetches on the agent's behalf:
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
uv lock # python — writes uv.lock from pyproject.toml
|
|
344
|
+
uv sync --locked # installs exactly uv.lock; fails on any drift
|
|
345
|
+
npm install --package-lock-only # node — writes package-lock.json
|
|
346
|
+
npm ci # installs exactly package-lock.json
|
|
347
|
+
dotnet restore --use-lock-file # .NET — writes packages.lock.json
|
|
348
|
+
dotnet restore --locked-mode # installs exactly packages.lock.json
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Keep the lockfile tracked (do not gitignore it) and re-run the locked restore
|
|
352
|
+
to prove it holds from a clean cache before setting `deps_policy: locked`.
|
|
353
|
+
|
|
281
354
|
## Host boundary (security model)
|
|
282
355
|
|
|
283
356
|
The orchestrator, tool layer, and the `commit` action run in the **host
|
|
@@ -174,6 +174,8 @@ steps:
|
|
|
174
174
|
| `isolation:` (optional) | Configures the sandbox — image override, resource caps, `allow_git_mutations`. It can never disable it. |
|
|
175
175
|
| `state_dir` | Defaults to `~/.agentic-dev-loop/runs/<pipeline-hash>/` — artifacts and checkpoints live outside the sandbox mount. |
|
|
176
176
|
| `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. |
|
|
177
|
+
| `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). |
|
|
178
|
+
| `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. |
|
|
177
179
|
|
|
178
180
|
### Secrets (`secrets:`) — refs only, never values
|
|
179
181
|
|
|
@@ -239,6 +241,76 @@ steps:
|
|
|
239
241
|
for stages that need it (QA, integration). The residual risk is documented
|
|
240
242
|
in [docs/setup.md](docs/setup.md).
|
|
241
243
|
|
|
244
|
+
## Dependency policy (locked by default)
|
|
245
|
+
|
|
246
|
+
The supply-chain dial is a pipeline-level `deps_policy:` field, **locked by
|
|
247
|
+
default**. The engine injects the policy into every agent step's context —
|
|
248
|
+
the rule lives in the engine, not in hand-authored `dev.agent.md` prose (the
|
|
249
|
+
class of rule that deadlocked the NuGet cache).
|
|
250
|
+
|
|
251
|
+
```yaml
|
|
252
|
+
deps_policy: locked # or: open (default: locked)
|
|
253
|
+
lockfiles: [uv.lock] # or: [packages.lock.json, package-lock.json]
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
- **`locked`** — the human-owned lockfile is the source of truth; the agent
|
|
257
|
+
may not add dependencies. The injected rule says *"You may only use
|
|
258
|
+
dependencies already present in `<lockfile>`. If the design requires a
|
|
259
|
+
dependency that is not present, declare it in your final response under the
|
|
260
|
+
fixed header `## Missing dependencies` (one `name==version` per line) and
|
|
261
|
+
finish what you can. Do not add it to the manifest."* A non-empty
|
|
262
|
+
`## Missing dependencies` declaration **fails the run fast** with
|
|
263
|
+
`Extend <lockfile> with these pins and re-run: …` — one human lockfile edit,
|
|
264
|
+
not a retry loop. The declaration is advisory to the human; the engine never
|
|
265
|
+
auto-fetches the declared packages.
|
|
266
|
+
- **`open`** — the bootstrap protocol (018): the agent may add exact-pinned
|
|
267
|
+
manifest entries directly (never a package-manager `add` command); the
|
|
268
|
+
network-on restore step fetches them. The engine injects the same rule
|
|
269
|
+
canonically. Nothing about `open` is default.
|
|
270
|
+
|
|
271
|
+
The **restore step** runs the toolchain's locked mode and fails when the
|
|
272
|
+
manifest and lockfile disagree:
|
|
273
|
+
|
|
274
|
+
| Toolchain | Locked restore | Lockfile |
|
|
275
|
+
| --- | --- | --- |
|
|
276
|
+
| .NET | `dotnet restore --locked-mode` | `packages.lock.json` (set `RestorePackagesWithLockFile=true`) |
|
|
277
|
+
| node | `npm ci` | `package-lock.json` |
|
|
278
|
+
| python | `uv sync --locked` (or pip-tools `pip-sync requirements.lock`) | `uv.lock` — pip has no native lockfile; `uv` is the canonical locked-install tool |
|
|
279
|
+
|
|
280
|
+
### Audit escalation in the restore step
|
|
281
|
+
|
|
282
|
+
The restore step also fails on known-CVE findings, at a per-toolchain
|
|
283
|
+
threshold:
|
|
284
|
+
|
|
285
|
+
| Toolchain | Audit (fails the step) |
|
|
286
|
+
| --- | --- |
|
|
287
|
+
| .NET | `NuGetAudit` at `error` — any audit finding fails the restore |
|
|
288
|
+
| node | `npm audit --audit-level=high` |
|
|
289
|
+
| python | `pip-audit` — any finding fails |
|
|
290
|
+
|
|
291
|
+
Under `locked`, a failing audit means the lockfile pins a vulnerable version:
|
|
292
|
+
the run fails and the human regenerates the lockfile at a patched version.
|
|
293
|
+
Under `open`, the failure routes to `dev` like any restore failure — the dev
|
|
294
|
+
agent can bump the pin, but that pin is audited again on the next pass, so
|
|
295
|
+
the loop self-corrects.
|
|
296
|
+
|
|
297
|
+
### Generating a lockfile
|
|
298
|
+
|
|
299
|
+
A human generates the lockfile once, on a network machine, and commits it —
|
|
300
|
+
the engine never fetches on the agent's behalf:
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
uv lock # python — writes uv.lock from pyproject.toml
|
|
304
|
+
uv sync --locked # installs exactly uv.lock; fails on any drift
|
|
305
|
+
npm install --package-lock-only # node — writes package-lock.json
|
|
306
|
+
npm ci # installs exactly package-lock.json
|
|
307
|
+
dotnet restore --use-lock-file # .NET — writes packages.lock.json
|
|
308
|
+
dotnet restore --locked-mode # installs exactly packages.lock.json
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Keep the lockfile tracked (do not gitignore it) and re-run the locked restore
|
|
312
|
+
to prove it holds from a clean cache before setting `deps_policy: locked`.
|
|
313
|
+
|
|
242
314
|
## Host boundary (security model)
|
|
243
315
|
|
|
244
316
|
The orchestrator, tool layer, and the `commit` action run in the **host
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agentic-dev-loop"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.0a11"
|
|
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"
|
|
@@ -55,6 +55,7 @@ agentic-dev-loop = "agentic_dev_loop.engine.cli:main"
|
|
|
55
55
|
dev = [
|
|
56
56
|
"hypothesis>=6.0",
|
|
57
57
|
"pytest>=8.0",
|
|
58
|
+
"pip-audit>=2.0",
|
|
58
59
|
]
|
|
59
60
|
|
|
60
61
|
[tool.setuptools.packages.find]
|
|
@@ -214,6 +214,27 @@ class PipelineConfig(BaseModel):
|
|
|
214
214
|
max_attempts: int = 3
|
|
215
215
|
agents_dir: str
|
|
216
216
|
steps: list[StepConfig] = Field(min_length=1)
|
|
217
|
+
# 021 supply-chain dependency dial: `locked` (default) = the human-owned
|
|
218
|
+
# lockfile is the source of truth and the agent may not add dependencies;
|
|
219
|
+
# `open` = the bootstrap protocol (the agent may add exact-pinned manifest
|
|
220
|
+
# entries for the restore step to fetch). `lockfiles` names the pipeline's
|
|
221
|
+
# declared lockfile(s) — injected into agent context and named by the
|
|
222
|
+
# missing-dependency fail-fast.
|
|
223
|
+
deps_policy: str = "locked"
|
|
224
|
+
lockfiles: list[str] = []
|
|
225
|
+
|
|
226
|
+
@field_validator("deps_policy")
|
|
227
|
+
@classmethod
|
|
228
|
+
def _known_deps_policy(cls, v: str) -> str:
|
|
229
|
+
if v not in ("locked", "open"):
|
|
230
|
+
raise ValueError(
|
|
231
|
+
f"deps_policy must be 'locked' or 'open', got {v!r}")
|
|
232
|
+
return v
|
|
233
|
+
|
|
234
|
+
@field_validator("lockfiles", mode="before")
|
|
235
|
+
@classmethod
|
|
236
|
+
def _coerce_lockfiles(cls, v: Any) -> Any:
|
|
237
|
+
return [v] if isinstance(v, str) else v
|
|
217
238
|
|
|
218
239
|
@field_validator("secrets", mode="before")
|
|
219
240
|
@classmethod
|
|
@@ -232,6 +253,15 @@ class PipelineConfig(BaseModel):
|
|
|
232
253
|
"values never belong in the pipeline yaml")
|
|
233
254
|
return v
|
|
234
255
|
|
|
256
|
+
@model_validator(mode="after")
|
|
257
|
+
def _locked_requires_lockfiles(self) -> "PipelineConfig":
|
|
258
|
+
if self.deps_policy == "locked" and not self.lockfiles:
|
|
259
|
+
raise ValueError(
|
|
260
|
+
"deps_policy 'locked' requires at least one entry in "
|
|
261
|
+
"'lockfiles:' — name the lockfile(s) the locked boundary "
|
|
262
|
+
"enforces, or set 'deps_policy: open'")
|
|
263
|
+
return self
|
|
264
|
+
|
|
235
265
|
@model_validator(mode="after")
|
|
236
266
|
def _check_graph(self) -> "PipelineConfig":
|
|
237
267
|
names = [s.name for s in self.steps]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Supply-chain dependency dial (021): canonical policy injection and
|
|
2
|
+
missing-dependency declaration parsing.
|
|
3
|
+
|
|
4
|
+
The rule text lives HERE, in the engine, not in consumer `dev.agent.md` prose
|
|
5
|
+
— the hand-authored rule is exactly the class that deadlocked the NuGet cache
|
|
6
|
+
in 018. `deps_policy: locked` (the default) bounds the agent to the
|
|
7
|
+
human-owned lockfile and fails a run fast when an agent declares a missing
|
|
8
|
+
dependency; `deps_policy: open` injects the bootstrap protocol (the agent may
|
|
9
|
+
add exact-pinned manifest entries for the restore step to fetch).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from agentic_dev_loop.core.config import PipelineConfig
|
|
18
|
+
|
|
19
|
+
MISSING_DEPS_HEADER = "## Missing dependencies"
|
|
20
|
+
|
|
21
|
+
_LOCKED_TEMPLATE = (
|
|
22
|
+
"You may only use dependencies already present in {lockfiles}. If the "
|
|
23
|
+
"design requires a dependency that is not present, declare it in your "
|
|
24
|
+
"final response under the fixed header `{header}` (one `name==version` "
|
|
25
|
+
"per line) and finish what you can. Do not add it to the manifest."
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
_OPEN_TEXT = (
|
|
29
|
+
"If the design needs a dependency that is not already present, add the "
|
|
30
|
+
"exact-pinned entry to the dependency manifest directly (do not run a "
|
|
31
|
+
"package-manager `add` command — it may need the network). Your offline "
|
|
32
|
+
"builds may fail until the restore step fetches it — that is expected; "
|
|
33
|
+
"write the code to match and leave the manifest entry in place."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _lockfile_label(cfg: "PipelineConfig") -> str:
|
|
38
|
+
if cfg.lockfiles:
|
|
39
|
+
return ", ".join(f"`{name}`" for name in cfg.lockfiles)
|
|
40
|
+
return "the lockfile"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def policy_injection(cfg: "PipelineConfig") -> str:
|
|
44
|
+
"""The canonical dependency-policy paragraph injected into every agent
|
|
45
|
+
step's context, keyed on ``deps_policy`` and the declared lockfiles."""
|
|
46
|
+
if cfg.deps_policy == "open":
|
|
47
|
+
return _OPEN_TEXT
|
|
48
|
+
return _LOCKED_TEMPLATE.format(
|
|
49
|
+
lockfiles=_lockfile_label(cfg), header=MISSING_DEPS_HEADER)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def parse_missing_dependencies(text: str | None) -> list[str]:
|
|
53
|
+
"""Parse a final agent response for the fixed ``## Missing dependencies``
|
|
54
|
+
section. Returns the declared pins (one non-empty line each) beneath the
|
|
55
|
+
header, until the next ``## `` section or end of text. An absent header or
|
|
56
|
+
an empty/malformed section yields an empty list — only a non-empty
|
|
57
|
+
declaration triggers the fail-fast."""
|
|
58
|
+
if not text:
|
|
59
|
+
return []
|
|
60
|
+
lines = text.splitlines()
|
|
61
|
+
for i, line in enumerate(lines):
|
|
62
|
+
if line.strip() == MISSING_DEPS_HEADER:
|
|
63
|
+
pins: list[str] = []
|
|
64
|
+
for rest in lines[i + 1:]:
|
|
65
|
+
if rest.startswith("## "):
|
|
66
|
+
break
|
|
67
|
+
stripped = rest.strip()
|
|
68
|
+
if stripped:
|
|
69
|
+
pins.append(stripped)
|
|
70
|
+
return pins
|
|
71
|
+
return []
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def extension_message(cfg: "PipelineConfig", pins: list[str]) -> str:
|
|
75
|
+
"""The fail-fast message naming the lockfile(s) and the declared pins."""
|
|
76
|
+
return (f"Extend {_lockfile_label(cfg)} with these pins and re-run:\n\n"
|
|
77
|
+
+ "\n".join(pins))
|
{agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/stages.py
RENAMED
|
@@ -32,6 +32,7 @@ from openhands.sdk import Conversation
|
|
|
32
32
|
|
|
33
33
|
from .agents import load_agent, register_sandbox_terminal
|
|
34
34
|
from agentic_dev_loop.core.config import TERMINALS, PipelineConfig, Route, StepConfig, route_target
|
|
35
|
+
from .deps import extension_message, parse_missing_dependencies, policy_injection
|
|
35
36
|
from .isolation import SandboxExecutionRequest, command_argv, resolve_sandbox_env
|
|
36
37
|
from .outputs import CapturedOutput
|
|
37
38
|
from .secrets import redact_values, step_secret_env
|
|
@@ -198,11 +199,13 @@ def _run_command(command: str, workspace: Path, timeout: float,
|
|
|
198
199
|
def build_context(runtime: Runtime, docs: dict[str, str], subject: str,
|
|
199
200
|
notes: list[str] | None = None) -> str:
|
|
200
201
|
"""Assemble the attempt's shared message: subject, doc bundle, the
|
|
201
|
-
|
|
202
|
-
|
|
202
|
+
canonical dependency-policy paragraph (021), the expected-output-paths
|
|
203
|
+
section generated from step defs (a prompt merely naming files lets agents
|
|
204
|
+
skip them), and previous-attempt notes."""
|
|
203
205
|
parts = [f"You are working on {subject}.\n"]
|
|
204
206
|
for name, content in docs.items():
|
|
205
207
|
parts.append(f"## {name}\n\n{content}")
|
|
208
|
+
parts.append("## Dependency policy\n\n" + policy_injection(runtime.cfg))
|
|
206
209
|
lines = []
|
|
207
210
|
for step in runtime.cfg.steps:
|
|
208
211
|
if step.agent is None:
|
|
@@ -301,6 +304,22 @@ def make_agent_node(runtime: Runtime, step: StepConfig):
|
|
|
301
304
|
result.truncated, max_iterations)],
|
|
302
305
|
"retry_target": _unclear_target(runtime, step)}
|
|
303
306
|
|
|
307
|
+
# 021: under `locked`, a declared missing dependency is a fail-fast —
|
|
308
|
+
# one human lockfile edit, never a retry loop. The extension message
|
|
309
|
+
# is written to last-error.md (the run-failure channel) and printed.
|
|
310
|
+
if (runtime.cfg.deps_policy == "locked"
|
|
311
|
+
and result.response is not None):
|
|
312
|
+
pins = parse_missing_dependencies(result.response)
|
|
313
|
+
if pins:
|
|
314
|
+
msg = extension_message(runtime.cfg, pins)
|
|
315
|
+
runtime.state_dir.mkdir(parents=True, exist_ok=True)
|
|
316
|
+
(runtime.state_dir / LAST_ERROR).write_text(
|
|
317
|
+
f"# Missing dependencies\n\n```\n{msg}\n```\n",
|
|
318
|
+
encoding="utf-8")
|
|
319
|
+
print(redact_values(msg, runtime.secrets.values()),
|
|
320
|
+
file=sys.stderr, flush=True)
|
|
321
|
+
return {"failed": True, "failed_step": step.name}
|
|
322
|
+
|
|
304
323
|
if verdicts_map is None:
|
|
305
324
|
merged = {**state["step_verdicts"], step.name: "pass"} if captures else \
|
|
306
325
|
state["step_verdicts"]
|
{agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentic-dev-loop
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0a11
|
|
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
|
|
@@ -35,6 +35,7 @@ Requires-Dist: langgraph-checkpoint-sqlite>=3.0
|
|
|
35
35
|
Provides-Extra: dev
|
|
36
36
|
Requires-Dist: hypothesis>=6.0; extra == "dev"
|
|
37
37
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
38
|
+
Requires-Dist: pip-audit>=2.0; extra == "dev"
|
|
38
39
|
Dynamic: license-file
|
|
39
40
|
|
|
40
41
|
# agentic-dev-loop
|
|
@@ -213,6 +214,8 @@ steps:
|
|
|
213
214
|
| `isolation:` (optional) | Configures the sandbox — image override, resource caps, `allow_git_mutations`. It can never disable it. |
|
|
214
215
|
| `state_dir` | Defaults to `~/.agentic-dev-loop/runs/<pipeline-hash>/` — artifacts and checkpoints live outside the sandbox mount. |
|
|
215
216
|
| `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. |
|
|
217
|
+
| `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). |
|
|
218
|
+
| `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. |
|
|
216
219
|
|
|
217
220
|
### Secrets (`secrets:`) — refs only, never values
|
|
218
221
|
|
|
@@ -278,6 +281,76 @@ steps:
|
|
|
278
281
|
for stages that need it (QA, integration). The residual risk is documented
|
|
279
282
|
in [docs/setup.md](docs/setup.md).
|
|
280
283
|
|
|
284
|
+
## Dependency policy (locked by default)
|
|
285
|
+
|
|
286
|
+
The supply-chain dial is a pipeline-level `deps_policy:` field, **locked by
|
|
287
|
+
default**. The engine injects the policy into every agent step's context —
|
|
288
|
+
the rule lives in the engine, not in hand-authored `dev.agent.md` prose (the
|
|
289
|
+
class of rule that deadlocked the NuGet cache).
|
|
290
|
+
|
|
291
|
+
```yaml
|
|
292
|
+
deps_policy: locked # or: open (default: locked)
|
|
293
|
+
lockfiles: [uv.lock] # or: [packages.lock.json, package-lock.json]
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
- **`locked`** — the human-owned lockfile is the source of truth; the agent
|
|
297
|
+
may not add dependencies. The injected rule says *"You may only use
|
|
298
|
+
dependencies already present in `<lockfile>`. If the design requires a
|
|
299
|
+
dependency that is not present, declare it in your final response under the
|
|
300
|
+
fixed header `## Missing dependencies` (one `name==version` per line) and
|
|
301
|
+
finish what you can. Do not add it to the manifest."* A non-empty
|
|
302
|
+
`## Missing dependencies` declaration **fails the run fast** with
|
|
303
|
+
`Extend <lockfile> with these pins and re-run: …` — one human lockfile edit,
|
|
304
|
+
not a retry loop. The declaration is advisory to the human; the engine never
|
|
305
|
+
auto-fetches the declared packages.
|
|
306
|
+
- **`open`** — the bootstrap protocol (018): the agent may add exact-pinned
|
|
307
|
+
manifest entries directly (never a package-manager `add` command); the
|
|
308
|
+
network-on restore step fetches them. The engine injects the same rule
|
|
309
|
+
canonically. Nothing about `open` is default.
|
|
310
|
+
|
|
311
|
+
The **restore step** runs the toolchain's locked mode and fails when the
|
|
312
|
+
manifest and lockfile disagree:
|
|
313
|
+
|
|
314
|
+
| Toolchain | Locked restore | Lockfile |
|
|
315
|
+
| --- | --- | --- |
|
|
316
|
+
| .NET | `dotnet restore --locked-mode` | `packages.lock.json` (set `RestorePackagesWithLockFile=true`) |
|
|
317
|
+
| node | `npm ci` | `package-lock.json` |
|
|
318
|
+
| python | `uv sync --locked` (or pip-tools `pip-sync requirements.lock`) | `uv.lock` — pip has no native lockfile; `uv` is the canonical locked-install tool |
|
|
319
|
+
|
|
320
|
+
### Audit escalation in the restore step
|
|
321
|
+
|
|
322
|
+
The restore step also fails on known-CVE findings, at a per-toolchain
|
|
323
|
+
threshold:
|
|
324
|
+
|
|
325
|
+
| Toolchain | Audit (fails the step) |
|
|
326
|
+
| --- | --- |
|
|
327
|
+
| .NET | `NuGetAudit` at `error` — any audit finding fails the restore |
|
|
328
|
+
| node | `npm audit --audit-level=high` |
|
|
329
|
+
| python | `pip-audit` — any finding fails |
|
|
330
|
+
|
|
331
|
+
Under `locked`, a failing audit means the lockfile pins a vulnerable version:
|
|
332
|
+
the run fails and the human regenerates the lockfile at a patched version.
|
|
333
|
+
Under `open`, the failure routes to `dev` like any restore failure — the dev
|
|
334
|
+
agent can bump the pin, but that pin is audited again on the next pass, so
|
|
335
|
+
the loop self-corrects.
|
|
336
|
+
|
|
337
|
+
### Generating a lockfile
|
|
338
|
+
|
|
339
|
+
A human generates the lockfile once, on a network machine, and commits it —
|
|
340
|
+
the engine never fetches on the agent's behalf:
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
uv lock # python — writes uv.lock from pyproject.toml
|
|
344
|
+
uv sync --locked # installs exactly uv.lock; fails on any drift
|
|
345
|
+
npm install --package-lock-only # node — writes package-lock.json
|
|
346
|
+
npm ci # installs exactly package-lock.json
|
|
347
|
+
dotnet restore --use-lock-file # .NET — writes packages.lock.json
|
|
348
|
+
dotnet restore --locked-mode # installs exactly packages.lock.json
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Keep the lockfile tracked (do not gitignore it) and re-run the locked restore
|
|
352
|
+
to prove it holds from a clean cache before setting `deps_policy: locked`.
|
|
353
|
+
|
|
281
354
|
## Host boundary (security model)
|
|
282
355
|
|
|
283
356
|
The orchestrator, tool layer, and the `commit` action run in the **host
|
{agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop.egg-info/SOURCES.txt
RENAMED
|
@@ -17,6 +17,7 @@ src/agentic_dev_loop/engine/__init__.py
|
|
|
17
17
|
src/agentic_dev_loop/engine/actions.py
|
|
18
18
|
src/agentic_dev_loop/engine/agents.py
|
|
19
19
|
src/agentic_dev_loop/engine/cli.py
|
|
20
|
+
src/agentic_dev_loop/engine/deps.py
|
|
20
21
|
src/agentic_dev_loop/engine/dryrun.py
|
|
21
22
|
src/agentic_dev_loop/engine/graph.py
|
|
22
23
|
src/agentic_dev_loop/engine/hostboundary.py
|
|
@@ -38,6 +39,7 @@ tests/test_agents.py
|
|
|
38
39
|
tests/test_aidlc.py
|
|
39
40
|
tests/test_cli.py
|
|
40
41
|
tests/test_config.py
|
|
42
|
+
tests/test_deps.py
|
|
41
43
|
tests/test_dry_run.py
|
|
42
44
|
tests/test_e2e_fixtures.py
|
|
43
45
|
tests/test_graph.py
|
|
@@ -22,6 +22,7 @@ llm:
|
|
|
22
22
|
api_key_env: TEST_LLM_KEY
|
|
23
23
|
state_dir: .pr
|
|
24
24
|
agents_dir: sdk_agents
|
|
25
|
+
deps_policy: open
|
|
25
26
|
steps:
|
|
26
27
|
- name: dev
|
|
27
28
|
agent: dev
|
|
@@ -37,6 +38,7 @@ llm:
|
|
|
37
38
|
api_key_env: TEST_LLM_KEY
|
|
38
39
|
state_dir: .pr
|
|
39
40
|
agents_dir: sdk_agents
|
|
41
|
+
deps_policy: open
|
|
40
42
|
steps:
|
|
41
43
|
- name: dev
|
|
42
44
|
agent: dev
|
|
@@ -58,6 +60,7 @@ llm:
|
|
|
58
60
|
api_key_env: TEST_LLM_KEY
|
|
59
61
|
max_attempts: 1
|
|
60
62
|
agents_dir: sdk_agents
|
|
63
|
+
deps_policy: open
|
|
61
64
|
steps:
|
|
62
65
|
- name: dev
|
|
63
66
|
agent: dev
|
|
@@ -329,6 +332,7 @@ llm:
|
|
|
329
332
|
api_key_env: TEST_LLM_KEY
|
|
330
333
|
state_dir: .pr
|
|
331
334
|
agents_dir: sdk_agents
|
|
335
|
+
deps_policy: open
|
|
332
336
|
isolation:
|
|
333
337
|
state_dir: {iso_state_dir}
|
|
334
338
|
steps:
|
|
@@ -29,6 +29,7 @@ llm:
|
|
|
29
29
|
state_dir: .pr
|
|
30
30
|
max_attempts: 3
|
|
31
31
|
agents_dir: sdk_agents
|
|
32
|
+
deps_policy: open
|
|
32
33
|
|
|
33
34
|
steps:
|
|
34
35
|
- name: dev
|
|
@@ -69,6 +70,7 @@ def _base() -> dict:
|
|
|
69
70
|
"name": "test-pipeline",
|
|
70
71
|
"llm": {"model": "m", "api_key_env": "K"},
|
|
71
72
|
"agents_dir": "sdk_agents",
|
|
73
|
+
"lockfiles": ["uv.lock"],
|
|
72
74
|
"steps": [{"name": "dev", "agent": "dev", "on_pass": "success"}],
|
|
73
75
|
}
|
|
74
76
|
|
|
@@ -494,3 +496,36 @@ class TestStepNetwork:
|
|
|
494
496
|
data["steps"] = [{"name": "commit", "action": "commit", "network": False,
|
|
495
497
|
"on_pass": "success"}]
|
|
496
498
|
assert _load(tmp_path, data).steps[0].network is False
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
class TestDepsPolicy:
|
|
502
|
+
def test_defaults_to_locked_with_declared_lockfiles(self, tmp_path):
|
|
503
|
+
cfg = _load(tmp_path, _base())
|
|
504
|
+
assert cfg.deps_policy == "locked"
|
|
505
|
+
assert cfg.lockfiles == ["uv.lock"]
|
|
506
|
+
|
|
507
|
+
def test_locked_without_lockfiles_rejected(self, tmp_path):
|
|
508
|
+
data = {k: v for k, v in _base().items() if k != "lockfiles"}
|
|
509
|
+
with pytest.raises(ValidationError,
|
|
510
|
+
match="requires at least one entry"):
|
|
511
|
+
_load(tmp_path, data)
|
|
512
|
+
|
|
513
|
+
def test_open_policy_loads_without_lockfiles(self, tmp_path):
|
|
514
|
+
data = {k: v for k, v in _base().items() if k != "lockfiles"} \
|
|
515
|
+
| {"deps_policy": "open"}
|
|
516
|
+
assert _load(tmp_path, data).deps_policy == "open"
|
|
517
|
+
assert _load(tmp_path, data).lockfiles == []
|
|
518
|
+
|
|
519
|
+
def test_unknown_policy_rejected(self, tmp_path):
|
|
520
|
+
data = _base() | {"deps_policy": "bootstrap"}
|
|
521
|
+
with pytest.raises(ValidationError,
|
|
522
|
+
match="deps_policy must be 'locked' or 'open'"):
|
|
523
|
+
_load(tmp_path, data)
|
|
524
|
+
|
|
525
|
+
def test_lockfiles_accepts_a_list(self, tmp_path):
|
|
526
|
+
data = _base() | {"lockfiles": ["uv.lock", "package-lock.json"]}
|
|
527
|
+
assert _load(tmp_path, data).lockfiles == ["uv.lock", "package-lock.json"]
|
|
528
|
+
|
|
529
|
+
def test_lockfiles_accepts_a_single_string(self, tmp_path):
|
|
530
|
+
data = _base() | {"lockfiles": "uv.lock"}
|
|
531
|
+
assert _load(tmp_path, data).lockfiles == ["uv.lock"]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Dependency-policy injection and missing-dependency parsing (021)."""
|
|
2
|
+
|
|
3
|
+
from agentic_dev_loop.core.config import PipelineConfig
|
|
4
|
+
from agentic_dev_loop.engine.deps import (
|
|
5
|
+
MISSING_DEPS_HEADER, extension_message, parse_missing_dependencies,
|
|
6
|
+
policy_injection)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _cfg(**over):
|
|
10
|
+
base = {"name": "t", "llm": {"model": "m", "api_key_env": "K"},
|
|
11
|
+
"agents_dir": "a", "lockfiles": ["uv.lock"],
|
|
12
|
+
"steps": [{"name": "d", "agent": "dev", "on_pass": "success"}]}
|
|
13
|
+
base.update(over)
|
|
14
|
+
return PipelineConfig.model_validate(base)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestPolicyInjection:
|
|
18
|
+
def test_locked_names_declared_lockfiles(self):
|
|
19
|
+
cfg = _cfg(deps_policy="locked", lockfiles=["uv.lock"])
|
|
20
|
+
text = policy_injection(cfg)
|
|
21
|
+
assert "`uv.lock`" in text
|
|
22
|
+
assert MISSING_DEPS_HEADER in text
|
|
23
|
+
assert "Do not add it to the manifest." in text
|
|
24
|
+
|
|
25
|
+
def test_locked_with_multiple_lockfiles_joins_them(self):
|
|
26
|
+
cfg = _cfg(lockfiles=["packages.lock.json", "package-lock.json"])
|
|
27
|
+
text = policy_injection(cfg)
|
|
28
|
+
assert "`packages.lock.json`, `package-lock.json`" in text
|
|
29
|
+
|
|
30
|
+
def test_open_injects_bootstrap_protocol(self):
|
|
31
|
+
cfg = _cfg(deps_policy="open")
|
|
32
|
+
text = policy_injection(cfg)
|
|
33
|
+
assert "add the exact-pinned entry to the dependency manifest" in text
|
|
34
|
+
assert MISSING_DEPS_HEADER not in text
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TestParseMissingDependencies:
|
|
38
|
+
def test_header_with_pins(self):
|
|
39
|
+
text = ("Done.\n\n"
|
|
40
|
+
+ MISSING_DEPS_HEADER + "\n\nrequests==2.32.0\n"
|
|
41
|
+
"pydantic==2.7.0\n")
|
|
42
|
+
assert parse_missing_dependencies(text) == \
|
|
43
|
+
["requests==2.32.0", "pydantic==2.7.0"]
|
|
44
|
+
|
|
45
|
+
def test_header_absent(self):
|
|
46
|
+
assert parse_missing_dependencies("No dependencies needed.") == []
|
|
47
|
+
|
|
48
|
+
def test_none_text(self):
|
|
49
|
+
assert parse_missing_dependencies(None) == []
|
|
50
|
+
|
|
51
|
+
def test_header_present_but_empty(self):
|
|
52
|
+
text = f"All done.\n\n{MISSING_DEPS_HEADER}\n\n\n"
|
|
53
|
+
assert parse_missing_dependencies(text) == []
|
|
54
|
+
|
|
55
|
+
def test_section_ends_at_next_header(self):
|
|
56
|
+
text = (MISSING_DEPS_HEADER + "\nrequests==2.32.0\n"
|
|
57
|
+
"## Summary\n\nDone.\n")
|
|
58
|
+
assert parse_missing_dependencies(text) == ["requests==2.32.0"]
|
|
59
|
+
|
|
60
|
+
def test_header_name_is_exact(self):
|
|
61
|
+
# Only the fixed header matches — decoration or case drift is malformed.
|
|
62
|
+
assert parse_missing_dependencies("### Missing dependencies\nfoo==1.0\n") == []
|
|
63
|
+
assert parse_missing_dependencies("## missing dependencies\nfoo==1.0\n") == []
|
|
64
|
+
|
|
65
|
+
def test_blank_and_padded_lines_are_pins(self):
|
|
66
|
+
text = f"{MISSING_DEPS_HEADER}\n\n requests==2.32.0 \n"
|
|
67
|
+
assert parse_missing_dependencies(text) == ["requests==2.32.0"]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TestExtensionMessage:
|
|
71
|
+
def test_names_lockfile_and_pins(self):
|
|
72
|
+
cfg = _cfg(lockfiles=["uv.lock"])
|
|
73
|
+
msg = extension_message(cfg, ["requests==2.32.0"])
|
|
74
|
+
assert msg == ("Extend `uv.lock` with these pins and re-run:\n\n"
|
|
75
|
+
"requests==2.32.0")
|
|
76
|
+
|
|
@@ -34,6 +34,7 @@ llm:
|
|
|
34
34
|
state_dir: .pr
|
|
35
35
|
max_attempts: 3
|
|
36
36
|
agents_dir: sdk_agents
|
|
37
|
+
deps_policy: open
|
|
37
38
|
steps:
|
|
38
39
|
- name: dev
|
|
39
40
|
agent: dev
|
|
@@ -76,6 +77,7 @@ llm:
|
|
|
76
77
|
state_dir: .pr
|
|
77
78
|
max_attempts: 3
|
|
78
79
|
agents_dir: sdk_agents
|
|
80
|
+
deps_policy: open
|
|
79
81
|
steps:
|
|
80
82
|
- name: dev
|
|
81
83
|
agent: dev
|
|
@@ -265,6 +267,42 @@ class TestRunPipeline:
|
|
|
265
267
|
assert fake_agents.calls == []
|
|
266
268
|
|
|
267
269
|
|
|
270
|
+
LOCKED_YAML = YOSK_YAML.replace(
|
|
271
|
+
"deps_policy: open\n",
|
|
272
|
+
"deps_policy: locked\nlockfiles: [uv.lock]\n",
|
|
273
|
+
1)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class TestDependencyDial:
|
|
277
|
+
def _write_locked(self, repo):
|
|
278
|
+
(repo / "locked.yaml").write_text(LOCKED_YAML)
|
|
279
|
+
git(repo, "add", "-A")
|
|
280
|
+
git(repo, "commit", "-m", "add locked pipeline")
|
|
281
|
+
|
|
282
|
+
def test_missing_dependency_fails_fast_not_retry_loop(self, repo,
|
|
283
|
+
fake_agents):
|
|
284
|
+
self._write_locked(repo)
|
|
285
|
+
fake_agents.set("dev", [("write", "implementation-summary.md",
|
|
286
|
+
"## Missing dependencies\n\nrequests==2.32.0\n")])
|
|
287
|
+
ok, attempts = run_pipeline("locked.yaml", args())
|
|
288
|
+
assert (ok, attempts) == (False, 1)
|
|
289
|
+
assert fake_agents.agents_called() == ["dev"]
|
|
290
|
+
err = (repo / ".pr" / "last-error.md").read_text()
|
|
291
|
+
assert "Extend `uv.lock`" in err
|
|
292
|
+
assert "requests==2.32.0" in err
|
|
293
|
+
|
|
294
|
+
def test_locked_context_names_lockfile_in_dev_message(self, repo,
|
|
295
|
+
fake_agents):
|
|
296
|
+
self._write_locked(repo)
|
|
297
|
+
script_all_pass(fake_agents)
|
|
298
|
+
ok, attempts = run_pipeline("locked.yaml", args())
|
|
299
|
+
assert (ok, attempts) == (True, 1)
|
|
300
|
+
(dev_msg,) = fake_agents.messages_for("dev")
|
|
301
|
+
assert "## Dependency policy" in dev_msg
|
|
302
|
+
assert "`uv.lock`" in dev_msg
|
|
303
|
+
assert "Do not add it to the manifest." in dev_msg
|
|
304
|
+
|
|
305
|
+
|
|
268
306
|
class TestCommandStep:
|
|
269
307
|
def _write(self, repo: Path, text: str) -> None:
|
|
270
308
|
(repo / "commandstage.yaml").write_text(text)
|
|
@@ -21,6 +21,7 @@ def _cfg(agents_dir="sdk_agents", additional_files=()):
|
|
|
21
21
|
"llm": {"model": "m", "api_key_env": "K"},
|
|
22
22
|
"agents_dir": agents_dir,
|
|
23
23
|
"additional_files": list(additional_files),
|
|
24
|
+
"lockfiles": ["uv.lock"],
|
|
24
25
|
"steps": [{"name": "dev", "agent": "dev", "on_pass": "success"}],
|
|
25
26
|
})
|
|
26
27
|
|
{agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/tests/test_hostboundary_integration.py
RENAMED
|
@@ -60,6 +60,7 @@ def _runtime(backend, ws, script, tmp_path):
|
|
|
60
60
|
"name": "probe",
|
|
61
61
|
"llm": {"model": "openai/test-model", "api_key_env": "K"},
|
|
62
62
|
"agents_dir": "agents",
|
|
63
|
+
"lockfiles": ["uv.lock"],
|
|
63
64
|
"steps": [{"name": "probe", "action": "probe", "on_pass": "success"}],
|
|
64
65
|
})
|
|
65
66
|
runtime = Runtime(cfg=cfg, registry=reg,
|
|
@@ -790,6 +790,7 @@ class TestStartupSandbox:
|
|
|
790
790
|
return PipelineConfig.model_validate({
|
|
791
791
|
"name": "p", "llm": {"model": "m", "api_key_env": "K"},
|
|
792
792
|
"agents_dir": "agents", "steps": steps,
|
|
793
|
+
"lockfiles": ["uv.lock"],
|
|
793
794
|
"isolation": {"state_dir": str(tmp_path / "iso-state")},
|
|
794
795
|
})
|
|
795
796
|
|
|
@@ -835,6 +836,7 @@ class TestStartupSandbox:
|
|
|
835
836
|
cfg = PipelineConfig.model_validate({
|
|
836
837
|
"name": "p", "llm": {"model": "m", "api_key_env": "K"},
|
|
837
838
|
"agents_dir": "agents",
|
|
839
|
+
"lockfiles": ["uv.lock"],
|
|
838
840
|
"steps": [{"name": "dev", "agent": "dev", "on_pass": "test"},
|
|
839
841
|
{"name": "test", "command": "make", "network": True,
|
|
840
842
|
"on_pass": "success"}],
|
|
@@ -19,6 +19,7 @@ llm:
|
|
|
19
19
|
model: m
|
|
20
20
|
api_key_env: TEST_LLM_KEY
|
|
21
21
|
agents_dir: agents
|
|
22
|
+
deps_policy: open
|
|
22
23
|
secrets:
|
|
23
24
|
probe-secret:
|
|
24
25
|
from: env
|
|
@@ -37,6 +38,7 @@ llm:
|
|
|
37
38
|
model: m
|
|
38
39
|
api_key_env: TEST_LLM_KEY
|
|
39
40
|
agents_dir: agents
|
|
41
|
+
deps_policy: open
|
|
40
42
|
steps:
|
|
41
43
|
- name: dev
|
|
42
44
|
agent: dev
|
|
@@ -24,7 +24,7 @@ from agentic_dev_loop.engine.stages import (
|
|
|
24
24
|
|
|
25
25
|
def make_cfg(steps, **over):
|
|
26
26
|
base = {"name": "t", "llm": {"model": "m", "api_key_env": "K"},
|
|
27
|
-
"agents_dir": "agents", "steps": steps}
|
|
27
|
+
"agents_dir": "agents", "lockfiles": ["uv.lock"], "steps": steps}
|
|
28
28
|
base.update(over)
|
|
29
29
|
return PipelineConfig.model_validate(base)
|
|
30
30
|
|
|
@@ -115,6 +115,57 @@ class TestBuildContext:
|
|
|
115
115
|
assert "test agent writes:" not in ctx
|
|
116
116
|
|
|
117
117
|
|
|
118
|
+
class TestDepsPolicyContext:
|
|
119
|
+
def test_locked_policy_injected(self, runtime):
|
|
120
|
+
cfg = make_cfg(STEPS, deps_policy="locked", lockfiles=["uv.lock"])
|
|
121
|
+
rt = dataclasses.replace(runtime, cfg=cfg)
|
|
122
|
+
ctx = build_context(rt, {}, "s", [])
|
|
123
|
+
assert "## Dependency policy" in ctx
|
|
124
|
+
assert "`uv.lock`" in ctx
|
|
125
|
+
assert "## Missing dependencies" in ctx
|
|
126
|
+
assert "Do not add it to the manifest." in ctx
|
|
127
|
+
|
|
128
|
+
def test_open_policy_injected(self, runtime):
|
|
129
|
+
cfg = make_cfg(STEPS, deps_policy="open")
|
|
130
|
+
rt = dataclasses.replace(runtime, cfg=cfg)
|
|
131
|
+
ctx = build_context(rt, {}, "s", [])
|
|
132
|
+
assert "## Dependency policy" in ctx
|
|
133
|
+
assert "add the exact-pinned entry to the dependency manifest" in ctx
|
|
134
|
+
assert "## Missing dependencies" not in ctx
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class TestMissingDependencyFailFast:
|
|
138
|
+
def _rt(self, runtime, **over):
|
|
139
|
+
cfg = make_cfg(STEPS, **over)
|
|
140
|
+
return dataclasses.replace(runtime, cfg=cfg)
|
|
141
|
+
|
|
142
|
+
def test_locked_dev_declaring_missing_dep_fails_fast(self, runtime,
|
|
143
|
+
fake_agents):
|
|
144
|
+
rt = self._rt(runtime, deps_policy="locked", lockfiles=["uv.lock"])
|
|
145
|
+
fake_agents.set("dev", [("write", "implementation-summary.md",
|
|
146
|
+
"Built most of it.\n\n## Missing dependencies\n\n"
|
|
147
|
+
"requests==2.32.0\n")])
|
|
148
|
+
update = make_agent_node(rt, rt.cfg.steps[0])(make_state())
|
|
149
|
+
assert update == {"failed": True, "failed_step": "dev"}
|
|
150
|
+
err = (rt.state_dir / "last-error.md").read_text()
|
|
151
|
+
assert "Extend `uv.lock`" in err
|
|
152
|
+
assert "requests==2.32.0" in err
|
|
153
|
+
|
|
154
|
+
def test_no_header_no_fail(self, runtime, fake_agents):
|
|
155
|
+
rt = self._rt(runtime, deps_policy="locked", lockfiles=["uv.lock"])
|
|
156
|
+
fake_agents.set("dev", [("write", "implementation-summary.md",
|
|
157
|
+
"no new deps")])
|
|
158
|
+
update = make_agent_node(rt, rt.cfg.steps[0])(make_state())
|
|
159
|
+
assert update["failed"] is False
|
|
160
|
+
|
|
161
|
+
def test_open_policy_does_not_fail_on_header(self, runtime, fake_agents):
|
|
162
|
+
rt = self._rt(runtime, deps_policy="open")
|
|
163
|
+
fake_agents.set("dev", [("write", "implementation-summary.md",
|
|
164
|
+
"## Missing dependencies\n\nrequests==2.32.0\n")])
|
|
165
|
+
update = make_agent_node(rt, rt.cfg.steps[0])(make_state())
|
|
166
|
+
assert update["failed"] is False
|
|
167
|
+
|
|
168
|
+
|
|
118
169
|
class TestAgentNode:
|
|
119
170
|
def test_pass_verdict(self, runtime, fake_agents):
|
|
120
171
|
fake_agents.set("check", [("write", "ci-fix.md", "all good\nVERDICT: PASS\n")])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/core/__init__.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/core/results.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/core/verdict.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/__init__.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/actions.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/agents.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/dryrun.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/graph.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/hostboundary.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/isolation.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/loaders.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/outputs.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/registry.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/secrets.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/viz/__init__.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/viz/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a10 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|