drydock-cli 3.1.7__tar.gz → 3.1.8__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 (192) hide show
  1. {drydock_cli-3.1.7/drydock_cli.egg-info → drydock_cli-3.1.8}/PKG-INFO +5 -3
  2. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/README.md +4 -2
  3. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/ratchet.py +89 -24
  4. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/tui/app.py +30 -16
  5. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/tui/widgets.py +100 -8
  6. {drydock_cli-3.1.7 → drydock_cli-3.1.8/drydock_cli.egg-info}/PKG-INFO +5 -3
  7. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/pyproject.toml +1 -1
  8. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_events.py +1 -1
  9. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_ratchet.py +64 -9
  10. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_task_state.py +1 -1
  11. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_tui.py +79 -0
  12. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_verification_gate.py +3 -3
  13. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/LICENSE +0 -0
  14. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/NOTICE +0 -0
  15. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/__init__.py +0 -0
  16. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/__main__.py +0 -0
  17. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/advisor.py +0 -0
  18. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/agent.py +0 -0
  19. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/bash_safety.py +0 -0
  20. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/budget.py +0 -0
  21. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/__init__.py +0 -0
  22. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/document-canvas.md +0 -0
  23. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/fiar-assess.md +0 -0
  24. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/fiar-cap.md +0 -0
  25. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/fiar-evidence.md +0 -0
  26. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/fiar-readiness.md +0 -0
  27. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/ml-data.md +0 -0
  28. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/ml-debug.md +0 -0
  29. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/ml-finetune.md +0 -0
  30. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/ml-metrics.md +0 -0
  31. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/ml-rl.md +0 -0
  32. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/ml-train.md +0 -0
  33. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/nist-ai-rmf.md +0 -0
  34. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/nist-csf.md +0 -0
  35. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/rmf-categorize.md +0 -0
  36. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/rmf-control.md +0 -0
  37. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/rmf-poam.md +0 -0
  38. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/rmf-review.md +0 -0
  39. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/stig-assess.md +0 -0
  40. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/stig-remediate.md +0 -0
  41. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/cci.py +0 -0
  42. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/cli.py +0 -0
  43. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/compaction.py +0 -0
  44. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/config.py +0 -0
  45. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/detect.py +0 -0
  46. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/doccanvas.py +0 -0
  47. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/events.py +0 -0
  48. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/extract.py +0 -0
  49. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/fiar.py +0 -0
  50. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/gittools.py +0 -0
  51. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/graphrag.py +0 -0
  52. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/guards.py +0 -0
  53. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/loop_detect.py +0 -0
  54. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/mcp.py +0 -0
  55. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/phases.py +0 -0
  56. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/poam.py +0 -0
  57. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/progress.py +0 -0
  58. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/providers.py +0 -0
  59. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/recipes.py +0 -0
  60. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/recovery.py +0 -0
  61. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/resume.py +0 -0
  62. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/rmf.py +0 -0
  63. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/rmf_graph.py +0 -0
  64. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/skills.py +0 -0
  65. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/stig.py +0 -0
  66. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/subagents.py +0 -0
  67. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/suggest.py +0 -0
  68. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/task_state.py +0 -0
  69. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/tool_policy.py +0 -0
  70. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/tool_registry.py +0 -0
  71. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/tool_result.py +0 -0
  72. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/tool_select.py +0 -0
  73. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/tool_validate.py +0 -0
  74. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/tools/__init__.py +0 -0
  75. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/trajectory.py +0 -0
  76. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/tui/__init__.py +0 -0
  77. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/tui/approval.py +0 -0
  78. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/tui/messages.py +0 -0
  79. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/tuning.py +0 -0
  80. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/verification.py +0 -0
  81. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/web.py +0 -0
  82. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock_cli.egg-info/SOURCES.txt +0 -0
  83. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock_cli.egg-info/dependency_links.txt +0 -0
  84. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock_cli.egg-info/entry_points.txt +0 -0
  85. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock_cli.egg-info/requires.txt +0 -0
  86. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock_cli.egg-info/top_level.txt +0 -0
  87. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/setup.cfg +0 -0
  88. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_advisor.py +0 -0
  89. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_approval.py +0 -0
  90. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_back_command.py +0 -0
  91. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_bash_background.py +0 -0
  92. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_bash_binary_output.py +0 -0
  93. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_bash_crossplatform.py +0 -0
  94. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_bash_output_bounding.py +0 -0
  95. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_bash_process_group.py +0 -0
  96. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_bash_safety.py +0 -0
  97. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_bash_sanitize.py +0 -0
  98. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_bash_shell.py +0 -0
  99. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_bash_stdin.py +0 -0
  100. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_bash_stop_partial.py +0 -0
  101. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_bash_timeout_network.py +0 -0
  102. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_bash_timeout_param.py +0 -0
  103. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_budget.py +0 -0
  104. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_cci.py +0 -0
  105. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_cli_agents.py +0 -0
  106. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_compact_command.py +0 -0
  107. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_compaction.py +0 -0
  108. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_config.py +0 -0
  109. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_config_migration.py +0 -0
  110. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_context_limit_config.py +0 -0
  111. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_context_limit_issue25.py +0 -0
  112. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_criteria_coverage.py +0 -0
  113. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_cycling_detection.py +0 -0
  114. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_degenerate_argument.py +0 -0
  115. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_detect.py +0 -0
  116. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_dispatch.py +0 -0
  117. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_doccanvas.py +0 -0
  118. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_e2e_connected.py +0 -0
  119. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_edit_replace_all.py +0 -0
  120. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_effort_governor.py +0 -0
  121. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_empty_response.py +0 -0
  122. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_extract.py +0 -0
  123. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_failure_loop.py +0 -0
  124. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_fiar.py +0 -0
  125. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_first_run_setup.py +0 -0
  126. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_gittools.py +0 -0
  127. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_glob_edit_edges.py +0 -0
  128. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_graphify_example.py +0 -0
  129. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_graphrag.py +0 -0
  130. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_graphrag_quoted_path.py +0 -0
  131. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_graphrag_sqlite.py +0 -0
  132. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_grep_and_read_robust.py +0 -0
  133. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_guards_and_tools.py +0 -0
  134. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_hallucinated_tools.py +0 -0
  135. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_leaked_tool_call.py +0 -0
  136. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_loop_detect.py +0 -0
  137. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_mcp.py +0 -0
  138. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_model_registry.py +0 -0
  139. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_oneshot_unreachable.py +0 -0
  140. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_overthink_interrupt.py +0 -0
  141. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_phases.py +0 -0
  142. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_plan_autocontinue.py +0 -0
  143. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_poam.py +0 -0
  144. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_progress.py +0 -0
  145. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_providers_unreachable.py +0 -0
  146. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_read_index.py +0 -0
  147. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_recipes.py +0 -0
  148. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_recovery.py +0 -0
  149. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_recovery_config.py +0 -0
  150. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_recovery_integration.py +0 -0
  151. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_repeated_outcome.py +0 -0
  152. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_repetition_interrupt.py +0 -0
  153. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_resume_events.py +0 -0
  154. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_resume_restore.py +0 -0
  155. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_resume_snapshot.py +0 -0
  156. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_rmf.py +0 -0
  157. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_rmf_graph.py +0 -0
  158. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_rmf_stig_graph.py +0 -0
  159. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_rolling_plan.py +0 -0
  160. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_runaway_repetition.py +0 -0
  161. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_screenshot.py +0 -0
  162. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_server_probe.py +0 -0
  163. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_skills.py +0 -0
  164. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_sqlite_events.py +0 -0
  165. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_stall_retry.py +0 -0
  166. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_stig.py +0 -0
  167. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_stop.py +0 -0
  168. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_streaming_newlines.py +0 -0
  169. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_subagent.py +0 -0
  170. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_subagents.py +0 -0
  171. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_suggest.py +0 -0
  172. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_system_prompt_help.py +0 -0
  173. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_timeline.py +0 -0
  174. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_todo.py +0 -0
  175. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_tool_arg_coercion.py +0 -0
  176. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_tool_arg_coercion_more.py +0 -0
  177. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_tool_arg_parsing.py +0 -0
  178. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_tool_canonical.py +0 -0
  179. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_tool_policy.py +0 -0
  180. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_tool_result.py +0 -0
  181. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_tool_select.py +0 -0
  182. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_tool_validate.py +0 -0
  183. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_tools_undo.py +0 -0
  184. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_trajectory.py +0 -0
  185. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_tuning.py +0 -0
  186. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_viewimage.py +0 -0
  187. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_vision_input.py +0 -0
  188. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_web_tools.py +0 -0
  189. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_windows_shell.py +0 -0
  190. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_worker_subagent.py +0 -0
  191. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_write_content_coerce.py +0 -0
  192. {drydock_cli-3.1.7 → drydock_cli-3.1.8}/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.7
