brainiac-cli 0.20.41__tar.gz → 0.20.43__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 (359) hide show
  1. {brainiac_cli-0.20.41/src/brainiac_cli.egg-info → brainiac_cli-0.20.43}/PKG-INFO +2 -2
  2. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/pyproject.toml +5 -2
  3. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/_version.py +1 -1
  4. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/doctor_checks.py +4 -1
  5. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/doctor_context.py +2 -0
  6. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/doctor_desktop.py +203 -11
  7. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/doctor_plugins.py +68 -12
  8. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/folds/intake.py +4 -4
  9. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/ingest/handlers/tables.py +7 -6
  10. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/interview_detect.py +31 -0
  11. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/remediation.py +1 -0
  12. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/update_channels_2.py +28 -20
  13. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/update_plugins.py +4 -3
  14. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43/src/brainiac_cli.egg-info}/PKG-INFO +2 -2
  15. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brainiac_cli.egg-info/requires.txt +1 -1
  16. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/LICENSE +0 -0
  17. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/MANIFEST.in +0 -0
  18. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/README.md +0 -0
  19. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/setup.cfg +0 -0
  20. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/__init__.py +0 -0
  21. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/__main__.py +0 -0
  22. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/_assets/AGENTS.md +0 -0
  23. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/_assets/assets/graph-explorer-template.html +0 -0
  24. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/_assets/overlay/template/brand/brand-guide.md +0 -0
  25. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/_assets/overlay/template/keywords/glossary.md +0 -0
  26. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/_assets/overlay/template/people/roster.md +0 -0
  27. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/_assets/overlay/template/voice/voice-profile.md +0 -0
  28. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/_assets/routines/manifest.json +0 -0
  29. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/_assets/scripts/brain-brief-mac.plist +0 -0
  30. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/_assets/scripts/brain-brief.sh +0 -0
  31. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/_assets/scripts/brain-synthesis-mac.plist +0 -0
  32. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/_assets/scripts/brain-synthesis.sh +0 -0
  33. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/_assets/scripts/brain_synthesis_codex.py +0 -0
  34. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/_assets/scripts/brainiac-alerts.sh +0 -0
  35. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/_assets/scripts/brainiac-egress-guard.sh +0 -0
  36. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/_assets/scripts/install-brief-mac.sh +0 -0
  37. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/_assets/scripts/install-brief-windows.ps1 +0 -0
  38. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/_assets/scripts/register_tasks.py +0 -0
  39. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/_assets/scripts/vm-boundary-probe.sh +0 -0
  40. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/_assets/scripts/vm-selftest.sh +0 -0
  41. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/_assets/templates/company.md +0 -0
  42. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/_assets/templates/concept.md +0 -0
  43. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/_assets/templates/daily.md +0 -0
  44. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/_assets/templates/decision.md +0 -0
  45. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/_assets/templates/deliverable.md +0 -0
  46. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/_assets/templates/meeting.md +0 -0
  47. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/_assets/templates/person.md +0 -0
  48. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/_assets/templates/project.md +0 -0
  49. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/_assets/templates/state-moc.md +0 -0
  50. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/_optional.py +0 -0
  51. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/alerts.py +0 -0
  52. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/alerts_render.py +0 -0
  53. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/alerts_staging.py +0 -0
  54. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/anchor.py +0 -0
  55. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/atomic_temp.py +0 -0
  56. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/attestation.py +0 -0
  57. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/audit.py +0 -0
  58. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/audit_chain.py +0 -0
  59. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/audit_drift.py +0 -0
  60. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/autolink.py +0 -0
  61. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/backup.py +0 -0
  62. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/brief.py +0 -0
  63. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/brief_render.py +0 -0
  64. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/capture.py +0 -0
  65. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/chunk.py +0 -0
  66. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/classification.py +0 -0
  67. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cli.py +0 -0
  68. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cli_cmds/__init__.py +0 -0
  69. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cli_cmds/common_parser.py +0 -0
  70. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cli_cmds/connect.py +0 -0
  71. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cli_cmds/connect_parser.py +0 -0
  72. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cli_cmds/eval.py +0 -0
  73. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cli_cmds/eval_parser.py +0 -0
  74. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cli_cmds/index_storage.py +0 -0
  75. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cli_cmds/index_storage_parser.py +0 -0
  76. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cli_cmds/ingest_storage.py +0 -0
  77. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cli_cmds/ingest_storage_parser.py +0 -0
  78. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cli_cmds/lifecycle.py +0 -0
  79. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cli_cmds/lifecycle_parser.py +0 -0
  80. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cli_cmds/maintenance.py +0 -0
  81. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cli_cmds/maintenance_parser.py +0 -0
  82. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cli_cmds/navigation.py +0 -0
  83. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cli_cmds/navigation_parser.py +0 -0
  84. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cli_cmds/owner_workflow.py +0 -0
  85. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cli_cmds/owner_workflow_parser.py +0 -0
  86. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cli_cmds/presentation.py +0 -0
  87. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cli_cmds/presentation_parser.py +0 -0
  88. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cli_cmds/retrieval.py +0 -0
  89. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cli_cmds/retrieval_parser.py +0 -0
  90. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cli_cmds/update.py +0 -0
  91. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cli_cmds/update_parser.py +0 -0
  92. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cli_cmds/versioning.py +0 -0
  93. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cli_cmds/versioning_parser.py +0 -0
  94. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cli_help.py +0 -0
  95. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cli_read_record.py +0 -0
  96. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cli_render.py +0 -0
  97. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/config.py +0 -0
  98. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/config_hostpaths.py +0 -0
  99. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/connect.py +0 -0
  100. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/context.py +0 -0
  101. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/conventions.py +0 -0
  102. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/conventions_scan.py +0 -0
  103. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/core/__init__.py +0 -0
  104. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/core/_audit.py +0 -0
  105. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/core/_briefing.py +0 -0
  106. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/core/_capture.py +0 -0
  107. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/core/_capture_staging.py +0 -0
  108. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/core/_durability.py +0 -0
  109. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/core/_health.py +0 -0
  110. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/core/_maintenance.py +0 -0
  111. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/core/_maintenance_state.py +0 -0
  112. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/core/_retrieval.py +0 -0
  113. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/core/_shared.py +0 -0
  114. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/core/_supersede_batch.py +0 -0
  115. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/core/_supersession.py +0 -0
  116. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/core/_supersession_journal.py +0 -0
  117. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cowork_leak_artifact.py +0 -0
  118. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cowork_leak_databases.py +0 -0
  119. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cowork_leak_notes.py +0 -0
  120. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cowork_leak_scan.py +0 -0
  121. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cowork_snapshot_guard.py +0 -0
  122. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/cowork_staging.py +0 -0
  123. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/dbretry.py +0 -0
  124. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/deliverables_absorb.py +0 -0
  125. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/deliverables_apply.py +0 -0
  126. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/deliverables_ledger.py +0 -0
  127. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/deliverables_materialize.py +0 -0
  128. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/deliverables_previous.py +0 -0
  129. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/deliverables_readme.py +0 -0
  130. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/deliverables_shelf.py +0 -0
  131. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/deliverables_sync.py +0 -0
  132. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/doctor.py +0 -0
  133. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/doctor_health.py +0 -0
  134. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/doctor_mount_leak.py +0 -0
  135. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/doctor_staging.py +0 -0
  136. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/doctor_vendor.py +0 -0
  137. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/doctor_vm.py +0 -0
  138. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/doctor_wiring.py +0 -0
  139. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/draft_drain.py +0 -0
  140. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/draft_update.py +0 -0
  141. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/egress.py +0 -0
  142. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/egress_terms.py +0 -0
  143. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/embed.py +0 -0
  144. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/embed_backends.py +0 -0
  145. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/embed_catalog.py +0 -0
  146. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/embed_onnx.py +0 -0
  147. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/embed_qwen.py +0 -0
  148. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/encryption.py +0 -0
  149. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/exceptions_cli.py +0 -0
  150. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/exceptions_page.py +0 -0
  151. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/exceptions_render.py +0 -0
  152. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/exceptions_tray.py +0 -0
  153. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/exceptions_verify.py +0 -0
  154. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/folds/__init__.py +0 -0
  155. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/folds/context.py +0 -0
  156. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/folds/daily.py +0 -0
  157. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/folds/deliverables.py +0 -0
  158. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/folds/golden.py +0 -0
  159. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/folds/graph.py +0 -0
  160. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/folds/invariants.py +0 -0
  161. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/folds/orchestrator.py +0 -0
  162. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/folds/organization.py +0 -0
  163. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/folds/preflight.py +0 -0
  164. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/folds/publish.py +0 -0
  165. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/folds/remediation.py +0 -0
  166. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/folds/remediation_bookkeeping.py +0 -0
  167. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/folds/reporting.py +0 -0
  168. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/folds/retention.py +0 -0
  169. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/folds/watchdogs.py +0 -0
  170. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/folds/weekly.py +0 -0
  171. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/frontmatter.py +0 -0
  172. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/golden_ops.py +0 -0
  173. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/golden_probe.py +0 -0
  174. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/golden_probe_cases.py +0 -0
  175. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/graph.py +0 -0
  176. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/graph_hygiene.py +0 -0
  177. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/graph_ops.py +0 -0
  178. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/graphify.py +0 -0
  179. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/graphify_edges.py +0 -0
  180. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/graphreport.py +0 -0
  181. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/graphreport_sections.py +0 -0
  182. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/graphreport_sections_2.py +0 -0
  183. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/guard_settings.py +0 -0
  184. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/healthreport.py +0 -0
  185. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/healthreport_sections.py +0 -0
  186. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/healthreport_unsigned.py +0 -0
  187. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/inbox.py +0 -0
  188. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/index/__init__.py +0 -0
  189. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/index/_connection.py +0 -0
  190. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/index/_families.py +0 -0
  191. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/index/_graph.py +0 -0
  192. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/index/_identity.py +0 -0
  193. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/index/_lifecycle.py +0 -0
  194. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/index/_models.py +0 -0
  195. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/index/_planning.py +0 -0
  196. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/index/_records.py +0 -0
  197. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/index/_retirement.py +0 -0
  198. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/index/_schema.py +0 -0
  199. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/index/_search.py +0 -0
  200. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/index/_settings.py +0 -0
  201. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/index/_shared.py +0 -0
  202. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/index/_tools.py +0 -0
  203. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/index/_zones.py +0 -0
  204. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/index_stages/__init__.py +0 -0
  205. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/index_stages/connection.py +0 -0
  206. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/index_stages/diagnostics.py +0 -0
  207. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/index_stages/duplicate_families.py +0 -0
  208. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/index_stages/near_duplicates.py +0 -0
  209. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/index_stages/rebuild.py +0 -0
  210. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/index_stages/reranking.py +0 -0
  211. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/index_stages/search.py +0 -0
  212. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/index_stages/search_finalization.py +0 -0
  213. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/index_stages/search_identity.py +0 -0
  214. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/index_stages/search_ranking.py +0 -0
  215. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/index_stages/sync.py +0 -0
  216. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/ingest/__init__.py +0 -0
  217. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/ingest/deliverables.py +0 -0
  218. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/ingest/handlers/__init__.py +0 -0
  219. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/ingest/handlers/base.py +0 -0
  220. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/ingest/handlers/concealment.py +0 -0
  221. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/ingest/handlers/concealment_gate.py +0 -0
  222. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/ingest/handlers/concealment_html.py +0 -0
  223. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/ingest/handlers/concealment_ledger.py +0 -0
  224. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/ingest/handlers/concealment_office.py +0 -0
  225. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/ingest/handlers/concealment_pdf.py +0 -0
  226. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/ingest/handlers/docx.py +0 -0
  227. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/ingest/handlers/email.py +0 -0
  228. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/ingest/handlers/html.py +0 -0
  229. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/ingest/handlers/image.py +0 -0
  230. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/ingest/handlers/ole.py +0 -0
  231. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/ingest/handlers/pdf.py +0 -0
  232. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/ingest/handlers/pptx.py +0 -0
  233. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/ingest/handlers/text.py +0 -0
  234. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/ingest/handlers/xlsx.py +0 -0
  235. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/ingest/handlers/zip.py +0 -0
  236. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/ingest/pipeline.py +0 -0
  237. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/ingest/pipeline_duplicates.py +0 -0
  238. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/ingest/pipeline_files.py +0 -0
  239. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/ingest/pipeline_injection.py +0 -0
  240. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/ingest/pipeline_nested.py +0 -0
  241. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/ingest/pipeline_run.py +0 -0
  242. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/ingest/pipeline_stages.py +0 -0
  243. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/ingest/tierguard.py +0 -0
  244. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/ingest/tierguard_verdict.py +0 -0
  245. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/ingest/transcript.py +0 -0
  246. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/init.py +0 -0
  247. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/init_import.py +0 -0
  248. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/init_samples.py +0 -0
  249. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/init_seed.py +0 -0
  250. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/init_steps.py +0 -0
  251. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/injection_fold.py +0 -0
  252. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/injection_scan.py +0 -0
  253. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/interview.py +0 -0
  254. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/interview_apply.py +0 -0
  255. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/invariant_coverage.py +0 -0
  256. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/invariant_crosstier.py +0 -0
  257. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/invariant_deliverables.py +0 -0
  258. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/invariant_floors.py +0 -0
  259. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/invariant_ingest_guard.py +0 -0
  260. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/invariant_lane_health.py +0 -0
  261. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/invariant_reachability.py +0 -0
  262. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/invariant_shared.py +0 -0
  263. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/invariant_unsigned_notes.py +0 -0
  264. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/invariants.py +0 -0
  265. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/invariants_metrics.py +0 -0
  266. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/language.py +0 -0
  267. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/link_targets.py +0 -0
  268. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/lock.py +0 -0
  269. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/maintenance.py +0 -0
  270. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/maintenance_escalation.py +0 -0
  271. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/maintenance_folds.py +0 -0
  272. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/maintenance_folds_2.py +0 -0
  273. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/maintenance_folds_3.py +0 -0
  274. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/maintenance_folds_4.py +0 -0
  275. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/maintenance_golden.py +0 -0
  276. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/maintenance_graphfolds.py +0 -0
  277. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/maintenance_healthlog.py +0 -0
  278. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/maintenance_hotmd.py +0 -0
  279. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/maintenance_invariant_fields.py +0 -0
  280. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/maintenance_notify.py +0 -0
  281. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/maintenance_outcomes.py +0 -0
  282. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/maintenance_para.py +0 -0
  283. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/maintenance_recommendations.py +0 -0
  284. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/maintenance_retention.py +0 -0
  285. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/maintenance_sweep.py +0 -0
  286. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/maintenance_watchdog.py +0 -0
  287. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/mcp_adapter.py +0 -0
  288. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/mcp_capture_verbs.py +0 -0
  289. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/mcp_housekeeping_verbs.py +0 -0
  290. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/mcp_mediation.py +0 -0
  291. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/mcp_supersede_verbs.py +0 -0
  292. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/mcp_tools/__init__.py +0 -0
  293. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/mcp_tools/capture.py +0 -0
  294. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/mcp_tools/core_read.py +0 -0
  295. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/mcp_tools/housekeeping.py +0 -0
  296. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/mcp_tools/retrieval.py +0 -0
  297. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/mcp_tools/supersession.py +0 -0
  298. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/mcp_verbs.py +0 -0
  299. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/multihop.py +0 -0
  300. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/notes.py +0 -0
  301. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/originals_resolve.py +0 -0
  302. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/overlay.py +0 -0
  303. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/overlay_core.py +0 -0
  304. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/overlay_generated.py +0 -0
  305. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/overlay_keywords.py +0 -0
  306. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/overlay_rules.py +0 -0
  307. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/pathkey.py +0 -0
  308. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/progress.py +0 -0
  309. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/projection.py +0 -0
  310. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/provenance.py +0 -0
  311. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/provision.py +0 -0
  312. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/provision_mcp.py +0 -0
  313. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/provision_stage.py +0 -0
  314. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/provision_wire.py +0 -0
  315. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/querylog.py +0 -0
  316. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/querylog_digest.py +0 -0
  317. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/querylog_replay.py +0 -0
  318. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/querylog_status.py +0 -0
  319. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/read_log.py +0 -0
  320. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/reexec.py +0 -0
  321. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/remediation_answers.py +0 -0
  322. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/remediation_branches.py +0 -0
  323. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/remediation_converged.py +0 -0
  324. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/remediation_exceptions.py +0 -0
  325. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/remediation_exceptions_store.py +0 -0
  326. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/remediation_extract_retry.py +0 -0
  327. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/remediation_folds.py +0 -0
  328. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/remediation_questions.py +0 -0
  329. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/remediation_routing.py +0 -0
  330. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/remediation_schema.py +0 -0
  331. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/remediation_state.py +0 -0
  332. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/rerank.py +0 -0
  333. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/rerank_onnx.py +0 -0
  334. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/retrieval_ops.py +0 -0
  335. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/retro.py +0 -0
  336. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/session_hook.py +0 -0
  337. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/session_hook_settings.py +0 -0
  338. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/session_hook_spec.py +0 -0
  339. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/snapshot.py +0 -0
  340. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/status_ops.py +0 -0
  341. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/supersede_declared.py +0 -0
  342. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/sweepdirs.py +0 -0
  343. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/update.py +0 -0
  344. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/update_channels.py +0 -0
  345. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/update_cos_guard.py +0 -0
  346. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/update_model.py +0 -0
  347. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/update_ops.py +0 -0
  348. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/update_state.py +0 -0
  349. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/update_venv.py +0 -0
  350. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/vectors.py +0 -0
  351. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/versionlink.py +0 -0
  352. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/versionlink_stages.py +0 -0
  353. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/vm_ceiling.py +0 -0
  354. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/vmstaging.py +0 -0
  355. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brain/workspaces.py +0 -0
  356. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brainiac_cli.egg-info/SOURCES.txt +0 -0
  357. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brainiac_cli.egg-info/dependency_links.txt +0 -0
  358. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/src/brainiac_cli.egg-info/entry_points.txt +0 -0
  359. {brainiac_cli-0.20.41 → brainiac_cli-0.20.43}/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.41
