agentic-dev-loop 1.0.0a38__tar.gz → 1.0.0a39__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.
Potentially problematic release.
This version of agentic-dev-loop might be problematic. Click here for more details.
- {agentic_dev_loop-1.0.0a38/src/agentic_dev_loop.egg-info → agentic_dev_loop-1.0.0a39}/PKG-INFO +13 -1
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/README.md +12 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/pyproject.toml +1 -1
- agentic_dev_loop-1.0.0a39/src/agentic_dev_loop/engine/brief_coverage.py +96 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/graph.py +7 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/stages.py +87 -12
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39/src/agentic_dev_loop.egg-info}/PKG-INFO +13 -1
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop.egg-info/SOURCES.txt +2 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_033_brief_step.py +26 -10
- agentic_dev_loop-1.0.0a39/tests/test_041_brief_coverage.py +254 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_e2e_fixtures.py +12 -1
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/LICENSE +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/setup.cfg +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/core/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/core/config.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/core/paths.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/core/results.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/core/verdict.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/actions.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/agent_error_guide.md +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/agent_error_guide.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/agents.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/allowlist.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/cli.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/deps.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/dryrun.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/egress_policy.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/egress_proxy.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/forbidden.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/hostboundary.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/isolation.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/llm.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/loaders.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/mounts.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/outputs.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/presets.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/proc.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/registry.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/secrets.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/step_workspace.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/sync_back.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/py.typed +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/viz/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/viz/__main__.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/viz/server.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_028_declared_caches.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_029_merge_on_conflict.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_032_stop_when_green.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_035_finalize_nudge.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_036_banned_commands_and_lane_boundary.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_actions.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_agent_error_guide.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_agents.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_cli.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_config.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_deps.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_dry_run.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_egress_proxy.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_graph.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_hostboundary.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_hostboundary_integration.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_isolation.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_isolation_integration.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_isolation_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_llm.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_loaders.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_module_contract.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_outputs.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_outputs_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_proc.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_publish.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_qa_prompt.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_registry.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_repo_ascii.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_results.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_sdk_compat.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_secrets.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_secrets_integration.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_secrets_pipeline.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_source_resolution.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_stages.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_step_workspace.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_sync_back.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_trigger_script.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_unit5_config.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_unit5_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_unit5_pure.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_verdict.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_viz.py +0 -0
{agentic_dev_loop-1.0.0a38/src/agentic_dev_loop.egg-info → agentic_dev_loop-1.0.0a39}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentic-dev-loop
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0a39
|
|
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
|
|
@@ -260,6 +260,18 @@ run start (or when its inputs change), its `produces:` artifact survives attempt
|
|
|
260
260
|
starts, and the dev/review agents read that digest instead of re-deriving repo
|
|
261
261
|
rules and environment facts every attempt.
|
|
262
262
|
|
|
263
|
+
The brief digest is mechanically coverage-checked: the four section headings
|
|
264
|
+
(`Repo rules digest`, `Unit scope and lane digest`, `Environment facts`, and
|
|
265
|
+
`Open risks`) must appear, every anchor extracted from the `include` files
|
|
266
|
+
(markdown `##` headings, and YAML category names plus `enforcement_levels`)
|
|
267
|
+
must appear on its own line (case-insensitive, whitespace-collapsed, `**`
|
|
268
|
+
decoration allowed), and every `env_facts` entry must appear verbatim. A miss
|
|
269
|
+
re-runs the brief agent once per missing set with a `## Coverage retry`
|
|
270
|
+
correction note, bounded to three retries per input set; if the same items are
|
|
271
|
+
still dropped after the cap, the run fails and `last-error.md` names the
|
|
272
|
+
persistent drops. Include files with no extractable anchors pass vacuously, and
|
|
273
|
+
unusual YAML rule shapes are under-checked, never over-checked.
|
|
274
|
+
|
|
263
275
|
### Secrets (`secrets:`) - refs only, never values
|
|
264
276
|
|
|
265
277
|
Secret values never belong in the pipeline yaml, the workspace, or `state_dir`.
|
|
@@ -218,6 +218,18 @@ run start (or when its inputs change), its `produces:` artifact survives attempt
|
|
|
218
218
|
starts, and the dev/review agents read that digest instead of re-deriving repo
|
|
219
219
|
rules and environment facts every attempt.
|
|
220
220
|
|
|
221
|
+
The brief digest is mechanically coverage-checked: the four section headings
|
|
222
|
+
(`Repo rules digest`, `Unit scope and lane digest`, `Environment facts`, and
|
|
223
|
+
`Open risks`) must appear, every anchor extracted from the `include` files
|
|
224
|
+
(markdown `##` headings, and YAML category names plus `enforcement_levels`)
|
|
225
|
+
must appear on its own line (case-insensitive, whitespace-collapsed, `**`
|
|
226
|
+
decoration allowed), and every `env_facts` entry must appear verbatim. A miss
|
|
227
|
+
re-runs the brief agent once per missing set with a `## Coverage retry`
|
|
228
|
+
correction note, bounded to three retries per input set; if the same items are
|
|
229
|
+
still dropped after the cap, the run fails and `last-error.md` names the
|
|
230
|
+
persistent drops. Include files with no extractable anchors pass vacuously, and
|
|
231
|
+
unusual YAML rule shapes are under-checked, never over-checked.
|
|
232
|
+
|
|
221
233
|
### Secrets (`secrets:`) - refs only, never values
|
|
222
234
|
|
|
223
235
|
Secret values never belong in the pipeline yaml, the workspace, or `state_dir`.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agentic-dev-loop"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.0a39"
|
|
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"
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Mechanical coverage gate for the brief step's section-anchored digest (041).
|
|
2
|
+
|
|
3
|
+
The brief agent's prompt contract (Part A) promises a structure-preserving
|
|
4
|
+
digest: every top-level section or category name survives on its own line.
|
|
5
|
+
Prompt compliance is not a guarantee, so this module derives the required
|
|
6
|
+
anchors from the include files themselves and checks the captured digest
|
|
7
|
+
against them. The brief node owns the retry budget; these functions stay pure
|
|
8
|
+
and file-agnostic.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
|
|
15
|
+
_REQUIRED_SECTIONS = (
|
|
16
|
+
"Repo rules digest",
|
|
17
|
+
"Unit scope and lane digest",
|
|
18
|
+
"Environment facts",
|
|
19
|
+
"Open risks",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _normalize(text: str) -> str:
|
|
24
|
+
"""Case-fold and collapse whitespace - the matching normal form."""
|
|
25
|
+
return " ".join(text.split()).casefold()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _normalize_line(line: str) -> str:
|
|
29
|
+
"""Normalize one digest line, stripping the decorations Part A permits.
|
|
30
|
+
|
|
31
|
+
A compliant digest writes anchors as headings on their own line, wrapped
|
|
32
|
+
in ``**`` and (for the four section headings) prefixed with ``##``. List
|
|
33
|
+
bullets are tolerated; the whole-line comparison is what makes a mid-line
|
|
34
|
+
prose mention fail."""
|
|
35
|
+
stripped = line.strip().replace("**", "")
|
|
36
|
+
stripped = re.sub(r"^#+\s*", "", stripped)
|
|
37
|
+
stripped = re.sub(r"^[-*+]\s+", "", stripped)
|
|
38
|
+
return _normalize(stripped)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def required_sections() -> list[str]:
|
|
42
|
+
"""The four template headings the brief must emit."""
|
|
43
|
+
return list(_REQUIRED_SECTIONS)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def extract_anchors(content: str) -> list[str]:
|
|
47
|
+
"""Derive the mechanical anchor set from one include file.
|
|
48
|
+
|
|
49
|
+
Markdown contributes every ``## `` heading. YAML contributes each
|
|
50
|
+
``- name:`` value under a ``categories:`` block plus the
|
|
51
|
+
``enforcement_levels`` key. Anchors are normalized once, at extraction,
|
|
52
|
+
and de-duplicated in first-seen order."""
|
|
53
|
+
anchors: list[str] = []
|
|
54
|
+
in_categories = False
|
|
55
|
+
for raw in content.splitlines():
|
|
56
|
+
line = raw.rstrip()
|
|
57
|
+
stripped = line.strip()
|
|
58
|
+
if not stripped:
|
|
59
|
+
continue
|
|
60
|
+
if stripped == "enforcement_levels:":
|
|
61
|
+
anchors.append(_normalize("enforcement_levels"))
|
|
62
|
+
if stripped == "categories:":
|
|
63
|
+
in_categories = True
|
|
64
|
+
continue
|
|
65
|
+
if in_categories:
|
|
66
|
+
if not line[:1].isspace():
|
|
67
|
+
in_categories = False
|
|
68
|
+
else:
|
|
69
|
+
match = re.match(r"-\s*name:\s*(.+)$", stripped)
|
|
70
|
+
if match:
|
|
71
|
+
anchors.append(_normalize(match.group(1).strip()))
|
|
72
|
+
if line.startswith("## "):
|
|
73
|
+
anchors.append(_normalize(line[3:].strip()))
|
|
74
|
+
return list(dict.fromkeys(anchors))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def missing_anchors(brief_text: str, anchors: list[str],
|
|
78
|
+
env_facts: list[str]) -> list[str]:
|
|
79
|
+
"""Return every required item the captured brief fails to cover.
|
|
80
|
+
|
|
81
|
+
Required section headings and anchors are matched line-anchored
|
|
82
|
+
(case-insensitive, whitespace-collapsed, ``**`` decoration stripped). Env
|
|
83
|
+
facts are matched verbatim, exactly as handed to the brief agent. An empty
|
|
84
|
+
list means the digest passes coverage."""
|
|
85
|
+
missing: list[str] = []
|
|
86
|
+
lines = {_normalize_line(line) for line in brief_text.splitlines()}
|
|
87
|
+
for section in _REQUIRED_SECTIONS:
|
|
88
|
+
if _normalize(section) not in lines:
|
|
89
|
+
missing.append(section)
|
|
90
|
+
for anchor in anchors:
|
|
91
|
+
if anchor not in lines:
|
|
92
|
+
missing.append(anchor)
|
|
93
|
+
for fact in env_facts:
|
|
94
|
+
if fact not in brief_text:
|
|
95
|
+
missing.append(fact)
|
|
96
|
+
return missing
|
{agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/graph.py
RENAMED
|
@@ -43,6 +43,11 @@ def _merge_artifacts(a: dict[str, str], b: dict[str, str]) -> dict[str, str]:
|
|
|
43
43
|
return {**a, **b}
|
|
44
44
|
|
|
45
45
|
|
|
46
|
+
def _merge_counts(a: dict[str, int], b: dict[str, int]) -> dict[str, int]:
|
|
47
|
+
"""Accumulate the per-step brief coverage retry counts (041)."""
|
|
48
|
+
return {**a, **b}
|
|
49
|
+
|
|
50
|
+
|
|
46
51
|
class LoopState(TypedDict):
|
|
47
52
|
"""State threaded through the graph. `notes` accumulates across attempts
|
|
48
53
|
(append reducer), `artifacts` accumulates across stages (merge reducer);
|
|
@@ -65,6 +70,7 @@ class LoopState(TypedDict):
|
|
|
65
70
|
retry_target: str | None # where the bump node loops back to
|
|
66
71
|
thread_id: str
|
|
67
72
|
brief_hashes: Annotated[dict[str, str], _merge_artifacts] # step name -> input hash (033)
|
|
73
|
+
brief_retries: Annotated[dict[str, int], _merge_counts] # step name -> coverage retries used (041)
|
|
68
74
|
|
|
69
75
|
|
|
70
76
|
def build_graph(runtime: Runtime, checkpointer=None):
|
|
@@ -215,6 +221,7 @@ def _run_pipeline(cfg, base, registry, state_dir, max_attempts, args, viz_bus,
|
|
|
215
221
|
"retry_target": None,
|
|
216
222
|
"thread_id": thread_id,
|
|
217
223
|
"brief_hashes": {},
|
|
224
|
+
"brief_retries": {},
|
|
218
225
|
}
|
|
219
226
|
bus = viz_bus
|
|
220
227
|
with SqliteSaver.from_conn_string(db) as saver:
|
{agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/stages.py
RENAMED
|
@@ -34,6 +34,7 @@ from openhands.sdk import Conversation
|
|
|
34
34
|
|
|
35
35
|
from .agent_error_guide import guide_section
|
|
36
36
|
from .agents import load_agent, register_sandbox_terminal
|
|
37
|
+
from .brief_coverage import extract_anchors, missing_anchors
|
|
37
38
|
from agentic_dev_loop.core.config import TERMINALS, PipelineConfig, Route, StepConfig, route_target
|
|
38
39
|
from agentic_dev_loop.core.paths import _has_wildcards
|
|
39
40
|
from .deps import extension_message, parse_missing_dependencies, policy_injection
|
|
@@ -57,6 +58,7 @@ if TYPE_CHECKING:
|
|
|
57
58
|
DEFAULT_MAX_ITERATIONS = 500
|
|
58
59
|
DEFAULT_COMMAND_TIMEOUT = 600.0 # seconds - a pytest-class suite budget
|
|
59
60
|
LAST_ERROR = "last-error.md"
|
|
61
|
+
COVERAGE_RETRY_CAP = 3 # per-run brief coverage re-runs before the run fails (041)
|
|
60
62
|
ARTIFACT_CONTEXT_CAP = 24_000 # chars per artifact injected into a step's context
|
|
61
63
|
CUSTOM_PROMPT_HEADING = "Run-specific instructions"
|
|
62
64
|
|
|
@@ -554,6 +556,23 @@ def resolve_brief_inputs(runtime: Runtime, step: StepConfig
|
|
|
554
556
|
return included, list(step.brief_inputs.env_facts)
|
|
555
557
|
|
|
556
558
|
|
|
559
|
+
def _brief_anchors(runtime: Runtime, step: StepConfig) -> list[str]:
|
|
560
|
+
"""The mechanical anchor set for a brief step (041), derived from the
|
|
561
|
+
resolved ``brief_inputs.include`` files only - plan docs reach dev/review
|
|
562
|
+
verbatim through the loader channel, so their sections need no anchor
|
|
563
|
+
enforcement. Duplicate headings across files merge into one anchor."""
|
|
564
|
+
included, _ = resolve_brief_inputs(runtime, step)
|
|
565
|
+
anchors: list[str] = []
|
|
566
|
+
for content in included.values():
|
|
567
|
+
anchors.extend(extract_anchors(content))
|
|
568
|
+
return list(dict.fromkeys(anchors))
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def _coverage_note_text(missing: list[str]) -> str:
|
|
572
|
+
"""The missing-items list for the ``## Coverage retry`` section (041)."""
|
|
573
|
+
return "\n".join(f"- {item}" for item in missing)
|
|
574
|
+
|
|
575
|
+
|
|
557
576
|
def validate_brief_inputs(workspace: Path, step: StepConfig) -> None:
|
|
558
577
|
"""Fail fast on ``brief_inputs.include`` entries that name no files (033).
|
|
559
578
|
|
|
@@ -620,13 +639,16 @@ def brief_inputs_hash(runtime: Runtime, step: StepConfig,
|
|
|
620
639
|
|
|
621
640
|
def build_brief_context(runtime: Runtime, step: StepConfig,
|
|
622
641
|
docs: dict[str, str],
|
|
623
|
-
doc_sources: dict[str, str] | None = None
|
|
642
|
+
doc_sources: dict[str, str] | None = None,
|
|
643
|
+
coverage_note: str | None = None) -> str:
|
|
624
644
|
"""Assemble the brief agent's task message (033): the always-on
|
|
625
645
|
agent-facing error guide (034), the resolved unit/plan docs (re-read from
|
|
626
646
|
disk where sources are known), the closed ``brief_inputs.include`` file
|
|
627
647
|
set, the run-specific instructions appended from ``custom_prompt`` (040),
|
|
628
|
-
and the env-facts block.
|
|
629
|
-
|
|
648
|
+
and the env-facts block. A ``coverage_note`` (041) appends a correction
|
|
649
|
+
request against the digest contract - it is not prior verdicts or
|
|
650
|
+
feedback, so the 033 rule is unaffected. Prior verdicts and feedback are
|
|
651
|
+
deliberately absent - the notes channel delivers them per attempt."""
|
|
630
652
|
sections, env_facts = _brief_sections(runtime, step, docs, doc_sources)
|
|
631
653
|
parts = [guide_section()]
|
|
632
654
|
parts += [f"## {name}\n\n{content}" for name, content in sections.items()]
|
|
@@ -635,6 +657,13 @@ def build_brief_context(runtime: Runtime, step: StepConfig,
|
|
|
635
657
|
if env_facts:
|
|
636
658
|
parts.append("## Environment facts\n\n"
|
|
637
659
|
+ "\n".join(f"- {fact}" for fact in env_facts))
|
|
660
|
+
if coverage_note is not None:
|
|
661
|
+
parts.append(
|
|
662
|
+
"## Coverage retry\n\n"
|
|
663
|
+
"The captured brief is missing the following required items. "
|
|
664
|
+
"Regenerate the digest so every listed item appears on its own "
|
|
665
|
+
"line, verbatim where the contract requires verbatim:\n\n"
|
|
666
|
+
+ coverage_note)
|
|
638
667
|
return "\n\n".join(parts)
|
|
639
668
|
|
|
640
669
|
|
|
@@ -789,7 +818,13 @@ def make_brief_node(runtime: Runtime, step: StepConfig):
|
|
|
789
818
|
LLM call is skipped entirely. A changed include file or env fact re-runs
|
|
790
819
|
the step and refreshes the hash. Prior verdicts and feedback never enter
|
|
791
820
|
the brief message - they already reach dev/review through the notes
|
|
792
|
-
channel.
|
|
821
|
+
channel.
|
|
822
|
+
|
|
823
|
+
The 041 coverage gate runs after every successful capture AND on the reuse
|
|
824
|
+
path: the four section headings, the include-file anchors, and the env
|
|
825
|
+
facts must all survive. A miss consumes one per-run retry unit (cap 3) and
|
|
826
|
+
re-runs the agent with a ``## Coverage retry`` correction note. Exhausting
|
|
827
|
+
the budget writes last-error.md and fails the run."""
|
|
793
828
|
captures = step.artifact is not None or bool(step.produces)
|
|
794
829
|
|
|
795
830
|
def node(state: dict) -> dict:
|
|
@@ -800,14 +835,9 @@ def make_brief_node(runtime: Runtime, step: StepConfig):
|
|
|
800
835
|
try:
|
|
801
836
|
current_hash = brief_inputs_hash(runtime, step, state["docs"],
|
|
802
837
|
state.get("doc_sources"))
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
flush=True)
|
|
807
|
-
merged = {**state["step_verdicts"], step.name: "pass"}
|
|
808
|
-
return {"step_verdicts": merged, "failed": False}
|
|
809
|
-
message = build_brief_context(runtime, step, state["docs"],
|
|
810
|
-
state.get("doc_sources"))
|
|
838
|
+
anchors = _brief_anchors(runtime, step)
|
|
839
|
+
env_facts = (list(step.brief_inputs.env_facts)
|
|
840
|
+
if step.brief_inputs is not None else [])
|
|
811
841
|
except Exception:
|
|
812
842
|
err = redact_values(traceback.format_exc(), runtime.secrets.values())
|
|
813
843
|
print(err[-4000:], file=sys.stderr, flush=True)
|
|
@@ -817,6 +847,24 @@ def make_brief_node(runtime: Runtime, step: StepConfig):
|
|
|
817
847
|
encoding="utf-8")
|
|
818
848
|
return {"failed": True, "failed_step": step.name}
|
|
819
849
|
|
|
850
|
+
existing = state.get("artifacts", {}).get(step.name)
|
|
851
|
+
reused = (existing is not None
|
|
852
|
+
and state.get("brief_hashes", {}).get(step.name) == current_hash)
|
|
853
|
+
|
|
854
|
+
if reused and not missing_anchors(existing, anchors, env_facts):
|
|
855
|
+
print(f"[{subject}] {step.name} brief inputs unchanged - reusing.",
|
|
856
|
+
flush=True)
|
|
857
|
+
merged = {**state["step_verdicts"], step.name: "pass"}
|
|
858
|
+
return {"step_verdicts": merged, "failed": False}
|
|
859
|
+
|
|
860
|
+
coverage_note = None
|
|
861
|
+
if reused:
|
|
862
|
+
coverage_note = _coverage_note_text(
|
|
863
|
+
missing_anchors(existing, anchors, env_facts))
|
|
864
|
+
message = build_brief_context(runtime, step, state["docs"],
|
|
865
|
+
state.get("doc_sources"),
|
|
866
|
+
coverage_note=coverage_note)
|
|
867
|
+
|
|
820
868
|
print(f"[{subject}] Running {step.agent} agent...", flush=True)
|
|
821
869
|
result = runtime.run_agent(step.agent, message,
|
|
822
870
|
max_iterations=max_iterations, step=step)
|
|
@@ -834,11 +882,38 @@ def make_brief_node(runtime: Runtime, step: StepConfig):
|
|
|
834
882
|
result.truncated, max_iterations)],
|
|
835
883
|
"retry_target": _unclear_target(runtime, step)}
|
|
836
884
|
|
|
885
|
+
missing = missing_anchors(result.response or "", anchors, env_facts)
|
|
886
|
+
if missing:
|
|
887
|
+
prev_hash = state.get("brief_hashes", {}).get(step.name)
|
|
888
|
+
retries = (0 if prev_hash != current_hash
|
|
889
|
+
else state.get("brief_retries", {}).get(step.name, 0))
|
|
890
|
+
if retries >= COVERAGE_RETRY_CAP:
|
|
891
|
+
dropped = "\n".join(f"- {item}" for item in missing)
|
|
892
|
+
runtime.state_dir.mkdir(parents=True, exist_ok=True)
|
|
893
|
+
(runtime.state_dir / LAST_ERROR).write_text(
|
|
894
|
+
f"# Brief coverage failed\n\n"
|
|
895
|
+
f"The brief step repeatedly dropped the following "
|
|
896
|
+
f"required items:\n\n{dropped}\n",
|
|
897
|
+
encoding="utf-8")
|
|
898
|
+
print(f"[{subject}] {step.name} brief coverage retries exhausted - "
|
|
899
|
+
f"persistent drops: {', '.join(missing)}", flush=True)
|
|
900
|
+
return {"failed": True, "failed_step": step.name}
|
|
901
|
+
print(f"[{subject}] {step.name} brief coverage miss on attempt "
|
|
902
|
+
f"{attempt}, retrying.", flush=True)
|
|
903
|
+
merged = {**state["step_verdicts"], step.name: None}
|
|
904
|
+
update = {"step_verdicts": merged, "retry_target": step.name}
|
|
905
|
+
if captures and result.response is not None:
|
|
906
|
+
update["artifacts"] = {step.name: result.response}
|
|
907
|
+
update["brief_hashes"] = {step.name: current_hash}
|
|
908
|
+
update["brief_retries"] = {step.name: retries + 1}
|
|
909
|
+
return update
|
|
910
|
+
|
|
837
911
|
merged = {**state["step_verdicts"], step.name: "pass"}
|
|
838
912
|
update = {"step_verdicts": merged, "failed": False}
|
|
839
913
|
if captures and result.response is not None:
|
|
840
914
|
update["artifacts"] = {step.name: result.response}
|
|
841
915
|
update["brief_hashes"] = {step.name: current_hash}
|
|
916
|
+
update["brief_retries"] = {step.name: 0}
|
|
842
917
|
return update
|
|
843
918
|
|
|
844
919
|
return node
|
{agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39/src/agentic_dev_loop.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentic-dev-loop
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0a39
|
|
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
|
|
@@ -260,6 +260,18 @@ run start (or when its inputs change), its `produces:` artifact survives attempt
|
|
|
260
260
|
starts, and the dev/review agents read that digest instead of re-deriving repo
|
|
261
261
|
rules and environment facts every attempt.
|
|
262
262
|
|
|
263
|
+
The brief digest is mechanically coverage-checked: the four section headings
|
|
264
|
+
(`Repo rules digest`, `Unit scope and lane digest`, `Environment facts`, and
|
|
265
|
+
`Open risks`) must appear, every anchor extracted from the `include` files
|
|
266
|
+
(markdown `##` headings, and YAML category names plus `enforcement_levels`)
|
|
267
|
+
must appear on its own line (case-insensitive, whitespace-collapsed, `**`
|
|
268
|
+
decoration allowed), and every `env_facts` entry must appear verbatim. A miss
|
|
269
|
+
re-runs the brief agent once per missing set with a `## Coverage retry`
|
|
270
|
+
correction note, bounded to three retries per input set; if the same items are
|
|
271
|
+
still dropped after the cap, the run fails and `last-error.md` names the
|
|
272
|
+
persistent drops. Include files with no extractable anchors pass vacuously, and
|
|
273
|
+
unusual YAML rule shapes are under-checked, never over-checked.
|
|
274
|
+
|
|
263
275
|
### Secrets (`secrets:`) - refs only, never values
|
|
264
276
|
|
|
265
277
|
Secret values never belong in the pipeline yaml, the workspace, or `state_dir`.
|
{agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop.egg-info/SOURCES.txt
RENAMED
|
@@ -20,6 +20,7 @@ src/agentic_dev_loop/engine/agent_error_guide.md
|
|
|
20
20
|
src/agentic_dev_loop/engine/agent_error_guide.py
|
|
21
21
|
src/agentic_dev_loop/engine/agents.py
|
|
22
22
|
src/agentic_dev_loop/engine/allowlist.py
|
|
23
|
+
src/agentic_dev_loop/engine/brief_coverage.py
|
|
23
24
|
src/agentic_dev_loop/engine/cli.py
|
|
24
25
|
src/agentic_dev_loop/engine/deps.py
|
|
25
26
|
src/agentic_dev_loop/engine/dryrun.py
|
|
@@ -53,6 +54,7 @@ tests/test_032_stop_when_green.py
|
|
|
53
54
|
tests/test_033_brief_step.py
|
|
54
55
|
tests/test_035_finalize_nudge.py
|
|
55
56
|
tests/test_036_banned_commands_and_lane_boundary.py
|
|
57
|
+
tests/test_041_brief_coverage.py
|
|
56
58
|
tests/test_actions.py
|
|
57
59
|
tests/test_agent_error_guide.py
|
|
58
60
|
tests/test_agents.py
|
|
@@ -42,6 +42,15 @@ def _load(tmp_path, data):
|
|
|
42
42
|
return load_config(p)
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
BRIEF_OK = (
|
|
46
|
+
"## Repo rules digest\n\nREPO RULES\n\n"
|
|
47
|
+
"## Unit scope and lane digest\n\nSCOPE\n\n"
|
|
48
|
+
"## Environment facts\n\n- offline; never fetch\n\n"
|
|
49
|
+
"## Open risks\n\nNone\n"
|
|
50
|
+
)
|
|
51
|
+
BRIEF_OK_NEW = BRIEF_OK.replace("SCOPE", "SCOPE NEW")
|
|
52
|
+
|
|
53
|
+
|
|
45
54
|
BRIEF_STEP = {
|
|
46
55
|
"name": "brief",
|
|
47
56
|
"agent": "brief",
|
|
@@ -303,11 +312,11 @@ class TestBriefInputsHash:
|
|
|
303
312
|
|
|
304
313
|
class TestBriefNode:
|
|
305
314
|
def test_first_run_calls_agent_and_stores_hash(self, runtime, fake_agents):
|
|
306
|
-
fake_agents.set("brief", [("write", "attempt-brief.md",
|
|
315
|
+
fake_agents.set("brief", [("write", "attempt-brief.md", BRIEF_OK)])
|
|
307
316
|
update = make_brief_node(runtime, runtime.cfg.steps[0])(make_state())
|
|
308
317
|
assert update["step_verdicts"] == {"brief": "pass"}
|
|
309
318
|
assert update["failed"] is False
|
|
310
|
-
assert update["artifacts"] == {"brief":
|
|
319
|
+
assert update["artifacts"] == {"brief": BRIEF_OK}
|
|
311
320
|
assert update["brief_hashes"] == {
|
|
312
321
|
"brief": brief_inputs_hash(runtime, runtime.cfg.steps[0],
|
|
313
322
|
make_state()["docs"])}
|
|
@@ -316,7 +325,7 @@ class TestBriefNode:
|
|
|
316
325
|
assert "## Environment facts\n\n- offline; never fetch" in msg
|
|
317
326
|
|
|
318
327
|
def test_unchanged_inputs_skip_llm(self, runtime, fake_agents):
|
|
319
|
-
fake_agents.set("brief", [("write", "attempt-brief.md",
|
|
328
|
+
fake_agents.set("brief", [("write", "attempt-brief.md", BRIEF_OK)])
|
|
320
329
|
node = make_brief_node(runtime, runtime.cfg.steps[0])
|
|
321
330
|
first = node(make_state())
|
|
322
331
|
second = node(make_state(artifacts=first["artifacts"],
|
|
@@ -326,15 +335,15 @@ class TestBriefNode:
|
|
|
326
335
|
|
|
327
336
|
def test_changed_inputs_rerun(self, runtime, fake_agents):
|
|
328
337
|
fake_agents.set("brief", [
|
|
329
|
-
("write", "attempt-brief.md",
|
|
330
|
-
("write", "attempt-brief.md",
|
|
338
|
+
("write", "attempt-brief.md", BRIEF_OK),
|
|
339
|
+
("write", "attempt-brief.md", BRIEF_OK_NEW),
|
|
331
340
|
])
|
|
332
341
|
node = make_brief_node(runtime, runtime.cfg.steps[0])
|
|
333
342
|
first = node(make_state())
|
|
334
343
|
(runtime.workspace / "AGENTS.md").write_text("EDITED")
|
|
335
344
|
second = node(make_state(artifacts=first["artifacts"],
|
|
336
345
|
brief_hashes=first["brief_hashes"]))
|
|
337
|
-
assert second["artifacts"] == {"brief":
|
|
346
|
+
assert second["artifacts"] == {"brief": BRIEF_OK_NEW}
|
|
338
347
|
assert fake_agents.agents_called() == ["brief", "brief"]
|
|
339
348
|
|
|
340
349
|
def test_loader_doc_disk_edit_reruns_brief(self, runtime, fake_agents,
|
|
@@ -342,8 +351,8 @@ class TestBriefNode:
|
|
|
342
351
|
plan = tmp_path / "plan.md"
|
|
343
352
|
plan.write_text("PLAN V1")
|
|
344
353
|
fake_agents.set("brief", [
|
|
345
|
-
("write", "attempt-brief.md",
|
|
346
|
-
("write", "attempt-brief.md",
|
|
354
|
+
("write", "attempt-brief.md", BRIEF_OK),
|
|
355
|
+
("write", "attempt-brief.md", BRIEF_OK_NEW),
|
|
347
356
|
])
|
|
348
357
|
node = make_brief_node(runtime, runtime.cfg.steps[0])
|
|
349
358
|
first = node(make_state(docs={"plan.md": "PLAN V1"},
|
|
@@ -354,7 +363,7 @@ class TestBriefNode:
|
|
|
354
363
|
doc_sources={"plan.md": str(plan)},
|
|
355
364
|
artifacts=first["artifacts"],
|
|
356
365
|
brief_hashes=first["brief_hashes"]))
|
|
357
|
-
assert second["artifacts"] == {"brief":
|
|
366
|
+
assert second["artifacts"] == {"brief": BRIEF_OK_NEW}
|
|
358
367
|
assert fake_agents.agents_called() == ["brief", "brief"]
|
|
359
368
|
|
|
360
369
|
def test_empty_response_is_unclear(self, runtime, fake_agents):
|
|
@@ -569,7 +578,14 @@ class TestBriefPipeline:
|
|
|
569
578
|
monkeypatch.chdir(repo)
|
|
570
579
|
monkeypatch.setenv("TEST_LLM_KEY", "sk-test")
|
|
571
580
|
|
|
572
|
-
|
|
581
|
+
brief_digest = (
|
|
582
|
+
"## Repo rules digest\n\nREPO RULES\n\n"
|
|
583
|
+
"## Unit scope and lane digest\n\nBRIEF DIGEST\n\n"
|
|
584
|
+
"## Environment facts\n\n"
|
|
585
|
+
"- the workspace is an export with no .git; verify with find/ls, never git status\n\n"
|
|
586
|
+
"## Open risks\n\nNone\n"
|
|
587
|
+
)
|
|
588
|
+
fake_agents.set("brief", [("write", "attempt-brief.md", brief_digest)])
|
|
573
589
|
fake_agents.set("dev", [("write", "implementation-summary.md", "did the work")])
|
|
574
590
|
|
|
575
591
|
ok, attempts = run_pipeline("pipeline.yaml", _args())
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""Brief coverage gate (041): anchor extraction and the coverage check over
|
|
2
|
+
the captured brief, plus the brief node's bounded retry and failure path."""
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from openhands.sdk import LLM
|
|
9
|
+
|
|
10
|
+
from agentic_dev_loop.core.config import PipelineConfig
|
|
11
|
+
from agentic_dev_loop.engine.brief_coverage import (
|
|
12
|
+
extract_anchors,
|
|
13
|
+
missing_anchors,
|
|
14
|
+
required_sections,
|
|
15
|
+
)
|
|
16
|
+
from agentic_dev_loop.engine.registry import default_registry
|
|
17
|
+
from agentic_dev_loop.engine.stages import (
|
|
18
|
+
Runtime,
|
|
19
|
+
brief_inputs_hash,
|
|
20
|
+
make_brief_node,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
24
|
+
AGENTS_MD = (ROOT / "AGENTS.md").read_text(encoding="utf-8")
|
|
25
|
+
STANDARDS_YAML = (ROOT / "coding-standards-ddd.yaml").read_text(encoding="utf-8")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _brief_ok() -> str:
|
|
29
|
+
return (
|
|
30
|
+
"## Repo rules digest\n\n"
|
|
31
|
+
"**Error handling**\nNever fall back silently.\n\n"
|
|
32
|
+
"**Mock policy**\nNo mocks unless unavoidable.\n\n"
|
|
33
|
+
"## Unit scope and lane digest\n\nunit scope\n\n"
|
|
34
|
+
"## Environment facts\n\n- offline; never fetch\n\n"
|
|
35
|
+
"## Open risks\n\nNone\n"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _brief_missing_mock() -> str:
|
|
40
|
+
return _brief_ok().replace("**Mock policy**\nNo mocks unless unavoidable.\n\n", "")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _merge_state(state, update):
|
|
44
|
+
merged = dict(state)
|
|
45
|
+
for key, value in update.items():
|
|
46
|
+
if key in ("artifacts", "brief_hashes", "brief_retries"):
|
|
47
|
+
merged[key] = {**state.get(key, {}), **value}
|
|
48
|
+
else:
|
|
49
|
+
merged[key] = value
|
|
50
|
+
return merged
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _make_cfg(steps):
|
|
54
|
+
return PipelineConfig.model_validate({
|
|
55
|
+
"name": "t",
|
|
56
|
+
"llm": {"model": "m", "api_key_env": "K"},
|
|
57
|
+
"agents_dir": "agents",
|
|
58
|
+
"lockfiles": ["uv.lock"],
|
|
59
|
+
"steps": steps,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _make_state(**over):
|
|
64
|
+
state = {"subject": "unit u", "docs": {"plan.md": "THE PLAN"},
|
|
65
|
+
"artifacts": {}, "attempt": 1, "max_attempts": 10, "notes": [],
|
|
66
|
+
"failed": False, "step_verdicts": {}, "retry_target": None,
|
|
67
|
+
"thread_id": "t1", "brief_hashes": {}, "brief_retries": {}}
|
|
68
|
+
state.update(over)
|
|
69
|
+
return state
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@pytest.fixture
|
|
73
|
+
def runtime(tmp_path, fake_sandbox):
|
|
74
|
+
state_dir = tmp_path / ".pr"
|
|
75
|
+
state_dir.mkdir()
|
|
76
|
+
(tmp_path / "AGENTS.md").write_text(
|
|
77
|
+
"## Error handling\n\nNever fall back silently.\n\n"
|
|
78
|
+
"## Mock policy\n\nNo mocks unless unavoidable.\n")
|
|
79
|
+
steps = [
|
|
80
|
+
{"name": "brief", "agent": "brief", "produces": "attempt-brief.md",
|
|
81
|
+
"max_iterations": 12,
|
|
82
|
+
"brief_inputs": {"include": ["AGENTS.md"],
|
|
83
|
+
"env_facts": ["offline; never fetch"]},
|
|
84
|
+
"on_pass": "dev"},
|
|
85
|
+
{"name": "dev", "agent": "dev", "produces": "implementation-summary.md",
|
|
86
|
+
"on_pass": "success"},
|
|
87
|
+
]
|
|
88
|
+
return Runtime(cfg=_make_cfg(steps), registry=default_registry(),
|
|
89
|
+
llm=LLM(model="openai/test-model", api_key="sk-test"),
|
|
90
|
+
agents_dir=tmp_path / "agents", state_dir=state_dir,
|
|
91
|
+
workspace=tmp_path, sandbox=fake_sandbox)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class TestExtractAnchors:
|
|
95
|
+
def test_required_sections_are_the_four_template_headings(self):
|
|
96
|
+
assert required_sections() == [
|
|
97
|
+
"Repo rules digest",
|
|
98
|
+
"Unit scope and lane digest",
|
|
99
|
+
"Environment facts",
|
|
100
|
+
"Open risks",
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
def test_markdown_headings_from_real_agents_md(self):
|
|
104
|
+
anchors = extract_anchors(AGENTS_MD)
|
|
105
|
+
assert anchors == [
|
|
106
|
+
"default development mode - ask before coding",
|
|
107
|
+
"slash commands",
|
|
108
|
+
"github urls",
|
|
109
|
+
"file search",
|
|
110
|
+
"group session mode",
|
|
111
|
+
"error handling",
|
|
112
|
+
"em dash ban",
|
|
113
|
+
"consumer name ban",
|
|
114
|
+
"debugging",
|
|
115
|
+
"reverse engineering undocumented systems",
|
|
116
|
+
"development workflow",
|
|
117
|
+
"backward compatibility",
|
|
118
|
+
"mock policy",
|
|
119
|
+
"sdk compat shims",
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
def test_yaml_categories_and_enforcement_levels_from_real_file(self):
|
|
123
|
+
anchors = extract_anchors(STANDARDS_YAML)
|
|
124
|
+
assert anchors == [
|
|
125
|
+
"enforcement_levels",
|
|
126
|
+
"general_principles",
|
|
127
|
+
"architecture_and_boundaries",
|
|
128
|
+
"domain_and_persistence",
|
|
129
|
+
"error_handling",
|
|
130
|
+
"coding_style",
|
|
131
|
+
"services_are_exceptions",
|
|
132
|
+
"stack_conventions",
|
|
133
|
+
"forbidden_patterns",
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
def test_plain_file_has_no_anchors(self):
|
|
137
|
+
assert extract_anchors("just prose\nno headings here\n") == []
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class TestMissingAnchors:
|
|
141
|
+
def test_all_present_passes(self):
|
|
142
|
+
assert missing_anchors(_brief_ok(),
|
|
143
|
+
["error handling", "mock policy"],
|
|
144
|
+
["offline; never fetch"]) == []
|
|
145
|
+
|
|
146
|
+
def test_missing_anchor_is_named(self):
|
|
147
|
+
assert "mock policy" in missing_anchors(
|
|
148
|
+
_brief_missing_mock(), ["error handling", "mock policy"],
|
|
149
|
+
["offline; never fetch"])
|
|
150
|
+
|
|
151
|
+
def test_missing_section_heading_is_named(self):
|
|
152
|
+
text = _brief_ok().replace("## Open risks\n\nNone\n", "")
|
|
153
|
+
assert "Open risks" in missing_anchors(
|
|
154
|
+
text, ["error handling", "mock policy"], ["offline; never fetch"])
|
|
155
|
+
|
|
156
|
+
def test_altered_env_fact_is_named(self):
|
|
157
|
+
text = _brief_ok().replace("offline; never fetch", "offline maybe")
|
|
158
|
+
assert "offline; never fetch" in missing_anchors(
|
|
159
|
+
text, ["error handling", "mock policy"], ["offline; never fetch"])
|
|
160
|
+
|
|
161
|
+
def test_mid_line_mention_does_not_satisfy(self):
|
|
162
|
+
text = _brief_ok().replace(
|
|
163
|
+
"**Error handling**\nNever fall back silently.\n\n",
|
|
164
|
+
"See error handling for the rules.\n\n")
|
|
165
|
+
assert "error handling" in missing_anchors(
|
|
166
|
+
text, ["error handling", "mock policy"], ["offline; never fetch"])
|
|
167
|
+
|
|
168
|
+
def test_case_and_whitespace_variants_pass(self):
|
|
169
|
+
text = _brief_ok().replace(
|
|
170
|
+
"**Error handling**", "** ERROR HANDLING **")
|
|
171
|
+
assert missing_anchors(text, ["error handling", "mock policy"],
|
|
172
|
+
["offline; never fetch"]) == []
|
|
173
|
+
|
|
174
|
+
def test_bold_wrapped_heading_passes(self):
|
|
175
|
+
assert missing_anchors(_brief_ok(), ["error handling", "mock policy"],
|
|
176
|
+
["offline; never fetch"]) == []
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class TestBriefNodeCoverage:
|
|
180
|
+
def test_miss_returns_retry_note_and_reruns(self, runtime, fake_agents):
|
|
181
|
+
good = _brief_ok()
|
|
182
|
+
bad = _brief_missing_mock()
|
|
183
|
+
fake_agents.set("brief", [
|
|
184
|
+
("write", "attempt-brief.md", bad),
|
|
185
|
+
("write", "attempt-brief.md", good),
|
|
186
|
+
])
|
|
187
|
+
node = make_brief_node(runtime, runtime.cfg.steps[0])
|
|
188
|
+
state = _make_state()
|
|
189
|
+
|
|
190
|
+
first = node(state)
|
|
191
|
+
assert first["step_verdicts"] == {"brief": None}
|
|
192
|
+
assert first["retry_target"] == "brief"
|
|
193
|
+
assert first["brief_retries"] == {"brief": 1}
|
|
194
|
+
assert first["artifacts"] == {"brief": bad}
|
|
195
|
+
assert first["brief_hashes"] == {
|
|
196
|
+
"brief": brief_inputs_hash(runtime, runtime.cfg.steps[0],
|
|
197
|
+
state["docs"])}
|
|
198
|
+
|
|
199
|
+
second = node(_merge_state(state, first))
|
|
200
|
+
assert second["step_verdicts"] == {"brief": "pass"}
|
|
201
|
+
assert second["failed"] is False
|
|
202
|
+
assert second["artifacts"] == {"brief": good}
|
|
203
|
+
assert second["brief_retries"] == {"brief": 0}
|
|
204
|
+
assert fake_agents.agents_called() == ["brief", "brief"]
|
|
205
|
+
messages = fake_agents.messages_for("brief")
|
|
206
|
+
assert "## Coverage retry" in messages[1]
|
|
207
|
+
assert "mock policy" in messages[1]
|
|
208
|
+
|
|
209
|
+
def test_cap_exhausted_writes_last_error_and_fails(self, runtime,
|
|
210
|
+
fake_agents):
|
|
211
|
+
bad = _brief_missing_mock()
|
|
212
|
+
fake_agents.set("brief", [("write", "attempt-brief.md", bad)])
|
|
213
|
+
node = make_brief_node(runtime, runtime.cfg.steps[0])
|
|
214
|
+
state = _make_state()
|
|
215
|
+
|
|
216
|
+
for _ in range(3):
|
|
217
|
+
update = node(state)
|
|
218
|
+
assert update.get("failed") is not True
|
|
219
|
+
state = _merge_state(state, update)
|
|
220
|
+
|
|
221
|
+
update = node(state)
|
|
222
|
+
assert update["failed"] is True
|
|
223
|
+
assert update["failed_step"] == "brief"
|
|
224
|
+
assert fake_agents.agents_called() == ["brief", "brief", "brief", "brief"]
|
|
225
|
+
error = (runtime.state_dir / "last-error.md").read_text(encoding="utf-8")
|
|
226
|
+
assert "Brief coverage failed" in error
|
|
227
|
+
assert "mock policy" in error
|
|
228
|
+
|
|
229
|
+
def test_reused_digest_failing_coverage_regenerates(self, runtime,
|
|
230
|
+
fake_agents):
|
|
231
|
+
good = _brief_ok()
|
|
232
|
+
bad = _brief_missing_mock()
|
|
233
|
+
fake_agents.set("brief", [("write", "attempt-brief.md", good)])
|
|
234
|
+
node = make_brief_node(runtime, runtime.cfg.steps[0])
|
|
235
|
+
state = _make_state(artifacts={"brief": bad},
|
|
236
|
+
brief_hashes={"brief": brief_inputs_hash(
|
|
237
|
+
runtime, runtime.cfg.steps[0],
|
|
238
|
+
_make_state()["docs"])})
|
|
239
|
+
update = node(state)
|
|
240
|
+
assert update["artifacts"] == {"brief": good}
|
|
241
|
+
assert fake_agents.agents_called() == ["brief"]
|
|
242
|
+
(message,) = fake_agents.messages_for("brief")
|
|
243
|
+
assert "## Coverage retry" in message
|
|
244
|
+
|
|
245
|
+
def test_passing_reused_digest_skips_llm(self, runtime, fake_agents):
|
|
246
|
+
good = _brief_ok()
|
|
247
|
+
node = make_brief_node(runtime, runtime.cfg.steps[0])
|
|
248
|
+
state = _make_state(artifacts={"brief": good},
|
|
249
|
+
brief_hashes={"brief": brief_inputs_hash(
|
|
250
|
+
runtime, runtime.cfg.steps[0],
|
|
251
|
+
_make_state()["docs"])})
|
|
252
|
+
update = node(state)
|
|
253
|
+
assert update == {"step_verdicts": {"brief": "pass"}, "failed": False}
|
|
254
|
+
assert fake_agents.agents_called() == []
|
|
@@ -62,9 +62,20 @@ def _script_test(*behaviors):
|
|
|
62
62
|
_COMMAND_QUEUE[:] = list(behaviors)
|
|
63
63
|
|
|
64
64
|
|
|
65
|
+
_BRIEF_DIGEST = (
|
|
66
|
+
"## Repo rules digest\n\n# AGENTS.md\n# coding-standards-ddd.yaml\n\n"
|
|
67
|
+
"## Unit scope and lane digest\n\nplan scope\n\n"
|
|
68
|
+
"## Environment facts\n\n"
|
|
69
|
+
"- the workspace is an export with no .git; verify with find/ls, never git status\n"
|
|
70
|
+
"- the sandbox is network-off by default; never run package-manager fetches\n"
|
|
71
|
+
"- Python packages resolve from .venv-sandbox; run .venv-sandbox/bin/python\n\n"
|
|
72
|
+
"## Open risks\n\nNone\n"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
65
76
|
def _passing_scripts(fake_agents, **overrides):
|
|
66
77
|
scripts = {
|
|
67
|
-
"brief": [("write", "attempt-brief.md",
|
|
78
|
+
"brief": [("write", "attempt-brief.md", _BRIEF_DIGEST)],
|
|
68
79
|
"dev": [("write", "implementation-summary.md", "Implemented the fix.\n")],
|
|
69
80
|
"review": [("write", "review-findings.md",
|
|
70
81
|
"Looks good.\n\nVERDICT: APPROVED\n")],
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/core/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/core/results.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/core/verdict.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/__init__.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/actions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/agents.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/allowlist.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/dryrun.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/egress_policy.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/egress_proxy.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/forbidden.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/hostboundary.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/isolation.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/loaders.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/mounts.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/outputs.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/presets.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/registry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/secrets.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/sync_back.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/viz/__init__.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/viz/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop.egg-info/requires.txt
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a39}/tests/test_hostboundary_integration.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|