This commit is contained in:
Dmitry 2025-11-21 14:13:03 +03:00
parent 3ca6b4d5f8
commit 07176f9b79
8 changed files with 749 additions and 57 deletions

View file

@ -11,6 +11,35 @@ on:
- web_v2 - web_v2
- backend - backend
- common - 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: schedule:
# Run every 30 minutes # Run every 30 minutes
- cron: '*/20 * * * *' - cron: '*/20 * * * *'
@ -76,11 +105,13 @@ jobs:
- name: Run Development Agent - name: Run Development Agent
env: env:
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }} CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
CURSOR_MODEL: ${{ secrets.CURSOR_MODEL }} CURSOR_MODEL: ${{ inputs.llm_model || secrets.CURSOR_MODEL }}
PROJECT_ROOT: ${{ github.workspace }} PROJECT_ROOT: ${{ github.workspace }}
MAX_ITERATIONS: 10 MAX_ITERATIONS: 10
MAX_RETRIES: 3 MAX_RETRIES: 3
TIMEOUT_HOURS: 6 TIMEOUT_HOURS: 6
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
COMMIT_MODE: ${{ inputs.commit_mode || 'master' }}
run: | run: |
# Verify cursor-agent is accessible # Verify cursor-agent is accessible
echo "Current PATH: $PATH" echo "Current PATH: $PATH"

View 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 }}

View file

@ -11,6 +11,27 @@ on:
- web_v2 - web_v2
- backend - backend
- common - 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: schedule:
- cron: '0 */4 * * *' - cron: '0 */4 * * *'
@ -70,7 +91,7 @@ jobs:
- name: Run Planning Agent - name: Run Planning Agent
env: env:
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }} CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
CURSOR_MODEL: ${{ secrets.CURSOR_MODEL }} CURSOR_MODEL: ${{ inputs.llm_model || secrets.CURSOR_MODEL }}
PROJECT_ROOT: ${{ github.workspace }} PROJECT_ROOT: ${{ github.workspace }}
MAX_ITERATIONS: 10 MAX_ITERATIONS: 10
PATH: /usr/bin:/bin:$HOME/.local/bin:$HOME/.cursor/bin:${{ env.PATH }} PATH: /usr/bin:/bin:$HOME/.local/bin:$HOME/.cursor/bin:${{ env.PATH }}

View file