3
+ Version: 0.20.43
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
@@ -60,7 +60,7 @@ Requires-Dist: numpy>=1.24; extra == "eval"
60
60
  Provides-Extra: quant-tools
61
61
  Requires-Dist: onnx>=1.14; extra == "quant-tools"
62
62
  Provides-Extra: mcp
63
- Requires-Dist: mcp<2,>=1.28.1; extra == "mcp"
63
+ Requires-Dist: mcp<2,>=1.30.0; extra == "mcp"
64
64
  Provides-Extra: index
65
65
  Requires-Dist: regex>=2023.0.0; extra == "index"
66
66
  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.41"
11
+ version = "0.20.43"
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"
@@ -122,7 +122,10 @@ quant-tools = ["onnx>=1.14"]
122
122
  # release pipeline's release-asset handshake gate, after it had already shipped).
123
123
  # Upgrade path: port the adapter off FastMCP (mcp.server.lowlevel, or the
124
124
  # separate `fastmcp` package) and lift the cap — a real migration, not a bump.
125
- mcp = ["mcp>=1.28.1,<2"]
125
+ # >=1.30.0: CVE-2026-59951 (Streamable HTTP stateful sessions never reclaimed,
126
+ # unbounded memory) is fixed there. The FLOOR, not just the lock, for the same
127
+ # reason as pypdf above: a plain `pip install` reads this, not requirements.lock.
128
+ mcp = ["mcp>=1.30.0,<2"]
126
129
  # Timeout-bounded regex engine (mrab-regex) used by BrainIndex.grep's --regex
