drydock-cli 3.0.56__tar.gz → 3.0.58__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. {drydock_cli-3.0.56/drydock_cli.egg-info → drydock_cli-3.0.58}/PKG-INFO +50 -20
  2. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/README.md +49 -19
  3. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/tools/__init__.py +59 -22
  4. {drydock_cli-3.0.56 → drydock_cli-3.0.58/drydock_cli.egg-info}/PKG-INFO +50 -20
  5. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock_cli.egg-info/SOURCES.txt +1 -0
  6. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/pyproject.toml +1 -1
  7. drydock_cli-3.0.58/tests/test_bash_output_bounding.py +37 -0
  8. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_failure_loop.py +23 -0
  9. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/LICENSE +0 -0
  10. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/NOTICE +0 -0
  11. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/__init__.py +0 -0
  12. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/__main__.py +0 -0
  13. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/agent.py +0 -0
  14. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/bash_safety.py +0 -0
  15. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/cli.py +0 -0
  16. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/compaction.py +0 -0
  17. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/config.py +0 -0
  18. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/detect.py +0 -0
  19. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/gittools.py +0 -0
  20. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/graphrag.py +0 -0
  21. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/guards.py +0 -0
  22. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/loop_detect.py +0 -0
  23. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/mcp.py +0 -0
  24. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/providers.py +0 -0
  25. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/skills.py +0 -0
  26. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/tool_registry.py +0 -0
  27. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/tui/__init__.py +0 -0
  28. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/tui/app.py +0 -0
  29. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/tui/approval.py +0 -0
  30. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/tui/messages.py +0 -0
  31. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/tui/widgets.py +0 -0
  32. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/tuning.py +0 -0
  33. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock/web.py +0 -0
  34. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock_cli.egg-info/dependency_links.txt +0 -0
  35. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock_cli.egg-info/entry_points.txt +0 -0
  36. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock_cli.egg-info/requires.txt +0 -0
  37. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/drydock_cli.egg-info/top_level.txt +0 -0
  38. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/setup.cfg +0 -0
  39. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_approval.py +0 -0
  40. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_back_command.py +0 -0
  41. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_bash_process_group.py +0 -0
  42. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_bash_safety.py +0 -0
  43. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_bash_timeout_network.py +0 -0
  44. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_cli_agents.py +0 -0
  45. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_compact_command.py +0 -0
  46. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_compaction.py +0 -0
  47. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_config.py +0 -0
  48. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_config_migration.py +0 -0
  49. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_context_limit_config.py +0 -0
  50. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_detect.py +0 -0
  51. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_dispatch.py +0 -0
  52. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_empty_response.py +0 -0
  53. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_first_run_setup.py +0 -0
  54. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_gittools.py +0 -0
  55. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_graphrag.py +0 -0
  56. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_guards_and_tools.py +0 -0
  57. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_hallucinated_tools.py +0 -0
  58. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_leaked_tool_call.py +0 -0
  59. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_loop_detect.py +0 -0
  60. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_mcp.py +0 -0
  61. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_oneshot_unreachable.py +0 -0
  62. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_plan_autocontinue.py +0 -0
  63. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_providers_unreachable.py +0 -0
  64. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_read_index.py +0 -0
  65. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_runaway_repetition.py +0 -0
  66. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_skills.py +0 -0
  67. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_stop.py +0 -0
  68. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_streaming_newlines.py +0 -0
  69. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_subagent.py +0 -0
  70. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_todo.py +0 -0
  71. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_tool_arg_parsing.py +0 -0
  72. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_tools_undo.py +0 -0
  73. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_tui.py +0 -0
  74. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_tuning.py +0 -0
  75. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_vision_input.py +0 -0
  76. {drydock_cli-3.0.56 → drydock_cli-3.0.58}/tests/test_web_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.56
3
+ Version: 3.0.58
4
4
  Summary: Drydock — a local, provider-agnostic terminal coding agent for local LLMs
5
5
  Author: Frank Bobe III
6
6
  License-Expression: Apache-2.0
@@ -23,9 +23,10 @@ Dynamic: license-file
23
23
  # ⚓ Drydock
24
24
 
25
25
  A local-first, provider-agnostic **terminal coding agent** for your own LLM.
