brainiac-cli 0.20.47__tar.gz → 0.20.49__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 (368) hide show
  1. {brainiac_cli-0.20.47/src/brainiac_cli.egg-info → brainiac_cli-0.20.49}/PKG-INFO +4 -3
  2. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/pyproject.toml +20 -8
  3. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/_assets/AGENTS.md +12 -5
  4. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/_assets/scripts/brain-synthesis.sh +13 -1
  5. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/_assets/scripts/brain_synthesis_codex.py +16 -3
  6. brainiac_cli-0.20.49/src/brain/_assets/scripts/shorten_long_note_ids.py +100 -0
  7. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/_version.py +1 -1
  8. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/alerts.py +7 -7
  9. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/alerts_staging.py +36 -0
  10. brainiac_cli-0.20.49/src/brain/atomic_temp.py +49 -0
  11. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/audit.py +7 -3
  12. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/audit_drift.py +2 -5
  13. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/autolink.py +12 -9
  14. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/backup.py +13 -2
  15. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/brief.py +21 -15
  16. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/chunk.py +24 -19
  17. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cli.py +4 -1
  18. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cli_cmds/index_storage.py +59 -51
  19. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cli_cmds/lifecycle.py +1 -1
  20. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cli_cmds/lifecycle_parser.py +1 -1
  21. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cli_cmds/owner_workflow.py +17 -14
  22. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cli_read_record.py +1 -1
  23. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/config.py +0 -7
  24. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/context.py +2 -7
  25. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/conventions_scan.py +20 -12
  26. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/core/_briefing.py +19 -10
  27. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/core/_capture.py +19 -17
  28. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/core/_health.py +1 -1
  29. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/core/_maintenance_state.py +2 -5
  30. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/core/_shared.py +4 -1
  31. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/core/_supersession_journal.py +16 -9
  32. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/deliverables_materialize.py +11 -6
  33. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/deliverables_sync.py +6 -3
  34. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/doctor.py +3 -3
  35. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/doctor_checks.py +1 -1
  36. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/doctor_context.py +9 -2
  37. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/doctor_desktop.py +1 -1
  38. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/doctor_health.py +19 -12
  39. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/doctor_plugins.py +5 -0
  40. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/doctor_staging.py +3 -3
  41. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/doctor_vendor.py +17 -11
  42. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/doctor_vm.py +1 -1
  43. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/doctor_wiring.py +59 -36
  44. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/draft_drain.py +33 -2
  45. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/draft_update.py +3 -1
  46. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/egress.py +0 -11
  47. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/embed.py +4 -4
  48. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/embed_catalog.py +1 -1
  49. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/embed_onnx.py +2 -2
  50. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/embed_qwen.py +1 -1
  51. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/encryption.py +1 -1
  52. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/exceptions_page.py +3 -8
  53. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/exceptions_verify.py +2 -4
  54. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/folds/context.py +3 -0
  55. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/folds/deliverables.py +2 -1
  56. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/folds/invariants.py +4 -1
  57. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/folds/publish.py +3 -1
  58. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/frontmatter.py +1 -1
  59. brainiac_cli-0.20.49/src/brain/golden_executor.py +107 -0
  60. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/golden_ops.py +59 -26
  61. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/golden_probe_cases.py +15 -10
  62. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/graph_ops.py +3 -1
  63. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/healthreport_sections.py +55 -40
  64. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/index/_tools.py +4 -3
  65. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/index/_zones.py +25 -17
  66. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/index_stages/connection.py +5 -1
  67. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/ingest/handlers/base.py +76 -31
  68. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/ingest/handlers/concealment.py +32 -18
  69. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/ingest/handlers/concealment_html.py +28 -19
  70. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/ingest/handlers/concealment_office.py +3 -3
  71. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/ingest/handlers/concealment_pdf.py +15 -8
  72. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/ingest/handlers/email.py +5 -0
  73. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/ingest/handlers/html.py +10 -2
  74. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/ingest/handlers/image.py +8 -1
  75. brainiac_cli-0.20.49/src/brain/ingest/handlers/ocr_retry.py +35 -0
  76. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/ingest/handlers/pdf.py +45 -6
  77. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/ingest/handlers/pptx.py +8 -1
  78. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/ingest/handlers/xlsx.py +19 -4
  79. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/ingest/handlers/zip.py +12 -1
  80. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/ingest/pipeline_files.py +3 -7
  81. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/ingest/pipeline_stages.py +16 -14
  82. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/ingest/tierguard.py +10 -4
  83. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/init_seed.py +0 -50
  84. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/injection_scan.py +32 -3
  85. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/invariant_coverage.py +25 -15
  86. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/invariant_deliverables.py +4 -4
  87. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/invariant_reachability.py +12 -8
  88. brainiac_cli-0.20.49/src/brain/invariant_shared.py +32 -0
  89. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/invariants.py +6 -26
  90. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/maintenance_folds.py +7 -7
  91. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/maintenance_folds_3.py +2 -5
  92. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/maintenance_folds_4.py +3 -1
  93. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/maintenance_hotmd.py +18 -10
  94. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/maintenance_notify.py +73 -50
  95. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/maintenance_retention.py +38 -32
  96. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/mcp_housekeeping_verbs.py +1 -1
  97. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/notes.py +45 -40
  98. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/overlay.py +38 -27
  99. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/overlay_generated.py +6 -7
  100. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/provision.py +1 -3
  101. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/querylog.py +4 -0
  102. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/querylog_digest.py +2 -2
  103. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/querylog_status.py +19 -10
  104. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/read_log.py +42 -33
  105. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/reexec.py +1 -1
  106. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/remediation.py +15 -90
  107. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/remediation_exceptions_store.py +2 -4
  108. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/remediation_extract_retry.py +15 -10
  109. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/remediation_folds.py +5 -1
  110. brainiac_cli-0.20.49/src/brain/remediation_invariants.py +93 -0
  111. brainiac_cli-0.20.49/src/brain/remediation_security.py +21 -0
  112. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/remediation_state.py +2 -4
  113. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/rerank.py +3 -3
  114. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/rerank_onnx.py +3 -3
  115. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/session_hook_settings.py +2 -4
  116. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/snapshot.py +5 -1
  117. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/update.py +10 -2
  118. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/update_channels.py +44 -36
  119. brainiac_cli-0.20.49/src/brain/update_engine_only.py +48 -0
  120. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/update_ops.py +2 -1
  121. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/update_state.py +5 -4
  122. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/update_venv.py +3 -1
  123. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/vectors.py +3 -3
  124. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/vm_ceiling.py +2 -1
  125. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/vmstaging.py +0 -61
  126. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/workspaces.py +2 -4
  127. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49/src/brainiac_cli.egg-info}/PKG-INFO +4 -3
  128. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brainiac_cli.egg-info/SOURCES.txt +6 -0
  129. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brainiac_cli.egg-info/requires.txt +3 -2
  130. brainiac_cli-0.20.47/src/brain/atomic_temp.py +0 -6
  131. brainiac_cli-0.20.47/src/brain/invariant_shared.py +0 -4
  132. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/LICENSE +0 -0
  133. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/MANIFEST.in +0 -0
  134. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/README.md +0 -0
  135. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/setup.cfg +0 -0
  136. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/__init__.py +0 -0
  137. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/__main__.py +0 -0
  138. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/_assets/assets/graph-explorer-template.html +0 -0
  139. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/_assets/overlay/template/brand/brand-guide.md +0 -0
  140. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/_assets/overlay/template/keywords/glossary.md +0 -0
  141. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/_assets/overlay/template/people/roster.md +0 -0
  142. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/_assets/overlay/template/voice/voice-profile.md +0 -0
  143. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/_assets/routines/manifest.json +0 -0
  144. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/_assets/scripts/brain-brief-mac.plist +0 -0
  145. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/_assets/scripts/brain-brief.sh +0 -0
  146. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/_assets/scripts/brain-synthesis-mac.plist +0 -0
  147. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/_assets/scripts/brainiac-alerts.sh +0 -0
  148. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/_assets/scripts/brainiac-egress-guard.sh +0 -0
  149. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/_assets/scripts/install-brief-mac.sh +0 -0
  150. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/_assets/scripts/install-brief-windows.ps1 +0 -0
  151. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/_assets/scripts/register_tasks.py +0 -0
  152. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/_assets/scripts/vm-boundary-probe.sh +0 -0
  153. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/_assets/scripts/vm-selftest.sh +0 -0
  154. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/_assets/templates/company.md +0 -0
  155. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/_assets/templates/concept.md +0 -0
  156. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/_assets/templates/daily.md +0 -0
  157. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/_assets/templates/decision.md +0 -0
  158. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/_assets/templates/deliverable.md +0 -0
  159. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/_assets/templates/meeting.md +0 -0
  160. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/_assets/templates/person.md +0 -0
  161. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/_assets/templates/project.md +0 -0
  162. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/_assets/templates/state-moc.md +0 -0
  163. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/_optional.py +0 -0
  164. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/alerts_render.py +0 -0
  165. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/anchor.py +0 -0
  166. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/attestation.py +0 -0
  167. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/audit_chain.py +0 -0
  168. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/brief_render.py +0 -0
  169. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/capture.py +0 -0
  170. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/classification.py +0 -0
  171. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cli_cmds/__init__.py +0 -0
  172. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cli_cmds/common_parser.py +0 -0
  173. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cli_cmds/connect.py +0 -0
  174. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cli_cmds/connect_parser.py +0 -0
  175. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cli_cmds/eval.py +0 -0
  176. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cli_cmds/eval_parser.py +0 -0
  177. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cli_cmds/index_storage_parser.py +0 -0
  178. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cli_cmds/ingest_storage.py +0 -0
  179. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cli_cmds/ingest_storage_parser.py +0 -0
  180. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cli_cmds/maintenance.py +0 -0
  181. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cli_cmds/maintenance_parser.py +0 -0
  182. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cli_cmds/navigation.py +0 -0
  183. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cli_cmds/navigation_parser.py +0 -0
  184. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cli_cmds/owner_workflow_parser.py +0 -0
  185. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cli_cmds/presentation.py +0 -0
  186. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cli_cmds/presentation_parser.py +0 -0
  187. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cli_cmds/retrieval.py +0 -0
  188. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cli_cmds/retrieval_parser.py +0 -0
  189. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cli_cmds/update.py +0 -0
  190. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cli_cmds/update_parser.py +0 -0
  191. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cli_cmds/versioning.py +0 -0
  192. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cli_cmds/versioning_parser.py +0 -0
  193. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cli_help.py +0 -0
  194. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cli_render.py +0 -0
  195. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/config_hostpaths.py +0 -0
  196. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/connect.py +0 -0
  197. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/conventions.py +0 -0
  198. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/core/__init__.py +0 -0
  199. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/core/_audit.py +0 -0
  200. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/core/_capture_staging.py +0 -0
  201. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/core/_durability.py +0 -0
  202. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/core/_maintenance.py +0 -0
  203. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/core/_retrieval.py +0 -0
  204. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/core/_supersede_batch.py +0 -0
  205. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/core/_supersession.py +0 -0
  206. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cowork_leak_artifact.py +0 -0
  207. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cowork_leak_databases.py +0 -0
  208. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cowork_leak_notes.py +0 -0
  209. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cowork_leak_scan.py +0 -0
  210. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cowork_snapshot_guard.py +0 -0
  211. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/cowork_staging.py +0 -0
  212. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/dbretry.py +0 -0
  213. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/deliverables_absorb.py +0 -0
  214. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/deliverables_apply.py +0 -0
  215. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/deliverables_ledger.py +0 -0
  216. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/deliverables_previous.py +0 -0
  217. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/deliverables_readme.py +0 -0
  218. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/deliverables_shelf.py +0 -0
  219. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/doctor_mount_leak.py +0 -0
  220. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/egress_terms.py +0 -0
  221. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/embed_backends.py +0 -0
  222. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/exceptions_cli.py +0 -0
  223. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/exceptions_render.py +0 -0
  224. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/exceptions_tray.py +0 -0
  225. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/folds/__init__.py +0 -0
  226. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/folds/daily.py +0 -0
  227. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/folds/golden.py +0 -0
  228. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/folds/graph.py +0 -0
  229. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/folds/intake.py +0 -0
  230. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/folds/orchestrator.py +0 -0
  231. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/folds/organization.py +0 -0
  232. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/folds/preflight.py +0 -0
  233. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/folds/remediation.py +0 -0
  234. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/folds/remediation_bookkeeping.py +0 -0
  235. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/folds/reporting.py +0 -0
  236. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/folds/retention.py +0 -0
  237. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/folds/watchdogs.py +0 -0
  238. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/folds/weekly.py +0 -0
  239. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/golden_probe.py +0 -0
  240. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/graph.py +0 -0
  241. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/graph_hygiene.py +0 -0
  242. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/graphify.py +0 -0
  243. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/graphify_edges.py +0 -0
  244. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/graphreport.py +0 -0
  245. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/graphreport_sections.py +0 -0
  246. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/graphreport_sections_2.py +0 -0
  247. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/guard_settings.py +0 -0
  248. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/healthreport.py +0 -0
  249. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/healthreport_unsigned.py +0 -0
  250. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/inbox.py +0 -0
  251. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/index/__init__.py +0 -0
  252. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/index/_connection.py +0 -0
  253. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/index/_families.py +0 -0
  254. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/index/_graph.py +0 -0
  255. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/index/_identity.py +0 -0
  256. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/index/_lifecycle.py +0 -0
  257. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/index/_models.py +0 -0
  258. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/index/_planning.py +0 -0
  259. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/index/_records.py +0 -0
  260. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/index/_retirement.py +0 -0
  261. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/index/_schema.py +0 -0
  262. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/index/_search.py +0 -0
  263. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/index/_settings.py +0 -0
  264. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/index/_shared.py +0 -0
  265. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/index_stages/__init__.py +0 -0
  266. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/index_stages/diagnostics.py +0 -0
  267. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/index_stages/duplicate_families.py +0 -0
  268. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/index_stages/near_duplicates.py +0 -0
  269. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/index_stages/rebuild.py +0 -0
  270. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/index_stages/reranking.py +0 -0
  271. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/index_stages/search.py +0 -0
  272. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/index_stages/search_finalization.py +0 -0
  273. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/index_stages/search_identity.py +0 -0
  274. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/index_stages/search_ranking.py +0 -0
  275. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/index_stages/sync.py +0 -0
  276. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/ingest/__init__.py +0 -0
  277. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/ingest/deliverables.py +0 -0
  278. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/ingest/handlers/__init__.py +0 -0
  279. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/ingest/handlers/concealment_gate.py +0 -0
  280. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/ingest/handlers/concealment_ledger.py +0 -0
  281. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/ingest/handlers/docx.py +0 -0
  282. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/ingest/handlers/ole.py +0 -0
  283. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/ingest/handlers/tables.py +0 -0
  284. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/ingest/handlers/text.py +0 -0
  285. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/ingest/pipeline.py +0 -0
  286. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/ingest/pipeline_duplicates.py +0 -0
  287. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/ingest/pipeline_injection.py +0 -0
  288. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/ingest/pipeline_nested.py +0 -0
  289. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/ingest/pipeline_run.py +0 -0
  290. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/ingest/tierguard_verdict.py +0 -0
  291. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/ingest/transcript.py +0 -0
  292. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/init.py +0 -0
  293. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/init_import.py +0 -0
  294. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/init_samples.py +0 -0
  295. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/init_steps.py +0 -0
  296. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/injection_fold.py +0 -0
  297. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/interview.py +0 -0
  298. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/interview_apply.py +0 -0
  299. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/interview_detect.py +0 -0
  300. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/invariant_crosstier.py +0 -0
  301. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/invariant_floors.py +0 -0
  302. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/invariant_ingest_guard.py +0 -0
  303. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/invariant_lane_health.py +0 -0
  304. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/invariant_unsigned_notes.py +0 -0
  305. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/invariants_metrics.py +0 -0
  306. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/language.py +0 -0
  307. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/link_targets.py +0 -0
  308. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/lock.py +0 -0
  309. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/maintenance.py +0 -0
  310. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/maintenance_escalation.py +0 -0
  311. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/maintenance_folds_2.py +0 -0
  312. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/maintenance_golden.py +0 -0
  313. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/maintenance_graphfolds.py +0 -0
  314. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/maintenance_healthlog.py +0 -0
  315. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/maintenance_invariant_fields.py +0 -0
  316. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/maintenance_outcomes.py +0 -0
  317. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/maintenance_para.py +0 -0
  318. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/maintenance_recommendations.py +0 -0
  319. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/maintenance_sweep.py +0 -0
  320. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/maintenance_watchdog.py +0 -0
  321. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/mcp_adapter.py +0 -0
  322. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/mcp_capture_verbs.py +0 -0
  323. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/mcp_mediation.py +0 -0
  324. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/mcp_supersede_verbs.py +0 -0
  325. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/mcp_tools/__init__.py +0 -0
  326. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/mcp_tools/capture.py +0 -0
  327. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/mcp_tools/core_read.py +0 -0
  328. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/mcp_tools/housekeeping.py +0 -0
  329. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/mcp_tools/retrieval.py +0 -0
  330. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/mcp_tools/supersession.py +0 -0
  331. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/mcp_verbs.py +0 -0
  332. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/model_slot.py +0 -0
  333. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/multihop.py +0 -0
  334. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/originals_resolve.py +0 -0
  335. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/overlay_core.py +0 -0
  336. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/overlay_keywords.py +0 -0
  337. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/overlay_rules.py +0 -0
  338. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/pathkey.py +0 -0
  339. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/progress.py +0 -0
  340. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/projection.py +0 -0
  341. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/provenance.py +0 -0
  342. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/provision_mcp.py +0 -0
  343. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/provision_stage.py +0 -0
  344. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/provision_wire.py +0 -0
  345. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/querylog_replay.py +0 -0
  346. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/remediation_answers.py +0 -0
  347. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/remediation_branches.py +0 -0
  348. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/remediation_converged.py +0 -0
  349. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/remediation_exceptions.py +0 -0
  350. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/remediation_questions.py +0 -0
  351. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/remediation_routing.py +0 -0
  352. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/remediation_schema.py +0 -0
  353. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/retrieval_ops.py +0 -0
  354. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/retro.py +0 -0
  355. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/session_hook.py +0 -0
  356. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/session_hook_spec.py +0 -0
  357. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/status_ops.py +0 -0
  358. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/supersede_declared.py +0 -0
  359. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/sweepdirs.py +0 -0
  360. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/update_channels_2.py +0 -0
  361. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/update_cos_guard.py +0 -0
  362. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/update_model.py +0 -0
  363. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/update_plugins.py +0 -0
  364. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/versionlink.py +0 -0
  365. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brain/versionlink_stages.py +0 -0
  366. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brainiac_cli.egg-info/dependency_links.txt +0 -0
  367. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brainiac_cli.egg-info/entry_points.txt +0 -0
  368. {brainiac_cli-0.20.47 → brainiac_cli-0.20.49}/src/brainiac_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: brainiac-cli
