codespy-ai 1.2.3__tar.gz → 1.2.4__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 (111) hide show
  1. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/PKG-INFO +1 -1
  2. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/pyproject.toml +1 -1
  3. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/__init__.py +1 -1
  4. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/agents/review/audit/agent.py +27 -16
  5. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/agents/review/scope/agent.py +57 -9
  6. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/config.py +12 -0
  7. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/config_dspy.py +1 -0
  8. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/workflows/review/pipeline.py +4 -3
  9. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/LICENSE +0 -0
  10. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/README.md +0 -0
  11. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/agents/__init__.py +0 -0
  12. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/agents/context_safe.py +0 -0
  13. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/agents/cost_tracker.py +0 -0
  14. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/agents/dspy_config.py +0 -0
  15. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/agents/memory/__init__.py +0 -0
  16. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/agents/memory/hippocampus/__init__.py +0 -0
  17. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/agents/memory/hippocampus/budget.py +0 -0
  18. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/agents/memory/hippocampus/cartographer.py +0 -0
  19. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/agents/memory/hippocampus/context_memory.py +0 -0
  20. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/agents/memory/hippocampus/distiller.py +0 -0
  21. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/agents/memory/hippocampus/episode.py +0 -0
  22. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/agents/memory/hippocampus/hippocampus.py +0 -0
  23. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/agents/memory/pg0_manager.py +0 -0
  24. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/agents/memory/postgres.py +0 -0
  25. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/agents/review/__init__.py +0 -0
  26. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/agents/review/audit/__init__.py +0 -0
  27. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/agents/review/code_review/__init__.py +0 -0
  28. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/agents/review/code_review/agent.py +0 -0
  29. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/agents/review/doc/__init__.py +0 -0
  30. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/agents/review/doc/agent.py +0 -0
  31. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/agents/review/doc/doc_extractor.py +0 -0
  32. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/agents/review/helpers.py +0 -0
  33. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/agents/review/models.py +0 -0
  34. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/agents/review/scope/__init__.py +0 -0
  35. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/agents/review/scope/manifest_parser.py +0 -0
  36. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/agents/review/scope/models.py +0 -0
  37. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/agents/review/summary/__init__.py +0 -0
  38. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/agents/review/summary/agent.py +0 -0
  39. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/agents/review/supply_chain/__init__.py +0 -0
  40. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/agents/review/supply_chain/agent.py +0 -0
  41. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/cli.py +0 -0
  42. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/cli_local.py +0 -0
  43. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/cli_mcp_server.py +0 -0
  44. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/cli_remote.py +0 -0
  45. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/config_git.py +0 -0
  46. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/config_io.py +0 -0
  47. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/config_llm.py +0 -0
  48. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/config_memory.py +0 -0
  49. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/config_utils.py +0 -0
  50. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/__init__.py +0 -0
  51. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/cyber/__init__.py +0 -0
  52. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/cyber/osv/__init__.py +0 -0
  53. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/cyber/osv/client.py +0 -0
  54. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/cyber/osv/models.py +0 -0
  55. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/cyber/osv/server.py +0 -0
  56. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/git/__init__.py +0 -0
  57. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/git/base.py +0 -0
  58. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/git/client.py +0 -0
  59. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/git/github_client.py +0 -0
  60. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/git/gitlab_client.py +0 -0
  61. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/git/local_diff.py +0 -0
  62. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/git/models.py +0 -0
  63. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/git/patch_utils.py +0 -0
  64. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/git/server.py +0 -0
  65. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/mcp_utils.py +0 -0
  66. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/parsers/__init__.py +0 -0
  67. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/parsers/ripgrep/__init__.py +0 -0
  68. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/parsers/ripgrep/client.py +0 -0
  69. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/parsers/ripgrep/server.py +0 -0
  70. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/__init__.py +0 -0
  71. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/base_extractor.py +0 -0
  72. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/extractors/__init__.py +0 -0
  73. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/extractors/bash.py +0 -0
  74. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/extractors/cpp.py +0 -0
  75. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/extractors/csharp.py +0 -0
  76. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/extractors/go.py +0 -0
  77. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/extractors/java.py +0 -0
  78. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/extractors/javascript.py +0 -0
  79. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/extractors/kotlin.py +0 -0
  80. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/extractors/objc.py +0 -0
  81. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/extractors/php.py +0 -0
  82. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/extractors/python.py +0 -0
  83. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/extractors/ripgrep_fallback.py +0 -0
  84. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/extractors/ruby.py +0 -0
  85. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/extractors/rust.py +0 -0
  86. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/extractors/swift.py +0 -0
  87. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/extractors/terraform.py +0 -0
  88. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/models.py +0 -0
  89. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/parser.py +0 -0
  90. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/parsers/treesitter/server.py +0 -0
  91. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/storage/__init__.py +0 -0
  92. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/storage/base.py +0 -0
  93. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/storage/filesystem/__init__.py +0 -0
  94. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/storage/filesystem/client.py +0 -0
  95. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/storage/filesystem/server.py +0 -0
  96. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/storage/models.py +0 -0
  97. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/storage/s3/__init__.py +0 -0
  98. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/storage/s3/client.py +0 -0
  99. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/storage/s3/server.py +0 -0
  100. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/web/__init__.py +0 -0
  101. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/web/client.py +0 -0
  102. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/web/models.py +0 -0
  103. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/tools/web/server.py +0 -0
  104. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/workflows/__init__.py +0 -0
  105. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/workflows/review/__init__.py +0 -0
  106. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/workflows/review/models.py +0 -0
  107. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/workflows/review/reporters/__init__.py +0 -0
  108. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/workflows/review/reporters/base.py +0 -0
  109. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/workflows/review/reporters/git.py +0 -0
  110. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/src/codespy/workflows/review/reporters/stdout.py +0 -0
  111. {codespy_ai-1.2.3 → codespy_ai-1.2.4}/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: 1.2.3