127
130
  # path (RET-04 hardening) as a ReDoS guard: regex.search(text, timeout=...)
128
131
  # genuinely bounds catastrophic backtracking, unlike stdlib `re`. Absent ->
@@ -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.41"
12
+ __version__ = "0.20.43"
@@ -61,6 +61,7 @@ class DoctorChecks:
61
61
  mcp_vault_paths: DoctorCheck
62
62
  cos_deployed_skill: DoctorCheck
63
63
  desktop_plugin_store: DoctorCheck
64
+ mcp_server_code: DoctorCheck
64
65
  cowork_vault_dir: DoctorCheck
65
66
  quarantine: DoctorCheck
66
67
  ingest_capability: DoctorCheck
@@ -208,7 +209,9 @@ def _terminal_rows(
208
209
  rows.extend([checks.mcpb_desktop_collision(context.app_support_dir),
209
210
  *checks.mcp_vault_paths()])
210
211
  rows.extend(row for row in [checks.cos_deployed_skill()] if row)
211
- rows.extend(checks.desktop_plugin_store(context.app_support_dir, ssot))
212
+ rows.extend(checks.desktop_plugin_store(context.app_support_dir, ssot,
213
+ registry_entries=context.registry_entries))
214
+ rows.extend(checks.mcp_server_code())
212
215
  from .guard_settings import guard_settings_row as _guard_settings_row
213
216
  from .guard_settings import session_state_row as _session_state_row
214
217
  from .guard_settings import registry_pin_row as _registry_pin_row
@@ -131,6 +131,7 @@ def build_context_and_checks(
131
131
  """
132
132
  from . import config as _config
133
133
  from . import doctor as _doctor
134
+ from . import doctor_desktop as _desktop
134
135
  from .doctor_checks import DoctorCheckContext, DoctorChecks
135
136
 
136
137
  context = DoctorCheckContext(
@@ -175,6 +176,7 @@ def build_context_and_checks(
175
176
  mcp_vault_paths=_doctor.check_mcp_vault_paths,
176
177
  cos_deployed_skill=_doctor.check_cos_deployed_skill,
177
178
  desktop_plugin_store=_doctor.check_desktop_plugin_store,
179
+ mcp_server_code=_desktop.check_mcp_server_code,
178
180
  cowork_vault_dir=_doctor._cowork_vault_dir,
179
181
  quarantine=_doctor.check_quarantine,
180
182
  ingest_capability=_doctor.check_ingest_capability,
@@ -11,6 +11,7 @@ keeps working unchanged. Covered by ``tests/test_doctor_workspace.py``.
11
11
  """
12
12
  from __future__ import annotations
13
13
 
14
+ import re
14
15
  from pathlib import Path
15
16
  from typing import Optional
16
17
 
@@ -124,8 +125,85 @@ def check_mcpb_desktop_collision(
124
125
  # manual-required, NEVER gates the exit code — ADR-0005 Ruling 2/4).
125
126
  # --------------------------------------------------------------------------
126
127
 
128
+ _STAMP = re.compile(rb"<!-- SKILL_VERSION:[^\n]*-->\n?")
129
+
130
+ # brainiac-manager's skills install/update the HOST and refuse to run in
131
+ # Cowork (brainiac-update is host-only by design), so its absence from the
132
+ # Cowork store is the expected state, not a step the owner owes.
133
+ _OPTIONAL_IN_COWORK = {"brainiac-manager"}
134
+
135
+
136
+ def _skill_reference(registry_entries: Optional[list[dict]], ssot: str) -> dict[str, bytes]:
137
+ """name -> current SKILL.md bytes (version stamp stripped), read from the
138
+ workspaces' staged ``.skill`` bundles at the SSOT version.
139
+
140
+ Cowork's marketplace is a cloud copy that can lag weeks behind GitHub and
141
+ re-label identical skill text with an old version (measured 2026-10-05:
142
+ 0.20.39 installed, text identical to 0.20.41). Comparing TEXT against the
143
+ bundles `brain update` just staged is what tells the owner whether to act.
144
+ """
145
+ import zipfile
146
+
147
+ from .vmstaging import _cowork_staging_root
148
+
149
+ ref: dict[str, bytes] = {}
150
+ for entry in registry_entries or []:
151
+ if entry.get("target") == "host":
152
+ continue
153
+ skills_dir = _cowork_staging_root(entry) / "skills"
154
+ for bundle in sorted(skills_dir.glob("*.skill")) if skills_dir.is_dir() else []:
155
+ if bundle.stem in ref:
156
+ continue
157
+ try:
158
+ with zipfile.ZipFile(bundle) as zf:
159
+ if zf.read(f"{bundle.stem}/VERSION").decode("utf-8").strip() != ssot:
160
+ continue
161
+ ref[bundle.stem] = _STAMP.sub(b"", zf.read(f"{bundle.stem}/SKILL.md"))
162
+ except (OSError, KeyError, UnicodeDecodeError, zipfile.BadZipFile):
163
+ continue
164
+ return ref
165
+
166
+
167
+ def _content_drift(plugin_dir: Path, reference: dict[str, bytes]) -> Optional[tuple[int, list[str]]]:
168
+ """(skills compared, names whose text differs), or None when nothing in
169
+ the installed plugin has a reference to compare against."""
170
+ skills_root = plugin_dir / "skills"
171
+ if not skills_root.is_dir():
172
+ return None
173
+ names = sorted(p.name for p in skills_root.iterdir()
174
+ if p.name in reference and (p / "SKILL.md").is_file())
175
+ if not names:
176
+ return None
177
+ differs = [n for n in names
178
+ if _STAMP.sub(b"", (skills_root / n / "SKILL.md").read_bytes()) != reference[n]]
179
+ return len(names), differs
180
+
181
+
182
+ def _store_refresh_steps(pname: str, registry_entries: Optional[list[dict]]) -> str:
183
+ from .vmstaging import _cowork_staging_root
184
+
185
+ staged = next((str(_cowork_staging_root(e) / "skills") for e in registry_entries or []
186
+ if e.get("target") != "host"), "<workspace>/vault/.brain/skills")
187
+ return (f"in Cowork: Customize -> Plugins -> refresh the brainiac marketplace, then "
188
+ f"update {pname}; if it still offers older text, uninstall it and upload "
189
+ f"the .skill bundles from {staged}; re-run brain doctor on the host to confirm")
190
+
191
+
192
+ def _newest_store_copy(sessions_root: Path, pname: str) -> list[tuple[float, Path]]:
193
+ candidates: list[tuple[float, Path]] = []
194
+ try:
195
+ for pjson in sessions_root.glob("*/*/rpm/plugin_*/.claude-plugin/plugin.json"):
196
+ data = _read_json(pjson)
197
+ if data and data.get("name") == pname:
198
+ candidates.append((pjson.stat().st_mtime, pjson))
199
+ except Exception:
200
+ return []
201
+ return sorted(candidates, key=lambda t: t[0], reverse=True)
202
+
203
+
127
204
  def check_desktop_plugin_store(
128
205
  app_support_dir: Path, ssot: str, plugin_dir_names: tuple[str, ...] = PLUGIN_NAMES,
206
+ registry_entries: Optional[list[dict]] = None,
129
207
  ) -> list[dict]:
130
208
  """Best-effort read of
131
209
  ``.../local-agent-mode-sessions/<uuid>/<uuid>/rpm/plugin_*/.claude-plugin/plugin.json``.
@@ -137,8 +215,14 @@ def check_desktop_plugin_store(
137
215
  (not scriptable from here); if multiple candidates tie or none can be
138
216
  confidently chosen it reports 'unknown (N candidate sessions)' rather than
139
217
  inventing a version.
218
+
219
+ When the registered workspaces carry staged bundles at the SSOT version,
220
+ the verdict is the installed skill TEXT against them, not the label: a
221
+ match reads `current` (never gating either way, ADR-0005 Ruling 2/4).
222
+ Every row that needs the owner sets ``raw["action_needed"]``.
140
223
  """
141
224
  sessions_root = app_support_dir / "local-agent-mode-sessions"
225
+ reference = _skill_reference(registry_entries, ssot)
142
226
  rows = []
143
227
  for pname in plugin_dir_names:
144
228
  surface = f"Desktop/Cowork plugin store ({pname})"
@@ -148,27 +232,30 @@ def check_desktop_plugin_store(
148
232
  "verify manually in the Cowork/Desktop client",
149
233
  remediation="Open Cowork/Desktop -> Plugins -> check for update"))
150
234
  continue
151
- candidates: list[tuple[float, Path]] = []
152
- try:
153
- for pjson in sessions_root.glob("*/*/rpm/plugin_*/.claude-plugin/plugin.json"):
154
- data = _read_json(pjson)
155
- if data and data.get("name") == pname:
156
- candidates.append((pjson.stat().st_mtime, pjson))
157
- except Exception:
158
- candidates = []
235
+ candidates = _newest_store_copy(sessions_root, pname)
236
+ if not candidates and pname in _OPTIONAL_IN_COWORK:
237
+ rows.append(_row(surface, UNMANAGED,
238
+ "not installed in Cowork — optional: its skills manage the "
239
+ "host install and refuse to run in Cowork; nothing to do",
240
+ raw={"action_needed": False}))
241
+ continue
159
242
  if not candidates:
160
243
  rows.append(_row(surface, MANUAL_REQUIRED,
161
244
  "not found in any session dir — best-effort, "
162
245
  "verify manually in the Cowork/Desktop client",
163
- remediation="Open Cowork/Desktop -> Plugins -> check for update"))
246
+ remediation=_store_refresh_steps(pname, registry_entries),
247
+ raw={"action_needed": True}))
164
248
  continue