3
+ Version: 3.1.8
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
@@ -115,7 +115,9 @@ A full agentic CLI harness — every tool below is clean-room and dependency-fre
115
115
  - **Ratchet** — `/ratchet <goal>` keeps solving across rounds, snapshotting the
116
116
  workspace whenever more tests pass and rolling back regressions, until it goes
117
117
  green. The verifier auto-detects (pytest/cargo/go/npm/make); `--verify "<cmd>"`
118
- overrides. Progress can't slip backward.
118
+ overrides. Progress can't slip backward. **`--effort low|medium|high|xhigh|max`**
119
+ is one dial from a cheap plain pawl (low) to full evolutionary search — fan-out
120
+ and crossover of partial solutions — for the hardest tasks (high+).
119
121
 
120
122
  ## Slash commands
121
123
 
@@ -133,7 +135,7 @@ Typed into the prompt. The agent also knows these, so you can just **ask it**
133
135
  | `/skills new <name> <prompt>` | Create a reusable `/<name>` skill (use `$ARGS` for input) |
134
136
  | `/<name>` | Run a skill |
135
137
  | `/loop <count> <prompt>` | Repeat a prompt N times (Esc stops) |
136
- | `/ratchet <goal>` | Solve across rounds, snapshotting on verifier gains, rolling back regressions. Verifier auto-detects; override with `--verify "<cmd>"` (`--rounds N`, `--fitness auto\|exitcode\|<regex>`) |
138
+ | `/ratchet <goal>` | Solve across rounds, snapshotting on verifier gains, rolling back regressions. `--effort low..max` scales plain-pawl→evolutionary; verifier auto-detects (`--verify "<cmd>"` to override; `--rounds N`, `--fitness auto\|exitcode\|<regex>`) |
137
139
  | `/mcp` | List connected MCP servers + their tools |
138
140
  | `/rmf bootstrap [families]` | Ingest the NIST SP 800-53 catalog (RMF automation) |
139
141
  | `/rmf-control` · `/rmf-categorize` · `/rmf-review` · `/rmf-poam` | Bundled RMF skills |
@@ -91,7 +91,9 @@ A full agentic CLI harness — every tool below is clean-room and dependency-fre
91
91
  - **Ratchet** — `/ratchet <goal>` keeps solving across rounds, snapshotting the
92
92
  workspace whenever more tests pass and rolling back regressions, until it goes
93
93
  green. The verifier auto-detects (pytest/cargo/go/npm/make); `--verify "<cmd>"`