3
+ Version: 1.2.4
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 = "1.2.3"
3
+ version = "1.2.4"
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__ = "1.2.3"
3
+ __version__ = "1.2.4"
@@ -9,6 +9,7 @@ from codespy.agents import SignatureContext, get_cost_tracker
9
9
  from codespy.agents.context_safe import ContextSafe
10
10
  from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus, inject_context_memory
11
11
  from codespy.agents.memory.hippocampus.context_memory import Topic
12
+ from codespy.agents.memory.hippocampus.episode import submit_episode_save
12
13
  from codespy.agents.review.models import Issue, ReviewContext
13
14
  from codespy.agents.review.helpers import deepest_common_folder
14
15
  from codespy.config import get_settings
@@ -16,6 +17,7 @@ from codespy.config_memory import get_episode_store
16
17
 
17
18
  if TYPE_CHECKING:
18
19
  from codespy.agents.review.scope.models import ScopeResult
20
+ from codespy.agents.memory.hippocampus.episode import EpisodeStore
19
21
 
20
22
  logger = logging.getLogger(__name__)
21
23
 
@@ -57,8 +59,13 @@ class Auditor(dspy.Module):
57
59
  run_id: str | None,
58
60
  scopes: list["ScopeResult"] | None,
59
61
  topics: list["ScopeResult"] | None = None,
60
- ) -> dspy.Prediction:
61
- """Execute the auditor predictor (with or without Hippocampus memory)."""
62
+ ) -> tuple[dspy.Prediction, Hippocampus | None, "EpisodeStore | None", dict[str, str] | None]:
63
+ """Execute the auditor predictor (with or without Hippocampus memory).
64
+
65
+ Returns:
66
+ Tuple of (result, hippo, store, artifacts) where hippo/store/artifacts
67
+ are None when memory is disabled.
68
+ """
62
69
  question = (
63
70
  f"final audit of {review_context.pr_context.repo_slug}: "
64
71
  f"pull request {review_context.pr_context.pr_number} "
@@ -110,25 +117,20 @@ class Auditor(dspy.Module):
110
117
  all_issues=all_issues,
111
118
  )
112
119
  hippo.observe(result)
113
- # Run episode save synchronously (auditor is the last module)
114
- try:
115
- _artifacts = {
116
- "audit": (
117
- f"## Quality Assessment\n\n{result.quality_assessment}\n\n"
118
- f"## Recommendation\n\n{result.recommendation}\n"
119
- )
120
- }
121
- hippo.end_episode(store, artifacts=_artifacts)
122
- except Exception:
123
- logger.warning("Audit episode save failed", exc_info=True)
120
+ _artifacts = {
121
+ "audit": (
122
+ f"## Quality Assessment\n\n{result.quality_assessment}\n\n"
123
+ f"## Recommendation\n\n{result.recommendation}\n"
124
+ )
125
+ }
126
+ return result, hippo, store, _artifacts
124
127
  else:
