diff --git a/src/timmy/thinking.py b/src/timmy/thinking.py index 9a872653..e71ba501 100644 --- a/src/timmy/thinking.py +++ b/src/timmy/thinking.py @@ -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"(? 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]) diff --git a/tests/timmy/test_thinking.py b/tests/timmy/test_thinking.py index 28b25b8a..70c8449d 100644 --- a/tests/timmy/test_thinking.py +++ b/tests/timmy/test_thinking.py @@ -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