drydock-cli 3.0.126__tar.gz → 3.0.128__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.128}/PKG-INFO +1 -1
  2. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/agent.py +39 -24
  3. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/events.py +51 -0
  4. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/tui/app.py +23 -0
  5. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/tui/widgets.py +1 -1
  6. drydock_cli-3.0.128/drydock/verification.py +75 -0
  7. {drydock_cli-3.0.126 → drydock_cli-3.0.128/drydock_cli.egg-info}/PKG-INFO +1 -1
  8. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/pyproject.toml +1 -1
  9. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_events.py +33 -0
  10. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_verification_gate.py +32 -0
  11. drydock_cli-3.0.126/drydock/verification.py +0 -34
  12. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/LICENSE +0 -0
  13. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/NOTICE +0 -0
  14. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/README.md +0 -0
  15. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/__init__.py +0 -0
  16. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/__main__.py +0 -0
  17. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/advisor.py +0 -0
  18. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/bash_safety.py +0 -0
  19. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/builtin_skills/__init__.py +0 -0
  20. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/builtin_skills/ml-data.md +0 -0
  21. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/builtin_skills/ml-debug.md +0 -0
  22. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/builtin_skills/ml-finetune.md +0 -0
  23. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/builtin_skills/ml-metrics.md +0 -0
  24. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/builtin_skills/ml-rl.md +0 -0
  25. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/builtin_skills/ml-train.md +0 -0
  26. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/builtin_skills/rmf-categorize.md +0 -0
  27. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/builtin_skills/rmf-control.md +0 -0
  28. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/builtin_skills/rmf-poam.md +0 -0
  29. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/builtin_skills/rmf-review.md +0 -0
  30. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/builtin_skills/stig-assess.md +0 -0
  31. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/builtin_skills/stig-remediate.md +0 -0
  32. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/cci.py +0 -0
  33. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/cli.py +0 -0
  34. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/compaction.py +0 -0
  35. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/config.py +0 -0
  36. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/detect.py +0 -0
  37. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/extract.py +0 -0
  38. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/gittools.py +0 -0
  39. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/graphrag.py +0 -0
  40. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/guards.py +0 -0
  41. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/loop_detect.py +0 -0
  42. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/mcp.py +0 -0
  43. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/poam.py +0 -0
  44. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/providers.py +0 -0
  45. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/recipes.py +0 -0
  46. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/rmf.py +0 -0
  47. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/rmf_graph.py +0 -0
  48. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/skills.py +0 -0
  49. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/stig.py +0 -0
  50. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/suggest.py +0 -0
  51. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/task_state.py +0 -0
  52. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/tool_registry.py +0 -0
  53. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/tools/__init__.py +0 -0
  54. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/tui/__init__.py +0 -0
  55. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/tui/approval.py +0 -0
  56. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/tui/messages.py +0 -0
  57. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/tuning.py +0 -0
  58. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/web.py +0 -0
  59. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock_cli.egg-info/SOURCES.txt +0 -0
  60. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock_cli.egg-info/dependency_links.txt +0 -0
  61. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock_cli.egg-info/entry_points.txt +0 -0
  62. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock_cli.egg-info/requires.txt +0 -0
  63. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock_cli.egg-info/top_level.txt +0 -0
  64. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/setup.cfg +0 -0
  65. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_advisor.py +0 -0
  66. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_approval.py +0 -0
  67. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_back_command.py +0 -0
  68. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_bash_background.py +0 -0
  69. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_bash_binary_output.py +0 -0
  70. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_bash_crossplatform.py +0 -0
  71. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_bash_output_bounding.py +0 -0
  72. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_bash_process_group.py +0 -0
  73. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_bash_safety.py +0 -0
  74. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_bash_sanitize.py +0 -0
  75. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_bash_shell.py +0 -0
  76. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_bash_stdin.py +0 -0
  77. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_bash_stop_partial.py +0 -0
  78. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_bash_timeout_network.py +0 -0
  79. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_bash_timeout_param.py +0 -0
  80. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_cci.py +0 -0
  81. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_cli_agents.py +0 -0
  82. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_compact_command.py +0 -0
  83. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_compaction.py +0 -0
  84. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_config.py +0 -0
  85. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_config_migration.py +0 -0
  86. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_context_limit_config.py +0 -0
  87. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_detect.py +0 -0
  88. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_dispatch.py +0 -0
  89. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_e2e_connected.py +0 -0
  90. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_edit_replace_all.py +0 -0
  91. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_empty_response.py +0 -0
  92. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_extract.py +0 -0
  93. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_failure_loop.py +0 -0
  94. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_first_run_setup.py +0 -0
  95. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_gittools.py +0 -0
  96. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_glob_edit_edges.py +0 -0
  97. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_graphify_example.py +0 -0
  98. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_graphrag.py +0 -0
  99. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_graphrag_quoted_path.py +0 -0
  100. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_grep_and_read_robust.py +0 -0
  101. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_guards_and_tools.py +0 -0
  102. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_hallucinated_tools.py +0 -0
  103. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_leaked_tool_call.py +0 -0
  104. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_loop_detect.py +0 -0
  105. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_mcp.py +0 -0
  106. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_oneshot_unreachable.py +0 -0
  107. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_overthink_interrupt.py +0 -0
  108. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_plan_autocontinue.py +0 -0
  109. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_poam.py +0 -0
  110. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_providers_unreachable.py +0 -0
  111. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_read_index.py +0 -0
  112. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_recipes.py +0 -0
  113. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_repetition_interrupt.py +0 -0
  114. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_rmf.py +0 -0
  115. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_rmf_graph.py +0 -0
  116. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_rmf_stig_graph.py +0 -0
  117. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_runaway_repetition.py +0 -0
  118. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_screenshot.py +0 -0
  119. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_server_probe.py +0 -0
  120. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_skills.py +0 -0
  121. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_stall_retry.py +0 -0
  122. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_stig.py +0 -0
  123. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_stop.py +0 -0
  124. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_streaming_newlines.py +0 -0
  125. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_subagent.py +0 -0
  126. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_suggest.py +0 -0
  127. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_system_prompt_help.py +0 -0
  128. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_task_state.py +0 -0
  129. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_todo.py +0 -0
  130. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_tool_arg_coercion.py +0 -0
  131. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_tool_arg_coercion_more.py +0 -0
  132. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_tool_arg_parsing.py +0 -0
  133. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_tools_undo.py +0 -0
  134. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_tui.py +0 -0
  135. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_tuning.py +0 -0
  136. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_viewimage.py +0 -0
  137. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_vision_input.py +0 -0
  138. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_web_tools.py +0 -0
  139. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_windows_shell.py +0 -0
  140. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_write_content_coerce.py +0 -0
  141. {drydock_cli-3.0.126 → drydock_cli-3.0.128}/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.128
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)),
@@ -66,3 +66,54 @@ def default_event_log_path():
66
66
  import time
