drydock-cli 3.1.7__tar.gz → 3.1.9__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.9}/PKG-INFO +18 -3
  2. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/README.md +17 -2
  3. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/cli.py +6 -2
  4. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/config.py +37 -0
  5. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/ratchet.py +89 -24
  6. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/tui/app.py +39 -20
  7. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/tui/widgets.py +100 -8
  8. {drydock_cli-3.1.7 → drydock_cli-3.1.9/drydock_cli.egg-info}/PKG-INFO +18 -3
  9. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/pyproject.toml +1 -1
  10. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_config.py +36 -0
  11. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_events.py +1 -1
  12. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_ratchet.py +64 -9
  13. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_task_state.py +1 -1
  14. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_tui.py +79 -0
  15. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_verification_gate.py +3 -3
  16. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/LICENSE +0 -0
  17. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/NOTICE +0 -0
  18. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/__init__.py +0 -0
  19. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/__main__.py +0 -0
  20. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/advisor.py +0 -0
  21. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/agent.py +0 -0
  22. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/bash_safety.py +0 -0
  23. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/budget.py +0 -0
  24. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/builtin_skills/__init__.py +0 -0
  25. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/builtin_skills/document-canvas.md +0 -0
  26. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/builtin_skills/fiar-assess.md +0 -0
  27. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/builtin_skills/fiar-cap.md +0 -0
  28. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/builtin_skills/fiar-evidence.md +0 -0
  29. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/builtin_skills/fiar-readiness.md +0 -0
  30. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/builtin_skills/ml-data.md +0 -0
  31. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/builtin_skills/ml-debug.md +0 -0
  32. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/builtin_skills/ml-finetune.md +0 -0
  33. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/builtin_skills/ml-metrics.md +0 -0
  34. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/builtin_skills/ml-rl.md +0 -0
  35. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/builtin_skills/ml-train.md +0 -0
  36. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/builtin_skills/nist-ai-rmf.md +0 -0
  37. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/builtin_skills/nist-csf.md +0 -0
  38. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/builtin_skills/rmf-categorize.md +0 -0
  39. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/builtin_skills/rmf-control.md +0 -0
  40. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/builtin_skills/rmf-poam.md +0 -0
  41. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/builtin_skills/rmf-review.md +0 -0
  42. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/builtin_skills/stig-assess.md +0 -0
  43. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/builtin_skills/stig-remediate.md +0 -0
  44. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/cci.py +0 -0
  45. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/compaction.py +0 -0
  46. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/detect.py +0 -0
  47. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/doccanvas.py +0 -0
  48. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/events.py +0 -0
  49. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/extract.py +0 -0
  50. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/fiar.py +0 -0
  51. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/gittools.py +0 -0
  52. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/graphrag.py +0 -0
  53. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/guards.py +0 -0
  54. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/loop_detect.py +0 -0
  55. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/mcp.py +0 -0
  56. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/phases.py +0 -0
  57. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/poam.py +0 -0
  58. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/progress.py +0 -0
  59. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/providers.py +0 -0
  60. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/recipes.py +0 -0
  61. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/recovery.py +0 -0
  62. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/resume.py +0 -0
  63. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/rmf.py +0 -0
  64. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/rmf_graph.py +0 -0
  65. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/skills.py +0 -0
  66. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/stig.py +0 -0
  67. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/subagents.py +0 -0
  68. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/suggest.py +0 -0
  69. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/task_state.py +0 -0
  70. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/tool_policy.py +0 -0
  71. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/tool_registry.py +0 -0
  72. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/tool_result.py +0 -0
  73. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/tool_select.py +0 -0
  74. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/tool_validate.py +0 -0
  75. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/tools/__init__.py +0 -0
  76. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/trajectory.py +0 -0
  77. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/tui/__init__.py +0 -0
  78. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/tui/approval.py +0 -0
  79. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/tui/messages.py +0 -0
  80. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/tuning.py +0 -0
  81. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/verification.py +0 -0
  82. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock/web.py +0 -0
  83. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock_cli.egg-info/SOURCES.txt +0 -0
  84. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock_cli.egg-info/dependency_links.txt +0 -0
  85. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock_cli.egg-info/entry_points.txt +0 -0
  86. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock_cli.egg-info/requires.txt +0 -0
  87. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/drydock_cli.egg-info/top_level.txt +0 -0
  88. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/setup.cfg +0 -0
  89. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_advisor.py +0 -0
  90. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_approval.py +0 -0
  91. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_back_command.py +0 -0
  92. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_bash_background.py +0 -0
  93. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_bash_binary_output.py +0 -0
  94. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_bash_crossplatform.py +0 -0
  95. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_bash_output_bounding.py +0 -0
  96. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_bash_process_group.py +0 -0
  97. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_bash_safety.py +0 -0
  98. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_bash_sanitize.py +0 -0
  99. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_bash_shell.py +0 -0
  100. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_bash_stdin.py +0 -0
  101. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_bash_stop_partial.py +0 -0
  102. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_bash_timeout_network.py +0 -0
  103. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_bash_timeout_param.py +0 -0
  104. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_budget.py +0 -0
  105. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_cci.py +0 -0
  106. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_cli_agents.py +0 -0
  107. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_compact_command.py +0 -0
  108. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_compaction.py +0 -0
  109. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_config_migration.py +0 -0
  110. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_context_limit_config.py +0 -0
  111. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_context_limit_issue25.py +0 -0
  112. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_criteria_coverage.py +0 -0
  113. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_cycling_detection.py +0 -0
  114. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_degenerate_argument.py +0 -0
  115. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_detect.py +0 -0
  116. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_dispatch.py +0 -0
  117. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_doccanvas.py +0 -0
  118. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_e2e_connected.py +0 -0
  119. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_edit_replace_all.py +0 -0
  120. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_effort_governor.py +0 -0
  121. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_empty_response.py +0 -0
  122. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_extract.py +0 -0
  123. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_failure_loop.py +0 -0
  124. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_fiar.py +0 -0
  125. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_first_run_setup.py +0 -0
  126. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_gittools.py +0 -0
  127. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_glob_edit_edges.py +0 -0
  128. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_graphify_example.py +0 -0
  129. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_graphrag.py +0 -0
  130. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_graphrag_quoted_path.py +0 -0
  131. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_graphrag_sqlite.py +0 -0
  132. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_grep_and_read_robust.py +0 -0
  133. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_guards_and_tools.py +0 -0
  134. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_hallucinated_tools.py +0 -0
  135. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_leaked_tool_call.py +0 -0
  136. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_loop_detect.py +0 -0
  137. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_mcp.py +0 -0
  138. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_model_registry.py +0 -0
  139. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_oneshot_unreachable.py +0 -0
  140. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_overthink_interrupt.py +0 -0
  141. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_phases.py +0 -0
  142. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_plan_autocontinue.py +0 -0
  143. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_poam.py +0 -0
  144. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_progress.py +0 -0
  145. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_providers_unreachable.py +0 -0
  146. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_read_index.py +0 -0
  147. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_recipes.py +0 -0
  148. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_recovery.py +0 -0
  149. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_recovery_config.py +0 -0
  150. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_recovery_integration.py +0 -0
  151. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_repeated_outcome.py +0 -0
  152. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_repetition_interrupt.py +0 -0
  153. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_resume_events.py +0 -0
  154. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_resume_restore.py +0 -0
  155. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_resume_snapshot.py +0 -0
  156. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_rmf.py +0 -0
  157. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_rmf_graph.py +0 -0
  158. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_rmf_stig_graph.py +0 -0
  159. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_rolling_plan.py +0 -0
  160. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_runaway_repetition.py +0 -0
  161. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_screenshot.py +0 -0
  162. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_server_probe.py +0 -0
  163. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_skills.py +0 -0
  164. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_sqlite_events.py +0 -0
  165. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_stall_retry.py +0 -0
  166. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_stig.py +0 -0
  167. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_stop.py +0 -0
  168. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_streaming_newlines.py +0 -0
  169. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_subagent.py +0 -0
  170. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_subagents.py +0 -0
  171. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_suggest.py +0 -0
  172. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_system_prompt_help.py +0 -0
  173. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_timeline.py +0 -0
  174. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_todo.py +0 -0
  175. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_tool_arg_coercion.py +0 -0
  176. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_tool_arg_coercion_more.py +0 -0
  177. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_tool_arg_parsing.py +0 -0
  178. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_tool_canonical.py +0 -0
  179. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_tool_policy.py +0 -0
  180. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_tool_result.py +0 -0
  181. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_tool_select.py +0 -0
  182. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_tool_validate.py +0 -0
  183. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_tools_undo.py +0 -0
  184. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_trajectory.py +0 -0
  185. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_tuning.py +0 -0
  186. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_viewimage.py +0 -0
  187. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_vision_input.py +0 -0
  188. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_web_tools.py +0 -0
  189. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_windows_shell.py +0 -0
  190. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_worker_subagent.py +0 -0
  191. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/tests/test_write_content_coerce.py +0 -0
  192. {drydock_cli-3.1.7 → drydock_cli-3.1.9}/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.9
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 |
@@ -445,6 +447,19 @@ commands to do the work, showing each as a collapsible tool card.
445
447
  It honors `AGENTS.md` / `DRYDOCK.md` in the working directory for project
