Skip to content

Latest commit

 

History

History
1225 lines (936 loc) · 35 KB

File metadata and controls

1225 lines (936 loc) · 35 KB

patchdeck — Local API & MCP Server

Security notice: Loopback API access is allowed without login. Remote web and API access is disabled unless PATCHDECK_WEB_USERNAME and PATCHDECK_WEB_PASSWORD are set, and then requires a signed dashboard session. Put TLS in front of the server before using remote access on an untrusted network.


Table of contents

  1. Overview
  2. Quick start
  3. Security model
  4. MCP server (for AI agents)
  5. REST API reference
  6. Data types
  7. Error handling
  8. Environment variables

Overview

patchdeck runs an Express HTTP server (default port 5001) that serves both its React dashboard and a machine-readable REST API. The same API surface is also exposed as an MCP (Model Context Protocol) server, letting any MCP-compatible agent (Claude Desktop, OpenClaw, etc.) drive patchdeck through structured tool calls without writing a single line of HTTP client code.

┌─────────────────────────────────┐
│  local-first machine            │
│                                 │
│  ┌──────────┐    HTTP           │
│  │ OpenClaw │──────────────┐    │
│  │ / Claude │   127.0.0.1  │    │
│  │ Desktop  │   :5001/api  │    │
│  └──────────┘              ▼    │
│       │              ┌──────────┤
│       │ stdio MCP    │ patchdeck │
│       └─────────────►│ server   │
│                       │ server  │
│                       └─────────┤
│                            │    │
│                       SQLite DB │
│                       ~/.patchdeck/
└─────────────────────────────────┘

Most long-running runtime actions are durable and queue-backed. Repository sync, initial and manual babysit/apply runs, feedback retries, PR question answering, release processing, and merge-triggered deployment healing are first persisted in SQLite and then claimed by a dispatcher with leases and heartbeats. Queued work survives process restarts; on startup, expired leases are re-queued, and interrupted babysitter runs are resumed from stored run context when possible.


Quick start

1. Start the patchdeck server

# development (auto-reloads)
npm run dev

# production
npm run build && npm start

