drydock-cli 3.0.117__tar.gz → 3.0.119__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.117/drydock_cli.egg-info → drydock_cli-3.0.119}/PKG-INFO +1 -1
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/recipes.py +19 -0
- drydock_cli-3.0.119/drydock/suggest.py +30 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/tui/app.py +45 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock_cli.egg-info/SOURCES.txt +2 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/pyproject.toml +1 -1
- drydock_cli-3.0.119/tests/test_suggest.py +20 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/LICENSE +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/NOTICE +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/README.md +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/advisor.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/agent.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/cci.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/cli.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/config.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/detect.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/extract.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/guards.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/poam.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/providers.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/skills.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/stig.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/tools/__init__.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock/web.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/setup.cfg +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_advisor.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_bash_background.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_bash_binary_output.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_bash_crossplatform.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_bash_sanitize.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_bash_shell.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_bash_stdin.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_bash_stop_partial.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_bash_timeout_param.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_cci.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_config.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_edit_replace_all.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_glob_edit_edges.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_graphify_example.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_graphrag_quoted_path.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_grep_and_read_robust.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_overthink_interrupt.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_poam.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_recipes.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_repetition_interrupt.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_screenshot.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_stall_retry.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_tool_arg_coercion.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_tool_arg_coercion_more.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_viewimage.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_windows_shell.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_write_content_coerce.py +0 -0
- {drydock_cli-3.0.117 → drydock_cli-3.0.119}/tests/test_xccdf.py +0 -0
|
@@ -245,6 +245,25 @@ RECIPES: list[tuple[str, list[str], str]] = [
|
|
|
245
245
|
"Build: pdflatex -interaction=nonstopmode report.tex (run twice for refs).\n"
|
|
246
246
|
"Escape %, _, &, # in text. pandas: df.to_latex('t.tex', index=False). Confirm\n"
|
|
247
247
|
"report.pdf exists and pdflatex exit code is 0."),
|
|
248
|
+
|
|
249
|
+
("Reinforcement learning — Q-learning / policy on a small environment",
|
|
250
|
+
["rl", "reinforcement", "q-learning", "qlearning", "policy", "reward", "agent",
|
|
251
|
+
"environment", "gridworld", "episode", "bellman", "value", "action", "dqn",
|
|
252
|
+
"exploration", "epsilon", "gym", "cartpole", "bandit"],
|
|
253
|
+
"Tabular Q-learning for a discrete env with reset()/step(a)->(s2,r,done):\n"
|
|
254
|
+
" Q = np.zeros((nS, nA))\n"
|
|
255
|
+
" for ep in range(N):\n"
|
|
256
|
+
" s = env.reset()\n"
|
|
257
|
+
" for t in range(max_steps):\n"
|
|
258
|
+
" a = randint(nA) if rand() < eps else int(Q[s].argmax()) # eps-greedy\n"
|
|
259
|
+
" s2, r, done = env.step(a)\n"
|
|
260
|
+
" Q[s,a] += alpha * (r + gamma * Q[s2].max() - Q[s,a]) # Bellman update\n"
|
|
261
|
+
" s = s2\n"
|
|
262
|
+
" if done: break\n"
|
|
263
|
+
"Greedy policy = Q.argmax(1). Typical alpha=0.5, gamma=0.95, eps=0.1-0.2 (decay it).\n"
|
|
264
|
+
"Sanity: average episode reward should rise and the greedy policy should reach the\n"
|
|
265
|
+
"goal. For large/continuous states use a neural Q (DQN: replay buffer + target net)\n"
|
|
266
|
+
"or a policy network (REINFORCE: maximize sum of log_prob(a)*return)."),
|
|
248
267
|
]
|
|
249
268
|
|
|
250
269
|
_WORD = re.compile(r"[a-z0-9.]+")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Recommended next command — a single dimmed hint shown after each turn (à la
|
|
2
|
+
Claude Code's ghost suggestion). Pure logic so it's testable; the TUI renders it.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def suggest_next_command(
|
|
8
|
+
*,
|
|
9
|
+
ctx_pct: int,
|
|
10
|
+
wrote_files: bool,
|
|
11
|
+
ran_bash: bool,
|
|
12
|
+
had_error: bool,
|
|
13
|
+
in_git: bool,
|
|
14
|
+
plan_remaining: bool,
|
|
15
|
+
) -> str | None:
|
|
16
|
+
"""The single most useful next step given what the last turn did, or None to
|
|
17
|
+
show nothing (better no hint than a noisy one). Order = priority."""
|
|
18
|
+
if ctx_pct >= 78:
|
|
19
|
+
return "/compact" # context nearly full — shrink it before continuing
|
|
20
|
+
if had_error:
|
|
21
|
+
return "fix the error above"
|
|
22
|
+
if plan_remaining:
|
|
23
|
+
return "continue" # the model left plan steps unfinished
|
|
24
|
+
if wrote_files and in_git:
|
|
25
|
+
return "git diff" # review what changed
|
|
26
|
+
if wrote_files:
|
|
27
|
+
return "run the tests"
|
|
28
|
+
if ran_bash:
|
|
29
|
+
return None # ran a command but nothing else obvious — no clutter
|
|
30
|
+
return None
|
|
@@ -111,6 +111,8 @@ class DrydockApp(App):
|
|
|
111
111
|
height: auto; margin: 0 2 1 2; padding: 0 1; color: #d7e6ee;
|
|
112
112
|
border-left: thick #c9a227; background: #14241c;
|
|
113
113
|
}
|
|
114
|
+
/* Dimmed recommended-next-command hint (empty → 0 lines). */
|
|
115
|
+
#suggest { height: auto; margin: 0 3; color: #4a6b78; text-style: italic; }
|
|
114
116
|
/* One bottom-docked footer holds, top→bottom: working-line, prompt, status. */
|
|
115
117
|
#footer { dock: bottom; height: auto; background: #0b1f2a; }
|
|
116
118
|
#prompt {
|
|
@@ -140,6 +142,7 @@ class DrydockApp(App):
|
|
|
140
142
|
# does the same. priority so it isn't swallowed by the focused prompt.
|
|
141
143
|
Binding("escape", "stop", "Stop the running turn", priority=True),
|
|
142
144
|
Binding("ctrl+o", "toggle_tools", "Expand/collapse details"),
|
|
145
|
+
Binding("ctrl+n", "use_suggestion", "Use suggested next command", show=False),
|
|
143
146
|
# Scroll the transcript from the keyboard (focus stays on the prompt,
|
|
144
147
|
# and SSH sessions often don't forward the mouse wheel). priority=True
|
|
145
148
|
# so the prompt's TextArea doesn't swallow PageUp/PageDown first.
|
|
@@ -250,6 +253,13 @@ class DrydockApp(App):
|
|
|
250
253
|
self._skills = load_skills(config.get("cwd") or ".")
|
|
251
254
|
self._current_assistant: AssistantMessage | None = None
|
|
252
255
|
self._last_card: ToolCard | None = None
|
|
256
|
+
# Recommended-next-command hint state.
|
|
257
|
+
self._turn_tools: set[str] = set() # tool names used this turn
|
|
258
|
+
self._turn_error = False
|
|
259
|
+
self._plan_remaining = False
|
|
260
|
+
self._suggestion = ""
|
|
261
|
+
import os
|
|
262
|
+
self._in_git = os.path.isdir(os.path.join(config.get("cwd") or ".", ".git"))
|
|
253
263
|
self._busy = False
|
|
254
264
|
# STOP signal: Escape / "/stop" sets it; the agent loop checks it at safe
|
|
255
265
|
# points and ends the turn cleanly (session preserved). Lives in config
|
|
@@ -300,6 +310,7 @@ class DrydockApp(App):
|
|
|
300
310
|
with Vertical(id="footer"):
|
|
301
311
|
yield Static("", id="todo") # pinned task checklist (empty = hidden)
|
|
302
312
|
yield Static("", id="working") # in-line activity (empty when idle)
|
|
313
|
+
yield Static("", id="suggest") # dimmed recommended-next-command hint
|
|
303
314
|
yield PromptArea(id="prompt")
|
|
304
315
|
yield Static(self._status_text(), id="status")
|
|
305
316
|
|
|
@@ -446,6 +457,9 @@ class DrydockApp(App):
|
|
|
446
457
|
# turn. If this turn emits its own todo, _render_todo repopulates it.
|
|
447
458
|
self.config.pop("_todo", None)
|
|
448
459
|
self.query_one("#todo", Static).update("")
|
|
460
|
+
self._turn_tools = set()
|
|
461
|
+
self._turn_error = False
|
|
462
|
+
self.query_one("#suggest", Static).update("") # hide the hint while working
|
|
449
463
|
self._busy = True
|
|
450
464
|
self._work_start = time.monotonic()
|
|
451
465
|
self._last_progress = self._work_start
|
|
@@ -1210,6 +1224,7 @@ class DrydockApp(App):
|
|
|
1210
1224
|
|
|
1211
1225
|
def on_agent_tool_start(self, m: AgentToolStart) -> None:
|
|
1212
1226
|
self._current_assistant = None # end the current text block
|
|
1227
|
+
self._turn_tools.add(m.name)
|
|
1213
1228
|
if m.name == "todo":
|
|
1214
1229
|
self._render_todo(m.inputs.get("tasks", ""))
|
|
1215
1230
|
self._last_card = None # tool_end is a no-op for the checklist
|
|
@@ -1229,6 +1244,7 @@ class DrydockApp(App):
|
|
|
1229
1244
|
|
|
1230
1245
|
items = parse_todo(tasks)
|
|
1231
1246
|
panel = self.query_one("#todo", Static)
|
|
1247
|
+
self._plan_remaining = any(s != "done" for _, s in items)
|
|
1232
1248
|
if not items:
|
|
1233
1249
|
panel.update("") # collapses to 0 height
|
|
1234
1250
|
return
|
|
@@ -1269,9 +1285,38 @@ class DrydockApp(App):
|
|
|
1269
1285
|
return
|
|
1270
1286
|
self._repeat = None
|
|
1271
1287
|
self._refresh_status()
|
|
1288
|
+
self._update_suggestion()
|
|
1272
1289
|
self.query_one("#prompt", PromptArea).focus()
|
|
1273
1290
|
|
|
1291
|
+
def _update_suggestion(self) -> None:
|
|
1292
|
+
"""Compute + render the dimmed recommended-next-command hint (Claude-Code-
|
|
1293
|
+
style). Empty when nothing useful to suggest."""
|
|
1294
|
+
from drydock.suggest import suggest_next_command
|
|
1295
|
+
limit = self.config.get("context_limit", 65536) or 65536
|
|
1296
|
+
pct = min(100, round(self._ctx_tokens / limit * 100)) if limit else 0
|
|
1297
|
+
wrote = bool(self._turn_tools & {"Write", "Edit"})
|
|
1298
|
+
ran = "Bash" in self._turn_tools
|
|
1299
|
+
self._suggestion = suggest_next_command(
|
|
1300
|
+
ctx_pct=pct, wrote_files=wrote, ran_bash=ran, had_error=self._turn_error,
|
|
1301
|
+
in_git=self._in_git, plan_remaining=self._plan_remaining,
|
|
1302
|
+
) or ""
|
|
1303
|
+
hint = self.query_one("#suggest", Static)
|
|
1304
|
+
if self._suggestion:
|
|
1305
|
+
from rich.markup import escape
|
|
1306
|
+
hint.update(f"→ next: {escape(self._suggestion)} [dim](ctrl+n to use)[/]")
|
|
1307
|
+
else:
|
|
1308
|
+
hint.update("")
|
|
1309
|
+
|
|
1310
|
+
def action_use_suggestion(self) -> None:
|
|
1311
|
+
"""Accept the recommended next command — drop it into the prompt to edit/send."""
|
|
1312
|
+
if self._suggestion and not self._busy:
|
|
1313
|
+
prompt = self.query_one("#prompt", PromptArea)
|
|
1314
|
+
prompt.text = self._suggestion
|
|
1315
|
+
prompt.move_cursor(prompt.document.end)
|
|
1316
|
+
prompt.focus()
|
|
1317
|
+
|
|
1274
1318
|
def on_agent_error(self, m: AgentError) -> None:
|
|
1319
|
+
self._turn_error = True
|
|
1275
1320
|
self._mount(ErrorMessage(m.error))
|
|
1276
1321
|
|
|
1277
1322
|
|
|
@@ -25,6 +25,7 @@ drydock/rmf.py
|
|
|
25
25
|
drydock/rmf_graph.py
|
|
26
26
|
drydock/skills.py
|
|
27
27
|
drydock/stig.py
|
|
28
|
+
drydock/suggest.py
|
|
28
29
|
drydock/tool_registry.py
|
|
29
30
|
drydock/tuning.py
|
|
30
31
|
drydock/web.py
|
|
@@ -108,6 +109,7 @@ tests/test_stig.py
|
|
|
108
109
|
tests/test_stop.py
|
|
109
110
|
tests/test_streaming_newlines.py
|
|
110
111
|
tests/test_subagent.py
|
|
112
|
+
tests/test_suggest.py
|
|
111
113
|
tests/test_system_prompt_help.py
|
|
112
114
|
tests/test_todo.py
|
|
113
115
|
tests/test_tool_arg_coercion.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.119"
|
|
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,20 @@
|
|
|
1
|
+
"""Recommended-next-command suggestion logic (priority-ordered, None when noisy)."""
|
|
2
|
+
from drydock.suggest import suggest_next_command as s
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_priority_and_cases():
|
|
6
|
+
base = dict(ctx_pct=10, wrote_files=False, ran_bash=False, had_error=False,
|
|
7
|
+
in_git=False, plan_remaining=False)
|
|
8
|
+
assert s(**{**base, "ctx_pct": 80}) == "/compact" # ctx wins
|
|
9
|
+
assert s(**{**base, "ctx_pct": 80, "had_error": True}) == "/compact"
|
|
10
|
+
assert s(**{**base, "had_error": True}) == "fix the error above"
|
|
11
|
+
assert s(**{**base, "plan_remaining": True}) == "continue"
|
|
12
|
+
assert s(**{**base, "wrote_files": True, "in_git": True}) == "git diff"
|
|
13
|
+
assert s(**{**base, "wrote_files": True}) == "run the tests"
|
|
14
|
+
assert s(**{**base, "ran_bash": True}) is None # no clutter
|
|
15
|
+
assert s(**base) is None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_error_beats_plan_and_writes():
|
|
19
|
+
assert s(ctx_pct=10, wrote_files=True, ran_bash=True, had_error=True,
|
|
20
|
+
in_git=True, plan_remaining=True) == "fix the error above"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|