drydock-cli 3.0.106__tar.gz → 3.0.108__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.106/drydock_cli.egg-info → drydock_cli-3.0.108}/PKG-INFO +1 -1
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/agent.py +31 -8
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/tools/__init__.py +35 -6
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/tuning.py +35 -1
- {drydock_cli-3.0.106 → drydock_cli-3.0.108/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock_cli.egg-info/SOURCES.txt +2 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/pyproject.toml +1 -1
- drydock_cli-3.0.108/tests/test_overthink_interrupt.py +47 -0
- drydock_cli-3.0.108/tests/test_windows_shell.py +67 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/LICENSE +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/NOTICE +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/README.md +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/advisor.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/cci.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/cli.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/config.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/detect.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/extract.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/guards.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/poam.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/providers.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/skills.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/stig.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/tui/app.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock/web.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/setup.cfg +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_advisor.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_bash_background.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_bash_binary_output.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_bash_crossplatform.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_bash_sanitize.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_bash_shell.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_bash_stdin.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_bash_stop_partial.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_bash_timeout_param.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_cci.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_config.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_edit_replace_all.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_glob_edit_edges.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_graphify_example.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_grep_and_read_robust.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_poam.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_stall_retry.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_tool_arg_coercion.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_tool_arg_coercion_more.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_viewimage.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_write_content_coerce.py +0 -0
- {drydock_cli-3.0.106 → drydock_cli-3.0.108}/tests/test_xccdf.py +0 -0
|
@@ -13,6 +13,20 @@ from typing import Generator
|
|
|
13
13
|
# stall-loop while letting a long, productive plan run as far as it needs.
|
|
14
14
|
PLAN_CONTINUE_CAP = 3
|
|
15
15
|
|
|
16
|
+
# Over-think interrupt: when a call keeps stalling/over-thinking (repeated
|
|
17
|
+
# StallRetry), re-issue in "decisive mode" — a forcing suffix + a hard token cap
|
|
18
|
+
# so the model physically cannot burn thousands of reasoning tokens without
|
|
19
|
+
# acting. The model that produced this loop (gemma) generates 5-15 min / 5k+
|
|
20
|
+
# token "thinking" turns that stall_retry alone can't break (tokens flow); this
|
|
21
|
+
# forces a short, single-action turn instead.
|
|
22
|
+
_DECISIVE_MAX_TOKENS = 1500
|
|
23
|
+
_DECISIVE_SUFFIX = (
|
|
24
|
+
"\n\nURGENT — you have spent too long reasoning without acting. STOP "
|
|
25
|
+
"analyzing. Your reply must be SHORT and take exactly ONE concrete action "
|
|
26
|
+
"right now: emit a single tool call (run a command or write a file) with "
|
|
27
|
+
"little or no explanation. Do NOT lay out a plan; act."
|
|
28
|
+
)
|
|
29
|
+
|
|
16
30
|
|
|
17
31
|
def _plan_has_unfinished(config: dict) -> bool:
|
|
18
32
|
"""True if the model laid out a `todo` plan that still has non-done items."""
|
|
@@ -160,6 +174,7 @@ def run(
|
|
|
160
174
|
# Stream from LLM — with retry on context-length 400 error
|
|
161
175
|
retries = 0
|
|
162
176
|
stall_retries = 0
|
|
177
|
+
decisive = False # over-think interrupt: force a short, single-action turn
|
|
163
178
|
while retries < 2:
|
|
164
179
|
try:
|
|
165
180
|
available = schemas()
|
|
@@ -167,7 +182,7 @@ def run(
|
|
|
167
182
|
available = [s for s in available if s.get("name") in allow]
|
|
168
183
|
for event in stream(
|
|
169
184
|
model=turn_config["model"],
|
|
170
|
-
system=system_prompt,
|
|
185
|
+
system=(system_prompt + _DECISIVE_SUFFIX) if decisive else system_prompt,
|
|
171
186
|
messages=state.messages,
|
|
172
187
|
tool_schemas=filter_tool_schemas(available, turn_config.get("model")),
|
|
173
188
|
config=turn_config,
|
|
@@ -180,17 +195,25 @@ def run(
|
|
|
180
195
|
assistant_turn = event
|
|
181
196
|
break # success
|
|
182
197
|
except StallRetry:
|
|
183
|
-
# The
|
|
184
|
-
#
|
|
185
|
-
#
|
|
186
|
-
#
|
|
187
|
-
#
|
|
198
|
+
# The call stalled or over-thought past stall_retry_secs. First
|
|
199
|
+
# retry re-issues as-is (handles a transient server hang cheaply);
|
|
200
|
+
# if it KEEPS happening it's over-thinking, so escalate to decisive
|
|
201
|
+
# mode — a forcing suffix + a hard token cap that make a long
|
|
202
|
+
# reasoning turn impossible. Bounded; on exhaustion end cleanly.
|
|
188
203
|
stall_retries += 1
|
|
189
204
|
if stall_retries > 3:
|
|
190
|
-
yield TextChunk("\n[model
|
|
205
|
+
yield TextChunk("\n[model kept stalling/over-thinking — giving up on this step.]\n")
|
|
191
206
|
assistant_turn = None
|
|
192
207
|
break
|
|
193
|
-
|
|
208
|
+
if stall_retries >= 2 and not decisive:
|
|
209
|
+
decisive = True
|
|
210
|
+
turn_config = dict(turn_config)
|
|
211
|
+
cur = int(turn_config.get("max_tokens", 8192) or 8192)
|
|
212
|
+
turn_config["max_tokens"] = min(cur, _DECISIVE_MAX_TOKENS)
|
|
213
|
+
turn_config["reasoning_effort"] = "low"
|
|
214
|
+
yield TextChunk("\n[taking too long — forcing a decisive, single-action step...]\n")
|
|
215
|
+
else:
|
|
216
|
+
yield TextChunk("\n[model server stalled — retrying...]\n")
|
|
194
217
|
continue
|
|
195
218
|
except Exception as e:
|
|
196
219
|
err = str(e)
|
|
@@ -93,9 +93,32 @@ def _detect_bash() -> str | None:
|
|
|
93
93
|
|
|
94
94
|
|
|
95
95
|
_IS_WINDOWS = os.name == "nt"
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _detect_shell() -> tuple[str, str]:
|
|
99
|
+
"""Pick the shell tool_bash runs commands in — returns (kind, path).
|
|
100
|
+
|
|
101
|
+
POSIX → bash (so the model's bash-isms work), else /bin/sh.
|
|
102
|
+
Windows → PowerShell (what users actually launch drydock in — pwsh, else
|
|
103
|
+
Windows PowerShell), else cmd.exe. WSL / Git-Bash is NOT required on Windows;
|
|
104
|
+
if you'd rather use them, install bash and it's picked up on POSIX-like paths.
|
|
105
|
+
"""
|
|
106
|
+
import shutil
|
|
107
|
+
if _IS_WINDOWS:
|
|
108
|
+
for name in ("pwsh", "powershell"):
|
|
109
|
+
p = shutil.which(name)
|
|
110
|
+
if p:
|
|
111
|
+
return ("powershell", p)
|
|
112
|
+
return ("cmd", shutil.which("cmd") or "cmd.exe")
|
|
113
|
+
b = _detect_bash()
|
|
114
|
+
if b:
|
|
115
|
+
return ("bash", b)
|
|
116
|
+
return ("sh", shutil.which("sh") or "/bin/sh")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# Resolved once at import (in whatever environment drydock runs — Linux/macOS,
|
|
120
|
+
# the task container, or native Windows). _BASH_SHELL kept for back-compat.
|
|
121
|
+
_SHELL_KIND, _SHELL_PATH = _detect_shell()
|
|
99
122
|
_BASH_SHELL = _detect_bash()
|
|
100
123
|
|
|
101
124
|
|
|
@@ -1040,9 +1063,15 @@ def tool_bash(params: dict, config: dict) -> str:
|
|
|
1040
1063
|
# would be called as `bash /c ...` and fail. `[bash, "-c", cmd]` runs the same
|
|
1041
1064
|
# on Linux and Windows. Without bash, fall back to the platform default shell
|
|
1042
1065
|
# (cmd.exe on Windows, /bin/sh on POSIX) via shell=True.
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1066
|
+
# Build the invocation for the detected shell. Explicit argv (shell=False)
|
|
1067
|
+
# for bash/PowerShell so it's correct on every OS; shell=True lets the
|
|
1068
|
+
# platform default (/bin/sh on POSIX, cmd.exe on Windows) handle the rest.
|
|
1069
|
+
if _SHELL_KIND == "bash":
|
|
1070
|
+
popen_cmd, use_shell = [_SHELL_PATH, "-c", cmd], False
|
|
1071
|
+
elif _SHELL_KIND == "powershell":
|
|
1072
|
+
popen_cmd, use_shell = [_SHELL_PATH, "-NoProfile", "-NonInteractive",
|
|
1073
|
+
"-Command", cmd], False
|
|
1074
|
+
else: # "sh" (POSIX) or "cmd" (Windows) → platform default shell
|
|
1046
1075
|
popen_cmd, use_shell = cmd, True
|
|
1047
1076
|
try:
|
|
1048
1077
|
proc = subprocess.Popen(
|
|
@@ -97,6 +97,10 @@ _DEFAULT_SYSTEM_PROMPT = (
|
|
|
97
97
|
"RUN it and fix failures until it passes before you stop. Prefer acting — write "
|
|
98
98
|
"files and run commands — over long planning; don't produce a big plan without "
|
|
99
99
|
"taking a concrete step.\n"
|
|
100
|
+
"A script you write must run with ONLY what is already installed — prefer "
|
|
101
|
+
"the standard library or shelling out to the task's own tools (openssl, git, "
|
|
102
|
+
"sqlite3); do NOT depend on a package you just pip-installed unless the task "
|
|
103
|
+
"explicitly says to (the grader runs in a clean environment).\n"
|
|
100
104
|
"You have VISION: when the user references an image file path (.png/.jpg/"
|
|
101
105
|
".jpeg/.gif/.webp/.bmp on disk — a screenshot, mockup, diagram, or photo), it "
|
|
102
106
|
"is attached and you can SEE it; you can also call the `ViewImage` tool to "
|
|
@@ -133,6 +137,10 @@ _GEMMA_SYSTEM_PROMPT = (
|
|
|
133
137
|
"RUN it and fix failures until it passes before you stop. Prefer acting — write "
|
|
134
138
|
"files and run commands — over long planning; don't produce a big plan without "
|
|
135
139
|
"taking a concrete step.\n"
|
|
140
|
+
"A script you write must run with ONLY what is already installed — prefer "
|
|
141
|
+
"the standard library or shelling out to the task's own tools (openssl, git, "
|
|
142
|
+
"sqlite3); do NOT depend on a package you just pip-installed unless the task "
|
|
143
|
+
"explicitly says to (the grader runs in a clean environment).\n"
|
|
136
144
|
"You have VISION: when the user's message references an image file path "
|
|
137
145
|
"(.png/.jpg/.jpeg/.gif/.webp/.bmp that exists on disk — a screenshot, mockup, "
|
|
138
146
|
"diagram, or photo), that image is attached and you can SEE it. You can also "
|
|
@@ -264,10 +272,36 @@ _DRYDOCK_COMMANDS_HELP = (
|
|
|
264
272
|
)
|
|
265
273
|
|
|
266
274
|
|
|
275
|
+
def _shell_env_note() -> str:
|
|
276
|
+
"""On Windows, tell the model its shell is PowerShell/cmd so it stops
|
|
277
|
+
emitting bash it can't run. Empty on POSIX (bash is the default assumption)."""
|
|
278
|
+
import os
|
|
279
|
+
import shutil
|
|
280
|
+
if os.name != "nt":
|
|
281
|
+
return ""
|
|
282
|
+
if shutil.which("pwsh") or shutil.which("powershell"):
|
|
283
|
+
return (
|
|
284
|
+
"\nENVIRONMENT: You are on WINDOWS and the Bash tool runs your commands "
|
|
285
|
+
"through POWERSHELL, not bash. Use PowerShell / Windows commands: "
|
|
286
|
+
"Get-ChildItem (ls), Get-Content (cat), Select-String (grep), Copy-Item, "
|
|
287
|
+
"Move-Item, Remove-Item, New-Item, Test-Path, Where-Object. Many Unix "
|
|
288
|
+
"aliases (ls, cat, cp, rm, pwd, echo) also work. Do NOT use bash-only "
|
|
289
|
+
"syntax ([[ ]], <<<, $(...) subshells, grep/sed/awk pipelines) — it "
|
|
290
|
+
"fails. Use Windows paths (backslashes) and $env:VAR for environment "
|
|
291
|
+
"variables. Chain commands with ';' (not '&&')."
|
|
292
|
+
)
|
|
293
|
+
return (
|
|
294
|
+
"\nENVIRONMENT: You are on WINDOWS and the Bash tool runs your commands "
|
|
295
|
+
"through cmd.exe, not bash. Use Windows/cmd commands (dir, type, findstr, "
|
|
296
|
+
"copy, del, move, mkdir); do NOT use bash syntax. Use backslash paths, "
|
|
297
|
+
"%VAR% for environment variables, and '&' or '&&' to chain commands."
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
267
301
|
def system_prompt_for_model(model: str | None) -> str:
|
|
268
302
|
"""Return the system prompt best suited to the model."""
|
|
269
303
|
base = _GEMMA_SYSTEM_PROMPT if is_gemma(model) else _DEFAULT_SYSTEM_PROMPT
|
|
270
|
-
return base + _DRYDOCK_COMMANDS_HELP
|
|
304
|
+
return base + _DRYDOCK_COMMANDS_HELP + _shell_env_note()
|
|
271
305
|
|
|
272
306
|
|
|
273
307
|
def thinking_level_for_turn(turn_count: int, is_user_turn: bool) -> str:
|
|
@@ -87,6 +87,7 @@ tests/test_leaked_tool_call.py
|
|
|
87
87
|
tests/test_loop_detect.py
|
|
88
88
|
tests/test_mcp.py
|
|
89
89
|
tests/test_oneshot_unreachable.py
|
|
90
|
+
tests/test_overthink_interrupt.py
|
|
90
91
|
tests/test_plan_autocontinue.py
|
|
91
92
|
tests/test_poam.py
|
|
92
93
|
tests/test_providers_unreachable.py
|
|
@@ -113,5 +114,6 @@ tests/test_tuning.py
|
|
|
113
114
|
tests/test_viewimage.py
|
|
114
115
|
tests/test_vision_input.py
|
|
115
116
|
tests/test_web_tools.py
|
|
117
|
+
tests/test_windows_shell.py
|
|
116
118
|
tests/test_write_content_coerce.py
|
|
117
119
|
tests/test_xccdf.py
|
|
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|
|
7
7
|
# PyPI distribution name is drydock-cli (the established install name, continued
|
|
8
8
|
# from the retired v2 fork); the import package + CLI command stay `drydock`.
|
|
9
9
|
name = "drydock-cli"
|
|
10
|
-
version = "3.0.
|
|
10
|
+
version = "3.0.108"
|
|
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"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Over-think interrupt: when a call keeps stalling/over-thinking (repeated
|
|
2
|
+
StallRetry), the agent escalates to 'decisive mode' — a forcing suffix on the
|
|
3
|
+
system prompt + a hard max_tokens cap — so the model can't burn a 5k-token
|
|
4
|
+
reasoning turn without acting. First retry re-issues as-is; 2nd+ goes decisive."""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from drydock import agent
|
|
8
|
+
from drydock.agent import AgentState, _DECISIVE_SUFFIX, _DECISIVE_MAX_TOKENS
|
|
9
|
+
from drydock.providers import StallRetry, AssistantTurn
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _drive(monkeypatch, stalls):
|
|
13
|
+
calls = []
|
|
14
|
+
|
|
15
|
+
def fake_stream(model, system, messages, tool_schemas, config):
|
|
16
|
+
calls.append({"system": system, "max_tokens": config.get("max_tokens"),
|
|
17
|
+
"effort": config.get("reasoning_effort")})
|
|
18
|
+
if len(calls) <= stalls:
|
|
19
|
+
raise StallRetry(1)
|
|
20
|
+
yield AssistantTurn(text="done", tool_calls=[], input_tokens=10, output_tokens=5)
|
|
21
|
+
|
|
22
|
+
monkeypatch.setattr(agent, "stream", fake_stream)
|
|
23
|
+
state = AgentState()
|
|
24
|
+
cfg = {"model": "gemma4", "max_tokens": 8192, "stall_retry_secs": 1, "context_limit": 65536}
|
|
25
|
+
list(agent.run("do it", state, cfg, "SYS"))
|
|
26
|
+
return calls
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_first_retry_not_decisive_second_is(monkeypatch):
|
|
30
|
+
calls = _drive(monkeypatch, stalls=2) # stall twice, succeed on 3rd
|
|
31
|
+
assert len(calls) == 3
|
|
32
|
+
assert _DECISIVE_SUFFIX not in calls[0]["system"] # attempt 1: as-is
|
|
33
|
+
assert _DECISIVE_SUFFIX not in calls[1]["system"] # attempt 2 (retry 1): as-is
|
|
34
|
+
assert _DECISIVE_SUFFIX in calls[2]["system"] # attempt 3 (retry 2): decisive
|
|
35
|
+
assert calls[2]["max_tokens"] <= _DECISIVE_MAX_TOKENS # hard token cap
|
|
36
|
+
assert calls[2]["effort"] == "low"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_gives_up_after_bounded_retries(monkeypatch):
|
|
40
|
+
# stalls forever → bounded (>3) → ends cleanly, doesn't loop forever
|
|
41
|
+
calls = _drive(monkeypatch, stalls=99)
|
|
42
|
+
assert len(calls) <= 5 # 1 initial + up to ~3 retries then give up
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_no_stall_no_decisive(monkeypatch):
|
|
46
|
+
calls = _drive(monkeypatch, stalls=0) # succeeds first try
|
|
47
|
+
assert len(calls) == 1 and _DECISIVE_SUFFIX not in calls[0]["system"]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""drydock runs natively on Windows via PowerShell/cmd — no WSL/bash required.
|
|
2
|
+
tool_bash picks the shell (POSIX→bash, Windows→PowerShell else cmd) and builds
|
|
3
|
+
the right invocation; the system prompt tells the model which shell it's on."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import subprocess
|
|
7
|
+
from unittest import mock
|
|
8
|
+
|
|
9
|
+
import drydock.tools as T
|
|
10
|
+
from drydock import tuning
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_posix_uses_bash():
|
|
14
|
+
assert T._SHELL_KIND in ("bash", "sh")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_windows_prefers_powershell():
|
|
18
|
+
with mock.patch.object(T, "_IS_WINDOWS", True), \
|
|
19
|
+
mock.patch("shutil.which", side_effect=lambda n: r"C:\ps\pwsh.exe" if n == "pwsh" else None):
|
|
20
|
+
kind, path = T._detect_shell()
|
|
21
|
+
assert kind == "powershell" and path.endswith("pwsh.exe")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_windows_falls_back_to_cmd():
|
|
25
|
+
with mock.patch.object(T, "_IS_WINDOWS", True), \
|
|
26
|
+
mock.patch("shutil.which", side_effect=lambda n: r"C:\Windows\System32\cmd.exe" if n == "cmd" else None):
|
|
27
|
+
kind, _ = T._detect_shell()
|
|
28
|
+
assert kind == "cmd"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_powershell_invocation_argv(monkeypatch):
|
|
32
|
+
"""When the shell is PowerShell, Popen is called [pwsh -NoProfile
|
|
33
|
+
-NonInteractive -Command <cmd>] with shell=False — not shell=True."""
|
|
34
|
+
seen = {}
|
|
35
|
+
real = subprocess.Popen
|
|
36
|
+
|
|
37
|
+
class FakeProc:
|
|
38
|
+
stdout = None
|
|
39
|
+
pid = 1
|
|
40
|
+
def wait(self): return 0
|
|
41
|
+
def poll(self): return 0
|
|
42
|
+
def spy(popen_cmd, *a, **kw):
|
|
43
|
+
seen["cmd"] = popen_cmd; seen["shell"] = kw.get("shell")
|
|
44
|
+
raise RuntimeError("stop-after-capture") # don't actually run pwsh on Linux
|
|
45
|
+
|
|
46
|
+
monkeypatch.setattr(T, "_SHELL_KIND", "powershell")
|
|
47
|
+
monkeypatch.setattr(T, "_SHELL_PATH", r"C:\ps\pwsh.exe")
|
|
48
|
+
monkeypatch.setattr(subprocess, "Popen", spy)
|
|
49
|
+
out = T.tool_bash({"command": "Get-ChildItem", "timeout": 5}, {"cwd": "."})
|
|
50
|
+
assert seen["shell"] is False
|
|
51
|
+
assert seen["cmd"][:4] == [r"C:\ps\pwsh.exe", "-NoProfile", "-NonInteractive", "-Command"]
|
|
52
|
+
assert seen["cmd"][4] == "Get-ChildItem"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_prompt_note_windows_powershell():
|
|
56
|
+
with mock.patch("os.name", "nt"), \
|
|
57
|
+
mock.patch("shutil.which", side_effect=lambda n: "x" if n in ("pwsh", "powershell") else None):
|
|
58
|
+
assert "POWERSHELL" in tuning._shell_env_note()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_prompt_note_windows_cmd():
|
|
62
|
+
with mock.patch("os.name", "nt"), mock.patch("shutil.which", return_value=None):
|
|
63
|
+
assert "cmd.exe" in tuning._shell_env_note()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_prompt_note_empty_on_posix():
|
|
67
|
+
assert tuning._shell_env_note() == ""
|
|
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
|