drydock-cli 3.0.84__tar.gz → 3.0.86__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.84/drydock_cli.egg-info → drydock_cli-3.0.86}/PKG-INFO +8 -4
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/README.md +7 -3
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/tui/app.py +50 -9
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/tui/widgets.py +2 -2
- {drydock_cli-3.0.84 → drydock_cli-3.0.86/drydock_cli.egg-info}/PKG-INFO +8 -4
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/pyproject.toml +1 -1
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_tui.py +61 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/LICENSE +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/NOTICE +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/advisor.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/agent.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/cci.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/cli.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/config.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/detect.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/extract.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/guards.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/poam.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/providers.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/skills.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/stig.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/tools/__init__.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock/web.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock_cli.egg-info/SOURCES.txt +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/setup.cfg +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_advisor.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_cci.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_config.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_poam.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_viewimage.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.84 → drydock_cli-3.0.86}/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.
|
|
3
|
+
Version: 3.0.86
|
|
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
|
|
@@ -156,10 +156,14 @@ stance).
|
|
|
156
156
|
/advisor # show current config (key masked)
|
|
157
157
|
```
|
|
158
158
|
|
|
159
|
-
**Use it**
|
|
160
|
-
- **You:** `/ask <question>` — consults the advisor and shows its
|
|
159
|
+
**Use it** three ways:
|
|
160
|
+
- **You (private):** `/ask <question>` — consults the advisor and shows its
|
|
161
|
+
answer to *you only* (not added to the agent's context).
|
|
162
|
+
- **You (feed the agent):** `/ask! <question>` — same, but also **injects** the
|
|
163
|
+
answer into the agent's context and has the primary model process it, so a
|
|
164
|
+
second opinion can steer the current task.
|
|
161
165
|
- **The agent:** it can call the read-only `Consult` tool on its own when it hits
|
|
162
|
-
something hard
|
|
166
|
+
something hard (the answer comes back as a tool result, so it's in context).
|
|
163
167
|
|
|
164
168
|
**Pointing it at Gemini** — two options:
|
|
165
169
|
1. **Gemini's official OpenAI-compatible endpoint** (if this box has internet):
|
|
@@ -132,10 +132,14 @@ stance).
|
|
|
132
132
|
/advisor # show current config (key masked)
|
|
133
133
|
```
|
|
134
134
|
|
|
135
|
-
**Use it**
|
|
136
|
-
- **You:** `/ask <question>` — consults the advisor and shows its
|
|
135
|
+
**Use it** three ways:
|
|
136
|
+
- **You (private):** `/ask <question>` — consults the advisor and shows its
|
|
137
|
+
answer to *you only* (not added to the agent's context).
|
|
138
|
+
- **You (feed the agent):** `/ask! <question>` — same, but also **injects** the
|
|
139
|
+
answer into the agent's context and has the primary model process it, so a
|
|
140
|
+
second opinion can steer the current task.
|
|
137
141
|
- **The agent:** it can call the read-only `Consult` tool on its own when it hits
|
|
138
|
-
something hard
|
|
142
|
+
something hard (the answer comes back as a tool result, so it's in context).
|
|
139
143
|
|
|
140
144
|
**Pointing it at Gemini** — two options:
|
|
141
145
|
1. **Gemini's official OpenAI-compatible endpoint** (if this box has internet):
|
|
@@ -59,6 +59,12 @@ BANNER = (
|
|
|
59
59
|
# Animated "working" line: a turning-helm arc spinner + a nautical gerund +
|
|
60
60
|
# elapsed time + streamed-token count. Rendered in-line above the status bar.
|
|
61
61
|
_SPINNER = "◜◠◝◞◡◟"
|
|
62
|
+
# No agent event for this long → the activity line warns the model may be stalled
|
|
63
|
+
# (the gemma llama.cpp server can hang mid-generation on hard prompts). Advisory
|
|
64
|
+
# only. Set above the longest LEGIT silent stretch seen (a ~135s high-effort
|
|
65
|
+
# think before the first tool call) so normal slow turns don't nag; observed
|
|
66
|
+
# stalls ran 3–5 min+, so 180s still flags them well before the 30-min timeout.
|
|
67
|
+
_STALL_HINT_SECS = 180
|
|
62
68
|
_WORKING_WORDS = [
|
|
63
69
|
"Battening", "Splicing", "Hoisting", "Heaving", "Trimming", "Tacking",
|
|
64
70
|
"Mooring", "Charting", "Navigating", "Sounding", "Caulking", "Rigging",
|
|
@@ -262,6 +268,7 @@ class DrydockApp(App):
|
|
|
262
268
|
self._work_start = 0.0
|
|
263
269
|
self._work_word = ""
|
|
264
270
|
self._work_chars = 0 # streamed output chars this turn (→ ~tokens)
|
|
271
|
+
self._last_progress = 0.0 # monotonic time of the last agent event (stall watchdog)
|
|
265
272
|
self._spinner_i = 0
|
|
266
273
|
|
|
267
274
|
# The agent (worker thread) calls this to gate a sensitive command on
|
|
@@ -349,7 +356,13 @@ class DrydockApp(App):
|
|
|
349
356
|
effort = self.state.current_effort
|
|
350
357
|
eff = f" · thinking with {effort} effort" if effort else ""
|
|
351
358
|
queued = f" · {len(self._queue)} queued" if self._queue else ""
|
|
352
|
-
|
|
359
|
+
# Stall watchdog (advisory): if no agent event has arrived for a while,
|
|
360
|
+
# the model server may be hung. Flag it so the user isn't left guessing
|
|
361
|
+
# whether it's thinking or stalled — but never act (Esc is theirs).
|
|
362
|
+
silent = time.monotonic() - self._last_progress if self._last_progress else 0.0
|
|
363
|
+
stall = (f" ⚠ no output for {int(silent)}s — the model may be slow or stalled; "
|
|
364
|
+
"Esc to stop") if silent > _STALL_HINT_SECS else ""
|
|
365
|
+
return f"{spin} {self._work_word}… ({elapsed} · ↓ {toks} tokens{eff}{queued}){stall}"
|
|
353
366
|
|
|
354
367
|
def _refresh_status(self) -> None:
|
|
355
368
|
# The 0.18s _tick_work timer can fire one last time DURING app teardown,
|
|
@@ -435,6 +448,7 @@ class DrydockApp(App):
|
|
|
435
448
|
self.query_one("#todo", Static).update("")
|
|
436
449
|
self._busy = True
|
|
437
450
|
self._work_start = time.monotonic()
|
|
451
|
+
self._last_progress = self._work_start
|
|
438
452
|
self._work_word = random.choice(_WORKING_WORDS)
|
|
439
453
|
self._work_chars = 0
|
|
440
454
|
self._spinner_i = 0
|
|
@@ -491,6 +505,8 @@ class DrydockApp(App):
|
|
|
491
505
|
self._cmd_advisor(arg)
|
|
492
506
|
elif cmd == "/ask":
|
|
493
507
|
self._cmd_ask(arg)
|
|
508
|
+
elif cmd == "/ask!":
|
|
509
|
+
self._cmd_ask(arg, inject=True)
|
|
494
510
|
elif cmd == "/graphrag":
|
|
495
511
|
self._cmd_graphrag(arg)
|
|
496
512
|
elif cmd == "/status":
|
|
@@ -513,7 +529,7 @@ class DrydockApp(App):
|
|
|
513
529
|
" /status session model, cwd, turns, tokens\n"
|
|
514
530
|
" /compact shrink old context to free up the window\n"
|
|
515
531
|
" /context view/set the context-window budget (e.g. /context 65536)\n"
|
|
516
|
-
" /advisor set up a 2nd 'advisor' model (
|
|
532
|
+
" /advisor set up a 2nd 'advisor' model (Gemini etc.); /ask <q> = you, /ask! = feed to agent\n"
|
|
517
533
|
" /graphrag ingest docs into a knowledge base the agent can use\n"
|
|
518
534
|
" build <path> · add <path> · query <q> · status · clear\n"
|
|
519
535
|
" /skills list skills · /skills new <name> <prompt> to create one\n"
|
|
@@ -891,23 +907,43 @@ class DrydockApp(App):
|
|
|
891
907
|
shown = "•••••" if keymap[sub] == "advisor_api_key" else val
|
|
892
908
|
self._info(f"✓ advisor {sub} set to {shown} (saved to config.toml).")
|
|
893
909
|
|
|
894
|
-
def _cmd_ask(self, arg: str) -> None:
|
|
895
|
-
"""
|
|
910
|
+
def _cmd_ask(self, arg: str, *, inject: bool = False) -> None:
|
|
911
|
+
"""Consult the advisor model. `/ask` shows the answer to YOU only;
|
|
912
|
+
`/ask!` also INJECTS it into the agent's context and has the primary model
|
|
913
|
+
process it (so a second opinion can steer the current task)."""
|
|
896
914
|
from drydock import advisor
|
|
897
915
|
q = (arg or "").strip()
|
|
898
916
|
if not q:
|
|
899
|
-
self._info("usage: /ask <question>
|
|
917
|
+
self._info("usage: /ask <question> (show to you) · /ask! <question> "
|
|
918
|
+
"(also feed the answer to the agent)")
|
|
900
919
|
return
|
|
901
920
|
if not advisor.is_configured(self.config):
|
|
902
921
|
self._info(advisor.not_configured_message())
|
|
903
922
|
return
|
|
904
|
-
self._info(f"Asking the advisor ({self.config.get('advisor_model')})
|
|
905
|
-
|
|
923
|
+
self._info(f"Asking the advisor ({self.config.get('advisor_model')})"
|
|
924
|
+
+ (" — its answer will be added to the agent's context…" if inject else "…"))
|
|
925
|
+
self.run_worker(lambda: self._ask_worker(q, inject), thread=True)
|
|
906
926
|
|
|
907
|
-
def _ask_worker(self, question: str) -> None:
|
|
927
|
+
def _ask_worker(self, question: str, inject: bool = False) -> None:
|
|
908
928
|
from drydock import advisor
|
|
909
929
|
answer = advisor.consult(question, self.config)
|
|
910
|
-
self.call_from_thread(self.
|
|
930
|
+
self.call_from_thread(self._deliver_advice, question, answer, inject)
|
|
931
|
+
|
|
932
|
+
def _deliver_advice(self, question: str, answer: str, inject: bool) -> None:
|
|
933
|
+
"""Show the advisor's answer; if inject, feed it into the agent's context
|
|
934
|
+
so the primary model processes it (queued if a turn is already running)."""
|
|
935
|
+
failed = answer.startswith(("No advisor", "Could not reach", "Error", "✗"))
|
|
936
|
+
if not inject or failed:
|
|
937
|
+
self._info(f"💡 advisor:\n{answer}")
|
|
938
|
+
return
|
|
939
|
+
self._info(f"💡 advisor (added to the agent's context):\n{answer}")
|
|
940
|
+
turn = (f"[Second opinion from the advisor model — I asked it: {question}]\n\n"
|
|
941
|
+
f"{answer}\n\n[Consider this advice for the current task.]")
|
|
942
|
+
if self._busy:
|
|
943
|
+
self._queue.append(turn)
|
|
944
|
+
self._refresh_status()
|
|
945
|
+
else:
|
|
946
|
+
self._begin(turn)
|
|
911
947
|
|
|
912
948
|
def _cmd_compact(self) -> None:
|
|
913
949
|
"""Manually compact the conversation to reclaim context NOW, without
|
|
@@ -1111,6 +1147,11 @@ class DrydockApp(App):
|
|
|
1111
1147
|
def _run_agent(self, text: str) -> None:
|
|
1112
1148
|
try:
|
|
1113
1149
|
for ev in run(text, self.state, self.config, self.system):
|
|
1150
|
+
# Any event = the model/server is making progress → reset the
|
|
1151
|
+
# stall watchdog. During a real stall the provider stream blocks
|
|
1152
|
+
# and NO event arrives, so this timestamp freezes and
|
|
1153
|
+
# _working_text surfaces a hint.
|
|
1154
|
+
self._last_progress = time.monotonic()
|
|
1114
1155
|
if isinstance(ev, ReasoningChunk):
|
|
1115
1156
|
self.post_message(AgentReasoning(ev.text))
|
|
1116
1157
|
elif isinstance(ev, TextChunk):
|
|
@@ -153,8 +153,8 @@ class PromptHistory:
|
|
|
153
153
|
# completes the prefix.
|
|
154
154
|
SLASH_COMMANDS = [
|
|
155
155
|
"/help", "/model", "/cwd", "/undo", "/back", "/stop", "/status",
|
|
156
|
-
"/compact", "/context", "/advisor", "/ask", "/
|
|
157
|
-
"/mcp", "/rmf", "/stig",
|
|
156
|
+
"/compact", "/context", "/advisor", "/ask", "/ask!", "/graphrag", "/skills",
|
|
157
|
+
"/loop", "/mcp", "/rmf", "/stig",
|
|
158
158
|
"/clear", "/quit",
|
|
159
159
|
]
|
|
160
160
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: drydock-cli
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.86
|
|
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
|
|
@@ -156,10 +156,14 @@ stance).
|
|
|
156
156
|
/advisor # show current config (key masked)
|
|
157
157
|
```
|
|
158
158
|
|
|
159
|
-
**Use it**
|
|
160
|
-
- **You:** `/ask <question>` — consults the advisor and shows its
|
|
159
|
+
**Use it** three ways:
|
|
160
|
+
- **You (private):** `/ask <question>` — consults the advisor and shows its
|
|
161
|
+
answer to *you only* (not added to the agent's context).
|
|
162
|
+
- **You (feed the agent):** `/ask! <question>` — same, but also **injects** the
|
|
163
|
+
answer into the agent's context and has the primary model process it, so a
|
|
164
|
+
second opinion can steer the current task.
|
|
161
165
|
- **The agent:** it can call the read-only `Consult` tool on its own when it hits
|
|
162
|
-
something hard
|
|
166
|
+
something hard (the answer comes back as a tool result, so it's in context).
|
|
163
167
|
|
|
164
168
|
**Pointing it at Gemini** — two options:
|
|
165
169
|
1. **Gemini's official OpenAI-compatible endpoint** (if this box has internet):
|
|
@@ -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.86"
|
|
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"
|
|
@@ -422,3 +422,64 @@ def test_context_command_sets_and_persists(tmp_path, monkeypatch):
|
|
|
422
422
|
import tomllib
|
|
423
423
|
saved = tomllib.loads((tmp_path / ".drydock" / "config.toml").read_text())
|
|
424
424
|
assert saved["context_limit"] == 65536 # survived to disk
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def test_ask_bang_injects_advice_but_plain_ask_does_not():
|
|
428
|
+
"""/ask! feeds the advisor's answer into the agent's context (starts a turn
|
|
429
|
+
with it); plain /ask (inject=False) only displays it."""
|
|
430
|
+
async def main():
|
|
431
|
+
app = DrydockApp({"model": "gemma4", "provider": "vllm", "cwd": "/tmp"})
|
|
432
|
+
async with app.run_test() as pilot:
|
|
433
|
+
await pilot.pause()
|
|
434
|
+
started: list[str] = []
|
|
435
|
+
app._run_agent = lambda text: started.append(text) # type: ignore[method-assign]
|
|
436
|
+
|
|
437
|
+
# inject=True → a turn begins carrying the advice
|
|
438
|
+
app._deliver_advice("what DS for an LRU cache?",
|
|
439
|
+
"Use a hash map + a doubly linked list.", True)
|
|
440
|
+
for _ in range(40):
|
|
441
|
+
await pilot.pause()
|
|
442
|
+
if started:
|
|
443
|
+
break
|
|
444
|
+
assert started, "/ask! should start a turn"
|
|
445
|
+
assert "hash map + a doubly linked list" in started[0]
|
|
446
|
+
assert "advisor model" in started[0] # framed as a second opinion
|
|
447
|
+
|
|
448
|
+
# inject=False → display only, no new turn
|
|
449
|
+
started.clear()
|
|
450
|
+
app._deliver_advice("q", "some answer", False)
|
|
451
|
+
await pilot.pause()
|
|
452
|
+
assert started == []
|
|
453
|
+
|
|
454
|
+
# a FAILED consult must not inject even with inject=True
|
|
455
|
+
app._deliver_advice("q", "Could not reach the advisor model at http://x: boom", True)
|
|
456
|
+
await pilot.pause()
|
|
457
|
+
assert started == []
|
|
458
|
+
|
|
459
|
+
asyncio.run(main())
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def test_stall_watchdog_hint_appears_after_silence():
|
|
463
|
+
"""The activity line warns when no agent event has arrived for a while
|
|
464
|
+
(a hung model server) — advisory, and it clears once progress resumes."""
|
|
465
|
+
import time as _time
|
|
466
|
+
from drydock.tui.app import _STALL_HINT_SECS
|
|
467
|
+
|
|
468
|
+
async def main():
|
|
469
|
+
app = DrydockApp({"model": "gemma4", "provider": "vllm", "cwd": "/tmp"})
|
|
470
|
+
async with app.run_test() as pilot:
|
|
471
|
+
await pilot.pause()
|
|
472
|
+
app._busy = True
|
|
473
|
+
app._work_start = _time.monotonic()
|
|
474
|
+
app._work_word = "Working"
|
|
475
|
+
# fresh progress → no hint
|
|
476
|
+
app._last_progress = _time.monotonic()
|
|
477
|
+
assert "stalled" not in app._working_text()
|
|
478
|
+
# simulate silence beyond the threshold → hint appears
|
|
479
|
+
app._last_progress = _time.monotonic() - (_STALL_HINT_SECS + 5)
|
|
480
|
+
assert "stalled" in app._working_text() and "Esc to stop" in app._working_text()
|
|
481
|
+
# progress resumes → hint clears
|
|
482
|
+
app._last_progress = _time.monotonic()
|
|
483
|
+
assert "stalled" not in app._working_text()
|
|
484
|
+
|
|
485
|
+
asyncio.run(main())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|