From d796fe7c53563ef44c41e5fcf8f2dfb25155f4f1 Mon Sep 17 00:00:00 2001 From: "Claude (Opus 4.6)" Date: Mon, 23 Mar 2026 20:47:06 +0000 Subject: [PATCH] [claude] Refactor thinking.py::_maybe_file_issues() into focused helpers (#1170) (#1173) Co-authored-by: Claude (Opus 4.6) Co-committed-by: Claude (Opus 4.6) --- src/timmy/thinking.py | 167 ++++++++++++++++++++++++------------------ 1 file changed, 94 insertions(+), 73 deletions(-) diff --git a/src/timmy/thinking.py b/src/timmy/thinking.py index 6bab20c..a018fd8 100644 --- a/src/timmy/thinking.py +++ b/src/timmy/thinking.py @@ -692,91 +692,112 @@ class ThinkingEngine: file paths actually exist on disk, preventing phantom-bug reports. """ try: - interval = settings.thinking_issue_every - if interval <= 0: + recent = self._get_recent_thoughts_for_issues() + if recent is None: return - count = self.count_thoughts() - if count == 0 or count % interval != 0: - return - - # Check Gitea availability before spending LLM tokens - if not settings.gitea_enabled or not settings.gitea_token: - return - - recent = self.get_recent_thoughts(limit=interval) - if len(recent) < interval: - return - - thought_text = "\n".join(f"- [{t.seed_type}] {t.content}" for t in reversed(recent)) - - classify_prompt = ( - "You are reviewing your own recent thoughts for actionable items.\n" - "Extract 0-2 items that are CONCRETE bugs, broken features, stale " - "state, or clear improvement opportunities in your own codebase.\n\n" - "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" - "- 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' - " **What's happening:** Describe the current (broken) behavior.\n" - " **Expected behavior:** What should happen instead.\n" - " **Suggested fix:** Which file(s) to change and what the fix looks like.\n" - " **Acceptance criteria:** How to verify the fix works.\n" - '- "category": One of bug, feature, suggestion, maintenance\n\n' - "Return ONLY a JSON array of objects with keys: " - '"title", "body", "category"\n' - "Return [] if nothing is actionable.\n\n" - f"Recent thoughts:\n{thought_text}\n\nJSON array:" - ) - + classify_prompt = self._build_issue_classify_prompt(recent) raw = await self._call_agent(classify_prompt) - if not raw or not raw.strip(): - return - - import json - - # Strip markdown code fences if present - cleaned = raw.strip() - if cleaned.startswith("```"): - cleaned = cleaned.split("\n", 1)[-1].rsplit("```", 1)[0].strip() - - items = json.loads(cleaned) - if not isinstance(items, list) or not items: + items = self._parse_issue_items(raw) + if items is None: return from timmy.mcp_tools import create_gitea_issue_via_mcp for item in items[:2]: # Safety cap - if not isinstance(item, dict): - continue - title = item.get("title", "").strip() - body = item.get("body", "").strip() - category = item.get("category", "suggestion").strip() - 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]) + await self._file_single_issue(item, create_gitea_issue_via_mcp) except Exception as exc: logger.debug("Thought issue filing skipped: %s", exc) + def _get_recent_thoughts_for_issues(self): + """Return recent thoughts if conditions for filing issues are met, else None.""" + interval = settings.thinking_issue_every + if interval <= 0: + return None + + count = self.count_thoughts() + if count == 0 or count % interval != 0: + return None + + if not settings.gitea_enabled or not settings.gitea_token: + return None + + recent = self.get_recent_thoughts(limit=interval) + if len(recent) < interval: + return None + + return recent + + @staticmethod + def _build_issue_classify_prompt(recent) -> str: + """Build the LLM prompt that extracts actionable issues from recent thoughts.""" + thought_text = "\n".join(f"- [{t.seed_type}] {t.content}" for t in reversed(recent)) + return ( + "You are reviewing your own recent thoughts for actionable items.\n" + "Extract 0-2 items that are CONCRETE bugs, broken features, stale " + "state, or clear improvement opportunities in your own codebase.\n\n" + "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" + "- 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' + " **What's happening:** Describe the current (broken) behavior.\n" + " **Expected behavior:** What should happen instead.\n" + " **Suggested fix:** Which file(s) to change and what the fix looks like.\n" + " **Acceptance criteria:** How to verify the fix works.\n" + '- "category": One of bug, feature, suggestion, maintenance\n\n' + "Return ONLY a JSON array of objects with keys: " + '"title", "body", "category"\n' + "Return [] if nothing is actionable.\n\n" + f"Recent thoughts:\n{thought_text}\n\nJSON array:" + ) + + @staticmethod + def _parse_issue_items(raw: str): + """Strip markdown fences and parse JSON issue list; return None on failure.""" + import json + + if not raw or not raw.strip(): + return None + + cleaned = raw.strip() + if cleaned.startswith("```"): + cleaned = cleaned.split("\n", 1)[-1].rsplit("```", 1)[0].strip() + + items = json.loads(cleaned) + if not isinstance(items, list) or not items: + return None + + return items + + async def _file_single_issue(self, item: dict, create_fn) -> None: + """Validate one issue dict and create it via *create_fn* if it passes checks.""" + if not isinstance(item, dict): + return + title = item.get("title", "").strip() + body = item.get("body", "").strip() + category = item.get("category", "suggestion").strip() + if not title or len(title) < 10: + return + + combined = f"{title}\n{body}" + if not self._references_real_files(combined): + logger.info( + "Skipped phantom issue: %s (references non-existent files)", + title[:60], + ) + return + + label = category if category in ("bug", "feature") else "" + result = await create_fn(title=title, body=body, labels=label) + logger.info("Thought→Issue: %s → %s", title[:60], result[:80]) + # ── System snapshot helpers ──────────────────────────────────────────── def _snap_thought_count(self, now: datetime) -> str | None: