agentic-dev-loop 0.12.1__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.
Files changed (147) hide show
  1. {agentic_dev_loop-0.12.1/src/agentic_dev_loop.egg-info → agentic_dev_loop-0.12.2}/PKG-INFO +34 -9
  2. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/README.md +32 -8
  3. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/pyproject.toml +4 -1
  4. agentic_dev_loop-0.12.2/src/agentic_dev_loop/core/failure.py +21 -0
  5. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/core/verdict.py +87 -0
  6. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/actions.py +6 -5
  7. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/agents.py +94 -2
  8. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/cli.py +3 -0
  9. agentic_dev_loop-0.12.2/src/agentic_dev_loop/engine/completion.py +48 -0
  10. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/dryrun.py +8 -0
  11. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/graph.py +11 -0
  12. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/llm.py +59 -1
  13. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/onboarding/templates/qa.agent.md +25 -18
  14. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/onboarding/templates/review.agent.md +11 -14
  15. agentic_dev_loop-0.12.2/src/agentic_dev_loop/engine/provider_wire.py +190 -0
  16. agentic_dev_loop-0.12.2/src/agentic_dev_loop/engine/run_status.py +360 -0
  17. agentic_dev_loop-0.12.2/src/agentic_dev_loop/engine/run_status_command.md +23 -0
  18. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/stages.py +174 -45
  19. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2/src/agentic_dev_loop.egg-info}/PKG-INFO +34 -9
  20. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop.egg-info/SOURCES.txt +7 -0
  21. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop.egg-info/requires.txt +1 -0
  22. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_033_brief_step.py +8 -4
  23. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_041_brief_coverage.py +2 -1
  24. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_055_cli.py +1 -1
  25. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_actions.py +3 -3
  26. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_agent_error_guide.py +4 -3
  27. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_dry_run.py +1 -1
  28. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_e2e_fixtures.py +15 -8
  29. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_graph.py +6 -4
  30. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_llm.py +2 -0
  31. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_qa_prompt.py +8 -6
  32. agentic_dev_loop-0.12.2/tests/test_run_status.py +283 -0
  33. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_sdk_compat.py +65 -0
  34. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_stages.py +57 -30
  35. agentic_dev_loop-0.12.2/tests/test_unit11.py +487 -0
  36. agentic_dev_loop-0.12.2/tests/test_unit11_pbt.py +125 -0
  37. agentic_dev_loop-0.12.1/src/agentic_dev_loop/engine/provider_wire.py +0 -38
  38. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/LICENSE +0 -0
  39. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/setup.cfg +0 -0
  40. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/__init__.py +0 -0
  41. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/core/__init__.py +0 -0
  42. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/core/config.py +0 -0
  43. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/core/paths.py +0 -0
  44. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/core/profiles/__init__.py +0 -0
  45. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/core/profiles/data/dotnet-node.yaml +0 -0
  46. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/core/profiles/data/python.yaml +0 -0
  47. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/core/results.py +0 -0
  48. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/__init__.py +0 -0
  49. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/agent_error_guide.md +0 -0
  50. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/agent_error_guide.py +0 -0
  51. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/allowlist.py +0 -0
  52. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/brief_coverage.py +0 -0
  53. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/brief_prompt.md +0 -0
  54. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/brief_prompt.py +0 -0
  55. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/containers.py +0 -0
  56. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/copilot_auth.py +0 -0
  57. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/deps.py +0 -0
  58. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/egress_policy.py +0 -0
  59. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/egress_proxy.py +0 -0
  60. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
  61. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
  62. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/forbidden.py +0 -0
  63. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/hostboundary.py +0 -0
  64. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/isolation.py +0 -0
  65. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/loaders.py +0 -0
  66. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/mounts.py +0 -0
  67. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/onboarding/__init__.py +0 -0
  68. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/onboarding/checks.py +0 -0
  69. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/onboarding/doctor.py +0 -0
  70. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/onboarding/init.py +0 -0
  71. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/onboarding/recipes/dotnet-node.yaml +0 -0
  72. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/onboarding/recipes/python.yaml +0 -0
  73. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/onboarding/recipes.py +0 -0
  74. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/onboarding/templates/dev.agent.md +0 -0
  75. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/onboarding/templates/env.example +0 -0
  76. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/onboarding/templates/trigger-agentic-dev-loop.sh +0 -0
  77. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/onboarding/templates.py +0 -0
  78. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/onboarding/upgrade.py +0 -0
  79. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/outputs.py +0 -0
  80. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/presets.py +0 -0
  81. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/proc.py +0 -0
  82. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/registry.py +0 -0
  83. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -0
  84. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
  85. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/secrets.py +0 -0
  86. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/step_workspace.py +0 -0
  87. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/engine/sync_back.py +0 -0
  88. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/py.typed +0 -0
  89. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/viz/__init__.py +0 -0
  90. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/viz/__main__.py +0 -0
  91. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/viz/layout.py +0 -0
  92. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/viz/page.py +0 -0
  93. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop/viz/server.py +0 -0
  94. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
  95. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
  96. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
  97. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_029_merge_on_conflict.py +0 -0
  98. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_035_finalize_nudge.py +0 -0
  99. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_036_banned_commands_and_lane_boundary.py +0 -0
  100. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_046_brief_prompt.py +0 -0
  101. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_055_doctor.py +0 -0
  102. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_055_init.py +0 -0
  103. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_055_profile_loading.py +0 -0
  104. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_055_profiles.py +0 -0
  105. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_055_recipes.py +0 -0
  106. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_055_schema_lock.py +0 -0
  107. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_055_templates.py +0 -0
  108. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_055_upgrade.py +0 -0
  109. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_agents.py +0 -0
  110. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_aidlc.py +0 -0
  111. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_build_viz_page.py +0 -0
  112. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_cli.py +0 -0
  113. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_config.py +0 -0
  114. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_containers.py +0 -0
  115. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_copilot_auth.py +0 -0
  116. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_copilot_cli.py +0 -0
  117. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_deps.py +0 -0
  118. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_egress_proxy.py +0 -0
  119. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_hostboundary.py +0 -0
  120. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_hostboundary_integration.py +0 -0
  121. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_isolation.py +0 -0
  122. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_isolation_integration.py +0 -0
  123. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_isolation_pbt.py +0 -0
  124. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_loaders.py +0 -0
  125. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_module_contract.py +0 -0
  126. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_outputs.py +0 -0
  127. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_outputs_pbt.py +0 -0
  128. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_proc.py +0 -0
  129. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_provider_wire.py +0 -0
  130. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_publish.py +0 -0
  131. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_registry.py +0 -0
  132. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_repo_ascii.py +0 -0
  133. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_results.py +0 -0
  134. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_secrets.py +0 -0
  135. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_secrets_integration.py +0 -0
  136. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_secrets_pipeline.py +0 -0
  137. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_source_resolution.py +0 -0
  138. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_step_workspace.py +0 -0
  139. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_sync_back.py +0 -0
  140. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_trigger_script.py +0 -0
  141. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_unit5_config.py +0 -0
  142. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_unit5_pbt.py +0 -0
  143. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_unit5_pure.py +0 -0
  144. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_verdict.py +0 -0
  145. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_viz.py +0 -0
  146. {agentic_dev_loop-0.12.1 → agentic_dev_loop-0.12.2}/tests/test_viz_demo.py +0 -0
  147. {agentic_dev_loop-0.12.1 → 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.1
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
@@ -165,6 +166,27 @@ reuses a pre-upload snapshot of the index for up to 10 minutes, reporting
165
166
  uv pip install --refresh "agentic-dev-loop==1.0.0a17"
166
167
  ```
167
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
+
168
190
  On the first run the engine pulls the **digest-pinned** dev image
169
191
  (`ghcr.io/lukedawilson/agentic-dev-loop/dev`) and verifies its digest - the
170
192
  supply-chain check refuses to continue on a mismatch. If you're running
@@ -219,8 +241,9 @@ flowchart LR
219
241
  Its edits are untracked; only declared `produces:` files are hygiene-tracked.
220
242
  2. **test** - a mechanical command step runs the suite inside the sandbox.
221
243
  Exit 0 moves on; anything else rewinds to `dev` with the failing output.
222
- 3. **review / qa** - verdict agents write artifacts whose `VERDICT:` lines route
223
- the graph. `fail` rewinds to `dev` and consumes an attempt.
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.
224
247
  4. **commit** - the single commit point fires only when all gates passed and
225
248
  the worktree was clean at start.
226
249
  5. **success** - committed; or **failure** when attempts are exhausted.
@@ -303,7 +326,7 @@ steps:
303
326
  | Key | Notes |
304
327
  | --- | --- |
305
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. |
306
- | `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
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. |
307
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. |
308
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. |
309
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). |
@@ -626,13 +649,15 @@ and the `commit` step never commits it). `init` writes them; the layout is:
626
649
 
