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.
Files changed (72) hide show
  1. agentic_dev_loop-1.0.0a6/README.md → agentic_dev_loop-1.0.0a8/PKG-INFO +40 -48
  2. agentic_dev_loop-1.0.0a6/src/agentic_dev_loop.egg-info/PKG-INFO → agentic_dev_loop-1.0.0a8/README.md +1 -90
  3. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/pyproject.toml +9 -22
  4. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/core/__init__.py +1 -8
  5. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/core/verdict.py +1 -1
  6. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/engine/__init__.py +1 -1
  7. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/engine/isolation.py +80 -2
  8. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/engine/outputs.py +3 -3
  9. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/viz/__init__.py +2 -3
  10. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/viz/__main__.py +2 -5
  11. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/viz/server.py +3 -36
  12. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8/src/agentic_dev_loop.egg-info}/PKG-INFO +7 -57
  13. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop.egg-info/SOURCES.txt +1 -11
  14. agentic_dev_loop-1.0.0a8/src/agentic_dev_loop.egg-info/entry_points.txt +2 -0
  15. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop.egg-info/requires.txt +3 -8
  16. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_isolation.py +89 -4
  17. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_isolation_integration.py +35 -0
  18. agentic_dev_loop-1.0.0a8/tests/test_module_contract.py +94 -0
  19. agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/core/project.py +0 -64
  20. agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/core/projection.py +0 -206
  21. agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/core/workflow.js.template +0 -236
  22. agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/engine/commit_shim.py +0 -56
  23. agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/viz/ingest.py +0 -25
  24. agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/workflow/__init__.py +0 -80
  25. agentic_dev_loop-1.0.0a6/src/agentic_dev_loop/workflow/cli.py +0 -40
  26. agentic_dev_loop-1.0.0a6/src/agentic_dev_loop.egg-info/entry_points.txt +0 -4
  27. agentic_dev_loop-1.0.0a6/tests/test_unit4_golden.py +0 -60
  28. agentic_dev_loop-1.0.0a6/tests/test_unit4_pbt.py +0 -283
  29. agentic_dev_loop-1.0.0a6/tests/test_unit4_viz_ingest.py +0 -92
  30. agentic_dev_loop-1.0.0a6/tests/test_unit4_workflow.py +0 -136
  31. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/LICENSE +0 -0
  32. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/setup.cfg +0 -0
  33. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/__init__.py +0 -0
  34. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/core/config.py +0 -0
  35. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/core/results.py +0 -0
  36. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/engine/actions.py +0 -0
  37. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/engine/agents.py +0 -0
  38. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/engine/cli.py +0 -0
  39. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/engine/dryrun.py +0 -0
  40. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
  41. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
  42. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/engine/graph.py +0 -0
  43. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/engine/llm.py +0 -0
  44. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/engine/loaders.py +0 -0
  45. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/engine/proc.py +0 -0
  46. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/engine/registry.py +0 -0
  47. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/engine/stages.py +0 -0
  48. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop/py.typed +0 -0
  49. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
  50. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
  51. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_actions.py +0 -0
  52. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_agents.py +0 -0
  53. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_aidlc.py +0 -0
  54. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_cli.py +0 -0
  55. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_config.py +0 -0
  56. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_dry_run.py +0 -0
  57. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_e2e_fixtures.py +0 -0
  58. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_graph.py +0 -0
  59. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_isolation_pbt.py +0 -0
  60. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_llm.py +0 -0
  61. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_loaders.py +0 -0
  62. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_outputs.py +0 -0
  63. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_outputs_pbt.py +0 -0
  64. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_proc.py +0 -0
  65. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_publish.py +0 -0
  66. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_registry.py +0 -0
  67. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_results.py +0 -0
  68. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_sdk_compat.py +0 -0
  69. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_source_resolution.py +0 -0
  70. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_stages.py +0 -0
  71. {agentic_dev_loop-1.0.0a6 → agentic_dev_loop-1.0.0a8}/tests/test_verdict.py +0 -0
  72. {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
- # engine mode (sandboxed loop — the full install)
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
@@ -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
- # engine mode (sandboxed loop — the full install)
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.0a6"
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
- # 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 = []
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 + workflow projection (unit-4 core).
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 workflow consumers: a clear
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
  """
@@ -1,2 +1,2 @@
1
1
  """Engine package — graph/stages/sandbox/CLI and the heavy OpenHands-SDK
2
- dependencies (unit-4 re-homing, FR-4.1)."""
2
+ dependencies."""
@@ -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
- return
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:
@@ -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` (shared with
6
- workflow mode); this module only owns the SDK-side capture. Expected
7
- conditions are typed `Result` errors, never exceptions.
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 + workflow-mode ingest surface (unit-4 viz)."""
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", "issue_ingest_token", "make_ingest_event",
12
+ "topology_mermaid", "translate",
14
13
  ]
@@ -1,7 +1,5 @@
1
- """Standalone workflow-mode viz launcher (FD-4): `python -m
2
- agentic_dev_loop.viz --pipeline <pipeline.yaml>` serves the live graph on
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
- if ingest_token is None:
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