drydock-cli 3.0.124__tar.gz → 3.0.126__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 (140) hide show
  1. {drydock_cli-3.0.124/drydock_cli.egg-info → drydock_cli-3.0.126}/PKG-INFO +1 -1
  2. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/agent.py +16 -0
  3. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/config.py +3 -0
  4. drydock_cli-3.0.126/drydock/events.py +68 -0
  5. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/tui/app.py +3 -0
  6. {drydock_cli-3.0.124 → drydock_cli-3.0.126/drydock_cli.egg-info}/PKG-INFO +1 -1
  7. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock_cli.egg-info/SOURCES.txt +2 -0
  8. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/pyproject.toml +1 -1
  9. drydock_cli-3.0.126/tests/test_events.py +58 -0
  10. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_failure_loop.py +3 -3
  11. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/LICENSE +0 -0
  12. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/NOTICE +0 -0
  13. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/README.md +0 -0
  14. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/__init__.py +0 -0
  15. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/__main__.py +0 -0
  16. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/advisor.py +0 -0
  17. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/bash_safety.py +0 -0
  18. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/builtin_skills/__init__.py +0 -0
  19. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/builtin_skills/ml-data.md +0 -0
  20. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/builtin_skills/ml-debug.md +0 -0
  21. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/builtin_skills/ml-finetune.md +0 -0
  22. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/builtin_skills/ml-metrics.md +0 -0
  23. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/builtin_skills/ml-rl.md +0 -0
  24. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/builtin_skills/ml-train.md +0 -0
  25. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/builtin_skills/rmf-categorize.md +0 -0
  26. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/builtin_skills/rmf-control.md +0 -0
  27. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/builtin_skills/rmf-poam.md +0 -0
  28. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/builtin_skills/rmf-review.md +0 -0
  29. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/builtin_skills/stig-assess.md +0 -0
  30. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/builtin_skills/stig-remediate.md +0 -0
  31. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/cci.py +0 -0
  32. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/cli.py +0 -0
  33. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/compaction.py +0 -0
  34. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/detect.py +0 -0
  35. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/extract.py +0 -0
  36. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/gittools.py +0 -0
  37. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/graphrag.py +0 -0
  38. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/guards.py +0 -0
  39. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/loop_detect.py +0 -0
  40. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/mcp.py +0 -0
  41. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/poam.py +0 -0
  42. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/providers.py +0 -0
  43. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/recipes.py +0 -0
  44. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/rmf.py +0 -0
  45. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/rmf_graph.py +0 -0
  46. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/skills.py +0 -0
  47. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/stig.py +0 -0
  48. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/suggest.py +0 -0
  49. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/task_state.py +0 -0
  50. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/tool_registry.py +0 -0
  51. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/tools/__init__.py +0 -0
  52. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/tui/__init__.py +0 -0
  53. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/tui/approval.py +0 -0
  54. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/tui/messages.py +0 -0
  55. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/tui/widgets.py +0 -0
  56. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/tuning.py +0 -0
  57. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/verification.py +0 -0
  58. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/web.py +0 -0
  59. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock_cli.egg-info/dependency_links.txt +0 -0
  60. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock_cli.egg-info/entry_points.txt +0 -0
  61. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock_cli.egg-info/requires.txt +0 -0
  62. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock_cli.egg-info/top_level.txt +0 -0
  63. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/setup.cfg +0 -0
  64. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_advisor.py +0 -0
  65. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_approval.py +0 -0
  66. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_back_command.py +0 -0
  67. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_bash_background.py +0 -0
  68. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_bash_binary_output.py +0 -0
  69. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_bash_crossplatform.py +0 -0
  70. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_bash_output_bounding.py +0 -0
  71. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_bash_process_group.py +0 -0
  72. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_bash_safety.py +0 -0
  73. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_bash_sanitize.py +0 -0
  74. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_bash_shell.py +0 -0
  75. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_bash_stdin.py +0 -0
  76. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_bash_stop_partial.py +0 -0
  77. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_bash_timeout_network.py +0 -0
  78. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_bash_timeout_param.py +0 -0
  79. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_cci.py +0 -0
  80. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_cli_agents.py +0 -0
  81. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_compact_command.py +0 -0
  82. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_compaction.py +0 -0
  83. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_config.py +0 -0
  84. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_config_migration.py +0 -0
  85. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_context_limit_config.py +0 -0
  86. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_detect.py +0 -0
  87. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_dispatch.py +0 -0
  88. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_e2e_connected.py +0 -0
  89. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_edit_replace_all.py +0 -0
  90. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_empty_response.py +0 -0
  91. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_extract.py +0 -0
  92. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_first_run_setup.py +0 -0
  93. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_gittools.py +0 -0
  94. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_glob_edit_edges.py +0 -0
  95. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_graphify_example.py +0 -0
  96. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_graphrag.py +0 -0
  97. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_graphrag_quoted_path.py +0 -0
  98. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_grep_and_read_robust.py +0 -0
  99. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_guards_and_tools.py +0 -0
  100. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_hallucinated_tools.py +0 -0
  101. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_leaked_tool_call.py +0 -0
  102. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_loop_detect.py +0 -0
  103. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_mcp.py +0 -0
  104. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_oneshot_unreachable.py +0 -0
  105. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_overthink_interrupt.py +0 -0
  106. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_plan_autocontinue.py +0 -0
  107. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_poam.py +0 -0
  108. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_providers_unreachable.py +0 -0
  109. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_read_index.py +0 -0
  110. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_recipes.py +0 -0
  111. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_repetition_interrupt.py +0 -0
  112. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_rmf.py +0 -0
  113. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_rmf_graph.py +0 -0
  114. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_rmf_stig_graph.py +0 -0
  115. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_runaway_repetition.py +0 -0
  116. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_screenshot.py +0 -0
  117. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_server_probe.py +0 -0
  118. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_skills.py +0 -0
  119. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_stall_retry.py +0 -0
  120. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_stig.py +0 -0
  121. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_stop.py +0 -0
  122. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_streaming_newlines.py +0 -0
  123. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_subagent.py +0 -0
  124. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_suggest.py +0 -0
  125. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_system_prompt_help.py +0 -0
  126. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_task_state.py +0 -0
  127. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_todo.py +0 -0
  128. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_tool_arg_coercion.py +0 -0
  129. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_tool_arg_coercion_more.py +0 -0
  130. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_tool_arg_parsing.py +0 -0
  131. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_tools_undo.py +0 -0
  132. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_tui.py +0 -0
  133. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_tuning.py +0 -0
  134. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_verification_gate.py +0 -0
  135. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_viewimage.py +0 -0
  136. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_vision_input.py +0 -0
  137. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_web_tools.py +0 -0
  138. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_windows_shell.py +0 -0
  139. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_write_content_coerce.py +0 -0
  140. {drydock_cli-3.0.124 → drydock_cli-3.0.126}/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.124
