blq-cli 1.2.1__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.
Files changed (215) hide show
  1. {blq_cli-1.2.1 → blq_cli-2.0.0}/CHANGELOG.md +137 -0
  2. {blq_cli-1.2.1 → blq_cli-2.0.0}/PKG-INFO +2 -2
  3. blq_cli-2.0.0/docs/slurm.md +94 -0
  4. {blq_cli-1.2.1 → blq_cli-2.0.0}/mkdocs.yml +1 -0
  5. {blq_cli-1.2.1 → blq_cli-2.0.0}/pyproject.toml +13 -2
  6. blq_cli-2.0.0/src/blq/__init__.py +33 -0
  7. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/commands/core.py +40 -0
  8. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/commands/execution.py +40 -5
  9. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/commands/registry.py +5 -1
  10. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/commands/watch_cmd.py +1 -0
  11. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/ext/__init__.py +1 -2
  12. blq_cli-2.0.0/src/blq/ext/slurm_executor.py +175 -0
  13. blq_cli-2.0.0/src/blq/ext/slurm_policy.py +133 -0
  14. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/serve.py +512 -185
  15. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/conftest.py +30 -0
  16. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_config_consumption.py +7 -5
  17. blq_cli-2.0.0/tests/test_ext_slurm_executor.py +380 -0
  18. blq_cli-2.0.0/tests/test_ext_slurm_policy.py +252 -0
  19. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_import_capture_storage.py +3 -10
  20. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_inspect.py +4 -3
  21. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_log_content_persistence.py +2 -1
  22. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_mcp_server.py +6 -5
  23. blq_cli-2.0.0/tests/test_output_schemas.py +177 -0
  24. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_security_containment.py +6 -4
  25. blq_cli-2.0.0/tests/test_slurm_integration.py +521 -0
  26. blq_cli-2.0.0/tests/test_strict_root_resolution.py +320 -0
  27. blq_cli-2.0.0/tests/test_version.py +37 -0
  28. blq_cli-1.2.1/src/blq/__init__.py +0 -21
  29. blq_cli-1.2.1/tests/test_output_schemas.py +0 -90
  30. {blq_cli-1.2.1 → blq_cli-2.0.0}/.claude/hooks/blq-suggest.sh +0 -0
  31. {blq_cli-1.2.1 → blq_cli-2.0.0}/.github/workflows/ci.yml +0 -0
  32. {blq_cli-1.2.1 → blq_cli-2.0.0}/.github/workflows/docs.yml +0 -0
  33. {blq_cli-1.2.1 → blq_cli-2.0.0}/.github/workflows/publish.yml +0 -0
  34. {blq_cli-1.2.1 → blq_cli-2.0.0}/.gitignore +0 -0
  35. {blq_cli-1.2.1 → blq_cli-2.0.0}/.mcp.json +0 -0
  36. {blq_cli-1.2.1 → blq_cli-2.0.0}/.readthedocs.yml +0 -0
  37. {blq_cli-1.2.1 → blq_cli-2.0.0}/AGENT.md +0 -0
  38. {blq_cli-1.2.1 → blq_cli-2.0.0}/AGENTS.md +0 -0
  39. {blq_cli-1.2.1 → blq_cli-2.0.0}/CLAUDE.md +0 -0
  40. {blq_cli-1.2.1 → blq_cli-2.0.0}/CONTRIBUTING.md +0 -0
  41. {blq_cli-1.2.1 → blq_cli-2.0.0}/README.md +0 -0
  42. {blq_cli-1.2.1 → blq_cli-2.0.0}/SKILL.md +0 -0
  43. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/api-reference.md +0 -0
  44. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/commands/capture.md +0 -0
  45. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/commands/ci.md +0 -0
  46. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/commands/completions.md +0 -0
  47. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/commands/errors.md +0 -0
  48. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/commands/exec.md +0 -0
  49. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/commands/filter.md +0 -0
  50. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/commands/index.md +0 -0
  51. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/commands/init.md +0 -0
  52. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/commands/maintenance.md +0 -0
  53. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/commands/query.md +0 -0
  54. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/commands/registry.md +0 -0
  55. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/commands/report.md +0 -0
  56. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/commands/run.md +0 -0
  57. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/commands/sql.md +0 -0
  58. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/commands/status.md +0 -0
  59. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/commands/sync.md +0 -0
  60. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/commands/watch.md +0 -0
  61. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/design/design-bird-migration.md +0 -0
  62. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/design/design-commands.md +0 -0
  63. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/design/design-config-command.md +0 -0
  64. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/design/design-extensions.md +0 -0
  65. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/design/design-git-integration.md +0 -0
  66. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/design/design-hooks-v2.md +0 -0
  67. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/design/design-live-inspection.md +0 -0
  68. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/design/design-parameterized-commands.md +0 -0
  69. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/design/design-run-args.md +0 -0
  70. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/design/design-sandbox-specs.md +0 -0
  71. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/design/design-sync.md +0 -0
  72. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/design/design-track-save.md +0 -0
  73. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/design/duck-hunt-v2-migration.md +0 -0
  74. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/design/duck-hunt-v3-migration.md +0 -0
  75. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/design/proposal-bird-v5.md +0 -0
  76. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/design/skill-inspect-enrichment.md +0 -0
  77. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/getting-started.md +0 -0
  78. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/index.md +0 -0
  79. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/integration.md +0 -0
  80. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/mcp.md +0 -0
  81. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/plans/explore-bird-sqlite-backend.md +0 -0
  82. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/plans/explore-nsjail-python-wrapper.md +0 -0
  83. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/plans/explore-nsjail-spack-package.md +0 -0
  84. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/plans/patterns-integration-prompt.md +0 -0
  85. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/plans/roadmap-to-1.0.md +0 -0
  86. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/plugin-guide.md +0 -0
  87. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/python-api.md +0 -0
  88. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/query-guide.md +0 -0
  89. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/requirements.txt +0 -0
  90. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/sandbox.md +0 -0
  91. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/schema-stability.md +0 -0
  92. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/schema.md +0 -0
  93. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/superpowers/plans/2026-03-22-extension-system.md +0 -0
  94. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/superpowers/plans/2026-03-28-bwrap-engine.md +0 -0
  95. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/superpowers/plans/2026-03-28-command-locks.md +0 -0
  96. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/superpowers/plans/2026-03-29-annotators-and-tighten.md +0 -0
  97. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/superpowers/plans/2026-03-29-service-layer-phase1.md +0 -0
  98. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/superpowers/plans/2026-03-29-strace-profiling.md +0 -0
  99. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/superpowers/specs/2026-03-22-extension-system-design.md +0 -0
  100. {blq_cli-1.2.1 → blq_cli-2.0.0}/docs/superpowers/specs/2026-03-29-unified-service-layer-design.md +0 -0
  101. {blq_cli-1.2.1 → blq_cli-2.0.0}/experiments/agent-build-test.md +0 -0
  102. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/__main__.py +0 -0
  103. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/bird.py +0 -0
  104. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/bird_schema.sql +0 -0
  105. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/cli.py +0 -0
  106. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/commands/README.md +0 -0
  107. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/commands/__init__.py +0 -0
  108. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/commands/ci_cmd.py +0 -0
  109. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/commands/clean_cmd.py +0 -0
  110. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/commands/config_cmd.py +0 -0
  111. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/commands/events.py +0 -0
  112. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/commands/hooks_cmd.py +0 -0
  113. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/commands/hooks_gen.py +0 -0
  114. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/commands/init_cmd.py +0 -0
  115. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/commands/management.py +0 -0
  116. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/commands/management_cmd.py +0 -0
  117. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/commands/mcp_cmd.py +0 -0
  118. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/commands/migrate.py +0 -0
  119. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/commands/query_cmd.py +0 -0
  120. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/commands/record_cmd.py +0 -0
  121. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/commands/report_cmd.py +0 -0
  122. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/commands/sandbox_cmd.py +0 -0
  123. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/commands/serve_cmd.py +0 -0
  124. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/commands/sync_cmd.py +0 -0
  125. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/config_format.py +0 -0
  126. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/ext/annotator.py +0 -0
  127. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/ext/discovery.py +0 -0
  128. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/ext/local_executor.py +0 -0
  129. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/ext/pipeline.py +0 -0
  130. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/git.py +0 -0
  131. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/github.py +0 -0
  132. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/locks.py +0 -0
  133. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/output.py +0 -0
  134. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/query.py +0 -0
  135. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/runtime.py +0 -0
  136. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/schema.sql +0 -0
  137. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/services/__init__.py +0 -0
  138. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/services/execution.py +0 -0
  139. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/services/inspect.py +0 -0
  140. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/services/query.py +0 -0
  141. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/services/refs.py +0 -0
  142. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/storage.py +0 -0
  143. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/templates/drone.yml.j2 +0 -0
  144. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/templates/git_hook.sh.j2 +0 -0
  145. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/templates/github_workflow.yml.j2 +0 -0
  146. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/templates/gitlab_ci.yml.j2 +0 -0
  147. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/templates/hook_script.sh.j2 +0 -0
  148. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq/user_config.py +0 -0
  149. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq_sandbox/__init__.py +0 -0
  150. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq_sandbox/engines.py +0 -0
  151. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq_sandbox/profile.py +0 -0
  152. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq_sandbox/source_annotator.py +0 -0
  153. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq_sandbox/spec.py +0 -0
  154. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq_sandbox/strace_parser.py +0 -0
  155. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq_sandbox/tighten.py +0 -0
  156. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq_sandbox/violations.py +0 -0
  157. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq_sandbox_bwrap/__init__.py +0 -0
  158. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq_sandbox_bwrap/args.py +0 -0
  159. {blq_cli-1.2.1 → blq_cli-2.0.0}/src/blq_sandbox_systemd/__init__.py +0 -0
  160. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/__init__.py +0 -0
  161. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/sql/test_duck_hunt_v2_migration.sql +0 -0
  162. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_annotator.py +0 -0
  163. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_attempts_outcomes.py +0 -0
  164. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_auto_init.py +0 -0
  165. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_autoprune.py +0 -0
  166. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_bird.py +0 -0
  167. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_bwrap_args.py +0 -0
  168. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_bwrap_engine.py +0 -0
  169. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_ci.py +0 -0
  170. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_ci_generators.py +0 -0
  171. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_command_args.py +0 -0
  172. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_command_lock_field.py +0 -0
  173. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_core.py +0 -0
  174. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_execution_locks.py +0 -0
  175. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_exit_code_reason.py +0 -0
  176. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_ext_discovery.py +0 -0
  177. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_ext_integration.py +0 -0
  178. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_ext_local_executor.py +0 -0
  179. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_ext_pipeline.py +0 -0
  180. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_ext_types.py +0 -0
  181. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_git.py +0 -0
  182. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_hooks.py +0 -0
  183. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_hooks_gen.py +0 -0
  184. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_json_null_filter.py +0 -0
  185. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_locks.py +0 -0
  186. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_mcp_ci_tools.py +0 -0
  187. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_mcp_merge.py +0 -0
  188. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_phase1_structured_output.py +0 -0
  189. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_phase2_command_registry.py +0 -0
  190. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_query_api.py +0 -0
  191. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_query_filter.py +0 -0
  192. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_record_invocation.py +0 -0
  193. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_report.py +0 -0
  194. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_runtime_config.py +0 -0
  195. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_sandbox.py +0 -0
  196. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_sandbox_cmd.py +0 -0
  197. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_sandbox_events.py +0 -0
  198. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_sandbox_ext.py +0 -0
  199. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_sandbox_profile.py +0 -0
  200. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_sandbox_register.py +0 -0
  201. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_sandbox_systemd.py +0 -0
  202. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_sandbox_tighten.py +0 -0
  203. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_sandbox_violations.py +0 -0
  204. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_schema_repair.py +0 -0
  205. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_services_execution.py +0 -0
  206. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_services_inspect.py +0 -0
  207. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_services_query.py +0 -0
  208. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_services_refs.py +0 -0
  209. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_source_annotator.py +0 -0
  210. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_storage.py +0 -0
  211. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_storage_prune.py +0 -0
  212. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_strace_parser.py +0 -0
  213. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_user_config.py +0 -0
  214. {blq_cli-1.2.1 → blq_cli-2.0.0}/tests/test_watch.py +0 -0
  215. {blq_cli-1.2.1 → blq_cli-2.0.0}/uv.lock +0 -0