26
- No accounts, no telemetry, no cloud — the only network call it makes is to
27
- the model endpoint you configure. Primary target: **Gemma-4-26B-A4B** served
28
- by llama.cpp on a single workstation.
26
+ No accounts, no telemetry, no cloud — the only outbound calls are to the model
27
+ endpoint you configure and (optionally) the web-search tools you invoke.
28
+ Primary target: **dense Gemma-4-31B** (QAT, 64K) served by llama.cpp on a
29
+ single workstation.
29
30
 
30
31
  > **v3 — clean-room rebuild.** Drydock is being rebuilt as an original,
31
32
  > Apache-2.0 codebase owned end to end (no upstream fork). Every release is
@@ -44,9 +45,31 @@ your box.
44
45
 
45
46
  Shipping. Published on PyPI as **`drydock-cli`** (v3.x). The Textual TUI is the
46
47
  default surface: a scrolling transcript with streamed assistant text, collapsible
47
- tool cards, a live nautical activity line, and a multi-line prompt. The agent
48
- loop, OpenAI-compatible provider, two-tier compaction, and the core tools
49
- (Read/Write/Edit/Bash/Glob/Grep) are in, with Gemma reliability hardening verified.
48
+ tool cards, collapsible reasoning ("thinking") cards, a live nautical activity
49
+ line, and a multi-line prompt. The agent loop, OpenAI-compatible provider,
50
+ two-tier compaction, and the full agentic toolset (below) are in, with Gemma
51
+ reliability hardening verified hands-on.
52
+
53
+ ## Capabilities
54
+
55
+ A full agentic CLI harness — every tool below is clean-room and dependency-free
56
+ (nothing beyond `openai` + `textual`), and the model calls them autonomously:
57
+
58
+ - **Files & shell** — `Read` (with a structure index for huge files), `Write`,
59
+ `Edit`, `Bash`, `Glob`, `Grep`.
60
+ - **Version control** — `GitStatus`, `GitDiff`, `GitLog`, `GitCommit`
61
+ (structured + truncated; commit is local and reversible).
62
+ - **Internet** — `WebSearch` + `WebFetch` (DuckDuckGo; offline-safe).
63
+ - **Knowledge base (GraphRAG)** — build a local entity-graph index from your
64
+ docs/code with `/graphrag build <path>`; the agent retrieves from it via the
65
+ read-only `Knowledge` tool.
66
+ - **Multi-agent** — `Dispatch` runs several read-only sub-agents in parallel;
67
+ `task` runs one — each in a fresh context, for focused investigation.
68
+ - **MCP** — connect to Model Context Protocol servers (`~/.drydock/mcp.json`);
69
+ their tools appear as `mcp__<server>__<tool>`. List them with `/mcp`.
70
+ - **Skills** — reusable `/<name>` commands authored as markdown in
71
+ `~/.drydock/skills/` (or `<project>/.drydock/skills/`); `$ARGS` substitution.
72
+ - **Loops** — `/loop <count> <prompt>` runs a prompt iteratively (Esc stops).
50
73
 
51
74
  ## Install
52
75
 
@@ -55,7 +78,7 @@ pip install drydock-cli
55
78
  drydock
56
79
  ```
57
80
 
58
- Requires Python 3.12+. From source instead:
81
+ Requires Python 3.11+. From source instead:
59
82
  `git clone https://github.com/fbobe321/drydock-v3.git && cd drydock-v3 && pip install -e .`
60
83
 
61
84
  On first launch with no config, Drydock probes localhost for a running local
@@ -76,8 +99,11 @@ commands to do the work, showing each as a collapsible tool card.
76
99
  - A live activity line shows progress while it works:
77
100
  `◡ Keelhauling… (12s · ↓ 6.2k tokens · thinking with high effort)`
78
101
  - Submit while it's working and the prompt **queues** (drains in order)
79
- - Slash commands: `/model [name]` · `/cwd [path]` · `/undo` (revert last write)
80
- · `/back` (rewind last turn) · `/status` · `/clear` · `/help` · `/quit`
102
+ - Slash commands: `/model` · `/cwd` · `/undo` (revert last write) · `/back`
103
+ (rewind last turn) · `/status` · `/compact` (shrink context) · `/graphrag`
104
+ (build/query a knowledge base) · `/skills` (list your `/<name>` skills) ·
105
+ `/loop` (repeat a prompt) · `/mcp` (list MCP servers) · `/clear` · `/help` ·
106
+ `/quit`
81
107
 
82
108
  It honors `AGENTS.md` / `DRYDOCK.md` in the working directory for project
83
109
  conventions.
