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.
Files changed (216) hide show
  1. {drydock_cli-3.1.27/drydock_cli.egg-info → drydock_cli-3.1.28}/PKG-INFO +1 -1
  2. drydock_cli-3.1.28/drydock/capacity.py +135 -0
  3. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/config.py +6 -0
  4. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/eratchet.py +60 -8
  5. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/swarm.py +18 -4
  6. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/tui/app.py +8 -6
  7. {drydock_cli-3.1.27 → drydock_cli-3.1.28/drydock_cli.egg-info}/PKG-INFO +1 -1
  8. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock_cli.egg-info/SOURCES.txt +2 -0
  9. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/pyproject.toml +1 -1
  10. drydock_cli-3.1.28/tests/test_capacity.py +78 -0
  11. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_eratchet.py +49 -0
  12. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/LICENSE +0 -0
  13. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/NOTICE +0 -0
  14. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/README.md +0 -0
  15. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/__init__.py +0 -0
  16. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/__main__.py +0 -0
  17. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/advisor.py +0 -0
  18. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/agent.py +0 -0
  19. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/bash_safety.py +0 -0
  20. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/bottleneck.py +0 -0
  21. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/budget.py +0 -0
  22. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/__init__.py +0 -0
  23. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/document-canvas.md +0 -0
  24. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/fiar-assess.md +0 -0
  25. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/fiar-cap.md +0 -0
  26. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/fiar-evidence.md +0 -0
  27. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/fiar-readiness.md +0 -0
  28. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/ml-data.md +0 -0
  29. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/ml-debug.md +0 -0
  30. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/ml-finetune.md +0 -0
  31. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/ml-metrics.md +0 -0
  32. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/ml-rl.md +0 -0
  33. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/ml-train.md +0 -0
  34. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/nist-ai-rmf.md +0 -0
  35. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/nist-csf.md +0 -0
  36. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/rmf-categorize.md +0 -0
  37. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/rmf-control.md +0 -0
  38. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/rmf-poam.md +0 -0
  39. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/rmf-review.md +0 -0
  40. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/stig-assess.md +0 -0
  41. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/builtin_skills/stig-remediate.md +0 -0
  42. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/cci.py +0 -0
  43. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/cli.py +0 -0
  44. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/comms/__init__.py +0 -0
  45. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/comms/attention.py +0 -0
  46. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/comms/channels.py +0 -0
  47. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/comms/events.py +0 -0
  48. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/compaction.py +0 -0
  49. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/detect.py +0 -0
  50. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/doccanvas.py +0 -0
  51. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/events.py +0 -0
  52. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/extract.py +0 -0
  53. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/fiar.py +0 -0
  54. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/gittools.py +0 -0
  55. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/graphrag.py +0 -0
  56. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/groundtruth.py +0 -0
  57. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/guards.py +0 -0
  58. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/jobs.py +0 -0
  59. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/loop_detect.py +0 -0
  60. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/mcp.py +0 -0
  61. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/pdfredbox.py +0 -0
  62. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/phases.py +0 -0
  63. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/poam.py +0 -0
  64. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/predictions.py +0 -0
  65. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/progress.py +0 -0
  66. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/providers.py +0 -0
  67. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/ratchet.py +0 -0
  68. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/recipes.py +0 -0
  69. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/recovery.py +0 -0
  70. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/resume.py +0 -0
  71. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/rmf.py +0 -0
  72. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/rmf_graph.py +0 -0
  73. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/skills.py +0 -0
  74. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/stig.py +0 -0
  75. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/subagents.py +0 -0
  76. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/suggest.py +0 -0
  77. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/task_state.py +0 -0
  78. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/tool_policy.py +0 -0
  79. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/tool_registry.py +0 -0
  80. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/tool_result.py +0 -0
  81. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/tool_select.py +0 -0
  82. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/tool_validate.py +0 -0
  83. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/tools/__init__.py +0 -0
  84. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/trajectory.py +0 -0
  85. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/tui/__init__.py +0 -0
  86. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/tui/approval.py +0 -0
  87. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/tui/messages.py +0 -0
  88. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/tui/widgets.py +0 -0
  89. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/tuning.py +0 -0
  90. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/verification.py +0 -0
  91. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock/web.py +0 -0
  92. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock_cli.egg-info/dependency_links.txt +0 -0
  93. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock_cli.egg-info/entry_points.txt +0 -0
  94. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock_cli.egg-info/requires.txt +0 -0
  95. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/drydock_cli.egg-info/top_level.txt +0 -0
  96. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/setup.cfg +0 -0
  97. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_advisor.py +0 -0
  98. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_approval.py +0 -0
  99. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_back_command.py +0 -0
  100. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_bash_background.py +0 -0
  101. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_bash_binary_output.py +0 -0
  102. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_bash_crossplatform.py +0 -0
  103. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_bash_output_bounding.py +0 -0
  104. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_bash_process_group.py +0 -0
  105. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_bash_safety.py +0 -0
  106. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_bash_sanitize.py +0 -0
  107. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_bash_shell.py +0 -0
  108. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_bash_stdin.py +0 -0
  109. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_bash_stop_partial.py +0 -0
  110. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_bash_timeout_network.py +0 -0
  111. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_bash_timeout_param.py +0 -0
  112. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_bottleneck.py +0 -0
  113. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_budget.py +0 -0
  114. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_cci.py +0 -0
  115. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_cli_agents.py +0 -0
  116. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_comms.py +0 -0
  117. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_compact_command.py +0 -0
  118. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_compaction.py +0 -0
  119. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_config.py +0 -0
  120. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_config_migration.py +0 -0
  121. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_context_limit_config.py +0 -0
  122. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_context_limit_issue25.py +0 -0
  123. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_criteria_coverage.py +0 -0
  124. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_cycling_detection.py +0 -0
  125. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_declared_deps.py +0 -0
  126. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_degenerate_argument.py +0 -0
  127. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_detect.py +0 -0
  128. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_dispatch.py +0 -0
  129. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_doccanvas.py +0 -0
  130. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_e2e_connected.py +0 -0
  131. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_edit_replace_all.py +0 -0
  132. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_edit_thrash_and_compaction.py +0 -0
  133. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_effort_governor.py +0 -0
  134. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_empty_response.py +0 -0
  135. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_events.py +0 -0
  136. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_extract.py +0 -0
  137. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_failure_loop.py +0 -0
  138. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_fiar.py +0 -0
  139. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_first_run_setup.py +0 -0
  140. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_gittools.py +0 -0
  141. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_glob_edit_edges.py +0 -0
  142. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_graphify_example.py +0 -0
  143. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_graphrag.py +0 -0
  144. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_graphrag_quoted_path.py +0 -0
  145. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_graphrag_sqlite.py +0 -0
  146. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_grep_and_read_robust.py +0 -0
  147. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_groundtruth_predictions.py +0 -0
  148. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_guards_and_tools.py +0 -0
  149. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_hallucinated_tools.py +0 -0
  150. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_jobs.py +0 -0
  151. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_knowledge_tool_available.py +0 -0
  152. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_leaked_tool_call.py +0 -0
  153. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_loop_detect.py +0 -0
  154. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_mcp.py +0 -0
  155. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_model_registry.py +0 -0
  156. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_oneshot_unreachable.py +0 -0
  157. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_overthink_interrupt.py +0 -0
  158. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_pdfredbox.py +0 -0
  159. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_phases.py +0 -0
  160. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_plan_autocontinue.py +0 -0
  161. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_poam.py +0 -0
  162. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_progress.py +0 -0
  163. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_providers_unreachable.py +0 -0
  164. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_ratchet.py +0 -0
  165. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_ratchet_offer.py +0 -0
  166. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_read_index.py +0 -0
  167. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_recipes.py +0 -0
  168. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_recovery.py +0 -0
  169. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_recovery_config.py +0 -0
  170. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_recovery_integration.py +0 -0
  171. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_repeated_outcome.py +0 -0
  172. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_repetition_interrupt.py +0 -0
  173. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_resume_events.py +0 -0
  174. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_resume_restore.py +0 -0
  175. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_resume_snapshot.py +0 -0
  176. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_rmf.py +0 -0
  177. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_rmf_graph.py +0 -0
  178. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_rmf_stig_graph.py +0 -0
  179. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_rolling_plan.py +0 -0
  180. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_runaway_repetition.py +0 -0
  181. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_screenshot.py +0 -0
  182. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_server_probe.py +0 -0
  183. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_skills.py +0 -0
  184. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_sqlite_events.py +0 -0
  185. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_stall_retry.py +0 -0
  186. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_stig.py +0 -0
  187. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_stop.py +0 -0
  188. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_streaming_newlines.py +0 -0
  189. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_subagent.py +0 -0
  190. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_subagents.py +0 -0
  191. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_suggest.py +0 -0
  192. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_swarm.py +0 -0
  193. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_system_prompt_help.py +0 -0
  194. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_task_state.py +0 -0
  195. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_timeline.py +0 -0
  196. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_todo.py +0 -0
  197. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_tool_arg_coercion.py +0 -0
  198. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_tool_arg_coercion_more.py +0 -0
  199. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_tool_arg_parsing.py +0 -0
  200. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_tool_canonical.py +0 -0
  201. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_tool_policy.py +0 -0
  202. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_tool_result.py +0 -0
  203. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_tool_select.py +0 -0
  204. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_tool_validate.py +0 -0
  205. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_tools_undo.py +0 -0
  206. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_trajectory.py +0 -0
  207. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_tui.py +0 -0
  208. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_tuning.py +0 -0
  209. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_verification_gate.py +0 -0
  210. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_viewimage.py +0 -0
  211. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_vision_input.py +0 -0
  212. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_web_tools.py +0 -0
  213. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_windows_shell.py +0 -0
  214. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_worker_subagent.py +0 -0
  215. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/tests/test_write_content_coerce.py +0 -0
  216. {drydock_cli-3.1.27 → drydock_cli-3.1.28}/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.27
3
+ Version: 3.1.28
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
@@ -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
- return (
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
- op = _MODE_OP.get(mode, "exploit")
266
- if op in ("diversify", "restart"):
267
- return diversify_prompt(goal, passed, total, op)
268
- return continuation_prompt(goal, passed, total)
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 in ("--effort", "--verify", "--servers", "--fanout", "--generations",
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 = 4, base_config: dict | None = None,
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
- n = max(1, agents)
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 = 4, []
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
- self._info(f"{head} — {agents} agents exploring in isolated worktrees "
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, 4, verify_cmd=vcmd, auto=True)
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}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.1.27
3
+ Version: 3.1.28
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
@@ -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.27"
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