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.
Files changed (138) hide show
  1. {drydock_cli-3.0.123/drydock_cli.egg-info → drydock_cli-3.0.125}/PKG-INFO +1 -1
  2. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/agent.py +34 -0
  3. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/config.py +4 -0
  4. drydock_cli-3.0.125/drydock/verification.py +34 -0
  5. {drydock_cli-3.0.123 → drydock_cli-3.0.125/drydock_cli.egg-info}/PKG-INFO +1 -1
  6. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock_cli.egg-info/SOURCES.txt +2 -0
  7. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/pyproject.toml +1 -1
  8. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_failure_loop.py +3 -3
  9. drydock_cli-3.0.125/tests/test_verification_gate.py +71 -0
  10. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/LICENSE +0 -0
  11. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/NOTICE +0 -0
  12. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/README.md +0 -0
  13. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/__init__.py +0 -0
  14. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/__main__.py +0 -0
  15. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/advisor.py +0 -0
  16. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/bash_safety.py +0 -0
  17. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/builtin_skills/__init__.py +0 -0
  18. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/builtin_skills/ml-data.md +0 -0
  19. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/builtin_skills/ml-debug.md +0 -0
  20. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/builtin_skills/ml-finetune.md +0 -0
  21. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/builtin_skills/ml-metrics.md +0 -0
  22. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/builtin_skills/ml-rl.md +0 -0
  23. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/builtin_skills/ml-train.md +0 -0
  24. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/builtin_skills/rmf-categorize.md +0 -0
  25. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/builtin_skills/rmf-control.md +0 -0
  26. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/builtin_skills/rmf-poam.md +0 -0
  27. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/builtin_skills/rmf-review.md +0 -0
  28. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/builtin_skills/stig-assess.md +0 -0
  29. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/builtin_skills/stig-remediate.md +0 -0
  30. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/cci.py +0 -0
  31. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/cli.py +0 -0
  32. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/compaction.py +0 -0
  33. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/detect.py +0 -0
  34. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/extract.py +0 -0
  35. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/gittools.py +0 -0
  36. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/graphrag.py +0 -0
  37. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/guards.py +0 -0
  38. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/loop_detect.py +0 -0
  39. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/mcp.py +0 -0
  40. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/poam.py +0 -0
  41. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/providers.py +0 -0
  42. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/recipes.py +0 -0
  43. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/rmf.py +0 -0
  44. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/rmf_graph.py +0 -0
  45. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/skills.py +0 -0
  46. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/stig.py +0 -0
  47. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/suggest.py +0 -0
  48. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/task_state.py +0 -0
  49. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/tool_registry.py +0 -0
  50. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/tools/__init__.py +0 -0
  51. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/tui/__init__.py +0 -0
  52. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/tui/app.py +0 -0
  53. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/tui/approval.py +0 -0
  54. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/tui/messages.py +0 -0
  55. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/tui/widgets.py +0 -0
  56. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/tuning.py +0 -0
  57. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock/web.py +0 -0
  58. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock_cli.egg-info/dependency_links.txt +0 -0
  59. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock_cli.egg-info/entry_points.txt +0 -0
  60. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock_cli.egg-info/requires.txt +0 -0
  61. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/drydock_cli.egg-info/top_level.txt +0 -0
  62. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/setup.cfg +0 -0
  63. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_advisor.py +0 -0
  64. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_approval.py +0 -0
  65. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_back_command.py +0 -0
  66. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_bash_background.py +0 -0
  67. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_bash_binary_output.py +0 -0
  68. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_bash_crossplatform.py +0 -0
  69. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_bash_output_bounding.py +0 -0
  70. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_bash_process_group.py +0 -0
  71. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_bash_safety.py +0 -0
  72. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_bash_sanitize.py +0 -0
  73. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_bash_shell.py +0 -0
  74. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_bash_stdin.py +0 -0
  75. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_bash_stop_partial.py +0 -0
  76. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_bash_timeout_network.py +0 -0
  77. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_bash_timeout_param.py +0 -0
  78. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_cci.py +0 -0
  79. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_cli_agents.py +0 -0
  80. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_compact_command.py +0 -0
  81. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_compaction.py +0 -0
  82. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_config.py +0 -0
  83. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_config_migration.py +0 -0
  84. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_context_limit_config.py +0 -0
  85. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_detect.py +0 -0
  86. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_dispatch.py +0 -0
  87. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_e2e_connected.py +0 -0
  88. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_edit_replace_all.py +0 -0
  89. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_empty_response.py +0 -0
  90. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_extract.py +0 -0
  91. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_first_run_setup.py +0 -0
  92. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_gittools.py +0 -0
  93. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_glob_edit_edges.py +0 -0
  94. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_graphify_example.py +0 -0
  95. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_graphrag.py +0 -0
  96. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_graphrag_quoted_path.py +0 -0
  97. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_grep_and_read_robust.py +0 -0
  98. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_guards_and_tools.py +0 -0
  99. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_hallucinated_tools.py +0 -0
  100. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_leaked_tool_call.py +0 -0
  101. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_loop_detect.py +0 -0
  102. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_mcp.py +0 -0
  103. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_oneshot_unreachable.py +0 -0
  104. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_overthink_interrupt.py +0 -0
  105. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_plan_autocontinue.py +0 -0
  106. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_poam.py +0 -0
  107. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_providers_unreachable.py +0 -0
  108. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_read_index.py +0 -0
  109. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_recipes.py +0 -0
  110. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_repetition_interrupt.py +0 -0
  111. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_rmf.py +0 -0
  112. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_rmf_graph.py +0 -0
  113. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_rmf_stig_graph.py +0 -0
  114. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_runaway_repetition.py +0 -0
  115. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_screenshot.py +0 -0
  116. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_server_probe.py +0 -0
  117. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_skills.py +0 -0
  118. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_stall_retry.py +0 -0
  119. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_stig.py +0 -0
  120. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_stop.py +0 -0
  121. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_streaming_newlines.py +0 -0
  122. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_subagent.py +0 -0
  123. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_suggest.py +0 -0
  124. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_system_prompt_help.py +0 -0
  125. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_task_state.py +0 -0
  126. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_todo.py +0 -0
  127. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_tool_arg_coercion.py +0 -0
  128. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_tool_arg_coercion_more.py +0 -0
  129. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_tool_arg_parsing.py +0 -0
  130. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_tools_undo.py +0 -0
  131. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_tui.py +0 -0
  132. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_tuning.py +0 -0
  133. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_viewimage.py +0 -0
  134. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_vision_input.py +0 -0
  135. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_web_tools.py +0 -0
  136. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_windows_shell.py +0 -0
  137. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_write_content_coerce.py +0 -0
  138. {drydock_cli-3.0.123 → drydock_cli-3.0.125}/tests/test_xccdf.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.123
3
+ Version: 3.0.125
4
4
  Summary: Drydock — a local, provider-agnostic terminal coding agent for local LLMs
5
5
  Author: Frank Bobe III
6
6
  License-Expression: Apache-2.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))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.123
3
+ Version: 3.0.125
4
4
  Summary: Drydock — a local, provider-agnostic terminal coding agent for local LLMs
5
5
  Author: Frank Bobe III
6
6
  License-Expression: Apache-2.0
@@ -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.123"
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