@@ -96,22 +122,26 @@ blocked:
96
122
  `raise` outside an except, and refuses to write git conflict-marker content.
97
123
 
98
124
  Point it at a local OpenAI-compatible endpoint (e.g. llama.cpp's `server-cuda`
99
- serving Gemma 4 26B).
125
+ serving Gemma-4-31B). The web tools (`WebSearch`/`WebFetch`) are read-only and
126
+ degrade cleanly offline; the release scanner allowlists only the search backend.
100
127
 
101
128
  ## Model server (reference setup)
102
129
 
103
130
  Drydock is provider-agnostic, but it's tuned and measured against this rig:
104
131
 
105
- - **Model:** Gemma-4-26B-A4B-it (26B MoE, ~4B active/token), Unsloth `Q3_K_M`
106
- GGUF, served by `ghcr.io/ggml-org/llama.cpp:server-cuda` with `--jinja`.
107
- - **GPUs:** NVIDIA RTX 4060 Ti 16GB. The Q3_K_M weights (~13 GB) fit on a
108
- **single 16 GB card**, so each GPU runs a **full, independent instance**
109
- two cards give two parallel instances for throughput; the model is **not**
110
- tensor-split or "pooled" across both.
132
+ - **Model:** dense **Gemma-4-31B** (QAT `Q4_K_XL` GGUF), served by
133
+ `ghcr.io/ggml-org/llama.cpp:server-cuda` with `--jinja`. Swapped from the
134
+ 26B-A4B MoE, whose ~4B active params caused fatal agentic tool-loops; the
135
+ dense 31B is loop-free (slower, but it finishes).
136
+ - **GPUs:** NVIDIA RTX 4060 Ti 16GB, **tensor-split** across both cards
137
+ (`--tensor-split 1,1`) so the 31B weights fit.
111
138
  - **Context:** 64k (`-c 65536`) with `q8_0` KV-cache quantization
112
- (`-ctk q8_0 -ctv q8_0`) 64k @ q8 fits roughly the same VRAM as 32k @ f16.
113
- - **Throughput:** ~**64 tok/s** decode, ~94 tok/s prompt (per instance, Q3_K_M).
114
- - **Minimum:** any single 16 GB+ CUDA card runs it.
139
+ (`-ctk q8_0 -ctv q8_0`); set `context_limit` in `~/.drydock/config.toml` to
140
+ match your server's `-c`.
141
+ - **Throughput:** ~15 tok/s decode (tensor-split 31B). Faster single-GPU
142
+ options exist if you drop to a smaller model.
143
+ - **Provider-agnostic:** any OpenAI-compatible endpoint (llama.cpp, vLLM,
144
+ Ollama, LM Studio) works — point `--base-url` at it.
115
145
 
116
146
  ## Principles
117
147
 
@@ -1,9 +1,10 @@
1
1
  # ⚓ Drydock
2
2
 
3
3
  A local-first, provider-agnostic **terminal coding agent** for your own LLM.
4
- No accounts, no telemetry, no cloud — the only network call it makes is to
5
- the model endpoint you configure. Primary target: **Gemma-4-26B-A4B** served
6
- by llama.cpp on a single workstation.
4
+ No accounts, no telemetry, no cloud — the only outbound calls are to the model
5
+ endpoint you configure and (optionally) the web-search tools you invoke.
6
+ Primary target: **dense Gemma-4-31B** (QAT, 64K) served by llama.cpp on a
7
+ single workstation.
7
8
 
8
9
  > **v3 — clean-room rebuild.** Drydock is being rebuilt as an original,
9
10
  > Apache-2.0 codebase owned end to end (no upstream fork). Every release is
@@ -22,9 +23,31 @@ your box.
22
23
 
23
24
  Shipping. Published on PyPI as **`drydock-cli`** (v3.x). The Textual TUI is the
24
25
  default surface: a scrolling transcript with streamed assistant text, collapsible
