agentic-dev-loop 1.0.0a9__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.0a9 → agentic_dev_loop-1.0.0a11}/PKG-INFO +152 -10
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/README.md +150 -9
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/pyproject.toml +2 -1
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/core/config.py +41 -6
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/actions.py +24 -3
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/cli.py +2 -1
- agentic_dev_loop-1.0.0a11/src/agentic_dev_loop/engine/deps.py +77 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/dryrun.py +17 -5
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/graph.py +26 -8
- agentic_dev_loop-1.0.0a11/src/agentic_dev_loop/engine/hostboundary.py +137 -0
- agentic_dev_loop-1.0.0a11/src/agentic_dev_loop/engine/registry.py +291 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/stages.py +96 -8
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop.egg-info/PKG-INFO +152 -10
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop.egg-info/SOURCES.txt +5 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop.egg-info/requires.txt +1 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_actions.py +39 -1
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_cli.py +6 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_config.py +41 -5
- agentic_dev_loop-1.0.0a11/tests/test_deps.py +76 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_dry_run.py +40 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_graph.py +60 -2
- agentic_dev_loop-1.0.0a11/tests/test_hostboundary.py +137 -0
- agentic_dev_loop-1.0.0a11/tests/test_hostboundary_integration.py +92 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_isolation.py +2 -0
- agentic_dev_loop-1.0.0a11/tests/test_registry.py +243 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_secrets.py +1 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_secrets_pipeline.py +2 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_stages.py +102 -2
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_viz.py +1 -0
- agentic_dev_loop-1.0.0a9/src/agentic_dev_loop/engine/registry.py +0 -133
- agentic_dev_loop-1.0.0a9/tests/test_registry.py +0 -114
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/LICENSE +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/setup.cfg +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/core/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/core/results.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/core/verdict.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/agents.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/isolation.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/llm.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/loaders.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/outputs.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/proc.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/secrets.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/py.typed +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/viz/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/viz/__main__.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/viz/server.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_agents.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_e2e_fixtures.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_isolation_integration.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_isolation_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_llm.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_loaders.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_module_contract.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_outputs.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_outputs_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_proc.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_publish.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_results.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_sdk_compat.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_secrets_integration.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_source_resolution.py +0 -0
- {agentic_dev_loop-1.0.0a9 → 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
|
|
@@ -90,10 +91,27 @@ LLM timeout (agent runs are long).
|
|
|
90
91
|
| 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. |
|
|
91
92
|
|
|
92
93
|
```bash
|
|
93
|
-
pip install "agentic-dev-loop==1.0.
|
|
94
|
+
pip install "agentic-dev-loop==1.0.0a9"
|
|
94
95
|
agentic-dev-loop --version
|
|
95
96
|
```
|
|
96
97
|
|
|
98
|
+
The canonical consumer install is **pipx**, which puts the engine's venv
|
|
99
|
+
*outside* the workspace — the sandbox mount set is exactly `{workspace}`, so an
|
|
100
|
+
out-of-workspace install is unmounted and never agent-writable:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
pipx install "agentic-dev-loop==1.0.0a9"
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
If you install with `uv` shortly after a release, pass `--refresh` — PyPI
|
|
107
|
+
serves the package index with `Cache-Control: max-age=600`, and uv otherwise
|
|
108
|
+
reuses a pre-upload snapshot of the index for up to 10 minutes, reporting
|
|
109
|
+
"no version of agentic-dev-loop==X.Y.Z" for a version that is already live:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
uv pip install --refresh "agentic-dev-loop==1.0.0a9"
|
|
113
|
+
```
|
|
114
|
+
|
|
97
115
|
On the first run the engine pulls the **digest-pinned** dev image
|
|
98
116
|
(`ghcr.io/lukedawilson/agentic-dev-loop/dev`) and verifies its digest — the
|
|
99
117
|
supply-chain check refuses to continue on a mismatch. If you're running
|
|
@@ -188,13 +206,16 @@ steps:
|
|
|
188
206
|
|
|
189
207
|
| Key | Notes |
|
|
190
208
|
| --- | --- |
|
|
191
|
-
| `agent:` / `command:` / `action:` | Exactly one per step. Agents live in `agents_dir` as `<name>.agent.md`; commands run sandboxed (mechanical, never an LLM call); actions are
|
|
209
|
+
| `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)). |
|
|
192
210
|
| `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
|
|
193
211
|
| `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
|
|
194
212
|
| `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` grants egress to a command or agent step (default off) — see [Per-stage networking](#per-stage-networking). |
|
|
195
213
|
| `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). |
|
|
196
214
|
| `isolation:` (optional) | Configures the sandbox — image override, resource caps, `allow_git_mutations`. It can never disable it. |
|
|
197
215
|
| `state_dir` | Defaults to `~/.agentic-dev-loop/runs/<pipeline-hash>/` — artifacts and checkpoints live outside the sandbox mount. |
|
|
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. |
|
|
198
219
|
|
|
199
220
|
### Secrets (`secrets:`) — refs only, never values
|
|
200
221
|
|
|
@@ -253,28 +274,149 @@ steps:
|
|
|
253
274
|
sandbox container (`--read-only`, `--cap-drop ALL`, single workspace mount)
|
|
254
275
|
on the Docker bridge network — the default network-none container is always
|
|
255
276
|
created too, so no infrastructure work happens mid-run.
|
|
256
|
-
- **Command and
|
|
257
|
-
never
|
|
277
|
+
- **Command, agent, and sandbox-action steps** may declare it. The host-side
|
|
278
|
+
built-in `commit` action never takes it (refused once the registry is known).
|
|
258
279
|
- **Author opt-in is the control.** A networked step gives its agent raw
|
|
259
280
|
egress for that stage; the default stays network-off everywhere. Reserve it
|
|
260
281
|
for stages that need it (QA, integration). The residual risk is documented
|
|
261
282
|
in [docs/setup.md](docs/setup.md).
|
|
262
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
|
+
|
|
354
|
+
## Host boundary (security model)
|
|
355
|
+
|
|
356
|
+
The orchestrator, tool layer, and the `commit` action run in the **host
|
|
357
|
+
process** — they hold your credentials and your full user account, so the
|
|
358
|
+
engine keeps anything agent-writable out of that process:
|
|
359
|
+
|
|
360
|
+
- **Consumer actions are sandbox-executed.** An action you register via an
|
|
361
|
+
extension is a command-shaped script (`script + params`), run through the
|
|
362
|
+
same sandbox path as command steps — the exit code is the verdict, and the
|
|
363
|
+
code never runs in the host. Only the engine's trusted built-ins (`commit`,
|
|
364
|
+
document loaders) run host-side.
|
|
365
|
+
- **Extensions load from outside the workspace.** `extensions_dir` defaults to
|
|
366
|
+
`~/.agentic-dev-loop/extensions`; at load time any extension or tool module
|
|
367
|
+
whose file resolves under the workspace is refused *before import*, naming
|
|
368
|
+
the file.
|
|
369
|
+
- **The engine installs outside the workspace.** Install with pipx so the venv
|
|
370
|
+
is unmounted from the sandbox; a startup self-check refuses to run if
|
|
371
|
+
`agentic_dev_loop` resolves under the workspace (an editable install bypasses
|
|
372
|
+
everything).
|
|
373
|
+
- **Tooling is read-only by the loop.** At run start the pipeline file,
|
|
374
|
+
`agents_dir`, `additional_files`, `aidlc-docs/`, and `tools/` must be
|
|
375
|
+
git-clean relative to HEAD — otherwise the run refuses, naming the dirty
|
|
376
|
+
files. The `commit` action excludes those paths from its sweep, so the loop
|
|
377
|
+
never commits its own tooling.
|
|
378
|
+
- **`commit` stays host-side, deliberately.** It holds push credentials; moving
|
|
379
|
+
it into the sandbox would trade host-exec-of-trusted-code for
|
|
380
|
+
credential-in-sandbox. It runs argv-style with a scrubbed child env and
|
|
381
|
+
`-c core.hooksPath=~/.agentic-dev-loop/state/hooks` so repo hooks never
|
|
382
|
+
execute.
|
|
383
|
+
|
|
384
|
+
Net: the host executes nothing from the workspace, reads only git-clean
|
|
385
|
+
control files, and the only agent-writable host-consumed thing left is the code
|
|
386
|
+
the loop is building.
|
|
387
|
+
|
|
388
|
+
### Consumer migration notes
|
|
389
|
+
|
|
390
|
+
- **Actions** — replace a host-side `fn` registration with
|
|
391
|
+
`registry.add_sandbox_action(name, script)`. `script` is a command string
|
|
392
|
+
rendered with the step's `params:` plus `{subject}`; the exit code is the
|
|
393
|
+
verdict. `add_action` (host-side) is now engine-built-in-only and is refused
|
|
394
|
+
from extensions.
|
|
395
|
+
- **Extensions** — move workspace-resident extension modules (e.g.
|
|
396
|
+
`extensions/*.py`) into `~/.agentic-dev-loop/extensions/` (or whatever
|
|
397
|
+
`extensions_dir:` names), and reinstall the engine via pipx so it is not
|
|
398
|
+
editable/in-workspace.
|
|
399
|
+
- **Tooling** — commit the pipeline, agent defs, shared `additional_files`,
|
|
400
|
+
`aidlc-docs/`, and `tools/` before running: the run refuses dirty tooling,
|
|
401
|
+
and `commit` never commits it.
|
|
402
|
+
|
|
263
403
|
## Development environment
|
|
264
404
|
|
|
265
405
|
```bash
|
|
266
406
|
git clone git@github.com:lukedawilson/agentic-dev-loop.git
|
|
267
407
|
cd agentic-dev-loop
|
|
268
|
-
|
|
408
|
+
pipx install "agentic-dev-loop==1.0.0a9" # engine venv OUTSIDE the workspace (self-check)
|
|
269
409
|
export OPENAI_API_KEY=... # set your LLM key in the host env (no .env in the repo)
|
|
270
|
-
python -m pytest tests/ -q # fast suite
|
|
410
|
+
python -m pytest tests/ -q # fast suite (in-repo .venv is for tests only)
|
|
271
411
|
python -m pytest tests/ -q -m "not docker" # skip engine-dependent tests
|
|
272
|
-
|
|
412
|
+
agentic-dev-loop agentic-dev-loop/pipeline.yaml --dry-run # run the shipped pipeline
|
|
273
413
|
./run-demo.sh # viz demo — scripted run replay, no pipeline needed
|
|
274
414
|
```
|
|
275
415
|
|
|
276
|
-
- `setup-venv.sh`
|
|
277
|
-
|
|
416
|
+
- `setup-venv.sh` builds the throwaway `.venv` for local `pytest` runs (the
|
|
417
|
+
pipeline's `test` step provisions its own `.venv-sandbox` inside the
|
|
418
|
+
sandbox). The loop itself must run from the pipx install — never an
|
|
419
|
+
in-workspace `.venv`, which the startup self-check refuses.
|
|
278
420
|
- Docker-marked integration tests need a local daemon; CI runs them on Linux.
|
|
279
421
|
- The shipped pipeline (`agentic-dev-loop/pipeline.yaml`) **dogfoods the tool on
|
|
280
422
|
its own repo** — dev → test → review → qa → commit, with the engine's own
|
|
@@ -51,10 +51,27 @@ LLM timeout (agent runs are long).
|
|
|
51
51
|
| 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. |
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
|
-
pip install "agentic-dev-loop==1.0.
|
|
54
|
+
pip install "agentic-dev-loop==1.0.0a9"
|
|
55
55
|
agentic-dev-loop --version
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
+
The canonical consumer install is **pipx**, which puts the engine's venv
|
|
59
|
+
*outside* the workspace — the sandbox mount set is exactly `{workspace}`, so an
|
|
60
|
+
out-of-workspace install is unmounted and never agent-writable:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pipx install "agentic-dev-loop==1.0.0a9"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
If you install with `uv` shortly after a release, pass `--refresh` — PyPI
|
|
67
|
+
serves the package index with `Cache-Control: max-age=600`, and uv otherwise
|
|
68
|
+
reuses a pre-upload snapshot of the index for up to 10 minutes, reporting
|
|
69
|
+
"no version of agentic-dev-loop==X.Y.Z" for a version that is already live:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
uv pip install --refresh "agentic-dev-loop==1.0.0a9"
|
|
73
|
+
```
|
|
74
|
+
|
|
58
75
|
On the first run the engine pulls the **digest-pinned** dev image
|
|
59
76
|
(`ghcr.io/lukedawilson/agentic-dev-loop/dev`) and verifies its digest — the
|
|
60
77
|
supply-chain check refuses to continue on a mismatch. If you're running
|
|
@@ -149,13 +166,16 @@ steps:
|
|
|
149
166
|
|
|
150
167
|
| Key | Notes |
|
|
151
168
|
| --- | --- |
|
|
152
|
-
| `agent:` / `command:` / `action:` | Exactly one per step. Agents live in `agents_dir` as `<name>.agent.md`; commands run sandboxed (mechanical, never an LLM call); actions are
|
|
169
|
+
| `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)). |
|
|
153
170
|
| `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
|
|
154
171
|
| `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
|
|
155
172
|
| `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` grants egress to a command or agent step (default off) — see [Per-stage networking](#per-stage-networking). |
|
|
156
173
|
| `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). |
|
|
157
174
|
| `isolation:` (optional) | Configures the sandbox — image override, resource caps, `allow_git_mutations`. It can never disable it. |
|
|
158
175
|
| `state_dir` | Defaults to `~/.agentic-dev-loop/runs/<pipeline-hash>/` — artifacts and checkpoints live outside the sandbox mount. |
|
|
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. |
|
|
159
179
|
|
|
160
180
|
### Secrets (`secrets:`) — refs only, never values
|
|
161
181
|
|
|
@@ -214,28 +234,149 @@ steps:
|
|
|
214
234
|
sandbox container (`--read-only`, `--cap-drop ALL`, single workspace mount)
|
|
215
235
|
on the Docker bridge network — the default network-none container is always
|
|
216
236
|
created too, so no infrastructure work happens mid-run.
|
|
217
|
-
- **Command and
|
|
218
|
-
never
|
|
237
|
+
- **Command, agent, and sandbox-action steps** may declare it. The host-side
|
|
238
|
+
built-in `commit` action never takes it (refused once the registry is known).
|
|
219
239
|
- **Author opt-in is the control.** A networked step gives its agent raw
|
|
220
240
|
egress for that stage; the default stays network-off everywhere. Reserve it
|
|
221
241
|
for stages that need it (QA, integration). The residual risk is documented
|
|
222
242
|
in [docs/setup.md](docs/setup.md).
|
|
223
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
|
+
|
|
314
|
+
## Host boundary (security model)
|
|
315
|
+
|
|
316
|
+
The orchestrator, tool layer, and the `commit` action run in the **host
|
|
317
|
+
process** — they hold your credentials and your full user account, so the
|
|
318
|
+
engine keeps anything agent-writable out of that process:
|
|
319
|
+
|
|
320
|
+
- **Consumer actions are sandbox-executed.** An action you register via an
|
|
321
|
+
extension is a command-shaped script (`script + params`), run through the
|
|
322
|
+
same sandbox path as command steps — the exit code is the verdict, and the
|
|
323
|
+
code never runs in the host. Only the engine's trusted built-ins (`commit`,
|
|
324
|
+
document loaders) run host-side.
|
|
325
|
+
- **Extensions load from outside the workspace.** `extensions_dir` defaults to
|
|
326
|
+
`~/.agentic-dev-loop/extensions`; at load time any extension or tool module
|
|
327
|
+
whose file resolves under the workspace is refused *before import*, naming
|
|
328
|
+
the file.
|
|
329
|
+
- **The engine installs outside the workspace.** Install with pipx so the venv
|
|
330
|
+
is unmounted from the sandbox; a startup self-check refuses to run if
|
|
331
|
+
`agentic_dev_loop` resolves under the workspace (an editable install bypasses
|
|
332
|
+
everything).
|
|
333
|
+
- **Tooling is read-only by the loop.** At run start the pipeline file,
|
|
334
|
+
`agents_dir`, `additional_files`, `aidlc-docs/`, and `tools/` must be
|
|
335
|
+
git-clean relative to HEAD — otherwise the run refuses, naming the dirty
|
|
336
|
+
files. The `commit` action excludes those paths from its sweep, so the loop
|
|
337
|
+
never commits its own tooling.
|
|
338
|
+
- **`commit` stays host-side, deliberately.** It holds push credentials; moving
|
|
339
|
+
it into the sandbox would trade host-exec-of-trusted-code for
|
|
340
|
+
credential-in-sandbox. It runs argv-style with a scrubbed child env and
|
|
341
|
+
`-c core.hooksPath=~/.agentic-dev-loop/state/hooks` so repo hooks never
|
|
342
|
+
execute.
|
|
343
|
+
|
|
344
|
+
Net: the host executes nothing from the workspace, reads only git-clean
|
|
345
|
+
control files, and the only agent-writable host-consumed thing left is the code
|
|
346
|
+
the loop is building.
|
|
347
|
+
|
|
348
|
+
### Consumer migration notes
|
|
349
|
+
|
|
350
|
+
- **Actions** — replace a host-side `fn` registration with
|
|
351
|
+
`registry.add_sandbox_action(name, script)`. `script` is a command string
|
|
352
|
+
rendered with the step's `params:` plus `{subject}`; the exit code is the
|
|
353
|
+
verdict. `add_action` (host-side) is now engine-built-in-only and is refused
|
|
354
|
+
from extensions.
|
|
355
|
+
- **Extensions** — move workspace-resident extension modules (e.g.
|
|
356
|
+
`extensions/*.py`) into `~/.agentic-dev-loop/extensions/` (or whatever
|
|
357
|
+
`extensions_dir:` names), and reinstall the engine via pipx so it is not
|
|
358
|
+
editable/in-workspace.
|
|
359
|
+
- **Tooling** — commit the pipeline, agent defs, shared `additional_files`,
|
|
360
|
+
`aidlc-docs/`, and `tools/` before running: the run refuses dirty tooling,
|
|
361
|
+
and `commit` never commits it.
|
|
362
|
+
|
|
224
363
|
## Development environment
|
|
225
364
|
|
|
226
365
|
```bash
|
|
227
366
|
git clone git@github.com:lukedawilson/agentic-dev-loop.git
|
|
228
367
|
cd agentic-dev-loop
|
|
229
|
-
|
|
368
|
+
pipx install "agentic-dev-loop==1.0.0a9" # engine venv OUTSIDE the workspace (self-check)
|
|
230
369
|
export OPENAI_API_KEY=... # set your LLM key in the host env (no .env in the repo)
|
|
231
|
-
python -m pytest tests/ -q # fast suite
|
|
370
|
+
python -m pytest tests/ -q # fast suite (in-repo .venv is for tests only)
|
|
232
371
|
python -m pytest tests/ -q -m "not docker" # skip engine-dependent tests
|
|
233
|
-
|
|
372
|
+
agentic-dev-loop agentic-dev-loop/pipeline.yaml --dry-run # run the shipped pipeline
|
|
234
373
|
./run-demo.sh # viz demo — scripted run replay, no pipeline needed
|
|
235
374
|
```
|
|
236
375
|
|
|
237
|
-
- `setup-venv.sh`
|
|
238
|
-
|
|
376
|
+
- `setup-venv.sh` builds the throwaway `.venv` for local `pytest` runs (the
|
|
377
|
+
pipeline's `test` step provisions its own `.venv-sandbox` inside the
|
|
378
|
+
sandbox). The loop itself must run from the pipx install — never an
|
|
379
|
+
in-workspace `.venv`, which the startup self-check refuses.
|
|
239
380
|
- Docker-marked integration tests need a local daemon; CI runs them on Linux.
|
|
240
381
|
- The shipped pipeline (`agentic-dev-loop/pipeline.yaml`) **dogfoods the tool on
|
|
241
382
|
its own repo** — dev → test → review → qa → commit, with the engine's own
|
|
@@ -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]
|
|
@@ -114,7 +114,7 @@ class StepConfig(BaseModel):
|
|
|
114
114
|
timeout: float | None = Field(default=None, gt=0) # seconds, command steps only
|
|
115
115
|
env: dict[str, str | None] | None = None # NFR-1: null = inherit host value
|
|
116
116
|
setup: str | None = None # BR-13: command steps only
|
|
117
|
-
network: bool = False # per-stage egress (command +
|
|
117
|
+
network: bool = False # per-stage egress (command/agent + sandbox actions; host-side actions reject at registry time)
|
|
118
118
|
secrets: list[str] = [] # declared ref names; action steps reject
|
|
119
119
|
on_pass: RouteTarget | None = None
|
|
120
120
|
on_fail: RouteTarget | None = None
|
|
@@ -173,11 +173,11 @@ class StepConfig(BaseModel):
|
|
|
173
173
|
raise ValueError(
|
|
174
174
|
f"step {self.name!r}: 'setup' is only allowed on 'command' steps"
|
|
175
175
|
)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
176
|
+
# `network` is NOT rejected here: the schema can't tell a host-side
|
|
177
|
+
# built-in action (e.g. `commit`, which must never take egress) from
|
|
178
|
+
# a sandbox-executed consumer action (which may, like a command step).
|
|
179
|
+
# That distinction is enforced once the registry is known — see
|
|
180
|
+
# `validate_action_network` (020).
|
|
181
181
|
if self.secrets and self.action is not None:
|
|
182
182
|
raise ValueError(
|
|
183
183
|
f"step {self.name!r}: 'secrets' is only allowed on 'agent' or "
|
|
@@ -201,6 +201,11 @@ class PipelineConfig(BaseModel):
|
|
|
201
201
|
name: str
|
|
202
202
|
extensions: list[str] = []
|
|
203
203
|
additional_files: list[str] = []
|
|
204
|
+
# Where consumer `extensions:` modules resolve (work item 020). Must live
|
|
205
|
+
# OUTSIDE the workspace — the host refuses workspace-resident extension and
|
|
206
|
+
# tool-handler modules, so agent-writable files are never imported by the
|
|
207
|
+
# host process.
|
|
208
|
+
extensions_dir: str = "~/.agentic-dev-loop/extensions"
|
|
204
209
|
env_allowlist: list[str] = []
|
|
205
210
|
llm: LlmConfig
|
|
206
211
|
isolation: IsolationConfig | None = None # None = always-on defaults
|
|
@@ -209,6 +214,27 @@ class PipelineConfig(BaseModel):
|
|
|
209
214
|
max_attempts: int = 3
|
|
210
215
|
agents_dir: str
|
|
211
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
|
|
212
238
|
|
|
213
239
|
@field_validator("secrets", mode="before")
|
|
214
240
|
@classmethod
|
|
@@ -227,6 +253,15 @@ class PipelineConfig(BaseModel):
|
|
|
227
253
|
"values never belong in the pipeline yaml")
|
|
228
254
|
return v
|
|
229
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
|
+
|
|
230
265
|
@model_validator(mode="after")
|
|
231
266
|
def _check_graph(self) -> "PipelineConfig":
|
|
232
267
|
names = [s.name for s in self.steps]
|
{agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/actions.py
RENAMED
|
@@ -186,6 +186,23 @@ def checkpoint_exclude_pathspec(state_dir: str | None, cwd: Path) -> str | None:
|
|
|
186
186
|
return f":(exclude){rel}/*.sqlite*"
|
|
187
187
|
|
|
188
188
|
|
|
189
|
+
def tooling_exclude_pathspec(tooling_paths: list[str] | None) -> list[str]:
|
|
190
|
+
"""git pathspecs excluding tooling paths from ``git add -A`` (020).
|
|
191
|
+
|
|
192
|
+
The loop must never commit its own tooling (pipeline, agent defs,
|
|
193
|
+
additional files, aidlc-docs, tools): the run-start integrity check
|
|
194
|
+
requires those paths to be clean relative to HEAD, so the auto-commit
|
|
195
|
+
must sweep everything *except* them. Empty when there is nothing to
|
|
196
|
+
exclude."""
|
|
197
|
+
specs = []
|
|
198
|
+
for rel in (tooling_paths or []):
|
|
199
|
+
rel = rel.replace(os.sep, "/")
|
|
200
|
+
if rel in ("", ".", ".."):
|
|
201
|
+
continue
|
|
202
|
+
specs.append(f":(exclude){rel}")
|
|
203
|
+
return specs
|
|
204
|
+
|
|
205
|
+
|
|
189
206
|
def commit(state: dict, params: dict) -> dict:
|
|
190
207
|
"""The pipeline's only commit point. Skipped loudly when the worktree was
|
|
191
208
|
dirty at run start, so the auto-commit can never swallow pre-existing
|
|
@@ -205,9 +222,13 @@ def commit(state: dict, params: dict) -> dict:
|
|
|
205
222
|
cwd = Path.cwd()
|
|
206
223
|
hooks = _trusted_hooks_dir()
|
|
207
224
|
add_argv = ["-c", f"core.hooksPath={hooks}", "add", "-A"]
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
225
|
+
excludes = []
|
|
226
|
+
checkpoint_exclude = checkpoint_exclude_pathspec(state.get("state_dir"), cwd)
|
|
227
|
+
if checkpoint_exclude:
|
|
228
|
+
excludes.append(checkpoint_exclude)
|
|
229
|
+
excludes.extend(tooling_exclude_pathspec(state.get("tooling_paths")))
|
|
230
|
+
if excludes:
|
|
231
|
+
add_argv += ["--", *excludes]
|
|
211
232
|
added = _git(add_argv, cwd=cwd)
|
|
212
233
|
if added.returncode != 0:
|
|
213
234
|
# The add is the step that touches the most files (a locked DB, a
|
|
@@ -71,7 +71,8 @@ def _registry_for(cfg_path: Path) -> tuple[Registry, PipelineConfig]:
|
|
|
71
71
|
run path instead of being rebuilt."""
|
|
72
72
|
cfg = load_config(cfg_path)
|
|
73
73
|
registry = default_registry()
|
|
74
|
-
load_extensions(cfg.extensions, registry
|
|
74
|
+
load_extensions(cfg.extensions, registry,
|
|
75
|
+
extensions_dir=cfg.extensions_dir, workspace=Path.cwd())
|
|
75
76
|
return registry, cfg
|
|
76
77
|
|
|
77
78
|
|
|
@@ -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))
|