drydock-cli 3.0.74__tar.gz → 3.0.76__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 (98) hide show
  1. {drydock_cli-3.0.74/drydock_cli.egg-info → drydock_cli-3.0.76}/PKG-INFO +37 -10
  2. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/README.md +36 -9
  3. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/providers.py +30 -0
  4. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/tools/__init__.py +23 -4
  5. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/tui/app.py +28 -4
  6. {drydock_cli-3.0.74 → drydock_cli-3.0.76/drydock_cli.egg-info}/PKG-INFO +37 -10
  7. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock_cli.egg-info/SOURCES.txt +1 -0
  8. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/pyproject.toml +1 -1
  9. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_dispatch.py +11 -0
  10. drydock_cli-3.0.76/tests/test_server_probe.py +39 -0
  11. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/LICENSE +0 -0
  12. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/NOTICE +0 -0
  13. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/__init__.py +0 -0
  14. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/__main__.py +0 -0
  15. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/agent.py +0 -0
  16. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/bash_safety.py +0 -0
  17. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/builtin_skills/__init__.py +0 -0
  18. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/builtin_skills/rmf-categorize.md +0 -0
  19. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/builtin_skills/rmf-control.md +0 -0
  20. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/builtin_skills/rmf-poam.md +0 -0
  21. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/builtin_skills/rmf-review.md +0 -0
  22. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/builtin_skills/stig-assess.md +0 -0
  23. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/builtin_skills/stig-remediate.md +0 -0
  24. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/cci.py +0 -0
  25. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/cli.py +0 -0
  26. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/compaction.py +0 -0
  27. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/config.py +0 -0
  28. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/detect.py +0 -0
  29. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/extract.py +0 -0
  30. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/gittools.py +0 -0
  31. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/graphrag.py +0 -0
  32. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/guards.py +0 -0
  33. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/loop_detect.py +0 -0
  34. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/mcp.py +0 -0
  35. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/rmf.py +0 -0
  36. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/rmf_graph.py +0 -0
  37. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/skills.py +0 -0
  38. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/stig.py +0 -0
  39. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/tool_registry.py +0 -0
  40. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/tui/__init__.py +0 -0
  41. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/tui/approval.py +0 -0
  42. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/tui/messages.py +0 -0
  43. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/tui/widgets.py +0 -0
  44. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/tuning.py +0 -0
  45. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock/web.py +0 -0
  46. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock_cli.egg-info/dependency_links.txt +0 -0
  47. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock_cli.egg-info/entry_points.txt +0 -0
  48. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock_cli.egg-info/requires.txt +0 -0
  49. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/drydock_cli.egg-info/top_level.txt +0 -0
  50. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/setup.cfg +0 -0
  51. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_approval.py +0 -0
  52. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_back_command.py +0 -0
  53. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_bash_output_bounding.py +0 -0
  54. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_bash_process_group.py +0 -0
  55. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_bash_safety.py +0 -0
  56. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_bash_timeout_network.py +0 -0
  57. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_cci.py +0 -0
  58. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_cli_agents.py +0 -0
  59. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_compact_command.py +0 -0
  60. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_compaction.py +0 -0
  61. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_config.py +0 -0
  62. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_config_migration.py +0 -0
  63. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_context_limit_config.py +0 -0
  64. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_detect.py +0 -0
  65. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_e2e_connected.py +0 -0
  66. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_empty_response.py +0 -0
  67. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_extract.py +0 -0
  68. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_failure_loop.py +0 -0
  69. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_first_run_setup.py +0 -0
  70. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_gittools.py +0 -0
  71. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_graphrag.py +0 -0
  72. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_guards_and_tools.py +0 -0
  73. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_hallucinated_tools.py +0 -0
  74. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_leaked_tool_call.py +0 -0
  75. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_loop_detect.py +0 -0
  76. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_mcp.py +0 -0
  77. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_oneshot_unreachable.py +0 -0
  78. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_plan_autocontinue.py +0 -0
  79. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_providers_unreachable.py +0 -0
  80. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_read_index.py +0 -0
  81. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_rmf.py +0 -0
  82. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_rmf_graph.py +0 -0
  83. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_rmf_stig_graph.py +0 -0
  84. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_runaway_repetition.py +0 -0
  85. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_skills.py +0 -0
  86. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_stig.py +0 -0
  87. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_stop.py +0 -0
  88. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_streaming_newlines.py +0 -0
  89. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_subagent.py +0 -0
  90. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_system_prompt_help.py +0 -0
  91. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_todo.py +0 -0
  92. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_tool_arg_parsing.py +0 -0
  93. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_tools_undo.py +0 -0
  94. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_tui.py +0 -0
  95. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_tuning.py +0 -0
  96. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_vision_input.py +0 -0
  97. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_web_tools.py +0 -0
  98. {drydock_cli-3.0.74 → drydock_cli-3.0.76}/tests/test_xccdf.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.74