25
- tool cards, a live nautical activity line, and a multi-line prompt. The agent
26
- loop, OpenAI-compatible provider, two-tier compaction, and the core tools
27
- (Read/Write/Edit/Bash/Glob/Grep) are in, with Gemma reliability hardening verified.
26
+ tool cards, collapsible reasoning ("thinking") cards, a live nautical activity
27
+ line, and a multi-line prompt. The agent loop, OpenAI-compatible provider,
28
+ two-tier compaction, and the full agentic toolset (below) are in, with Gemma
29
+ reliability hardening verified hands-on.
30
+
31
+ ## Capabilities
32
+
33
+ A full agentic CLI harness — every tool below is clean-room and dependency-free
34
+ (nothing beyond `openai` + `textual`), and the model calls them autonomously:
35
+
36
+ - **Files & shell** — `Read` (with a structure index for huge files), `Write`,
37
+ `Edit`, `Bash`, `Glob`, `Grep`.
38
+ - **Version control** — `GitStatus`, `GitDiff`, `GitLog`, `GitCommit`
39
+ (structured + truncated; commit is local and reversible).
40
+ - **Internet** — `WebSearch` + `WebFetch` (DuckDuckGo; offline-safe).
41
+ - **Knowledge base (GraphRAG)** — build a local entity-graph index from your
42
+ docs/code with `/graphrag build <path>`; the agent retrieves from it via the
43
+ read-only `Knowledge` tool.
44
+ - **Multi-agent** — `Dispatch` runs several read-only sub-agents in parallel;
45
+ `task` runs one — each in a fresh context, for focused investigation.
46
+ - **MCP** — connect to Model Context Protocol servers (`~/.drydock/mcp.json`);
47
+ their tools appear as `mcp__<server>__<tool>`. List them with `/mcp`.
48
+ - **Skills** — reusable `/<name>` commands authored as markdown in
49
+ `~/.drydock/skills/` (or `<project>/.drydock/skills/`); `$ARGS` substitution.
50
+ - **Loops** — `/loop <count> <prompt>` runs a prompt iteratively (Esc stops).
28
51
 
29
52
  ## Install
30
53
 
@@ -33,7 +56,7 @@ pip install drydock-cli
33
56
  drydock
34
57
  ```
35
58
 
36
- Requires Python 3.12+. From source instead:
59
+ Requires Python 3.11+. From source instead:
37
60
  `git clone https://github.com/fbobe321/drydock-v3.git && cd drydock-v3 && pip install -e .`
38
61
 
39
62
  On first launch with no config, Drydock probes localhost for a running local
@@ -54,8 +77,11 @@ commands to do the work, showing each as a collapsible tool card.
54
77
  - A live activity line shows progress while it works:
55
78
  `◡ Keelhauling… (12s · ↓ 6.2k tokens · thinking with high effort)`
56
79
  - Submit while it's working and the prompt **queues** (drains in order)
57
- - Slash commands: `/model [name]` · `/cwd [path]` · `/undo` (revert last write)
58
- · `/back` (rewind last turn) · `/status` · `/clear` · `/help` · `/quit`
80
+ - Slash commands: `/model` · `/cwd` · `/undo` (revert last write) · `/back`
81
+ (rewind last turn) · `/status` · `/compact` (shrink context) · `/graphrag`
82
+ (build/query a knowledge base) · `/skills` (list your `/<name>` skills) ·
83
+ `/loop` (repeat a prompt) · `/mcp` (list MCP servers) · `/clear` · `/help` ·
84
+ `/quit`
59
85
 
60
86
  It honors `AGENTS.md` / `DRYDOCK.md` in the working directory for project
61
87
  conventions.
@@ -74,22 +100,26 @@ blocked:
74
100
  `raise` outside an except, and refuses to write git conflict-marker content.
75
101
 
76
102
  Point it at a local OpenAI-compatible endpoint (e.g. llama.cpp's `server-cuda`
77
- serving Gemma 4 26B).
103
+ serving Gemma-4-31B). The web tools (`WebSearch`/`WebFetch`) are read-only and
104
+ degrade cleanly offline; the release scanner allowlists only the search backend.
78
105
 
79
106
  ## Model server (reference setup)
80
107
 
81
108
  Drydock is provider-agnostic, but it's tuned and measured against this rig:
82
109
 
83
- - **Model:** Gemma-4-26B-A4B-it (26B MoE, ~4B active/token), Unsloth `Q3_K_M`
84
- GGUF, served by `ghcr.io/ggml-org/llama.cpp:server-cuda` with `--jinja`.
85
- - **GPUs:** NVIDIA RTX 4060 Ti 16GB. The Q3_K_M weights (~13 GB) fit on a
86
- **single 16 GB card**, so each GPU runs a **full, independent instance**
87
- two cards give two parallel instances for throughput; the model is **not**
88
- tensor-split or "pooled" across both.
110
+ - **Model:** dense **Gemma-4-31B** (QAT `Q4_K_XL` GGUF), served by
111
+ `ghcr.io/ggml-org/llama.cpp:server-cuda` with `--jinja`. Swapped from the
112
+ 26B-A4B MoE, whose ~4B active params caused fatal agentic tool-loops; the
113
+ dense 31B is loop-free (slower, but it finishes).
114
+ - **GPUs:** NVIDIA RTX 4060 Ti 16GB, **tensor-split** across both cards
115
+ (`--tensor-split 1,1`) so the 31B weights fit.
89
116
  - **Context:** 64k (`-c 65536`) with `q8_0` KV-cache quantization
