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.
Files changed (49) hide show
  1. {drydock_cli-3.0.16/drydock_cli.egg-info → drydock_cli-3.0.18}/PKG-INFO +1 -1
  2. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/__init__.py +1 -1
  3. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/cli.py +6 -25
  4. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/tools/__init__.py +10 -4
  5. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/tui/app.py +3 -3
  6. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/tui/widgets.py +11 -1
  7. {drydock_cli-3.0.16 → drydock_cli-3.0.18/drydock_cli.egg-info}/PKG-INFO +1 -1
  8. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock_cli.egg-info/SOURCES.txt +2 -0
  9. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/pyproject.toml +1 -1
  10. drydock_cli-3.0.18/tests/test_cli_agents.py +34 -0
  11. drydock_cli-3.0.18/tests/test_todo.py +38 -0
  12. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/tests/test_tui.py +7 -0
  13. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/LICENSE +0 -0
  14. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/NOTICE +0 -0
  15. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/README.md +0 -0
  16. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/__main__.py +0 -0
  17. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/agent.py +0 -0
  18. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/bash_safety.py +0 -0
  19. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/compaction.py +0 -0
  20. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/config.py +0 -0
  21. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/detect.py +0 -0
  22. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/guards.py +0 -0
  23. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/loop_detect.py +0 -0
  24. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/providers.py +0 -0
  25. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/tool_registry.py +0 -0
  26. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/tui/__init__.py +0 -0
  27. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/tui/approval.py +0 -0
  28. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/tui/messages.py +0 -0
  29. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock/tuning.py +0 -0
  30. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock_cli.egg-info/dependency_links.txt +0 -0
  31. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock_cli.egg-info/entry_points.txt +0 -0
  32. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock_cli.egg-info/requires.txt +0 -0
  33. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/drydock_cli.egg-info/top_level.txt +0 -0
  34. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/setup.cfg +0 -0
  35. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/tests/test_approval.py +0 -0
  36. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/tests/test_back_command.py +0 -0
  37. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/tests/test_bash_safety.py +0 -0
  38. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/tests/test_compaction.py +0 -0
  39. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/tests/test_config.py +0 -0
  40. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/tests/test_detect.py +0 -0
  41. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/tests/test_guards_and_tools.py +0 -0
  42. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/tests/test_hallucinated_tools.py +0 -0
  43. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/tests/test_leaked_tool_call.py +0 -0
  44. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/tests/test_loop_detect.py +0 -0
  45. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/tests/test_plan_autocontinue.py +0 -0
  46. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/tests/test_providers_unreachable.py +0 -0
  47. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/tests/test_subagent.py +0 -0
  48. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/tests/test_tools_undo.py +0 -0
  49. {drydock_cli-3.0.16 → drydock_cli-3.0.18}/tests/test_tuning.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.16
3
+ Version: 3.0.18
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
@@ -7,4 +7,4 @@ try:
7
7
  __version__ = _version("drydock-cli")
8
8
  except PackageNotFoundError:
9
9
  # Running from source before install (e.g. PYTHONPATH dev runs).
10
- __version__ = "3.0.16"
10
+ __version__ = "3.0.18"
@@ -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
- ensure_agents_md()
249
- # Loaded after ensure_agents_md so the auto-created AGENTS.md is included.
250
- # The TUI reads this from config; CLI modes call load_project_instructions
251
- # directly (see run_interactive/run_oneshot).
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: str) -> list[tuple[str, str]]:
500
- """Parse a plain-text checklist into (text, status) pairs. Each non-empty
501
- line is one task, optionally prefixed with [x] done / [~] in_progress /
502
- [ ] pending. Deliberately forgiving (a bare line counts as pending)."""
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 must honor AGENTS.md/DRYDOCK.md like the CLI does — it's the
214
- # primary surface, and project instructions carry the user's tool/style
215
- # conventions. cli.main() loads them into config after ensure_agents_md.
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
- return not cleaned.lstrip().startswith(_FAILURE_PREFIXES)
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.16
3
+ Version: 3.0.18
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
@@ -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.16"
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