struff
This commit is contained in:
parent
3ca6b4d5f8
commit
07176f9b79
8 changed files with 749 additions and 57 deletions
|
|
@ -11,6 +11,35 @@ on:
|
|||
- web_v2
|
||||
- backend
|
||||
- common
|
||||
llm_model:
|
||||
description: 'LLM Model'
|
||||
required: true
|
||||
default: 'auto'
|
||||
type: choice
|
||||
options:
|
||||
- auto
|
||||
- composer-1
|
||||
- sonnet-4.5
|
||||
- sonnet-4.5-thinking
|
||||
- gemini-3-pro
|
||||
- gpt-5
|
||||
- gpt-5.1
|
||||
- gpt-5-high
|
||||
- gpt-5.1-high
|
||||
- gpt-5-codex
|
||||
- gpt-5-codex-high
|
||||
- gpt-5.1-codex
|
||||
- gpt-5.1-codex-high
|
||||
- opus-4.1
|
||||
- grok
|
||||
commit_mode:
|
||||
description: 'Commit mode: master (direct commit with PR fallback) or pr (always create PR)'
|
||||
required: false
|
||||
default: 'master'
|
||||
type: choice
|
||||
options:
|
||||
- master
|
||||
- pr
|
||||
schedule:
|
||||
# Run every 30 minutes
|
||||
- cron: '*/20 * * * *'
|
||||
|
|
@ -76,11 +105,13 @@ jobs:
|
|||
- name: Run Development Agent
|
||||
env:
|
||||
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
|
||||
CURSOR_MODEL: ${{ secrets.CURSOR_MODEL }}
|
||||
CURSOR_MODEL: ${{ inputs.llm_model || secrets.CURSOR_MODEL }}
|
||||
PROJECT_ROOT: ${{ github.workspace }}
|
||||
MAX_ITERATIONS: 10
|
||||
MAX_RETRIES: 3
|
||||
TIMEOUT_HOURS: 6
|
||||
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
||||
COMMIT_MODE: ${{ inputs.commit_mode || 'master' }}
|
||||
run: |
|
||||
# Verify cursor-agent is accessible
|
||||
echo "Current PATH: $PATH"
|
||||
|
|
|
|||
86
.forgejo/workflows/agent-documentation.yml
Normal file
86
.forgejo/workflows/agent-documentation.yml
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
name: AI Agent - Documentation Update
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
component:
|
||||
description: 'Component to document'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- web_v2
|
||||
- backend
|
||||
- common
|
||||
llm_model:
|
||||
description: 'LLM Model'
|
||||
required: true
|
||||
default: 'auto'
|
||||
type: choice
|
||||
options:
|
||||
- auto
|
||||
- composer-1
|
||||
- sonnet-4.5
|
||||
- sonnet-4.5-thinking
|
||||
- gemini-3-pro
|
||||
- gpt-5
|
||||
- gpt-5.1
|
||||
- gpt-5-high
|
||||
- gpt-5.1-high
|
||||
- gpt-5-codex
|
||||
- gpt-5-codex-high
|
||||
- gpt-5.1-codex
|
||||
- gpt-5.1-codex-high
|
||||
- opus-4.1
|
||||
- grok
|
||||
|
||||
jobs:
|
||||
update-docs:
|
||||
runs-on: ubuntu-latest
|
||||
container: code.mnemo-cards.online/cinnabarflower/cards/agent:latest
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config --global user.name "AI Agent"
|
||||
git config --global user.email "ai-agent@mnemo-cards.com"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
COMPONENT="${{ inputs.component }}"
|
||||
if [ "$COMPONENT" = "web_v2" ]; then
|
||||
cd mnemo_cards_web_v2
|
||||
flutter pub get
|
||||
elif [ "$COMPONENT" = "backend" ]; then
|
||||
cd mnemo_cards_backend
|
||||
dart pub get
|
||||
elif [ "$COMPONENT" = "common" ]; then
|
||||
cd mnemo_cards_common
|
||||
dart pub get
|
||||
fi
|
||||
|
||||
- name: Run Documentation Agent
|
||||
env:
|
||||
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
|
||||
CURSOR_MODEL: ${{ inputs.llm_model }}
|
||||
PROJECT_ROOT: ${{ github.workspace }}
|
||||
run: |
|
||||
# Verify cursor-agent is accessible
|
||||
if ! command -v cursor-agent &> /dev/null; then
|
||||
echo "❌ cursor-agent not found in PATH"
|
||||
# Try to find it in common locations
|
||||
if [ -f "$HOME/.local/bin/cursor-agent" ]; then
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
elif [ -f "$HOME/.cursor/bin/cursor-agent" ]; then
|
||||
export PATH="$HOME/.cursor/bin:$PATH"
|
||||
fi
|
||||
fi
|
||||
|
||||
cd ${{ github.workspace }}
|
||||
python3 tools/agent/doc_updater.py ${{ inputs.component }} --model ${{ inputs.llm_model }}
|
||||
|
|
@ -11,6 +11,27 @@ on:
|
|||
- web_v2
|
||||
- backend
|
||||
- common
|
||||
llm_model:
|
||||
description: 'LLM Model'
|
||||
required: true
|
||||
default: 'auto'
|
||||
type: choice
|
||||
options:
|
||||
- auto
|
||||
- composer-1
|
||||
- sonnet-4.5
|
||||
- sonnet-4.5-thinking
|
||||
- gemini-3-pro
|
||||
- gpt-5
|
||||
- gpt-5.1
|
||||
- gpt-5-high
|
||||
- gpt-5.1-high
|
||||
- gpt-5-codex
|
||||
- gpt-5-codex-high
|
||||
- gpt-5.1-codex
|
||||
- gpt-5.1-codex-high
|
||||
- opus-4.1
|
||||
- grok
|
||||
schedule:
|
||||
- cron: '0 */4 * * *'
|
||||
|
||||
|
|
@ -70,7 +91,7 @@ jobs:
|
|||
- name: Run Planning Agent
|
||||
env:
|
||||
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
|
||||
CURSOR_MODEL: ${{ secrets.CURSOR_MODEL }}
|
||||
CURSOR_MODEL: ${{ inputs.llm_model || secrets.CURSOR_MODEL }}
|
||||
PROJECT_ROOT: ${{ github.workspace }}
|
||||
MAX_ITERATIONS: 10
|
||||
PATH: /usr/bin:/bin:$HOME/.local/bin:$HOME/.cursor/bin:${{ env.PATH }}
|
||||
|
|
|
|||
|
|
@ -5,16 +5,46 @@ You are an autonomous AI software engineer working on the mnemo_cards project. Y
|
|||
## Project Context
|
||||
|
||||
This is a language learning application consisting of multiple components:
|
||||
- **mnemo_cards_web_v2**: Flutter web frontend (main user interface)
|
||||
- **mnemo_cards_web_v2**: Flutter web frontend (main user interface). **WEB ONLY**.
|
||||
- **mnemo_cards_backend**: Dart backend server (API, data storage)
|
||||
- **mnemo_cards_common**: Shared code between frontend and backend
|
||||
|
||||
## Autonomous Workflow Rules
|
||||
|
||||
### 1. Operating Mode
|
||||
- **Act as an IMPLEMENTER**: Execute changes directly. Do not ask for confirmation unless a critical "Denylisted" action is required.
|
||||
- **State Files**: You must maintain and read from these files at the repo root:
|
||||
- `project_config.md`: Goals, constraints, stack specifics. **Read First**.
|
||||
- `workflow_state.md`: Volatile loop state (Plan, Next Actions, Progress). **Update Constantly**.
|
||||
- `PROGRESS.md` & `TODO.md`: High-level project tracking. **Update on Completion**.
|
||||
- **Context Budget**: Summarize code context. Do not paste whole files.
|
||||
|
||||
### 2. The Autonomous Loop
|
||||
1. **Read**: Load only necessary snippets.
|
||||
2. **Plan**: Update `workflow_state.md` (Plan & Next Actions).
|
||||
3. **Act**:
|
||||
- Apply targeted diffs.
|
||||
- Run fast checks (lint, test).
|
||||
4. **Verify**:
|
||||
- Parse outputs.
|
||||
- Update `workflow_state.md` (Progress Log).
|
||||
- If failure: Analyze -> Fix -> Loop.
|
||||
- **Auto-Fix**: If `mnemo_cards_web` errors occur, run `mnemo_cards_web/complete_auto_debug.sh`, wait, read report, fix, repeat.
|
||||
5. **Trim**: Prune chat context. Rely on state files.
|
||||
6. **Repeat** until acceptance criteria met.
|
||||
|
||||
### 3. Web Specifics (mnemo_cards_web_v2)
|
||||
- **WEB ONLY**: This app runs ONLY on web. No mobile, no macOS.
|
||||
- **Tests**: Run tests ONLY for web platform.
|
||||
- **Access**: Check `.access` for credentials if needed.
|
||||
|
||||
## Your Responsibilities
|
||||
|
||||
1. **Read and understand the task** - Analyze the requirements and acceptance criteria
|
||||
2. **Implement the solution** - Write clean, maintainable code following project conventions
|
||||
3. **Write comprehensive tests** - Ensure all new code has unit tests
|
||||
4. **Verify your work** - Make sure tests pass and linting is clean
|
||||
1. **Read and understand the task** - Analyze requirements from `task.md` or user input.
|
||||
2. **Manage State** - Create/Update `workflow_state.md`, `PROGRESS.md`, `TODO.md`.
|
||||
3. **Implement the solution** - Write clean code, following `project_config.md`.
|
||||
4. **Write comprehensive tests** - Unit tests are mandatory.
|
||||
5. **Verify your work** - Tests, Lints, Auto-Debug.
|
||||
|
||||
## Code Standards
|
||||
|
||||
|
|
@ -244,12 +274,11 @@ try {
|
|||
## Git Workflow
|
||||
|
||||
Your commits will be automatically created. Make sure your changes are:
|
||||
- **Atomic**: One logical change per commit
|
||||
- **Complete**: All files needed for the feature
|
||||
- **Tested**: All tests pass
|
||||
- **Clean**: Linter passes
|
||||
- **Size-limited**: Maximum 500 changed lines per commit
|
||||
- If changes exceed this limit, split the work into multiple commits/tasks
|
||||
- **Atomic**: One logical change per commit.
|
||||
- **Complete**: All files needed for the feature.
|
||||
- **Tested**: All tests pass.
|
||||
- **Clean**: Linter passes.
|
||||
- **Targeted**: Prefer surgical refactors over massive rewrites.
|
||||
|
||||
## Reference Materials
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,9 @@ Each task should have:
|
|||
|
||||
**CRITICAL: Tasks must be small and focused. Large tasks MUST be broken down into smaller subtasks.**
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Agent Timeout**: The agent performing the task will be automatically interrupted if the task takes longer than 20 minutes.
|
||||
|
||||
- **Small tasks** (1-3 hours): Single feature or bug fix - **PREFERRED SIZE**
|
||||
- **Medium tasks** (4-5 hours): Feature with multiple files - **ACCEPTABLE, but prefer smaller**
|
||||
- **Large tasks** (3+ hours): **MUST be broken down** into smaller subtasks before adding to task list
|
||||
|
|
|
|||
|
|
@ -5,9 +5,13 @@ Executes a single task per run: read task -> execute -> test -> commit
|
|||
import sys
|
||||
import subprocess
|
||||
import time
|
||||
import json
|
||||
import re
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional, List
|
||||
from typing import Optional, List, Tuple
|
||||
|
||||
from config import AgentConfig, get_test_command, get_lint_command
|
||||
from task_manager import TaskManager, GlobalLock, Task
|
||||
|
|
@ -293,15 +297,16 @@ class AgentOrchestrator:
|
|||
print("⚠️ No changes to commit")
|
||||
|
||||
# Push changes (with pull before push to avoid conflicts)
|
||||
# If push fails, _git_push() will automatically create a PR
|
||||
print("\n📤 Pushing changes...")
|
||||
if not self._git_push():
|
||||
print("⚠️ Push failed, pulling latest changes and retrying...")
|
||||
# Pull latest changes before retrying push
|
||||
if self._git_pull():
|
||||
print("✅ Pulled latest changes, retrying push...")
|
||||
self._git_push()
|
||||
else:
|
||||
print("❌ Failed to pull latest changes")
|
||||
push_success = self._git_push()
|
||||
|
||||
if not push_success:
|
||||
print("⚠️ Push failed, attempting to create PR...")
|
||||
# _git_push() already handles PR creation, but if it still failed,
|
||||
# we'll continue anyway as the changes are committed locally
|
||||
print("⚠️ Changes are committed locally but not pushed")
|
||||
print(" You may need to manually push or create a PR")
|
||||
|
||||
return True
|
||||
|
||||
|
|
@ -488,8 +493,363 @@ Changes:
|
|||
# No changes or error
|
||||
return False
|
||||
|
||||
def _get_git_repo_info(self) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""Get repository owner and name from git remote."""
|
||||
result = subprocess.run(
|
||||
["git", "remote", "get-url", "origin"],
|
||||
cwd=self.config.project_root,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
return None, None
|
||||
|
||||
remote_url = result.stdout.strip()
|
||||
|
||||
# Parse different URL formats:
|
||||
# https://forgejo.example.com/owner/repo.git
|
||||
# https://forgejo.example.com/owner/repo
|
||||
# git@forgejo.example.com:owner/repo.git
|
||||
# https://github.com/owner/repo.git
|
||||
|
||||
patterns = [
|
||||
r'https?://[^/]+/([^/]+)/([^/]+?)(?:\.git)?/?$',
|
||||
r'git@[^:]+:([^/]+)/([^/]+?)(?:\.git)?$',
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, remote_url)
|
||||
if match:
|
||||
owner = match.group(1)
|
||||
repo = match.group(2)
|
||||
return owner, repo
|
||||
|
||||
return None, None
|
||||
|
||||
def _get_forgejo_api_url(self) -> Optional[str]:
|
||||
"""Get Forgejo/GitHub API URL from git remote."""
|
||||
if self.config.forgejo_api_url:
|
||||
return self.config.forgejo_api_url
|
||||
|
||||
result = subprocess.run(
|
||||
["git", "remote", "get-url", "origin"],
|
||||
cwd=self.config.project_root,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
return None
|
||||
|
||||
remote_url = result.stdout.strip()
|
||||
|
||||
# Extract base URL
|
||||
# https://forgejo.example.com/owner/repo.git -> https://forgejo.example.com
|
||||
# https://github.com/owner/repo.git -> https://api.github.com
|
||||
# git@forgejo.example.com:owner/repo.git -> https://forgejo.example.com
|
||||
|
||||
if 'github.com' in remote_url:
|
||||
return 'https://api.github.com'
|
||||
|
||||
# For Forgejo, extract the base URL
|
||||
match = re.search(r'https?://([^/]+)', remote_url)
|
||||
if match:
|
||||
base_url = f"https://{match.group(1)}"
|
||||
return f"{base_url}/api/v1"
|
||||
|
||||
match = re.search(r'git@([^:]+)', remote_url)
|
||||
if match:
|
||||
base_url = f"https://{match.group(1)}"
|
||||
return f"{base_url}/api/v1"
|
||||
|
||||
return None
|
||||
|
||||
def _check_existing_pr(self, branch_name: str) -> Optional[str]:
|
||||
"""Check if PR already exists for this branch. Returns PR URL if found."""
|
||||
if not self.config.forgejo_token:
|
||||
return None
|
||||
|
||||
owner, repo = self._get_git_repo_info()
|
||||
if not owner or not repo:
|
||||
return None
|
||||
|
||||
api_url = self._get_forgejo_api_url()
|
||||
if not api_url:
|
||||
return None
|
||||
|
||||
# List open PRs for this branch
|
||||
pr_url = f"{api_url}/repos/{owner}/{repo}/pulls?head={owner}:{branch_name}&state=open"
|
||||
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
pr_url,
|
||||
headers={
|
||||
"Authorization": f"token {self.config.forgejo_token}",
|
||||
"Accept": "application/json"
|
||||
}
|
||||
)
|
||||
|
||||
with urllib.request.urlopen(req) as response:
|
||||
if response.status == 200:
|
||||
prs = json.loads(response.read().decode('utf-8'))
|
||||
if prs and len(prs) > 0:
|
||||
return prs[0].get('html_url')
|
||||
except Exception:
|
||||
# If check fails, continue anyway
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def _create_pull_request(self, task: Task, branch_name: str) -> bool:
|
||||
"""Create a pull request via Forgejo/GitHub API."""
|
||||
if not self.config.forgejo_token:
|
||||
print("⚠️ FORGEJO_TOKEN not set, cannot create PR")
|
||||
return False
|
||||
|
||||
owner, repo = self._get_git_repo_info()
|
||||
if not owner or not repo:
|
||||
print("⚠️ Could not determine repository owner/name from git remote")
|
||||
return False
|
||||
|
||||
api_url = self._get_forgejo_api_url()
|
||||
if not api_url:
|
||||
print("⚠️ Could not determine API URL from git remote")
|
||||
return False
|
||||
|
||||
# Check if PR already exists
|
||||
existing_pr = self._check_existing_pr(branch_name)
|
||||
if existing_pr:
|
||||
print(f"✅ Pull Request already exists: {existing_pr}")
|
||||
return True
|
||||
|
||||
# Get current branch name (should match branch_name, but verify)
|
||||
branch_result = subprocess.run(
|
||||
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
||||
cwd=self.config.project_root,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if branch_result.returncode != 0:
|
||||
print("⚠️ Could not determine current branch")
|
||||
return False
|
||||
|
||||
current_branch = branch_result.stdout.strip()
|
||||
|
||||
# Use the provided branch_name, not current_branch (they should match)
|
||||
if current_branch != branch_name:
|
||||
print(f"⚠️ Branch mismatch: current={current_branch}, expected={branch_name}")
|
||||
# Use current_branch for PR creation
|
||||
branch_name = current_branch
|
||||
|
||||
# PR title and body
|
||||
pr_title = f"feat({self.config.component}): {task.title}"
|
||||
pr_body = f"""## Task: {task.id}
|
||||
|
||||
**Priority:** {task.priority}
|
||||
|
||||
### Description
|
||||
{task.description}
|
||||
|
||||
### Changes
|
||||
This PR contains changes for task {task.id} completed by AI Agent.
|
||||
|
||||
### Acceptance Criteria
|
||||
"""
|
||||
for i, criterion in enumerate(task.acceptance_criteria, 1):
|
||||
pr_body += f"{i}. {criterion}\n"
|
||||
|
||||
pr_body += f"""
|
||||
---
|
||||
*Created automatically by AI Agent*
|
||||
*Component: {self.config.component}*
|
||||
"""
|
||||
|
||||
# Create PR via API
|
||||
pr_url = f"{api_url}/repos/{owner}/{repo}/pulls"
|
||||
|
||||
data = {
|
||||
"title": pr_title,
|
||||
"body": pr_body,
|
||||
"head": current_branch,
|
||||
"base": "master"
|
||||
}
|
||||
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
pr_url,
|
||||
data=json.dumps(data).encode('utf-8'),
|
||||
headers={
|
||||
"Authorization": f"token {self.config.forgejo_token}",
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
}
|
||||
)
|
||||
|
||||
with urllib.request.urlopen(req) as response:
|
||||
if response.status in [200, 201]:
|
||||
result = json.loads(response.read().decode('utf-8'))
|
||||
pr_number = result.get('number', 'unknown')
|
||||
pr_html_url = result.get('html_url', pr_url)
|
||||
print(f"✅ Created Pull Request #{pr_number}")
|
||||
print(f" URL: {pr_html_url}")
|
||||
return True
|
||||
else:
|
||||
error_body = response.read().decode('utf-8')
|
||||
print(f"❌ Failed to create PR: HTTP {response.status}")
|
||||
print(f" Response: {error_body}")
|
||||
return False
|
||||
|
||||
except urllib.error.HTTPError as e:
|
||||
error_body = e.read().decode('utf-8')
|
||||
print(f"❌ Failed to create PR: HTTP {e.code}")
|
||||
print(f" Response: {error_body}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to create PR: {e}")
|
||||
return False
|
||||
|
||||
def _create_branch_and_pr(self, task: Task) -> bool:
|
||||
"""Create a feature branch and PR for the task."""
|
||||
# Get current task if not provided
|
||||
if not task:
|
||||
state = self.task_manager.load_state()
|
||||
if not state.current_task_id:
|
||||
print("⚠️ No current task ID, cannot create PR")
|
||||
return False
|
||||
|
||||
tasks = self.task_manager.load_tasks()
|
||||
task = next((t for t in tasks if t.id == state.current_task_id), None)
|
||||
if not task:
|
||||
print("⚠️ Current task not found, cannot create PR")
|
||||
return False
|
||||
|
||||
# Create branch name from task ID
|
||||
branch_name = f"agent/{self.config.component}/{task.id}"
|
||||
branch_name = re.sub(r'[^a-zA-Z0-9/-]', '-', branch_name)
|
||||
|
||||
# Get current branch name
|
||||
branch_result = subprocess.run(
|
||||
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
||||
cwd=self.config.project_root,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
current_branch = branch_result.stdout.strip() if branch_result.returncode == 0 else "unknown"
|
||||
|
||||
# If we're already on the target branch, just push it
|
||||
if current_branch == branch_name:
|
||||
print(f"✅ Already on branch: {branch_name}")
|
||||
else:
|
||||
# Check if branch already exists locally
|
||||
check_branch = subprocess.run(
|
||||
["git", "show-ref", "--verify", "--quiet", f"refs/heads/{branch_name}"],
|
||||
cwd=self.config.project_root,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if check_branch.returncode == 0:
|
||||
# Branch exists locally, just checkout
|
||||
print(f"⚠️ Branch {branch_name} already exists locally, checking out...")
|
||||
branch_result = subprocess.run(
|
||||
["git", "checkout", branch_name],
|
||||
cwd=self.config.project_root,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
else:
|
||||
# Create and checkout new branch
|
||||
branch_result = subprocess.run(
|
||||
["git", "checkout", "-b", branch_name],
|
||||
cwd=self.config.project_root,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if branch_result.returncode != 0:
|
||||
print(f"⚠️ Failed to create/checkout branch: {branch_result.stderr}")
|
||||
return False
|
||||
|
||||
print(f"✅ Created/checked out branch: {branch_name}")
|
||||
|
||||
# Push branch to remote
|
||||
push_branch_result = subprocess.run(
|
||||
["git", "push", "-u", "origin", branch_name],
|
||||
cwd=self.config.project_root,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if push_branch_result.returncode != 0:
|
||||
print(f"⚠️ Failed to push branch: {push_branch_result.stderr}")
|
||||
return False
|
||||
|
||||
print(f"✅ Pushed branch to remote")
|
||||
|
||||
# Create PR
|
||||
if self._create_pull_request(task, branch_name):
|
||||
print("✅ Successfully created PR")
|
||||
return True
|
||||
else:
|
||||
print("⚠️ Failed to create PR, but branch was pushed")
|
||||
return False
|
||||
|
||||
def _git_push(self) -> bool:
|
||||
"""Push changes to remote. Pulls first if needed to avoid conflicts."""
|
||||
"""Push changes to remote. Behavior depends on commit_mode:
|
||||
- 'pr': Always create PR (don't try to push to master)
|
||||
- 'master': Try to push to master, create PR as fallback if push fails
|
||||
"""
|
||||
# Get current branch name
|
||||
branch_result = subprocess.run(
|
||||
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
||||
cwd=self.config.project_root,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
current_branch = branch_result.stdout.strip() if branch_result.returncode == 0 else "unknown"
|
||||
|
||||
# If commit_mode is 'pr', always create PR (don't try to push to master)
|
||||
if self.config.commit_mode == "pr":
|
||||
print("\n📝 Commit mode is 'pr', creating PR instead of pushing to master...")
|
||||
|
||||
# Get current task
|
||||
state = self.task_manager.load_state()
|
||||
if not state.current_task_id:
|
||||
print("⚠️ No current task ID, cannot create PR")
|
||||
return False
|
||||
|
||||
tasks = self.task_manager.load_tasks()
|
||||
current_task = next((t for t in tasks if t.id == state.current_task_id), None)
|
||||
if not current_task:
|
||||
print("⚠️ Current task not found, cannot create PR")
|
||||
return False
|
||||
|
||||
# If we're on master, create branch and PR
|
||||
if current_branch == "master":
|
||||
return self._create_branch_and_pr(current_task)
|
||||
else:
|
||||
# We're already on a feature branch, just push it and create PR
|
||||
push_branch_result = subprocess.run(
|
||||
["git", "push", "-u", "origin", current_branch],
|
||||
cwd=self.config.project_root,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if push_branch_result.returncode == 0:
|
||||
print(f"✅ Pushed branch to remote")
|
||||
if self._create_pull_request(current_task, current_branch):
|
||||
return True
|
||||
else:
|
||||
print("⚠️ Failed to create PR, but branch was pushed")
|
||||
return True # Still consider it success
|
||||
else:
|
||||
print(f"⚠️ Push failed: {push_branch_result.stderr}")
|
||||
return False
|
||||
|
||||
# commit_mode is 'master' - try to push to master, fallback to PR if fails
|
||||
# First, fetch to check if there are remote changes
|
||||
fetch_result = subprocess.run(
|
||||
["git", "fetch"],
|
||||
|
|
@ -518,6 +878,8 @@ Changes:
|
|||
if not self._git_pull():
|
||||
print("❌ Failed to pull, push may fail")
|
||||
|
||||
# If we're on master, try to push directly
|
||||
if current_branch == "master":
|
||||
# Now try to push
|
||||
result = subprocess.run(
|
||||
["git", "push"],
|
||||
|
|
@ -547,12 +909,51 @@ Changes:
|
|||
return True
|
||||
else:
|
||||
print(f"❌ Push failed after pull: {retry_result.stderr}")
|
||||
return False
|
||||
# Fall through to create PR (fallback)
|
||||
else:
|
||||
print("❌ Failed to pull before retry")
|
||||
# Fall through to create PR (fallback)
|
||||
|
||||
# Fallback: create branch and PR
|
||||
print("\n📝 Push to master failed, creating branch and PR as fallback...")
|
||||
|
||||
# Get current task
|
||||
state = self.task_manager.load_state()
|
||||
if not state.current_task_id:
|
||||
print("⚠️ No current task ID, cannot create PR")
|
||||
return False
|
||||
|
||||
tasks = self.task_manager.load_tasks()
|
||||
current_task = next((t for t in tasks if t.id == state.current_task_id), None)
|
||||
if not current_task:
|
||||
print("⚠️ Current task not found, cannot create PR")
|
||||
return False
|
||||
|
||||
return self._create_branch_and_pr(current_task)
|
||||
else:
|
||||
print(f"⚠️ Push failed: {result.stderr}")
|
||||
# We're already on a feature branch, just push it
|
||||
print(f"\n📝 Pushing to feature branch: {current_branch}")
|
||||
push_branch_result = subprocess.run(
|
||||
["git", "push", "-u", "origin", current_branch],
|
||||
cwd=self.config.project_root,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if push_branch_result.returncode == 0:
|
||||
print(f"✅ Pushed branch to remote")
|
||||
|
||||
# Try to create PR if we have task info
|
||||
state = self.task_manager.load_state()
|
||||
if state.current_task_id:
|
||||
tasks = self.task_manager.load_tasks()
|
||||
current_task = next((t for t in tasks if t.id == state.current_task_id), None)
|
||||
if current_task:
|
||||
self._create_pull_request(current_task, current_branch)
|
||||
|
||||
return True
|
||||
else:
|
||||
print(f"⚠️ Push failed: {push_branch_result.stderr}")
|
||||
return False
|
||||
|
||||
def _create_issue_for_failed_task(self, task: Task) -> None:
|
||||
|
|
|
|||
|
|
@ -24,6 +24,14 @@ class AgentConfig:
|
|||
cursor_api_key: Optional[str] = None
|
||||
cursor_model: str = "claude-3-5-sonnet-20241022"
|
||||
|
||||
# Forgejo/GitHub API settings
|
||||
forgejo_token: Optional[str] = None
|
||||
forgejo_api_url: Optional[str] = None # Will be auto-detected from git remote
|
||||
forgejo_repo: Optional[str] = None # Will be auto-detected from git remote
|
||||
|
||||
# Commit mode: 'master' (direct commit with PR fallback) or 'pr' (always create PR)
|
||||
commit_mode: str = "master"
|
||||
|
||||
# State file paths
|
||||
@property
|
||||
def agent_dir(self) -> Path:
|
||||
|
|
@ -70,6 +78,9 @@ class AgentConfig:
|
|||
timeout_hours=int(os.getenv("TIMEOUT_HOURS", "6")),
|
||||
cursor_api_key=os.getenv("CURSOR_API_KEY"),
|
||||
cursor_model=os.getenv("CURSOR_MODEL", "claude-3-5-sonnet-20241022"),
|
||||
forgejo_token=os.getenv("FORGEJO_TOKEN"),
|
||||
forgejo_api_url=os.getenv("FORGEJO_API_URL") or os.getenv("GITHUB_API_URL"),
|
||||
commit_mode=os.getenv("COMMIT_MODE", "master"),
|
||||
)
|
||||
|
||||
def validate(self) -> None:
|
||||
|
|
|
|||
110
tools/agent/doc_updater.py
Normal file
110
tools/agent/doc_updater.py
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
"""
|
||||
Script to update documentation for a component using AI agent.
|
||||
"""
|
||||
import sys
|
||||
import argparse
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from config import AgentConfig
|
||||
from cursor_cli_wrapper import CursorCLI, CursorResultStatus
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser(description="Update documentation for a component")
|
||||
parser.add_argument("component", choices=["web_v2", "backend", "common"], help="Component to document")
|
||||
parser.add_argument("--model", default="claude-3-5-sonnet-20241022", help="LLM model to use")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Load configuration
|
||||
try:
|
||||
config = AgentConfig.from_env(args.component)
|
||||
# Override model if provided in args (though config loads from env, we want to support CLI override if needed,
|
||||
# but here we primarily rely on env vars set by workflow, so we'll stick to config or args)
|
||||
# Actually, AgentConfig.from_env reads CURSOR_MODEL.
|
||||
# If we pass it via args, we should update it.
|
||||
if args.model:
|
||||
config.cursor_model = args.model
|
||||
|
||||
except ValueError as e:
|
||||
print(f"❌ Configuration error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"🚀 Starting Documentation Update for: {args.component}")
|
||||
print(f"🤖 Model: {config.cursor_model}")
|
||||
|
||||
# Initialize Cursor CLI
|
||||
cursor_cli = CursorCLI(
|
||||
project_root=config.project_root,
|
||||
api_key=config.cursor_api_key,
|
||||
model=config.cursor_model,
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Define the task
|
||||
task_description = f"""
|
||||
You are an expert technical writer and software engineer.
|
||||
Your task is to update the documentation for the '{args.component}' component.
|
||||
|
||||
1. Analyze the current codebase in '{config.component_root}'.
|
||||
2. Check existing documentation in 'ai_docs/{args.component}' and 'README.md' files.
|
||||
3. Identify missing or outdated documentation.
|
||||
4. Update or create documentation files to reflect the current state of the code.
|
||||
- Focus on architecture, API endpoints, setup instructions, and key features.
|
||||
- Ensure 'README.md' in the component root is up to date.
|
||||
- If there are significant changes, update 'ai_docs/{args.component}/architecture.md' or similar.
|
||||
|
||||
Do NOT modify any code files. ONLY modify markdown documentation files.
|
||||
"""
|
||||
|
||||
print("\n📝 Task Description:")
|
||||
print(task_description)
|
||||
print(f"\n{'='*80}\n")
|
||||
|
||||
# Run agent
|
||||
result = cursor_cli.run_agent(
|
||||
task_description=task_description,
|
||||
force=True,
|
||||
max_iterations=5
|
||||
)
|
||||
|
||||
if result.status != CursorResultStatus.SUCCESS:
|
||||
print(f"\n❌ Documentation update failed: {result.error}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"\n✅ Agent completed successfully")
|
||||
print(f" - Files created: {len(result.files_created)}")
|
||||
print(f" - Files modified: {len(result.files_modified)}")
|
||||
|
||||
if not result.files_created and not result.files_modified:
|
||||
print("\n⚠️ No changes made to documentation.")
|
||||
sys.exit(0)
|
||||
|
||||
# Commit changes
|
||||
print("\n💾 Committing changes...")
|
||||
|
||||
# Git config
|
||||
subprocess.run(["git", "config", "--global", "user.name", "AI Agent"], check=False)
|
||||
subprocess.run(["git", "config", "--global", "user.email", "ai-agent@mnemo-cards.com"], check=False)
|
||||
|
||||
# Add changes
|
||||
subprocess.run(["git", "add", "."], cwd=config.project_root, check=True)
|
||||
|
||||
# Commit
|
||||
commit_msg = f"docs({args.component}): Update documentation via AI Agent\n\nModel: {config.cursor_model}"
|
||||
subprocess.run(["git", "commit", "-m", commit_msg], cwd=config.project_root, check=False)
|
||||
|
||||
# Push
|
||||
print("\n📤 Pushing changes...")
|
||||
# Pull first
|
||||
subprocess.run(["git", "pull", "--rebase"], cwd=config.project_root, check=False)
|
||||
push_result = subprocess.run(["git", "push"], cwd=config.project_root, check=False)
|
||||
|
||||
if push_result.returncode == 0:
|
||||
print("✅ Changes pushed successfully")
|
||||
else:
|
||||
print("❌ Failed to push changes")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in a new issue