90
- (`-ctk q8_0 -ctv q8_0`) 64k @ q8 fits roughly the same VRAM as 32k @ f16.
91
- - **Throughput:** ~**64 tok/s** decode, ~94 tok/s prompt (per instance, Q3_K_M).
92
- - **Minimum:** any single 16 GB+ CUDA card runs it.
117
+ (`-ctk q8_0 -ctv q8_0`); set `context_limit` in `~/.drydock/config.toml` to
118
+ match your server's `-c`.
119
+ - **Throughput:** ~15 tok/s decode (tensor-split 31B). Faster single-GPU
120
+ options exist if you drop to a smaller model.
121
+ - **Provider-agnostic:** any OpenAI-compatible endpoint (llama.cpp, vLLM,
122
+ Ollama, LM Studio) works — point `--base-url` at it.
93
123
 
94
124
  ## Principles
95
125
 
@@ -11,9 +11,17 @@ import difflib
11
11
  import glob as _glob
12
12
  import signal
13
13
  import subprocess
14
+ import threading
14
15
  import time
15
16
  from pathlib import Path
16
17
 
18
+ # Hard ceiling on bytes read from a command's output. communicate() buffers ALL
19
+ # stdout in RAM before we ever truncate for context, so a runaway/infinite-output
20
+ # command (`yes`, `cat /dev/urandom`, a massive build log) could balloon memory
21
+ # to gigabytes within the timeout. We stream with a byte cap and kill the command
22
+ # once it's hit — bounding RAM regardless of how much it tries to produce.
23
+ _MAX_BASH_OUTPUT_BYTES = 256 * 1024 # 256 KB — plenty of context, safe for RAM
24
+
17
25
  from drydock.tool_registry import ToolDef, register
18
26
  from drydock.guards import (
19
27
  conflict_marker_refusal,
@@ -728,30 +736,59 @@ def tool_bash(params: dict, config: dict) -> str:
728
736
  text=True, cwd=config.get("cwd"), start_new_session=True,
729
737
  )
730
738
  config.setdefault("_abort", {})["proc"] = proc
739
+ # Read output in a daemon thread with a HARD byte cap, so memory can't
740
+ # balloon on a runaway-output command. The thread stops (and we kill the
741
+ # process) once the cap is hit; the main loop polls cancel + timeout.
742
+ chunks: list[str] = []
743
+ total = [0]
744
+ capped = threading.Event()
745
+
746
+ def _drain():
747
+ assert proc.stdout is not None
748
+ while True:
749
+ block = proc.stdout.read(8192)
750
+ if not block:
751
+ break
752
+ chunks.append(block)
753
+ total[0] += len(block)
754
+ if total[0] >= _MAX_BASH_OUTPUT_BYTES:
755
+ capped.set()
756
+ break
757
+
758
+ reader = threading.Thread(target=_drain, daemon=True)
759
+ reader.start()
731
760
  start = time.monotonic()
732
- while True:
733
- try:
734
- out, _ = proc.communicate(timeout=0.5)
761
+ while reader.is_alive():
762
+ reader.join(0.3)
763
+ if capped.is_set():
764
+ kill_process_group(proc) # stop it producing more
735
765
  break