3
+ Version: 3.0.76
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
@@ -91,10 +91,14 @@ Typed into the prompt. The agent also knows these, so you can just **ask it**
91
91
  | `/mcp` | List connected MCP servers + their tools |
92
92
  | `/rmf bootstrap [families]` | Ingest the NIST SP 800-53 catalog (RMF automation) |
93
93
  | `/rmf-control` · `/rmf-categorize` · `/rmf-review` · `/rmf-poam` | Bundled RMF skills |
94
+ | `/stig new <xccdf>` | Generate a blank `.ckl` from a DISA STIG benchmark |
95
+ | `/stig <ckl>` · `/stig <ckl> open` | Summarize a checklist · list findings by status |
96
+ | `/stig graph <ckl>` | Ingest a checklist into the RMF graph (auto-links rules→controls via CCI) |
97
+ | `/stig-assess <ckl>` · `/stig-remediate <ckl> <rule>` | Assess a rule vs evidence · write a fix script |
94
98
  | `/model` · `/cwd` | Show/set model & endpoint · working directory |
95
99
  | `/undo` · `/back` | Revert the last write · rewind the last turn |
96
- | `/compact` · `/status` · `/clear` | Shrink context · session stats · reset |
97
- | `/help` · `/quit` | Help · exit |
100
+ | `/compact` · `/context [n]` | Shrink context now · view/set the context-window budget |
101
+ | `/status` · `/clear` · `/help` · `/quit` | Session stats · reset · help · exit |
98
102
 
99
103
  ### Knowledge base (GraphRAG) — ingesting your documents
100
104
 
@@ -107,9 +111,11 @@ Typed into the prompt. The agent also knows these, so you can just **ask it**
107
111
 
108
112
  Once built, the agent **automatically** retrieves from it (read-only `Knowledge`
109
113
  tool) when a question touches your material. Ingests text formats
110
- (`.md .txt .py .js .json .yaml .sql …`) **plus PDF and Word (`.docx`)**. `.docx`
111
- needs nothing extra; PDF uses the `pdftotext` binary (poppler) if present, else
112
- `pip install drydock-cli[pdf]` (pypdf). The index is a single JSON at
114
+ (`.md .txt .py .js .json .yaml .sql …`), **PDF and Word (`.docx`)**, and **STIG
115
+ checklists (`.ckl`/`.cklb`)** checklists are flattened to per-rule findings so
116
+ you can ask "which findings are open?". `.docx`/`.ckl` need nothing extra; PDF
117
+ uses the `pdftotext` binary (poppler) if present, else `pip install
118
+ drydock-cli[pdf]` (pypdf). The index is a single JSON at
113
119
  `<project>/.drydock/graphrag.json` — clean-room, no embeddings.
114
120
 
115
121
  ### Custom skills
