codespy-ai 2.0.1__tar.gz → 2.0.2__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 (126) hide show
  1. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/PKG-INFO +1 -1
  2. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/pyproject.toml +1 -1
  3. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/__init__.py +1 -1
  4. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/cerebral/cerebral.py +163 -13
  5. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/prefrontal/prefrontal.py +2 -1
  6. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/prefrontal/reach.py +46 -10
  7. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/scope/agent.py +15 -29
  8. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/LICENSE +0 -0
  9. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/README.md +0 -0
  10. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/__init__.py +0 -0
  11. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/context_safe.py +0 -0
  12. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/cost_tracker.py +0 -0
  13. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/diagnostic_adapter.py +0 -0
  14. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/dspy_config.py +0 -0
  15. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/__init__.py +0 -0
  16. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/cerebral/__init__.py +0 -0
  17. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/cerebral/cost.py +0 -0
  18. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/cerebral/delta_ops_compat.py +0 -0
  19. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/cerebral/orphans.py +0 -0
  20. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/cerebral/routines.py +0 -0
  21. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/cerebral/structured_output.py +0 -0
  22. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/hippocampus/__init__.py +0 -0
  23. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/hippocampus/budget.py +0 -0
  24. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/hippocampus/cartographer.py +0 -0
  25. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/hippocampus/context_memory.py +0 -0
  26. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/hippocampus/distiller.py +0 -0
  27. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/hippocampus/episode.py +0 -0
  28. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/hippocampus/hippocampus.py +0 -0
  29. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/pg0_manager.py +0 -0
  30. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/postgres.py +0 -0
  31. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/prefrontal/__init__.py +0 -0
  32. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/prefrontal/facets.py +0 -0
  33. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/prefrontal/format.py +0 -0
  34. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/prefrontal/query.py +0 -0
  35. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/memory/recall.py +0 -0
  36. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/__init__.py +0 -0
  37. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/audit/__init__.py +0 -0
  38. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/audit/agent.py +0 -0
  39. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/code_review/__init__.py +0 -0
  40. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/code_review/agent.py +0 -0
  41. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/doc/__init__.py +0 -0
  42. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/doc/agent.py +0 -0
  43. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/doc/doc_extractor.py +0 -0
  44. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/helpers.py +0 -0
  45. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/models.py +0 -0
  46. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/scope/__init__.py +0 -0
  47. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/scope/manifest_parser.py +0 -0
  48. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/scope/models.py +0 -0
  49. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/summary/__init__.py +0 -0
  50. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/summary/agent.py +0 -0
  51. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/supply_chain/__init__.py +0 -0
  52. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/review/supply_chain/agent.py +0 -0
  53. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/agents/rlm_tools.py +0 -0
  54. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/cli.py +0 -0
  55. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/cli_local.py +0 -0
  56. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/cli_mcp_server.py +0 -0
  57. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/cli_remote.py +0 -0
  58. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/config.py +0 -0
  59. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/config_dspy.py +0 -0
  60. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/config_git.py +0 -0
  61. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/config_io.py +0 -0
  62. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/config_llm.py +0 -0
  63. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/config_memory.py +0 -0
  64. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/config_utils.py +0 -0
  65. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/__init__.py +0 -0
  66. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/cyber/__init__.py +0 -0
  67. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/cyber/osv/__init__.py +0 -0
  68. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/cyber/osv/client.py +0 -0
  69. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/cyber/osv/models.py +0 -0
  70. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/cyber/osv/server.py +0 -0
  71. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/git/__init__.py +0 -0
  72. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/git/base.py +0 -0
  73. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/git/client.py +0 -0
  74. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/git/github_client.py +0 -0
  75. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/git/gitlab_client.py +0 -0
  76. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/git/local_diff.py +0 -0
  77. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/git/models.py +0 -0
  78. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/git/server.py +0 -0
  79. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/mcp_utils.py +0 -0
  80. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/__init__.py +0 -0
  81. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/ripgrep/__init__.py +0 -0
  82. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/ripgrep/client.py +0 -0
  83. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/ripgrep/server.py +0 -0
  84. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/__init__.py +0 -0
  85. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/base_extractor.py +0 -0
  86. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/__init__.py +0 -0
  87. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/bash.py +0 -0
  88. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/cpp.py +0 -0
  89. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/csharp.py +0 -0
  90. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/go.py +0 -0
  91. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/java.py +0 -0
  92. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/javascript.py +0 -0
  93. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/kotlin.py +0 -0
  94. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/objc.py +0 -0
  95. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/php.py +0 -0
  96. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/python.py +0 -0
  97. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/ripgrep_fallback.py +0 -0
  98. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/ruby.py +0 -0
  99. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/rust.py +0 -0
  100. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/swift.py +0 -0
  101. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/terraform.py +0 -0
  102. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/models.py +0 -0
  103. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/parser.py +0 -0
  104. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/server.py +0 -0
  105. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/storage/__init__.py +0 -0
  106. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/storage/base.py +0 -0
  107. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/storage/filesystem/__init__.py +0 -0
  108. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/storage/filesystem/client.py +0 -0
  109. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/storage/filesystem/server.py +0 -0
  110. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/storage/models.py +0 -0
  111. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/storage/s3/__init__.py +0 -0
  112. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/storage/s3/client.py +0 -0
  113. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/storage/s3/server.py +0 -0
  114. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/web/__init__.py +0 -0
  115. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/web/client.py +0 -0
  116. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/web/models.py +0 -0
  117. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/tools/web/server.py +0 -0
  118. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/workflows/__init__.py +0 -0
  119. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/workflows/review/__init__.py +0 -0
  120. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/workflows/review/models.py +0 -0
  121. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/workflows/review/pipeline.py +0 -0
  122. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/workflows/review/reporters/__init__.py +0 -0
  123. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/workflows/review/reporters/base.py +0 -0
  124. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/workflows/review/reporters/git.py +0 -0
  125. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/workflows/review/reporters/stdout.py +0 -0
  126. {codespy_ai-2.0.1 → codespy_ai-2.0.2}/src/codespy/workflows/review/server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codespy-ai