736
- except subprocess.TimeoutExpired:
737
- if cancel is not None and cancel.is_set():
738
- kill_process_group(proc)
739
- proc.communicate()
740
- return "[stopped by user]"
741
- if time.monotonic() - start > timeout:
742
- kill_process_group(proc)
743
- proc.communicate()
744
- bigger = min(timeout * 4, 1800)
745
- msg = (
746
- f"Error: command timed out after {timeout}s. If it is "
747
- f"legitimately slow (a big query, build, download, or "
748
- f"test run), retry with a larger timeout — pass "
749
- f"timeout: {bigger}. Otherwise it may be hung."
750
- )
751
- if _is_network_command(cmd):
752
- msg += _OFFLINE_HINT
753
- return msg
754
- output = out or ""
766
+ if cancel is not None and cancel.is_set():
767
+ kill_process_group(proc)
768
+ proc.wait()
769
+ return "[stopped by user]"
770
+ if time.monotonic() - start > timeout:
771
+ kill_process_group(proc)
772
+ proc.wait()
773
+ bigger = min(timeout * 4, 1800)
774
+ msg = (
775
+ f"Error: command timed out after {timeout}s. If it is "
776
+ f"legitimately slow (a big query, build, download, or "
777
+ f"test run), retry with a larger timeout pass "
778
+ f"timeout: {bigger}. Otherwise it may be hung."
779
+ )
780
+ if _is_network_command(cmd):
781
+ msg += _OFFLINE_HINT
782
+ return msg
783
+ proc.wait()
784
+ output = "".join(chunks)
785
+ if capped.is_set():
786
+ return (
787
+ output[:_MAX_BASH_OUTPUT_BYTES]
788
+ + f"\n[output truncated at {_MAX_BASH_OUTPUT_BYTES // 1024} KB — "
789
+ "the command produced more; redirect to a file and inspect it in "
790
+ "pieces (head/tail/grep) instead of dumping it all]"
791
+ )
755
792
  if proc.returncode != 0:
756
793
  output += f"\n[exit code: {proc.returncode}]"
757
794
  # Offline environments make downloads fail forever; the model tends
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.56
3
+ Version: 3.0.58
4
4
  Summary: Drydock — a local, provider-agnostic terminal coding agent for local LLMs
5
5
  Author: Frank Bobe III
6
6
  License-Expression: Apache-2.0
@@ -23,9 +23,10 @@ Dynamic: license-file
23
23
  # ⚓ Drydock
24
24
 
25
25
  A local-first, provider-agnostic **terminal coding agent** for your own LLM.
26
- No accounts, no telemetry, no cloud — the only network call it makes is to
27
- the model endpoint you configure. Primary target: **Gemma-4-26B-A4B** served
28
- by llama.cpp on a single workstation.
26
+ No accounts, no telemetry, no cloud — the only outbound calls are to the model
27
+ endpoint you configure and (optionally) the web-search tools you invoke.
28
+ Primary target: **dense Gemma-4-31B** (QAT, 64K) served by llama.cpp on a
29
+ single workstation.
29
30
 
30
31
  > **v3 — clean-room rebuild.** Drydock is being rebuilt as an original,
31
32
  > Apache-2.0 codebase owned end to end (no upstream fork). Every release is
@@ -44,9 +45,31 @@ your box.
44
45
 
45
46
  Shipping. Published on PyPI as **`drydock-cli`** (v3.x). The Textual TUI is the
46
47
  default surface: a scrolling transcript with streamed assistant text, collapsible
47
- tool cards, a live nautical activity line, and a multi-line prompt. The agent
48
- loop, OpenAI-compatible provider, two-tier compaction, and the core tools
49
- (Read/Write/Edit/Bash/Glob/Grep) are in, with Gemma reliability hardening verified.
48
+ tool cards, collapsible reasoning ("thinking") cards, a live nautical activity
49
+ line, and a multi-line prompt. The agent loop, OpenAI-compatible provider,
50
+ two-tier compaction, and the full agentic toolset (below) are in, with Gemma
51
+ reliability hardening verified hands-on.
52
+
53
+ ## Capabilities
54
+
55
+ A full agentic CLI harness — every tool below is clean-room and dependency-free
56
+ (nothing beyond `openai` + `textual`), and the model calls them autonomously:
57
+
58
+ - **Files & shell** — `Read` (with a structure index for huge files), `Write`,
59
+ `Edit`, `Bash`, `Glob`, `Grep`.
60
+ - **Version control** — `GitStatus`, `GitDiff`, `GitLog`, `GitCommit`
61
+ (structured + truncated; commit is local and reversible).
62
+ - **Internet** — `WebSearch` + `WebFetch` (DuckDuckGo; offline-safe).
63
+ - **Knowledge base (GraphRAG)** — build a local entity-graph index from your
64
+ docs/code with `/graphrag build <path>`; the agent retrieves from it via the
65
+ read-only `Knowledge` tool.
66
+ - **Multi-agent** — `Dispatch` runs several read-only sub-agents in parallel;
67
+ `task` runs one — each in a fresh context, for focused investigation.
68
+ - **MCP** — connect to Model Context Protocol servers (`~/.drydock/mcp.json`);
69
+ their tools appear as `mcp__<server>__<tool>`. List them with `/mcp`.
70
+ - **Skills** — reusable `/<name>` commands authored as markdown in
71
+ `~/.drydock/skills/` (or `<project>/.drydock/skills/`); `$ARGS` substitution.
72
+ - **Loops** — `/loop <count> <prompt>` runs a prompt iteratively (Esc stops).
50
73
 
