agentic-dev-loop 1.0.0a14__tar.gz → 1.0.0a15__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.0a14 → agentic_dev_loop-1.0.0a15}/PKG-INFO +1 -1
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/pyproject.toml +1 -1
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/core/config.py +3 -1
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/engine/stages.py +17 -2
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/viz/server.py +12 -3
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop.egg-info/PKG-INFO +1 -1
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop.egg-info/SOURCES.txt +1 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/tests/test_config.py +22 -0
- agentic_dev_loop-1.0.0a15/tests/test_qa_prompt.py +29 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/tests/test_stages.py +59 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/tests/test_verdict.py +20 -1
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/tests/test_viz.py +22 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/LICENSE +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/README.md +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/setup.cfg +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/core/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/core/results.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/core/verdict.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/engine/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/engine/actions.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/engine/agents.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/engine/cli.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/engine/deps.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/engine/dryrun.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/engine/graph.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/engine/hostboundary.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/engine/isolation.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/engine/llm.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/engine/loaders.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/engine/outputs.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/engine/proc.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/engine/registry.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/engine/secrets.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/py.typed +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/viz/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/viz/__main__.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/tests/test_actions.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/tests/test_agents.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/tests/test_aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/tests/test_cli.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/tests/test_deps.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/tests/test_dry_run.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/tests/test_e2e_fixtures.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/tests/test_graph.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/tests/test_hostboundary.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/tests/test_hostboundary_integration.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/tests/test_isolation.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/tests/test_isolation_integration.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/tests/test_isolation_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/tests/test_llm.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/tests/test_loaders.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/tests/test_module_contract.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/tests/test_outputs.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/tests/test_outputs_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/tests/test_proc.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/tests/test_publish.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/tests/test_registry.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/tests/test_repo_ascii.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/tests/test_results.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/tests/test_sdk_compat.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/tests/test_secrets.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/tests/test_secrets_integration.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/tests/test_secrets_pipeline.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/tests/test_source_resolution.py +0 -0
- {agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/tests/test_trigger_script.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.0a15"
|
|
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"
|
|
@@ -19,6 +19,7 @@ class VerdictSpec(BaseModel):
|
|
|
19
19
|
|
|
20
20
|
pass_: str = Field(alias="pass")
|
|
21
21
|
fail: str = Field(alias="fail")
|
|
22
|
+
unverifiable: str | None = None
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
class Route(BaseModel):
|
|
@@ -131,6 +132,7 @@ class StepConfig(BaseModel):
|
|
|
131
132
|
secrets: list[str] = [] # declared ref names; action steps reject
|
|
132
133
|
on_pass: RouteTarget | None = None
|
|
133
134
|
on_fail: RouteTarget | None = None
|
|
135
|
+
on_unverifiable: RouteTarget | None = None
|
|
134
136
|
|
|
135
137
|
@property
|
|
136
138
|
def kind(self) -> str:
|
|
@@ -286,7 +288,7 @@ class PipelineConfig(BaseModel):
|
|
|
286
288
|
raise ValueError(f"step name {n!r} collides with a reserved terminal")
|
|
287
289
|
valid = set(names) | TERMINALS
|
|
288
290
|
for step in self.steps:
|
|
289
|
-
for route in (step.on_pass, step.on_fail):
|
|
291
|
+
for route in (step.on_pass, step.on_fail, step.on_unverifiable):
|
|
290
292
|
target = route_target(route)
|
|
291
293
|
if target is not None and target not in valid:
|
|
292
294
|
raise ValueError(
|
{agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/engine/stages.py
RENAMED
|
@@ -254,8 +254,13 @@ def _unclear_note(runtime: Runtime, step: StepConfig, attempt: int,
|
|
|
254
254
|
captured response when it exists, so real findings are never swallowed."""
|
|
255
255
|
expected = ""
|
|
256
256
|
if step.verdicts is not None:
|
|
257
|
-
|
|
258
|
-
|
|
257
|
+
if step.verdicts.unverifiable is not None:
|
|
258
|
+
expected = (f" (expected `VERDICT: {step.verdicts.pass_}`, "
|
|
259
|
+
f"`VERDICT: {step.verdicts.fail}`, or "
|
|
260
|
+
f"`VERDICT: {step.verdicts.unverifiable}` on its own line)")
|
|
261
|
+
else:
|
|
262
|
+
expected = (f" (expected `VERDICT: {step.verdicts.pass_}` or "
|
|
263
|
+
f"`VERDICT: {step.verdicts.fail}` on its own line)")
|
|
259
264
|
if truncated:
|
|
260
265
|
lead = (f"{step.name} agent was cut short on attempt {attempt} "
|
|
261
266
|
f"(hit the {max_iterations}-turn limit or was stopped "
|
|
@@ -282,6 +287,8 @@ def make_agent_node(runtime: Runtime, step: StepConfig):
|
|
|
282
287
|
verdicts_map = None
|
|
283
288
|
if step.verdicts is not None:
|
|
284
289
|
verdicts_map = {step.verdicts.pass_: "pass", step.verdicts.fail: "fail"}
|
|
290
|
+
if step.verdicts.unverifiable is not None:
|
|
291
|
+
verdicts_map[step.verdicts.unverifiable] = "unverifiable"
|
|
285
292
|
captures = step.artifact is not None or bool(step.produces)
|
|
286
293
|
|
|
287
294
|
def node(state: dict) -> dict:
|
|
@@ -334,6 +341,10 @@ def make_agent_node(runtime: Runtime, step: StepConfig):
|
|
|
334
341
|
print(f"[{subject}] {step.name} VERDICT: PASS on attempt {attempt}.",
|
|
335
342
|
flush=True)
|
|
336
343
|
return {"step_verdicts": merged, "failed": False}
|
|
344
|
+
if verdict == "unverifiable":
|
|
345
|
+
print(f"[{subject}] {step.name} qa verdict: UNVERIFIABLE "
|
|
346
|
+
f"(moves on).", flush=True)
|
|
347
|
+
return {"step_verdicts": merged, "failed": False}
|
|
337
348
|
print(f"[{subject}] {step.name} verdict: fail on attempt {attempt}.",
|
|
338
349
|
flush=True)
|
|
339
350
|
update = {"step_verdicts": merged,
|
|
@@ -519,6 +530,10 @@ def make_router(runtime: Runtime, step: StepConfig):
|
|
|
519
530
|
verdict = state["step_verdicts"].get(step.name)
|
|
520
531
|
if verdict == "pass":
|
|
521
532
|
return resolve(step.on_pass)
|
|
533
|
+
if verdict == "unverifiable":
|
|
534
|
+
if step.on_unverifiable is not None:
|
|
535
|
+
return resolve(step.on_unverifiable)
|
|
536
|
+
return resolve(step.on_pass)
|
|
522
537
|
if verdict == "fail":
|
|
523
538
|
if step.on_fail is None:
|
|
524
539
|
return END
|
|
@@ -472,6 +472,7 @@ PAGE = """<!doctype html>
|
|
|
472
472
|
background: #f0f0f0; color: #333; }
|
|
473
473
|
.badge-pass { background: #e8f5e9; color: #2e7d32; }
|
|
474
474
|
.badge-fail { background: #ffebee; color: #c62828; }
|
|
475
|
+
.badge-unverifiable { background: #ede7f6; color: #4527a0; }
|
|
475
476
|
.badge-unclear { background: #fff8e1; color: #f57f17; }
|
|
476
477
|
#stages { flex: 1 1 auto; min-height: 0; overflow-y: auto; display: flex;
|
|
477
478
|
flex-direction: column; gap: 8px; }
|
|
@@ -487,6 +488,7 @@ PAGE = """<!doctype html>
|
|
|
487
488
|
.status-pass { color: #2e7d32; }
|
|
488
489
|
.status-fail { color: #b26a00; }
|
|
489
490
|
.status-fatal { color: #c62828; }
|
|
491
|
+
.status-unverifiable { color: #7e57c2; }
|
|
490
492
|
.console-wrap { max-height: 240px; overflow-y: auto; background: #1e1e1e; }
|
|
491
493
|
.stage-console { margin: 0; padding: 8px 10px; background: #1e1e1e;
|
|
492
494
|
color: #e0e0e0; font: 12px/1.5 ui-monospace, SFMono-Regular,
|
|
@@ -543,6 +545,7 @@ PAGE = """<!doctype html>
|
|
|
543
545
|
var currentVerdicts = {};
|
|
544
546
|
var verdictFail = new Set();
|
|
545
547
|
var verdictPass = new Set();
|
|
548
|
+
var verdictUnverifiable = new Set();
|
|
546
549
|
var fatalNodes = new Set();
|
|
547
550
|
var maxAttempts = null;
|
|
548
551
|
var attempt = null;
|
|
@@ -610,6 +613,7 @@ PAGE = """<!doctype html>
|
|
|
610
613
|
if (nodeStatus[n] === "fatal") { return "fatal"; }
|
|
611
614
|
if (verdictFail.has(n)) { return "fail"; }
|
|
612
615
|
if (verdictPass.has(n)) { return "pass"; }
|
|
616
|
+
if (verdictUnverifiable.has(n)) { return "unverifiable"; }
|
|
613
617
|
return nodeStatus[n] || "pending";
|
|
614
618
|
}
|
|
615
619
|
|
|
@@ -622,6 +626,7 @@ PAGE = """<!doctype html>
|
|
|
622
626
|
b.className = "badge";
|
|
623
627
|
if (v === "pass") { b.className += " badge-pass"; }
|
|
624
628
|
else if (v === "fail") { b.className += " badge-fail"; }
|
|
629
|
+
else if (v === "unverifiable") { b.className += " badge-unverifiable"; }
|
|
625
630
|
else { b.className += " badge-unclear"; }
|
|
626
631
|
b.textContent = step + ": " + (v === null || v === undefined ? "no verdict" : v);
|
|
627
632
|
box.appendChild(b);
|
|
@@ -632,6 +637,7 @@ PAGE = """<!doctype html>
|
|
|
632
637
|
verdictSteps.clear();
|
|
633
638
|
verdictPass.clear();
|
|
634
639
|
verdictFail.clear();
|
|
640
|
+
verdictUnverifiable.clear();
|
|
635
641
|
currentVerdicts = {};
|
|
636
642
|
nodeStatus = {};
|
|
637
643
|
stageWord = {};
|
|
@@ -653,6 +659,7 @@ PAGE = """<!doctype html>
|
|
|
653
659
|
"classDef pass fill:#2e7d32,stroke:#2e7d32,color:#fff",
|
|
654
660
|
"classDef fail fill:#ffb300,stroke:#ffb300,color:#fff",
|
|
655
661
|
"classDef fatal fill:#c62828,stroke:#c62828,color:#fff",
|
|
662
|
+
"classDef unverifiable fill:#7e57c2,stroke:#7e57c2,color:#fff",
|
|
656
663
|
"classDef pending fill:#f2f0ff,stroke:#555,color:#000"
|
|
657
664
|
];
|
|
658
665
|
var classLines = Object.keys(nodeStatus).filter(function (n) {
|
|
@@ -748,6 +755,7 @@ PAGE = """<!doctype html>
|
|
|
748
755
|
var v = currentVerdicts[node];
|
|
749
756
|
if (v === "pass") { suffix = " (verdict: PASS)"; }
|
|
750
757
|
else if (v === "fail") { suffix = " (verdict: FAIL)"; }
|
|
758
|
+
else if (v === "unverifiable") { suffix = " (verdict: UNVERIFIABLE)"; }
|
|
751
759
|
}
|
|
752
760
|
status.textContent = (word ? " " + word : "") + suffix;
|
|
753
761
|
header.className = "stage-header status-" + statusOf(node);
|
|
@@ -1064,9 +1072,10 @@ PAGE = """<!doctype html>
|
|
|
1064
1072
|
Object.keys(ev.step_verdicts).forEach(function (k) {
|
|
1065
1073
|
verdictSteps.add(k);
|
|
1066
1074
|
var v = ev.step_verdicts[k];
|
|
1067
|
-
if (v === "pass") { verdictPass.add(k); verdictFail.delete(k); }
|
|
1068
|
-
else if (v === "fail") { verdictFail.add(k); verdictPass.delete(k); }
|
|
1069
|
-
else { verdictPass.delete(k); verdictFail.delete(k); }
|
|
1075
|
+
if (v === "pass") { verdictPass.add(k); verdictFail.delete(k); verdictUnverifiable.delete(k); }
|
|
1076
|
+
else if (v === "fail") { verdictFail.add(k); verdictPass.delete(k); verdictUnverifiable.delete(k); }
|
|
1077
|
+
else if (v === "unverifiable") { verdictUnverifiable.add(k); verdictPass.delete(k); verdictFail.delete(k); }
|
|
1078
|
+
else { verdictPass.delete(k); verdictFail.delete(k); verdictUnverifiable.delete(k); }
|
|
1070
1079
|
currentVerdicts[k] = v;
|
|
1071
1080
|
});
|
|
1072
1081
|
updateBadges();
|
|
@@ -195,6 +195,28 @@ def test_unknown_verdicts_key_rejected(tmp_path):
|
|
|
195
195
|
_load(tmp_path, data)
|
|
196
196
|
|
|
197
197
|
|
|
198
|
+
def test_three_keyword_verdict_and_on_unverifiable_load(tmp_path):
|
|
199
|
+
data = _base()
|
|
200
|
+
data["steps"][0] = {
|
|
201
|
+
"name": "qa", "agent": "qa", "artifact": "qa-report.md",
|
|
202
|
+
"verdicts": {"pass": "PASS", "fail": "FAIL",
|
|
203
|
+
"unverifiable": "UNVERIFIABLE"},
|
|
204
|
+
"on_pass": "success", "on_unverifiable": "success",
|
|
205
|
+
}
|
|
206
|
+
step = _load(tmp_path, data).steps[0]
|
|
207
|
+
assert step.verdicts.pass_ == "PASS"
|
|
208
|
+
assert step.verdicts.fail == "FAIL"
|
|
209
|
+
assert step.verdicts.unverifiable == "UNVERIFIABLE"
|
|
210
|
+
assert step.on_unverifiable == "success"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_dangling_on_unverifiable_rejected(tmp_path):
|
|
214
|
+
data = _base()
|
|
215
|
+
data["steps"][0]["on_unverifiable"] = "nope"
|
|
216
|
+
with pytest.raises(ValidationError, match="unknown route target 'nope'"):
|
|
217
|
+
_load(tmp_path, data)
|
|
218
|
+
|
|
219
|
+
|
|
198
220
|
def test_dangling_on_pass_rejected(tmp_path):
|
|
199
221
|
data = _base()
|
|
200
222
|
data["steps"][0]["on_pass"] = "nope"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""QA prompt contract (024): the no-substitution rule and the three-way
|
|
2
|
+
verdict rules live in the dogfood copy of sdk_agents/qa.agent.md."""
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
QA_PROMPT = Path(__file__).parent.parent / "agentic-dev-loop" / "sdk_agents" / "qa.agent.md"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _text() -> str:
|
|
10
|
+
return QA_PROMPT.read_text(encoding="utf-8")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_no_substitution_rule_present():
|
|
14
|
+
text = _text()
|
|
15
|
+
assert "Never substitute unrelated generic probes" in text
|
|
16
|
+
assert "UNVERIFIABLE" in text
|
|
17
|
+
assert "blocking reason" in text
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_unverifiable_verdict_rules_present():
|
|
21
|
+
text = _text()
|
|
22
|
+
assert "VERDICT: PASS" in text
|
|
23
|
+
assert "VERDICT: UNVERIFIABLE" in text
|
|
24
|
+
assert "VERDICT: FAIL" in text
|
|
25
|
+
assert "## UNVERIFIABLE" in text
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_inconclusive_budget_rule_retained():
|
|
29
|
+
assert "INCONCLUSIVE" in _text()
|
|
@@ -251,6 +251,36 @@ class TestAgentNode:
|
|
|
251
251
|
make_agent_node(runtime, step)(make_state())
|
|
252
252
|
assert fake_agents.calls[0]["max_iterations"] == 200
|
|
253
253
|
|
|
254
|
+
def test_unverifiable_verdict_records_and_moves_on(self, runtime,
|
|
255
|
+
fake_agents, capsys):
|
|
256
|
+
step = make_cfg([
|
|
257
|
+
{"name": "dev", "agent": "dev", "on_pass": "qa"},
|
|
258
|
+
{"name": "qa", "agent": "qa", "artifact": "qa-report.md",
|
|
259
|
+
"verdicts": {"pass": "PASS", "fail": "FAIL",
|
|
260
|
+
"unverifiable": "UNVERIFIABLE"},
|
|
261
|
+
"on_pass": "commit", "on_fail": {"goto": "dev", "retry": True},
|
|
262
|
+
"on_unverifiable": "commit"},
|
|
263
|
+
{"name": "commit", "action": "commit", "on_pass": "success"}]).steps[1]
|
|
264
|
+
fake_agents.set("qa", [("write", "qa-report.md", "VERDICT: UNVERIFIABLE")])
|
|
265
|
+
update = make_agent_node(runtime, step)(make_state())
|
|
266
|
+
assert update["step_verdicts"] == {"qa": "unverifiable"}
|
|
267
|
+
assert update["failed"] is False
|
|
268
|
+
assert "notes" not in update
|
|
269
|
+
assert "UNVERIFIABLE (moves on)" in capsys.readouterr().out
|
|
270
|
+
|
|
271
|
+
def test_unclear_note_mentions_third_keyword(self, runtime, fake_agents):
|
|
272
|
+
step = make_cfg([
|
|
273
|
+
{"name": "qa", "agent": "qa", "artifact": "qa-report.md",
|
|
274
|
+
"verdicts": {"pass": "PASS", "fail": "FAIL",
|
|
275
|
+
"unverifiable": "UNVERIFIABLE"},
|
|
276
|
+
"on_pass": "success"}]).steps[0]
|
|
277
|
+
fake_agents.set("qa", [("write", "qa-report.md", "undecided")])
|
|
278
|
+
update = make_agent_node(runtime, step)(make_state())
|
|
279
|
+
(note,) = update["notes"]
|
|
280
|
+
assert "VERDICT: PASS" in note
|
|
281
|
+
assert "VERDICT: FAIL" in note
|
|
282
|
+
assert "VERDICT: UNVERIFIABLE" in note
|
|
283
|
+
|
|
254
284
|
|
|
255
285
|
class TestCommandNode:
|
|
256
286
|
def _node(self, runtime, steps, idx=0):
|
|
@@ -464,6 +494,35 @@ class TestRouter:
|
|
|
464
494
|
router = make_router(runtime, cfg.steps[1])
|
|
465
495
|
assert router(make_state(step_verdicts={"c": "fail"})) == "a"
|
|
466
496
|
|
|
497
|
+
def test_unverifiable_routes_on_unverifiable(self, runtime):
|
|
498
|
+
cfg = make_cfg([
|
|
499
|
+
{"name": "dev", "agent": "dev", "on_pass": "qa"},
|
|
500
|
+
{"name": "qa", "agent": "qa", "artifact": "qa-report.md",
|
|
501
|
+
"verdicts": {"pass": "PASS", "fail": "FAIL",
|
|
502
|
+
"unverifiable": "UNVERIFIABLE"},
|
|
503
|
+
"on_pass": "commit", "on_fail": {"goto": "dev", "retry": True},
|
|
504
|
+
"on_unverifiable": "commit"},
|
|
505
|
+
{"name": "commit", "action": "commit", "on_pass": "success"},
|
|
506
|
+
])
|
|
507
|
+
runtime = dataclasses.replace(runtime, cfg=cfg)
|
|
508
|
+
router = make_router(runtime, cfg.steps[1])
|
|
509
|
+
assert router(make_state(step_verdicts={"qa": "unverifiable"})) == "commit"
|
|
510
|
+
|
|
511
|
+
def test_unverifiable_falls_back_to_on_pass_and_never_retries(self, runtime):
|
|
512
|
+
cfg = make_cfg([
|
|
513
|
+
{"name": "dev", "agent": "dev", "on_pass": "qa"},
|
|
514
|
+
{"name": "qa", "agent": "qa", "artifact": "qa-report.md",
|
|
515
|
+
"verdicts": {"pass": "PASS", "fail": "FAIL",
|
|
516
|
+
"unverifiable": "UNVERIFIABLE"},
|
|
517
|
+
"on_pass": "commit", "on_fail": {"goto": "dev", "retry": True}},
|
|
518
|
+
{"name": "commit", "action": "commit", "on_pass": "success"},
|
|
519
|
+
])
|
|
520
|
+
runtime = dataclasses.replace(runtime, cfg=cfg)
|
|
521
|
+
router = make_router(runtime, cfg.steps[1])
|
|
522
|
+
# unset on_unverifiable -> on_pass; never on_fail's bump retry
|
|
523
|
+
assert router(make_state(attempt=1,
|
|
524
|
+
step_verdicts={"qa": "unverifiable"})) == "commit"
|
|
525
|
+
|
|
467
526
|
|
|
468
527
|
class TestBumpNode:
|
|
469
528
|
def test_consumes_attempt_resets_verdicts_rebuilds_cleans(self, runtime):
|
|
@@ -3,10 +3,11 @@ Result API (BR-2): decisions route as `Result.ok(Verdict)`; verdict lines
|
|
|
3
3
|
without a decision, or conflicting decisions, are typed errors that feed
|
|
4
4
|
the attempt-bounded retry path - never exceptions, never silent routing."""
|
|
5
5
|
|
|
6
|
-
from agentic_dev_loop.core.verdict import Verdict, VerdictError
|
|
6
|
+
from agentic_dev_loop.core.verdict import Verdict, VerdictError, parse_verdict
|
|
7
7
|
|
|
8
8
|
REVIEW_VERDICTS = {"APPROVED": "approved", "NEEDS CHANGES": "needs_changes"}
|
|
9
9
|
TEST_VERDICTS = {"PASS": "pass", "FAIL": "fail"}
|
|
10
|
+
QA_VERDICTS = {"PASS": "pass", "FAIL": "fail", "UNVERIFIABLE": "unverifiable"}
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
def _parse(text, verdicts):
|
|
@@ -169,6 +170,24 @@ class TestVerdictParsing:
|
|
|
169
170
|
def test_not_unknown_keyword_rejected(self):
|
|
170
171
|
_err("VERDICT: NOT MAYBE", REVIEW_VERDICTS, VerdictError.AMBIGUOUS)
|
|
171
172
|
|
|
173
|
+
def test_parse_unverifiable(self):
|
|
174
|
+
_ok("VERDICT: UNVERIFIABLE", QA_VERDICTS, "unverifiable")
|
|
175
|
+
_ok("VERDICT: UNVERIFIABLE - no docker in the sandbox",
|
|
176
|
+
QA_VERDICTS, "unverifiable")
|
|
177
|
+
_ok("verdict: unverifiable", QA_VERDICTS, "unverifiable")
|
|
178
|
+
|
|
179
|
+
def test_parse_verdict_unverifiable_returns_canonical_value(self):
|
|
180
|
+
assert parse_verdict("VERDICT: UNVERIFIABLE", QA_VERDICTS) == "unverifiable"
|
|
181
|
+
|
|
182
|
+
def test_negated_unverifiable_stays_ambiguous_on_three_key_sets(self):
|
|
183
|
+
_err("VERDICT: NOT UNVERIFIABLE", QA_VERDICTS, VerdictError.AMBIGUOUS)
|
|
184
|
+
|
|
185
|
+
def test_conflicting_pass_and_unverifiable_is_ambiguous(self):
|
|
186
|
+
_err("VERDICT: PASS\nVERDICT: UNVERIFIABLE", QA_VERDICTS,
|
|
187
|
+
VerdictError.AMBIGUOUS)
|
|
188
|
+
assert parse_verdict("VERDICT: PASS\nVERDICT: UNVERIFIABLE",
|
|
189
|
+
QA_VERDICTS) == "retry"
|
|
190
|
+
|
|
172
191
|
|
|
173
192
|
class TestConflictingDecisions:
|
|
174
193
|
"""BR-2.4: conflicting *decisions* are never silently resolved."""
|
|
@@ -734,6 +734,15 @@ class TestServer:
|
|
|
734
734
|
assert "verdictFail" in PAGE
|
|
735
735
|
assert "verdictPass" in PAGE
|
|
736
736
|
|
|
737
|
+
def test_page_constant_has_unverifiable_verdict_support(self):
|
|
738
|
+
assert "classDef unverifiable" in PAGE
|
|
739
|
+
assert ".badge-unverifiable" in PAGE
|
|
740
|
+
assert ".status-unverifiable" in PAGE
|
|
741
|
+
assert "verdictUnverifiable" in PAGE
|
|
742
|
+
# the new value maps to badge-unverifiable, not the unclear fallthrough
|
|
743
|
+
assert 'else if (v === "unverifiable")' in PAGE
|
|
744
|
+
assert " (verdict: UNVERIFIABLE)" in PAGE
|
|
745
|
+
|
|
737
746
|
def test_page_constant_color_palette(self):
|
|
738
747
|
assert 'classDef running fill:#1976d2' in PAGE
|
|
739
748
|
assert 'classDef fail fill:#ffb300' in PAGE
|
|
@@ -1097,6 +1106,19 @@ main();
|
|
|
1097
1106
|
assert snaps[1]["classes"]["test"] == "pass"
|
|
1098
1107
|
assert snaps[1]["classes"]["review"] == "fail"
|
|
1099
1108
|
|
|
1109
|
+
def test_unverifiable_verdict_badge_and_class(self, node, tmp_path):
|
|
1110
|
+
events = [
|
|
1111
|
+
{"type": "run_started", "subject": "s", "max_attempts": 5,
|
|
1112
|
+
"attempt": 1, "thread_id": "t"},
|
|
1113
|
+
{"type": "node_finished", "node": "review", "ok": True},
|
|
1114
|
+
{"type": "state", "attempt": 1,
|
|
1115
|
+
"step_verdicts": {"review": "unverifiable"}, "failed": False},
|
|
1116
|
+
]
|
|
1117
|
+
snaps = self._run_page(node, events, tmp_path)
|
|
1118
|
+
|
|
1119
|
+
assert snaps[0]["badges"] == ["review: unverifiable"]
|
|
1120
|
+
assert snaps[0]["classes"]["review"] == "unverifiable"
|
|
1121
|
+
|
|
1100
1122
|
def test_copy_button_copies_console_text(self, node, tmp_path):
|
|
1101
1123
|
events = [
|
|
1102
1124
|
{"type": "run_started", "subject": "s", "max_attempts": 5,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/core/__init__.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/core/results.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/core/verdict.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/engine/__init__.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/engine/actions.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/engine/agents.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/engine/dryrun.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/engine/graph.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/engine/hostboundary.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/engine/isolation.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/engine/loaders.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/engine/outputs.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/engine/registry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/engine/secrets.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/viz/__init__.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop/viz/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/src/agentic_dev_loop.egg-info/requires.txt
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a14 → agentic_dev_loop-1.0.0a15}/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.0a14 → agentic_dev_loop-1.0.0a15}/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
|