2 Commits

Author SHA1 Message Date
5e4c03d0cd Case sync: refresh remote URL with current token before each push
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m28s
Cases failed to push silently after the Gitea token in Infisical was
rotated: the embedded credential in each case repo's origin URL was
the old token, the rotation never propagated, and capture_output=True
hid the auth failure as a logger.warning. Three cases (1033-25,
1130-25, 1194-25) accumulated unpushed commits over weeks before
this was noticed.

Fixes the root cause in two places: web/gitea_client.py for uploads
through the FastAPI endpoint, and mcp-server/services/git_sync.py
for case_update / document_upload through MCP tools (which previously
committed but never pushed at all).

The new commit_and_push helper:
- re-injects the current GITEA_ACCESS_TOKEN into the existing origin
  URL on every call, so pushes survive token rotation
- logs push failures at WARNING with the actual stderr (the previous
  code suppressed errors entirely)
- continues to push even when the commit was a no-op, in case earlier
  commits are still unpushed

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 17:14:57 +00:00
6b5d6586dc Agents: voice docs awareness for qa/researcher/analyst/ceo
Until now only legal-writer referenced the voice corpus. Without these
references the qa agent can't validate writer output, the researcher
chooses precedents outside Daphna's canon, and the analyst's claims
classification doesn't match block-zayin rules.

- legal-qa: adds 8th check "voice_compliance" — block ז structure,
  block י voice (אכן/אולם, "אנחנו" verbs, no numbered lists), correct
  precedent from canon, acceptance template match.
- legal-researcher: must check daphna-precedent-network.md before
  proposing any precedent; cross-reference with Daphna's own past
  decisions via search_decisions.
- legal-analyst: reads block-zayin-claims.md — its output is the
  writer's input for block ז.
- legal-ceo: lists all 6 voice docs and which agent reads each.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 17:14:44 +00:00
9 changed files with 266 additions and 64 deletions

View File

@@ -30,7 +30,9 @@ tools:
1. **`docs/decision-methodology.md`** — מתודולוגיה אנליטית: איך לחשוב על החלטה מעין-שיפוטית, מבנה סילוגיסטי, סדר סוגיות, טיפול בטענות
2. **`docs/block-schema.md`** — ארכיטקטורת 12 בלוקים
3. **`docs/legal-decision-lessons.md`** — לקחים מהחלטות קודמות
3. **`docs/daphna-block-zayin-claims.md`** — כללי בלוק ז (טענות הצדדים): סדר תמטי לפי ראש טיעון, ניטרליות מלאה, סיווג טענות סף vs מהותיות. **הניתוח שלך הוא הקלט לבלוק ז של ה-writer — אם תסווג שגוי או תפספס טענה, זה ייכשל גם בבלוק ז וגם בבלוק י.**
4. **`docs/daphna-precedent-network.md`** — לכל סוגיה משפטית, איזה תקדם מועדף של דפנה. שימושי כשעורר/משיב מסתמך על תקדם — לדעת אם זה תקדם בקאנון.
5. **`docs/legal-decision-lessons.md`** — לקחים מהחלטות קודמות
## שפה

View File

@@ -48,10 +48,24 @@ tools:
| מסמך | תוכן | מתי לקרוא |
|------|-------|-----------|
| `docs/daphna-decision-tree.md` | **כלי הפעולה היומיומי** — עץ החלטה: מהי הראיה הניצחת? איזו תבנית? איזה אורך? | **לפני כל החלטה** |
| `docs/decision-methodology.md` | מתודולוגיה אנליטית — סילוגיזמים, סדר סוגיות, איזון | **לפני כל החלטה** |
| `docs/block-schema.md` | הגדרת 12 בלוקים — content model, constraints | **לפני כל החלטה** |
| `docs/legal-decision-lessons.md` | לקחים מ-3 החלטות — מה עבד, מה השתנה | **לפני כל החלטה** |
### מסמכי הקול של דפנה (להפנייה לסוכנים)
הסוכנים שלך (writer, qa, researcher, analyst) קוראים את מסמכי הקול בעצמם. **התפקיד שלך**: לוודא שהם **קוראים** אותם, ולנתב את הסוכן הנכון לפי סוג התיק.
| מסמך | תפקיד | סוכן רלוונטי |
|------|--------|---------------|
| `docs/daphna-voice-fingerprint.md` | קבועי הקול | writer + qa |
| `docs/daphna-precedent-network.md` | קאנון תקדמים | researcher + writer + qa |
| `docs/daphna-architecture-by-outcome.md` | מבנה בלוק י לפי תוצאה | writer + qa |
| `docs/daphna-acceptance-architecture.md` | 5 תבניות קבלה | writer + qa (אם תוצאה = קבלה) |
| `docs/daphna-block-zayin-claims.md` | כללי בלוק ז | analyst + writer + qa |
| `docs/voice-1130-25.md` | דוגמה עמוקה | writer (אם תיק 1xxx מורכב) |
## הסוכנים שלך
| סוכן | Agent ID | תפקיד |

View File

@@ -32,7 +32,18 @@ tools:
אם issue מכוון לתיק שלא בטווח שלך — סרב ודווח ב-comment.
## 6 בדיקות
## לפני שאתה מתחיל — קרא את מסמכי הקול
בלי קריאת מסמכי הקול, אינך יכול לבדוק שה-writer עקב אחר הסגנון של דפנה.
1. **`docs/daphna-decision-tree.md`** — תקציר תפעולי. ממנו תגיע למסמכים הספציפיים לפי שאלה.
2. **`docs/daphna-voice-fingerprint.md`** — קבועי הקול (פעלי "אנחנו", אנטי-דפוסים, ביטויי קישור)
3. **`docs/daphna-architecture-by-outcome.md`** — מבנה בלוק י לפי תוצאה
4. **`docs/daphna-acceptance-architecture.md`** — חמש תבניות קבלה. **חובה אם התיק קבלה (לא חלקית)**
5. **`docs/daphna-block-zayin-claims.md`** — כללי בלוק ז (טענות הצדדים)
6. **`docs/daphna-precedent-network.md`** — לכל סוגיה משפטית, איזה תקדם דפנה מצטטת
## 7 בדיקות
### 1. שלמות מבנית (structural_integrity)
- כל בלוקי חובה קיימים (ה עד יא)
@@ -74,6 +85,44 @@ tools:
- אין "נוסחאות ריקות" (משפטים שמחיקתם לא משנה כלום)?
- ציטוטים עטופים בסנדוויץ' (הקדמה → ציטוט → ניתוח)?
### 8. עמידה בקול דפנה (voice_compliance)
מבוסס על 6 מסמכי הקול. בדוק:
#### בלוק ז (מ-`daphna-block-zayin-claims.md`)
- כותרת **"תמצית טענות הצדדים"** (לא "טענות הצדדים")?
- כל צד מקבל כותרת משנה (טענות העוררים / תגובת הוועדה / תגובת מבקשי ההיתר)?
- אין רשימה ממוספרת `(1)... (2)...` בתוך פסקה?
- אין מילות הערכה ("בצדק", "בטעות", "משכנעת")?
- אין גילוי מסקנה עתידית ("טענה זו תידחה בהמשך")?
- אין ציטוטי פסיקה ארוכים — רק שם + הפניה?
- קול פעיל ("העורר טוען") ולא פסיביזציה ("טענות העורר היו")?
#### בלוק י (מ-`daphna-voice-fingerprint.md` + `daphna-architecture-by-outcome.md`)
- כותרת בלוק י = **"דיון והכרעה"** (קבוע)?
- קול "אנחנו" פעיל — אין "הוועדה מוצאת" אלא "מצאנו"?
- כל פועל "אנחנו" נושא תפקיד — אין "נחדד" כפתיחת פסקה אקראית?
- דפוס "אכן... אולם" לטענות שנדחות (לא דחייה במשפט אחד)?
- אין רשימה ממוספרת באנליזה?
- אין מספור פסקאות סדרתי (1., 2., 3.) — מגמה ישנה שנטושה ב-2025+?
- כותרות משנה רק אם 3+ סוגיות מובחנות (לא בתיק עם סוגיה אחת)?
- ציטוטי פסיקה במלואם (4-15 שורות), לא תמציות?
- אם תיק 1xxx מורכב — מסגור פילוסופי בפתיחה?
- אם תיק 8xxx עם הכרעה שמאית — ציטוט בר"מ 3644/13 קיים?
- "למעלה מן הצורך" לטיעונים מרכזיים?
- אין רטוריקה דרמטית של הצדדים בקול ההכרעה?
- אין תוצאה הכל-או-לא-כלום בתיק עם טענות מהותיות משני הצדדים?
#### תקדמים (מ-`daphna-precedent-network.md`)
- לכל סוגיה משפטית — האם נבחר התקדים המועדף של דפנה?
- האם יש תקדים אישי שלה רלוונטי? אם כן — האם הופנה אליו (חיסכון / דחייה / הבחנה)?
#### תבנית קבלה (מ-`daphna-acceptance-architecture.md` — אם תוצאה = קבלה)
- האם הסיבה לקבלה ברורה: פגם פנימי / החזרה / תיקונים / 8xxx מהותית / שומה?
- האם התבנית הנבחרת (A/B/C/D/E) מתאימה לסיבה?
- האם פורמט הסיום נכון לתבנית? (תבנית A: "מתבטלת"; B: "תיקבע לדיון" + הוראת הבהרה; C: "בכפוף לתיקונים"; D: "דרישת התשלום בטלה"; E: "השומה תושב לתיקון")
- בתבנית A: יש "הודאת צד נגדי" ו"השמטה רחבה"?
- בתבנית C: יש פסקת הכרה בוועדה ("פעלה נכון בקיום הדיון")?
## חומרה
| בדיקה | חומרה | משמעות |
@@ -85,6 +134,7 @@ tools:
| כפילות | warning | מדווח, לא חוסם |
| מספור | warning | מדווח, לא חוסם |
| מתודולוגיה | critical | חוסם ייצוא |
| **קול דפנה** | **critical** | **חוסם ייצוא** |
## תהליך עבודה

View File

@@ -37,8 +37,10 @@ tools:
## לפני שאתה מתחיל — קרא!
1. **מתודולוגיה אנליטית**: `docs/decision-methodology.md` — במיוחד סעיפים ד.2 (התחל מלשון הטקסט), ד.3 (שלושה מקורות להנחה עליונה), ז (ציטוטים ואזכורי פסיקה)
2. לקחים מהחלטות קודמות: `docs/legal-decision-lessons.md`
1. **רשת תקדמים של דפנה**: `docs/daphna-precedent-network.md`**קריאת חובה**. לכל סוגיה משפטית, יש לדפנה תקדם **מועדף** שהיא מצטטת באופן עקבי (אייזן/רוזן/שפר/הרמלין/חוף השרון/בר"מ 3644/13 גלר וכו'). אל תחפש תקדמים אקראיים — בדוק את הקאנון שלה תחילה.
2. **מתודולוגיה אנליטית**: `docs/decision-methodology.md` — במיוחד סעיפים ד.2 (התחל מלשון הטקסט), ד.3 (שלושה מקורות להנחה עליונה), ז (ציטוטים ואזכורי פסיקה)
3. **תקדמים אישיים של דפנה**: השתמש ב-`search_decisions` לפני שמציעים תקדם חיצוני. אם דפנה כבר הכריעה בסוגיה זהה — התקדם שלה הוא חלק מהקאנון.
4. לקחים מהחלטות קודמות: `docs/legal-decision-lessons.md`
## סוגי מסמכים שאתה מטפל בהם
@@ -69,8 +71,19 @@ tools:
- **רמת התקדים**: עליון / מנהלי / ועדת ערר ארצית / ועדת ערר מחוזית
- **הלכה מחייבת או אמרת אגב**
- **כיצד ישרת את מבנה ההנמקה**: כ"כלל" (הנחה עליונה), כ"הרחבה" (Explanation ב-CREAC), או כאנלוגיה
- **האם זה תקדם מהקאנון של דפנה?** (בדוק `docs/daphna-precedent-network.md` — אם כן, ציין שזה התקדם המועדף שלה לסוגיה)
4. הפק הפניות (`extract_references`)
### שלב 2ב: בדיקה מצטלבת מול הקאנון של דפנה
אחרי שאספת את הפסיקה הרלוונטית בתיק:
1. **לכל סוגיה משפטית** בתיק — בדוק ב-`daphna-precedent-network.md`:
- האם יש תקדם מועדף של דפנה לסוגיה?
- האם הוא הוצג בכתבי הטענות? אם לא — סמן כתקדם שיש להוסיף
2. **תקדמים אישיים**: `search_decisions` בקטגוריה זהה לתיק. אם דפנה כבר הכריעה בסוגיה דומה:
- אם תוצאה דומה: תקדם לחיסכון דוקטרינרי ("כפי שקבענו ב-X")
- אם תוצאה הפוכה: ציין כי **חובה** הבחנה (distinguishing)
3. **דווח** איזה תקדמים מהקאנון רלוונטיים, ואיזה תקדמים אישיים נמצאו
### שלב 3: מיפוי תכנית
1. קרא הוראות התכנית **במלואן** — לא רק את הסעיף הנטען
2. זהה סעיפים רלוונטיים למחלוקת

View File

@@ -0,0 +1,92 @@
"""Git sync helpers for case repos.
Each case lives in its own git repo with a Gitea remote. The remote URL
embeds an auth token (https://chaim:TOKEN@host/...). When the token is
rotated in Infisical, repos created with the old token will fail to
push silently — only logged at WARNING level. ``commit_and_push``
re-injects the *current* token into the existing origin URL on every
call, so push survives token rotation.
"""
from __future__ import annotations
import logging
import os
import subprocess
from pathlib import Path
logger = logging.getLogger(__name__)
def _gitea_token() -> str:
return os.environ.get("GITEA_ACCESS_TOKEN") or os.environ.get("GITEA_TOKEN", "")
def _git_env() -> dict:
return {
"GIT_AUTHOR_NAME": "Ezer Mishpati",
"GIT_AUTHOR_EMAIL": "legal@local",
"GIT_COMMITTER_NAME": "Ezer Mishpati",
"GIT_COMMITTER_EMAIL": "legal@local",
"PATH": os.environ.get("PATH", "/usr/bin:/bin"),
"GIT_TERMINAL_PROMPT": "0",
}
def _refresh_remote_url(case_dir: Path, env: dict) -> bool:
result = subprocess.run(
["git", "remote", "get-url", "origin"],
cwd=case_dir, capture_output=True, text=True,
)
if result.returncode != 0:
return False
current_url = result.stdout.strip()
if "@" in current_url and current_url.startswith("https://"):
bare_url = "https://" + current_url.split("@", 1)[1]
else:
bare_url = current_url
token = _gitea_token()
if not token:
return True # Push without auth — will fail, but caller decides what to do
auth_url = bare_url.replace("https://", f"https://chaim:{token}@")
if auth_url != current_url:
subprocess.run(
["git", "remote", "set-url", "origin", auth_url],
cwd=case_dir, capture_output=True, env=env,
)
return True
def commit_and_push(case_dir: str | Path, message: str) -> bool:
"""Stage, commit, refresh origin URL with current token, and push.
Best-effort: on failure logs at WARNING and returns False, but never
raises. Continues to push even if the commit was a no-op (in case
earlier commits are unpushed).
"""
case_dir = Path(case_dir)
if not (case_dir / ".git").exists():
return False
env = _git_env()
subprocess.run(["git", "add", "."], cwd=case_dir, capture_output=True, env=env)
commit = subprocess.run(
["git", "commit", "-m", message],
cwd=case_dir, capture_output=True, text=True, env=env,
)
if commit.returncode != 0 and "nothing to commit" not in commit.stdout:
logger.warning("Git commit failed in %s: %s", case_dir, commit.stderr or commit.stdout)
if not _refresh_remote_url(case_dir, env):
logger.warning("No origin remote configured in %s — skipping push", case_dir)
return False
push = subprocess.run(
["git", "push"],
cwd=case_dir, capture_output=True, text=True, env=env,
)
if push.returncode != 0:
logger.warning("Git push failed in %s: %s", case_dir, push.stderr)
return False
return True

View File

@@ -13,7 +13,7 @@ from uuid import UUID
import httpx
from legal_mcp import config
from legal_mcp.services import audit, db, practice_area as pa
from legal_mcp.services import audit, db, git_sync, practice_area as pa
logger = logging.getLogger(__name__)
@@ -315,21 +315,13 @@ async def case_update(
updated = await db.update_case(UUID(case["id"]), **fields)
# Git commit the update (best-effort)
# Git commit + push the update (best-effort)
try:
case_dir = config.find_case_dir(case_number)
if case_dir.exists():
case_json = case_dir / "case.json"
case_json.write_text(json.dumps(updated, default=str, ensure_ascii=False, indent=2))
subprocess.run(["git", "add", "case.json"], cwd=case_dir, capture_output=True)
subprocess.run(
["git", "commit", "-m", f"עדכון תיק: {', '.join(fields.keys())}"],
cwd=case_dir,
capture_output=True,
env={"GIT_AUTHOR_NAME": "Ezer Mishpati", "GIT_AUTHOR_EMAIL": "legal@local",
"GIT_COMMITTER_NAME": "Ezer Mishpati", "GIT_COMMITTER_EMAIL": "legal@local",
"PATH": "/usr/bin:/bin"},
)
git_sync.commit_and_push(case_dir, f"עדכון תיק: {', '.join(fields.keys())}")
except Exception:
pass # git not available — non-critical

View File

@@ -4,12 +4,11 @@ from __future__ import annotations
import json
import shutil
import subprocess
from pathlib import Path
from uuid import UUID
from legal_mcp import config
from legal_mcp.services import db, processor
from legal_mcp.services import db, git_sync, processor
async def document_upload(
@@ -67,11 +66,10 @@ async def document_upload(
await db.update_document(UUID(doc["id"]), doc_type=classified_type)
doc["doc_type"] = classified_type
# Git commit (best-effort — don't fail upload on git errors)
# Git commit + push (best-effort — don't fail upload on git errors)
try:
repo_dir = config.find_case_dir(case_number)
if repo_dir.exists():
subprocess.run(["git", "add", "."], cwd=repo_dir, capture_output=True)
doc_type_hebrew = {
"appeal": "כתב ערר",
"response": "תשובה",
@@ -85,14 +83,7 @@ async def document_upload(
"exhibit": "נספח",
"reference": "מסמך עזר",
}.get(actual_doc_type, actual_doc_type)
subprocess.run(
["git", "commit", "-m", f"הוספת {doc_type_hebrew}: {title}"],
cwd=repo_dir,
capture_output=True,
env={"GIT_AUTHOR_NAME": "Ezer Mishpati", "GIT_AUTHOR_EMAIL": "legal@local",
"GIT_COMMITTER_NAME": "Ezer Mishpati", "GIT_COMMITTER_EMAIL": "legal@local",
"PATH": "/usr/bin:/bin"},
)
git_sync.commit_and_push(repo_dir, f"הוספת {doc_type_hebrew}: {title}")
except Exception:
pass # git not available in container — non-critical

View File

@@ -34,7 +34,7 @@ from legal_mcp.tools import cases as cases_tools, search as search_tools, workfl
# Import integration clients (same directory)
_web_dir = Path(__file__).resolve().parent
sys.path.insert(0, str(_web_dir.parent))
from web.gitea_client import create_repo, setup_remote_and_push
from web.gitea_client import commit_and_push, create_repo, setup_remote_and_push
from web.paperclip_client import (
archive_project as pc_archive_project,
create_project as pc_create_project,
@@ -3005,26 +3005,11 @@ async def _process_tagged_document(task_id: str, dest: Path, case_number: str, c
_progress[task_id] = {"status": "processing", "filename": display_name, "step": "extracting"}
result = await processor.process_document(doc_id, case_id)
# Git commit + push (best-effort — don't fail upload on git errors)
try:
repo_dir = config.find_case_dir(case_number)
if repo_dir.exists():
env = {
"GIT_AUTHOR_NAME": "Ezer Mishpati", "GIT_AUTHOR_EMAIL": "legal@local",
"GIT_COMMITTER_NAME": "Ezer Mishpati", "GIT_COMMITTER_EMAIL": "legal@local",
"PATH": "/usr/bin:/bin",
}
doc_type_hebrew = DOC_TYPE_NAMES.get(doc_type, doc_type)
subprocess.run(["git", "add", "."], cwd=repo_dir, capture_output=True)
subprocess.run(
["git", "commit", "-m", f"הוספת {doc_type_hebrew}: {display_name}"],
cwd=repo_dir, capture_output=True, env=env,
)
# Try to push to Gitea (non-blocking)
subprocess.run(["git", "push"], cwd=repo_dir, capture_output=True, env={
**env,
"GIT_TERMINAL_PROMPT": "0",
})
commit_and_push(repo_dir, f"הוספת {doc_type_hebrew}: {display_name}")
except Exception:
logger.warning("Git commit/push failed for %s (non-critical)", display_name)
@@ -3360,20 +3345,13 @@ async def _process_case_document(task_id: str, source: Path, req: ClassifyReques
try:
repo_dir = config.find_case_dir(req.case_number)
if repo_dir.exists():
subprocess.run(["git", "add", "."], cwd=repo_dir, capture_output=True)
doc_type_hebrew = {
"appeal": "כתב ערר", "response": "תשובה", "decision": "החלטה",
"reference": "מסמך עזר", "exhibit": "נספח",
}.get(req.doc_type, req.doc_type)
subprocess.run(
["git", "commit", "-m", f"הוספת {doc_type_hebrew}: {title}"],
cwd=repo_dir, capture_output=True,
env={"GIT_AUTHOR_NAME": "Ezer Mishpati", "GIT_AUTHOR_EMAIL": "legal@local",
"GIT_COMMITTER_NAME": "Ezer Mishpati", "GIT_COMMITTER_EMAIL": "legal@local",
"PATH": "/usr/bin:/bin"},
)
commit_and_push(repo_dir, f"הוספת {doc_type_hebrew}: {title}")
except Exception:
logger.warning("Git commit failed for %s (non-critical)", req.filename)
logger.warning("Git commit/push failed for %s (non-critical)", req.filename)
# Remove from uploads
source.unlink(missing_ok=True)

View File

@@ -48,24 +48,96 @@ async def create_repo(case_number: str, title: str, description: str = "") -> di
return resp.json()
def setup_remote_and_push(case_dir: str | Path, repo_clone_url: str) -> bool:
"""Add Gitea as remote 'origin' (or update it) and push."""
case_dir = Path(case_dir)
if not (case_dir / ".git").exists():
return False
env = {
def _git_env() -> dict:
return {
"GIT_AUTHOR_NAME": "Ezer Mishpati",
"GIT_AUTHOR_EMAIL": "legal@local",
"GIT_COMMITTER_NAME": "Ezer Mishpati",
"GIT_COMMITTER_EMAIL": "legal@local",
"PATH": os.environ.get("PATH", "/usr/bin:/bin"),
"GIT_TERMINAL_PROMPT": "0",
}
# Inject token into clone URL for auth
def _refresh_remote_url(case_dir: Path, env: dict) -> bool:
"""Re-inject current token into existing origin URL.
Handles the case where the token in Infisical was rotated but the
case repo's origin URL still has the old token embedded.
Returns True if origin exists (and was refreshed), False otherwise.
"""
result = subprocess.run(
["git", "remote", "get-url", "origin"],
cwd=case_dir, capture_output=True, text=True,
)
if result.returncode != 0:
return False
current_url = result.stdout.strip()
# Strip any embedded credentials (https://user:token@host -> https://host)
if "@" in current_url and current_url.startswith("https://"):
bare_url = "https://" + current_url.split("@", 1)[1]
else:
bare_url = current_url
auth_url = bare_url.replace("https://", f"https://chaim:{_token()}@")
if auth_url != current_url:
subprocess.run(
["git", "remote", "set-url", "origin", auth_url],
cwd=case_dir, capture_output=True, env=env,
)
return True
def commit_and_push(case_dir: str | Path, message: str) -> bool:
"""Stage, commit, refresh remote URL with current token, and push.
Best-effort: returns False on any failure, but logs the reason at
WARNING level so issues are visible (the previous code swallowed
push failures silently).
"""
case_dir = Path(case_dir)
if not (case_dir / ".git").exists():
return False
env = _git_env()
subprocess.run(["git", "add", "."], cwd=case_dir, capture_output=True, env=env)
commit = subprocess.run(
["git", "commit", "-m", message],
cwd=case_dir, capture_output=True, text=True, env=env,
)
if commit.returncode != 0 and "nothing to commit" not in commit.stdout:
logger.warning("Git commit failed in %s: %s", case_dir, commit.stderr or commit.stdout)
# Continue anyway — maybe there were no new changes but we still
# want to push earlier unpushed commits.
if not _refresh_remote_url(case_dir, env):
logger.warning("No origin remote configured in %s — skipping push", case_dir)
return False
push = subprocess.run(
["git", "push"],
cwd=case_dir, capture_output=True, text=True, env=env,
)
if push.returncode != 0:
logger.warning("Git push failed in %s: %s", case_dir, push.stderr)
return False
return True
def setup_remote_and_push(case_dir: str | Path, repo_clone_url: str) -> bool:
"""Add Gitea as remote 'origin' (or update it) and push.
Used on initial case creation. For routine commits + push on
existing repos, prefer ``commit_and_push``.
"""
case_dir = Path(case_dir)
if not (case_dir / ".git").exists():
return False
env = _git_env()
auth_url = repo_clone_url.replace("https://", f"https://chaim:{_token()}@")
# Check if remote exists
result = subprocess.run(
["git", "remote", "get-url", "origin"],
cwd=case_dir, capture_output=True, text=True,
@@ -81,13 +153,11 @@ def setup_remote_and_push(case_dir: str | Path, repo_clone_url: str) -> bool:
cwd=case_dir, capture_output=True, env=env,
)
# Push
push_result = subprocess.run(
["git", "push", "-u", "origin", "main"],
cwd=case_dir, capture_output=True, text=True, env=env,
)
if push_result.returncode != 0:
# Try master branch
push_result = subprocess.run(
["git", "push", "-u", "origin", "master"],
cwd=case_dir, capture_output=True, text=True, env=env,