51
74
  ## Install
52
75
 
@@ -55,7 +78,7 @@ pip install drydock-cli
55
78
  drydock
56
79
  ```
57
80
 
58
- Requires Python 3.12+. From source instead:
81
+ Requires Python 3.11+. From source instead:
59
82
  `git clone https://github.com/fbobe321/drydock-v3.git && cd drydock-v3 && pip install -e .`
60
83
 
61
84
  On first launch with no config, Drydock probes localhost for a running local
@@ -76,8 +99,11 @@ commands to do the work, showing each as a collapsible tool card.
76
99
  - A live activity line shows progress while it works:
77
100
  `◡ Keelhauling… (12s · ↓ 6.2k tokens · thinking with high effort)`
78
101
  - Submit while it's working and the prompt **queues** (drains in order)
79
- - Slash commands: `/model [name]` · `/cwd [path]` · `/undo` (revert last write)
80
- · `/back` (rewind last turn) · `/status` · `/clear` · `/help` · `/quit`
102
+ - Slash commands: `/model` · `/cwd` · `/undo` (revert last write) · `/back`
103
+ (rewind last turn) · `/status` · `/compact` (shrink context) · `/graphrag`
104
+ (build/query a knowledge base) · `/skills` (list your `/<name>` skills) ·
105
+ `/loop` (repeat a prompt) · `/mcp` (list MCP servers) · `/clear` · `/help` ·
106
+ `/quit`
81
107
 
82
108
  It honors `AGENTS.md` / `DRYDOCK.md` in the working directory for project
83
109
  conventions.
@@ -96,22 +122,26 @@ blocked:
96
122
  `raise` outside an except, and refuses to write git conflict-marker content.
97
123
 
98
124
  Point it at a local OpenAI-compatible endpoint (e.g. llama.cpp's `server-cuda`
99
- serving Gemma 4 26B).
125
+ serving Gemma-4-31B). The web tools (`WebSearch`/`WebFetch`) are read-only and
126
+ degrade cleanly offline; the release scanner allowlists only the search backend.
100
127
 
101
128
  ## Model server (reference setup)
102
129
 
103
130
  Drydock is provider-agnostic, but it's tuned and measured against this rig:
104
131
 
105
- - **Model:** Gemma-4-26B-A4B-it (26B MoE, ~4B active/token), Unsloth `Q3_K_M`
106
- GGUF, served by `ghcr.io/ggml-org/llama.cpp:server-cuda` with `--jinja`.
107
- - **GPUs:** NVIDIA RTX 4060 Ti 16GB. The Q3_K_M weights (~13 GB) fit on a
108
- **single 16 GB card**, so each GPU runs a **full, independent instance**
109
- two cards give two parallel instances for throughput; the model is **not**
110
- tensor-split or "pooled" across both.
132
+ - **Model:** dense **Gemma-4-31B** (QAT `Q4_K_XL` GGUF), served by
133
+ `ghcr.io/ggml-org/llama.cpp:server-cuda` with `--jinja`. Swapped from the
134
+ 26B-A4B MoE, whose ~4B active params caused fatal agentic tool-loops; the
135
+ dense 31B is loop-free (slower, but it finishes).
136
+ - **GPUs:** NVIDIA RTX 4060 Ti 16GB, **tensor-split** across both cards
137
+ (`--tensor-split 1,1`) so the 31B weights fit.
111
138
  - **Context:** 64k (`-c 65536`) with `q8_0` KV-cache quantization
112
- (`-ctk q8_0 -ctv q8_0`) 64k @ q8 fits roughly the same VRAM as 32k @ f16.
113
- - **Throughput:** ~**64 tok/s** decode, ~94 tok/s prompt (per instance, Q3_K_M).
114
- - **Minimum:** any single 16 GB+ CUDA card runs it.
139
+ (`-ctk q8_0 -ctv q8_0`); set `context_limit` in `~/.drydock/config.toml` to
140
+ match your server's `-c`.
141
+ - **Throughput:** ~15 tok/s decode (tensor-split 31B). Faster single-GPU
142
+ options exist if you drop to a smaller model.
143
+ - **Provider-agnostic:** any OpenAI-compatible endpoint (llama.cpp, vLLM,
144
+ Ollama, LM Studio) works — point `--base-url` at it.
115
145
 