94
- overrides. Progress can't slip backward.
94
+ overrides. Progress can't slip backward. **`--effort low|medium|high|xhigh|max`**
95
+ is one dial from a cheap plain pawl (low) to full evolutionary search — fan-out
96
+ and crossover of partial solutions — for the hardest tasks (high+).
95
97
 
96
98
  ## Slash commands
97
99
 
@@ -109,7 +111,7 @@ Typed into the prompt. The agent also knows these, so you can just **ask it**
109
111
  | `/skills new <name> <prompt>` | Create a reusable `/<name>` skill (use `$ARGS` for input) |
110
112
  | `/<name>` | Run a skill |
111
113
  | `/loop <count> <prompt>` | Repeat a prompt N times (Esc stops) |
112
- | `/ratchet <goal>` | Solve across rounds, snapshotting on verifier gains, rolling back regressions. Verifier auto-detects; override with `--verify "<cmd>"` (`--rounds N`, `--fitness auto\|exitcode\|<regex>`) |
114
+ | `/ratchet <goal>` | Solve across rounds, snapshotting on verifier gains, rolling back regressions. `--effort low..max` scales plain-pawl→evolutionary; verifier auto-detects (`--verify "<cmd>"` to override; `--rounds N`, `--fitness auto\|exitcode\|<regex>`) |
113
115
  | `/mcp` | List connected MCP servers + their tools |
114
116
  | `/rmf bootstrap [families]` | Ingest the NIST SP 800-53 catalog (RMF automation) |
115
117
  | `/rmf-control` · `/rmf-categorize` · `/rmf-review` · `/rmf-poam` | Bundled RMF skills |
@@ -51,8 +51,10 @@ def score_output(output: str, mode: str, returncode: int) -> tuple[int, int]:
51
51
  if mt:
52
52
  total = int(mt.group(1))
53
53
  else:
54
- failed = int(_FAILED.search(output).group(1)) if _FAILED.search(output) else 0
55
- errors = int(_ERRORS.search(output).group(1)) if _ERRORS.search(output) else 0
54
+ mf = _FAILED.search(output)
55
+ me = _ERRORS.search(output)
56
+ failed = int(mf.group(1)) if mf else 0
57
+ errors = int(me.group(1)) if me else 0
56
58
  total = passed + failed + errors
57
59
  return passed, max(total, passed)
58
60
  # nothing recognizable → treat as all-or-nothing on the exit code
@@ -271,11 +273,12 @@ def detect_verifier(cwd: str) -> tuple[str, str] | None:
271
273
  return None
272
274
 
273
275
 
274
- def parse_ratchet_args(arg: str) -> tuple[str, str, int, str]:
275
- """Parse `/ratchet <goal…> [--verify <cmd>] [--rounds N] [--fitness M]`.
276
- Returns (goal, verify_cmd, rounds, fitness_mode); verify_cmd and fitness_mode
277
- are "" when unspecified (the caller auto-detects). Raises ValueError on misuse.
278
- The --verify value must be a single (quoted) shell token."""
276
+ def parse_ratchet_args(arg: str) -> tuple[str, str, int, str, str]:
277
+ """Parse `/ratchet <goal…> [--verify <cmd>] [--effort L] [--rounds N] [--fitness M]`.
278
+ Returns (goal, verify_cmd, rounds, fitness_mode, effort). rounds is 0 when
279
+ unspecified (the caller derives it from effort); verify_cmd/fitness_mode/effort
280
+ are "" when unspecified. --effort ∈ low|medium|high|xhigh|max scales the whole
281
+ thing from a plain pawl (low) to full evolutionary search (high+)."""
279
282
  import shlex
280
283
 
281
284
  try:
@@ -284,8 +287,9 @@ def parse_ratchet_args(arg: str) -> tuple[str, str, int, str]:
284
287
  raise ValueError(f"could not parse arguments: {e}") from e
285
288
  goal: list[str] = []
286
289
  verify = ""
287
- rounds = 6
290
+ rounds = 0
288
291
  fitness = ""
292
+ effort = ""
289
293
  seen_flag = False
290
294
  i = 0
291
295
  while i < len(toks):
@@ -301,7 +305,13 @@ def parse_ratchet_args(arg: str) -> tuple[str, str, int, str]:
301
305
  i += 1
302
306
  if i >= len(toks) or not toks[i].isdigit():
303
307
  raise ValueError("--rounds needs a number, e.g. --rounds 6")
304
- rounds = max(1, min(int(toks[i]), 20))
308
+ rounds = max(1, min(int(toks[i]), 30))
309
+ elif t == "--effort":
310
+ seen_flag = True
311
+ i += 1
312
+ if i >= len(toks) or toks[i] not in EFFORT_LEVELS:
313
+ raise ValueError(f"--effort must be one of {'|'.join(EFFORT_LEVELS)}")
314
+ effort = toks[i]
305
315
  elif t == "--fitness":
306
316
  seen_flag = True
307
317
  i += 1
@@ -315,7 +325,7 @@ def parse_ratchet_args(arg: str) -> tuple[str, str, int, str]:
315
325
  i += 1
316
326
  if not goal:
317
327
  raise ValueError("no goal given")
318
- return " ".join(goal), verify, rounds, fitness
328
+ return " ".join(goal), verify, rounds, fitness, effort
319
329
 
320
330
 
321
331
  # ═══════════════════════════════════════════════════════════════════════════════