165
- candidates.sort(key=lambda t: t[0], reverse=True)
166
249
  newest_mtime, newest_path = candidates[0]
167
250
  data = _read_json(newest_path) or {}
168
251
  version = data.get("version")
169
252
  if version is None:
170
253
  rows.append(_row(surface, UNKNOWN, f"unknown ({len(candidates)} candidate sessions, unparseable)"))
171
254
  continue
255
+ drift = _content_drift(newest_path.parent.parent, reference)
256
+ if drift is not None:
257
+ rows.append(_content_row(surface, pname, str(version), ssot, drift, registry_entries))
258
+ continue
172
259
  import datetime
173
260
 
174
261
  mtime_str = datetime.datetime.fromtimestamp(newest_mtime).isoformat(timespec="seconds")
@@ -204,7 +291,112 @@ def check_desktop_plugin_store(
204
291
  remediation = "looks current — no action needed"
205
292
  rows.append(_row(surface, MANUAL_REQUIRED, detail, remediation=remediation,
206
293
  raw={"version": version, "candidates": len(candidates),
207
- "newest_mtime": mtime_str}))
294
+ "newest_mtime": mtime_str, "action_needed": skew != 0}))
295
+ return rows
296
+
297
+
298
+ def _content_row(surface: str, pname: str, version: str, ssot: str,
299
+ drift: tuple[int, list[str]], registry_entries: Optional[list[dict]]) -> dict:
300
+ checked, differs = drift
301
+ raw = {"version": version, "checked": checked, "differs": differs,
302
+ "action_needed": bool(differs)}
303
+ if not differs:
304
+ return _row(surface, CURRENT,
305
+ f"{checked} skill(s) match the {ssot} text (Cowork labels it {version}; "
306
+ "the label can lag, the text is what runs)", raw=raw)
307
+ return _row(surface, MANUAL_REQUIRED,
308
+ f"{len(differs)} of {checked} skill(s) differ from the {ssot} text "
309
+ f"(Cowork labels it {version}): {', '.join(differs)}",
310
+ remediation=_store_refresh_steps(pname, registry_entries), raw=raw)
311
+
312
+
313
+ # --------------------------------------------------------------------------
314
+ # Claude Desktop's running brain-mcp servers. A Python process keeps the code
315
+ # it imported at start, so after `brain update` reinstalls the engine every
316
+ # server Desktop already started still serves the OLD engine until Desktop is
317
+ # quit and reopened. Measured 2026-10-04: 8 servers started 22:30, engine
318
+ # reinstalled 23:15, nothing reported it.
319
+ # --------------------------------------------------------------------------
320
+
321
+ def _ps_rows() -> Optional[list[tuple[int, float, str]]]:
322
+ """(pid, start epoch, command) for every process, or None where `ps` with
323
+ `lstart` is unavailable (Windows)."""
324
+ import datetime
325
+ import os
326
+ import subprocess
327
+ import sys
328
+
329
+ if sys.platform == "win32":
330
+ return None
331
+ try:
332
+ out = subprocess.run(["ps", "-axo", "pid=,lstart=,command="], capture_output=True,
333
+ text=True, timeout=15, env={**os.environ, "LC_ALL": "C"}).stdout
334
+ except (OSError, subprocess.SubprocessError):
335
+ return None
336
+ rows = []
337
+ for line in out.splitlines():
338
+ parts = line.split(None, 6)
339
+ if len(parts) < 7:
340
+ continue
341
+ try:
342
+ start = datetime.datetime.strptime(" ".join(parts[1:6]), "%a %b %d %H:%M:%S %Y")
343
+ rows.append((int(parts[0]), start.timestamp(), parts[6]))
344
+ except ValueError:
345
+ continue
346
+ return rows
347
+
348
+
349
+ def _engine_install(command: str) -> Optional[tuple[float, str]]:
350
+ """(install time, version) of the engine a `<venv>/bin/brain-mcp` runs."""
351
+ for stamp in sorted(Path(command).parent.parent.glob("lib/python*/site-packages/brain/_version.py")):
352
+ match = re.search(r'__version__\s*=\s*"([^"]+)"', stamp.read_text(encoding="utf-8"))
353
+ return stamp.stat().st_mtime, (match.group(1) if match else "?")
354
+ return None
355
+
356
+
357
+ def check_mcp_server_code(config_path: Optional[Path] = None,
358
+ ps_rows: Optional[list[tuple[int, float, str]]] = None) -> list[dict]:
359
+ """One row per brain-mcp command Desktop launches: are its running
360
+ servers newer than the engine they would load today? Never gating — a
361
+ Desktop restart must not hold `brain update` or CI hostage."""
362
+ import datetime
363
+
364
+ from . import connect as _connect
365
+
366
+ data = _read_json(config_path or _connect.claude_desktop_config_path()) or {}
367
+ commands = sorted({str((e or {}).get("command")) for e in (data.get("mcpServers") or {}).values()
368
+ if "brain-mcp" in str((e or {}).get("command", ""))})
369
+ procs = _ps_rows() if ps_rows is None else ps_rows
370
+ rows = []
371
+ for command in commands:
372
+ surface = f"Claude Desktop Brainiac servers ({command})"
373
+ installed = _engine_install(command)
374
+ if procs is None or installed is None:
375
+ rows.append(_row(surface, NOT_DETECTABLE, "cannot read the running servers or the "
376
+ "engine they load on this platform", raw={"action_needed": False}))
377
+ continue
378
+ at, version = installed
379
+ when = datetime.datetime.fromtimestamp(at).strftime("%Y-%m-%d %H:%M")
380
+ # Desktop runs each server under its `disclaimer --pgroup --` helper,
381
+ # which carries the same command line: count the server, not the
382
+ # wrapper too (it read "16 processes" for 8 servers, 2026-10-11).
383
+ starts = [start for _pid, start, cmd in procs
384
+ if command in cmd and "/Helpers/disclaimer " not in cmd]
385
+ old = [s for s in starts if s < at]
386
+ if not starts:
387
+ rows.append(_row(surface, NOT_DETECTABLE,
388
+ f"none running — Claude Desktop will load {version} when it starts them",
389
+ raw={"action_needed": False}))
390
+ elif old:
391
+ rows.append(_row(surface, MANUAL_REQUIRED,
392
+ f"{len(old)} of {len(starts)} running server(s) started before the "
393
+ f"engine was installed ({version}, {when}) — they still serve the old code",
394
+ remediation="quit Claude Desktop completely (Cmd+Q) and reopen it",
395
+ raw={"action_needed": True, "version": version}))
396
+ else:
397
+ rows.append(_row(surface, CURRENT,
398
+ f"{len(starts)} running server(s), all started after the engine "
399
+ f"install ({version}, {when})", raw={"action_needed": False}))
208
400
  return rows
