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.
- {drydock_cli-3.0.126/drydock_cli.egg-info → drydock_cli-3.0.128}/PKG-INFO +1 -1
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/agent.py +39 -24
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/events.py +51 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/tui/app.py +23 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/tui/widgets.py +1 -1
- drydock_cli-3.0.128/drydock/verification.py +75 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/pyproject.toml +1 -1
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_events.py +33 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_verification_gate.py +32 -0
- drydock_cli-3.0.126/drydock/verification.py +0 -34
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/LICENSE +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/NOTICE +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/README.md +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/advisor.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/builtin_skills/ml-data.md +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/builtin_skills/ml-debug.md +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/builtin_skills/ml-finetune.md +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/builtin_skills/ml-metrics.md +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/builtin_skills/ml-rl.md +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/builtin_skills/ml-train.md +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/cci.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/cli.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/config.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/detect.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/extract.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/guards.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/poam.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/providers.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/recipes.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/skills.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/stig.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/suggest.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/task_state.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/tools/__init__.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock/web.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock_cli.egg-info/SOURCES.txt +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/setup.cfg +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_advisor.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_bash_background.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_bash_binary_output.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_bash_crossplatform.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_bash_sanitize.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_bash_shell.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_bash_stdin.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_bash_stop_partial.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_bash_timeout_param.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_cci.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_config.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_edit_replace_all.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_glob_edit_edges.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_graphify_example.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_graphrag_quoted_path.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_grep_and_read_robust.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_overthink_interrupt.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_poam.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_recipes.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_repetition_interrupt.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_screenshot.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_stall_retry.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_suggest.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_task_state.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_tool_arg_coercion.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_tool_arg_coercion_more.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_viewimage.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_windows_shell.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_write_content_coerce.py +0 -0
- {drydock_cli-3.0.126 → drydock_cli-3.0.128}/tests/test_xccdf.py +0 -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
|
-
|
|
163
|
-
verify_gate_nudges = 0
|
|
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
|
-
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
"
|
|
365
|
-
"
|
|
366
|
-
"
|
|
367
|
-
|
|
368
|
-
|
|
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
|
|
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=
|
|
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])
|
|
@@ -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.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
|
|
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
|