agentic-dev-loop 1.0.0a12__tar.gz → 1.0.0a13__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.0a12 → agentic_dev_loop-1.0.0a13}/PKG-INFO +1 -1
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/pyproject.toml +1 -1
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/engine/cli.py +5 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/engine/isolation.py +23 -2
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/engine/loaders.py +41 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop.egg-info/PKG-INFO +1 -1
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/tests/test_cli.py +48 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/tests/test_isolation.py +15 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/tests/test_isolation_integration.py +9 -2
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/tests/test_loaders.py +110 -1
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/tests/test_trigger_script.py +80 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/LICENSE +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/README.md +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/setup.cfg +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/core/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/core/config.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/core/results.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/core/verdict.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/engine/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/engine/actions.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/engine/agents.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/engine/deps.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/engine/dryrun.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/engine/graph.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/engine/hostboundary.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/engine/llm.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/engine/outputs.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/engine/proc.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/engine/registry.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/engine/secrets.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/engine/stages.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/py.typed +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/viz/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/viz/__main__.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/viz/server.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop.egg-info/SOURCES.txt +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/tests/test_actions.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/tests/test_agents.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/tests/test_aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/tests/test_config.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/tests/test_deps.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/tests/test_dry_run.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/tests/test_e2e_fixtures.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/tests/test_graph.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/tests/test_hostboundary.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/tests/test_hostboundary_integration.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/tests/test_isolation_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/tests/test_llm.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/tests/test_module_contract.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/tests/test_outputs.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/tests/test_outputs_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/tests/test_proc.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/tests/test_publish.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/tests/test_registry.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/tests/test_results.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/tests/test_sdk_compat.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/tests/test_secrets.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/tests/test_secrets_integration.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/tests/test_secrets_pipeline.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/tests/test_source_resolution.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/tests/test_stages.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/tests/test_verdict.py +0 -0
- {agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/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.0a13"
|
|
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"
|
|
@@ -12,6 +12,7 @@ from pathlib import Path
|
|
|
12
12
|
from agentic_dev_loop.core.config import PipelineConfig, load_config
|
|
13
13
|
from .dryrun import dry_run
|
|
14
14
|
from .isolation import clean_stamps, isolation_state_dir
|
|
15
|
+
from .loaders import mark_plan_completed
|
|
15
16
|
from .registry import Registry, default_registry, load_extensions
|
|
16
17
|
|
|
17
18
|
|
|
@@ -112,6 +113,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
112
113
|
# dry run, --clean-stamps, and --help stay side-effect free.
|
|
113
114
|
from .graph import run_pipeline
|
|
114
115
|
success, _attempts = run_pipeline(args.pipeline, args, registry=registry)
|
|
116
|
+
if success and args.plan is not None:
|
|
117
|
+
# Only a completed run earns the stamp — failed runs leave the plan
|
|
118
|
+
# as-is. AI-DLC runs never pass --plan, so aidlc docs stay untouched.
|
|
119
|
+
mark_plan_completed(args.plan)
|
|
115
120
|
if not success:
|
|
116
121
|
print("agentic-dev-loop: pipeline failed — exiting with code 1",
|
|
117
122
|
file=sys.stderr)
|
{agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/engine/isolation.py
RENAMED
|
@@ -274,11 +274,32 @@ def is_git_mutation(argv: list[str]) -> bool:
|
|
|
274
274
|
return True
|
|
275
275
|
|
|
276
276
|
|
|
277
|
+
def _expand_home(path: str) -> str:
|
|
278
|
+
"""Expand a leading ``~`` against ``$HOME``, falling back to
|
|
279
|
+
``USERPROFILE`` (Windows).
|
|
280
|
+
|
|
281
|
+
The engine is driven from bash on every platform (Windows via Git Bash),
|
|
282
|
+
where HOME is the authoritative home. CPython's Windows expanduser ignores
|
|
283
|
+
HOME in favour of USERPROFILE, so a literal ``~`` in a config file would
|
|
284
|
+
resolve inconsistently on Windows. Only a leading tilde (``~``, ``~/``,
|
|
285
|
+
``~\\``) is handled here; ``~user`` forms and the no-HOME/no-USERPROFILE
|
|
286
|
+
case fall through to the stdlib below.
|
|
287
|
+
"""
|
|
288
|
+
if path != "~" and not path.startswith(("~/", "~\\")):
|
|
289
|
+
return path
|
|
290
|
+
home = os.environ.get("HOME") or os.environ.get("USERPROFILE")
|
|
291
|
+
if home:
|
|
292
|
+
return home + path[1:]
|
|
293
|
+
return path
|
|
294
|
+
|
|
295
|
+
|
|
277
296
|
def translate_mount_path(path: str) -> str:
|
|
278
297
|
"""expanduser + absolute resolution without symlink chasing. Identity on
|
|
279
298
|
macOS/Windows/WSL2-with-Docker-Desktop (verbatim passthrough — Docker
|
|
280
|
-
Desktop translates). Idempotent: translate(translate(p)) == translate(p).
|
|
281
|
-
|
|
299
|
+
Desktop translates). Idempotent: translate(translate(p)) == translate(p).
|
|
300
|
+
A leading ``~`` resolves against $HOME first (USERPROFILE fallback), then
|
|
301
|
+
the stdlib handles anything left (pwd on POSIX)."""
|
|
302
|
+
p = Path(_expand_home(path)).expanduser()
|
|
282
303
|
if not p.is_absolute():
|
|
283
304
|
p = p.absolute()
|
|
284
305
|
return str(p)
|
{agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/engine/loaders.py
RENAMED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Built-in `plan` document loader + the loader-selection flow."""
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
+
import re
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
6
7
|
from agentic_dev_loop.core.config import PipelineConfig
|
|
@@ -18,6 +19,46 @@ def resolve_plan_docs(plan_path: Path | str) -> dict[str, str]:
|
|
|
18
19
|
return {path.name: path.read_text()}
|
|
19
20
|
|
|
20
21
|
|
|
22
|
+
# A status line at the top of a planning doc, in any recorded form:
|
|
23
|
+
# `> **Status: COMPLETED** (date)`, `**Status:** ...`, `- **Status**: ...`,
|
|
24
|
+
# or bare `Status: ...`.
|
|
25
|
+
_STATUS_LINE = re.compile(r"^\s*(?:>\s*)?(?:[-*]\s+)?\*{0,2}Status\*{0,2}\s*:")
|
|
26
|
+
# The status only counts as "recorded at the top" when it appears early —
|
|
27
|
+
# buried history (e.g. `## Status history` or a later `Status: ...` line)
|
|
28
|
+
# is not the doc's current status.
|
|
29
|
+
_STATUS_SCAN_LINES = 10
|
|
30
|
+
_STAMP = "> **Status: COMPLETED**"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def mark_plan_completed(plan_path: Path | str) -> None:
|
|
34
|
+
"""Stamp the plan doc as COMPLETED after a successful run.
|
|
35
|
+
|
|
36
|
+
Replaces the first status line found within the top of the doc, or
|
|
37
|
+
inserts the stamp right after the `# ` title (blank separators around
|
|
38
|
+
it) — or at the very top when there is no title. Trailing-newline
|
|
39
|
+
presence is preserved."""
|
|
40
|
+
path = Path(plan_path)
|
|
41
|
+
text = path.read_text(encoding="utf-8")
|
|
42
|
+
lines = text.splitlines()
|
|
43
|
+
trailing = "\n" if text.endswith("\n") else ""
|
|
44
|
+
|
|
45
|
+
for idx in range(min(len(lines), _STATUS_SCAN_LINES)):
|
|
46
|
+
if _STATUS_LINE.match(lines[idx]):
|
|
47
|
+
lines[idx] = _STAMP
|
|
48
|
+
break
|
|
49
|
+
else:
|
|
50
|
+
title_idx = next((i for i, line in enumerate(lines)
|
|
51
|
+
if line.startswith("# ")), None)
|
|
52
|
+
insert_at = 0 if title_idx is None else title_idx + 1
|
|
53
|
+
stamp_block = [_STAMP]
|
|
54
|
+
if insert_at > 0 and lines[insert_at - 1] != "":
|
|
55
|
+
stamp_block.insert(0, "") # blank separator above the stamp
|
|
56
|
+
if insert_at < len(lines) and lines[insert_at] != "":
|
|
57
|
+
stamp_block.append("") # blank separator below the stamp
|
|
58
|
+
lines[insert_at:insert_at] = stamp_block
|
|
59
|
+
path.write_text("\n".join(lines) + trailing, encoding="utf-8")
|
|
60
|
+
|
|
61
|
+
|
|
21
62
|
class PlanDocumentLoader:
|
|
22
63
|
name = "plan"
|
|
23
64
|
defaultable = False # needs a path — never autodetects
|
|
@@ -202,6 +202,54 @@ class TestRun:
|
|
|
202
202
|
# dev completed before the crash — only test re-ran on resume
|
|
203
203
|
assert fake_agents.agents_called() == ["dev", "check", "check"]
|
|
204
204
|
|
|
205
|
+
def test_success_stamps_plan_completed(self, repo, fake_agents):
|
|
206
|
+
fake_agents.set("dev", [("write", "implementation-summary.md", "done")])
|
|
207
|
+
|
|
208
|
+
assert main(["pipeline.yaml", "--plan", "plan.md", "--no-viz"]) == 0
|
|
209
|
+
|
|
210
|
+
assert (repo / "plan.md").read_text() == (
|
|
211
|
+
"> **Status: COMPLETED**\n\nTHE PLAN")
|
|
212
|
+
|
|
213
|
+
def test_success_replaces_existing_status(self, repo, fake_agents):
|
|
214
|
+
(repo / "plan.md").write_text(
|
|
215
|
+
"# Plan\n\n> **Status: DONE**\n\nTHE PLAN\n")
|
|
216
|
+
git(repo, "add", "-A")
|
|
217
|
+
git(repo, "commit", "-m", "record status")
|
|
218
|
+
fake_agents.set("dev", [("write", "implementation-summary.md", "done")])
|
|
219
|
+
|
|
220
|
+
assert main(["pipeline.yaml", "--plan", "plan.md", "--no-viz"]) == 0
|
|
221
|
+
|
|
222
|
+
assert (repo / "plan.md").read_text() == (
|
|
223
|
+
"# Plan\n\n> **Status: COMPLETED**\n\nTHE PLAN\n")
|
|
224
|
+
|
|
225
|
+
def test_failed_run_does_not_stamp(self, repo, fake_agents):
|
|
226
|
+
write_pipeline(repo, "retry.yaml", RETRY_YAML)
|
|
227
|
+
fake_agents.set("dev", [("write", "implementation-summary.md", "done")])
|
|
228
|
+
fake_agents.set("check", [("write", "ci-fix.md", "VERDICT: FAIL")])
|
|
229
|
+
|
|
230
|
+
assert main(["retry.yaml", "--plan", "plan.md", "--no-viz"]) == 1
|
|
231
|
+
|
|
232
|
+
assert (repo / "plan.md").read_text() == "THE PLAN"
|
|
233
|
+
|
|
234
|
+
def test_aidlc_run_leaves_docs_untouched(self, repo, fake_agents):
|
|
235
|
+
write_pipeline(repo, "aidlc.yaml", with_extensions(
|
|
236
|
+
PLAIN_YAML, "agentic_dev_loop.engine.extensions.aidlc:AidlcExtension"))
|
|
237
|
+
unit_dir = repo / "aidlc-docs" / "construction" / "unit-3"
|
|
238
|
+
unit_dir.mkdir(parents=True)
|
|
239
|
+
(unit_dir / "design.md").write_text("# Design")
|
|
240
|
+
(repo / "aidlc-docs" / "aidlc-state.md").write_text(
|
|
241
|
+
"# State\n\n- **Current Stage**: CONSTRUCTION — Unit unit-3 (SCAN).\n")
|
|
242
|
+
git(repo, "add", "-A")
|
|
243
|
+
git(repo, "commit", "-m", "aidlc fixture")
|
|
244
|
+
fake_agents.set("dev", [("write", "implementation-summary.md", "done")])
|
|
245
|
+
state_before = (repo / "aidlc-docs" / "aidlc-state.md").read_text()
|
|
246
|
+
|
|
247
|
+
assert main(["aidlc.yaml", "--aidlc-unit", "unit-3", "--no-viz"]) == 0
|
|
248
|
+
|
|
249
|
+
assert (repo / "aidlc-docs" / "aidlc-state.md").read_text() == state_before
|
|
250
|
+
assert (repo / "aidlc-docs" / "construction" / "unit-3" / "design.md").read_text() == "# Design"
|
|
251
|
+
assert (repo / "plan.md").read_text() == "THE PLAN"
|
|
252
|
+
|
|
205
253
|
|
|
206
254
|
class TestViz:
|
|
207
255
|
def test_no_viz_flags_parse(self, repo):
|
|
@@ -459,6 +459,21 @@ class TestResolveSandboxVenvDir:
|
|
|
459
459
|
assert out == str(tmp_path / "venvs" / "engine")
|
|
460
460
|
assert os.path.isabs(out)
|
|
461
461
|
|
|
462
|
+
def test_home_wins_over_userprofile(self, tmp_path, monkeypatch):
|
|
463
|
+
# The engine is driven from bash, where HOME is the authoritative home
|
|
464
|
+
# on every platform — CPython's Windows expanduser ignores HOME in
|
|
465
|
+
# favour of USERPROFILE, which would resolve `~` inconsistently.
|
|
466
|
+
monkeypatch.setenv("HOME", str(tmp_path / "home"))
|
|
467
|
+
monkeypatch.setenv("USERPROFILE", str(tmp_path / "prof"))
|
|
468
|
+
out = resolve_sandbox_venv_dir("~/venvs/engine", tmp_path / "ws")
|
|
469
|
+
assert out == str(tmp_path / "home" / "venvs" / "engine")
|
|
470
|
+
|
|
471
|
+
def test_userprofile_fallback_when_home_unset(self, tmp_path, monkeypatch):
|
|
472
|
+
monkeypatch.delenv("HOME", raising=False)
|
|
473
|
+
monkeypatch.setenv("USERPROFILE", str(tmp_path / "prof"))
|
|
474
|
+
out = resolve_sandbox_venv_dir("~/venvs/engine", tmp_path / "ws")
|
|
475
|
+
assert out == str(tmp_path / "prof" / "venvs" / "engine")
|
|
476
|
+
|
|
462
477
|
def test_outside_workspace_accepted(self, tmp_path):
|
|
463
478
|
ws = tmp_path / "ws"
|
|
464
479
|
out = resolve_sandbox_venv_dir(str(tmp_path / "venvs"), ws)
|
|
@@ -3,6 +3,7 @@ reason when no daemon or integration image is present; never silently skipped
|
|
|
3
3
|
in CI (the workflows pre-install Docker and the image)."""
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
+
import os
|
|
6
7
|
import shutil
|
|
7
8
|
|
|
8
9
|
import pytest
|
|
@@ -249,8 +250,14 @@ class TestRelocatedVenvE2E:
|
|
|
249
250
|
try:
|
|
250
251
|
b.provision("python -m venv .venv-sandbox", "k1",
|
|
251
252
|
str(tmp_path / "stamps"), force=False)
|
|
252
|
-
|
|
253
|
-
|
|
253
|
+
# 022: the venv materialized OUTSIDE the workspace. bin/python is
|
|
254
|
+
# a symlink to the CONTAINER's interpreter, which dangles on the
|
|
255
|
+
# host — existence must be checked without following links.
|
|
256
|
+
assert os.path.lexists(venv_dir / "bin" / "python")
|
|
257
|
+
assert (venv_dir / "pyvenv.cfg").exists()
|
|
258
|
+
# No venv content leaked into the workspace. Docker auto-creates
|
|
259
|
+
# the empty mountpoint dir, so its mere existence is not a leak.
|
|
260
|
+
assert not (ws / ".venv-sandbox" / "pyvenv.cfg").exists()
|
|
254
261
|
req = SandboxExecutionRequest(
|
|
255
262
|
argv=[".venv-sandbox/bin/python", "-c", "import sys; print('ok')"],
|
|
256
263
|
cwd=ws,
|
|
@@ -5,7 +5,9 @@ from pathlib import Path
|
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
7
7
|
|
|
8
|
-
from agentic_dev_loop.engine.loaders import
|
|
8
|
+
from agentic_dev_loop.engine.loaders import (
|
|
9
|
+
PlanDocumentLoader, mark_plan_completed, resolve_plan_docs, select_loader,
|
|
10
|
+
)
|
|
9
11
|
from agentic_dev_loop.engine.registry import Registry, default_registry
|
|
10
12
|
|
|
11
13
|
|
|
@@ -116,3 +118,110 @@ class TestSelectLoader:
|
|
|
116
118
|
|
|
117
119
|
with pytest.raises(ValueError, match="[Dd]efaultable"):
|
|
118
120
|
select_loader(reg, _args(a=None, b=None))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class TestMarkPlanCompleted:
|
|
124
|
+
def _write(self, tmp_path, text):
|
|
125
|
+
plan = tmp_path / "plan.md"
|
|
126
|
+
plan.write_text(text)
|
|
127
|
+
return plan
|
|
128
|
+
|
|
129
|
+
@pytest.mark.parametrize("status_line", [
|
|
130
|
+
"> **Status: COMPLETED** (2026-10-23)",
|
|
131
|
+
"> **Status: DONE**",
|
|
132
|
+
"Status: COMPLETED — shipped",
|
|
133
|
+
"**Status:** COMPLETED (2026-10-10)",
|
|
134
|
+
"- **Status**: COMPLETED — all stages done",
|
|
135
|
+
])
|
|
136
|
+
def test_replaces_existing_status_line(self, tmp_path, status_line):
|
|
137
|
+
plan = self._write(tmp_path, f"# Plan\n\n{status_line}\n\nBody\n")
|
|
138
|
+
|
|
139
|
+
mark_plan_completed(plan)
|
|
140
|
+
|
|
141
|
+
assert plan.read_text() == "# Plan\n\n> **Status: COMPLETED**\n\nBody\n"
|
|
142
|
+
|
|
143
|
+
def test_replaces_inline_bold_status_line_entirely(self, tmp_path):
|
|
144
|
+
"""The 013-style line with trailing prose is replaced wholesale."""
|
|
145
|
+
plan = self._write(
|
|
146
|
+
tmp_path,
|
|
147
|
+
"# Plan\n\n"
|
|
148
|
+
"**Status**: Completed — shipped: ... **Status: COMPLETED** (2026-10-23).\n\n"
|
|
149
|
+
"Body\n")
|
|
150
|
+
|
|
151
|
+
mark_plan_completed(plan)
|
|
152
|
+
|
|
153
|
+
assert plan.read_text() == "# Plan\n\n> **Status: COMPLETED**\n\nBody\n"
|
|
154
|
+
|
|
155
|
+
def test_inserts_after_title_when_no_status(self, tmp_path):
|
|
156
|
+
plan = self._write(tmp_path, "# Plan\n\nBody\n")
|
|
157
|
+
|
|
158
|
+
mark_plan_completed(plan)
|
|
159
|
+
|
|
160
|
+
assert plan.read_text() == "# Plan\n\n> **Status: COMPLETED**\n\nBody\n"
|
|
161
|
+
|
|
162
|
+
def test_inserts_at_top_when_no_title(self, tmp_path):
|
|
163
|
+
plan = self._write(tmp_path, "THE PLAN")
|
|
164
|
+
|
|
165
|
+
mark_plan_completed(plan)
|
|
166
|
+
|
|
167
|
+
assert plan.read_text() == "> **Status: COMPLETED**\n\nTHE PLAN"
|
|
168
|
+
|
|
169
|
+
def test_title_without_blank_separator_still_gets_separators(self, tmp_path):
|
|
170
|
+
plan = self._write(tmp_path, "# Plan\nBody\n")
|
|
171
|
+
|
|
172
|
+
mark_plan_completed(plan)
|
|
173
|
+
|
|
174
|
+
assert plan.read_text() == "# Plan\n\n> **Status: COMPLETED**\n\nBody\n"
|
|
175
|
+
|
|
176
|
+
def test_status_heading_is_not_treated_as_status_line(self, tmp_path):
|
|
177
|
+
plan = self._write(tmp_path, "# Plan\n\n## Status history\n\nBody\n")
|
|
178
|
+
|
|
179
|
+
mark_plan_completed(plan)
|
|
180
|
+
|
|
181
|
+
assert plan.read_text() == (
|
|
182
|
+
"# Plan\n\n> **Status: COMPLETED**\n\n## Status history\n\nBody\n")
|
|
183
|
+
|
|
184
|
+
def test_deep_status_line_is_ignored_and_stamp_inserted_after_title(self, tmp_path):
|
|
185
|
+
"""Only the top of the doc is scanned; a status buried beyond the
|
|
186
|
+
first ten lines is not the 'existing status recorded' — the stamp
|
|
187
|
+
goes in after the title and the deep line stays untouched."""
|
|
188
|
+
filler = "\n".join(f"line {i}" for i in range(1, 15))
|
|
189
|
+
plan = self._write(tmp_path,
|
|
190
|
+
f"# Plan\n\n{filler}\n> **Status: IN PROGRESS**\n")
|
|
191
|
+
|
|
192
|
+
mark_plan_completed(plan)
|
|
193
|
+
|
|
194
|
+
text = plan.read_text()
|
|
195
|
+
assert text.startswith("# Plan\n\n> **Status: COMPLETED**\n\n")
|
|
196
|
+
assert "> **Status: IN PROGRESS**" in text
|
|
197
|
+
|
|
198
|
+
def test_idempotent_rerun(self, tmp_path):
|
|
199
|
+
plan = self._write(tmp_path, "# Plan\n\n> **Status: DONE**\n\nBody\n")
|
|
200
|
+
|
|
201
|
+
mark_plan_completed(plan)
|
|
202
|
+
once = plan.read_text()
|
|
203
|
+
mark_plan_completed(plan)
|
|
204
|
+
|
|
205
|
+
assert once == "# Plan\n\n> **Status: COMPLETED**\n\nBody\n"
|
|
206
|
+
assert plan.read_text() == once
|
|
207
|
+
|
|
208
|
+
def test_preserves_trailing_newline_absence(self, tmp_path):
|
|
209
|
+
plan = self._write(tmp_path, "# Plan\n\nBody") # no trailing newline
|
|
210
|
+
|
|
211
|
+
mark_plan_completed(plan)
|
|
212
|
+
|
|
213
|
+
assert plan.read_text() == "# Plan\n\n> **Status: COMPLETED**\n\nBody"
|
|
214
|
+
|
|
215
|
+
def test_preserves_trailing_newline(self, tmp_path):
|
|
216
|
+
plan = self._write(tmp_path, "# Plan\n\nBody\n")
|
|
217
|
+
|
|
218
|
+
mark_plan_completed(plan)
|
|
219
|
+
|
|
220
|
+
assert plan.read_text() == "# Plan\n\n> **Status: COMPLETED**\n\nBody\n"
|
|
221
|
+
|
|
222
|
+
def test_accepts_string_path(self, tmp_path):
|
|
223
|
+
plan = self._write(tmp_path, "THE PLAN")
|
|
224
|
+
|
|
225
|
+
mark_plan_completed(str(plan))
|
|
226
|
+
|
|
227
|
+
assert plan.read_text() == "> **Status: COMPLETED**\n\nTHE PLAN"
|
|
@@ -109,3 +109,83 @@ def test_trigger_errors_when_cli_absent():
|
|
|
109
109
|
|
|
110
110
|
assert result.returncode != 0
|
|
111
111
|
assert "pipx install" in result.stderr
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _stub_env(tmp_path, *, cli_body='echo "cli-ran: $*"', pipx_body="exit 0"):
|
|
115
|
+
bin_dir = tmp_path / "bin"
|
|
116
|
+
bin_dir.mkdir()
|
|
117
|
+
_make_stub(bin_dir, "agentic-dev-loop", cli_body)
|
|
118
|
+
_make_stub(bin_dir, "pipx", pipx_body)
|
|
119
|
+
return bin_dir
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_trigger_rejects_both_flags(tmp_path):
|
|
123
|
+
bin_dir = _stub_env(tmp_path)
|
|
124
|
+
|
|
125
|
+
result = _run_script("--plan", "plan.md", "--aidlc-unit", "unit-3",
|
|
126
|
+
env=_env_with(bin_dir))
|
|
127
|
+
|
|
128
|
+
assert result.returncode != 0
|
|
129
|
+
assert ("pass exactly one of --plan <path> or --aidlc-unit <slug>, "
|
|
130
|
+
"not both") in result.stderr
|
|
131
|
+
assert "cli-ran:" not in result.stdout
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_trigger_requires_exactly_one_flag(tmp_path):
|
|
135
|
+
bin_dir = _stub_env(tmp_path)
|
|
136
|
+
|
|
137
|
+
result = _run_script(env=_env_with(bin_dir))
|
|
138
|
+
|
|
139
|
+
assert result.returncode != 0
|
|
140
|
+
assert ("pass exactly one of --plan <path> or --aidlc-unit <slug>"
|
|
141
|
+
in result.stderr)
|
|
142
|
+
assert "cli-ran:" not in result.stdout
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_trigger_forwards_aidlc_unit(tmp_path):
|
|
146
|
+
bin_dir = _stub_env(tmp_path)
|
|
147
|
+
|
|
148
|
+
result = _run_script("--aidlc-unit", "unit-3", env=_env_with(bin_dir))
|
|
149
|
+
|
|
150
|
+
assert result.returncode == 0, result.stderr
|
|
151
|
+
assert "cli-ran:" in result.stdout
|
|
152
|
+
assert "--aidlc-unit unit-3" in result.stdout
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_trigger_unit_positional_is_aidlc_shorthand(tmp_path):
|
|
156
|
+
bin_dir = _stub_env(tmp_path)
|
|
157
|
+
|
|
158
|
+
result = _run_script("unit-3", "--dry-run", env=_env_with(bin_dir))
|
|
159
|
+
|
|
160
|
+
assert result.returncode == 0, result.stderr
|
|
161
|
+
assert "--aidlc-unit unit-3" in result.stdout
|
|
162
|
+
assert "--dry-run" in result.stdout
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_trigger_passes_other_flags_through(tmp_path):
|
|
166
|
+
bin_dir = _stub_env(tmp_path)
|
|
167
|
+
|
|
168
|
+
result = _run_script("--plan", "plan.md", "--dry-run", "--max-attempts", "3",
|
|
169
|
+
env=_env_with(bin_dir))
|
|
170
|
+
|
|
171
|
+
assert result.returncode == 0, result.stderr
|
|
172
|
+
assert "--plan plan.md --dry-run --max-attempts 3" in result.stdout
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_trigger_flag_missing_value_is_usage_error(tmp_path):
|
|
176
|
+
bin_dir = _stub_env(tmp_path)
|
|
177
|
+
|
|
178
|
+
result = _run_script("--plan", env=_env_with(bin_dir))
|
|
179
|
+
|
|
180
|
+
assert result.returncode != 0
|
|
181
|
+
assert "--plan requires a path" in result.stderr
|
|
182
|
+
assert "cli-ran:" not in result.stdout
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_trigger_equals_forms_accepted(tmp_path):
|
|
186
|
+
bin_dir = _stub_env(tmp_path)
|
|
187
|
+
|
|
188
|
+
result = _run_script("--aidlc-unit=unit-3", env=_env_with(bin_dir))
|
|
189
|
+
|
|
190
|
+
assert result.returncode == 0, result.stderr
|
|
191
|
+
assert "--aidlc-unit unit-3" in result.stdout
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/core/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/core/results.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/core/verdict.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/engine/__init__.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/engine/actions.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/engine/agents.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/engine/dryrun.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/engine/graph.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/engine/hostboundary.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/engine/outputs.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/engine/registry.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/engine/secrets.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/engine/stages.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/viz/__init__.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop/viz/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/src/agentic_dev_loop.egg-info/requires.txt
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/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
|
{agentic_dev_loop-1.0.0a12 → agentic_dev_loop-1.0.0a13}/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
|