446
448
  conventions.
447
449
 
450
+ ### Custom system prompt
451
+
452
+ For standing instructions applied on **every** turn (stronger than the
453
+ per-project `AGENTS.md`, which is framed as optional background), set a custom
454
+ system prompt via either:
455
+
456
+ - a file: `~/.drydock/system_prompt.md` — write your instructions there, or
457
+ - a config key in `~/.drydock/config.toml`: `system_prompt = "..."`.
458
+
459
+ The file wins if both are set; contents are capped at 8000 chars. It's injected
460
+ after drydock's base prompt and before any project `AGENTS.md`, and applies in
461
+ both the TUI and CLI.
462
+
448
463
  ## Safety
449
464
 
450
465
  Two tiers, plus advisory guards — all designed so legitimate work is never
@@ -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 |
@@ -421,6 +423,19 @@ commands to do the work, showing each as a collapsible tool card.
421
423
  It honors `AGENTS.md` / `DRYDOCK.md` in the working directory for project
422
424
  conventions.
423
425
 
426
+ ### Custom system prompt
427
+
428
+ For standing instructions applied on **every** turn (stronger than the
429
+ per-project `AGENTS.md`, which is framed as optional background), set a custom
430
+ system prompt via either:
431
+
432
+ - a file: `~/.drydock/system_prompt.md` — write your instructions there, or
433
+ - a config key in `~/.drydock/config.toml`: `system_prompt = "..."`.
434
+
435
+ The file wins if both are set; contents are capped at 8000 chars. It's injected
436
+ after drydock's base prompt and before any project `AGENTS.md`, and applies in
437
+ both the TUI and CLI.
438
+
424
439
  ## Safety