3
- Version: 2.0.1
3
+ Version: 2.0.2
4
4
  Summary: Code review agent powered by DSPy
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "codespy-ai"
3
- version = "2.0.1"
3
+ version = "2.0.2"
4
4
  description = "Code review agent powered by DSPy"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,3 +1,3 @@
1
1
  """codespy - Code review agent powered by DSPy."""
2
2
 
3
- __version__ = "2.0.1"
3
+ __version__ = "2.0.2"
@@ -238,8 +238,10 @@ from codespy.agents.memory.prefrontal.reach import (
238
238
  TAG_ORG,
239
239
  TAG_PROJECT_SCOPE,
240
240
  TAG_REPO,
241
+ is_root_scope,
241
242
  mental_model_id,
242
243
  repo_tags,
244
+ root_scope_id,
243
245
  scope_tags,
244
246
  )
245
247
 
@@ -300,6 +302,26 @@ MENTAL_MODEL_SOURCE_QUERY = (
300
302
  "dependencies and integrations, pitfalls and recurring problems, facts shown to be wrong."
301
303
  )
302
304
 
305
+ # Question the repo-root briefing answers. The root scope of a monorepo sees
306
+ # facts from many scopes; this briefing must keep only what holds for all of them.
307
+ ROOT_MENTAL_MODEL_SOURCE_QUERY = (
308
+ "What should an agent starting work anywhere in this repository know, that holds for "
309
+ "every scope of it: repository layout and naming conventions, how scopes and manifests "
310
+ "are organized, shared tooling, build, CI and deployment, cross-scope integrations and "
311
+ "contracts, and repository-wide pitfalls or facts shown to be wrong. "
312
+ "Do not describe individual services, packages or libraries, their files, pull requests "
313
+ "or review findings; omit anything that is true for only one scope."
314
+ )
315
+
316
+ # A delta refresh only adds content, so briefings grow past max_tokens. Past this
317
+ # factor of max_tokens, consolidate_run forces one full refresh (rewrite from scratch).
318
+ BRIEFING_FULL_REFRESH_FACTOR = 1.5
319
+
320
+
321
+ def _estimate_tokens(text: str) -> int:
322
+ """Cheap token estimate (~4 chars per token), same heuristic as prefrontal.format."""
323
+ return (len(text or "") + 3) // 4
324
+
303
325
  OBSERVATIONS_MISSION = (
304
326
  "Merge facts about the same subject into one observation. Keep details that matter only "
305
327
  "to one kind of work (e.g. documentation, security, dependencies) instead of generalizing "
@@ -823,16 +845,21 @@ class Cerebral:
823
845
  if topic.type == "project_scope"
824
846
  ]
