agentic-dev-loop 1.0.0a38__tar.gz → 1.0.0a40__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.0a40}/PKG-INFO +13 -1
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/README.md +12 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/pyproject.toml +1 -1
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/core/config.py +4 -1
- agentic_dev_loop-1.0.0a40/src/agentic_dev_loop/engine/brief_coverage.py +96 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/engine/graph.py +7 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/engine/stages.py +112 -18
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/viz/server.py +17 -6
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40/src/agentic_dev_loop.egg-info}/PKG-INFO +13 -1
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop.egg-info/SOURCES.txt +2 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_033_brief_step.py +26 -10
- agentic_dev_loop-1.0.0a40/tests/test_041_brief_coverage.py +254 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_config.py +37 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_e2e_fixtures.py +12 -1
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_qa_prompt.py +16 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_stages.py +87 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_verdict.py +24 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_viz.py +20 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/LICENSE +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/setup.cfg +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/core/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/core/paths.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/core/results.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/core/verdict.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/engine/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/engine/actions.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/engine/agent_error_guide.md +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/engine/agent_error_guide.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/engine/agents.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/engine/allowlist.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/engine/cli.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/engine/deps.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/engine/dryrun.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/engine/egress_policy.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/engine/egress_proxy.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/engine/forbidden.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/engine/hostboundary.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/engine/isolation.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/engine/llm.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/engine/loaders.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/engine/mounts.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/engine/outputs.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/engine/presets.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/engine/proc.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/engine/registry.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/engine/secrets.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/engine/step_workspace.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/engine/sync_back.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/py.typed +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/viz/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop/viz/__main__.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_028_declared_caches.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_029_merge_on_conflict.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_032_stop_when_green.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_035_finalize_nudge.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_036_banned_commands_and_lane_boundary.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_actions.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_agent_error_guide.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_agents.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_cli.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_deps.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_dry_run.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_egress_proxy.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_graph.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_hostboundary.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_hostboundary_integration.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_isolation.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_isolation_integration.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_isolation_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_llm.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_loaders.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_module_contract.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_outputs.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_outputs_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_proc.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_publish.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_registry.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_repo_ascii.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_results.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_sdk_compat.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_secrets.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_secrets_integration.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_secrets_pipeline.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_source_resolution.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_step_workspace.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_sync_back.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_trigger_script.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_unit5_config.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_unit5_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40}/tests/test_unit5_pure.py +0 -0
{agentic_dev_loop-1.0.0a38/src/agentic_dev_loop.egg-info → agentic_dev_loop-1.0.0a40}/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.0a40
|
|
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.0a40"
|
|
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"
|
|
@@ -148,6 +148,7 @@ class VerdictSpec(BaseModel):
|
|
|
148
148
|
pass_: str = Field(alias="pass")
|
|
149
149
|
fail: str = Field(alias="fail")
|
|
150
150
|
unverifiable: str | None = None
|
|
151
|
+
env: str | None = None
|
|
151
152
|
|
|
152
153
|
|
|
153
154
|
class Route(BaseModel):
|
|
@@ -404,6 +405,7 @@ class StepConfig(BaseModel):
|
|
|
404
405
|
on_pass: RouteTarget | None = None
|
|
405
406
|
on_fail: RouteTarget | None = None
|
|
406
407
|
on_unverifiable: RouteTarget | None = None
|
|
408
|
+
on_env: RouteTarget | None = None
|
|
407
409
|
|
|
408
410
|
@property
|
|
409
411
|
def kind(self) -> str:
|
|
@@ -794,7 +796,8 @@ class PipelineConfig(BaseModel):
|
|
|
794
796
|
raise ValueError(f"step name {n!r} collides with a reserved terminal")
|
|
795
797
|
valid = set(names) | TERMINALS
|
|
796
798
|
for step in self.steps:
|
|
797
|
-
for route in (step.on_pass, step.on_fail, step.on_unverifiable
|
|
799
|
+
for route in (step.on_pass, step.on_fail, step.on_unverifiable,
|
|
800
|
+
step.on_env):
|
|
798
801
|
target = route_target(route)
|
|
799
802
|
if target is not None and target not in valid:
|
|
800
803
|
raise ValueError(
|
|
@@ -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.0a40}/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.0a40}/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
|
|
|
@@ -654,13 +683,13 @@ def _unclear_note(runtime: Runtime, step: StepConfig, attempt: int,
|
|
|
654
683
|
captured response when it exists, so real findings are never swallowed."""
|
|
655
684
|
expected = ""
|
|
656
685
|
if step.verdicts is not None:
|
|
686
|
+
keywords = [f"`VERDICT: {step.verdicts.pass_}`",
|
|
687
|
+
f"`VERDICT: {step.verdicts.fail}`"]
|
|
657
688
|
if step.verdicts.unverifiable is not None:
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
expected = (f" (expected `VERDICT: {step.verdicts.pass_}` or "
|
|
663
|
-
f"`VERDICT: {step.verdicts.fail}` on its own line)")
|
|
689
|
+
keywords.append(f"`VERDICT: {step.verdicts.unverifiable}`")
|
|
690
|
+
if step.verdicts.env is not None:
|
|
691
|
+
keywords.append(f"`VERDICT: {step.verdicts.env}`")
|
|
692
|
+
expected = f" (expected {' or '.join(keywords)} on its own line)"
|
|
664
693
|
if truncated:
|
|
665
694
|
lead = (f"{step.name} agent was cut short on attempt {attempt} "
|
|
666
695
|
f"(hit the {max_iterations}-turn limit or was stopped "
|
|
@@ -689,6 +718,8 @@ def make_agent_node(runtime: Runtime, step: StepConfig):
|
|
|
689
718
|
verdicts_map = {step.verdicts.pass_: "pass", step.verdicts.fail: "fail"}
|
|
690
719
|
if step.verdicts.unverifiable is not None:
|
|
691
720
|
verdicts_map[step.verdicts.unverifiable] = "unverifiable"
|
|
721
|
+
if step.verdicts.env is not None:
|
|
722
|
+
verdicts_map[step.verdicts.env] = "env"
|
|
692
723
|
captures = step.artifact is not None or bool(step.produces)
|
|
693
724
|
|
|
694
725
|
def node(state: dict) -> dict:
|
|
@@ -758,6 +789,21 @@ def make_agent_node(runtime: Runtime, step: StepConfig):
|
|
|
758
789
|
if captures and result.response is not None:
|
|
759
790
|
update["artifacts"] = {step.name: result.response}
|
|
760
791
|
return update
|
|
792
|
+
if verdict == "env":
|
|
793
|
+
print(f"[{subject}] {step.name} verdict: ENV "
|
|
794
|
+
f"(environmental, halting).", flush=True)
|
|
795
|
+
if step.on_env is None:
|
|
796
|
+
runtime.state_dir.mkdir(parents=True, exist_ok=True)
|
|
797
|
+
(runtime.state_dir / LAST_ERROR).write_text(
|
|
798
|
+
f"# Environmental failure (verdict: ENV)\n\n"
|
|
799
|
+
f"{result.response or 'no captured response'}\n",
|
|
800
|
+
encoding="utf-8")
|
|
801
|
+
return {"step_verdicts": merged,
|
|
802
|
+
"failed": True, "failed_step": step.name}
|
|
803
|
+
update = {"step_verdicts": merged, "failed": False}
|
|
804
|
+
if captures and result.response is not None:
|
|
805
|
+
update["artifacts"] = {step.name: result.response}
|
|
806
|
+
return update
|
|
761
807
|
print(f"[{subject}] {step.name} verdict: fail on attempt {attempt}.",
|
|
762
808
|
flush=True)
|
|
763
809
|
update = {"step_verdicts": merged,
|
|
@@ -789,7 +835,13 @@ def make_brief_node(runtime: Runtime, step: StepConfig):
|
|
|
789
835
|
LLM call is skipped entirely. A changed include file or env fact re-runs
|
|
790
836
|
the step and refreshes the hash. Prior verdicts and feedback never enter
|
|
791
837
|
the brief message - they already reach dev/review through the notes
|
|
792
|
-
channel.
|
|
838
|
+
channel.
|
|
839
|
+
|
|
840
|
+
The 041 coverage gate runs after every successful capture AND on the reuse
|
|
841
|
+
path: the four section headings, the include-file anchors, and the env
|
|
842
|
+
facts must all survive. A miss consumes one per-run retry unit (cap 3) and
|
|
843
|
+
re-runs the agent with a ``## Coverage retry`` correction note. Exhausting
|
|
844
|
+
the budget writes last-error.md and fails the run."""
|
|
793
845
|
captures = step.artifact is not None or bool(step.produces)
|
|
794
846
|
|
|
795
847
|
def node(state: dict) -> dict:
|
|
@@ -800,14 +852,9 @@ def make_brief_node(runtime: Runtime, step: StepConfig):
|
|
|
800
852
|
try:
|
|
801
853
|
current_hash = brief_inputs_hash(runtime, step, state["docs"],
|
|
802
854
|
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"))
|
|
855
|
+
anchors = _brief_anchors(runtime, step)
|
|
856
|
+
env_facts = (list(step.brief_inputs.env_facts)
|
|
857
|
+
if step.brief_inputs is not None else [])
|
|
811
858
|
except Exception:
|
|
812
859
|
err = redact_values(traceback.format_exc(), runtime.secrets.values())
|
|
813
860
|
print(err[-4000:], file=sys.stderr, flush=True)
|
|
@@ -817,6 +864,24 @@ def make_brief_node(runtime: Runtime, step: StepConfig):
|
|
|
817
864
|
encoding="utf-8")
|
|
818
865
|
return {"failed": True, "failed_step": step.name}
|
|
819
866
|
|
|
867
|
+
existing = state.get("artifacts", {}).get(step.name)
|
|
868
|
+
reused = (existing is not None
|
|
869
|
+
and state.get("brief_hashes", {}).get(step.name) == current_hash)
|
|
870
|
+
|
|
871
|
+
if reused and not missing_anchors(existing, anchors, env_facts):
|
|
872
|
+
print(f"[{subject}] {step.name} brief inputs unchanged - reusing.",
|
|
873
|
+
flush=True)
|
|
874
|
+
merged = {**state["step_verdicts"], step.name: "pass"}
|
|
875
|
+
return {"step_verdicts": merged, "failed": False}
|
|
876
|
+
|
|
877
|
+
coverage_note = None
|
|
878
|
+
if reused:
|
|
879
|
+
coverage_note = _coverage_note_text(
|
|
880
|
+
missing_anchors(existing, anchors, env_facts))
|
|
881
|
+
message = build_brief_context(runtime, step, state["docs"],
|
|
882
|
+
state.get("doc_sources"),
|
|
883
|
+
coverage_note=coverage_note)
|
|
884
|
+
|
|
820
885
|
print(f"[{subject}] Running {step.agent} agent...", flush=True)
|
|
821
886
|
result = runtime.run_agent(step.agent, message,
|
|
822
887
|
max_iterations=max_iterations, step=step)
|
|
@@ -834,11 +899,38 @@ def make_brief_node(runtime: Runtime, step: StepConfig):
|
|
|
834
899
|
result.truncated, max_iterations)],
|
|
835
900
|
"retry_target": _unclear_target(runtime, step)}
|
|
836
901
|
|
|
902
|
+
missing = missing_anchors(result.response or "", anchors, env_facts)
|
|
903
|
+
if missing:
|
|
904
|
+
prev_hash = state.get("brief_hashes", {}).get(step.name)
|
|
905
|
+
retries = (0 if prev_hash != current_hash
|
|
906
|
+
else state.get("brief_retries", {}).get(step.name, 0))
|
|
907
|
+
if retries >= COVERAGE_RETRY_CAP:
|
|
908
|
+
dropped = "\n".join(f"- {item}" for item in missing)
|
|
909
|
+
runtime.state_dir.mkdir(parents=True, exist_ok=True)
|
|
910
|
+
(runtime.state_dir / LAST_ERROR).write_text(
|
|
911
|
+
f"# Brief coverage failed\n\n"
|
|
912
|
+
f"The brief step repeatedly dropped the following "
|
|
913
|
+
f"required items:\n\n{dropped}\n",
|
|
914
|
+
encoding="utf-8")
|
|
915
|
+
print(f"[{subject}] {step.name} brief coverage retries exhausted - "
|
|
916
|
+
f"persistent drops: {', '.join(missing)}", flush=True)
|
|
917
|
+
return {"failed": True, "failed_step": step.name}
|
|
918
|
+
print(f"[{subject}] {step.name} brief coverage miss on attempt "
|
|
919
|
+
f"{attempt}, retrying.", flush=True)
|
|
920
|
+
merged = {**state["step_verdicts"], step.name: None}
|
|
921
|
+
update = {"step_verdicts": merged, "retry_target": step.name}
|
|
922
|
+
if captures and result.response is not None:
|
|
923
|
+
update["artifacts"] = {step.name: result.response}
|
|
924
|
+
update["brief_hashes"] = {step.name: current_hash}
|
|
925
|
+
update["brief_retries"] = {step.name: retries + 1}
|
|
926
|
+
return update
|
|
927
|
+
|
|
837
928
|
merged = {**state["step_verdicts"], step.name: "pass"}
|
|
838
929
|
update = {"step_verdicts": merged, "failed": False}
|
|
839
930
|
if captures and result.response is not None:
|
|
840
931
|
update["artifacts"] = {step.name: result.response}
|
|
841
932
|
update["brief_hashes"] = {step.name: current_hash}
|
|
933
|
+
update["brief_retries"] = {step.name: 0}
|
|
842
934
|
return update
|
|
843
935
|
|
|
844
936
|
return node
|
|
@@ -1040,6 +1132,8 @@ def make_router(runtime: Runtime, step: StepConfig):
|
|
|
1040
1132
|
if step.on_unverifiable is not None:
|
|
1041
1133
|
return resolve(step.on_unverifiable)
|
|
1042
1134
|
return resolve(step.on_pass)
|
|
1135
|
+
if verdict == "env":
|
|
1136
|
+
return resolve(step.on_env) if step.on_env is not None else END
|
|
1043
1137
|
if verdict == "fail":
|
|
1044
1138
|
if step.on_fail is None:
|
|
1045
1139
|
return END
|
|
@@ -546,6 +546,7 @@ PAGE = """<!doctype html>
|
|
|
546
546
|
var verdictFail = new Set();
|
|
547
547
|
var verdictPass = new Set();
|
|
548
548
|
var verdictUnverifiable = new Set();
|
|
549
|
+
var verdictEnv = new Set();
|
|
549
550
|
var fatalNodes = new Set();
|
|
550
551
|
var maxAttempts = null;
|
|
551
552
|
var attempt = null;
|
|
@@ -614,9 +615,15 @@ PAGE = """<!doctype html>
|
|
|
614
615
|
if (verdictFail.has(n)) { return "fail"; }
|
|
615
616
|
if (verdictPass.has(n)) { return "pass"; }
|
|
616
617
|
if (verdictUnverifiable.has(n)) { return "unverifiable"; }
|
|
618
|
+
if (verdictEnv.has(n)) { return "env"; }
|
|
617
619
|
return nodeStatus[n] || "pending";
|
|
618
620
|
}
|
|
619
621
|
|
|
622
|
+
function renderStatusOf(n) {
|
|
623
|
+
var s = statusOf(n);
|
|
624
|
+
return s === "env" ? "fatal" : s;
|
|
625
|
+
}
|
|
626
|
+
|
|
620
627
|
function updateBadges() {
|
|
621
628
|
var box = el("verdicts");
|
|
622
629
|
box.innerHTML = "";
|
|
@@ -627,6 +634,7 @@ PAGE = """<!doctype html>
|
|
|
627
634
|
if (v === "pass") { b.className += " badge-pass"; }
|
|
628
635
|
else if (v === "fail") { b.className += " badge-fail"; }
|
|
629
636
|
else if (v === "unverifiable") { b.className += " badge-unverifiable"; }
|
|
637
|
+
else if (v === "env") { b.className += " badge-fail"; }
|
|
630
638
|
else { b.className += " badge-unclear"; }
|
|
631
639
|
b.textContent = step + ": " + (v === null || v === undefined ? "no verdict" : v);
|
|
632
640
|
box.appendChild(b);
|
|
@@ -638,6 +646,7 @@ PAGE = """<!doctype html>
|
|
|
638
646
|
verdictPass.clear();
|
|
639
647
|
verdictFail.clear();
|
|
640
648
|
verdictUnverifiable.clear();
|
|
649
|
+
verdictEnv.clear();
|
|
641
650
|
currentVerdicts = {};
|
|
642
651
|
nodeStatus = {};
|
|
643
652
|
stageWord = {};
|
|
@@ -665,7 +674,7 @@ PAGE = """<!doctype html>
|
|
|
665
674
|
var classLines = Object.keys(nodeStatus).filter(function (n) {
|
|
666
675
|
return nodeIds.has(n);
|
|
667
676
|
}).map(function (n) {
|
|
668
|
-
return "class " + n + " " +
|
|
677
|
+
return "class " + n + " " + renderStatusOf(n) + ";";
|
|
669
678
|
});
|
|
670
679
|
var src = mermaidSource + "\\n" + classDefs.join("\\n") + "\\n" + classLines.join("\\n");
|
|
671
680
|
mermaid.render("graph-" + (renderCount++), src).then(function (res) {
|
|
@@ -756,9 +765,10 @@ PAGE = """<!doctype html>
|
|
|
756
765
|
if (v === "pass") { suffix = " (verdict: PASS)"; }
|
|
757
766
|
else if (v === "fail") { suffix = " (verdict: FAIL)"; }
|
|
758
767
|
else if (v === "unverifiable") { suffix = " (verdict: UNVERIFIABLE)"; }
|
|
768
|
+
else if (v === "env") { suffix = " (verdict: ENV)"; }
|
|
759
769
|
}
|
|
760
770
|
status.textContent = (word ? " " + word : "") + suffix;
|
|
761
|
-
header.className = "stage-header status-" +
|
|
771
|
+
header.className = "stage-header status-" + renderStatusOf(node);
|
|
762
772
|
}
|
|
763
773
|
|
|
764
774
|
function setStageStatus(node, word) {
|
|
@@ -1072,10 +1082,11 @@ PAGE = """<!doctype html>
|
|
|
1072
1082
|
Object.keys(ev.step_verdicts).forEach(function (k) {
|
|
1073
1083
|
verdictSteps.add(k);
|
|
1074
1084
|
var v = ev.step_verdicts[k];
|
|
1075
|
-
if (v === "pass") { verdictPass.add(k); verdictFail.delete(k); verdictUnverifiable.delete(k); }
|
|
1076
|
-
else if (v === "fail") { verdictFail.add(k); verdictPass.delete(k); verdictUnverifiable.delete(k); }
|
|
1077
|
-
else if (v === "unverifiable") { verdictUnverifiable.add(k); verdictPass.delete(k); verdictFail.delete(k); }
|
|
1078
|
-
else { verdictPass.delete(k); verdictFail.delete(k); verdictUnverifiable.delete(k); }
|
|
1085
|
+
if (v === "pass") { verdictPass.add(k); verdictFail.delete(k); verdictUnverifiable.delete(k); verdictEnv.delete(k); }
|
|
1086
|
+
else if (v === "fail") { verdictFail.add(k); verdictPass.delete(k); verdictUnverifiable.delete(k); verdictEnv.delete(k); }
|
|
1087
|
+
else if (v === "unverifiable") { verdictUnverifiable.add(k); verdictPass.delete(k); verdictFail.delete(k); verdictEnv.delete(k); }
|
|
1088
|
+
else if (v === "env") { verdictEnv.add(k); verdictPass.delete(k); verdictFail.delete(k); verdictUnverifiable.delete(k); }
|
|
1089
|
+
else { verdictPass.delete(k); verdictFail.delete(k); verdictUnverifiable.delete(k); verdictEnv.delete(k); }
|
|
1079
1090
|
currentVerdicts[k] = v;
|
|
1080
1091
|
});
|
|
1081
1092
|
updateBadges();
|
{agentic_dev_loop-1.0.0a38 → agentic_dev_loop-1.0.0a40/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.0a40
|
|
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.0a40}/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())
|