drydock-cli 3.0.115__tar.gz → 3.0.117__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.115/drydock_cli.egg-info → drydock_cli-3.0.117}/PKG-INFO +1 -1
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/agent.py +10 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/config.py +5 -0
- drydock_cli-3.0.117/drydock/recipes.py +279 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock_cli.egg-info/SOURCES.txt +2 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/pyproject.toml +1 -1
- drydock_cli-3.0.117/tests/test_recipes.py +55 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/LICENSE +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/NOTICE +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/README.md +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/advisor.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/cci.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/cli.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/detect.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/extract.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/guards.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/poam.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/providers.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/skills.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/stig.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/tools/__init__.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/tui/app.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock/web.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/setup.cfg +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_advisor.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_bash_background.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_bash_binary_output.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_bash_crossplatform.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_bash_sanitize.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_bash_shell.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_bash_stdin.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_bash_stop_partial.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_bash_timeout_param.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_cci.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_config.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_edit_replace_all.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_glob_edit_edges.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_graphify_example.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_graphrag_quoted_path.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_grep_and_read_robust.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_overthink_interrupt.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_poam.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_repetition_interrupt.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_screenshot.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_stall_retry.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_tool_arg_coercion.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_tool_arg_coercion_more.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_viewimage.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_windows_shell.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_write_content_coerce.py +0 -0
- {drydock_cli-3.0.115 → drydock_cli-3.0.117}/tests/test_xccdf.py +0 -0
|
@@ -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
|
|
@@ -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
|
|
@@ -0,0 +1,279 @@
|
|
|
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
|
+
("Classification metrics — accuracy, precision, recall, F1, MCC, ROC/AUC, confusion matrix",
|
|
137
|
+
["metric", "metrics", "accuracy", "precision", "recall", "f1", "mcc", "roc",
|
|
138
|
+
"auc", "confusion", "tp", "fp", "fn", "tn", "classification", "eval",
|
|
139
|
+
"evaluate", "score", "true", "positive", "negative"],
|
|
140
|
+
"Given y_true and y_pred (and y_score = P(positive) for ROC):\n"
|
|
141
|
+
" from sklearn.metrics import (accuracy_score, precision_score, recall_score,\n"
|
|
142
|
+
" f1_score, matthews_corrcoef, roc_auc_score, roc_curve, confusion_matrix)\n"
|
|
143
|
+
" acc=accuracy_score(y_true,y_pred); f1=f1_score(y_true,y_pred)\n"
|
|
144
|
+
" prec=precision_score(y_true,y_pred); rec=recall_score(y_true,y_pred)\n"
|
|
145
|
+
" mcc=matthews_corrcoef(y_true,y_pred); auc=roc_auc_score(y_true,y_score)\n"
|
|
146
|
+
" tn,fp,fn,tp = confusion_matrix(y_true,y_pred).ravel() # BINARY order!\n"
|
|
147
|
+
" fpr,tpr,thr = roc_curve(y_true,y_score) # plot tpr vs fpr; AUC=area\n"
|
|
148
|
+
"Definitions: precision=TP/(TP+FP), recall=TP/(TP+FN), F1=2PR/(P+R),\n"
|
|
149
|
+
"MCC=(TP*TN-FP*FN)/sqrt((TP+FP)(TP+FN)(TN+FP)(TN+FN)). Multiclass: pass\n"
|
|
150
|
+
"average='macro'/'weighted'. Save metrics as JSON; plot ROC with matplotlib\n"
|
|
151
|
+
"(plt.plot(fpr,tpr); savefig). Watch class order and the positive label."),
|
|
152
|
+
|
|
153
|
+
("PyTorch training / eval loop (correct structure)",
|
|
154
|
+
["pytorch", "torch", "train", "training", "model", "loss", "optimizer",
|
|
155
|
+
"epoch", "cnn", "network", "nn", "fit", "gpu", "cuda", "dataloader"],
|
|
156
|
+
" import torch, torch.nn as nn\n"
|
|
157
|
+
" dev = 'cuda' if torch.cuda.is_available() else 'cpu'; model.to(dev)\n"
|
|
158
|
+
" opt = torch.optim.AdamW(model.parameters(), lr=1e-3)\n"
|
|
159
|
+
" loss_fn = nn.CrossEntropyLoss() # expects raw logits + int class targets\n"
|
|
160
|
+
" for epoch in range(E):\n"
|
|
161
|
+
" model.train()\n"
|
|
162
|
+
" for xb,yb in train_loader:\n"
|
|
163
|
+
" xb,yb = xb.to(dev), yb.to(dev)\n"
|
|
164
|
+
" opt.zero_grad(); out = model(xb); loss = loss_fn(out, yb)\n"
|
|
165
|
+
" loss.backward(); opt.step()\n"
|
|
166
|
+
" model.eval()\n"
|
|
167
|
+
" with torch.no_grad(): ... # compute val metric, no grad\n"
|
|
168
|
+
"Gotchas: zero_grad EVERY step; CrossEntropyLoss takes logits (no softmax);\n"
|
|
169
|
+
"targets are Long class indices; move BOTH model and batch to the same device;\n"
|
|
170
|
+
"set model.eval() (disables dropout/BN update) for validation. Seed torch for repro."),
|
|
171
|
+
|
|
172
|
+
("LoRA / full fine-tuning with transformers + peft",
|
|
173
|
+
["lora", "peft", "finetune", "fine-tune", "fine", "tuning", "adapter",
|
|
174
|
+
"transformers", "pretrained", "huggingface", "bert", "gpt", "vit", "freeze"],
|
|
175
|
+
"FULL fine-tune: load a pretrained model, train ALL params on your data with a\n"
|
|
176
|
+
"small LR (2e-5..5e-5), AdamW, a few epochs; save with model.save_pretrained(dir).\n"
|
|
177
|
+
"LoRA (train ~0.1% of params): \n"
|
|
178
|
+
" from peft import LoraConfig, get_peft_model, TaskType\n"
|
|
179
|
+
" cfg = LoraConfig(r=8, lora_alpha=16, lora_dropout=0.05,\n"
|
|
180
|
+
" target_modules=['q_proj','v_proj'], task_type=TaskType.SEQ_CLS)\n"
|
|
181
|
+
" model = get_peft_model(base_model, cfg)\n"
|
|
182
|
+
" model.print_trainable_parameters() # sanity: only adapters train\n"
|
|
183
|
+
" ... train ... ; model.save_pretrained('adapter')\n"
|
|
184
|
+
"Reload: PeftModel.from_pretrained(base, 'adapter'). target_modules names differ\n"
|
|
185
|
+
"per architecture — inspect model.named_modules() to find the linear layers."),
|
|
186
|
+
|
|
187
|
+
("Prepare / organize / split a dataset",
|
|
188
|
+
["data", "dataset", "prep", "prepare", "organize", "split", "train", "val",
|
|
189
|
+
"test", "normalize", "preprocess", "balance", "stratify", "label", "clean"],
|
|
190
|
+
" from sklearn.model_selection import train_test_split\n"
|
|
191
|
+
" Xtr,Xtmp,ytr,ytmp = train_test_split(X,y,test_size=0.3,stratify=y,random_state=0)\n"
|
|
192
|
+
" Xval,Xte,yval,yte = train_test_split(Xtmp,ytmp,test_size=0.5,stratify=ytmp,random_state=0)\n"
|
|
193
|
+
"Normalize features on TRAIN ONLY, then apply to val/test (no leakage):\n"
|
|
194
|
+
" from sklearn.preprocessing import StandardScaler\n"
|
|
195
|
+
" sc=StandardScaler().fit(Xtr); Xtr=sc.transform(Xtr); Xte=sc.transform(Xte)\n"
|
|
196
|
+
"Images: resize + ToTensor + Normalize(mean,std). Keep a fixed split (seed) and\n"
|
|
197
|
+
"check class balance (np.bincount(y)). Never fit scalers/encoders on test data."),
|
|
198
|
+
|
|
199
|
+
("Read/write HDF5 (.h5) and convert to/from JSON",
|
|
200
|
+
["h5", "hdf5", "h5py", "json", "convert", "dataset", "array", "save", "load",
|
|
201
|
+
"file", "format", "serialize"],
|
|
202
|
+
" import h5py, numpy as np, json\n"
|
|
203
|
+
" with h5py.File('data.h5','r') as f:\n"
|
|
204
|
+
" print(list(f.keys())); arr = f['images'][:]; labels = f['labels'][:]\n"
|
|
205
|
+
" attrs = dict(f['images'].attrs) # metadata\n"
|
|
206
|
+
" with h5py.File('out.h5','w') as f:\n"
|
|
207
|
+
" f.create_dataset('images', data=arr, compression='gzip')\n"
|
|
208
|
+
"H5<->JSON: arrays aren't JSON-native — use arr.tolist() to dump, np.array to\n"
|
|
209
|
+
"reload. json.dump({'shape':list(arr.shape),'data':arr.tolist()}, open('x.json','w')).\n"
|
|
210
|
+
"For big arrays keep them in H5 and put only metadata/paths in JSON."),
|
|
211
|
+
|
|
212
|
+
("Implement a Transformer block / ViT",
|
|
213
|
+
["transformer", "attention", "vit", "vision", "encoder", "block", "self-attention",
|
|
214
|
+
"multihead", "patch", "embedding", "positional", "layernorm"],
|
|
215
|
+
"Transformer encoder block (pre-norm):\n"
|
|
216
|
+
" attn = nn.MultiheadAttention(d, heads, batch_first=True)\n"
|
|
217
|
+
" x = x + attn(ln1(x), ln1(x), ln1(x))[0] # residual\n"
|
|
218
|
+
" x = x + mlp(ln2(x)) # mlp: Linear(d,4d)->GELU->Linear(4d,d)\n"
|
|
219
|
+
"ViT: split image into patches (Conv2d(kernel=stride=patch) -> flatten), prepend a\n"
|
|
220
|
+
"learnable [CLS] token, ADD positional embeddings, run N encoder blocks, classify\n"
|
|
221
|
+
"from the CLS token. Shapes: (B,C,H,W)->(B, num_patches+1, d). Verify a forward\n"
|
|
222
|
+
"pass runs and output is (B, num_classes); check grads flow (loss.backward())."),
|
|
223
|
+
|
|
224
|
+
("Diagnose a broken PyTorch training run",
|
|
225
|
+
["debug", "diagnose", "nan", "loss", "not", "learning", "overfit", "underfit",
|
|
226
|
+
"shape", "mismatch", "error", "troubleshoot", "fix", "exploding", "gradient"],
|
|
227
|
+
"- Loss is NaN/Inf: LR too high (lower it), log/sqrt of <=0, divide-by-zero, or\n"
|
|
228
|
+
" unnormalized inputs. Add torch.autograd.set_detect_anomaly(True); clip grads\n"
|
|
229
|
+
" (nn.utils.clip_grad_norm_); check for NaN in the data.\n"
|
|
230
|
+
"- Loss flat / not learning: forgot opt.zero_grad or opt.step; LR too low; wrong\n"
|
|
231
|
+
" loss (softmax before CrossEntropyLoss); labels/logits misaligned; model not in\n"
|
|
232
|
+
" train(); frozen params. Print loss each step + a param's grad norm.\n"
|
|
233
|
+
"- Shape mismatch: print .shape at each layer; CrossEntropyLoss wants logits\n"
|
|
234
|
+
" (N,C) + targets (N,) Long. - Train acc high, val low: overfitting -> augment/\n"
|
|
235
|
+
" regularize/early-stop. Always overfit a tiny batch first as a sanity check."),
|
|
236
|
+
|
|
237
|
+
("Generate a LaTeX report / results table",
|
|
238
|
+
["latex", "report", "table", "pdf", "document", "tex", "pdflatex", "figure",
|
|
239
|
+
"results", "write", "tabular", "booktabs"],
|
|
240
|
+
"Emit a compilable .tex, then build it:\n"
|
|
241
|
+
" \\documentclass{article}\\usepackage{booktabs,graphicx}\\begin{document}\n"
|
|
242
|
+
" \\begin{tabular}{lr}\\toprule Metric & Value\\\\\\midrule\n"
|
|
243
|
+
" Accuracy & 0.94\\\\ F1 & 0.92\\\\\\bottomrule\\end{tabular}\n"
|
|
244
|
+
" \\includegraphics[width=.6\\linewidth]{roc.png}\\end{document}\n"
|
|
245
|
+
"Build: pdflatex -interaction=nonstopmode report.tex (run twice for refs).\n"
|
|
246
|
+
"Escape %, _, &, # in text. pandas: df.to_latex('t.tex', index=False). Confirm\n"
|
|
247
|
+
"report.pdf exists and pdflatex exit code is 0."),
|
|
248
|
+
]
|
|
249
|
+
|
|
250
|
+
_WORD = re.compile(r"[a-z0-9.]+")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def retrieve_recipes(task_text: str, k: int = 2) -> list[tuple[str, str]]:
|
|
254
|
+
"""Return up to k (title, body) recipes most relevant to the task, by keyword
|
|
255
|
+
overlap. Empty if nothing scores — never inject irrelevant noise."""
|
|
256
|
+
if not task_text:
|
|
257
|
+
return []
|
|
258
|
+
words = set(_WORD.findall(task_text.lower()))
|
|
259
|
+
scored = []
|
|
260
|
+
for title, keywords, body in RECIPES:
|
|
261
|
+
score = sum(1 for kw in keywords if kw in words)
|
|
262
|
+
# also credit multiword keywords found as substrings (e.g. "pip install")
|
|
263
|
+
score += sum(1 for kw in keywords if " " in kw and kw in task_text.lower())
|
|
264
|
+
if score:
|
|
265
|
+
scored.append((score, title, body))
|
|
266
|
+
scored.sort(key=lambda x: -x[0])
|
|
267
|
+
return [(t, b) for _, t, b in scored[:k]]
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def recipe_context(task_text: str, k: int = 2) -> str:
|
|
271
|
+
"""A system-prompt appendix of the recipes relevant to this task, or ''."""
|
|
272
|
+
hits = retrieve_recipes(task_text, k)
|
|
273
|
+
if not hits:
|
|
274
|
+
return ""
|
|
275
|
+
blocks = "\n\n".join(f"### {title}\n{body}" for title, body in hits)
|
|
276
|
+
return (
|
|
277
|
+
"\n\nRELEVANT TECHNIQUE RECIPES (reference for THIS task — use if applicable):\n"
|
|
278
|
+
+ blocks
|
|
279
|
+
)
|
|
@@ -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,7 @@ 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
|
|
96
98
|
tests/test_repetition_interrupt.py
|
|
97
99
|
tests/test_rmf.py
|
|
98
100
|
tests/test_rmf_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.117"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|