blq-cli 1.2.2__tar.gz → 2.0.0__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.
- {blq_cli-1.2.2 → blq_cli-2.0.0}/CHANGELOG.md +128 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/PKG-INFO +2 -2
- blq_cli-2.0.0/docs/slurm.md +94 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/mkdocs.yml +1 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/pyproject.toml +13 -2
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/__init__.py +2 -1
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/commands/core.py +40 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/commands/execution.py +40 -5
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/commands/registry.py +5 -1
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/commands/watch_cmd.py +1 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/ext/__init__.py +1 -2
- blq_cli-2.0.0/src/blq/ext/slurm_executor.py +175 -0
- blq_cli-2.0.0/src/blq/ext/slurm_policy.py +133 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/serve.py +510 -183
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/conftest.py +30 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_config_consumption.py +7 -5
- blq_cli-2.0.0/tests/test_ext_slurm_executor.py +380 -0
- blq_cli-2.0.0/tests/test_ext_slurm_policy.py +252 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_import_capture_storage.py +3 -10
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_inspect.py +4 -3
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_log_content_persistence.py +2 -1
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_mcp_server.py +6 -5
- blq_cli-2.0.0/tests/test_output_schemas.py +177 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_security_containment.py +6 -4
- blq_cli-2.0.0/tests/test_slurm_integration.py +521 -0
- blq_cli-2.0.0/tests/test_strict_root_resolution.py +320 -0
- blq_cli-1.2.2/tests/test_output_schemas.py +0 -90
- {blq_cli-1.2.2 → blq_cli-2.0.0}/.claude/hooks/blq-suggest.sh +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/.github/workflows/ci.yml +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/.github/workflows/docs.yml +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/.github/workflows/publish.yml +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/.gitignore +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/.mcp.json +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/.readthedocs.yml +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/AGENT.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/AGENTS.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/CLAUDE.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/CONTRIBUTING.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/README.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/SKILL.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/api-reference.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/commands/capture.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/commands/ci.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/commands/completions.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/commands/errors.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/commands/exec.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/commands/filter.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/commands/index.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/commands/init.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/commands/maintenance.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/commands/query.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/commands/registry.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/commands/report.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/commands/run.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/commands/sql.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/commands/status.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/commands/sync.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/commands/watch.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/design/design-bird-migration.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/design/design-commands.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/design/design-config-command.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/design/design-extensions.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/design/design-git-integration.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/design/design-hooks-v2.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/design/design-live-inspection.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/design/design-parameterized-commands.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/design/design-run-args.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/design/design-sandbox-specs.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/design/design-sync.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/design/design-track-save.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/design/duck-hunt-v2-migration.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/design/duck-hunt-v3-migration.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/design/proposal-bird-v5.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/design/skill-inspect-enrichment.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/getting-started.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/index.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/integration.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/mcp.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/plans/explore-bird-sqlite-backend.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/plans/explore-nsjail-python-wrapper.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/plans/explore-nsjail-spack-package.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/plans/patterns-integration-prompt.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/plans/roadmap-to-1.0.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/plugin-guide.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/python-api.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/query-guide.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/requirements.txt +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/sandbox.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/schema-stability.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/schema.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/superpowers/plans/2026-03-22-extension-system.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/superpowers/plans/2026-03-28-bwrap-engine.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/superpowers/plans/2026-03-28-command-locks.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/superpowers/plans/2026-03-29-annotators-and-tighten.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/superpowers/plans/2026-03-29-service-layer-phase1.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/superpowers/plans/2026-03-29-strace-profiling.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/superpowers/specs/2026-03-22-extension-system-design.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/docs/superpowers/specs/2026-03-29-unified-service-layer-design.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/experiments/agent-build-test.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/__main__.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/bird.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/bird_schema.sql +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/cli.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/commands/README.md +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/commands/__init__.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/commands/ci_cmd.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/commands/clean_cmd.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/commands/config_cmd.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/commands/events.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/commands/hooks_cmd.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/commands/hooks_gen.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/commands/init_cmd.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/commands/management.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/commands/management_cmd.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/commands/mcp_cmd.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/commands/migrate.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/commands/query_cmd.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/commands/record_cmd.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/commands/report_cmd.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/commands/sandbox_cmd.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/commands/serve_cmd.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/commands/sync_cmd.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/config_format.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/ext/annotator.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/ext/discovery.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/ext/local_executor.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/ext/pipeline.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/git.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/github.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/locks.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/output.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/query.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/runtime.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/schema.sql +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/services/__init__.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/services/execution.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/services/inspect.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/services/query.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/services/refs.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/storage.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/templates/drone.yml.j2 +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/templates/git_hook.sh.j2 +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/templates/github_workflow.yml.j2 +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/templates/gitlab_ci.yml.j2 +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/templates/hook_script.sh.j2 +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq/user_config.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq_sandbox/__init__.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq_sandbox/engines.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq_sandbox/profile.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq_sandbox/source_annotator.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq_sandbox/spec.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq_sandbox/strace_parser.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq_sandbox/tighten.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq_sandbox/violations.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq_sandbox_bwrap/__init__.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq_sandbox_bwrap/args.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/src/blq_sandbox_systemd/__init__.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/__init__.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/sql/test_duck_hunt_v2_migration.sql +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_annotator.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_attempts_outcomes.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_auto_init.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_autoprune.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_bird.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_bwrap_args.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_bwrap_engine.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_ci.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_ci_generators.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_command_args.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_command_lock_field.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_core.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_execution_locks.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_exit_code_reason.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_ext_discovery.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_ext_integration.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_ext_local_executor.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_ext_pipeline.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_ext_types.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_git.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_hooks.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_hooks_gen.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_json_null_filter.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_locks.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_mcp_ci_tools.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_mcp_merge.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_phase1_structured_output.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_phase2_command_registry.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_query_api.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_query_filter.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_record_invocation.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_report.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_runtime_config.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_sandbox.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_sandbox_cmd.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_sandbox_events.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_sandbox_ext.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_sandbox_profile.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_sandbox_register.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_sandbox_systemd.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_sandbox_tighten.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_sandbox_violations.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_schema_repair.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_services_execution.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_services_inspect.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_services_query.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_services_refs.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_source_annotator.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_storage.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_storage_prune.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_strace_parser.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_user_config.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_version.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/tests/test_watch.py +0 -0
- {blq_cli-1.2.2 → blq_cli-2.0.0}/uv.lock +0 -0
|
@@ -1,5 +1,133 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2.0.0
|
|
4
|
+
|
|
5
|
+
### Fixed (breaking) — a run and the read of it now agree on the workspace (#55)
|
|
6
|
+
|
|
7
|
+
`config(active_root=...)` steered only the read tools. `_run_impl` and
|
|
8
|
+
`_exec_impl` passed no `cwd` to their child process, so `blq exec`/`blq run`
|
|
9
|
+
walked up from the *server's* directory to find `.bird/` and recorded the run
|
|
10
|
+
there, whatever root the caller had named. `_get_storage()` honoured
|
|
11
|
+
`active_root` and read a different workspace. A run went into one store and was
|
|
12
|
+
looked for in another.
|
|
13
|
+
|
|
14
|
+
Two more places resolved from the process cwd, and both matter once a call can
|
|
15
|
+
name its own workspace:
|
|
16
|
+
|
|
17
|
+
* `BlqConfig.find()` supplied the registered-command registry, so a prefix
|
|
18
|
+
match on `exec(command=..., cwd=X)` would reroute through whatever the
|
|
19
|
+
*server's* workspace had registered under that prefix and run that
|
|
20
|
+
definition instead;
|
|
21
|
+
* the same lookup supplied `mcp.disabled_tools`, so a call aimed at project X
|
|
22
|
+
ran under a policy file belonging to another project;
|
|
23
|
+
* `_clean_impl` walked up from `Path.cwd()` with its own loop. `clean` deletes
|
|
24
|
+
directories, so a call aimed at one project could clear another's `.bird/`.
|
|
25
|
+
|
|
26
|
+
Every workspace tool now takes `cwd=`, and the resolution order is the same
|
|
27
|
+
everywhere with no third step:
|
|
28
|
+
|
|
29
|
+
1. the `cwd=` argument,
|
|
30
|
+
2. the session's `active_root`,
|
|
31
|
+
3. otherwise the call is refused.
|
|
32
|
+
|
|
33
|
+
**This breaks bare calls**, which is the point: a wrong-workspace answer
|
|
34
|
+
becomes a loud failure instead of a confident one. Pass `cwd=`, or set the root
|
|
35
|
+
once with `config(set={"active_root": "..."})`, or seed `BLQ_ACTIVE_ROOT` in the
|
|
36
|
+
launch environment. The retritis plugin seeds it from `${PWD}`.
|
|
37
|
+
|
|
38
|
+
The refusal **raises** rather than returning an error dict. Every tool declares
|
|
39
|
+
a TypedDict return and fastmcp coerces the value to that schema, so an error
|
|
40
|
+
dict arrived as `{"sources": None}` with its own keys dropped — a refusal
|
|
41
|
+
indistinguishable from an empty answer. Raising matches `_check_tool_enabled`,
|
|
42
|
+
which is already how this module refuses a call.
|
|
43
|
+
|
|
44
|
+
Ordering inside each tool is server-level policy, then argument validation,
|
|
45
|
+
then the root, then the workspace's own policy. The cached server-level set
|
|
46
|
+
(CLI flags, env, `safe_mode`) is still checked first, so a caller cannot point
|
|
47
|
+
`cwd=` at a permissive project to escape it.
|
|
48
|
+
|
|
49
|
+
`root` is a **required keyword-only** parameter on the 29 helpers that reach a
|
|
50
|
+
store or a config, so a missed call site is a `TypeError` and a mypy error
|
|
51
|
+
rather than a silent read of the server's workspace. mypy found all 19
|
|
52
|
+
`_get_storage()` sites that way. The MCP resources and prompts pass
|
|
53
|
+
`root=None` explicitly: a `blq://` URI cannot carry a workspace, so they keep
|
|
54
|
+
the cwd fallback on purpose.
|
|
55
|
+
|
|
56
|
+
Both sides of the store lookup now walk up through `BlqConfig`, so a `cwd=`
|
|
57
|
+
that names a subdirectory resolves to the same `.bird/` the child process
|
|
58
|
+
finds.
|
|
59
|
+
|
|
60
|
+
Verified: 12 new tests, measured red before the change. The decisive one is a
|
|
61
|
+
round trip — a run made through workspace X is read back through X, *and* the
|
|
62
|
+
server's own workspace is asserted to have gained no run, because a `pwd`
|
|
63
|
+
check passes while the two halves still disagree. Suite: 1373 passed before,
|
|
64
|
+
1385 after, with a failure set identical to the baseline's 11. mypy unchanged
|
|
65
|
+
at its 14 pre-existing errors; `ruff check` on `serve.py` clean.
|
|
66
|
+
|
|
67
|
+
### Fixed — 10 MCP tools were deleting keys they returned
|
|
68
|
+
|
|
69
|
+
fastmcp coerces a tool's value to its declared `outputSchema`, so a key the
|
|
70
|
+
impl returns but the TypedDict omits is **removed before the caller sees it**.
|
|
71
|
+
The TypedDicts added in #47 were incomplete, which made that silent data loss
|
|
72
|
+
rather than a loose type.
|
|
73
|
+
|
|
74
|
+
Measured 2026-11-03, by calling each impl and comparing its top-level keys
|
|
75
|
+
against its declared schema:
|
|
76
|
+
|
|
77
|
+
| tool | keys the caller never received |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `inspect` | all 13 of the single-event shape — `message`, `severity`, `ref_file`, `ref_line`, `log_context`, … |
|
|
80
|
+
| `output` | `content` — the tool whose purpose is returning captured text returned none |
|
|
81
|
+
| `exec` | `cmd`, `duration_sec`, `output_stats`, `preview`, and `matched_command` on a reroute |
|
|
82
|
+
| `register_command` | `existing`, `command`, `format_detected` |
|
|
83
|
+
| `ci_check` | `has_new_errors` — the schema declared `has_errors`, which the impl does not return |
|
|
84
|
+
| `run` | `extension_data`, `output_tail` |
|
|
85
|
+
|
|
86
|
+
**This was the cause of all 11 of the suite's failing tests.** They assert on
|
|
87
|
+
keys like `result["existing"]` and `result["message"]`, which arrived deleted.
|
|
88
|
+
Completing the schemas took the suite from 11 failed / 1385 passed to **1400
|
|
89
|
+
passed**, with no change to any implementation.
|
|
90
|
+
|
|
91
|
+
Two of the types were wrong in the first attempt, and the schema caught both
|
|
92
|
+
loudly rather than silently: `recommendation` is a dict carrying
|
|
93
|
+
`message`/`suggested_call`/`reason`, not a string, and `log_context` is
|
|
94
|
+
nullable. Every single-event `inspect` field comes from
|
|
95
|
+
`_to_json_safe(event_data.get(...))`, so each one is now declared nullable.
|
|
96
|
+
|
|
97
|
+
`tests/test_output_schemas.py` gains the direction it was missing. Its existing
|
|
98
|
+
tests assert that a *declared* key is published; they cannot see a *returned*
|
|
99
|
+
key that is undeclared. Four new tests pin the keys that were lost, measured
|
|
100
|
+
red against the schemas as #47 left them.
|
|
101
|
+
|
|
102
|
+
### Fixed — mypy is clean
|
|
103
|
+
|
|
104
|
+
The 14 remaining errors were all one pattern: a tool declares a TypedDict while
|
|
105
|
+
its impl returns `dict[str, Any]`. Each now casts at the tool boundary, which
|
|
106
|
+
is honest only because the schemas above are complete — a cast onto an
|
|
107
|
+
incomplete TypedDict would have asserted a shape that drops data.
|
|
108
|
+
|
|
109
|
+
### Added — an optional Slurm route for `run` and `exec` (#58)
|
|
110
|
+
|
|
111
|
+
blq can now run a command as one Slurm job, with `srun`. The output still
|
|
112
|
+
streams to blq, so the run is captured and queryable in the same way as a local
|
|
113
|
+
run.
|
|
114
|
+
|
|
115
|
+
The route stays off until a policy file exists at `~/.config/blq/slurm.toml`.
|
|
116
|
+
A command takes the Slurm route only when its working directory is inside one of
|
|
117
|
+
the policy's `roots`. A bad value in the file produces a warning, and the
|
|
118
|
+
command uses the local route.
|
|
119
|
+
|
|
120
|
+
A command in `.bird/commands.toml` can set `slurm = false` to stay local, or a
|
|
121
|
+
`slurm` table to change its CPUs, memory and partition. `local_memory_max`
|
|
122
|
+
caps the memory of a command on the local route.
|
|
123
|
+
|
|
124
|
+
`srun` prints its task status line at any position in the merged output, and it
|
|
125
|
+
reorders the task's stdout and stderr. On the Slurm route the task merges the
|
|
126
|
+
two streams before the command runs, and blq moves the status line to the end of
|
|
127
|
+
the output, so a multi-line record stays whole for the parser.
|
|
128
|
+
|
|
129
|
+
See `docs/slurm.md`.
|
|
130
|
+
|
|
3
131
|
## v1.2.2
|
|
4
132
|
|
|
5
133
|
### Fixed — Suppress FastMCP banners on stdio transport
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: blq-cli
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: Build Log Query - capture and query build/test logs with DuckDB
|
|
5
5
|
Project-URL: Homepage, https://blq-cli.readthedocs.io/
|
|
6
6
|
Project-URL: Repository, https://github.com/teaguesterling/blq-cli
|
|
@@ -20,7 +20,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
20
20
|
Classifier: Topic :: Software Development :: Build Tools
|
|
21
21
|
Classifier: Topic :: Software Development :: Testing
|
|
22
22
|
Requires-Python: >=3.10
|
|
23
|
-
Requires-Dist: duckdb
|
|
23
|
+
Requires-Dist: duckdb<2,>=1.4.4
|
|
24
24
|
Requires-Dist: jinja2>=3.0.0
|
|
25
25
|
Requires-Dist: pandas>=2.0.0
|
|
26
26
|
Requires-Dist: pyyaml>=6.0
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Slurm Routing
|
|
2
|
+
|
|
3
|
+
blq can run commands through a Slurm queue with `srun`. Output still streams to blq, so the run is captured and queryable as usual.
|
|
4
|
+
|
|
5
|
+
This guide uses two terms:
|
|
6
|
+
|
|
7
|
+
- **Slurm route**: blq runs the command as one Slurm job.
|
|
8
|
+
- **Local route**: blq runs the command as a local process.
|
|
9
|
+
|
|
10
|
+
With no policy file, blq uses the local route and does not call any Slurm tool.
|
|
11
|
+
|
|
12
|
+
## Policy File
|
|
13
|
+
|
|
14
|
+
The policy file is `~/.config/blq/slurm.toml`. It applies to all projects for this user.
|
|
15
|
+
|
|
16
|
+
```toml
|
|
17
|
+
roots = ["~/code"]
|
|
18
|
+
partition = "batch"
|
|
19
|
+
cpus = 4
|
|
20
|
+
mem = "8G"
|
|
21
|
+
local_memory_max = "16G"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
| Key | Default | Meaning |
|
|
25
|
+
|-----|---------|---------|
|
|
26
|
+
| `enabled` | `true` | Set to `false` to turn off all Slurm routing. |
|
|
27
|
+
| `roots` | `[]` | Directories whose commands use the Slurm route. |
|
|
28
|
+
| `partition` | `"batch"` | The Slurm partition for each job. |
|
|
29
|
+
| `cpus` | `4` | The CPUs for each job. This must be an integer above 0. |
|
|
30
|
+
| `mem` | `"8G"` | The memory for each job, for example `512M` or `16G`. |
|
|
31
|
+
| `local_memory_max` | not set | The memory limit for commands on the local route. |
|
|
32
|
+
|
|
33
|
+
If the file has a bad value, blq writes a warning and ignores the whole file. The command then uses the local route.
|
|
34
|
+
|
|
35
|
+
## Roots
|
|
36
|
+
|
|
37
|
+
A command uses the Slurm route only if its working directory is in one of the `roots`. Subdirectories count. A `~` at the start of a root expands to the home directory.
|
|
38
|
+
|
|
39
|
+
## Settings for One Command
|
|
40
|
+
|
|
41
|
+
Add a `slurm` value to a command in `.bird/commands.toml`.
|
|
42
|
+
|
|
43
|
+
Use `slurm = false` to keep a command on the local route:
|
|
44
|
+
|
|
45
|
+
```toml
|
|
46
|
+
[commands.serve]
|
|
47
|
+
cmd = "npm run dev"
|
|
48
|
+
slurm = false
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Use a table to change the resources of a command. A key that you do not set comes from the policy file:
|
|
52
|
+
|
|
53
|
+
```toml
|
|
54
|
+
[commands.build]
|
|
55
|
+
cmd = "make -j8"
|
|
56
|
+
slurm = { cpus = 8, mem = "16G", partition = "batch" }
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
If the value is not valid, blq writes a warning and ignores the value. The command still loads. `blq commands register --force` keeps the existing `slurm` value.
|
|
60
|
+
|
|
61
|
+
## Turn Off Routing for One Run
|
|
62
|
+
|
|
63
|
+
Set `BLQ_SLURM=0` to use the local route. The values `false`, `no` and `off` have the same effect.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
BLQ_SLURM=0 blq run build
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
A command that blq starts inside a Slurm job also uses the local route.
|
|
70
|
+
|
|
71
|
+
## When the Queue Is Down
|
|
72
|
+
|
|
73
|
+
Before each run on the Slurm route, blq runs `scontrol ping`. If the check fails, blq writes this line to stderr and uses the local route:
|
|
74
|
+
|
|
75
|
+
```text
|
|
76
|
+
[blq] Slurm unavailable, running locally: <reason>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
If `srun` then cannot contact the controller, blq writes the same warning and runs the command again on the local route. This rerun occurs only if the command did not start. Captured runs store the warning in their output. Runs with `--no-capture` do not have this rerun.
|
|
80
|
+
|
|
81
|
+
## Limits
|
|
82
|
+
|
|
83
|
+
These limits apply on the Slurm route:
|
|
84
|
+
|
|
85
|
+
- **Stdin is empty.** The command cannot read input from the terminal or from a pipe.
|
|
86
|
+
- **Background daemons die with the job.** A process that the command leaves behind stops when the job ends.
|
|
87
|
+
- **Interactive commands need `slurm = false`.** These commands need a terminal and stdin.
|
|
88
|
+
- **GPU and NPU commands need `slurm = false`.** blq does not request these devices from Slurm.
|
|
89
|
+
- **A timeout includes the time in the queue.** A job that waits in the queue can use all of its timeout before it starts.
|
|
90
|
+
- **Signal exit codes appear as 128+N.** For example, a command that receives SIGSEGV gives 139, and SIGKILL gives 137. They do not appear as negative numbers.
|
|
91
|
+
|
|
92
|
+
This limit applies on the local route:
|
|
93
|
+
|
|
94
|
+
- **A memory kill can show two results.** A command above `local_memory_max` shows `Terminated` with exit code 143, or `Killed` with exit code 137.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "blq-cli"
|
|
7
|
-
version = "
|
|
7
|
+
version = "2.0.0"
|
|
8
8
|
description = "Build Log Query - capture and query build/test logs with DuckDB"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -27,7 +27,12 @@ classifiers = [
|
|
|
27
27
|
"Topic :: Software Development :: Testing",
|
|
28
28
|
]
|
|
29
29
|
dependencies = [
|
|
30
|
-
|
|
30
|
+
# Upper bound: duckdb 2.0 removed DuckDBPyRelation.fetchone() and tightened
|
|
31
|
+
# connection reuse, which fails 71 of this suite's tests (24 AttributeError,
|
|
32
|
+
# 18 ResourceInUseException). Measured 2026-11-03 on py3.12.3 and py3.13.13
|
|
33
|
+
# with a clean HOME. CI never caught it: the 3.10 leg resolves 1.5.6, fails
|
|
34
|
+
# first, and matrix fail-fast cancels the 3.11/3.12 legs that do get 2.0.
|
|
35
|
+
"duckdb>=1.4.4,<2",
|
|
31
36
|
"jinja2>=3.0.0",
|
|
32
37
|
"pandas>=2.0.0",
|
|
33
38
|
"pyyaml>=6.0",
|
|
@@ -81,6 +86,12 @@ packages = ["src/blq", "src/blq_sandbox", "src/blq_sandbox_systemd", "src/blq_sa
|
|
|
81
86
|
[tool.ruff]
|
|
82
87
|
target-version = "py310"
|
|
83
88
|
line-length = 100
|
|
89
|
+
# ruff formats Python code blocks inside markdown too. In this repo that
|
|
90
|
+
# collapses the aligned trailing comments the docs use as a two-column
|
|
91
|
+
# listing, so `ruff format --check .` reported 19 documentation files as
|
|
92
|
+
# unformatted. Excluding markdown keeps the docs as written; ruff's lint
|
|
93
|
+
# rules never applied to them anyway.
|
|
94
|
+
extend-exclude = ["*.md"]
|
|
84
95
|
|
|
85
96
|
[tool.ruff.lint]
|
|
86
97
|
select = ["E", "F", "I", "N", "W", "UP"]
|
|
@@ -14,7 +14,8 @@ Example usage:
|
|
|
14
14
|
events = LogQuery.from_file("build.log").filter(severity="error").df()
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
-
from importlib.metadata import PackageNotFoundError
|
|
17
|
+
from importlib.metadata import PackageNotFoundError
|
|
18
|
+
from importlib.metadata import version as _dist_version
|
|
18
19
|
|
|
19
20
|
# Derived, never written by hand. The literal here read "1.0.1" against a
|
|
20
21
|
# distribution version of 1.2.1 — two releases stale, and immune to
|
|
@@ -30,6 +30,7 @@ from blq.config_format import (
|
|
|
30
30
|
)
|
|
31
31
|
|
|
32
32
|
# Git integration - re-exported for backward compatibility
|
|
33
|
+
from blq.ext.slurm_policy import MEM_FORMAT
|
|
33
34
|
from blq.git import GitInfo, capture_git_info # noqa: F401
|
|
34
35
|
|
|
35
36
|
if TYPE_CHECKING:
|
|
@@ -1109,6 +1110,7 @@ class RegisteredCommand:
|
|
|
1109
1110
|
suppress: list[str] = field(default_factory=list) # Fingerprints to suppress in reports
|
|
1110
1111
|
lines: str | None = None # Default line selection for run/exec output (e.g., "+20-")
|
|
1111
1112
|
lock: str | None = None # Lock name for resource contention (shared across commands)
|
|
1113
|
+
slurm: bool | dict[str, Any] | None = None # False opts out; a table sets resources
|
|
1112
1114
|
_extra: dict[str, Any] = field(default_factory=dict)
|
|
1113
1115
|
|
|
1114
1116
|
@property
|
|
@@ -1207,6 +1209,8 @@ class RegisteredCommand:
|
|
|
1207
1209
|
d["lines"] = self.lines
|
|
1208
1210
|
if self.lock is not None:
|
|
1209
1211
|
d["lock"] = self.lock
|
|
1212
|
+
if self.slurm is not None:
|
|
1213
|
+
d["slurm"] = self.slurm
|
|
1210
1214
|
d.update(self._extra)
|
|
1211
1215
|
return d
|
|
1212
1216
|
|
|
@@ -1396,9 +1400,44 @@ _KNOWN_COMMAND_KEYS = {
|
|
|
1396
1400
|
"suppress",
|
|
1397
1401
|
"lines",
|
|
1398
1402
|
"lock",
|
|
1403
|
+
"slurm",
|
|
1399
1404
|
}
|
|
1400
1405
|
|
|
1401
1406
|
|
|
1407
|
+
def _slurm_override_problem(value: Any) -> str | None:
|
|
1408
|
+
"""Return why a command's `slurm` value is invalid, or None if it is valid."""
|
|
1409
|
+
if isinstance(value, bool):
|
|
1410
|
+
return None
|
|
1411
|
+
if not isinstance(value, dict):
|
|
1412
|
+
return "must be a bool or a table"
|
|
1413
|
+
for key, item in value.items():
|
|
1414
|
+
if key == "cpus":
|
|
1415
|
+
# bool is an int subclass, so `cpus = true` needs its own check.
|
|
1416
|
+
if isinstance(item, bool) or not isinstance(item, int) or item <= 0:
|
|
1417
|
+
return "cpus must be a positive integer"
|
|
1418
|
+
elif key == "mem":
|
|
1419
|
+
if not isinstance(item, str) or not MEM_FORMAT.fullmatch(item):
|
|
1420
|
+
return "mem must look like 16G"
|
|
1421
|
+
elif key == "partition":
|
|
1422
|
+
if not isinstance(item, str) or not item:
|
|
1423
|
+
return "partition must be a non-empty string"
|
|
1424
|
+
else:
|
|
1425
|
+
return f"unknown key {key!r}"
|
|
1426
|
+
return None
|
|
1427
|
+
|
|
1428
|
+
|
|
1429
|
+
def _parse_slurm_override(name: str, value: Any) -> bool | dict[str, Any] | None:
|
|
1430
|
+
"""Validate a command's `slurm` value. A bad value warns and counts as unset."""
|
|
1431
|
+
if value is None:
|
|
1432
|
+
return None
|
|
1433
|
+
problem = _slurm_override_problem(value)
|
|
1434
|
+
if problem:
|
|
1435
|
+
logger.warning("Ignoring slurm setting of command %r: %s", name, problem)
|
|
1436
|
+
return None
|
|
1437
|
+
override: bool | dict[str, Any] = value
|
|
1438
|
+
return override
|
|
1439
|
+
|
|
1440
|
+
|
|
1402
1441
|
def _load_commands_impl(lq_dir: Path) -> dict[str, RegisteredCommand]:
|
|
1403
1442
|
"""Internal implementation of load_commands."""
|
|
1404
1443
|
commands_path = lq_dir / COMMANDS_FILE
|
|
@@ -1444,6 +1483,7 @@ def _load_commands_impl(lq_dir: Path) -> dict[str, RegisteredCommand]:
|
|
|
1444
1483
|
suppress=suppress,
|
|
1445
1484
|
lines=config.get("lines"), # Default line selection for output
|
|
1446
1485
|
lock=config.get("lock"),
|
|
1486
|
+
slurm=_parse_slurm_override(name, config.get("slurm")),
|
|
1447
1487
|
_extra=extra,
|
|
1448
1488
|
)
|
|
1449
1489
|
return commands
|
|
@@ -17,6 +17,7 @@ import subprocess
|
|
|
17
17
|
import sys
|
|
18
18
|
import threading
|
|
19
19
|
import time
|
|
20
|
+
import uuid
|
|
20
21
|
from datetime import datetime
|
|
21
22
|
from pathlib import Path
|
|
22
23
|
from types import FrameType
|
|
@@ -45,10 +46,12 @@ from blq.commands.core import (
|
|
|
45
46
|
parse_log_content,
|
|
46
47
|
write_run_parquet,
|
|
47
48
|
)
|
|
48
|
-
from blq.ext import CommandSpec, capture_env
|
|
49
|
+
from blq.ext import CommandSpec, Executor, capture_env
|
|
49
50
|
from blq.ext.discovery import load_extensions, order_extensions
|
|
50
51
|
from blq.ext.local_executor import LocalExecutor
|
|
51
52
|
from blq.ext.pipeline import run_pipeline
|
|
53
|
+
from blq.ext.slurm_executor import SlurmExecutor, memory_capped, srun_command
|
|
54
|
+
from blq.ext.slurm_policy import decide_route, load_policy
|
|
52
55
|
from blq.locks import LockHeldError, acquire_lock, release_lock
|
|
53
56
|
|
|
54
57
|
# Logger for lq status messages
|
|
@@ -245,6 +248,7 @@ def _execute_with_live_output(
|
|
|
245
248
|
lock_name: str | None = None,
|
|
246
249
|
no_lock: bool = False,
|
|
247
250
|
wait_lock: int | None = None,
|
|
251
|
+
slurm: bool | dict[str, Any] | None = None,
|
|
248
252
|
) -> RunResult:
|
|
249
253
|
"""Execute command with live output streaming (attempts/outcomes pattern).
|
|
250
254
|
|
|
@@ -403,7 +407,13 @@ def _execute_with_live_output(
|
|
|
403
407
|
ordered = order_extensions(extensions)
|
|
404
408
|
|
|
405
409
|
# Create executor with live output path
|
|
406
|
-
executor
|
|
410
|
+
executor: Executor
|
|
411
|
+
policy = load_policy()
|
|
412
|
+
if policy is None:
|
|
413
|
+
executor = LocalExecutor(quiet=quiet, live_output_path=live_output_path)
|
|
414
|
+
else:
|
|
415
|
+
route = decide_route(policy, cmd_spec.cwd, slurm, {**os.environ, **cmd_spec.env})
|
|
416
|
+
executor = SlurmExecutor(route, policy, quiet=quiet, live_output_path=live_output_path)
|
|
407
417
|
|
|
408
418
|
# Run the pipeline: prepare -> execute -> collect
|
|
409
419
|
exec_result = run_pipeline(cmd_spec, ordered, executor)
|
|
@@ -665,6 +675,7 @@ def _execute_command(
|
|
|
665
675
|
lock_name: str | None = None,
|
|
666
676
|
no_lock: bool = False,
|
|
667
677
|
wait_lock: int | None = None,
|
|
678
|
+
slurm: bool | dict[str, Any] | None = None,
|
|
668
679
|
) -> RunResult:
|
|
669
680
|
"""Execute a command and capture its output.
|
|
670
681
|
|
|
@@ -687,6 +698,8 @@ def _execute_command(
|
|
|
687
698
|
lock_name: Optional lock name for resource contention.
|
|
688
699
|
no_lock: If True, bypass lock even if lock_name is set.
|
|
689
700
|
wait_lock: If set, wait up to this many seconds for the lock.
|
|
701
|
+
slurm: The command's Slurm override (False opts out, a table sets resources).
|
|
702
|
+
Only BIRD mode routes through Slurm; legacy parquet mode always runs locally.
|
|
690
703
|
|
|
691
704
|
Returns:
|
|
692
705
|
RunResult with execution details and parsed events
|
|
@@ -709,6 +722,7 @@ def _execute_command(
|
|
|
709
722
|
lock_name=lock_name,
|
|
710
723
|
no_lock=no_lock,
|
|
711
724
|
wait_lock=wait_lock,
|
|
725
|
+
slurm=slurm,
|
|
712
726
|
)
|
|
713
727
|
|
|
714
728
|
# Legacy parquet mode - write everything at the end
|
|
@@ -1094,17 +1108,35 @@ def _parse_command_args(
|
|
|
1094
1108
|
return named_args, positional_args, extra_args
|
|
1095
1109
|
|
|
1096
1110
|
|
|
1097
|
-
def
|
|
1111
|
+
def _route_no_capture(command: str, slurm: bool | dict[str, Any] | None) -> str:
|
|
1112
|
+
"""Return `command`, wrapped in `srun` if the Slurm route says so."""
|
|
1113
|
+
policy = load_policy()
|
|
1114
|
+
if policy is None:
|
|
1115
|
+
return command
|
|
1116
|
+
cwd = Path.cwd()
|
|
1117
|
+
route = decide_route(policy, cwd, slurm, os.environ)
|
|
1118
|
+
if route.kind == "slurm" and route.resources is not None:
|
|
1119
|
+
return srun_command(command, cwd, route.resources, uuid.uuid4().hex, None)
|
|
1120
|
+
if route.warn:
|
|
1121
|
+
print(f"[blq] Slurm unavailable, running locally: {route.reason}", file=sys.stderr)
|
|
1122
|
+
return memory_capped(command, policy)
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
def _run_no_capture(
|
|
1126
|
+
command: str, quiet: bool = False, slurm: bool | dict[str, Any] | None = None
|
|
1127
|
+
) -> int:
|
|
1098
1128
|
"""Run a command without capturing output to parquet.
|
|
1099
1129
|
|
|
1100
1130
|
Args:
|
|
1101
1131
|
command: Shell command to run
|
|
1102
1132
|
quiet: If True, don't stream output
|
|
1133
|
+
slurm: The command's Slurm override (False opts out, a table sets resources)
|
|
1103
1134
|
|
|
1104
1135
|
Returns:
|
|
1105
1136
|
Exit code from the command
|
|
1106
1137
|
"""
|
|
1107
1138
|
started_at = datetime.now()
|
|
1139
|
+
command = _route_no_capture(command, slurm)
|
|
1108
1140
|
|
|
1109
1141
|
# Track subprocess for signal handling
|
|
1110
1142
|
process: subprocess.Popen[str] | None = None
|
|
@@ -1284,9 +1316,11 @@ def cmd_run(args: argparse.Namespace) -> None:
|
|
|
1284
1316
|
else:
|
|
1285
1317
|
logger.setLevel(logging.WARNING)
|
|
1286
1318
|
|
|
1319
|
+
slurm = registered_commands[cmd_name].slurm if cmd_name in registered_commands else None
|
|
1320
|
+
|
|
1287
1321
|
# No-capture mode: just run and exit with the command's exit code
|
|
1288
1322
|
if not should_capture:
|
|
1289
|
-
exit_code = _run_no_capture(command, quiet)
|
|
1323
|
+
exit_code = _run_no_capture(command, quiet, slurm)
|
|
1290
1324
|
sys.exit(exit_code)
|
|
1291
1325
|
|
|
1292
1326
|
# Determine timeout: CLI flag overrides command config
|
|
@@ -1326,6 +1360,7 @@ def cmd_run(args: argparse.Namespace) -> None:
|
|
|
1326
1360
|
lock_name=lock_name,
|
|
1327
1361
|
no_lock=no_lock,
|
|
1328
1362
|
wait_lock=wait_lock,
|
|
1363
|
+
slurm=slurm,
|
|
1329
1364
|
)
|
|
1330
1365
|
except LockHeldError as e:
|
|
1331
1366
|
print(f"Error: {e}", file=sys.stderr)
|
|
@@ -1564,7 +1599,7 @@ def cmd_exec(args: argparse.Namespace) -> None:
|
|
|
1564
1599
|
def _write_imported_run(
|
|
1565
1600
|
events: list[dict[str, Any]],
|
|
1566
1601
|
run_meta: dict[str, Any],
|
|
1567
|
-
config:
|
|
1602
|
+
config: BlqConfig,
|
|
1568
1603
|
lq_dir: Path,
|
|
1569
1604
|
) -> Path:
|
|
1570
1605
|
"""Persist an imported/captured run to the ACTIVE storage backend.
|
|
@@ -218,8 +218,10 @@ def cmd_register(args: argparse.Namespace) -> None:
|
|
|
218
218
|
)
|
|
219
219
|
break # Only show first match
|
|
220
220
|
|
|
221
|
-
# Register new command
|
|
221
|
+
# Register new command. The Slurm setting survives an overwrite: dropping `slurm = false`
|
|
222
|
+
# would silently put the command back on the Slurm route.
|
|
222
223
|
capture = not getattr(args, "no_capture", False)
|
|
224
|
+
slurm = commands[name].slurm if name in commands else None
|
|
223
225
|
|
|
224
226
|
# Detect format if not specified
|
|
225
227
|
format_hint = args.format
|
|
@@ -240,6 +242,7 @@ def cmd_register(args: argparse.Namespace) -> None:
|
|
|
240
242
|
timeout=args.timeout,
|
|
241
243
|
format=format_hint,
|
|
242
244
|
capture=capture,
|
|
245
|
+
slurm=slurm,
|
|
243
246
|
)
|
|
244
247
|
defaults_note = f" (defaults: {defaults})" if defaults else ""
|
|
245
248
|
format_note = f" [format: {format_hint}]" if format_detected else ""
|
|
@@ -255,6 +258,7 @@ def cmd_register(args: argparse.Namespace) -> None:
|
|
|
255
258
|
timeout=args.timeout,
|
|
256
259
|
format=format_hint,
|
|
257
260
|
capture=capture,
|
|
261
|
+
slurm=slurm,
|
|
258
262
|
)
|
|
259
263
|
capture_note = " (no capture)" if not capture else ""
|
|
260
264
|
format_note = f" [format: {format_hint}]" if format_detected else ""
|
|
@@ -6,13 +6,12 @@ Extension.prepare() → Executor.execute() → Collector.collect().
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import os
|
|
9
10
|
from dataclasses import dataclass, field
|
|
10
11
|
from datetime import datetime
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
from typing import Any, Protocol
|
|
13
14
|
|
|
14
|
-
import os
|
|
15
|
-
|
|
16
15
|
|
|
17
16
|
@dataclass
|
|
18
17
|
class CommandSpec:
|