drydock-cli 3.0.126__tar.gz → 3.0.127__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 (141) hide show
  1. {drydock_cli-3.0.126/drydock_cli.egg-info → drydock_cli-3.0.127}/PKG-INFO +1 -1
  2. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/agent.py +39 -24
  3. drydock_cli-3.0.127/drydock/verification.py +75 -0
  4. {drydock_cli-3.0.126 → drydock_cli-3.0.127/drydock_cli.egg-info}/PKG-INFO +1 -1
  5. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/pyproject.toml +1 -1
  6. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_verification_gate.py +32 -0
  7. drydock_cli-3.0.126/drydock/verification.py +0 -34
  8. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/LICENSE +0 -0
  9. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/NOTICE +0 -0
  10. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/README.md +0 -0
  11. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/__init__.py +0 -0
  12. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/__main__.py +0 -0
  13. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/advisor.py +0 -0
  14. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/bash_safety.py +0 -0
  15. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/builtin_skills/__init__.py +0 -0
  16. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/builtin_skills/ml-data.md +0 -0
  17. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/builtin_skills/ml-debug.md +0 -0
  18. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/builtin_skills/ml-finetune.md +0 -0
  19. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/builtin_skills/ml-metrics.md +0 -0
  20. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/builtin_skills/ml-rl.md +0 -0
  21. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/builtin_skills/ml-train.md +0 -0
  22. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/builtin_skills/rmf-categorize.md +0 -0
  23. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/builtin_skills/rmf-control.md +0 -0
  24. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/builtin_skills/rmf-poam.md +0 -0
  25. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/builtin_skills/rmf-review.md +0 -0
  26. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/builtin_skills/stig-assess.md +0 -0
  27. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/builtin_skills/stig-remediate.md +0 -0
  28. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/cci.py +0 -0
  29. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/cli.py +0 -0
  30. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/compaction.py +0 -0
  31. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/config.py +0 -0
  32. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/detect.py +0 -0
  33. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/events.py +0 -0
  34. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/extract.py +0 -0
  35. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/gittools.py +0 -0
  36. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/graphrag.py +0 -0
  37. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/guards.py +0 -0
  38. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/loop_detect.py +0 -0
  39. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/mcp.py +0 -0
  40. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/poam.py +0 -0
  41. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/providers.py +0 -0
  42. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/recipes.py +0 -0
  43. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/rmf.py +0 -0
  44. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/rmf_graph.py +0 -0
  45. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/skills.py +0 -0
  46. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/stig.py +0 -0
  47. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/suggest.py +0 -0
  48. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/task_state.py +0 -0
  49. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/tool_registry.py +0 -0
  50. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/tools/__init__.py +0 -0
  51. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/tui/__init__.py +0 -0
  52. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/tui/app.py +0 -0
  53. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/tui/approval.py +0 -0
  54. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/tui/messages.py +0 -0
  55. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/tui/widgets.py +0 -0
  56. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/tuning.py +0 -0
  57. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock/web.py +0 -0
  58. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock_cli.egg-info/SOURCES.txt +0 -0
  59. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock_cli.egg-info/dependency_links.txt +0 -0
  60. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock_cli.egg-info/entry_points.txt +0 -0
  61. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock_cli.egg-info/requires.txt +0 -0
  62. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/drydock_cli.egg-info/top_level.txt +0 -0
  63. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/setup.cfg +0 -0
  64. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_advisor.py +0 -0
  65. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_approval.py +0 -0
  66. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_back_command.py +0 -0
  67. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_bash_background.py +0 -0
  68. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_bash_binary_output.py +0 -0
  69. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_bash_crossplatform.py +0 -0
  70. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_bash_output_bounding.py +0 -0
  71. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_bash_process_group.py +0 -0
  72. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_bash_safety.py +0 -0
  73. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_bash_sanitize.py +0 -0
  74. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_bash_shell.py +0 -0
  75. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_bash_stdin.py +0 -0
  76. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_bash_stop_partial.py +0 -0
  77. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_bash_timeout_network.py +0 -0
  78. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_bash_timeout_param.py +0 -0
  79. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_cci.py +0 -0
  80. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_cli_agents.py +0 -0
  81. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_compact_command.py +0 -0
  82. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_compaction.py +0 -0
  83. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_config.py +0 -0
  84. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_config_migration.py +0 -0
  85. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_context_limit_config.py +0 -0
  86. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_detect.py +0 -0
  87. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_dispatch.py +0 -0
  88. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_e2e_connected.py +0 -0
  89. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_edit_replace_all.py +0 -0
  90. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_empty_response.py +0 -0
  91. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_events.py +0 -0
  92. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_extract.py +0 -0
  93. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_failure_loop.py +0 -0
  94. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_first_run_setup.py +0 -0
  95. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_gittools.py +0 -0
  96. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_glob_edit_edges.py +0 -0
  97. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_graphify_example.py +0 -0
  98. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_graphrag.py +0 -0
  99. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_graphrag_quoted_path.py +0 -0
  100. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_grep_and_read_robust.py +0 -0
  101. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_guards_and_tools.py +0 -0
  102. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_hallucinated_tools.py +0 -0
  103. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_leaked_tool_call.py +0 -0
  104. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_loop_detect.py +0 -0
  105. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_mcp.py +0 -0
  106. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_oneshot_unreachable.py +0 -0
  107. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_overthink_interrupt.py +0 -0
  108. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_plan_autocontinue.py +0 -0
  109. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_poam.py +0 -0
  110. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_providers_unreachable.py +0 -0
  111. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_read_index.py +0 -0
  112. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_recipes.py +0 -0
  113. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_repetition_interrupt.py +0 -0
  114. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_rmf.py +0 -0
  115. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_rmf_graph.py +0 -0
  116. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_rmf_stig_graph.py +0 -0
  117. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_runaway_repetition.py +0 -0
  118. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_screenshot.py +0 -0
  119. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_server_probe.py +0 -0
  120. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_skills.py +0 -0
  121. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_stall_retry.py +0 -0
  122. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_stig.py +0 -0
  123. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_stop.py +0 -0
  124. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_streaming_newlines.py +0 -0
  125. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_subagent.py +0 -0
  126. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_suggest.py +0 -0
  127. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_system_prompt_help.py +0 -0
  128. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_task_state.py +0 -0
  129. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_todo.py +0 -0
  130. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_tool_arg_coercion.py +0 -0
  131. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_tool_arg_coercion_more.py +0 -0
  132. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_tool_arg_parsing.py +0 -0
  133. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_tools_undo.py +0 -0
  134. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_tui.py +0 -0
  135. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_tuning.py +0 -0
  136. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_viewimage.py +0 -0
  137. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_vision_input.py +0 -0
  138. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_web_tools.py +0 -0
  139. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_windows_shell.py +0 -0
  140. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/tests/test_write_content_coerce.py +0 -0
  141. {drydock_cli-3.0.126 → drydock_cli-3.0.127}/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.126