627
650
  Each `sdk_agents/<name>.agent.md` is a markdown prompt plus YAML frontmatter:
628
651
  `name` matches a step's `agent:` value; `tools` lists the tool names the step
629
- may use (`terminal` always; a repo can add more via extensions, e.g.
630
- `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.
631
656
 
632
657
  ```markdown
633
658
  ---
634
659
  name: review # matches a step's `agent:` value
635
- tools: [terminal]
660
+ tools: [terminal, record_verdict]
636
661
  ---
637
662
  # Review Agent
638
663
  ## WORKFLOW # numbered steps
@@ -646,8 +671,8 @@ Three roles, three contracts:
646
671
  | Role | Verdict contract | Typical sandbox |
647
672
  | --- | --- | --- |
648
673
  | dev | none - the final response is captured as the `produces:` summary | rw mount, offline |
649
- | review | `VERDICT: APPROVED` / `VERDICT: NEEDS CHANGES` on the first content line | ro mount, offline |
650
- | qa | `VERDICT: PASS` / `VERDICT: 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) |
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) |
651
676
 
652
677
  The conventions that make a good agent file - each one below exists because
653
678
  an agent burned budget learning it mid-run:
@@ -121,6 +121,27 @@ reuses a pre-upload snapshot of the index for up to 10 minutes, reporting
121
121
  uv pip install --refresh "agentic-dev-loop==1.0.0a17"
122
122
  ```
123
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
+
124
145
  On the first run the engine pulls the **digest-pinned** dev image
125
146
  (`ghcr.io/lukedawilson/agentic-dev-loop/dev`) and verifies its digest - the
126
147
  supply-chain check refuses to continue on a mismatch. If you're running
@@ -175,8 +196,9 @@ flowchart LR
175
196
  Its edits are untracked; only declared `produces:` files are hygiene-tracked.
176
197
  2. **test** - a mechanical command step runs the suite inside the sandbox.
177
198
  Exit 0 moves on; anything else rewinds to `dev` with the failing output.
178
- 3. **review / qa** - verdict agents write artifacts whose `VERDICT:` lines route
179
- the graph. `fail` rewinds to `dev` and consumes an attempt.
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.
180
202
  4. **commit** - the single commit point fires only when all gates passed and
181
203
  the worktree was clean at start.
182
204
  5. **success** - committed; or **failure** when attempts are exhausted.
@@ -259,7 +281,7 @@ steps:
259
281
  | Key | Notes |
260
282
  | --- | --- |
261
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. |
262
- | `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
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. |
263
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. |
264
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. |
265
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). |
@@ -582,13 +604,15 @@ and the `commit` step never commits it). `init` writes them; the layout is:
582
604
 
583
605
  Each `sdk_agents/<name>.agent.md` is a markdown prompt plus YAML frontmatter:
584
606
  `name` matches a step's `agent:` value; `tools` lists the tool names the step
585
- may use (`terminal` always; a repo can add more via extensions, e.g.
586
- `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.
587
611
 
588
612
  ```markdown
589
613
  ---
590
614
  name: review # matches a step's `agent:` value
591
- tools: [terminal]
615
+ tools: [terminal, record_verdict]
592
616
  ---
593
617
  # Review Agent
594
618
  ## WORKFLOW # numbered steps
@@ -602,8 +626,8 @@ Three roles, three contracts:
602
626
  | Role | Verdict contract | Typical sandbox |
603
627
  | --- | --- | --- |
604
628
  | dev | none - the final response is captured as the `produces:` summary | rw mount, offline |
605
- | review | `VERDICT: APPROVED` / `VERDICT: NEEDS CHANGES` on the first content line | ro mount, offline |
606
- | qa | `VERDICT: PASS` / `VERDICT: 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) |
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) |
607
631
 