67
67
  from pathlib import Path
68
68
  return Path.home() / ".drydock" / "events" / f"session-{int(time.time())}.jsonl"
69
+
70
+
71
+ def _events(events_or_path):
72
+ return events_or_path if isinstance(events_or_path, list) else EventLog.read(events_or_path)
73
+
74
+
75
+ def reconstruct_task_state(events_or_path):
76
+ """Rebuild a TaskState (objective, acceptance criteria, phase) from an event
77
+ trace — the PRD "a task can be reconstructed from serialized state" / resume.
78
+ Returns a fresh TaskState (empty if the trace has no task_start)."""
79
+ from drydock.task_state import TaskState
80
+ ts = TaskState()
81
+ for e in _events(events_or_path):
82
+ t = e.get("type")
83
+ if t == "task_start":
84
+ ts.objective = e.get("objective", "") or ts.objective
85
+ ts.acceptance_criteria = list(e.get("acceptance_criteria", []) or ts.acceptance_criteria)
86
+ elif t in ("verify_gate", "verification", "done") and e.get("phase"):
87
+ ts.phase = e.get("phase", ts.phase)
88
+ elif t == "verify_gate":
89
+ ts.phase = "repair" if e.get("kind") == "failed" else "verify"
90
+ elif t == "done":
91
+ ts.phase = e.get("phase", ts.phase)
92
+ return ts
93
+
94
+
95
+ def summarize(events_or_path) -> dict:
96
+ """A compact digest of a task's execution trace for inspection/diagnosis."""
97
+ evs = _events(events_or_path)
98
+ tools: dict[str, int] = {}
99
+ turns = in_tok = out_tok = verify_pass = verify_fail = 0
100
+ phase = "understand"
101
+ for e in evs:
102
+ t = e.get("type")
103
+ if t == "turn":
104
+ turns += 1; in_tok += e.get("in_tok", 0) or 0; out_tok += e.get("out_tok", 0) or 0
105
+ elif t == "tool":
106
+ tools[e.get("name", "?")] = tools.get(e.get("name", "?"), 0) + 1
107
+ elif t == "verification":
108
+ if e.get("status") == "pass": verify_pass += 1
109
+ elif e.get("status") == "fail": verify_fail += 1
110
+ elif t == "done":
111
+ phase = e.get("phase", phase)
112
+ ts = reconstruct_task_state(evs)
113
+ return {
114
+ "objective": ts.objective, "acceptance_criteria": ts.acceptance_criteria,
115
+ "final_phase": phase, "turns": turns, "tools": tools,
116
+ "in_tok": in_tok, "out_tok": out_tok,
117
+ "verifications": {"pass": verify_pass, "fail": verify_fail},
118
+ "event_count": len(evs),
119
+ }
@@ -531,6 +531,8 @@ class DrydockApp(App):
531
531
  self._cmd_context(arg)