@ -5,16 +5,46 @@ You are an autonomous AI software engineer working on the mnemo_cards project. Y
## Project Context ## Project Context
This is a language learning application consisting of multiple components: 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_backend**: Dart backend server (API, data storage)
- **mnemo_cards_common**: Shared code between frontend and backend - **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 ## Your Responsibilities
1. **Read and understand the task** - Analyze the requirements and acceptance criteria 1. **Read and understand the task** - Analyze requirements from `task.md` or user input.
2. **Implement the solution** - Write clean, maintainable code following project conventions 2. **Manage State** - Create/Update `workflow_state.md`, `PROGRESS.md`, `TODO.md`.
3. **Write comprehensive tests** - Ensure all new code has unit tests 3. **Implement the solution** - Write clean code, following `project_config.md`.
4. **Verify your work** - Make sure tests pass and linting is clean 4. **Write comprehensive tests** - Unit tests are mandatory.
5. **Verify your work** - Tests, Lints, Auto-Debug.
## Code Standards ## Code Standards
@ -244,12 +274,11 @@ try {
## Git Workflow ## Git Workflow
Your commits will be automatically created. Make sure your changes are: Your commits will be automatically created. Make sure your changes are:
- **Atomic**: One logical change per commit - **Atomic**: One logical change per commit.
- **Complete**: All files needed for the feature - **Complete**: All files needed for the feature.
- **Tested**: All tests pass - **Tested**: All tests pass.
- **Clean**: Linter passes - **Clean**: Linter passes.
- **Size-limited**: Maximum 500 changed lines per commit - **Targeted**: Prefer surgical refactors over massive rewrites.
- If changes exceed this limit, split the work into multiple commits/tasks
## Reference Materials ## Reference Materials

View file

@ -60,6 +60,9 @@ Each task should have:
**CRITICAL: Tasks must be small and focused. Large tasks MUST be broken down into smaller subtasks.** **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** - **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** - **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 - **Large tasks** (3+ hours): **MUST be broken down** into smaller subtasks before adding to task list

View file

@ -5,9 +5,13 @@ Executes a single task per run: read task -> execute -> test -> commit
import sys import sys
import subprocess import subprocess
import time import time
import json
import re
import urllib.request
import urllib.error
from pathlib import Path from pathlib import Path
from datetime import datetime, timezone 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 config import AgentConfig, get_test_command, get_lint_command
from task_manager import TaskManager, GlobalLock, Task from task_manager import TaskManager, GlobalLock, Task
@ -293,15 +297,16 @@ class AgentOrchestrator:
print("⚠️ No changes to commit") print("⚠️ No changes to commit")
# Push changes (with pull before push to avoid conflicts) # Push changes (with pull before push to avoid conflicts)
# If push fails, _git_push() will automatically create a PR
print("\n📤 Pushing changes...") print("\n📤 Pushing changes...")
if not self._git_push(): push_success = self._git_push()
print("⚠️ Push failed, pulling latest changes and retrying...")
# Pull latest changes before retrying push if not push_success:
if self._git_pull(): print("⚠️ Push failed, attempting to create PR...")
print("✅ Pulled latest changes, retrying push...") # _git_push() already handles PR creation, but if it still failed,
self._git_push() # we'll continue anyway as the changes are committed locally
else: print("⚠️ Changes are committed locally but not pushed")
print("❌ Failed to pull latest changes") print(" You may need to manually push or create a PR")
return True return True
@ -488,8 +493,363 @@ Changes:
# No changes or error # No changes or error
return False 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: 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 # First, fetch to check if there are remote changes
fetch_result = subprocess.run( fetch_result = subprocess.run(
["git", "fetch"], ["git", "fetch"],
@ -518,41 +878,82 @@ Changes:
if not self._git_pull(): if not self._git_pull():
print("❌ Failed to pull, push may fail") print("❌ Failed to pull, push may fail")
# Now try to push # If we're on master, try to push directly
result = subprocess.run( if current_branch == "master":
["git", "push"], # Now try to push
cwd=self.config.project_root, result = subprocess.run(
capture_output=True, ["git", "push"],
text=True cwd=self.config.project_root,
) capture_output=True,
text=True
if result.returncode == 0: )
print("✅ Pushed to remote")
return True if result.returncode == 0:
else: print("✅ Pushed to remote")
# If push failed due to remote changes, try pull and push again return True
if "fetch first" in result.stderr or "Updates were rejected" in result.stderr:
print("⚠️ Push rejected due to remote changes")
print(" Pulling and retrying push...")
if self._git_pull():
# Retry push after pull
retry_result = subprocess.run(
["git", "push"],
cwd=self.config.project_root,
capture_output=True,
text=True
)
if retry_result.returncode == 0:
print("✅ Pushed to remote after pull")
return True
else:
print(f"❌ Push failed after pull: {retry_result.stderr}")
return False
else:
print("❌ Failed to pull before retry")
return False
else: else:
print(f"⚠️ Push failed: {result.stderr}") # If push failed due to remote changes, try pull and push again
if "fetch first" in result.stderr or "Updates were rejected" in result.stderr:
print("⚠️ Push rejected due to remote changes")
print(" Pulling and retrying push...")
if self._git_pull():
# Retry push after pull
retry_result = subprocess.run(
["git", "push"],
cwd=self.config.project_root,
capture_output=True,
text=True
)
if retry_result.returncode == 0:
print("✅ Pushed to remote after pull")
return True
else:
print(f"❌ Push failed after pull: {retry_result.stderr}")
# 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:
# 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 return False
def _create_issue_for_failed_task(self, task: Task) -> None: def _create_issue_for_failed_task(self, task: Task) -> None:

View file

@ -24,6 +24,14 @@ class AgentConfig:
cursor_api_key: Optional[str] = None cursor_api_key: Optional[str] = None
cursor_model: str = "claude-3-5-sonnet-20241022" 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 # State file paths
@property @property
def agent_dir(self) -> Path: def agent_dir(self) -> Path:
@ -70,6 +78,9 @@ class AgentConfig:
timeout_hours=int(os.getenv("TIMEOUT_HOURS", "6")), timeout_hours=int(os.getenv("TIMEOUT_HOURS", "6")),
cursor_api_key=os.getenv("CURSOR_API_KEY"), cursor_api_key=os.getenv("CURSOR_API_KEY"),
cursor_model=os.getenv("CURSOR_MODEL", "claude-3-5-sonnet-20241022"), 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: def validate(self) -> None:

110
tools/agent/doc_updater.py Normal file
View 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()