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.
- {drydock_cli-3.1.7/drydock_cli.egg-info → drydock_cli-3.1.8}/PKG-INFO +5 -3
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/README.md +4 -2
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/ratchet.py +89 -24
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/tui/app.py +30 -16
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/tui/widgets.py +100 -8
- {drydock_cli-3.1.7 → drydock_cli-3.1.8/drydock_cli.egg-info}/PKG-INFO +5 -3
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/pyproject.toml +1 -1
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_events.py +1 -1
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_ratchet.py +64 -9
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_task_state.py +1 -1
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_tui.py +79 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_verification_gate.py +3 -3
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/LICENSE +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/NOTICE +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/__init__.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/__main__.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/advisor.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/agent.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/budget.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/document-canvas.md +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/fiar-assess.md +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/fiar-cap.md +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/fiar-evidence.md +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/fiar-readiness.md +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/ml-data.md +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/ml-debug.md +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/ml-finetune.md +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/ml-metrics.md +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/ml-rl.md +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/ml-train.md +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/nist-ai-rmf.md +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/nist-csf.md +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/cci.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/cli.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/compaction.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/config.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/detect.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/doccanvas.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/events.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/extract.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/fiar.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/gittools.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/graphrag.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/guards.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/mcp.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/phases.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/poam.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/progress.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/providers.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/recipes.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/recovery.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/resume.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/rmf.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/skills.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/stig.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/subagents.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/suggest.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/task_state.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/tool_policy.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/tool_result.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/tool_select.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/tool_validate.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/tools/__init__.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/trajectory.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/tuning.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/verification.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock/web.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock_cli.egg-info/SOURCES.txt +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/setup.cfg +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_advisor.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_approval.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_back_command.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_bash_background.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_bash_binary_output.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_bash_crossplatform.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_bash_sanitize.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_bash_shell.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_bash_stdin.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_bash_stop_partial.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_bash_timeout_param.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_budget.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_cci.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_compaction.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_config.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_context_limit_issue25.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_criteria_coverage.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_cycling_detection.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_degenerate_argument.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_detect.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_doccanvas.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_edit_replace_all.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_effort_governor.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_extract.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_fiar.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_gittools.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_glob_edit_edges.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_graphify_example.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_graphrag_quoted_path.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_graphrag_sqlite.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_grep_and_read_robust.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_mcp.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_model_registry.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_overthink_interrupt.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_phases.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_poam.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_progress.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_read_index.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_recipes.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_recovery.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_recovery_config.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_recovery_integration.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_repeated_outcome.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_repetition_interrupt.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_resume_events.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_resume_restore.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_resume_snapshot.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_rmf.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_rolling_plan.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_screenshot.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_skills.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_sqlite_events.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_stall_retry.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_stig.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_stop.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_subagent.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_subagents.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_suggest.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_timeline.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_todo.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_tool_arg_coercion.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_tool_arg_coercion_more.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_tool_canonical.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_tool_policy.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_tool_result.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_tool_select.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_tool_validate.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_trajectory.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_tuning.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_viewimage.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_windows_shell.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_worker_subagent.py +0 -0
- {drydock_cli-3.1.7 → drydock_cli-3.1.8}/tests/test_write_content_coerce.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
55
|
-
|
|
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)
|
|
277
|
-
|
|
278
|
-
|
|
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 =
|
|
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]),
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
487
|
-
op = self.
|
|
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"
|
|
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
|
-
"""
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
|
461
|
-
|
|
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
|
-
|
|
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
|
|
480
|
+
except Exception: # noqa: BLE001
|
|
467
481
|
return
|
|
468
|
-
|
|
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>\"]
|
|
919
|
-
"
|
|
920
|
-
|
|
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
|
-
#
|
|
152
|
-
#
|
|
153
|
-
#
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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": "
|
|
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 ==
|
|
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 ==
|
|
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 ==
|
|
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": "
|
|
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}
|