drydock-cli 3.1.29__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.29/drydock_cli.egg-info → drydock_cli-3.1.30}/PKG-INFO +1 -1
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/cli.py +9 -0
- 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.29 → drydock_cli-3.1.30}/drydock/ratchet.py +16 -11
- {drydock_cli-3.1.29 → drydock_cli-3.1.30/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock_cli.egg-info/SOURCES.txt +6 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/pyproject.toml +1 -1
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_capacity.py +2 -2
- 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.29 → drydock_cli-3.1.30}/tests/test_ratchet.py +13 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/LICENSE +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/NOTICE +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/README.md +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/__init__.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/__main__.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/advisor.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/agent.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/bottleneck.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/budget.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/builtin_skills/document-canvas.md +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/builtin_skills/fiar-assess.md +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/builtin_skills/fiar-cap.md +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/builtin_skills/fiar-evidence.md +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/builtin_skills/fiar-readiness.md +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/builtin_skills/ml-data.md +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/builtin_skills/ml-debug.md +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/builtin_skills/ml-finetune.md +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/builtin_skills/ml-metrics.md +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/builtin_skills/ml-rl.md +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/builtin_skills/ml-train.md +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/builtin_skills/nist-ai-rmf.md +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/builtin_skills/nist-csf.md +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/capacity.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/cci.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/comms/__init__.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/comms/attention.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/comms/channels.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/comms/events.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/compaction.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/config.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/detect.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/doccanvas.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/eratchet.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/events.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/extract.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/fiar.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/gittools.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/graphrag.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/groundtruth.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/guards.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/jobs.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/mcp.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/pdfredbox.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/phases.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/poam.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/predictions.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/progress.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/providers.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/recipes.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/recovery.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/resume.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/rmf.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/skills.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/stig.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/subagents.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/suggest.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/swarm.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/task_state.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/tool_policy.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/tool_result.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/tool_select.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/tool_validate.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/tools/__init__.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/trajectory.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/tui/app.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/tuning.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/verification.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock/web.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/setup.cfg +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_advisor.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_approval.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_back_command.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_bash_background.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_bash_binary_output.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_bash_crossplatform.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_bash_sanitize.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_bash_shell.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_bash_stdin.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_bash_stop_partial.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_bash_timeout_param.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_bottleneck.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_budget.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_cci.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_comms.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_compaction.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_config.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_context_limit_issue25.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_criteria_coverage.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_cycling_detection.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_declared_deps.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_degenerate_argument.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_detect.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_doccanvas.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_edit_replace_all.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_edit_thrash_and_compaction.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_effort_governor.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_eratchet.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_events.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_extract.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_fiar.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_gittools.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_glob_edit_edges.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_graphify_example.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_graphrag_quoted_path.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_graphrag_sqlite.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_grep_and_read_robust.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_groundtruth_predictions.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_jobs.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_knowledge_tool_available.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_mcp.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_model_registry.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_overthink_interrupt.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_pdfredbox.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_phases.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_poam.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_progress.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_ratchet_offer.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_read_index.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_recipes.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_recovery.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_recovery_config.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_recovery_integration.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_repeated_outcome.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_repetition_interrupt.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_resume_events.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_resume_restore.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_resume_snapshot.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_rmf.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_rolling_plan.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_screenshot.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_skills.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_sqlite_events.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_stall_retry.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_stig.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_stop.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_subagent.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_subagents.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_suggest.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_swarm.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_task_state.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_timeline.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_todo.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_tool_arg_coercion.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_tool_arg_coercion_more.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_tool_canonical.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_tool_policy.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_tool_result.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_tool_select.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_tool_validate.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_trajectory.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_tui.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_tuning.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_verification_gate.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_viewimage.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_windows_shell.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_worker_subagent.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_write_content_coerce.py +0 -0
- {drydock_cli-3.1.29 → drydock_cli-3.1.30}/tests/test_xccdf.py +0 -0
|
@@ -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__}"
|
|
@@ -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 []
|