3
+ Version: 3.0.127
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,7 +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
+ from drydock.verification import looks_like_verification, parse_evidence
52
52
  from drydock.events import emit as _emit
53
53
  from drydock.tuning import (
54
54
  filter_tool_schemas,
@@ -159,8 +159,8 @@ def run(
159
159
  return cancel is not None and cancel.is_set()
160
160
  tool_call_count = 0
161
161
  session_has_edited = False
162
- ran_verification = False # a test/check/exec command ran (verification gate)
163
- verify_gate_nudges = 0 # bounded "verify before you finish" nudges
162
+ last_verification = None # VerificationEvidence of the most recent check (gate)
163
+ verify_gate_nudges = 0 # bounded "verify before you finish" nudges
164
164
  leaked_call_retries = 0
165
165
  plan_continue_nudges = 0 # consecutive "you stopped mid-plan" nudges
166
166
  empty_response_nudges = 0 # consecutive "you returned nothing" nudges
@@ -347,32 +347,42 @@ def run(
347
347
  # If the agent CHANGED files but never ran a test/check/its own code,
348
348
  # don't accept completion — make it verify first. Bounded so it can't
349
349
  # wedge; once it runs any check (ran_verification) the gate is satisfied.
350
- if (
350
+ # A text-only "done" after editing is accepted only when a check has
351
+ # PASSED. Never ran one → nudge to VERIFY. Ran one that FAILED → the work
352
+ # isn't done → nudge to REPAIR. Bounded so it can't wedge.
353
+ _needs_gate = (
351
354
  config.get("verify_gate", True)
352
355
  and allow is None # main task only — not scoped sub-agents
353
356
  and session_has_edited
354
- and not ran_verification
355
357
  and verify_gate_nudges < 2
356
- ):
358
+ and (last_verification is None or last_verification.status == "fail")
359
+ )
360
+ if _needs_gate:
357
361
  verify_gate_nudges += 1
358
- state.task.phase = "verify"
359
- _emit(state, "verify_gate", nudge=verify_gate_nudges)
360
- state.messages.append({
361
- "role": "user",
362
- "content": (
362
+ if last_verification is None:
363
+ state.task.phase = "verify"
364
+ _emit(state, "verify_gate", kind="unverified", nudge=verify_gate_nudges)
365
+ msg = (
363
366
  "[SYSTEM] You changed files but have not VERIFIED the work. "
364
- "Before finishing, run a concrete check: the task's own test/"
365
- "eval/build (pytest, test.sh, make, npm test, …) or run the "
366
- "code/script you produced and confirm the output is correct "
367
- "and meets EVERY requirement. Do that now with a tool call; "
368
- "if it fails, fix it and re-check."
369
- ),
370
- })
367
+ "Run a concrete check now: the task's own test/eval/build "
368
+ "(pytest, test.sh, make, npm test, …) or run the code you "
369
+ "produced, and confirm it meets EVERY requirement."
370
+ )
371
+ else: # a check ran and FAILED
372
+ state.task.phase = "repair"
373
+ _emit(state, "verify_gate", kind="failed", nudge=verify_gate_nudges,
374
+ summary=last_verification.summary)
375
+ msg = (
376
+ f"[SYSTEM] Your verification FAILED ({last_verification.summary}). "
377
+ "The task is NOT complete. Read the failure, fix the cause, then "
378
+ "re-run the SAME check until it passes."
379
+ )
380
+ state.messages.append({"role": "user", "content": msg})
371
381
  continue
372
- if session_has_edited and ran_verification:
382
+ if session_has_edited and last_verification and last_verification.status == "pass":
373
383
  state.task.phase = "complete"
374
384
  _emit(state, "done", phase=state.task.phase, edited=session_has_edited,
375
- verified=ran_verification)
385
+ verified=bool(last_verification and last_verification.status == "pass"))
376
386
  break
377
387
 
378
388
  # Execute each tool call
@@ -380,10 +390,6 @@ def run(
380
390
  tool_call_count += 1
381
391
  if tc["name"] in ("Edit", "Write"):
382
392
  session_has_edited = True
383
- elif tc["name"] == "Bash" and looks_like_verification(
384
- (tc.get("input") or {}).get("command", "")
385
- ):
386
- ran_verification = True # the agent actually ran a test/check/its code
387
393
 
388
394
  # STOP pressed: don't run the remaining tools, but still record a
389
395
  # paired result for each (the assistant message already lists all
@@ -435,6 +441,15 @@ def run(
435
441
  # advisory note when the same call is made again.
436
442
  result = loop_tracker.annotate(tc["name"], tc["input"], result)
437
443
 
444
+ # Verification evidence: if this Bash call was a test/check/exec, parse
445
+ # its result so the completion gate knows whether it PASSED, not just ran.
446
+ if tc["name"] == "Bash":
447
+ _vcmd = (tc.get("input") or {}).get("command", "")
448
+ if looks_like_verification(_vcmd):
449
+ last_verification = parse_evidence(_vcmd, result)
450
+ _emit(state, "verification", status=last_verification.status,
451
+ exit_code=last_verification.exit_code)
452
+
438
453
  _emit(state, "tool", name=tc["name"],
439
454
  input=str(tc.get("input"))[:200],
440
455
  result_chars=len(str(result)),
@@ -0,0 +1,75 @@
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
+ from dataclasses import dataclass
15
+
16
+ # Explicit test / lint / type-check / build commands.
17
+ _VERIFY_KEYWORDS = re.compile(
18
+ r"\b(pytest|py\.test|unittest|nosetests?|tox|test\.sh|run_tests|"
19
+ r"make(\s+\w+)?|npm\s+(test|run)|yarn\s+(test|run)|pnpm\s+(test|run)|jest|mocha|vitest|"
20
+ r"cargo\s+(test|check|build|run|clippy)|go\s+(test|build|run|vet)|"
21
+ r"ruff|flake8|pylint|mypy|pyright|eslint|tsc|prettier|"
22
+ r"ctest|cmake|gradle|mvn|rspec|phpunit|dotnet\s+test|bats)\b",
23
+ re.I,
24
+ )
25
+ # Executing something — running the produced script/binary counts as checking it.
26
+ _EXEC = re.compile(r"(?:^|[\s;&|(])(?:python3?|node|deno|bun|bash|sh|ruby|perl|"
27
+ r"java|javac|gcc|g\+\+|clang|\./|\.\\)")
28
+
29
+
30
+ def looks_like_verification(cmd) -> bool:
31
+ """True if a shell command constitutes verifying work — running tests/checks/
32
+ builds, or executing the produced code. Best-effort; never raises."""
33
+ if not cmd or not isinstance(cmd, str):
34
+ return False
35
+ return bool(_VERIFY_KEYWORDS.search(cmd) or _EXEC.search(cmd))
36
+
37
+
38
+ @dataclass
39
+ class VerificationEvidence:
40
+ """Structured outcome of a verification command — did the check PASS, not just
41
+ run (PRD verification/evidence). status: 'pass' | 'fail' | 'unknown'."""
42
+ command: str
43
+ status: str
44
+ tests_passed: int | None = None
45
+ tests_failed: int | None = None
46
+ exit_code: int | None = None
47
+ summary: str = ""
48
+
49
+
50
+ # tool_bash appends "[exit code: N]" ONLY on non-zero exit; absent => exit 0.
51
+ _EXIT = re.compile(r"\[exit code:\s*(-?\d+)\]")
52
+ # pytest/unittest-style tallies: "3 passed", "2 failed", "1 error".
53
+ _TALLY = re.compile(r"(\d+)\s+(passed|failed|errors?|skipped|xfailed|xpassed)\b", re.I)
54
+
55
+
56
+ def parse_evidence(command, result_text) -> VerificationEvidence:
57
+ """Classify a verification command's result as pass/fail. Exit code is the
58
+ primary signal (drydock reports non-zero); pytest-style tallies refine it.
59
+ Best-effort; never raises."""
60
+ cmd = command if isinstance(command, str) else ""
61
+ text = result_text if isinstance(result_text, str) else str(result_text or "")
62
+ m = _EXIT.search(text)
63
+ exit_code = int(m.group(1)) if m else 0
64
+ passed = failed = None
65
+ tally = {}
66
+ for n, k in _TALLY.findall(text):
67
+ tally[k.lower().rstrip("s")] = tally.get(k.lower().rstrip("s"), 0) + int(n)
68
+ if tally:
69
+ passed = tally.get("passed")
70
+ failed = tally.get("failed", 0) + tally.get("error", 0)
71
+ status = "fail" if ((failed and failed > 0) or exit_code != 0) else "pass"
72
+ parts = [f"exit {exit_code}"]
73
+ if passed is not None or failed:
74
+ parts.append(f"{passed or 0} passed, {failed or 0} failed")
75
+ return VerificationEvidence(cmd, status, passed, failed, exit_code, "; ".join(parts)[:200])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.126
3
+ Version: 3.0.127
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
@@ -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.126"
10
+ version = "3.0.127"
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"
@@ -69,3 +69,35 @@ def test_disabled():
69
69
  done = AssistantTurn("done", [], 5, 5)
70
70
  st, _ = _drive({**CFG, "verify_gate": False}, [write, done])
71
71
  assert not any("VERIFIED" in m.get("content", "") for m in st.messages if m.get("role") == "user")
72
+
73
+
74
+ def test_evidence_pass_fail():
75
+ from drydock.verification import parse_evidence
76
+ assert parse_evidence("pytest", "5 passed in 0.1s").status == "pass"
77
+ assert parse_evidence("pytest", "2 failed, 3 passed\n\n[exit code: 1]").status == "fail"
78
+ assert parse_evidence("python x.py", "boom\n\n[exit code: 1]").status == "fail"
79
+ ev = parse_evidence("pytest", "2 failed, 3 passed\n\n[exit code: 1]")
80
+ assert ev.tests_passed == 3 and ev.tests_failed == 2 and ev.exit_code == 1
81
+
82
+
83
+ def test_failed_check_triggers_repair_not_complete():
84
+ # write, then run a FAILING check, then claim done -> gate nudges to REPAIR
85
+ write = AssistantTurn("", [{"id": "1", "name": "Write",
86
+ "input": {"file_path": "/tmp/dd_vg_f.txt", "content": "x"}}], 5, 5)
87
+ fail = AssistantTurn("", [{"id": "2", "name": "Bash",
88
+ "input": {"command": "python -c 'import sys; sys.exit(1)'"}}], 5, 5)
89
+ done = AssistantTurn("all done", [], 5, 5)
90
+ st, n = _drive(CFG, [write, fail, done, done])
91
+ assert st.task.phase == "repair"
92
+ assert any("FAILED" in m.get("content", "") for m in st.messages if m.get("role") == "user")
93
+
94
+
95
+ def test_passing_check_accepts_completion():
96
+ write = AssistantTurn("", [{"id": "1", "name": "Write",
97
+ "input": {"file_path": "/tmp/dd_vg_p.txt", "content": "x"}}], 5, 5)
98
+ ok = AssistantTurn("", [{"id": "2", "name": "Bash",
99
+ "input": {"command": "python -c 'print(1)'"}}], 5, 5)
100
+ done = AssistantTurn("done, verified", [], 5, 5)
101
+ st, _ = _drive(CFG, [write, ok, done])
102
+ assert st.task.phase == "complete"
103
+ assert not any("FAILED" in m.get("content", "") for m in st.messages if m.get("role") == "user")
@@ -1,34 +0,0 @@
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))
File without changes
File without changes
File without changes
File without changes