agentic-dev-loop
Turn an LLM into a gated, sandboxed, resumable construction loop over your codebase.
Describe your pipeline in one YAML file - agent steps, mechanical test gates, and
trusted actions - and agentic-dev-loop runs it end to end: agent and command steps get
kernel-level sandboxing via Docker, verdicts route the graph, every boundary is
checkpointed, and nothing gets committed until every gate passes.
flowchart LR
dev["agent: dev"] --> test{{"test gate"}} --> review["agent: review"] --> qa["agent: qa"] --> commit["action: commit"]
test -- fail --> dev
review -- fail --> dev
qa -- fail --> dev
- A tool, not a wiring project. One CLI entry point, opinionated defaults, fixed safety policies. You write the pipeline and the agent definitions - nothing else.
- Deterministic. Same pipeline + same state → same routing. Verdicts are parsed from files, retries are bounded, nothing is best-effort.
- Sandboxed by default. The agent's shell and every command step run inside an always-on Docker sandbox. There is no opt-out.
Core features
| Feature | What it gives you |
|---|---|
| Stage-gated agent loop | Agents do the work, test stages verify it, and the pipeline only advances when each stage passes its gates - looping back with full context when one fails. |
| Kernel-level sandboxing | Agent work and command stages run isolated from your machine: no network access by default - authors opt specific stages in, writes only to declared mounts:, and no access to .git (steps run against a fresh export). No sandbox available → the run stops instead of degrading. |
| Crash-safe resume | Interrupt a run at any time and pick it up later - finished stages stay finished, and you resume from exactly where it stopped. |
| Live graph visualisation | Follow every run in the browser: stage status, pass/fail verdicts, retry cycles, and live console output as it happens. |
| Extensible | Plug in your own actions, tools, and document loaders to adapt the loop to your projects. |
The fixed policies are the product: fail fast on missing config/secrets, no silent fallbacks, verdicts only from artifacts, bounded retries, per-attempt artifact hygiene, a single clean-worktree-gated commit point, no default LLM timeout (agent runs are long), and fail fast on a broken sandbox - daemon-side exec failures are classified and stop the run without an agent retry loop, while a startup reaper reclaims containers leaked by a crashed run.
Supported platforms
The engine is plain Linux under the hood and runs wherever a compliant Docker engine does:
| Platform | Support |
|---|---|
| Linux | Native. Docker Engine, OrbStack, Colima, or podman with docker compat. |
| macOS | Native. Docker Desktop, OrbStack, or Colima. |
| Windows | Via WSL2 (Ubuntu distro). Enable Docker Desktop's WSL integration (or run Docker Engine inside the distro) and keep the repo on the WSL filesystem (~/dev/...), not /mnt/c - bind-mounting a 9p path into containers breaks permissions and is slow. |
Native Windows (outside WSL2) is not supported: every Windows-specific code path and test was removed, and CI runs on Ubuntu only.
Installation
Prerequisites
| Requirement | Detail |
|---|---|
| Python | ≥ 3.12 |
| Docker engine | Engine API ≥ 1.43 (Engine 24). Any compliant engine works - Docker Desktop, OrbStack, Colima, Rancher Desktop, podman docker-compat. On Windows, run under WSL2: the repo must live on the WSL filesystem, not /mnt/c. |
| 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. |
pip install "agentic-dev-loop==1.0.0a17"
agentic-dev-loop --version
The canonical consumer install is pipx, which puts the engine's venv
outside the workspace - the sandbox mounts only the step's declared mounts:
from a fresh export, so an out-of-workspace install is unmounted and never
agent-writable:
pipx install "agentic-dev-loop==1.0.0a17"
If you install with uv shortly after a release, pass --refresh - PyPI
serves the package index with Cache-Control: max-age=600, and uv otherwise
reuses a pre-upload snapshot of the index for up to 10 minutes, reporting
"no version of agentic-dev-loop==X.Y.Z" for a version that is already live:
uv pip install --refresh "agentic-dev-loop==1.0.0a17"
On the first run the engine pulls the digest-pinned dev image
(ghcr.io/lukedawilson/agentic-dev-loop/dev) and verifies its digest - the
supply-chain check refuses to continue on a mismatch. If you're running
pre-publish tooling, point AGENTIC_DEV_LOOP_DEV_IMAGE at your locally built
image. Engine setup, first-run details, the full pipeline.yaml contract, and
troubleshooting for every fail-fast code live in
docs/setup.md.
Usage
export OPENAI_API_KEY=... # whatever api_key_env names
agentic-dev-loop pipeline.yaml # run it
agentic-dev-loop pipeline.yaml --dry-run # validate first - no LLM, no Docker
agentic-dev-loop pipeline.yaml --resume <thread-id> --max-attempts 5
| Flag | Effect |
|---|---|
--dry-run |
Full load-and-validate pass, then print the resolved plan. No LLM, no Docker, no state writes, no API key. Exit 0 on a clean load. |
--resume <thread-id> |
Continue the latest checkpoint of a previous run (crash, Ctrl-C, exhaustion) with all accumulated notes intact. |
--max-attempts <n> |
Override the pipeline's max_attempts. |
--no-viz / --viz-port <port> |
Suppress the live graph view / set a preferred port (default 8321; falls back to a free port when busy). |
| extension flags | Document-loader flags (e.g. --plan, --aidlc-unit) appear once the pipeline's extensions are loaded - agentic-dev-loop PIPELINE --help shows them. |
Exit code is 0 on pipeline success, 1 otherwise.
agentic-dev-loop containers inspects the engine's sandbox containers and
networks from the host: the default list is read-only and resolves each
engine-owned object to its owning run and a live, orphaned,
stale-entry, or unregistered verdict; containers --reap removes only
registry-owned orphans whose owner pid is dead (never unregistered names).
A SIGTERM to a running pipeline still closes the sandbox (the graph's
finally runs); SIGKILL is unhandleable, so its containers are reclaimed by
the startup reaper on the next run or by containers --reap. The reaper
and --reap prune a registry entry only after the daemon confirms the
object is gone; a surviving object (a failed removal) keeps its entry so
the next startup retries it. Pass
--state-dir to point at a custom isolation.state_dir.
What a run looks like (the shipped self-loop example):
- dev - the dev agent works with full context (loaded docs + shared files).
Its edits are untracked; only declared
produces:files are hygiene-tracked. - test - a mechanical command step runs the suite inside the sandbox.
Exit 0 moves on; anything else rewinds to
devwith the failing output. - review / qa - verdict agents write artifacts whose
VERDICT:lines route the graph.failrewinds todevand consumes an attempt. - commit - the single commit point fires only when all gates passed and the worktree was clean at start.
- success - committed; or failure when attempts are exhausted.
pipeline.yaml reference
name: my-construction-loop
extensions:
- agentic_dev_loop.engine.extensions.aidlc:AidlcExtension # bundled AI-DLC doc loader
additional_files:
- ../README.md # shared context for every agent step
llm:
model: openai/deepseek-v4-pro
base_url: https://api.deepseek.com
api_key_env: OPENAI_API_KEY
# reasoning_effort: medium # optional: low | medium | high | xhigh | none
max_attempts: 10 # CLI --max-attempts overrides
agents_dir: sdk_agents # your <name>.agent.md files
steps:
- name: brief
reasoning_effort: medium
brief_inputs:
include: [AGENTS.md, coding-standards-ddd.yaml]
sections: [Repo rules digest, Unit scope digest, Environment facts, Open risks]
env_facts: ["the workspace is an export with no .git; verify with find/ls, never git status"]
on_pass: dev
- name: dev
agent: dev
produces: implementation-summary.md
terminal_timeout: 900
on_pass: test
- name: test
command: .venv-sandbox/bin/python -m pytest tests/ -q -m "not docker"
setup: python -m venv .venv-sandbox && .venv-sandbox/bin/pip install -e ".[dev]"
timeout: 600
on_pass: review
on_fail: { goto: dev, retry: true }
- name: review
agent: review
artifact: review-findings.md
verdicts: { pass: APPROVED, fail: NEEDS CHANGES }
# reasoning_effort: xhigh # per-step override; falls back to the llm block, then SDK default
on_pass: qa
on_fail: { goto: dev, retry: true }
- name: qa
agent: qa
artifact: qa-report.md
verdicts: { pass: PASS, fail: FAIL }
max_iterations: 100
finalize_nudge_turns: 10
on_pass: commit
on_fail: { goto: dev, retry: true }
- name: commit
action: commit
params: { message: "feat: construct {subject} (agent dev loop)" }
| Key | Notes |
|---|---|
agent: / command: / action: / brief_inputs: |
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). brief_inputs: is a native brief step - see its row below. |
produces: / artifact: + verdicts: |
Hygiene-tracked outputs vs verdict-parsed routing. The last VERDICT: line wins; stale artifacts are deleted at each attempt start. |
brief_inputs: |
The discriminator for a native brief step: { include: [<paths/globs>], env_facts: [<strings>], sections: [<headings>] }. There is no agent:, produces:, or max_iterations: - the prompt is engine-embedded, the capture file is attempt-brief.md, and the iteration cap is 12, all engine-owned. The engine resolves the include list against the workspace, passes the resolved docs + include contents + env facts as the step's task message, and regenerates the step only when those inputs change. sections is the ordered list of digest headings the brief must emit (default [Repo rules digest, Unit scope digest, Environment facts, Open risks]; non-empty and unique). name stays free-form, so multiple brief steps are allowed. |
terminal_timeout: |
Optional agent-step per-action timeout in seconds (gt 0). SandboxTerminalExecutor uses it when the agent leaves the terminal action timeout unset, overriding the SDK tool's default. None = current behavior. |
finalize_nudge_turns: |
Optional agent-step final-response nudge (gt 0). Requires max_iterations: and must be less than it. At max_iterations - finalize_nudge_turns completed turns the engine injects one advisory turn ("N turns remain: produce your final response now"), exactly once; the agent may still exhaust its turns and existing stuck/unclear handling applies unchanged. Absent = current behavior. Provisional pending one consumer run (revert if noisy or the raised cap inflates cost). |
custom_prompt: |
Planned (planning-docs/040). Optional agent-step value: literal text, or a path whose file contents are read at load time, appended verbatim to the agent prompt at context assembly (after the prompt, before the task/artifacts). A value that looks like a path but names no file is a validation error. Agent steps only. |
reasoning_effort: |
Optional low / medium / high / xhigh / none. Set on the llm block (pipeline-global) or per-step (overrides the block). Unset anywhere = the field is not passed and the SDK default (high) applies, so today's behavior is unchanged. Forwarded only for models the SDK/LiteLLM mark as supporting it; an unsupported value fails at the first LLM call. See Reasoning effort. |
on_pass: / on_fail: |
A step name, terminal success/failure, or {goto: X, retry: true}. No loops happen unless you write them. |
env: / setup: / network: |
env: declares step vars (fixed safe base + declared vars only); setup: runs fresh at every step boundary in a one-shot provision container targeting the export (command or agent steps; network ON; no stamping); network: true lets a command/agent step reach the hosts its egress: allowlist names (default off) - see Per-stage networking. |
mounts: / egress: |
Per-step workspace isolation. mounts: lists {path, mode} entries (rw/ro, default ro) and is strict-by-default - empty/missing mounts nothing; only declared-rw paths sync back to the repo. egress: is {class, presets, targets} (class: browser = broad-but-logged; otherwise a strict allowlist). See Per-stage networking and docs/setup.md. |
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. |
isolation: (optional) |
Configures the sandbox - image override, resource caps, allow_git_mutations, seccomp_profile. It can never disable it. See browser sandbox. |
state_dir |
Defaults to ~/.agentic-dev-loop/runs/<pipeline-hash>/ - artifacts and checkpoints live outside the sandbox mount. |
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. |
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. |
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. |
caches: / sync_excludes: |
Top-level run-wide lists. caches: names repo-relative toolchain cache dirs bind-mounted rw from host-side stores under {state_dir}/caches/<name> (auto-created); a setup: hook can warm them (the provision container carries the same cache mounts); sync_excludes: names non-gitignored scratch paths skipped from both the export copy and sync-back (wildcard * spans separators, so **/bin/** matches a /bin/ segment at any depth). Both default to []. See docs/setup.md. |
merge_on_conflict: |
Top-level run-wide list of append-style logs (same path vocabulary as sync_excludes:, default []). A concurrent host edit to a listed path is union-merged instead of raising: the host file wins and the sandbox's export-only lines are appended in order; both sides converge and the run continues. See docs/setup.md. |
Reasoning effort (opt-in)
reasoning_effort is an opt-in latency dial on the pipeline llm block and
on individual agent steps. It takes one of low, medium, high, xhigh,
or none; unset means the field is never passed and the SDK default
(high) applies, so a pipeline that omits it behaves exactly as before. A
step's value overrides the llm block's value, so the review lane can hold
a higher effort than the rest.
The value is forwarded only for models the SDK/LiteLLM mark as supporting it; an unsupported value fails at the first LLM call (the schema rejects unknown names at load time, but cannot know provider capability). No pipeline ships a value: adopt one only after an observed run proves lower per-turn latency with a green suite and no provider 400s.
Recommended lane defaults (guidance only, not enforced):
| Lane | reasoning_effort | Why |
|---|---|---|
| brief | medium | digest and classify, no design work |
| dev | high | hold the line: dev caught a real plan bug and a real sandbox triage this unit |
| qa | medium | classification discipline must stay reliable |
| review | xhigh | shortest lane, highest stakes: a missed detection sails past the last gate into commit |
A brief_inputs: step is the per-run context-digestion pattern: it runs once at
run start (or when its inputs change), its engine-owned attempt-brief.md
digest survives attempt starts, and the dev/review agents read that digest
instead of re-deriving repo rules and environment facts every attempt.
The brief digest is mechanically coverage-checked: the configured
brief_inputs.sections headings must each appear on their own line, every
anchor extracted from the include files (markdown ## headings, and YAML
category names plus enforcement_levels) must appear on its own line
(case-insensitive, whitespace-collapsed, ** decoration allowed), and every
env_facts entry must appear verbatim. The section list feeds both the
engine-embedded brief prompt and the coverage gate, so the two can never
disagree; a miss
re-runs the brief agent once per missing set with a ## Coverage retry
correction note, bounded to three retries per input set; if the same items are
still dropped after the cap, the run fails and last-error.md names the
persistent drops. Include files with no extractable anchors pass vacuously, and
unusual YAML rule shapes are under-checked, never over-checked.
Secrets (secrets:) - refs only, never values
Secret values never belong in the pipeline yaml, the workspace, or state_dir.
Declare the refs at the top level and opt each step into the refs it needs:
secrets: # refs only - never values
kinde-client-secret: # ref name (arbitrary)
from: env # provider name
key: KINDIE_CLIENT_SECRET # provider-specific locator
r2-keys:
from: env
key: R2_KEYS
steps:
- name: qa
agent: qa
secrets: [kinde-client-secret, r2-keys]
- The engine resolves every declared ref host-side at run start (the
envprovider reads the named host environment variable). A missing variable, an unknown provider, or a ref a step lists without declaring it fails fast - naming the ref, the step, and the provider error. No silent fallback. - A resolved secret is injected into a step's exec env only when that step
lists the ref - the same path step
env:uses, no new channel. The secret never reaches the provision container,state_dir, or any other step. - Prefer a mechanical
commandstep for secret-consuming work (a script runs the app with creds) so the LLM and the secret never meet; anagentstep holds a secret only when its job genuinely requires it. .env*ban. At run start the workspace root is scanned for.env*files. Any hit fails fast, naming the files, with the remedy text. The convention fixtures.env.example,.env.template, and.env.sampleare whitelisted. This applies regardless ofnetwork:flags.- Output scrubbing. Resolved secret values are scrubbed from every
engine-written output - captured step output, retry notes, artifacts, and
state_dirfiles. (Hygiene: the declaring step's agent has already seen the value in its own env.)
Per-stage networking
The sandbox is network-off by default. A step that legitimately needs egress - a QA step that curls a service it booted, an agent that must hit an API or fetch package metadata mid-conversation - declares it explicitly, and names exactly what it may reach:
steps:
- name: fetch-docs
command: curl -fsSL https://example.com/spec.json -o spec.json
mounts: [{ path: ".", mode: rw }] # the cwd + a place to write spec.json
network: true # egress for this step only
egress: { targets: [example.com] } # ...and only these hosts
- name: restore
command: .venv-sandbox/bin/pip install -e ".[dev]"
mounts: [{ path: ".", mode: rw }]
network: true
egress: { presets: [package-registries] } # pypi/npm/NuGet/cargo + VCS
- Default off. Every step runs with
--network noneunless it declaresnetwork: true. - A networked step that writes output must also declare a writable mount.
mounts:is strict-by-default: with nomounts:a step mounts nothing, so a command likecurl ... -o spec.jsonhas no writable cwd to write into. Declaremounts: [{ path: ".", mode: rw }](or a narrower rw path) for any networked step that needs to produce files. network: trueis scoped by theegress:allowlist, and egress is structurally enforced. A networked step runs on a per-run internal docker network with no route to the host. Its traffic is forced through a containerized egress proxy (injected viaHTTP_PROXY/HTTPS_PROXY, pointed at the proxy's internal IP on the internal network; each distinct egress policy gets its own proxy with a distinct internal IP); a request to an undeclared host is denied and logged as an enforcement event. The engine never adds an undeclared target.egress: { class: browser }is the explicit broad-but-logged class (every connection logged, nothing filtered); command and agent steps default to a strict allowlist.- Command, agent, and sandbox-action steps may declare it. The host-side
built-in
commitaction never takes it (refused once the registry is known). - Author opt-in is the control. A networked step grants its agent egress to exactly the declared hosts for that stage; the default stays network-off everywhere. Reserve it for stages that need it (QA, integration). The residual risks are documented in docs/setup.md.
- Host-service reachability (planning-docs/037, 038). The egress
proxy is a container on the run's internal network with a second
host-access attachment carrying the
host.docker.internalhost-gateway alias, so the name resolves natively inside the proxy container and host services (an S3 SDK against a local MinIO, say) are reached through the enforced, logged path. Sandbox containers have no host route: the internal network carries no default route to the host, and sandbox containers get no host-gateway alias.dial_rewriteson the proxy remains the platform override for hosts where the alias does not resolve.
Sync-back conflicts (host edits during a run)
Only declared-rw paths sync back to the real repo after a step. If the host
also edits a declared-rw path while the step is running, sync-back prints
[isolation] sync-back conflict: ... and raises AE-SBX-016; the host's
version wins, the baseline is re-snapshotted, and the next step (even
echo hello) runs clean instead of re-raising forever. For append-style logs
edited by both sides (an audit log, say), declare them in the top-level
merge_on_conflict: list: those paths are union-merged - the host keeps its
lines, the sandbox's new lines are appended in order, both sides converge, and
the run continues with [isolation] merged concurrent edits: ... on the
console.
Authoring a loop for your own repo
A consumer repo needs two things written by hand - a pipeline file and the
agent files it names - both committed before the first run (the host-boundary
check refuses dirty tooling, and the commit step never commits it). Two
reference layouts ship today: this repo dogfoods the engine on itself
(python + pytest, no frontend, CLI-only qa), and the consumer repo carries
the wider shape (.NET + npm frontend + a networked browser-probing qa step).
<repo>/agentic-dev-loop/pipeline.yaml
<repo>/agentic-dev-loop/sdk_agents/dev.agent.md
<repo>/agentic-dev-loop/sdk_agents/review.agent.md
<repo>/agentic-dev-loop/sdk_agents/qa.agent.md
The agent files
Each sdk_agents/<name>.agent.md is a markdown prompt plus YAML frontmatter:
name matches a step's agent: value; tools lists the tool names the step
may use (terminal always; a repo can add more via extensions, e.g.
figma).
---
name: review # matches a step's `agent:` value
tools: [terminal]
---
# Review Agent
## WORKFLOW # numbered steps
## RULES # absolute never/always rules
## SANDBOX NOTES # the sandbox truth for THIS step
## OUTPUT # the verdict contract
Three roles, three contracts:
| Role | Verdict contract | Typical sandbox |
|---|---|---|
| dev | none - the final response is captured as the produces: summary |
rw mount, offline |
| review | VERDICT: APPROVED / VERDICT: NEEDS CHANGES on the first content line |
ro mount, offline |
| qa | VERDICT: PASS / VERDICT: FAIL; map unverifiable: UNVERIFIABLE if you want a third way |
shaped by the repo: rw + network + class: browser when it boots servers and drives a browser (the consumer repo); ro + offline when it probes the CLI only (this repo) |
The conventions that make a good agent file - each one below exists because an agent burned budget learning it mid-run:
- State the sandbox truth in the prompt. Every agent file carries a
SANDBOX NOTESsection naming exactly what its step allows: mounts ro or rw, network on or off, and which gates have already run. Never make the agent discover it. - Gates are the source of truth - cite the record, never re-run. A
command the test gate, the editor-tests gate, or the dev summary reports
as run is coverage evidence, cited as
covered by <test>or the gate record. Review and qa never re-run builds or suites; qa maps every acceptance criterion to existing coverage and probes only the remainder. - dev enumerates its impossible set and stops at it. The dev prompt says: stop the moment your next step needs a command this sandbox cannot run, and lists the set (package-manager fetches after a new pin, npm or vitest offline, playwright downloads, database-category tests), one attempt per command. That keeps the productive edit/build/test loop and kills doomed retries. The set is toolchain-specific - write yours; this repo's is a single bullet (no package managers), the consumer repo's is four.
- Failure output returns as Previous attempt feedback. Steps route
on_fail: { goto: dev, retry: true }, so gate failure output lands in the dev agent's next context; the dev prompt makes every listed issue a required fix. - No git mutations, ever. The
commitstep is the single commit point. max_iterationsper role. qa is bounded probing (100 in this repo's dogfood loop, withfinalize_nudge_turns: 10so it never ends silently at the cap - both provisional); dev is open-ended construction and stays uncapped (the engine default).
The pipeline file
The full field reference is above; this is the stage lineup and what to adapt per repo.
| Stage | Kind | Typical shape | Why |
|---|---|---|---|
| dev | agent | rw, offline | does the work; edits are untracked |
| restore | command | network + presets: [package-registries] |
fetches what dev pinned (deps_policy: open) or the locked install; also the audit escalation point |
| test | command | offline is fine (warm cache) | exit code is the verdict; the source of truth |
| editor-tests | command | network + presets: [package-registries] |
frontend suites that cannot run offline (npm ci && npm test). A separate step, never merged into test: the step name is the failure diagnosis, its timeout does not contend the backend suite's, and a retry re-runs only its own chain |
| conformance | command | network + presets: [package-registries] |
repo-specific fitness checks (the consumer repo: jscpd + DDD rules) |
| review | agent | ro, offline | static conformance verdict |
| qa | agent | per repo (see table above) | live-behaviour probes; max_iterations: 100 + finalize_nudge_turns: 10 (provisional); inject secrets only if the probes need them |
| commit | action | host-side | the single commit point |
Adaptation checklist for a new repo:
llm:- model,base_url,api_key_env.isolation:- toolchain image override (the consumer repo pins a toolchain image by digest) andseccomp_profile: bundled:chromium-usernsonly if a stage runs Chromium.caches:- toolchain cache dirs as rw bind-mounts shared across runs (.nuget-packages,qa/node_modules,qa/.browsers,frontend-editor/node_modules); caches are never synced back, and a step'ssetup:hook runs with them mounted so it can warm them.sync_excludes:- build output trees (**/bin/**,**/obj/**).secrets:- refs only, resolved from host env; each step opts in to the refs it needs.egress:-presets: [package-registries]for fetch steps;class: browser(broad-but-logged) for browser-tier qa only.deps_policy:andlockfiles:- see Dependency policy.- The dev prompt's impossible set - write it for your toolchain.
additional_files:- the repo'sAGENTS.md, coding standards, design docs: shared context for every agent step.
Validate before the first run - dry-run loads and prints the resolved plan with no LLM, no Docker, no state writes:
agentic-dev-loop agentic-dev-loop/pipeline.yaml --dry-run
Then commit the tooling and run for real.
Dependency policy (locked by default)
The supply-chain dial is a pipeline-level deps_policy: field, locked by
default. The engine injects the policy into every agent step's context -
the rule lives in the engine, not in hand-authored dev.agent.md prose (the
class of rule that deadlocked the NuGet cache).
deps_policy: locked # or: open (default: locked)
lockfiles: [uv.lock] # or: [packages.lock.json, package-lock.json]
locked- the human-owned lockfile is the source of truth; the agent may not add dependencies. The injected rule says "You may only use dependencies already present in<lockfile>. If the design requires a dependency that is not present, declare it in your final response under the fixed header## Missing dependencies(onename==versionper line) and finish what you can. Do not add it to the manifest." A non-empty## Missing dependenciesdeclaration fails the run fast withExtend <lockfile> with these pins and re-run: …- one human lockfile edit, not a retry loop. The declaration is advisory to the human; the engine never auto-fetches the declared packages.open- the bootstrap protocol (018): the agent may add exact-pinned manifest entries directly (never a package-manageraddcommand); the network-on restore step fetches them. The engine injects the same rule canonically. Nothing aboutopenis default.
The restore step runs the toolchain's locked mode and fails when the manifest and lockfile disagree:
| Toolchain | Locked restore | Lockfile |
|---|---|---|
| .NET | dotnet restore --locked-mode |
packages.lock.json (set RestorePackagesWithLockFile=true) |
| node | npm ci |
package-lock.json |
| 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 |
Audit escalation in the restore step
The restore step also fails on known-CVE findings, at a per-toolchain threshold:
| Toolchain | Audit (fails the step) |
|---|---|
| .NET | NuGetAudit at error - any audit finding fails the restore |
| node | npm audit --audit-level=high |
| python | pip-audit - any finding fails |
Under locked, a failing audit means the lockfile pins a vulnerable version:
the run fails and the human regenerates the lockfile at a patched version.
Under open, the failure routes to dev like any restore failure - the dev
agent can bump the pin, but that pin is audited again on the next pass, so
the loop self-corrects.
Generating a lockfile
A human generates the lockfile once, on a network machine, and commits it - the engine never fetches on the agent's behalf:
uv lock # python - writes uv.lock from pyproject.toml
uv sync --locked # installs exactly uv.lock; fails on any drift
npm install --package-lock-only # node - writes package-lock.json
npm ci # installs exactly package-lock.json
dotnet restore --use-lock-file # .NET - writes packages.lock.json
dotnet restore --locked-mode # installs exactly packages.lock.json
Keep the lockfile tracked (do not gitignore it) and re-run the locked restore
to prove it holds from a clean cache before setting deps_policy: locked.
Agent-facing error guide
The engine bundles an always-on guide for the engine error codes an agent
can actually observe while running terminal commands. The guide lives at
src/agentic_dev_loop/engine/agent_error_guide.md, versioned with the engine
code that emits the codes, and is injected into every agent step's task
context at assembly time. There is no opt-in and no opt-out in v1; command
and action steps never receive it because no agent reads them.
Each agent-visible code has exactly one table row, shaped like this:
| Code | Meaning | Correct response |
|---|---|---|
| AE-SBX-016 | The loop refused to sync an edit because the real repo changed underneath on a path this workspace also touches; the file in this workspace is stale and its edits will not be applied. | The command is not the problem: retry once, do not edit or work around the named path, and mention the code in your final response if it persists. |
The guide is consumer-agnostic: it explains the general mechanism and never
names a consumer or a consumer domain rule ID. If an ID must appear inside a
quoted engine message, the placeholder <domain-rule-id> is used. Any engine
change that adds or changes an agent-visible error code must update the guide
in the same commit; the test suite enforces the pairing by parsing the
agent-visible SandboxError raise sites against the bundled table.
Host boundary (security model)
The orchestrator, tool layer, and the commit action run in the host
process - they hold your credentials and your full user account, so the
engine keeps anything agent-writable out of that process:
- Consumer actions are sandbox-executed. An action you register via an
extension is a command-shaped script (
script + params), run through the same sandbox path as command steps - the exit code is the verdict, and the code never runs in the host. Only the engine's trusted built-ins (commit, document loaders) run host-side. - Extensions load from outside the workspace.
extensions_dirdefaults to~/.agentic-dev-loop/extensions; at load time any extension or tool module whose file resolves under the workspace is refused before import, naming the file. - The engine installs outside the workspace. Install with pipx so the venv
is unmounted from the sandbox; a startup self-check refuses to run if
agentic_dev_loopresolves under the workspace (an editable install bypasses everything). - Tooling is read-only by the loop. At run start the pipeline file,
agents_dir,additional_files,aidlc-docs/, andtools/must be git-clean relative to HEAD - otherwise the run refuses, naming the dirty files. Thecommitaction excludes those paths from its sweep, so the loop never commits its own tooling. commitstays host-side, deliberately. It holds push credentials; moving it into the sandbox would trade host-exec-of-trusted-code for credential-in-sandbox. It runs argv-style with a scrubbed child env and-c core.hooksPath=~/.agentic-dev-loop/state/hooksso repo hooks never execute.
Net: the host executes nothing from the workspace, reads only git-clean control files, and the only agent-writable host-consumed thing left is the code the loop is building.
Consumer migration notes
- Actions - replace a host-side
fnregistration withregistry.add_sandbox_action(name, script).scriptis a command string rendered with the step'sparams:plus{subject}; the exit code is the verdict.add_action(host-side) is now engine-built-in-only and is refused from extensions. - Extensions - move workspace-resident extension modules (e.g.
extensions/*.py) into~/.agentic-dev-loop/extensions/(or whateverextensions_dir:names), and reinstall the engine via pipx so it is not editable/in-workspace. - Tooling - commit the pipeline, agent defs, shared
additional_files,aidlc-docs/, andtools/before running: the run refuses dirty tooling, andcommitnever commits it.
Development environment
git clone git@github.com:lukedawilson/agentic-dev-loop.git
cd agentic-dev-loop
pipx install "agentic-dev-loop==1.0.0a17" # engine venv OUTSIDE the workspace (self-check)
export OPENAI_API_KEY=... # set your LLM key in the host env (no .env in the repo)
python -m pytest tests/ -q # fast suite (in-repo .venv is for tests only)
python -m pytest tests/ -q -m "not docker" # skip engine-dependent tests
agentic-dev-loop agentic-dev-loop/pipeline.yaml --dry-run # run the shipped pipeline
./run-demo.sh # viz demo - scripted run replay, no pipeline needed
setup-venv.shbuilds the throwaway.venvfor localpytestruns (the pipeline'steststep provisions its own.venv-sandboxinside the sandbox). The loop itself must run from the pipx install - never an in-workspace.venv, which the startup self-check refuses.- Docker-marked integration tests need a local daemon; CI runs them on Linux.
- The shipped pipeline (
agentic-dev-loop/pipeline.yaml) dogfoods the tool on its own repo - dev → test → review → qa → commit, with the engine's own three agent definitions insdk_agents/. - CI:
ci.yml(unit + integration suite, ubuntu),publish-image.yml(multi-arch dev image, amd64 + arm64),release.yml(PyPI publishing).
Contributing
- Read AGENTS.md first - it is the instruction set for both human and agent contributors (default mode: the user drives, ask before coding, TDD, no silent fallbacks).
- Development is structured by AI-DLC - the lifecycle workflow in
.aidlc/aidlc-rules/aws-aidlc-rules/with working docs (plans, units, state) inaidlc-docs/. The bundled document loader autodetects the current unit fromaidlc-state.mdvia--aidlc-unit, or load any plan with--plan. - OpenCode commands over the workflow:
/unit-status,/epic-status,/ticket-details,/update-subtask,/apply-aidlc-patches, and more. - Working extension samples (actions, tools, document loaders) ship as
tests/sample_ext.py.
Built on OpenHands SDK and LangGraph.
Metadata
Release files for agentic-dev-loop 0.9.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| agentic_dev_loop-0.9.1.tar.gz | 296.8 kB | With provenance | Details for agentic_dev_loop-0.9.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| agentic_dev_loop-0.9.1-py3-none-any.whl | Python 3 | none | any | Details for agentic_dev_loop-0.9.1-py3-none-any.whl |
Total release size: 452.1 kB
Release files / agentic_dev_loop-0.9.1.tar.gz
| Download URL | agentic_dev_loop-0.9.1.tar.gz |
|---|---|
| Size | 296.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
37089183ddf02cb49cc1d8939e69db5abffd9912b3c24663cc130c7f44dd7861
|
|
BLAKE2b-256 checksum How to use checksums |
af4e507dd126265d9b0f9eb6017179eb6fb932ba129b3ac56dbf330cce8f1c5b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.
Transparency logRelease files / agentic_dev_loop-0.9.1-py3-none-any.whl
| Download URL | agentic_dev_loop-0.9.1-py3-none-any.whl |
|---|---|
| Size | 155.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
bffe7674ec38614112a710e6535af52835071528a0b0d6f5d56b76faf7b928d2
|
|
BLAKE2b-256 checksum How to use checksums |
e6d6741d05eb7a427085e700758304715ffe52213700c4f7b045c65f75a93d6c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.
Transparency logSecurity for agentic-dev-loop 0.9.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for agentic_dev_loop-0.9.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.