Base URL: http://localhost:4000
Two API surfaces:
- UI API (
/api/*) — called by the React frontend and external tools - Agent API (
/agent/*) — called by agents during task execution
These endpoints are what agents call to get their task context and signal
completion. Defined in server/src/agent-api.ts.
Returns everything the agent needs to execute its task, including rework context when the agent is on a subsequent review cycle.
Response:
{
"id": "uuid",
"title": "Fix the login bug",
"description": "Users can't log in on mobile",
"branch": "issue/abc123-fix-the-login-bug",
"worktreePath": "/tmp/hermes-worktrees/uuid/",
"repoPath": "/home/user/project",
"targetBranch": "master",
"isRework": true,
"attempt": 2,
"changedFiles": ["src/auth.ts", "src/auth.test.ts"],
"previousReviews": [
{
"author": "hermes-reviewer",
"verdict": "changes_requested",
"body": "VERDICT: CHANGES_REQUESTED\n- Missing null check in auth handler\n- Add test for expired tokens",
"createdAt": 1700000002,
"isLatest": true,
"actionItems": ["Missing null check in auth handler", "Add test for expired tokens"]
},
{
"author": "hermes-reviewer",
"verdict": "changes_requested",
"body": "...",
"createdAt": 1700000001,
"isLatest": false,
"actionItems": []
}
],
"recentMerges": [
{
"title": "Add user profile page",
"changedFiles": ["src/profile.ts", "src/routes.ts"],
"mergedAt": 1700000000
}
],
"reviewUrl": "http://localhost:4000/agent/uuid/review",
"screenshotUploadUrl": "http://localhost:4000/agent/uuid/screenshots",
"screenshotUploadInstructions": "...",
"guidelines": {
"screenshots": "...",
"requireScreenshotsForUiChanges": true
}
}Rework context fields:
| Field | Type | Description |
|---|---|---|
isRework |
boolean |
true if the issue has been in review before (a PR exists) |
attempt |
number |
Which review cycle this is (1 = first attempt, 2 = first rework, etc.). Counts all comments containing a VERDICT: line, regardless of author. |
changedFiles |
string[] |
Files the agent changed in its branch (from git diff --name-only) |
previousReviews |
ReviewInfo[] |
All PR comments sorted latest first. Each has a verdict parsed from that comment's body (not the PR-level verdict — can be null for comments without a VERDICT: line). isLatest flag is set on the latest hermes-reviewer comment specifically. actionItems are extracted bullet/numbered items, excluding VERDICT: lines. |
recentMerges |
RecentMerge[] |
Last 5 merged PRs (excluding the current issue's own PR) with titles and changed files (warns about potential conflicts) |
Agent calls this when done working. Kills agent terminal, creates PR, spawns adversarial reviewer.
Request body (optional):
{ "details": "Notes about what was changed" }Query params:
no_ui_changes=true— bypass screenshot requirement
Response:
{ "ok": true, "status": "review", "message": "..." }Error (400): Returns if UI files were changed but no screenshots uploaded.
Upload a screenshot for the task.
curl -X POST --data-binary @screenshot.png \
-H 'Content-Type: image/png' \
'http://localhost:4000/agent/:id/screenshots?filename=my-screenshot.png&description=Before+changes'Response (201):
{ "url": "/screenshots/id/file.png", "fullUrl": "http://...", "markdown": "", "filename": "..." }List uploaded screenshots for an issue.
Report structured progress during agent execution. Progress is transient
(in-memory only, not persisted to SQLite) and automatically cleared when the
issue leaves in_progress status.
Request body:
{ "message": "Running tests...", "percent": 75 }Both fields are optional, but at least one must be provided. An empty body
{} is rejected with 400.
Validation rules:
| Field | Type | Constraints |
|---|---|---|
message |
string |
Optional. Must be a string if provided. Truncated to 200 characters server-side. |
percent |
number |
Optional. Must be a finite number between 0 and 100 (inclusive). NaN, Infinity, and -Infinity are rejected. |
Response (200):
{ "ok": true, "message": "Running tests...", "percent": 75 }Errors:
| Status | Condition |
|---|---|
| 404 | Issue not found |
| 400 | Issue is not in_progress |
| 400 | Empty body (neither message nor percent provided) |
| 400 | message is not a string |
| 400 | percent is not a finite number between 0–100 |
Side effects:
- Updates the in-memory
progressMessage,progressPercent, andprogressUpdatedAtfields on the issue - Broadcasts an
issue:progressWebSocket event to all connected clients
Defined in server/src/issue-api.ts.
List all issues.
Response: Issue[]
Get a single issue.
Create a new issue (starts in backlog status).
Request body:
{
"title": "Fix the bug", // required
"description": "...", // optional
"agent": "hermes", // optional, default: "hermes"
"command": "custom command...", // optional, overrides preset
"branch": "custom-branch", // optional
"parentId": "uuid" // optional, makes this a subtask
}Response (201): Issue
Update issue fields (title, description, command, branch).
Change issue status. This is the trigger for all side effects:
| Transition | Side Effect |
|---|---|
→ in_progress |
Creates worktree + spawns agent terminal |
→ review |
Creates PR + spawns adversarial reviewer |
→ done |
Kills terminal, cleans up worktree |
→ backlog/todo |
Kills terminal if running |
Request body:
{ "status": "in_progress" }Valid statuses: backlog, todo, in_progress, review, done
Delete issue (cascade-deletes subtasks, kills terminal, cleans worktree).
List subtasks of an issue.
Create a subtask under an issue. Same body as POST /api/issues.
Start a planning terminal for a backlog issue.
Stop the planning terminal.
Defined in server/src/issue-api.ts.
List available agent presets with install status.
Response: AgentPreset[] with installed: boolean field.
Defined in server/src/terminal-api.ts.
List all active terminals.
Create a manual terminal.
Request body:
{ "title": "My terminal", "command": "bash", "cwd": "/path", "cols": 80, "rows": 24 }Kill a terminal.
Resize a terminal. Body: { "cols": 120, "rows": 40 }
Defined in server/src/pr-api.ts.
List all PRs (enriched with screenshot data).
Get single PR with comments and screenshots.
Add a comment to a PR.
Request body:
{ "author": "human", "body": "Looks good!", "file": "src/index.ts", "line": 42 }Set verdict on a PR.
Request body:
{ "verdict": "approved" } // or "changes_requested"Kill existing reviewer and spawn a new one. Cannot relaunch on merged/closed PRs.
Check if merge would have conflicts (dry-run, async).
Response:
{ "canMerge": true, "hasConflicts": false }Spawn an agent to resolve merge conflicts.
Merge the PR branch. Behavior depends on mergeMode config:
| Mode | Behavior |
|---|---|
local (default) |
Merge locally, move issue to done. If githubEnabled, push merge + close GH PR. |
github |
Push branch + create GitHub PR. Issue stays in review. Returns { status: 'github_pr_created', prUrl: '...' }. |
both |
Create GitHub PR first, then merge locally. Issue moves to done. GH PR creation is best-effort — local merge proceeds even if it fails. |
Confirm that a PR was merged on GitHub. Used when mergeMode is github to mark the PR as merged after the actual merge happened on GitHub.
- Marks PR status as
merged - Moves the linked issue to
done - Cleans up the local branch and worktree
- Deletes the remote branch (if
githubEnabled)
Response: The updated PullRequest object.
Errors:
| Status | Condition |
|---|---|
| 404 | PR not found |
| 400 | PR is already merged or closed |
List screenshots associated with a PR (via its linked issue).
Defined in server/src/pr-api.ts.
Get current app configuration.
Update configuration.
Request body:
{ "repoPath": "/path/to/repo", "worktreeBase": "/tmp/hermes-worktrees", "reviewBase": "/tmp/hermes-reviews", "screenshotBase": "/tmp/hermes-screenshots", "targetBranch": "main", "requireScreenshotsForUiChanges": true, "mergeMode": "local" }mergeMode options:
| Value | Description | Env var |
|---|---|---|
local (default) |
Merge locally, optionally push to GitHub | HERMES_MERGE_MODE=local |
github |
Push branch + create GitHub PR, skip local merge | HERMES_MERGE_MODE=github |
both |
Merge locally AND create GitHub PR | HERMES_MERGE_MODE=both |
Defined in server/src/batch-api.ts. Mounted at /api/batch.
Enables a manager agent (or the ManagerView UI) to perform bulk operations in a single call instead of making individual API calls for each action.
Merges all PRs with verdict=approved and status!=merged. Handles
conflicts by spawning fixer agents automatically.
Note: PRs are merged sequentially. Each successful merge changes the git state on the target branch, so later PRs may conflict even if they would merge cleanly individually. The order of PRs in the internal list affects which ones succeed. Conflict handling (spawning fixers) covers this case automatically.
Response:
{
"merged": [{ "id": "uuid", "title": "...", "status": "merged" }],
"conflicts": [{ "id": "uuid", "title": "..." }],
"errors": [{ "id": "uuid", "title": "...", "error": "..." }]
}| Array | Description |
|---|---|
merged |
PRs that were successfully merged (linked issues moved to done) |
conflicts |
PRs that had merge conflicts (fixer agents spawned automatically) |
errors |
PRs that failed to merge for other reasons |
Restarts crashed agents — issues in todo status that have a non-null
branch (indicating they were previously started but crashed back to todo).
Fresh todo issues that were never started (branch=null) are not affected.
Moves matching issues to in_progress which spawns a new agent terminal.
Response:
{
"restarted": [{ "id": "uuid", "title": "..." }],
"errors": [{ "id": "uuid", "title": "...", "error": "..." }]
}Finds all PRs in reviewing status with no live reviewer terminal and
relaunches their reviews.
Response:
{
"relaunched": [{ "prId": "uuid", "title": "..." }],
"errors": [{ "prId": "uuid", "title": "...", "error": "..." }]
}For all PRs with verdict=changes_requested (excluding merged/closed PRs),
moves their linked issue back to in_progress (only if the issue is
currently in review status). Also resets the PR to open/pending so
the dashboard correctly reflects the send-back.
Response:
{
"sentBack": [{ "issueId": "uuid", "title": "..." }]
}Returns a comprehensive manager dashboard in one call.
Response:
{
"done": 5,
"active": 3,
"inProgress": [{ "id": "uuid", "title": "...", "agent": "hermes" }],
"review": [{ "id": "uuid", "title": "..." }],
"todo": [{ "id": "uuid", "title": "..." }],
"approvedPrs": [{ "id": "uuid", "title": "...", "issueId": "uuid" }],
"changesRequested": [{ "id": "uuid", "title": "...", "issueId": "uuid" }],
"deadReviewers": [{ "id": "uuid", "title": "...", "issueId": "uuid" }],
"terminalCount": 8
}| Field | Description |
|---|---|
done |
Count of issues with status done |
active |
Count of issues with status in_progress |
inProgress |
List of in-progress issues with agent info |
review |
List of issues in review |
todo |
List of todo issues |
approvedPrs |
PRs approved but not yet merged |
changesRequested |
PRs with changes requested (excludes merged/closed) |
deadReviewers |
PRs in reviewing status with no live reviewer terminal |
terminalCount |
Total number of active terminals |
Defined in server/src/git-api.ts. All endpoints validate inputs to prevent injection.
Get commit graph for the repo.
Query params:
limit— max commits (default: 50, max: 200)branch— branch name or--all(default:--all)
Response:
{ "commits": [GitCommit], "graph": [GraphNode] }Get files changed in a commit with additions/deletions.
Get full diff for a commit. Optional ?file=path for single-file diff.
List all branches.
Connect to ws://localhost:4000/ws
{ "type": "stdin", "terminalId": "uuid", "data": "ls\n" }
{ "type": "resize", "terminalId": "uuid", "cols": 120, "rows": 40 }
{ "type": "replay", "terminalId": "uuid" }replay requests scrollback replay for a specific terminal (e.g. on component mount).
On new connections, the server automatically replays scrollback for all active terminals.
{ "type": "stdout", "terminalId": "uuid", "data": "..." }
{ "type": "exit", "terminalId": "uuid", "exitCode": 0 }
{ "type": "terminal:removed", "terminalId": "uuid" }
{ "type": "error", "terminalId": "uuid", "message": "..." }
{ "type": "terminal:awaitingInput", "terminalId": "uuid", "awaitingInput": true }
{ "type": "issue:created", "issue": Issue }
{ "type": "issue:updated", "issue": Issue }
{ "type": "issue:deleted", "issueId": "uuid" }
{ "type": "issue:progress", "issueId": "uuid", "message": "string | null", "percent": "number | null" }
{ "type": "pr:created", "pr": PullRequest }
{ "type": "pr:updated", "pr": PullRequest }