The server binds to 0.0.0.0:5001 (configurable via PORT). Requests from 127.0.0.1 or ::1 can call /api/* directly. Remote requests require a web login when PATCHDECK_WEB_USERNAME and PATCHDECK_WEB_PASSWORD are configured; otherwise they are rejected.

If you enable deployment healing for Vercel or Railway repositories, install and authenticate the matching platform CLI on the same machine: vercel for Vercel and railway for Railway.

2. Call the API

# List watched repos
curl http://localhost:5001/api/repos

# Add a PR
curl -X POST http://localhost:5001/api/prs \
  -H "Content-Type: application/json" \
  -d '{"url":"https://github.com/owner/repo/pull/42"}'

3. Use the MCP server

# Start the MCP server (talks to the running patchdeck server on port 5001)
npm run mcp

# Custom port
PATCHDECK_PORT=5001 npm run mcp

Security model

Local-first enforcement

The web access middleware runs before every protected /api/* handler. It checks the resolved IP address of each incoming request:

Source IP Allowed?
127.0.0.1 yes, no login required
::1 yes, no login required
::ffff:127.x.x.x yes, no login required
127.x.x.x (any /8) yes, no login required
Everything else yes only after dashboard login

What this means in practice

  • Processes running on the same machine can call the API without login.
  • Docker containers, VMs, LAN peers, and the internet need a dashboard session.
  • Remote login is opt-in; without credentials, non-loopback API calls receive 403.
  • The UI is served at /; remote browsers see a login screen before API data loads.

Remote dashboard login

Set these variables before starting the server:

PATCHDECK_WEB_USERNAME=operator
PATCHDECK_WEB_PASSWORD='choose-a-long-password'
PATCHDECK_SESSION_SECRET='choose-a-long-random-secret'

PATCHDECK_SESSION_SECRET is optional; when omitted, patchdeck creates an in-memory secret at startup and existing sessions are invalidated on restart. Use HTTPS at the proxy/load-balancer layer before sending credentials across an untrusted network.

The login endpoints are:

Route Description
GET /api/auth/status Return whether the current caller needs login and is authenticated
POST /api/auth/login Authenticate with { "username": "...", "password": "..." }
POST /api/auth/logout Destroy the current dashboard session

Running behind a reverse proxy

If you put Nginx or Caddy in front of patchdeck, make sure it forwards X-Forwarded-For and that Express's trust proxy setting is configured appropriately. If trust proxy is not set, req.ip will always be 127.0.0.1 (the proxy itself) — which is fine for a purely local setup but could allow any traffic the proxy accepts if the proxy is network-facing.


MCP server

The MCP server (server/mcp.ts) implements the Model Context Protocol over stdio, the standard transport used by Claude Desktop and most agent frameworks.

It translates every MCP tool call into an HTTP request to 127.0.0.1:5001 by default, or to the port in PATCHDECK_PORT when configured, so the patchdeck main server must be running for any tool to work.

Claude Desktop / OpenClaw config

Add the following block to your MCP host's configuration file (claude_desktop_config.json for Claude Desktop):

{
  "mcpServers": {
    "patchdeck": {
      "command": "npx",
      "args": ["tsx", "/absolute/path/to/patchdeck/server/mcp.ts"],
      "env": {
        "PATCHDECK_PORT": "5001"
      }
    }
  }
}

Replace /absolute/path/to/patchdeck with the actual path.

If you have already built the project (npm run build), you can use the compiled output instead:

{
  "mcpServers": {
    "patchdeck": {
      "command": "node",
      "args": ["/absolute/path/to/patchdeck/dist/mcp.cjs"],
      "env": {
        "PATCHDECK_PORT": "5001"
      }
    }
  }
}

All MCP tools

Tool name Description
list_repos List watched repositories plus repos inferred from tracked PRs
add_repo Add a repo to the watch list (defaults to My PRs only discovery)
sync_repos Queue an immediate durable sync across all watched repos
list_prs List all tracked pull requests
list_archived_prs List archived (closed/merged) PRs
get_pr Get full PR details including feedback
add_pr Register a PR by GitHub URL and queue its initial babysit run
remove_pr Remove a PR from tracking
fetch_pr_feedback Force-refresh GitHub feedback for a PR
triage_pr Auto-triage all un-triaged feedback on a PR
apply_pr_fixes Queue AI work to apply accepted fixes
babysit_pr Queue a full babysit cycle on a PR
set_feedback_decision Manually override triage for a feedback item
retry_feedback_item Retry a failed/warned feedback item
list_pr_questions List Q&A history for a PR
ask_pr_question Store a PR question and queue asynchronous answering
get_logs Get activity logs (optional PR filter)
get_config Read current configuration
update_config Partially update configuration
get_runtime Get runtime state (drain mode, active queue handlers)
set_drain_mode Enable/disable drain mode for new queue claims
list_changelogs List social-media changelogs
get_changelog Get one changelog by ID
get_onboarding_status Check repo onboarding status
install_review_workflow Install GitHub Actions review workflow
list_deployment_healing_sessions List deployment-healing sessions, optionally filtered by repo
get_deployment_healing_session Get one deployment-healing session by ID

update_config exposes the MCP schema's config subset. Use PATCH /api/config for fields that are REST-writable but not exposed by the MCP schema today, including release automation, CI healing, and deployment-healing keys.


REST API reference

Base URL: http://localhost:5001 Content-Type: application/json for all request bodies


Repositories

GET /api/repos

Returns the union of explicitly watched repos and repos inferred from tracked PRs. Use GET /api/repos/settings when you need per-repo settings such as ownPrsOnly.

Response 200

["owner/repo-a", "owner/repo-b"]

GET /api/repos/settings

Returns repo-level settings for explicitly watched repos plus any repos inferred from currently tracked PRs.

ownPrsOnly: true means the watcher auto-discovers only PRs authored by the authenticated GitHub user for that repo. ownPrsOnly: false enables team-wide discovery and auto-discovers all open PRs in the repo.

Response 200 — array of WatchedRepo objects


POST /api/repos

Add a repository to the watch list.

Body

{ "repo": "owner/repo" }

Accepts "owner/repo" slugs or full https://github.com/owner/repo URLs.

New watched repos default to ownPrsOnly: true (My PRs only). To switch a repo to team-wide discovery, call PATCH /api/repos/settings after adding it.

Response 201

{ "repo": "owner/repo" }

PATCH /api/repos/settings

Update repo-level settings.

ownPrsOnly: true keeps auto-discovery limited to PRs authored by the authenticated GitHub user. ownPrsOnly: false switches the repo to team-wide auto-discovery. Changing this setting does not remove PRs that were already tracked directly by URL.

Body

{
  "repo": "owner/repo",
  "ownPrsOnly": false,
  "autoCreateReleases": true
}

Provide repo plus one or both of ownPrsOnly and autoCreateReleases.

Response 200 — updated WatchedRepo object Response 400 — invalid body or no settings provided


POST /api/repos/sync

Queue an immediate durable watcher pass across all watched repos. The queued sync job performs GitHub reconciliation and then enqueues follow-up babysit and release work as needed. It does not enqueue social changelog generation. During drain mode, the sync job still refreshes repository status but does not enqueue new automation work.

Response 200

{ "ok": true }

POST /api/repos/release

Queue a manual release run for the latest unreleased merged PR in a watched repository. The route returns once the durable release job has been stored, not when the release finishes. Returns 409 when drain mode is active.

Body

{ "repo": "owner/repo" }

Accepts "owner/repo" slugs or full https://github.com/owner/repo URLs.

Response 201ReleaseRun object Response 400 — invalid body or repository slug Response 409 — drain mode is active, or no unreleased merged PR exists


Pull Requests

GET /api/prs

List all actively tracked PRs with full feedback arrays.

Response 200 — array of PR objects


GET /api/prs/archived

List archived (closed/merged) PRs.

Response 200 — array of PR objects


GET /api/prs/:id

Get a single PR by its internal patchdeck ID.

Response 200PR object Response 404{ "error": "PR not found" }


POST /api/prs

Register a GitHub PR by URL. patchdeck fetches the PR summary from GitHub, stores it, and queues an initial durable babysit run.

This direct registration path is independent of watched-repo discovery scope, so the PR stays tracked even if its repo is configured as ownPrsOnly: true.

Body

{ "url": "https://github.com/owner/repo/pull/42" }

Response 201PR object (newly created) Response 201PR object (already tracked) Response 400 — invalid URL Response 4xx — GitHub API error


DELETE /api/prs/:id

Remove a PR from tracking.

Response 200{ "ok": true } Response 404 — not found


PATCH /api/prs/:id/watch

Pause or resume background automation for a single tracked PR.

When enabled is false, the background watcher skips autonomous sync/apply cycles for that PR, but manual routes such as POST /api/prs/:id/fetch, POST /api/prs/:id/triage, POST /api/prs/:id/apply, and POST /api/prs/:id/babysit still work. Re-enabling watch schedules an immediate watcher pass.

Body

{ "enabled": false }

Response 200 — updated PR object Response 400 — invalid body Response 404 — PR not found


POST /api/prs/:id/fetch

Force a fresh pull of comments and reviews from GitHub for this PR.

Response 200 — updated PR object


POST /api/prs/:id/triage

Run auto-triage on all un-triaged feedback items.

Classification rules (keyword-based):

  • reject — contains "lgtm", "looks good"
  • accept — contains "please", "should", "fix", "error", "fail"
  • flag — everything else

Response 200 — updated PR object


POST /api/prs/:id/apply

Queue the configured AI agent to apply all accepted feedback in an isolated git worktree. The route returns once the durable job has been stored, not when the agent run completes. Returns 409 when drain mode is active.

Response 200 — updated PR object


POST /api/prs/:id/babysit

Queue a full babysit cycle: sync → triage → apply → report. The route returns once the durable job has been stored, not when the run finishes. Returns 409 when drain mode is active.

Response 200 — updated PR object


Feedback items

PATCH /api/prs/:id/feedback/:feedbackId

Manually override the triage decision for a single feedback item.

Body

{ "decision": "accept" }

Valid values: "accept" | "reject" | "flag"

Response 200 — updated PR object


POST /api/prs/:id/feedback/:feedbackId/retry

Re-queue a failed or warned feedback item by scheduling another durable babysit pass for the PR. Returns 409 when drain mode is active; the feedback item is left unchanged and no babysit job is queued.

Response 200 — updated PR object Response 404 — PR or feedback item not found Response 400 — item is not in a retryable state Response 409 — drain mode is active


PR Q&A

GET /api/prs/:id/questions

List the full question/answer history for a PR.

Response 200 — array of PRQuestion objects


POST /api/prs/:id/questions

Ask the configured AI agent a question about the PR. The question is stored immediately; a durable background job fills in the answer asynchronously. Both the question row and the queued answer job persist in SQLite, so pending questions survive app restarts. Returns 409 when drain mode is active, before storing the question or queueing the answer job.

Body

{ "question": "Why does the linter keep failing on line 42?" }

Maximum 2000 characters.

Response 201PRQuestion object (answer will be null until the agent responds) Response 409 — drain mode is active


Logs

GET /api/logs

Retrieve activity logs, optionally filtered by PR.

Query parameters

Parameter Type Description
prId string Filter to a single PR (optional)

Response 200 — array of LogEntry objects

GET /api/server-logs

Retrieve recent structured server log records from the in-memory ring buffer that backs the dashboard /logs page.

Query parameters

Parameter Type Description
level string Minimum level: trace, debug, info, warn, error, or fatal
source string Filter to one structured log source
since number Return records with a sequence number greater than this cursor
search string Case-insensitive match against the message or structured fields
limit number Maximum number of records to return; defaults to 500

Response 200

{
  "records": [],
  "sources": ["babysitter", "github"],
  "latestSeq": 123
}

GET /api/server-logs/stream

Tail server logs as Server-Sent Events for the dashboard /logs page. Pass since to replay up to the latest 1000 missed records before live events begin. Slow clients may be disconnected rather than allowing unbounded response buffering.


CI healing sessions

GET /api/healing-sessions

List all persisted healing sessions, newest first.

Response 200 — array of HealingSession objects


GET /api/healing-sessions/:id

Get one healing session by its internal patchdeck ID.

Response 200HealingSession object Response 404{ "error": "Healing session not found" }


Deployment healing sessions

Deployment-healing sessions are created when a merged PR belongs to a detected Vercel or Railway repository and deployment healing is enabled. The background job waits for the deployment, captures failure logs when the deployment enters an error state, and records the repair outcome.

GET /api/deployment-healing-sessions

List persisted deployment-healing sessions, newest first.

Query parameters

Parameter Type Description
repo string Optional repository slug (owner/repo) filter

Response 200 — array of DeploymentHealingSession objects


GET /api/deployment-healing-sessions/:id

Get one deployment-healing session by its internal patchdeck ID.

Response 200DeploymentHealingSession object Response 404{ "error": "Deployment healing session not found" }


Configuration

GET /api/config

Read the current configuration. GitHub tokens are redacted to ordered ***xxxx values.

Response 200Config object (tokens redacted)


PATCH /api/config

Partially update the configuration. Only the provided fields are changed.

Body (all fields optional)

{
  "githubTokens": ["ghp_xxxxxxxxxxxx", "github_pat_yyyyyyyyyyyy"],
  "codingAgent": "claude",
  "maxTurns": 15,
  "batchWindowMs": 300000,
  "pollIntervalMs": 120000,
  "maxChangesPerRun": 10,
  "autoResolveMergeConflicts": true,
  "autoCreateReleases": true,
  "autoUpdateDocs": true,
  "githubCommentAppName": "patchdeck",
  "includeRepositoryLinksInGitHubComments": true,
  "postGitHubProgressReplies": false,
  "autoHealCI": false,
  "maxHealingAttemptsPerSession": 3,
  "maxHealingAttemptsPerFingerprint": 2,
  "maxConcurrentHealingRuns": 1,
  "healingCooldownMs": 300000,
  "autoHealDeployments": false,
  "deploymentCheckDelayMs": 60000,
  "deploymentCheckTimeoutMs": 600000,
  "deploymentCheckPollIntervalMs": 15000,
  "watchedRepos": ["owner/repo"],
  "trustedReviewers": ["alice", "bob"],
  "priorityIssueAuthors": ["octocat"],
  "ignoredBots": ["dependabot", "codecov"]
}

Response 200 — updated Config object (tokens redacted)

Some configuration is REST-writable but not exposed by the MCP update_config tool schema today. Use this REST endpoint when changing release automation, CI-healing, or deployment-healing keys.

priorityIssueAuthors accepts GitHub account logins such as octocat; values are matched case-insensitively and may include or omit a leading @. Issues from matching authors are evaluated and worked before the regular issue queue, and their evaluation/work jobs use elevated background-job priority. The default is an empty array, which leaves issue ordering and job priority unchanged.


App updates

GET /api/app-update

Return the release-check result used by the dashboard update banner.

The server reads the running app version from APP_VERSION and falls back to "dev" when unset. If the current version is a stable semver string, patchdeck checks the latest stable GitHub release for jeremymcs/patchdeck, ignores draft and prerelease releases, and compares the versions. If the current build is not semver or the GitHub release check fails, the endpoint returns a quiet fallback with latestVersion: null, the releases index URL, and updateAvailable: false.

Response 200AppUpdateStatus object

Example 200 — newer release available

{
  "currentVersion": "1.0.0",
  "latestVersion": "v1.1.0",
  "latestReleaseUrl": "https://github.com/jeremymcs/patchdeck/releases/tag/v1.1.0",
  "updateAvailable": true
}

Example 200 — quiet fallback for a non-versioned build or failed check

{
  "currentVersion": "dev",
  "latestVersion": null,
  "latestReleaseUrl": "https://github.com/jeremymcs/patchdeck/releases",
  "updateAvailable": false
}

Runtime & drain mode

Runtime state is queue-aware. activeRuns counts currently executing queue handlers (leased jobs); jobs still waiting in SQLite are not included.

GET /api/runtime

Get the current runtime state.

Response 200

{
  "drainMode": false,
  "drainRequestedAt": null,
  "drainReason": null,
  "activeRuns": 2
}

POST /api/runtime/drain

Enable or disable drain mode. When enabled, the dispatcher stops claiming new automation jobs but still claims sync_watched_repos jobs so repository status can refresh. Other queued rows remain intact in SQLite. Already-running handlers are allowed to finish, and waitForIdle also waits on any started babysitter or release work before reporting success.

patchdeck may also enable drain mode automatically when an agent health check reports deterministic unavailability, such as a missing CLI, auth failure, or unsupported agent setting. Transient health-check failures, including command timeouts, skip the affected automation cycle with an Automation skipped log instead of setting drainMode.

Manual agent-triggering endpoints return 409 instead of storing new work while drain mode is active. This includes dashboard Run now (POST /api/prs/:id/apply), POST /api/prs/:id/babysit, feedback retry, PR Q&A, manual repository release, and release retry. Drain-safe routes such as POST /api/repos/sync may still enqueue status refresh work that does not start new agent automation.

Body

{
  "enabled": true,
  "reason": "Deploying new version",
  "waitForIdle": true,
  "timeoutMs": 120000
}
Field Type Required Description
enabled boolean yes true to enable drain mode, false to disable
reason string no Human-readable reason (stored in state)
waitForIdle boolean no Wait until queue workers and started babysitter/release runs go idle
timeoutMs number no Max wait in ms when waitForIdle is true (≤600s)

Response 200 — drained successfully (or disabled) Response 202 — drain enabled but timed out before idle


Social changelogs

Social media changelog generation has been removed. These endpoints remain read-only compatibility surfaces for historical changelog rows.

GET /api/changelogs

List all generated social-media changelog posts.

Response 200 — array of SocialChangelog objects


GET /api/changelogs/:id

Get a single social-media changelog by ID.

Response 200SocialChangelog object Response 404 — not found


Releases

Release evaluation and publishing are also durable background jobs. When a tracked PR is archived as merged and automatic releases are enabled, patchdeck persists a release run and queues background processing. Retrying a failed release run resets it to detected and re-queues processing.

GET /api/releases

List all persisted release runs, newest first.

Response 200 — array of ReleaseRun objects


GET /api/releases/:id

Get one release run by its internal patchdeck ID.

Response 200ReleaseRun object Response 404 — not found


POST /api/releases/:id/retry

Reset an existing release run to detected and queue it for durable reprocessing. Returns 409 when drain mode is active; the release run is left unchanged and no release job is queued.

Response 200 — updated ReleaseRun object Response 404 — not found Response 409 — drain mode is active


Onboarding

GET /api/onboarding/status

Check the onboarding status of all watched repositories (e.g., whether the patchdeck GitHub Actions workflow is installed).

Response 200

{
  "githubConnected": true,
  "githubUser": "octocat",
  "repos": [
    {
      "repo": "owner/repo",
      "accessible": true,
      "codeReviews": {
        "claude": true,
        "codex": false,
        "gemini": false
      }
    }
  ]
}

POST /api/onboarding/install-review

Install the patchdeck code-review GitHub Actions workflow on a repository.

Body

{
  "repo": "owner/repo",
  "tool": "claude"
}

tool must be "claude" or "codex".

Response 200

{
  "path": ".github/workflows/claude-code-review.yml",
  "url": "https://github.com/owner/repo/blob/main/.github/workflows/claude-code-review.yml"
}

Data types

PR

{
  id: string;                  // Internal UUID
  number: number;              // GitHub PR number
  title: string;
  repo: string;                // "owner/repo"
  branch: string;
  author: string;
  url: string;                 // Full GitHub URL
  status: "watching" | "processing" | "done" | "error" | "archived";
  feedbackItems: FeedbackItem[];
  accepted: number;            // Count of accepted items
  rejected: number;
  flagged: number;
  testsPassed: boolean | null;
  lintPassed: boolean | null;
  lastChecked: string | null;  // ISO 8601
  watchEnabled: boolean;       // false pauses autonomous watcher runs for this PR
  docsAssessment?: {
    headSha: string;
    status: "needed" | "not_needed" | "failed";
    summary: string;
    assessedAt: string;        // ISO 8601
  } | null;
  addedAt: string;             // ISO 8601
}

FeedbackItem

{
  id: string;
  author: string;
  body: string;
  bodyHtml: string;
  replyKind: "review_thread" | "review" | "general_comment";
  sourceId: string;
  sourceNodeId: string | null;
  sourceUrl: string | null;
  threadId: string | null;
  threadResolved: boolean | null;
  auditToken: string;
  file: string | null;
  line: number | null;
  type: "review_comment" | "review" | "general_comment";
  createdAt: string;           // ISO 8601
  decision: "accept" | "reject" | "flag" | null;
  decisionReason: string | null;
  action: string | null;
  status: "pending" | "queued" | "in_progress" | "resolved" | "failed" | "warning" | "rejected" | "flagged";
  statusReason: string | null;
}

PRQuestion

{
  id: string;
  prId: string;
  question: string;
  answer: string | null;       // null until the agent responds
  status: "pending" | "answering" | "answered" | "error";
  error: string | null;
  createdAt: string;           // ISO 8601
  answeredAt: string | null;   // ISO 8601
}

LogEntry

{
  id: string;
  prId: string;
  runId: string | null;
  timestamp: string;           // ISO 8601
  level: "info" | "warn" | "error";
  phase: string | null;
  message: string;
  metadata: Record<string, unknown> | null;
}

Config

{
  githubTokens: string[];      // Ordered and redacted to "***xxxx" in GET responses
  githubToken?: string;        // Legacy single-token field, redacted when present
  codingAgent: "claude" | "codex";
  maxTurns: number;
  batchWindowMs: number;
  pollIntervalMs: number;
  maxChangesPerRun: number;
  autoResolveMergeConflicts: boolean;
  autoCreateReleases: boolean;
  autoUpdateDocs: boolean;
  githubCommentAppName: string; // Defaults to "patchdeck"; blank removes the GitHub reply signature
  includeRepositoryLinksInGitHubComments: boolean;
  postGitHubProgressReplies: boolean;
  autoHealCI: boolean;
  maxHealingAttemptsPerSession: number;
  maxHealingAttemptsPerFingerprint: number;
  maxConcurrentHealingRuns: number;
  healingCooldownMs: number;
  autoHealDeployments: boolean;
  deploymentCheckDelayMs: number;
  deploymentCheckTimeoutMs: number;
  deploymentCheckPollIntervalMs: number;
  watchedRepos: string[];
  trustedReviewers: string[];
  priorityIssueAuthors: string[]; // GitHub logins whose issues receive queue and job priority; empty by default
  ignoredBots: string[];
}

WatchedRepo

{
  repo: string;               // "owner/repo"
  autoCreateReleases: boolean;
  ownPrsOnly: boolean;        // true => only auto-discover the authenticated user's PRs
}

AppUpdateStatus

{
  currentVersion: string;          // APP_VERSION or "dev"
  latestVersion: string | null;    // latest stable GitHub release tag, e.g. "v1.1.0"
  latestReleaseUrl: string;        // release page or releases index fallback
  updateAvailable: boolean;        // true when latestVersion is newer than currentVersion
}

RuntimeSnapshot

{
  drainMode: boolean;
  drainRequestedAt: string | null; // ISO 8601
  drainReason: string | null;
  activeRuns: number;              // currently executing durable queue jobs
}

HealingSession

{
  id: string;
  prId: string;
  repo: string;                // "owner/repo"
  prNumber: number;
  initialHeadSha: string;
  currentHeadSha: string;
  state:
    | "idle"
    | "triaging"
    | "awaiting_repair_slot"
    | "repairing"
    | "awaiting_ci"
    | "verifying"
    | "healed"
    | "cooldown"
    | "blocked"
    | "escalated"
    | "superseded";
  startedAt: string;           // ISO 8601
  updatedAt: string;           // ISO 8601
  endedAt: string | null;      // ISO 8601
  blockedReason: string | null;
  escalationReason: string | null;
  latestFingerprint: string | null;
  attemptCount: number;
  lastImprovementScore: number | null;
}

DeploymentHealingSession

{
  id: string;
  repo: string;                // "owner/repo"
  platform: "vercel" | "railway";
  triggerPrNumber: number;
  triggerPrTitle: string;
  triggerPrUrl: string;
  mergeSha: string;
  deploymentId: string | null;
  deploymentLog: string | null;
  fixBranch: string | null;
  fixPrNumber: number | null;
  fixPrUrl: string | null;
  state: "monitoring" | "failed" | "fixing" | "fix_submitted" | "escalated";
  error: string | null;
  createdAt: string;           // ISO 8601
  updatedAt: string;           // ISO 8601
  completedAt: string | null;  // ISO 8601
}

ReleaseRun

{
  id: string;
  repo: string;                // "owner/repo"
  baseBranch: string;
  triggerPrNumber: number;
  triggerPrTitle: string;
  triggerPrUrl: string;
  triggerMergeSha: string;
  triggerMergedAt: string;     // ISO 8601
  status: "detected" | "evaluating" | "skipped" | "proposed" | "publishing" | "published" | "error";
  decisionReason: string | null;
  recommendedBump: "patch" | "minor" | "major" | null;
  proposedVersion: string | null;
  releaseTitle: string | null;
  releaseNotes: string | null;
  includedPrs: Array<{
    number: number;
    title: string;
    url: string;
    author: string;
    mergedAt: string;          // ISO 8601
    mergeSha: string;
  }>;
  targetSha: string | null;
  githubReleaseId: number | null;
  githubReleaseUrl: string | null;
  error: string | null;
  createdAt: string;           // ISO 8601
  updatedAt: string;           // ISO 8601
  completedAt: string | null;  // ISO 8601
}

SocialChangelog

{
  id: string;
  date: string;                // "YYYY-MM-DD"
  triggerCount: number;        // nth merge that triggered this (5, 10, 15…)
  prSummaries: Array<{
    number: number;
    title: string;
    url: string;
    author: string;
    repo: string;
  }>;
  content: string | null;      // Generated social-media post
  status: "generating" | "done" | "error";
  error: string | null;
  createdAt: string;           // ISO 8601
  completedAt: string | null;  // ISO 8601
}

Error handling

All error responses share this shape:

{ "error": "Human-readable description" }
Status Meaning
400 Validation error (bad input)
401 Login required or invalid credentials
403 Request blocked — remote login is not configured
404 Resource not found
409 Conflict — e.g. drain mode is active
500 Internal server error

Environment variables

Variable Default Description
PORT 5001 HTTP port for the patchdeck server
PATCHDECK_PORT 5001 Port the MCP server connects to (MCP only)
PATCHDECK_HOME ~/.patchdeck Directory for SQLite DB, logs, repos, worktrees
CODEFACTORY_HOME Legacy alias used only when PATCHDECK_HOME is not set
PATCHDECK_WEB_USERNAME Username for opt-in remote dashboard/API login
PATCHDECK_WEB_PASSWORD Password for opt-in remote dashboard/API login
PATCHDECK_SESSION_SECRET generated at startup Secret for signing remote dashboard sessions
GITHUB_TOKEN Fallback GitHub token after saved app tokens and before gh auth
NODE_ENV development Set to production for production builds
TAURI_DEV Set to skip auto-opening the browser (used by Tauri)

The MCP server also accepts CODEFACTORY_PORT as a legacy fallback when PATCHDECK_PORT is unset.