532
532
  elif cmd == "/shell":
533
533
  self._cmd_shell()
534
+ elif cmd == "/events":
535
+ self._cmd_events()
534
536
  elif cmd == "/advisor":
535
537
  self._cmd_advisor(arg)
536
538
  elif cmd == "/ask":
@@ -837,6 +839,27 @@ class DrydockApp(App):
837
839
  else:
838
840
  self._begin(prompt)
839
841
 
842
+ def _cmd_events(self) -> None:
843
+ """Show a digest of this session's durable execution trace (event log)."""
844
+ log = self.state.events
845
+ if log is None:
846
+ self._info("Event log is off (config event_log = false).")
847
+ return
848
+ from drydock.events import summarize
849
+ s = summarize(str(log.path))
850
+ tools = ", ".join(f"{k}×{v}" for k, v in sorted(s["tools"].items())) or "none"
851
+ v = s["verifications"]
852
+ lines = [
853
+ f"Task trace — {log.path}",
854
+ f" objective : {(s['objective'] or '(none yet)')[:80]}",
855
+ f" criteria : {len(s['acceptance_criteria'])}",
856
+ f" phase : {s['final_phase']}",
857
+ f" turns : {s['turns']} tools: {tools}",
858
+ f" verifications: {v['pass']} passed, {v['fail']} failed",
859
+ f" tokens : {s['in_tok']:,} in / {s['out_tok']:,} out ({s['event_count']} events)",
860
+ ]
861
+ self._info("\n".join(lines))
862
+
840
863
  def _cmd_shell(self) -> None:
841
864
  """Show exactly which shell the Bash tool runs commands through, plus the
842
865
  platform signals — so a 'still using bash on Windows' report is instantly
@@ -153,7 +153,7 @@ class PromptHistory:
153
153
  # completes the prefix.
154
154
  SLASH_COMMANDS = [
155
155
  "/help", "/model", "/cwd", "/undo", "/back", "/stop", "/status",
156
- "/compact", "/context", "/shell", "/advisor", "/ask", "/ask!", "/graphrag", "/skills",
156
+ "/compact", "/context", "/shell", "/events", "/advisor", "/ask", "/ask!", "/graphrag", "/skills",
157
157
  "/loop", "/mcp", "/rmf", "/stig",
158
158
  "/clear", "/quit",
159
159
  ]
@@ -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.128
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.128"
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"
@@ -56,3 +56,36 @@ def test_run_writes_full_trace():
56
56
  def test_disabled_no_events_attr():
57
57
  st = agent.AgentState()
58
58
  assert st.events is None # off by default on a bare AgentState
59
+
60
+
61
+ def test_reconstruct_task_state_from_trace():
62
+ from drydock.events import EventLog, reconstruct_task_state
63
+ p = os.path.join(tempfile.mkdtemp(), "r.jsonl")
64
+ log = EventLog(p)
65
+ log.emit("task_start", objective="Recover the password", acceptance_criteria=["23 chars", "write out.txt"])
66
+ log.emit("tool", name="Write")
67
+ log.emit("done", phase="complete")
68
+ ts = reconstruct_task_state(p)
69
+ assert ts.objective == "Recover the password"
70
+ assert ts.acceptance_criteria == ["23 chars", "write out.txt"]
71
+ assert ts.phase == "complete"
72
+
73
+
74
+ def test_summarize_digest():
75
+ from drydock.events import EventLog, summarize
76
+ p = os.path.join(tempfile.mkdtemp(), "s.jsonl")
77
+ log = EventLog(p)
78
+ log.emit("task_start", objective="do X", acceptance_criteria=["a"])
79
+ log.emit("turn", in_tok=100, out_tok=50)
80
+ log.emit("tool", name="Bash"); log.emit("tool", name="Bash")
81
+ log.emit("verification", status="fail"); log.emit("verification", status="pass")
82
+ log.emit("done", phase="complete")
83
+ s = summarize(p)
84
+ assert s["turns"] == 1 and s["tools"] == {"Bash": 2}
85
+ assert s["verifications"] == {"pass": 1, "fail": 1} and s["final_phase"] == "complete"
86
+ assert s["in_tok"] == 100 and s["out_tok"] == 50
87
+
88
+
89
+ def test_reconstruct_empty_trace():
90
+ from drydock.events import reconstruct_task_state
91
+ assert reconstruct_task_state([]).objective == ""
@@ -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