drydock-cli 3.1.27__tar.gz → 3.1.28__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.27/drydock_cli.egg-info → drydock_cli-3.1.28}/PKG-INFO +1 -1
- drydock_cli-3.1.28/drydock/capacity.py +135 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/config.py +6 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/eratchet.py +60 -8
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/swarm.py +18 -4
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/tui/app.py +8 -6
- {drydock_cli-3.1.27 → drydock_cli-3.1.28/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock_cli.egg-info/SOURCES.txt +2 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/pyproject.toml +1 -1
- drydock_cli-3.1.28/tests/test_capacity.py +78 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_eratchet.py +49 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/LICENSE +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/NOTICE +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/README.md +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/__init__.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/__main__.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/advisor.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/agent.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/bottleneck.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/budget.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/document-canvas.md +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/fiar-assess.md +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/fiar-cap.md +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/fiar-evidence.md +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/fiar-readiness.md +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/ml-data.md +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/ml-debug.md +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/ml-finetune.md +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/ml-metrics.md +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/ml-rl.md +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/ml-train.md +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/nist-ai-rmf.md +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/nist-csf.md +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/cci.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/cli.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/comms/__init__.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/comms/attention.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/comms/channels.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/comms/events.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/compaction.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/detect.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/doccanvas.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/events.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/extract.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/fiar.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/gittools.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/graphrag.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/groundtruth.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/guards.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/jobs.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/mcp.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/pdfredbox.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/phases.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/poam.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/predictions.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/progress.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/providers.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/ratchet.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/recipes.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/recovery.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/resume.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/rmf.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/skills.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/stig.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/subagents.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/suggest.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/task_state.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/tool_policy.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/tool_result.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/tool_select.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/tool_validate.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/tools/__init__.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/trajectory.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/tuning.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/verification.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/web.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/setup.cfg +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_advisor.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_approval.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_back_command.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_bash_background.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_bash_binary_output.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_bash_crossplatform.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_bash_sanitize.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_bash_shell.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_bash_stdin.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_bash_stop_partial.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_bash_timeout_param.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_bottleneck.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_budget.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_cci.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_comms.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_compaction.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_config.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_context_limit_issue25.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_criteria_coverage.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_cycling_detection.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_declared_deps.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_degenerate_argument.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_detect.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_doccanvas.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_edit_replace_all.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_edit_thrash_and_compaction.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_effort_governor.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_events.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_extract.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_fiar.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_gittools.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_glob_edit_edges.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_graphify_example.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_graphrag_quoted_path.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_graphrag_sqlite.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_grep_and_read_robust.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_groundtruth_predictions.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_jobs.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_knowledge_tool_available.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_mcp.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_model_registry.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_overthink_interrupt.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_pdfredbox.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_phases.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_poam.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_progress.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_ratchet.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_ratchet_offer.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_read_index.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_recipes.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_recovery.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_recovery_config.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_recovery_integration.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_repeated_outcome.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_repetition_interrupt.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_resume_events.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_resume_restore.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_resume_snapshot.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_rmf.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_rolling_plan.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_screenshot.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_skills.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_sqlite_events.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_stall_retry.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_stig.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_stop.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_subagent.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_subagents.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_suggest.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_swarm.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_task_state.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_timeline.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_todo.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_tool_arg_coercion.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_tool_arg_coercion_more.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_tool_canonical.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_tool_policy.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_tool_result.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_tool_select.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_tool_validate.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_trajectory.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_tui.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_tuning.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_verification_gate.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_viewimage.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_windows_shell.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_worker_subagent.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_write_content_coerce.py +0 -0
- {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_xccdf.py +0 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Inference-server capacity detection for swarm auto-sizing.
|
|
2
|
+
|
|
3
|
+
The swarm's useful agent count is bounded by how many requests the server actually serves
|
|
4
|
+
IN PARALLEL (§21/§22): the agents share one server, so past its real concurrency, extra
|
|
5
|
+
agents just queue — more tokens, no more wall-clock. So "hammer the 8-GPU vLLM box, tone
|
|
6
|
+
down the -np 2 laptop" reduces to: estimate the server's concurrency C, then size the swarm
|
|
7
|
+
to min(C, task-demand, budget). This module estimates C.
|
|
8
|
+
|
|
9
|
+
Detection order (first hit wins), all swallow-error / never-raise:
|
|
10
|
+
1. explicit — `config['swarm_concurrency']` or $DRYDOCK_SWARM_CONCURRENCY (the operator
|
|
11
|
+
knows their hardware; always the most reliable);
|
|
12
|
+
2. llama.cpp — GET /slots (its `-np` parallel slots);
|
|
13
|
+
3. empirical probe — fire a burst of tiny concurrent requests and infer parallelism from
|
|
14
|
+
wall-time vs single-request latency (server-agnostic; the honest signal for vLLM, whose
|
|
15
|
+
max_num_seqs isn't exposed over the OpenAI API);
|
|
16
|
+
4. fallback — a conservative default.
|
|
17
|
+
|
|
18
|
+
Stdlib-only; the probe funcs are injectable so this is testable without a live server.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import concurrent.futures
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import time
|
|
26
|
+
import urllib.request
|
|
27
|
+
from collections.abc import Callable
|
|
28
|
+
|
|
29
|
+
DEFAULT_CONCURRENCY = 4 # conservative when nothing else is known
|
|
30
|
+
DEFAULT_TASK_DEMAND = 6 # distinct approaches worth trying before agents just duplicate
|
|
31
|
+
HARD_MAX = 64 # never spawn more than this regardless of hardware
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _server_root(base_url: str) -> str:
|
|
35
|
+
"""Strip a trailing /v1 so we can hit sibling paths like /slots."""
|
|
36
|
+
root = base_url.rstrip("/")
|
|
37
|
+
if root.endswith("/v1"):
|
|
38
|
+
root = root[:-3].rstrip("/")
|
|
39
|
+
return root
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _get_json(url: str, timeout: float = 4.0):
|
|
43
|
+
try:
|
|
44
|
+
with urllib.request.urlopen(url, timeout=timeout) as r: # noqa: S310 — operator's own server
|
|
45
|
+
return json.loads(r.read().decode("utf-8"))
|
|
46
|
+
except Exception: # noqa: BLE001 — detection is best-effort, never raises
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def slots_concurrency(base_url: str, *, getter: Callable[[str], object] | None = None) -> int | None:
|
|
51
|
+
"""llama.cpp exposes its `-np` parallel slots at /slots (a list). len = concurrency."""
|
|
52
|
+
get = getter or (lambda u: _get_json(u))
|
|
53
|
+
d = get(_server_root(base_url) + "/slots")
|
|
54
|
+
if isinstance(d, list) and d:
|
|
55
|
+
return len(d)
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _default_requester(base_url: str, model: str) -> Callable[[], None]:
|
|
60
|
+
def req() -> None:
|
|
61
|
+
payload = json.dumps({
|
|
62
|
+
"model": model,
|
|
63
|
+
"messages": [{"role": "user", "content": "ping"}],
|
|
64
|
+
"max_tokens": 1, "temperature": 0,
|
|
65
|
+
}).encode("utf-8")
|
|
66
|
+
r = urllib.request.Request( # noqa: S310 — operator's own server
|
|
67
|
+
base_url.rstrip("/") + "/chat/completions", data=payload,
|
|
68
|
+
headers={"Content-Type": "application/json"})
|
|
69
|
+
with urllib.request.urlopen(r, timeout=60) as resp:
|
|
70
|
+
resp.read()
|
|
71
|
+
return req
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def probe_concurrency(base_url: str, model: str, *, burst: int = 16,
|
|
75
|
+
requester: Callable[[], None] | None = None) -> int:
|
|
76
|
+
"""Empirically estimate useful parallelism: one warm request costs t1; `burst` concurrent
|
|
77
|
+
requests finish in wall time W. Total work ≈ burst·t1 done in W ⇒ parallelism ≈ burst·t1/W
|
|
78
|
+
(≈burst if fully batched, ≈1 if serialized). Server-agnostic. Returns 1..burst."""
|
|
79
|
+
req = requester or _default_requester(base_url, model)
|
|
80
|
+
try:
|
|
81
|
+
t0 = time.monotonic()
|
|
82
|
+
req()
|
|
83
|
+
t1 = time.monotonic() - t0
|
|
84
|
+
except Exception: # noqa: BLE001
|
|
85
|
+
return 1
|
|
86
|
+
if t1 <= 0:
|
|
87
|
+
return burst
|
|
88
|
+
start = time.monotonic()
|
|
89
|
+
try:
|
|
90
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=burst) as ex:
|
|
91
|
+
list(ex.map(lambda _: _safe_call(req), range(burst)))
|
|
92
|
+
except Exception: # noqa: BLE001
|
|
93
|
+
return 1
|
|
94
|
+
wall = time.monotonic() - start
|
|
95
|
+
if wall <= 0:
|
|
96
|
+
return burst
|
|
97
|
+
return max(1, min(burst, round(burst * t1 / wall)))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _safe_call(req: Callable[[], None]) -> None:
|
|
101
|
+
try:
|
|
102
|
+
req()
|
|
103
|
+
except Exception: # noqa: BLE001
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def detect_concurrency(base_url: str, *, provider: str = "vllm", model: str = "",
|
|
108
|
+
config: dict | None = None, probe: bool = True) -> int:
|
|
109
|
+
"""Estimate the server's useful concurrency C (see module docstring for the order)."""
|
|
110
|
+
config = config or {}
|
|
111
|
+
override = config.get("swarm_concurrency") or os.environ.get("DRYDOCK_SWARM_CONCURRENCY")
|
|
112
|
+
if override:
|
|
113
|
+
try:
|
|
114
|
+
return max(1, int(override))
|
|
115
|
+
except (TypeError, ValueError):
|
|
116
|
+
pass
|
|
117
|
+
slots = slots_concurrency(base_url)
|
|
118
|
+
if slots:
|
|
119
|
+
return slots
|
|
120
|
+
if probe and model:
|
|
121
|
+
c = probe_concurrency(base_url, model)
|
|
122
|
+
if c:
|
|
123
|
+
return c
|
|
124
|
+
return int(config.get("swarm_concurrency_default", DEFAULT_CONCURRENCY))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def swarm_size(concurrency: int, *, task_demand: int = DEFAULT_TASK_DEMAND,
|
|
128
|
+
budget_agents: int | None = None, hard_max: int = HARD_MAX) -> int:
|
|
129
|
+
"""N = min(hardware concurrency, task-demand, budget) — never maximized (§39: more
|
|
130
|
+
agents ≠ more intelligence). On an 8-GPU box concurrency is high so task-demand binds
|
|
131
|
+
(no wasteful cloning); on a small box concurrency binds (auto tone-down)."""
|
|
132
|
+
n = min(max(1, concurrency), max(1, task_demand), hard_max)
|
|
133
|
+
if budget_agents:
|
|
134
|
+
n = min(n, max(1, budget_agents))
|
|
135
|
+
return max(1, n)
|
|
@@ -102,6 +102,12 @@ DEFAULTS: dict[str, object] = {
|
|
|
102
102
|
# (docs/multi_agent_swarm_prd.md). On by default; set false to keep every request single-
|
|
103
103
|
# agent unless the user runs /swarm explicitly.
|
|
104
104
|
"swarm_auto_escalate": True,
|
|
105
|
+
# Swarm sizing: the harness caps agents at the inference server's real concurrency
|
|
106
|
+
# (drydock/capacity.py) — hammer an 8-GPU vLLM box, tone down a -np 2 laptop. Set
|
|
107
|
+
# swarm_concurrency to your server's parallel capacity to skip auto-detection (most
|
|
108
|
+
# reliable); 0 = auto-detect (llama.cpp /slots, else an empirical burst probe).
|
|
109
|
+
"swarm_concurrency": 0,
|
|
110
|
+
"swarm_concurrency_default": 4,
|
|
105
111
|
# URL substrings the web tools refuse: WebSearch drops matching results,
|
|
106
112
|
# WebFetch declines matching URLs (with a plain message, never an error).
|
|
107
113
|
# Used to keep benchmark/solution sites out of harvested training runs.
|
|
@@ -106,6 +106,7 @@ def run_eratchet(
|
|
|
106
106
|
on_event: Optional[Callable[[str, dict], None]] = None,
|
|
107
107
|
should_stop: Optional[Callable[[], bool]] = None,
|
|
108
108
|
capture: Optional[Callable[[dict], None]] = None,
|
|
109
|
+
share: bool = False,
|
|
109
110
|
) -> EratchetResult:
|
|
110
111
|
"""Drive the parallel evolutionary ratchet. Pure control flow: every side
|
|
111
112
|
effect (running an attempt, verifying, snapshotting) happens inside the
|
|
@@ -137,6 +138,7 @@ def run_eratchet(
|
|
|
137
138
|
base_ref: Optional[str] = None
|
|
138
139
|
prev_best = -1
|
|
139
140
|
flat = 0
|
|
141
|
+
peer_log: list = [] # (generation, passed, total, summary) — the blackboard when share=True
|
|
140
142
|
|
|
141
143
|
emit("start", goal=goal, effort=effort, generations=gens,
|
|
142
144
|
fanout=policy.fanout, servers=list(servers))
|
|
@@ -154,6 +156,15 @@ def run_eratchet(
|
|
|
154
156
|
if pairs:
|
|
155
157
|
xplan = plan_crossover(pairs[0][0], pairs[0][1], g)
|
|
156
158
|
|
|
159
|
+
# BLACKBOARD (§10): feed prior variants' attempts+scores into this generation's
|
|
160
|
+
# specs so its variants generate INFORMED rather than blind. share=False → today's
|
|
161
|
+
# blind eratchet (the baseline arm of the eratchet ± blackboard control).
|
|
162
|
+
if share and peer_log:
|
|
163
|
+
notes = _peer_notes_erx(peer_log)
|
|
164
|
+
for s in specs:
|
|
165
|
+
s["peer_notes"] = notes
|
|
166
|
+
emit("share", generation=g, prior_attempts=len(peer_log))
|
|
167
|
+
|
|
157
168
|
emit("generation_start", generation=g, operator=op,
|
|
158
169
|
variants=len(specs), modes=[s.get("mode") for s in specs])
|
|
159
170
|
|
|
@@ -169,6 +180,8 @@ def run_eratchet(
|
|
|
169
180
|
cost=float(g),
|
|
170
181
|
)
|
|
171
182
|
archive.consider(cand)
|
|
183
|
+
if share:
|
|
184
|
+
peer_log.append((g, oc.passed, oc.total, _last_msg(oc.messages)))
|
|
172
185
|
if capture:
|
|
173
186
|
capture({
|
|
174
187
|
"goal": goal, "generation": g, "index": i,
|
|
@@ -251,21 +264,54 @@ def _git(args: list, cwd: str) -> subprocess.CompletedProcess:
|
|
|
251
264
|
return subprocess.run(["git", *args], cwd=cwd, capture_output=True, text=True)
|
|
252
265
|
|
|
253
266
|
|
|
267
|
+
def _last_msg(messages) -> str:
|
|
268
|
+
"""The variant's final assistant line — a one-line 'what it tried' for the blackboard."""
|
|
269
|
+
for m in reversed(messages or []):
|
|
270
|
+
if isinstance(m, dict) and m.get("role") == "assistant":
|
|
271
|
+
c = (m.get("content") or "").strip()
|
|
272
|
+
if c:
|
|
273
|
+
return c.splitlines()[0][:160]
|
|
274
|
+
return ""
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _peer_notes_erx(log: list, limit: int = 8) -> str:
|
|
278
|
+
"""Digest of what earlier variants already tried, best-scoring first (§10). Turns
|
|
279
|
+
eratchet's blind parallel search into a coordinated one: a new variant sees the prior
|
|
280
|
+
attempts' scores + approaches so it builds on the partials and skips the dead ends.
|
|
281
|
+
`log` items are (generation, passed, total, summary)."""
|
|
282
|
+
ranked = sorted(log, key=lambda r: ((r[1] / r[2]) if r[2] else 0.0, r[1]), reverse=True)
|
|
283
|
+
lines = []
|
|
284
|
+
for (g, p, t, summ) in ranked[:limit]:
|
|
285
|
+
score = f"{p}/{t}" if t else "unverified"
|
|
286
|
+
lines.append(f"- gen{g} [{score}]: {summ or '(no summary)'}")
|
|
287
|
+
return "\n".join(lines)
|
|
288
|
+
|
|
289
|
+
|
|
254
290
|
def _variant_prompt(goal: str, spec: dict, progress: dict, xplan, donor_path: str) -> str:
|
|
255
291
|
passed, total = progress.get("passed", 0), progress.get("total", 0)
|
|
256
292
|
mode = spec.get("mode", "continue")
|
|
257
293
|
if mode == "crossover" and xplan and donor_path:
|
|
258
294
|
wants = ", ".join(xplan.get("wants", [])) or "the checks it passes that this tree fails"
|
|
259
|
-
|
|
295
|
+
base = (
|
|
260
296
|
f"{goal}\n\n[CROSSOVER — a COMPLEMENTARY solution is checked out at "
|
|
261
297
|
f"{donor_path}. It passes: {wants}. Study how it does so and fold ONLY "
|
|
262
298
|
f"those parts into THIS working tree, keeping everything that already "
|
|
263
299
|
f"passes here. A verifier will score the result.]"
|
|
264
300
|
)
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
301
|
+
else:
|
|
302
|
+
op = _MODE_OP.get(mode, "exploit")
|
|
303
|
+
if op in ("diversify", "restart"):
|
|
304
|
+
base = diversify_prompt(goal, passed, total, op)
|
|
305
|
+
else:
|
|
306
|
+
base = continuation_prompt(goal, passed, total)
|
|
307
|
+
# BLACKBOARD (§10): if the orchestrator attached peers' notes to this spec, the variant
|
|
308
|
+
# generates INFORMED instead of blind — this is what turns eratchet from redundant
|
|
309
|
+
# parallel search into coordinated search.
|
|
310
|
+
notes = spec.get("peer_notes")
|
|
311
|
+
if notes:
|
|
312
|
+
base += (f"\n\n[WHAT OTHER VARIANTS ALREADY TRIED on this SAME goal — build past the "
|
|
313
|
+
f"highest partial below; do NOT repeat the failed approaches:\n{notes}]")
|
|
314
|
+
return base
|
|
269
315
|
|
|
270
316
|
|
|
271
317
|
@dataclass
|
|
@@ -403,10 +449,15 @@ def parse_eratchet(tokens: list) -> dict:
|
|
|
403
449
|
capture = ""
|
|
404
450
|
servers: list = []
|
|
405
451
|
fanout = generations = 0
|
|
452
|
+
share = False
|
|
406
453
|
i, seen_flag = 0, False
|
|
407
454
|
while i < len(tokens):
|
|
408
455
|
t = tokens[i]
|
|
409
|
-
if t
|
|
456
|
+
if t == "--share":
|
|
457
|
+
# blackboard: later generations' variants read prior attempts' scores+approaches
|
|
458
|
+
share = True
|
|
459
|
+
seen_flag = True
|
|
460
|
+
elif t in ("--effort", "--verify", "--servers", "--fanout", "--generations",
|
|
410
461
|
"--model", "--provider", "--base-url", "--capture"):
|
|
411
462
|
seen_flag = True
|
|
412
463
|
i += 1
|
|
@@ -443,7 +494,7 @@ def parse_eratchet(tokens: list) -> dict:
|
|
|
443
494
|
return {"goal": " ".join(goal), "effort": effort, "verify": verify,
|
|
444
495
|
"servers": servers, "fanout": fanout, "generations": generations,
|
|
445
496
|
"model": model, "provider": provider, "base_url": base_url,
|
|
446
|
-
"capture": capture}
|
|
497
|
+
"capture": capture, "share": share}
|
|
447
498
|
|
|
448
499
|
|
|
449
500
|
def resolve_config(opts: dict, config: dict) -> "ExecConfig | str":
|
|
@@ -511,7 +562,8 @@ def run_cli(argv: list, config: dict | None = None) -> int:
|
|
|
511
562
|
try:
|
|
512
563
|
res = run_eratchet(cfg.goal, servers=servers, runner=make_variant_runner(cfg),
|
|
513
564
|
effort=opts["effort"], max_generations=opts["generations"],
|
|
514
|
-
fanout=opts["fanout"], on_event=on_event, capture=capture
|
|
565
|
+
fanout=opts["fanout"], on_event=on_event, capture=capture,
|
|
566
|
+
share=opts.get("share", False))
|
|
515
567
|
finally:
|
|
516
568
|
if cap_fh:
|
|
517
569
|
cap_fh.close()
|
|
@@ -733,7 +733,8 @@ class SwarmResult:
|
|
|
733
733
|
candidates: list[Candidate]
|
|
734
734
|
|
|
735
735
|
|
|
736
|
-
def run_swarm(cwd: str | Path, objective: str, *, agents: int
|
|
736
|
+
def run_swarm(cwd: str | Path, objective: str, *, agents: "int | str" = 4,
|
|
737
|
+
base_config: dict | None = None,
|
|
737
738
|
base_ref: str = "HEAD", verify_cmd: str | None = None, fitness: str = "auto",
|
|
738
739
|
max_turns: int = 40, max_tool_calls: int = 40, max_workers: int | None = None,
|
|
739
740
|
runner: AgentRunner = default_agent_runner, verify: VerifyFn | None = None,
|
|
@@ -751,11 +752,24 @@ def run_swarm(cwd: str | Path, objective: str, *, agents: int = 4, base_config:
|
|
|
751
752
|
if repo is None:
|
|
752
753
|
raise ValueError("swarm needs a git repository (run `git init` first) for worktree "
|
|
753
754
|
"isolation")
|
|
754
|
-
|
|
755
|
+
bc = base_config or {}
|
|
756
|
+
# Auto-size to the server's real concurrency (§21/§22): hammer a big box, tone down a
|
|
757
|
+
# small one. N = min(hardware concurrency, task-demand, budget) — never maximized.
|
|
758
|
+
detected = 0
|
|
759
|
+
if isinstance(agents, str) and agents == "auto":
|
|
760
|
+
from drydock.capacity import detect_concurrency, swarm_size
|
|
761
|
+
detected = detect_concurrency(str(bc.get("base_url", "")),
|
|
762
|
+
provider=str(bc.get("provider", "vllm")),
|
|
763
|
+
model=str(bc.get("model", "")), config=bc)
|
|
764
|
+
n = swarm_size(detected)
|
|
765
|
+
else:
|
|
766
|
+
n = max(1, int(agents))
|
|
755
767
|
bb = create_swarm(repo, objective, {"agents": n, "strategy": "parallel",
|
|
756
|
-
"verify_cmd": verify_cmd or "", "fitness": fitness
|
|
768
|
+
"verify_cmd": verify_cmd or "", "fitness": fitness,
|
|
769
|
+
"concurrency_detected": detected},
|
|
757
770
|
swarm_id=swarm_id)
|
|
758
|
-
bb.emit("SWARM_START", agents=n, strategy="parallel", base_ref=base_ref
|
|
771
|
+
bb.emit("SWARM_START", agents=n, strategy="parallel", base_ref=base_ref,
|
|
772
|
+
concurrency_detected=detected)
|
|
759
773
|
|
|
760
774
|
def _ev(kind: str, **d) -> None:
|
|
761
775
|
if on_event is not None:
|
|
@@ -1200,7 +1200,7 @@ class DrydockApp(App):
|
|
|
1200
1200
|
self._info("Something is already running — stop it (Esc) before /swarm.")
|
|
1201
1201
|
return
|
|
1202
1202
|
parts = arg.split()
|
|
1203
|
-
agents, obj =
|
|
1203
|
+
agents, obj = "auto", [] # auto-size to the server's concurrency unless --agents given
|
|
1204
1204
|
i = 0
|
|
1205
1205
|
while i < len(parts):
|
|
1206
1206
|
if parts[i] == "--agents" and i + 1 < len(parts):
|
|
@@ -1224,20 +1224,22 @@ class DrydockApp(App):
|
|
|
1224
1224
|
return
|
|
1225
1225
|
self._launch_swarm(cwd, objective, agents)
|
|
1226
1226
|
|
|
1227
|
-
def _launch_swarm(self, cwd: str, objective: str, agents: int,
|
|
1227
|
+
def _launch_swarm(self, cwd: str, objective: str, agents: "int | str",
|
|
1228
1228
|
verify_cmd: str | None = None, auto: bool = False) -> None:
|
|
1229
1229
|
"""Start an in-process swarm streaming into this session. `auto=True` is the
|
|
1230
1230
|
harness escalating on its own (no user command); `verify_cmd` pins the checker
|
|
1231
|
-
(e.g. the one the single agent kept failing).""
|
|
1231
|
+
(e.g. the one the single agent kept failing). agents="auto" sizes to the server's
|
|
1232
|
+
detected concurrency (hammer a big box, tone down a small one)."""
|
|
1232
1233
|
self._swarm = {"active": True}
|
|
1233
1234
|
head = ("⚙ the harness is escalating to a parallel swarm" if auto
|
|
1234
1235
|
else f"⇶ /swarm launching: {objective!r}")
|
|
1235
|
-
|
|
1236
|
+
count = f"{agents}" if isinstance(agents, int) else "auto-sized"
|
|
1237
|
+
self._info(f"{head} — {count} agents exploring in isolated worktrees "
|
|
1236
1238
|
"(in-process, your working tree is left alone). Esc to stop.")
|
|
1237
1239
|
self.run_worker(
|
|
1238
1240
|
lambda: self._swarm_worker(cwd, objective, agents, verify_cmd), thread=True)
|
|
1239
1241
|
|
|
1240
|
-
def _swarm_worker(self, cwd: str, objective: str, agents: int,
|
|
1242
|
+
def _swarm_worker(self, cwd: str, objective: str, agents: "int | str",
|
|
1241
1243
|
verify_cmd: str | None = None) -> None:
|
|
1242
1244
|
"""Off-thread: drive run_swarm, streaming milestones into this session's transcript."""
|
|
1243
1245
|
from drydock import swarm as swarmmod
|
|
@@ -2011,7 +2013,7 @@ class DrydockApp(App):
|
|
|
2011
2013
|
already_escalated=self._auto_swarmed)):
|
|
2012
2014
|
self._auto_swarmed = True
|
|
2013
2015
|
self._info(f"⚙ That check has failed {streak}× — escalating.")
|
|
2014
|
-
self._launch_swarm(cwd, goal,
|
|
2016
|
+
self._launch_swarm(cwd, goal, "auto", verify_cmd=vcmd, auto=True)
|
|
2015
2017
|
elif not self._offered_ratchet:
|
|
2016
2018
|
self._offered_ratchet = True
|
|
2017
2019
|
self._ratchet_offer = {"goal": goal, "verify": vcmd}
|
|
@@ -9,6 +9,7 @@ drydock/agent.py
|
|
|
9
9
|
drydock/bash_safety.py
|
|
10
10
|
drydock/bottleneck.py
|
|
11
11
|
drydock/budget.py
|
|
12
|
+
drydock/capacity.py
|
|
12
13
|
drydock/cci.py
|
|
13
14
|
drydock/cli.py
|
|
14
15
|
drydock/compaction.py
|
|
@@ -106,6 +107,7 @@ tests/test_bash_timeout_network.py
|
|
|
106
107
|
tests/test_bash_timeout_param.py
|
|
107
108
|
tests/test_bottleneck.py
|
|
108
109
|
tests/test_budget.py
|
|
110
|
+
tests/test_capacity.py
|
|
109
111
|
tests/test_cci.py
|
|
110
112
|
tests/test_cli_agents.py
|
|
111
113
|
tests/test_comms.py
|
|
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|
|
7
7
|
# PyPI distribution name is drydock-cli (the established install name, continued
|
|
8
8
|
# from the retired v2 fork); the import package + CLI command stay `drydock`.
|
|
9
9
|
name = "drydock-cli"
|
|
10
|
-
version = "3.1.
|
|
10
|
+
version = "3.1.28"
|
|
11
11
|
description = "Drydock — a local, provider-agnostic terminal coding agent for local LLMs"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.11"
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Tests for swarm capacity detection (drydock/capacity.py) — no live server needed."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from drydock import capacity
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_swarm_size_bounds_are_a_min():
|
|
11
|
+
# small box: concurrency binds → tone down
|
|
12
|
+
assert capacity.swarm_size(2, task_demand=6) == 2
|
|
13
|
+
# big box: task-demand binds (never wasteful cloning past distinct approaches)
|
|
14
|
+
assert capacity.swarm_size(64, task_demand=6) == 6
|
|
15
|
+
# budget caps below both
|
|
16
|
+
assert capacity.swarm_size(64, task_demand=6, budget_agents=3) == 3
|
|
17
|
+
# hard_max ceiling
|
|
18
|
+
assert capacity.swarm_size(9999, task_demand=9999, hard_max=32) == 32
|
|
19
|
+
assert capacity.swarm_size(0) == 1 # never zero
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_slots_concurrency_reads_llamacpp_slots():
|
|
23
|
+
assert capacity.slots_concurrency("http://h:8000/v1",
|
|
24
|
+
getter=lambda u: [{"id": 0}, {"id": 1}]) == 2
|
|
25
|
+
assert capacity.slots_concurrency("http://h:8000/v1", getter=lambda u: None) is None
|
|
26
|
+
assert capacity.slots_concurrency("http://h:8000/v1", getter=lambda u: []) is None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_slots_url_strips_v1():
|
|
30
|
+
seen = {}
|
|
31
|
+
|
|
32
|
+
def getter(u):
|
|
33
|
+
seen["url"] = u
|
|
34
|
+
return [{"id": 0}]
|
|
35
|
+
|
|
36
|
+
capacity.slots_concurrency("http://box:8000/v1", getter=getter)
|
|
37
|
+
assert seen["url"] == "http://box:8000/slots"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_detect_prefers_explicit_override(monkeypatch):
|
|
41
|
+
# config override wins over everything (operator knows their 8-GPU box)
|
|
42
|
+
assert capacity.detect_concurrency("http://x/v1", config={"swarm_concurrency": 32}) == 32
|
|
43
|
+
# env override
|
|
44
|
+
monkeypatch.setenv("DRYDOCK_SWARM_CONCURRENCY", "24")
|
|
45
|
+
assert capacity.detect_concurrency("http://x/v1", config={}) == 24
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_detect_order_slots_then_probe_then_default(monkeypatch):
|
|
49
|
+
monkeypatch.delenv("DRYDOCK_SWARM_CONCURRENCY", raising=False)
|
|
50
|
+
# slots hit
|
|
51
|
+
monkeypatch.setattr(capacity, "slots_concurrency", lambda u, **k: 3)
|
|
52
|
+
assert capacity.detect_concurrency("http://x/v1", model="m", config={}) == 3
|
|
53
|
+
# no slots → empirical probe
|
|
54
|
+
monkeypatch.setattr(capacity, "slots_concurrency", lambda u, **k: None)
|
|
55
|
+
monkeypatch.setattr(capacity, "probe_concurrency", lambda u, m, **k: 7)
|
|
56
|
+
assert capacity.detect_concurrency("http://x/v1", model="m", config={}) == 7
|
|
57
|
+
# no slots, no model (can't probe) → default
|
|
58
|
+
assert capacity.detect_concurrency("http://x/v1", model="", config={}) == capacity.DEFAULT_CONCURRENCY
|
|
59
|
+
assert capacity.detect_concurrency("http://x/v1", model="", config={"swarm_concurrency_default": 9}) == 9
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_probe_detects_parallel_vs_serial():
|
|
63
|
+
# parallel server: burst finishes in ~one request's time → P near burst
|
|
64
|
+
def parallel_req():
|
|
65
|
+
time.sleep(0.03)
|
|
66
|
+
|
|
67
|
+
p = capacity.probe_concurrency("http://x/v1", "m", burst=8, requester=parallel_req)
|
|
68
|
+
assert p >= 4 # detected substantial parallelism
|
|
69
|
+
|
|
70
|
+
# serialized server: a lock forces one-at-a-time → P collapses toward 1
|
|
71
|
+
lock = threading.Lock()
|
|
72
|
+
|
|
73
|
+
def serial_req():
|
|
74
|
+
with lock:
|
|
75
|
+
time.sleep(0.03)
|
|
76
|
+
|
|
77
|
+
s = capacity.probe_concurrency("http://x/v1", "m", burst=8, requester=serial_req)
|
|
78
|
+
assert s <= 3 # detected near-serial
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
variant EXECUTOR is injected, so the whole generation loop — selection, the QD
|
|
3
3
|
archive, escalation, and the stop conditions — is driven deterministically with
|
|
4
4
|
no subprocesses or network."""
|
|
5
|
+
from drydock import eratchet as erx
|
|
5
6
|
from drydock.eratchet import (
|
|
6
7
|
VariantOutcome,
|
|
7
8
|
_run_generation_parallel,
|
|
9
|
+
parse_eratchet,
|
|
8
10
|
run_eratchet,
|
|
9
11
|
)
|
|
10
12
|
|
|
@@ -183,3 +185,50 @@ def test_contrastive_pairs_from_captured_records():
|
|
|
183
185
|
assert len(pairs) == 1 # only the same-base pair
|
|
184
186
|
assert pairs[0]["chosen"]["reward"] == 5 and pairs[0]["rejected"]["reward"] == 3
|
|
185
187
|
assert pairs[0]["margin"] == 2
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ── blackboard graft: eratchet variants read prior attempts (share=True) ─────
|
|
191
|
+
def test_peer_notes_ranks_best_first_and_reads_summary():
|
|
192
|
+
log = [(1, 2, 8, "tried a hash map"), (1, 5, 8, "tried memoization"),
|
|
193
|
+
(2, 0, 8, "")]
|
|
194
|
+
notes = erx._peer_notes_erx(log)
|
|
195
|
+
lines = notes.splitlines()
|
|
196
|
+
assert "5/8" in lines[0] and "memoization" in lines[0] # best score first
|
|
197
|
+
assert "(no summary)" in notes # empty summary handled
|
|
198
|
+
assert erx._last_msg([{"role": "user", "content": "x"},
|
|
199
|
+
{"role": "assistant", "content": "did the thing\nline2"}]) == "did the thing"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_variant_prompt_appends_peer_notes_only_when_present():
|
|
203
|
+
prog = {"passed": 2, "total": 8}
|
|
204
|
+
base = erx._variant_prompt("goal", {"mode": "continue"}, prog, None, "")
|
|
205
|
+
assert "WHAT OTHER VARIANTS" not in base
|
|
206
|
+
withn = erx._variant_prompt("goal", {"mode": "continue", "peer_notes": "- gen1 [3/8]: X"},
|
|
207
|
+
prog, None, "")
|
|
208
|
+
assert "WHAT OTHER VARIANTS" in withn and "gen1 [3/8]: X" in withn
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_share_feeds_prior_attempts_into_later_generations():
|
|
212
|
+
seen = []
|
|
213
|
+
|
|
214
|
+
def runner(base, server, spec, xplan):
|
|
215
|
+
seen.append(spec.get("peer_notes"))
|
|
216
|
+
return VariantOutcome(spec, server, 2, 8, ref="r", descriptor=_desc(2),
|
|
217
|
+
messages=[{"role": "assistant", "content": f"approach {server}"}])
|
|
218
|
+
|
|
219
|
+
seen.clear()
|
|
220
|
+
run_eratchet("goal", servers=["s1"], runner=runner, effort="high",
|
|
221
|
+
max_generations=2, share=True)
|
|
222
|
+
# gen 1 is blind (no notes); a later generation must have received prior attempts' notes
|
|
223
|
+
assert seen[0] is None
|
|
224
|
+
assert any(n and "gen1" in n for n in seen), "later variants never saw the blackboard"
|
|
225
|
+
|
|
226
|
+
seen.clear()
|
|
227
|
+
run_eratchet("goal", servers=["s1"], runner=runner, effort="high",
|
|
228
|
+
max_generations=2, share=False)
|
|
229
|
+
assert all(n is None for n in seen) # baseline arm = blind (no notes ever)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def test_parse_eratchet_share_flag():
|
|
233
|
+
assert parse_eratchet(["fix", "the", "bug", "--share"])["share"] is True
|
|
234
|
+
assert parse_eratchet(["fix", "the", "bug"])["share"] is False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|