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.
Files changed (72) hide show
  1. {agentic_dev_loop-1.0.0a4/src/agentic_dev_loop.egg-info → agentic_dev_loop-1.0.0a6}/PKG-INFO +58 -8
  2. agentic_dev_loop-1.0.0a4/PKG-INFO → agentic_dev_loop-1.0.0a6/README.md +49 -41
  3. {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/pyproject.toml +24 -11
  4. agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/core/__init__.py +25 -0
  5. {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
  6. agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/core/project.py +64 -0
  7. agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/core/projection.py +206 -0
  8. 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
  9. agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/core/workflow.js.template +236 -0
  10. agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/engine/__init__.py +2 -0
  11. {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
  12. agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/engine/commit_shim.py +56 -0
  13. {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
  14. {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
  15. {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
  16. {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
  17. {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
  18. {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
  19. agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/engine/outputs.py +60 -0
  20. {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
  21. agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/viz/__init__.py +14 -0
  22. agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/viz/__main__.py +45 -0
  23. agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/viz/ingest.py +25 -0
  24. 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
  25. agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/workflow/__init__.py +80 -0
  26. agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/workflow/cli.py +40 -0
  27. agentic_dev_loop-1.0.0a4/README.md → agentic_dev_loop-1.0.0a6/src/agentic_dev_loop.egg-info/PKG-INFO +91 -2
  28. agentic_dev_loop-1.0.0a6/src/agentic_dev_loop.egg-info/SOURCES.txt +68 -0
  29. agentic_dev_loop-1.0.0a6/src/agentic_dev_loop.egg-info/entry_points.txt +4 -0
  30. {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/src/agentic_dev_loop.egg-info/requires.txt +10 -5
  31. {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_actions.py +8 -8
  32. {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_agents.py +10 -10
  33. {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_aidlc.py +2 -2
  34. {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_cli.py +28 -7
  35. {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_config.py +4 -4
  36. {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_dry_run.py +14 -3
  37. {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_e2e_fixtures.py +6 -6
  38. {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_graph.py +20 -8
  39. {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_isolation.py +17 -17
  40. {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_isolation_integration.py +2 -2
  41. {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_isolation_pbt.py +2 -2
  42. {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_llm.py +5 -5
  43. {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_loaders.py +2 -2
  44. {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_outputs.py +2 -2
  45. {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_outputs_pbt.py +2 -1
  46. {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_proc.py +2 -2
  47. {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_registry.py +2 -2
  48. {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_results.py +1 -1
  49. {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_sdk_compat.py +2 -2
  50. {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_stages.py +6 -6
  51. agentic_dev_loop-1.0.0a6/tests/test_unit4_golden.py +60 -0
  52. agentic_dev_loop-1.0.0a6/tests/test_unit4_pbt.py +283 -0
  53. agentic_dev_loop-1.0.0a6/tests/test_unit4_viz_ingest.py +92 -0
  54. agentic_dev_loop-1.0.0a6/tests/test_unit4_workflow.py +136 -0
  55. {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_verdict.py +1 -1
  56. {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_viz.py +1 -1
  57. agentic_dev_loop-1.0.0a4/src/agentic_dev_loop.egg-info/SOURCES.txt +0 -52
  58. agentic_dev_loop-1.0.0a4/src/agentic_dev_loop.egg-info/entry_points.txt +0 -2
  59. {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/LICENSE +0 -0
  60. {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/setup.cfg +0 -0
  61. {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/src/agentic_dev_loop/__init__.py +0 -0
  62. {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
  63. {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
  64. {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
  65. {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
  66. {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
  67. {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
  68. {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/src/agentic_dev_loop/py.typed +0 -0
  69. {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
  70. {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
  71. {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_publish.py +0 -0
  72. {agentic_dev_loop-1.0.0a4 → agentic_dev_loop-1.0.0a6}/tests/test_source_resolution.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-dev-loop
3
- Version: 1.0.0a4
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
- pip install agentic-dev-loop==1.0.0a3 # pin the current alpha release explicitly
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
- pip install agentic-dev-loop==1.0.0a3 # pin the current alpha release explicitly
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.0a4"
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
- "openhands-sdk ~=1.17.0",
36
- "openhands-tools ~=1.17.0",
37
- # openhands-sdk declares agent-client-protocol>=0.8.1 with NO upper bound;
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()