425
440
 
426
441
  Two tiers, plus advisory guards — all designed so legitimate work is never
@@ -13,6 +13,7 @@ import sys
13
13
  from pathlib import Path
14
14
 
15
15
  from drydock import __version__
16
+ from drydock.config import load_user_system_prompt
16
17
  from drydock.agent import AgentState, run, TextChunk, ToolStart, ToolEnd, TurnDone
17
18
 
18
19
 
@@ -71,7 +72,7 @@ def print_colored(text: str, color: str = "") -> None:
71
72
 
72
73
  def run_interactive(config: dict) -> None:
73
74
  """Interactive REPL mode."""
74
- system = SYSTEM_PROMPT + load_project_instructions()
75
+ system = SYSTEM_PROMPT + load_user_system_prompt(config) + load_project_instructions()
75
76
  state = AgentState()
76
77
 
77
78
  print_colored(f"⚓ DryDock v{__version__} · {config['model']}", "cyan")
@@ -118,7 +119,7 @@ def run_oneshot(prompt: str, config: dict) -> None:
118
119
  """One-shot mode: run a single prompt and exit."""
119
120
  from drydock.providers import LLMUnreachable
120
121
 
121
- system = SYSTEM_PROMPT + load_project_instructions()
122
+ system = SYSTEM_PROMPT + load_user_system_prompt(config) + load_project_instructions()
122
123
  state = AgentState()