3
+ Version: 3.0.126
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
@@ -49,6 +49,7 @@ from drydock.compaction import (
49
49
  from drydock.loop_detect import LoopTracker
50
50
  from drydock.task_state import TaskState
51
51
  from drydock.verification import looks_like_verification
52
+ from drydock.events import emit as _emit
52
53
  from drydock.tuning import (
53
54
  filter_tool_schemas,
54
55
  hallucinated_tool_message,
@@ -83,6 +84,7 @@ class AgentState:
83
84
  turn_count: int = 0
84
85
  current_effort: str = "" # "high"/"low" of the in-flight LLM call (for the UI)
85
86
  task: "TaskState" = field(default_factory=lambda: TaskState()) # structured objective
87
+ events: object | None = None # optional EventLog for a durable execution trace
86
88
 
87
89
 
88
90
  def drop_last_turn(messages: list) -> bool:
@@ -121,6 +123,9 @@ def run(
121
123
  # outside the transcript. The original objective is authoritative for the task.
122
124
  if not state.task.is_set() and isinstance(user_message, str) and user_message.strip():
123
125
  state.task = TaskState.from_objective(user_message)
126
+ _emit(state, "task_start", objective=state.task.objective,
127
+ acceptance_criteria=state.task.acceptance_criteria)
128
+ _emit(state, "user_message", chars=len(user_message or ""))
124
129
  # Keep the objective + acceptance criteria in the SYSTEM PROMPT every turn, so
125
130
  # they survive compaction (which only touches the message transcript, never the
126
131
  # system prompt) — the model can't drift off the goal on a long task.
@@ -281,6 +286,9 @@ def run(
281
286
  state.total_output_tokens += assistant_turn.output_tokens
282
287
  state.last_input_tokens = assistant_turn.input_tokens
283
288
  yield TurnDone(assistant_turn.input_tokens, assistant_turn.output_tokens)
289
+ _emit(state, "turn", in_tok=assistant_turn.input_tokens,
290
+ out_tok=assistant_turn.output_tokens,
291
+ tool_calls=len(assistant_turn.tool_calls or []))
284
292
 
285
293
  # No tool calls = conversation complete — UNLESS the model emitted a
286
294
  # tool call as text (a Gemma quirk the API can't structure). In that
@@ -341,12 +349,14 @@ def run(
341
349
  # wedge; once it runs any check (ran_verification) the gate is satisfied.
342
350
  if (
343
351
  config.get("verify_gate", True)
352
+ and allow is None # main task only — not scoped sub-agents
344
353
  and session_has_edited
345
354
  and not ran_verification
346
355
  and verify_gate_nudges < 2
347
356
  ):
348
357
  verify_gate_nudges += 1
349
358
  state.task.phase = "verify"
359
+ _emit(state, "verify_gate", nudge=verify_gate_nudges)
350
360
  state.messages.append({
351
361
  "role": "user",
352
362
  "content": (
@@ -361,6 +371,8 @@ def run(
361
371
  continue
362
372
  if session_has_edited and ran_verification:
363
373
  state.task.phase = "complete"
374
+ _emit(state, "done", phase=state.task.phase, edited=session_has_edited,
375
+ verified=ran_verification)
364
376
  break
365
377
 
366
378
  # Execute each tool call
@@ -423,6 +435,10 @@ def run(
423
435
  # advisory note when the same call is made again.
424
436
  result = loop_tracker.annotate(tc["name"], tc["input"], result)
425
437
 
438
+ _emit(state, "tool", name=tc["name"],
439
+ input=str(tc.get("input"))[:200],
440
+ result_chars=len(str(result)),
441
+ error=str(result)[:80].lower().startswith("error"))
426
442
  yield ToolEnd(tc["name"], result)
427
443
 
428
444
  # Append tool result
@@ -52,6 +52,9 @@ DEFAULTS: dict[str, object] = {
52
52
  # files but never ran a test/check/its own code, nudge it to verify first
53
53
  # instead of accepting completion. Bounded. Set false to disable.
54
54
  "verify_gate": True,
55
+ # Durable per-session execution trace (events.py) written as JSONL to
56
+ # ~/.drydock/events/ — inspect/diagnose/replay a task. Set false to disable.
57
+ "event_log": True,
55
58
  "theme": "harbor",
56
59
  # Optional SECOND model ("advisor") for a stronger second opinion — e.g. a
57
60
  # Gemini OpenAI-compatible endpoint on another box. Empty = disabled. The
@@ -0,0 +1,68 @@
1
+ """Durable execution event log (Agent-Buildout PRD, Epic: events).
2
+
3
+ Every task run appends structured events to a per-session JSONL file so a task can
4
+ be inspected, diagnosed, replayed, and later resumed. Append-only, one event per
5
+ line: ``{"seq", "ts", "type", ...}``. DEFENSIVE — a logging failure must never crash
6
+ the agent, so every I/O path swallows its own errors. All logic original to Drydock.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import time
12
+ from pathlib import Path
13
+
14
+
15
+ class EventLog:
16
+ """Append-only JSONL sink for execution events. Cheap to call; never raises."""
17
+
18
+ def __init__(self, path):
19
+ self.path = Path(path)
20
+ self._seq = 0
21
+ try:
22
+ self.path.parent.mkdir(parents=True, exist_ok=True)
23
+ except OSError:
24
+ pass
25
+
26
+ def emit(self, type: str, **data) -> None:
27
+ self._seq += 1
28
+ rec = {"seq": self._seq, "ts": round(time.time(), 3), "type": type}
29
+ rec.update(data)
30
+ try:
31
+ with self.path.open("a", encoding="utf-8") as f:
32
+ f.write(json.dumps(rec, default=str, ensure_ascii=False) + "\n")
33
+ except (OSError, TypeError, ValueError):
34
+ pass # logging must never break a run
35
+
36
+ @staticmethod
37
+ def read(path) -> list[dict]:
38
+ """Parse an event-log JSONL into a list of event dicts (in order). Skips any
39
+ unparseable line; returns [] if the file is missing."""
40
+ out: list[dict] = []
41
+ try:
42
+ text = Path(path).read_text("utf-8", errors="ignore")
43
+ except OSError:
44
+ return out
45
+ for line in text.splitlines():
46
+ line = line.strip()
47
+ if not line:
48
+ continue
49
+ try:
50
+ out.append(json.loads(line))
51
+ except json.JSONDecodeError:
52
+ continue
53
+ return out
54
+
55
+
56
+ def emit(state, type: str, **data) -> None:
57
+ """Emit to the run's event log if it has one — a no-op otherwise. Lets the agent
58
+ loop log unconditionally without a log everywhere."""
59
+ log = getattr(state, "events", None)
60
+ if log is not None:
61
+ log.emit(type, **data)
62
+
63
+
64
+ def default_event_log_path():
65
+ """A fresh per-session JSONL path under ~/.drydock/events/."""
66
+ import time
67
+ from pathlib import Path
68
+ return Path.home() / ".drydock" / "events" / f"session-{int(time.time())}.jsonl"
@@ -259,6 +259,9 @@ class DrydockApp(App):
259
259
  super().__init__()
260
260
  self.config = config
261
261
  self.state = AgentState()
262
+ if config.get("event_log", True):
263
+ from drydock.events import EventLog, default_event_log_path
264
+ self.state.events = EventLog(config.get("event_log_path") or default_event_log_path())
262
265
  self.system = self._build_system(config.get("model"))
263
266
  from drydock.skills import load_skills
264
267
  self._skills = load_skills(config.get("cwd") or ".")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.124
3
+ Version: 3.0.126
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
@@ -12,6 +12,7 @@ drydock/cli.py
12
12
  drydock/compaction.py
13
13
  drydock/config.py
14
14
  drydock/detect.py
15
+ drydock/events.py
15
16
  drydock/extract.py
16
17
  drydock/gittools.py
17
18
  drydock/graphrag.py
@@ -83,6 +84,7 @@ tests/test_dispatch.py
83
84
  tests/test_e2e_connected.py
84
85
  tests/test_edit_replace_all.py
85
86
  tests/test_empty_response.py
87
+ tests/test_events.py
86
88
  tests/test_extract.py
87
89
  tests/test_failure_loop.py
88
90
  tests/test_first_run_setup.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.124"
10
+ version = "3.0.126"
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,58 @@
1
+ """Durable event log (Agent-Buildout PRD, Epic: events) — the run appends a JSONL
2
+ execution trace that can be read back for inspection/replay."""
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import tempfile
7
+
8
+ from drydock import agent
9
+ from drydock.events import EventLog
10
+ from drydock.providers import AssistantTurn
11
+
12
+
13
+ def test_emit_and_read_roundtrip():
14
+ p = os.path.join(tempfile.mkdtemp(), "e.jsonl")
15
+ log = EventLog(p)
16
+ log.emit("task_start", objective="do X", acceptance_criteria=["a", "b"])
17
+ log.emit("tool", name="Write", result_chars=10)
18
+ evs = EventLog.read(p)
19
+ assert [e["type"] for e in evs] == ["task_start", "tool"]
20
+ assert evs[0]["seq"] == 1 and evs[1]["seq"] == 2 and "ts" in evs[0]
21
+ assert evs[0]["acceptance_criteria"] == ["a", "b"]
22
+
23
+
24
+ def test_read_missing_file_is_empty():
25
+ assert EventLog.read("/no/such/file.jsonl") == []
26
+
27
+
28
+ def test_emit_never_raises_on_bad_path():
29
+ EventLog("/root/cannot/write/here.jsonl").emit("x", a=1) # no exception
30
+
31
+
32
+ def test_run_writes_full_trace():
33
+ p = os.path.join(tempfile.mkdtemp(), "run.jsonl")
34
+ st = agent.AgentState(); st.events = EventLog(p)
35
+ turns = [
36
+ AssistantTurn("", [{"id": "1", "name": "Write",
37
+ "input": {"file_path": "/tmp/dd_e.txt", "content": "x"}}], 10, 5),
38
+ AssistantTurn("", [{"id": "2", "name": "Bash", "input": {"command": "python -c '1'"}}], 8, 4),
39
+ AssistantTurn("done, verified", [], 6, 3),
40
+ ]
41
+ i = {"n": 0}
42
+ def fake(model, system, messages, tool_schemas, config):
43
+ t = turns[min(i["n"], len(turns) - 1)]; i["n"] += 1; yield t
44
+ orig = agent.stream; agent.stream = fake
45
+ try:
46
+ list(agent.run("Do the task.\n1. write out.txt", st,
47
+ {"model": "gemma4", "max_tokens": 8192, "context_limit": 65536}, "SYS"))
48
+ finally:
49
+ agent.stream = orig
50
+ evs = EventLog.read(p)
51
+ types = [e["type"] for e in evs]
52
+ assert types[0] == "task_start" and "turn" in types and "tool" in types and types[-1] == "done"
53
+ assert next(e for e in evs if e["type"] == "done")["phase"] == "complete"
54
+
55
+
56
+ def test_disabled_no_events_attr():
57
+ st = agent.AgentState()
58
+ assert st.events is None # off by default on a bare AgentState
@@ -38,7 +38,7 @@ def test_changing_args_do_not_trip_valve(monkeypatch):
38
38
  monkeypatch.setattr(agent_mod, "stream", lambda **kw: iter([seq.pop(0)]))
39
39
  st = AgentState()
40
40
  import tempfile
41
- list(run("go", st, {"model": "m", "cwd": tempfile.mkdtemp()}, "sys"))
41
+ list(run("go", st, {"model": "m", "cwd": tempfile.mkdtemp(), "verify_gate": False}, "sys"))
42
42
  assert any(m.get("role") == "assistant" and m.get("content") == "done"
43
43
  for m in st.messages)
44
44
 
@@ -57,7 +57,7 @@ def test_identical_SUCCESS_streak_ends_turn(monkeypatch):
57
57
 
58
58
  monkeypatch.setattr(agent_mod, "stream", counting)
59
59
  st = AgentState()
60
- list(run("go", st, {"model": "m", "cwd": tempfile.mkdtemp()}, "sys"))
60
+ list(run("go", st, {"model": "m", "cwd": tempfile.mkdtemp(), "verify_gate": False}, "sys"))
61
61
  assert calls["n"] <= 12 # identical success → stopped at the cap, not 200
62
62
 
63
63
 
@@ -73,7 +73,7 @@ def test_polling_with_changing_result_not_tripped(monkeypatch):
73
73
  seq.append(AssistantTurn("done", [], 1, 1))
74
74
  monkeypatch.setattr(agent_mod, "stream", lambda **kw: iter([seq.pop(0)]))
75
75
  st = AgentState()
76
- list(run("go", st, {"model": "m", "cwd": tempfile.mkdtemp()}, "sys"))
76
+ list(run("go", st, {"model": "m", "cwd": tempfile.mkdtemp(), "verify_gate": False}, "sys"))
77
77
  assert any(m.get("content") == "done" for m in st.messages)
78
78
 
79
79
 
File without changes
File without changes
File without changes
File without changes