825
847
  # Fallback to repo-root project scope instead of bare [org:, repo:]
826
- return scopes or [scope_tags(repo_full_name, repo_full_name)]
848
+ return scopes or [scope_tags(repo_full_name, root_scope_id(repo_full_name))]
827
849
 
828
850
  @staticmethod
829
851
  def _mental_model_scopes(
830
852
  scopes: Sequence[Sequence[str]], repo_full_name: str
831
853
  ) -> list[list[str]]:
832
- """Observation scopes plus the repo scope ``[org:, repo:]`` (always added, deduped)."""
854
+ """Observation scopes whose briefings are kept (deduped).
855
+
856
+ No bare ``[org:, repo:]`` scope: its briefing would read every fact of
857
+ the repo (all scopes of a monorepo). Repo-wide knowledge lives in the
858
+ repo-root scope ``[org:, repo:, project_scope:<repo>]`` instead.
859
+ """
833
860
  out: list[list[str]] = []
834
861
  seen: set[tuple[str, ...]] = set()
835
- for sc in [*scopes, repo_tags(repo_full_name)]:
862
+ for sc in scopes:
836
863
  key = tuple(sorted(sc))
837
864
  if key not in seen:
838
865
  seen.add(key)
@@ -840,15 +867,43 @@ class Cerebral:
840
867
  return out
841
868
 
842
869
  @staticmethod
843
- def _mental_model_name(tags: Sequence[str]) -> str:
870
+ def _scope_id_of(tags: Sequence[str]) -> str | None:
844
871
  for tag in tags:
845
872
  if tag.startswith(TAG_PROJECT_SCOPE):
846
- return f"Briefing: {tag[len(TAG_PROJECT_SCOPE):]}"
873
+ return tag[len(TAG_PROJECT_SCOPE):]
874
+ return None
875
+
876
+ @staticmethod
877
+ def _repo_of(tags: Sequence[str]) -> str | None:
847
878
  for tag in tags:
848
- if tag.startswith("repo:"):
849
- return f"Briefing: {tag[len('repo:'):]}"
879
+ if tag.startswith(TAG_REPO):
880
+ return tag[len(TAG_REPO):]
881
+ return None
882
+
883
+ @classmethod
884
+ def _is_root_briefing(cls, tags: Sequence[str]) -> bool:
885
+ scope_id = cls._scope_id_of(tags)
886
+ repo = cls._repo_of(tags)
887
+ return bool(scope_id and repo and is_root_scope(repo, scope_id))
888
+
889
+ @classmethod
890
+ def _mental_model_name(cls, tags: Sequence[str]) -> str:
891
+ scope_id = cls._scope_id_of(tags)
892
+ if scope_id:
893
+ if cls._is_root_briefing(tags):
894
+ return f"Briefing: {scope_id} (repo-wide)"
895
+ return f"Briefing: {scope_id}"
896
+ repo = cls._repo_of(tags)
897
+ if repo:
898
+ return f"Briefing: {repo}"
850
899
  return "Briefing"
