agentic-dev-loop 1.0.0a43__tar.gz → 1.0.0a45__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.0a43/src/agentic_dev_loop.egg-info → agentic_dev_loop-1.0.0a45}/PKG-INFO +11 -8
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/README.md +10 -7
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/pyproject.toml +1 -1
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/core/config.py +39 -1
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/agents.py +19 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/brief_coverage.py +7 -18
- agentic_dev_loop-1.0.0a45/src/agentic_dev_loop/engine/brief_prompt.md +46 -0
- agentic_dev_loop-1.0.0a45/src/agentic_dev_loop/engine/brief_prompt.py +47 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/dryrun.py +9 -5
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/isolation.py +35 -12
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/stages.py +35 -5
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45/src/agentic_dev_loop.egg-info}/PKG-INFO +11 -8
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop.egg-info/SOURCES.txt +3 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_033_brief_step.py +44 -6
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_041_brief_coverage.py +27 -16
- agentic_dev_loop-1.0.0a45/tests/test_046_brief_prompt.py +45 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_agents.py +20 -1
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_config.py +31 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_e2e_fixtures.py +1 -1
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_isolation.py +49 -2
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_isolation_integration.py +19 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/LICENSE +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/setup.cfg +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/core/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/core/paths.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/core/results.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/core/verdict.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/actions.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/agent_error_guide.md +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/agent_error_guide.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/allowlist.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/cli.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/containers.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/deps.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/egress_policy.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/egress_proxy.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/forbidden.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/graph.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/hostboundary.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/llm.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/loaders.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/mounts.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/outputs.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/presets.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/proc.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/registry.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/secrets.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/step_workspace.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/sync_back.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/py.typed +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/viz/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/viz/__main__.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/viz/server.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_028_declared_caches.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_029_merge_on_conflict.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_035_finalize_nudge.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_036_banned_commands_and_lane_boundary.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_actions.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_agent_error_guide.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_cli.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_containers.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_deps.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_dry_run.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_egress_proxy.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_graph.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_hostboundary.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_hostboundary_integration.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_isolation_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_llm.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_loaders.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_module_contract.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_outputs.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_outputs_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_proc.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_publish.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_qa_prompt.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_registry.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_repo_ascii.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_results.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_sdk_compat.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_secrets.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_secrets_integration.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_secrets_pipeline.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_source_resolution.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_stages.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_step_workspace.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_sync_back.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_trigger_script.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_unit5_config.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_unit5_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_unit5_pure.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_verdict.py +0 -0
- {agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/tests/test_viz.py +0 -0
{agentic_dev_loop-1.0.0a43/src/agentic_dev_loop.egg-info → agentic_dev_loop-1.0.0a45}/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.0a45
|
|
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
|
|
@@ -249,12 +249,13 @@ steps:
|
|
|
249
249
|
| --- | --- |
|
|
250
250
|
| `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)). |
|
|
251
251
|
| `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
|
|
252
|
-
| `brief_inputs:` | Optional agent-step input spec: `{ include: [<paths/globs>], env_facts: [<strings>] }`. The engine resolves the include list against the workspace, passes the resolved docs + include contents + env facts as that step's task message, and regenerates the step only when those inputs change. A produces-only agent step such as `brief` is the documented pattern for per-run context digestion. |
|
|
252
|
+
| `brief_inputs:` | Optional agent-step input spec: `{ include: [<paths/globs>], env_facts: [<strings>], sections: [<headings>] }`. The engine resolves the include list against the workspace, passes the resolved docs + include contents + env facts as that 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). A produces-only agent step such as `brief` is the documented pattern for per-run context digestion. |
|
|
253
253
|
| `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. |
|
|
254
254
|
| `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). |
|
|
255
255
|
| `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. |
|
|
256
256
|
| `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
|
|
257
257
|
| `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` provisions the sandbox before the step runs (command or agent steps; network ON; stamp-cached, re-runs when the pipeline's lockfiles change); `network: true` lets a command/agent step reach the hosts its `egress:` allowlist names (default off) - see [Per-stage networking](#per-stage-networking). |
|
|
258
|
+
| `setup_probe:` | Optional consumer-authored in-container probe run on a warm `setup:` stamp instead of the venv probe. Exit 0 trusts the stamp (skips); any nonzero re-provisions with network on (fail toward freshness). The probe text is NOT part of the stamp key, so a probe-only change needs `--refresh` or `--clean-stamps` to take effect. Example (asserting a warmed esbuild binary is Linux-shaped): `file frontend-editor/node_modules/esbuild/bin/esbuild | grep -q ELF`. |
|
|
258
259
|
| `mounts:` / `egress:` | Per-step workspace isolation. `mounts:` lists `{path, mode}` entries (`rw`/`ro`, default `ro`) and is **strict-by-default** - empty/missing mounts nothing; only declared-rw paths sync back to the repo. `egress:` is `{class, presets, targets}` (`class: browser` = broad-but-logged; otherwise a strict allowlist). See [Per-stage networking](#per-stage-networking) and [docs/setup.md](docs/setup.md). |
|
|
259
260
|
| `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). |
|
|
260
261
|
| `isolation:` (optional) | Configures the sandbox - image override, resource caps, `allow_git_mutations`, `seccomp_profile`. It can never disable it. See [browser sandbox](docs/browser-sandbox.md). |
|
|
@@ -270,12 +271,14 @@ run start (or when its inputs change), its `produces:` artifact survives attempt
|
|
|
270
271
|
starts, and the dev/review agents read that digest instead of re-deriving repo
|
|
271
272
|
rules and environment facts every attempt.
|
|
272
273
|
|
|
273
|
-
The brief digest is mechanically coverage-checked: the
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
274
|
+
The brief digest is mechanically coverage-checked: the configured
|
|
275
|
+
`brief_inputs.sections` headings must each appear on their own line, every
|
|
276
|
+
anchor extracted from the `include` files (markdown `##` headings, and YAML
|
|
277
|
+
category names plus `enforcement_levels`) must appear on its own line
|
|
278
|
+
(case-insensitive, whitespace-collapsed, `**` decoration allowed), and every
|
|
279
|
+
`env_facts` entry must appear verbatim. The section list feeds both the
|
|
280
|
+
engine-embedded brief prompt and the coverage gate, so the two can never
|
|
281
|
+
disagree; a miss
|
|
279
282
|
re-runs the brief agent once per missing set with a `## Coverage retry`
|
|
280
283
|
correction note, bounded to three retries per input set; if the same items are
|
|
281
284
|
still dropped after the cap, the run fails and `last-error.md` names the
|
|
@@ -205,12 +205,13 @@ steps:
|
|
|
205
205
|
| --- | --- |
|
|
206
206
|
| `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)). |
|
|
207
207
|
| `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
|
|
208
|
-
| `brief_inputs:` | Optional agent-step input spec: `{ include: [<paths/globs>], env_facts: [<strings>] }`. The engine resolves the include list against the workspace, passes the resolved docs + include contents + env facts as that step's task message, and regenerates the step only when those inputs change. A produces-only agent step such as `brief` is the documented pattern for per-run context digestion. |
|
|
208
|
+
| `brief_inputs:` | Optional agent-step input spec: `{ include: [<paths/globs>], env_facts: [<strings>], sections: [<headings>] }`. The engine resolves the include list against the workspace, passes the resolved docs + include contents + env facts as that 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). A produces-only agent step such as `brief` is the documented pattern for per-run context digestion. |
|
|
209
209
|
| `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. |
|
|
210
210
|
| `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). |
|
|
211
211
|
| `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. |
|
|
212
212
|
| `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
|
|
213
213
|
| `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` provisions the sandbox before the step runs (command or agent steps; network ON; stamp-cached, re-runs when the pipeline's lockfiles change); `network: true` lets a command/agent step reach the hosts its `egress:` allowlist names (default off) - see [Per-stage networking](#per-stage-networking). |
|
|
214
|
+
| `setup_probe:` | Optional consumer-authored in-container probe run on a warm `setup:` stamp instead of the venv probe. Exit 0 trusts the stamp (skips); any nonzero re-provisions with network on (fail toward freshness). The probe text is NOT part of the stamp key, so a probe-only change needs `--refresh` or `--clean-stamps` to take effect. Example (asserting a warmed esbuild binary is Linux-shaped): `file frontend-editor/node_modules/esbuild/bin/esbuild | grep -q ELF`. |
|
|
214
215
|
| `mounts:` / `egress:` | Per-step workspace isolation. `mounts:` lists `{path, mode}` entries (`rw`/`ro`, default `ro`) and is **strict-by-default** - empty/missing mounts nothing; only declared-rw paths sync back to the repo. `egress:` is `{class, presets, targets}` (`class: browser` = broad-but-logged; otherwise a strict allowlist). See [Per-stage networking](#per-stage-networking) and [docs/setup.md](docs/setup.md). |
|
|
215
216
|
| `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). |
|
|
216
217
|
| `isolation:` (optional) | Configures the sandbox - image override, resource caps, `allow_git_mutations`, `seccomp_profile`. It can never disable it. See [browser sandbox](docs/browser-sandbox.md). |
|
|
@@ -226,12 +227,14 @@ run start (or when its inputs change), its `produces:` artifact survives attempt
|
|
|
226
227
|
starts, and the dev/review agents read that digest instead of re-deriving repo
|
|
227
228
|
rules and environment facts every attempt.
|
|
228
229
|
|
|
229
|
-
The brief digest is mechanically coverage-checked: the
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
230
|
+
The brief digest is mechanically coverage-checked: the configured
|
|
231
|
+
`brief_inputs.sections` headings must each appear on their own line, every
|
|
232
|
+
anchor extracted from the `include` files (markdown `##` headings, and YAML
|
|
233
|
+
category names plus `enforcement_levels`) must appear on its own line
|
|
234
|
+
(case-insensitive, whitespace-collapsed, `**` decoration allowed), and every
|
|
235
|
+
`env_facts` entry must appear verbatim. The section list feeds both the
|
|
236
|
+
engine-embedded brief prompt and the coverage gate, so the two can never
|
|
237
|
+
disagree; a miss
|
|
235
238
|
re-runs the brief agent once per missing set with a `## Coverage retry`
|
|
236
239
|
correction note, bounded to three retries per input set; if the same items are
|
|
237
240
|
still dropped after the cap, the run fails and `last-error.md` names the
|
|
@@ -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.0a45"
|
|
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"
|
|
@@ -25,6 +25,16 @@ HOST_ONLY_MOUNT_DIRS = frozenset({".venv"})
|
|
|
25
25
|
KNOWN_EGRESS_PRESETS = frozenset({"package-registries"})
|
|
26
26
|
EGRESS_CLASSES = frozenset({"allowlist", "browser"})
|
|
27
27
|
|
|
28
|
+
# The neutral default section headings a brief step must emit (046). A
|
|
29
|
+
# consumer overrides them via `brief_inputs.sections`; the coverage gate
|
|
30
|
+
# and the embedded brief prompt both render from this list.
|
|
31
|
+
DEFAULT_BRIEF_SECTIONS = [
|
|
32
|
+
"Repo rules digest",
|
|
33
|
+
"Unit scope digest",
|
|
34
|
+
"Environment facts",
|
|
35
|
+
"Open risks",
|
|
36
|
+
]
|
|
37
|
+
|
|
28
38
|
|
|
29
39
|
def _validate_mount_path(path: str) -> str:
|
|
30
40
|
"""Validate one repo-relative mount path (BR-1.3).
|
|
@@ -318,8 +328,9 @@ class BriefInputsSpec(BaseModel):
|
|
|
318
328
|
|
|
319
329
|
include: list[str] = []
|
|
320
330
|
env_facts: list[str] = []
|
|
331
|
+
sections: list[str] = DEFAULT_BRIEF_SECTIONS
|
|
321
332
|
|
|
322
|
-
@field_validator("include", "env_facts", mode="before")
|
|
333
|
+
@field_validator("include", "env_facts", "sections", mode="before")
|
|
323
334
|
@classmethod
|
|
324
335
|
def _coerce_list(cls, v: Any) -> Any:
|
|
325
336
|
return [v] if isinstance(v, str) else v
|
|
@@ -338,6 +349,21 @@ class BriefInputsSpec(BaseModel):
|
|
|
338
349
|
"brief_inputs.env_facts entries must be non-empty strings")
|
|
339
350
|
return v
|
|
340
351
|
|
|
352
|
+
@field_validator("sections")
|
|
353
|
+
@classmethod
|
|
354
|
+
def _valid_sections(cls, v: list[str]) -> list[str]:
|
|
355
|
+
for section in v:
|
|
356
|
+
if not isinstance(section, str) or not section.strip():
|
|
357
|
+
raise ValueError(
|
|
358
|
+
"brief_inputs.sections entries must be non-empty strings")
|
|
359
|
+
if not v:
|
|
360
|
+
raise ValueError(
|
|
361
|
+
"brief_inputs.sections must be a non-empty list of headings")
|
|
362
|
+
if len(set(v)) != len(v):
|
|
363
|
+
raise ValueError(
|
|
364
|
+
"brief_inputs.sections entries must be unique")
|
|
365
|
+
return v
|
|
366
|
+
|
|
341
367
|
|
|
342
368
|
class StepConfig(BaseModel):
|
|
343
369
|
"""One pipeline step. Its type IS the key it carries: exactly one of
|
|
@@ -362,6 +388,7 @@ class StepConfig(BaseModel):
|
|
|
362
388
|
custom_prompt: str | None = None # agent steps only (040); file path resolved at load
|
|
363
389
|
env: dict[str, str | None] | None = None # NFR-1: null = inherit host value
|
|
364
390
|
setup: str | None = None # BR-13: command steps only
|
|
391
|
+
setup_probe: str | None = None # 045: consumer probe replacing the venv probe on a warm stamp; requires `setup`; probe text is NOT part of the stamp key (a probe-only change needs --refresh/--clean-stamps)
|
|
365
392
|
forbidden_commands: list[str] = [] # banned-command regex deny-list (036 D1)
|
|
366
393
|
network: bool = False # per-stage egress (command/agent + sandbox actions; host-side actions reject at registry time)
|
|
367
394
|
# unit-5: per-step mount granularity + egress policy. Empty mounts =
|
|
@@ -420,6 +447,13 @@ class StepConfig(BaseModel):
|
|
|
420
447
|
raise ValueError("custom_prompt must be a non-empty string")
|
|
421
448
|
return v
|
|
422
449
|
|
|
450
|
+
@field_validator("setup_probe")
|
|
451
|
+
@classmethod
|
|
452
|
+
def _nonempty_setup_probe(cls, v: str | None) -> str | None:
|
|
453
|
+
if v is not None and not v.strip():
|
|
454
|
+
raise ValueError("setup_probe must be a non-empty string")
|
|
455
|
+
return v
|
|
456
|
+
|
|
423
457
|
@model_validator(mode="after")
|
|
424
458
|
def _check_shape(self) -> "StepConfig":
|
|
425
459
|
for key in ("agent", "action", "command"):
|
|
@@ -494,6 +528,10 @@ class StepConfig(BaseModel):
|
|
|
494
528
|
raise ValueError(
|
|
495
529
|
f"step {self.name!r}: 'setup' is only allowed on 'command' or 'agent' steps"
|
|
496
530
|
)
|
|
531
|
+
if self.setup_probe is not None and self.setup is None:
|
|
532
|
+
raise ValueError(
|
|
533
|
+
f"step {self.name!r}: 'setup_probe' requires 'setup'"
|
|
534
|
+
)
|
|
497
535
|
# `network` is NOT rejected here: the schema can't tell a host-side
|
|
498
536
|
# built-in action (e.g. `commit`, which must never take egress) from
|
|
499
537
|
# a sandbox-executed consumer action (which may, like a command step).
|
{agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/agents.py
RENAMED
|
@@ -16,6 +16,7 @@ from openhands.tools.task_tracker import TaskTrackerTool # noqa: F401 - registe
|
|
|
16
16
|
from openhands.tools.terminal import TerminalTool # noqa: F401 - registers "terminal"
|
|
17
17
|
|
|
18
18
|
from .forbidden import ForbiddenRule, match_forbidden_command
|
|
19
|
+
from .brief_prompt import render_brief_prompt
|
|
19
20
|
from .isolation import (SandboxError, SandboxExecutionRequest, command_argv,
|
|
20
21
|
resolve_sandbox_env)
|
|
21
22
|
|
|
@@ -59,6 +60,24 @@ def load_agent(agents_dir: Path | str, name: str, llm: LLM) -> "Agent":
|
|
|
59
60
|
return agent_definition_to_factory(agent_def)(llm)
|
|
60
61
|
|
|
61
62
|
|
|
63
|
+
def load_brief_agent(name: str, sections: list[str], llm: LLM) -> "Agent":
|
|
64
|
+
"""Build the brief agent from the engine-embedded prompt (046).
|
|
65
|
+
|
|
66
|
+
The brief prompt is engine-owned and versioned; it is never loaded from
|
|
67
|
+
``agents_dir``. The OUTPUT contract's headings are rendered from the
|
|
68
|
+
configured ``brief_inputs.sections``, so the prompt and the coverage gate
|
|
69
|
+
share one list and cannot diverge (043)."""
|
|
70
|
+
from openhands.sdk.subagent.registry import agent_definition_to_factory
|
|
71
|
+
from openhands.sdk.subagent.schema import AgentDefinition
|
|
72
|
+
|
|
73
|
+
agent_def = AgentDefinition(
|
|
74
|
+
name=name,
|
|
75
|
+
tools=["terminal"],
|
|
76
|
+
system_prompt=render_brief_prompt(sections),
|
|
77
|
+
)
|
|
78
|
+
return agent_definition_to_factory(agent_def)(llm)
|
|
79
|
+
|
|
80
|
+
|
|
62
81
|
class SandboxTerminalExecutor(ToolExecutor):
|
|
63
82
|
"""A stateless terminal executor: each action is a fresh ``Sandbox.run``
|
|
64
83
|
(no persistent shell session). ``is_input`` degrades to an unsupported
|
|
@@ -12,13 +12,6 @@ from __future__ import annotations
|
|
|
12
12
|
|
|
13
13
|
import re
|
|
14
14
|
|
|
15
|
-
_REQUIRED_SECTIONS = (
|
|
16
|
-
"Repo rules digest",
|
|
17
|
-
"Unit scope and lane digest",
|
|
18
|
-
"Environment facts",
|
|
19
|
-
"Open risks",
|
|
20
|
-
)
|
|
21
|
-
|
|
22
15
|
|
|
23
16
|
def _normalize(text: str) -> str:
|
|
24
17
|
"""Case-fold and collapse whitespace - the matching normal form."""
|
|
@@ -38,11 +31,6 @@ def _normalize_line(line: str) -> str:
|
|
|
38
31
|
return _normalize(stripped)
|
|
39
32
|
|
|
40
33
|
|
|
41
|
-
def required_sections() -> list[str]:
|
|
42
|
-
"""The four template headings the brief must emit."""
|
|
43
|
-
return list(_REQUIRED_SECTIONS)
|
|
44
|
-
|
|
45
|
-
|
|
46
34
|
def extract_anchors(content: str) -> list[str]:
|
|
47
35
|
"""Derive the mechanical anchor set from one include file.
|
|
48
36
|
|
|
@@ -75,16 +63,17 @@ def extract_anchors(content: str) -> list[str]:
|
|
|
75
63
|
|
|
76
64
|
|
|
77
65
|
def missing_anchors(brief_text: str, anchors: list[str],
|
|
78
|
-
env_facts: list[str]) -> list[str]:
|
|
66
|
+
env_facts: list[str], sections: list[str]) -> list[str]:
|
|
79
67
|
"""Return every required item the captured brief fails to cover.
|
|
80
68
|
|
|
81
|
-
Required section headings
|
|
82
|
-
(case-insensitive,
|
|
83
|
-
|
|
84
|
-
|
|
69
|
+
Required section headings (from the configured ``brief_inputs.sections``)
|
|
70
|
+
and anchors are matched line-anchored (case-insensitive,
|
|
71
|
+
whitespace-collapsed, ``**`` decoration stripped). Env facts are matched
|
|
72
|
+
verbatim, exactly as handed to the brief agent. An empty list means the
|
|
73
|
+
digest passes coverage."""
|
|
85
74
|
missing: list[str] = []
|
|
86
75
|
lines = {_normalize_line(line) for line in brief_text.splitlines()}
|
|
87
|
-
for section in
|
|
76
|
+
for section in sections:
|
|
88
77
|
if _normalize(section) not in lines:
|
|
89
78
|
missing.append(section)
|
|
90
79
|
for anchor in anchors:
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Brief Agent
|
|
2
|
+
|
|
3
|
+
You are the per-run context digester for a gated construction loop. You run
|
|
4
|
+
once per run (or once when the inputs change) and turn a closed input set into
|
|
5
|
+
a compact brief that the dev and review agents reuse on every attempt.
|
|
6
|
+
|
|
7
|
+
## WORKFLOW
|
|
8
|
+
|
|
9
|
+
1. Read every input section in the task message below. The inputs are the
|
|
10
|
+
resolved unit/plan docs, the listed repo-rule files, and the
|
|
11
|
+
`Environment facts` block. That is the closed list - nothing else.
|
|
12
|
+
2. Digest the inputs into the configured sections named in OUTPUT below.
|
|
13
|
+
Digest, do not transcribe: the brief is compact working memory, not a copy
|
|
14
|
+
of the inputs.
|
|
15
|
+
3. State the brief in your final response. The pipeline captures it as
|
|
16
|
+
`attempt-brief.md`.
|
|
17
|
+
|
|
18
|
+
## RULES
|
|
19
|
+
|
|
20
|
+
- One pass, no exploration. Read the listed inputs, emit the brief, finish.
|
|
21
|
+
Do not explore, do not verify, do not run commands, and do not read anything
|
|
22
|
+
not listed in the task message. Your inputs are already embedded in the task
|
|
23
|
+
message; the terminal is available only if a listed input must be re-read.
|
|
24
|
+
- Prior verdicts and feedback are deliberately absent from your inputs. They
|
|
25
|
+
reach the dev and review agents through a separate notes channel every
|
|
26
|
+
attempt. Do not invent them and do not re-derive them.
|
|
27
|
+
- The repo-rules digest is structural: one line per top-level section of each
|
|
28
|
+
repo-rule input. Markdown anchors are the `##` headings: keep each heading
|
|
29
|
+
verbatim, then compress that section's rules into the one line that follows
|
|
30
|
+
it. YAML anchors are each `- name:` entry under `categories:` plus the
|
|
31
|
+
`enforcement_levels` preamble: keep each name verbatim. No top-level section
|
|
32
|
+
or category may be dropped; merging is allowed only between a section and
|
|
33
|
+
its own subsections. The anchor line holds the heading and nothing else; the
|
|
34
|
+
compressed rules go on the following line:
|
|
35
|
+
|
|
36
|
+
- general_principles
|
|
37
|
+
Simplest thing that works; defer decisions to the last responsible moment.
|
|
38
|
+
|
|
39
|
+
A line like `general_principles: Simplest thing that works; ...` does not
|
|
40
|
+
satisfy coverage; the heading must be the only text on its line.
|
|
41
|
+
- Keep the environment-facts entries verbatim: each fact and the behavior it
|
|
42
|
+
implies travel together.
|
|
43
|
+
- ASCII only. Do not use em dash characters.
|
|
44
|
+
- If the task message carries a `## Coverage retry` section, regenerate the
|
|
45
|
+
brief so every listed item appears, verbatim where the contract requires
|
|
46
|
+
verbatim. No exploration: the listed items are the only correction signal.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Bundled brief-agent prompt (046).
|
|
2
|
+
|
|
3
|
+
The brief prompt is an engine-owned resource, versioned with the engine so the
|
|
4
|
+
prompt contract and the coverage gate cannot drift (the 043 divergence class).
|
|
5
|
+
The static prose is packaged and read at runtime like the 034 error guide; the
|
|
6
|
+
section-dependent OUTPUT block is rendered here from the configured
|
|
7
|
+
``brief_inputs.sections`` list, so one list feeds the prompt and the gate.
|
|
8
|
+
|
|
9
|
+
``BRIEF_PROMPT_VERSION`` joins the brief digest hash: an engine upgrade (or a
|
|
10
|
+
sections edit) busts a cached digest and forces a fresh capture.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from importlib.resources import files
|
|
16
|
+
|
|
17
|
+
BRIEF_PROMPT_RESOURCE = "brief_prompt.md"
|
|
18
|
+
BRIEF_PROMPT_VERSION = "1"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def load_template() -> str:
|
|
22
|
+
"""The static brief-prompt prose (header, workflow, rules)."""
|
|
23
|
+
return (files("agentic_dev_loop.engine")
|
|
24
|
+
.joinpath(BRIEF_PROMPT_RESOURCE)
|
|
25
|
+
.read_text(encoding="utf-8"))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _output_block(sections: list[str]) -> str:
|
|
29
|
+
"""Render the OUTPUT contract from the configured section headings."""
|
|
30
|
+
headings = "\n\n".join(f"## {section}" for section in sections)
|
|
31
|
+
names = ", ".join(f"`## {section}`" for section in sections)
|
|
32
|
+
return (
|
|
33
|
+
"## OUTPUT\n\n"
|
|
34
|
+
"Start your final response with exactly this structure. Each configured "
|
|
35
|
+
"section heading appears as its own `## ` line, followed by the "
|
|
36
|
+
"compressed content for that section (write `None` when a section has "
|
|
37
|
+
"nothing):\n\n"
|
|
38
|
+
f"{headings}\n\n"
|
|
39
|
+
f"The section headings must appear exactly as written above: {names}. "
|
|
40
|
+
"Do not shorten or rename them, and do not merge a section with "
|
|
41
|
+
"another section."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def render_brief_prompt(sections: list[str]) -> str:
|
|
46
|
+
"""The full brief prompt: static prose plus the section-rendered OUTPUT."""
|
|
47
|
+
return load_template() + "\n\n" + _output_block(sections)
|
{agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/dryrun.py
RENAMED
|
@@ -25,12 +25,16 @@ from .secrets import scan_env_files
|
|
|
25
25
|
|
|
26
26
|
def _validate_agents(cfg: PipelineConfig, agents_dir: Path) -> None:
|
|
27
27
|
"""Every `agent:` step must resolve to an existing file under agents_dir
|
|
28
|
-
(the same fail-fast `load_agent` enforces at run time).
|
|
28
|
+
(the same fail-fast `load_agent` enforces at run time).
|
|
29
|
+
|
|
30
|
+
The brief prompt is engine-embedded (046), so a ``brief_inputs:`` step is
|
|
31
|
+
exempt from the agents_dir lookup - it never reads a vendored file."""
|
|
29
32
|
for step in cfg.steps:
|
|
30
|
-
if step.agent is not None:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
if step.agent is None or step.brief_inputs is not None:
|
|
34
|
+
continue
|
|
35
|
+
path = agents_dir / f"{step.agent}.agent.md"
|
|
36
|
+
if not path.is_file():
|
|
37
|
+
raise FileNotFoundError(f"Agent file not found: {path}")
|
|
34
38
|
|
|
35
39
|
|
|
36
40
|
def _validate_actions(cfg: PipelineConfig, registry: Registry) -> None:
|
{agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/isolation.py
RENAMED
|
@@ -534,11 +534,19 @@ def venv_probe_command() -> str:
|
|
|
534
534
|
)
|
|
535
535
|
|
|
536
536
|
|
|
537
|
+
def probe_argv(probe_cmd: str, container: str, workspace: str) -> list[str]:
|
|
538
|
+
"""``docker exec`` argv that runs an arbitrary in-container probe command
|
|
539
|
+
(045) inside the already-running execution container against the
|
|
540
|
+
bind-mounted workspace. ``probe_cmd`` is a consumer-authored shell command
|
|
541
|
+
string passed verbatim to ``/bin/sh -c``."""
|
|
542
|
+
return ["docker", "exec", "-w", workspace, container,
|
|
543
|
+
"/bin/sh", "-c", probe_cmd]
|
|
544
|
+
|
|
545
|
+
|
|
537
546
|
def venv_probe_argv(container: str, workspace: str) -> list[str]:
|
|
538
547
|
"""``docker exec`` argv that runs the venv probe inside the (already
|
|
539
548
|
running) execution container against the bind-mounted workspace."""
|
|
540
|
-
return
|
|
541
|
-
"/bin/sh", "-c", venv_probe_command()]
|
|
549
|
+
return probe_argv(venv_probe_command(), container, workspace)
|
|
542
550
|
|
|
543
551
|
|
|
544
552
|
def venv_probe_state(stdout: str, exit_code: int | None) -> str:
|
|
@@ -1133,7 +1141,7 @@ class DockerBackend(Sandbox):
|
|
|
1133
1141
|
self._sync_excludes)
|
|
1134
1142
|
|
|
1135
1143
|
def provision(self, setup_cmd: str, stamp_key: str, stamp_dir: str,
|
|
1136
|
-
force: bool) -> None:
|
|
1144
|
+
force: bool, setup_probe: str | None = None) -> None:
|
|
1137
1145
|
"""Run one ``setup:`` hook in the provision container (network ON),
|
|
1138
1146
|
stamp-cached. Nonzero exit → AE-SBX-009, no stamp written.
|
|
1139
1147
|
|
|
@@ -1143,13 +1151,21 @@ class DockerBackend(Sandbox):
|
|
|
1143
1151
|
broken in the Linux container. A ``broken`` probe removes the venv
|
|
1144
1152
|
(fail loud if removal is impossible) and falls through to re-provision;
|
|
1145
1153
|
``valid``/``missing``/``unknown`` keep the stamp skip.
|
|
1154
|
+
|
|
1155
|
+
045: when ``setup_probe`` is set it replaces the venv probe for this
|
|
1156
|
+
hook. Exit 0 trusts the stamp (skip); any nonzero - a broken artifact
|
|
1157
|
+
or a failed probe run - falls through to re-provision (fail toward
|
|
1158
|
+
freshness).
|
|
1146
1159
|
"""
|
|
1147
1160
|
if not force and read_stamp(Path(stamp_dir), stamp_key):
|
|
1148
|
-
if
|
|
1161
|
+
if setup_probe is not None:
|
|
1162
|
+
if self._probe(setup_probe).exit_code == 0:
|
|
1163
|
+
return
|
|
1164
|
+
else:
|
|
1165
|
+
if self._probe_venv() != "broken":
|
|
1166
|
+
return
|
|
1149
1167
|
remove_sandbox_venv(self._workspace,
|
|
1150
1168
|
sandbox_venv_dir=self._sandbox_venv_dir)
|
|
1151
|
-
else:
|
|
1152
|
-
return
|
|
1153
1169
|
payload = docker_provision_payload(
|
|
1154
1170
|
run_id=self._run_id, image=self._image, workspace=self._workspace,
|
|
1155
1171
|
setup_cmd=setup_cmd, sandbox_venv_dir=self._sandbox_venv_dir,
|
|
@@ -1162,13 +1178,18 @@ class DockerBackend(Sandbox):
|
|
|
1162
1178
|
f"- stamp not written (docs/setup.md §5)")
|
|
1163
1179
|
write_stamp(Path(stamp_dir), stamp_key)
|
|
1164
1180
|
|
|
1181
|
+
def _probe(self, probe_cmd: str) -> CmdResult:
|
|
1182
|
+
"""Run an arbitrary in-container probe command (045). The workdir
|
|
1183
|
+
follows the export root when one is materialised, else the workspace."""
|
|
1184
|
+
workdir = self._export_root or self._workspace
|
|
1185
|
+
return self._runner.run(
|
|
1186
|
+
probe_argv(probe_cmd, self._container, workdir), timeout=30)
|
|
1187
|
+
|
|
1165
1188
|
def _probe_venv(self) -> str:
|
|
1166
1189
|
"""Run the in-container venv probe and classify it (016 self-heal).
|
|
1167
1190
|
With an export the venv is mounted at ``{export}/.venv-sandbox``, so
|
|
1168
1191
|
the probe workdir follows the export root."""
|
|
1169
|
-
|
|
1170
|
-
res = self._runner.run(
|
|
1171
|
-
venv_probe_argv(self._container, workdir), timeout=30)
|
|
1192
|
+
res = self._probe(venv_probe_command())
|
|
1172
1193
|
return venv_probe_state(res.stdout, res.exit_code)
|
|
1173
1194
|
|
|
1174
1195
|
# -- execution ----------------------------------------------------------
|
|
@@ -1903,7 +1924,8 @@ def _declared_setups(cfg: PipelineConfig, workspace: Path):
|
|
|
1903
1924
|
if step.setup is not None and (
|
|
1904
1925
|
step.command is not None or step.agent is not None
|
|
1905
1926
|
):
|
|
1906
|
-
yield step.setup, setup_stamp_key(step.setup, hashes)
|
|
1927
|
+
yield (step.setup, setup_stamp_key(step.setup, hashes),
|
|
1928
|
+
step.setup_probe)
|
|
1907
1929
|
|
|
1908
1930
|
|
|
1909
1931
|
def isolation_state_dir(cfg: PipelineConfig) -> Path:
|
|
@@ -2017,10 +2039,11 @@ def startup_sandbox(cfg: PipelineConfig, *, workspace: Path,
|
|
|
2017
2039
|
_announce("execution container started")
|
|
2018
2040
|
if refresh is None:
|
|
2019
2041
|
refresh = bool(cfg.isolation and cfg.isolation.refresh)
|
|
2020
|
-
for setup_cmd, stamp_key in _declared_setups(cfg, workspace):
|
|
2042
|
+
for setup_cmd, stamp_key, setup_probe in _declared_setups(cfg, workspace):
|
|
2021
2043
|
_announce(f"provisioning setup (stamp {stamp_key[:8]})")
|
|
2022
2044
|
backend.provision(setup_cmd, stamp_key,
|
|
2023
|
-
str(iso_state_dir / "stamps"), force=refresh
|
|
2045
|
+
str(iso_state_dir / "stamps"), force=refresh,
|
|
2046
|
+
setup_probe=setup_probe)
|
|
2024
2047
|
_announce("sandbox ready")
|
|
2025
2048
|
return backend
|
|
2026
2049
|
except BaseException:
|
{agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45}/src/agentic_dev_loop/engine/stages.py
RENAMED
|
@@ -32,8 +32,9 @@ from langgraph.graph import END
|
|
|
32
32
|
from openhands.sdk import Conversation
|
|
33
33
|
|
|
34
34
|
from .agent_error_guide import guide_section
|
|
35
|
-
from .agents import load_agent, register_sandbox_terminal
|
|
35
|
+
from .agents import load_agent, load_brief_agent, register_sandbox_terminal
|
|
36
36
|
from .brief_coverage import extract_anchors, missing_anchors
|
|
37
|
+
from .brief_prompt import BRIEF_PROMPT_VERSION
|
|
37
38
|
from agentic_dev_loop.core.config import TERMINALS, PipelineConfig, Route, StepConfig, route_target
|
|
38
39
|
from agentic_dev_loop.core.paths import _has_wildcards
|
|
39
40
|
from .deps import extension_message, parse_missing_dependencies, policy_injection
|
|
@@ -173,7 +174,11 @@ class Runtime:
|
|
|
173
174
|
if step is not None else None),
|
|
174
175
|
forbidden_commands=forbidden_commands,
|
|
175
176
|
allowlist=allowlist)
|
|
176
|
-
|
|
177
|
+
if step is not None and step.brief_inputs is not None:
|
|
178
|
+
agent = load_brief_agent(
|
|
179
|
+
agent_name, step.brief_inputs.sections, self.llm)
|
|
180
|
+
else:
|
|
181
|
+
agent = load_agent(self.agents_dir, agent_name, self.llm)
|
|
177
182
|
callbacks = []
|
|
178
183
|
if finalize_controller is not None:
|
|
179
184
|
callbacks.append(finalize_controller.on_event)
|
|
@@ -543,6 +548,14 @@ def brief_inputs_hash(runtime: Runtime, step: StepConfig,
|
|
|
543
548
|
brief step, otherwise the existing digest is reused without an LLM call."""
|
|
544
549
|
sections, env_facts = _brief_sections(runtime, step, docs, doc_sources)
|
|
545
550
|
digest = hashlib.sha256()
|
|
551
|
+
# 046, D6: the embedded prompt version and the configured section headings
|
|
552
|
+
# join the hash, so an engine upgrade or a sections edit busts the cached
|
|
553
|
+
# digest and forces a fresh capture.
|
|
554
|
+
digest.update(BRIEF_PROMPT_VERSION.encode("utf-8"))
|
|
555
|
+
digest.update(b"\0")
|
|
556
|
+
for section in step.brief_inputs.sections:
|
|
557
|
+
digest.update(section.encode("utf-8"))
|
|
558
|
+
digest.update(b"\0")
|
|
546
559
|
for key in sorted(sections):
|
|
547
560
|
digest.update(key.encode("utf-8"))
|
|
548
561
|
digest.update(b"\0")
|
|
@@ -751,6 +764,20 @@ def make_agent_node(runtime: Runtime, step: StepConfig):
|
|
|
751
764
|
return node
|
|
752
765
|
|
|
753
766
|
|
|
767
|
+
def _warn_vendored_brief(runtime: Runtime, step: StepConfig) -> None:
|
|
768
|
+
"""Warn once per load when a vendored brief prompt exists (046, D5).
|
|
769
|
+
|
|
770
|
+
The embedded prompt is authoritative, so a leftover
|
|
771
|
+
``<agents_dir>/<agent>.agent.md`` for a ``brief_inputs:`` step is ignored;
|
|
772
|
+
the warning names it without failing the run."""
|
|
773
|
+
vendored = runtime.agents_dir / f"{step.agent}.agent.md"
|
|
774
|
+
if vendored.is_file():
|
|
775
|
+
print(
|
|
776
|
+
f"[{runtime.cfg.name}] ignoring vendored brief prompt {vendored} - "
|
|
777
|
+
"the engine-embedded prompt is authoritative",
|
|
778
|
+
file=sys.stderr, flush=True)
|
|
779
|
+
|
|
780
|
+
|
|
754
781
|
def make_brief_node(runtime: Runtime, step: StepConfig):
|
|
755
782
|
"""Node closure for a ``brief_inputs:`` agent step (033).
|
|
756
783
|
|
|
@@ -768,6 +795,7 @@ def make_brief_node(runtime: Runtime, step: StepConfig):
|
|
|
768
795
|
re-runs the agent with a ``## Coverage retry`` correction note. Exhausting
|
|
769
796
|
the budget writes last-error.md and fails the run."""
|
|
770
797
|
captures = step.artifact is not None or bool(step.produces)
|
|
798
|
+
_warn_vendored_brief(runtime, step)
|
|
771
799
|
|
|
772
800
|
def node(state: dict) -> dict:
|
|
773
801
|
attempt = state["attempt"]
|
|
@@ -780,6 +808,8 @@ def make_brief_node(runtime: Runtime, step: StepConfig):
|
|
|
780
808
|
anchors = _brief_anchors(runtime, step)
|
|
781
809
|
env_facts = (list(step.brief_inputs.env_facts)
|
|
782
810
|
if step.brief_inputs is not None else [])
|
|
811
|
+
sections = (list(step.brief_inputs.sections)
|
|
812
|
+
if step.brief_inputs is not None else [])
|
|
783
813
|
except Exception:
|
|
784
814
|
err = redact_values(traceback.format_exc(), runtime.secrets.values())
|
|
785
815
|
print(err[-4000:], file=sys.stderr, flush=True)
|
|
@@ -793,7 +823,7 @@ def make_brief_node(runtime: Runtime, step: StepConfig):
|
|
|
793
823
|
reused = (existing is not None
|
|
794
824
|
and state.get("brief_hashes", {}).get(step.name) == current_hash)
|
|
795
825
|
|
|
796
|
-
if reused and not missing_anchors(existing, anchors, env_facts):
|
|
826
|
+
if reused and not missing_anchors(existing, anchors, env_facts, sections):
|
|
797
827
|
print(f"[{subject}] {step.name} brief inputs unchanged - reusing.",
|
|
798
828
|
flush=True)
|
|
799
829
|
merged = {**state["step_verdicts"], step.name: "pass"}
|
|
@@ -802,7 +832,7 @@ def make_brief_node(runtime: Runtime, step: StepConfig):
|
|
|
802
832
|
coverage_note = None
|
|
803
833
|
if reused:
|
|
804
834
|
coverage_note = _coverage_note_text(
|
|
805
|
-
missing_anchors(existing, anchors, env_facts))
|
|
835
|
+
missing_anchors(existing, anchors, env_facts, sections))
|
|
806
836
|
message = build_brief_context(runtime, step, state["docs"],
|
|
807
837
|
state.get("doc_sources"),
|
|
808
838
|
coverage_note=coverage_note)
|
|
@@ -824,7 +854,7 @@ def make_brief_node(runtime: Runtime, step: StepConfig):
|
|
|
824
854
|
result.truncated, max_iterations)],
|
|
825
855
|
"retry_target": _unclear_target(runtime, step)}
|
|
826
856
|
|
|
827
|
-
missing = missing_anchors(result.response or "", anchors, env_facts)
|
|
857
|
+
missing = missing_anchors(result.response or "", anchors, env_facts, sections)
|
|
828
858
|
if missing:
|
|
829
859
|
prev_hash = state.get("brief_hashes", {}).get(step.name)
|
|
830
860
|
retries = (0 if prev_hash != current_hash
|
{agentic_dev_loop-1.0.0a43 → agentic_dev_loop-1.0.0a45/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.0a45
|
|
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
|
|
@@ -249,12 +249,13 @@ steps:
|
|
|
249
249
|
| --- | --- |
|
|
250
250
|
| `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)). |
|
|
251
251
|
| `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
|
|
252
|
-
| `brief_inputs:` | Optional agent-step input spec: `{ include: [<paths/globs>], env_facts: [<strings>] }`. The engine resolves the include list against the workspace, passes the resolved docs + include contents + env facts as that step's task message, and regenerates the step only when those inputs change. A produces-only agent step such as `brief` is the documented pattern for per-run context digestion. |
|
|
252
|
+
| `brief_inputs:` | Optional agent-step input spec: `{ include: [<paths/globs>], env_facts: [<strings>], sections: [<headings>] }`. The engine resolves the include list against the workspace, passes the resolved docs + include contents + env facts as that 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). A produces-only agent step such as `brief` is the documented pattern for per-run context digestion. |
|
|
253
253
|
| `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. |
|
|
254
254
|
| `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). |
|
|
255
255
|
| `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. |
|
|
256
256
|
| `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
|
|
257
257
|
| `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` provisions the sandbox before the step runs (command or agent steps; network ON; stamp-cached, re-runs when the pipeline's lockfiles change); `network: true` lets a command/agent step reach the hosts its `egress:` allowlist names (default off) - see [Per-stage networking](#per-stage-networking). |
|
|
258
|
+
| `setup_probe:` | Optional consumer-authored in-container probe run on a warm `setup:` stamp instead of the venv probe. Exit 0 trusts the stamp (skips); any nonzero re-provisions with network on (fail toward freshness). The probe text is NOT part of the stamp key, so a probe-only change needs `--refresh` or `--clean-stamps` to take effect. Example (asserting a warmed esbuild binary is Linux-shaped): `file frontend-editor/node_modules/esbuild/bin/esbuild | grep -q ELF`. |
|
|
258
259
|
| `mounts:` / `egress:` | Per-step workspace isolation. `mounts:` lists `{path, mode}` entries (`rw`/`ro`, default `ro`) and is **strict-by-default** - empty/missing mounts nothing; only declared-rw paths sync back to the repo. `egress:` is `{class, presets, targets}` (`class: browser` = broad-but-logged; otherwise a strict allowlist). See [Per-stage networking](#per-stage-networking) and [docs/setup.md](docs/setup.md). |
|
|
259
260
|
| `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). |
|
|
260
261
|
| `isolation:` (optional) | Configures the sandbox - image override, resource caps, `allow_git_mutations`, `seccomp_profile`. It can never disable it. See [browser sandbox](docs/browser-sandbox.md). |
|
|
@@ -270,12 +271,14 @@ run start (or when its inputs change), its `produces:` artifact survives attempt
|
|
|
270
271
|
starts, and the dev/review agents read that digest instead of re-deriving repo
|
|
271
272
|
rules and environment facts every attempt.
|
|
272
273
|
|
|
273
|
-
The brief digest is mechanically coverage-checked: the
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
274
|
+
The brief digest is mechanically coverage-checked: the configured
|
|
275
|
+
`brief_inputs.sections` headings must each appear on their own line, every
|
|
276
|
+
anchor extracted from the `include` files (markdown `##` headings, and YAML
|
|
277
|
+
category names plus `enforcement_levels`) must appear on its own line
|
|
278
|
+
(case-insensitive, whitespace-collapsed, `**` decoration allowed), and every
|
|
279
|
+
`env_facts` entry must appear verbatim. The section list feeds both the
|
|
280
|
+
engine-embedded brief prompt and the coverage gate, so the two can never
|
|
281
|
+
disagree; a miss
|
|
279
282
|
re-runs the brief agent once per missing set with a `## Coverage retry`
|
|
280
283
|
correction note, bounded to three retries per input set; if the same items are
|
|
281
284
|
still dropped after the cap, the run fails and `last-error.md` names the
|