3
- Version: 0.20.47
3
+ Version: 0.20.49
4
4
  Summary: Brainiac — local any-LLM second-brain core engine + brain CLI (Markdown truth, derived SQLite index).
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Homepage, https://github.com/Autopsias/brainiac
@@ -19,7 +19,7 @@ Requires-Dist: cryptography>=49
19
19
  Requires-Dist: PyYAML>=6
20
20
  Requires-Dist: regex>=2023.0.0
21
21
  Requires-Dist: keyring>=24; sys_platform == "win32"
22
- Requires-Dist: pypdf>=6.21.2
22
+ Requires-Dist: pypdf>=6.21.4
23
23
  Requires-Dist: python-docx>=1.1
24
24
  Requires-Dist: python-pptx>=0.6.23
25
25
  Requires-Dist: openpyxl>=3.1
@@ -44,7 +44,7 @@ Requires-Dist: PyYAML>=6; extra == "corporate"
44
44
  Provides-Extra: yaml
45
45
  Requires-Dist: PyYAML>=6; extra == "yaml"
46
46
  Provides-Extra: ingest
47
- Requires-Dist: pypdf>=6.21.2; extra == "ingest"
47
+ Requires-Dist: pypdf>=6.21.4; extra == "ingest"
48
48
  Requires-Dist: python-docx>=1.1; extra == "ingest"