123
124
 
124
125
  try:
@@ -435,6 +436,9 @@ def main():
435
436
  # greeting into a runaway build. The TUI reads this from config; CLI modes
436
437
  # call load_project_instructions directly (see run_interactive/run_oneshot).
437
438
  config["project_instructions"] = load_project_instructions()
439
+ # The user's custom system prompt (~/.drydock/system_prompt.md or the
440
+ # `system_prompt` config key) — standing instructions the TUI injects every turn.
441
+ config["user_system_prompt"] = load_user_system_prompt(config)
438
442
 
439
443
  # Connect any configured MCP servers and register their tools so the model
440
444
  # can call them like native tools. Failures are skipped (never block start);
@@ -106,6 +106,11 @@ DEFAULTS: dict[str, object] = {
106
106
  # (falls back to `model`). Manage with `/model add|default|remove`.
107
107
  "models": [],
108
108
  "default_model": "",
109
+ # A custom system prompt injected into EVERY turn as the user's standing
110
+ # instructions (unlike a per-project AGENTS.md, which is framed as optional
111
+ # background). Two sources, file wins: the file ~/.drydock/system_prompt.md,
112
+ # or this inline key. Empty = nothing added. Capped at 8000 chars.
113
+ "system_prompt": "",
109
114
  }
110
115
 
111
116
 
@@ -113,6 +118,38 @@ def default_config_path() -> Path:
113
118
  return Path.home() / ".drydock" / "config.toml"
114
119
 
115
120
 
121
+ def system_prompt_path() -> Path:
122
+ """User's global custom system-prompt file."""
123
+ return Path.home() / ".drydock" / "system_prompt.md"
124
+
125
+
126
+ def load_user_system_prompt(config: dict | None = None) -> str:
127
+ """The user's custom system prompt, injected every turn as standing orders.
128
+
129
+ Source (first non-empty wins): the file ~/.drydock/system_prompt.md, then the
130
+ inline `system_prompt` config key. Wrapped with a clear header and capped at
131
+ 8000 chars. Returns '' when neither is set. Never raises.
132
+ """
133
+ text = ""
134
+ try:
135
+ p = system_prompt_path()
136
+ if p.exists():
137
+ text = p.read_text()
138
+ except OSError:
139
+ text = ""
140
+ if not text and config:
141
+ text = str(config.get("system_prompt", "") or "")
142
+ text = text.strip()
143
+ if not text:
144
+ return ""
145
+ return (
146
+ "\n\n## Custom instructions (from the user's configuration)\n\n"
147
+ "The user has set these standing instructions; follow them throughout "
148
+ "the session in addition to the guidance above.\n\n"
149
+ f"{text[:8000]}"
150
+ )
151
+
152
+
116
153
  # ── Model registry ────────────────────────────────────────────────────────
117
154
  # A model entry is {"name", "base_url", "provider"}. Kept in cfg["models"];
118
155
  # cfg["default_model"] names the launch default. These helpers keep the list
