drydock-cli 3.1.28__tar.gz → 3.1.30__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.28/drydock_cli.egg-info → drydock_cli-3.1.30}/PKG-INFO +1 -1
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/capacity.py +61 -31
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/cli.py +9 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/config.py +5 -4
- drydock_cli-3.1.30/drydock/mission.py +471 -0
- drydock_cli-3.1.30/drydock/mission_cli.py +362 -0
- drydock_cli-3.1.30/drydock/mission_run.py +583 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/ratchet.py +16 -11
- {drydock_cli-3.1.28 → drydock_cli-3.1.30/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock_cli.egg-info/SOURCES.txt +6 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/pyproject.toml +1 -1
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_capacity.py +29 -9
- drydock_cli-3.1.30/tests/test_mission.py +136 -0
- drydock_cli-3.1.30/tests/test_mission_cli.py +155 -0
- drydock_cli-3.1.30/tests/test_mission_run.py +438 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_ratchet.py +13 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/LICENSE +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/NOTICE +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/README.md +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/__init__.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/__main__.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/advisor.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/agent.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/bottleneck.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/budget.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/builtin_skills/document-canvas.md +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/builtin_skills/fiar-assess.md +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/builtin_skills/fiar-cap.md +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/builtin_skills/fiar-evidence.md +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/builtin_skills/fiar-readiness.md +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/builtin_skills/ml-data.md +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/builtin_skills/ml-debug.md +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/builtin_skills/ml-finetune.md +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/builtin_skills/ml-metrics.md +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/builtin_skills/ml-rl.md +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/builtin_skills/ml-train.md +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/builtin_skills/nist-ai-rmf.md +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/builtin_skills/nist-csf.md +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/cci.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/comms/__init__.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/comms/attention.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/comms/channels.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/comms/events.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/compaction.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/detect.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/doccanvas.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/eratchet.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/events.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/extract.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/fiar.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/gittools.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/graphrag.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/groundtruth.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/guards.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/jobs.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/mcp.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/pdfredbox.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/phases.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/poam.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/predictions.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/progress.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/providers.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/recipes.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/recovery.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/resume.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/rmf.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/skills.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/stig.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/subagents.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/suggest.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/swarm.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/task_state.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/tool_policy.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/tool_result.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/tool_select.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/tool_validate.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/tools/__init__.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/trajectory.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/tui/app.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/tuning.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/verification.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock/web.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/setup.cfg +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_advisor.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_approval.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_back_command.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_bash_background.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_bash_binary_output.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_bash_crossplatform.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_bash_sanitize.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_bash_shell.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_bash_stdin.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_bash_stop_partial.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_bash_timeout_param.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_bottleneck.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_budget.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_cci.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_comms.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_compaction.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_config.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_context_limit_issue25.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_criteria_coverage.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_cycling_detection.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_declared_deps.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_degenerate_argument.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_detect.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_doccanvas.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_edit_replace_all.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_edit_thrash_and_compaction.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_effort_governor.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_eratchet.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_events.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_extract.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_fiar.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_gittools.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_glob_edit_edges.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_graphify_example.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_graphrag_quoted_path.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_graphrag_sqlite.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_grep_and_read_robust.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_groundtruth_predictions.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_jobs.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_knowledge_tool_available.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_mcp.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_model_registry.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_overthink_interrupt.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_pdfredbox.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_phases.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_poam.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_progress.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_ratchet_offer.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_read_index.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_recipes.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_recovery.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_recovery_config.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_recovery_integration.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_repeated_outcome.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_repetition_interrupt.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_resume_events.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_resume_restore.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_resume_snapshot.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_rmf.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_rolling_plan.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_screenshot.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_skills.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_sqlite_events.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_stall_retry.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_stig.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_stop.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_subagent.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_subagents.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_suggest.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_swarm.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_task_state.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_timeline.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_todo.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_tool_arg_coercion.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_tool_arg_coercion_more.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_tool_canonical.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_tool_policy.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_tool_result.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_tool_select.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_tool_validate.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_trajectory.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_tui.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_tuning.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_verification_gate.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_viewimage.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_windows_shell.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_worker_subagent.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_write_content_coerce.py +0 -0
- {drydock_cli-3.1.28 → drydock_cli-3.1.30}/tests/test_xccdf.py +0 -0
|
@@ -6,14 +6,15 @@ agents just queue — more tokens, no more wall-clock. So "hammer the 8-GPU vLLM
|
|
|
6
6
|
down the -np 2 laptop" reduces to: estimate the server's concurrency C, then size the swarm
|
|
7
7
|
to min(C, task-demand, budget). This module estimates C.
|
|
8
8
|
|
|
9
|
-
Detection
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
2.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
Detection is FULLY AUTOMATIC — the user configures nothing. Order (first hit wins), all
|
|
10
|
+
swallow-error / never-raise:
|
|
11
|
+
1. llama.cpp — GET /slots (its `-np` parallel slots), a free exact read;
|
|
12
|
+
2. empirical ramp probe — double a burst of tiny concurrent requests until the server
|
|
13
|
+
saturates (server-agnostic; the honest signal for vLLM, whose max_num_seqs isn't exposed
|
|
14
|
+
over the OpenAI API — an 8-GPU box reads big, a -np 2 laptop reads small, with no config);
|
|
15
|
+
3. fallback — a conservative default.
|
|
16
|
+
An OPTIONAL escape hatch, checked first and needed by no one: `config['swarm_concurrency']`
|
|
17
|
+
or $DRYDOCK_SWARM_CONCURRENCY, for an operator who wants to pin it or skip probing entirely.
|
|
17
18
|
|
|
18
19
|
Stdlib-only; the probe funcs are injectable so this is testable without a live server.
|
|
19
20
|
"""
|
|
@@ -71,37 +72,66 @@ def _default_requester(base_url: str, model: str) -> Callable[[], None]:
|
|
|
71
72
|
return req
|
|
72
73
|
|
|
73
74
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
_PROBE_CACHE: dict[str, int] = {}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def clear_probe_cache() -> None:
|
|
79
|
+
"""Forget cached probe results (mainly for tests / a changed server)."""
|
|
80
|
+
_PROBE_CACHE.clear()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _safe_call(req: Callable[[], None]) -> None:
|
|
80
84
|
try:
|
|
81
|
-
t0 = time.monotonic()
|
|
82
85
|
req()
|
|
83
|
-
t1 = time.monotonic() - t0
|
|
84
86
|
except Exception: # noqa: BLE001
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _burst_wall(req: Callable[[], None], b: int) -> float:
|
|
88
91
|
start = time.monotonic()
|
|
89
92
|
try:
|
|
90
|
-
with concurrent.futures.ThreadPoolExecutor(max_workers=
|
|
91
|
-
list(ex.map(lambda _: _safe_call(req), range(
|
|
93
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=b) as ex:
|
|
94
|
+
list(ex.map(lambda _: _safe_call(req), range(b)))
|
|
92
95
|
except Exception: # noqa: BLE001
|
|
93
|
-
return 1
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
96
|
+
return -1.0
|
|
97
|
+
return time.monotonic() - start
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def probe_concurrency(base_url: str, model: str, *, max_probe: int = 64,
|
|
101
|
+
requester: Callable[[], None] | None = None, cache: bool = True) -> int:
|
|
102
|
+
"""Empirically estimate useful parallelism with ZERO configuration, by RAMPING the burst
|
|
103
|
+
until the server saturates — so an 8-GPU vLLM box is detected as big and a -np 2 laptop
|
|
104
|
+
as small, without anyone declaring anything.
|
|
105
|
+
|
|
106
|
+
One warm request costs t1; a burst of B concurrent requests finishes in wall W, so
|
|
107
|
+
parallelism P ≈ B·t1/W (≈B if fully batched, ≈1 if serialized). Ramp B = 4, 8, 16, …:
|
|
108
|
+
while the server keeps the whole burst parallel (P ≈ B) it can take more, so double B;
|
|
109
|
+
the first time it can't (P < B) is its ceiling. Cached per server (the probe costs a
|
|
110
|
+
handful of max_tokens=1 requests, run once)."""
|
|
111
|
+
if cache and base_url in _PROBE_CACHE:
|
|
112
|
+
return _PROBE_CACHE[base_url]
|
|
113
|
+
req = requester or _default_requester(base_url, model)
|
|
101
114
|
try:
|
|
115
|
+
t0 = time.monotonic()
|
|
102
116
|
req()
|
|
103
|
-
|
|
104
|
-
|
|
117
|
+
t1 = time.monotonic() - t0
|
|
118
|
+
except Exception: # noqa: BLE001 — server unreachable
|
|
119
|
+
return 1
|
|
120
|
+
if t1 <= 0:
|
|
121
|
+
result = max_probe
|
|
122
|
+
else:
|
|
123
|
+
best, burst = 1, 4
|
|
124
|
+
while burst <= max_probe:
|
|
125
|
+
wall = _burst_wall(req, burst)
|
|
126
|
+
p = burst if wall <= 0 else max(1, min(burst, round(burst * t1 / wall)))
|
|
127
|
+
best = max(best, p)
|
|
128
|
+
if p < burst * 0.75: # couldn't keep the whole burst parallel → ceiling
|
|
129
|
+
break
|
|
130
|
+
burst *= 2
|
|
131
|
+
result = best
|
|
132
|
+
if cache:
|
|
133
|
+
_PROBE_CACHE[base_url] = result
|
|
134
|
+
return result
|
|
105
135
|
|
|
106
136
|
|
|
107
137
|
def detect_concurrency(base_url: str, *, provider: str = "vllm", model: str = "",
|
|
@@ -383,6 +383,15 @@ def main():
|
|
|
383
383
|
cfg["cwd"] = os.getcwd()
|
|
384
384
|
sys.exit(swarm_run_cli(sys.argv[2:], config=cfg))
|
|
385
385
|
|
|
386
|
+
# `drydock mission <create|status|run|resume|…>` — long-horizon autonomous missions.
|
|
387
|
+
if len(sys.argv) > 1 and sys.argv[1] == "mission":
|
|
388
|
+
from drydock import config as cfgmod
|
|
389
|
+
from drydock.mission_cli import run_cli as mission_run_cli
|
|
390
|
+
cfg = cfgmod.resolve({}, cfgmod.default_config_path())
|
|
391
|
+
cfgmod.resolve_active_model(cfg)
|
|
392
|
+
cfg["cwd"] = os.getcwd()
|
|
393
|
+
sys.exit(mission_run_cli(sys.argv[2:], config=cfg))
|
|
394
|
+
|
|
386
395
|
parser = argparse.ArgumentParser(description="DryDock — local coding agent")
|
|
387
396
|
parser.add_argument(
|
|
388
397
|
"--version", "-V", action="version", version=f"drydock {__version__}"
|
|
@@ -102,10 +102,11 @@ 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
|
|
106
|
-
# (drydock/capacity.py
|
|
107
|
-
#
|
|
108
|
-
#
|
|
105
|
+
# Swarm sizing is FULLY AUTOMATIC: the harness detects the inference server's real
|
|
106
|
+
# concurrency (drydock/capacity.py — llama.cpp /slots, else an empirical ramp probe) and
|
|
107
|
+
# caps agents at it, so an 8-GPU vLLM box gets hammered and a -np 2 laptop toned down with
|
|
108
|
+
# NO configuration. swarm_concurrency is an optional pin (0 = auto-detect, the default);
|
|
109
|
+
# nobody needs to set it.
|
|
109
110
|
"swarm_concurrency": 0,
|
|
110
111
|
"swarm_concurrency_default": 4,
|
|
111
112
|
# URL substrings the web tools refuse: WebSearch drops matching results,
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
"""Long-horizon autonomous missions — durable state layer.
|
|
2
|
+
|
|
3
|
+
See docs/mission_prd.md. The architectural principle (§50/§52): *missions* run for
|
|
4
|
+
hours/days, *agents* are disposable workers doing bounded units against durable state — "do
|
|
5
|
+
not make the agent's loop longer, make the work durable." So the mission's objective, tasks,
|
|
6
|
+
experiments, knowledge, checkpoints, budgets, and event log all live OUTSIDE any model
|
|
7
|
+
context, in SQLite (§11), and survive worker crashes and Drydock restarts (§28, G1).
|
|
8
|
+
|
|
9
|
+
This module is the state layer (Phase 1 foundation): schema + `MissionStore`, a transactional,
|
|
10
|
+
model-neutral (§33) CRUD/queue API. Higher layers (planner, worker, evaluator, controller,
|
|
11
|
+
CLI) build on it. Stdlib-only (sqlite3); logging-style writes never raise.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import re
|
|
17
|
+
import sqlite3
|
|
18
|
+
import time
|
|
19
|
+
import uuid
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
# ── mission lifecycle (§8) ────────────────────────────────────────────────────
|
|
23
|
+
M_CREATED = "CREATED"
|
|
24
|
+
M_INITIALIZING = "INITIALIZING"
|
|
25
|
+
M_BASELINING = "BASELINING"
|
|
26
|
+
M_PLANNING = "PLANNING"
|
|
27
|
+
M_EXECUTING = "EXECUTING"
|
|
28
|
+
M_EVALUATING = "EVALUATING"
|
|
29
|
+
M_STRATEGIC_REVIEW = "STRATEGIC_REVIEW"
|
|
30
|
+
M_COMPLETED = "COMPLETED"
|
|
31
|
+
M_PAUSED = "PAUSED"
|
|
32
|
+
M_BLOCKED = "BLOCKED"
|
|
33
|
+
M_FAILED = "FAILED"
|
|
34
|
+
M_CANCELLED = "CANCELLED"
|
|
35
|
+
M_BUDGET_EXHAUSTED = "BUDGET_EXHAUSTED"
|
|
36
|
+
M_AWAITING_HUMAN = "AWAITING_HUMAN"
|
|
37
|
+
_TERMINAL = {M_COMPLETED, M_FAILED, M_CANCELLED, M_BUDGET_EXHAUSTED}
|
|
38
|
+
|
|
39
|
+
# ── task lifecycle (§25) ──────────────────────────────────────────────────────
|
|
40
|
+
T_PENDING = "PENDING"
|
|
41
|
+
T_READY = "READY"
|
|
42
|
+
T_LEASED = "LEASED"
|
|
43
|
+
T_RUNNING = "RUNNING"
|
|
44
|
+
T_EVALUATING = "EVALUATING"
|
|
45
|
+
T_COMPLETED = "COMPLETED"
|
|
46
|
+
T_FAILED = "FAILED"
|
|
47
|
+
T_BLOCKED = "BLOCKED"
|
|
48
|
+
T_CANCELLED = "CANCELLED"
|
|
49
|
+
|
|
50
|
+
_SCHEMA = """
|
|
51
|
+
CREATE TABLE IF NOT EXISTS missions (
|
|
52
|
+
id TEXT PRIMARY KEY, objective TEXT NOT NULL, success_criteria TEXT, status TEXT,
|
|
53
|
+
budget TEXT, config TEXT, baseline TEXT, current_metric REAL, best_metric REAL,
|
|
54
|
+
created_at REAL, updated_at REAL
|
|
55
|
+
);
|
|
56
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
57
|
+
id TEXT PRIMARY KEY, mission_id TEXT, milestone TEXT, objective TEXT, reason TEXT,
|
|
58
|
+
status TEXT, priority INTEGER, deps TEXT, inputs TEXT, constraints TEXT,
|
|
59
|
+
allowed_tools TEXT, success_criteria TEXT, assignee TEXT, lease_expires REAL,
|
|
60
|
+
attempts INTEGER DEFAULT 0, result TEXT, created_at REAL, updated_at REAL
|
|
61
|
+
);
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_mission ON tasks(mission_id, status);
|
|
63
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
64
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT, mission_id TEXT, ts REAL, type TEXT, data TEXT
|
|
65
|
+
);
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_events_mission ON events(mission_id, seq);
|
|
67
|
+
CREATE TABLE IF NOT EXISTS usage (
|
|
68
|
+
mission_id TEXT PRIMARY KEY, tokens INTEGER DEFAULT 0, wall_s REAL DEFAULT 0,
|
|
69
|
+
experiments INTEGER DEFAULT 0, failures INTEGER DEFAULT 0
|
|
70
|
+
);
|
|
71
|
+
CREATE TABLE IF NOT EXISTS knowledge (
|
|
72
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, mission_id TEXT, ts REAL, type TEXT,
|
|
73
|
+
statement TEXT, confidence REAL, sources TEXT
|
|
74
|
+
);
|
|
75
|
+
CREATE INDEX IF NOT EXISTS idx_knowledge_mission ON knowledge(mission_id, type);
|
|
76
|
+
CREATE TABLE IF NOT EXISTS reviews (
|
|
77
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, mission_id TEXT, ts REAL, summary TEXT
|
|
78
|
+
);
|
|
79
|
+
CREATE INDEX IF NOT EXISTS idx_reviews_mission ON reviews(mission_id, id);
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
# Knowledge types (§15). negative_result/failure are the "don't repeat" memory (§16).
|
|
83
|
+
K_FINDING = "finding"
|
|
84
|
+
K_ASSUMPTION = "assumption"
|
|
85
|
+
K_HYPOTHESIS = "hypothesis"
|
|
86
|
+
K_DECISION = "decision"
|
|
87
|
+
K_FAILURE = "failure"
|
|
88
|
+
K_CONSTRAINT = "constraint"
|
|
89
|
+
K_MEASUREMENT = "measurement"
|
|
90
|
+
K_NEGATIVE = "negative_result"
|
|
91
|
+
_STOP = {"the", "a", "an", "to", "of", "and", "or", "in", "on", "for", "is", "it", "this",
|
|
92
|
+
"that", "with", "as", "by", "at", "be", "make", "one", "toward", "objective"}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _now() -> float:
|
|
96
|
+
return time.time()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _j(v) -> str:
|
|
100
|
+
try:
|
|
101
|
+
return json.dumps(v, default=str)
|
|
102
|
+
except (TypeError, ValueError):
|
|
103
|
+
return "null"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _u(s) -> object:
|
|
107
|
+
try:
|
|
108
|
+
return json.loads(s) if s else None
|
|
109
|
+
except (TypeError, ValueError):
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _words(text: str) -> set[str]:
|
|
114
|
+
"""Lower-cased content words (len>2, not stop-words) — the similarity key for §16."""
|
|
115
|
+
return {w for w in re.findall(r"[a-z0-9_]+", (text or "").lower())
|
|
116
|
+
if len(w) > 2 and w not in _STOP}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class MissionStore:
|
|
120
|
+
"""SQLite-backed durable mission state. Transactional for every state change (§11);
|
|
121
|
+
the event log is append-only and immutable (§12). Model-neutral — a mission started by
|
|
122
|
+
one model resumes under another (§33)."""
|
|
123
|
+
|
|
124
|
+
def __init__(self, db_path: str | Path):
|
|
125
|
+
self.path = Path(db_path)
|
|
126
|
+
try:
|
|
127
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
128
|
+
except OSError:
|
|
129
|
+
pass
|
|
130
|
+
self.conn = sqlite3.connect(str(self.path), timeout=30, isolation_level=None)
|
|
131
|
+
self.conn.row_factory = sqlite3.Row
|
|
132
|
+
self.conn.execute("PRAGMA journal_mode=WAL")
|
|
133
|
+
self.conn.executescript(_SCHEMA)
|
|
134
|
+
|
|
135
|
+
def close(self) -> None:
|
|
136
|
+
try:
|
|
137
|
+
self.conn.close()
|
|
138
|
+
except sqlite3.Error:
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
# ── missions ──────────────────────────────────────────────────────────────
|
|
142
|
+
def create_mission(self, objective: str, *, success_criteria: dict | None = None,
|
|
143
|
+
budget: dict | None = None, config: dict | None = None,
|
|
144
|
+
mission_id: str | None = None) -> str:
|
|
145
|
+
mid = mission_id or f"mission-{uuid.uuid4().hex[:8]}"
|
|
146
|
+
now = _now()
|
|
147
|
+
with self.conn:
|
|
148
|
+
self.conn.execute(
|
|
149
|
+
"INSERT INTO missions(id,objective,success_criteria,status,budget,config,"
|
|
150
|
+
"created_at,updated_at) VALUES(?,?,?,?,?,?,?,?)",
|
|
151
|
+
(mid, objective, _j(success_criteria or {}), M_CREATED, _j(budget or {}),
|
|
152
|
+
_j(config or {}), now, now))
|
|
153
|
+
self.conn.execute("INSERT OR IGNORE INTO usage(mission_id) VALUES(?)", (mid,))
|
|
154
|
+
self.event(mid, "mission_created", objective=objective,
|
|
155
|
+
success_criteria=success_criteria or {}, budget=budget or {})
|
|
156
|
+
return mid
|
|
157
|
+
|
|
158
|
+
def get_mission(self, mission_id: str) -> dict | None:
|
|
159
|
+
r = self.conn.execute("SELECT * FROM missions WHERE id=?", (mission_id,)).fetchone()
|
|
160
|
+
if r is None:
|
|
161
|
+
return None
|
|
162
|
+
m = dict(r)
|
|
163
|
+
for k in ("success_criteria", "budget", "config", "baseline"):
|
|
164
|
+
m[k] = _u(m.get(k))
|
|
165
|
+
return m
|
|
166
|
+
|
|
167
|
+
def list_missions(self) -> list[dict]:
|
|
168
|
+
rows = self.conn.execute(
|
|
169
|
+
"SELECT id,objective,status,current_metric,best_metric,created_at "
|
|
170
|
+
"FROM missions ORDER BY created_at").fetchall()
|
|
171
|
+
return [dict(r) for r in rows]
|
|
172
|
+
|
|
173
|
+
def set_status(self, mission_id: str, status: str) -> None:
|
|
174
|
+
with self.conn:
|
|
175
|
+
self.conn.execute("UPDATE missions SET status=?,updated_at=? WHERE id=?",
|
|
176
|
+
(status, _now(), mission_id))
|
|
177
|
+
self.event(mission_id, "status", status=status)
|
|
178
|
+
|
|
179
|
+
def set_baseline(self, mission_id: str, baseline: dict) -> None:
|
|
180
|
+
with self.conn:
|
|
181
|
+
self.conn.execute("UPDATE missions SET baseline=?,updated_at=? WHERE id=?",
|
|
182
|
+
(_j(baseline), _now(), mission_id))
|
|
183
|
+
self.event(mission_id, "baseline", baseline=baseline) # immutable in history (§9)
|
|
184
|
+
|
|
185
|
+
def set_metric(self, mission_id: str, current: float) -> None:
|
|
186
|
+
"""Record the mission's current metric, tracking the best seen (§38)."""
|
|
187
|
+
with self.conn:
|
|
188
|
+
m = self.conn.execute("SELECT best_metric FROM missions WHERE id=?",
|
|
189
|
+
(mission_id,)).fetchone()
|
|
190
|
+
best = m["best_metric"] if m and m["best_metric"] is not None else None
|
|
191
|
+
new_best = current if best is None else max(best, current)
|
|
192
|
+
self.conn.execute("UPDATE missions SET current_metric=?,best_metric=?,updated_at=? "
|
|
193
|
+
"WHERE id=?", (current, new_best, _now(), mission_id))
|
|
194
|
+
|
|
195
|
+
# ── tasks / queue (§25) ─────────────────────────────────────────────────────
|
|
196
|
+
def add_task(self, mission_id: str, objective: str, *, reason: str = "", priority: int = 0,
|
|
197
|
+
deps: list[str] | None = None, milestone: str = "", inputs: dict | None = None,
|
|
198
|
+
constraints: dict | None = None, allowed_tools: list[str] | None = None,
|
|
199
|
+
success_criteria: dict | None = None) -> str:
|
|
200
|
+
tid = f"task-{uuid.uuid4().hex[:8]}"
|
|
201
|
+
now = _now()
|
|
202
|
+
status = T_READY if not deps else T_PENDING
|
|
203
|
+
with self.conn:
|
|
204
|
+
self.conn.execute(
|
|
205
|
+
"INSERT INTO tasks(id,mission_id,milestone,objective,reason,status,priority,"
|
|
206
|
+
"deps,inputs,constraints,allowed_tools,success_criteria,created_at,updated_at) "
|
|
207
|
+
"VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
|
208
|
+
(tid, mission_id, milestone, objective, reason, status, priority, _j(deps or []),
|
|
209
|
+
_j(inputs or {}), _j(constraints or {}), _j(allowed_tools or []),
|
|
210
|
+
_j(success_criteria or {}), now, now))
|
|
211
|
+
self.event(mission_id, "task_created", task=tid, objective=objective, priority=priority)
|
|
212
|
+
return tid
|
|
213
|
+
|
|
214
|
+
def get_task(self, task_id: str) -> dict | None:
|
|
215
|
+
r = self.conn.execute("SELECT * FROM tasks WHERE id=?", (task_id,)).fetchone()
|
|
216
|
+
if r is None:
|
|
217
|
+
return None
|
|
218
|
+
t = dict(r)
|
|
219
|
+
for k in ("deps", "inputs", "constraints", "allowed_tools", "success_criteria", "result"):
|
|
220
|
+
t[k] = _u(t.get(k))
|
|
221
|
+
return t
|
|
222
|
+
|
|
223
|
+
def tasks(self, mission_id: str) -> list[dict]:
|
|
224
|
+
rows = self.conn.execute(
|
|
225
|
+
"SELECT * FROM tasks WHERE mission_id=? ORDER BY priority DESC, created_at",
|
|
226
|
+
(mission_id,)).fetchall()
|
|
227
|
+
out = []
|
|
228
|
+
for r in rows:
|
|
229
|
+
t = dict(r)
|
|
230
|
+
for k in ("deps", "inputs", "constraints", "allowed_tools", "success_criteria", "result"):
|
|
231
|
+
t[k] = _u(t.get(k))
|
|
232
|
+
out.append(t)
|
|
233
|
+
return out
|
|
234
|
+
|
|
235
|
+
def _completed_ids(self, mission_id: str) -> set[str]:
|
|
236
|
+
return {r["id"] for r in self.conn.execute(
|
|
237
|
+
"SELECT id FROM tasks WHERE mission_id=? AND status=?",
|
|
238
|
+
(mission_id, T_COMPLETED)).fetchall()}
|
|
239
|
+
|
|
240
|
+
def promote_ready(self, mission_id: str) -> int:
|
|
241
|
+
"""Move PENDING tasks whose deps are all COMPLETED to READY. Returns #promoted."""
|
|
242
|
+
done = self._completed_ids(mission_id)
|
|
243
|
+
n = 0
|
|
244
|
+
with self.conn:
|
|
245
|
+
for r in self.conn.execute(
|
|
246
|
+
"SELECT id,deps FROM tasks WHERE mission_id=? AND status=?",
|
|
247
|
+
(mission_id, T_PENDING)).fetchall():
|
|
248
|
+
deps = _u(r["deps"])
|
|
249
|
+
deps = deps if isinstance(deps, list) else []
|
|
250
|
+
if all(d in done for d in deps):
|
|
251
|
+
self.conn.execute("UPDATE tasks SET status=?,updated_at=? WHERE id=?",
|
|
252
|
+
(T_READY, _now(), r["id"]))
|
|
253
|
+
n += 1
|
|
254
|
+
return n
|
|
255
|
+
|
|
256
|
+
def ready_tasks(self, mission_id: str) -> list[dict]:
|
|
257
|
+
self.promote_ready(mission_id)
|
|
258
|
+
rows = self.conn.execute(
|
|
259
|
+
"SELECT * FROM tasks WHERE mission_id=? AND status=? "
|
|
260
|
+
"ORDER BY priority DESC, created_at", (mission_id, T_READY)).fetchall()
|
|
261
|
+
return [t for t in (self.get_task(r["id"]) for r in rows) if t is not None]
|
|
262
|
+
|
|
263
|
+
def claim_task(self, task_id: str, worker: str, lease_secs: int = 300) -> bool:
|
|
264
|
+
"""Atomically lease a READY task (§26). Returns False if it isn't claimable —
|
|
265
|
+
crash-safe and swarm-ready (many workers can race for the same queue)."""
|
|
266
|
+
now = _now()
|
|
267
|
+
with self.conn:
|
|
268
|
+
cur = self.conn.execute(
|
|
269
|
+
"UPDATE tasks SET status=?,assignee=?,lease_expires=?,attempts=attempts+1,"
|
|
270
|
+
"updated_at=? WHERE id=? AND status=?",
|
|
271
|
+
(T_RUNNING, worker, now + lease_secs, now, task_id, T_READY))
|
|
272
|
+
ok = cur.rowcount == 1
|
|
273
|
+
if ok:
|
|
274
|
+
t = self.get_task(task_id)
|
|
275
|
+
if t:
|
|
276
|
+
self.event(t["mission_id"], "task_leased", task=task_id, worker=worker,
|
|
277
|
+
lease_expires=now + lease_secs)
|
|
278
|
+
return ok
|
|
279
|
+
|
|
280
|
+
def heartbeat(self, task_id: str, lease_secs: int = 300) -> None:
|
|
281
|
+
with self.conn:
|
|
282
|
+
self.conn.execute("UPDATE tasks SET lease_expires=?,updated_at=? WHERE id=?",
|
|
283
|
+
(_now() + lease_secs, _now(), task_id))
|
|
284
|
+
|
|
285
|
+
def reclaim_expired(self, mission_id: str) -> int:
|
|
286
|
+
"""Return RUNNING/LEASED tasks whose lease expired to READY (§26/§28 recovery)."""
|
|
287
|
+
now = _now()
|
|
288
|
+
n = 0
|
|
289
|
+
with self.conn:
|
|
290
|
+
for r in self.conn.execute(
|
|
291
|
+
"SELECT id FROM tasks WHERE mission_id=? AND status IN (?,?) "
|
|
292
|
+
"AND lease_expires IS NOT NULL AND lease_expires<?",
|
|
293
|
+
(mission_id, T_RUNNING, T_LEASED, now)).fetchall():
|
|
294
|
+
self.conn.execute("UPDATE tasks SET status=?,assignee=NULL,lease_expires=NULL,"
|
|
295
|
+
"updated_at=? WHERE id=?", (T_READY, now, r["id"]))
|
|
296
|
+
self.event(mission_id, "lease_expired", task=r["id"])
|
|
297
|
+
n += 1
|
|
298
|
+
return n
|
|
299
|
+
|
|
300
|
+
def complete_task(self, task_id: str, status: str, result: dict | None = None) -> None:
|
|
301
|
+
with self.conn:
|
|
302
|
+
self.conn.execute("UPDATE tasks SET status=?,result=?,assignee=NULL,"
|
|
303
|
+
"lease_expires=NULL,updated_at=? WHERE id=?",
|
|
304
|
+
(status, _j(result or {}), _now(), task_id))
|
|
305
|
+
t = self.get_task(task_id)
|
|
306
|
+
if t:
|
|
307
|
+
self.event(t["mission_id"], "task_done", task=task_id, status=status)
|
|
308
|
+
if status == T_FAILED:
|
|
309
|
+
self.add_usage(t["mission_id"], failures=1)
|
|
310
|
+
|
|
311
|
+
# ── event log (§12) — immutable, append-only ───────────────────────────────
|
|
312
|
+
def event(self, mission_id: str, type: str, **data) -> None:
|
|
313
|
+
try:
|
|
314
|
+
with self.conn:
|
|
315
|
+
self.conn.execute("INSERT INTO events(mission_id,ts,type,data) VALUES(?,?,?,?)",
|
|
316
|
+
(mission_id, _now(), type, _j(data)))
|
|
317
|
+
except sqlite3.Error:
|
|
318
|
+
pass # logging must never break the mission
|
|
319
|
+
|
|
320
|
+
def events(self, mission_id: str, limit: int = 0) -> list[dict]:
|
|
321
|
+
q = "SELECT seq,ts,type,data FROM events WHERE mission_id=? ORDER BY seq"
|
|
322
|
+
if limit:
|
|
323
|
+
q += f" DESC LIMIT {int(limit)}"
|
|
324
|
+
rows = self.conn.execute(q, (mission_id,)).fetchall()
|
|
325
|
+
out = []
|
|
326
|
+
for r in rows:
|
|
327
|
+
d = _u(r["data"])
|
|
328
|
+
d = d if isinstance(d, dict) else {}
|
|
329
|
+
out.append({"seq": r["seq"], "ts": r["ts"], "type": r["type"], **d})
|
|
330
|
+
return out[::-1] if limit else out
|
|
331
|
+
|
|
332
|
+
# ── knowledge store (§15) + negative knowledge (§16) ────────────────────────
|
|
333
|
+
def add_knowledge(self, mission_id: str, type: str, statement: str, *,
|
|
334
|
+
confidence: float = 0.5, sources: list | None = None) -> int:
|
|
335
|
+
now = _now()
|
|
336
|
+
with self.conn:
|
|
337
|
+
cur = self.conn.execute(
|
|
338
|
+
"INSERT INTO knowledge(mission_id,ts,type,statement,confidence,sources) "
|
|
339
|
+
"VALUES(?,?,?,?,?,?)", (mission_id, now, type, statement, confidence,
|
|
340
|
+
_j(sources or [])))
|
|
341
|
+
return int(cur.lastrowid or 0)
|
|
342
|
+
|
|
343
|
+
def knowledge(self, mission_id: str, type: str | None = None) -> list[dict]:
|
|
344
|
+
if type:
|
|
345
|
+
rows = self.conn.execute(
|
|
346
|
+
"SELECT * FROM knowledge WHERE mission_id=? AND type=? ORDER BY id",
|
|
347
|
+
(mission_id, type)).fetchall()
|
|
348
|
+
else:
|
|
349
|
+
rows = self.conn.execute("SELECT * FROM knowledge WHERE mission_id=? ORDER BY id",
|
|
350
|
+
(mission_id,)).fetchall()
|
|
351
|
+
out = []
|
|
352
|
+
for r in rows:
|
|
353
|
+
k = dict(r)
|
|
354
|
+
k["sources"] = _u(k.get("sources")) or []
|
|
355
|
+
out.append(k)
|
|
356
|
+
return out
|
|
357
|
+
|
|
358
|
+
def similar_knowledge(self, mission_id: str, text: str, *, type: str | None = None,
|
|
359
|
+
top: int = 5) -> list[dict]:
|
|
360
|
+
"""Retrieve prior knowledge most similar to `text` by content-word overlap (§16). A
|
|
361
|
+
cheap, model-free relevance signal — the Planner uses it to surface failed approaches
|
|
362
|
+
before repeating them; semantic embeddings are a later phase."""
|
|
363
|
+
want = _words(text)
|
|
364
|
+
if not want:
|
|
365
|
+
return []
|
|
366
|
+
scored = []
|
|
367
|
+
for k in self.knowledge(mission_id, type):
|
|
368
|
+
have = _words(k["statement"])
|
|
369
|
+
if not have:
|
|
370
|
+
continue
|
|
371
|
+
overlap = len(want & have) / len(want | have)
|
|
372
|
+
if overlap > 0:
|
|
373
|
+
scored.append((overlap, k))
|
|
374
|
+
scored.sort(key=lambda x: x[0], reverse=True)
|
|
375
|
+
return [k for _, k in scored[:top]]
|
|
376
|
+
|
|
377
|
+
# ── strategic reviews (§24) ─────────────────────────────────────────────────
|
|
378
|
+
def record_review(self, mission_id: str, summary: dict) -> int:
|
|
379
|
+
with self.conn:
|
|
380
|
+
cur = self.conn.execute(
|
|
381
|
+
"INSERT INTO reviews(mission_id,ts,summary) VALUES(?,?,?)",
|
|
382
|
+
(mission_id, _now(), _j(summary)))
|
|
383
|
+
return int(cur.lastrowid or 0)
|
|
384
|
+
|
|
385
|
+
def reviews(self, mission_id: str) -> list[dict]:
|
|
386
|
+
rows = self.conn.execute("SELECT * FROM reviews WHERE mission_id=? ORDER BY id",
|
|
387
|
+
(mission_id,)).fetchall()
|
|
388
|
+
out = []
|
|
389
|
+
for r in rows:
|
|
390
|
+
d = dict(r)
|
|
391
|
+
d["summary"] = _u(d.get("summary")) or {}
|
|
392
|
+
out.append(d)
|
|
393
|
+
return out
|
|
394
|
+
|
|
395
|
+
def last_review_ts(self, mission_id: str) -> float:
|
|
396
|
+
"""ts of the most recent review, else the mission's created_at (so the first
|
|
397
|
+
elapsed-time trigger measures from mission start). Survives resume (§28)."""
|
|
398
|
+
row = self.conn.execute(
|
|
399
|
+
"SELECT MAX(ts) AS t FROM reviews WHERE mission_id=?", (mission_id,)).fetchone()
|
|
400
|
+
if row and row["t"] is not None:
|
|
401
|
+
return float(row["t"])
|
|
402
|
+
m = self.get_mission(mission_id)
|
|
403
|
+
return float(m["created_at"]) if m else _now()
|
|
404
|
+
|
|
405
|
+
def experiments_since(self, mission_id: str, ts: float) -> int:
|
|
406
|
+
"""Count of KEEP/REVERT experiments recorded after `ts` — the task-count review
|
|
407
|
+
trigger (§24). Derived from the durable event log, so it is resume-correct."""
|
|
408
|
+
row = self.conn.execute(
|
|
409
|
+
"SELECT COUNT(*) AS n FROM events WHERE mission_id=? AND type='experiment' AND ts>?",
|
|
410
|
+
(mission_id, ts)).fetchone()
|
|
411
|
+
return int(row["n"]) if row else 0
|
|
412
|
+
|
|
413
|
+
# ── budgets / usage (§29/§30) ───────────────────────────────────────────────
|
|
414
|
+
def add_usage(self, mission_id: str, *, tokens: int = 0, wall_s: float = 0.0,
|
|
415
|
+
experiments: int = 0, failures: int = 0) -> None:
|
|
416
|
+
with self.conn:
|
|
417
|
+
self.conn.execute(
|
|
418
|
+
"UPDATE usage SET tokens=tokens+?,wall_s=wall_s+?,experiments=experiments+?,"
|
|
419
|
+
"failures=failures+? WHERE mission_id=?",
|
|
420
|
+
(tokens, wall_s, experiments, failures, mission_id))
|
|
421
|
+
|
|
422
|
+
def usage(self, mission_id: str) -> dict:
|
|
423
|
+
r = self.conn.execute("SELECT * FROM usage WHERE mission_id=?", (mission_id,)).fetchone()
|
|
424
|
+
return dict(r) if r else {"tokens": 0, "wall_s": 0.0, "experiments": 0, "failures": 0}
|
|
425
|
+
|
|
426
|
+
def budget_exhausted(self, mission_id: str) -> tuple[bool, str]:
|
|
427
|
+
"""True + reason if any finite budget is spent (§30). Wall-clock is measured from
|
|
428
|
+
mission creation; the others from recorded usage."""
|
|
429
|
+
m = self.get_mission(mission_id)
|
|
430
|
+
if not m:
|
|
431
|
+
return False, ""
|
|
432
|
+
b = m.get("budget") or {}
|
|
433
|
+
u = self.usage(mission_id)
|
|
434
|
+
wall_h = (_now() - (m.get("created_at") or _now())) / 3600.0
|
|
435
|
+
checks = [
|
|
436
|
+
("wall_time_hours", wall_h, "wall-clock budget"),
|
|
437
|
+
("token_budget", u["tokens"], "token budget"),
|
|
438
|
+
("max_experiments", u["experiments"], "experiment budget"),
|
|
439
|
+
("max_failures", u["failures"], "failure budget"),
|
|
440
|
+
]
|
|
441
|
+
for key, spent, label in checks:
|
|
442
|
+
cap = b.get(key)
|
|
443
|
+
if cap and spent >= cap:
|
|
444
|
+
return True, f"{label} reached ({spent:.0f} ≥ {cap})"
|
|
445
|
+
return False, ""
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
# ── workspace + factory (§40) ─────────────────────────────────────────────────
|
|
449
|
+
def missions_dir(cwd: str | Path) -> Path:
|
|
450
|
+
return Path(cwd) / ".drydock" / "missions"
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def open_store(cwd: str | Path, mission_id: str) -> MissionStore:
|
|
454
|
+
return MissionStore(missions_dir(cwd) / mission_id / "state.db")
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def create_mission(cwd: str | Path, objective: str, *, success_criteria: dict | None = None,
|
|
458
|
+
budget: dict | None = None, config: dict | None = None) -> tuple[str, MissionStore]:
|
|
459
|
+
"""Create a mission with its own workspace under <cwd>/.drydock/missions/<id>/ (§40)."""
|
|
460
|
+
mid = f"mission-{uuid.uuid4().hex[:8]}"
|
|
461
|
+
store = MissionStore(missions_dir(cwd) / mid / "state.db")
|
|
462
|
+
store.create_mission(objective, success_criteria=success_criteria, budget=budget,
|
|
463
|
+
config=config, mission_id=mid)
|
|
464
|
+
return mid, store
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def list_missions(cwd: str | Path) -> list[str]:
|
|
468
|
+
try:
|
|
469
|
+
return sorted(p.name for p in missions_dir(cwd).iterdir() if p.is_dir())
|
|
470
|
+
except OSError:
|
|
471
|
+
return []
|