agentic-dev-loop 1.0.0a4__tar.gz → 1.0.0a6__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.0a4/src/agentic_dev_loop.egg-info → agentic_dev_loop-1.0.0a6}/PKG-INFO +58 -8
- agentic_dev_loop-1.0.0a4/PKG-INFO → agentic_dev_loop-1.0.0a6/README.md +49 -41
- {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/pyproject.toml +24 -11
- agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/core/__init__.py +25 -0
- {agentic_dev_loop-1.0.0a4/src/agentic_dev_loop → agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/core}/config.py +1 -1
- agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/core/project.py +64 -0
- agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/core/projection.py +206 -0
- agentic_dev_loop-1.0.0a4/src/agentic_dev_loop/outputs.py → agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/core/verdict.py +16 -49
- agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/core/workflow.js.template +236 -0
- agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/engine/__init__.py +2 -0
- {agentic_dev_loop-1.0.0a4/src/agentic_dev_loop → agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/engine}/cli.py +21 -8
- agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/engine/commit_shim.py +56 -0
- {agentic_dev_loop-1.0.0a4/src/agentic_dev_loop → agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/engine}/dryrun.py +11 -5
- {agentic_dev_loop-1.0.0a4/src/agentic_dev_loop → agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/engine}/extensions/aidlc.py +1 -1
- {agentic_dev_loop-1.0.0a4/src/agentic_dev_loop → agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/engine}/graph.py +13 -6
- {agentic_dev_loop-1.0.0a4/src/agentic_dev_loop → agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/engine}/isolation.py +1 -1
- {agentic_dev_loop-1.0.0a4/src/agentic_dev_loop → agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/engine}/llm.py +1 -1
- {agentic_dev_loop-1.0.0a4/src/agentic_dev_loop → agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/engine}/loaders.py +1 -1
- agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/engine/outputs.py +60 -0
- {agentic_dev_loop-1.0.0a4/src/agentic_dev_loop → agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/engine}/stages.py +3 -2
- agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/viz/__init__.py +14 -0
- agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/viz/__main__.py +45 -0
- agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/viz/ingest.py +25 -0
- agentic_dev_loop-1.0.0a4/src/agentic_dev_loop/viz.py → agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/viz/server.py +78 -20
- agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/workflow/__init__.py +80 -0
- agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/workflow/cli.py +40 -0
- agentic_dev_loop-1.0.0a4/README.md → agentic_dev_loop-1.0.0a6/src/agentic_dev_loop.egg-info/PKG-INFO +91 -2
- agentic_dev_loop-1.0.0a6/src/agentic_dev_loop.egg-info/SOURCES.txt +68 -0
- agentic_dev_loop-1.0.0a6/src/agentic_dev_loop.egg-info/entry_points.txt +4 -0
- {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/src/agentic_dev_loop.egg-info/requires.txt +10 -5
- {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_actions.py +8 -8
- {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_agents.py +10 -10
- {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_aidlc.py +2 -2
- {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_cli.py +28 -7
- {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_config.py +4 -4
- {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_dry_run.py +14 -3
- {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_e2e_fixtures.py +6 -6
- {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_graph.py +20 -8
- {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_isolation.py +17 -17
- {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_isolation_integration.py +2 -2
- {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_isolation_pbt.py +2 -2
- {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_llm.py +5 -5
- {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_loaders.py +2 -2
- {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_outputs.py +2 -2
- {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_outputs_pbt.py +2 -1
- {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_proc.py +2 -2
- {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_registry.py +2 -2
- {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_results.py +1 -1
- {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_sdk_compat.py +2 -2
- {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_stages.py +6 -6
- agentic_dev_loop-1.0.0a6/tests/test_unit4_golden.py +60 -0
- agentic_dev_loop-1.0.0a6/tests/test_unit4_pbt.py +283 -0
- agentic_dev_loop-1.0.0a6/tests/test_unit4_viz_ingest.py +92 -0
- agentic_dev_loop-1.0.0a6/tests/test_unit4_workflow.py +136 -0
- {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_verdict.py +1 -1
- {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_viz.py +1 -1
- agentic_dev_loop-1.0.0a4/src/agentic_dev_loop.egg-info/SOURCES.txt +0 -52
- agentic_dev_loop-1.0.0a4/src/agentic_dev_loop.egg-info/entry_points.txt +0 -2
- {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/LICENSE +0 -0
- {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/setup.cfg +0 -0
- {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/src/agentic_dev_loop/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a4/src/agentic_dev_loop → agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/core}/results.py +0 -0
- {agentic_dev_loop-1.0.0a4/src/agentic_dev_loop → agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/engine}/actions.py +0 -0
- {agentic_dev_loop-1.0.0a4/src/agentic_dev_loop → agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/engine}/agents.py +0 -0
- {agentic_dev_loop-1.0.0a4/src/agentic_dev_loop → agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/engine}/extensions/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a4/src/agentic_dev_loop → agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/engine}/proc.py +0 -0
- {agentic_dev_loop-1.0.0a4/src/agentic_dev_loop → agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/engine}/registry.py +0 -0
- {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/src/agentic_dev_loop/py.typed +0 -0
- {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
- {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
- {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_publish.py +0 -0
- {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_source_resolution.py +0 -0
{agentic_dev_loop-1.0.0a4/src/agentic_dev_loop.egg-info → agentic_dev_loop-1.0.0a6}/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.0a6
|
|
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
|
|
@@ -25,14 +25,17 @@ Classifier: Typing :: Typed
|
|
|
25
25
|
Requires-Python: >=3.12
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
27
27
|
License-File: LICENSE
|
|
28
|
-
Requires-Dist: openhands-sdk~=1.17.0
|
|
29
|
-
Requires-Dist: openhands-tools~=1.17.0
|
|
30
|
-
Requires-Dist: agent-client-protocol<1.0.0
|
|
31
|
-
Requires-Dist: langgraph>=1.2
|
|
32
|
-
Requires-Dist: langgraph-checkpoint-sqlite>=3.0
|
|
33
28
|
Requires-Dist: pydantic>=2.0
|
|
34
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
|
|
35
37
|
Provides-Extra: dev
|
|
38
|
+
Requires-Dist: agentic-dev-loop[engine]; extra == "dev"
|
|
36
39
|
Requires-Dist: hypothesis>=6.0; extra == "dev"
|
|
37
40
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
38
41
|
Dynamic: license-file
|
|
@@ -62,6 +65,11 @@ flowchart LR
|
|
|
62
65
|
- **Sandboxed by default.** The agent's shell and every command step run inside
|
|
63
66
|
an always-on Docker sandbox. There is no opt-out.
|
|
64
67
|
|
|
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
|
+
|
|
65
73
|
---
|
|
66
74
|
|
|
67
75
|
## Core features
|
|
@@ -90,8 +98,13 @@ LLM timeout (agent runs are long).
|
|
|
90
98
|
| LLM API key | The env var named by your pipeline's `api_key_env` (e.g. `OPENAI_API_KEY`) must be set — a missing key fails fast, naming it. |
|
|
91
99
|
|
|
92
100
|
```bash
|
|
93
|
-
|
|
101
|
+
# engine mode (sandboxed loop — the full install)
|
|
102
|
+
pip install "agentic-dev-loop[engine]==1.0.0a6"
|
|
94
103
|
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
|
|
95
108
|
```
|
|
96
109
|
|
|
97
110
|
On the first run the engine pulls the **digest-pinned** dev image
|
|
@@ -102,6 +115,43 @@ image. Engine setup, first-run details, the full `pipeline.yaml` contract, and
|
|
|
102
115
|
troubleshooting for every fail-fast code live in
|
|
103
116
|
[docs/setup.md](docs/setup.md).
|
|
104
117
|
|
|
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
|
+
|
|
105
155
|
## Usage
|
|
106
156
|
|
|
107
157
|
```bash
|
|
@@ -142,7 +192,7 @@ Exit code is `0` on pipeline success, `1` otherwise.
|
|
|
142
192
|
```yaml
|
|
143
193
|
name: my-construction-loop
|
|
144
194
|
extensions:
|
|
145
|
-
- agentic_dev_loop.extensions.aidlc:AidlcExtension # bundled AI-DLC doc loader
|
|
195
|
+
- agentic_dev_loop.engine.extensions.aidlc:AidlcExtension # bundled AI-DLC doc loader
|
|
146
196
|
additional_files:
|
|
147
197
|
- ../README.md # shared context for every agent step
|
|
148
198
|
|
|
@@ -1,42 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: agentic-dev-loop
|
|
3
|
-
Version: 1.0.0a4
|
|
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: openhands-sdk~=1.17.0
|
|
29
|
-
Requires-Dist: openhands-tools~=1.17.0
|
|
30
|
-
Requires-Dist: agent-client-protocol<1.0.0
|
|
31
|
-
Requires-Dist: langgraph>=1.2
|
|
32
|
-
Requires-Dist: langgraph-checkpoint-sqlite>=3.0
|
|
33
|
-
Requires-Dist: pydantic>=2.0
|
|
34
|
-
Requires-Dist: PyYAML>=6.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
|
-
|
|
40
1
|
# agentic-dev-loop
|
|
41
2
|
|
|
42
3
|
> **Turn an LLM into a gated, sandboxed, resumable construction loop over your codebase.**
|
|
@@ -62,6 +23,11 @@ flowchart LR
|
|
|
62
23
|
- **Sandboxed by default.** The agent's shell and every command step run inside
|
|
63
24
|
an always-on Docker sandbox. There is no opt-out.
|
|
64
25
|
|
|
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
|
+
|
|
65
31
|
---
|
|
66
32
|
|
|
67
33
|
## Core features
|
|
@@ -90,8 +56,13 @@ LLM timeout (agent runs are long).
|
|
|
90
56
|
| LLM API key | The env var named by your pipeline's `api_key_env` (e.g. `OPENAI_API_KEY`) must be set — a missing key fails fast, naming it. |
|
|
91
57
|
|
|
92
58
|
```bash
|
|
93
|
-
|
|
59
|
+
# engine mode (sandboxed loop — the full install)
|
|
60
|
+
pip install "agentic-dev-loop[engine]==1.0.0a6"
|
|
94
61
|
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
|
|
95
66
|
```
|
|
96
67
|
|
|
97
68
|
On the first run the engine pulls the **digest-pinned** dev image
|
|
@@ -102,6 +73,43 @@ image. Engine setup, first-run details, the full `pipeline.yaml` contract, and
|
|
|
102
73
|
troubleshooting for every fail-fast code live in
|
|
103
74
|
[docs/setup.md](docs/setup.md).
|
|
104
75
|
|
|
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
|
+
|
|
105
113
|
## Usage
|
|
106
114
|
|
|
107
115
|
```bash
|
|
@@ -142,7 +150,7 @@ Exit code is `0` on pipeline success, `1` otherwise.
|
|
|
142
150
|
```yaml
|
|
143
151
|
name: my-construction-loop
|
|
144
152
|
extensions:
|
|
145
|
-
- agentic_dev_loop.extensions.aidlc:AidlcExtension # bundled AI-DLC doc loader
|
|
153
|
+
- agentic_dev_loop.engine.extensions.aidlc:AidlcExtension # bundled AI-DLC doc loader
|
|
146
154
|
additional_files:
|
|
147
155
|
- ../README.md # shared context for every agent step
|
|
148
156
|
|
|
@@ -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.0a6"
|
|
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,15 +32,9 @@ classifiers = [
|
|
|
32
32
|
"Typing :: Typed",
|
|
33
33
|
]
|
|
34
34
|
dependencies = [
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
#
|
|
38
|
-
# the 1.0.0rc pre-releases drop acp.helpers (breaks sdk 1.17.0 imports).
|
|
39
|
-
# Without this cap, --pre installs resolve to the broken rc. See AGENTS.md
|
|
40
|
-
# error-handling rules + COMPAT-SHIMS.md for the SDK-compat pattern.
|
|
41
|
-
"agent-client-protocol<1.0.0",
|
|
42
|
-
"langgraph>=1.2",
|
|
43
|
-
"langgraph-checkpoint-sqlite>=3.0",
|
|
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.
|
|
44
38
|
"pydantic>=2.0",
|
|
45
39
|
"PyYAML>=6.0",
|
|
46
40
|
]
|
|
@@ -49,10 +43,28 @@ dependencies = [
|
|
|
49
43
|
Homepage = "https://github.com/lukedawilson/agentic-dev-loop"
|
|
50
44
|
|
|
51
45
|
[project.scripts]
|
|
52
|
-
agentic-dev-loop = "agentic_dev_loop.cli:main"
|
|
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"
|
|
53
49
|
|
|
54
50
|
[project.optional-dependencies]
|
|
51
|
+
engine = [
|
|
52
|
+
"openhands-sdk ~=1.17.0",
|
|
53
|
+
"openhands-tools ~=1.17.0",
|
|
54
|
+
# openhands-sdk declares agent-client-protocol>=0.8.1 with NO upper bound;
|
|
55
|
+
# the 1.0.0rc pre-releases drop acp.helpers (breaks sdk 1.17.0 imports).
|
|
56
|
+
# Without this cap, --pre installs resolve to the broken rc. See AGENTS.md
|
|
57
|
+
# error-handling rules + COMPAT-SHIMS.md for the SDK-compat pattern.
|
|
58
|
+
"agent-client-protocol<1.0.0",
|
|
59
|
+
"langgraph>=1.2",
|
|
60
|
+
"langgraph-checkpoint-sqlite>=3.0",
|
|
61
|
+
]
|
|
62
|
+
# Workflow mode is the BASE install — this empty extra exists only so
|
|
63
|
+
# `pip install agentic-dev-loop[workflow]` reads naturally and pulls nothing
|
|
64
|
+
# beyond the light base (core + viz). See README / NFR-4.1.
|
|
65
|
+
workflow = []
|
|
55
66
|
dev = [
|
|
67
|
+
"agentic-dev-loop[engine]",
|
|
56
68
|
"hypothesis>=6.0",
|
|
57
69
|
"pytest>=8.0",
|
|
58
70
|
]
|
|
@@ -62,6 +74,7 @@ where = ["src"]
|
|
|
62
74
|
|
|
63
75
|
[tool.setuptools.package-data]
|
|
64
76
|
agentic_dev_loop = ["py.typed"]
|
|
77
|
+
"agentic_dev_loop.core" = ["workflow.js.template"]
|
|
65
78
|
|
|
66
79
|
[tool.pytest.ini_options]
|
|
67
80
|
markers = [
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Pipeline model + parse + workflow projection (unit-4 core).
|
|
2
|
+
|
|
3
|
+
Dependency-light by contract (BR-2): pydantic + PyYAML + stdlib only. A guard
|
|
4
|
+
test fails if this package imports engine/viz or heavy deps.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .config import (PipelineConfig, StepConfig, LlmConfig, IsolationConfig,
|
|
8
|
+
VerdictSpec, Route, route_target, load_config,
|
|
9
|
+
default_state_dir, TERMINALS)
|
|
10
|
+
from .results import Result
|
|
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
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"PipelineConfig", "StepConfig", "LlmConfig", "IsolationConfig",
|
|
19
|
+
"VerdictSpec", "Route", "route_target", "load_config",
|
|
20
|
+
"default_state_dir", "TERMINALS",
|
|
21
|
+
"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
|
+
]
|
|
@@ -206,7 +206,7 @@ def default_state_dir(pipeline_path: Path) -> str:
|
|
|
206
206
|
|
|
207
207
|
def load_config(path: Path) -> PipelineConfig:
|
|
208
208
|
path = Path(path)
|
|
209
|
-
data = yaml.safe_load(path.read_text())
|
|
209
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
210
210
|
if not isinstance(data, dict):
|
|
211
211
|
raise ValueError(
|
|
212
212
|
f"pipeline config must be a mapping, got {type(data).__name__}: {path}"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Build-time projection entry point (FD-5).
|
|
2
|
+
|
|
3
|
+
`python -m agentic_dev_loop.core.project <pipeline.yaml>` loads + validates
|
|
4
|
+
the pipeline, projects it, checks prompt references (BR-4), and renders the
|
|
5
|
+
self-contained workflow JS to `.agentic-dev-loop/workflow.js` in the
|
|
6
|
+
pipeline's directory. Runtime never parses the YAML (BR-1).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from .config import load_config
|
|
17
|
+
from .projection import (WORKFLOW_TEMPLATE, core_version, pipeline_hash,
|
|
18
|
+
project_workflow, render_workflow_js)
|
|
19
|
+
|
|
20
|
+
OUTPUT_DIR = ".agentic-dev-loop"
|
|
21
|
+
OUTPUT_FILE = "workflow.js"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main(argv: list[str] | None = None) -> int:
|
|
25
|
+
parser = argparse.ArgumentParser(
|
|
26
|
+
prog="agentic-dev-loop-project",
|
|
27
|
+
description="Project pipeline.yaml into a Claude Dynamic Workflow JS.")
|
|
28
|
+
parser.add_argument("pipeline", type=Path, nargs="?",
|
|
29
|
+
default=Path("pipeline.yaml"),
|
|
30
|
+
help="path to the pipeline YAML (default: pipeline.yaml)")
|
|
31
|
+
parser.add_argument("-o", "--output", type=Path, default=None,
|
|
32
|
+
help="where to write the generated JS (default: "
|
|
33
|
+
"<pipeline dir>/.agentic-dev-loop/workflow.js)")
|
|
34
|
+
args = parser.parse_args(argv)
|
|
35
|
+
|
|
36
|
+
pipeline_path = args.pipeline
|
|
37
|
+
cfg = load_config(pipeline_path)
|
|
38
|
+
base = pipeline_path.resolve().parent
|
|
39
|
+
agents_dir = base / cfg.agents_dir
|
|
40
|
+
phash = pipeline_hash(pipeline_path)
|
|
41
|
+
|
|
42
|
+
# Bake workspace-relative prompt references (BR-5): the generated
|
|
43
|
+
# workflow re-reads each `.agent.md` from the workspace at run time, so
|
|
44
|
+
# the reference must be relative to the invocation directory (Claude
|
|
45
|
+
# Code's workspace), never an absolute host path. Fall back to the
|
|
46
|
+
# absolute path when the pipeline lives on a different volume (Windows).
|
|
47
|
+
try:
|
|
48
|
+
agents_ref = Path(os.path.relpath(agents_dir, Path.cwd().resolve()))
|
|
49
|
+
except ValueError:
|
|
50
|
+
agents_ref = agents_dir
|
|
51
|
+
|
|
52
|
+
projection = project_workflow(cfg, agents_ref, phash)
|
|
53
|
+
js = render_workflow_js(WORKFLOW_TEMPLATE, projection, core_version(), phash)
|
|
54
|
+
|
|
55
|
+
out = args.output or (base / OUTPUT_DIR / OUTPUT_FILE)
|
|
56
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
out.write_text(js, encoding="utf-8")
|
|
58
|
+
print(f"Projected {cfg.name!r} ({len(projection.steps)} steps) -> {out}",
|
|
59
|
+
flush=True)
|
|
60
|
+
return 0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
sys.exit(main())
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Workflow projection — pure functions that turn a validated PipelineConfig
|
|
2
|
+
into a Claude Dynamic Workflow (unit-4).
|
|
3
|
+
|
|
4
|
+
The projection runs at workflow-creation time; the generated JS is
|
|
5
|
+
self-contained (step data + prompt refs + routing baked in). Runtime never
|
|
6
|
+
parses pipeline.yaml (BR-1). Everything here is dependency-light: pydantic +
|
|
7
|
+
PyYAML + stdlib only (BR-2).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import importlib.metadata
|
|
14
|
+
import json
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from .config import Route, PipelineConfig, route_target
|
|
19
|
+
|
|
20
|
+
_KINDS = ("agent", "command", "action")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def core_version() -> str:
|
|
24
|
+
"""The installed distribution version — the single source of truth for
|
|
25
|
+
the generated-JS header stamp (FD-5)."""
|
|
26
|
+
try:
|
|
27
|
+
return importlib.metadata.version("agentic-dev-loop")
|
|
28
|
+
except importlib.metadata.PackageNotFoundError:
|
|
29
|
+
return "0.0.0+dev"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def pipeline_hash(pipeline_path: str | Path) -> str:
|
|
33
|
+
"""The first 12 chars of the sha256 of the absolute pipeline-file path —
|
|
34
|
+
the same digest the engine uses for its per-pipeline state dir."""
|
|
35
|
+
return hashlib.sha256(
|
|
36
|
+
str(Path(pipeline_path).resolve()).encode("utf-8")).hexdigest()[:12]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class ProjectedStep:
|
|
41
|
+
"""One step, projected for the workflow: routing + verdict config + the
|
|
42
|
+
prompt *reference* (BR-5 — never embedded)."""
|
|
43
|
+
|
|
44
|
+
id: str
|
|
45
|
+
kind: str
|
|
46
|
+
command: str | None = None
|
|
47
|
+
prompt_path: str | None = None
|
|
48
|
+
action: str | None = None # action steps (workflow mode supports commit)
|
|
49
|
+
params: dict | None = None # action params (e.g. commit message template)
|
|
50
|
+
verdicts: dict[str, str] | None = None # keyword -> canonical pass/fail
|
|
51
|
+
on_pass: str | None = None
|
|
52
|
+
on_fail: str | None = None
|
|
53
|
+
retry: bool = False # on_fail is {goto, retry: true}
|
|
54
|
+
max_attempts: int = 3
|
|
55
|
+
timeout: float | None = None
|
|
56
|
+
|
|
57
|
+
def __post_init__(self) -> None:
|
|
58
|
+
if self.kind not in _KINDS:
|
|
59
|
+
raise ValueError(f"step {self.id!r}: unknown kind {self.kind!r}")
|
|
60
|
+
if self.kind == "agent" and self.prompt_path is None:
|
|
61
|
+
raise ValueError(f"agent step {self.id!r}: missing prompt reference")
|
|
62
|
+
if self.kind == "command" and not self.command:
|
|
63
|
+
raise ValueError(f"command step {self.id!r}: missing command")
|
|
64
|
+
if self.kind == "action" and not self.action:
|
|
65
|
+
raise ValueError(f"action step {self.id!r}: missing action name")
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def from_config(step, agents_dir: Path, max_attempts: int) -> "ProjectedStep":
|
|
69
|
+
command: str | None = None
|
|
70
|
+
prompt_path: str | None = None
|
|
71
|
+
action: str | None = None
|
|
72
|
+
if step.kind == "agent":
|
|
73
|
+
# POSIX form (BR-5): the reference is a workspace-relative
|
|
74
|
+
# path, and forward slashes keep the golden render machine-
|
|
75
|
+
# independent (Windows CI bakes the same bytes as POSIX).
|
|
76
|
+
prompt_path = (agents_dir / f"{step.agent}.agent.md").as_posix()
|
|
77
|
+
elif step.kind == "command":
|
|
78
|
+
command = step.command
|
|
79
|
+
elif step.kind == "action":
|
|
80
|
+
action = step.action
|
|
81
|
+
verdicts: dict[str, str] | None = None
|
|
82
|
+
if step.verdicts is not None:
|
|
83
|
+
verdicts = {step.verdicts.pass_: "pass", step.verdicts.fail: "fail"}
|
|
84
|
+
on_fail = route_target(step.on_fail)
|
|
85
|
+
return ProjectedStep(
|
|
86
|
+
id=step.name,
|
|
87
|
+
kind=step.kind,
|
|
88
|
+
command=command,
|
|
89
|
+
prompt_path=prompt_path,
|
|
90
|
+
action=action,
|
|
91
|
+
params=dict(step.params) if step.params else None,
|
|
92
|
+
verdicts=verdicts,
|
|
93
|
+
on_pass=route_target(step.on_pass),
|
|
94
|
+
on_fail=on_fail,
|
|
95
|
+
retry=isinstance(step.on_fail, Route) and step.on_fail.retry,
|
|
96
|
+
max_attempts=max_attempts,
|
|
97
|
+
timeout=step.timeout,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def to_dict(self) -> dict:
|
|
101
|
+
return {
|
|
102
|
+
"id": self.id,
|
|
103
|
+
"kind": self.kind,
|
|
104
|
+
"command": self.command,
|
|
105
|
+
"prompt_path": self.prompt_path,
|
|
106
|
+
"action": self.action,
|
|
107
|
+
"params": self.params,
|
|
108
|
+
"verdicts": self.verdicts,
|
|
109
|
+
"on_pass": self.on_pass,
|
|
110
|
+
"on_fail": self.on_fail,
|
|
111
|
+
"retry": self.retry,
|
|
112
|
+
"max_attempts": self.max_attempts,
|
|
113
|
+
"timeout": self.timeout,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass(frozen=True)
|
|
118
|
+
class WorkflowProjection:
|
|
119
|
+
"""The frozen result of `project_workflow` — everything the generated JS
|
|
120
|
+
needs, nothing it doesn't (no `llm` block, no engine-only fields)."""
|
|
121
|
+
|
|
122
|
+
pipeline_name: str
|
|
123
|
+
pipeline_hash: str
|
|
124
|
+
core_version: str
|
|
125
|
+
steps: list[ProjectedStep] = field(default_factory=list)
|
|
126
|
+
|
|
127
|
+
def __post_init__(self) -> None:
|
|
128
|
+
if not self.steps:
|
|
129
|
+
raise ValueError("workflow projection requires at least one step")
|
|
130
|
+
|
|
131
|
+
def steps_data(self) -> list[dict]:
|
|
132
|
+
return [s.to_dict() for s in self.steps]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def project_workflow(cfg: PipelineConfig, agents_dir: str | Path,
|
|
136
|
+
pipeline_hash: str) -> WorkflowProjection:
|
|
137
|
+
"""Project a validated PipelineConfig into a WorkflowProjection.
|
|
138
|
+
|
|
139
|
+
Reuses the already-loaded/validated config (routing targets and step
|
|
140
|
+
shape are checked by `PipelineConfig`); the only build-time check added
|
|
141
|
+
here is prompt-reference existence (BR-4)."""
|
|
142
|
+
agents_dir = Path(agents_dir)
|
|
143
|
+
steps = [ProjectedStep.from_config(s, agents_dir, cfg.max_attempts)
|
|
144
|
+
for s in cfg.steps]
|
|
145
|
+
projection = WorkflowProjection(
|
|
146
|
+
pipeline_name=cfg.name,
|
|
147
|
+
pipeline_hash=pipeline_hash,
|
|
148
|
+
core_version=core_version(),
|
|
149
|
+
steps=steps,
|
|
150
|
+
)
|
|
151
|
+
validate_prompt_refs(projection, agents_dir)
|
|
152
|
+
return projection
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def validate_prompt_refs(projection: WorkflowProjection,
|
|
156
|
+
agents_dir: str | Path) -> None:
|
|
157
|
+
"""Every agent step's `.agent.md` must exist at build time (BR-4).
|
|
158
|
+
|
|
159
|
+
A missing prompt fails the build loudly, naming the file — never
|
|
160
|
+
silently generates a broken workflow."""
|
|
161
|
+
agents_dir = Path(agents_dir)
|
|
162
|
+
for step in projection.steps:
|
|
163
|
+
if step.kind == "agent" and step.prompt_path is not None:
|
|
164
|
+
prompt = Path(step.prompt_path)
|
|
165
|
+
if not prompt.is_file():
|
|
166
|
+
rel = prompt
|
|
167
|
+
try:
|
|
168
|
+
rel = prompt.relative_to(agents_dir)
|
|
169
|
+
except ValueError:
|
|
170
|
+
pass
|
|
171
|
+
raise FileNotFoundError(
|
|
172
|
+
f"workflow agent step {step.id!r}: prompt file not found: "
|
|
173
|
+
f"{rel} (under {agents_dir})")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# The wheel-shipped workflow JS template (FD-5). It ships as package data
|
|
177
|
+
# next to this module (a real file, not an inline string — TS-6/KD-3) and is
|
|
178
|
+
# filled by render_workflow_js at build time. The deterministic loop — step
|
|
179
|
+
# routing, the attempt counter, retry-note injection, clean-at-start, the viz
|
|
180
|
+
# launch, and the gated commit — lives entirely inside the template.
|
|
181
|
+
WORKFLOW_TEMPLATE = (
|
|
182
|
+
Path(__file__).with_name("workflow.js.template").read_text(encoding="utf-8")
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def render_workflow_js(template: str, projection: WorkflowProjection,
|
|
187
|
+
core_version: str, pipeline_hash: str) -> str:
|
|
188
|
+
"""Fill the wheel-shipped template from a projection (FD-5).
|
|
189
|
+
|
|
190
|
+
Deterministic given (template, projection, core_version, pipeline_hash):
|
|
191
|
+
the version + hash appear only in the first-line header, so a differing
|
|
192
|
+
stamp changes the header alone — the baked steps never vary with it."""
|
|
193
|
+
steps_json = json.dumps(projection.steps_data(), sort_keys=True, indent=2)
|
|
194
|
+
return (
|
|
195
|
+
template
|
|
196
|
+
.replace("{{CORE_VERSION}}", core_version)
|
|
197
|
+
.replace("{{PIPELINE_HASH}}", pipeline_hash)
|
|
198
|
+
.replace("{{PIPELINE_NAME}}", json.dumps(projection.pipeline_name))
|
|
199
|
+
.replace("{{STEPS_JSON}}", steps_json)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def clean_at_start_flag(git_porcelain_output: str) -> bool:
|
|
204
|
+
"""True when `git status --porcelain` is empty — the worktree was clean
|
|
205
|
+
at run start, so the auto-commit is allowed (BR-8, FD-3)."""
|
|
206
|
+
return not git_porcelain_output.strip()
|