agentic-dev-loop 1.0.0a37__tar.gz → 1.0.0a38__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.0a38}/PKG-INFO +1 -1
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/pyproject.toml +1 -1
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/core/config.py +41 -1
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/stages.py +22 -7
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38/src/agentic_dev_loop.egg-info}/PKG-INFO +1 -1
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_033_brief_step.py +12 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_config.py +60 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_stages.py +24 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/LICENSE +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/README.md +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/setup.cfg +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/core/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/core/paths.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/core/results.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/core/verdict.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/actions.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/agent_error_guide.md +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/agent_error_guide.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/agents.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/allowlist.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/cli.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/deps.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/dryrun.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/egress_policy.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/egress_proxy.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/forbidden.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/graph.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/hostboundary.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/isolation.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/llm.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/loaders.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/mounts.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/outputs.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/presets.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/proc.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/registry.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/secrets.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/step_workspace.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/sync_back.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/py.typed +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/viz/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/viz/__main__.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/viz/server.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop.egg-info/SOURCES.txt +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_028_declared_caches.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_029_merge_on_conflict.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_032_stop_when_green.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_035_finalize_nudge.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_036_banned_commands_and_lane_boundary.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_actions.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_agent_error_guide.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_agents.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_cli.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_deps.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_dry_run.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_e2e_fixtures.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_egress_proxy.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_graph.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_hostboundary.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_hostboundary_integration.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_isolation.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_isolation_integration.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_isolation_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_llm.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_loaders.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_module_contract.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_outputs.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_outputs_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_proc.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_publish.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_qa_prompt.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_registry.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_repo_ascii.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_results.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_sdk_compat.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_secrets.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_secrets_integration.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_secrets_pipeline.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_source_resolution.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_step_workspace.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_sync_back.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_trigger_script.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_unit5_config.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_unit5_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_unit5_pure.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_verdict.py +0 -0
- {agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/tests/test_viz.py +0 -0
|
@@ -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.0a38"
|
|
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
|
{agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/stages.py
RENAMED
|
@@ -58,6 +58,13 @@ DEFAULT_MAX_ITERATIONS = 500
|
|
|
58
58
|
DEFAULT_COMMAND_TIMEOUT = 600.0 # seconds - a pytest-class suite budget
|
|
59
59
|
LAST_ERROR = "last-error.md"
|
|
60
60
|
ARTIFACT_CONTEXT_CAP = 24_000 # chars per artifact injected into a step's context
|
|
61
|
+
CUSTOM_PROMPT_HEADING = "Run-specific instructions"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def custom_prompt_section(text: str) -> str:
|
|
65
|
+
"""The per-step ``custom_prompt`` section (040): appended verbatim, no
|
|
66
|
+
rewording, under a fixed heading."""
|
|
67
|
+
return f"## {CUSTOM_PROMPT_HEADING}\n\n{text}"
|
|
61
68
|
|
|
62
69
|
|
|
63
70
|
class _TurnPauseController:
|
|
@@ -432,15 +439,19 @@ def _artifact_file(runtime: Runtime, step_name: str) -> str | None:
|
|
|
432
439
|
|
|
433
440
|
def build_context(runtime: Runtime, docs: dict[str, str], subject: str,
|
|
434
441
|
notes: list[str] | None = None,
|
|
435
|
-
artifacts: dict[str, str] | None = None
|
|
442
|
+
artifacts: dict[str, str] | None = None,
|
|
443
|
+
custom_prompt: str | None = None) -> str:
|
|
436
444
|
"""Assemble the attempt's shared message: the always-on agent-facing error
|
|
437
|
-
guide (034), subject, doc bundle, the
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
(
|
|
445
|
+
guide (034), subject, doc bundle, the run-specific instructions appended
|
|
446
|
+
from the step's ``custom_prompt`` (040), the canonical dependency-policy
|
|
447
|
+
paragraph (021), the expected-output-paths section generated from step
|
|
448
|
+
defs (a prompt merely naming files lets agents skip them), accumulated
|
|
449
|
+
stage artifacts (027), and previous-attempt notes."""
|
|
441
450
|
parts = [f"You are working on {subject}.\n", guide_section()]
|
|
442
451
|
for name, content in docs.items():
|
|
443
452
|
parts.append(f"## {name}\n\n{content}")
|
|
453
|
+
if custom_prompt:
|
|
454
|
+
parts.append(custom_prompt_section(custom_prompt))
|
|
444
455
|
if artifacts:
|
|
445
456
|
parts.append("## Stage artifacts")
|
|
446
457
|
for step_name, content in artifacts.items():
|
|
@@ -613,11 +624,14 @@ def build_brief_context(runtime: Runtime, step: StepConfig,
|
|
|
613
624
|
"""Assemble the brief agent's task message (033): the always-on
|
|
614
625
|
agent-facing error guide (034), the resolved unit/plan docs (re-read from
|
|
615
626
|
disk where sources are known), the closed ``brief_inputs.include`` file
|
|
616
|
-
set,
|
|
627
|
+
set, the run-specific instructions appended from ``custom_prompt`` (040),
|
|
628
|
+
and the env-facts block. Prior verdicts and feedback are deliberately
|
|
617
629
|
absent - the notes channel delivers them per attempt."""
|
|
618
630
|
sections, env_facts = _brief_sections(runtime, step, docs, doc_sources)
|
|
619
631
|
parts = [guide_section()]
|
|
620
632
|
parts += [f"## {name}\n\n{content}" for name, content in sections.items()]
|
|
633
|
+
if step.custom_prompt:
|
|
634
|
+
parts.append(custom_prompt_section(step.custom_prompt))
|
|
621
635
|
if env_facts:
|
|
622
636
|
parts.append("## Environment facts\n\n"
|
|
623
637
|
+ "\n".join(f"- {fact}" for fact in env_facts))
|
|
@@ -683,7 +697,8 @@ def make_agent_node(runtime: Runtime, step: StepConfig):
|
|
|
683
697
|
max_iterations = step.max_iterations or DEFAULT_MAX_ITERATIONS
|
|
684
698
|
message = build_context(runtime, state["docs"], state["subject"],
|
|
685
699
|
state["notes"],
|
|
686
|
-
artifacts=state.get("artifacts", {})
|
|
700
|
+
artifacts=state.get("artifacts", {}),
|
|
701
|
+
custom_prompt=step.custom_prompt)
|
|
687
702
|
print(f"[{subject}] Running {step.agent} agent...", flush=True)
|
|
688
703
|
result = runtime.run_agent(step.agent, message,
|
|
689
704
|
max_iterations=max_iterations, step=step)
|
|
@@ -266,6 +266,18 @@ class TestBuildBriefContext:
|
|
|
266
266
|
assert ctx.count("## AGENTS.md") == 1
|
|
267
267
|
assert "REPO RULES FROM DOCS" in ctx
|
|
268
268
|
|
|
269
|
+
def test_custom_prompt_between_docs_and_env_facts(self, runtime):
|
|
270
|
+
step = make_cfg([{
|
|
271
|
+
"name": "brief", "agent": "brief", "produces": "attempt-brief.md",
|
|
272
|
+
"brief_inputs": {"include": ["AGENTS.md"], "env_facts": ["offline"]},
|
|
273
|
+
"custom_prompt": "brief scope", "on_pass": "success",
|
|
274
|
+
}]).steps[0]
|
|
275
|
+
ctx = build_brief_context(runtime, step, {"plan.md": "THE PLAN"})
|
|
276
|
+
assert "## Run-specific instructions\n\nbrief scope" in ctx
|
|
277
|
+
assert ctx.count("## Run-specific instructions") == 1
|
|
278
|
+
assert ctx.index("## plan.md") < ctx.index("## Run-specific instructions")
|
|
279
|
+
assert ctx.index("## Run-specific instructions") < ctx.index("## Environment facts")
|
|
280
|
+
|
|
269
281
|
|
|
270
282
|
class TestBriefInputsHash:
|
|
271
283
|
def test_stable_and_content_sensitive(self, runtime):
|
|
@@ -331,6 +331,66 @@ def test_timeout_without_command_rejected(tmp_path):
|
|
|
331
331
|
_load(tmp_path, data)
|
|
332
332
|
|
|
333
333
|
|
|
334
|
+
class TestCustomPrompt:
|
|
335
|
+
def test_defaults_none(self, tmp_path):
|
|
336
|
+
assert _load(tmp_path, _base()).steps[0].custom_prompt is None
|
|
337
|
+
|
|
338
|
+
def test_literal_text_round_trips(self, tmp_path):
|
|
339
|
+
data = _base()
|
|
340
|
+
data["steps"][0]["custom_prompt"] = "verify the export tab first"
|
|
341
|
+
assert _load(tmp_path, data).steps[0].custom_prompt == \
|
|
342
|
+
"verify the export tab first"
|
|
343
|
+
|
|
344
|
+
def test_existing_file_path_reads_contents(self, tmp_path):
|
|
345
|
+
(tmp_path / "qa-plan.md").write_text("verify the export tab\n")
|
|
346
|
+
data = _base()
|
|
347
|
+
data["steps"][0]["custom_prompt"] = "qa-plan.md"
|
|
348
|
+
assert _load(tmp_path, data).steps[0].custom_prompt == \
|
|
349
|
+
"verify the export tab\n"
|
|
350
|
+
|
|
351
|
+
def test_absolute_file_path_reads_contents(self, tmp_path):
|
|
352
|
+
plan = tmp_path / "plan.txt"
|
|
353
|
+
plan.write_text("ABSOLUTE PLAN")
|
|
354
|
+
data = _base()
|
|
355
|
+
data["steps"][0]["custom_prompt"] = str(plan)
|
|
356
|
+
assert _load(tmp_path, data).steps[0].custom_prompt == "ABSOLUTE PLAN"
|
|
357
|
+
|
|
358
|
+
def test_missing_path_with_extension_rejected(self, tmp_path):
|
|
359
|
+
data = _base()
|
|
360
|
+
data["steps"][0]["custom_prompt"] = "qa-plan.md"
|
|
361
|
+
with pytest.raises(ValueError,
|
|
362
|
+
match="'custom_prompt' file not found: qa-plan.md"):
|
|
363
|
+
_load(tmp_path, data)
|
|
364
|
+
|
|
365
|
+
def test_missing_path_with_separator_rejected(self, tmp_path):
|
|
366
|
+
data = _base()
|
|
367
|
+
data["steps"][0]["custom_prompt"] = "tests/qa-plan"
|
|
368
|
+
with pytest.raises(ValueError, match="tests/qa-plan"):
|
|
369
|
+
_load(tmp_path, data)
|
|
370
|
+
|
|
371
|
+
def test_requires_agent(self, tmp_path):
|
|
372
|
+
data = _base()
|
|
373
|
+
data["steps"] = [{"name": "x", "command": "true",
|
|
374
|
+
"custom_prompt": "hi"}]
|
|
375
|
+
with pytest.raises(ValidationError,
|
|
376
|
+
match="'custom_prompt' requires 'agent'"):
|
|
377
|
+
_load(tmp_path, data)
|
|
378
|
+
|
|
379
|
+
def test_rejected_on_action_step(self, tmp_path):
|
|
380
|
+
data = _base()
|
|
381
|
+
data["steps"] = [{"name": "x", "action": "commit",
|
|
382
|
+
"custom_prompt": "hi"}]
|
|
383
|
+
with pytest.raises(ValidationError,
|
|
384
|
+
match="'custom_prompt' requires 'agent'"):
|
|
385
|
+
_load(tmp_path, data)
|
|
386
|
+
|
|
387
|
+
def test_empty_value_rejected(self, tmp_path):
|
|
388
|
+
data = _base()
|
|
389
|
+
data["steps"][0]["custom_prompt"] = ""
|
|
390
|
+
with pytest.raises(ValidationError, match="non-empty"):
|
|
391
|
+
_load(tmp_path, data)
|
|
392
|
+
|
|
393
|
+
|
|
334
394
|
@pytest.mark.parametrize("field,value", [
|
|
335
395
|
("artifact", "a.md"),
|
|
336
396
|
("verdicts", {"pass": "P", "fail": "F"}),
|
|
@@ -137,6 +137,19 @@ class TestBuildContext:
|
|
|
137
137
|
ctx = build_context(runtime, {}, "s", [])
|
|
138
138
|
assert "## Stage artifacts" not in ctx
|
|
139
139
|
|
|
140
|
+
def test_custom_prompt_section_between_prompt_and_artifacts(self, runtime):
|
|
141
|
+
ctx = build_context(runtime, {"plan.md": "THE PLAN"}, "s", [],
|
|
142
|
+
artifacts={"dev": "summary"},
|
|
143
|
+
custom_prompt="verify export first")
|
|
144
|
+
assert "## Run-specific instructions\n\nverify export first" in ctx
|
|
145
|
+
assert ctx.count("## Run-specific instructions") == 1
|
|
146
|
+
assert ctx.index("## plan.md") < ctx.index("## Run-specific instructions")
|
|
147
|
+
assert ctx.index("## Run-specific instructions") < ctx.index("## Stage artifacts")
|
|
148
|
+
|
|
149
|
+
def test_no_custom_prompt_leaves_context_unchanged(self, runtime):
|
|
150
|
+
ctx = build_context(runtime, {"plan.md": "THE PLAN"}, "s", [])
|
|
151
|
+
assert "## Run-specific instructions" not in ctx
|
|
152
|
+
|
|
140
153
|
|
|
141
154
|
class TestDepsPolicyContext:
|
|
142
155
|
def test_locked_policy_injected(self, runtime):
|
|
@@ -210,6 +223,17 @@ class TestAgentNode:
|
|
|
210
223
|
update = make_agent_node(runtime, runtime.cfg.steps[0])(make_state())
|
|
211
224
|
assert update["artifacts"] == {"dev": "MARKER-DEV"}
|
|
212
225
|
|
|
226
|
+
def test_custom_prompt_reaches_agent_message(self, runtime, fake_agents):
|
|
227
|
+
cfg = make_cfg([{"name": "dev", "agent": "dev",
|
|
228
|
+
"produces": ["summary.md"],
|
|
229
|
+
"custom_prompt": "scope note", "on_pass": "success"}])
|
|
230
|
+
rt = dataclasses.replace(runtime, cfg=cfg)
|
|
231
|
+
fake_agents.set("dev", [("write", "summary.md", "done")])
|
|
232
|
+
make_agent_node(rt, rt.cfg.steps[0])(make_state())
|
|
233
|
+
(message,) = fake_agents.messages_for("dev")
|
|
234
|
+
assert "## Run-specific instructions\n\nscope note" in message
|
|
235
|
+
assert message.count("## Run-specific instructions") == 1
|
|
236
|
+
|
|
213
237
|
def test_pass_verdict_captures_artifact(self, runtime, fake_agents):
|
|
214
238
|
fake_agents.set("check", [("write", "ci-fix.md", "MARKER-CHECK\nVERDICT: PASS\n")])
|
|
215
239
|
update = make_agent_node(runtime, runtime.cfg.steps[1])(make_state())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/core/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/core/results.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/core/verdict.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/__init__.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/actions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/agents.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/allowlist.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/dryrun.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/egress_policy.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/egress_proxy.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/forbidden.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/graph.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/hostboundary.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/isolation.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/loaders.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/mounts.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/outputs.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/presets.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/registry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/secrets.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/engine/sync_back.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/viz/__init__.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop/viz/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/src/agentic_dev_loop.egg-info/requires.txt
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a37 → agentic_dev_loop-1.0.0a38}/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.0a37 → agentic_dev_loop-1.0.0a38}/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
|