125
128
  result = auditor(
126
129
  pr_title=review_context.pr_context.pr_title,
127
130
  summary=review_context.pr_context.summary,
128
131
  all_issues=all_issues,
129
132
  )
130
-
131
- return result
133
+ return result, None, None, None
132
134
 
133
135
  def forward(
134
136
  self,
@@ -168,7 +170,7 @@ class Auditor(dspy.Module):
168
170
  logger.info("Running audit...")
169
171
 
170
172
  with SignatureContext("audit", self._cost_tracker):
171
- result = self._call_auditor(
173
+ result, hippo, store, _artifacts = self._call_auditor(
172
174
  auditor,
173
175
  review_context,
174
176
  all_issues,
@@ -177,4 +179,13 @@ class Auditor(dspy.Module):
177
179
  topics,
178
180
  )
179
181
 
182
+ # Background episode save (after SignatureContext closes)
183
+ if hippo is not None and store is not None:
184
+ def _persist():
185
+ try:
186
+ hippo.end_episode(store, artifacts=_artifacts)
187
+ except Exception:
188
+ logger.warning("Audit episode save failed", exc_info=True)
189
+ submit_episode_save(_persist, name="audit-episode-save")
190
+
180
191
  return result.quality_assessment, result.recommendation
@@ -625,10 +625,17 @@ class ScopeResolver(dspy.Module):
625
625
  # Extract package name from manifest
626
626
  package_name = extract_package_name(manifest_path, repo_path)
627
627
 
628
- # Build deterministic description
629
- description = (
630
- f"{pkg_mgr} package at {subroot}" if subroot != "." else f"{pkg_mgr} package (root)"
631
- )
628
+ # Build deterministic description with package name and scope type
629
+ parts = []
630
+ if package_name:
631
+ parts.append(package_name)
632
+ parts.append(f"{scope_type.value}")
633
+ if subroot != ".":
634
+ parts.append(f"at {subroot}")
635
+ else:
636
+ parts.append("(repository root)")
637
+ parts.append(f"managed by {pkg_mgr}")
638
+ description = " — ".join([parts[0], " ".join(parts[1:])]) if len(parts) > 1 else " ".join(parts)
632
639
 
633
640
  scopes[subroot] = ScopeResult(
634
641
  repo=repo,
@@ -658,7 +665,9 @@ class ScopeResolver(dspy.Module):
658
665
  indicator_type, indicator_path = self._find_scope_indicator(file.filename)
659
666
  if indicator_path and indicator_type and indicator_path not in scopes:
660
667
  # Build deterministic description for indicator-based scope
661
- description = f"{indicator_type.value} scope at {indicator_path}"
668
+ parts = [f"{indicator_type.value}", "scope"]
669
+ parts.append(f"at {indicator_path}")
670
+ description = " ".join(parts)
662
671
  scopes[indicator_path] = ScopeResult(
663
672
  repo=repo,
664
673
  subroot=indicator_path,
@@ -978,6 +987,33 @@ class ScopeResolver(dspy.Module):
978
987
 
979
988
  return [s for s in final_boundaries.values() if s.changed_files]
980
989
 
990
+ def _finalize_scopes(
991
+ self,
992
+ scopes: list[ScopeResult],
993
+ review_context: ReviewContext,
994
+ ) -> list[ScopeResult]:
995
+ """Finalize scopes by building topics and attaching hierarchical skills.
996
+
997
+ This helper is called both by the fast-path (deterministic resolution only)
998
+ and by _refine_scopes after LLM refinement. It does NOT handle Hippocampus
999
+ binding or episode persistence — those are handled separately.
1000
+
1001
+ Args:
1002
+ scopes: List of ScopeResult to finalize
1003
+ review_context: Review context with PR identity and metadata
1004
+
1005
+ Returns:
1006
+ Finalized list of ScopeResult with skills attached
1007
+ """
1008
+ pr = review_context.metadata.pr
1009
+ repo_path = review_context.metadata.repo_path
1010
+
1011
+ # Attach hierarchical skills to each scope
1012
+ for scope in scopes:
1013
+ scope.skills = collect_skills(repo_path, scope.subroot)
1014
+
1015
+ return scopes
1016
+
981
1017
  async def _refine_scopes(
982
1018
  self,
983
1019
  scopes: list[ScopeResult],
@@ -1120,9 +1156,8 @@ class ScopeResolver(dspy.Module):
1120
1156
  # Add PR URL topic (provides description; not a scope)
1121
1157
  pr_ctx = review_context.pr_context
1122
1158
  scope_topics.append(pr_ctx.to_topic())
1123
- # Attach hierarchical skills to each produced scope
1124
- for scope in final_scopes:
1125
- scope.skills = collect_skills(repo_path, scope.subroot)
1159
+ # Finalize scopes with hierarchical skills
1160
+ self._finalize_scopes(final_scopes, review_context)
1126
1161
  # Bind topics to hippocampus cmem for episode persistence
1127
1162
  if hippo is not None and stamp_topic_ids:
1128
1163
  stamp_topic_ids.append(pr_ctx.pr_url)
@@ -1204,7 +1239,20 @@ class ScopeResolver(dspy.Module):
1204
1239
  )
1205
1240
  if orphans:
1206
1241
  logger.info("Deterministic identification produced %d orphan(s)", len(orphans))
1207
- scopes = await self._refine_scopes(scopes, orphans, review_context)
1242
+
1243
+ # Fast-path: skip LLM refinement when clean single-scope resolution
1244
+ if (
1245
+ len(scopes) == 1
1246
+ and not orphans
1247
+ and self._settings.get_scope_skip_refinement()
1248
+ ):
1249
+ logger.info(
1250
+ "Deterministic resolution sufficient (1 scope, 0 orphans); "
1251
+ "skipping LLM refinement"
1252
+ )
1253
+ scopes = self._finalize_scopes(scopes, review_context)
1254
+ else:
1255
+ scopes = await self._refine_scopes(scopes, orphans, review_context)
1208
1256
  # Log final scopes for visibility
1209
1257
  scope_summary = "\n".join(
1210
1258
  f" - {s.subroot} ({s.scope_type.value}): {len(s.changed_files)} files"
@@ -281,6 +281,18 @@ class Settings(BaseSettings):
281
281
  compact_trajectory=self.memory.compact_trajectory,
282
282
  )
283
283
 
284
+ def get_scope_skip_refinement(self) -> bool:
285
+ """Whether to skip LLM refinement when deterministic resolution is clean.
286
+
287
+ Returns True (skip) when:
288
+ - There's 1 scope and 0 orphans (single scope, all files assigned)
289
+ - The setting skip_refinement_when_clean is True or not explicitly set
290
+
291
+ Per-signature ``skip_refinement_when_clean`` overrides default (True).
292
+ """
293
+ val = self.get_signature_config("scope").skip_refinement_when_clean
294
+ return val if val is not None else True # default: skip
295
+
284
296
  def get_rlm_threshold(self, module_type: str) -> float:
285
297
  """Resolve RLM fallback threshold for a module type.
286
298
 
@@ -51,6 +51,7 @@ class SignatureConfig(BaseModel):
51
51
  temperature: float | None = None
52
52
  max_tokens: int | None = None # Output token budget (reasoning tokens included)
53
53
  scan_unchanged: bool | None = None # For supply_chain: scan unmodified artifacts/manifests
54
+ skip_refinement_when_clean: bool | None = None # For scope: skip LLM refinement when 1 scope, 0 orphans
54
55
 
55
56
  memory: MemorySignatureConfig = Field(default_factory=MemorySignatureConfig)
56
57
 
@@ -249,10 +249,11 @@ class ReviewPipeline(dspy.Module):
249
249
  scopes=scopes,
250
250
  topics=all_scope_topics,
251
251
  )
252
- # Collect per-signature statistics
253
- signature_stats_list = self._collect_signature_stats()
254
- # Ensure all background episode saves complete before returning
252
+ # Ensure all background episode saves complete before stats collection
253
+ # (audit's episode save runs in background and contains distiller/cartographer calls)
255
254
  join_episode_saves()
255
+ # Collect per-signature statistics (after all saves complete)
256
+ signature_stats_list = self._collect_signature_stats()
256
257
  return ReviewResult(
257
258
  pr_number=pr.number,
258
259
  pr_title=pr.title,
File without changes
File without changes