49
49
  Requires-Dist: python-pptx>=0.6.23; extra == "ingest"
50
50
  Requires-Dist: openpyxl>=3.1; extra == "ingest"
@@ -63,6 +63,7 @@ Provides-Extra: quant-tools
63
63
  Requires-Dist: onnx>=1.14; extra == "quant-tools"
64
64
  Provides-Extra: mcp
65
65
  Requires-Dist: mcp<2,>=1.30.0; extra == "mcp"
66
+ Requires-Dist: PyJWT>=2.15.0; extra == "mcp"
66
67
  Provides-Extra: index
67
68
  Requires-Dist: regex>=2023.0.0; extra == "index"
68
69
  Dynamic: license-file
@@ -8,7 +8,7 @@ build-backend = "setuptools.build_meta"
8
8
  # check. The IMPORT package stays `brain` and the console script stays
9
9
  # `brain` — only the distribution/project name changes.
10
10
  name = "brainiac-cli"
11
- version = "0.20.47"
11
+ version = "0.20.49"
12
12
  description = "Brainiac — local any-LLM second-brain core engine + brain CLI (Markdown truth, derived SQLite index)."
13
13
  readme = "README.md"
14
14
  license = "Apache-2.0"
@@ -42,7 +42,7 @@ dependencies = [
42
42
  # because requirements.lock is CI/audit-only -- a plain
43
43
  # `pip install brainiac-cli` reads THIS constraint, and >=4.0 let the
44
44
  # vulnerable 6.14.2 survive an upgrade to 0.20.5 on a real host.
45
- "pypdf>=6.21.2",
45
+ "pypdf>=6.21.4",
46
46
  "python-docx>=1.1",
47
47
  "python-pptx>=0.6.23",
48
48
  "openpyxl>=3.1",
@@ -97,7 +97,7 @@ yaml = ["PyYAML>=6"]
97
97
  # message hints valid even though a default install already has these. Absent,
98
98
  # `brain ingest` degrades per-handler (reports which extensions are
99
99
  # unavailable) rather than crashing.
100
- ingest = ["pypdf>=6.21.2", "python-docx>=1.1", "python-pptx>=0.6.23", "openpyxl>=3.1", "Pillow>=13.0.0; python_version >= '3.11'", "Pillow>=12.3.0; python_version < '3.11'"]
100
+ ingest = ["pypdf>=6.21.4", "python-docx>=1.1", "python-pptx>=0.6.23", "openpyxl>=3.1", "Pillow>=13.0.0; python_version >= '3.11'", "Pillow>=12.3.0; python_version < '3.11'"]
101
101
  # pytest-xdist/-timeout back the documented parallel suite run (AGENTS.md §8,
102
102
  # "Running the test suite"): ~15 min sequential -> ~6 min at `-n 8`. Both are
103
103
  # pytest-dev org, actively maintained. Declared here so a fresh checkout can
@@ -128,7 +128,9 @@ quant-tools = ["onnx>=1.14"]
128
128
  # >=1.30.0: CVE-2026-59951 (Streamable HTTP stateful sessions never reclaimed,
129
129
  # unbounded memory) is fixed there. The FLOOR, not just the lock, for the same
130
130
  # reason as pypdf above: a plain `pip install` reads this, not requirements.lock.
131
- mcp = ["mcp>=1.30.0,<2"]
131
+ # PyJWT>=2.15.0: the floor mcp pulls in, stated here so a plain install
132
+ # cannot resolve an older PyJWT (public PR #36).
133
+ mcp = ["mcp>=1.30.0,<2", "PyJWT>=2.15.0"]
132
134
  # Timeout-bounded regex engine (mrab-regex) used by BrainIndex.grep's --regex
133
135
  # path (RET-04 hardening) as a ReDoS guard: regex.search(text, timeout=...)
134
136
  # genuinely bounds catastrophic backtracking, unlike stdlib `re`. Absent ->
@@ -183,7 +185,7 @@ known_first_party = ["brain", "workspace_registry", "framework_sync", "qwen3_emb
183
185
  # package_module_name_map: the 4 real import-name mismatches (distribution
184
186
  # name vs. the name actually imported).
185
187
  package_module_name_map = { PyYAML = "yaml", "python-docx" = "docx", "python-pptx" = "pptx", Pillow = "PIL" }
186
- per_rule_ignores = { DEP001 = ["llama_cpp", "lxml"], DEP002 = ["pytest", "pytest-xdist", "pytest-timeout", "ruff", "build", "onnx", "ranx"] }
188
+ per_rule_ignores = { DEP001 = ["llama_cpp", "lxml"], DEP002 = ["pytest", "pytest-xdist", "pytest-timeout", "ruff", "build", "onnx", "ranx", "PyJWT"] }
187
189
  # DEP001 llama_cpp/lxml: both are optional accelerants guarded by
188
190
  # try/except ImportError (src/brain/context.py, src/brain/ingest/handlers/html.py)
189
191
  # — the code degrades gracefully absent either, same pattern as the existing
@@ -195,12 +197,15 @@ per_rule_ignores = { DEP001 = ["llama_cpp", "lxml"], DEP002 = ["pytest", "pytest
195
197
  # src/brain package. ranx is a real [eval] runtime import, but its only
196
198
  # import site is eval/, which extend_exclude drops above for the DEP001
197
199
  # noise reason — excluding it flips ranx to a false DEP002.
200
+ # DEP002 PyJWT: a deliberate floor pin for mcp (see the mcp extra above,
201
+ # public PR #36); never imported by our own code.
198
202
 
199
203
  [tool.claude-quality]
200
204
  # Trees the size gates skip: frozen plan/archive/evidence dirs and the
201
205
  # generated _assets mirror of tools/ + scripts/ (re-synced by
202
206
  # tools/package_clients.py). Not authored source.
203
- exclude = ["_plans", "_archive", "_evidence", "_assets"]
207
+ # eval/ holds frozen, one-off experiment scripts, not shipped source (owner choice 2026-11-03).
208
+ exclude = ["_plans", "_archive", "_evidence", "_assets", "eval"]
204
209
 
205
210
  [tool.ruff]
206
211
  # CORRECTNESS ONLY — this is not a style gate. The repo already has three
@@ -257,6 +262,9 @@ select = ["F", "E9", "E722"]
257
262
  # exists to express that.
258
263
  ignore = ["F403", "F405"]
259
264
 
265
+ [tool.ruff.lint.mccabe]
266
+ max-complexity = 12 # matches the complexity ratchet limit (tools/check_complexity.py, limit=12)
267
+
260
268
  [tool.ruff.lint.per-file-ignores]
261
269
  # F811 cannot see pytest. A module-level `@pytest.fixture def text()` and a test
262
270
  # that takes `text` as a parameter is the framework's core idiom, and this rule
@@ -264,6 +272,10 @@ ignore = ["F403", "F405"]
264
272
  # real F811s all live outside tests/.
265
273
  "tests/**" = ["F811"]
266
274
 
275
+ # eval/ is out of the size and complexity gates (see [tool.claude-quality]); the
276
+ # correctness lint still runs on it.
277
+ "eval/**" = ["C901"]
278
+
267
279
 
268
280
  [tool.pytest.ini_options]
269
281
  # Scope collection to tests/ so pytest never descends into a local model venv
@@ -302,9 +314,9 @@ exclude = ["**/_plans", "**/_archive", "**/_evidence", "**/_assets"]
302
314
 
303
315
  [tool.uv]
304
316
  # Wait 7 days before resolving a newly published version (a new release can be
305
- # malicious or broken). shortcut: pypdf 6.21.2, filelock 4.2.4 and certifi
317
+ # malicious or broken). shortcut: pypdf 6.21.4 (fixes 7 DoS advisories, 2026-10-31), filelock 4.2.4 and certifi
306
318
  # 2026.10.23 are security fixes under 7 days old on 2026-10-24, so they are
307
- # exempt; remove the exemptions after 2026-10-31.
319
+ # exempt; re-check the exemptions after 2026-11-08.
308
320
  exclude-newer = "7 days"
309
321
  exclude-newer-package = { pypdf = false, filelock = false, certifi = false }
310
322
  # fastembed (the optional `embed` extra, not installed by default) caps
@@ -449,7 +449,7 @@ gates.
449
449
  signal (`emitter_output`, 2026-08-20).** The paragraph above is right and
450
450
  is not weakened: `proposal`, `report`, `analysis` and `review` stay OUT of
451
451
  `OPERATIONAL_SOURCE_TYPES` because a human writes those about the
452
- business. The gap it leaves is narrower — when `_log_rotation.py` emits
452
+ business. The gap it leaves is narrower — when a log-rotation script (not tracked here) emits
453
453
  `type: proposal` once a day, type says "keep" and nothing else disagrees.
454
454
  The reference vault carried **22** daily `Log rotation proposal` files
455
455
  from one script, each counted as a source awaiting a note. So the second
@@ -1028,6 +1028,12 @@ rules, in order:
1028
1028
  env-exported `$BRAIN_VM_MAX_EGRESS_TIER` is IGNORED (the session's own
1029
1029
  shell can set it; VULN-3386). Raising a VM's ceiling is a host-operator
1030
1030
  action (`brain vm-egress-tier <TIER>`, signed), never the model's own.
1031
+ That clamp binds the VM's own `brain --role vm` CLI only. A Cowork session
1032
+ that reads through the host broker (`brain-mcp`, the Desktop MCP entry)
1033
+ gets the BROKER's ceiling — the full vault unless `$BRAIN_MAX_EGRESS_TIER`
1034
+ narrows that entry — because the broker receives no caller role (owner
1035
+ ruling, `docs/security-acceptances.md` A-01; pentest VULN-3419). When
1036
+ asked for your cap, report the broker's, never the VM file's.
1031
1037
 
1032
1038
  3. **Ask it in every language the vault holds — the VARIANT CONTRACT
1033
1039
  (CON-01) — and paraphrase within your own.** The cross-language half is a
@@ -1850,8 +1856,9 @@ entry formats in `docs/session-memory.md`. Rules an agent needs at a glance:
1850
1856
  count match the host's; there is nothing enforcing that a session actually
1851
1857
  does it. When `N > 0`, do not tell the owner to go run `/brain-inbox` —
1852
1858
  open the exceptions page yourself and walk them through what is open using
1853
- `AskUserQuestion`, one at a time. The two host-home sources
1854
- (`~/.brainiac/update-state.json`, `~/.brain/synthesis-state.json`) stay out
1859
+ `AskUserQuestion`, one at a time. The three host-home sources
1860
+ (`~/.brainiac/update-state.json`, `~/.brain/synthesis-state.json`,
1861
+ `~/.brain/retrieval-watch-state.json`) stay out
1855
1862
  of the VM's reach and are listed under `unreachable` rather than skipped.
1856
1863
  Claude Code and Codex both fire this from a SessionStart hook; run it first
1857
1864
  regardless of harness.
@@ -1928,8 +1935,8 @@ summary is the one authority for open owner questions on the VM leg now.
1928
1935
  That is the whole point of the first bullet above — until 2026-08-14 this
1929
1936
  logic lived only in a Claude Code hook, so a Cowork session worked for days
1930
1937
  against a vault whose invariants had regressed with no surface that could
1931
- tell it. Only the two HOST-HOME sources (`~/.brainiac/update-state.json`,
1932
- `~/.brain/synthesis-state.json`) are out of the VM's reach, and `alerts`
1938
+ tell it. Only the three HOST-HOME sources (`~/.brainiac/update-state.json`,
1939
+ `~/.brain/synthesis-state.json`, `~/.brain/retrieval-watch-state.json`) are out of the VM's reach, and `alerts`
1933
1940
  reports those under `unreachable` rather than skipping them. The reads are
1934
1941
  file-reads of counts and two findings files; the VM still never WRITES
1935
1942
  anything here,
@@ -82,7 +82,9 @@ note and will be rejected; cite the source in the BODY as [[<bare-id>]], never
82
82
  edge, so it inflates the metric while linking nothing); put the [[raw/<id>]]
83
83
  form in the note's `source:` frontmatter as usual; set the note's
84
84
  classification to at least the HIGHEST classification among the sources it
85
- cites, read live off each source; and NEVER append these links into an
85
+ cites, read live off each source; give each new note a SHORT kebab-case
86
+ id of at most 80 characters, the same as its file name (a sentence-long id
87
+ is refused at signing and the note stays unsigned); and NEVER append these links into an
86
88
  existing hand-curated note — always a new note. Work the list in order; in
87
89
  your final summary state HOW MANY of the file's candidates you covered (the
88
90
  file's own "consumed_today" field is the fold's count — quote yours beside
@@ -335,6 +337,16 @@ doctor: $DOCTOR_DIAG"
335
337
  find "$VAULT/brain" -type f -name '*.md' \
336
338
  ! -name 'backlinks.md' ! -name 'catalog.md' \
337
339
  -newer "$SIGN_MARKER" > "$SIGN_LIST" 2>/dev/null || true
340
+ # A sentence-long note name is refused by the drain's safe-id check
341
+ # (2026-10-25: 20 of 45 notes unsigned, rc=75). Give those a short id
342
+ # first; anything it cannot shorten safely is left for the drain to refuse.
343
+ SHORT_LIST="$(mktemp -t brain-synthesis-shortlist)"
344
+ if python3 "$(dirname "$0")/shorten_long_note_ids.py" "$SIGN_LIST" \
345
+ > "$SHORT_LIST" 2>>"$LOG"; then
346
+ mv -f -- "$SHORT_LIST" "$SIGN_LIST"
347
+ else
348
+ rm -f -- "$SHORT_LIST"
349
+ fi
338
350
  SIGNED=0; SIGN_FAIL=0
339
351
  # fd 3, not stdin: `brain write` reads the note body FROM STDIN, so a
340
352
  # plain `while read` loop would feed it the file list instead.
@@ -58,7 +58,11 @@ existing-note link suggestions, or missing watchdogs in action_required rather
58
58
  than changing existing notes. This runner never edits or retires existing notes.
59
59
  Do not create a duplicate of a supplied existing note. If there is no honest
60
60
  new synthesis, return notes=[] and explain why in summary/action_required.
61
+ Maintenance status="ok" with a recent last_run is success; last_success and rc
62
+ are optional fields on many folds. Missing optional fields are not failures.
61
63
  Note bodies must be 120–8000 characters; titles 1–200 characters.
64
+ Use each distinct set of source_ids for at most one note. Combine related
65
+ claims from the same source set rather than repeating that set in several notes.
62
66
 
63
67
  UNTRUSTED DATA PACKET:
64
68
  {packet}
@@ -113,8 +117,10 @@ def make_packet(args, core):
113
117
  lane = link_lane_candidates(core.index.conn, args.vault, limit=100000)
114
118
  gate = classification.ClassificationFilter(args.max_tier)
115
119
  eligible = [x for x in lane["candidates"] if gate.allows(x.get("classification"))]
120
+ offset = getattr(args, "source_offset", 0) % max(len(eligible), 1)
121
+ ordered = eligible[offset:] + eligible[:offset]
116
122
  sources, remaining = [], args.packet_chars
117
- for row in eligible:
123
+ for row in ordered:
118
124
  if len(sources) >= args.source_budget or remaining < 120:
119
125
  break
120
126
  note = get_gated(args, row["id"])
@@ -145,7 +151,7 @@ def make_packet(args, core):
145
151
  except (OSError, ValueError):
146
152
  state = {}
147
153
  # Diagnostics expose cadence/status, not arbitrary strings from host stores.
148
- diagnostics = {key: {k: value.get(k) for k in ("last_run", "last_success", "date", "rc")}
154
+ diagnostics = {key: {k: value.get(k) for k in ("last_run", "last_success", "date", "rc", "status", "failed", "consecutive_failures")}
149
155
  for key, value in state.items() if isinstance(value, dict)}
150
156
  return {"sources": sources, "knowledge_context": knowledge,
151
157
  "diagnostics": diagnostics,
@@ -322,6 +328,8 @@ def parser():
322
328
  p.add_argument("--model", default=None)
323
329
  p.add_argument("--max-tier", choices=classification.TIERS, default="Internal")
324
330
  p.add_argument("--source-budget", type=int, default=40)
331
+ p.add_argument("--source-offset", type=int, default=0,
332
+ help="Rotate the canonical eligible lane for an operator-run backfill; no exclusions")
325
333
  p.add_argument("--note-budget", type=int, default=8)
326
334
  p.add_argument("--packet-chars", type=int, default=120000)
327
335
  p.add_argument("--timeout", type=int, default=1200)
@@ -337,7 +345,7 @@ def parser():
337
345
 
338
346
 
339
347
  def validate_args(args):
340
- if not (1 <= args.source_budget <= 40 and 1 <= args.note_budget <= 8
348
+ if not (0 <= args.source_offset <= 100000 and 1 <= args.source_budget <= 40 and 1 <= args.note_budget <= 8
341
349
  and 1000 <= args.packet_chars <= 120000 and 30 <= args.timeout <= 1800):
342
350
  raise SystemExit("Bounds refused")
343
351
  for binary in (args.brain_bin, args.codex_bin):
@@ -441,6 +449,11 @@ def main():
441
449
  # A dry-run/smoke test must never earn the production watchdog heartbeat.
442
450
  if not args.smoke_test and not args.dry_run:
443
451
  update_state(args, outcome)
452
+ # Numbers only: summary/action_required/error carry vault-derived text and
453
+ # stay in the owner-only run_dir, never in the launchd log this lands in.
454
+ print(json.dumps({"rc": outcome["rc"], "run_dir": outcome["run_dir"],
455
+ "created": len(outcome.get("created") or []),
456
+ **{k: outcome.get(k) for k in ("tokens", "duration_s", "est_cost_usd")}}))
444
457
  return outcome["rc"]
445
458
 
446
459
 
@@ -0,0 +1,100 @@
1
+ """Give a too-long note a short id before the synthesis sign-drain signs it.
2
+
3
+ 2026-10-25: the confined session named 20 of its 45 notes after a whole
4
+ sentence (up to 208 characters). `notes.safe_slug` refuses an id over
5
+ MAX_SLUG_BYTES, so all 20 stayed unsigned and the run failed rc=75.
6
+
7
+ Reads the drain's file list (one path per line), and for each note whose
8
+ file stem is over the limit AND equals its frontmatter `id:`, moves it to
9
+ `<first words>-<8 hex of the old id>.md` and rewrites that one `id:` line.
10
+ Anything else is left exactly as it was, so the drain still refuses it.
11
+ Then rewrites `[[old-id]]` links to `[[new-id]]` in every listed note: the
12
+ same session wrote the notes that link to these, and on 2026-10-26 a rename
13
+ without this left 18 links pointing at files that no longer existed.
14
+ Prints the list back with the new paths. Stdlib only: the script runs it
15
+ with the host's `python3`, outside the engine's venv.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import hashlib
20
+ import re
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ #: Same number as `brain.notes.MAX_SLUG_BYTES` (a test holds them equal).
25
+ MAX_SLUG_BYTES = 180
26
+ #: Short enough to read, long enough to keep the subject.
27
+ KEEP_BYTES = 80
28
+
29
+
30
+ def short_id(old: str) -> str:
31
+ head = old.encode("utf-8")[:KEEP_BYTES].decode("utf-8", "ignore")
32
+ if "-" in head:
33
+ head = head.rsplit("-", 1)[0]
34
+ return f"{head.rstrip('-.')}-{hashlib.sha256(old.encode('utf-8')).hexdigest()[:8]}"
35
+
36
+
37
+ def shorten(path: Path) -> Path:
38
+ old = path.stem
39
+ if len(old.encode("utf-8")) <= MAX_SLUG_BYTES:
40
+ return path
41
+ lines = path.read_text(encoding="utf-8").split("\n")
42
+ if not lines or lines[0].strip() != "---":
43
+ return path
44
+ for i, line in enumerate(lines[1:], start=1):
45
+ if line.strip() == "---":
46
+ return path # no `id:` line: leave it to the drain
47
+ if line.startswith("id:"):
48
+ if line[3:].strip().strip("\"'") != old:
49
+ return path # the id is not the file name: leave it
50
+ new = short_id(old)
51
+ target = path.with_name(f"{new}.md")
52
+ if target.exists():
53
+ return path
54
+ lines[i] = f"id: {new}"
55
+ target.write_text("\n".join(lines), encoding="utf-8")
56
+ path.unlink()
57
+ return target
58
+ return path
59
+
60
+
61
+ def relink(paths: list[str], renames: dict[str, str]) -> None:
62
+ """Point `[[old]]`, `[[old|..]]` and `[[old#..]]` at the new id."""
63
+ if not renames:
64
+ return
65
+ pattern = re.compile(r"\[\[(" + "|".join(map(re.escape, renames)) + r")(?=[\]|#])")
66
+ for raw in paths:
67
+ path = Path(raw)
68
+ try:
69
+ text = path.read_text(encoding="utf-8")
70
+ except (OSError, UnicodeDecodeError):
71
+ continue
72
+ new = pattern.sub(lambda m: "[[" + renames[m.group(1)], text)
73
+ if new != text:
74
+ path.write_text(new, encoding="utf-8")
75
+
76
+
77
+ def main(argv: list[str]) -> int:
78
+ # "\n" only, as the shell's `read` splits: splitlines() also breaks on
79
+ # \r, \x1c, \u2028 ..., so a crafted file name could add a list entry.
80
+ with open(argv[1], encoding="utf-8", newline="") as fh: # no \r translation
81
+ entries = fh.read().split("\n")
82
+ out: list[str] = []
83
+ renames: dict[str, str] = {}
84
+ for raw in entries:
85
+ if raw:
86
+ try:
87
+ new = shorten(Path(raw))
88
+ except (OSError, UnicodeDecodeError):
89
+ new = Path(raw)
90
+ if str(new) != raw:
91
+ renames[Path(raw).stem] = new.stem
92
+ out.append(str(new))
93
+ relink(out, renames)
94
+ for line in out:
95
+ print(line)
96
+ return 0
97
+
98
+
99
+ if __name__ == "__main__":
100
+ raise SystemExit(main(sys.argv))
@@ -9,4 +9,4 @@ pip-installed host, importlib.metadata stays primary; brain/__init__.py reads
9
9
  this file only as the fallback before 0.0.0+unknown.
10
10
  """
11
11
 
12
- __version__ = "0.20.47"
12
+ __version__ = "0.20.49"
@@ -12,7 +12,7 @@ identical list.
12
12
 
13
13
  Two design rules, both learned from earlier failures in this repo:
14
14
 
15
- * **Silence must never be able to mean "could not look."** Two of the five
15
+ * **Silence must never be able to mean "could not look."** Some of the
16
16
  inputs live in the HOST home (``~/.brainiac``, ``~/.brain``) and the Cowork
17
17
  VM cannot reach them. On ``role=vm`` they are reported in ``unreachable``
18
18
  rather than quietly contributing nothing — the same posture ``doctor`` takes
@@ -42,7 +42,7 @@ from .alerts_staging import ( # noqa: F401 — re-exported for callers/tests
42
42
  _untriaged,
43
43
  _untriaged_key,
44
44
  _unwrap_untriaged,
45
- host_vaults,
45
+ host_vaults, retrieval_watch_alerts, RETRIEVAL_WATCH_UNREACHABLE,
46
46
  staging_alerts,
47
47
  )
48
48
 
@@ -79,7 +79,7 @@ SYNTHESIS_STALE_DAYS = 8
79
79
  STUCK_DRAFT_HOURS = 48
80
80
 
81
81
  # The feed's `findings[].key` vocabulary is closed (see maintenance_notify.py:
82
- # "blocked", "synthesis-watchdog", "trend:<metric>", "invariant:<metric>",
82
+ # "blocked", "synthesis-watchdog", "trend:<metric>", "invariant:<metric>", # noqa: ERA001
83
83
  # "branch-escalate:<branch>", "ingest_quarantine*", ...). The dir is on the
84
84
  # VirtioFS mount the Cowork VM can write, so a key failing this shape must
85
85
  # never reach the rendered alert text — it is the one thing standing between
@@ -91,7 +91,7 @@ def _read_json(path: Path) -> Any:
91
91
  try:
92
92
  with open(path, encoding="utf-8") as fh:
93
93
  return json.load(fh)
94
- except Exception:
94
+ except Exception: # fail-open: missing or corrupt feed file reads as no data
95
95
  return None
96
96
 
97
97
 
@@ -443,7 +443,7 @@ def collect(
443
443
  """Every alert this role can see, plus what it could not look at.
444
444
 
445
445
  ``host`` sweeps the workspace registry; ``vm`` reads only its own vault and
446
- reports the two host-home sources as unreachable."""
446
+ reports the host-home sources as unreachable."""
447
447
  home = home or Path.home()
448
448
  today = today or datetime.date.today()
449
449
  alerts: list[dict[str, str]] = []
@@ -454,7 +454,7 @@ def collect(
454
454
  "auto-update state (~/.brainiac/update-state.json) — host-only, "
455
455
  "run `brain alerts` on the host Mac",
456
456
  "weekly synthesis task health (~/.brain/synthesis-state.json) — "
457
- "host-only, run `brain alerts` on the host Mac",
457
+ "host-only, run `brain alerts` on the host Mac", RETRIEVAL_WATCH_UNREACHABLE,
458
458
  ]
459
459
  vaults = [Path(vault)] if vault else []
460
460
  if not vaults:
@@ -467,7 +467,7 @@ def collect(
467
467
  else:
468
468
  alerts += update_alerts(home, today)
469
469
  alerts += staging_alerts(home)
470
- alerts += synthesis_alerts(home, today)
470
+ alerts += synthesis_alerts(home, today) + retrieval_watch_alerts(home, today)
471
471
  alerts += wiring_alerts()
472
472
  vaults = host_vaults(home)
473
473
  if not vaults and vault:
@@ -186,3 +186,39 @@ def host_vaults(home: Path) -> list[Path]:
186
186
  if path not in seen:
187
187
  seen.append(path)
188
188
  return seen
189
+
190
+
191
+ #: The monthly retrieval watch fires on day 1; past this its silence is the news.
192
+ RETRIEVAL_WATCH_STALE_DAYS = 35
193
+
194
+
195
+ def retrieval_watch_alerts(home: Path, today: Any) -> list[dict[str, str]]:
196
+ """The monthly retrieval-quality watch (``~/.brain/retrieval-watch-state.json``,
197
+ written by ``scripts/brain-retrieval-watch.sh``). Until 2026-10-27 a FAIL
198
+ went only to ``hot.md``, a log nobody has to read, and it failed unread on
199
+ three months running against a baseline from a retired embedder."""
200
+ from .alerts import _alert, _date, _read_json
201
+
202
+ state = _read_json(home / ".brain" / "retrieval-watch-state.json")
203
+ if not isinstance(state, dict):
204
+ return [] # never installed on this host — nothing to watch
205
+ day = _date(state.get("at"))
206
+ if state.get("verdict") == "FAIL":
207
+ return [_alert(
208
+ "retrieval-watch:fail",
209
+ f"monthly retrieval check FAILED on {state.get('at')} — run /autoresearch, "
210
+ "or re-baseline if the engine or embedder changed")]
211
+ if state.get("verdict") == "ERROR":
212
+ return [_alert(
213
+ "retrieval-watch:error",
214
+ f"monthly retrieval check could not run on {state.get('at')} — "
215
+ "read ~/.brain/logs/launchd-retrievalwatch-stderr.log")]
216
+ if day is None or (today - day).days > RETRIEVAL_WATCH_STALE_DAYS:
217
+ return [_alert(
218
+ "retrieval-watch:stale",
219
+ f"monthly retrieval check has not run since {state.get('at') or 'never'}")]
220
+ return []
221
+
222
+ RETRIEVAL_WATCH_UNREACHABLE = (
223
+ "monthly retrieval check (~/.brain/retrieval-watch-state.json) — host-only, "
224
+ "run `brain alerts` on the host Mac")
@@ -0,0 +1,49 @@
1
+ """Durable-write temp-name helper and the one write-then-replace helper."""
2
+ from __future__ import annotations
3
+
4
+ import errno
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from .lock import _atomic_temp_path
11
+
12
+ atomic_temp_path = _atomic_temp_path
13
+
14
+
15
+ def _fsync_dir(d: Path) -> None:
16
+ """fsync a directory so a rename in it survives a crash; unsupported is fine."""
17
+ if os.name == "nt":
18
+ return # no directory descriptors on Windows
19
+ fd = os.open(d, os.O_RDONLY)
20
+ try:
21
+ os.fsync(fd)
22
+ except OSError as exc:
23
+ if exc.errno not in (errno.EINVAL, errno.ENOTSUP, errno.EBADF):
24
+ raise
25
+ finally:
26
+ os.close(fd)
27
+
28
+
29
+ def write_text_atomic(path: Path, text: str) -> None:
30
+ """Write ``text`` to a sibling temp file, fsync, then ``os.replace`` it in.
31
+
32
+ The temp file is removed on any error, so the target keeps its old content.
33
+ """
34
+ tmp = atomic_temp_path(path)
35
+ try:
36
+ with open(tmp, "w", encoding="utf-8") as fh:
37
+ fh.write(text)
38
+ fh.flush()
39
+ os.fsync(fh.fileno())
40
+ os.replace(tmp, path)
41
+ except BaseException:
42
+ tmp.unlink(missing_ok=True)
43
+ raise
44
+ _fsync_dir(path.parent) # after the replace: the target must not be unlinked on failure
45
+
46
+
47
+ def write_json_atomic(path: Path, obj: Any) -> None:
48
+ """``write_text_atomic`` of ``json.dumps(obj, indent=2)`` plus a newline."""
49
+ write_text_atomic(path, json.dumps(obj, indent=2) + "\n")
@@ -18,6 +18,7 @@ Resolution precedence (first hit wins, all yield identical PEM bytes):
18
18
  """
19
19
  from __future__ import annotations
20
20
 
21
+ import logging
21
22
  import base64 as base64
22
23
  import contextlib
23
24
  import hashlib
@@ -31,6 +32,8 @@ from datetime import datetime as datetime, timezone as timezone
31
32
  from pathlib import Path
32
33
  from typing import Iterator, Optional
33
34
 
35
+ logger = logging.getLogger(__name__)
36
+
34
37
  JSONL_FORMAT = "brain-audit-chain-jsonl-v1"
35
38
  NULL_PREV_HASH = "0" * 64
36
39
  _REQUIRED_KEYS = frozenset({"format", "ts", "verb", "path", "reason", "prev_hash", "sig"})
@@ -137,7 +140,7 @@ def _pem_from_credential_manager(service: str, account: str) -> Optional[bytes]:
137
140
 
138
141
  secret = keyring.get_password(service, account)
139
142
  return secret.encode("utf-8") if secret else None
140
- except Exception:
143
+ except Exception: # fail-open: keyring is optional; None means no key from this source
141
144
  return None
142
145
 
143
146
 
@@ -334,7 +337,8 @@ def _exclusive_lock(log_path: Path) -> Iterator[None]:
334
337
 
335
338
  msvcrt.locking(fd, msvcrt.LK_LOCK, 1)
336
339
  locked_with = "msvcrt"
337
- except Exception:
340
+ except Exception: # fail-open: best-effort lock; locked_with=None records that none was taken
341
+ logger.warning("audit lock not taken (msvcrt locking failed); writing unlocked", exc_info=True)
338
342
  locked_with = None # best-effort fallback
339
343
  yield
340
344
  finally:
@@ -348,7 +352,7 @@ def _exclusive_lock(log_path: Path) -> Iterator[None]:
348
352
 
349
353
  os.lseek(fd, 0, os.SEEK_SET)
350
354
  msvcrt.locking(fd, msvcrt.LK_UNLCK, 1)
351
- except Exception:
355
+ except Exception: # fail-open: unlock failure is harmless, the fd is closed next
352
356
  pass
353
357
  os.close(fd)
354
358
 
@@ -1,12 +1,11 @@
1
1
  """Host-private audit drift-disposition triage (INT-02)."""
2
2
  from __future__ import annotations
3
+ from .atomic_temp import write_text_atomic
3
4
 
4
5
  import json
5
- import os
6
6
  from datetime import date
7
7
  from pathlib import Path
8
8
  from typing import TYPE_CHECKING
9
- from .atomic_temp import atomic_temp_path
10
9
 
11
10
  if TYPE_CHECKING: # annotation-only; a runtime import here would be a cycle
12
11
  from .audit_chain import AuditChain
@@ -33,9 +32,7 @@ def _write_dispositions(path: Path, payload: dict) -> None:
33
32
  turn every explained finding back into an unexplained one. `os.replace` is
34
33
  atomic within a filesystem, so a reader sees the old file or the new one.
35
34
  """
36
- tmp = atomic_temp_path(path)
37
- tmp.write_text(json.dumps(payload, indent=2), encoding="utf-8")
38
- os.replace(tmp, path)
35
+ write_text_atomic(path, json.dumps(payload, indent=2))
39
36
 
40
37
 
41
38
  def drift_dispositions_path(vault: Path) -> Path:
@@ -45,16 +45,18 @@ def _fold(text: str) -> str:
45
45
  return re.sub(r"[^a-z0-9]+", "-", folded).strip("-")
46
46
 
47
47
 
48
- def _typed_notes(vault: Path, types: tuple[str, ...]) -> list[tuple[str, str]]:
49
- """[(note_id, folded-title-slug)] for every brain/ note of one of ``types``."""
48
+ def _typed_notes(vault: Path, types: tuple[str, ...], notes=None) -> list[tuple[str, str]]:
49
+ """[(note_id, folded-title-slug)] for every brain/ note of one of ``types``.
50
+
51
+ ``notes`` is an already-scanned vault (one scan shared by both finders)."""
50
52
  out: list[tuple[str, str]] = []
51
- for note in scan_vault(vault):
53
+ for note in (scan_vault(vault) if notes is None else notes):
52
54
  if note.zone == "brain" and note.type in types:
53
55
  out.append((note.id, _fold(note.title)))
54
56
  return out
55
57
 
56
58
 
57
- def find_person_links(text: str, vault: Path) -> list[str]:
59
+ def find_person_links(text: str, vault: Path, notes=None) -> list[str]:
58
60
  """Full-name matches of existing ``type: person`` notes in ``text``.
59
61
 
60
62
  A person note's title must fold to at least two hyphen-separated parts
@@ -63,7 +65,7 @@ def find_person_links(text: str, vault: Path) -> list[str]:
63
65
  ``text`` never links on its own."""
64
66
  folded_text = _fold(text)
65
67
  hits: list[str] = []
66
- for note_id, name_slug in _typed_notes(vault, ("person",)):
68
+ for note_id, name_slug in _typed_notes(vault, ("person",), notes):
67
69
  parts = [p for p in name_slug.split("-") if p]
68
70
  if len(parts) < 2:
69
71
  continue
@@ -72,7 +74,7 @@ def find_person_links(text: str, vault: Path) -> list[str]:
72
74
  return hits
73
75
 
74
76
 
75
- def find_project_link(text: str, vault: Path) -> list[str]:
77
+ def find_project_link(text: str, vault: Path, notes=None) -> list[str]:
76
78
  """Origin-context match against ``type: project``/``type: moc`` notes.
77
79
 
78
80
  Returns a match only when exactly one candidate's folded title is a
@@ -80,7 +82,7 @@ def find_project_link(text: str, vault: Path) -> list[str]:
80
82
  nothing."""
81
83
  folded_text = _fold(text)
82
84
  candidates = [
83
- note_id for note_id, title_slug in _typed_notes(vault, ("project", "moc"))
85
+ note_id for note_id, title_slug in _typed_notes(vault, ("project", "moc"), notes)
84
86
  if title_slug and title_slug in folded_text
85
87
  ]
86
88
  return candidates if len(candidates) == 1 else []
@@ -109,8 +111,9 @@ def apply_autolinks(
109
111
  return body, []
110
112
  try:
111
113
  search_text = "\n".join(t for t in (title, body) if t)
112
- candidates = list(find_person_links(search_text, vault))
113
- candidates.extend(find_project_link("\n".join(t for t in (title, origin) if t), vault))
114
+ notes = list(scan_vault(vault)) # one scan per record; no cross-record cache
115
+ candidates = list(find_person_links(search_text, vault, notes))
116
+ candidates.extend(find_project_link("\n".join(t for t in (title, origin) if t), vault, notes))
114
117
  existing = set(parse_wikilinks(body))
115
118
  new_ids = [nid for nid in dict.fromkeys(candidates) if nid not in existing]
116
119
  if not new_ids: