Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .github/workflows/sync-subtrees.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Synchronize Remote Repository Context

on:
schedule:
# Run daily at 00:00 UTC
- cron: '0 0 * * *'
workflow_dispatch:
Comment thread
coderabbitai[bot] marked this conversation as resolved.

concurrency:
group: sync-subtrees
cancel-in-progress: false

permissions:
contents: write

jobs:
sync-context:
name: Sync repository context
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: '3.11'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install httpx==0.28.1

- name: Run subtree context sync script
run: |
python scripts/update_subtrees.py
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- name: Configure git remote authentication
env:
GH_TOKEN: ${{ github.token }}
run: |
git config --local http.https://github.com/.extraheader "AUTHORIZATION: basic $(echo -n x-access-token:${GH_TOKEN} | base64)"

- name: Commit and push updated context files
uses: stefanzweifel/git-auto-commit-action@b863ae1933cb653a53c021fe36dbb774e1fb9403 # v5.2.0
with:
commit_message: "chore(context): auto-sync client repo .agent context files"
file_pattern: "repos/**"
20 changes: 0 additions & 20 deletions context.md

This file was deleted.

78 changes: 78 additions & 0 deletions context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import logging
from pathlib import Path

logger = logging.getLogger("pr-dashboard.context")

MAX_FILE_CHARS = 20_000
MAX_TOTAL_CHARS = 120_000


def get_repo_dir(repo_name: str) -> Path | None:
"""Find repository context directory inside repos/ or workspace."""
bot_root = Path(__file__).resolve().parent
candidates = [
bot_root / "repos" / repo_name,
bot_root / repo_name,
bot_root.parent / repo_name,
]
for c in candidates:
if c.exists() and c.is_dir():
return c
return None


def load_full_repo_context(repo_name: str) -> str:
"""Dynamically scan and load ALL markdown files inside repos/<repo_name>/ without hardcoding file lists."""
repo_dir = get_repo_dir(repo_name)
if not repo_dir:
logger.warning(f"Repository directory for '{repo_name}' not found.")
return f"=== REPOSITORY: {repo_name} ==="

context_parts = [f"=== REPOSITORY: {repo_name} ==="]
loaded_files = 0
skipped_files = 0
total_chars = 0

# Recursively scan for all .md files inside the target repository directory
for md_file in sorted(repo_dir.rglob("*.md")):
# Skip internal cache or git folders
if any(part.startswith(".") and part not in [".agent"] for part in md_file.parts):
continue

if total_chars >= MAX_TOTAL_CHARS:
skipped_files += 1
continue

rel_path = md_file.relative_to(repo_dir)
try:
content = md_file.read_text(encoding="utf-8").strip()
except Exception as e:
logger.error(f"Error reading context file {md_file}: {e}")
continue

if not content:
continue

truncated = len(content) > MAX_FILE_CHARS
if truncated:
content = content[:MAX_FILE_CHARS]

remaining = MAX_TOTAL_CHARS - total_chars
if len(content) > remaining:
content = content[:remaining]
truncated = True

suffix = "\n... [truncated]" if truncated else ""
section = f"--- {rel_path} ---\n{content}{suffix}"
context_parts.append(section)
total_chars += len(section)
loaded_files += 1
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if skipped_files:
logger.warning(
f"Context budget ({MAX_TOTAL_CHARS} chars) reached for '{repo_name}'; "
f"skipped {skipped_files} additional markdown file(s)."
)

logger.info(f"Loaded {loaded_files} markdown context files for '{repo_name}' ({total_chars} chars)")
return "\n\n".join(context_parts)
23 changes: 7 additions & 16 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
main.py — entry point

Flow:
1. Load context.md (repo context — what MiniChain is, what's already built)
1. Load complete target repo context from repos/<repo_name>/ via context.py
2. Fetch all PRs + extract CodeRabbit walkthrough + changes only
3. One combined Ollama call -> groups PRs by problem
4. Deep Ollama analysis per conflict group (all PRs in group together)
Expand All @@ -11,29 +11,19 @@

Run: python main.py
Requires: gh (authenticated), ollama running on localhost:11434
Optional: context.md in same folder (drop it in when ready)
Context Sync: python scripts/update_subtrees.py (syncs target repo context into repos/<repo_name>/)
"""

import os, time, webbrowser
from github import fetch_prs, fetch_pr_files, fetch_coderabbit_sections, check_gh_auth, REPO
from ollama import check_ollama
from grouping import resolve_groups
from render import build_conflict_html, build_isolated_html
from context import load_full_repo_context

OUT_DIR = os.path.dirname(os.path.abspath(__file__))
CONTEXT_FILE = os.path.join(OUT_DIR, "context.md")
OUT_DIR = os.path.dirname(os.path.abspath(__file__))


def load_context():
if os.path.exists(CONTEXT_FILE):
with open(CONTEXT_FILE, "r", encoding="utf-8") as f:
content = f.read().strip()
print(f" Loaded context.md ({len(content)} chars)")
return content
print(" No context.md found — running without repo context")
print(" (Drop context.md in the same folder to enable it)")
return ""


def _build_fallback_pr(raw):
"""Build a minimal PR dict when fetching fails — keeps the pipeline alive."""
Expand Down Expand Up @@ -99,8 +89,9 @@ def main():
print("ERROR: Ollama not reachable at localhost:11434")
return

print(f"\nLoading repo context...")
repo_context = load_context()
target_repo_name = REPO.split("/")[-1]
print(f"\nLoading full repo context for '{target_repo_name}'...")
repo_context = load_full_repo_context(target_repo_name)

print(f"\nFetching PRs for {REPO}...")
raw_prs = fetch_prs()
Expand Down
14 changes: 14 additions & 0 deletions repo_metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
REPO_METADATA = {
"SocialShareButton": {
"url": "https://github.com/kpj2006/SocialShareButton/tree/matt-skills",
},
"OrgExplorer": {
"url": "https://github.com/kpj2006/OrgExplorer/tree/skills",
},
"GSoC-Proposal-Assistant": {
"url": "https://github.com/kpj2006/GSoC-Proposal-Assistant",
},
"Template-Repo": {
"url": "https://github.com/AOSSIE-Org/Template-Repo",
},
}
110 changes: 110 additions & 0 deletions scripts/update_subtrees.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import os
import re
import sys
import logging
import httpx
from pathlib import Path

# Add parent directory to sys.path to import repo_metadata
script_dir = Path(__file__).resolve().parent
bot_root = script_dir.parent
sys.path.insert(0, str(bot_root))

from repo_metadata import REPO_METADATA

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("subtree-sync")

# Key context files to sync from remote client repositories
KNOWN_CONTEXT_FILES = [
".agent/info/operational-data.md",
".agent/core/architecture.md",
".agent/core/code-mapping.md",
".agent/core/edge-cases.md",
".agent/core/examples.md",
".agent/instructions/setup.md",
".agent/instructions/testing.md",
".agent/instructions/deployment.md",
".agent/instructions/ci-cd.md",
".agent/instructions/format-guide.md",
".agent/instructions/checklist.md",
".agent/instructions/bad-patterns.md",
"references/checklist.md",
"references/format-guide.md",
"references/bad-patterns.md",
"SKILL.md",
"AGENTS.md",
"README.md",
]


def parse_github_url(url: str) -> tuple[str, str, str]:
"""Parse owner, repo, and ref from GitHub URL."""
match = re.match(r"https://github\.com/([^/]+)/([^/]+)(?:/tree/([^/]+))?", url)
if match:
owner, repo, ref = match.groups()
repo = repo.removesuffix(".git")
ref = ref or "main"
return owner, repo, ref
return "", "", "main"


def sync_repo_context(repo_name: str, meta: dict, client: httpx.Client):
"""Fetch specified .agent context files directly from raw GitHub endpoint."""
url = meta.get("url")
if not url:
logger.warning(f"No URL defined for {repo_name}, skipping.")
return

owner, repo, ref = parse_github_url(url)
if not owner or not repo:
logger.error(f"Invalid GitHub URL for {repo_name}: {url}")
return

logger.info(f"Syncing context for '{repo_name}' ({owner}/{repo}@{ref})...")
target_dir = bot_root / "repos" / repo_name
target_dir.mkdir(parents=True, exist_ok=True)

headers = {"User-Agent": "PullRequestDashboard-Context-Sync"}
downloaded_count = 0
removed_count = 0

for rel_file in KNOWN_CONTEXT_FILES:
raw_url = f"https://raw.githubusercontent.com/{owner}/{repo}/{ref}/{rel_file}"
dest_path = target_dir / rel_file
try:
res = client.get(raw_url, headers=headers)
if res.status_code == 200:
dest_path.parent.mkdir(parents=True, exist_ok=True)
dest_path.write_bytes(res.content)
logger.info(f"Downloaded: repos/{repo_name}/{rel_file}")
downloaded_count += 1
elif res.status_code == 404 and dest_path.exists():
# Confirmed absent upstream: remove the stale local copy so it
# doesn't keep getting fed into the dashboard prompt.
dest_path.unlink()
logger.info(f"Removed stale file (deleted upstream): repos/{repo_name}/{rel_file}")
removed_count += 1
# Any other status (rate limit, server error, etc.) is not a
# confirmed absence — leave the existing local copy untouched.
except Exception as e:
logger.debug(f"Failed downloading {rel_file} for {repo_name}: {e}")

logger.info(
f"Synced {downloaded_count} context files into repos/{repo_name} "
f"({removed_count} stale file(s) removed)"
)


def sync_all_subtrees():
os.chdir(bot_root)
repos_dir = bot_root / "repos"
repos_dir.mkdir(exist_ok=True)

with httpx.Client(timeout=20.0, follow_redirects=True) as client:
for repo_name, meta in REPO_METADATA.items():
sync_repo_context(repo_name, meta, client)


if __name__ == "__main__":
sync_all_subtrees()
Loading