@@ -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 {
@@ -336,10 +341,15 @@ class DrydockApp(App):
336
341
  return "deny"
337
342
 
338
343
  def _build_system(self, model: str | None) -> str:
339
- # The TUI honors a user's OWN AGENTS.md/DRYDOCK.md like the CLI does —
340
- # cli.main() loads it into config["project_instructions"] as background
341
- # context (drydock never auto-creates one).
342
- return system_prompt_for_model(model) + self.config.get("project_instructions", "")
344
+ # base (model-specific) + the user's custom system prompt (standing orders
345
+ # from ~/.drydock/system_prompt.md or the `system_prompt` config key) + a
346
+ # per-project AGENTS.md/DRYDOCK.md as background context. cli.main() loads
347
+ # both into config; drydock never auto-creates either file.
348
+ return (
349
+ system_prompt_for_model(model)
350
+ + self.config.get("user_system_prompt", "")
351
+ + self.config.get("project_instructions", "")
352
+ )
343
353
 
344
354
  # ── layout ────────────────────────────────────────────────────────────
345
355
 
@@ -350,6 +360,7 @@ class DrydockApp(App):
350
360
  yield Static("", id="todo") # pinned task checklist (empty = hidden)
351
361
  yield Static("", id="working") # in-line activity (empty when idle)
352
362
  yield Static("", id="suggest") # dimmed recommended-next-command hint
363
+ yield Static("", id="slashmenu") # as-you-type slash-command menu (empty = hidden)
353
364
  yield PromptArea(id="prompt")
354
365
  yield Static(self._status_text(), id="status")
355
366
 
@@ -457,21 +468,23 @@ class DrydockApp(App):
457
468
  # ── input ─────────────────────────────────────────────────────────────
458
469
 
459
470
  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."""
471
+ """As the user types a slash command, surface a discovery menu in the
472
+ #slashmenu line: the matching commands with their args + one-line help
473
+ while typing the name ('/' lists everything), then the chosen command's
474
+ switches once it's settled. Tab completes, Enter runs. Rendered in a
475
+ dedicated widget so the ghost next-command suggestion is never touched."""
476
+ try:
477
+ menu = self.query_one("#slashmenu", Static)
478
+ except Exception: # noqa: BLE001 — widget not mounted yet
479
+ return
462
480
  if self._busy:
463
- return # #working shows live activity while a turn is running
481
+ menu.update("") # a turn is running; nothing to discover
482
+ return
464
483
  try:
465
484
  text = self.query_one("#prompt", PromptArea).text
466
- except Exception: # noqa: BLE001 — widget not mounted yet
485
+ except Exception: # noqa: BLE001
467
486
  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)
487
+ menu.update(slash_menu(text, skills=tuple(self._skills)))
475
488
 
476
489
  def on_prompt_area_submitted(self, event: PromptArea.Submitted) -> None:
477
490
  text = event.text.strip()
@@ -911,15 +924,21 @@ class DrydockApp(App):
911
924
  from drydock import ratchet as rmod
912
925
 
913
926
  try:
914
- goal, verify, rounds, fitness = rmod.parse_ratchet_args(arg)
927
+ goal, verify, rounds, fitness, effort = rmod.parse_ratchet_args(arg)
915
928
  except ValueError as e:
916
929
  self._info(
917
930
  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)'
931
+ "usage: /ratchet <goal> [--effort low|medium|high|xhigh|max] [--verify \"<cmd>\"] "
932
+ "[--rounds N] [--fitness auto|exitcode|<regex>]\n"
933
+ " effort scales it from a plain pawl (low) to full evolutionary search (high+)\n"
934
+ " e.g. /ratchet make the failing suite pass (verifier auto-detected)\n"
935
+ ' /ratchet crack this hard bug --effort high (more rounds + exploration)'
921
936
  )
922
937
  return
938
+ # effort sets the round budget unless --rounds was given explicitly
939
+ prof = rmod.effort_profile(effort or "medium")
940
+ if not rounds:
941
+ rounds = prof["max_rounds"]
923
942
  if self._busy:
924
943
  self._info("A turn is already running — stop it (Esc) before starting a ratchet.")
925
944
  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.9
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 |
@@ -445,6 +447,19 @@ commands to do the work, showing each as a collapsible tool card.
445
447
  It honors `AGENTS.md` / `DRYDOCK.md` in the working directory for project
446
448
  conventions.
447
449
 
450
+ ### Custom system prompt
451
+
452
+ For standing instructions applied on **every** turn (stronger than the
453
+ per-project `AGENTS.md`, which is framed as optional background), set a custom
454
+ system prompt via either:
455
+
456
+ - a file: `~/.drydock/system_prompt.md` — write your instructions there, or
457
+ - a config key in `~/.drydock/config.toml`: `system_prompt = "..."`.
458
+
459
+ The file wins if both are set; contents are capped at 8000 chars. It's injected
460
+ after drydock's base prompt and before any project `AGENTS.md`, and applies in
461
+ both the TUI and CLI.
462
+
448
463
  ## Safety
449
464
 
450
465
  Two tiers, plus advisory guards — all designed so legitimate work is never