@@ -144,6 +150,26 @@ relationships with `GraphQuery` — including **control inheritance** ("which
144
150
  servers inherit physical controls from their enclave?"). Stdlib in-memory graph,
145
151
  no Neo4j.
146
152
 
153
+ ### STIG checklists (DISA `.ckl`/`.cklb`)
154
+
155
+ Take a raw DISA STIG benchmark all the way to a completed, eMASS/STIG-Viewer-
156
+ compatible checklist — entirely local (hostnames, IPs, and findings are CUI):
157
+
158
+ ```
159
+ /stig new U_ASD_STIG_V6R1_Manual-xccdf.xml app.ckl # benchmark → blank .ckl
160
+ /graphrag build ./app # pull in the app's evidence
161
+ /loop 286 /stig-assess app.ckl # assess each rule vs evidence
162
+ /stig app.ckl open # list the open findings
163
+ /stig-remediate app.ckl SV-900010r1_rule # write an idempotent fix script
164
+ /stig graph app.ckl # ingest + auto-link rules → NIST controls
165
+ ```
166
+
167
+ `/stig new` parses the XCCDF benchmark (validated against the full 286-rule
168
+ Application STIG); `/stig-assess` reads your evidence and writes status +
169
+ finding-details back in place; `/stig graph` builds `STIG`/`STIG-Rule` nodes and
170
+ **auto-links each rule to its NIST 800-53 control** through DISA's CCI map
171
+ (`Control —SATISFIED_BY→ rule`), fetched once and cached offline.
172
+
147
173
  ## Install
148
174
 
149
175
  ```bash
@@ -173,10 +199,11 @@ commands to do the work, showing each as a collapsible tool card.
173
199
  `◡ Keelhauling… (12s · ↓ 6.2k tokens · thinking with high effort)`
174
200
  - Submit while it's working and the prompt **queues** (drains in order)
175
201
  - Slash commands: `/model` · `/cwd` · `/undo` (revert last write) · `/back`
176
- (rewind last turn) · `/status` · `/compact` (shrink context) · `/graphrag`
177
- (build/query a knowledge base) · `/skills` (list your `/<name>` skills) ·
178
- `/loop` (repeat a prompt) · `/mcp` (list MCP servers) · `/clear` · `/help` ·
179
- `/quit`
202
+ (rewind last turn) · `/status` · `/compact` (shrink context) · `/context`
203
+ (view/set the context-window budget) · `/graphrag` (build/query a knowledge
204
+ base) · `/skills` (list your `/<name>` skills) · `/loop` (repeat a prompt) ·
205
+ `/mcp` (list MCP servers) · `/rmf` & `/stig` (NIST 800-53 / DISA STIG
206
+ automation) · `/clear` · `/help` · `/quit`
180
207
 
181
208
  It honors `AGENTS.md` / `DRYDOCK.md` in the working directory for project
182
209
  conventions.
@@ -67,10 +67,14 @@ Typed into the prompt. The agent also knows these, so you can just **ask it**
67
67
  | `/mcp` | List connected MCP servers + their tools |
68
68
  | `/rmf bootstrap [families]` | Ingest the NIST SP 800-53 catalog (RMF automation) |
69
69
  | `/rmf-control` · `/rmf-categorize` · `/rmf-review` · `/rmf-poam` | Bundled RMF skills |
70
+ | `/stig new <xccdf>` | Generate a blank `.ckl` from a DISA STIG benchmark |
71
+ | `/stig <ckl>` · `/stig <ckl> open` | Summarize a checklist · list findings by status |
72
+ | `/stig graph <ckl>` | Ingest a checklist into the RMF graph (auto-links rules→controls via CCI) |
73
+ | `/stig-assess <ckl>` · `/stig-remediate <ckl> <rule>` | Assess a rule vs evidence · write a fix script |
70
74
  | `/model` · `/cwd` | Show/set model & endpoint · working directory |
71
75
  | `/undo` · `/back` | Revert the last write · rewind the last turn |
72
- | `/compact` · `/status` · `/clear` | Shrink context · session stats · reset |
73
- | `/help` · `/quit` | Help · exit |
76
+ | `/compact` · `/context [n]` | Shrink context now · view/set the context-window budget |
77
+ | `/status` · `/clear` · `/help` · `/quit` | Session stats · reset · help · exit |
74
78
 
75
79
  ### Knowledge base (GraphRAG) — ingesting your documents
76
80
 
@@ -83,9 +87,11 @@ Typed into the prompt. The agent also knows these, so you can just **ask it**
83
87
 
84
88
  Once built, the agent **automatically** retrieves from it (read-only `Knowledge`
85
89
  tool) when a question touches your material. Ingests text formats
86
- (`.md .txt .py .js .json .yaml .sql …`) **plus PDF and Word (`.docx`)**. `.docx`
87
- needs nothing extra; PDF uses the `pdftotext` binary (poppler) if present, else
88
- `pip install drydock-cli[pdf]` (pypdf). The index is a single JSON at
90
+ (`.md .txt .py .js .json .yaml .sql …`), **PDF and Word (`.docx`)**, and **STIG
91
+ checklists (`.ckl`/`.cklb`)** checklists are flattened to per-rule findings so
92
+ you can ask "which findings are open?". `.docx`/`.ckl` need nothing extra; PDF
93
+ uses the `pdftotext` binary (poppler) if present, else `pip install
94
+ drydock-cli[pdf]` (pypdf). The index is a single JSON at
89
95
  `<project>/.drydock/graphrag.json` — clean-room, no embeddings.
90
96
 
91
97
  ### Custom skills
@@ -120,6 +126,26 @@ relationships with `GraphQuery` — including **control inheritance** ("which
120
126
  servers inherit physical controls from their enclave?"). Stdlib in-memory graph,
121
127
  no Neo4j.
122
128
 
129
+ ### STIG checklists (DISA `.ckl`/`.cklb`)
130
+
131
+ Take a raw DISA STIG benchmark all the way to a completed, eMASS/STIG-Viewer-
132
+ compatible checklist — entirely local (hostnames, IPs, and findings are CUI):
133
+
134
+ ```
135
+ /stig new U_ASD_STIG_V6R1_Manual-xccdf.xml app.ckl # benchmark → blank .ckl
136
+ /graphrag build ./app # pull in the app's evidence
137
+ /loop 286 /stig-assess app.ckl # assess each rule vs evidence
138
+ /stig app.ckl open # list the open findings
139
+ /stig-remediate app.ckl SV-900010r1_rule # write an idempotent fix script
140
+ /stig graph app.ckl # ingest + auto-link rules → NIST controls
141
+ ```
142
+
143
+ `/stig new` parses the XCCDF benchmark (validated against the full 286-rule
144
+ Application STIG); `/stig-assess` reads your evidence and writes status +
145
+ finding-details back in place; `/stig graph` builds `STIG`/`STIG-Rule` nodes and
146
+ **auto-links each rule to its NIST 800-53 control** through DISA's CCI map
147
+ (`Control —SATISFIED_BY→ rule`), fetched once and cached offline.
148
+
123
149
  ## Install
124
150
 
125
151
  ```bash
@@ -149,10 +175,11 @@ commands to do the work, showing each as a collapsible tool card.
149
175
  `◡ Keelhauling… (12s · ↓ 6.2k tokens · thinking with high effort)`
150
176
  - Submit while it's working and the prompt **queues** (drains in order)
151
177
  - Slash commands: `/model` · `/cwd` · `/undo` (revert last write) · `/back`
152
- (rewind last turn) · `/status` · `/compact` (shrink context) · `/graphrag`
153
- (build/query a knowledge base) · `/skills` (list your `/<name>` skills) ·
154
- `/loop` (repeat a prompt) · `/mcp` (list MCP servers) · `/clear` · `/help` ·
155
- `/quit`
178
+ (rewind last turn) · `/status` · `/compact` (shrink context) · `/context`
179
+ (view/set the context-window budget) · `/graphrag` (build/query a knowledge
180
+ base) · `/skills` (list your `/<name>` skills) · `/loop` (repeat a prompt) ·
181
+ `/mcp` (list MCP servers) · `/rmf` & `/stig` (NIST 800-53 / DISA STIG
182
+ automation) · `/clear` · `/help` · `/quit`
156
183
 
157
184
  It honors `AGENTS.md` / `DRYDOCK.md` in the working directory for project
158
185
  conventions.
@@ -56,6 +56,36 @@ PROVIDERS: dict[str, dict] = {
56
56
  }
57
57
 
58
58
 
59
+ def probe_server_context(base_url: str, *, timeout: float = 4.0) -> int | None:
60
+ """Best-effort: ask the model server its REAL context window (tokens).
61
+ llama.cpp exposes it on `/props` (n_ctx); vLLM puts `max_model_len` on
62
+ `/v1/models`. Returns None if unknown/unreachable — never raises. This is the
63
+ definitive diagnostic for a "stuck at N tokens" cap that isn't drydock's
64
+ config (it's the server's own `-c` / `--ctx-size` / `max_model_len`)."""
65
+ import json
66
+ import urllib.request
67
+
68
+ root = base_url.rstrip("/")
69
+ base_no_v1 = root[:-3].rstrip("/") if root.endswith("/v1") else root
70
+
71
+ def _n_ctx(d):
72
+ return (d.get("default_generation_settings") or {}).get("n_ctx") or d.get("n_ctx")
73
+
74
+ def _max_model_len(d):
75
+ return next((m.get("max_model_len") for m in d.get("data", []) if m.get("max_model_len")), None)
76
+
77
+ for url, pick in ((base_no_v1 + "/props", _n_ctx), (root + "/models", _max_model_len)):
78
+ try:
79
+ with urllib.request.urlopen(url, timeout=timeout) as r: # noqa: S310 (user's own server)
80
+ data = json.loads(r.read().decode("utf-8", "ignore"))
81
+ val = pick(data)
82
+ if isinstance(val, int) and val > 0:
83
+ return val
84
+ except Exception: # noqa: BLE001 — best-effort probe
85
+ continue
86
+ return None
87
+
88
+
59
89
  class LLMUnreachable(RuntimeError):
60
90
  """The configured LLM endpoint could not be reached. Carries a
61
91
  user-facing message with remediation steps (shown verbatim in the TUI)."""
@@ -1017,15 +1017,34 @@ _SUBAGENT_SYSTEM = (
1017
1017
  "tools: Read, Glob, Grep, and Bash (use Bash only to INSPECT — ls, cat, "
1018
1018
  "grep, find, git log — never to modify files). Investigate the task you are "
1019
1019
  "given, then STOP and reply with a concise, factual summary of what you "
1020
- "found: concrete file:line references and the key code, not narration. Do "
1021
- "NOT try to edit or create files the main agent acts on your findings."
1020
+ "found: concrete file:line references and only the few key code snippets that "
1021
+ "matter, not narration and not whole files. Aim for under ~250 words the "
1022
+ "main agent only gets this summary (not your tool output), so distill, don't "
1023
+ "dump. Do NOT try to edit or create files — the main agent acts on your findings."
1022
1024
  )
1023
1025
 
1024
1026
 
1027
+ # A sub-agent's whole job is to keep its investigation OUT of the main agent's
1028
+ # context and hand back only a partition. The system prompt asks for a concise
1029
+ # summary, but a runaway model could still return a wall of text — so cap what
1030
+ # crosses back into the parent's window. ~4000 chars ≈ ~1000 tokens.
1031
+ _SUBAGENT_SUMMARY_CAP = 4000
1032
+
1033
+
1034
+ def _cap_summary(text: str) -> str:
1035
+ if len(text) <= _SUBAGENT_SUMMARY_CAP:
1036
+ return text
1037
+ head = text[:_SUBAGENT_SUMMARY_CAP].rsplit("\n", 1)[0] or text[:_SUBAGENT_SUMMARY_CAP]
1038
+ dropped = len(text) - len(head)
1039
+ return (head + f"\n[… sub-agent summary truncated, {dropped} chars dropped to keep it out "
1040
+ "of the main context. Ask a narrower follow-up sub-agent task if you need more.]")
1041
+
1042
+
1025
1043
  def _run_subagent(prompt: str, config: dict) -> str:
1026
1044
  """Run one read-only sub-agent to completion and return its final summary.
1027
1045
  Shared by `task` (one) and `Dispatch` (many in parallel). Hard-capped; never
1028
- raises (a sub-agent must not crash the parent turn)."""
1046
+ raises (a sub-agent must not crash the parent turn). The returned summary is
1047
+ size-capped (_cap_summary) so a sub-agent can never bloat the main context."""
1029
1048
  from drydock.agent import run as agent_run, AgentState, TurnDone
1030
1049
 
1031
1050
  sub_state = AgentState()
@@ -1048,7 +1067,7 @@ def _run_subagent(prompt: str, config: dict) -> str:
1048
1067
  return f"[sub-agent error: {e}]"
1049
1068
  for msg in reversed(sub_state.messages):
1050
1069
  if msg.get("role") == "assistant" and (msg.get("content") or "").strip():
1051
- return msg["content"].strip()
1070
+ return _cap_summary(msg["content"].strip())
1052
1071
  return f"[sub-agent finished {steps} step(s) with no summary]"
1053
1072
 
1054
1073
 
@@ -758,12 +758,12 @@ class DrydockApp(App):
758
758
  arg = (arg or "").strip()
759
759
  if not arg:
760
760
  self._info(
761
- f"context_limit: {limit:,} tokens (the '/{limit // 1024}k' in the ctx gauge).\n"
761
+ f"context_limit (drydock): {limit:,} tokens (the '/{limit // 1024}k' in the ctx gauge).\n"
762
762
  "Source order: built-in default 65536 < ~/.drydock/config.toml < --context-limit.\n"
763
- "If you're stuck below this, your MODEL SERVER's context is smaller — raise its\n"
764
- "`-c`/`--ctx-size`/`max_model_len` to match. To change drydock's budget:\n"
765
- " /context <tokens> e.g. /context 65536 (saved to config.toml)"
763
+ "Probing your model server for its REAL context window…\n"
764
+ " To change drydock's budget: /context <tokens> (saved to config.toml)"
766
765
  )
766
+ self.run_worker(lambda: self._probe_server_context(limit), thread=True)
767
767
  return
768
768
  try:
769
769
  n = int(arg.replace(",", "").replace("k", "000").replace("K", "000"))
@@ -781,6 +781,30 @@ class DrydockApp(App):
781
781
  "least this, or the server will still cap you below it."
782
782
  )
783
783
 
784
+ def _probe_server_context(self, limit: int) -> None:
785
+ """Worker: ask the model server its real context window and report whether
786
+ IT (not drydock's config) is the thing capping you — the definitive answer
787
+ to 'stuck at N tokens'."""
788
+ from drydock import providers
789
+
790
+ base_url = self.config.get("base_url") or providers.PROVIDERS.get(
791
+ self.config.get("provider") or "vllm", {}).get("base_url", "http://localhost:8000/v1")
792
+ n_ctx = providers.probe_server_context(base_url)
793
+ if n_ctx is None:
794
+ msg = (f"Model server ({base_url}) didn't report its context size "
795
+ "(not llama.cpp /props or vLLM max_model_len, or unreachable). "
796
+ f"Your effective cap is the smaller of drydock's {limit:,} and the "
797
+ "server's own -c/--ctx-size.")
798
+ elif n_ctx < limit:
799
+ msg = (f"⚠ Model server reports n_ctx = {n_ctx:,} tokens — SMALLER than "
800
+ f"drydock's {limit:,}. The SERVER is your real cap (you'll stick near "
801
+ f"{n_ctx // 1024}k). Restart it with a larger -c/--ctx-size/max_model_len, "
802
+ "or lower /context to match.")
803
+ else:
804
+ msg = (f"✓ Model server reports n_ctx = {n_ctx:,} tokens (≥ drydock's {limit:,}), "
805
+ "so drydock's budget is the effective limit — no server-side cap.")
806
+ self.call_from_thread(self._info, msg)
807
+
784
808
  def _cmd_compact(self) -> None:
785
809
  """Manually compact the conversation to reclaim context NOW, without
786
810
  waiting for the automatic 60%-of-window threshold (agent.maybe_compact).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.74
3
+ Version: 3.0.76
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
@@ -91,10 +91,14 @@ Typed into the prompt. The agent also knows these, so you can just **ask it**
91
91
  | `/mcp` | List connected MCP servers + their tools |
92
92
  | `/rmf bootstrap [families]` | Ingest the NIST SP 800-53 catalog (RMF automation) |
93
93
  | `/rmf-control` · `/rmf-categorize` · `/rmf-review` · `/rmf-poam` | Bundled RMF skills |
94
+ | `/stig new <xccdf>` | Generate a blank `.ckl` from a DISA STIG benchmark |
95
+ | `/stig <ckl>` · `/stig <ckl> open` | Summarize a checklist · list findings by status |
96
+ | `/stig graph <ckl>` | Ingest a checklist into the RMF graph (auto-links rules→controls via CCI) |
97
+ | `/stig-assess <ckl>` · `/stig-remediate <ckl> <rule>` | Assess a rule vs evidence · write a fix script |
94
98
  | `/model` · `/cwd` | Show/set model & endpoint · working directory |
95
99
  | `/undo` · `/back` | Revert the last write · rewind the last turn |
96
- | `/compact` · `/status` · `/clear` | Shrink context · session stats · reset |
97
- | `/help` · `/quit` | Help · exit |
100
+ | `/compact` · `/context [n]` | Shrink context now · view/set the context-window budget |
101
+ | `/status` · `/clear` · `/help` · `/quit` | Session stats · reset · help · exit |
98
102
 
99
103
  ### Knowledge base (GraphRAG) — ingesting your documents
100
104
 
@@ -107,9 +111,11 @@ Typed into the prompt. The agent also knows these, so you can just **ask it**
107
111
 
108
112
  Once built, the agent **automatically** retrieves from it (read-only `Knowledge`
109
113
  tool) when a question touches your material. Ingests text formats
110
- (`.md .txt .py .js .json .yaml .sql …`) **plus PDF and Word (`.docx`)**. `.docx`
111
- needs nothing extra; PDF uses the `pdftotext` binary (poppler) if present, else
112
- `pip install drydock-cli[pdf]` (pypdf). The index is a single JSON at
114
+ (`.md .txt .py .js .json .yaml .sql …`), **PDF and Word (`.docx`)**, and **STIG
115
+ checklists (`.ckl`/`.cklb`)** checklists are flattened to per-rule findings so
116
+ you can ask "which findings are open?". `.docx`/`.ckl` need nothing extra; PDF
117
+ uses the `pdftotext` binary (poppler) if present, else `pip install
118
+ drydock-cli[pdf]` (pypdf). The index is a single JSON at
113
119
  `<project>/.drydock/graphrag.json` — clean-room, no embeddings.
114
120
 
115
121
  ### Custom skills
@@ -144,6 +150,26 @@ relationships with `GraphQuery` — including **control inheritance** ("which
144
150
  servers inherit physical controls from their enclave?"). Stdlib in-memory graph,
145
151
  no Neo4j.
146
152
 
153
+ ### STIG checklists (DISA `.ckl`/`.cklb`)
154
+
155
+ Take a raw DISA STIG benchmark all the way to a completed, eMASS/STIG-Viewer-
156
+ compatible checklist — entirely local (hostnames, IPs, and findings are CUI):
157
+
158
+ ```
159
+ /stig new U_ASD_STIG_V6R1_Manual-xccdf.xml app.ckl # benchmark → blank .ckl
160
+ /graphrag build ./app # pull in the app's evidence
161
+ /loop 286 /stig-assess app.ckl # assess each rule vs evidence
162
+ /stig app.ckl open # list the open findings
163
+ /stig-remediate app.ckl SV-900010r1_rule # write an idempotent fix script
164
+ /stig graph app.ckl # ingest + auto-link rules → NIST controls
165
+ ```
166
+
167
+ `/stig new` parses the XCCDF benchmark (validated against the full 286-rule
168
+ Application STIG); `/stig-assess` reads your evidence and writes status +
169
+ finding-details back in place; `/stig graph` builds `STIG`/`STIG-Rule` nodes and
170
+ **auto-links each rule to its NIST 800-53 control** through DISA's CCI map
171
+ (`Control —SATISFIED_BY→ rule`), fetched once and cached offline.
172
+
147
173
  ## Install
148
174
 
149
175
  ```bash
@@ -173,10 +199,11 @@ commands to do the work, showing each as a collapsible tool card.
173
199
  `◡ Keelhauling… (12s · ↓ 6.2k tokens · thinking with high effort)`
174
200
  - Submit while it's working and the prompt **queues** (drains in order)
175
201
  - Slash commands: `/model` · `/cwd` · `/undo` (revert last write) · `/back`
176
- (rewind last turn) · `/status` · `/compact` (shrink context) · `/graphrag`
177
- (build/query a knowledge base) · `/skills` (list your `/<name>` skills) ·
178
- `/loop` (repeat a prompt) · `/mcp` (list MCP servers) · `/clear` · `/help` ·
179
- `/quit`
202
+ (rewind last turn) · `/status` · `/compact` (shrink context) · `/context`
203
+ (view/set the context-window budget) · `/graphrag` (build/query a knowledge
204
+ base) · `/skills` (list your `/<name>` skills) · `/loop` (repeat a prompt) ·
205
+ `/mcp` (list MCP servers) · `/rmf` & `/stig` (NIST 800-53 / DISA STIG
206
+ automation) · `/clear` · `/help` · `/quit`
180
207
 
181
208
  It honors `AGENTS.md` / `DRYDOCK.md` in the working directory for project
182
209
  conventions.
@@ -79,6 +79,7 @@ tests/test_rmf.py
79
79
  tests/test_rmf_graph.py
80
80
  tests/test_rmf_stig_graph.py
81
81
  tests/test_runaway_repetition.py
82
+ tests/test_server_probe.py
82
83
  tests/test_skills.py
83
84
  tests/test_stig.py
84
85
  tests/test_stop.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.74"
10
+ version = "3.0.76"
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"
@@ -60,3 +60,14 @@ def test_task_still_works_via_shared_runner(monkeypatch):
60
60
  monkeypatch.setattr(T, "_run_subagent", lambda p, c: f"summary:{p}")
61
61
  assert T.tool_task({"prompt": "explore X"}, {}) == "summary:explore X"
62
62
  assert "needs a `prompt`" in T.tool_task({}, {})
63
+
64
+
65
+ def test_subagent_summary_is_capped():
66
+ """A sub-agent's return is size-capped so its investigation can never bloat
67
+ the main agent's context — only a bounded partition crosses back."""
68
+ from drydock.tools import _cap_summary, _SUBAGENT_SUMMARY_CAP
69
+ assert _cap_summary("brief finding") == "brief finding" # short passes through
70
+ big = "\n".join(f"line {i} with some detail" for i in range(2000))
71
+ out = _cap_summary(big)
72
+ assert len(out) < _SUBAGENT_SUMMARY_CAP + 200 # bounded
73
+ assert "truncated" in out and "main context" in out # explains the cut
@@ -0,0 +1,39 @@
1
+ """Server context-window probe — the definitive 'what's capping me' diagnostic."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+
6
+ from drydock import providers
7
+
8
+
9
+ class _Resp:
10
+ def __init__(self, body): self._b = json.dumps(body).encode()
11
+ def read(self): return self._b
12
+ def __enter__(self): return self
13
+ def __exit__(self, *a): return False
14
+
15
+
16
+ def test_probe_llamacpp_props_n_ctx(monkeypatch):
17
+ import urllib.request
18
+ def fake(url, timeout=0):
19
+ assert url.endswith("/props") # llama.cpp endpoint, /v1 stripped
20
+ return _Resp({"default_generation_settings": {"n_ctx": 32768}})
21
+ monkeypatch.setattr(urllib.request, "urlopen", fake)
22
+ assert providers.probe_server_context("http://localhost:8000/v1") == 32768
23
+
24
+
25
+ def test_probe_vllm_max_model_len(monkeypatch):
26
+ import urllib.request
27
+ def fake(url, timeout=0):
28
+ if url.endswith("/props"):
29
+ raise OSError("no props on vLLM")
30
+ return _Resp({"data": [{"id": "m", "max_model_len": 65536}]})
31
+ monkeypatch.setattr(urllib.request, "urlopen", fake)
32
+ assert providers.probe_server_context("http://localhost:8000/v1") == 65536
33
+
34
+
35
+ def test_probe_unreachable_is_none(monkeypatch):
36
+ import urllib.request
37
+ def boom(url, timeout=0): raise OSError("offline")
38
+ monkeypatch.setattr(urllib.request, "urlopen", boom)
39
+ assert providers.probe_server_context("http://localhost:9999/v1") is None
File without changes
File without changes
File without changes