851
900
 
901
+ @classmethod
902
+ def _mental_model_source_query(cls, tags: Sequence[str]) -> str:
903
+ if cls._is_root_briefing(tags):
904
+ return ROOT_MENTAL_MODEL_SOURCE_QUERY
905
+ return MENTAL_MODEL_SOURCE_QUERY
906
+
852
907
  def _sync_briefing_triggers(self, scopes: list[list[str]]) -> None:
853
908
  """Sync briefing triggers to match current settings before consolidation.
854
909
 
@@ -863,11 +918,7 @@ class Cerebral:
863
918
  if not scopes:
864
919
  return
865
920
 
866
- desired_trigger = {
867
- **_BRIEFING_TRIGGER_STATIC,
868
- "refresh_after_consolidation": self._mental_models,
869
- "min_refresh_interval_seconds": self._min_mental_model_refresh_seconds,
870
- }
921
+ desired_trigger = self._desired_trigger()
871
922
 
872
923
  for tags in scopes:
873
924
  mm_id = mental_model_id(tags)
@@ -891,7 +942,7 @@ class Cerebral:
891
942
  self._engine.create_mental_model(
892
943
  self._bank_id,
893
944
  self._mental_model_name(tags),
894
- MENTAL_MODEL_SOURCE_QUERY,
945
+ self._mental_model_source_query(tags),
895
946
  MENTAL_MODEL_PLACEHOLDER,
896
947
  mental_model_id=mm_id,
897
948
  tags=sorted(tags),
@@ -919,6 +970,7 @@ class Cerebral:
919
970
  needs_update = (
920
971
  current_trigger.get("refresh_after_consolidation") != self._mental_models
921
972
  or current_trigger.get("exclude_mental_models") is not True
973
+ or current_trigger.get("mode", "delta") != "delta"
922
974
  )
923
975
  if needs_update:
924
976
  self._run_async(
@@ -935,11 +987,103 @@ class Cerebral:
935
987
  "on" if current_trigger.get("refresh_after_consolidation") else "off",
936
988
  "on" if self._mental_models else "off",
937
989
  )
990
+ # Name / source query drift (e.g. the repo-root briefing moved to
991
+ # its repo-wide question). A changed source query makes Hindsight's
992
+ # next delta refresh fall back to a full rewrite.
993
+ desired_name = self._mental_model_name(tags)
994
+ desired_query = self._mental_model_source_query(tags)
995
+ if (
996
+ existing.get("name") != desired_name
997
+ or existing.get("source_query") != desired_query
998
+ ):
999
+ self._run_async(
1000
+ self._engine.update_mental_model(
1001
+ self._bank_id,
1002
+ mm_id,
1003
+ name=desired_name,
1004
+ source_query=desired_query,
1005
+ request_context=ctx,
1006
+ )
1007
+ )
1008
+ logger.info(
1009
+ "cerebral: briefing %s renamed/re-queried as %r (next refresh is full)",
1010
+ mm_id,
1011
+ desired_name,
1012
+ )
938
1013
 
939
1014
  self._ensured_mental_models.add(mm_id)
940
1015
  except Exception:
941
1016
  logger.warning("cerebral: failed to sync briefing trigger for %s", mm_id, exc_info=True)
942
1017
 
1018
+ def _desired_trigger(self) -> dict[str, Any]:
1019
+ """Briefing trigger for the current settings (delta mode)."""
1020
+ return {
1021
+ **_BRIEFING_TRIGGER_STATIC,
1022
+ "refresh_after_consolidation": self._mental_models,
1023
+ "min_refresh_interval_seconds": self._min_mental_model_refresh_seconds,
1024
+ }
1025
+
1026
+ def _enforce_briefing_size(self, scopes: Sequence[Sequence[str]]) -> None:
1027
+ """Force a full refresh of briefings grown past the size limit. Never raises.
1028
+
1029
+ Delta refreshes only add content, so a briefing drifts past
1030
+ ``max_mental_model_tokens``. Above ``BRIEFING_FULL_REFRESH_FACTOR`` times
1031
+ that budget, the briefing is rewritten from scratch once: the trigger is
1032
+ switched to ``mode=full``, one explicit refresh runs inline (sync task
1033
+ backend), then the delta trigger is restored. Never deletes anything.
1034
+ """
1035
+ if not self._mental_models or not scopes:
1036
+ return
1037
+ limit = int(self._max_mental_model_tokens * BRIEFING_FULL_REFRESH_FACTOR)
1038
+ for tags in scopes:
1039
+ mm_id = mental_model_id(tags)
1040
+ try:
1041
+ ctx = RequestContext()
1042
+ mm = self._run_async(
1043
+ self._engine.get_mental_model(self._bank_id, mm_id, request_context=ctx)
1044
+ )
1045
+ if not mm:
1046
+ continue
1047
+ before = _estimate_tokens((mm.get("content") or "").strip())
1048
+ if before <= limit:
1049
+ continue
1050
+ desired = self._desired_trigger()
1051
+ self._run_async(
1052
+ self._engine.update_mental_model(
1053
+ self._bank_id,
1054
+ mm_id,
1055
+ trigger={**desired, "mode": "full"},
1056
+ request_context=ctx,
1057
+ )
1058
+ )
1059
+ try:
1060
+ self._run_async(
1061
+ self._engine.submit_async_refresh_mental_model(
1062
+ self._bank_id, mm_id, request_context=ctx
1063
+ )
1064
+ )
1065
+ finally:
1066
+ self._run_async(
1067
+ self._engine.update_mental_model(
1068
+ self._bank_id, mm_id, trigger=desired, request_context=ctx
1069
+ )
1070
+ )
1071
+ after_mm = self._run_async(
1072
+ self._engine.get_mental_model(self._bank_id, mm_id, request_context=ctx)
1073
+ )
1074
+ after = _estimate_tokens(((after_mm or {}).get("content") or "").strip())
1075
+ logger.info(
1076
+ "cerebral: briefing %s (%s) over size (%d > %d tokens); "
1077
+ "full refresh -> %d tokens",
1078
+ mm_id,
1079
+ self._mental_model_name(tags),
1080
+ before,
1081
+ limit,
1082
+ after,
1083
+ )
1084
+ except Exception:
1085
+ logger.warning("cerebral: briefing size guard failed for %s", mm_id, exc_info=True)
1086
+
943
1087
  def consolidate_run(self, run_id: str) -> None:
944
1088
  """Trigger one consolidation for all scopes touched during a run.
