drydock-cli 3.0.123__tar.gz → 3.0.125__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.
- {drydock_cli-3.0.123/drydock_cli.egg-info → drydock_cli-3.0.125}/PKG-INFO +1 -1
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/agent.py +34 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/config.py +4 -0
- drydock_cli-3.0.125/drydock/verification.py +34 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock_cli.egg-info/SOURCES.txt +2 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/pyproject.toml +1 -1
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_failure_loop.py +3 -3
- drydock_cli-3.0.125/tests/test_verification_gate.py +71 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/LICENSE +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/NOTICE +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/README.md +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/advisor.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/builtin_skills/ml-data.md +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/builtin_skills/ml-debug.md +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/builtin_skills/ml-finetune.md +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/builtin_skills/ml-metrics.md +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/builtin_skills/ml-rl.md +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/builtin_skills/ml-train.md +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/cci.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/cli.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/detect.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/extract.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/guards.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/poam.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/providers.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/recipes.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/skills.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/stig.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/suggest.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/task_state.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/tools/__init__.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/tui/app.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/web.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/setup.cfg +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_advisor.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_bash_background.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_bash_binary_output.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_bash_crossplatform.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_bash_sanitize.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_bash_shell.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_bash_stdin.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_bash_stop_partial.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_bash_timeout_param.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_cci.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_config.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_edit_replace_all.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_glob_edit_edges.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_graphify_example.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_graphrag_quoted_path.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_grep_and_read_robust.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_overthink_interrupt.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_poam.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_recipes.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_repetition_interrupt.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_screenshot.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_stall_retry.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_suggest.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_task_state.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_tool_arg_coercion.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_tool_arg_coercion_more.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_viewimage.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_windows_shell.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_write_content_coerce.py +0 -0
- {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_xccdf.py +0 -0
|
@@ -48,6 +48,7 @@ from drydock.compaction import (
|
|
|
48
48
|
)
|
|
49
49
|
from drydock.loop_detect import LoopTracker
|
|
50
50
|
from drydock.task_state import TaskState
|
|
51
|
+
from drydock.verification import looks_like_verification
|
|
51
52
|
from drydock.tuning import (
|
|
52
53
|
filter_tool_schemas,
|
|
53
54
|
hallucinated_tool_message,
|
|
@@ -153,6 +154,8 @@ def run(
|
|
|
153
154
|
return cancel is not None and cancel.is_set()
|
|
154
155
|
tool_call_count = 0
|
|
155
156
|
session_has_edited = False
|
|
157
|
+
ran_verification = False # a test/check/exec command ran (verification gate)
|
|
158
|
+
verify_gate_nudges = 0 # bounded "verify before you finish" nudges
|
|
156
159
|
leaked_call_retries = 0
|
|
157
160
|
plan_continue_nudges = 0 # consecutive "you stopped mid-plan" nudges
|
|
158
161
|
empty_response_nudges = 0 # consecutive "you returned nothing" nudges
|
|
@@ -332,6 +335,33 @@ def run(
|
|
|
332
335
|
),
|
|
333
336
|
})
|
|
334
337
|
continue
|
|
338
|
+
# VERIFICATION GATE (PRD Epic B): a text-only "done" is not evidence.
|
|
339
|
+
# If the agent CHANGED files but never ran a test/check/its own code,
|
|
340
|
+
# don't accept completion — make it verify first. Bounded so it can't
|
|
341
|
+
# wedge; once it runs any check (ran_verification) the gate is satisfied.
|
|
342
|
+
if (
|
|
343
|
+
config.get("verify_gate", True)
|
|
344
|
+
and allow is None # main task only — not scoped sub-agents
|
|
345
|
+
and session_has_edited
|
|
346
|
+
and not ran_verification
|
|
347
|
+
and verify_gate_nudges < 2
|
|
348
|
+
):
|
|
349
|
+
verify_gate_nudges += 1
|
|
350
|
+
state.task.phase = "verify"
|
|
351
|
+
state.messages.append({
|
|
352
|
+
"role": "user",
|
|
353
|
+
"content": (
|
|
354
|
+
"[SYSTEM] You changed files but have not VERIFIED the work. "
|
|
355
|
+
"Before finishing, run a concrete check: the task's own test/"
|
|
356
|
+
"eval/build (pytest, test.sh, make, npm test, …) or run the "
|
|
357
|
+
"code/script you produced and confirm the output is correct "
|
|
358
|
+
"and meets EVERY requirement. Do that now with a tool call; "
|
|
359
|
+
"if it fails, fix it and re-check."
|
|
360
|
+
),
|
|
361
|
+
})
|
|
362
|
+
continue
|
|
363
|
+
if session_has_edited and ran_verification:
|
|
364
|
+
state.task.phase = "complete"
|
|
335
365
|
break
|
|
336
366
|
|
|
337
367
|
# Execute each tool call
|
|
@@ -339,6 +369,10 @@ def run(
|
|
|
339
369
|
tool_call_count += 1
|
|
340
370
|
if tc["name"] in ("Edit", "Write"):
|
|
341
371
|
session_has_edited = True
|
|
372
|
+
elif tc["name"] == "Bash" and looks_like_verification(
|
|
373
|
+
(tc.get("input") or {}).get("command", "")
|
|
374
|
+
):
|
|
375
|
+
ran_verification = True # the agent actually ran a test/check/its code
|
|
342
376
|
|
|
343
377
|
# STOP pressed: don't run the remaining tools, but still record a
|
|
344
378
|
# paired result for each (the assistant message already lists all
|
|
@@ -48,6 +48,10 @@ DEFAULTS: dict[str, object] = {
|
|
|
48
48
|
# system prompt every turn so they survive context compaction — the model can't
|
|
49
49
|
# drift off the original goal on a long task. Set false to disable.
|
|
50
50
|
"task_anchor": True,
|
|
51
|
+
# Verification gate (PRD Epic B): if the model claims "done" after changing
|
|
52
|
+
# files but never ran a test/check/its own code, nudge it to verify first
|
|
53
|
+
# instead of accepting completion. Bounded. Set false to disable.
|
|
54
|
+
"verify_gate": True,
|
|
51
55
|
"theme": "harbor",
|
|
52
56
|
# Optional SECOND model ("advisor") for a stronger second opinion — e.g. a
|
|
53
57
|
# Gemini OpenAI-compatible endpoint on another box. Empty = disabled. The
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Verification gating (Agent-Buildout PRD, Epic B / verification):
|
|
2
|
+
|
|
3
|
+
"A text-only model response is not evidence that a task is complete."
|
|
4
|
+
|
|
5
|
+
The agent loop uses this to decide, when the model claims "done", whether it
|
|
6
|
+
actually VERIFIED — i.e. ran a test, a linter/type-check, a build, or executed the
|
|
7
|
+
code it produced. If it changed files but ran nothing, the loop nudges it to verify
|
|
8
|
+
instead of accepting completion. Deliberately small (a command classifier + the loop
|
|
9
|
+
gate) so it drops into the current loop without a rewrite. Logic original to Drydock.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
|
|
15
|
+
# Explicit test / lint / type-check / build commands.
|
|
16
|
+
_VERIFY_KEYWORDS = re.compile(
|
|
17
|
+
r"\b(pytest|py\.test|unittest|nosetests?|tox|test\.sh|run_tests|"
|
|
18
|
+
r"make(\s+\w+)?|npm\s+(test|run)|yarn\s+(test|run)|pnpm\s+(test|run)|jest|mocha|vitest|"
|
|
19
|
+
r"cargo\s+(test|check|build|run|clippy)|go\s+(test|build|run|vet)|"
|
|
20
|
+
r"ruff|flake8|pylint|mypy|pyright|eslint|tsc|prettier|"
|
|
21
|
+
r"ctest|cmake|gradle|mvn|rspec|phpunit|dotnet\s+test|bats)\b",
|
|
22
|
+
re.I,
|
|
23
|
+
)
|
|
24
|
+
# Executing something — running the produced script/binary counts as checking it.
|
|
25
|
+
_EXEC = re.compile(r"(?:^|[\s;&|(])(?:python3?|node|deno|bun|bash|sh|ruby|perl|"
|
|
26
|
+
r"java|javac|gcc|g\+\+|clang|\./|\.\\)")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def looks_like_verification(cmd) -> bool:
|
|
30
|
+
"""True if a shell command constitutes verifying work — running tests/checks/
|
|
31
|
+
builds, or executing the produced code. Best-effort; never raises."""
|
|
32
|
+
if not cmd or not isinstance(cmd, str):
|
|
33
|
+
return False
|
|
34
|
+
return bool(_VERIFY_KEYWORDS.search(cmd) or _EXEC.search(cmd))
|
|
@@ -29,6 +29,7 @@ drydock/suggest.py
|
|
|
29
29
|
drydock/task_state.py
|
|
30
30
|
drydock/tool_registry.py
|
|
31
31
|
drydock/tuning.py
|
|
32
|
+
drydock/verification.py
|
|
32
33
|
drydock/web.py
|
|
33
34
|
drydock/builtin_skills/__init__.py
|
|
34
35
|
drydock/builtin_skills/ml-data.md
|
|
@@ -126,6 +127,7 @@ tests/test_tool_arg_parsing.py
|
|
|
126
127
|
tests/test_tools_undo.py
|
|
127
128
|
tests/test_tui.py
|
|
128
129
|
tests/test_tuning.py
|
|
130
|
+
tests/test_verification_gate.py
|
|
129
131
|
tests/test_viewimage.py
|
|
130
132
|
tests/test_vision_input.py
|
|
131
133
|
tests/test_web_tools.py
|
|
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|
|
7
7
|
# PyPI distribution name is drydock-cli (the established install name, continued
|
|
8
8
|
# from the retired v2 fork); the import package + CLI command stay `drydock`.
|
|
9
9
|
name = "drydock-cli"
|
|
10
|
-
version = "3.0.
|
|
10
|
+
version = "3.0.125"
|
|
11
11
|
description = "Drydock — a local, provider-agnostic terminal coding agent for local LLMs"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.11"
|
|
@@ -38,7 +38,7 @@ def test_changing_args_do_not_trip_valve(monkeypatch):
|
|
|
38
38
|
monkeypatch.setattr(agent_mod, "stream", lambda **kw: iter([seq.pop(0)]))
|
|
39
39
|
st = AgentState()
|
|
40
40
|
import tempfile
|
|
41
|
-
list(run("go", st, {"model": "m", "cwd": tempfile.mkdtemp()}, "sys"))
|
|
41
|
+
list(run("go", st, {"model": "m", "cwd": tempfile.mkdtemp(), "verify_gate": False}, "sys"))
|
|
42
42
|
assert any(m.get("role") == "assistant" and m.get("content") == "done"
|
|
43
43
|
for m in st.messages)
|
|
44
44
|
|
|
@@ -57,7 +57,7 @@ def test_identical_SUCCESS_streak_ends_turn(monkeypatch):
|
|
|
57
57
|
|
|
58
58
|
monkeypatch.setattr(agent_mod, "stream", counting)
|
|
59
59
|
st = AgentState()
|
|
60
|
-
list(run("go", st, {"model": "m", "cwd": tempfile.mkdtemp()}, "sys"))
|
|
60
|
+
list(run("go", st, {"model": "m", "cwd": tempfile.mkdtemp(), "verify_gate": False}, "sys"))
|
|
61
61
|
assert calls["n"] <= 12 # identical success → stopped at the cap, not 200
|
|
62
62
|
|
|
63
63
|
|
|
@@ -73,7 +73,7 @@ def test_polling_with_changing_result_not_tripped(monkeypatch):
|
|
|
73
73
|
seq.append(AssistantTurn("done", [], 1, 1))
|
|
74
74
|
monkeypatch.setattr(agent_mod, "stream", lambda **kw: iter([seq.pop(0)]))
|
|
75
75
|
st = AgentState()
|
|
76
|
-
list(run("go", st, {"model": "m", "cwd": tempfile.mkdtemp()}, "sys"))
|
|
76
|
+
list(run("go", st, {"model": "m", "cwd": tempfile.mkdtemp(), "verify_gate": False}, "sys"))
|
|
77
77
|
assert any(m.get("content") == "done" for m in st.messages)
|
|
78
78
|
|
|
79
79
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Verification gating (Agent-Buildout PRD Epic B): a text-only 'done' after editing
|
|
2
|
+
files is not accepted until the agent runs a check. Model can't self-declare done."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from drydock import agent
|
|
6
|
+
from drydock.providers import AssistantTurn
|
|
7
|
+
from drydock.verification import looks_like_verification as v
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_classifier():
|
|
11
|
+
for cmd in ("pytest -q", "python -m pytest tests/", "bash test.sh", "make test",
|
|
12
|
+
"npm test", "cargo test", "ruff check .", "mypy src", "python solve.py",
|
|
13
|
+
"./run_check", "node index.js"):
|
|
14
|
+
assert v(cmd), cmd
|
|
15
|
+
for cmd in ("ls -la", "cat file.txt", "grep foo bar", "echo hi", "git status", ""):
|
|
16
|
+
assert not v(cmd), cmd
|
|
17
|
+
assert not v(None) and not v(123)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _drive(cfg, turns):
|
|
21
|
+
"""turns: list of AssistantTurn to yield in order; returns the messages seen."""
|
|
22
|
+
calls = {"i": 0}
|
|
23
|
+
def fake_stream(model, system, messages, tool_schemas, config):
|
|
24
|
+
i = calls["i"]; calls["i"] += 1
|
|
25
|
+
yield turns[min(i, len(turns) - 1)]
|
|
26
|
+
orig = agent.stream; agent.stream = fake_stream
|
|
27
|
+
try:
|
|
28
|
+
st = agent.AgentState()
|
|
29
|
+
list(agent.run("do it", st, cfg, "SYS"))
|
|
30
|
+
return st, calls["i"]
|
|
31
|
+
finally:
|
|
32
|
+
agent.stream = orig
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
CFG = {"model": "gemma4", "max_tokens": 8192, "context_limit": 65536, "verify_gate": True}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_gate_fires_after_edit_without_verification():
|
|
39
|
+
# turn 1: Write a file (no tool result needed — execute() runs the real tool);
|
|
40
|
+
# turn 2: text-only "done". Gate should nudge -> a 3rd call happens.
|
|
41
|
+
write = AssistantTurn("", [{"id": "1", "name": "Write",
|
|
42
|
+
"input": {"file_path": "/tmp/dd_vg.txt", "content": "x"}}], 5, 5)
|
|
43
|
+
done = AssistantTurn("All done!", [], 5, 5)
|
|
44
|
+
st, n_calls = _drive(CFG, [write, done, done])
|
|
45
|
+
# the gate injected a verify nudge → the loop asked the model again (>2 calls)
|
|
46
|
+
assert n_calls >= 3
|
|
47
|
+
assert any("VERIFIED" in m.get("content", "") for m in st.messages if m.get("role") == "user")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_no_gate_when_verification_ran():
|
|
51
|
+
write = AssistantTurn("", [{"id": "1", "name": "Write",
|
|
52
|
+
"input": {"file_path": "/tmp/dd_vg2.txt", "content": "x"}}], 5, 5)
|
|
53
|
+
check = AssistantTurn("", [{"id": "2", "name": "Bash", "input": {"command": "python -c 'print(1)'"}}], 5, 5)
|
|
54
|
+
done = AssistantTurn("Done, verified.", [], 5, 5)
|
|
55
|
+
st, _ = _drive(CFG, [write, check, done])
|
|
56
|
+
assert not any("VERIFIED" in m.get("content", "") for m in st.messages if m.get("role") == "user")
|
|
57
|
+
assert st.task.phase == "complete"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_no_gate_when_no_edits():
|
|
61
|
+
# pure Q&A: no files changed → nothing to verify → accept immediately
|
|
62
|
+
st, _ = _drive(CFG, [AssistantTurn("The answer is 42.", [], 5, 5)])
|
|
63
|
+
assert not any("VERIFIED" in m.get("content", "") for m in st.messages if m.get("role") == "user")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_disabled():
|
|
67
|
+
write = AssistantTurn("", [{"id": "1", "name": "Write",
|
|
68
|
+
"input": {"file_path": "/tmp/dd_vg3.txt", "content": "x"}}], 5, 5)
|
|
69
|
+
done = AssistantTurn("done", [], 5, 5)
|
|
70
|
+
st, _ = _drive({**CFG, "verify_gate": False}, [write, done])
|
|
71
|
+
assert not any("VERIFIED" in m.get("content", "") for m in st.messages if m.get("role") == "user")
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|