This commit was merged in pull request #329.
This commit is contained in:
@@ -628,6 +628,35 @@ class ThinkingEngine:
|
||||
except Exception as exc:
|
||||
logger.warning("Memory status check failed: %s", exc)
|
||||
|
||||
@staticmethod
|
||||
def _references_real_files(text: str) -> bool:
|
||||
"""Check that all source-file paths mentioned in *text* actually exist.
|
||||
|
||||
Extracts paths that look like Python/config source references
|
||||
(e.g. ``src/timmy/session.py``, ``config/foo.yaml``) and verifies
|
||||
each one on disk relative to the project root. Returns ``True``
|
||||
only when **every** referenced path resolves to a real file — or
|
||||
when no paths are referenced at all (pure prose is fine).
|
||||
"""
|
||||
# Match paths like src/thing.py swarm/init.py config/x.yaml
|
||||
# Requires at least one slash and a file extension.
|
||||
path_pattern = re.compile(
|
||||
r"(?<![/\w])" # not preceded by path chars (avoid partial matches)
|
||||
r"((?:src|tests|config|scripts|data|swarm|timmy)"
|
||||
r"(?:/[\w./-]+\.(?:py|yaml|yml|json|toml|md|txt|cfg|ini)))"
|
||||
)
|
||||
paths = path_pattern.findall(text)
|
||||
if not paths:
|
||||
return True # No file refs → nothing to validate
|
||||
|
||||
# Project root: two levels up from this file (src/timmy/thinking.py)
|
||||
project_root = Path(__file__).resolve().parent.parent.parent
|
||||
for p in paths:
|
||||
if not (project_root / p).is_file():
|
||||
logger.info("Phantom file reference blocked: %s (not in %s)", p, project_root)
|
||||
return False
|
||||
return True
|
||||
|
||||
async def _maybe_file_issues(self) -> None:
|
||||
"""Every N thoughts, classify recent thoughts and file Gitea issues.
|
||||
|
||||
@@ -639,6 +668,9 @@ class ThinkingEngine:
|
||||
- Gitea is enabled and configured
|
||||
- Thought count is divisible by thinking_issue_every
|
||||
- LLM extracts at least one actionable item
|
||||
|
||||
Safety: every generated issue is validated to ensure referenced
|
||||
file paths actually exist on disk, preventing phantom-bug reports.
|
||||
"""
|
||||
try:
|
||||
interval = settings.thinking_issue_every
|
||||
@@ -666,7 +698,10 @@ class ThinkingEngine:
|
||||
"Rules:\n"
|
||||
"- Only include things that could become a real code fix or feature\n"
|
||||
"- Skip vague reflections, philosophical musings, or repeated themes\n"
|
||||
"- Category must be one of: bug, feature, suggestion, maintenance\n\n"
|
||||
"- Category must be one of: bug, feature, suggestion, maintenance\n"
|
||||
"- ONLY reference files that you are CERTAIN exist in the project\n"
|
||||
"- Do NOT invent or guess file paths — if unsure, describe the "
|
||||
"area of concern without naming specific files\n\n"
|
||||
"For each item, write an ENGINEER-QUALITY issue:\n"
|
||||
'- "title": A clear, specific title (e.g. "[Memory] MEMORY.md timestamp not updating")\n'
|
||||
'- "body": A detailed body with these sections:\n'
|
||||
@@ -707,6 +742,15 @@ class ThinkingEngine:
|
||||
if not title or len(title) < 10:
|
||||
continue
|
||||
|
||||
# Validate all referenced file paths exist on disk
|
||||
combined = f"{title}\n{body}"
|
||||
if not self._references_real_files(combined):
|
||||
logger.info(
|
||||
"Skipped phantom issue: %s (references non-existent files)",
|
||||
title[:60],
|
||||
)
|
||||
continue
|
||||
|
||||
label = category if category in ("bug", "feature") else ""
|
||||
result = await create_gitea_issue_via_mcp(title=title, body=body, labels=label)
|
||||
logger.info("Thought→Issue: %s → %s", title[:60], result[:80])
|
||||
|
||||
@@ -1140,3 +1140,51 @@ def test_maybe_check_memory_graceful_on_error(tmp_path):
|
||||
mock_settings.thinking_memory_check_every = 50
|
||||
# Should not raise
|
||||
engine._maybe_check_memory()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phantom file validation (_references_real_files)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_references_real_files_passes_existing_file(tmp_path):
|
||||
"""Existing source files should pass validation."""
|
||||
from timmy.thinking import ThinkingEngine
|
||||
|
||||
# src/timmy/thinking.py definitely exists in the project
|
||||
text = "The bug is in src/timmy/thinking.py where the loop crashes."
|
||||
assert ThinkingEngine._references_real_files(text) is True
|
||||
|
||||
|
||||
def test_references_real_files_blocks_phantom_file(tmp_path):
|
||||
"""Non-existent files should be blocked."""
|
||||
from timmy.thinking import ThinkingEngine
|
||||
|
||||
# A completely fabricated module path
|
||||
text = "The bug is in src/timmy/quantum_brain.py where sessions aren't tracked."
|
||||
assert ThinkingEngine._references_real_files(text) is False
|
||||
|
||||
|
||||
def test_references_real_files_blocks_phantom_swarm(tmp_path):
|
||||
"""Non-existent swarm files should be blocked."""
|
||||
from timmy.thinking import ThinkingEngine
|
||||
|
||||
text = "swarm/initialization.py needs to be fixed for proper startup."
|
||||
assert ThinkingEngine._references_real_files(text) is False
|
||||
|
||||
|
||||
def test_references_real_files_allows_no_paths(tmp_path):
|
||||
"""Text with no file references should pass (pure prose is fine)."""
|
||||
from timmy.thinking import ThinkingEngine
|
||||
|
||||
text = "The memory system should persist across restarts."
|
||||
assert ThinkingEngine._references_real_files(text) is True
|
||||
|
||||
|
||||
def test_references_real_files_blocks_mixed(tmp_path):
|
||||
"""If any referenced file is phantom, the whole text fails."""
|
||||
from timmy.thinking import ThinkingEngine
|
||||
|
||||
# Mix of real and fake files — should fail because of the fake one
|
||||
text = "Fix src/timmy/thinking.py and also src/timmy/nonexistent_module.py for the memory leak."
|
||||
assert ThinkingEngine._references_real_files(text) is False
|
||||
|
||||
Reference in New Issue
Block a user