drydock-cli 3.0.16__tar.gz → 3.0.18__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.16/drydock_cli.egg-info → drydock_cli-3.0.18}/PKG-INFO +1 -1
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/__init__.py +1 -1
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/cli.py +6 -25
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/tools/__init__.py +10 -4
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/tui/app.py +3 -3
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/tui/widgets.py +11 -1
- {drydock_cli-3.0.16 → drydock_cli-3.0.18/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock_cli.egg-info/SOURCES.txt +2 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/pyproject.toml +1 -1
- drydock_cli-3.0.18/tests/test_cli_agents.py +34 -0
- drydock_cli-3.0.18/tests/test_todo.py +38 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/tests/test_tui.py +7 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/LICENSE +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/NOTICE +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/README.md +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/agent.py +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/config.py +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/detect.py +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/guards.py +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/providers.py +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/setup.cfg +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/tests/test_config.py +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.16 → drydock_cli-3.0.18}/tests/test_tuning.py +0 -0
|
@@ -59,27 +59,6 @@ def load_project_instructions() -> str:
|
|
|
59
59
|
return ""
|
|
60
60
|
|
|
61
61
|
|
|
62
|
-
def ensure_agents_md() -> None:
|
|
63
|
-
"""Auto-create AGENTS.md if no project instructions exist."""
|
|
64
|
-
for name in ("AGENTS.md", "DRYDOCK.md", ".drydock.md", "CLAUDE.md"):
|
|
65
|
-
if (Path.cwd() / name).exists():
|
|
66
|
-
return
|
|
67
|
-
(Path.cwd() / "AGENTS.md").write_text(
|
|
68
|
-
"# Project Instructions\n\n"
|
|
69
|
-
"DO NOT ask for confirmation. ACT IMMEDIATELY.\n"
|
|
70
|
-
"If there is a PRD.md, implement it. If there is code, work on it.\n\n"
|
|
71
|
-
"## Workflow\n"
|
|
72
|
-
"1. Read requirements or explore existing code\n"
|
|
73
|
-
"2. Create/edit files with Write or Edit\n"
|
|
74
|
-
"3. Test with Bash\n"
|
|
75
|
-
"4. Fix errors and verify\n\n"
|
|
76
|
-
"## Rules\n"
|
|
77
|
-
"- Use absolute imports for Python packages\n"
|
|
78
|
-
"- Always create __init__.py and __main__.py\n"
|
|
79
|
-
"- Test with python3 -m package_name\n"
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
|
|
83
62
|
# ── REPL ──────────────────────────────────────────────────────────────────
|
|
84
63
|
|
|
85
64
|
def print_colored(text: str, color: str = "") -> None:
|
|
@@ -245,10 +224,12 @@ def main():
|
|
|
245
224
|
"onboarding": onboarding,
|
|
246
225
|
}
|
|
247
226
|
|
|
248
|
-
|
|
249
|
-
#
|
|
250
|
-
#
|
|
251
|
-
#
|
|
227
|
+
# Load a user's OWN AGENTS.md/DRYDOCK.md as background context if present.
|
|
228
|
+
# Drydock never CREATES one: auto-writing an AGENTS.md littered every
|
|
229
|
+
# directory the user ran drydock in, and its "ACT IMMEDIATELY, implement
|
|
230
|
+
# the PRD" content fought the system prompt and risked turning a plain
|
|
231
|
+
# greeting into a runaway build. The TUI reads this from config; CLI modes
|
|
232
|
+
# call load_project_instructions directly (see run_interactive/run_oneshot).
|
|
252
233
|
config["project_instructions"] = load_project_instructions()
|
|
253
234
|
|
|
254
235
|
if args.prompt:
|
|
@@ -496,10 +496,16 @@ _TODO_DONE = {"x", "X", "✓"}
|
|
|
496
496
|
_TODO_DOING = {"~", "-", "/", "*"}
|
|
497
497
|
|
|
498
498
|
|
|
499
|
-
def parse_todo(tasks
|
|
500
|
-
"""Parse a
|
|
501
|
-
|
|
502
|
-
|
|
499
|
+
def parse_todo(tasks) -> list[tuple[str, str]]:
|
|
500
|
+
"""Parse a checklist into (text, status) pairs. Each non-empty line is one
|
|
501
|
+
task, optionally prefixed with [x] done / [~] in_progress / [ ] pending.
|
|
502
|
+
Deliberately forgiving: a bare line counts as pending, and `tasks` may be a
|
|
503
|
+
newline-joined string OR a list of lines (the model sometimes sends a JSON
|
|
504
|
+
array instead of one string — handle both rather than crash the TUI)."""
|
|
505
|
+
if isinstance(tasks, (list, tuple)):
|
|
506
|
+
tasks = "\n".join(str(t) for t in tasks)
|
|
507
|
+
elif tasks is not None and not isinstance(tasks, str):
|
|
508
|
+
tasks = str(tasks)
|
|
503
509
|
out: list[tuple[str, str]] = []
|
|
504
510
|
for raw in (tasks or "").splitlines():
|
|
505
511
|
line = raw.strip().lstrip("-*•").strip()
|
|
@@ -210,9 +210,9 @@ class DrydockApp(App):
|
|
|
210
210
|
return "deny"
|
|
211
211
|
|
|
212
212
|
def _build_system(self, model: str | None) -> str:
|
|
213
|
-
# The TUI
|
|
214
|
-
#
|
|
215
|
-
#
|
|
213
|
+
# The TUI honors a user's OWN AGENTS.md/DRYDOCK.md like the CLI does —
|
|
214
|
+
# cli.main() loads it into config["project_instructions"] as background
|
|
215
|
+
# context (drydock never auto-creates one).
|
|
216
216
|
return system_prompt_for_model(model) + self.config.get("project_instructions", "")
|
|
217
217
|
|
|
218
218
|
# ── layout ────────────────────────────────────────────────────────────
|
|
@@ -16,12 +16,22 @@ _FAILURE_PREFIXES = ("Error", "REFUSED")
|
|
|
16
16
|
# "[NOTE: …]" lines (see loop_detect.annotate). Strip them before judging
|
|
17
17
|
# success, or a failed Edit behind a "[NOTE: write #9…]" reads as ✓.
|
|
18
18
|
_LEADING_NOTES = re.compile(r"^\s*(?:\[NOTE:[^\]]*\]\s*)+")
|
|
19
|
+
# Bash appends "[exit code: N]" only when a command FAILED (non-zero return).
|
|
20
|
+
# Without this, a failed shell command (e.g. `command not found`, a compile
|
|
21
|
+
# error, a failing test) renders as a green ✓ — which hides exactly the kind
|
|
22
|
+
# of failure the operator relies on the TUI to surface.
|
|
23
|
+
_BASH_EXIT = re.compile(r"\[exit code: (\d+)\]")
|
|
19
24
|
|
|
20
25
|
|
|
21
26
|
def result_is_ok(result: str) -> bool:
|
|
22
27
|
"""Whether a tool result should render as success (✓) vs failure (✗)."""
|
|
23
28
|
cleaned = _LEADING_NOTES.sub("", result or "")
|
|
24
|
-
|
|
29
|
+
if cleaned.lstrip().startswith(_FAILURE_PREFIXES):
|
|
30
|
+
return False
|
|
31
|
+
m = _BASH_EXIT.search(cleaned)
|
|
32
|
+
if m and m.group(1) != "0":
|
|
33
|
+
return False
|
|
34
|
+
return True
|
|
25
35
|
|
|
26
36
|
|
|
27
37
|
_TOOL_BODY_MAX = 8000
|
|
@@ -30,6 +30,7 @@ drydock_cli.egg-info/top_level.txt
|
|
|
30
30
|
tests/test_approval.py
|
|
31
31
|
tests/test_back_command.py
|
|
32
32
|
tests/test_bash_safety.py
|
|
33
|
+
tests/test_cli_agents.py
|
|
33
34
|
tests/test_compaction.py
|
|
34
35
|
tests/test_config.py
|
|
35
36
|
tests/test_detect.py
|
|
@@ -40,6 +41,7 @@ tests/test_loop_detect.py
|
|
|
40
41
|
tests/test_plan_autocontinue.py
|
|
41
42
|
tests/test_providers_unreachable.py
|
|
42
43
|
tests/test_subagent.py
|
|
44
|
+
tests/test_todo.py
|
|
43
45
|
tests/test_tools_undo.py
|
|
44
46
|
tests/test_tui.py
|
|
45
47
|
tests/test_tuning.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.18"
|
|
11
11
|
description = "Drydock — a local, provider-agnostic terminal coding agent for local LLMs"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.12"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Drydock must LOAD a user's own AGENTS.md/DRYDOCK.md as background context,
|
|
2
|
+
but must NEVER auto-create one (it littered every working directory and its
|
|
3
|
+
'ACT IMMEDIATELY, implement the PRD' content fought the system prompt)."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
from drydock import cli
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _in_dir(tmp_path, fn):
|
|
12
|
+
cwd = os.getcwd()
|
|
13
|
+
os.chdir(tmp_path)
|
|
14
|
+
try:
|
|
15
|
+
return fn()
|
|
16
|
+
finally:
|
|
17
|
+
os.chdir(cwd)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_no_agents_md_is_created(tmp_path):
|
|
21
|
+
# An empty project must stay empty after loading instructions.
|
|
22
|
+
text = _in_dir(tmp_path, cli.load_project_instructions)
|
|
23
|
+
assert text == ""
|
|
24
|
+
assert list(tmp_path.iterdir()) == [] # nothing written
|
|
25
|
+
assert not hasattr(cli, "ensure_agents_md") # the auto-creator is gone
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_existing_agents_md_loaded_as_background(tmp_path):
|
|
29
|
+
(tmp_path / "AGENTS.md").write_text("Use 4-space indent. Prefer pathlib.")
|
|
30
|
+
text = _in_dir(tmp_path, cli.load_project_instructions)
|
|
31
|
+
assert "Use 4-space indent" in text
|
|
32
|
+
# Framed as background context, NOT a command to act now.
|
|
33
|
+
assert "background context" in text.lower()
|
|
34
|
+
assert "not a" in text.lower() and "act now" in text.lower()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Tests for the `todo` checklist tool and its forgiving parser."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from drydock.tools import parse_todo, tool_todo
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_parse_todo_basic_statuses():
|
|
8
|
+
items = parse_todo("[x] done one\n[~] doing two\n[ ] pending three")
|
|
9
|
+
assert items == [
|
|
10
|
+
("done one", "done"),
|
|
11
|
+
("doing two", "in_progress"),
|
|
12
|
+
("pending three", "pending"),
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_parse_todo_accepts_list_input():
|
|
17
|
+
# The model sometimes sends a JSON array instead of one newline string —
|
|
18
|
+
# this must NOT crash (it took down the whole TUI before).
|
|
19
|
+
items = parse_todo([
|
|
20
|
+
"[~] Explore existing tests and run them",
|
|
21
|
+
"[ ] Add edge-case tests",
|
|
22
|
+
"[ ] Verify all tests pass",
|
|
23
|
+
])
|
|
24
|
+
assert [s for _, s in items] == ["in_progress", "pending", "pending"]
|
|
25
|
+
assert items[0][0] == "Explore existing tests and run them"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_parse_todo_none_and_empty():
|
|
29
|
+
assert parse_todo(None) == []
|
|
30
|
+
assert parse_todo("") == []
|
|
31
|
+
assert parse_todo([]) == []
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_tool_todo_accepts_list(tmp_path):
|
|
35
|
+
cfg: dict = {}
|
|
36
|
+
out = tool_todo({"tasks": ["[ ] a", "[x] b"]}, cfg)
|
|
37
|
+
assert "2 tasks" in out and "1 done" in out
|
|
38
|
+
assert len(cfg["_todo"]) == 2
|
|
@@ -123,6 +123,13 @@ def test_result_is_ok_marks_failures():
|
|
|
123
123
|
assert not result_is_ok("Error: nope")
|
|
124
124
|
assert not result_is_ok("REFUSED: this command reformats a filesystem...")
|
|
125
125
|
assert not result_is_ok(" Error: leading whitespace still counts")
|
|
126
|
+
# A failed shell command (Bash appends "[exit code: N]" only on failure)
|
|
127
|
+
# must render ✗, not a green ✓ — otherwise the TUI hides real failures.
|
|
128
|
+
assert not result_is_ok("bash: sqlite3: command not found\n[exit code: 127]")
|
|
129
|
+
assert not result_is_ok("test.c:1: error\n[exit code: 1]")
|
|
130
|
+
# Successful command output (no exit-code marker) stays ✓, even if it
|
|
131
|
+
# happens to mention the word 'error' in normal output.
|
|
132
|
+
assert result_is_ok("0 errors, 0 warnings\nBuild succeeded")
|
|
126
133
|
|
|
127
134
|
|
|
128
135
|
def test_summarize_inputs_prefers_meaningful_keys():
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|