agentic-dev-loop 1.0.0a6__tar.gz → 1.0.0a8__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.
- agentic_dev_loop-1.0.0a6/README.md → agentic_dev_loop-1.0.0a8/PKG-INFO +40 -48
- agentic_dev_loop-1.0.0a6/src/agentic_dev_loop.egg-info/PKG-INFO → agentic_dev_loop-1.0.0a8/README.md +1 -90
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/pyproject.toml +9 -22
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/core/__init__.py +1 -8
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/core/verdict.py +1 -1
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/engine/__init__.py +1 -1
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/engine/isolation.py +80 -2
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/engine/outputs.py +3 -3
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/viz/__init__.py +2 -3
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/viz/__main__.py +2 -5
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/viz/server.py +3 -36
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8/src/agentic_dev_loop.egg-info}/PKG-INFO +7 -57
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop.egg-info/SOURCES.txt +1 -11
- agentic_dev_loop-1.0.0a8/src/agentic_dev_loop.egg-info/entry_points.txt +2 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop.egg-info/requires.txt +3 -8
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_isolation.py +89 -4
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_isolation_integration.py +35 -0
- agentic_dev_loop-1.0.0a8/tests/test_module_contract.py +94 -0
- agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/core/project.py +0 -64
- agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/core/projection.py +0 -206
- agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/core/workflow.js.template +0 -236
- agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/engine/commit_shim.py +0 -56
- agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/viz/ingest.py +0 -25
- agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/workflow/__init__.py +0 -80
- agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/workflow/cli.py +0 -40
- agentic_dev_loop-1.0.0a6/src/agentic_dev_loop.egg-info/entry_points.txt +0 -4
- agentic_dev_loop-1.0.0a6/tests/test_unit4_golden.py +0 -60
- agentic_dev_loop-1.0.0a6/tests/test_unit4_pbt.py +0 -283
- agentic_dev_loop-1.0.0a6/tests/test_unit4_viz_ingest.py +0 -92
- agentic_dev_loop-1.0.0a6/tests/test_unit4_workflow.py +0 -136
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/LICENSE +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/setup.cfg +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/core/config.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/core/results.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/engine/actions.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/engine/agents.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/engine/cli.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/engine/dryrun.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/engine/graph.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/engine/llm.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/engine/loaders.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/engine/proc.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/engine/registry.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/engine/stages.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/py.typed +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_actions.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_agents.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_cli.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_config.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_dry_run.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_e2e_fixtures.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_graph.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_isolation_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_llm.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_loaders.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_outputs.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_outputs_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_proc.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_publish.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_registry.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_results.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_sdk_compat.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_source_resolution.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_stages.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_verdict.py +0 -0
- {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_viz.py +0 -0
|
@@ -1,3 +1,42 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentic-dev-loop
|
|
3
|
+
Version: 1.0.0a8
|
|
4
|
+
Summary: Turn an LLM into a gated, sandboxed, resumable construction loop over your codebase.
|
|
5
|
+
Author: Luke Wilson
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/lukedawilson/agentic-dev-loop
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Operating System :: MacOS
|
|
12
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
13
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Topic :: Software Development
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
23
|
+
Classifier: Topic :: Security
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.12
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: pydantic>=2.0
|
|
29
|
+
Requires-Dist: PyYAML>=6.0
|
|
30
|
+
Requires-Dist: openhands-sdk~=1.17.0
|
|
31
|
+
Requires-Dist: openhands-tools~=1.17.0
|
|
32
|
+
Requires-Dist: agent-client-protocol<1.0.0
|
|
33
|
+
Requires-Dist: langgraph>=1.2
|
|
34
|
+
Requires-Dist: langgraph-checkpoint-sqlite>=3.0
|
|
35
|
+
Provides-Extra: dev
|
|
36
|
+
Requires-Dist: hypothesis>=6.0; extra == "dev"
|
|
37
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
1
40
|
# agentic-dev-loop
|
|
2
41
|
|
|
3
42
|
> **Turn an LLM into a gated, sandboxed, resumable construction loop over your codebase.**
|
|
@@ -23,11 +62,6 @@ flowchart LR
|
|
|
23
62
|
- **Sandboxed by default.** The agent's shell and every command step run inside
|
|
24
63
|
an always-on Docker sandbox. There is no opt-out.
|
|
25
64
|
|
|
26
|
-
Two modes share one `pipeline.yaml`: **engine mode** (the sandboxed, resumable
|
|
27
|
-
loop above) and **workflow mode** — a Claude Dynamic Workflow exported from the
|
|
28
|
-
same pipeline that runs in Claude Code's own environment, with the live graph
|
|
29
|
-
visualiser. See [Workflow mode](#workflow-mode-claude-code).
|
|
30
|
-
|
|
31
65
|
---
|
|
32
66
|
|
|
33
67
|
## Core features
|
|
@@ -56,13 +90,8 @@ LLM timeout (agent runs are long).
|
|
|
56
90
|
| LLM API key | The env var named by your pipeline's `api_key_env` (e.g. `OPENAI_API_KEY`) must be set — a missing key fails fast, naming it. |
|
|
57
91
|
|
|
58
92
|
```bash
|
|
59
|
-
|
|
60
|
-
pip install "agentic-dev-loop[engine]==1.0.0a6"
|
|
93
|
+
pip install "agentic-dev-loop==1.0.0a8"
|
|
61
94
|
agentic-dev-loop --version
|
|
62
|
-
|
|
63
|
-
# workflow mode (Claude Code — core + viz only, no SDK/Docker deps)
|
|
64
|
-
pip install "agentic-dev-loop==1.0.0a6"
|
|
65
|
-
agentic-dev-loop-workflow install-skill
|
|
66
95
|
```
|
|
67
96
|
|
|
68
97
|
On the first run the engine pulls the **digest-pinned** dev image
|
|
@@ -73,43 +102,6 @@ image. Engine setup, first-run details, the full `pipeline.yaml` contract, and
|
|
|
73
102
|
troubleshooting for every fail-fast code live in
|
|
74
103
|
[docs/setup.md](docs/setup.md).
|
|
75
104
|
|
|
76
|
-
## Workflow mode (Claude Code)
|
|
77
|
-
|
|
78
|
-
For developers who know and love the agentic dev loop but are forced to use
|
|
79
|
-
Claude Code at work: workflow mode exports a **Claude Dynamic Workflow** from
|
|
80
|
-
the `pipeline.yaml` they already trust, plus the visualiser they trust. It adds
|
|
81
|
-
the deterministic loop through Claude's mechanism — nothing more. **No Docker
|
|
82
|
-
sandboxing, and no new security claims**: workflow steps run in Claude Code's
|
|
83
|
-
own environment under its permission model (a documented trust-model change
|
|
84
|
-
from engine mode).
|
|
85
|
-
|
|
86
|
-
Install the skill once:
|
|
87
|
-
|
|
88
|
-
```bash
|
|
89
|
-
pip install "agentic-dev-loop==1.0.0a6" # base install: core + viz, no SDK/Docker
|
|
90
|
-
agentic-dev-loop-workflow install-skill # → ~/.claude/skills/agentic-dev-loop/
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
Then invoke `/agentic-dev-loop` in Claude Code. The skill walks through:
|
|
94
|
-
|
|
95
|
-
1. **Build** — `python -m agentic_dev_loop.core.project <pipeline.yaml>` loads +
|
|
96
|
-
validates the pipeline, verifies every agent step's `.agent.md` prompt exists
|
|
97
|
-
(a missing prompt fails the build loudly, naming the file), and writes a
|
|
98
|
-
self-contained `.agentic-dev-loop/workflow.js`. Steps are frozen at creation
|
|
99
|
-
— mid-run `pipeline.yaml` edits never affect a running workflow.
|
|
100
|
-
2. **Run** — the generated JS routes `on_pass` / `on_fail` / `goto` with its own
|
|
101
|
-
attempt counter, injects the failing subagent's output as the next retry note,
|
|
102
|
-
launches the live-graph server (localhost, token-authenticated ingest), and
|
|
103
|
-
invokes the docker-free commit shim, which refuses loudly unless the worktree
|
|
104
|
-
was clean at run start.
|
|
105
|
-
|
|
106
|
-
Engine-only fields (`isolation:`, per-step `network:`, `setup:`) are ignored in
|
|
107
|
-
workflow mode with no effect on the build or run.
|
|
108
|
-
|
|
109
|
-
The base install is deliberately dependency-light (`pydantic` + `PyYAML` only);
|
|
110
|
-
the heavy OpenHands-SDK engine lives in the `[engine]` extra. The `core` and
|
|
111
|
-
`viz` packages are guarded against pulling engine deps.
|
|
112
|
-
|
|
113
105
|
## Usage
|
|
114
106
|
|
|
115
107
|
```bash
|
agentic_dev_loop-1.0.0a6/src/agentic_dev_loop.egg-info/PKG-INFO → agentic_dev_loop-1.0.0a8/README.md
RENAMED
|
@@ -1,45 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: agentic-dev-loop
|
|
3
|
-
Version: 1.0.0a6
|
|
4
|
-
Summary: Turn an LLM into a gated, sandboxed, resumable construction loop over your codebase.
|
|
5
|
-
Author: Luke Wilson
|
|
6
|
-
License-Expression: Apache-2.0
|
|
7
|
-
Project-URL: Homepage, https://github.com/lukedawilson/agentic-dev-loop
|
|
8
|
-
Classifier: Development Status :: 4 - Beta
|
|
9
|
-
Classifier: Environment :: Console
|
|
10
|
-
Classifier: Intended Audience :: Developers
|
|
11
|
-
Classifier: Operating System :: MacOS
|
|
12
|
-
Classifier: Operating System :: Microsoft :: Windows
|
|
13
|
-
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
-
Classifier: Programming Language :: Python :: 3
|
|
15
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
-
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
-
Classifier: Topic :: Software Development
|
|
21
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
-
Classifier: Topic :: Software Development :: Quality Assurance
|
|
23
|
-
Classifier: Topic :: Security
|
|
24
|
-
Classifier: Typing :: Typed
|
|
25
|
-
Requires-Python: >=3.12
|
|
26
|
-
Description-Content-Type: text/markdown
|
|
27
|
-
License-File: LICENSE
|
|
28
|
-
Requires-Dist: pydantic>=2.0
|
|
29
|
-
Requires-Dist: PyYAML>=6.0
|
|
30
|
-
Provides-Extra: engine
|
|
31
|
-
Requires-Dist: openhands-sdk~=1.17.0; extra == "engine"
|
|
32
|
-
Requires-Dist: openhands-tools~=1.17.0; extra == "engine"
|
|
33
|
-
Requires-Dist: agent-client-protocol<1.0.0; extra == "engine"
|
|
34
|
-
Requires-Dist: langgraph>=1.2; extra == "engine"
|
|
35
|
-
Requires-Dist: langgraph-checkpoint-sqlite>=3.0; extra == "engine"
|
|
36
|
-
Provides-Extra: workflow
|
|
37
|
-
Provides-Extra: dev
|
|
38
|
-
Requires-Dist: agentic-dev-loop[engine]; extra == "dev"
|
|
39
|
-
Requires-Dist: hypothesis>=6.0; extra == "dev"
|
|
40
|
-
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
41
|
-
Dynamic: license-file
|
|
42
|
-
|
|
43
1
|
# agentic-dev-loop
|
|
44
2
|
|
|
45
3
|
> **Turn an LLM into a gated, sandboxed, resumable construction loop over your codebase.**
|
|
@@ -65,11 +23,6 @@ flowchart LR
|
|
|
65
23
|
- **Sandboxed by default.** The agent's shell and every command step run inside
|
|
66
24
|
an always-on Docker sandbox. There is no opt-out.
|
|
67
25
|
|
|
68
|
-
Two modes share one `pipeline.yaml`: **engine mode** (the sandboxed, resumable
|
|
69
|
-
loop above) and **workflow mode** — a Claude Dynamic Workflow exported from the
|
|
70
|
-
same pipeline that runs in Claude Code's own environment, with the live graph
|
|
71
|
-
visualiser. See [Workflow mode](#workflow-mode-claude-code).
|
|
72
|
-
|
|
73
26
|
---
|
|
74
27
|
|
|
75
28
|
## Core features
|
|
@@ -98,13 +51,8 @@ LLM timeout (agent runs are long).
|
|
|
98
51
|
| LLM API key | The env var named by your pipeline's `api_key_env` (e.g. `OPENAI_API_KEY`) must be set — a missing key fails fast, naming it. |
|
|
99
52
|
|
|
100
53
|
```bash
|
|
101
|
-
|
|
102
|
-
pip install "agentic-dev-loop[engine]==1.0.0a6"
|
|
54
|
+
pip install "agentic-dev-loop==1.0.0a8"
|
|
103
55
|
agentic-dev-loop --version
|
|
104
|
-
|
|
105
|
-
# workflow mode (Claude Code — core + viz only, no SDK/Docker deps)
|
|
106
|
-
pip install "agentic-dev-loop==1.0.0a6"
|
|
107
|
-
agentic-dev-loop-workflow install-skill
|
|
108
56
|
```
|
|
109
57
|
|
|
110
58
|
On the first run the engine pulls the **digest-pinned** dev image
|
|
@@ -115,43 +63,6 @@ image. Engine setup, first-run details, the full `pipeline.yaml` contract, and
|
|
|
115
63
|
troubleshooting for every fail-fast code live in
|
|
116
64
|
[docs/setup.md](docs/setup.md).
|
|
117
65
|
|
|
118
|
-
## Workflow mode (Claude Code)
|
|
119
|
-
|
|
120
|
-
For developers who know and love the agentic dev loop but are forced to use
|
|
121
|
-
Claude Code at work: workflow mode exports a **Claude Dynamic Workflow** from
|
|
122
|
-
the `pipeline.yaml` they already trust, plus the visualiser they trust. It adds
|
|
123
|
-
the deterministic loop through Claude's mechanism — nothing more. **No Docker
|
|
124
|
-
sandboxing, and no new security claims**: workflow steps run in Claude Code's
|
|
125
|
-
own environment under its permission model (a documented trust-model change
|
|
126
|
-
from engine mode).
|
|
127
|
-
|
|
128
|
-
Install the skill once:
|
|
129
|
-
|
|
130
|
-
```bash
|
|
131
|
-
pip install "agentic-dev-loop==1.0.0a6" # base install: core + viz, no SDK/Docker
|
|
132
|
-
agentic-dev-loop-workflow install-skill # → ~/.claude/skills/agentic-dev-loop/
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
Then invoke `/agentic-dev-loop` in Claude Code. The skill walks through:
|
|
136
|
-
|
|
137
|
-
1. **Build** — `python -m agentic_dev_loop.core.project <pipeline.yaml>` loads +
|
|
138
|
-
validates the pipeline, verifies every agent step's `.agent.md` prompt exists
|
|
139
|
-
(a missing prompt fails the build loudly, naming the file), and writes a
|
|
140
|
-
self-contained `.agentic-dev-loop/workflow.js`. Steps are frozen at creation
|
|
141
|
-
— mid-run `pipeline.yaml` edits never affect a running workflow.
|
|
142
|
-
2. **Run** — the generated JS routes `on_pass` / `on_fail` / `goto` with its own
|
|
143
|
-
attempt counter, injects the failing subagent's output as the next retry note,
|
|
144
|
-
launches the live-graph server (localhost, token-authenticated ingest), and
|
|
145
|
-
invokes the docker-free commit shim, which refuses loudly unless the worktree
|
|
146
|
-
was clean at run start.
|
|
147
|
-
|
|
148
|
-
Engine-only fields (`isolation:`, per-step `network:`, `setup:`) are ignored in
|
|
149
|
-
workflow mode with no effect on the build or run.
|
|
150
|
-
|
|
151
|
-
The base install is deliberately dependency-light (`pydantic` + `PyYAML` only);
|
|
152
|
-
the heavy OpenHands-SDK engine lives in the `[engine]` extra. The `core` and
|
|
153
|
-
`viz` packages are guarded against pulling engine deps.
|
|
154
|
-
|
|
155
66
|
## Usage
|
|
156
67
|
|
|
157
68
|
```bash
|
|
@@ -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.0a8"
|
|
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"
|
|
@@ -32,23 +32,8 @@ classifiers = [
|
|
|
32
32
|
"Typing :: Typed",
|
|
33
33
|
]
|
|
34
34
|
dependencies = [
|
|
35
|
-
# Base install = workflow mode (core + viz + workflow CLI). Deliberately
|
|
36
|
-
# dependency-light (TS-1 / NFR-4.1 / BR-2): pydantic + PyYAML only. The
|
|
37
|
-
# heavy OpenHands-SDK engine deps live in the [engine] extra below.
|
|
38
35
|
"pydantic>=2.0",
|
|
39
36
|
"PyYAML>=6.0",
|
|
40
|
-
]
|
|
41
|
-
|
|
42
|
-
[project.urls]
|
|
43
|
-
Homepage = "https://github.com/lukedawilson/agentic-dev-loop"
|
|
44
|
-
|
|
45
|
-
[project.scripts]
|
|
46
|
-
agentic-dev-loop = "agentic_dev_loop.engine.cli:main"
|
|
47
|
-
agentic-dev-loop-workflow = "agentic_dev_loop.workflow.cli:main"
|
|
48
|
-
agentic-dev-loop-commit = "agentic_dev_loop.engine.commit_shim:main"
|
|
49
|
-
|
|
50
|
-
[project.optional-dependencies]
|
|
51
|
-
engine = [
|
|
52
37
|
"openhands-sdk ~=1.17.0",
|
|
53
38
|
"openhands-tools ~=1.17.0",
|
|
54
39
|
# openhands-sdk declares agent-client-protocol>=0.8.1 with NO upper bound;
|
|
@@ -59,12 +44,15 @@ engine = [
|
|
|
59
44
|
"langgraph>=1.2",
|
|
60
45
|
"langgraph-checkpoint-sqlite>=3.0",
|
|
61
46
|
]
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
47
|
+
|
|
48
|
+
[project.urls]
|
|
49
|
+
Homepage = "https://github.com/lukedawilson/agentic-dev-loop"
|
|
50
|
+
|
|
51
|
+
[project.scripts]
|
|
52
|
+
agentic-dev-loop = "agentic_dev_loop.engine.cli:main"
|
|
53
|
+
|
|
54
|
+
[project.optional-dependencies]
|
|
66
55
|
dev = [
|
|
67
|
-
"agentic-dev-loop[engine]",
|
|
68
56
|
"hypothesis>=6.0",
|
|
69
57
|
"pytest>=8.0",
|
|
70
58
|
]
|
|
@@ -74,7 +62,6 @@ where = ["src"]
|
|
|
74
62
|
|
|
75
63
|
[tool.setuptools.package-data]
|
|
76
64
|
agentic_dev_loop = ["py.typed"]
|
|
77
|
-
"agentic_dev_loop.core" = ["workflow.js.template"]
|
|
78
65
|
|
|
79
66
|
[tool.pytest.ini_options]
|
|
80
67
|
markers = [
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Pipeline model + parse +
|
|
1
|
+
"""Pipeline model + parse + results (core).
|
|
2
2
|
|
|
3
3
|
Dependency-light by contract (BR-2): pydantic + PyYAML + stdlib only. A guard
|
|
4
4
|
test fails if this package imports engine/viz or heavy deps.
|
|
@@ -9,17 +9,10 @@ from .config import (PipelineConfig, StepConfig, LlmConfig, IsolationConfig,
|
|
|
9
9
|
default_state_dir, TERMINALS)
|
|
10
10
|
from .results import Result
|
|
11
11
|
from .verdict import Verdict, VerdictError, parse_verdict, RETRY
|
|
12
|
-
from .projection import (ProjectedStep, WorkflowProjection, project_workflow,
|
|
13
|
-
validate_prompt_refs, render_workflow_js,
|
|
14
|
-
clean_at_start_flag, pipeline_hash, core_version,
|
|
15
|
-
WORKFLOW_TEMPLATE)
|
|
16
12
|
|
|
17
13
|
__all__ = [
|
|
18
14
|
"PipelineConfig", "StepConfig", "LlmConfig", "IsolationConfig",
|
|
19
15
|
"VerdictSpec", "Route", "route_target", "load_config",
|
|
20
16
|
"default_state_dir", "TERMINALS",
|
|
21
17
|
"Result", "Verdict", "VerdictError", "parse_verdict", "RETRY",
|
|
22
|
-
"ProjectedStep", "WorkflowProjection", "project_workflow",
|
|
23
|
-
"validate_prompt_refs", "render_workflow_js", "clean_at_start_flag",
|
|
24
|
-
"pipeline_hash", "core_version", "WORKFLOW_TEMPLATE",
|
|
25
18
|
]
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`Verdict.from_text` parses the captured response for routing; expected
|
|
4
4
|
conditions are typed `Result` errors, never exceptions. `parse_verdict`
|
|
5
|
-
collapses that `Result` to a route string for
|
|
5
|
+
collapses that `Result` to a route string for routing consumers: a clear
|
|
6
6
|
decision maps to its canonical value, anything unclear maps to ``retry`` —
|
|
7
7
|
totality for arbitrary input (no crash, no silent routing).
|
|
8
8
|
"""
|
{agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/engine/__init__.py
RENAMED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""Engine package — graph/stages/sandbox/CLI and the heavy OpenHands-SDK
|
|
2
|
-
dependencies
|
|
2
|
+
dependencies."""
|
{agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/engine/isolation.py
RENAMED
|
@@ -20,6 +20,7 @@ import json
|
|
|
20
20
|
import math
|
|
21
21
|
import os
|
|
22
22
|
import re
|
|
23
|
+
import shlex
|
|
23
24
|
import shutil
|
|
24
25
|
import subprocess
|
|
25
26
|
import time
|
|
@@ -66,6 +67,14 @@ AUDIT_MAX_BYTES = 5 * 1024 * 1024
|
|
|
66
67
|
# hung `setup:` hook can never wedge startup (BR-8 fail fast, not a hang).
|
|
67
68
|
PROVISION_TIMEOUT_SECONDS = 3600
|
|
68
69
|
|
|
70
|
+
# The sandbox venv the shipped pipeline relies on (016 sandbox-venv
|
|
71
|
+
# hardening). The engine probes this path INSIDE the exec container
|
|
72
|
+
# before trusting a warm provision stamp — a host-built (darwin) venv
|
|
73
|
+
# symlinks to a host interpreter that does not exist in the Linux
|
|
74
|
+
# container, so it must be detected and re-provisioned there.
|
|
75
|
+
SANDBOX_VENV_DIR = ".venv-sandbox"
|
|
76
|
+
SANDBOX_VENV_PYTHON = ".venv-sandbox/bin/python"
|
|
77
|
+
|
|
69
78
|
|
|
70
79
|
class SandboxError(RuntimeError):
|
|
71
80
|
"""A loud fail-fast sandbox failure; the message names the problem and
|
|
@@ -270,6 +279,58 @@ def setup_stamp_key(setup_cmd: str, lockfile_hashes: dict[str, str]) -> str:
|
|
|
270
279
|
json.dumps(canonical, sort_keys=True).encode("utf-8")).hexdigest()
|
|
271
280
|
|
|
272
281
|
|
|
282
|
+
def venv_probe_command() -> str:
|
|
283
|
+
"""In-container probe classifying the sandbox venv (016 self-heal).
|
|
284
|
+
|
|
285
|
+
Returns a shell command that prints exactly one of ``valid`` /
|
|
286
|
+
``missing`` / ``broken`` and exits 0. ``broken`` is the host-tampered
|
|
287
|
+
class: the python entry exists as a path (including a dangling symlink —
|
|
288
|
+
the host-darwin venv case) but is not executable/importable in the
|
|
289
|
+
container. ``missing`` means no such entry at all (the setup hook may
|
|
290
|
+
legitimately not create a venv), so it is treated as a stamp hit, never a
|
|
291
|
+
self-heal trigger.
|
|
292
|
+
"""
|
|
293
|
+
py = shlex.quote(SANDBOX_VENV_PYTHON)
|
|
294
|
+
return (
|
|
295
|
+
f"if [ -L {py} ] && [ ! -x {py} ]; then echo broken; "
|
|
296
|
+
f"elif [ ! -e {py} ]; then echo missing; "
|
|
297
|
+
f"elif {py} -c 'import sys' >/dev/null 2>&1; then echo valid; "
|
|
298
|
+
f"else echo broken; fi"
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def venv_probe_argv(container: str, workspace: str) -> list[str]:
|
|
303
|
+
"""``docker exec`` argv that runs the venv probe inside the (already
|
|
304
|
+
running) execution container against the bind-mounted workspace."""
|
|
305
|
+
return ["docker", "exec", "-w", workspace, container,
|
|
306
|
+
"/bin/sh", "-c", venv_probe_command()]
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def venv_probe_state(stdout: str, exit_code: int | None) -> str:
|
|
310
|
+
"""Classify a probe result. A nonzero exit (docker exec failed) is
|
|
311
|
+
``unknown`` — the caller leaves the stamp alone rather than guessing."""
|
|
312
|
+
if exit_code != 0:
|
|
313
|
+
return "unknown"
|
|
314
|
+
line = stdout.strip().splitlines()[-1] if stdout.strip() else ""
|
|
315
|
+
return line if line in ("valid", "missing", "broken") else "unknown"
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def remove_sandbox_venv(workspace: str | Path) -> None:
|
|
319
|
+
"""Remove the workspace's sandbox venv before re-provision (016 self-heal).
|
|
320
|
+
Fails loud when the venv exists but cannot be removed — a silent skip
|
|
321
|
+
would re-create the ENOENT failure the next run."""
|
|
322
|
+
venv = Path(workspace) / SANDBOX_VENV_DIR
|
|
323
|
+
try:
|
|
324
|
+
if venv.is_symlink() or venv.is_file():
|
|
325
|
+
venv.unlink()
|
|
326
|
+
elif venv.is_dir():
|
|
327
|
+
shutil.rmtree(venv)
|
|
328
|
+
except OSError as e:
|
|
329
|
+
raise SandboxError(
|
|
330
|
+
f"AE-SBX-014: cannot remove invalid sandbox venv at {venv}: {e} "
|
|
331
|
+
f"— remove it manually and re-run (docs/setup.md §5)") from e
|
|
332
|
+
|
|
333
|
+
|
|
273
334
|
def host_user_flag() -> str | None:
|
|
274
335
|
"""The ``docker --user`` value binding the sandbox to the host user.
|
|
275
336
|
|
|
@@ -507,9 +568,20 @@ class DockerBackend(Sandbox):
|
|
|
507
568
|
def provision(self, setup_cmd: str, stamp_key: str, stamp_dir: str,
|
|
508
569
|
force: bool) -> None:
|
|
509
570
|
"""Run one ``setup:`` hook in the provision container (network ON),
|
|
510
|
-
stamp-cached. Nonzero exit → AE-SBX-009, no stamp written.
|
|
571
|
+
stamp-cached. Nonzero exit → AE-SBX-009, no stamp written.
|
|
572
|
+
|
|
573
|
+
016 self-heal: a warm stamp no longer blind-skips. Before trusting it,
|
|
574
|
+
probe the sandbox venv inside the exec container — a host-built venv
|
|
575
|
+
(darwin symlinks to a host interpreter) looks valid on the host but is
|
|
576
|
+
broken in the Linux container. A ``broken`` probe removes the venv
|
|
577
|
+
(fail loud if removal is impossible) and falls through to re-provision;
|
|
578
|
+
``valid``/``missing``/``unknown`` keep the stamp skip.
|
|
579
|
+
"""
|
|
511
580
|
if not force and read_stamp(Path(stamp_dir), stamp_key):
|
|
512
|
-
|
|
581
|
+
if self._probe_venv() == "broken":
|
|
582
|
+
remove_sandbox_venv(self._workspace)
|
|
583
|
+
else:
|
|
584
|
+
return
|
|
513
585
|
payload = docker_provision_payload(
|
|
514
586
|
run_id=self._run_id, image=self._image, workspace=self._workspace,
|
|
515
587
|
setup_cmd=setup_cmd)
|
|
@@ -521,6 +593,12 @@ class DockerBackend(Sandbox):
|
|
|
521
593
|
f"— stamp not written (docs/setup.md §5)")
|
|
522
594
|
write_stamp(Path(stamp_dir), stamp_key)
|
|
523
595
|
|
|
596
|
+
def _probe_venv(self) -> str:
|
|
597
|
+
"""Run the in-container venv probe and classify it (016 self-heal)."""
|
|
598
|
+
res = self._runner.run(
|
|
599
|
+
venv_probe_argv(self._container, self._workspace), timeout=30)
|
|
600
|
+
return venv_probe_state(res.stdout, res.exit_code)
|
|
601
|
+
|
|
524
602
|
# -- execution ----------------------------------------------------------
|
|
525
603
|
|
|
526
604
|
def run(self, request: SandboxExecutionRequest) -> SandboxExecutionResult:
|
{agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/engine/outputs.py
RENAMED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
channel (unit-1).
|
|
3
3
|
|
|
4
4
|
The pipeline — not the agent — writes the step's artifact from the agent's
|
|
5
|
-
final SDK response. Verdict parsing lives in `core.verdict
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
final SDK response. Verdict parsing lives in `core.verdict`; this module only
|
|
6
|
+
owns the SDK-side capture. Expected conditions are typed `Result` errors,
|
|
7
|
+
never exceptions.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
"""The live-graph visualiser
|
|
1
|
+
"""The live-graph visualiser."""
|
|
2
2
|
|
|
3
3
|
import time # noqa: F401 — re-exported so tests can pin NodeWatch's clock
|
|
4
4
|
|
|
5
5
|
from .server import (MAX_LINE, PAGE, ConsoleCapture, HeartbeatThread,
|
|
6
6
|
NodeWatch, VizBus, _ReuseSafeServer, _Tee,
|
|
7
7
|
_handler_class, serve_viz, topology_mermaid, translate)
|
|
8
|
-
from .ingest import issue_ingest_token, make_ingest_event
|
|
9
8
|
|
|
10
9
|
__all__ = [
|
|
11
10
|
"MAX_LINE", "PAGE", "ConsoleCapture", "HeartbeatThread", "NodeWatch",
|
|
12
11
|
"VizBus", "_ReuseSafeServer", "_Tee", "_handler_class", "serve_viz",
|
|
13
|
-
"topology_mermaid", "translate",
|
|
12
|
+
"topology_mermaid", "translate",
|
|
14
13
|
]
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
"""Standalone
|
|
2
|
-
|
|
3
|
-
localhost, issues an ingest token, and prints both. Step subagents POST stage
|
|
4
|
-
events to `/ingest` with that token (BR-9)."""
|
|
1
|
+
"""Standalone viz launcher: `python -m agentic_dev_loop.viz --pipeline
|
|
2
|
+
<pipeline.yaml>` serves the live graph on localhost."""
|
|
5
3
|
|
|
6
4
|
from __future__ import annotations
|
|
7
5
|
|
|
@@ -31,7 +29,6 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
31
29
|
bound = httpd.server_address[1]
|
|
32
30
|
url = f"http://127.0.0.1:{bound}"
|
|
33
31
|
print(f"Live graph viz: {url}", flush=True)
|
|
34
|
-
print(f"ingest-token: {httpd.ingest_token}", flush=True)
|
|
35
32
|
if args.port and bound != args.port:
|
|
36
33
|
print(f"viz port {args.port} busy — serving on {url}", flush=True)
|
|
37
34
|
try:
|
|
@@ -27,8 +27,6 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
|
27
27
|
|
|
28
28
|
from agentic_dev_loop.core.config import PipelineConfig, Route, route_target
|
|
29
29
|
|
|
30
|
-
from .ingest import issue_ingest_token
|
|
31
|
-
|
|
32
30
|
STATE_WHITELIST = ("attempt", "step_verdicts", "outcome", "failed")
|
|
33
31
|
|
|
34
32
|
|
|
@@ -341,15 +339,13 @@ class HeartbeatThread(threading.Thread):
|
|
|
341
339
|
|
|
342
340
|
|
|
343
341
|
def _handler_class(bus: VizBus, topology_mermaid: str,
|
|
344
|
-
subject: str, nodes: Iterable[str]
|
|
345
|
-
ingest_token: str | None = None) -> type[BaseHTTPRequestHandler]:
|
|
342
|
+
subject: str, nodes: Iterable[str]) -> type[BaseHTTPRequestHandler]:
|
|
346
343
|
class Handler(BaseHTTPRequestHandler):
|
|
347
344
|
protocol_version = "HTTP/1.1"
|
|
348
345
|
viz_bus = bus
|
|
349
346
|
viz_topology = topology_mermaid
|
|
350
347
|
viz_subject = subject
|
|
351
348
|
viz_nodes = list(nodes)
|
|
352
|
-
viz_ingest_token = ingest_token
|
|
353
349
|
|
|
354
350
|
def log_message(self, *_args) -> None: # keep test output quiet
|
|
355
351
|
pass
|
|
@@ -373,30 +369,6 @@ def _handler_class(bus: VizBus, topology_mermaid: str,
|
|
|
373
369
|
else:
|
|
374
370
|
self.send_error(404)
|
|
375
371
|
|
|
376
|
-
def do_POST(self) -> None:
|
|
377
|
-
if self.path != "/ingest":
|
|
378
|
-
self.send_error(404)
|
|
379
|
-
return
|
|
380
|
-
supplied = self.headers.get("X-Ingest-Token", "")
|
|
381
|
-
if not self.viz_ingest_token or supplied != self.viz_ingest_token:
|
|
382
|
-
self._send(403, "application/json",
|
|
383
|
-
json.dumps({"error": "forbidden"}))
|
|
384
|
-
return
|
|
385
|
-
length = int(self.headers.get("Content-Length", "0") or "0")
|
|
386
|
-
body = self.rfile.read(length).decode("utf-8")
|
|
387
|
-
try:
|
|
388
|
-
event = json.loads(body)
|
|
389
|
-
except json.JSONDecodeError:
|
|
390
|
-
self._send(400, "application/json",
|
|
391
|
-
json.dumps({"error": "invalid json"}))
|
|
392
|
-
return
|
|
393
|
-
if not isinstance(event, dict):
|
|
394
|
-
self._send(400, "application/json",
|
|
395
|
-
json.dumps({"error": "event must be an object"}))
|
|
396
|
-
return
|
|
397
|
-
self.viz_bus.publish(event)
|
|
398
|
-
self._send(200, "application/json", json.dumps({"ok": True}))
|
|
399
|
-
|
|
400
372
|
def _send(self, status: int, content_type: str, body: str) -> None:
|
|
401
373
|
data = body.encode("utf-8")
|
|
402
374
|
self.send_response(status)
|
|
@@ -439,8 +411,7 @@ class _ReuseSafeServer(ThreadingHTTPServer):
|
|
|
439
411
|
|
|
440
412
|
|
|
441
413
|
def serve_viz(bus: VizBus, topology_mermaid: str, subject: str,
|
|
442
|
-
port: int, nodes: Iterable[str] = (),
|
|
443
|
-
ingest_token: str | None = None) -> tuple[_ReuseSafeServer, threading.Thread]:
|
|
414
|
+
port: int, nodes: Iterable[str] = ()) -> tuple[_ReuseSafeServer, threading.Thread]:
|
|
444
415
|
"""Bind a loopback HTTP server on ``port`` and serve it on a daemon
|
|
445
416
|
thread. Returns ``(httpd, thread)`` — the caller reads the real bound
|
|
446
417
|
port from ``httpd.server_address[1]``.
|
|
@@ -449,10 +420,7 @@ def serve_viz(bus: VizBus, topology_mermaid: str, subject: str,
|
|
|
449
420
|
(``errno.EADDRINUSE``) the server rebinds port 0 and lets the OS pick a
|
|
450
421
|
free port. Any other bind error (e.g. ``EACCES`` on a privileged port)
|
|
451
422
|
still fails loud, naming ``--viz-port``."""
|
|
452
|
-
|
|
453
|
-
ingest_token = issue_ingest_token()
|
|
454
|
-
handler = _handler_class(bus, topology_mermaid, subject, nodes,
|
|
455
|
-
ingest_token)
|
|
423
|
+
handler = _handler_class(bus, topology_mermaid, subject, nodes)
|
|
456
424
|
try:
|
|
457
425
|
httpd = _ReuseSafeServer(("127.0.0.1", port), handler)
|
|
458
426
|
except OSError as exc:
|
|
@@ -462,7 +430,6 @@ def serve_viz(bus: VizBus, topology_mermaid: str, subject: str,
|
|
|
462
430
|
raise OSError(
|
|
463
431
|
f"Could not bind the viz server to 127.0.0.1:{port} — the "
|
|
464
432
|
f"--viz-port may already be in use. ({exc})") from exc
|
|
465
|
-
httpd.ingest_token = ingest_token
|
|
466
433
|
thread = threading.Thread(target=httpd.serve_forever, daemon=True)
|
|
467
434
|
thread.start()
|
|
468
435
|
return httpd, thread
|