drydock-cli 3.0.130__tar.gz → 3.0.133__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 (145) hide show
  1. {drydock_cli-3.0.130/drydock_cli.egg-info → drydock_cli-3.0.133}/PKG-INFO +1 -1
  2. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/agent.py +15 -8
  3. drydock_cli-3.0.133/drydock/builtin_skills/nist-ai-rmf.md +28 -0
  4. drydock_cli-3.0.133/drydock/builtin_skills/nist-csf.md +27 -0
  5. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/tool_registry.py +21 -4
  6. drydock_cli-3.0.133/drydock/tool_result.py +101 -0
  7. {drydock_cli-3.0.130 → drydock_cli-3.0.133/drydock_cli.egg-info}/PKG-INFO +1 -1
  8. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock_cli.egg-info/SOURCES.txt +4 -0
  9. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/pyproject.toml +1 -1
  10. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_leaked_tool_call.py +5 -2
  11. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_skills.py +1 -0
  12. drydock_cli-3.0.133/tests/test_tool_result.py +50 -0
  13. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/LICENSE +0 -0
  14. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/NOTICE +0 -0
  15. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/README.md +0 -0
  16. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/__init__.py +0 -0
  17. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/__main__.py +0 -0
  18. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/advisor.py +0 -0
  19. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/bash_safety.py +0 -0
  20. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/builtin_skills/__init__.py +0 -0
  21. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/builtin_skills/ml-data.md +0 -0
  22. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/builtin_skills/ml-debug.md +0 -0
  23. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/builtin_skills/ml-finetune.md +0 -0
  24. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/builtin_skills/ml-metrics.md +0 -0
  25. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/builtin_skills/ml-rl.md +0 -0
  26. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/builtin_skills/ml-train.md +0 -0
  27. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/builtin_skills/rmf-categorize.md +0 -0
  28. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/builtin_skills/rmf-control.md +0 -0
  29. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/builtin_skills/rmf-poam.md +0 -0
  30. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/builtin_skills/rmf-review.md +0 -0
  31. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/builtin_skills/stig-assess.md +0 -0
  32. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/builtin_skills/stig-remediate.md +0 -0
  33. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/cci.py +0 -0
  34. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/cli.py +0 -0
  35. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/compaction.py +0 -0
  36. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/config.py +0 -0
  37. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/detect.py +0 -0
  38. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/events.py +0 -0
  39. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/extract.py +0 -0
  40. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/gittools.py +0 -0
  41. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/graphrag.py +0 -0
  42. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/guards.py +0 -0
  43. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/loop_detect.py +0 -0
  44. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/mcp.py +0 -0
  45. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/poam.py +0 -0
  46. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/providers.py +0 -0
  47. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/recipes.py +0 -0
  48. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/rmf.py +0 -0
  49. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/rmf_graph.py +0 -0
  50. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/skills.py +0 -0
  51. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/stig.py +0 -0
  52. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/suggest.py +0 -0
  53. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/task_state.py +0 -0
  54. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/tools/__init__.py +0 -0
  55. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/tui/__init__.py +0 -0
  56. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/tui/app.py +0 -0
  57. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/tui/approval.py +0 -0
  58. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/tui/messages.py +0 -0
  59. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/tui/widgets.py +0 -0
  60. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/tuning.py +0 -0
  61. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/verification.py +0 -0
  62. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock/web.py +0 -0
  63. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock_cli.egg-info/dependency_links.txt +0 -0
  64. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock_cli.egg-info/entry_points.txt +0 -0
  65. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock_cli.egg-info/requires.txt +0 -0
  66. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/drydock_cli.egg-info/top_level.txt +0 -0
  67. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/setup.cfg +0 -0
  68. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_advisor.py +0 -0
  69. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_approval.py +0 -0
  70. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_back_command.py +0 -0
  71. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_bash_background.py +0 -0
  72. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_bash_binary_output.py +0 -0
  73. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_bash_crossplatform.py +0 -0
  74. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_bash_output_bounding.py +0 -0
  75. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_bash_process_group.py +0 -0
  76. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_bash_safety.py +0 -0
  77. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_bash_sanitize.py +0 -0
  78. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_bash_shell.py +0 -0
  79. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_bash_stdin.py +0 -0
  80. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_bash_stop_partial.py +0 -0
  81. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_bash_timeout_network.py +0 -0
  82. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_bash_timeout_param.py +0 -0
  83. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_cci.py +0 -0
  84. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_cli_agents.py +0 -0
  85. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_compact_command.py +0 -0
  86. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_compaction.py +0 -0
  87. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_config.py +0 -0
  88. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_config_migration.py +0 -0
  89. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_context_limit_config.py +0 -0
  90. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_detect.py +0 -0
  91. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_dispatch.py +0 -0
  92. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_e2e_connected.py +0 -0
  93. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_edit_replace_all.py +0 -0
  94. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_empty_response.py +0 -0
  95. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_events.py +0 -0
  96. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_extract.py +0 -0
  97. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_failure_loop.py +0 -0
  98. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_first_run_setup.py +0 -0
  99. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_gittools.py +0 -0
  100. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_glob_edit_edges.py +0 -0
  101. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_graphify_example.py +0 -0
  102. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_graphrag.py +0 -0
  103. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_graphrag_quoted_path.py +0 -0
  104. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_grep_and_read_robust.py +0 -0
  105. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_guards_and_tools.py +0 -0
  106. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_hallucinated_tools.py +0 -0
  107. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_loop_detect.py +0 -0
  108. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_mcp.py +0 -0
  109. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_oneshot_unreachable.py +0 -0
  110. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_overthink_interrupt.py +0 -0
  111. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_plan_autocontinue.py +0 -0
  112. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_poam.py +0 -0
  113. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_providers_unreachable.py +0 -0
  114. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_read_index.py +0 -0
  115. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_recipes.py +0 -0
  116. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_repetition_interrupt.py +0 -0
  117. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_rmf.py +0 -0
  118. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_rmf_graph.py +0 -0
  119. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_rmf_stig_graph.py +0 -0
  120. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_rolling_plan.py +0 -0
  121. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_runaway_repetition.py +0 -0
  122. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_screenshot.py +0 -0
  123. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_server_probe.py +0 -0
  124. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_stall_retry.py +0 -0
  125. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_stig.py +0 -0
  126. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_stop.py +0 -0
  127. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_streaming_newlines.py +0 -0
  128. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_subagent.py +0 -0
  129. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_suggest.py +0 -0
  130. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_system_prompt_help.py +0 -0
  131. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_task_state.py +0 -0
  132. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_todo.py +0 -0
  133. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_tool_arg_coercion.py +0 -0
  134. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_tool_arg_coercion_more.py +0 -0
  135. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_tool_arg_parsing.py +0 -0
  136. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_tools_undo.py +0 -0
  137. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_tui.py +0 -0
  138. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_tuning.py +0 -0
  139. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_verification_gate.py +0 -0
  140. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_viewimage.py +0 -0
  141. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_vision_input.py +0 -0
  142. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_web_tools.py +0 -0
  143. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_windows_shell.py +0 -0
  144. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/tests/test_write_content_coerce.py +0 -0
  145. {drydock_cli-3.0.130 → drydock_cli-3.0.133}/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.130
