agentic-dev-loop 1.0.0a37__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.0a37/src/agentic_dev_loop.egg-info → agentic_dev_loop-1.0.0a39}/PKG-INFO +13 -1
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/README.md +12 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/pyproject.toml +1 -1
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/core/config.py +41 -1
- agentic_dev_loop-1.0.0a39/src/agentic_dev_loop/engine/brief_coverage.py +96 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/graph.py +7 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/stages.py +108 -18
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39/src/agentic_dev_loop.egg-info}/PKG-INFO +13 -1
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop.egg-info/SOURCES.txt +2 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_033_brief_step.py +38 -10
- agentic_dev_loop-1.0.0a39/tests/test_041_brief_coverage.py +254 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_config.py +60 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_e2e_fixtures.py +12 -1
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_stages.py +24 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/LICENSE +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/setup.cfg +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/core/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/core/paths.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/core/results.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/core/verdict.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/actions.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/agent_error_guide.md +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/agent_error_guide.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/agents.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/allowlist.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/cli.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/deps.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/dryrun.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/egress_policy.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/egress_proxy.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/forbidden.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/hostboundary.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/isolation.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/llm.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/loaders.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/mounts.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/outputs.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/presets.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/proc.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/registry.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/secrets.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/step_workspace.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/engine/sync_back.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/py.typed +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/viz/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/viz/__main__.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop/viz/server.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_028_declared_caches.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_029_merge_on_conflict.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_032_stop_when_green.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_035_finalize_nudge.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_036_banned_commands_and_lane_boundary.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_actions.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_agent_error_guide.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_agents.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_cli.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_deps.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_dry_run.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_egress_proxy.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_graph.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_hostboundary.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_hostboundary_integration.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_isolation.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_isolation_integration.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_isolation_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_llm.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_loaders.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_module_contract.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_outputs.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_outputs_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_proc.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_publish.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_qa_prompt.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_registry.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_repo_ascii.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_results.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_sdk_compat.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_secrets.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_secrets_integration.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_secrets_pipeline.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_source_resolution.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_step_workspace.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_sync_back.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_trigger_script.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_unit5_config.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_unit5_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_unit5_pure.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_verdict.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a39}/tests/test_viz.py +0 -0
{agentic_dev_loop-1.0.0a37/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"
|
|
@@ -389,6 +389,7 @@ class StepConfig(BaseModel):
|
|
|
389
389
|
stop_when: StopWhenSpec | None = None # agent steps only (032)
|
|
390
390
|
finalize_nudge_turns: int | None = Field(default=None, gt=0) # agent steps only (035)
|
|
391
391
|
brief_inputs: BriefInputsSpec | None = None # agent steps only (033)
|
|
392
|
+
custom_prompt: str | None = None # agent steps only (040); file path resolved at load
|
|
392
393
|
env: dict[str, str | None] | None = None # NFR-1: null = inherit host value
|
|
393
394
|
setup: str | None = None # BR-13: command steps only
|
|
394
395
|
forbidden_commands: list[str] = [] # banned-command regex deny-list (036 D1)
|
|
@@ -441,6 +442,13 @@ class StepConfig(BaseModel):
|
|
|
441
442
|
f"regex: {e}") from e
|
|
442
443
|
return v
|
|
443
444
|
|
|
445
|
+
@field_validator("custom_prompt")
|
|
446
|
+
@classmethod
|
|
447
|
+
def _nonempty_custom_prompt(cls, v: str | None) -> str | None:
|
|
448
|
+
if v is not None and not v.strip():
|
|
449
|
+
raise ValueError("custom_prompt must be a non-empty string")
|
|
450
|
+
return v
|
|
451
|
+
|
|
444
452
|
@model_validator(mode="after")
|
|
445
453
|
def _check_shape(self) -> "StepConfig":
|
|
446
454
|
for key in ("agent", "action", "command"):
|
|
@@ -501,6 +509,10 @@ class StepConfig(BaseModel):
|
|
|
501
509
|
raise ValueError(
|
|
502
510
|
f"step {self.name!r}: 'brief_inputs' requires 'agent'"
|
|
503
511
|
)
|
|
512
|
+
if self.custom_prompt is not None and self.agent is None:
|
|
513
|
+
raise ValueError(
|
|
514
|
+
f"step {self.name!r}: 'custom_prompt' requires 'agent'"
|
|
515
|
+
)
|
|
504
516
|
if (self.brief_inputs is not None
|
|
505
517
|
and (self.artifact is not None or self.verdicts is not None)):
|
|
506
518
|
raise ValueError(
|
|
@@ -797,6 +809,28 @@ class PipelineConfig(BaseModel):
|
|
|
797
809
|
return self
|
|
798
810
|
|
|
799
811
|
|
|
812
|
+
def _custom_prompt_looks_like_path(value: str) -> bool:
|
|
813
|
+
"""040/D2: a value counts as path-shaped when it carries a path
|
|
814
|
+
separator or a file extension (a dot). A path-shaped value that names no
|
|
815
|
+
existing file is a hard error, never silent text."""
|
|
816
|
+
return "/" in value or "\\" in value or Path(value).suffix != ""
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
def resolve_custom_prompt(value: str, pipeline_dir: Path, step_name: str) -> str:
|
|
820
|
+
"""Resolve a ``custom_prompt`` value (040/D2): an existing file
|
|
821
|
+
(relative to the pipeline's directory, or absolute) is read as the
|
|
822
|
+
prompt; any other value is literal text. A path-shaped value that names
|
|
823
|
+
no file fails loud, naming the path."""
|
|
824
|
+
candidate = Path(value)
|
|
825
|
+
path = candidate if candidate.is_absolute() else pipeline_dir / candidate
|
|
826
|
+
if path.is_file():
|
|
827
|
+
return path.read_text(encoding="utf-8")
|
|
828
|
+
if _custom_prompt_looks_like_path(value):
|
|
829
|
+
raise ValueError(
|
|
830
|
+
f"step {step_name!r}: 'custom_prompt' file not found: {value}")
|
|
831
|
+
return value
|
|
832
|
+
|
|
833
|
+
|
|
800
834
|
def default_state_dir(pipeline_path: Path) -> str:
|
|
801
835
|
"""The new run-state default (Q1): checkpoint DB + last-error.md live OUTSIDE
|
|
802
836
|
the workspace mount, under ~/.agentic-dev-loop/runs/<pipeline-hash>/ where
|
|
@@ -816,4 +850,10 @@ def load_config(path: Path) -> PipelineConfig:
|
|
|
816
850
|
)
|
|
817
851
|
if "state_dir" not in data:
|
|
818
852
|
data["state_dir"] = default_state_dir(path)
|
|
819
|
-
|
|
853
|
+
cfg = PipelineConfig.model_validate(data)
|
|
854
|
+
pipeline_dir = path.resolve().parent
|
|
855
|
+
for step in cfg.steps:
|
|
856
|
+
if step.custom_prompt is not None:
|
|
857
|
+
step.custom_prompt = resolve_custom_prompt(
|
|
858
|
+
step.custom_prompt, pipeline_dir, step.name)
|
|
859
|
+
return cfg
|
|
@@ -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.0a37 → 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.0a37 → 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,7 +58,15 @@ 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
|
|
63
|
+
CUSTOM_PROMPT_HEADING = "Run-specific instructions"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def custom_prompt_section(text: str) -> str:
|
|
67
|
+
"""The per-step ``custom_prompt`` section (040): appended verbatim, no
|
|
68
|
+
rewording, under a fixed heading."""
|
|
69
|
+
return f"## {CUSTOM_PROMPT_HEADING}\n\n{text}"
|
|
61
70
|
|
|
62
71
|
|
|
63
72
|
class _TurnPauseController:
|
|
@@ -432,15 +441,19 @@ def _artifact_file(runtime: Runtime, step_name: str) -> str | None:
|
|
|
432
441
|
|
|
433
442
|
def build_context(runtime: Runtime, docs: dict[str, str], subject: str,
|
|
434
443
|
notes: list[str] | None = None,
|
|
435
|
-
artifacts: dict[str, str] | None = None
|
|
444
|
+
artifacts: dict[str, str] | None = None,
|
|
445
|
+
custom_prompt: str | None = None) -> str:
|
|
436
446
|
"""Assemble the attempt's shared message: the always-on agent-facing error
|
|
437
|
-
guide (034), subject, doc bundle, the
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
(
|
|
447
|
+
guide (034), subject, doc bundle, the run-specific instructions appended
|
|
448
|
+
from the step's ``custom_prompt`` (040), the canonical dependency-policy
|
|
449
|
+
paragraph (021), the expected-output-paths section generated from step
|
|
450
|
+
defs (a prompt merely naming files lets agents skip them), accumulated
|
|
451
|
+
stage artifacts (027), and previous-attempt notes."""
|
|
441
452
|
parts = [f"You are working on {subject}.\n", guide_section()]
|
|
442
453
|
for name, content in docs.items():
|
|
443
454
|
parts.append(f"## {name}\n\n{content}")
|
|
455
|
+
if custom_prompt:
|
|
456
|
+
parts.append(custom_prompt_section(custom_prompt))
|
|
444
457
|
if artifacts:
|
|
445
458
|
parts.append("## Stage artifacts")
|
|
446
459
|
for step_name, content in artifacts.items():
|
|
@@ -543,6 +556,23 @@ def resolve_brief_inputs(runtime: Runtime, step: StepConfig
|
|
|
543
556
|
return included, list(step.brief_inputs.env_facts)
|
|
544
557
|
|
|
545
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
|
+
|
|
546
576
|
def validate_brief_inputs(workspace: Path, step: StepConfig) -> None:
|
|
547
577
|
"""Fail fast on ``brief_inputs.include`` entries that name no files (033).
|
|
548
578
|
|
|
@@ -609,18 +639,31 @@ def brief_inputs_hash(runtime: Runtime, step: StepConfig,
|
|
|
609
639
|
|
|
610
640
|
def build_brief_context(runtime: Runtime, step: StepConfig,
|
|
611
641
|
docs: dict[str, str],
|
|
612
|
-
doc_sources: dict[str, str] | None = None
|
|
642
|
+
doc_sources: dict[str, str] | None = None,
|
|
643
|
+
coverage_note: str | None = None) -> str:
|
|
613
644
|
"""Assemble the brief agent's task message (033): the always-on
|
|
614
645
|
agent-facing error guide (034), the resolved unit/plan docs (re-read from
|
|
615
646
|
disk where sources are known), the closed ``brief_inputs.include`` file
|
|
616
|
-
set,
|
|
617
|
-
|
|
647
|
+
set, the run-specific instructions appended from ``custom_prompt`` (040),
|
|
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."""
|
|
618
652
|
sections, env_facts = _brief_sections(runtime, step, docs, doc_sources)
|
|
619
653
|
parts = [guide_section()]
|
|
620
654
|
parts += [f"## {name}\n\n{content}" for name, content in sections.items()]
|
|
655
|
+
if step.custom_prompt:
|
|
656
|
+
parts.append(custom_prompt_section(step.custom_prompt))
|
|
621
657
|
if env_facts:
|
|
622
658
|
parts.append("## Environment facts\n\n"
|
|
623
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)
|
|
624
667
|
return "\n\n".join(parts)
|
|
625
668
|
|
|
626
669
|
|
|
@@ -683,7 +726,8 @@ def make_agent_node(runtime: Runtime, step: StepConfig):
|
|
|
683
726
|
max_iterations = step.max_iterations or DEFAULT_MAX_ITERATIONS
|
|
684
727
|
message = build_context(runtime, state["docs"], state["subject"],
|
|
685
728
|
state["notes"],
|
|
686
|
-
artifacts=state.get("artifacts", {})
|
|
729
|
+
artifacts=state.get("artifacts", {}),
|
|
730
|
+
custom_prompt=step.custom_prompt)
|
|
687
731
|
print(f"[{subject}] Running {step.agent} agent...", flush=True)
|
|
688
732
|
result = runtime.run_agent(step.agent, message,
|
|
689
733
|
max_iterations=max_iterations, step=step)
|
|
@@ -774,7 +818,13 @@ def make_brief_node(runtime: Runtime, step: StepConfig):
|
|
|
774
818
|
LLM call is skipped entirely. A changed include file or env fact re-runs
|
|
775
819
|
the step and refreshes the hash. Prior verdicts and feedback never enter
|
|
776
820
|
the brief message - they already reach dev/review through the notes
|
|
777
|
-
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."""
|
|
778
828
|
captures = step.artifact is not None or bool(step.produces)
|
|
779
829
|
|
|
780
830
|
def node(state: dict) -> dict:
|
|
@@ -785,14 +835,9 @@ def make_brief_node(runtime: Runtime, step: StepConfig):
|
|
|
785
835
|
try:
|
|
786
836
|
current_hash = brief_inputs_hash(runtime, step, state["docs"],
|
|
787
837
|
state.get("doc_sources"))
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
flush=True)
|
|
792
|
-
merged = {**state["step_verdicts"], step.name: "pass"}
|
|
793
|
-
return {"step_verdicts": merged, "failed": False}
|
|
794
|
-
message = build_brief_context(runtime, step, state["docs"],
|
|
795
|
-
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 [])
|
|
796
841
|
except Exception:
|
|
797
842
|
err = redact_values(traceback.format_exc(), runtime.secrets.values())
|
|
798
843
|
print(err[-4000:], file=sys.stderr, flush=True)
|
|
@@ -802,6 +847,24 @@ def make_brief_node(runtime: Runtime, step: StepConfig):
|
|
|
802
847
|
encoding="utf-8")
|
|
803
848
|
return {"failed": True, "failed_step": step.name}
|
|
804
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
|
+
|
|
805
868
|
print(f"[{subject}] Running {step.agent} agent...", flush=True)
|
|
806
869
|
result = runtime.run_agent(step.agent, message,
|
|
807
870
|
max_iterations=max_iterations, step=step)
|
|
@@ -819,11 +882,38 @@ def make_brief_node(runtime: Runtime, step: StepConfig):
|
|
|
819
882
|
result.truncated, max_iterations)],
|
|
820
883
|
"retry_target": _unclear_target(runtime, step)}
|
|
821
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
|
+
|
|
822
911
|
merged = {**state["step_verdicts"], step.name: "pass"}
|
|
823
912
|
update = {"step_verdicts": merged, "failed": False}
|
|
824
913
|
if captures and result.response is not None:
|
|
825
914
|
update["artifacts"] = {step.name: result.response}
|
|
826
915
|
update["brief_hashes"] = {step.name: current_hash}
|
|
916
|
+
update["brief_retries"] = {step.name: 0}
|
|
827
917
|
return update
|
|
828
918
|
|
|
829
919
|
return node
|
{agentic_dev_loop-1.0.0a37 → 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.0a37 → 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",
|
|
@@ -266,6 +275,18 @@ class TestBuildBriefContext:
|
|
|
266
275
|
assert ctx.count("## AGENTS.md") == 1
|
|
267
276
|
assert "REPO RULES FROM DOCS" in ctx
|
|
268
277
|
|
|
278
|
+
def test_custom_prompt_between_docs_and_env_facts(self, runtime):
|
|
279
|
+
step = make_cfg([{
|
|
280
|
+
"name": "brief", "agent": "brief", "produces": "attempt-brief.md",
|
|
281
|
+
"brief_inputs": {"include": ["AGENTS.md"], "env_facts": ["offline"]},
|
|
282
|
+
"custom_prompt": "brief scope", "on_pass": "success",
|
|
283
|
+
}]).steps[0]
|
|
284
|
+
ctx = build_brief_context(runtime, step, {"plan.md": "THE PLAN"})
|
|
285
|
+
assert "## Run-specific instructions\n\nbrief scope" in ctx
|
|
286
|
+
assert ctx.count("## Run-specific instructions") == 1
|
|
287
|
+
assert ctx.index("## plan.md") < ctx.index("## Run-specific instructions")
|
|
288
|
+
assert ctx.index("## Run-specific instructions") < ctx.index("## Environment facts")
|
|
289
|
+
|
|
269
290
|
|
|
270
291
|
class TestBriefInputsHash:
|
|
271
292
|
def test_stable_and_content_sensitive(self, runtime):
|
|
@@ -291,11 +312,11 @@ class TestBriefInputsHash:
|
|
|
291
312
|
|
|
292
313
|
class TestBriefNode:
|
|
293
314
|
def test_first_run_calls_agent_and_stores_hash(self, runtime, fake_agents):
|
|
294
|
-
fake_agents.set("brief", [("write", "attempt-brief.md",
|
|
315
|
+
fake_agents.set("brief", [("write", "attempt-brief.md", BRIEF_OK)])
|
|
295
316
|
update = make_brief_node(runtime, runtime.cfg.steps[0])(make_state())
|
|
296
317
|
assert update["step_verdicts"] == {"brief": "pass"}
|
|
297
318
|
assert update["failed"] is False
|
|
298
|
-
assert update["artifacts"] == {"brief":
|
|
319
|
+
assert update["artifacts"] == {"brief": BRIEF_OK}
|
|
299
320
|
assert update["brief_hashes"] == {
|
|
300
321
|
"brief": brief_inputs_hash(runtime, runtime.cfg.steps[0],
|
|
301
322
|
make_state()["docs"])}
|
|
@@ -304,7 +325,7 @@ class TestBriefNode:
|
|
|
304
325
|
assert "## Environment facts\n\n- offline; never fetch" in msg
|
|
305
326
|
|
|
306
327
|
def test_unchanged_inputs_skip_llm(self, runtime, fake_agents):
|
|
307
|
-
fake_agents.set("brief", [("write", "attempt-brief.md",
|
|
328
|
+
fake_agents.set("brief", [("write", "attempt-brief.md", BRIEF_OK)])
|
|
308
329
|
node = make_brief_node(runtime, runtime.cfg.steps[0])
|
|
309
330
|
first = node(make_state())
|
|
310
331
|
second = node(make_state(artifacts=first["artifacts"],
|
|
@@ -314,15 +335,15 @@ class TestBriefNode:
|
|
|
314
335
|
|
|
315
336
|
def test_changed_inputs_rerun(self, runtime, fake_agents):
|
|
316
337
|
fake_agents.set("brief", [
|
|
317
|
-
("write", "attempt-brief.md",
|
|
318
|
-
("write", "attempt-brief.md",
|
|
338
|
+
("write", "attempt-brief.md", BRIEF_OK),
|
|
339
|
+
("write", "attempt-brief.md", BRIEF_OK_NEW),
|
|
319
340
|
])
|
|
320
341
|
node = make_brief_node(runtime, runtime.cfg.steps[0])
|
|
321
342
|
first = node(make_state())
|
|
322
343
|
(runtime.workspace / "AGENTS.md").write_text("EDITED")
|
|
323
344
|
second = node(make_state(artifacts=first["artifacts"],
|
|
324
345
|
brief_hashes=first["brief_hashes"]))
|
|
325
|
-
assert second["artifacts"] == {"brief":
|
|
346
|
+
assert second["artifacts"] == {"brief": BRIEF_OK_NEW}
|
|
326
347
|
assert fake_agents.agents_called() == ["brief", "brief"]
|
|
327
348
|
|
|
328
349
|
def test_loader_doc_disk_edit_reruns_brief(self, runtime, fake_agents,
|
|
@@ -330,8 +351,8 @@ class TestBriefNode:
|
|
|
330
351
|
plan = tmp_path / "plan.md"
|
|
331
352
|
plan.write_text("PLAN V1")
|
|
332
353
|
fake_agents.set("brief", [
|
|
333
|
-
("write", "attempt-brief.md",
|
|
334
|
-
("write", "attempt-brief.md",
|
|
354
|
+
("write", "attempt-brief.md", BRIEF_OK),
|
|
355
|
+
("write", "attempt-brief.md", BRIEF_OK_NEW),
|
|
335
356
|
])
|
|
336
357
|
node = make_brief_node(runtime, runtime.cfg.steps[0])
|
|
337
358
|
first = node(make_state(docs={"plan.md": "PLAN V1"},
|
|
@@ -342,7 +363,7 @@ class TestBriefNode:
|
|
|
342
363
|
doc_sources={"plan.md": str(plan)},
|
|
343
364
|
artifacts=first["artifacts"],
|
|
344
365
|
brief_hashes=first["brief_hashes"]))
|
|
345
|
-
assert second["artifacts"] == {"brief":
|
|
366
|
+
assert second["artifacts"] == {"brief": BRIEF_OK_NEW}
|
|
346
367
|
assert fake_agents.agents_called() == ["brief", "brief"]
|
|
347
368
|
|
|
348
369
|
def test_empty_response_is_unclear(self, runtime, fake_agents):
|
|
@@ -557,7 +578,14 @@ class TestBriefPipeline:
|
|
|
557
578
|
monkeypatch.chdir(repo)
|
|
558
579
|
monkeypatch.setenv("TEST_LLM_KEY", "sk-test")
|
|
559
580
|
|
|
560
|
-
|
|
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)])
|
|
561
589
|
fake_agents.set("dev", [("write", "implementation-summary.md", "did the work")])
|
|
562
590
|
|
|
563
591
|
ok, attempts = run_pipeline("pipeline.yaml", _args())
|