@@ -1,5 +1,142 @@
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
+
131
+ ## v1.2.2
132
+
133
+ ### Fixed — Suppress FastMCP banners on stdio transport
134
+
135
+ When starting the MCP server over stdio, FastMCP banner rendering is suppressed
136
+ (`show_banner=False`) so that stdout contains strictly valid JSON-RPC messages
137
+ without terminal box decorations. Also derives `__version__` from installed package
138
+ metadata.
139
+
3
140
  ## v1.2.1
4
141
 
5
142
  ### Fixed — 1.2.0 was broken on Python 3.10 and 3.11
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: blq-cli
3
- Version: 1.2.1
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>=1.4.4
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.
@@ -66,6 +66,7 @@ nav:
66
66
  - Python API: python-api.md
67
67
  - Integration: integration.md
68
68
  - MCP Server: mcp.md
69
+ - Slurm Routing: slurm.md
69
70
  - Development:
70
71
  - CI/CD Setup: ci-cd.md
71
72
  - Design - Sync: design-sync.md
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "blq-cli"
7
- version = "1.2.1"
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
- "duckdb>=1.4.4",
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"]
@@ -0,0 +1,33 @@
1
+ """
2
+ blq - Log Query
3
+
4
+ Capture and query build/test logs with DuckDB.
5
+
6
+ Example usage:
7
+ from blq import LogStore, LogQuery
8
+
9
+ # Query stored events
10
+ store = LogStore.open()
11
+ errors = store.errors().filter(ref_file="%main%").df()
12
+
13
+ # Query a log file directly
14
+ events = LogQuery.from_file("build.log").filter(severity="error").df()
15
+ """
16
+
17
+ from importlib.metadata import PackageNotFoundError
18
+ from importlib.metadata import version as _dist_version
19
+
20
+ # Derived, never written by hand. The literal here read "1.0.1" against a
21
+ # distribution version of 1.2.1 — two releases stale, and immune to
22
+ # --force-reinstall because it was not derived from anything. Nothing consumes
23
+ # it today (the CLI reads importlib.metadata directly), which is exactly why it
24
+ # drifted unnoticed; a version string that lies makes a healthy install look
25
+ # broken, which is the most expensive direction to send a reader.
26
+ try:
27
+ __version__ = _dist_version("blq-cli")
28
+ except PackageNotFoundError: # running from a source tree with no install
29
+ __version__ = "0.0.0.dev0"
30
+
31
+ from blq.query import LogQuery, LogQueryGrouped, LogStore
32
+
33
+ __all__ = ["LogQuery", "LogStore", "LogQueryGrouped", "__version__"]
@@ -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 = LocalExecutor(quiet=quiet, live_output_path=live_output_path)
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 _run_no_capture(command: str, quiet: bool = False) -> int:
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: "BlqConfig",
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 ""
@@ -264,6 +264,7 @@ class WatchController:
264
264
  format_hint=reg_cmd.format,
265
265
  quiet=self._quiet,
266
266
  session_id=self._session.session_id,
267
+ slurm=reg_cmd.slurm,
267
268
  )
268
269
 
269
270
  self._session.run_count += 1
@@ -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: