Status: Implementation complete for MVP scope. This document describes the architecture and design principles. Audience: engineers maintaining or extending GitVibe. Version: 0.1.0 Goal: a GitHub-like agent coding management system where each WorkItem owns a single persistent workspace (git worktree + branch), agent runs are serialized, backend auto-commits after each run, and merge is controlled via a first-class Pull Request model.
-
One WorkItem = one workspace
- A workspace is a git worktree checked out on a dedicated branch.
- The workspace persists across multiple agent runs.
-
PR-first UX
- Users work through a Pull Request view: diffs, commits, checks (agent runs), approvals, and merge controls.
- A WorkItem typically has exactly one PR (1:1 by default).
-
No concurrent agent runs per WorkItem
- Enforced with a lock at the WorkItem level.
- Merge operations also coordinate with this lock.
-
Backend auto-commits after each agent run
- Agents may edit files freely. At run end, backend stages + commits changes (if any).
- This produces a clean commit history and stable PR diffs.
-
sessionId is required
- Every AgentRun has a
session_iddetermined before launching the agent. - For Claude Code (and similar), we pass
--session-id=<session_id>at launch. - Resume is implemented by re-launching with the same session_id (agent-specific semantics).
- Every AgentRun has a
-
Single source of truth for "running state"
- Running state is driven by
agent_runs.status. - WorkItem/PR statuses represent lifecycle, not execution.
- Running state is driven by
Represents a unit of work and owns a persistent workspace.
Key responsibilities
- Own the workspace (worktree path + head branch)
- Provide a stable target for agent runs
- Provide metadata for PR creation and review
A first-class entity controlling review and merge.
Key responsibilities
- Define base and head (branch and/or SHA)
- Render diff and commits
- Track approvals and merge gates
- Execute merge into base branch under controlled rules
An immutable-ish execution record per run attempt.
Key responsibilities
- Track status, logs, timestamps
- Record head SHA before and after
- Persist
session_id(required) - Associate to a WorkItem (and indirectly to its PR)
A destination repository for importing patches.
Key responsibilities
- Store target repository path and default branch
- Track import history from PRs
- Create WorkItem
- Initialize workspace (explicit or implicit)
- Open PR (often auto-created)
- Trigger AgentRun(s) until satisfied
- Review PR diff/commits, optionally approve
- Merge PR (squash/merge/rebase)
- Close WorkItem / PR lifecycle completed
- (Optional) Import patch to target repository
GitVibe uses a relay repository (local or server-side) as the execution environment:
- Holds a clone of the "project repo" (or a managed repo)
- Creates worktrees for WorkItems
- Runs agents in worktrees
- Performs merges in the relay repo
If you later integrate with GitHub-hosted PRs, you can map this PR model to GitHub via API. This PLAN assumes GitVibe controls the repo locally (relay) for simplicity and reliability.
- Base branch: typically
main(configurable per project viadefault_branch) - WorkItem head branch:
wi/<work_item_id>(deterministic, same WorkItem always gets same branch) - Worktree directory:
<storage_base_dir>/worktrees/<work_item_id>/
PR diff correctness depends on base selection.
Recommended:
- On PR creation, store a frozen
base_shafrombase_branch. - Allow explicit "Update base" action later if desired.
Names are suggestions; adjust to your stack. Use UUIDs if preferred; examples use integer IDs for readability.
id(UUID)name(unique)source_repo_path(path to source repository)source_repo_url(optional, for reference)relay_repo_path(path to relay repo clone)default_branch(e.g.,main)default_agent(e.g.,opencode,claudecode)agent_params(JSON string for agent configuration)max_agent_concurrency(default: 3)- timestamps
id(UUID)project_id(foreign key)type(issue|feature-request)titlebody(optional description)status(open|closed)- workspace fields
workspace_status(not_initialized|ready|error)worktree_path(unique)head_branch(unique within project, format:wi/<work_item_id>)base_branch(from project default)base_sha(set when workspace initialized)head_sha(cached; update after runs and on demand)
- locking fields (to serialize runs/merge)
lock_owner_run_id(nullable)lock_expires_at(nullable; for crash recovery)
- timestamps
Constraints:
- unique
(project_id, head_branch)(enforced via index) - unique
worktree_path(enforced via index)
id(UUID)project_id(foreign key)work_item_id(unique, enforcing 1:1 by default)title(from work item)description(from work item body)status(open|merged|closed)source_branch(from work item head_branch)target_branch(from work item base_branch)merge_strategy(merge|squash|rebase, default:merge)merged_at(nullable)merged_by(nullable, currently 'system')merge_commit_sha(nullable)synced_commit_sha(nullable, for source repo sync)- timestamps
Constraints:
- unique
work_item_id(enforced)
Note: Base SHA and head SHA are tracked in the WorkItem, not duplicated in PR table.
id(UUID)project_id(foreign key)work_item_id(foreign key)agent_key(e.g.,opencode,claudecode)session_id(required; WorkItem-scoped by default:wi-<work_item_id>)status(queued|running|succeeded|failed|cancelled)input_summary(truncated prompt for display)input_json(full prompt and config as JSON)linked_agent_run_id(nullable, for resume/correction chains)log(text, for small logs)log_path(file path for large logs)stdout_path(file path for stdout)stderr_path(file path for stderr)head_sha_before(SHA before run)head_sha_after(SHA after auto-commit; may equal before if no changes)commit_sha(the auto-commit SHA if created; nullable if no changes)started_at(nullable)finished_at(nullable)- timestamps
Indexes:
(work_item_id)(for listing runs per work item)(session_id)(for session-based queries)(status)(for filtering by status)
id(UUID)pull_request_id(foreign key)status(open|resolved|outdated)severity(info|warning|error)anchor(file path and line reference, JSON stringified)- timestamps
id(UUID)thread_id(foreign key)body(comment text)- timestamps
id(UUID)namerepo_path(unique, path to target repository)default_branch- timestamps
id(UUID)pull_request_id(foreign key)target_repo_id(foreign key)strategy(patch- currently only patch strategy)status(pending|running|succeeded|failed)source_base_sha(from PR base)source_head_sha(from PR head)target_base_sha(target repo SHA before import)target_result_sha(target repo SHA after import)log(import log text)started_at(nullable)finished_at(nullable)- timestamps
idpull_request_iduser_idstate(approved|rejected)- timestamps
GET /api/projects- List all projects with paginationPOST /api/projects- Create a projectGET /api/projects/:id- Get project detailsPATCH /api/projects/:id- Update project settingsDELETE /api/projects/:id- Delete a projectPOST /api/projects/:id/sync- Sync relay repo with source repoGET /api/projects/:id/branches- List branchesGET /api/projects/:id/files- List repository filesGET /api/models- List available agent modelsPOST /api/models/refresh- Refresh model cache
GET /api/target-repos- List all target reposPOST /api/target-repos- Create a target repoGET /api/target-repos/:id- Get target repo details
GET /api/workitems- List work items with optional project filter and paginationPOST /api/projects/:projectId/work-items- Create a work itemGET /api/workitems/:id- Get work item detailsPATCH /api/workitems/:id- Update work itemDELETE /api/workitems/:id- Delete work itemPOST /api/work-items/:id/init-workspace- Initialize workspace (optional)POST /api/workitems/:id/start- Start agent runPOST /api/workitems/:id/resume- Resume task with same session_idGET /api/workitems/:id/tasks- List all runs for work itemPOST /api/workitems/:id/tasks/:taskId/cancel- Cancel running taskPOST /api/workitems/:id/tasks/:taskId/restart- Restart task with same promptGET /api/workitems/:id/tasks/:taskId/status- Get task statusGET /api/workitems/:id/prs- Get PRs for work itemPOST /api/workitems/:id/create-pr- Create PR from work item
GET /api/pull-requests- List PRs (with optional project filter and pagination)GET /api/pull-requests/:id- Get PR detailsGET /api/pull-requests/:id/diff- Get PR diffGET /api/pull-requests/:id/commits- Get PR commitsGET /api/pull-requests/:id/commits-with-tasks- Get PR commits grouped by tasksGET /api/pull-requests/:id/statistics- Get PR statisticsPOST /api/pull-requests/:id/merge- Merge PRPOST /api/pull-requests/:id/close- Close PR without mergePOST /api/pull-requests/:id/update-base- Update base branch and optionally rebaseGET /api/pull-requests/:id/patch- Export patch (optional)
GET /api/agent-runs/:id- Get run status and logsPOST /api/agent-runs/:id/cancel- Cancel running agentGET /api/agent-runs/:id/stdout- Get stdout logGET /api/agent-runs/:id/stderr- Get stderr logGET /api/agent-runs/:id/logs- Get both stdout and stderr logs
GET /api/pull-requests/:id/reviews/threads- List review threadsPOST /api/pull-requests/:id/reviews/threads- Create threadGET /api/pull-requests/:id/reviews/threads/:threadId- Get thread detailsPOST /api/pull-requests/:id/reviews/threads/:threadId/resolve- Resolve threadPOST /api/pull-requests/:id/reviews/threads/:threadId/unresolve- Unresolve threadPOST /api/pull-requests/:id/reviews/threads/:threadId/comments- Add commentPOST /api/pull-requests/:id/reviews/threads/:threadId/address- Address with agentPOST /api/pull-requests/:id/reviews/threads/:threadId/resume- Resume from thread
Recommended default:
- Initialize workspace automatically on the first AgentRun request
- Still provide explicit init endpoint for admin/troubleshooting
Given project.repo_path and work_item:
- Ensure relay repo is present and clean enough for operations.
- Fetch/refresh base branch if needed.
- Resolve base SHA:
base_sha = git rev-parse <base_branch>
- Create head branch name:
head_branch = "wi/<workItemId>"
- Create worktree:
git worktree add -b <head_branch> <worktree_path> <base_branch>
- Persist:
worktree_path, head_branch, base_branch, base_sha, head_sha=base_shaworkspace_status=ready
Idempotency:
- If worktree exists and is valid, return success and refresh
head_sha.
Before starting a run:
- Acquire WorkItem lock:
- if
lock_owner_run_idis set and not expired → reject (409 Conflict) - else set
lock_owner_run_id = runIdandlock_expires_at = now + TTL
- if
- Refresh TTL heartbeat periodically while running
- Release lock in
finallyon success/failure/cancel
Also enforce:
- Only one
agent_runs.status in (queued, running)per work item.
session_id must be known before spawning the agent.
Recommended default policy options (pick one and document it):
- WorkItem-scoped session (best for "continuous conversation"):
session_id = "wi-" + work_item_id
- Run-scoped session (best for strict audit isolation):
session_id = "run-" + agent_run_id
This PLAN assumes WorkItem-scoped unless caller overrides.
- Check project concurrency limit (enforced per project, not just per WorkItem)
- Ensure workspace initialized (
worktree_pathexists viaensureWorkspace) - Acquire WorkItem lock (with TTL for crash recovery)
- Determine
head_sha_before = git rev-parse HEADin worktree - Create AgentRun row with
status=running,session_id,head_sha_before - Spawn agent asynchronously with:
- CWD = worktree_path
- Agent-specific arguments (e.g.,
--session-idfor ClaudeCode) - Logs streamed to files (
log_path,stdout_path,stderr_path)
- Agent adapter handles process lifecycle and updates status
- On agent completion (via adapter callback):
- Call
finalizeAgentRun():- Stage changes:
git add -A - Check if staged changes exist
- If changes: commit with message
AgentRun <id>: <input_summary> - Capture
commit_shaandhead_sha_after
- Stage changes:
- Update AgentRun: status, finished_at, head_sha_after, commit_sha
- Update WorkItem cached
head_sha - Release lock and untrack from project concurrency
- Call
- Error handling: On failure, still attempt finalization but mark status as
failed
If agent fails:
- Still attempt to capture logs
- Still attempt to stage/commit? Recommended:
- Do NOT auto-commit on failure by default to avoid committing partial changes.
- Provide an admin setting
commit_on_failureif you want.
- Leave workspace as-is for debugging/resume.
PR diff is computed from frozen base SHA to current head SHA:
git diff --no-color <base_sha>..<head_sha>
Option A (simple):
git log --oneline <base_sha>..<head_sha>
Option B (more GitHub-like):
- compute merge-base and list commits reachable from head not from base.
Define a mergeability function that returns:
mergeable: true/falsereasons: [](strings)
Minimal checks:
- PR.status == open
- No AgentRun running for WorkItem
- Workspace lock is free
- Head is not behind base in a conflicting way (optional)
- No conflicts when merging head into base (recommended)
Merging must coordinate with WorkItem lock:
- Acquire the same WorkItem lock for merge
- Reject merge if a run is currently running
Given PR base_branch, head_branch in relay repo.
git checkout <base_branch>git merge --no-ff <head_branch> -m "Merge PR #<id>: <title>"- record
merge_commit_sha
git checkout <base_branch>git merge --squash <head_branch>git commit -m "Squash PR #<id>: <title>"- record
merge_commit_sha
git checkout <head_branch>git rebase <base_branch>git checkout <base_branch>git merge --ff-only <head_branch>- record resulting base HEAD as merge sha
Before merge, test mergeability:
git checkout <base_branch>git merge --no-commit --no-ff <head_branch>(dry-ish)- If conflicts:
- abort
git merge --abort - return mergeable=false with reason
conflicts
- abort
- If no conflicts:
- abort (if just testing) and proceed with chosen strategy
- Set PR status to
merged - Set WorkItem status optionally to
closed - Update cached SHAs
- Optionally clean up workspace:
- keep worktree for audit, or
- prune worktree after merge (configurable)
flowchart TB
U[User] --> UI[GitVibe UI]
UI --> API[GitVibe API]
API --> DB[(Database)]
API --> GIT[Relay Git Repo]
API --> AG[Agent Runner]
AG -->|reads/writes| WT[Worktree (WorkItem Workspace)]
WT --> GIT
API --> UI
sequenceDiagram
autonumber
participant UI as UI
participant API as API
participant DB as DB
participant G as Git (relay repo)
UI->>API: POST /work-items/:id/init-workspace (optional)
API->>DB: Load WorkItem + Project config
API->>G: git rev-parse base_branch -> base_sha
API->>G: git worktree add -b head_branch worktree_path base_branch
API->>G: git -C worktree rev-parse HEAD -> head_sha
API->>DB: Update WorkItem(worktree_path, head_branch, base_sha, head_sha, status=ready)
API-->>UI: 200 OK (workspace ready)
sequenceDiagram
autonumber
participant UI as UI
participant API as API
participant DB as DB
participant G as Git
participant R as Agent Runner
participant WT as Worktree
UI->>API: POST /work-items/:id/start {agent_key, prompt, session_id?}
API->>DB: Load WorkItem
alt workspace not initialized
API->>API: initWorkspace(workItem)
end
API->>DB: Acquire WorkItem lock (lock_owner_run_id=runId, TTL)
alt lock busy
API-->>UI: 409 Conflict (run already in progress)
end
API->>G: git -C WT rev-parse HEAD -> head_before
API->>DB: Create AgentRun(status=running, session_id, head_sha_before)
API->>R: spawn agent (cwd=WT, --session-id session_id)
R->>WT: agent edits files
R-->>API: stream logs (log_path, stdout_path, stderr_path)
R-->>API: process exit (code)
API->>G: git -C WT add -A
API->>G: git -C WT diff --cached --quiet?
alt changes present
API->>G: git -C WT commit -m "AgentRun #id: <summary>" -> commit_sha
else no changes
API->>API: commit_sha = null
end
API->>G: git -C WT rev-parse HEAD -> head_after
alt agent exit success
API->>DB: Update AgentRun(status=succeeded, head_after, commit_sha, finished_at)
else agent exit failure
API->>DB: Update AgentRun(status=failed, head_after, commit_sha?, finished_at, error)
end
API->>DB: Update WorkItem.head_sha = head_after
API->>DB: Update PR.head_sha = head_after (if PR exists)
API->>DB: Release WorkItem lock
API-->>UI: 200 OK {agent_run_id}
stateDiagram-v2
[*] --> NoRunYet
NoRunYet --> Running: startRun(session_id = policy or provided)
Running --> Succeeded: agent exits OK + finalize
Running --> Failed: agent exits nonzero + finalize
Running --> Canceled: cancel request honored
Succeeded --> Running: resume (new run, same session_id)
Failed --> Running: resume (new run, same session_id)
Canceled --> Running: resume (new run, same session_id)
Note: "resume" creates a new AgentRun record but reuses the same session_id. This keeps execution history immutable and audit-friendly while enabling conversation continuity.
stateDiagram-v2
[*] --> Open
Open --> Open: new AgentRun updates head_sha
Open --> Open: approvals added/removed
Open --> Closed: close without merge
Open --> Merged: merge (if mergeable)
Closed --> [*]
Merged --> [*]
flowchart LR
A[Merge button pressed] --> B{PR status == open?}
B -- no --> X[Reject]
B -- yes --> C{Any AgentRun running?}
C -- yes --> X
C -- no --> D{Conflicts when merging head into base?}
D -- yes --> X
D -- no --> E{Approvals satisfied? (optional)}
E -- no --> X
E -- yes --> F[Perform merge strategy]
F --> G[Mark PR merged, write merge_commit_sha]
For auto-commits, use a consistent format:
AgentRun <id>: <input_summary>Whereinput_summaryis the first 200 characters of the prompt. Full prompt and config stored ininput_json.
Prefer log_path, stdout_path, and stderr_path on disk with rotation; store a small tail in DB if needed.
- Use a TTL on the WorkItem lock (default: 6 hours)
- Lock is released in
finallyblock after agent completion - If TTL expires, new runs can acquire lock (previous run may be marked as failed if detected)
- Current implementation: Lock released immediately after finalization, no heartbeat renewal (simplified)
- Run agents in a sandbox where possible
- Validate prompts/instructions storage (PII/secret handling)
- Restrict file system scope to worktree
Storage paths are configurable via environment variables:
STORAGE_BASE_DIR: Base directory for all GitVibe data- Defaults to system temp directory (
/tmp/git-vibeon Unix,%TEMP%\git-vibeon Windows)
Core Features
- ✅ WorkItem CRUD
- ✅ Workspace init (implicit on first agent run)
- ✅ PR open + PR view (diff + commits)
- ✅ AgentRun start + logs + status
- ✅ Backend auto-commit (after successful runs)
- ✅ WorkItem lock (no concurrent run per WorkItem)
- ✅ Merge (all three strategies: merge, squash, rebase) with conflict detection
- ✅ Project-level concurrency limits (configurable per project)
- ✅ Multiple agent adapters (OpenCode, ClaudeCode)
- ✅ Session-based resume functionality
- ✅ Review threads and comments
- ✅ Patch import to source repositories
- ✅ Agent run cancellation
- ✅ Update base / rebase PR functionality
- ✅ Patch export endpoint (GET /pull-requests/:id/patch)
Additional Features Implemented
- ✅ Models cache for agent adapters
- ✅ Review comment addressing (agent correction)
- ✅ Import job tracking and history
- ✅ Worktree cleanup on WorkItem deletion
- ✅ Comprehensive error handling and logging
- ✅ Separate stdout/stderr log files
- ✅ Git relay repository support
- ✅ Source repository sync functionality
- Approvals / required reviewers
- GitHub integration (sync PR / statuses)
- Distributed runners across machines (job queue + remote workspace)
- Multiple workspaces per WorkItem (non-goal for MVP)
- Real-time log streaming via WebSocket
- File browser in worktree
- Inline code editing in UI
- Multiple workspaces per WorkItem
- Concurrent agents on the same WorkItem (enforced by lock)
- Fully GitHub-compatible review comment threading (basic threading implemented)
- Distributed runners across machines (add later with job queue + remote workspace)
- User authentication/authorization (single-user local-first design)
- Webhooks or external integrations (can be added later)
Backend
- Node.js 20+ + TypeScript
- Fastify web framework
- SQLite database with Drizzle ORM
- Git CLI integration
- Agent adapter system (OpenCode, ClaudeCode)
- Zod for validation
Frontend
- React 18 + TypeScript
- Vite build tool
- TanStack Query for data fetching
- TanStack Router for routing
- Tailwind CSS for styling
- React Hook Form for forms
- Lucide React for icons
Shared
- TypeScript types and Zod schemas
- Shared between backend and frontend
Two agent adapters are implemented:
- OpenCodeAgentAdapter: For OpenCode CLI agent
- ClaudeCodeAgentAdapter: For Claude Code agent
Both extend AgentAdapter base class and implement:
validate(): Check executable availabilityrun(): Execute agent with promptcorrectWithReviewComments(): Resume/correct with review feedbackgetModels(): List available modelscancel(): Cancel running processgetStatus(): Check run status
Projects have a max_agent_concurrency setting (default: 3) that limits concurrent agent runs across all WorkItems in a project. This is tracked in-memory by AgentService.
Storage paths are configurable via environment variables:
STORAGE_BASE_DIR: Base directory for all GitVibe data- Defaults to system temp directory (
/tmp/git-vibeon Unix,%TEMP%\git-vibeon Windows)
Directory structure:
git-vibe/
├── data/
│ └── db.sqlite # SQLite database
├── logs/ # Agent run logs
│ ├── agent-run-<id>.log
│ ├── agent-run-<id>-stdout.log
│ └── agent-run-<id>-stderr.log
└── worktrees/ # Git worktrees for WorkItems
└── <work_item_id>/ # WorkItem workspace
Two migration systems supported:
- Drizzle Kit migrations (recommended): Uses
drizzle-kit generateanddrizzle-orm/migrator - Raw SQL migrations: Fallback for
.sqlfiles indrizzle/directory
Migration system auto-detects which to use based on presence of drizzle/meta/_journal.json.
Git operations are organized into specialized services:
- GitService: Main facade for all Git operations
- GitWorktreeService: Worktree-specific operations
- GitCommitService: Commit, log, and diff operations
- GitFileService: File listing and content operations
- GitRelayService: Relay repository operations
This separation provides better organization and testability.
The frontend is organized into:
- Routes: TanStack Router routes for pages
- Components: Reusable UI components organized by feature
- Hooks: Custom React hooks for data fetching and state management
- Lib: API client and utility functions
Key components:
- Project shell with tab navigation (Overview, Code, Pull Requests, WorkItems, Settings, Actions)
- PR detail view with tabs (Overview, Diff, Commits, Files Changed, Checks, Reviews)
- WorkItem detail view with tabs (Discussion, Log Detail, PR Status, Task Management, Agent Config)
- Diff viewer for code changes
- Review thread composer and management
# Install dependencies
npm run install:all
# Run database migrations
npm run db:migrate
# Start development servers
npm run devThis starts:
- Backend API server at
http://127.0.0.1:11031 - Frontend UI at
http://localhost:11990
# Build all packages
npm run build
# Build individual packages
npm run build:backend
npm run build:frontend
npm run build:shared# Run tests (Vitest)
cd backend && npm test
# Run tests once
cd backend && npm run test:run# Lint all packages
npm run lint
# Format all packages
npm run format
# Lint/format individual packages
npm run lint:backend
npm run format:backend
# etc.See the separate API documentation or the frontend api.ts file for complete API reference.
Key endpoints:
- Projects:
/api/projects - Target Repos:
/api/target-repos - WorkItems:
/api/workitems - Pull Requests:
/api/pull-requests - Agent Runs:
/api/agent-runs - Reviews:
/api/pull-requests/:id/reviews
If you get "Executable not found" errors:
- Verify the agent executable is in your PATH
- Or provide the full path in project settings
- Check that the executable has execute permissions
If a WorkItem is stuck in locked state:
- Check if an agent run is actually running
- If not, the lock TTL will expire (default: 6 hours)
- Or manually release the lock via database
If worktree operations fail:
- Ensure the relay repository path is correct
- Check that the repository is a valid Git repo
- Run
git worktree pruneto clean up stale worktrees
If merge fails due to conflicts:
- Update the PR base to the latest base branch
- Rebase the head branch onto the new base
- Resolve conflicts manually in the worktree
- Try merge again
When contributing to GitVibe:
- Follow the existing code style (ESLint + Prettier)
- Add tests for new features
- Update this PLAN.md for architectural changes
- Update README.md for user-facing changes
- Ensure all packages build successfully
MIT