drydock-cli 3.0.114__tar.gz → 3.0.116__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.114/drydock_cli.egg-info → drydock_cli-3.0.116}/PKG-INFO +1 -1
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/agent.py +24 -10
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/config.py +5 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/providers.py +28 -4
- drydock_cli-3.0.116/drydock/recipes.py +166 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock_cli.egg-info/SOURCES.txt +3 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/pyproject.toml +1 -1
- drydock_cli-3.0.116/tests/test_recipes.py +55 -0
- drydock_cli-3.0.116/tests/test_repetition_interrupt.py +68 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/LICENSE +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/NOTICE +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/README.md +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/advisor.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/cci.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/cli.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/detect.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/extract.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/guards.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/poam.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/skills.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/stig.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/tools/__init__.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/tui/app.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock/web.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/setup.cfg +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_advisor.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_bash_background.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_bash_binary_output.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_bash_crossplatform.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_bash_sanitize.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_bash_shell.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_bash_stdin.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_bash_stop_partial.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_bash_timeout_param.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_cci.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_config.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_edit_replace_all.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_glob_edit_edges.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_graphify_example.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_graphrag_quoted_path.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_grep_and_read_robust.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_overthink_interrupt.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_poam.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_screenshot.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_stall_retry.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_tool_arg_coercion.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_tool_arg_coercion_more.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_viewimage.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_windows_shell.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_write_content_coerce.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.116}/tests/test_xccdf.py +0 -0
|
@@ -33,7 +33,7 @@ def _plan_has_unfinished(config: dict) -> bool:
|
|
|
33
33
|
todo = config.get("_todo")
|
|
34
34
|
return bool(todo) and any(status != "done" for _, status in todo)
|
|
35
35
|
|
|
36
|
-
from drydock.providers import stream, AssistantTurn, ReasoningChunk, TextChunk, StallRetry
|
|
36
|
+
from drydock.providers import stream, AssistantTurn, ReasoningChunk, TextChunk, StallRetry, RepetitionDetected
|
|
37
37
|
from drydock.tool_registry import schemas, execute
|
|
38
38
|
from drydock.tools import register_all
|
|
39
39
|
|
|
@@ -114,6 +114,16 @@ def run(
|
|
|
114
114
|
"""
|
|
115
115
|
state.messages.append({"role": "user", "content": user_message})
|
|
116
116
|
|
|
117
|
+
# Recipe retrieval: give the model the TECHNIQUE this task needs (forensics,
|
|
118
|
+
# git-history rewrite, numpy-2.0 fix, cert gen, …) by appending the relevant
|
|
119
|
+
# bundled recipes to the system prompt. Keyword-matched, so only fitting ones
|
|
120
|
+
# are added; none matched = no change. Gated by config `recipes`.
|
|
121
|
+
if config.get("recipes", True):
|
|
122
|
+
from drydock.recipes import recipe_context
|
|
123
|
+
extra = recipe_context(user_message)
|
|
124
|
+
if extra:
|
|
125
|
+
system_prompt = system_prompt + extra
|
|
126
|
+
|
|
117
127
|
max_turns = config.get("max_turns", 200)
|
|
118
128
|
max_tool_calls = config.get("max_tool_calls", 0) # 0 = unlimited
|
|
119
129
|
# When set (sub-agents pass this), the model is offered ONLY these tools and
|
|
@@ -194,24 +204,28 @@ def run(
|
|
|
194
204
|
elif isinstance(event, AssistantTurn):
|
|
195
205
|
assistant_turn = event
|
|
196
206
|
break # success
|
|
197
|
-
except StallRetry:
|
|
198
|
-
#
|
|
199
|
-
#
|
|
200
|
-
#
|
|
201
|
-
#
|
|
202
|
-
#
|
|
207
|
+
except StallRetry as _sr:
|
|
208
|
+
# Stalled/over-thought past stall_retry_secs (wall-time), OR collapsed
|
|
209
|
+
# into a pure-repetition loop (RepetitionDetected, content-based). A
|
|
210
|
+
# first wall-time retry re-issues as-is (a transient hang clears); a
|
|
211
|
+
# persistent stall OR any repetition loop goes straight to decisive
|
|
212
|
+
# mode — a forcing suffix + hard token cap that make a long reasoning
|
|
213
|
+
# turn impossible. Repetition escalates immediately (re-issuing a loop
|
|
214
|
+
# just loops again). Bounded; on exhaustion end cleanly.
|
|
215
|
+
is_rep = isinstance(_sr, RepetitionDetected)
|
|
203
216
|
stall_retries += 1
|
|
204
217
|
if stall_retries > 3:
|
|
205
|
-
yield TextChunk("\n[model kept stalling/
|
|
218
|
+
yield TextChunk("\n[model kept stalling/looping — giving up on this step.]\n")
|
|
206
219
|
assistant_turn = None
|
|
207
220
|
break
|
|
208
|
-
if stall_retries >= 2 and not decisive:
|
|
221
|
+
if (is_rep or stall_retries >= 2) and not decisive:
|
|
209
222
|
decisive = True
|
|
210
223
|
turn_config = dict(turn_config)
|
|
211
224
|
cur = int(turn_config.get("max_tokens", 8192) or 8192)
|
|
212
225
|
turn_config["max_tokens"] = min(cur, _DECISIVE_MAX_TOKENS)
|
|
213
226
|
turn_config["reasoning_effort"] = "low"
|
|
214
|
-
|
|
227
|
+
why = "output looping" if is_rep else "taking too long"
|
|
228
|
+
yield TextChunk(f"\n[{why} — forcing a decisive, single-action step...]\n")
|
|
215
229
|
else:
|
|
216
230
|
yield TextChunk("\n[model server stalled — retrying...]\n")
|
|
217
231
|
continue
|
|
@@ -39,6 +39,11 @@ DEFAULTS: dict[str, object] = {
|
|
|
39
39
|
# generation usually isn't stalled — a known gemma/llama.cpp hang). 0 = off.
|
|
40
40
|
# Set to e.g. 600 on a stall-prone local server. Bounded to a few retries.
|
|
41
41
|
"stall_retry_secs": 0,
|
|
42
|
+
# Inject bundled technique recipes (drydock/recipes.py) relevant to the task
|
|
43
|
+
# into the system prompt, so a local model has the *method* a task needs
|
|
44
|
+
# instead of guessing. Retrieval is keyword-overlap; only relevant recipes are
|
|
45
|
+
# added (none = nothing injected). Set false to disable.
|
|
46
|
+
"recipes": True,
|
|
42
47
|
"theme": "harbor",
|
|
43
48
|
# Optional SECOND model ("advisor") for a stronger second opinion — e.g. a
|
|
44
49
|
# Gemini OpenAI-compatible endpoint on another box. Empty = disabled. The
|
|
@@ -30,6 +30,14 @@ class StallRetry(Exception):
|
|
|
30
30
|
abandons the wedged request and re-issues it; a fresh generation usually
|
|
31
31
|
doesn't stall. Opt-in via config `stall_retry_secs` (0 = off)."""
|
|
32
32
|
|
|
33
|
+
|
|
34
|
+
class RepetitionDetected(StallRetry):
|
|
35
|
+
"""CONTENT trigger for the over-think interrupt: the model collapsed into a
|
|
36
|
+
pure-repetition loop (same short unit ×6+, 600+ chars) and produced no usable
|
|
37
|
+
action. Unlike the wall-time StallRetry this fires on the *content*, so it
|
|
38
|
+
never trips on productive-but-long reasoning — only genuine spinning. A plain
|
|
39
|
+
re-issue would just loop again, so the agent goes straight to decisive mode."""
|
|
40
|
+
|
|
33
41
|
from drydock.loop_detect import runaway_repetition_len
|
|
34
42
|
from drydock.tuning import extract_thinking, strip_leaked_tool_calls, strip_thinking_tokens, use_streaming
|
|
35
43
|
|
|
@@ -540,6 +548,11 @@ def stream(
|
|
|
540
548
|
_rep_checked_at = len(text)
|
|
541
549
|
run = runaway_repetition_len(text)
|
|
542
550
|
if run:
|
|
551
|
+
# With the over-think interrupt on, a genuine loop escalates
|
|
552
|
+
# to a decisive retry (recover) instead of returning a
|
|
553
|
+
# truncated turn. Otherwise keep the advisory trim+stop.
|
|
554
|
+
if stall_secs:
|
|
555
|
+
raise RepetitionDetected(0)
|
|
543
556
|
text = (
|
|
544
557
|
text[: len(text) - run]
|
|
545
558
|
+ "\n[… output began repeating — stopped by drydock]"
|
|
@@ -599,10 +612,6 @@ def _complete_nonstreaming(
|
|
|
599
612
|
# the generic special-token pass would eat the tool_call markers.
|
|
600
613
|
text, had_leak = strip_leaked_tool_calls(msg.content or "")
|
|
601
614
|
thinking, text = extract_thinking(text)
|
|
602
|
-
if thinking:
|
|
603
|
-
yield ReasoningChunk(thinking)
|
|
604
|
-
if text.strip():
|
|
605
|
-
yield TextChunk(text)
|
|
606
615
|
|
|
607
616
|
tool_calls = []
|
|
608
617
|
for i, tc in enumerate(getattr(msg, "tool_calls", None) or []):
|
|
@@ -613,6 +622,21 @@ def _complete_nonstreaming(
|
|
|
613
622
|
"input": inp,
|
|
614
623
|
})
|
|
615
624
|
|
|
625
|
+
# Repetition trigger for the over-think interrupt (gated by the same
|
|
626
|
+
# stall_secs feature). Non-streaming gemma turns are where the over-think
|
|
627
|
+
# lives; if the turn collapsed into a pure loop and produced NO usable action,
|
|
628
|
+
# go decisive rather than feeding the garbage turn into the conversation. Only
|
|
629
|
+
# fires on genuine repetition, so productive-but-long reasoning is never hit.
|
|
630
|
+
if stall_secs and not tool_calls and (
|
|
631
|
+
runaway_repetition_len(thinking or "") or runaway_repetition_len(text or "")
|
|
632
|
+
):
|
|
633
|
+
raise RepetitionDetected(0)
|
|
634
|
+
|
|
635
|
+
if thinking:
|
|
636
|
+
yield ReasoningChunk(thinking)
|
|
637
|
+
if text.strip():
|
|
638
|
+
yield TextChunk(text)
|
|
639
|
+
|
|
616
640
|
usage = getattr(resp, "usage", None)
|
|
617
641
|
in_tok = usage.prompt_tokens if usage else 0
|
|
618
642
|
out_tok = usage.completion_tokens if usage else 0
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Bundled technique recipes — compact "how to do X" references retrieved by task
|
|
2
|
+
text and injected into context, so a fixed local model has the *technique* a task
|
|
3
|
+
needs instead of guessing. Retrieval is keyword-overlap (stdlib, no embeddings):
|
|
4
|
+
cheap, deterministic, and good enough for a small curated set.
|
|
5
|
+
|
|
6
|
+
All content original to Drydock. Gated by config `recipes` (default on).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
# (title, keywords, body). Keep bodies tight and COMMAND-first — the model needs
|
|
13
|
+
# the move, not an essay. Grounded in tbench failure categories we observed.
|
|
14
|
+
RECIPES: list[tuple[str, list[str], str]] = [
|
|
15
|
+
("Recover deleted / hidden / fragmented data from files",
|
|
16
|
+
["recover", "deleted", "forensic", "forensics", "password", "strings", "binary",
|
|
17
|
+
"carve", "fragment", "hidden", "launchcode", "undelete", "disk", "raw"],
|
|
18
|
+
"Deleted or embedded content often still sits in a file/blob as raw bytes.\n"
|
|
19
|
+
"1. Search binary-safe for the pattern across everything:\n"
|
|
20
|
+
" grep -aroE 'PATTERN' /path (grep -a treats binary as text)\n"
|
|
21
|
+
"2. Pull printable strings and grep them — the target may be split across a\n"
|
|
22
|
+
" non-printable byte, so `strings` shows it on TWO adjacent lines:\n"
|
|
23
|
+
" strings BIGFILE | grep -A1 'MARKER'\n"
|
|
24
|
+
" Then CONCATENATE the fragments (strip the break) to rebuild the value.\n"
|
|
25
|
+
"3. Inspect bytes to understand structure: xxd FILE | less (or od -A x -t x1z).\n"
|
|
26
|
+
"If a value has a known shape (e.g. 23 chars, starts 8XD ends W54), build the\n"
|
|
27
|
+
"exact ERE: 8XD[A-Z0-9]{17}W54 — count the middle length precisely."),
|
|
28
|
+
|
|
29
|
+
("Remove a secret / file from ALL git history",
|
|
30
|
+
["git", "history", "secret", "sanitize", "remove", "rewrite", "filter", "purge",
|
|
31
|
+
"credential", "commit", "bfg", "filter-branch", "filter-repo"],
|
|
32
|
+
"Deleting a file in a new commit does NOT remove it from history. Rewrite it:\n"
|
|
33
|
+
" git filter-repo --path SECRET --invert-paths # preferred if installed\n"
|
|
34
|
+
" git filter-repo --replace-text <(echo 'SECRET==>REDACTED')\n"
|
|
35
|
+
"If filter-repo is absent, use the built-in (slower):\n"
|
|
36
|
+
" git filter-branch --force --index-filter \\\n"
|
|
37
|
+
" 'git rm --cached --ignore-unmatch PATH' --prune-empty --tag-name-filter cat -- --all\n"
|
|
38
|
+
"Then: rm -rf .git/refs/original; git reflog expire --expire=now --all; git gc --prune=now.\n"
|
|
39
|
+
"Verify it's gone from every commit: git log --all --oneline -- PATH (should be empty),\n"
|
|
40
|
+
"and grep history: git rev-list --all | xargs -I{} git grep -I SECRET {} 2>/dev/null."),
|
|
41
|
+
|
|
42
|
+
("Fix NumPy 2.0 incompatibility",
|
|
43
|
+
["numpy", "np", "compat", "compatibility", "attributeerror", "deprecated",
|
|
44
|
+
"alias", "float", "int", "bool", "migrate", "2.0"],
|
|
45
|
+
"NumPy 2.0 removed the deprecated scalar aliases and renamed a few APIs:\n"
|
|
46
|
+
" np.float -> float (or np.float64) np.int -> int (or np.int64)\n"
|
|
47
|
+
" np.bool -> bool np.object -> object np.str -> str\n"
|
|
48
|
+
" np.NaN -> np.nan np.Inf -> np.inf np.product -> np.prod\n"
|
|
49
|
+
" np.NINF -> -np.inf np.float_ -> np.float64 np.unicode_ -> np.str_\n"
|
|
50
|
+
" np.in1d -> np.isin np.round_ -> np.round\n"
|
|
51
|
+
"Find them: grep -rnE 'np\\.(float|int|bool|object|str|NaN|Inf|product|in1d)\\b' .\n"
|
|
52
|
+
"For C/Cython ext: rebuild against the installed NumPy headers and pin\n"
|
|
53
|
+
"'oldest-supported-numpy' at build time; the ABI is forward-compatible from 2.0."),
|
|
54
|
+
|
|
55
|
+
("Build & install a Python C/Cython extension from source",
|
|
56
|
+
["cython", "extension", "compile", "build", "setup.py", "pyx", "c extension",
|
|
57
|
+
"build_ext", "install", "source", "pip install"],
|
|
58
|
+
" pip install -e . # build + editable-install using pyproject/setup\n"
|
|
59
|
+
" python setup.py build_ext --inplace # just compile the .so next to sources\n"
|
|
60
|
+
" pip install . # normal install\n"
|
|
61
|
+
"If Cython isn't run: pip install cython numpy first, then rebuild. To force a\n"
|
|
62
|
+
"clean rebuild: rm -rf build/ **/*.so **/*.c(generated) and repeat. Check the\n"
|
|
63
|
+
"extension imports: python -c 'import pkg.mod'. Read the FIRST compiler error —\n"
|
|
64
|
+
"usually a missing header (apt/pip a -dev package) or a NumPy API mismatch."),
|
|
65
|
+
|
|
66
|
+
("Create & verify a self-signed certificate with openssl",
|
|
67
|
+
["openssl", "certificate", "cert", "self-signed", "tls", "ssl", "x509", "key",
|
|
68
|
+
"pem", "csr"],
|
|
69
|
+
"One-shot self-signed cert + key (no prompts):\n"
|
|
70
|
+
" openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem \\\n"
|
|
71
|
+
" -days 365 -subj '/CN=dev-internal.company.local'\n"
|
|
72
|
+
"Read fields back (scriptable, no extra deps):\n"
|
|
73
|
+
" openssl x509 -in cert.pem -noout -subject # CN=...\n"
|
|
74
|
+
" openssl x509 -in cert.pem -noout -enddate # notAfter=...\n"
|
|
75
|
+
" openssl x509 -in cert.pem -noout -dates -subject\n"
|
|
76
|
+
"Prefer shelling out to `openssl` over a Python crypto lib — the grader's env\n"
|
|
77
|
+
"may not have `cryptography` installed."),
|
|
78
|
+
|
|
79
|
+
("Parse logs / extract fields from text",
|
|
80
|
+
["log", "parse", "extract", "field", "grep", "awk", "sed", "regex", "count",
|
|
81
|
+
"lines", "match", "column"],
|
|
82
|
+
" grep -c 'ERROR' file # count matching LINES\n"
|
|
83
|
+
" grep -oE 'PATTERN' file | wc -l # count matching OCCURRENCES\n"
|
|
84
|
+
" awk -F',' '{print $3}' file # 3rd CSV field\n"
|
|
85
|
+
" awk '{sum+=$1} END{print sum}' # sum a column\n"
|
|
86
|
+
" sed -n '10,20p' file # lines 10-20\n"
|
|
87
|
+
" grep -oE '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+' f # emails\n"
|
|
88
|
+
"Use grep -P for lookaround if available; -a for binary-safe; sort | uniq -c to tally."),
|
|
89
|
+
|
|
90
|
+
("Work with JSON / CSV / SQLite data files",
|
|
91
|
+
["json", "csv", "sqlite", "data", "parse", "query", "database", "db", "jq",
|
|
92
|
+
"pandas", "table", "schema"],
|
|
93
|
+
"JSON: jq '.key[]' f.json | python -c 'import json;d=json.load(open(\"f\"))'\n"
|
|
94
|
+
"CSV: python -c 'import csv;[print(r) for r in csv.DictReader(open(\"f.csv\"))]'\n"
|
|
95
|
+
"SQLite: sqlite3 db '.schema' sqlite3 db 'SELECT * FROM t LIMIT 5;'\n"
|
|
96
|
+
" sqlite3 -header -csv db 'SELECT ...' > out.csv\n"
|
|
97
|
+
"Prefer stdlib (csv, json, sqlite3) — always present. For big data, stream row\n"
|
|
98
|
+
"by row instead of loading all into memory."),
|
|
99
|
+
|
|
100
|
+
("Always verify against the task's own check before finishing",
|
|
101
|
+
["test", "verify", "check", "eval", "grader", "pass", "requirement", "make",
|
|
102
|
+
"pytest", "assert", "expected", "output"],
|
|
103
|
+
"Before you declare done:\n"
|
|
104
|
+
"1. Re-read the task and list EVERY requirement (exact filenames, paths, formats,\n"
|
|
105
|
+
" values, exit codes). Check each literally — a near-miss (5/6) still fails.\n"
|
|
106
|
+
"2. If the task ships a checker, RUN it and fix until green:\n"
|
|
107
|
+
" ls *test* *eval* Makefile 2>/dev/null; bash test.sh; python eval.py; make test\n"
|
|
108
|
+
"3. Anything you write must run in a CLEAN env — don't depend on a package you\n"
|
|
109
|
+
" just pip-installed unless the task says to; prefer stdlib / the task's tools.\n"
|
|
110
|
+
"4. Print the produced file/output and eyeball it against the spec."),
|
|
111
|
+
|
|
112
|
+
("Search / optimization over parameters (grid, random, bisect)",
|
|
113
|
+
["search", "optimize", "distribution", "parameter", "fit", "minimize",
|
|
114
|
+
"grid", "bisect", "tune", "best", "objective"],
|
|
115
|
+
"To find a parameter/value that satisfies a target:\n"
|
|
116
|
+
"- Monotonic target -> BISECTION: lo,hi; while hi-lo>eps: mid=(lo+hi)/2; move the\n"
|
|
117
|
+
" bound based on f(mid) vs target. (also `bisect` module for sorted lookup)\n"
|
|
118
|
+
"- Unknown landscape -> coarse GRID scan, then refine around the best point.\n"
|
|
119
|
+
"- Fitting a distribution: compute the empirical moments/quantiles and match, or\n"
|
|
120
|
+
" scipy.optimize.minimize on the negative log-likelihood. Validate with a KS test.\n"
|
|
121
|
+
"Always define the objective explicitly and print it each iteration so you can see\n"
|
|
122
|
+
"it converge instead of guessing."),
|
|
123
|
+
|
|
124
|
+
("Numerical sampling methods (rejection, inverse-CDF, adaptive)",
|
|
125
|
+
["sampling", "sample", "rejection", "adaptive", "distribution", "random",
|
|
126
|
+
"monte", "carlo", "pdf", "cdf", "envelope"],
|
|
127
|
+
"Draw from a density f you can evaluate:\n"
|
|
128
|
+
"- INVERSE-CDF: if F^-1 is known, x = F^-1(U), U~Uniform(0,1).\n"
|
|
129
|
+
"- REJECTION: pick envelope g with f(x) <= M*g(x); sample x~g, accept if\n"
|
|
130
|
+
" U*M*g(x) <= f(x). Efficiency = 1/M, so keep M tight.\n"
|
|
131
|
+
"- ADAPTIVE rejection (log-concave f): build a piecewise-linear upper hull of\n"
|
|
132
|
+
" log f from tangent lines at support points; sample from the hull's exp, accept/\n"
|
|
133
|
+
" reject, and ADD each rejected point to the hull so it tightens over time.\n"
|
|
134
|
+
"Seed the RNG for reproducibility and validate the sample mean/var against f."),
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
_WORD = re.compile(r"[a-z0-9.]+")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def retrieve_recipes(task_text: str, k: int = 2) -> list[tuple[str, str]]:
|
|
141
|
+
"""Return up to k (title, body) recipes most relevant to the task, by keyword
|
|
142
|
+
overlap. Empty if nothing scores — never inject irrelevant noise."""
|
|
143
|
+
if not task_text:
|
|
144
|
+
return []
|
|
145
|
+
words = set(_WORD.findall(task_text.lower()))
|
|
146
|
+
scored = []
|
|
147
|
+
for title, keywords, body in RECIPES:
|
|
148
|
+
score = sum(1 for kw in keywords if kw in words)
|
|
149
|
+
# also credit multiword keywords found as substrings (e.g. "pip install")
|
|
150
|
+
score += sum(1 for kw in keywords if " " in kw and kw in task_text.lower())
|
|
151
|
+
if score:
|
|
152
|
+
scored.append((score, title, body))
|
|
153
|
+
scored.sort(key=lambda x: -x[0])
|
|
154
|
+
return [(t, b) for _, t, b in scored[:k]]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def recipe_context(task_text: str, k: int = 2) -> str:
|
|
158
|
+
"""A system-prompt appendix of the recipes relevant to this task, or ''."""
|
|
159
|
+
hits = retrieve_recipes(task_text, k)
|
|
160
|
+
if not hits:
|
|
161
|
+
return ""
|
|
162
|
+
blocks = "\n\n".join(f"### {title}\n{body}" for title, body in hits)
|
|
163
|
+
return (
|
|
164
|
+
"\n\nRELEVANT TECHNIQUE RECIPES (reference for THIS task — use if applicable):\n"
|
|
165
|
+
+ blocks
|
|
166
|
+
)
|
|
@@ -20,6 +20,7 @@ drydock/loop_detect.py
|
|
|
20
20
|
drydock/mcp.py
|
|
21
21
|
drydock/poam.py
|
|
22
22
|
drydock/providers.py
|
|
23
|
+
drydock/recipes.py
|
|
23
24
|
drydock/rmf.py
|
|
24
25
|
drydock/rmf_graph.py
|
|
25
26
|
drydock/skills.py
|
|
@@ -93,6 +94,8 @@ tests/test_plan_autocontinue.py
|
|
|
93
94
|
tests/test_poam.py
|
|
94
95
|
tests/test_providers_unreachable.py
|
|
95
96
|
tests/test_read_index.py
|
|
97
|
+
tests/test_recipes.py
|
|
98
|
+
tests/test_repetition_interrupt.py
|
|
96
99
|
tests/test_rmf.py
|
|
97
100
|
tests/test_rmf_graph.py
|
|
98
101
|
tests/test_rmf_stig_graph.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.116"
|
|
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,55 @@
|
|
|
1
|
+
"""Recipe retrieval + injection: the right technique recipe reaches the model for a
|
|
2
|
+
task, nothing is injected for irrelevant chat, and it's gated by config `recipes`."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from drydock import agent
|
|
6
|
+
from drydock.providers import AssistantTurn
|
|
7
|
+
from drydock.recipes import recipe_context, retrieve_recipes
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _titles(text, k=2):
|
|
11
|
+
return [t for t, _ in retrieve_recipes(text, k)]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_matches_expected_recipe():
|
|
15
|
+
assert "Recover" in _titles("recover the deleted password forensic launchcode")[0]
|
|
16
|
+
assert "git" in _titles("remove the secret from git history sanitize")[0].lower()
|
|
17
|
+
assert any("NumPy" in t for t in _titles("fix numpy 2.0 compatibility cython build"))
|
|
18
|
+
assert "openssl" in _titles("self-signed certificate openssl x509")[0].lower()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_no_injection_for_chat():
|
|
22
|
+
assert retrieve_recipes("hello, how are you today?") == []
|
|
23
|
+
assert recipe_context("just say hi") == ""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_recipe_context_wraps_hits():
|
|
27
|
+
ctx = recipe_context("recover deleted password from binary file")
|
|
28
|
+
assert "TECHNIQUE RECIPES" in ctx and "strings" in ctx
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _run_capture(cfg, msg):
|
|
32
|
+
seen = {}
|
|
33
|
+
def fake_stream(model, system, messages, tool_schemas, config):
|
|
34
|
+
seen["system"] = system
|
|
35
|
+
yield AssistantTurn("done", [], 5, 5)
|
|
36
|
+
import drydock.agent as A
|
|
37
|
+
orig = A.stream
|
|
38
|
+
A.stream = fake_stream
|
|
39
|
+
try:
|
|
40
|
+
list(agent.run(msg, agent.AgentState(), cfg, "BASE-PROMPT"))
|
|
41
|
+
finally:
|
|
42
|
+
A.stream = orig
|
|
43
|
+
return seen["system"]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_injected_when_enabled():
|
|
47
|
+
cfg = {"model": "gemma4", "max_tokens": 8192, "context_limit": 65536, "recipes": True}
|
|
48
|
+
sysp = _run_capture(cfg, "recover the deleted password from launchcode.txt forensic")
|
|
49
|
+
assert "TECHNIQUE RECIPES" in sysp and "strings" in sysp
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_not_injected_when_disabled():
|
|
53
|
+
cfg = {"model": "gemma4", "max_tokens": 8192, "context_limit": 65536, "recipes": False}
|
|
54
|
+
sysp = _run_capture(cfg, "recover the deleted password from launchcode.txt forensic")
|
|
55
|
+
assert "TECHNIQUE RECIPES" not in sysp
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Content-based repetition trigger for the over-think interrupt: a non-streaming
|
|
2
|
+
turn that collapses into a pure loop AND produces no action escalates straight to
|
|
3
|
+
decisive mode. It must NOT fire on varied reasoning, on a turn with a tool call,
|
|
4
|
+
or when the interrupt is disabled (stall_secs=0)."""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from drydock import agent, providers
|
|
10
|
+
from drydock.providers import AssistantTurn, RepetitionDetected, _complete_nonstreaming
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _Fn:
|
|
14
|
+
def __init__(self, name, args): self.name = name; self.arguments = args
|
|
15
|
+
class _TC:
|
|
16
|
+
def __init__(self, name, args): self.id = "c1"; self.function = _Fn(name, args)
|
|
17
|
+
class _Msg:
|
|
18
|
+
def __init__(self, content, tcs=None): self.content = content; self.tool_calls = tcs or []
|
|
19
|
+
class _Choice:
|
|
20
|
+
def __init__(self, msg): self.message = msg
|
|
21
|
+
class _Resp:
|
|
22
|
+
def __init__(self, content, tcs=None): self.choices = [_Choice(_Msg(content, tcs))]; self.usage = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _patch(monkeypatch, content, tcs=None):
|
|
26
|
+
monkeypatch.setattr(providers, "_create_abortable", lambda *a, **k: _Resp(content, tcs))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_pure_loop_no_action_raises(monkeypatch):
|
|
30
|
+
_patch(monkeypatch, "loop " * 300) # 1500 chars of one unit
|
|
31
|
+
with pytest.raises(RepetitionDetected):
|
|
32
|
+
list(_complete_nonstreaming(None, {}, stall_secs=300))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_varied_reasoning_does_not_raise(monkeypatch):
|
|
36
|
+
_patch(monkeypatch, "First check the file, then run grep, then write the result and verify it.")
|
|
37
|
+
out = list(_complete_nonstreaming(None, {}, stall_secs=300))
|
|
38
|
+
assert any(isinstance(o, AssistantTurn) for o in out) # completed normally
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_loop_but_has_toolcall_does_not_raise(monkeypatch):
|
|
42
|
+
_patch(monkeypatch, "loop " * 300, [_TC("Bash", '{"command":"ls"}')])
|
|
43
|
+
out = list(_complete_nonstreaming(None, {}, stall_secs=300))
|
|
44
|
+
turn = [o for o in out if isinstance(o, AssistantTurn)][0]
|
|
45
|
+
assert turn.tool_calls and turn.tool_calls[0]["name"] == "Bash" # action preserved
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_disabled_when_stall_secs_zero(monkeypatch):
|
|
49
|
+
_patch(monkeypatch, "loop " * 300)
|
|
50
|
+
out = list(_complete_nonstreaming(None, {}, stall_secs=0)) # feature off
|
|
51
|
+
assert any(isinstance(o, AssistantTurn) for o in out)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_agent_goes_decisive_immediately_on_repetition(monkeypatch):
|
|
55
|
+
calls = []
|
|
56
|
+
def fake_stream(model, system, messages, tool_schemas, config):
|
|
57
|
+
calls.append({"system": system, "max_tokens": config.get("max_tokens")})
|
|
58
|
+
if len(calls) == 1:
|
|
59
|
+
raise RepetitionDetected(0)
|
|
60
|
+
yield AssistantTurn("done", [], 10, 5)
|
|
61
|
+
monkeypatch.setattr(agent, "stream", fake_stream)
|
|
62
|
+
st = agent.AgentState()
|
|
63
|
+
cfg = {"model": "gemma4", "max_tokens": 8192, "stall_retry_secs": 300, "context_limit": 65536}
|
|
64
|
+
list(agent.run("go", st, cfg, "SYS"))
|
|
65
|
+
# first call raised repetition → SECOND call is already decisive (no plain retry)
|
|
66
|
+
assert len(calls) == 2
|
|
67
|
+
assert agent._DECISIVE_SUFFIX in calls[1]["system"]
|
|
68
|
+
assert calls[1]["max_tokens"] <= agent._DECISIVE_MAX_TOKENS
|
|
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
|