608
632
  The conventions that make a good agent file - each one below exists because
609
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.1"
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 ToolExecutor
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 load_agent(agents_dir: Path | str, name: str, llm: LLM) -> "Agent":
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:])
@@ -0,0 +1,48 @@
1
+ """Completion classification from the conversation event stream (unit-11).
2
+
3
+ Completion is a structural fact: an agent step completes only when it took at
4
+ least one verifiable action. The OpenHands SDK marks a content-bearing turn
5
+ with zero tool calls as FINISHED, which is how tool-less narration used to
6
+ read as a clean pass (planning-docs/059). This module reclassifies that shape
7
+ as ``FailureClass.NO_ACTIONS``; truncation semantics stay with ``stages.py``.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Iterable
13
+
14
+ from openhands.sdk.event.llm_convertible.action import ActionEvent
15
+
16
+ from agentic_dev_loop.core.failure import FailureClass
17
+
18
+
19
+ def count_actions(events: Iterable[object]) -> int:
20
+ """The number of tool calls in the event stream.
21
+
22
+ Each tool call emits exactly one ``ActionEvent`` (a ``FinishAction``
23
+ included: the SDK's done signal is itself a tool call)."""
24
+ return sum(1 for event in events if isinstance(event, ActionEvent))
25
+
26
+
27
+ def classify_completion(events: Iterable[object], *,
28
+ finished: bool) -> FailureClass | None:
29
+ """Classify a completed conversation, or return None when it took actions.
30
+
31
+ A FINISHED conversation with zero ActionEvents is NO_ACTIONS. A truncated
32
+ conversation keeps the existing truncation path: ``finished`` is False and
33
+ classification is left to the caller."""
34
+ events = list(events)
35
+ if count_actions(events) > 0:
36
+ return None
37
+ if not finished:
38
+ return None
39
+ return FailureClass.NO_ACTIONS
40
+
41
+
42
+ def completion_evidence(events: Iterable[object]) -> str:
43
+ """The classifier evidence recorded in a structural failure report."""
44
+ events = list(events)
45
+ actions = [e for e in events if isinstance(e, ActionEvent)]
46
+ tools = ", ".join(sorted({getattr(e, "tool_name", "?") for e in actions}))
47
+ return (f"event_count={len(events)} action_events={len(actions)} "
48
+ f"tool_calls=[{tools}]")
@@ -13,6 +13,8 @@ from __future__ import annotations
13
13
  from pathlib import Path
14
14
 
15
15
  from agentic_dev_loop.core.config import PipelineConfig, StepConfig, load_config
16
+ from agentic_dev_loop.core.verdict import RECORD_VERDICT_TOOL
17
+ from .agents import check_required_tools
16
18
  from .hostboundary import check_orchestrator_install, check_tooling_integrity
17
19
  from .loaders import resolve_docs
18
20
  from .isolation import (isolation_state_dir, resolve_effective_profile,
@@ -30,12 +32,18 @@ def _validate_agents(cfg: PipelineConfig, agents_dir: Path) -> None:
30
32
  The brief prompt is engine-embedded (046), so a native ``brief_inputs:``
31
33
  step (kind 'brief') has `agent is None` and is exempt from the agents_dir
32
34
  lookup - it never reads a vendored file."""
35
+ from openhands.sdk.subagent.schema import AgentDefinition
36
+
33
37
  for step in cfg.steps:
34
38
  if step.agent is None:
35
39
  continue
36
40
  path = agents_dir / f"{step.agent}.agent.md"
37
41
  if not path.is_file():
38
42
  raise FileNotFoundError(f"Agent file not found: {path}")
43
+ if step.verdicts is not None:
44
+ agent_def = AgentDefinition.load(path)
45
+ check_required_tools(agent_def.tools, (RECORD_VERDICT_TOOL,),
46
+ agent_name=step.agent)
39
47
 
40
48
 
41
49
  def _validate_actions(cfg: PipelineConfig, registry: Registry) -> None:
@@ -19,6 +19,7 @@ from langgraph.graph import END, START, StateGraph
19
19
 
20
20
  from agentic_dev_loop import viz
21
21
  from .actions import git_worktree_clean, kill_listeners_on_port
22
+ from .agents import validate_agent_contracts
22
23
  from .allowlist import resolve_engine_allowlist
23
24
  from agentic_dev_loop.core.config import load_config
24
25
  from .hostboundary import (check_orchestrator_install,
@@ -68,6 +69,9 @@ class LoopState(TypedDict):
68
69
  state_dir: str # resolved pipeline state dir (for the commit action)
69
70
  tooling_paths: list[str] # workspace-relative paths the commit never sweeps
70
71
  step_verdicts: dict[str, str | None] # per-step pass/fail/None(unclear)
72
+ failure_class: str | None # structural failure class (unit-11)
73
+ mechanical_notes: Annotated[list[str], operator.add] # command/action output
74
+ verified_state: Annotated[dict[str, str], _merge_artifacts] # engine-verified facts
71
75
  retry_target: str | None # where the bump node loops back to
72
76
  thread_id: str
73
77
  brief_hashes: Annotated[dict[str, str], _merge_artifacts] # step name -> input hash (033)
@@ -78,6 +82,7 @@ def build_graph(runtime: Runtime, checkpointer=None):
78
82
  """Compile the pipeline's state graph: one node per step (agent, command,
79
83
  or action), plus the bump retry node and the success terminal. Unknown
80
84
  action names fail here, at build time, not mid-run."""
85
+ validate_agent_contracts(runtime.cfg, runtime.agents_dir)
81
86
  known_actions = (set(runtime.registry.actions)
82
87
  | set(runtime.registry.sandbox_actions))
83
88
  for step in runtime.cfg.steps:
@@ -235,6 +240,9 @@ def _run_pipeline(cfg, base, registry, state_dir, max_attempts, args, viz_bus,
235
240
  "state_dir": str(state_dir),
236
241
  "tooling_paths": tooling_paths,
237
242
  "step_verdicts": {},
243
+ "failure_class": None,
244
+ "mechanical_notes": [],
245
+ "verified_state": {},
238
246
  "retry_target": None,
239
247
  "thread_id": thread_id,
240
248
  "brief_hashes": {},
@@ -362,6 +370,9 @@ def _run_pipeline(cfg, base, registry, state_dir, max_attempts, args, viz_bus,
362
370
  f"{final.get('failed_step', 'unknown')!r}; see "
363
371
  f"{state_dir}/last-error.md",
364
372
  flush=True)
373
+ if final.get("failure_class"):
374
+ print(f"[{final['subject']}] structural failure class: "
375
+ f"{final['failure_class']}", flush=True)
365
376
  else:
366
377
  print(f"[{final['subject']}] FAILED after {final['attempt']} "
367
378
  f"attempt(s).", flush=True)