116
146
  ## Principles
117
147
 
@@ -34,6 +34,7 @@ drydock_cli.egg-info/requires.txt
34
34
  drydock_cli.egg-info/top_level.txt
35
35
  tests/test_approval.py
36
36
  tests/test_back_command.py
37
+ tests/test_bash_output_bounding.py
37
38
  tests/test_bash_process_group.py
38
39
  tests/test_bash_safety.py
39
40
  tests/test_bash_timeout_network.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.56"
10
+ version = "3.0.58"
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,37 @@
1
+ """Critical VRAM/RAM protection (PRD §3.B): tool_bash must bound a command's
2
+ output DURING capture so a runaway/infinite-output command can't balloon memory
3
+ before the context-truncation cap applies."""
4
+ from __future__ import annotations
5
+
6
+ import time
7
+
8
+ from drydock.tools import tool_bash, _MAX_BASH_OUTPUT_BYTES
9
+
10
+
11
+ def test_infinite_output_is_capped_and_fast(tmp_path):
12
+ cfg = {"cwd": str(tmp_path)}
13
+ t0 = time.monotonic()
14
+ out = tool_bash({"command": "yes"}, cfg) # infinite stream
15
+ elapsed = time.monotonic() - t0
16
+ assert "truncated at" in out # capped, not dumped
17
+ assert len(out) < _MAX_BASH_OUTPUT_BYTES + 500 # bounded memory
18
+ assert elapsed < 20 # killed promptly, no hang
19
+
20
+
21
+ def test_large_finite_output_is_capped(tmp_path):
22
+ cfg = {"cwd": str(tmp_path)}
23
+ # ~1 MB of output → must come back truncated near the cap, not 1 MB.
24
+ out = tool_bash({"command": "head -c 1000000 /dev/zero | tr '\\0' 'a'"}, cfg)
25
+ assert len(out) < _MAX_BASH_OUTPUT_BYTES + 500
26
+
27
+
28
+ def test_normal_output_untouched(tmp_path):
29
+ cfg = {"cwd": str(tmp_path)}
30
+ out = tool_bash({"command": "echo hello world"}, cfg)
31
+ assert out == "hello world" and "truncated" not in out
32
+
33
+
34
+ def test_exit_code_still_reported(tmp_path):
35
+ cfg = {"cwd": str(tmp_path)}
36
+ out = tool_bash({"command": "echo oops; exit 3"}, cfg)
37
+ assert "oops" in out and "exit code: 3" in out
@@ -75,3 +75,26 @@ def test_polling_with_changing_result_not_tripped(monkeypatch):
75
75
  st = AgentState()
76
76
  list(run("go", st, {"model": "m", "cwd": tempfile.mkdtemp()}, "sys"))
77
77
  assert any(m.get("content") == "done" for m in st.messages)
78
+
79
+
80
+ def test_infinite_distinct_tool_calls_terminate_at_max_turns(monkeypatch):
81
+ """PRD §3.D: a model that keeps calling tools (DIFFERENT, succeeding calls)
82
+ and never yields 'Task Complete' must be forcibly stopped at the hard
83
+ max-iteration ceiling — protecting VRAM/context from an unbounded loop."""
84
+ calls = {"n": 0}
85
+
86
+ def never_done(**kw):
87
+ calls["n"] += 1
88
+ # A unique, succeeding command each turn → the identical-repeat valve
89
+ # never trips, so only the hard max_turns cap can end it.
90
+ return iter([AssistantTurn(
91
+ "", [{"id": str(calls["n"]), "name": "Bash",
92
+ "input": {"command": f"echo step {calls['n']}"}}], 1, 1)])
93
+
94
+ monkeypatch.setattr(agent_mod, "stream", never_done)
95
+ st = AgentState()
96
+ import tempfile
97
+ # Low cap so the test is fast; the real default is 200.
98
+ list(run("loop forever", st, {"model": "m", "max_turns": 12,
99
+ "cwd": tempfile.mkdtemp()}, "sys"))
100
+ assert calls["n"] == 12 # ran exactly to the ceiling, then terminated
File without changes
File without changes
File without changes