shellbrain 0.1.25__tar.gz → 0.1.27__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 (225) hide show
  1. {shellbrain-0.1.25 → shellbrain-0.1.27}/PKG-INFO +1 -1
  2. shellbrain-0.1.27/app/migrations/versions/20260422_0015_problem_runs.py +101 -0
  3. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/cli/main.py +39 -50
  4. shellbrain-0.1.27/app/periphery/db/models/problem_runs.py +60 -0
  5. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/db/models/registry.py +2 -1
  6. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/db/models/views.py +212 -0
  7. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/metrics/artifacts.py +18 -1
  8. shellbrain-0.1.27/app/periphery/metrics/pager.py +250 -0
  9. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/metrics/queries.py +23 -0
  10. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/metrics/render_html.py +260 -16
  11. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/metrics/service.py +8 -0
  12. {shellbrain-0.1.25 → shellbrain-0.1.27}/pyproject.toml +1 -1
  13. {shellbrain-0.1.25 → shellbrain-0.1.27}/shellbrain.egg-info/PKG-INFO +1 -1
  14. {shellbrain-0.1.25 → shellbrain-0.1.27}/shellbrain.egg-info/SOURCES.txt +3 -0
  15. {shellbrain-0.1.25 → shellbrain-0.1.27}/README.md +0 -0
  16. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/__init__.py +0 -0
  17. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/__main__.py +0 -0
  18. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/boot/__init__.py +0 -0
  19. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/boot/_dsn_resolution.py +0 -0
  20. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/boot/admin_db.py +0 -0
  21. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/boot/config.py +0 -0
  22. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/boot/create_policy.py +0 -0
  23. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/boot/db.py +0 -0
  24. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/boot/embeddings.py +0 -0
  25. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/boot/home.py +0 -0
  26. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/boot/migrations.py +0 -0
  27. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/boot/read_policy.py +0 -0
  28. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/boot/repos.py +0 -0
  29. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/boot/retrieval.py +0 -0
  30. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/boot/thresholds.py +0 -0
  31. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/boot/update_policy.py +0 -0
  32. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/boot/use_cases.py +0 -0
  33. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/config/__init__.py +0 -0
  34. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/config/defaults/create_policy.yaml +0 -0
  35. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/config/defaults/read_policy.yaml +0 -0
  36. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/config/defaults/runtime.yaml +0 -0
  37. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/config/defaults/thresholds.yaml +0 -0
  38. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/config/defaults/update_policy.yaml +0 -0
  39. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/config/loader.py +0 -0
  40. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/__init__.py +0 -0
  41. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/contracts/__init__.py +0 -0
  42. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/contracts/concepts.py +0 -0
  43. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/contracts/errors.py +0 -0
  44. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/contracts/requests.py +0 -0
  45. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/contracts/responses.py +0 -0
  46. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/entities/__init__.py +0 -0
  47. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/entities/associations.py +0 -0
  48. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/entities/concepts.py +0 -0
  49. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/entities/episodes.py +0 -0
  50. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/entities/evidence.py +0 -0
  51. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/entities/facts.py +0 -0
  52. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/entities/guidance.py +0 -0
  53. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/entities/identity.py +0 -0
  54. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/entities/memory.py +0 -0
  55. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/entities/runtime_context.py +0 -0
  56. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/entities/session_state.py +0 -0
  57. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/entities/telemetry.py +0 -0
  58. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/entities/utility.py +0 -0
  59. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/interfaces/__init__.py +0 -0
  60. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/interfaces/clock.py +0 -0
  61. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/interfaces/config.py +0 -0
  62. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/interfaces/embeddings.py +0 -0
  63. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/interfaces/idgen.py +0 -0
  64. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/interfaces/repos.py +0 -0
  65. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/interfaces/retrieval.py +0 -0
  66. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/interfaces/session_state_store.py +0 -0
  67. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/interfaces/unit_of_work.py +0 -0
  68. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/policies/__init__.py +0 -0
  69. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/policies/_shared/__init__.py +0 -0
  70. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/policies/_shared/executor.py +0 -0
  71. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/policies/_shared/side_effects.py +0 -0
  72. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/policies/create_policy/__init__.py +0 -0
  73. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/policies/create_policy/pipeline.py +0 -0
  74. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/policies/read_policy/__init__.py +0 -0
  75. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/policies/read_policy/bm25.py +0 -0
  76. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/policies/read_policy/context_pack_builder.py +0 -0
  77. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/policies/read_policy/expansion.py +0 -0
  78. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/policies/read_policy/fusion_rrf.py +0 -0
  79. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/policies/read_policy/lexical_query.py +0 -0
  80. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/policies/read_policy/pipeline.py +0 -0
  81. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/policies/read_policy/scoring.py +0 -0
  82. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/policies/read_policy/seed_retrieval.py +0 -0
  83. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/policies/update_policy/__init__.py +0 -0
  84. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/policies/update_policy/pipeline.py +0 -0
  85. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/use_cases/__init__.py +0 -0
  86. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/use_cases/build_guidance.py +0 -0
  87. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/use_cases/create_memory.py +0 -0
  88. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/use_cases/manage_concepts.py +0 -0
  89. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/use_cases/manage_session_state.py +0 -0
  90. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/use_cases/read_concepts.py +0 -0
  91. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/use_cases/read_memory.py +0 -0
  92. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/use_cases/record_episode_sync_telemetry.py +0 -0
  93. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/use_cases/record_model_usage_telemetry.py +0 -0
  94. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/use_cases/record_operation_telemetry.py +0 -0
  95. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/use_cases/sync_episode.py +0 -0
  96. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/core/use_cases/update_memory.py +0 -0
  97. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/migrations/__init__.py +0 -0
  98. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/migrations/env.py +0 -0
  99. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/migrations/versions/20260226_0001_initial_schema.py +0 -0
  100. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/migrations/versions/20260312_0002_add_hard_invariants.py +0 -0
  101. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/migrations/versions/20260312_0003_drop_create_confidence.py +0 -0
  102. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/migrations/versions/20260313_0004_episode_sync_hardening.py +0 -0
  103. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/migrations/versions/20260313_0005_evidence_episode_event_refs.py +0 -0
  104. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/migrations/versions/20260318_0006_usage_telemetry_schema.py +0 -0
  105. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/migrations/versions/20260319_0007_identity_session_guidance.py +0 -0
  106. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/migrations/versions/20260320_0008_instance_metadata_and_backup_safety.py +0 -0
  107. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/migrations/versions/20260410_0009_frontier_memory_family.py +0 -0
  108. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/migrations/versions/20260414_0010_model_usage_telemetry.py +0 -0
  109. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/migrations/versions/20260414_0011_usage_problem_tokens_multi_solution_metrics.py +0 -0
  110. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/migrations/versions/20260415_0012_read_pack_cost_and_read_roi.py +0 -0
  111. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/migrations/versions/20260421_0013_concept_context_graph.py +0 -0
  112. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/migrations/versions/20260422_0014_concept_read_telemetry.py +0 -0
  113. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/migrations/versions/__init__.py +0 -0
  114. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/onboarding_assets/__init__.py +0 -0
  115. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/onboarding_assets/claude/CLAUDE.md +0 -0
  116. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/onboarding_assets/claude/skills/shellbrain-session-start/SKILL.md +0 -0
  117. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/onboarding_assets/claude/skills/shellbrain-usage-review/SKILL.md +0 -0
  118. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/onboarding_assets/codex/AGENTS.md +0 -0
  119. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/onboarding_assets/codex/shellbrain-session-start/SKILL.md +0 -0
  120. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/onboarding_assets/codex/shellbrain-session-start/agents/openai.yaml +0 -0
  121. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/onboarding_assets/codex/shellbrain-session-start/assets/shellbrain-large.svg +0 -0
  122. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/onboarding_assets/codex/shellbrain-session-start/assets/shellbrain-small.svg +0 -0
  123. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/onboarding_assets/codex/shellbrain-session-start/assets/shellbrain_logo.png +0 -0
  124. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/onboarding_assets/codex/shellbrain-session-start/references/request-shapes.md +0 -0
  125. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/onboarding_assets/codex/shellbrain-session-start/references/session-workflow.md +0 -0
  126. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/onboarding_assets/codex/shellbrain-usage-review/SKILL.md +0 -0
  127. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/onboarding_assets/codex/shellbrain-usage-review/agents/openai.yaml +0 -0
  128. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/onboarding_assets/codex/shellbrain-usage-review/assets/shellbrain-small.svg +0 -0
  129. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/onboarding_assets/codex/shellbrain-usage-review/assets/shellbrain_logo.png +0 -0
  130. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/onboarding_assets/cursor/skills/shellbrain-session-start/SKILL.md +0 -0
  131. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/onboarding_assets/cursor/skills/shellbrain-usage-review/SKILL.md +0 -0
  132. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/__init__.py +0 -0
  133. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/admin/__init__.py +0 -0
  134. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/admin/agent_behavior_analysis.py +0 -0
  135. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/admin/analytics.py +0 -0
  136. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/admin/analytics_diagnostics.py +0 -0
  137. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/admin/analytics_queries.py +0 -0
  138. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/admin/backup.py +0 -0
  139. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/admin/destructive_guard.py +0 -0
  140. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/admin/doctor.py +0 -0
  141. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/admin/external_runtime.py +0 -0
  142. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/admin/init.py +0 -0
  143. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/admin/init_errors.py +0 -0
  144. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/admin/instance_guard.py +0 -0
  145. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/admin/machine_state.py +0 -0
  146. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/admin/managed_runtime.py +0 -0
  147. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/admin/model_usage_backfill.py +0 -0
  148. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/admin/privileges.py +0 -0
  149. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/admin/repo_state.py +0 -0
  150. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/admin/restore.py +0 -0
  151. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/admin/storage_setup.py +0 -0
  152. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/admin/upgrade.py +0 -0
  153. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/cli/__init__.py +0 -0
  154. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/cli/handlers.py +0 -0
  155. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/cli/hydration.py +0 -0
  156. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/cli/presenter_json.py +0 -0
  157. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/cli/schema_validation.py +0 -0
  158. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/db/__init__.py +0 -0
  159. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/db/engine.py +0 -0
  160. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/db/models/__init__.py +0 -0
  161. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/db/models/associations.py +0 -0
  162. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/db/models/concepts.py +0 -0
  163. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/db/models/episodes.py +0 -0
  164. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/db/models/evidence.py +0 -0
  165. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/db/models/experiences.py +0 -0
  166. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/db/models/instance_metadata.py +0 -0
  167. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/db/models/memories.py +0 -0
  168. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/db/models/metadata.py +0 -0
  169. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/db/models/telemetry.py +0 -0
  170. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/db/models/utility.py +0 -0
  171. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/db/repos/__init__.py +0 -0
  172. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/db/repos/relational/__init__.py +0 -0
  173. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/db/repos/relational/associations_repo.py +0 -0
  174. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/db/repos/relational/concepts_repo.py +0 -0
  175. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/db/repos/relational/episodes_repo.py +0 -0
  176. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/db/repos/relational/evidence_repo.py +0 -0
  177. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/db/repos/relational/experiences_repo.py +0 -0
  178. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/db/repos/relational/memories_repo.py +0 -0
  179. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/db/repos/relational/read_policy_repo.py +0 -0
  180. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/db/repos/relational/telemetry_repo.py +0 -0
  181. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/db/repos/relational/utility_repo.py +0 -0
  182. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/db/repos/semantic/__init__.py +0 -0
  183. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/db/repos/semantic/keyword_retrieval_repo.py +0 -0
  184. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/db/repos/semantic/semantic_retrieval_repo.py +0 -0
  185. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/db/session.py +0 -0
  186. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/db/uow.py +0 -0
  187. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/embeddings/__init__.py +0 -0
  188. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/embeddings/local_provider.py +0 -0
  189. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/embeddings/query_vector_search.py +0 -0
  190. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/episodes/__init__.py +0 -0
  191. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/episodes/claude_code.py +0 -0
  192. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/episodes/codex.py +0 -0
  193. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/episodes/cursor.py +0 -0
  194. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/episodes/launcher.py +0 -0
  195. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/episodes/model_usage.py +0 -0
  196. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/episodes/normalization.py +0 -0
  197. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/episodes/poller.py +0 -0
  198. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/episodes/poller_lock.py +0 -0
  199. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/episodes/source_discovery.py +0 -0
  200. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/episodes/tool_filter.py +0 -0
  201. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/identity/__init__.py +0 -0
  202. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/identity/claude_hook_install.py +0 -0
  203. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/identity/claude_runtime.py +0 -0
  204. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/identity/codex_runtime.py +0 -0
  205. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/identity/compatibility.py +0 -0
  206. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/identity/cursor_statusline.py +0 -0
  207. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/identity/resolver.py +0 -0
  208. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/metrics/__init__.py +0 -0
  209. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/metrics/browser.py +0 -0
  210. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/onboarding/__init__.py +0 -0
  211. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/onboarding/host_assets.py +0 -0
  212. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/session_state/__init__.py +0 -0
  213. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/session_state/file_store.py +0 -0
  214. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/telemetry/__init__.py +0 -0
  215. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/telemetry/operation_summary.py +0 -0
  216. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/telemetry/session_selection.py +0 -0
  217. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/telemetry/sync_summary.py +0 -0
  218. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/validation/__init__.py +0 -0
  219. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/validation/integrity_validation.py +0 -0
  220. {shellbrain-0.1.25 → shellbrain-0.1.27}/app/periphery/validation/semantic_validation.py +0 -0
  221. {shellbrain-0.1.25 → shellbrain-0.1.27}/setup.cfg +0 -0
  222. {shellbrain-0.1.25 → shellbrain-0.1.27}/shellbrain.egg-info/dependency_links.txt +0 -0
  223. {shellbrain-0.1.25 → shellbrain-0.1.27}/shellbrain.egg-info/entry_points.txt +0 -0
  224. {shellbrain-0.1.25 → shellbrain-0.1.27}/shellbrain.egg-info/requires.txt +0 -0
  225. {shellbrain-0.1.25 → shellbrain-0.1.27}/shellbrain.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shellbrain
3
- Version: 0.1.25
3
+ Version: 0.1.27
4
4
  Summary: Repo-scoped Shellbrain CLI with explicit evidence-backed writes.
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -0,0 +1,101 @@
1
+ """Add explicit problem-run token metrics."""
2
+
3
+ from alembic import op
4
+
5
+ from app.periphery.db.models.views import (
6
+ USAGE_PROBLEM_READ_ROI_LEGACY_SQL,
7
+ USAGE_PROBLEM_RUN_TOKENS_SQL,
8
+ USAGE_PROBLEM_TOKENS_LEGACY_SQL,
9
+ USAGE_READ_BEFORE_SOLVE_ROI_LEGACY_SQL,
10
+ )
11
+
12
+ revision = "20260422_0015"
13
+ down_revision = "20260422_0014"
14
+ branch_labels = None
15
+ depends_on = None
16
+
17
+
18
+ def _unsuffixed_proxy_sql(sql: str) -> str:
19
+ """Return the pre-legacy view name for downgrade compatibility."""
20
+
21
+ return (
22
+ sql.replace("usage_read_before_solve_roi_legacy", "usage_read_before_solve_roi")
23
+ .replace("usage_problem_read_roi_legacy", "usage_problem_read_roi")
24
+ .replace("usage_problem_tokens_legacy", "usage_problem_tokens")
25
+ )
26
+
27
+
28
+ def upgrade() -> None:
29
+ """Create problem-run windows and rename memory-derived token proxies."""
30
+
31
+ op.execute(
32
+ """
33
+ DROP VIEW IF EXISTS usage_read_before_solve_roi;
34
+ DROP VIEW IF EXISTS usage_problem_read_roi;
35
+ DROP VIEW IF EXISTS usage_problem_tokens;
36
+
37
+ CREATE TABLE problem_runs (
38
+ id TEXT PRIMARY KEY,
39
+ repo_id TEXT NOT NULL,
40
+ thread_id TEXT,
41
+ host_app TEXT,
42
+ host_session_key TEXT,
43
+ episode_id TEXT REFERENCES episodes(id) ON DELETE SET NULL,
44
+ status TEXT NOT NULL,
45
+ opened_at TIMESTAMPTZ NOT NULL,
46
+ closed_at TIMESTAMPTZ,
47
+ opened_by TEXT NOT NULL,
48
+ closed_by TEXT,
49
+ problem_memory_id TEXT REFERENCES memories(id) ON DELETE SET NULL,
50
+ solution_memory_id TEXT REFERENCES memories(id) ON DELETE SET NULL,
51
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
52
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
53
+ CONSTRAINT ck_problem_runs_status CHECK (status IN ('open', 'closed', 'abandoned')),
54
+ CONSTRAINT ck_problem_runs_opened_by CHECK (opened_by IN ('worker', 'librarian', 'manual', 'system')),
55
+ CONSTRAINT ck_problem_runs_closed_by CHECK (closed_by IS NULL OR closed_by IN ('worker', 'librarian', 'manual', 'system')),
56
+ CONSTRAINT ck_problem_runs_closed_after_opened CHECK (closed_at IS NULL OR closed_at >= opened_at),
57
+ CONSTRAINT ck_problem_runs_status_closed_at CHECK (
58
+ (
59
+ status = 'open'
60
+ AND closed_at IS NULL
61
+ ) OR (
62
+ status IN ('closed', 'abandoned')
63
+ AND closed_at IS NOT NULL
64
+ )
65
+ )
66
+ );
67
+
68
+ CREATE INDEX idx_problem_runs_repo_thread_window
69
+ ON problem_runs(repo_id, thread_id, opened_at, closed_at);
70
+ CREATE INDEX idx_problem_runs_repo_host_session_window
71
+ ON problem_runs(repo_id, host_app, host_session_key, opened_at, closed_at);
72
+ CREATE INDEX idx_problem_runs_repo_status_opened_at
73
+ ON problem_runs(repo_id, status, opened_at);
74
+ CREATE INDEX idx_problem_runs_problem_memory
75
+ ON problem_runs(problem_memory_id);
76
+ CREATE INDEX idx_problem_runs_solution_memory
77
+ ON problem_runs(solution_memory_id);
78
+ """
79
+ )
80
+ op.execute(USAGE_PROBLEM_TOKENS_LEGACY_SQL)
81
+ op.execute(USAGE_PROBLEM_READ_ROI_LEGACY_SQL)
82
+ op.execute(USAGE_READ_BEFORE_SOLVE_ROI_LEGACY_SQL)
83
+ op.execute(USAGE_PROBLEM_RUN_TOKENS_SQL)
84
+
85
+
86
+ def downgrade() -> None:
87
+ """Drop problem-run metrics and restore the previous proxy view names."""
88
+
89
+ op.execute(
90
+ """
91
+ DROP VIEW IF EXISTS usage_problem_run_tokens;
92
+ DROP TABLE IF EXISTS problem_runs;
93
+
94
+ DROP VIEW IF EXISTS usage_read_before_solve_roi_legacy;
95
+ DROP VIEW IF EXISTS usage_problem_read_roi_legacy;
96
+ DROP VIEW IF EXISTS usage_problem_tokens_legacy;
97
+ """
98
+ )
99
+ op.execute(_unsuffixed_proxy_sql(USAGE_PROBLEM_TOKENS_LEGACY_SQL))
100
+ op.execute(_unsuffixed_proxy_sql(USAGE_PROBLEM_READ_ROI_LEGACY_SQL))
101
+ op.execute(_unsuffixed_proxy_sql(USAGE_READ_BEFORE_SOLVE_ROI_LEGACY_SQL))
@@ -60,7 +60,7 @@ _TOP_LEVEL_HELP = dedent(
60
60
  Examples:
61
61
  shellbrain init
62
62
  shellbrain upgrade
63
- shellbrain metrics --days 30
63
+ shellbrain metrics
64
64
  shellbrain read --json '{"query":"Have we seen this migration lock timeout before?","kinds":["problem","solution","failed_tactic"]}'
65
65
  shellbrain read --json '{"query":"What repo constraints or user preferences matter for this auth refactor?","kinds":["fact","preference","change"]}'
66
66
  shellbrain events --json '{"limit":10}'
@@ -260,12 +260,10 @@ _BACKFILL_TOKEN_USAGE_HELP = dedent(
260
260
 
261
261
  _METRICS_HELP = dedent(
262
262
  """\
263
- Generate one lightweight repo-scoped metrics snapshot, write local artifacts, and open a static dashboard.
263
+ Generate lightweight metrics snapshots, write local artifacts, and open one browser dashboard that switches repos with arrow keys.
264
264
 
265
- Examples:
265
+ Example:
266
266
  shellbrain metrics
267
- shellbrain metrics --days 30
268
- shellbrain metrics --days 14 --no-open
269
267
  """
270
268
  )
271
269
 
@@ -386,23 +384,11 @@ def build_parser() -> argparse.ArgumentParser:
386
384
 
387
385
  metrics_parser = subparsers.add_parser(
388
386
  "metrics",
389
- help="Open a lightweight repo-scoped metrics dashboard.",
390
- description="Generate one local metrics snapshot and open a static Shellbrain dashboard.",
387
+ help="Browse Shellbrain metrics across repos.",
388
+ description="Generate local metrics snapshots and open one browser dashboard for all repos.",
391
389
  epilog=_METRICS_HELP,
392
390
  formatter_class=_HelpFormatter,
393
391
  )
394
- _add_repo_context_arguments(metrics_parser, suppress_default=True)
395
- metrics_parser.add_argument(
396
- "--days",
397
- type=int,
398
- default=30,
399
- help="Number of trailing days to include in the snapshot. Defaults to 30.",
400
- )
401
- metrics_parser.add_argument(
402
- "--no-open",
403
- action="store_true",
404
- help="Generate artifacts without opening the dashboard in the browser.",
405
- )
406
392
 
407
393
  create_parser = subparsers.add_parser(
408
394
  "create",
@@ -877,51 +863,54 @@ def _run_admin_command(args: argparse.Namespace) -> int:
877
863
 
878
864
 
879
865
  def _run_metrics_command(args: argparse.Namespace) -> int:
880
- """Generate one repo-scoped metrics snapshot and local dashboard artifacts."""
866
+ """Generate metrics snapshots and artifacts for one or many repos."""
881
867
 
882
868
  try:
883
869
  from app.boot.admin_db import get_optional_admin_db_dsn
884
870
  from app.boot.db import get_optional_db_dsn
885
871
  from app.periphery.db.engine import get_engine
886
- from app.periphery.metrics.artifacts import write_metrics_artifacts
872
+ from app.periphery.metrics.artifacts import write_metrics_artifacts, write_metrics_index_artifact
887
873
  from app.periphery.metrics.browser import open_metrics_dashboard
888
- from app.periphery.metrics.render_html import render_metrics_dashboard
889
- from app.periphery.metrics.service import build_metrics_snapshot
874
+ from app.periphery.metrics.render_html import render_metrics_browser_dashboard, render_metrics_dashboard
875
+ from app.periphery.metrics.service import build_metrics_snapshot, list_metrics_repo_ids
876
+
877
+ if bool(getattr(args, "repo_id", None) or getattr(args, "repo_root", None) or getattr(args, "no_sync", False)):
878
+ raise ValueError("`shellbrain metrics` does not accept options. Run `shellbrain metrics`.")
890
879
 
891
- repo_context = resolve_repo_context(
892
- repo_root_arg=getattr(args, "repo_root", None),
893
- repo_id_arg=getattr(args, "repo_id", None),
894
- )
895
880
  _warn_or_fail_on_unsafe_app_role()
896
- _ensure_repo_registration_for_operation(
897
- repo_context=repo_context,
898
- repo_id_override=getattr(args, "repo_id", None),
899
- )
900
881
  dsn = get_optional_db_dsn() or get_optional_admin_db_dsn()
901
882
  if not dsn:
902
883
  raise RuntimeError("Shellbrain database is not configured. Run `shellbrain init` first.")
903
- snapshot = build_metrics_snapshot(
904
- engine=get_engine(dsn),
905
- repo_id=repo_context.repo_id,
906
- days=int(args.days),
884
+ engine = get_engine(dsn)
885
+
886
+ target_repo_ids = list_metrics_repo_ids(engine=engine)
887
+ if not target_repo_ids:
888
+ print("No tracked repos found in metrics telemetry yet.")
889
+ return 0
890
+
891
+ entries: list[dict[str, Any]] = []
892
+ window_days = 30
893
+ for repo_id in target_repo_ids:
894
+ snapshot = build_metrics_snapshot(
895
+ engine=engine,
896
+ repo_id=repo_id,
897
+ days=window_days,
898
+ )
899
+ html = render_metrics_dashboard(snapshot)
900
+ paths = write_metrics_artifacts(repo_id=repo_id, snapshot=snapshot, html=html)
901
+ entries.append({"snapshot": snapshot, "paths": paths})
902
+
903
+ overview_path = write_metrics_index_artifact(
904
+ html=render_metrics_browser_dashboard([entry["snapshot"] for entry in entries])
907
905
  )
908
- html = render_metrics_dashboard(snapshot)
909
- paths = write_metrics_artifacts(repo_id=repo_context.repo_id, snapshot=snapshot, html=html)
910
- opened_dashboard = False
911
- if not bool(getattr(args, "no_open", False)):
912
- opened_dashboard = bool(open_metrics_dashboard(paths["html_path"]))
913
- print(f"Generated Shellbrain metrics for {repo_context.repo_id}")
914
- print(f"Status: {snapshot['status']} ({snapshot['confidence']} confidence)")
915
- print(f"JSON: {paths['json_path']}")
916
- print(f"Markdown: {paths['md_path']}")
917
- print(f"Dashboard: {paths['html_path']}")
906
+ opened_dashboard = bool(open_metrics_dashboard(overview_path))
907
+ print(f"Generated Shellbrain metrics for {len(entries)} repos")
908
+ print(f"Window: last {window_days} days")
918
909
  print("Artifacts: updated in place")
919
- if bool(getattr(args, "no_open", False)):
920
- print("Browser: skipped")
921
- elif opened_dashboard:
922
- print("Browser: opened dashboard")
910
+ if opened_dashboard:
911
+ print("Browser: opened dashboard; use left/right arrow keys in the browser to switch repos")
923
912
  else:
924
- print("Browser: could not open automatically")
913
+ print(f"Browser: could not open automatically; open {overview_path}")
925
914
  return 0
926
915
  except (RuntimeError, ValueError) as exc:
927
916
  print(str(exc), file=sys.stderr)
@@ -0,0 +1,60 @@
1
+ """SQLAlchemy Core table for explicit problem-run telemetry windows."""
2
+
3
+ from sqlalchemy import CheckConstraint, Column, ForeignKey, Index, String, Table, text
4
+ from sqlalchemy.dialects.postgresql import TIMESTAMP
5
+
6
+ from app.periphery.db.models.metadata import metadata
7
+
8
+
9
+ _PROBLEM_RUN_STATUSES = "'open', 'closed', 'abandoned'"
10
+ _PROBLEM_RUN_ACTORS = "'worker', 'librarian', 'manual', 'system'"
11
+
12
+
13
+ problem_runs = Table(
14
+ "problem_runs",
15
+ metadata,
16
+ Column("id", String, primary_key=True),
17
+ Column("repo_id", String, nullable=False),
18
+ Column("thread_id", String),
19
+ Column("host_app", String),
20
+ Column("host_session_key", String),
21
+ Column("episode_id", String, ForeignKey("episodes.id", ondelete="SET NULL")),
22
+ Column("status", String, nullable=False),
23
+ Column("opened_at", TIMESTAMP(timezone=True), nullable=False),
24
+ Column("closed_at", TIMESTAMP(timezone=True)),
25
+ Column("opened_by", String, nullable=False),
26
+ Column("closed_by", String),
27
+ Column("problem_memory_id", String, ForeignKey("memories.id", ondelete="SET NULL")),
28
+ Column("solution_memory_id", String, ForeignKey("memories.id", ondelete="SET NULL")),
29
+ Column("created_at", TIMESTAMP(timezone=True), nullable=False, server_default=text("NOW()")),
30
+ Column("updated_at", TIMESTAMP(timezone=True), nullable=False, server_default=text("NOW()")),
31
+ CheckConstraint(f"status IN ({_PROBLEM_RUN_STATUSES})", name="ck_problem_runs_status"),
32
+ CheckConstraint(f"opened_by IN ({_PROBLEM_RUN_ACTORS})", name="ck_problem_runs_opened_by"),
33
+ CheckConstraint(f"closed_by IS NULL OR closed_by IN ({_PROBLEM_RUN_ACTORS})", name="ck_problem_runs_closed_by"),
34
+ CheckConstraint("closed_at IS NULL OR closed_at >= opened_at", name="ck_problem_runs_closed_after_opened"),
35
+ CheckConstraint(
36
+ """
37
+ (
38
+ status = 'open'
39
+ AND closed_at IS NULL
40
+ ) OR (
41
+ status IN ('closed', 'abandoned')
42
+ AND closed_at IS NOT NULL
43
+ )
44
+ """,
45
+ name="ck_problem_runs_status_closed_at",
46
+ ),
47
+ )
48
+
49
+ Index("idx_problem_runs_repo_thread_window", problem_runs.c.repo_id, problem_runs.c.thread_id, problem_runs.c.opened_at, problem_runs.c.closed_at)
50
+ Index(
51
+ "idx_problem_runs_repo_host_session_window",
52
+ problem_runs.c.repo_id,
53
+ problem_runs.c.host_app,
54
+ problem_runs.c.host_session_key,
55
+ problem_runs.c.opened_at,
56
+ problem_runs.c.closed_at,
57
+ )
58
+ Index("idx_problem_runs_repo_status_opened_at", problem_runs.c.repo_id, problem_runs.c.status, problem_runs.c.opened_at)
59
+ Index("idx_problem_runs_problem_memory", problem_runs.c.problem_memory_id)
60
+ Index("idx_problem_runs_solution_memory", problem_runs.c.solution_memory_id)
@@ -8,12 +8,13 @@ from app.periphery.db.models import (
8
8
  experiences,
9
9
  instance_metadata,
10
10
  memories,
11
+ problem_runs,
11
12
  telemetry,
12
13
  utility,
13
14
  )
14
15
  from app.periphery.db.models.metadata import metadata
15
16
 
16
17
 
17
- _ = (associations, concepts, episodes, evidence, experiences, instance_metadata, memories, telemetry, utility)
18
+ _ = (associations, concepts, episodes, evidence, experiences, instance_metadata, memories, problem_runs, telemetry, utility)
18
19
 
19
20
  target_metadata = metadata
@@ -616,6 +616,218 @@ GROUP BY repo_id, solve_window, read_cohort;
616
616
  """
617
617
 
618
618
 
619
+ USAGE_PROBLEM_TOKENS_LEGACY_SQL = USAGE_PROBLEM_TOKENS_SQL.replace(
620
+ "CREATE OR REPLACE VIEW usage_problem_tokens AS",
621
+ "CREATE OR REPLACE VIEW usage_problem_tokens_legacy AS",
622
+ )
623
+ USAGE_PROBLEM_READ_ROI_LEGACY_SQL = (
624
+ USAGE_PROBLEM_READ_ROI_SQL.replace(
625
+ "CREATE OR REPLACE VIEW usage_problem_read_roi AS",
626
+ "CREATE OR REPLACE VIEW usage_problem_read_roi_legacy AS",
627
+ )
628
+ .replace("FROM usage_problem_tokens upt", "FROM usage_problem_tokens_legacy upt")
629
+ )
630
+ USAGE_READ_BEFORE_SOLVE_ROI_LEGACY_SQL = (
631
+ USAGE_READ_BEFORE_SOLVE_ROI_SQL.replace(
632
+ "CREATE OR REPLACE VIEW usage_read_before_solve_roi AS",
633
+ "CREATE OR REPLACE VIEW usage_read_before_solve_roi_legacy AS",
634
+ )
635
+ .replace("FROM usage_problem_read_roi", "FROM usage_problem_read_roi_legacy")
636
+ )
637
+
638
+
639
+ USAGE_PROBLEM_RUN_TOKENS_SQL = """
640
+ CREATE OR REPLACE VIEW usage_problem_run_tokens AS
641
+ WITH session_quality AS (
642
+ SELECT
643
+ repo_id,
644
+ host_app,
645
+ host_session_key,
646
+ BOOL_OR(capture_quality = 'exact') AS has_exact_rows,
647
+ BOOL_OR(
648
+ capture_quality = 'exact'
649
+ AND (
650
+ COALESCE(input_tokens, 0) > 0
651
+ OR COALESCE(output_tokens, 0) > 0
652
+ OR COALESCE(cached_input_tokens_total, 0) > 0
653
+ OR COALESCE(reasoning_output_tokens, 0) > 0
654
+ )
655
+ ) AS has_nonzero_exact_rows,
656
+ BOOL_OR(capture_quality = 'estimated') AS has_estimated_rows
657
+ FROM model_usage
658
+ GROUP BY repo_id, host_app, host_session_key
659
+ ),
660
+ preferred_rows AS (
661
+ SELECT mu.*
662
+ FROM model_usage mu
663
+ JOIN session_quality sq
664
+ ON sq.repo_id = mu.repo_id
665
+ AND sq.host_app = mu.host_app
666
+ AND sq.host_session_key = mu.host_session_key
667
+ WHERE (
668
+ sq.has_nonzero_exact_rows
669
+ AND mu.capture_quality = 'exact'
670
+ ) OR (
671
+ NOT sq.has_nonzero_exact_rows
672
+ AND mu.capture_quality = 'estimated'
673
+ ) OR (
674
+ NOT sq.has_nonzero_exact_rows
675
+ AND NOT sq.has_estimated_rows
676
+ AND sq.has_exact_rows
677
+ AND mu.capture_quality = 'exact'
678
+ )
679
+ ),
680
+ eligible_runs AS (
681
+ SELECT *
682
+ FROM problem_runs
683
+ WHERE status IN ('closed', 'abandoned')
684
+ AND closed_at IS NOT NULL
685
+ ),
686
+ run_usage AS (
687
+ SELECT
688
+ prun.id AS problem_run_id,
689
+ COUNT(mu.id)::INTEGER AS usage_row_count,
690
+ COUNT(mu.id) FILTER (WHERE mu.capture_quality = 'exact')::INTEGER AS exact_usage_row_count,
691
+ COUNT(mu.id) FILTER (WHERE mu.capture_quality = 'estimated')::INTEGER AS estimated_usage_row_count,
692
+ COALESCE(SUM(mu.input_tokens), 0)::BIGINT AS input_tokens,
693
+ COALESCE(SUM(mu.output_tokens), 0)::BIGINT AS output_tokens,
694
+ COALESCE(SUM(mu.reasoning_output_tokens), 0)::BIGINT AS reasoning_output_tokens,
695
+ COALESCE(SUM(mu.cached_input_tokens_total), 0)::BIGINT AS cached_input_tokens_total,
696
+ COALESCE(SUM(mu.cache_read_input_tokens), 0)::BIGINT AS cache_read_input_tokens,
697
+ COALESCE(SUM(mu.cache_creation_input_tokens), 0)::BIGINT AS cache_creation_input_tokens,
698
+ COALESCE(SUM(mu.input_tokens) FILTER (WHERE mu.agent_role IN ('foreground', 'worker')), 0)::BIGINT AS foreground_worker_input_tokens,
699
+ COALESCE(SUM(mu.output_tokens) FILTER (WHERE mu.agent_role IN ('foreground', 'worker')), 0)::BIGINT AS foreground_worker_output_tokens,
700
+ (
701
+ COALESCE(SUM(mu.input_tokens) FILTER (WHERE mu.agent_role IN ('foreground', 'worker')), 0)
702
+ + COALESCE(SUM(mu.output_tokens) FILTER (WHERE mu.agent_role IN ('foreground', 'worker')), 0)
703
+ )::BIGINT AS foreground_worker_fresh_work_tokens,
704
+ (
705
+ COALESCE(SUM(mu.input_tokens) FILTER (WHERE mu.agent_role IN ('foreground', 'worker')), 0)
706
+ + COALESCE(SUM(mu.cached_input_tokens_total) FILTER (WHERE mu.agent_role IN ('foreground', 'worker')), 0)
707
+ + COALESCE(SUM(mu.output_tokens) FILTER (WHERE mu.agent_role IN ('foreground', 'worker')), 0)
708
+ )::BIGINT AS foreground_worker_all_tokens_including_cache,
709
+ COALESCE(SUM(mu.input_tokens) FILTER (WHERE mu.agent_role = 'librarian'), 0)::BIGINT AS librarian_input_tokens,
710
+ COALESCE(SUM(mu.output_tokens) FILTER (WHERE mu.agent_role = 'librarian'), 0)::BIGINT AS librarian_output_tokens,
711
+ (
712
+ COALESCE(SUM(mu.input_tokens) FILTER (WHERE mu.agent_role = 'librarian'), 0)
713
+ + COALESCE(SUM(mu.output_tokens) FILTER (WHERE mu.agent_role = 'librarian'), 0)
714
+ )::BIGINT AS librarian_fresh_work_tokens,
715
+ (
716
+ COALESCE(SUM(mu.input_tokens) FILTER (WHERE mu.agent_role = 'librarian'), 0)
717
+ + COALESCE(SUM(mu.cached_input_tokens_total) FILTER (WHERE mu.agent_role = 'librarian'), 0)
718
+ + COALESCE(SUM(mu.output_tokens) FILTER (WHERE mu.agent_role = 'librarian'), 0)
719
+ )::BIGINT AS librarian_all_tokens_including_cache,
720
+ COALESCE(SUM(mu.input_tokens) FILTER (WHERE mu.agent_role NOT IN ('foreground', 'worker', 'librarian')), 0)::BIGINT AS other_role_input_tokens,
721
+ COALESCE(SUM(mu.output_tokens) FILTER (WHERE mu.agent_role NOT IN ('foreground', 'worker', 'librarian')), 0)::BIGINT AS other_role_output_tokens,
722
+ (
723
+ COALESCE(SUM(mu.input_tokens) FILTER (WHERE mu.agent_role NOT IN ('foreground', 'worker', 'librarian')), 0)
724
+ + COALESCE(SUM(mu.output_tokens) FILTER (WHERE mu.agent_role NOT IN ('foreground', 'worker', 'librarian')), 0)
725
+ )::BIGINT AS other_role_fresh_work_tokens,
726
+ (
727
+ COALESCE(SUM(mu.input_tokens) FILTER (WHERE mu.agent_role NOT IN ('foreground', 'worker', 'librarian')), 0)
728
+ + COALESCE(SUM(mu.cached_input_tokens_total) FILTER (WHERE mu.agent_role NOT IN ('foreground', 'worker', 'librarian')), 0)
729
+ + COALESCE(SUM(mu.output_tokens) FILTER (WHERE mu.agent_role NOT IN ('foreground', 'worker', 'librarian')), 0)
730
+ )::BIGINT AS other_role_all_tokens_including_cache
731
+ FROM eligible_runs prun
732
+ LEFT JOIN preferred_rows mu
733
+ ON mu.repo_id = prun.repo_id
734
+ AND mu.occurred_at >= prun.opened_at
735
+ AND mu.occurred_at <= prun.closed_at
736
+ AND (
737
+ (
738
+ prun.host_app IS NOT NULL
739
+ AND prun.host_session_key IS NOT NULL
740
+ AND mu.host_app = prun.host_app
741
+ AND mu.host_session_key = prun.host_session_key
742
+ )
743
+ OR (
744
+ (prun.host_app IS NULL OR prun.host_session_key IS NULL)
745
+ AND prun.thread_id IS NOT NULL
746
+ AND mu.thread_id = prun.thread_id
747
+ )
748
+ )
749
+ GROUP BY prun.id
750
+ ),
751
+ run_reads AS (
752
+ SELECT
753
+ prun.id AS problem_run_id,
754
+ COUNT(oi.id)::INTEGER AS shellbrain_read_count,
755
+ COALESCE(SUM(ris.pack_token_estimate), 0)::BIGINT AS shellbrain_pack_tokens,
756
+ COALESCE(SUM(ris.concept_token_estimate), 0)::BIGINT AS shellbrain_concept_tokens
757
+ FROM eligible_runs prun
758
+ LEFT JOIN operation_invocations oi
759
+ ON oi.repo_id = prun.repo_id
760
+ AND oi.command = 'read'
761
+ AND oi.outcome = 'ok'
762
+ AND oi.created_at >= prun.opened_at
763
+ AND oi.created_at <= prun.closed_at
764
+ AND (
765
+ (
766
+ prun.host_app IS NOT NULL
767
+ AND prun.host_session_key IS NOT NULL
768
+ AND oi.selected_host_app = prun.host_app
769
+ AND oi.selected_host_session_key = prun.host_session_key
770
+ )
771
+ OR (
772
+ (prun.host_app IS NULL OR prun.host_session_key IS NULL)
773
+ AND prun.thread_id IS NOT NULL
774
+ AND oi.selected_thread_id = prun.thread_id
775
+ )
776
+ )
777
+ LEFT JOIN read_invocation_summaries ris ON ris.invocation_id = oi.id
778
+ GROUP BY prun.id
779
+ )
780
+ SELECT
781
+ prun.id AS problem_run_id,
782
+ prun.repo_id,
783
+ prun.thread_id,
784
+ prun.host_app,
785
+ prun.host_session_key,
786
+ prun.status,
787
+ prun.opened_at,
788
+ prun.closed_at,
789
+ EXTRACT(EPOCH FROM (prun.closed_at - prun.opened_at))::DOUBLE PRECISION AS duration_seconds,
790
+ prun.problem_memory_id,
791
+ prun.solution_memory_id,
792
+ ru.usage_row_count,
793
+ ru.exact_usage_row_count,
794
+ ru.estimated_usage_row_count,
795
+ ru.input_tokens,
796
+ ru.output_tokens,
797
+ ru.reasoning_output_tokens,
798
+ ru.cached_input_tokens_total,
799
+ ru.cache_read_input_tokens,
800
+ ru.cache_creation_input_tokens,
801
+ (
802
+ ru.input_tokens
803
+ + ru.output_tokens
804
+ )::BIGINT AS fresh_work_tokens,
805
+ (
806
+ ru.input_tokens
807
+ + ru.cached_input_tokens_total
808
+ + ru.output_tokens
809
+ )::BIGINT AS all_tokens_including_cache,
810
+ ru.foreground_worker_input_tokens,
811
+ ru.foreground_worker_output_tokens,
812
+ ru.foreground_worker_fresh_work_tokens,
813
+ ru.foreground_worker_all_tokens_including_cache,
814
+ ru.librarian_input_tokens,
815
+ ru.librarian_output_tokens,
816
+ ru.librarian_fresh_work_tokens,
817
+ ru.librarian_all_tokens_including_cache,
818
+ ru.other_role_input_tokens,
819
+ ru.other_role_output_tokens,
820
+ ru.other_role_fresh_work_tokens,
821
+ ru.other_role_all_tokens_including_cache,
822
+ rr.shellbrain_read_count,
823
+ rr.shellbrain_pack_tokens,
824
+ rr.shellbrain_concept_tokens
825
+ FROM eligible_runs prun
826
+ JOIN run_usage ru ON ru.problem_run_id = prun.id
827
+ JOIN run_reads rr ON rr.problem_run_id = prun.id;
828
+ """
829
+
830
+
619
831
  USAGE_TOKEN_CAPTURE_HEALTH_SQL = """
620
832
  CREATE OR REPLACE VIEW usage_token_capture_health AS
621
833
  WITH synced_sessions AS (
@@ -14,12 +14,18 @@ from app.boot.home import get_shellbrain_home
14
14
  _NON_ALNUM = re.compile(r"[^a-z0-9]+")
15
15
 
16
16
 
17
+ def get_metrics_root_dir() -> Path:
18
+ """Return the machine-owned root directory for metrics browser artifacts."""
19
+
20
+ return get_shellbrain_home() / "reports" / "metrics"
21
+
22
+
17
23
  def get_metrics_artifact_dir(*, repo_id: str) -> Path:
18
24
  """Return the machine-owned artifact directory for one repo's metrics outputs."""
19
25
 
20
26
  normalized = _NON_ALNUM.sub("-", repo_id.lower()).strip("-") or "repo"
21
27
  digest = hashlib.sha1(repo_id.encode("utf-8")).hexdigest()[:8]
22
- return get_shellbrain_home() / "reports" / "metrics" / f"{normalized}-{digest}"
28
+ return get_metrics_root_dir() / f"{normalized}-{digest}"
23
29
 
24
30
 
25
31
  def write_metrics_artifacts(*, repo_id: str, snapshot: dict[str, Any], html: str) -> dict[str, Path]:
@@ -42,3 +48,14 @@ def write_metrics_artifacts(*, repo_id: str, snapshot: dict[str, Any], html: str
42
48
  "md_path": md_path,
43
49
  "html_path": html_path,
44
50
  }
51
+
52
+
53
+ def write_metrics_index_artifact(*, html: str) -> Path:
54
+ """Write the combined browser dashboard for all repo metrics snapshots."""
55
+
56
+ artifact_dir = get_metrics_root_dir()
57
+ artifact_dir.mkdir(parents=True, exist_ok=True)
58
+
59
+ html_path = artifact_dir / "index.html"
60
+ html_path.write_text(html, encoding="utf-8")
61
+ return html_path