@@ -452,7 +462,9 @@ def plan_crossover(a: Candidate, b: Candidate, gen: int) -> dict | None:
452
462
  return {
453
463
  "base": base.snapshot, # start from the stronger parent
454
464
  "donor": donor.snapshot, # graft the missing capabilities
455
- "wants": sorted(donor.descriptor - base.descriptor), # checks to import
465
+ # descriptors are guaranteed non-empty by the guard above; the `or`
466
+ # keeps the type checker happy across the base/donor reassignment.
467
+ "wants": sorted((donor.descriptor or frozenset()) - (base.descriptor or frozenset())), # checks to import
456
468
  "target": sorted(union),
457
469
  "parents": (a.id, b.id),
458
470
  "generation": gen,
@@ -468,9 +480,14 @@ class VariationPolicy:
468
480
 
469
481
  LADDER = ("exploit", "diversify", "fanout", "crossover", "restart")
470
482
 
471
- def __init__(self, patience: int = 1, fanout: int = 3):
483
+ def __init__(self, patience: int = 2, fanout: int = 3, ladder: tuple | None = None):
484
+ # patience default 2: give the steady pawl a couple of rounds to grind
485
+ # before spending compute on exploration (patience=1 escalated too fast).
486
+ # `ladder` sets how FAR the policy may escalate — the effort dial truncates
487
+ # it (low = ("exploit",) → a pure hill-climber == the plain ratchet).
472
488
  self.patience = max(1, patience) # stalls tolerated before escalating
473
- self.fanout = fanout
489
+ self.fanout = max(1, fanout)
490
+ self.ladder = tuple(ladder) if ladder else self.LADDER
474
491
  self.stall = 0
475
492
  self.rung = 0
476
493
 
@@ -483,21 +500,69 @@ class VariationPolicy:
483
500
  self.stall += 1
484
501
  if self.stall >= self.patience:
485
502
  self.stall = 0
486
- self.rung = min(self.rung + 1, len(self.LADDER) - 1)
487
- op = self.LADDER[self.rung]
503
+ self.rung = min(self.rung + 1, len(self.ladder) - 1)
504
+ op = self.ladder[self.rung]
488
505
  if op == "crossover" and not have_crossover:
489
- op = "restart" # nothing to recombine → jump to restarting from an elite
506
+ op = "restart" if "restart" in self.ladder else "exploit"
490
507
  return op
491
508
 
509
+ def variant_specs(self, op: str) -> list[dict]:
510
+ """Per-variant (mode, temperature) attempts for one round. ELITISM: the
511
+ FIRST variant is ALWAYS the steady low-temp exploit move, so a round can
512
+ never score below the plain pawl; any extra variants only EXPLORE on top.
513
+ The driver runs them from the same best snapshot and keeps the best — which
514
+ makes the eratchet a strict superset of the plain ratchet."""
515
+ exploit = {"mode": "continue", "temperature": 0.2}
516
+ # fan-out width scales with self.fanout (the effort dial): fanout total
517
+ # attempts = 1 exploit + (fanout-1) explorers.
518
+ pool = [{"mode": "continue", "temperature": 0.5},
519
+ {"mode": "rethink", "temperature": 0.7},
520
+ {"mode": "continue", "temperature": 0.6},
521
+ {"mode": "rethink", "temperature": 0.85}]
522
+ fan = [(pool * 4)[i] for i in range(max(0, self.fanout - 1))]
523
+ extra = {
524
+ "exploit": [],
525
+ "diversify": [{"mode": "rethink", "temperature": 0.6}],
526
+ "fanout": fan,
527
+ "crossover": [{"mode": "crossover", "temperature": 0.4}],
528
+ "restart": [{"mode": "restart", "temperature": 0.8}],
529
+ }.get(op, [])
530
+ return [exploit, *extra]
531
+
492
532
  def params_for(self, op: str) -> dict:
493
- """Concrete knobs the driver applies for an operator (temperature, λ, prompt)."""
494
- return {
495
- "exploit": {"temperature": 0.2, "variants": 1, "mode": "continue"},
496
- "diversify": {"temperature": 0.9, "variants": 1, "mode": "rethink"},
497
- "fanout": {"temperature": 0.8, "variants": self.fanout, "mode": "continue"},
498
- "crossover": {"temperature": 0.4, "variants": 1, "mode": "crossover"},
499
- "restart": {"temperature": 1.0, "variants": 1, "mode": "restart"},
500
- }[op]
533
+ """Knobs the driver applies: per-variant specs (exploit-first), variant
534
+ count, and the primary mode (the exploit move)."""
535
+ specs = self.variant_specs(op)
536
+ return {"variants": len(specs), "specs": specs, "mode": specs[0]["mode"]}
537
+
538
+
539
+ # ── the effort spectrum (one dial: plain pawl → full evolutionary) ──
540
+
541
+ _FULL_LADDER = ("exploit", "diversify", "fanout", "crossover", "restart")
542
+
543
+ # low = a pure hill-climber (the plain ratchet); each step up spends more compute
544
+ # and unlocks more of the evolutionary ladder. `abort_flat` = rounds to tolerate a
545
+ # 0/N no-gradient task before bailing (0 = never bail).
546
+ _EFFORT = {
547
+ "low": {"max_rounds": 3, "patience": 2, "fanout": 1, "ladder": ("exploit",), "abort_flat": 1},
548
+ "medium": {"max_rounds": 5, "patience": 2, "fanout": 2, "ladder": ("exploit", "diversify"), "abort_flat": 2},
549
+ "high": {"max_rounds": 8, "patience": 2, "fanout": 3, "ladder": _FULL_LADDER, "abort_flat": 3},
550
+ "xhigh": {"max_rounds": 12, "patience": 3, "fanout": 4, "ladder": _FULL_LADDER, "abort_flat": 4},
551
+ "max": {"max_rounds": 16, "patience": 3, "fanout": 5, "ladder": _FULL_LADDER, "abort_flat": 0},
552
+ }
553
+ EFFORT_LEVELS = tuple(_EFFORT)
554
+
555
+
556
+ def effort_profile(level: str) -> dict:
557
+ """Map an effort level (low|medium|high|xhigh|max) to ratchet knobs. Unknown →
558
+ 'high'. `low` is exactly the plain pawl (exploit-only, no exploration)."""
559
+ return dict(_EFFORT.get(level, _EFFORT["high"]))
560
+
561
+
562
+ def policy_for(level: str) -> VariationPolicy:
563
+ """A VariationPolicy pre-configured for an effort level."""
564
+ p = effort_profile(level)
565
+ return VariationPolicy(patience=p["patience"], fanout=p["fanout"], ladder=p["ladder"])
501
566
 
502
567
 
503
568
  def diversify_prompt(goal: str, passed: int, total: int, op: str) -> str:
@@ -37,7 +37,6 @@ from drydock.tui.messages import (
37
37
  AgentTurnDone,
38
38
  )
39
39
  from drydock.tui.widgets import (
40
- SLASH_COMMANDS,
41
40
  AssistantMessage,
42
41
  ErrorMessage,
43
42
  PromptArea,
@@ -45,6 +44,7 @@ from drydock.tui.widgets import (
45
44
  ToolCard,
46
45
  UserMessage,
47
46
  result_is_ok,
47
+ slash_menu,
48
48
  summarize_inputs,
49
49
  )
50
50
  from drydock.tuning import system_prompt_for_model
@@ -128,6 +128,11 @@ class DrydockApp(App):
128
128
  }
129
129
  /* Dimmed recommended-next-command hint (empty → 0 lines). */
130
130
  #suggest { height: auto; margin: 0 3; color: #4a6b78; text-style: italic; }
131
+ /* As-you-type slash-command discovery menu (height auto → 0 lines when empty). */
132
+ #slashmenu {
133
+ height: auto; max-height: 16; overflow-y: auto; margin: 0 2;
134
+ padding: 0 1; color: #8fb2c0; background: #0e2731; border-left: thick #2e5a6b;
135
+ }
131
136
  /* One bottom-docked footer holds, top→bottom: working-line, prompt, status. */
132
137
  #footer { dock: bottom; height: auto; background: #0b1f2a; }
133
138
  #prompt {
@@ -350,6 +355,7 @@ class DrydockApp(App):
350
355
  yield Static("", id="todo") # pinned task checklist (empty = hidden)
351
356
  yield Static("", id="working") # in-line activity (empty when idle)
352
357
  yield Static("", id="suggest") # dimmed recommended-next-command hint
358
+ yield Static("", id="slashmenu") # as-you-type slash-command menu (empty = hidden)
353
359
  yield PromptArea(id="prompt")
354
360
  yield Static(self._status_text(), id="status")
355
361
 
@@ -457,21 +463,23 @@ class DrydockApp(App):
457
463
  # ── input ─────────────────────────────────────────────────────────────
458
464
 
459
465
  def on_text_area_changed(self, event) -> None:
460
- """As the user types a bare slash command, surface the matching commands
461
- in the (idle) activity line — '/m' shows '/model'. Tab completes them."""
466
+ """As the user types a slash command, surface a discovery menu in the
467
+ #slashmenu line: the matching commands with their args + one-line help
468
+ while typing the name ('/' lists everything), then the chosen command's
469
+ switches once it's settled. Tab completes, Enter runs. Rendered in a
470
+ dedicated widget so the ghost next-command suggestion is never touched."""
471
+ try:
472
+ menu = self.query_one("#slashmenu", Static)
473
+ except Exception: # noqa: BLE001 — widget not mounted yet
474
+ return
462
475
  if self._busy:
463
- return # #working shows live activity while a turn is running
476
+ menu.update("") # a turn is running; nothing to discover
477
+ return
464
478
  try:
465
479
  text = self.query_one("#prompt", PromptArea).text
466
- except Exception: # noqa: BLE001 — widget not mounted yet
480
+ except Exception: # noqa: BLE001
467
481
  return
468
- hint = ""
469
- if text.startswith("/") and " " not in text and "\n" not in text:
470
- commands = SLASH_COMMANDS + ["/skills"] + [f"/{n}" for n in sorted(self._skills)]
471
- matches = [c for c in commands if c.startswith(text.lower())]
472
- if matches:
473
- hint = " " + " ".join(matches) + " · Tab to complete"
474
- self.query_one("#working", Static).update(hint)
482
+ menu.update(slash_menu(text, skills=tuple(self._skills)))
475
483
 
476
484
  def on_prompt_area_submitted(self, event: PromptArea.Submitted) -> None:
477
485
  text = event.text.strip()
@@ -911,15 +919,21 @@ class DrydockApp(App):
911
919
  from drydock import ratchet as rmod
912
920
 
913
921
  try:
914
- goal, verify, rounds, fitness = rmod.parse_ratchet_args(arg)
922
+ goal, verify, rounds, fitness, effort = rmod.parse_ratchet_args(arg)
915
923
  except ValueError as e:
916
924
  self._info(
917
925
  f"{e}\n"
918
- "usage: /ratchet <goal> [--verify \"<cmd>\"] [--rounds N] [--fitness auto|exitcode|<regex>]\n"
919
- " e.g. /ratchet make the failing suite pass (verifier auto-detected)\n"
920
- ' /ratchet fix the parser --verify "npm test" (explicit override)'
926
+ "usage: /ratchet <goal> [--effort low|medium|high|xhigh|max] [--verify \"<cmd>\"] "
927
+ "[--rounds N] [--fitness auto|exitcode|<regex>]\n"
928
+ " effort scales it from a plain pawl (low) to full evolutionary search (high+)\n"
929
+ " e.g. /ratchet make the failing suite pass (verifier auto-detected)\n"
930
+ ' /ratchet crack this hard bug --effort high (more rounds + exploration)'
921
931
  )
