drydock-cli 3.1.36__tar.gz → 3.1.37__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 (220) hide show
  1. {drydock_cli-3.1.36/drydock_cli.egg-info → drydock_cli-3.1.37}/PKG-INFO +1 -1
  2. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/agent.py +12 -0
  3. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/cli.py +9 -0
  4. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/compaction.py +13 -0
  5. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/providers.py +43 -7
  6. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/tui/app.py +1 -1
  7. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/tuning.py +21 -3
  8. {drydock_cli-3.1.36 → drydock_cli-3.1.37/drydock_cli.egg-info}/PKG-INFO +1 -1
  9. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock_cli.egg-info/SOURCES.txt +1 -0
  10. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/pyproject.toml +1 -1
  11. drydock_cli-3.1.37/tests/test_text_only_model.py +78 -0
  12. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_tuning.py +11 -0
  13. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/LICENSE +0 -0
  14. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/NOTICE +0 -0
  15. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/README.md +0 -0
  16. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/__init__.py +0 -0
  17. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/__main__.py +0 -0
  18. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/advisor.py +0 -0
  19. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/asr.py +0 -0
  20. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/bash_safety.py +0 -0
  21. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/bottleneck.py +0 -0
  22. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/budget.py +0 -0
  23. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/builtin_skills/__init__.py +0 -0
  24. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/builtin_skills/document-canvas.md +0 -0
  25. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/builtin_skills/ml-data.md +0 -0
  26. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/builtin_skills/ml-debug.md +0 -0
  27. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/builtin_skills/ml-finetune.md +0 -0
  28. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/builtin_skills/ml-metrics.md +0 -0
  29. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/builtin_skills/ml-rl.md +0 -0
  30. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/builtin_skills/ml-train.md +0 -0
  31. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/builtin_skills/nist-ai-rmf.md +0 -0
  32. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/builtin_skills/nist-csf.md +0 -0
  33. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/builtin_skills/rmf-categorize.md +0 -0
  34. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/builtin_skills/rmf-control.md +0 -0
  35. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/builtin_skills/rmf-poam.md +0 -0
  36. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/builtin_skills/rmf-review.md +0 -0
  37. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/builtin_skills/stig-assess.md +0 -0
  38. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/builtin_skills/stig-remediate.md +0 -0
  39. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/capacity.py +0 -0
  40. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/cci.py +0 -0
  41. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/comms/__init__.py +0 -0
  42. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/comms/attention.py +0 -0
  43. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/comms/channels.py +0 -0
  44. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/comms/events.py +0 -0
  45. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/config.py +0 -0
  46. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/detect.py +0 -0
  47. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/doccanvas.py +0 -0
  48. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/eratchet.py +0 -0
  49. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/events.py +0 -0
  50. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/extract.py +0 -0
  51. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/gittools.py +0 -0
  52. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/graphrag.py +0 -0
  53. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/groundtruth.py +0 -0
  54. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/guards.py +0 -0
  55. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/jobs.py +0 -0
  56. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/loop_detect.py +0 -0
  57. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/mcp.py +0 -0
  58. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/mission.py +0 -0
  59. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/mission_cli.py +0 -0
  60. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/mission_run.py +0 -0
  61. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/pdfredbox.py +0 -0
  62. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/phases.py +0 -0
  63. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/poam.py +0 -0
  64. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/predictions.py +0 -0
  65. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/progress.py +0 -0
  66. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/ratchet.py +0 -0
  67. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/recipes.py +0 -0
  68. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/recovery.py +0 -0
  69. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/resume.py +0 -0
  70. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/rmf.py +0 -0
  71. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/rmf_graph.py +0 -0
  72. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/skills.py +0 -0
  73. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/stig.py +0 -0
  74. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/subagents.py +0 -0
  75. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/suggest.py +0 -0
  76. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/swarm.py +0 -0
  77. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/task_state.py +0 -0
  78. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/tool_policy.py +0 -0
  79. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/tool_registry.py +0 -0
  80. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/tool_result.py +0 -0
  81. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/tool_select.py +0 -0
  82. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/tool_validate.py +0 -0
  83. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/tools/__init__.py +0 -0
  84. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/trajectory.py +0 -0
  85. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/tui/__init__.py +0 -0
  86. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/tui/approval.py +0 -0
  87. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/tui/messages.py +0 -0
  88. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/tui/widgets.py +0 -0
  89. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/verification.py +0 -0
  90. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock/web.py +0 -0
  91. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock_cli.egg-info/dependency_links.txt +0 -0
  92. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock_cli.egg-info/entry_points.txt +0 -0
  93. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock_cli.egg-info/requires.txt +0 -0
  94. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/drydock_cli.egg-info/top_level.txt +0 -0
  95. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/setup.cfg +0 -0
  96. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_advisor.py +0 -0
  97. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_approval.py +0 -0
  98. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_asr.py +0 -0
  99. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_back_command.py +0 -0
  100. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_bash_background.py +0 -0
  101. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_bash_binary_output.py +0 -0
  102. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_bash_crossplatform.py +0 -0
  103. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_bash_output_bounding.py +0 -0
  104. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_bash_process_group.py +0 -0
  105. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_bash_safety.py +0 -0
  106. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_bash_sanitize.py +0 -0
  107. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_bash_shell.py +0 -0
  108. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_bash_stdin.py +0 -0
  109. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_bash_stop_partial.py +0 -0
  110. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_bash_timeout_network.py +0 -0
  111. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_bash_timeout_param.py +0 -0
  112. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_bottleneck.py +0 -0
  113. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_budget.py +0 -0
  114. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_capacity.py +0 -0
  115. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_cci.py +0 -0
  116. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_cli_agents.py +0 -0
  117. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_comms.py +0 -0
  118. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_compact_command.py +0 -0
  119. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_compaction.py +0 -0
  120. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_config.py +0 -0
  121. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_config_migration.py +0 -0
  122. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_context_limit_config.py +0 -0
  123. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_context_limit_issue25.py +0 -0
  124. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_criteria_coverage.py +0 -0
  125. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_cycling_detection.py +0 -0
  126. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_declared_deps.py +0 -0
  127. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_degenerate_argument.py +0 -0
  128. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_detect.py +0 -0
  129. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_dispatch.py +0 -0
  130. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_doccanvas.py +0 -0
  131. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_e2e_connected.py +0 -0
  132. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_edit_replace_all.py +0 -0
  133. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_edit_thrash_and_compaction.py +0 -0
  134. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_effort_governor.py +0 -0
  135. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_empty_response.py +0 -0
  136. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_eratchet.py +0 -0
  137. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_events.py +0 -0
  138. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_extract.py +0 -0
  139. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_failure_loop.py +0 -0
  140. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_first_run_setup.py +0 -0
  141. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_gittools.py +0 -0
  142. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_glob_edit_edges.py +0 -0
  143. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_graphify_example.py +0 -0
  144. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_graphrag.py +0 -0
  145. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_graphrag_quoted_path.py +0 -0
  146. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_graphrag_sqlite.py +0 -0
  147. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_grep_and_read_robust.py +0 -0
  148. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_groundtruth_predictions.py +0 -0
  149. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_guards_and_tools.py +0 -0
  150. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_hallucinated_tools.py +0 -0
  151. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_jobs.py +0 -0
  152. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_knowledge_tool_available.py +0 -0
  153. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_leaked_tool_call.py +0 -0
  154. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_loop_detect.py +0 -0
  155. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_main_api_key.py +0 -0
  156. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_mcp.py +0 -0
  157. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_mission.py +0 -0
  158. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_mission_cli.py +0 -0
  159. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_mission_run.py +0 -0
  160. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_model_registry.py +0 -0
  161. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_oneshot_unreachable.py +0 -0
  162. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_overthink_interrupt.py +0 -0
  163. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_pdfredbox.py +0 -0
  164. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_phases.py +0 -0
  165. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_plan_autocontinue.py +0 -0
  166. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_poam.py +0 -0
  167. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_progress.py +0 -0
  168. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_providers_unreachable.py +0 -0
  169. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_ratchet.py +0 -0
  170. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_ratchet_offer.py +0 -0
  171. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_read_index.py +0 -0
  172. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_recipes.py +0 -0
  173. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_recovery.py +0 -0
  174. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_recovery_config.py +0 -0
  175. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_recovery_integration.py +0 -0
  176. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_repeated_outcome.py +0 -0
  177. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_repetition_interrupt.py +0 -0
  178. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_resume_events.py +0 -0
  179. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_resume_restore.py +0 -0
  180. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_resume_snapshot.py +0 -0
  181. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_rmf.py +0 -0
  182. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_rmf_graph.py +0 -0
  183. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_rmf_stig_graph.py +0 -0
  184. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_rolling_plan.py +0 -0
  185. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_runaway_repetition.py +0 -0
  186. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_screenshot.py +0 -0
  187. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_server_probe.py +0 -0
  188. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_skills.py +0 -0
  189. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_sqlite_events.py +0 -0
  190. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_stall_retry.py +0 -0
  191. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_stig.py +0 -0
  192. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_stop.py +0 -0
  193. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_streaming_newlines.py +0 -0
  194. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_subagent.py +0 -0
  195. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_subagents.py +0 -0
  196. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_suggest.py +0 -0
  197. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_swarm.py +0 -0
  198. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_system_prompt_help.py +0 -0
  199. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_task_state.py +0 -0
  200. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_timeline.py +0 -0
  201. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_todo.py +0 -0
  202. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_tool_arg_coercion.py +0 -0
  203. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_tool_arg_coercion_more.py +0 -0
  204. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_tool_arg_parsing.py +0 -0
  205. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_tool_canonical.py +0 -0
  206. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_tool_policy.py +0 -0
  207. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_tool_result.py +0 -0
  208. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_tool_select.py +0 -0
  209. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_tool_validate.py +0 -0
  210. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_tools_undo.py +0 -0
  211. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_trajectory.py +0 -0
  212. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_tui.py +0 -0
  213. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_verification_gate.py +0 -0
  214. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_viewimage.py +0 -0
  215. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_vision_input.py +0 -0
  216. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_web_tools.py +0 -0
  217. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_windows_shell.py +0 -0
  218. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_worker_subagent.py +0 -0
  219. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_write_content_coerce.py +0 -0
  220. {drydock_cli-3.1.36 → drydock_cli-3.1.37}/tests/test_xccdf.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.1.36