209
401
 
210
402
 
@@ -237,6 +237,44 @@ def check_plugin_manifests(repo_root: Path, ssot: str) -> list[dict]:
237
237
  # marketplace + installed_plugins.json are both local files, no network).
238
238
  # --------------------------------------------------------------------------
239
239
 
240
+ def _plugin_copies(entries: list, folder_version: Optional[str] = None,
241
+ ) -> tuple[list[dict], Optional[str], int]:
242
+ """Each installed copy of one plugin as (scope, project_path, version),
243
+ plus the last unreadable installPath and the count of orphaned copies.
244
+
245
+ An orphaned copy is a project-scope entry whose project no longer exists
246
+ (a deleted worktree): Claude Code never loads it, and `brain update`
247
+ crashed running `claude plugin update` from the missing directory
248
+ (v0.20.42 deploy, 2026-10-11). It is not a copy to judge or update.
249
+
250
+ ``folder_version`` is set when the marketplace is a local folder. Claude
251
+ Code then reads a project copy straight from that folder ("is read from
252
+ its folder: nothing to update") and never rewrites its registry record, so
253
+ the recorded cache path stays at an old version forever: judge the copy
254
+ by the folder it actually loads (false stale rows after 0.20.42)."""
255
+ copies, unreadable, orphaned = [], None, 0
256
+ for entry in entries:
257
+ if not isinstance(entry, dict):
258
+ unreadable = None
259
+ continue
260
+ project = entry.get("projectPath")
261
+ if entry.get("scope") in ("project", "local") and project and not Path(project).is_dir():
262
+ orphaned += 1
263
+ continue
264
+ if folder_version and entry.get("scope") in ("project", "local"):
265
+ copies.append({"scope": entry["scope"], "project_path": project,
266
+ "version": folder_version})
267
+ continue
268
+ install_path = entry.get("installPath")
269
+ pjson = _read_json(Path(install_path) / ".claude-plugin" / "plugin.json") if install_path else None
270
+ if not (pjson and pjson.get("version")):
271
+ unreadable = install_path
272
+ continue
273
+ copies.append({"scope": entry.get("scope") or "user",
274
+ "project_path": project, "version": pjson["version"]})
275
+ return copies, unreadable, orphaned
276
+
277
+
240
278
  def check_installed_cli_plugins(
241
279
  claude_home: Path, ssot: str, marketplace_name: str = "brainiac",
242
280
  marketplace_dir: Optional[Path] = None,
@@ -246,6 +284,8 @@ def check_installed_cli_plugins(
246
284
  # installLocation, NOT the hardcoded marketplaces/<name> dir — the caller
247
285
  # passes the resolved dir; keep the hardcoded guess only as the fallback.
248
286
  marketplace_dir = marketplace_dir or (claude_home / "plugins" / "marketplaces" / marketplace_name)
287
+ known = (_read_json(claude_home / "plugins" / "known_marketplaces.json") or {}).get(marketplace_name)
288
+ from_folder = ((known or {}).get("source") or {}).get("source") == "directory"
249
289
  for pname in PLUGIN_NAMES:
250
290
  surface = f"Installed CLI plugin ({pname})"
251
291
  mkt_json = marketplace_dir / "plugins" / pname / ".claude-plugin" / "plugin.json"
@@ -258,11 +298,28 @@ def check_installed_cli_plugins(
258
298
  mkt_version = mkt_data.get("version")
259
299
  installed_json = claude_home / "plugins" / "installed_plugins.json"
260
300
  installed_data = _read_json(installed_json) or {}
261
- plugin_entries = (installed_data.get("plugins") or {}).get(f"{pname}@{marketplace_name}")
301
+ all_entries = installed_data.get("plugins") or {}
302
+ plugin_entries = all_entries.get(f"{pname}@{marketplace_name}")
303
+ elsewhere = sorted(k for k in all_entries if k.startswith(f"{pname}@") and all_entries[k])
304
+ if not plugin_entries and elsewhere:
305
+ # Installed from ANOTHER marketplace (e.g. a Desktop upload). That
306
+ # copy is not ours to manage: installing @<marketplace_name> beside
307
+ # it creates a same-name duplicate (measured 2026-10-04).
308
+ first = all_entries[elsewhere[0]]
309
+ first = first[0] if isinstance(first, list) else first
310
+ install_path = first.get("installPath") if isinstance(first, dict) else None
311
+ pjson = _read_json(Path(install_path) / ".claude-plugin" / "plugin.json") if install_path else None
312
+ version = (pjson or {}).get("version") or "unknown version"
313
+ rows.append(_row(surface, NOT_DETECTABLE,
314
+ f"installed as {elsewhere[0]} ({version}), not from {marketplace_name} "
315
+ f"(marketplace has {mkt_version}) — update it where it was installed",
316
+ raw={"installed_elsewhere": elsewhere, "marketplace": mkt_version}))
317
+ continue
262
318
  if not plugin_entries:
263
319
  rows.append(_row(surface, NOT_DETECTABLE,
264
320
  f"not installed (marketplace has {mkt_version})",
265
- remediation=f"/plugin install {pname}@{marketplace_name}"))
321
+ remediation=f"/plugin install {pname}@{marketplace_name}",
322
+ raw={"marketplace": mkt_version}))
266
323
  continue
267
324
  # installed_plugins.json version field is a cache-dir label, not always
268
325
  # semver (it can be a git sha for github-sourced plugins) — read the
@@ -272,16 +329,13 @@ def check_installed_cli_plugins(
272
329
  # at its own version. Judge the oldest copy: reading only the first
273
330
  # entry reported a stale copy current (or the reverse) by list order.
274
331
  entries = plugin_entries if isinstance(plugin_entries, list) else [plugin_entries]
275
- copies, unreadable = [], None
276
- for entry in entries:
277
- install_path = entry.get("installPath") if isinstance(entry, dict) else None
278
- pjson = _read_json(Path(install_path) / ".claude-plugin" / "plugin.json") if install_path else None
279
- if not (pjson and pjson.get("version")):
280
- unreadable = install_path
281
- continue
282
- copies.append({"scope": entry.get("scope") or "user",
283
- "project_path": entry.get("projectPath"),
284
- "version": pjson["version"]})
332
+ copies, unreadable, orphaned = _plugin_copies(entries, mkt_version if from_folder else None)
333
+ if orphaned and not copies and unreadable is None:
334
+ rows.append(_row(surface, NOT_DETECTABLE,
335
+ f"installed only for {orphaned} deleted project(s) "
336
+ f"(marketplace has {mkt_version})",
337
+ raw={"marketplace": mkt_version, "orphaned_copies": orphaned}))
338
+ continue
285
339
  if unreadable is not None or not copies:
286
340
  rows.append(_row(surface, UNKNOWN,
287
341
  f"installed but version unreadable at {unreadable}"))
@@ -292,6 +346,8 @@ def check_installed_cli_plugins(
292
346
  pick = min if stale_copies else max
293
347
  installed_version = pick((c["version"] for c in copies), key=cmp_to_key(_compare))
294
348
  where = "" if len(copies) == 1 else f" ({len(stale_copies)} of {len(copies)} copies behind)"
349
+ if orphaned:
350
+ where += f" ({orphaned} copy(ies) for deleted projects ignored)"
295
351
  cmp_ = _compare(installed_version, mkt_version or "")
296
352
  if cmp_ == 0:
297
353
  rows.append(_row(surface, CURRENT, f"installed {installed_version} == marketplace {mkt_version}",
@@ -11,8 +11,7 @@ from .. import _optional, config, maintenance
11
11
  # ADR 0013: both constants feed only the COS folds, which never run without COS.
12
12
  if _optional.cos_available():
13
13
  from ..cos.sheet_threads import BODY_NEVER_READABLE as _BODY_NEVER_READABLE
14
- from ..cos._attachment_gate import (
15
- BODY_ERROR_NIGHTS as _BODY_ERROR_NIGHTS)
14
+ from ..cos._attachment_gate import BODY_ERROR_NIGHTS as _BODY_ERROR_NIGHTS
16
15
  else:
17
16
  _BODY_NEVER_READABLE = frozenset()
18
17
  _BODY_ERROR_NIGHTS = 0
@@ -115,8 +114,9 @@ class IntakeFoldsMixin:
115
114
  self._cos_night_stopped(run)
116
115
  from ..cos.night_died import night_died_items # noqa: PLC0415
117
116
  run.action_required.extend(night_died_items(self.vault, run.results))
118
- from ..cos._ingest_claim import encrypted_decline_items # noqa: PLC0415
119
- run.action_required.extend(encrypted_decline_items(self.vault))
117
+ from ..cos import _ingest_claim, calendar_block # noqa: PLC0415
118
+ run.action_required.extend(_ingest_claim.encrypted_decline_items(self.vault))
119
+ run.action_required.extend(calendar_block.read_failed_items(self.vault))
120
120
 
121
121
  def _cos_night_stopped(self, run: MaintenanceRun) -> None:
122
122
  """A NIGHT THAT STOPPED EARLY REACHES `brain alerts` (2026-09-27).
@@ -51,14 +51,15 @@ def rows_to_markdown(rows: list[list[object]], *, label: str = "table",
51
51
  rows = [r for r in rows if any(_cell(c) for c in r)] # drop fully-blank rows
52
52
  if not rows:
53
53
  return ""
54
+ ragged = any(len(r) != len(rows[0]) for r in rows[1:])
55
+ # Drop fully-blank columns too: a sheet's used range can run thousands of
56
+ # empty columns wide, and every row then carries them as "| |" padding.
57
+ width = max(len(r) for r in rows)
58
+ cols = [i for i in range(width) if any(i < len(r) and _cell(r[i]) for r in rows)]
59
+ rows = [[r[i] for i in cols if i < len(r)] for r in rows]
54
60
  header = [_cell(c) for c in rows[0]]
55
61
  ncols = len(header)
56
- unparsed = (
57
- ncols == 0
58
- or not any(header)
59
- or len(rows) < 2
60
- or any(len(r) != ncols for r in rows[1:])
61
- )
62
+ unparsed = ncols == 0 or not any(header) or len(rows) < 2 or ragged
62
63
  if unparsed:
63
64
  raw = "\n".join(" | ".join(_cell(c) for c in r) for r in rows)
64
65
  keep("chrome", "```table-unparsed")
@@ -233,6 +233,34 @@ def _link_targets(core: Any, query: str, sid: str,
233
233
  for _, hid, title in ranked[:limit]]
234
234
 
235
235
 
236
+ _SERIES_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}-")
237
+ _SERIES_NUMBER = re.compile(r"-\d+$")
238
+
239
+
240
+ def _series(sid: str) -> str:
241
+ """`2026-09-14-safety-environment-week-33` -> `safety-environment-week-#`.
242
+
243
+ ONLY A TRAILING NUMBER, so `doc-48367-1-signed` stays its own series: a
244
+ numbered document is not a sibling of another numbered document."""
245
+ return _SERIES_NUMBER.sub("-#", _SERIES_DATE.sub("", sid))
246
+
247
+
248
+ def noise_series(state: dict[str, Any], min_answers: int = 2) -> set[str]:
249
+ """Numbered series the owner has answered `noise` on at least twice.
250
+
251
+ Sheet 2026-10-09: ten `noise` answers on weekly HSSEQ reports (weeks 24-32)
252
+ and the lane still asked weeks 33, 34 and 36, because each answer silenced
253
+ one source and a new week arrives every week. Two answers, not one, so a
254
+ single noise answer never hides a sibling the owner has not seen."""
255
+ counts: dict[str, int] = {}
256
+ for r in state["rows"]:
257
+ if (r.get("shape") == "orphan" and r.get("answer") == "noise"
258
+ and _SERIES_NUMBER.search(str((r.get("target") or {}).get("id") or ""))):
259
+ key = _series(str(r["target"]["id"]))
260
+ counts[key] = counts.get(key, 0) + 1
261
+ return {k for k, n in counts.items() if n >= min_answers}
262
+
263
+
236
264
  def _orphan_candidates(core: Any, state: dict[str, Any], today: _dt.date,
237
265
  per_night: int = 3, max_searches: int = 10):
238
266
  """Sources nothing cites, from the linking lane's own worklist, with the
@@ -249,12 +277,15 @@ def _orphan_candidates(core: Any, state: dict[str, Any], today: _dt.date,
249
277
  except (OSError, ValueError):
250
278
  return
251
279
  yielded = searched = 0
280
+ silenced = noise_series(state)
252
281
  for c in lane.get("candidates") or []:
253
282
  if yielded >= per_night or searched >= max_searches:
254
283
  return
255
284
  sid = str(c.get("id") or "")
256
285
  if not sid or blocked(state, question_key("orphan", [sid]), sid, today):
257
286
  continue
287
+ if _series(sid) in silenced:
288
+ continue
258
289
  searched += 1
259
290
  title = str(c.get("title") or sid)
260
291
  query = (title + " " + note_excerpt(core.vault, str(c.get("path") or ""),
@@ -369,6 +369,7 @@ _EXACT: dict[str, Remedy] = {
369
369
  BANNER,
370
370
  note="ADR 0013: this vault has COS folders but the installed engine "
371
371
  "has no COS code, so the nightly cannot run; never silent"),
372
+ "cos-calendar-read-failed": Remedy(BANNER, note="next night retries; a good read clears it"),
372
373
  "exceptions": Remedy(
373
374
  BANNER, note="EXC-03: the unified 'N exception(s) — open <page>' "
374
375
  "banner, read the SAME way on host and VM from the "
@@ -68,12 +68,11 @@ def _run_plugin_reinstall(
68
68
  marketplace = raw.get("marketplace")
69
69
  plugin_name = row["surface"].split("(", 1)[1].rstrip(")")
70
70
  action = callbacks.decide_plugin_action(installed, marketplace)
71
- if dry_run:
72
- applied = {
73
- "action": action,
74
- "ok": True,
75
- "detail": f"[dry-run] would {action}",
76
- }
71
+ if raw.get("installed_elsewhere"): # another marketplace's copy: never add a duplicate
72
+ applied = {"action": "skip", "ok": True,
73
+ "detail": f"installed as {', '.join(raw['installed_elsewhere'])} — not managed here"}
74
+ elif dry_run:
75
+ applied = {"action": action, "ok": True, "detail": f"[dry-run] would {action}"}
77
76
  else:
78
77
  applied = callbacks.apply_plugin_action(
79
78
  action,
@@ -211,6 +210,28 @@ def _run_workspace_restage(
211
210
  )
212
211
 
213
212
 
213
+ _DESKTOP_SURFACES = ("Desktop/Cowork plugin store", "Claude Desktop Brainiac servers")
214
+
215
+
216
+ def _desktop_residual(after_doctor: dict[str, Any], compare: Any) -> list[str]:
217
+ """One owner step per Desktop row that needs one. Each row states its own
218
+ verdict in ``raw["action_needed"]`` (the plugin store judges skill TEXT,
219
+ not Cowork's lagging label); a row without it falls back to the label."""
220
+ ssot = after_doctor.get("ssot_version") or "0.0.0"
221
+ steps = []
222
+ for row in after_doctor["rows"]:
223
+ if not row["surface"].startswith(_DESKTOP_SURFACES):
224
+ continue
225
+ raw = row.get("raw") or {}
226
+ need = raw.get("action_needed")
227
+ if need is None:
228
+ need = bool(raw.get("version")) and compare(str(raw["version"]), ssot) < 0
229
+ if need:
230
+ steps.append(f"{row['surface']} is STALE ({row['detail']}) -> "
231
+ f"{row.get('remediation') or 'see brain doctor'}")
232
+ return steps
233
+
234
+
214
235
  def _finalize_update(
215
236
  result: dict[str, Any],
216
237
  before_table: dict[str, str],
@@ -219,20 +240,7 @@ def _finalize_update(
219
240
  ) -> None:
220
241
  """Build the final before/after table and completion note."""
221
242
  result["steps"]["doctor_after"] = callbacks.render_human(after_doctor)
222
- ssot_after = after_doctor.get("ssot_version") or "0.0.0"
223
- desktop_stale = any(
224
- row["surface"].startswith("Desktop/Cowork plugin store")
225
- and (row.get("raw") or {}).get("version")
226
- and callbacks.compare(str((row.get("raw") or {})["version"]), ssot_after) < 0
227
- for row in after_doctor["rows"]
228
- )
229
- if desktop_stale:
230
- result["residual_human_steps"].append(
231
- "Desktop/Cowork plugin store is STALE and has no external CLI: in a "
232
- "Cowork session use /skill-creator to repackage + Save-and-Replace the "
233
- "stale skill(s) — /brainiac-update is host-only and refuses in Cowork — "
234
- "then re-run `brain doctor` on the host to confirm it took."
235
- )
243
+ result["residual_human_steps"].extend(_desktop_residual(after_doctor, callbacks.compare))
236
244
 
237
245
  table: list[dict[str, str]] = []
238
246
  surfaces = sorted(set(before_table) | {