3
+ Version: 3.0.133
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
@@ -34,7 +34,7 @@ def _plan_has_unfinished(config: dict) -> bool:
34
34
  return bool(todo) and any(status != "done" for _, status in todo)
35
35
 
36
36
  from drydock.providers import stream, AssistantTurn, ReasoningChunk, TextChunk, StallRetry, RepetitionDetected
37
- from drydock.tool_registry import schemas, execute
37
+ from drydock.tool_registry import schemas, execute_structured
38
38
  from drydock.tools import register_all
39
39
 
40
40
  # Register the built-in tools as a side effect of importing the agent. This is
@@ -49,7 +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, parse_evidence
52
- from drydock.events import emit as _emit
52
+ from drydock.events import EventLog, emit as _emit
53
53
  from drydock.tuning import (
54
54
  filter_tool_schemas,
55
55
  hallucinated_tool_message,
@@ -84,7 +84,7 @@ class AgentState:
84
84
  turn_count: int = 0
85
85
  current_effort: str = "" # "high"/"low" of the in-flight LLM call (for the UI)
86
86
  task: "TaskState" = field(default_factory=lambda: TaskState()) # structured objective
87
- events: object | None = None # optional EventLog for a durable execution trace
87
+ events: "EventLog | None" = None # optional durable execution trace
88
88
 
89
89
 
90
90
  def drop_last_turn(messages: list) -> bool:
@@ -422,14 +422,17 @@ def run(
422
422
  halluc = hallucinated_tool_message(tc["name"])
423
423
  if halluc is not None:
424
424
  result = halluc
425
+ tool_result = None
425
426
  elif allow is not None and tc["name"] not in allow:
426
427
  result = (
427
428
  f"[The '{tc['name']}' tool is not available here. You may use "
428
429
  f"only: {', '.join(allow)}. Use one of those, or reply with "
429
430
  "your final summary.]"
430
431
  )
432
+ tool_result = None
431
433
  else:
432
- result = execute(tc["name"], tc["input"], config)
434
+ tool_result = execute_structured(tc["name"], tc["input"], config)
435
+ result = tool_result.text
433
436
  # Track consecutive byte-identical calls — same name, args AND raw
434
437
  # result (captured before annotate prepends its note, which changes
435
438
  # each call) — for the safety valve below. A differing result
@@ -459,10 +462,14 @@ def run(
459
462
  _emit(state, "plan", version=state.task.plan.version,
460
463
  steps=[(s["id"], s["status"]) for s in state.task.plan.steps])
461
464
 
462
- _emit(state, "tool", name=tc["name"],
463
- input=str(tc.get("input"))[:200],
464
- result_chars=len(str(result)),
465
- error=str(result)[:80].lower().startswith("error"))
465
+ if tool_result is not None:
466
+ _emit(state, "tool", input=str(tc.get("input"))[:200],
467
+ **tool_result.to_event())
468
+ else:
469
+ _emit(state, "tool", name=tc["name"],
470
+ input=str(tc.get("input"))[:200],
471
+ result_chars=len(str(result)),
472
+ status="ok")
466
473
  yield ToolEnd(tc["name"], result)
467
474
 
468
475
  # Append tool result
@@ -0,0 +1,28 @@
1
+ ---
2
+ name: nist-ai-rmf
3
+ description: Apply the NIST AI Risk Management Framework (AI RMF 1.0) to govern, map, measure, and manage AI system risk
4
+ ---
5
+ Help apply the NIST AI Risk Management Framework (AI RMF 1.0) to an AI system.
6
+ This is a GOVERNANCE and risk-management task — trustworthy, responsible AI. If the
7
+ user has not described the AI system, its purpose, data, and deployment context, ask
8
+ for those first. Then work through the four AI RMF functions:
9
+
10
+ 1. GOVERN — the culture and processes for managing AI risk. Recommend policies,
11
+ roles/accountability, risk tolerance, and how AI risks are documented and escalated
12
+ across the AI lifecycle and the supply chain.
13
+ 2. MAP — establish context and identify risks. Frame the system's purpose, stakeholders,
14
+ and potential impacts; enumerate risks to individuals, groups, and society (bias,
15
+ safety, privacy, security, misuse), and the assumptions/limitations.
16
+ 3. MEASURE — analyze, assess, and track the identified risks with concrete methods and
17
+ metrics: evaluate the trustworthiness characteristics (valid & reliable, safe, secure
18
+ & resilient, accountable & transparent, explainable & interpretable, privacy-enhanced,
19
+ fair with harmful bias managed), and define test/monitoring procedures.
20
+ 4. MANAGE — prioritize and act on risks: treatment decisions (mitigate/transfer/avoid/
21
+ accept), resource allocation, incident response, and ongoing monitoring with feedback
22
+ into GOVERN and MAP.
23
+
24
+ Be concrete: tie each recommendation to the relevant AI RMF function and the
25
+ trustworthiness characteristics, and produce actionable next steps (owners, artifacts,
26
+ review cadence). This is for building trustworthy AI — never for enabling misuse.
27
+
28
+ AI system / context: $ARGS
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: nist-csf
3
+ description: Assess and improve cybersecurity posture with the NIST Cybersecurity Framework (CSF) 2.0
4
+ ---
5
+ Help apply the NIST Cybersecurity Framework (CSF) 2.0 to an organization or system.
6
+ This is a DEFENSIVE governance and risk-management task. If the user hasn't described
7
+ the organization, its systems, and its current controls, ask first. Then work across
8
+ the six CSF 2.0 Functions:
9
+
10
+ 1. GOVERN (GV) — cybersecurity strategy, roles & responsibilities, policy, risk
11
+ management strategy, and oversight of the supply chain.
12
+ 2. IDENTIFY (ID) — asset management, business environment, risk assessment: know the
13
+ systems, data, and risks that must be protected.
14
+ 3. PROTECT (PR) — safeguards: access control, awareness & training, data security,
15
+ platform security, and resilience of technology.
16
+ 4. DETECT (DE) — continuous monitoring and adverse-event analysis to find incidents.
17
+ 5. RESPOND (RS) — incident management, analysis, mitigation, and communication.
18
+ 6. RECOVER (RC) — recovery planning and restoration of capabilities after an incident.
19
+
20
+ For an assessment, rate the organization against the CSF Implementation Tiers
21
+ (1 Partial → 2 Risk Informed → 3 Repeatable → 4 Adaptive) per Function, identify the
22
+ gap between the current and target profile, and produce a prioritized improvement
23
+ roadmap. Cite the relevant CSF Function/Category (e.g. GV.RM, ID.AM, PR.AA, DE.CM,
24
+ RS.MA, RC.RP). Be concrete and defensive — this supports protecting systems, not
25
+ attacking them.
26
+
27
+ Organization / system / scope: $ARGS
@@ -30,17 +30,34 @@ def schemas() -> list[dict]:
30
30
  return [t.schema for t in _registry.values()]
31
31
 
32
32
  def execute(name: str, params: dict, config: dict, max_output: int = 32000) -> str:
33
- """Execute a tool by name. Truncates large output."""
33
+ """Execute a tool by name. Truncates large output. Returns the text string
34
+ (backward-compatible); use execute_structured() for the ToolResult."""
35
+ return execute_structured(name, params, config, max_output).text
36
+
37
+
38
+ def execute_structured(name: str, params: dict, config: dict, max_output: int = 32000):
39
+ """Execute a tool and return a structured ToolResult (status/diagnostics/
40
+ retryable/duration + the text), classified from the tool's string output +
41
+ timing. The individual tools still return plain strings — this WRAPS them."""
42
+ import time as _time
43
+
44
+ from drydock.tool_result import ToolResult, classify
45
+
34
46
  tool = get(name)
35
47
  if tool is None:
36
- return f"Error: tool '{name}' not found."
48
+ return ToolResult(name=name, status="not_found", error_code="not_found",
49
+ text=f"Error: tool '{name}' not found.")
50
+ start = _time.monotonic()
37
51
  try:
38
52
  result = tool.func(params, config)
39
53
  except Exception as e:
40
- return f"Error executing {name}: {e}"
54
+ dur = int((_time.monotonic() - start) * 1000)
55
+ return ToolResult(name=name, status="error", error_code="exception",
56
+ text=f"Error executing {name}: {e}", duration_ms=dur)
57
+ dur = int((_time.monotonic() - start) * 1000)
41
58
 
42
59
  if len(result) > max_output:
43
60
  half = max_output // 2
44
61
  quarter = max_output // 4
45
62
  result = result[:half] + f"\n[... {len(result) - half - quarter} chars truncated ...]\n" + result[-quarter:]
46
- return result
63
+ return classify(name, params, result, dur)
@@ -0,0 +1,101 @@
1
+ """Structured tool results (Agent-Buildout PRD, section 7.4 / "structured tool
2
+ results"): a tool's output is not immediately reduced to a single text string —
3
+ status, diagnostics, retryability, and timing stay available to the controller for
4
+ loop/progress detection, verification, and recovery.
5
+
6
+ Incremental by design: tools still return plain strings (no rewrite), and
7
+ tool_registry.execute() still returns a string for full backward-compat. The new
8
+ execute_structured() WRAPS that string into a ToolResult, classifying status/error/
9
+ retryability from the text + timing. Logic original to Drydock.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ from dataclasses import dataclass, field
15
+
16
+
17
+ @dataclass
18
+ class ToolResult:
19
+ """A tool call's outcome with structure the controller can reason about."""
20
+ name: str
21
+ status: str = "ok" # "ok" | "error" | "not_found"
22
+ text: str = "" # the human/model-facing string (backward-compat)
23
+ changed_state: bool = False # did it mutate files/env (Write/Edit/mutating Bash)?
24
+ retryable: bool = False # transient failure worth retrying?
25
+ error_code: str | None = None # coarse classification when status == error
26
+ exit_code: int | None = None # for Bash, if present
27
+ duration_ms: int = 0
28
+ diagnostics: list[str] = field(default_factory=list)
29
+
30
+ def to_event(self) -> dict:
31
+ """Compact dict for the event log (no big blobs)."""
32
+ return {
33
+ "name": self.name, "status": self.status,
34
+ "changed_state": self.changed_state, "retryable": self.retryable,
35
+ "error_code": self.error_code, "exit_code": self.exit_code,
36
+ "duration_ms": self.duration_ms, "result_chars": len(self.text),
37
+ }
38
+
39
+
40
+ _EXIT = re.compile(r"\[exit code:\s*(-?\d+)\]")
41
+ _MUTATING_BASH = re.compile(
42
+ r"(?:^|[\s;&|])(?:rm|mv|cp|mkdir|touch|tee|sed\s+-i|dd|chmod|chown|ln|"
43
+ r"git\s+(?:commit|add|apply|checkout|reset|rm|mv)|pip\s+install|npm\s+i|"
44
+ r"apt|make(?:\s|$)|cargo\s+build|>|>>)\b", re.I,
45
+ )
46
+ # Error phrases that usually mean "try again might work" (transient).
47
+ _RETRYABLE = re.compile(
48
+ r"\b(timed out|timeout|connection (refused|reset|error)|temporarily|"
49
+ r"try again|rate limit|EAGAIN|ETIMEDOUT|network is unreachable|"
50
+ r"resource temporarily unavailable|503|502|429)\b", re.I,
51
+ )
52
+
53
+
54
+ def _classify_error(text: str) -> tuple[str, bool]:
55
+ """(error_code, retryable) for an error result string."""
56
+ t = text.lower()
57
+ if _RETRYABLE.search(text):
58
+ return "transient", True
59
+ if "not found" in t or "no such file" in t or "command not found" in t:
60
+ return "not_found", False
61
+ if "permission denied" in t:
62
+ return "permission", False
63
+ if "syntax error" in t or "invalid" in t:
64
+ return "invalid_input", False
65
+ if "timed out" in t:
66
+ return "timeout", True
67
+ return "error", False
68
+
69
+
70
+ def classify(name: str, params: dict, text: str, duration_ms: int) -> ToolResult:
71
+ """Build a ToolResult from a tool's (name, params, string result). Never raises."""
72
+ text = text if isinstance(text, str) else str(text or "")
73
+ r = ToolResult(name=name, text=text, duration_ms=max(0, int(duration_ms)))
74
+
75
+ m = _EXIT.search(text)
76
+ if m:
77
+ try:
78
+ r.exit_code = int(m.group(1))
79
+ except ValueError:
80
+ pass
81
+
82
+ # Status: an "Error…" prefixed string, a "tool not found", or a non-zero Bash
83
+ # exit means the call did not succeed. Otherwise ok.
84
+ lowered = text.lstrip()[:60].lower()
85
+ if lowered.startswith("error: tool") and "not found" in text.lower():
86
+ r.status, r.error_code = "not_found", "not_found"
87
+ elif lowered.startswith("error"):
88
+ r.status = "error"
89
+ r.error_code, r.retryable = _classify_error(text)
90
+ elif r.exit_code not in (None, 0):
91
+ r.status = "error"
92
+ r.error_code, r.retryable = _classify_error(text)
93
+
94
+ # State change: Write/Edit always mutate; Bash only for mutating commands.
95
+ if name in ("Write", "Edit", "GitCommit", "BuildKnowledge", "GraphAdd"):
96
+ r.changed_state = r.status == "ok"
97
+ elif name == "Bash":
98
+ cmd = (params or {}).get("command", "") if isinstance(params, dict) else ""
99
+ r.changed_state = bool(_MUTATING_BASH.search(cmd)) and r.status == "ok"
100
+
101
+ return r
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.130
3
+ Version: 3.0.133
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
@@ -29,6 +29,7 @@ drydock/stig.py
29
29
  drydock/suggest.py
30
30
  drydock/task_state.py
31
31
  drydock/tool_registry.py
32
+ drydock/tool_result.py
32
33
  drydock/tuning.py
33
34
  drydock/verification.py
34
35
  drydock/web.py
@@ -39,6 +40,8 @@ drydock/builtin_skills/ml-finetune.md
39
40
  drydock/builtin_skills/ml-metrics.md
40
41
  drydock/builtin_skills/ml-rl.md
41
42
  drydock/builtin_skills/ml-train.md
43
+ drydock/builtin_skills/nist-ai-rmf.md
44
+ drydock/builtin_skills/nist-csf.md
42
45
  drydock/builtin_skills/rmf-categorize.md
43
46
  drydock/builtin_skills/rmf-control.md
44
47
  drydock/builtin_skills/rmf-poam.md
@@ -127,6 +130,7 @@ tests/test_todo.py
127
130
  tests/test_tool_arg_coercion.py
128
131
  tests/test_tool_arg_coercion_more.py
129
132
  tests/test_tool_arg_parsing.py
133
+ tests/test_tool_result.py
130
134
  tests/test_tools_undo.py
131
135
  tests/test_tui.py
132
136
  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.130"
10
+ version = "3.0.133"
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"
@@ -83,8 +83,11 @@ def test_adaptive_reasoning_high_to_plan_then_low(monkeypatch):
83
83
  yield AssistantTurn("done", [], 1, 1)
84
84
 
85
85
  monkeypatch.setattr(agent_mod, "stream", fake_stream)
86
- # execute() would run a real Bash; stub it to keep the test hermetic.
87
- monkeypatch.setattr(agent_mod, "execute", lambda name, inp, cfg: "ok")
86
+ # execute would run a real Bash; stub it to keep the test hermetic. The loop
87
+ # now calls execute_structured (returns a ToolResult with .text).
88
+ from drydock.tool_result import ToolResult
89
+ monkeypatch.setattr(agent_mod, "execute_structured",
90
+ lambda name, inp, cfg: ToolResult(name=name, text="ok"))
88
91
  st = AgentState()
89
92
  list(run("go", st, {"model": "gemma4"}, "sys"))
90
93
  # First (planning) turn high, the continuation that consumes the tool result low.
@@ -59,6 +59,7 @@ def test_no_user_or_project_skills_yields_only_builtins(tmp_path):
59
59
  "rmf-control", "rmf-categorize", "rmf-review", "rmf-poam",
60
60
  "stig-assess", "stig-remediate",
61
61
  "ml-train", "ml-metrics", "ml-finetune", "ml-debug", "ml-rl", "ml-data",
62
+ "nist-ai-rmf", "nist-csf",
62
63
  }
63
64
 
64
65
 
@@ -0,0 +1,50 @@
1
+ """Structured tool results (Agent-Buildout PRD 7.4): execute_structured() wraps a
2
+ tool's string output in a ToolResult (status/error/retryable/changed_state/duration)
3
+ while execute() stays a plain string (backward compatibility)."""
4
+ from __future__ import annotations
5
+
6
+ from drydock.tool_result import ToolResult, classify
7
+ from drydock.tools import register_all
8
+ from drydock import tool_registry
9
+
10
+ register_all()
11
+
12
+
13
+ def test_execute_still_returns_str():
14
+ r = tool_registry.execute("Bash", {"command": "echo hi", "timeout": 5}, {"cwd": "/tmp"})
15
+ assert isinstance(r, str) and "hi" in r
16
+
17
+
18
+ def test_structured_success():
19
+ r = tool_registry.execute_structured("Bash", {"command": "echo hi", "timeout": 5}, {"cwd": "/tmp"})
20
+ assert isinstance(r, ToolResult) and r.status == "ok" and r.duration_ms >= 0 and not r.changed_state
21
+
22
+
23
+ def test_structured_failure_exit_code():
24
+ r = tool_registry.execute_structured(
25
+ "Bash", {"command": "python3 -c 'import sys;sys.exit(3)'", "timeout": 5}, {"cwd": "/tmp"})
26
+ assert r.status == "error" and r.exit_code == 3
27
+
28
+
29
+ def test_not_found():
30
+ r = tool_registry.execute_structured("NoSuchTool", {}, {})
31
+ assert r.status == "not_found" and r.error_code == "not_found"
32
+
33
+
34
+ def test_classify_changed_state_and_retryable():
35
+ assert classify("Write", {}, "wrote 10 bytes", 1).changed_state
36
+ assert not classify("Read", {}, "contents", 1).changed_state
37
+ assert classify("Bash", {"command": "touch x"}, "", 1).changed_state
38
+ assert not classify("Bash", {"command": "ls -la"}, "files", 1).changed_state
39
+ r = classify("Bash", {"command": "curl x"}, "Error: connection refused", 1)
40
+ assert r.status == "error" and r.retryable and r.error_code == "transient"
41
+
42
+
43
+ def test_error_prefix_string():
44
+ r = classify("Edit", {}, "Error: old_string not found", 1)
45
+ assert r.status == "error" and not r.changed_state # failed edit didn't mutate
46
+
47
+
48
+ def test_to_event_is_compact():
49
+ ev = classify("Bash", {"command": "echo x"}, "x", 5).to_event()
50
+ assert ev["status"] == "ok" and "result_chars" in ev and "text" not in ev
File without changes
File without changes
File without changes
File without changes