agentic-dev-loop 1.0.0a28__tar.gz → 1.0.0a30__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.0a28/src/agentic_dev_loop.egg-info → agentic_dev_loop-1.0.0a30}/PKG-INFO +18 -1
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/README.md +17 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/pyproject.toml +1 -1
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/core/__init__.py +3 -1
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/core/config.py +88 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/engine/agents.py +17 -3
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/engine/dryrun.py +4 -1
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/engine/extensions/aidlc.py +23 -1
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/engine/graph.py +13 -5
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/engine/isolation.py +11 -2
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/engine/loaders.py +18 -3
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/engine/stages.py +347 -7
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30/src/agentic_dev_loop.egg-info}/PKG-INFO +18 -1
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop.egg-info/SOURCES.txt +2 -0
- agentic_dev_loop-1.0.0a30/tests/test_032_stop_when_green.py +363 -0
- agentic_dev_loop-1.0.0a30/tests/test_033_brief_step.py +573 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_e2e_fixtures.py +10 -4
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_sdk_compat.py +27 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_viz.py +1 -1
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/LICENSE +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/setup.cfg +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/core/paths.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/core/results.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/core/verdict.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/engine/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/engine/actions.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/engine/cli.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/engine/deps.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/engine/egress_policy.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/engine/egress_proxy.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/engine/hostboundary.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/engine/llm.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/engine/mounts.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/engine/outputs.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/engine/presets.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/engine/proc.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/engine/registry.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/engine/secrets.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/engine/step_workspace.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/engine/sync_back.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/py.typed +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/viz/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/viz/__main__.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/viz/server.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_028_declared_caches.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_029_merge_on_conflict.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_actions.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_agents.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_cli.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_config.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_deps.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_dry_run.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_egress_proxy.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_graph.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_hostboundary.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_hostboundary_integration.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_isolation.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_isolation_integration.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_isolation_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_llm.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_loaders.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_module_contract.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_outputs.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_outputs_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_proc.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_publish.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_qa_prompt.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_registry.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_repo_ascii.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_results.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_secrets.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_secrets_integration.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_secrets_pipeline.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_source_resolution.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_stages.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_step_workspace.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_sync_back.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_trigger_script.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_unit5_config.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_unit5_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_unit5_pure.py +0 -0
- {agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/tests/test_verdict.py +0 -0
{agentic_dev_loop-1.0.0a28/src/agentic_dev_loop.egg-info → agentic_dev_loop-1.0.0a30}/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.0a30
|
|
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
|
|
@@ -192,9 +192,18 @@ max_attempts: 10 # CLI --max-attempts ove
|
|
|
192
192
|
agents_dir: sdk_agents # your <name>.agent.md files
|
|
193
193
|
|
|
194
194
|
steps:
|
|
195
|
+
- name: brief
|
|
196
|
+
agent: brief
|
|
197
|
+
produces: attempt-brief.md
|
|
198
|
+
max_iterations: 12
|
|
199
|
+
brief_inputs: { include: [AGENTS.md, coding-standards-ddd.yaml], env_facts: ["the workspace is an export with no .git; verify with find/ls, never git status"] }
|
|
200
|
+
on_pass: dev
|
|
201
|
+
|
|
195
202
|
- name: dev
|
|
196
203
|
agent: dev
|
|
197
204
|
produces: implementation-summary.md
|
|
205
|
+
terminal_timeout: 900
|
|
206
|
+
stop_when: { green_command: ".venv-sandbox/bin/python -m pytest tests/ -q", post_green_turns: 2 }
|
|
198
207
|
on_pass: test
|
|
199
208
|
|
|
200
209
|
- name: test
|
|
@@ -227,6 +236,9 @@ steps:
|
|
|
227
236
|
| --- | --- |
|
|
228
237
|
| `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)). |
|
|
229
238
|
| `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
|
|
239
|
+
| `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. |
|
|
240
|
+
| `stop_when:` | Optional agent-step termination: `{ green_command: <argv regex>, post_green_turns: <int, default 2> }`. When the agent's most recent terminal observation matching `green_command` exits 0 and no workspace writes have happened since, the engine injects one nudge turn, allows `post_green_turns` further turns, then closes the conversation. Absent = run to finish. |
|
|
241
|
+
| `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. |
|
|
230
242
|
| `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
|
|
231
243
|
| `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). |
|
|
232
244
|
| `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). |
|
|
@@ -239,6 +251,11 @@ steps:
|
|
|
239
251
|
| `caches:` / `sync_excludes:` | Top-level run-wide lists. `caches:` names repo-relative toolchain cache dirs bind-mounted rw from the real workspace (auto-created if missing); a `setup:` hook can warm them (the provision container carries the same cache mounts); `sync_excludes:` names scratch paths skipped from both the export copy and sync-back (wildcard `*` spans separators, so `**/bin/**` matches a `/bin/` segment at any depth). Both default to `[]`. See [docs/setup.md](docs/setup.md). |
|
|
240
252
|
| `merge_on_conflict:` | Top-level run-wide list of append-style logs (same path vocabulary as `sync_excludes:`, default `[]`). A concurrent host edit to a listed path is union-merged instead of raising: the host file wins and the sandbox's export-only lines are appended in order; both sides converge and the run continues. See [docs/setup.md](docs/setup.md). |
|
|
241
253
|
|
|
254
|
+
A `brief_inputs:` step is the per-run context-digestion pattern: it runs once at
|
|
255
|
+
run start (or when its inputs change), its `produces:` artifact survives attempt
|
|
256
|
+
starts, and the dev/review agents read that digest instead of re-deriving repo
|
|
257
|
+
rules and environment facts every attempt.
|
|
258
|
+
|
|
242
259
|
### Secrets (`secrets:`) - refs only, never values
|
|
243
260
|
|
|
244
261
|
Secret values never belong in the pipeline yaml, the workspace, or `state_dir`.
|
|
@@ -150,9 +150,18 @@ max_attempts: 10 # CLI --max-attempts ove
|
|
|
150
150
|
agents_dir: sdk_agents # your <name>.agent.md files
|
|
151
151
|
|
|
152
152
|
steps:
|
|
153
|
+
- name: brief
|
|
154
|
+
agent: brief
|
|
155
|
+
produces: attempt-brief.md
|
|
156
|
+
max_iterations: 12
|
|
157
|
+
brief_inputs: { include: [AGENTS.md, coding-standards-ddd.yaml], env_facts: ["the workspace is an export with no .git; verify with find/ls, never git status"] }
|
|
158
|
+
on_pass: dev
|
|
159
|
+
|
|
153
160
|
- name: dev
|
|
154
161
|
agent: dev
|
|
155
162
|
produces: implementation-summary.md
|
|
163
|
+
terminal_timeout: 900
|
|
164
|
+
stop_when: { green_command: ".venv-sandbox/bin/python -m pytest tests/ -q", post_green_turns: 2 }
|
|
156
165
|
on_pass: test
|
|
157
166
|
|
|
158
167
|
- name: test
|
|
@@ -185,6 +194,9 @@ steps:
|
|
|
185
194
|
| --- | --- |
|
|
186
195
|
| `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)). |
|
|
187
196
|
| `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
|
|
197
|
+
| `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. |
|
|
198
|
+
| `stop_when:` | Optional agent-step termination: `{ green_command: <argv regex>, post_green_turns: <int, default 2> }`. When the agent's most recent terminal observation matching `green_command` exits 0 and no workspace writes have happened since, the engine injects one nudge turn, allows `post_green_turns` further turns, then closes the conversation. Absent = run to finish. |
|
|
199
|
+
| `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. |
|
|
188
200
|
| `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
|
|
189
201
|
| `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). |
|
|
190
202
|
| `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). |
|
|
@@ -197,6 +209,11 @@ steps:
|
|
|
197
209
|
| `caches:` / `sync_excludes:` | Top-level run-wide lists. `caches:` names repo-relative toolchain cache dirs bind-mounted rw from the real workspace (auto-created if missing); a `setup:` hook can warm them (the provision container carries the same cache mounts); `sync_excludes:` names scratch paths skipped from both the export copy and sync-back (wildcard `*` spans separators, so `**/bin/**` matches a `/bin/` segment at any depth). Both default to `[]`. See [docs/setup.md](docs/setup.md). |
|
|
198
210
|
| `merge_on_conflict:` | Top-level run-wide list of append-style logs (same path vocabulary as `sync_excludes:`, default `[]`). A concurrent host edit to a listed path is union-merged instead of raising: the host file wins and the sandbox's export-only lines are appended in order; both sides converge and the run continues. See [docs/setup.md](docs/setup.md). |
|
|
199
211
|
|
|
212
|
+
A `brief_inputs:` step is the per-run context-digestion pattern: it runs once at
|
|
213
|
+
run start (or when its inputs change), its `produces:` artifact survives attempt
|
|
214
|
+
starts, and the dev/review agents read that digest instead of re-deriving repo
|
|
215
|
+
rules and environment facts every attempt.
|
|
216
|
+
|
|
200
217
|
### Secrets (`secrets:`) - refs only, never values
|
|
201
218
|
|
|
202
219
|
Secret values never belong in the pipeline yaml, the workspace, or `state_dir`.
|
|
@@ -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.0a30"
|
|
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"
|
{agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/core/__init__.py
RENAMED
|
@@ -6,7 +6,8 @@ test fails if this package imports engine/viz or heavy deps.
|
|
|
6
6
|
|
|
7
7
|
from .config import (PipelineConfig, StepConfig, LlmConfig, IsolationConfig,
|
|
8
8
|
VerdictSpec, Route, route_target, load_config,
|
|
9
|
-
default_state_dir, TERMINALS, MountSpec, EgressSpec
|
|
9
|
+
default_state_dir, TERMINALS, MountSpec, EgressSpec,
|
|
10
|
+
StopWhenSpec)
|
|
10
11
|
from .results import Result
|
|
11
12
|
from .verdict import Verdict, VerdictError, parse_verdict, RETRY
|
|
12
13
|
|
|
@@ -14,5 +15,6 @@ __all__ = [
|
|
|
14
15
|
"PipelineConfig", "StepConfig", "LlmConfig", "IsolationConfig",
|
|
15
16
|
"VerdictSpec", "Route", "route_target", "load_config",
|
|
16
17
|
"default_state_dir", "TERMINALS", "MountSpec", "EgressSpec",
|
|
18
|
+
"StopWhenSpec",
|
|
17
19
|
"Result", "Verdict", "VerdictError", "parse_verdict", "RETRY",
|
|
18
20
|
]
|
|
@@ -305,6 +305,69 @@ class EgressSpec(BaseModel):
|
|
|
305
305
|
return [_validate_egress_target(t) for t in v]
|
|
306
306
|
|
|
307
307
|
|
|
308
|
+
class StopWhenSpec(BaseModel):
|
|
309
|
+
"""Optional ``stop_when:`` termination signal on an agent step (032).
|
|
310
|
+
|
|
311
|
+
``green_command`` is a regex matched against the agent's terminal
|
|
312
|
+
observation command; a matching observation that exits 0 - and no
|
|
313
|
+
workspace writes since - triggers one nudge turn, then
|
|
314
|
+
``post_green_turns`` further turns before the engine closes the
|
|
315
|
+
conversation. Absent ``stop_when`` keeps the current run-to-finish
|
|
316
|
+
behaviour unchanged.
|
|
317
|
+
"""
|
|
318
|
+
|
|
319
|
+
model_config = ConfigDict(extra="forbid")
|
|
320
|
+
|
|
321
|
+
green_command: str
|
|
322
|
+
post_green_turns: int = Field(default=2, ge=0)
|
|
323
|
+
|
|
324
|
+
@field_validator("green_command")
|
|
325
|
+
@classmethod
|
|
326
|
+
def _valid_regex(cls, v: str) -> str:
|
|
327
|
+
if not isinstance(v, str) or not v.strip():
|
|
328
|
+
raise ValueError(
|
|
329
|
+
"stop_when.green_command must be a non-empty string")
|
|
330
|
+
try:
|
|
331
|
+
re.compile(v)
|
|
332
|
+
except re.error as e:
|
|
333
|
+
raise ValueError(
|
|
334
|
+
f"stop_when.green_command is not a valid regex: {e}") from e
|
|
335
|
+
return v
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class BriefInputsSpec(BaseModel):
|
|
339
|
+
"""Optional ``brief_inputs:`` on an agent step (033): the closed input
|
|
340
|
+
list a brief agent digests once per run. ``include`` is repo-relative
|
|
341
|
+
paths/globs (wildcards allowed); ``env_facts`` are the stable environment
|
|
342
|
+
facts handed to the agent verbatim so it never discovers them by running
|
|
343
|
+
commands. Both accept a single string or a list; unknown keys fail at
|
|
344
|
+
load."""
|
|
345
|
+
|
|
346
|
+
model_config = ConfigDict(extra="forbid")
|
|
347
|
+
|
|
348
|
+
include: list[str] = []
|
|
349
|
+
env_facts: list[str] = []
|
|
350
|
+
|
|
351
|
+
@field_validator("include", "env_facts", mode="before")
|
|
352
|
+
@classmethod
|
|
353
|
+
def _coerce_list(cls, v: Any) -> Any:
|
|
354
|
+
return [v] if isinstance(v, str) else v
|
|
355
|
+
|
|
356
|
+
@field_validator("include")
|
|
357
|
+
@classmethod
|
|
358
|
+
def _valid_include(cls, v: list[str]) -> list[str]:
|
|
359
|
+
return [_validate_declared_path(p, allow_wildcards=True) for p in v]
|
|
360
|
+
|
|
361
|
+
@field_validator("env_facts")
|
|
362
|
+
@classmethod
|
|
363
|
+
def _valid_env_facts(cls, v: list[str]) -> list[str]:
|
|
364
|
+
for fact in v:
|
|
365
|
+
if not isinstance(fact, str) or not fact.strip():
|
|
366
|
+
raise ValueError(
|
|
367
|
+
"brief_inputs.env_facts entries must be non-empty strings")
|
|
368
|
+
return v
|
|
369
|
+
|
|
370
|
+
|
|
308
371
|
class StepConfig(BaseModel):
|
|
309
372
|
"""One pipeline step. Its type IS the key it carries: exactly one of
|
|
310
373
|
`agent`, `action`, or `command` - empty values do not count, anything
|
|
@@ -322,6 +385,9 @@ class StepConfig(BaseModel):
|
|
|
322
385
|
verdicts: VerdictSpec | None = None
|
|
323
386
|
max_iterations: int | None = None
|
|
324
387
|
timeout: float | None = Field(default=None, gt=0) # seconds, command steps only
|
|
388
|
+
terminal_timeout: float | None = Field(default=None, gt=0) # seconds, agent steps only (032)
|
|
389
|
+
stop_when: StopWhenSpec | None = None # agent steps only (032)
|
|
390
|
+
brief_inputs: BriefInputsSpec | None = None # agent steps only (033)
|
|
325
391
|
env: dict[str, str | None] | None = None # NFR-1: null = inherit host value
|
|
326
392
|
setup: str | None = None # BR-13: command steps only
|
|
327
393
|
network: bool = False # per-stage egress (command/agent + sandbox actions; host-side actions reject at registry time)
|
|
@@ -386,6 +452,28 @@ class StepConfig(BaseModel):
|
|
|
386
452
|
raise ValueError(
|
|
387
453
|
f"step {self.name!r}: 'timeout' requires 'command'"
|
|
388
454
|
)
|
|
455
|
+
if self.terminal_timeout is not None and self.agent is None:
|
|
456
|
+
raise ValueError(
|
|
457
|
+
f"step {self.name!r}: 'terminal_timeout' requires 'agent'"
|
|
458
|
+
)
|
|
459
|
+
if self.stop_when is not None and self.agent is None:
|
|
460
|
+
raise ValueError(
|
|
461
|
+
f"step {self.name!r}: 'stop_when' requires 'agent'"
|
|
462
|
+
)
|
|
463
|
+
if self.brief_inputs is not None and self.agent is None:
|
|
464
|
+
raise ValueError(
|
|
465
|
+
f"step {self.name!r}: 'brief_inputs' requires 'agent'"
|
|
466
|
+
)
|
|
467
|
+
if (self.brief_inputs is not None
|
|
468
|
+
and (self.artifact is not None or self.verdicts is not None)):
|
|
469
|
+
raise ValueError(
|
|
470
|
+
f"step {self.name!r}: 'brief_inputs' is only valid on a "
|
|
471
|
+
"produces-only agent step"
|
|
472
|
+
)
|
|
473
|
+
if self.brief_inputs is not None and not self.produces:
|
|
474
|
+
raise ValueError(
|
|
475
|
+
f"step {self.name!r}: 'brief_inputs' requires 'produces'"
|
|
476
|
+
)
|
|
389
477
|
if self.setup is not None and self.command is None and self.agent is None:
|
|
390
478
|
raise ValueError(
|
|
391
479
|
f"step {self.name!r}: 'setup' is only allowed on 'command' or 'agent' steps"
|
{agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/engine/agents.py
RENAMED
|
@@ -66,7 +66,9 @@ class SandboxTerminalExecutor(ToolExecutor):
|
|
|
66
66
|
def __init__(self, sandbox: "Sandbox", *, workspace: str = "",
|
|
67
67
|
env: dict[str, str] | None = None,
|
|
68
68
|
pipeline: str | None = None, step: str | None = None,
|
|
69
|
-
network: bool = False, mount_set=None, egress_policy=None
|
|
69
|
+
network: bool = False, mount_set=None, egress_policy=None,
|
|
70
|
+
terminal_timeout: float | None = None,
|
|
71
|
+
on_terminal_result=None):
|
|
70
72
|
self._sandbox = sandbox
|
|
71
73
|
self._workspace = workspace
|
|
72
74
|
self._env = env or {}
|
|
@@ -75,6 +77,8 @@ class SandboxTerminalExecutor(ToolExecutor):
|
|
|
75
77
|
self._network = network
|
|
76
78
|
self._mount_set = mount_set
|
|
77
79
|
self._egress_policy = egress_policy
|
|
80
|
+
self._terminal_timeout = terminal_timeout
|
|
81
|
+
self._on_terminal_result = on_terminal_result
|
|
78
82
|
|
|
79
83
|
def __call__(self, action, conversation=None):
|
|
80
84
|
from openhands.tools.terminal import TerminalObservation
|
|
@@ -92,9 +96,12 @@ class SandboxTerminalExecutor(ToolExecutor):
|
|
|
92
96
|
command=action.command, exit_code=None, timeout=False,
|
|
93
97
|
metadata=CmdOutputMetadata(exit_code=-1))
|
|
94
98
|
|
|
99
|
+
timeout = action.timeout
|
|
100
|
+
if timeout is None and self._terminal_timeout is not None:
|
|
101
|
+
timeout = self._terminal_timeout
|
|
95
102
|
request = SandboxExecutionRequest(
|
|
96
103
|
argv=command_argv(action.command), cwd=self._workspace,
|
|
97
|
-
env=self._env, timeout=
|
|
104
|
+
env=self._env, timeout=timeout,
|
|
98
105
|
pipeline=self._pipeline, step=self._step, network=self._network,
|
|
99
106
|
mount_set=self._mount_set, egress_policy=self._egress_policy)
|
|
100
107
|
try:
|
|
@@ -112,6 +119,9 @@ class SandboxTerminalExecutor(ToolExecutor):
|
|
|
112
119
|
ConversationExecutionStatus.STUCK
|
|
113
120
|
)
|
|
114
121
|
raise
|
|
122
|
+
if self._on_terminal_result is not None:
|
|
123
|
+
self._on_terminal_result(
|
|
124
|
+
action.command, result.exit_code, result.wrote_workspace)
|
|
115
125
|
output = result.stdout + result.stderr
|
|
116
126
|
return TerminalObservation.from_text(
|
|
117
127
|
output, is_error=not result.succeeded and not result.is_timeout,
|
|
@@ -130,6 +140,8 @@ def register_sandbox_terminal(sandbox: "Sandbox", *, env: dict[str, str | None]
|
|
|
130
140
|
step: str | None = None,
|
|
131
141
|
network: bool = False,
|
|
132
142
|
mount_set=None, egress_policy=None,
|
|
143
|
+
terminal_timeout: float | None = None,
|
|
144
|
+
on_terminal_result=None,
|
|
133
145
|
secrets: dict[str, str] | None = None) -> None:
|
|
134
146
|
"""Per-run re-registration of the `terminal` name (KD-4): a callable
|
|
135
147
|
factory that builds the SDK terminal with a `SandboxTerminalExecutor`.
|
|
@@ -147,6 +159,8 @@ def register_sandbox_terminal(sandbox: "Sandbox", *, env: dict[str, str | None]
|
|
|
147
159
|
executor=SandboxTerminalExecutor(
|
|
148
160
|
sandbox, workspace=workspace, env=resolved_env,
|
|
149
161
|
pipeline=pipeline, step=step, network=network,
|
|
150
|
-
mount_set=mount_set, egress_policy=egress_policy
|
|
162
|
+
mount_set=mount_set, egress_policy=egress_policy,
|
|
163
|
+
terminal_timeout=terminal_timeout,
|
|
164
|
+
on_terminal_result=on_terminal_result))
|
|
151
165
|
|
|
152
166
|
register_tool("terminal", factory)
|
{agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/engine/dryrun.py
RENAMED
|
@@ -17,6 +17,7 @@ from .hostboundary import check_orchestrator_install, check_tooling_integrity
|
|
|
17
17
|
from .loaders import resolve_docs
|
|
18
18
|
from .isolation import (isolation_state_dir, resolve_effective_profile,
|
|
19
19
|
resolve_image_ref, resolved_seccomp_profile)
|
|
20
|
+
from .stages import validate_brief_inputs
|
|
20
21
|
from .registry import (Registry, default_registry, load_extensions,
|
|
21
22
|
validate_action_mounts_egress, validate_action_network)
|
|
22
23
|
from .secrets import scan_env_files
|
|
@@ -137,8 +138,10 @@ def dry_run(cfg_path, args, registry: Registry | None = None) -> int:
|
|
|
137
138
|
_validate_agents(cfg, base / cfg.agents_dir)
|
|
138
139
|
_validate_actions(cfg, registry)
|
|
139
140
|
resolved_seccomp_profile(cfg) # fails loud on an unknown bundled:<name>
|
|
141
|
+
for step in cfg.steps:
|
|
142
|
+
validate_brief_inputs(Path.cwd(), step)
|
|
140
143
|
|
|
141
|
-
loader, docs, subject = resolve_docs(cfg, base, registry, args)
|
|
144
|
+
loader, docs, subject, _doc_sources = resolve_docs(cfg, base, registry, args)
|
|
142
145
|
image, memory, cpus = _resolved_image(cfg)
|
|
143
146
|
state_dir = isolation_state_dir(cfg)
|
|
144
147
|
|
|
@@ -21,8 +21,15 @@ from pathlib import Path
|
|
|
21
21
|
_DOC_SUFFIXES = (".md", ".yaml", ".yml", ".txt")
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
def
|
|
24
|
+
def resolve_unit_docs_with_sources(
|
|
25
|
+
docs_dir: Path | str, unit_slug: str
|
|
26
|
+
) -> tuple[dict[str, str], dict[str, Path]]:
|
|
27
|
+
"""Resolve one AI-DLC unit into (docs, sources). ``docs`` maps each
|
|
28
|
+
doc's workspace-relative key to its content; ``sources`` maps the same
|
|
29
|
+
key to its on-disk file so the brief regeneration gate can re-read it
|
|
30
|
+
(033 D4)."""
|
|
25
31
|
docs: dict[str, str] = {}
|
|
32
|
+
sources: dict[str, Path] = {}
|
|
26
33
|
docs_dir = Path(docs_dir)
|
|
27
34
|
unit_slug = unit_slug.strip()
|
|
28
35
|
|
|
@@ -47,6 +54,7 @@ def resolve_unit_docs(docs_dir: Path | str, unit_slug: str) -> dict[str, str]:
|
|
|
47
54
|
if file.is_file() and file.suffix in _DOC_SUFFIXES:
|
|
48
55
|
key = file.relative_to(unit_dir).as_posix()
|
|
49
56
|
docs[key] = file.read_text(encoding="utf-8")
|
|
57
|
+
sources[key] = file
|
|
50
58
|
|
|
51
59
|
# At this point docs holds only unit-dir files - an empty load means the
|
|
52
60
|
# pipeline would build context from inception docs alone (the 2026-07-19
|
|
@@ -63,6 +71,7 @@ def resolve_unit_docs(docs_dir: Path | str, unit_slug: str) -> dict[str, str]:
|
|
|
63
71
|
for file in sorted(inception.iterdir()):
|
|
64
72
|
if file.is_file() and file.suffix in (".md", ".txt"):
|
|
65
73
|
docs[f"inception/{file.name}"] = file.read_text(encoding="utf-8")
|
|
74
|
+
sources[f"inception/{file.name}"] = file
|
|
66
75
|
|
|
67
76
|
# The unit's code-generation plans are the build spec - absent is fine,
|
|
68
77
|
# present is how the spec reaches agents.
|
|
@@ -70,7 +79,13 @@ def resolve_unit_docs(docs_dir: Path | str, unit_slug: str) -> dict[str, str]:
|
|
|
70
79
|
if plans_dir.is_dir():
|
|
71
80
|
for file in sorted(plans_dir.glob(f"{unit_slug}-*-code-generation-plan.md")):
|
|
72
81
|
docs[f"plans/{file.name}"] = file.read_text(encoding="utf-8")
|
|
82
|
+
sources[f"plans/{file.name}"] = file
|
|
83
|
+
|
|
84
|
+
return docs, sources
|
|
85
|
+
|
|
73
86
|
|
|
87
|
+
def resolve_unit_docs(docs_dir: Path | str, unit_slug: str) -> dict[str, str]:
|
|
88
|
+
docs, _ = resolve_unit_docs_with_sources(docs_dir, unit_slug)
|
|
74
89
|
return docs
|
|
75
90
|
|
|
76
91
|
|
|
@@ -116,6 +131,13 @@ class AidlcDocumentLoader:
|
|
|
116
131
|
unit = args.aidlc_unit or get_current_unit(self.docs_dir)
|
|
117
132
|
return resolve_unit_docs(self.docs_dir, unit), f"unit {unit}"
|
|
118
133
|
|
|
134
|
+
def sources(self, args: argparse.Namespace) -> dict[str, Path]:
|
|
135
|
+
"""Source path for every resolved doc key (033): the regeneration
|
|
136
|
+
gate re-reads these from disk so a mid-run unit-plan edit is detected."""
|
|
137
|
+
unit = args.aidlc_unit or get_current_unit(self.docs_dir)
|
|
138
|
+
_, sources = resolve_unit_docs_with_sources(self.docs_dir, unit)
|
|
139
|
+
return sources
|
|
140
|
+
|
|
119
141
|
|
|
120
142
|
class AidlcExtension:
|
|
121
143
|
name = "ai-dlc"
|
{agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/engine/graph.py
RENAMED
|
@@ -28,8 +28,9 @@ from .secrets import resolve_secrets, scan_env_files
|
|
|
28
28
|
from .registry import (Registry, default_registry, load_extensions,
|
|
29
29
|
validate_action_mounts_egress, validate_action_network)
|
|
30
30
|
from .stages import (Runtime, clear_attempt_artifacts,
|
|
31
|
-
make_action_node, make_agent_node,
|
|
32
|
-
make_command_node, make_router,
|
|
31
|
+
make_action_node, make_agent_node, make_brief_node,
|
|
32
|
+
make_bump_node, make_command_node, make_router,
|
|
33
|
+
success_node)
|
|
33
34
|
|
|
34
35
|
CHECKPOINT_DB = "loop-checkpoints.sqlite" # under state_dir, one DB per workspace
|
|
35
36
|
|
|
@@ -47,6 +48,7 @@ class LoopState(TypedDict):
|
|
|
47
48
|
every other key is last-write-wins."""
|
|
48
49
|
subject: str
|
|
49
50
|
docs: dict[str, str]
|
|
51
|
+
doc_sources: dict[str, str] # doc key -> source path (033 regeneration gate)
|
|
50
52
|
artifacts: Annotated[dict[str, str], _merge_artifacts] # step name -> artifact text
|
|
51
53
|
attempt: int
|
|
52
54
|
max_attempts: int
|
|
@@ -61,6 +63,7 @@ class LoopState(TypedDict):
|
|
|
61
63
|
step_verdicts: dict[str, str | None] # per-step pass/fail/None(unclear)
|
|
62
64
|
retry_target: str | None # where the bump node loops back to
|
|
63
65
|
thread_id: str
|
|
66
|
+
brief_hashes: Annotated[dict[str, str], _merge_artifacts] # step name -> input hash (033)
|
|
64
67
|
|
|
65
68
|
|
|
66
69
|
def build_graph(runtime: Runtime, checkpointer=None):
|
|
@@ -83,7 +86,10 @@ def build_graph(runtime: Runtime, checkpointer=None):
|
|
|
83
86
|
graph = StateGraph(LoopState)
|
|
84
87
|
for step in runtime.cfg.steps:
|
|
85
88
|
if step.kind == "agent":
|
|
86
|
-
|
|
89
|
+
if step.brief_inputs is not None:
|
|
90
|
+
node = make_brief_node(runtime, step)
|
|
91
|
+
else:
|
|
92
|
+
node = make_agent_node(runtime, step)
|
|
87
93
|
elif step.kind == "command":
|
|
88
94
|
node = make_command_node(runtime, step)
|
|
89
95
|
else:
|
|
@@ -178,14 +184,15 @@ def _run_pipeline(cfg, base, registry, state_dir, max_attempts, args, viz_bus,
|
|
|
178
184
|
if resume:
|
|
179
185
|
initial = None # state (docs/subject/artifacts) comes from the checkpoint
|
|
180
186
|
else:
|
|
181
|
-
_loader, docs, subject = resolve_docs(cfg, base, registry, args)
|
|
187
|
+
_loader, docs, subject, doc_sources = resolve_docs(cfg, base, registry, args)
|
|
182
188
|
# A fresh run owns state_dir: drop cross-run leftovers (a previous
|
|
183
189
|
# failure's last-error.md, stale artifacts) so nothing mistakes
|
|
184
190
|
# them for this run's output. --resume keeps them.
|
|
185
|
-
clear_attempt_artifacts(runtime)
|
|
191
|
+
clear_attempt_artifacts(runtime, exempt_run_scoped=False)
|
|
186
192
|
initial: LoopState = {
|
|
187
193
|
"subject": subject,
|
|
188
194
|
"docs": docs,
|
|
195
|
+
"doc_sources": doc_sources,
|
|
189
196
|
"artifacts": {},
|
|
190
197
|
"attempt": 1,
|
|
191
198
|
"max_attempts": max_attempts,
|
|
@@ -204,6 +211,7 @@ def _run_pipeline(cfg, base, registry, state_dir, max_attempts, args, viz_bus,
|
|
|
204
211
|
"step_verdicts": {},
|
|
205
212
|
"retry_target": None,
|
|
206
213
|
"thread_id": thread_id,
|
|
214
|
+
"brief_hashes": {},
|
|
207
215
|
}
|
|
208
216
|
bus = viz_bus
|
|
209
217
|
with SqliteSaver.from_conn_string(db) as saver:
|
{agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/engine/isolation.py
RENAMED
|
@@ -144,6 +144,7 @@ class SandboxExecutionResult:
|
|
|
144
144
|
duration_seconds: float
|
|
145
145
|
backend: str # "docker"
|
|
146
146
|
timed_out: bool = False
|
|
147
|
+
wrote_workspace: bool = False # declared-rw paths changed (032)
|
|
147
148
|
|
|
148
149
|
@property
|
|
149
150
|
def succeeded(self) -> bool:
|
|
@@ -1068,8 +1069,15 @@ class DockerBackend(Sandbox):
|
|
|
1068
1069
|
"failing closed (BR-11.3): no networked execution runs "
|
|
1069
1070
|
"without egress enforcement")
|
|
1070
1071
|
result = self._exec(request, prof.name)
|
|
1072
|
+
report: SyncBackReport | None = None
|
|
1071
1073
|
if mount_set.rw_paths() and self._export_root is not None:
|
|
1072
|
-
self._sync_back(mount_set)
|
|
1074
|
+
report = self._sync_back(mount_set)
|
|
1075
|
+
if report is not None and (report.synced_paths or report.merged_paths):
|
|
1076
|
+
result = SandboxExecutionResult(
|
|
1077
|
+
exit_code=result.exit_code, stdout=result.stdout,
|
|
1078
|
+
stderr=result.stderr, duration_seconds=result.duration_seconds,
|
|
1079
|
+
backend=result.backend, timed_out=result.timed_out,
|
|
1080
|
+
wrote_workspace=True)
|
|
1073
1081
|
return result
|
|
1074
1082
|
|
|
1075
1083
|
def _exec(self, request: SandboxExecutionRequest,
|
|
@@ -1161,7 +1169,7 @@ class DockerBackend(Sandbox):
|
|
|
1161
1169
|
export_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1162
1170
|
shutil.copy2(host_path, export_path, follow_symlinks=False)
|
|
1163
1171
|
|
|
1164
|
-
def _sync_back(self, mount_set: MountSet) -> None:
|
|
1172
|
+
def _sync_back(self, mount_set: MountSet) -> SyncBackReport | None:
|
|
1165
1173
|
"""Sync declared-rw export changes back to the real repo (BR-4/F1),
|
|
1166
1174
|
git-aware, host-side, with the sandbox step already complete.
|
|
1167
1175
|
|
|
@@ -1227,6 +1235,7 @@ class DockerBackend(Sandbox):
|
|
|
1227
1235
|
"synced_paths": list(report.synced_paths),
|
|
1228
1236
|
"merged_paths": list(report.merged_paths),
|
|
1229
1237
|
})
|
|
1238
|
+
return report
|
|
1230
1239
|
|
|
1231
1240
|
def close(self) -> None:
|
|
1232
1241
|
"""Best-effort stop + rm; idempotent (BR-8.3). With ``--rm`` the
|
{agentic_dev_loop-1.0.0a28 → agentic_dev_loop-1.0.0a30}/src/agentic_dev_loop/engine/loaders.py
RENAMED
|
@@ -71,6 +71,12 @@ class PlanDocumentLoader:
|
|
|
71
71
|
docs = resolve_plan_docs(args.plan)
|
|
72
72
|
return docs, f"plan {args.plan.name}"
|
|
73
73
|
|
|
74
|
+
def sources(self, args: argparse.Namespace) -> dict[str, Path]:
|
|
75
|
+
"""Source path for every resolved doc key (033): the regeneration
|
|
76
|
+
gate re-reads these from disk so a mid-run plan edit is detected."""
|
|
77
|
+
path = Path(args.plan)
|
|
78
|
+
return {path.name: path}
|
|
79
|
+
|
|
74
80
|
|
|
75
81
|
def select_loader(registry: Registry, args: argparse.Namespace) -> DocumentLoader | None:
|
|
76
82
|
"""The document-loader coexistence model - load many, invoke one:
|
|
@@ -103,11 +109,15 @@ def select_loader(registry: Registry, args: argparse.Namespace) -> DocumentLoade
|
|
|
103
109
|
|
|
104
110
|
|
|
105
111
|
def resolve_docs(cfg: PipelineConfig, base: Path, registry: Registry,
|
|
106
|
-
args) -> tuple[DocumentLoader | None, dict[str, str], str
|
|
112
|
+
args) -> tuple[DocumentLoader | None, dict[str, str], str,
|
|
113
|
+
dict[str, str]]:
|
|
107
114
|
"""Select and invoke the document loader, then append the pipeline's
|
|
108
115
|
additional_files (resolved relative to the pipeline file's dir;
|
|
109
|
-
listed-but-missing fails fast). Returns (loader, docs, subject
|
|
116
|
+
listed-but-missing fails fast). Returns (loader, docs, subject,
|
|
117
|
+
doc_sources) - the source path for every loader/additional_files doc key
|
|
118
|
+
so the brief regeneration gate can re-read them from disk (033 D4)."""
|
|
110
119
|
loader = select_loader(registry, args)
|
|
120
|
+
sources: dict[str, str] = {}
|
|
111
121
|
if loader is not None:
|
|
112
122
|
docs, subject = loader.resolve(args)
|
|
113
123
|
if not docs:
|
|
@@ -116,6 +126,10 @@ def resolve_docs(cfg: PipelineConfig, base: Path, registry: Registry,
|
|
|
116
126
|
raise RuntimeError(
|
|
117
127
|
f"Document loader {loader.name!r} returned no docs - "
|
|
118
128
|
"refusing to build context from nothing")
|
|
129
|
+
loader_sources = getattr(loader, "sources", None)
|
|
130
|
+
if callable(loader_sources):
|
|
131
|
+
for name, path in loader_sources(args).items():
|
|
132
|
+
sources[name] = str(Path(path).resolve())
|
|
119
133
|
else:
|
|
120
134
|
docs, subject = {}, cfg.name
|
|
121
135
|
for rel in cfg.additional_files:
|
|
@@ -123,4 +137,5 @@ def resolve_docs(cfg: PipelineConfig, base: Path, registry: Registry,
|
|
|
123
137
|
if not path.is_file():
|
|
124
138
|
raise FileNotFoundError(f"additional_files entry not found: {path}")
|
|
125
139
|
docs[path.name] = path.read_text(encoding="utf-8")
|
|
126
|
-
|
|
140
|
+
sources[path.name] = str(path.resolve())
|
|
141
|
+
return loader, docs, subject, sources
|