922
932
  return
933
+ # effort sets the round budget unless --rounds was given explicitly
934
+ prof = rmod.effort_profile(effort or "medium")
935
+ if not rounds:
936
+ rounds = prof["max_rounds"]
923
937
  if self._busy:
924
938
  self._info("A turn is already running — stop it (Esc) before starting a ratchet.")
925
939
  return
@@ -2,6 +2,7 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import re
5
+ from dataclasses import dataclass
5
6
  from pathlib import Path
6
7
 
7
8
  from textual.message import Message
@@ -148,16 +149,107 @@ class PromptHistory:
148
149
  return self._draft
149
150
 
150
151
 
151
- # The slash commands offered for completion (typing "/m" → /model). Keep in
152
- # sync with app._handle_slash. Shown as a hint as the user types, and Tab
153
- # completes the prefix.
154
- SLASH_COMMANDS = [
155
- "/help", "/model", "/cwd", "/undo", "/back", "/stop", "/status",
156
- "/compact", "/context", "/shell", "/events", "/trace", "/resume", "/advisor", "/ask", "/ask!", "/graphrag", "/skills",
157
- "/loop", "/mcp", "/rmf", "/stig", "/doc",
158
- "/clear", "/quit",
152
+ # Single source of truth for the built-in slash commands. Keep in sync with
153
+ # app._handle_slash. Drives three things: Tab completion (SLASH_COMMANDS, below),
154
+ # the as-you-type discovery menu (slash_menu, below), and — by convention — /help.
155
+ # `args` is the short inline hint shown next to the name; `switches` is the
156
+ # optional second line detailing sub-commands/flags, revealed once a command is
157
+ # chosen so the user can see what it takes.
158
+ @dataclass(frozen=True)
159
+ class SlashSpec:
160
+ name: str
161
+ args: str = ""
162
+ help: str = ""
163
+ switches: str = ""
164
+
165
+
166
+ SLASH_SPECS: list[SlashSpec] = [
167
+ SlashSpec("/help", "", "list every command"),
168
+ SlashSpec("/model", "[name]", "list or switch models; each routes to its endpoint",
169
+ "/model <name> · add <name> <url> [prov] · default <name> · url <url>"),
170
+ SlashSpec("/cwd", "[path]", "show or change the working directory"),
171
+ SlashSpec("/undo", "", "revert the last file write/edit"),
172
+ SlashSpec("/back", "", "rewind the last turn from the model's context"),
173
+ SlashSpec("/stop", "", "stop the running turn (or press Esc)"),
174
+ SlashSpec("/status", "", "session model, cwd, turns, tokens"),
175
+ SlashSpec("/compact", "", "shrink old context to free up the window"),
176
+ SlashSpec("/context", "[tokens]", "view/set the context-window budget", "/context 65536"),
177
+ SlashSpec("/shell", "", "show which shell the Bash tool runs commands through"),
178
+ SlashSpec("/events", "", "digest of this session's execution trace + governor activity"),
179
+ SlashSpec("/trace", "[N]", "ordered event timeline (last N)"),
180
+ SlashSpec("/resume", "[session_id]", "continue an interrupted session"),
181
+ SlashSpec("/advisor", "", "set up a 2nd 'advisor' model (Gemini etc.)"),
182
+ SlashSpec("/ask", "<q>", "ask the advisor a question (answer shown to you)"),
183
+ SlashSpec("/ask!", "<q>", "ask the advisor and feed the answer to the agent"),
184
+ SlashSpec("/graphrag", "<sub>", "ingest docs into a knowledge base the agent can search",
185
+ "build <path> · add <path> · query <q> · status · clear"),
186
+ SlashSpec("/doc", "<sub>", "drive the Document Canvas tools by hand",
187
+ "open <path> · new <name> ..."),
188
+ SlashSpec("/skills", "[new <name> <prompt>]", "list skills, or create a reusable one"),
189
+ SlashSpec("/loop", "<count> <prompt>", "repeat a prompt N times (Esc stops)"),
190
+ SlashSpec("/ratchet", "<goal>", "persist verified progress across rounds until tests pass",
191
+ '--verify "<cmd>" to override the auto-detected verifier'),
192
+ SlashSpec("/mcp", "", "list connected MCP servers and their tools"),
193
+ SlashSpec("/rmf", "<sub>", "RMF automation", "bootstrap · control · ..."),
194
+ SlashSpec("/stig", "<sub>", "STIG checklist tooling", "new <xccdf> · summarize · assess"),
195
+ SlashSpec("/clear", "", "reset the conversation"),
196
+ SlashSpec("/quit", "", "exit drydock"),
159
197
  ]
160
198
 
199
+ # Flat name list kept for Tab completion (typing "/m" → /model), derived so the
200
+ # spec table above stays the one place commands are declared.
201
+ SLASH_COMMANDS = [s.name for s in SLASH_SPECS]
202
+
203
+ _MENU_MAX_ROWS = 14 # cap the list view so a bare "/" doesn't fill the footer
204
+
205
+
206
+ def slash_menu(text: str, skills: tuple[str, ...] = ()) -> str:
207
+ """As-you-type discovery menu for a slash command in progress.
208
+
209
+ Returns a formatted, multi-line block for the footer, or "" when the input
210
+ isn't a bare slash command (so a normal message or a "/path/like/this" leaves
211
+ the menu hidden). Two views:
212
+
213
+ • list — while typing the NAME ("/", "/mo"): every matching command with
214
+ its args + one-line help, so the user can see what's available.
215
+ • detail — once a command is chosen (fully typed, or a space started its
216
+ args): that command's usage + switches, so the flags are discoverable too.
217
+
218
+ Pure logic (no Textual) so it's unit-testable; the TUI renders the string into
219
+ the #slashmenu Static. `skills` are the user's custom commands, appended live.
220
+ """
221
+ if not text.startswith("/") or "\n" in text:
222
+ return ""
223
+ first, _, _rest = text.partition(" ")
224
+ head = first.lower()
225
+ has_args = " " in text
226
+ specs = list(SLASH_SPECS) + [
227
+ SlashSpec(f"/{n}", "", "custom skill") for n in sorted(skills)
228
+ if f"/{n}" not in {s.name for s in SLASH_SPECS}
229
+ ]
230
+ exact = next((s for s in specs if s.name == head), None)
231
+ matches = [s for s in specs if s.name.startswith(head)]
232
+
233
+ # detail view: the command is settled (space-started args, or the sole full match)
234
+ if exact and (has_args or matches == [exact]):
235
+ usage = exact.name + (f" {exact.args}" if exact.args else "")
236
+ lines = [f" {usage} — {exact.help}" if exact.help else f" {usage}"]
237
+ if exact.switches:
238
+ lines.append(f" {exact.switches}")
239
+ return "\n".join(lines)
240
+
241
+ if not matches:
242
+ return ""
243
+ rows = []
244
+ for s in matches[:_MENU_MAX_ROWS]:
245
+ label = s.name + (f" {s.args}" if s.args else "")
246
+ rows.append(f" {label:<24}{s.help}" if s.help else f" {label}")
247
+ extra = len(matches) - _MENU_MAX_ROWS
248
+ if extra > 0:
249
+ rows.append(f" … +{extra} more — keep typing to filter")
250
+ rows.append(" Tab completes · Enter runs")
251
+ return "\n".join(rows)
252
+
161
253
 
162
254
  def _common_prefix(strings: list[str]) -> str:
163
255
  if not strings:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.1.7
3
+ Version: 3.1.8
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
@@ -115,7 +115,9 @@ A full agentic CLI harness — every tool below is clean-room and dependency-fre
115
115
  - **Ratchet** — `/ratchet <goal>` keeps solving across rounds, snapshotting the
116
116
  workspace whenever more tests pass and rolling back regressions, until it goes
117
117
  green. The verifier auto-detects (pytest/cargo/go/npm/make); `--verify "<cmd>"`
118
- overrides. Progress can't slip backward.
118
+ overrides. Progress can't slip backward. **`--effort low|medium|high|xhigh|max`**
119
+ is one dial from a cheap plain pawl (low) to full evolutionary search — fan-out
120
+ and crossover of partial solutions — for the hardest tasks (high+).
119
121
 
120
122
  ## Slash commands
121
123
 
@@ -133,7 +135,7 @@ Typed into the prompt. The agent also knows these, so you can just **ask it**
133
135
  | `/skills new <name> <prompt>` | Create a reusable `/<name>` skill (use `$ARGS` for input) |
134
136
  | `/<name>` | Run a skill |
135
137
  | `/loop <count> <prompt>` | Repeat a prompt N times (Esc stops) |
136
- | `/ratchet <goal>` | Solve across rounds, snapshotting on verifier gains, rolling back regressions. Verifier auto-detects; override with `--verify "<cmd>"` (`--rounds N`, `--fitness auto\|exitcode\|<regex>`) |
138
+ | `/ratchet <goal>` | Solve across rounds, snapshotting on verifier gains, rolling back regressions. `--effort low..max` scales plain-pawl→evolutionary; verifier auto-detects (`--verify "<cmd>"` to override; `--rounds N`, `--fitness auto\|exitcode\|<regex>`) |
137
139
  | `/mcp` | List connected MCP servers + their tools |
138
140
  | `/rmf bootstrap [families]` | Ingest the NIST SP 800-53 catalog (RMF automation) |
139
141
  | `/rmf-control` · `/rmf-categorize` · `/rmf-review` · `/rmf-poam` | Bundled RMF skills |
@@ -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.7"
10
+ version = "3.1.8"
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"
@@ -35,7 +35,7 @@ def test_run_writes_full_trace():
35
35
  turns = [
36
36
  AssistantTurn("", [{"id": "1", "name": "Write",
37
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),
38
+ AssistantTurn("", [{"id": "2", "name": "Bash", "input": {"command": "python3 -c '1'"}}], 8, 4),
39
39
  AssistantTurn("done, verified", [], 6, 3),
40
40
  ]
41
41
  i = {"n": 0}
@@ -132,23 +132,23 @@ def test_git_checkpoint_unavailable_outside_repo(tmp_path):
132
132
  # ───────────────────────── arg parsing ─────────────────────────
133
133
 
134
134
  def test_parse_basic():
135
- goal, verify, rounds, fitness = parse_ratchet_args(
136
- 'make the suite pass --verify "pytest -q" --rounds 8 --fitness auto')
135
+ goal, verify, rounds, fitness, effort = parse_ratchet_args(
136
+ 'make the suite pass --verify "pytest -q" --rounds 8 --fitness auto --effort high')
137
137
  assert goal == "make the suite pass"
138
138
  assert verify == "pytest -q"
139
- assert rounds == 8 and fitness == "auto"
139
+ assert rounds == 8 and fitness == "auto" and effort == "high"
140
140
 
141
141
 
142
142
  def test_parse_defaults():
143
- goal, verify, rounds, fitness = parse_ratchet_args('fix it --verify "go test ./..."')
143
+ goal, verify, rounds, fitness, effort = parse_ratchet_args('fix it --verify "go test ./..."')
144
144
  assert goal == "fix it" and verify == "go test ./..."
145
- assert rounds == 6 and fitness == "" # unspecified → caller resolves
145
+ assert rounds == 0 and fitness == "" and effort == "" # unspecified → caller resolves
146
146
 
147
147
 
148
148
  def test_parse_bare_goal_leaves_verify_empty_for_autodetect():
149
- goal, verify, rounds, fitness = parse_ratchet_args("make the failing test pass")
149
+ goal, verify, rounds, fitness, effort = parse_ratchet_args("make the failing test pass")
150
150
  assert goal == "make the failing test pass"
151
- assert verify == "" and fitness == "" and rounds == 6
151
+ assert verify == "" and fitness == "" and rounds == 0 and effort == ""
152
152
 
153
153
 
154
154
  @pytest.mark.parametrize("bad", [
@@ -156,6 +156,7 @@ def test_parse_bare_goal_leaves_verify_empty_for_autodetect():
156
156
  "--verify", # flag needs a value
157
157
  'goal --verify "x" --rounds notanumber', # bad number
158
158
  'goal --verify "x" trailing', # positional after a flag
159
+ 'goal --effort ludicrous', # invalid effort level
159
160
  ])
160
161
  def test_parse_errors(bad):
161
162
  with pytest.raises(ValueError):
@@ -163,8 +164,40 @@ def test_parse_errors(bad):
163
164
 
164
165
 
165
166
  def test_parse_rounds_clamped():
166
- _, _, rounds, _ = parse_ratchet_args('g --verify "x" --rounds 999')
167
- assert rounds == 20
167
+ _, _, rounds, _, _ = parse_ratchet_args('g --verify "x" --rounds 999')
168
+ assert rounds == 30
169
+
170
+
171
+ # ── effort spectrum ──
172
+
173
+ def test_effort_low_is_plain_pawl():
174
+ from drydock.ratchet import effort_profile, policy_for
175
+ p = effort_profile("low")
176
+ assert p["ladder"] == ("exploit",) # never escalates → pure hill-climber
177
+ pol = policy_for("low")
178
+ for _ in range(6):
179
+ assert pol.next_operator(improved=False) == "exploit" # stuck on exploit forever
180
+
181
+
182
+ def test_effort_medium_caps_at_diversify():
183
+ from drydock.ratchet import policy_for
184
+ pol = policy_for("medium") # ladder = (exploit, diversify), patience 2
185
+ ops = {pol.next_operator(improved=False) for _ in range(10)}
186
+ assert ops <= {"exploit", "diversify"} and "fanout" not in ops and "crossover" not in ops
187
+
188
+
189
+ def test_effort_high_unlocks_full_ladder_and_scales_fanout():
190
+ from drydock.ratchet import effort_profile, policy_for
191
+ assert effort_profile("high")["ladder"][-1] == "restart"
192
+ assert effort_profile("max")["fanout"] == 5
193
+ pol = policy_for("max") # fanout 5 → a fan-out round runs 5 variants (exploit + 4)
194
+ assert len(pol.variant_specs("fanout")) == 5
195
+ assert pol.variant_specs("fanout")[0]["mode"] == "continue" # still exploit-first
196
+
197
+
198
+ def test_effort_unknown_defaults_high():
199
+ from drydock.ratchet import effort_profile
200
+ assert effort_profile("bogus") == effort_profile("high")
168
201
 
169
202
 
170
203
  # ───────────────────────── verifier auto-detection ─────────────────────────
@@ -322,3 +355,25 @@ def test_variation_policy_uses_crossover_when_available():
322
355
  def test_diversify_prompt_tells_model_not_to_repeat():
323
356
  d = diversify_prompt("solve it", 5, 6, "diversify")
324
357
  assert "DIFFERENT" in d and "5/6" in d and "PRESERVED" in d
358
+
359
+
360
+ def test_variation_policy_elitism_exploit_always_first():
361
+ # every operator's variants START with the steady low-temp exploit move, so a
362
+ # round can never underperform the plain pawl (the fix for eratchet regressing)
363
+ p = VariationPolicy()
364
+ for op in VariationPolicy.LADDER:
365
+ specs = p.variant_specs(op)
366
+ assert specs[0] == {"mode": "continue", "temperature": 0.2}, op
367
+ assert len(specs) >= 1
368
+ # exploration adds EXTRA variants, never replaces exploit
369
+ assert len(p.variant_specs("diversify")) == 2
370
+ assert len(p.variant_specs("exploit")) == 1
371
+ params = p.params_for("fanout")
372
+ assert params["specs"][0]["mode"] == "continue" and params["variants"] == len(params["specs"])
373
+
374
+
375
+ def test_variation_policy_default_patience_grinds_before_escalating():
376
+ # default patience=2: two stalls on exploit before diversifying (was 1)
377
+ p = VariationPolicy()
378
+ assert p.next_operator(improved=False) == "exploit" # stall 1 → still grind
379
+ assert p.next_operator(improved=False) == "diversify" # stall 2 → escalate
@@ -75,7 +75,7 @@ def test_phase_transitions_understand_implement_complete():
75
75
  turns = [
76
76
  AssistantTurn("", [{"id": "1", "name": "Write",
77
77
  "input": {"file_path": "/tmp/dd_ph.txt", "content": "x"}}], 5, 5),
78
- AssistantTurn("", [{"id": "2", "name": "Bash", "input": {"command": "python -c 'print(1)'"}}], 5, 5),
78
+ AssistantTurn("", [{"id": "2", "name": "Bash", "input": {"command": "python3 -c 'print(1)'"}}], 5, 5),
79
79
  AssistantTurn("done, verified", [], 5, 5),
80
80
  ]
81
81
  i = {"n": 0}