945
1089
 
@@ -986,6 +1130,8 @@ class Cerebral:
986
1130
  observation_scopes=scopes,
987
1131
  )
988
1132
  )
1133
+ # Rewrite briefings that delta refreshes grew past their budget
1134
+ self._enforce_briefing_size(scopes)
989
1135
 
990
1136
  delta = {b: _calls(b) - baseline[b] for b in buckets}
991
1137
  logger.info(
@@ -1218,6 +1364,10 @@ class Cerebral:
1218
1364
  if topic.type == "project_scope":
1219
1365
  tags.append(f"{topic.type}:{topic.id}")
1220
1366
  if repo_full_name:
1367
+ # No scope topic: tag with the repo-root scope, matching the
1368
+ # _observation_scopes fallback (recall reads project_scope:, not repo:).
1369
+ if not tags:
1370
+ tags.append(f"{TAG_PROJECT_SCOPE}{root_scope_id(repo_full_name)}")
1221
1371
  tags.extend(repo_tags(repo_full_name))
1222
1372
  tags.append(f"task:{episode.task}")
1223
1373
  return tags
@@ -122,7 +122,8 @@ class Prefrontal:
122
122
  task_name: Agent task (e.g. ``code_review``).
123
123
  repo_full_name: ``owner/repo``.
124
124
  scope_topic_ids: ``project_scope`` topic ids of the agent's scope(s).
125
- include_repo: Also read repo-level facts (``repo:<owner/repo>``) and briefing.
125
+ include_repo: Also read the repo-root scope (``project_scope:<owner/repo>``):
126
+ repo-wide facts and the repo-wide briefing. Never every ``repo:`` fact.
126
127
  """
127
128
 
128
129
  def __init__(
@@ -41,6 +41,32 @@ def scope_tags(repo_full_name: str, scope_topic_id: str) -> list[str]:
41
41
  return [*repo_tags(repo_full_name), f"{TAG_PROJECT_SCOPE}{scope_topic_id}"]
42
42
 
43
43
 
44
+ def root_scope_id(repo_full_name: str) -> str:
45
+ """Topic id of the repo-root scope (same as ``make_topic_id(repo, '.')``).
46
+
47
+ The root scope holds repo-wide knowledge only (layout, conventions, shared
48
+ tooling); it is never a container of every scope's facts.
49
+ """
50
+ return repo_full_name
51
+
52
+
53
+ def is_root_scope(repo_full_name: str, scope_topic_id: str) -> bool:
54
+ """True when ``scope_topic_id`` is the repo-root scope of ``repo_full_name``."""
55
+ return bool(repo_full_name) and scope_topic_id == root_scope_id(repo_full_name)
56
+
57
+
58
+ def _with_root(
59
+ repo_full_name: str, scope_topic_ids: Sequence[str] | None, include_repo: bool
60
+ ) -> list[str]:
61
+ """Scope ids, plus the repo-root scope id when ``include_repo`` (deduped, order kept)."""
62
+ ids = [sid for sid in (scope_topic_ids or []) if sid]
63
+ if include_repo and repo_full_name:
64
+ root = root_scope_id(repo_full_name)
65
+ if root not in ids:
66
+ ids.append(root)
67
+ return ids
68
+
69
+
44
70
  def mental_model_id(tags: Iterable[str]) -> str:
45
71
  """Deterministic mental-model id for a tag scope.
46
72
 
@@ -55,11 +81,16 @@ def mental_model_scopes(
55
81
  scope_topic_ids: Sequence[str] | None,
56
82
  include_repo: bool,
57
83
  ) -> list[list[str]]:
58
- """Tag scopes whose briefings an agent reads: its scopes first, then the repo."""
59
- scopes = [scope_tags(repo_full_name, sid) for sid in (scope_topic_ids or [])]
60
- if include_repo:
61
- scopes.append(repo_tags(repo_full_name))
62
- return scopes
84
+ """Tag scopes whose briefings an agent reads: its scopes, then the repo-root scope.
85
+
86
+ ``include_repo`` adds the repo-root scope ``[org:, repo:, project_scope:<repo>]``
87
+ (repo-wide knowledge), never the bare ``[org:, repo:]`` scope, which would
88
+ cover every scope of a monorepo.
89
+ """
90
+ return [
91
+ scope_tags(repo_full_name, sid)
92
+ for sid in _with_root(repo_full_name, scope_topic_ids, include_repo)
93
+ ]
63
94
 
64
95
 
65
96
  def mental_model_ids(
@@ -88,11 +119,16 @@ def local_tags(
88
119
  scope_topic_ids: Sequence[str] | None,
89
120
  include_repo: bool,
90
121
  ) -> list[str]:
91
- """Tags of the local pool (OR-ed): ``project_scope:`` ids, plus ``repo:`` if requested."""
92
- tags = [f"{TAG_PROJECT_SCOPE}{sid}" for sid in (scope_topic_ids or [])]
93
- if include_repo:
94
- tags.insert(0, f"{TAG_REPO}{repo_full_name}")
95
- return tags
122
+ """Tags of the local pool (OR-ed): ``project_scope:`` ids, plus the repo-root
123
+ scope ``project_scope:<repo>`` if ``include_repo``.
124
+
125
+ Never ``repo:<repo>``: every fact of the repo carries it, so OR-ing it in
126
+ would pull every scope of a monorepo into the local pool.
127
+ """
128
+ return [
129
+ f"{TAG_PROJECT_SCOPE}{sid}"
130
+ for sid in _with_root(repo_full_name, scope_topic_ids, include_repo)
131
+ ]
96
132
 
97
133
 
98
134
  def build_local_groups(
@@ -1037,8 +1037,8 @@ class ScopeResolver(dspy.Module):
1037
1037
  from codespy.agents.memory.hippocampus import (
1038
1038
  ContextMemory,
1039
1039
  Topic,
1040
- compute_common_ancestor_topic_id,
1041
1040
  )
1041
+ from codespy.agents.memory.prefrontal.reach import root_scope_id
1042
1042
 
1043
1043
  # Local bindings from review_context metadata
1044
1044
  pr = review_context.metadata.pr
@@ -1145,38 +1145,24 @@ class ScopeResolver(dspy.Module):
1145
1145
  if scope.subroot in boundary_descriptions:
1146
1146
  scope.description = boundary_descriptions[scope.subroot]
1147
1147
 
1148
- # Build Topics
1149
- scope_topics: list[Topic] = []
1150
- scope_topic_ids: dict[str, str] = {}
1151
- for scope in final_scopes:
1152
- t = scope.topic(pr.repo_full_name)
1153
- scope_topics.append(t)
1154
- scope_topic_ids[scope.subroot] = t.id
1155
-
1156
- # Compute common ancestor topic if >1 scope
1157
- common_ancestor_topic_id = compute_common_ancestor_topic_id(
1158
- pr.repo_full_name, [s.subroot for s in final_scopes]
1159
- )
1160
- if common_ancestor_topic_id:
1161
- # Build description: "Common context for scopes: subroot1, subroot2, ..."
1162
- subroot_list = ", ".join(s.subroot for s in final_scopes)
1163
- common_desc = f"Common context for scopes: {subroot_list}"
1164
- scope_topics.append(Topic(id=common_ancestor_topic_id, type="project_scope", description=common_desc))
1165
- stamp_topic_ids = [common_ancestor_topic_id]
1166
- elif scope_topics:
1167
- # Single scope: stamp with its topic ID
1168
- stamp_topic_ids = [scope_topics[0].id]
1169
- else:
1170
- stamp_topic_ids = []
1171
- # Add PR URL topic (provides description; not a scope)
1148
+ # Scope-agent knowledge (repo layout, scope boundaries) is repo-wide:
1149
+ # bind it to the repo-root scope only. Binding every PR scope stamped
1150
+ # layout facts onto each service's briefing.
1151
+ root_topic_id = root_scope_id(pr.repo_full_name)
1172
1152
  pr_ctx = review_context.pr_context
1173
- scope_topics.append(pr_ctx.to_topic())
1153
+ scope_topics: list[Topic] = [
1154
+ Topic(
1155
+ id=root_topic_id,
1156
+ type="project_scope",
1157
+ description="Repository root: knowledge shared by every scope",
1158
+ ),
1159
+ pr_ctx.to_topic(),
1160
+ ]
1174
1161
  # Finalize scopes with hierarchical skills
1175
1162
  self._finalize_scopes(final_scopes, review_context)
1176
1163
  # Bind topics to hippocampus cmem for episode persistence
1177
- if hippo is not None and stamp_topic_ids:
1178
- stamp_topic_ids.append(pr_ctx.pr_url)
1179
- hippo.bind_topics(scope_topics, stamp_topic_ids)
1164
+ if hippo is not None and final_scopes:
1165
+ hippo.bind_topics(scope_topics, [root_topic_id, pr_ctx.pr_url])
1180
1166
 
1181
1167
  # Fire-and-forget background episode save
1182
1168
  cerebral = get_cerebral(self._settings)
File without changes
File without changes