3
+ Version: 3.1.37
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
@@ -47,6 +47,7 @@ from drydock.tools import register_all
47
47
  register_all()
48
48
  from drydock.compaction import (
49
49
  maybe_compact, emergency_compact, is_context_length_error, is_image_load_error,
50
+ is_text_only_model_error,
50
51
  extract_server_n_ctx,
51
52
  )
52
53
  from drydock.loop_detect import LoopTracker, degenerate_argument
@@ -409,6 +410,17 @@ def run(
409
410
  if retries >= 2:
410
411
  raise
411
412
  yield TextChunk("\n[context limit hit — compacting and retrying...]\n")
413
+ elif is_text_only_model_error(err):
414
+ # The model can't take images at all. Stop attaching them for this
415
+ # endpoint and retry the step as text — a vision-less model can
416
+ # still do the task (read the file with tools), it just can't SEE it.
417
+ from drydock.providers import mark_text_only
418
+ if not mark_text_only(config):
419
+ raise
420
+ yield TextChunk(
421
+ "\n[This model is text-only — sending image references as plain "
422
+ "text from now on and retrying...]\n"
423
+ )
412
424
  elif is_image_load_error(err):
413
425
  # Corrupt/truncated/unsupported image the server couldn't decode —
414
426
  # end the turn cleanly rather than dumping the raw 400.
@@ -507,6 +507,15 @@ def main():
507
507
  # the PRD" content fought the system prompt and risked turning a plain
508
508
  # greeting into a runaway build. The TUI reads this from config; CLI modes
509
509
  # call load_project_instructions directly (see run_interactive/run_oneshot).
510
+ # A context_limit above the server's real window overflows it mid-task and the
511
+ # ctx gauge lies. Unless the user pinned --context-limit, ask the server once and
512
+ # adopt its window when smaller (never grow past config). Best-effort, short timeout.
513
+ if not args.context_limit and config.get("base_url"):
514
+ from drydock.providers import probe_server_context
515
+ n_ctx = probe_server_context(config["base_url"], timeout=2.0)
516
+ if n_ctx and n_ctx < int(config.get("context_limit") or 65536):
517
+ config["context_limit"] = n_ctx
518
+
510
519
  config["project_instructions"] = load_project_instructions()
511
520
  # In a git project, drop a discoverable per-project prompt template at
512
521
  # <project>/.drydock/system_prompt.md (inert until edited). Then load the
@@ -51,6 +51,19 @@ def extract_server_n_ctx(err: str) -> int | None:
51
51
  return None
52
52
 
53
53
 
54
+ def is_text_only_model_error(err: str) -> bool:
55
+ """Whether a provider 400 says the MODEL can't take images at all (a text-only
56
+ model, e.g. vLLM "X is not a multimodal model", llama.cpp "image input is not
57
+ supported" when no --mmproj is loaded). Distinct from a bad image: the fix is to
58
+ drop attachments and retry, not to ask the user for a different file."""
59
+ e = (err or "").lower()
60
+ return (
61
+ "not a multimodal model" in e
62
+ or "image input is not supported" in e
63
+ or ("does not support" in e and ("image" in e or "multimodal" in e or "vision" in e))
64
+ )
65
+
66
+
54
67
  def is_image_load_error(err: str) -> bool:
55
68
  """Whether a provider 400 is the server failing to decode an attached image
56
69
  (corrupt / truncated / unsupported), so the agent ends the turn with a clean
@@ -39,7 +39,9 @@ class RepetitionDetected(StallRetry):
39
39
  re-issue would just loop again, so the agent goes straight to decisive mode."""
40
40
 
41
41
  from drydock.loop_detect import runaway_repetition_len
42
- from drydock.tuning import extract_thinking, strip_leaked_tool_calls, strip_thinking_tokens, use_streaming
42
+ from drydock.tuning import (
43
+ extract_thinking, split_think_tags, strip_leaked_tool_calls, strip_thinking_tokens, use_streaming,
44
+ )
43
45
 
44
46
  # ── Provider registry ─────────────────────────────────────────────────────
45
47
 
@@ -430,6 +432,31 @@ def _image_url_block(path: str) -> dict | None:
430
432
  return {"type": "image_url", "image_url": {"url": f"data:{mime};base64,{b64}"}}
431
433
 
432
434
 
435
+ # (base_url, model) pairs whose server rejected image input as text-only. Process-wide
436
+ # so every later request (and turn) sends plain text instead of re-hitting the 400.
437
+ _TEXT_ONLY: set[tuple[str, str]] = set()
438
+
439
+
440
+ def _vision_key(config: dict) -> tuple[str, str]:
441
+ prov = PROVIDERS.get(config.get("provider", "vllm"), PROVIDERS["vllm"])
442
+ base_url = config.get("base_url") or prov.get("base_url", "http://localhost:8000/v1")
443
+ return (str(base_url).rstrip("/"), str(config.get("model") or ""))
444
+
445
+
446
+ def mark_text_only(config: dict) -> bool:
447
+ """Record that this endpoint's model can't take images. Returns True if newly marked."""
448
+ key = _vision_key(config)
449
+ if key in _TEXT_ONLY:
450
+ return False
451
+ _TEXT_ONLY.add(key)
452
+ return True
453
+
454
+
455
+ def vision_enabled(config: dict) -> bool:
456
+ """False when vision is disabled in config or the server proved text-only."""
457
+ return config.get("vision", True) is not False and _vision_key(config) not in _TEXT_ONLY
458
+
459
+
433
460
  def _content_with_images(content):
434
461
  """Turn text that references on-disk image paths into a multimodal content
435
462
  list ([text, image_url…]); text without resolvable images passes through
@@ -451,15 +478,17 @@ def _content_with_images(content):
451
478
  _user_content_with_images = _content_with_images
452
479
 
453
480
 
454
- def messages_to_openai(messages: list, system: str) -> list:
455
- """Convert neutral messages to OpenAI API format."""
481
+ def messages_to_openai(messages: list, system: str, vision: bool = True) -> list:
482
+ """Convert neutral messages to OpenAI API format. vision=False sends image
483
+ references as plain text (text-only model)."""
456
484
  # value type is mixed (str content, multimodal list content, tool_calls list)
457
485
  result: list[dict] = [{"role": "system", "content": system}]
458
486
  for m in messages:
459
487
  role = m["role"]
460
488
  if role == "user":
461
489
  result.append({"role": "user",
462
- "content": _user_content_with_images(m["content"])})
490
+ "content": (_user_content_with_images(m["content"])
491
+ if vision else m["content"])})
463
492
  elif role == "assistant":
464
493
  msg = {"role": "assistant", "content": m.get("content") or None}
465
494
  tcs = m.get("tool_calls", [])
@@ -482,7 +511,7 @@ def messages_to_openai(messages: list, system: str) -> list:
482
511
  # to look at — attach it so the model SEES it (servers read images
483
512
  # from tool messages too, verified). Only ViewImage, so an unrelated
484
513
  # tool mentioning a .png path never balloons the request.
485
- if m.get("name") == "ViewImage" and isinstance(content, str):
514
+ if vision and m.get("name") == "ViewImage" and isinstance(content, str):
486
515
  content = _content_with_images(content)
487
516
  result.append({
488
517
  "role": "tool",
@@ -559,7 +588,7 @@ def stream(
559
588
  # dict(config) shallow-copy) so the TUI's STOP can reach this exact client.
560
589
  config.setdefault("_abort", {})["client"] = client
561
590
 
562
- oai_messages = messages_to_openai(messages, system)
591
+ oai_messages = messages_to_openai(messages, system, vision=vision_enabled(config))
563
592
 
564
593
  has_tools = bool(tool_schemas)
565
594
  # Gemma (and any local server whose model name we can't trust) corrupts
@@ -647,6 +676,9 @@ def stream(
647
676
  if delta.content:
648
677
  # Strip any leaked thinking-token markers (best-effort per chunk).
649
678
  chunk_text = strip_thinking_tokens(delta.content)
679
+ # <think>/</think> tags usually arrive as whole tokens; keep them in the
680
+ # buffer (split off at the end) but don't display the bare markers.
681
+ shown = chunk_text.replace("<think>", "").replace("</think>", "")
650
682
  # Keep ANY non-empty chunk — including whitespace-only ones. A
651
683
  # `.strip()` guard here used to DROP newline-only chunks (the blank
652
684
  # line a model streams between markdown blocks arrives as its own
@@ -654,7 +686,8 @@ def stream(
654
686
  # skips chunks that stripping emptied (e.g. a lone thinking marker).
655
687
  if chunk_text:
656
688
  text += chunk_text
657
- yield TextChunk(chunk_text)
689
+ if shown:
690
+ yield TextChunk(shown)
658
691
  # Throttled: only scan once text is long enough and every ~200
659
692
  # new chars, so the common path pays almost nothing.
660
693
  if len(text) - _rep_checked_at >= 200:
@@ -705,6 +738,9 @@ def stream(
705
738
  "input": inp,
706
739
  })
707
740
 
741
+ # A template-opened <think> leaves "reasoning…</think>answer" in the content —
742
+ # keep only the answer in the recorded turn (it was already shown while streaming).
743
+ _, text = split_think_tags(text)
708
744
  yield AssistantTurn(text, tool_calls, in_tok, out_tok)
709
745
 
710
746
 
@@ -515,7 +515,7 @@ class DrydockApp(App):
515
515
  # actual attach happens at the API boundary in providers).
516
516
  from drydock import providers
517
517
  imgs = providers.detect_image_paths(text)
518
- if imgs:
518
+ if imgs and providers.vision_enabled(self.config):
519
519
  import os as _os
520
520
  names = ", ".join(_os.path.basename(p) for p in imgs)
521
521
  self._info(f"📎 attached {len(imgs)} image(s) for the model to see: {names}")
@@ -157,13 +157,31 @@ def is_gemma(model: str | None) -> bool:
157
157
  return bool(model) and "gemma" in model.lower()
158
158
 
159
159
 
160
+ def split_think_tags(text: str) -> tuple[str, str]:
161
+ """Split <think>…</think> reasoning (Qwen/Nemotron/DeepSeek style) off the answer.
162
+
163
+ Chat templates often open <think> in the PROMPT, so the completion carries only
164
+ the closing tag: everything before the last </think> is reasoning. Returns
165
+ ("", text) when there is no closing tag."""
166
+ if not text or "</think>" not in text:
167
+ return "", text
168
+ thinking, answer = text.rsplit("</think>", 1)
169
+ thinking = thinking.replace("<think>", "").strip()
170
+ return thinking, answer.lstrip()
171
+
172
+
160
173
  def extract_thinking(text: str) -> tuple[str, str]:
161
174
  """Return (thinking_content, cleaned_text).
162
175
 
163
- Extracts the content of Gemma's <|channel>…<channel|> blocks so callers
164
- can surface it in the UI, then strips the markers from the returned text.
165
- Returns ("", text) when no thinking block is present.
176
+ Extracts the content of Gemma's <|channel>…<channel|> blocks (or a
177
+ <think>…</think> span) so callers can surface it in the UI, then strips the
178
+ markers from the returned text. Returns ("", text) when no thinking block is
179
+ present.
166
180
  """
181
+ if text and "</think>" in text:
182
+ thinking, answer = split_think_tags(text)
183
+ inner, answer = extract_thinking(answer)
184
+ return "\n\n".join(t for t in (thinking, inner) if t), answer
167
185
  if not text or "<|channel>" not in text:
168
186
  return "", text
169
187
  spans = _THINKING_RE.findall(text)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.1.36
3
+ Version: 3.1.37
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
@@ -193,6 +193,7 @@ tests/test_suggest.py
193
193
  tests/test_swarm.py
194
194
  tests/test_system_prompt_help.py
195
195
  tests/test_task_state.py
196
+ tests/test_text_only_model.py
196
197
  tests/test_timeline.py
197
198
  tests/test_todo.py
198
199
  tests/test_tool_arg_coercion.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.1.36"
10
+ version = "3.1.37"
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,78 @@
1
+ """Text-only model fallback: when the server rejects image input because the model
2
+ isn't multimodal (vLLM "X is not a multimodal model", llama.cpp "image input is not
3
+ supported"), the agent stops attaching images for that endpoint and retries the step
4
+ as plain text instead of dead-ending the turn on a raw 400."""
5
+ from __future__ import annotations
6
+
7
+ from drydock import agent, providers
8
+ from drydock.agent import AgentState
9
+ from drydock.compaction import is_image_load_error, is_text_only_model_error
10
+ from drydock.providers import AssistantTurn, messages_to_openai
11
+
12
+ from tests.test_vision_input import _write_png
13
+
14
+ VLLM_ERR = ("Error code: 400 - {'error': {'message': 'nemotron is not a multimodal model', "
15
+ "'type': 'BadRequestError', 'param': None, 'code': 400}}")
16
+ LLAMA_ERR = ("Error code: 400 - {'error': {'code': 400, 'message': 'image input is not supported "
17
+ "- hint: if this is unexpected, you may need to provide the mmproj', "
18
+ "'type': 'invalid_request_error'}}")
19
+
20
+
21
+ def test_classifies_text_only_errors():
22
+ assert is_text_only_model_error(VLLM_ERR)
23
+ assert is_text_only_model_error(LLAMA_ERR)
24
+ assert not is_text_only_model_error("Failed to load image or audio file")
25
+ assert not is_text_only_model_error("context_length_exceeded")
26
+ # a corrupt image is still an image-load error, not a text-only model
27
+ assert is_image_load_error("Failed to load image or audio file")
28
+
29
+
30
+ def test_vision_false_keeps_image_reference_as_text(tmp_path):
31
+ p = _write_png(tmp_path / "board.png")
32
+ msgs = messages_to_openai([{"role": "user", "content": f"see {p}"}], "sys", vision=False)
33
+ assert msgs[1]["content"] == f"see {p}"
34
+ tool = messages_to_openai([{"role": "tool", "tool_call_id": "c", "name": "ViewImage",
35
+ "content": f"viewing {p}"}], "sys", vision=False)
36
+ assert tool[1]["content"] == f"viewing {p}"
37
+
38
+
39
+ def test_agent_drops_images_and_retries(monkeypatch, tmp_path):
40
+ p = _write_png(tmp_path / "board.png")
41
+ cfg = {"model": "textonly-test", "base_url": "http://tx.invalid:9/v1",
42
+ "max_tokens": 1024, "context_limit": 65536}
43
+ calls = []
44
+
45
+ def fake_stream(model, system, messages, tool_schemas, config):
46
+ vision = providers.vision_enabled(config)
47
+ calls.append(vision)
48
+ if vision:
49
+ raise RuntimeError(VLLM_ERR)
50
+ yield AssistantTurn(text="done", tool_calls=[], input_tokens=10, output_tokens=5)
51
+
52
+ monkeypatch.setattr(agent, "stream", fake_stream)
53
+ monkeypatch.setattr(providers, "_TEXT_ONLY", set())
54
+ events = list(agent.run(f"what is in {p}", AgentState(), cfg, "SYS"))
55
+ assert calls == [True, False]
56
+ assert any("text-only" in getattr(e, "text", "") for e in events)
57
+ assert not providers.vision_enabled(cfg)
58
+ # a different endpoint is unaffected
59
+ assert providers.vision_enabled({**cfg, "base_url": "http://other.invalid:9/v1"})
60
+
61
+
62
+ def test_second_text_only_error_raises_not_loops(monkeypatch):
63
+ cfg = {"model": "textonly-test2", "base_url": "http://tx2.invalid:9/v1",
64
+ "max_tokens": 1024, "context_limit": 65536}
65
+ calls = []
66
+
67
+ def fake_stream(model, system, messages, tool_schemas, config):
68
+ calls.append(1)
69
+ raise RuntimeError(VLLM_ERR)
70
+ yield # pragma: no cover
71
+
72
+ monkeypatch.setattr(agent, "stream", fake_stream)
73
+ monkeypatch.setattr(providers, "_TEXT_ONLY", set())
74
+ try:
75
+ list(agent.run("hi", AgentState(), cfg, "SYS"))
76
+ except RuntimeError:
77
+ pass
78
+ assert len(calls) == 2
@@ -150,3 +150,14 @@ def test_extract_thinking_noop_when_no_marker():
150
150
  thinking, text = extract_thinking(raw)
151
151
  assert thinking == ""
152
152
  assert text == raw
153
+
154
+
155
+ def test_split_think_tags_template_opened():
156
+ from drydock.tuning import split_think_tags
157
+ assert split_think_tags("plan it\n</think>\n74 done") == ("plan it", "74 done")
158
+ assert split_think_tags("<think>x</think>ok") == ("x", "ok")
159
+ assert split_think_tags("no tags") == ("", "no tags")
160
+
161
+
162
+ def test_extract_thinking_handles_think_tags():
163
+ assert extract_thinking("reason</think>answer") == ("reason", "answer")
File without changes
File without changes
File without changes
File without changes