agentic-dev-loop 0.12.0__tar.gz → 0.12.2__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-0.12.0/src/agentic_dev_loop.egg-info → agentic_dev_loop-0.12.2}/PKG-INFO +73 -10
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/README.md +71 -9
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/pyproject.toml +4 -1
- agentic_dev_loop-0.12.2/src/agentic_dev_loop/core/failure.py +21 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/core/verdict.py +87 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/actions.py +6 -5
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/agents.py +94 -2
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/cli.py +3 -0
- agentic_dev_loop-0.12.2/src/agentic_dev_loop/engine/completion.py +48 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/dryrun.py +8 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/graph.py +11 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/llm.py +59 -1
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/onboarding/templates/qa.agent.md +25 -18
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/onboarding/templates/review.agent.md +11 -14
- agentic_dev_loop-0.12.2/src/agentic_dev_loop/engine/provider_wire.py +190 -0
- agentic_dev_loop-0.12.2/src/agentic_dev_loop/engine/run_status.py +360 -0
- agentic_dev_loop-0.12.2/src/agentic_dev_loop/engine/run_status_command.md +23 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/stages.py +174 -45
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2/src/agentic_dev_loop.egg-info}/PKG-INFO +73 -10
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop.egg-info/SOURCES.txt +7 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop.egg-info/requires.txt +1 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_033_brief_step.py +8 -4
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_041_brief_coverage.py +2 -1
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_055_cli.py +1 -1
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_actions.py +3 -3
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_agent_error_guide.py +4 -3
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_dry_run.py +1 -1
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_e2e_fixtures.py +15 -8
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_graph.py +6 -4
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_llm.py +2 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_qa_prompt.py +8 -6
- agentic_dev_loop-0.12.2/tests/test_run_status.py +283 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_sdk_compat.py +65 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_stages.py +57 -30
- agentic_dev_loop-0.12.2/tests/test_unit11.py +487 -0
- agentic_dev_loop-0.12.2/tests/test_unit11_pbt.py +125 -0
- agentic_dev_loop-0.12.0/src/agentic_dev_loop/engine/provider_wire.py +0 -38
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/LICENSE +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/setup.cfg +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/__init__.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/core/__init__.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/core/config.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/core/paths.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/core/profiles/__init__.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/core/profiles/data/dotnet-node.yaml +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/core/profiles/data/python.yaml +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/core/results.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/__init__.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/agent_error_guide.md +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/agent_error_guide.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/allowlist.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/brief_coverage.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/brief_prompt.md +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/brief_prompt.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/containers.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/copilot_auth.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/deps.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/egress_policy.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/egress_proxy.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/forbidden.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/hostboundary.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/isolation.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/loaders.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/mounts.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/onboarding/__init__.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/onboarding/checks.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/onboarding/doctor.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/onboarding/init.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/onboarding/recipes/dotnet-node.yaml +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/onboarding/recipes/python.yaml +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/onboarding/recipes.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/onboarding/templates/dev.agent.md +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/onboarding/templates/env.example +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/onboarding/templates/trigger-agentic-dev-loop.sh +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/onboarding/templates.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/onboarding/upgrade.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/outputs.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/presets.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/proc.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/registry.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/secrets.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/step_workspace.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/sync_back.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/py.typed +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/viz/__init__.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/viz/__main__.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/viz/layout.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/viz/page.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/viz/server.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_029_merge_on_conflict.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_035_finalize_nudge.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_036_banned_commands_and_lane_boundary.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_046_brief_prompt.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_055_doctor.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_055_init.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_055_profile_loading.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_055_profiles.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_055_recipes.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_055_schema_lock.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_055_templates.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_055_upgrade.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_agents.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_aidlc.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_build_viz_page.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_cli.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_config.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_containers.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_copilot_auth.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_copilot_cli.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_deps.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_egress_proxy.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_hostboundary.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_hostboundary_integration.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_isolation.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_isolation_integration.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_isolation_pbt.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_loaders.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_module_contract.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_outputs.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_outputs_pbt.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_proc.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_provider_wire.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_publish.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_registry.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_repo_ascii.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_results.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_secrets.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_secrets_integration.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_secrets_pipeline.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_source_resolution.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_step_workspace.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_sync_back.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_trigger_script.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_unit5_config.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_unit5_pbt.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_unit5_pure.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_verdict.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_viz.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_viz_demo.py +0 -0
- {agentic_dev_loop-0.12.0 → agentic_dev_loop-0.12.2}/tests/test_viz_layout.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentic-dev-loop
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.2
|
|
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
|
|
@@ -32,6 +32,7 @@ Requires-Dist: openhands-tools~=1.17.0
|
|
|
32
32
|
Requires-Dist: agent-client-protocol<1.0.0
|
|
33
33
|
Requires-Dist: langgraph>=1.2
|
|
34
34
|
Requires-Dist: langgraph-checkpoint-sqlite>=3.0
|
|
35
|
+
Requires-Dist: msgpack>=1.0
|
|
35
36
|
Requires-Dist: filelock>=4.2.1
|
|
36
37
|
Requires-Dist: pyasn1>=0.6.5
|
|
37
38
|
Requires-Dist: pypdf>=6.21.4
|
|
@@ -60,6 +61,10 @@ checkpointed, and nothing gets committed until every gate passes.
|
|
|
60
61
|
parsed from files, retries are bounded, nothing is best-effort.
|
|
61
62
|
- **Sandboxed by default.** The agent's shell and every command step run inside
|
|
62
63
|
an always-on Docker sandbox. There is no opt-out.
|
|
64
|
+
- **Any OpenAI-compatible model, or your Copilot subscription.** Point
|
|
65
|
+
`llm.base_url` at any OpenAI-compatible endpoint with an API key, or log in
|
|
66
|
+
with `agentic-dev-loop copilot-login` and run on Copilot models. See
|
|
67
|
+
[LLM providers](#llm-providers).
|
|
63
68
|
|
|
64
69
|
---
|
|
65
70
|
|
|
@@ -94,6 +99,40 @@ Docker engine does:
|
|
|
94
99
|
Native Windows (outside WSL2) is not supported: every Windows-specific code
|
|
95
100
|
path and test was removed, and CI runs on Ubuntu only.
|
|
96
101
|
|
|
102
|
+
## LLM providers
|
|
103
|
+
|
|
104
|
+
Two ways to run the same pipeline. The engine speaks OpenAI-compatible wire
|
|
105
|
+
format throughout, so every step, profile, and gate works identically on
|
|
106
|
+
either provider style:
|
|
107
|
+
|
|
108
|
+
| Provider | Setup |
|
|
109
|
+
| --- | --- |
|
|
110
|
+
| **Any OpenAI-compatible endpoint** | API key via `api_key_env` + `base_url` in the `llm:` block: DeepSeek, OpenAI, or any OpenAI-compatible API. Full reference: [pipeline.yaml reference](#pipelineyaml-reference). |
|
|
111
|
+
| **GitHub Copilot subscription** | `agentic-dev-loop copilot-login` once, then point the `llm:` block at `https://api.githubcopilot.com` with the file-backed token. Probe-validated pins: `openai/gpt-5.6-luna` (pro) and `openai/gpt-4.1` (lite). Full config and known limitations: [Copilot subscription endpoint](#copilot-subscription-endpoint). |
|
|
112
|
+
|
|
113
|
+
```yaml
|
|
114
|
+
# OpenAI-compatible endpoint (any provider)
|
|
115
|
+
llm:
|
|
116
|
+
model: openai/deepseek-v4-pro
|
|
117
|
+
base_url: https://api.deepseek.com
|
|
118
|
+
api_key_env: OPENAI_API_KEY
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```yaml
|
|
122
|
+
# GitHub Copilot subscription
|
|
123
|
+
llm:
|
|
124
|
+
model: openai/gpt-5.6-luna # pro; probe-validated pin
|
|
125
|
+
base_url: https://api.githubcopilot.com
|
|
126
|
+
api_key_secret:
|
|
127
|
+
from: file
|
|
128
|
+
key: ~/.agentic-dev-loop/auth/copilot.json
|
|
129
|
+
json_key: access_token
|
|
130
|
+
headers:
|
|
131
|
+
Copilot-Integration-Id: vscode-chat
|
|
132
|
+
Editor-Version: vscode/1.94.0
|
|
133
|
+
Openai-Intent: conversation-panel
|
|
134
|
+
```
|
|
135
|
+
|
|
97
136
|
## Installation
|
|
98
137
|
|
|
99
138
|
**Prerequisites**
|
|
@@ -102,7 +141,7 @@ path and test was removed, and CI runs on Ubuntu only.
|
|
|
102
141
|
| --- | --- |
|
|
103
142
|
| Python | ≥ 3.12 |
|
|
104
143
|
| Docker engine | Engine API ≥ 1.43 (Engine 24). Any compliant engine works - Docker Desktop, OrbStack, Colima, Rancher Desktop, podman docker-compat. On Windows, run under WSL2: the repo must live on the WSL filesystem, not /mnt/c. |
|
|
105
|
-
| 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. |
|
|
144
|
+
| 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. Alternatively, `agentic-dev-loop copilot-login` authenticates a Copilot subscription (see [LLM providers](#llm-providers)). |
|
|
106
145
|
|
|
107
146
|
```bash
|
|
108
147
|
pip install "agentic-dev-loop==1.0.0a17"
|
|
@@ -127,6 +166,27 @@ reuses a pre-upload snapshot of the index for up to 10 minutes, reporting
|
|
|
127
166
|
uv pip install --refresh "agentic-dev-loop==1.0.0a17"
|
|
128
167
|
```
|
|
129
168
|
|
|
169
|
+
### Inspecting a run
|
|
170
|
+
|
|
171
|
+
`agentic-dev-loop run-status <pipeline>` renders the latest run for a pipeline
|
|
172
|
+
as a mermaid flowchart with the current step highlighted, plus a bullet
|
|
173
|
+
summary, reading the run-state checkpoint database directly:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
agentic-dev-loop run-status agentic-dev-loop/pipeline.yaml
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
To drive that from OpenCode, install the bundled `/run-status` command once into
|
|
180
|
+
your global OpenCode commands directory:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
agentic-dev-loop run-status --install
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
This copies the command wrapper to
|
|
187
|
+
`${XDG_CONFIG_HOME:-~/.config}/opencode/commands/run-status.md`; from then on
|
|
188
|
+
`/run-status` in any OpenCode workspace shells out to the subcommand above.
|
|
189
|
+
|
|
130
190
|
On the first run the engine pulls the **digest-pinned** dev image
|
|
131
191
|
(`ghcr.io/lukedawilson/agentic-dev-loop/dev`) and verifies its digest - the
|
|
132
192
|
supply-chain check refuses to continue on a mismatch. If you're running
|
|
@@ -181,8 +241,9 @@ flowchart LR
|
|
|
181
241
|
Its edits are untracked; only declared `produces:` files are hygiene-tracked.
|
|
182
242
|
2. **test** - a mechanical command step runs the suite inside the sandbox.
|
|
183
243
|
Exit 0 moves on; anything else rewinds to `dev` with the failing output.
|
|
184
|
-
3. **review / qa** - verdict agents
|
|
185
|
-
the graph. `fail` rewinds to `dev`
|
|
244
|
+
3. **review / qa** - verdict agents finish by calling the `record_verdict`
|
|
245
|
+
tool; the validated tool call routes the graph. `fail` rewinds to `dev`
|
|
246
|
+
and consumes an attempt.
|
|
186
247
|
4. **commit** - the single commit point fires only when all gates passed and
|
|
187
248
|
the worktree was clean at start.
|
|
188
249
|
5. **success** - committed; or **failure** when attempts are exhausted.
|
|
@@ -265,7 +326,7 @@ steps:
|
|
|
265
326
|
| Key | Notes |
|
|
266
327
|
| --- | --- |
|
|
267
328
|
| `agent:` / `command:` / `action:` / `brief_inputs:` | Exactly one per step. Agents live in `agents_dir` as `<name>.agent.md`; commands run sandboxed (mechanical, never an LLM call). `commit` is the sole host-side built-in action; consumer actions are sandbox-executed, command-shaped scripts (see [Host boundary](#host-boundary-security-model)). `brief_inputs:` is a native brief step - see its row below. |
|
|
268
|
-
| `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict
|
|
329
|
+
| `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs structured-verdict routing. The verdict comes from a validated `record_verdict` tool call, never from prose; stale artifacts are deleted at each attempt start. |
|
|
269
330
|
| `brief_inputs:` | The discriminator for a native brief step: `{ include: [<paths/globs>], env_facts: [<strings>], sections: [<headings>] }`. There is no `agent:`, `produces:`, or `max_iterations:` - the prompt is engine-embedded, the capture file is `attempt-brief.md`, and the iteration cap is 12, all engine-owned. The engine resolves the include list against the workspace, passes the resolved docs + include contents + env facts as the step's task message, and regenerates the step only when those inputs change. `sections` is the ordered list of digest headings the brief must emit (default `[Repo rules digest, Unit scope digest, Environment facts, Open risks]`; non-empty and unique). `name` stays free-form, so multiple brief steps are allowed. |
|
|
270
331
|
| `terminal_timeout:` | Optional agent-step per-action timeout in seconds (gt 0). `SandboxTerminalExecutor` uses it when the agent leaves the terminal action timeout unset, overriding the SDK tool's default. `None` = current behavior. |
|
|
271
332
|
| `finalize_nudge_turns:` | Optional agent-step final-response nudge (gt 0). Requires `max_iterations:` and must be less than it. At `max_iterations - finalize_nudge_turns` completed turns the engine injects one advisory turn ("`N turns remain: produce your final response now`"), exactly once; the agent may still exhaust its turns and existing stuck/unclear handling applies unchanged. Absent = current behavior. Provisional pending one consumer run (revert if noisy or the raised cap inflates cost). |
|
|
@@ -588,13 +649,15 @@ and the `commit` step never commits it). `init` writes them; the layout is:
|
|
|
588
649
|
|
|
589
650
|
Each `sdk_agents/<name>.agent.md` is a markdown prompt plus YAML frontmatter:
|
|
590
651
|
`name` matches a step's `agent:` value; `tools` lists the tool names the step
|
|
591
|
-
may use (`terminal` always;
|
|
592
|
-
`figma`).
|
|
652
|
+
may use (`terminal` always; verdict steps also need `record_verdict`; a repo
|
|
653
|
+
can add more via extensions, e.g. `figma`). A step configured with `verdicts:`
|
|
654
|
+
whose agent file does not grant `record_verdict` fails at load, never at
|
|
655
|
+
routing time.
|
|
593
656
|
|
|
594
657
|
```markdown
|
|
595
658
|
---
|
|
596
659
|
name: review # matches a step's `agent:` value
|
|
597
|
-
tools: [terminal]
|
|
660
|
+
tools: [terminal, record_verdict]
|
|
598
661
|
---
|
|
599
662
|
# Review Agent
|
|
600
663
|
## WORKFLOW # numbered steps
|
|
@@ -608,8 +671,8 @@ Three roles, three contracts:
|
|
|
608
671
|
| Role | Verdict contract | Typical sandbox |
|
|
609
672
|
| --- | --- | --- |
|
|
610
673
|
| dev | none - the final response is captured as the `produces:` summary | rw mount, offline |
|
|
611
|
-
| review | `
|
|
612
|
-
| qa | `
|
|
674
|
+
| review | `record_verdict` with `APPROVED` / `NEEDS CHANGES`; the prose keeps the findings for humans | ro mount, offline |
|
|
675
|
+
| qa | `record_verdict` with `PASS` / `FAIL`; map `unverifiable: UNVERIFIABLE` if you want a third way | shaped by the repo: rw + network + `class: browser` when it boots servers and drives a browser (the consumer repo); ro + offline when it probes the CLI only (this repo) |
|
|
613
676
|
|
|
614
677
|
The conventions that make a good agent file - each one below exists because
|
|
615
678
|
an agent burned budget learning it mid-run:
|
|
@@ -16,6 +16,10 @@ checkpointed, and nothing gets committed until every gate passes.
|
|
|
16
16
|
parsed from files, retries are bounded, nothing is best-effort.
|
|
17
17
|
- **Sandboxed by default.** The agent's shell and every command step run inside
|
|
18
18
|
an always-on Docker sandbox. There is no opt-out.
|
|
19
|
+
- **Any OpenAI-compatible model, or your Copilot subscription.** Point
|
|
20
|
+
`llm.base_url` at any OpenAI-compatible endpoint with an API key, or log in
|
|
21
|
+
with `agentic-dev-loop copilot-login` and run on Copilot models. See
|
|
22
|
+
[LLM providers](#llm-providers).
|
|
19
23
|
|
|
20
24
|
---
|
|
21
25
|
|
|
@@ -50,6 +54,40 @@ Docker engine does:
|
|
|
50
54
|
Native Windows (outside WSL2) is not supported: every Windows-specific code
|
|
51
55
|
path and test was removed, and CI runs on Ubuntu only.
|
|
52
56
|
|
|
57
|
+
## LLM providers
|
|
58
|
+
|
|
59
|
+
Two ways to run the same pipeline. The engine speaks OpenAI-compatible wire
|
|
60
|
+
format throughout, so every step, profile, and gate works identically on
|
|
61
|
+
either provider style:
|
|
62
|
+
|
|
63
|
+
| Provider | Setup |
|
|
64
|
+
| --- | --- |
|
|
65
|
+
| **Any OpenAI-compatible endpoint** | API key via `api_key_env` + `base_url` in the `llm:` block: DeepSeek, OpenAI, or any OpenAI-compatible API. Full reference: [pipeline.yaml reference](#pipelineyaml-reference). |
|
|
66
|
+
| **GitHub Copilot subscription** | `agentic-dev-loop copilot-login` once, then point the `llm:` block at `https://api.githubcopilot.com` with the file-backed token. Probe-validated pins: `openai/gpt-5.6-luna` (pro) and `openai/gpt-4.1` (lite). Full config and known limitations: [Copilot subscription endpoint](#copilot-subscription-endpoint). |
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
# OpenAI-compatible endpoint (any provider)
|
|
70
|
+
llm:
|
|
71
|
+
model: openai/deepseek-v4-pro
|
|
72
|
+
base_url: https://api.deepseek.com
|
|
73
|
+
api_key_env: OPENAI_API_KEY
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```yaml
|
|
77
|
+
# GitHub Copilot subscription
|
|
78
|
+
llm:
|
|
79
|
+
model: openai/gpt-5.6-luna # pro; probe-validated pin
|
|
80
|
+
base_url: https://api.githubcopilot.com
|
|
81
|
+
api_key_secret:
|
|
82
|
+
from: file
|
|
83
|
+
key: ~/.agentic-dev-loop/auth/copilot.json
|
|
84
|
+
json_key: access_token
|
|
85
|
+
headers:
|
|
86
|
+
Copilot-Integration-Id: vscode-chat
|
|
87
|
+
Editor-Version: vscode/1.94.0
|
|
88
|
+
Openai-Intent: conversation-panel
|
|
89
|
+
```
|
|
90
|
+
|
|
53
91
|
## Installation
|
|
54
92
|
|
|
55
93
|
**Prerequisites**
|
|
@@ -58,7 +96,7 @@ path and test was removed, and CI runs on Ubuntu only.
|
|
|
58
96
|
| --- | --- |
|
|
59
97
|
| Python | ≥ 3.12 |
|
|
60
98
|
| Docker engine | Engine API ≥ 1.43 (Engine 24). Any compliant engine works - Docker Desktop, OrbStack, Colima, Rancher Desktop, podman docker-compat. On Windows, run under WSL2: the repo must live on the WSL filesystem, not /mnt/c. |
|
|
61
|
-
| 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
|
+
| 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. Alternatively, `agentic-dev-loop copilot-login` authenticates a Copilot subscription (see [LLM providers](#llm-providers)). |
|
|
62
100
|
|
|
63
101
|
```bash
|
|
64
102
|
pip install "agentic-dev-loop==1.0.0a17"
|
|
@@ -83,6 +121,27 @@ reuses a pre-upload snapshot of the index for up to 10 minutes, reporting
|
|
|
83
121
|
uv pip install --refresh "agentic-dev-loop==1.0.0a17"
|
|
84
122
|
```
|
|
85
123
|
|
|
124
|
+
### Inspecting a run
|
|
125
|
+
|
|
126
|
+
`agentic-dev-loop run-status <pipeline>` renders the latest run for a pipeline
|
|
127
|
+
as a mermaid flowchart with the current step highlighted, plus a bullet
|
|
128
|
+
summary, reading the run-state checkpoint database directly:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
agentic-dev-loop run-status agentic-dev-loop/pipeline.yaml
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
To drive that from OpenCode, install the bundled `/run-status` command once into
|
|
135
|
+
your global OpenCode commands directory:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
agentic-dev-loop run-status --install
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
This copies the command wrapper to
|
|
142
|
+
`${XDG_CONFIG_HOME:-~/.config}/opencode/commands/run-status.md`; from then on
|
|
143
|
+
`/run-status` in any OpenCode workspace shells out to the subcommand above.
|
|
144
|
+
|
|
86
145
|
On the first run the engine pulls the **digest-pinned** dev image
|
|
87
146
|
(`ghcr.io/lukedawilson/agentic-dev-loop/dev`) and verifies its digest - the
|
|
88
147
|
supply-chain check refuses to continue on a mismatch. If you're running
|
|
@@ -137,8 +196,9 @@ flowchart LR
|
|
|
137
196
|
Its edits are untracked; only declared `produces:` files are hygiene-tracked.
|
|
138
197
|
2. **test** - a mechanical command step runs the suite inside the sandbox.
|
|
139
198
|
Exit 0 moves on; anything else rewinds to `dev` with the failing output.
|
|
140
|
-
3. **review / qa** - verdict agents
|
|
141
|
-
the graph. `fail` rewinds to `dev`
|
|
199
|
+
3. **review / qa** - verdict agents finish by calling the `record_verdict`
|
|
200
|
+
tool; the validated tool call routes the graph. `fail` rewinds to `dev`
|
|
201
|
+
and consumes an attempt.
|
|
142
202
|
4. **commit** - the single commit point fires only when all gates passed and
|
|
143
203
|
the worktree was clean at start.
|
|
144
204
|
5. **success** - committed; or **failure** when attempts are exhausted.
|
|
@@ -221,7 +281,7 @@ steps:
|
|
|
221
281
|
| Key | Notes |
|
|
222
282
|
| --- | --- |
|
|
223
283
|
| `agent:` / `command:` / `action:` / `brief_inputs:` | Exactly one per step. Agents live in `agents_dir` as `<name>.agent.md`; commands run sandboxed (mechanical, never an LLM call). `commit` is the sole host-side built-in action; consumer actions are sandbox-executed, command-shaped scripts (see [Host boundary](#host-boundary-security-model)). `brief_inputs:` is a native brief step - see its row below. |
|
|
224
|
-
| `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict
|
|
284
|
+
| `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs structured-verdict routing. The verdict comes from a validated `record_verdict` tool call, never from prose; stale artifacts are deleted at each attempt start. |
|
|
225
285
|
| `brief_inputs:` | The discriminator for a native brief step: `{ include: [<paths/globs>], env_facts: [<strings>], sections: [<headings>] }`. There is no `agent:`, `produces:`, or `max_iterations:` - the prompt is engine-embedded, the capture file is `attempt-brief.md`, and the iteration cap is 12, all engine-owned. The engine resolves the include list against the workspace, passes the resolved docs + include contents + env facts as the step's task message, and regenerates the step only when those inputs change. `sections` is the ordered list of digest headings the brief must emit (default `[Repo rules digest, Unit scope digest, Environment facts, Open risks]`; non-empty and unique). `name` stays free-form, so multiple brief steps are allowed. |
|
|
226
286
|
| `terminal_timeout:` | Optional agent-step per-action timeout in seconds (gt 0). `SandboxTerminalExecutor` uses it when the agent leaves the terminal action timeout unset, overriding the SDK tool's default. `None` = current behavior. |
|
|
227
287
|
| `finalize_nudge_turns:` | Optional agent-step final-response nudge (gt 0). Requires `max_iterations:` and must be less than it. At `max_iterations - finalize_nudge_turns` completed turns the engine injects one advisory turn ("`N turns remain: produce your final response now`"), exactly once; the agent may still exhaust its turns and existing stuck/unclear handling applies unchanged. Absent = current behavior. Provisional pending one consumer run (revert if noisy or the raised cap inflates cost). |
|
|
@@ -544,13 +604,15 @@ and the `commit` step never commits it). `init` writes them; the layout is:
|
|
|
544
604
|
|
|
545
605
|
Each `sdk_agents/<name>.agent.md` is a markdown prompt plus YAML frontmatter:
|
|
546
606
|
`name` matches a step's `agent:` value; `tools` lists the tool names the step
|
|
547
|
-
may use (`terminal` always;
|
|
548
|
-
`figma`).
|
|
607
|
+
may use (`terminal` always; verdict steps also need `record_verdict`; a repo
|
|
608
|
+
can add more via extensions, e.g. `figma`). A step configured with `verdicts:`
|
|
609
|
+
whose agent file does not grant `record_verdict` fails at load, never at
|
|
610
|
+
routing time.
|
|
549
611
|
|
|
550
612
|
```markdown
|
|
551
613
|
---
|
|
552
614
|
name: review # matches a step's `agent:` value
|
|
553
|
-
tools: [terminal]
|
|
615
|
+
tools: [terminal, record_verdict]
|
|
554
616
|
---
|
|
555
617
|
# Review Agent
|
|
556
618
|
## WORKFLOW # numbered steps
|
|
@@ -564,8 +626,8 @@ Three roles, three contracts:
|
|
|
564
626
|
| Role | Verdict contract | Typical sandbox |
|
|
565
627
|
| --- | --- | --- |
|
|
566
628
|
| dev | none - the final response is captured as the `produces:` summary | rw mount, offline |
|
|
567
|
-
| review | `
|
|
568
|
-
| qa | `
|
|
629
|
+
| review | `record_verdict` with `APPROVED` / `NEEDS CHANGES`; the prose keeps the findings for humans | ro mount, offline |
|
|
630
|
+
| qa | `record_verdict` with `PASS` / `FAIL`; map `unverifiable: UNVERIFIABLE` if you want a third way | shaped by the repo: rw + network + `class: browser` when it boots servers and drives a browser (the consumer repo); ro + offline when it probes the CLI only (this repo) |
|
|
569
631
|
|
|
570
632
|
The conventions that make a good agent file - each one below exists because
|
|
571
633
|
an agent burned budget learning it mid-run:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agentic-dev-loop"
|
|
7
|
-
version = "0.12.
|
|
7
|
+
version = "0.12.2"
|
|
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"
|
|
@@ -43,6 +43,9 @@ dependencies = [
|
|
|
43
43
|
"agent-client-protocol<1.0.0",
|
|
44
44
|
"langgraph>=1.2",
|
|
45
45
|
"langgraph-checkpoint-sqlite>=3.0",
|
|
46
|
+
# msgpack is a transitive dep (langgraph-checkpoint-sqlite), but the
|
|
47
|
+
# run-status subcommand decodes checkpoints directly, so pin it explicitly.
|
|
48
|
+
"msgpack>=1.0",
|
|
46
49
|
# filelock is a transitive dep (openhands-sdk, litellm, virtualenv).
|
|
47
50
|
# 3.32.7 carries CVE-2026-112907/-112913/-112940 (fixed in 4.2.1); pin a
|
|
48
51
|
# floor so the restore step's pip-audit stays green.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Structural failure vocabulary for agent steps (unit-11).
|
|
2
|
+
|
|
3
|
+
These classes are value objects: they name *why* an agent step failed
|
|
4
|
+
structurally, never how the run should prose its way around the failure. A
|
|
5
|
+
structural failure halts the step and the run - it never enters the retry
|
|
6
|
+
path, because retrying re-runs the same refusal (FD-11, rule E-1).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import enum
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FailureClass(str, enum.Enum):
|
|
15
|
+
"""The named structural failure classes an agent step can raise."""
|
|
16
|
+
|
|
17
|
+
NO_ACTIONS = "no_actions"
|
|
18
|
+
NO_VERDICT_TOOL = "no_verdict_tool"
|
|
19
|
+
INVALID_VERDICT_VALUE = "invalid_verdict_value"
|
|
20
|
+
CONFLICTING_VERDICT = "conflicting_verdict"
|
|
21
|
+
WIRE_SHAPE = "wire_shape"
|
|
@@ -10,10 +10,17 @@ totality for arbitrary input (no crash, no silent routing).
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
import enum
|
|
13
|
+
import json
|
|
13
14
|
from dataclasses import dataclass
|
|
15
|
+
from typing import Iterable
|
|
14
16
|
|
|
17
|
+
from .failure import FailureClass
|
|
15
18
|
from .results import Result
|
|
16
19
|
|
|
20
|
+
# The structured verdict channel's tool name. The tool is registered by the
|
|
21
|
+
# engine (engine/agents.py); the entity owns the argument contract.
|
|
22
|
+
RECORD_VERDICT_TOOL = "record_verdict"
|
|
23
|
+
|
|
17
24
|
# Leading markdown/list/quote decoration tolerated on a verdict line.
|
|
18
25
|
_LEAD_DECOR = "#*_`>- "
|
|
19
26
|
# Decoration tolerated between VERDICT and ':' and after the keyword.
|
|
@@ -33,6 +40,15 @@ class VerdictError(enum.Enum):
|
|
|
33
40
|
AMBIGUOUS = "ambiguous"
|
|
34
41
|
|
|
35
42
|
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class VerdictChannelError:
|
|
45
|
+
"""A structured verdict channel failure: the class plus the evidence a
|
|
46
|
+
report must name (the offending value, the configured set, the conflict)."""
|
|
47
|
+
|
|
48
|
+
code: FailureClass
|
|
49
|
+
detail: str
|
|
50
|
+
|
|
51
|
+
|
|
36
52
|
@dataclass(frozen=True)
|
|
37
53
|
class Verdict:
|
|
38
54
|
"""A parsed decision: `value` is the canonical outcome (e.g. 'pass').
|
|
@@ -64,6 +80,77 @@ class Verdict:
|
|
|
64
80
|
return Result.err(VerdictError.NO_VERDICT)
|
|
65
81
|
|
|
66
82
|
|
|
83
|
+
@staticmethod
|
|
84
|
+
def from_events(
|
|
85
|
+
events: Iterable[object], verdicts: dict[str, str]
|
|
86
|
+
) -> "Result[Verdict, VerdictChannelError]":
|
|
87
|
+
"""Decode the verdict from record_verdict tool calls (FR-11.2).
|
|
88
|
+
|
|
89
|
+
The event stream is the only source: prose is never parsed here. Zero
|
|
90
|
+
calls is NO_VERDICT_TOOL; a value outside the configured set is
|
|
91
|
+
INVALID_VERDICT_VALUE; two different decisions are CONFLICTING_VERDICT
|
|
92
|
+
(fail fast, FD-Q1). Matching is case-insensitive because models vary
|
|
93
|
+
the casing of a configured keyword."""
|
|
94
|
+
values: list[str] = []
|
|
95
|
+
calls = 0
|
|
96
|
+
for event in events:
|
|
97
|
+
if getattr(event, "tool_name", None) != RECORD_VERDICT_TOOL:
|
|
98
|
+
continue
|
|
99
|
+
calls += 1
|
|
100
|
+
value = _verdict_argument(event)
|
|
101
|
+
if value is not None:
|
|
102
|
+
values.append(value)
|
|
103
|
+
if calls == 0:
|
|
104
|
+
return Result.err(VerdictChannelError(
|
|
105
|
+
FailureClass.NO_VERDICT_TOOL,
|
|
106
|
+
f"no {RECORD_VERDICT_TOOL} tool call in the event stream"))
|
|
107
|
+
lookup = {keyword.casefold(): value
|
|
108
|
+
for keyword, value in verdicts.items()}
|
|
109
|
+
decided: dict[str, str] = {}
|
|
110
|
+
for value in values:
|
|
111
|
+
canonical = lookup.get(value.casefold())
|
|
112
|
+
if canonical is None:
|
|
113
|
+
configured = ", ".join(sorted(verdicts))
|
|
114
|
+
return Result.err(VerdictChannelError(
|
|
115
|
+
FailureClass.INVALID_VERDICT_VALUE,
|
|
116
|
+
f"{RECORD_VERDICT_TOOL} value {value!r} is not in the "
|
|
117
|
+
f"configured set [{configured}]"))
|
|
118
|
+
decided[canonical] = value
|
|
119
|
+
if not decided:
|
|
120
|
+
return Result.err(VerdictChannelError(
|
|
121
|
+
FailureClass.INVALID_VERDICT_VALUE,
|
|
122
|
+
f"{RECORD_VERDICT_TOOL} call carried no readable verdict "
|
|
123
|
+
"argument"))
|
|
124
|
+
if len(decided) > 1:
|
|
125
|
+
return Result.err(VerdictChannelError(
|
|
126
|
+
FailureClass.CONFLICTING_VERDICT,
|
|
127
|
+
"conflicting verdicts: "
|
|
128
|
+
+ ", ".join(sorted(repr(v) for v in decided.values()))))
|
|
129
|
+
return Result.ok(Verdict(value=next(iter(decided))))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _verdict_argument(event: object) -> str | None:
|
|
133
|
+
"""The verdict argument carried by one RecordVerdictAction event.
|
|
134
|
+
|
|
135
|
+
Prefers the parsed action (the SDK's normal path); falls back to the raw
|
|
136
|
+
tool-call arguments JSON when the action was not parsed."""
|
|
137
|
+
action = getattr(event, "action", None)
|
|
138
|
+
value = getattr(action, "verdict", None)
|
|
139
|
+
if isinstance(value, str):
|
|
140
|
+
return value
|
|
141
|
+
tool_call = getattr(event, "tool_call", None)
|
|
142
|
+
arguments = getattr(tool_call, "arguments", None)
|
|
143
|
+
if not isinstance(arguments, str):
|
|
144
|
+
return None
|
|
145
|
+
try:
|
|
146
|
+
payload = json.loads(arguments)
|
|
147
|
+
except json.JSONDecodeError:
|
|
148
|
+
return None
|
|
149
|
+
if isinstance(payload, dict) and isinstance(payload.get("verdict"), str):
|
|
150
|
+
return payload["verdict"]
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
|
|
67
154
|
def parse_verdict(text: str, verdicts: dict[str, str]) -> str:
|
|
68
155
|
"""Route for a verdict artifact: the canonical decision value on a single
|
|
69
156
|
clear decision, else ``retry``. Never raises - unclear verdicts retry."""
|
|
@@ -215,7 +215,8 @@ def commit(state: dict, params: dict) -> dict:
|
|
|
215
215
|
if not state.get("commit_allowed", False):
|
|
216
216
|
print(f"[{subject}] Skipping auto-commit - worktree was dirty at run "
|
|
217
217
|
f"start; commit the constructed work manually.", flush=True)
|
|
218
|
-
return {
|
|
218
|
+
return {"verified_state": {
|
|
219
|
+
"commit": "skipped: worktree dirty at run start"}}
|
|
219
220
|
if "message" not in params:
|
|
220
221
|
raise ValueError("commit action requires params.message "
|
|
221
222
|
"(a template rendered with {subject})")
|
|
@@ -235,10 +236,10 @@ def commit(state: dict, params: dict) -> dict:
|
|
|
235
236
|
# permission problem) - its error is the diagnostic that matters.
|
|
236
237
|
print(f"WARNING: [{subject}] auto-commit failed - git add: "
|
|
237
238
|
f"{added.stderr.strip()}", file=sys.stderr, flush=True)
|
|
238
|
-
return {}
|
|
239
|
+
return {"verified_state": {"commit": "failed: add"}}
|
|
239
240
|
if not _git(["status", "--porcelain"], cwd=cwd).stdout.strip():
|
|
240
241
|
print(f"[{subject}] Nothing to commit.", flush=True)
|
|
241
|
-
return {}
|
|
242
|
+
return {"verified_state": {"commit": "nothing to commit"}}
|
|
242
243
|
message = params["message"].format(subject=subject)
|
|
243
244
|
result = _git(["-c", f"core.hooksPath={hooks}", "commit", "-m", message],
|
|
244
245
|
cwd=cwd)
|
|
@@ -247,7 +248,7 @@ def commit(state: dict, params: dict) -> dict:
|
|
|
247
248
|
# not a pipeline failure.
|
|
248
249
|
print(f"WARNING: [{subject}] auto-commit failed - commit manually.\n"
|
|
249
250
|
f"{result.stderr.strip()}", file=sys.stderr, flush=True)
|
|
250
|
-
return {}
|
|
251
|
+
return {"verified_state": {"commit": "failed: commit"}}
|
|
251
252
|
short = _git(["rev-parse", "--short", "HEAD"], cwd=cwd).stdout.strip()
|
|
252
253
|
print(f"[{subject}] Committed constructed work as {short}.", flush=True)
|
|
253
|
-
return {}
|
|
254
|
+
return {"verified_state": {"commit": f"committed {short}"}}
|
|
@@ -10,13 +10,17 @@ from pathlib import Path
|
|
|
10
10
|
from typing import TYPE_CHECKING
|
|
11
11
|
|
|
12
12
|
from openhands.sdk import LLM
|
|
13
|
-
from openhands.sdk.tool import
|
|
13
|
+
from openhands.sdk.tool import (Action, Observation, ToolAnnotations,
|
|
14
|
+
ToolDefinition, ToolExecutor, register_tool)
|
|
15
|
+
from pydantic import Field
|
|
14
16
|
from openhands.tools.file_editor import FileEditorTool # noqa: F401 - registers "file_editor"
|
|
15
17
|
from openhands.tools.task_tracker import TaskTrackerTool # noqa: F401 - registers "task_tracker"
|
|
16
18
|
from openhands.tools.terminal import TerminalTool # noqa: F401 - registers "terminal"
|
|
17
19
|
|
|
18
20
|
from .forbidden import ForbiddenRule, match_forbidden_command
|
|
19
21
|
from .brief_prompt import render_brief_prompt
|
|
22
|
+
from agentic_dev_loop.core.verdict import RECORD_VERDICT_TOOL
|
|
23
|
+
|
|
20
24
|
from .isolation import (SandboxError, SandboxExecutionRequest, command_argv,
|
|
21
25
|
resolve_sandbox_env)
|
|
22
26
|
|
|
@@ -32,7 +36,39 @@ if TYPE_CHECKING:
|
|
|
32
36
|
BRIEF_AGENT_NAME = "4f1c9e2b-7a3d-4e5f-8b6c-0d1a2f3c4d5e"
|
|
33
37
|
|
|
34
38
|
|
|
35
|
-
def
|
|
39
|
+
def check_required_tools(tools: list[str], required: tuple[str, ...],
|
|
40
|
+
*, agent_name: str) -> None:
|
|
41
|
+
"""Fail loudly when a step's contract needs a tool the agent file does not
|
|
42
|
+
grant (BR-1: the frontmatter list is the exact allowlist)."""
|
|
43
|
+
missing = [name for name in required if name not in tools]
|
|
44
|
+
if missing:
|
|
45
|
+
raise ValueError(
|
|
46
|
+
f"agent {agent_name!r} does not grant required tools "
|
|
47
|
+
f"{missing}; add them to its frontmatter 'tools:' allowlist")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def validate_agent_contracts(cfg, agents_dir: Path | str) -> None:
|
|
51
|
+
"""Load-time contract check (unit-11, B-2): a step configured with
|
|
52
|
+
``verdicts:`` requires ``record_verdict`` in its agent's tool allowlist.
|
|
53
|
+
|
|
54
|
+
A missing agent file is left to ``load_agent`` (which fails fast naming
|
|
55
|
+
it); this check only inspects files that exist so graph construction stays
|
|
56
|
+
a pure config pass for fake-agent test harnesses."""
|
|
57
|
+
from openhands.sdk.subagent.schema import AgentDefinition
|
|
58
|
+
|
|
59
|
+
for step in cfg.steps:
|
|
60
|
+
if step.agent is None or step.verdicts is None:
|
|
61
|
+
continue
|
|
62
|
+
path = Path(agents_dir) / f"{step.agent}.agent.md"
|
|
63
|
+
if not path.is_file():
|
|
64
|
+
continue
|
|
65
|
+
agent_def = AgentDefinition.load(path)
|
|
66
|
+
check_required_tools(agent_def.tools, (RECORD_VERDICT_TOOL,),
|
|
67
|
+
agent_name=step.agent)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def load_agent(agents_dir: Path | str, name: str, llm: LLM,
|
|
71
|
+
required_tools: tuple[str, ...] = ()) -> "Agent":
|
|
36
72
|
"""Load `<agents_dir>/<name>.agent.md` and build the Agent via the SDK
|
|
37
73
|
factory (frontmatter tool names resolved against the registry, unknown →
|
|
38
74
|
ValueError; the markdown body is delivered as the system prompt suffix).
|
|
@@ -63,6 +99,7 @@ def load_agent(agents_dir: Path | str, name: str, llm: LLM) -> "Agent":
|
|
|
63
99
|
raise ValueError(
|
|
64
100
|
f"Agent file {path} name {agent_def.name!r} does not match "
|
|
65
101
|
f"filename {stem!r}")
|
|
102
|
+
check_required_tools(agent_def.tools, required_tools, agent_name=name)
|
|
66
103
|
return agent_definition_to_factory(agent_def)(llm)
|
|
67
104
|
|
|
68
105
|
|
|
@@ -206,3 +243,58 @@ def register_sandbox_terminal(sandbox: "Sandbox", *, env: dict[str, str | None]
|
|
|
206
243
|
allowlist=allowlist))
|
|
207
244
|
|
|
208
245
|
register_tool("terminal", factory)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class RecordVerdictAction(Action):
|
|
249
|
+
"""The structured verdict channel's input (unit-11, FR-11.2)."""
|
|
250
|
+
|
|
251
|
+
verdict: str = Field(
|
|
252
|
+
description=(
|
|
253
|
+
"The verdict keyword for this step, exactly as configured in the "
|
|
254
|
+
"pipeline's `verdicts:` map (for example APPROVED or NEEDS "
|
|
255
|
+
"CHANGES)."))
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class RecordVerdictObservation(Observation):
|
|
259
|
+
"""A visible acknowledgement: the wire invariant (D-3) requires the tool
|
|
260
|
+
result to reach the model, so this is never empty."""
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class RecordVerdictExecutor(ToolExecutor):
|
|
264
|
+
def __call__(self, action: RecordVerdictAction,
|
|
265
|
+
conversation=None) -> RecordVerdictObservation:
|
|
266
|
+
return RecordVerdictObservation.from_text(
|
|
267
|
+
f"recorded verdict: {action.verdict}")
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class RecordVerdictTool(
|
|
271
|
+
ToolDefinition[RecordVerdictAction, RecordVerdictObservation]):
|
|
272
|
+
"""Registers `record_verdict`; validation against the step's configured
|
|
273
|
+
verdict set happens in `Verdict.from_events`, never in the tool schema."""
|
|
274
|
+
|
|
275
|
+
@classmethod
|
|
276
|
+
def create(cls, conv_state=None, **params):
|
|
277
|
+
if params:
|
|
278
|
+
raise ValueError("RecordVerdictTool does not accept parameters")
|
|
279
|
+
return [
|
|
280
|
+
cls(
|
|
281
|
+
action_type=RecordVerdictAction,
|
|
282
|
+
observation_type=RecordVerdictObservation,
|
|
283
|
+
description=(
|
|
284
|
+
"Record this step's verdict. Call exactly once when your "
|
|
285
|
+
"work is complete, passing the configured verdict keyword. "
|
|
286
|
+
"Never state the verdict only in prose: the pipeline reads "
|
|
287
|
+
"this tool call, not your final message."),
|
|
288
|
+
executor=RecordVerdictExecutor(),
|
|
289
|
+
annotations=ToolAnnotations(
|
|
290
|
+
title=RECORD_VERDICT_TOOL,
|
|
291
|
+
readOnlyHint=True,
|
|
292
|
+
destructiveHint=False,
|
|
293
|
+
idempotentHint=True,
|
|
294
|
+
openWorldHint=False,
|
|
295
|
+
),
|
|
296
|
+
)
|
|
297
|
+
]
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
register_tool(RECORD_VERDICT_TOOL, RecordVerdictTool)
|
|
@@ -226,6 +226,9 @@ def main(argv: list[str] | None = None, *, keep_alive: bool = True) -> int:
|
|
|
226
226
|
return _run_copilot_login(argv[1:])
|
|
227
227
|
if argv and argv[0] == "copilot-models":
|
|
228
228
|
return _run_copilot_models(argv[1:])
|
|
229
|
+
if argv and argv[0] == "run-status":
|
|
230
|
+
from .run_status import _run_status
|
|
231
|
+
return _run_status(argv[1:])
|
|
229
232
|
if argv and argv[0] == "init":
|
|
230
233
|
from .onboarding.init import run_init
|
|
231
234
|
return run_init(argv[1:])
|