Colony Monitor REST API
Colony Monitor REST API
Section titled “Colony Monitor REST API”The monitor process exposes an HTTP API for pipeline observability, issue lifecycle management, and Prometheus metrics scraping.
Authentication
Section titled “Authentication”When agents.monitor.auth is configured, all API and SSE endpoints are gated behind
a session. Browsers interact through the dashboard’s in-page login form — there is
no native browser Basic Auth prompt. Programmatic and CLI clients (e.g. the
@runcolony/sdk ColonyClient) authenticate by sending a valid Authorization: Basic
header directly.
agents: monitor: auth: username: colony password_env: COLONY_DASHBOARD_PASSWORD # env var holding the passwordLogin flow (browser): POST /api/auth/login with { "username", "password" } validates
against the configured credential and, on success, sets a signed COLONY_SESSION
httpOnly cookie. The cookie is valid for 12 hours. POST /api/auth/logout clears it.
GET /api/auth/session returns { "authenticated": boolean } and can be polled without
triggering the auth gate.
Session secret: the signing secret is generated fresh on each monitor process start — a monitor restart invalidates all existing session cookies and requires operators to log in again.
Credentials are compared using timing-safe equality. On failure the server returns:
- Status:
401 Unauthorized - Body: JSON
ApiError—{"error": "...", "code": "AUTH_REQUIRED" | "AUTH_FAILED", "status": 401}
No WWW-Authenticate: Basic challenge is ever sent.
Auth-exempt endpoints (no session or Authorization header required):
GET /healthGET /prometheusGET /api/openapi.jsonGET /— SPA shell (login form must be able to render before auth)GET /assets/*— static dashboard assetsPOST /api/auth/login,POST /api/auth/logout,GET /api/auth/session
If no auth block is configured, all endpoints are open.
Endpoints
Section titled “Endpoints”POST /api/auth/login
Section titled “POST /api/auth/login”Log in with the configured monitor credential. No prior auth required.
Request body — { "username": string, "password": string }
Response — AuthLoginResponse
On success: 200 { "ok": true } with Set-Cookie: COLONY_SESSION=<token>; HttpOnly; Secure; SameSite=Strict.
On failure: 401 { "code": "AUTH_FAILED" }. Malformed JSON: 400 { "code": "INVALID_JSON" }.
When no auth block is configured: 200 { "ok": true } (no cookie set, not needed).
POST /api/auth/logout
Section titled “POST /api/auth/logout”Clear the session cookie. No auth required.
Response — AuthLogoutResponse
200 { "ok": true } with Set-Cookie: COLONY_SESSION=; Max-Age=0 (cookie cleared).
GET /api/auth/session
Section titled “GET /api/auth/session”Return whether the current request is authenticated. No auth required.
Response — AuthSessionResponse
200 { "authenticated": boolean } — true when the request carries a valid session cookie
or a valid Authorization: Basic header, or when no auth block is configured.
Health
Section titled “Health”GET /health
Section titled “GET /health”Returns server liveness. No auth required.
Response — HealthResponse
{ "status": "healthy", "uptime": 3600 }| Field | Type | Description |
|---|---|---|
status | string | Always "healthy" |
uptime | number | Seconds since the process started |
Dashboard
Section titled “Dashboard”GET /dashboard
Section titled “GET /dashboard”Returns the full pipeline dashboard snapshot. Auth required.
Response — DashboardResponse
{ "status": "healthy", "uptime": 3600, "agents": { "sprint-master": { "agentName": "sprint-master", "reachable": true, "timestamp": 1711929600000, "responseTimeMs": 42, "health": { "status": "healthy" }, "costUsd": 1.23, "inFlightIssues": ["owner/repo#10"], "tenantSpend": { "default": 1.23 } } }, "alerts": [], "costTotalUsd": 12.5, "pipelineSummary": { "analyzing": 1, "ready-for-dev": 3 }, "workerPool": []}| Field | Type | Description |
|---|---|---|
status | "healthy" | "degraded" | "unhealthy" | Overall pipeline health |
uptime | number | Seconds since process start |
agents | Record<string, AgentHealthSnapshot> | Per-agent health snapshots |
alerts | Alert[] | Active alerts from failure detectors |
costTotalUsd | number | Total cost across all agents |
costByWindow? | CostByWindow | Cost broken down by time window |
pipelineSummary | Record<string, number> | Issue counts by pipeline state |
issueTimelines? | Record<number, IssueTimeline> | Per-issue timelines |
tenants? | TenantBudgetDashboard[] | Per-tenant budget info |
metrics? | Record<string, PipelineMetrics> | Per-repo pipeline metrics |
workerPool? | WorkerPoolSnapshot[] | Per-repo worker pool status |
Timeline
Section titled “Timeline”GET /api/timeline/:owner/:repo/:issue
Section titled “GET /api/timeline/:owner/:repo/:issue”Returns the timeline for a single issue. Auth required.
Path parameters:
| Param | Type | Description |
|---|---|---|
owner | string | Repository owner |
repo | string | Repository name |
issue | number | Issue number |
Response — TimelineResponse
TimelineResponse extends IssueTimeline with two additional fields:
{ "issueNumber": 42, "entries": [], "totalCostUsd": 0.85, "totalDurationMs": 120000, "currentState": "ready-for-dev", "stateTransitions": [], "isPaused": false, "isBlocked": false}| Field | Type | Description |
|---|---|---|
issueNumber | number | The issue number |
entries | TimelineEntry[] | Chronological timeline entries |
totalCostUsd | number | Total cost for this issue |
totalDurationMs | number | Total wall-clock duration |
currentState | string | Current pipeline state |
staleSinceMs? | number | How long the issue has been stale |
stateTransitions | StateTransition[] | State change history |
isPaused | boolean | Whether the issue is paused |
isBlocked | boolean | Whether the issue is blocked |
Errors:
| Status | Code | When |
|---|---|---|
| 400 | Expected /api/timeline/:owner/:repo/:issue | Malformed path |
| 400 | Invalid issue number | Non-numeric issue param |
| 503 | No repo context available for owner/repo | Repo not configured |
| 500 | Failed to build timeline | Internal error |
Tracks
Section titled “Tracks”GET /api/tracks?repo=owner/repo
Section titled “GET /api/tracks?repo=owner/repo”Lists all tracks for a repository. Auth required.
The repo query parameter is required. Requests without a ? query string do not
match this route and return a 404.
Query parameters:
| Param | Type | Required | Description |
|---|---|---|---|
repo | string | Yes | Repository as owner/repo |
Response — TrackListResponse
{ "tracks": [ { "name": "bugs", "label": "bug", "cooldown_minutes": 60, "enabled": true, "active_version": 1 } ]}Errors:
| Status | Code | When |
|---|---|---|
| 400 | Missing or invalid repo query parameter | Missing or malformed repo |
POST /api/tracks
Section titled “POST /api/tracks”Creates or updates a track. Auth required.
Request body — TrackCreateRequest
{ "repo": "owner/repo", "name": "bugs", "label": "bug", "cooldown_minutes": 30, "enabled": true}| Field | Type | Required | Default | Description |
|---|---|---|---|---|
repo | string | Yes | — | Repository as owner/repo |
name | string | Yes | — | Track name |
label | string | No | "" | GitHub label filter |
cooldown_minutes | number | No | 60 | Cooldown between runs |
enabled | boolean | No | true | Whether the track is active |
Response — TrackCreateResponse
{ "ok": true }Errors:
| Status | Code | When |
|---|---|---|
| 400 | Invalid JSON body | Unparseable request body |
| 400 | Missing required fields: repo, name | Missing repo or name |
POST /api/tracks/:owner/:repo/:name
Section titled “POST /api/tracks/:owner/:repo/:name”Updates a track’s proposal slate, cadence, or weight. Only the fields provided in the body are changed; omitted fields retain their current values. Auth required.
Path parameters:
| Param | Type | Description |
|---|---|---|
owner | string | Repository owner |
repo | string | Repository name |
name | string | Track name |
Request body — TrackUpdateRequest
{ "proposal": { "slate": [{ "size": "medium", "count": 3 }] }, "cadence": { "kind": "cron", "expr": "0 9 * * MON" }, "weight": 2}| Field | Type | Required | Description |
|---|---|---|---|
label | string | No | GitHub label filter |
cooldown_minutes | number | No | Cooldown between runs (≥ 1) |
enabled | boolean | No | Whether the track is active |
proposal | ProposalSlate or null | No | Custom proposal slate; null clears a previously set slate (uses default) |
cadence | { kind, expr? } | No | Cadence override; kind is "cooldown" or "cron", expr required for cron |
weight | number | No | Relative weight for track selection (≥ 0) |
Response — TrackUpdateResponse
{ "ok": true }Errors:
| Status | Code | When |
|---|---|---|
| 400 | INVALID_JSON | Unparseable request body |
| 400 | INVALID_BODY | Invalid slate, cadence, or weight |
| 404 | TRACK_NOT_FOUND | Track does not exist |
DELETE /api/tracks/:owner/:repo/:name
Section titled “DELETE /api/tracks/:owner/:repo/:name”Deletes a track. Auth required.
Path parameters:
| Param | Type | Description |
|---|---|---|
owner | string | Repository owner |
repo | string | Repository name |
name | string | Track name |
Response — TrackDeleteResponse
{ "ok": true }POST /api/tracks/:owner/:repo/:name/enable
Section titled “POST /api/tracks/:owner/:repo/:name/enable”Enables a track. Auth required.
Response — TrackToggleResponse
{ "ok": true }Errors:
| Status | Code | When |
|---|---|---|
| 404 | Track "name" not found | Track does not exist |
POST /api/tracks/:owner/:repo/:name/disable
Section titled “POST /api/tracks/:owner/:repo/:name/disable”Disables a track. Auth required.
Response — TrackToggleResponse
{ "ok": true }Errors:
| Status | Code | When |
|---|---|---|
| 404 | Track "name" not found | Track does not exist |
GET /api/tracks/:owner/:repo/:name/prompts
Section titled “GET /api/tracks/:owner/:repo/:name/prompts”Lists all prompt versions for a track. Auth required.
Response — TrackPromptListResponse
{ "versions": [{ "version": 1, "created_at": "2026-01-15T10:00:00Z", "notes": "initial" }]}POST /api/tracks/:owner/:repo/:name/prompts
Section titled “POST /api/tracks/:owner/:repo/:name/prompts”Creates a new prompt version for a track. Auth required.
Request body — TrackPromptCreateRequest
{ "seed_title": "Fix {{label}} issues", "seed_body": "Investigate and fix the problem.", "instructions": "Follow repo conventions.", "notes": "v2 with better instructions"}| Field | Type | Required | Default | Description |
|---|---|---|---|---|
seed_title | string | No | "" | Issue title template |
seed_body | string | No | "" | Issue body template |
instructions | string | No | "" | Agent instructions |
notes | string | No | "" | Human-readable version notes |
Response — TrackPromptCreateResponse
{ "version": 2 }Errors:
| Status | Code | When |
|---|---|---|
| 400 | Invalid JSON body | Unparseable request body |
| 404 | varies | Track not found |
GET /api/tracks/:owner/:repo/:name/prompts/:version
Section titled “GET /api/tracks/:owner/:repo/:name/prompts/:version”Returns a single prompt version. Auth required.
Path parameters:
| Param | Type | Description |
|---|---|---|
version | number | Prompt version number |
Response — TrackPromptGetResponse
{ "prompt": { "version": 1, "seed_title": "Fix {{label}} issues", "seed_body": "Investigate and fix the problem.", "instructions": "Follow repo conventions." }}Errors:
| Status | Code | When |
|---|---|---|
| 404 | Version N not found | Version does not exist |
POST /api/tracks/:owner/:repo/:name/prompts/:version/activate
Section titled “POST /api/tracks/:owner/:repo/:name/prompts/:version/activate”Sets a prompt version as the active version for a track. Auth required.
Response — TrackPromptActivateResponse
{ "ok": true }Errors:
| Status | Code | When |
|---|---|---|
| 404 | varies | Track or version not found |
POST /api/tracks/:owner/:repo/:name/cooldown
Section titled “POST /api/tracks/:owner/:repo/:name/cooldown”Sets the cooldown period for a track. Auth required.
Path parameters:
| Param | Type | Description |
|---|---|---|
owner | string | Repository owner |
repo | string | Repository name |
name | string | Track name |
Request body — TrackCooldownRequest
{ "cooldown_minutes": 120 }| Field | Type | Required | Description |
|---|---|---|---|
cooldown_minutes | number | Yes | Minimum minutes between track runs; must be a positive integer |
Response — TrackCooldownResponse
{ "ok": true }Errors:
| Status | Code | When |
|---|---|---|
| 400 | INVALID_JSON | Unparseable request body |
| 400 | INVALID_BODY | cooldown_minutes is not a positive integer |
| 404 | TRACK_NOT_FOUND | Track does not exist |
| 503 | STORE_UNAVAILABLE | Pipeline store (Postgres) not connected |
Issues
Section titled “Issues”All issue action endpoints follow the pattern
POST /api/issues/:owner/:repo/:issue/:action. Auth required.
Path parameters:
| Param | Type | Description |
|---|---|---|
owner | string | Repository owner |
repo | string | Repository name |
issue | number | Issue number |
Response — IssueActionResponse
resume, unblock, and retry include a state field with the issue’s current
pipeline state. pause and cancel do not.
POST /api/issues/:owner/:repo/:issue/pause
Section titled “POST /api/issues/:owner/:repo/:issue/pause”Pauses an issue. Adds the colony:paused label.
{ "ok": true }POST /api/issues/:owner/:repo/:issue/resume
Section titled “POST /api/issues/:owner/:repo/:issue/resume”Resumes a paused issue. Removes colony:paused and re-enqueues the work task.
{ "ok": true, "state": "ready-for-dev" }POST /api/issues/:owner/:repo/:issue/unblock
Section titled “POST /api/issues/:owner/:repo/:issue/unblock”Unblocks an issue. Removes colony:blocked and re-enqueues the work task.
{ "ok": true, "state": "analyzing" }POST /api/issues/:owner/:repo/:issue/retry
Section titled “POST /api/issues/:owner/:repo/:issue/retry”Re-enqueues the work task for an issue in its current state.
{ "ok": true, "state": "in-review" }Errors:
| Status | Code | When |
|---|---|---|
| 404 | Issue not found: owner/repo#N | Issue not in pipeline store |
POST /api/issues/:owner/:repo/:issue/cancel
Section titled “POST /api/issues/:owner/:repo/:issue/cancel”Cancels an issue by transitioning it to done.
{ "ok": true }POST /api/issues/:owner/:repo/:issue/transition
Section titled “POST /api/issues/:owner/:repo/:issue/transition”Forces an issue to a specific pipeline state. The target state must be a valid outgoing
edge in the issue’s active workflow snapshot (use
GET …/valid-transitions to retrieve the permitted targets). Auth required.
Request body — TransitionRequest
{ "targetState": "in-review" }| Field | Type | Required | Description |
|---|---|---|---|
targetState | string | Yes | The state to transition to; must be a valid edge in the workflow snapshot |
Response — TransitionResponse
{ "ok": true, "from": "analyzing", "to": "ready-for-dev" }| Field | Type | Description |
|---|---|---|
ok | true | Always true on success |
from | string | State the issue transitioned from |
to | string | State the issue transitioned to |
Errors:
| Status | Code | When |
|---|---|---|
| 400 | INVALID_JSON | Unparseable request body |
| 400 | INVALID_STATE | targetState is missing or not a string |
| 400 | WORKFLOW_UNAVAILABLE | No workflow snapshot is registered for this repo |
| 400 | INVALID_TRANSITION | targetState is not a valid outgoing edge from the current state; error message includes the valid targets |
| 404 | ISSUE_NOT_FOUND | Issue not found in the pipeline store |
Common issue action errors (repo not found, store unavailable, etc.) also apply — see the table below.
Common issue action errors:
| Status | Code | When |
|---|---|---|
| 404 | Not found | Malformed URL path |
| 404 | Repo not found: owner/repo | Repo not configured |
| 404 | varies | Error message containing “not found” |
| 500 | Internal server error | Unexpected failure |
| 503 | Pipeline store not available | No database connection |
Issue Detail
Section titled “Issue Detail”The six read-only endpoints below expose per-issue analytics and workflow state. They
follow the same path prefix as the issue action endpoints:
GET /api/issues/:owner/:repo/:issue/:detail. Auth required.
Shared path parameters:
| Param | Type | Description |
|---|---|---|
owner | string | Repository owner |
repo | string | Repository name |
issue | number | Issue number |
GET /api/issues/:owner/:repo/:issue/findings
Section titled “GET /api/issues/:owner/:repo/:issue/findings”Returns agent findings recorded against a specific issue, paginated. Auth required.
Query parameters:
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
limit | number | No | 100 | Page size (1–200; out-of-range values are clamped) |
offset | number | No | 0 | Row offset for pagination |
Response — FindingsResponse (PaginatedResponse<AgentFinding>)
{ "items": [ { "id": "f1a2b3c4-...", "agentName": "developer", "taskId": "t9z8y7x6-...", "findingType": "semantic_conflict", "category": "maintainability", "severity": "medium", "confidence": 0.85, "title": "Unused variable in auth handler", "summary": "Variable `ctx` is declared but never read in `handleAuth`.", "recommendation": "Remove the unused variable or use it in the handler.", "evidence": [{ "kind": "file", "path": "src/auth.ts", "lineStart": 42 }], "blocksProgress": false, "status": "open", "issueNumber": 42, "createdAt": "2026-01-15T10:00:00Z", "updatedAt": "2026-01-15T10:00:00Z" } ], "page": { "total": null, "limit": 100, "offset": 0, "hasMore": false }}AgentFinding fields:
| Field | Type | Description |
|---|---|---|
id | string | UUID |
agentName | string | Agent that emitted the finding |
taskId | string | null | Task that produced the finding |
findingType | string | Machine-readable type. Known values: ci_failure, cost_anomaly, deterministic_check_failure, integration_gap, integration_issue, obs, pre_existing_check_failure, protected_path_violation, requirements_gap, review_finding, risk, semantic_conflict. Free-form strings are also accepted. |
category | string | Broad category (e.g. "maintainability") |
severity | string | "info" | "low" | "medium" | "high" | "critical" |
confidence | number | null | 0–1 confidence score; null when not computed |
title | string | Short human-readable title |
summary | string | Detailed description |
recommendation | string | null | Suggested remediation |
evidence | AgentFindingEvidence[] | Supporting evidence references (see below) |
blocksProgress | boolean | Whether this finding stalls the pipeline |
status | string | "open" | "accepted" | "resolved" | "dismissed" | "superseded" |
issueNumber | number | null | Issue this finding is attached to |
createdAt | string | ISO 8601 timestamp |
updatedAt | string | ISO 8601 timestamp |
AgentFindingEvidence fields:
| Field | Type | Description |
|---|---|---|
kind | string | Evidence type: "file", "symbol", "issue", "pr", "url", "text", etc. |
ref? | string | Symbol or entity reference |
path? | string | File path (for "file" / "symbol") |
lineStart? | number | Start line (for "file" / "symbol") |
lineEnd? | number | End line |
url? | string | URL (for "url") |
summary? | string | Human-readable excerpt |
PageInfo fields (returned in page):
| Field | Type | Description |
|---|---|---|
total | number | null | Total matching rows; null for this endpoint |
limit | number | Applied page size |
offset | number | Applied offset |
hasMore | boolean | Whether more pages are available |
Errors:
| Status | Code | When |
|---|---|---|
| 400 | INVALID_INPUT | Malformed path (wrong segment count) |
| 400 | INVALID_ISSUE_NUMBER | Non-numeric issue number in path |
| 500 | INTERNAL_ERROR | Failed to query issue findings |
| 503 | STORE_UNAVAILABLE | Pipeline store (Postgres) not connected |
GET /api/issues/:owner/:repo/:issue/digest
Section titled “GET /api/issues/:owner/:repo/:issue/digest”Returns a diagnostic summary for a single issue: current state, block classification, recent failed runs, active tasks, and auto-generated recommendations. Auth required.
Response — IssueDiagnosticResponse
{ "issue": { "issueNumber": 42, "title": "Fix login redirect", "state": "failure-blocked", "isBlocked": true, "isPaused": false, "blockReason": "build_failure", "blockedFromState": "ready-for-dev", "stateEnteredAt": "2026-01-15T09:00:00Z" }, "blockClassification": "transient", "autoUnblockCount": 1, "maxAutoUnblocks": 3, "recentFailedRuns": [ { "agentName": "developer", "errorMessage": "tsc: error TS2304: Cannot find name 'Foo'", "finishedAt": "2026-01-15T09:05:00Z" } ], "activeTasks": [], "recommendations": ["Retry after resolving the TypeScript compilation error."]}| Field | Type | Description |
|---|---|---|
issue | object | null | Current issue state; null when not tracked in the pipeline |
issue.issueNumber | number | Issue number |
issue.title | string | Issue title |
issue.state | string | Current pipeline state |
issue.isBlocked | boolean | Whether the issue is blocked |
issue.isPaused | boolean | Whether the issue is paused |
issue.blockReason | string | null | Block reason code (e.g. "build_failure") |
issue.blockedFromState | string | null | State from which the issue was blocked |
issue.stateEnteredAt | string | null | ISO 8601 timestamp when the current state was entered |
blockClassification | "transient" | "permanent" | "quota" | "unknown" | null | Block type; null when not blocked |
autoUnblockCount | number | Number of automatic unblock attempts so far |
maxAutoUnblocks | number | Configured maximum auto-unblock attempts per issue |
recentFailedRuns | object[] | Recent agent run failures |
recentFailedRuns[].agentName | string | Agent that failed |
recentFailedRuns[].errorMessage | string | null | Error message from the failed run |
recentFailedRuns[].finishedAt | string | null | ISO 8601 timestamp when the run finished |
activeTasks | object[] | Tasks currently pending or claimed for this issue |
activeTasks[].taskType | string | Task type (e.g. "develop") |
activeTasks[].status | string | Task status ("pending" or "claimed") |
activeTasks[].claimedBy | string | null | Worker ID that claimed the task, if claimed |
recommendations | string[] | Auto-generated operator action suggestions |
Errors:
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Malformed path (regex did not match) |
| 500 | INTERNAL_ERROR | Failed to get issue digest |
| 503 | STORE_UNAVAILABLE | Pipeline store (Postgres) not connected |
GET /api/issues/:owner/:repo/:issue/status
Section titled “GET /api/issues/:owner/:repo/:issue/status”Returns the lightweight status snapshot for an issue: current state, when it was entered, and when the last event occurred. Auth required.
Response — IssueStatusResponse
{ "currentStatus": "Implementing fix", "state": "ready-for-dev", "stateEnteredAt": "2026-01-15T08:00:00Z", "lastEventAt": "2026-01-15T09:30:00Z"}| Field | Type | Description |
|---|---|---|
currentStatus | string | null | Latest status message emitted by the agent, if any |
state | string | Current pipeline state |
stateEnteredAt | string | null | ISO 8601 timestamp when the current state was entered |
lastEventAt | string | null | ISO 8601 timestamp of the most recent issue event |
Errors:
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Malformed path or issue not in pipeline |
| 500 | INTERNAL_ERROR | Failed to get issue status |
| 503 | STORE_UNAVAILABLE | Pipeline store (Postgres) not connected |
GET /api/issues/:owner/:repo/:issue/events
Section titled “GET /api/issues/:owner/:repo/:issue/events”Returns paginated issue events (status updates, state transitions, operator actions) for a single issue. Auth required.
Query parameters:
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
limit | number | No | 50 | Page size |
since | string | No | — | ISO 8601 timestamp; return only events after this |
offset | number | No | 0 | Row offset for pagination |
Response — IssueEventsResponse (PaginatedResponse<IssueEvent>)
{ "items": [ { "id": 1001, "issueId": 7, "actorType": "agent", "actorId": "worker-1", "status": "Starting implementation", "details": null, "state": "ready-for-dev", "createdAt": "2026-01-15T08:01:00Z" } ], "page": { "total": null, "limit": 50, "offset": 0, "hasMore": false }}IssueEvent fields:
| Field | Type | Description |
|---|---|---|
id | number | Event row ID |
issueId | number | Internal pipeline issue ID |
actorType | "agent" | "workflow-engine" | "operator" | "system" | Who produced this event |
actorId | string | null | Worker ID or operator identifier |
status | string | Human-readable status message |
details | object | null | Structured metadata attached to the event |
state | string | Pipeline state at the time of the event |
createdAt | string | ISO 8601 timestamp |
Errors:
| Status | Code | When |
|---|---|---|
| 400 | INVALID_INPUT | limit or offset is not a valid number |
| 404 | NOT_FOUND | Malformed path or issue not found in pipeline |
| 500 | INTERNAL_ERROR | Failed to get issue events |
| 503 | STORE_UNAVAILABLE | Pipeline store (Postgres) not connected |
GET /api/issues/:owner/:repo/:issue/retrospective
Section titled “GET /api/issues/:owner/:repo/:issue/retrospective”Returns the post-issue retrospective for a completed issue, or null when none has
been recorded yet. Auth required.
Response — IssueRetrospectiveResponse
{ "retrospective": { "id": "r1a2b3c4-...", "issueNumber": 42, "status": "complete", "outcomeClassification": "success", "summary": "Implemented login redirect fix with 2 retries.", "plannedFiles": { "src/auth.ts": "modify" }, "actualFiles": { "src/auth.ts": "modify", "src/auth.test.ts": "modify" }, "failures": null, "lessons": { "note": "Test coverage was added opportunistically." }, "createdAt": "2026-01-16T10:00:00Z", "updatedAt": "2026-01-16T10:00:00Z" }}When no retrospective has been recorded, retrospective is null:
{ "retrospective": null }| Field | Type | Description |
|---|---|---|
retrospective | object | null | Retrospective record; null if not yet recorded |
retrospective.id | string | UUID |
retrospective.issueNumber | number | Issue number |
retrospective.status | string | Processing status (e.g. "complete") |
retrospective.outcomeClassification | string | null | Outcome label (e.g. "success", "failure") |
retrospective.summary | string | Human-readable summary of the issue outcome |
retrospective.plannedFiles | object | null | Files the agent planned to touch |
retrospective.actualFiles | object | null | Files the agent actually touched |
retrospective.failures | object | null | Structured failure details, if any |
retrospective.lessons | object | null | Lessons learned, promoted to repo intelligence |
retrospective.createdAt | string | ISO 8601 timestamp |
retrospective.updatedAt | string | ISO 8601 timestamp |
Errors:
| Status | Code | When |
|---|---|---|
| 400 | INVALID_INPUT | Malformed path (wrong segment count) |
| 400 | INVALID_ISSUE_NUMBER | Non-numeric issue number in path |
| 500 | INTERNAL_ERROR | Failed to fetch issue retrospective |
| 503 | STORE_UNAVAILABLE | Pipeline store (Postgres) not connected |
GET /api/issues/:owner/:repo/:issue/valid-transitions
Section titled “GET /api/issues/:owner/:repo/:issue/valid-transitions”Returns the issue’s current pipeline state and the set of states it can legally
transition to, as defined by the active workflow snapshot. Use this to populate a
transition picker before calling POST …/transition. Auth required.
Response — ValidTransitionsResponse
{ "currentState": "ready-for-dev", "validTargets": ["in-review", "failure-blocked"]}| Field | Type | Description |
|---|---|---|
currentState | string | Current pipeline state of the issue |
validTargets | string[] | States the issue can transition to from currentState |
Errors:
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Malformed path (regex did not match) |
| 404 | ISSUE_NOT_FOUND | Issue not found in the pipeline store |
| 400 | WORKFLOW_UNAVAILABLE | No workflow snapshot is registered for this repo |
| 503 | STORE_UNAVAILABLE | Pipeline store (Postgres) not connected |
Findings & Intelligence
Section titled “Findings & Intelligence”GET /api/findings
Section titled “GET /api/findings”Lists agent findings for a repository. Auth required.
The repo query parameter is required. Requests missing it return 400.
Query parameters:
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
repo | string | Yes | — | Repository as owner/repo |
status | string | No | — | Filter by status (open, accepted, resolved, dismissed, superseded) |
category | string | No | — | Filter by category |
severity | string | No | — | Filter by severity (info, low, medium, high, critical) |
limit | integer | No | 100 | Page size (max 200) |
offset | integer | No | 0 | Page offset |
Response — FindingsResponse (PaginatedResponse<AgentFinding>)
{ "items": [ { "id": "f_abc123", "tenantId": 1, "repoId": 42, "issueId": 100, "issueNumber": 17, "prId": null, "prNumber": null, "agentName": "reviewer", "taskId": "task_xyz", "findingType": "semantic_conflict", "category": "injection", "severity": "high", "confidence": 0.92, "title": "SQL injection risk in user query path", "summary": "Raw string interpolation detected in a database query.", "recommendation": "Use parameterized queries.", "evidence": [], "metadata": {}, "blocksProgress": false, "status": "open", "resolvedByIssueId": null, "resolvedAt": null, "dismissedReason": null, "createdAt": "2026-01-15T10:00:00Z", "updatedAt": "2026-01-15T10:00:00Z" } ], "page": { "total": null, "limit": 100, "offset": 0, "hasMore": false }}| Field | Type | Description |
|---|---|---|
id | string | UUID identifying this finding |
tenantId | number | Tenant the finding belongs to |
repoId | number | Repository the finding belongs to |
issueId | number | null | Pipeline issue ID (internal) |
issueNumber | number | null | GitHub issue number |
prId | number | null | Pipeline PR ID (internal) |
prNumber | number | null | GitHub PR number |
agentName | string | Agent that emitted this finding |
taskId | string | null | Work task that created this finding |
findingType | string | Type tag. Known values: ci_failure, cost_anomaly, deterministic_check_failure, integration_gap, integration_issue, obs, pre_existing_check_failure, protected_path_violation, requirements_gap, review_finding, risk, semantic_conflict. Free-form strings are also accepted. |
category | string | Category within the type |
severity | "info" | "low" | "medium" | "high" | "critical" | Severity level |
confidence | number | null | Confidence score (0–1) |
title | string | Short finding title |
summary | string | Full finding description |
recommendation | string | null | Suggested remediation |
evidence | AgentFindingEvidence[] | Supporting evidence items |
metadata | object | Arbitrary agent-supplied metadata |
blocksProgress | boolean | Whether this finding blocks issue progress |
status | "open" | "accepted" | "resolved" | "dismissed" | "superseded" | Lifecycle status |
resolvedByIssueId | number | null | Pipeline issue that resolved this finding |
resolvedAt | string | null | ISO timestamp when resolved |
dismissedReason | string | null | Reason for dismissal |
createdAt | string | ISO timestamp of creation |
updatedAt | string | ISO timestamp of last update |
Errors:
| Status | Code | When |
|---|---|---|
| 400 | INVALID_INPUT | repo is missing or not in owner/repo format |
| 500 | INTERNAL_ERROR | Database query failure |
| 503 | STORE_UNAVAILABLE | Postgres not connected |
GET /api/intelligence/retrieve
Section titled “GET /api/intelligence/retrieve”Debug endpoint: runs the repo-intelligence retrieval for a given repo and agent role, returning the same ranked snippets, candidate files, and token estimate that the agent would receive. Auth required.
Context source: Issue title, body, and labels are loaded from pipeline_issues. Planned files are loaded from the planned_files column (developer role only). Changed files are loaded from pull_requests.changed_file_paths (reviewer role). This means:
- Results match agent retrieval for all stored fields.
- For the
reviewerrole, thechangedFilescontext is sourced from the storedchanged_file_pathscolumn rather than the live worktree diff the reviewer agent computes at review time — these may differ if enrichment hasn’t completed yet or the PR was updated after enrichment. - If
intelligence.embedding.enabledis configured, embedding-similarity recall (source 4) is included. CheckretrievalContext.embeddingEnabledto confirm.
Query parameters:
| Param | Type | Required | Description |
|---|---|---|---|
repo | string | Yes | Repository as owner/repo |
agent | string | Yes | Agent role: analyzer, planner, developer, reviewer, or retrospector |
issue | integer | No | Issue number — loads title/body/labels/plannedFiles from pipeline_issues |
pr | integer | No | Pull request number — loads changed_file_paths for the reviewer role |
budget | integer | No | Token budget for the rendered markdown (overrides intelligence.retrieval.token_budget) |
Response — IntelligenceRetrieveResponse
{ "snippets": [ { "kind": "architecture", "title": "Service layer boundaries", "summary": "HTTP handlers must not call repository methods directly.", "scope": [{ "kind": "repo", "ref": "owner/repo" }], "evidence": [], "confidence": 0.95, "reason": "approved invariant", "source": "intelligence", "itemId": "i_abc123" } ], "omitted": [{ "reason": "low-confidence with no scope", "count": 2 }], "candidateFiles": [ { "path": "packages/foo/src/bar.ts", "score": 9, "method": "anchor", "why": "matched code anchor `bar`" } ], "tokenEstimate": 420, "truncated": false, "methodBreakdown": { "anchorsExtracted": 12, "anchorHits": 3, "precedentHits": 1, "corpusSize": 47, "entityCount": 1820 }, "retrievalContext": { "issueFound": true, "prFound": null, "titleUsed": "Fix the authentication flow", "bodyLength": 342, "labelsUsed": ["bug", "priority:high"], "plannedFilesCount": null, "changedFilesCount": null, "changedFilesSource": "not_applicable", "embeddingEnabled": true }}The retrievalContext field reports which context was available and forwarded to the retrieval service. Use it to diagnose why results may differ from agent retrieval:
issueFound: false— issue not inpipeline_issues; title/body/labels were not availableprFound: false— PR not found;changedFilescould not be loaded (reviewer role)changedFilesSource: "unavailable"— PR exists butchanged_file_pathscolumn is NULLembeddingEnabled: false— embedding recall is disabled; configureintelligence.embedding.enabled: trueand restart the monitor
Errors:
| Status | Code | When |
|---|---|---|
| 400 | INVALID_INPUT | repo or agent missing/invalid |
| 500 | INTERNAL_ERROR | Retrieval failure |
| 503 | STORE_UNAVAILABLE | Postgres not connected |
GET /api/intelligence/:id
Section titled “GET /api/intelligence/:id”Returns a single intelligence item by ID with its full scope and evidence links. Auth required.
Path parameters:
| Param | Type | Description |
|---|---|---|
id | string | Intelligence item UUID |
Response — IntelligenceDetailResponse
{ "item": { "id": "i_abc123", "tenantId": 1, "repoId": 42, "kind": "architecture", "title": "Service layer boundaries", "summary": "HTTP handlers must not call repository methods directly.", "details": "All database access must go through the service layer.", "status": "approved", "confidence": 0.95, "qualityScore": 0.88, "source": "reviewer", "sourceAgent": "reviewer", "sourceIssueId": null, "sourcePrId": null, "sourceFindingId": null, "sourceRetrospectiveId": null, "metadata": {}, "tags": ["architecture", "boundaries"], "firstObservedAt": "2026-01-10T08:00:00Z", "lastObservedAt": "2026-01-15T10:00:00Z", "observationCount": 5, "approvedAt": "2026-01-12T09:00:00Z", "approvedBy": "operator", "dismissedAt": null, "dismissedReason": null, "supersededBy": null, "createdAt": "2026-01-10T08:00:00Z", "updatedAt": "2026-01-15T10:00:00Z", "scopes": [ { "id": "s_xyz", "itemId": "i_abc123", "scopeKind": "repo", "scopeRef": "owner/repo", "metadata": null, "createdAt": "2026-01-10T08:00:00Z" } ], "evidence": [ { "id": "e_xyz", "itemId": "i_abc123", "evidenceType": "pr", "ref": "42", "path": null, "url": "https://github.com/owner/repo/pull/42", "excerpt": "PR merged enforcing service layer pattern", "metadata": null, "createdAt": "2026-01-10T08:00:00Z" } ] }}The item object is a RepoIntelligenceItem extended with scopes and evidence arrays:
| Field | Type | Description |
|---|---|---|
id | string | UUID identifying this intelligence item |
tenantId | number | Tenant the item belongs to |
repoId | number | Repository the item belongs to |
kind | IntelligenceKind | Item kind (see values below) |
title | string | Short title |
summary | string | Human-readable summary |
details | string | null | Extended detail text |
status | IntelligenceStatus | Lifecycle status (see values below) |
confidence | number | null | Confidence score (0–1) |
qualityScore | number | null | Quality score (0–1) |
source | string | System or agent that created this item |
sourceAgent | string | null | Agent name if source is an agent |
sourceIssueId | number | null | Pipeline issue that created this item |
sourcePrId | number | null | PR that created this item |
sourceFindingId | string | null | Finding that created this item |
sourceRetrospectiveId | string | null | Retrospective that created this item |
metadata | object | Arbitrary metadata |
tags | string[] | null | Free-form tag list |
firstObservedAt | string | ISO timestamp of first observation |
lastObservedAt | string | ISO timestamp of most recent observation |
observationCount | number | Number of times this item has been observed |
approvedAt | string | null | ISO timestamp of approval |
approvedBy | string | null | Identity that approved this item |
dismissedAt | string | null | ISO timestamp of dismissal |
dismissedReason | string | null | Reason for dismissal |
supersededBy | string | null | ID of the item that supersedes this one |
createdAt | string | ISO timestamp of creation |
updatedAt | string | ISO timestamp of last update |
scopes | IntelligenceScope[] | Code-location scope links (detail endpoint only) |
evidence | IntelligenceEvidence[] | Evidence records backing this item (detail endpoint only) |
kind values: architecture, invariant, workflow_playbook, test_strategy, failure_pattern,
coupling, operator_preference, design_decision, risk_area, implementation_note.
status values: candidate, observed, proposed, approved, dismissed, superseded.
IntelligenceScope fields:
| Field | Type | Description |
|---|---|---|
id | string | UUID |
itemId | string | Parent intelligence item ID |
scopeKind | string | Scope granularity (repo, package, path, file, symbol, route, test, service, workflow) |
scopeRef | string | Reference string for the scope (e.g. a file path, symbol name) |
metadata | object | null | Additional scope metadata |
createdAt | string | ISO timestamp of creation |
IntelligenceEvidence fields:
| Field | Type | Description |
|---|---|---|
id | string | UUID |
itemId | string | Parent intelligence item ID |
evidenceType | string | Evidence source type (issue, pr, finding, retrospective, file, commit, ci, test, log, human) |
ref | string | null | Reference ID (issue number, PR number, etc.) |
path | string | null | File path if applicable |
url | string | null | URL to the evidence source |
excerpt | string | null | Short excerpt from the evidence |
metadata | object | null | Additional evidence metadata |
createdAt | string | ISO timestamp of creation |
Errors:
| Status | Code | When |
|---|---|---|
| 400 | INVALID_INPUT | id path param is empty |
| 404 | NOT_FOUND | Intelligence item not found |
| 500 | INTERNAL_ERROR | Database query failure |
| 503 | STORE_UNAVAILABLE | Postgres not connected |
GET /api/intelligence
Section titled “GET /api/intelligence”Lists v2 intelligence items for a repository. Auth required.
The repo query parameter is required. Requests missing it return 400.
Query parameters:
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
repo | string | Yes | — | Repository as owner/repo |
kind | string | No | — | Filter by IntelligenceKind |
status | string | No | — | Filter by IntelligenceStatus |
scope_kind | string | No | — | Filter by scope kind |
scope_ref | string | No | — | Filter by scope ref |
tag | string | No | — | Filter by tag |
source | string | No | — | Filter by source |
min_confidence | number | No | — | Minimum confidence threshold (0–1) |
limit | integer | No | 200 | Page size (max 500) |
offset | integer | No | 0 | Page offset |
Response — IntelligenceListResponse (PaginatedResponse<RepoIntelligenceItem>)
{ "items": [ { "id": "i_abc123", "tenantId": 1, "repoId": 42, "kind": "architecture", "title": "Service layer boundaries", "summary": "HTTP handlers must not call repository methods directly.", "details": null, "status": "approved", "confidence": 0.95, "qualityScore": 0.88, "source": "reviewer", "sourceAgent": "reviewer", "sourceIssueId": null, "sourcePrId": null, "sourceFindingId": null, "sourceRetrospectiveId": null, "metadata": {}, "tags": ["architecture"], "firstObservedAt": "2026-01-10T08:00:00Z", "lastObservedAt": "2026-01-15T10:00:00Z", "observationCount": 5, "approvedAt": "2026-01-12T09:00:00Z", "approvedBy": "operator", "dismissedAt": null, "dismissedReason": null, "supersededBy": null, "createdAt": "2026-01-10T08:00:00Z", "updatedAt": "2026-01-15T10:00:00Z" } ], "page": { "total": null, "limit": 200, "offset": 0, "hasMore": false }}Response item fields are the RepoIntelligenceItem shape documented in the
GET /api/intelligence/:id section above. List items do not include
scopes or evidence arrays — use the detail endpoint to retrieve those.
Errors:
| Status | Code | When |
|---|---|---|
| 400 | INVALID_INPUT | repo is missing or not in owner/repo format |
| 500 | INTERNAL_ERROR | Database query failure |
| 503 | STORE_UNAVAILABLE | Postgres not connected |
Digest
Section titled “Digest”GET /api/digest
Section titled “GET /api/digest”Returns the daily pipeline digest — a snapshot of current pipeline state counts, blocked issue totals, recent error patterns, aggregate cost, and top regressed KPIs from the regression-guard sweep. Auth required.
Response — DigestResponse
{ "digest": { "date": "2026-04-22", "pipelineByState": { "analyzing:acme/app": 2, "ready-for-dev:acme/app": 5 }, "blockedCount": 3, "recentErrors": [ { "fingerprint": "tsc_error", "message": "Cannot find name 'Foo'", "count": 2 } ], "dailyCostUsd": 12.5, "monthlyCostUsd": 245.0, "topRegressedKpis": [ { "owner": "acme", "repo": "app", "kpi": "autonomy_rate", "magnitude": "large", "currentSampleSize": 10, "baselineSampleSize": 20, "topBlockReasons": [{ "blockReason": "build_failure", "count": 3 }], "topFingerprints": [{ "fingerprint": "tsc_error", "count": 2 }] } ] }}DailyDigest fields:
| Field | Type | Description |
|---|---|---|
digest.date | string | ISO date string (e.g. "2026-04-22") |
digest.pipelineByState | Record<string, number> | Open issue counts keyed by "state:owner/repo" |
digest.blockedCount | number | Total number of blocked issues |
digest.recentErrors | object[] | Error clusters sorted by frequency descending |
digest.recentErrors[].fingerprint | string | Normalized error fingerprint |
digest.recentErrors[].message | string | Representative error message |
digest.recentErrors[].count | number | Number of occurrences |
digest.dailyCostUsd | number | null | Aggregate spend in the last 24 h; null when unavailable |
digest.monthlyCostUsd | number | null | Aggregate spend in the last 30 days; null when unavailable |
digest.topRegressedKpis | object[] | Top regressed KPIs from the regression-guard sweep, sorted by magnitude descending |
digest.topRegressedKpis[].owner | string | Repository owner |
digest.topRegressedKpis[].repo | string | Repository name |
digest.topRegressedKpis[].kpi | string | KPI identifier (e.g. "autonomy_rate") |
digest.topRegressedKpis[].magnitude | string | Regression magnitude (e.g. "large") |
digest.topRegressedKpis[].currentSampleSize | number | Issue count in the current measurement window |
digest.topRegressedKpis[].baselineSampleSize | number | Issue count in the baseline window |
digest.topRegressedKpis[].topBlockReasons | object[] | Top block-reason codes and their occurrence counts |
digest.topRegressedKpis[].topFingerprints | object[] | Top error fingerprints and their occurrence counts |
Errors:
| Status | Code | When |
|---|---|---|
| 500 | INTERNAL_ERROR | Failed to generate digest |
Projections
Section titled “Projections”GET /api/projections?status=
Section titled “GET /api/projections?status=”Lists VCS projections (queued label/state sync operations). Auth required.
Query parameters:
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
status | string | No | pending + failed | Filter by projection status (pending, failed, complete, discarded) |
When status is omitted, results are filtered to only pending and failed projections.
Response — ProjectionListResponse
{ "projections": [ { "id": 1, "repoOwner": "acme", "repoName": "app", "issueNumber": 42, "action": { "type": "addLabels", "labels": ["colony:analyzing"] }, "status": "pending", "attempts": 0, "maxAttempts": 3, "clientType": "ops", "retryAfter": null, "error": null, "createdAt": "2026-01-15T10:00:00Z", "updatedAt": "2026-01-15T10:00:00Z" } ]}The action field is a discriminated union keyed by type. See the ProjectionAction type in the SDK for all variants (e.g. addLabels, removeLabels, swapLabels, closeIssue, postComment, mergePR, etc.).
Errors:
| Status | Code | When |
|---|---|---|
| 500 | Internal server error | Unexpected failure |
| 503 | Pipeline store not available | No database connection |
POST /api/projections/:id/retry
Section titled “POST /api/projections/:id/retry”Resets a projection to pending for re-execution. Auth required.
Path parameters:
| Param | Type | Description |
|---|---|---|
id | number | Projection ID |
Response — ProjectionActionResponse
{ "ok": true }Errors:
| Status | Code | When |
|---|---|---|
| 400 | Invalid request | Malformed URL |
| 500 | Internal server error | Unexpected failure |
| 503 | Pipeline store not available | No database connection |
POST /api/projections/:id/discard
Section titled “POST /api/projections/:id/discard”Marks a projection as discarded, preventing further retries. Auth required.
Path parameters:
| Param | Type | Description |
|---|---|---|
id | number | Projection ID |
Response — ProjectionActionResponse
{ "ok": true }Errors: Same as POST /api/projections/:id/retry.
The two routes below are documented in the order the monitor router matches them:
GET /api/logs/messages is matched before GET /api/logs (prefix-match priority).
GET /api/logs/messages
Section titled “GET /api/logs/messages”Returns agent transcript messages (Claude Code conversation turns) for an issue invocation. Auth required.
Query parameters:
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
owner | string | Yes | — | Repository owner |
repo | string | Yes | — | Repository name |
issue | number | Yes | — | Issue number |
taskId | string | No | — | Filter to a specific task ID |
invocation | number | No | — | Filter to a specific invocation index within the task (zero-based) |
limit | number | No | 500 | Page size (1–2000; out-of-range values are clamped) |
offset | number | No | 0 | Row offset for pagination |
include | string | No | — | Pass tools to include tool_use and tool_result messages (omitted by default) |
Response — AgentMessagesResponse (PaginatedResponse<AgentMessageEntry>)
{ "items": [ { "taskId": "t9z8y7x6-...", "invocationIndex": 0, "sequence": 1, "agent": "worker-1", "role": "assistant", "subtype": "text", "toolName": null, "content": "I'll start by reading the issue description.", "truncated": false, "createdAt": "2026-01-15T08:01:00Z" } ], "page": { "total": null, "limit": 500, "offset": 0, "hasMore": false }}page.total is always null — the store does not compute a total row count for message
queries.
AgentMessageEntry fields:
| Field | Type | Description |
|---|---|---|
taskId | string | Task that produced this message |
invocationIndex | number | Zero-based index of the Claude Code invocation within the task |
sequence | number | Message order within the invocation |
agent | string | Worker or agent identifier |
role | "assistant" | "user" | "system" | Message role |
subtype | "text" | "thinking" | "tool_use" | "tool_result" | "system" | Message subtype |
toolName | string | null | Tool name (for tool_use and tool_result subtypes) |
toolInput | unknown | Tool call input (present when subtype is tool_use and include=tools) |
toolOutput | unknown | Tool call output (present when subtype is tool_result and include=tools) |
content | string | null | Message text content |
truncated | boolean | Whether the content was truncated due to size limits |
createdAt | string | ISO 8601 timestamp |
Errors:
| Status | Code | When |
|---|---|---|
| 400 | INVALID_INPUT | Missing or invalid owner, repo, or issue; non-integer invocation |
| 500 | INTERNAL_ERROR | Failed to query agent messages |
| 503 | STORE_UNAVAILABLE | Pipeline store (Postgres) not connected |
GET /api/logs
Section titled “GET /api/logs”Returns structured Pino log entries from agent log files. Auth required.
Query parameters:
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
agent | string | No | — | Filter to a specific agent name (matches the log filename without .log) |
level | string | No | — | Minimum log level: debug, info, warn, error, or fatal |
issue | string | No | — | Filter to entries whose issue field matches this value |
since | string | No | — | Return only entries after this time. Accepts an ISO 8601 timestamp or a relative duration (1h, 30m, 2d, 5s) |
search | string | No | — | Case-insensitive substring filter on the msg field |
limit | number | No | 200 | Page size (1–1000; values above 1000 are clamped to 1000) |
offset | number | No | 0 | Row offset for pagination |
Response — LogsResponse (PaginatedResponse<LogEntry>)
{ "items": [ { "timestamp": "2026-01-15T09:30:00Z", "level": "info", "msg": "Worker claimed task", "agent": "worker", "context": { "taskId": "t9z8y7x6-...", "issue": 42 } } ], "page": { "total": 142, "limit": 200, "offset": 0, "hasMore": false }}Results are sorted newest-first. page.total reflects the number of matching entries
across all log files (all files are read in memory on each request). When the
.colony/logs/ directory does not exist or is unreadable the endpoint returns an empty
result set rather than an error.
LogEntry fields:
| Field | Type | Description |
|---|---|---|
timestamp | string | ISO 8601 timestamp (derived from the Pino time field) |
level | string | Log level name (debug, info, warn, error, fatal) |
msg | string | Log message |
agent | string | Agent name (inferred from the log file name) |
context | Record<string, unknown> | All non-internal Pino fields (e.g. taskId, issue, repo) |
Errors:
| Status | Code | When |
|---|---|---|
| 400 | INVALID_INPUT | Invalid level; unparseable since (not ISO 8601 and not a relative duration); non-positive limit; negative offset |
Config
Section titled “Config”GET /api/config-summary
Section titled “GET /api/config-summary”Returns the monitor’s current parsed configuration: database connectivity, per-repo settings, self-healing policy, cost limits, alert channels, and live agent health. Auth required.
Unlike cache-backed observability endpoints, this route always reflects the in-memory state and returns 200 even when the pipeline store is not connected — the database.connected field indicates store availability.
Response — ConfigSummaryResponse
{ "configSource": "/etc/colony/colony.config.yaml", "database": { "host": "db.internal", "port": 5432, "migrationVersion": null, "poolSize": 10, "connected": true }, "repos": [ { "slug": "owner/repo", "branch": "main", "poolSize": 2, "setupCommand": null } ], "selfHealing": { "autoUnblockTransient": true, "autoRestart": false, "restartStrategy": "pid", "restartCooldownSec": 300, "maxRestartAttempts": 3, "maxAutoUnblocksPerIssue": 3 }, "pollIntervals": { "sprintMaster": 30, "monitor": 60, "metricsRefreshMin": 10 }, "costLimits": { "dailyUsd": null, "monthlyUsd": null, "perIssueUsd": 5.0 }, "alertChannels": [{ "type": "slack", "configured": true }], "agentHealth": [ { "name": "sprint-master", "status": "healthy", "uptime": 7200, "consecutiveFailures": 0, "lastError": null, "configReload": null } ], "configReload": null, "agentConfigReload": []}| Field | Type | Description |
|---|---|---|
configSource | string | Absolute path to the loaded config file, or "default" if no file was provided |
database.host | string | Database hostname parsed from DATABASE_URL |
database.port | number | Database port (default 5432) |
database.migrationVersion | number | null | Latest applied migration version (always null in the current implementation) |
database.poolSize | number | Configured max Postgres connections |
database.connected | boolean | Whether the pipeline store is currently connected |
repos | array | Per-repo configuration summaries |
repos[].slug | string | Repository as owner/repo |
repos[].branch | string | Base branch (defaults to "main") |
repos[].poolSize | number | Worker pool size for this repo |
repos[].setupCommand | string | null | Custom workspace setup command, or null for the default |
selfHealing.autoUnblockTransient | boolean | Whether transient blocks are automatically retried |
selfHealing.autoRestart | boolean | Whether crashed agents are automatically restarted |
selfHealing.restartStrategy | string | Restart mechanism ("pid" or "docker") |
selfHealing.restartCooldownSec | number | Minimum seconds between restart attempts |
selfHealing.maxRestartAttempts | number | Maximum consecutive restart attempts before giving up |
selfHealing.maxAutoUnblocksPerIssue | number | Maximum automatic unblocks per issue before requiring manual intervention |
pollIntervals.sprintMaster | number | Sprint-master poll interval in seconds |
pollIntervals.monitor | number | Monitor poll interval in seconds |
pollIntervals.metricsRefreshMin | number | Regression-guard metrics refresh cadence in minutes |
costLimits.dailyUsd | number | null | Daily spend cap across all agents, or null if unconfigured |
costLimits.monthlyUsd | number | null | Monthly spend cap (always null in the current implementation) |
costLimits.perIssueUsd | number | null | Per-issue cost cap, or null if unconfigured |
alertChannels[].type | string | Channel type (e.g. "slack", "pagerduty") |
alertChannels[].configured | boolean | Whether the required credential or URL is set in the environment |
agentHealth[].name | string | Agent name |
agentHealth[].status | "healthy" | "unhealthy" | "stopped" | Agent health status |
agentHealth[].uptime | number | null | Agent uptime in seconds, or null if unreachable |
agentHealth[].consecutiveFailures | number | Health check failures since last recovery |
agentHealth[].lastError | string | null | Most recent error message from the agent |
agentHealth[].configReload | ConfigReloadOutcome | null | Last config reload outcome reported by this agent |
configReload | ConfigReloadOutcome | null | Last config reload outcome for the monitor process; null if no reload has occurred |
agentConfigReload | Array<{ agentName: string; outcome: ConfigReloadOutcome }> | Per-agent config-reload outcomes read from the durable pipeline store; empty array when no store is connected |
POST /api/config/reload
Section titled “POST /api/config/reload”Hot-reloads the monitor’s configuration from the config file on disk, applies all reloadable keys immediately, and returns which keys were applied and which require a redeploy. Auth required. No request body.
Response — ConfigReloadResponse
{ "ok": true, "appliedKeys": ["agents.monitor.poll_interval", "logging.level"], "deferredKeys": ["claude", "agents.sprint_master"]}| Field | Type | Description |
|---|---|---|
ok | true | Always true on success |
appliedKeys | string[] | Monitor-owned config keys that were applied without a restart |
deferredKeys | string[] | Parsed keys that require a worker or sprint-master restart to take effect; the monitor ignores them |
Errors:
| Status | Code | When |
|---|---|---|
| 400 | CONFIG_LOAD_FAILED | The config file exists but cannot be parsed (e.g. invalid YAML) |
| 400 | CONFIG_VALIDATION_FAILED | The parsed config fails schema validation |
| 409 | NO_CONFIG_FILE | The monitor is running with the built-in default config — there is no file to reload |
Observability
Section titled “Observability”All three routes in this section are cache-backed: their responses are computed by the monitor’s background poll loop and served from memory. A 200 response with an empty result array may indicate either an empty pipeline or a poll cycle that has not yet completed.
GET /api/blocked-issues
Section titled “GET /api/blocked-issues”Returns all currently blocked pipeline issues with error-pattern clustering. Auth required.
Response — BlockedIssuesResponse
{ "issues": [ { "owner": "acme", "repo": "app", "issueNumber": 42, "title": "Fix login timeout", "state": "blocked", "blockReason": "ci_hard_failure", "blockedSince": "2026-07-01T09:00:00Z", "lastError": "E2E tests timed out after 600s", "errorPattern": "e2e_timeout" } ], "patterns": [ { "pattern": "e2e_timeout", "count": 1, "issues": [{ "owner": "acme", "repo": "app", "issueNumber": 42 }] } ], "total": 1}| Field | Type | Description |
|---|---|---|
issues | array | All blocked pipeline issues |
issues[].owner | string | Repository owner |
issues[].repo | string | Repository name |
issues[].issueNumber | number | GitHub issue number |
issues[].title | string | Issue title |
issues[].state | string | Current pipeline state |
issues[].blockReason | string | null | Taxonomy code for the block cause (e.g. "ci_hard_failure", "merge_conflict") |
issues[].blockedSince | string | null | ISO timestamp when the issue entered the blocked state |
issues[].lastError | string | null | Most recent error message |
issues[].errorPattern | string | null | Normalized fingerprint used for pattern clustering |
patterns | array | Error patterns with co-occurring issue references |
patterns[].pattern | string | Normalized error fingerprint |
patterns[].count | number | Number of issues sharing this pattern |
patterns[].issues | array | Minimal issue references (owner, repo, issueNumber) for this pattern |
total | number | Total number of blocked issues |
Errors:
| Status | Code | When |
|---|---|---|
| 503 | STORE_UNAVAILABLE | Pipeline store (Postgres) not connected |
GET /api/regression-guard
Section titled “GET /api/regression-guard”Returns the latest regression-guard KPI snapshots for every configured repository, including trend history for sparkline rendering. Auth required.
Response — RegressionGuardResponse
{ "repos": [ { "owner": "acme", "repo": "app", "kpis": [ { "kpi": "block_rate", "currentValue": 0.25, "baselineValue": 0.1, "relativeDelta": 1.5, "currentSampleSize": 20, "baselineSampleSize": 50, "regressed": true, "magnitude": "high", "direction": "rise", "history": [{ "computedAt": "2026-06-30T00:00:00Z", "value": 0.22 }] } ] } ]}| Field | Type | Description |
|---|---|---|
repos | array | Per-repository KPI snapshots |
repos[].owner | string | Repository owner |
repos[].repo | string | Repository name |
repos[].kpis | array | KPI results for this repo |
kpis[].kpi | string | KPI name (e.g. "block_rate", "cycle_time_p50") |
kpis[].currentValue | number | null | Measured value for the current window |
kpis[].baselineValue | number | null | Baseline (historical) value for comparison |
kpis[].relativeDelta | number | null | Fractional change from baseline ((current − baseline) / baseline) |
kpis[].currentSampleSize | number | Issues counted in the current window |
kpis[].baselineSampleSize | number | Issues counted in the baseline window |
kpis[].regressed | boolean | Whether this KPI has crossed the regression threshold |
kpis[].magnitude | string | Human-readable severity level (e.g. "low", "medium", "high") |
kpis[].direction | "rise" | "fall" | Whether an increase ("rise") or decrease ("fall") in this KPI signals regression |
kpis[].history | array | Recent data points for sparkline rendering, newest first |
history[].computedAt | string | ISO timestamp of the measurement |
history[].value | number | null | Measured KPI value at that point |
Errors:
| Status | Code | When |
|---|---|---|
| 503 | STORE_UNAVAILABLE | Pipeline store (Postgres) not connected |
GET /api/task-queue
Section titled “GET /api/task-queue”Returns all pending and claimed work tasks from the queue, plus aggregate queue stats. Auth required.
Response — TaskQueueResponse
{ "tasks": [ { "id": "task-uuid", "taskType": "develop", "issueNumber": 42, "repoOwner": "acme", "repoName": "app", "status": "claimed", "priority": 0, "claimedBy": "worker-1", "createdAt": "2026-07-01T10:00:00Z", "claimedAt": "2026-07-01T10:01:00Z", "waitTimeMs": 60000, "estimatedDurationMs": 900000 } ], "summary": { "totalPending": 2, "totalClaimed": 1, "avgWaitMs": 45000, "oldestPendingAgeMs": 120000 }}| Field | Type | Description |
|---|---|---|
tasks | array | All pending and claimed tasks |
tasks[].id | string | Task UUID |
tasks[].taskType | string | Task type (e.g. "develop", "review", "analyze", "merge") |
tasks[].issueNumber | number | Associated GitHub issue number |
tasks[].repoOwner | string | Repository owner |
tasks[].repoName | string | Repository name |
tasks[].status | "pending" | "claimed" | Task status |
tasks[].priority | number | Queue priority (lower value = higher priority) |
tasks[].claimedBy | string | null | Worker ID that claimed this task; null if still pending |
tasks[].createdAt | string | ISO timestamp when the task was created |
tasks[].claimedAt | string | null | ISO timestamp when the task was claimed; null if still pending |
tasks[].waitTimeMs | number | Milliseconds the task waited before being claimed (or has been waiting so far) |
tasks[].estimatedDurationMs | number | null | Estimated task duration in milliseconds; null if unknown |
summary.totalPending | number | Count of unclaimed tasks |
summary.totalClaimed | number | Count of currently claimed tasks |
summary.avgWaitMs | number | Average wait time in milliseconds across all tasks in the response |
summary.oldestPendingAgeMs | number | null | Age in milliseconds of the oldest pending task; null if no pending tasks |
Errors:
| Status | Code | When |
|---|---|---|
| 503 | STORE_UNAVAILABLE | Pipeline store (Postgres) not connected |
Dead Letters
Section titled “Dead Letters”Dead letter entries are state-transition attempts that failed permanently and were
written to the dead_letters table rather than retried. The four routes below let
operators inspect, retry, and resolve them. Auth required.
Dispatch order note:
POST /api/dead-letters/resolve-allis registered as a literal path in the route table beforePOST /api/dead-letters/:id/resolve, so the literal segmentresolve-allis matched first and is never treated as an:idvalue. CallingPOST /api/dead-letters/resolve-allalways hits the bulk resolver, not the per-id handler.
GET /api/dead-letters
Section titled “GET /api/dead-letters”Returns paginated dead letter entries. Auth required.
Query parameters:
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
limit | number | No | 100 | Page size (1–500; values above 500 are clamped) |
offset | number | No | 0 | Row offset for pagination |
Response — DeadLettersResponse (PaginatedResponse<DeadLetterEntry>)
{ "items": [ { "id": "d1a2b3c4-...", "repoOwner": "acme", "repoName": "app", "issueNumber": 42, "fromState": "ready-for-dev", "targetState": "in-review", "agentName": "developer", "error": "atomicTransitionWithSnapshot failed: transition already applied", "createdAt": "2026-01-15T10:00:00Z" } ], "page": { "total": 1, "limit": 100, "offset": 0, "hasMore": false }}DeadLetterEntry fields:
| Field | Type | Description |
|---|---|---|
id | string | UUID |
repoOwner | string | Repository owner |
repoName | string | Repository name |
issueNumber | number | Issue number |
fromState | string | null | Pipeline state from which the transition was attempted |
targetState | string | Target pipeline state of the failed transition |
agentName | string | null | Agent that attempted the transition |
error | string | Error message from the failed transition attempt |
createdAt | string | ISO 8601 timestamp when the dead letter was recorded |
page is a PageInfo object — see the findings endpoint for field definitions.
Errors:
| Status | Code | When |
|---|---|---|
| 503 | STORE_UNAVAILABLE | Pipeline store (Postgres) not connected |
POST /api/dead-letters/resolve-all
Section titled “POST /api/dead-letters/resolve-all”Marks all currently cached dead letter entries as resolved. Auth required.
This is a bulk operation: every entry returned by the in-memory dead-letter cache is
resolved via Promise.allSettled, so individual failures do not abort the batch.
The resolved count reflects the number of entries that were attempted, not just
the ones that succeeded.
No request body is required.
Response — DeadLetterResolveResponse
{ "ok": true, "resolved": 3 }| Field | Type | Description |
|---|---|---|
ok | true | Always true on success |
resolved | number | Number of dead letter entries that were resolved in this request |
Errors:
| Status | Code | When |
|---|---|---|
| 503 | STORE_UNAVAILABLE | Pipeline store (Postgres) not connected |
| 500 | INTERNAL_ERROR | Unexpected failure while resolving entries |
POST /api/dead-letters/:id/retry
Section titled “POST /api/dead-letters/:id/retry”Re-enqueues a single dead letter entry for another transition attempt. Auth required.
Path parameters:
| Param | Type | Description |
|---|---|---|
id | string | Dead letter entry UUID |
Response — DeadLetterRetryResponse
{ "ok": true, "newState": "in-review" }| Field | Type | Description |
|---|---|---|
ok | true | Always true on success |
newState | string (optional) | Pipeline state the issue transitioned to after the retry, if known |
Errors:
| Status | Code | When |
|---|---|---|
| 404 | DEAD_LETTER_NOT_FOUND | No dead letter entry with the given id |
| 500 | INTERNAL_ERROR | Unexpected failure during retry |
| 503 | STORE_UNAVAILABLE | Pipeline store (Postgres) not connected |
POST /api/dead-letters/:id/resolve
Section titled “POST /api/dead-letters/:id/resolve”Marks a single dead letter entry as resolved without retrying the transition. Auth required.
Path parameters:
| Param | Type | Description |
|---|---|---|
id | string | Dead letter entry UUID |
Response — DeadLetterResolveResponse
{ "ok": true }| Field | Type | Description |
|---|---|---|
ok | true | Always true on success |
Errors:
| Status | Code | When |
|---|---|---|
| 500 | INTERNAL_ERROR | Unexpected failure while resolving the entry |
| 503 | STORE_UNAVAILABLE | Pipeline store (Postgres) not connected |
Consistency
Section titled “Consistency”Data-consistency findings are materialized by the periodic reconciliation pass and cached in memory. The routes below expose the cache and provide one-click repairs for each finding class. Auth required.
GET /api/consistency
Section titled “GET /api/consistency”Returns paginated cached data-consistency findings. Auth required.
Query parameters:
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
limit | number | No | 100 | Page size (1–500; values above 500 are clamped) |
offset | number | No | 0 | Row offset for pagination |
Response — ConsistencyResponse (PaginatedResponse<ConsistencyFindingItem>)
{ "items": [ { "id": "stale_blocked:acme/app#42", "kind": "stale_blocked", "repoOwner": "acme", "repoName": "app", "issueNumber": 42, "detail": { "blockedFromState": "ready-for-dev" } } ], "page": { "total": 1, "limit": 100, "offset": 0, "hasMore": false }}ConsistencyFindingItem fields:
| Field | Type | Description |
|---|---|---|
id | string | Stable deterministic identifier, e.g. stale_blocked:owner/repo#42 |
kind | ConsistencyFindingKind | One of stale_blocked, stale_subtask_edge, orphan_missing_from_pg, si_linkage_gap, label_drift |
repoOwner | string | Repository owner |
repoName | string | Repository name |
issueNumber | number | null | Issue number (null for findings not tied to a specific pipeline issue) |
detail | object | Kind-specific detail fields |
Errors:
| Status | Code | When |
|---|---|---|
| 503 | STORE_UNAVAILABLE | Pipeline store (Postgres) not connected |
POST /api/consistency/:id/clear-flag
Section titled “POST /api/consistency/:id/clear-flag”Clears a stale is_blocked flag on a pipeline issue that has no active dependency
edges. Idempotent — a second call returns finding: null if the issue is already
unblocked. Auth required.
Path parameters:
| Param | Type | Description |
|---|---|---|
id | string | Finding ID from the cache |
Response — ConsistencyFixResponse
{ "ok": true, "finding": null }| Field | Type | Description |
|---|---|---|
ok | true | Always true on success |
finding | ConsistencyFindingItem | null | Re-materialized finding after repair, or null if resolved |
Errors:
| Status | Code | When |
|---|---|---|
| 404 | CONSISTENCY_FINDING_NOT_FOUND | No finding with the given id in the cache |
| 422 | INVALID_ARGUMENT | Finding has no issue number |
| 500 | INTERNAL_ERROR | Unexpected failure during repair |
| 503 | STORE_UNAVAILABLE | Pipeline store (Postgres) not connected |
POST /api/consistency/:id/resolve-subtask-edges
Section titled “POST /api/consistency/:id/resolve-subtask-edges”Marks all active subtask dependency edges for a completed epic as resolved.
Idempotent — a second call returns finding: null if edges are already resolved.
Auth required.
Path parameters:
| Param | Type | Description |
|---|---|---|
id | string | Finding ID from the cache |
Response — ConsistencyFixResponse
{ "ok": true, "finding": null }Errors:
| Status | Code | When |
|---|---|---|
| 404 | CONSISTENCY_FINDING_NOT_FOUND | No finding with the given id in the cache |
| 422 | INVALID_ARGUMENT | Finding has no issue number |
| 500 | INTERNAL_ERROR | Unexpected failure during repair |
| 503 | STORE_UNAVAILABLE | Pipeline store (Postgres) not connected |
POST /api/consistency/:id/enqueue-orphan
Section titled “POST /api/consistency/:id/enqueue-orphan”Applies the colony:enqueue label to a GitHub issue that is absent from the
pipeline database, seeding it into the pipeline via the normal label-intake path.
Uses the VCS write service rather than inline label manipulation so the outbox
drainer handles the label update. Auth required.
Path parameters:
| Param | Type | Description |
|---|---|---|
id | string | Finding ID from the cache |
Response — ConsistencyFixResponse
{ "ok": true, "finding": null }Errors:
| Status | Code | When |
|---|---|---|
| 404 | CONSISTENCY_FINDING_NOT_FOUND | No finding with the given id in the cache |
| 422 | INVALID_ARGUMENT | Finding has no issue number |
| 500 | INTERNAL_ERROR | Unexpected failure during label apply |
| 503 | STORE_UNAVAILABLE | Pipeline store or write service not connected |
POST /api/consistency/:id/link-si
Section titled “POST /api/consistency/:id/link-si”Marks an SI (self-improvement) issue as completed to close a si_linkage_gap
finding where completed_at is NULL despite the pipeline issue being done.
Idempotent — a second call returns finding: null if the linkage is already set.
Auth required.
Path parameters:
| Param | Type | Description |
|---|---|---|
id | string | Finding ID from the cache |
Response — ConsistencyFixResponse
{ "ok": true, "finding": null }Errors:
| Status | Code | When |
|---|---|---|
| 404 | CONSISTENCY_FINDING_NOT_FOUND | No finding with the given id in the cache |
| 422 | INVALID_ARGUMENT | Finding has no issue number |
| 500 | INTERNAL_ERROR | Unexpected failure during repair |
| 503 | STORE_UNAVAILABLE | Pipeline store (Postgres) not connected |
Workers
Section titled “Workers”POST /api/workers/:id/drain
Section titled “POST /api/workers/:id/drain”Toggles drain mode for a worker. A draining worker finishes its current task but does not claim new work. Auth required.
Path parameters:
| Param | Type | Description |
|---|---|---|
id | string | Worker ID (URL-encoded if necessary) |
Request body — WorkerDrainRequest
{ "drain": true }| Field | Type | Required | Description |
|---|---|---|---|
drain | boolean | Yes | true to drain, false to resume |
Response — WorkerDrainResponse
{ "ok": true, "draining": true }Errors:
| Status | Code | When |
|---|---|---|
| 400 | Invalid JSON body | Unparseable request body |
| 400 | Missing required boolean field: drain | drain is not a boolean |
| 500 | Failed to set worker drain status | Database error |
| 503 | Pipeline store not available | No database connection |
Pipeline Analytics
Section titled “Pipeline Analytics”GET /api/pipeline/state-timeseries
Section titled “GET /api/pipeline/state-timeseries”Returns issue counts over time bucketed by pipeline state. Auth required.
Query parameters:
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
window | string | No | 24h | Lookback window. One of: 1h, 6h, 12h, 24h, 48h, 7d |
buckets | integer | No | 48 | Number of time buckets (1–168) |
repo | string | No | — | Filter to a single repository (owner/repo) |
tenant | string | No | — | Filter to a tenant by external ID |
Response — StateTimeseriesResponse
{ "window": "24h", "buckets": [ { "ts": "2026-06-30T22:00:00.000Z", "states": { "new": 2, "analyzing": 1, "ready-for-dev": 5, "done": 0 } } ], "serverTimestamp": "2026-07-01T10:00:00.000Z"}| Field | Type | Description |
|---|---|---|
window | string | Echo of the requested window parameter |
buckets | StateTimeseriesBucket[] | Time-ordered array of per-bucket state counts |
buckets[].ts | string | ISO 8601 timestamp for the start of the bucket |
buckets[].states | Record<string, number> | Issue count per pipeline state, zero-filled |
serverTimestamp | string | ISO 8601 server time when the query ran |
Errors:
| Status | Code | When |
|---|---|---|
| 400 | INVALID_INPUT | Invalid window value, buckets out of 1–168 range, or malformed repo |
| 503 | STORE_UNAVAILABLE | Pipeline store not connected |
| 500 | INTERNAL_ERROR | Unexpected query failure |
GET /api/pipeline/cohort-funnel
Section titled “GET /api/pipeline/cohort-funnel”Returns cohort funnel metrics showing how many issues reached each pipeline state and the median time spent there. Auth required.
Query parameters:
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
window | string | No | 24h | Lookback window. One of: 1h, 6h, 12h, 24h, 48h, 7d |
repo | string | No | — | Filter to a single repository (owner/repo) |
tenant | string | No | — | Filter to a tenant by external ID |
Response — CohortFunnelResponse
{ "window": "24h", "cohortSize": 18, "byState": [ { "state": "new", "count": 18, "medianMinutes": 1 }, { "state": "analyzing", "count": 15, "medianMinutes": 8 }, { "state": "ready-for-dev", "count": 12, "medianMinutes": 45 }, { "state": "done", "count": 10, "medianMinutes": 0 } ], "drops": [{ "from": "analyzing", "to": "blocked", "count": 2 }], "serverTimestamp": "2026-07-01T10:00:00.000Z"}| Field | Type | Description |
|---|---|---|
window | string | Echo of the requested window parameter |
cohortSize | number | Total issues that entered the pipeline in the window |
byState | CohortFunnelStateEntry[] | Per-state counts; always includes standard spine states |
byState[].state | string | Pipeline state name |
byState[].count | number | Issues that reached this state |
byState[].medianMinutes | number | Median time spent in this state (minutes) |
drops | CohortFunnelDrop[] | Issues that left the funnel at off-spine transitions |
drops[].from | string | Source state |
drops[].to | string | Destination state (e.g. "blocked") |
drops[].count | number | Number of issues that took this transition |
serverTimestamp | string | ISO 8601 server time when the query ran |
Errors:
| Status | Code | When |
|---|---|---|
| 400 | INVALID_INPUT | Invalid window value or malformed repo |
| 503 | STORE_UNAVAILABLE | Pipeline store not connected |
| 500 | INTERNAL_ERROR | Unexpected query failure |
GET /api/pipeline/autonomy
Section titled “GET /api/pipeline/autonomy”Returns autonomous merge rate metrics, comparing the current window to a prior window of equal length. Auth required.
Query parameters:
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
window | string | No | 7d | Lookback window. One of: 1h, 6h, 12h, 24h, 48h, 7d |
repo | string | No | — | Filter to a single repository (owner/repo) |
tenant | string | No | — | Filter to a tenant by external ID |
Response — AutonomyResponse
{ "window": "7d", "mergedIssues": 40, "fullyAutonomousIssues": 34, "ratePct": 85.0, "byDay": [{ "date": "2026-06-25", "merged": 6, "autonomous": 5 }], "weekOverWeek": { "current": { "ratePct": 85.0 }, "previous": { "ratePct": 80.0 }, "deltaPp": 5.0 }, "serverTimestamp": "2026-07-01T10:00:00.000Z"}| Field | Type | Description |
|---|---|---|
window | string | Echo of the requested window parameter |
mergedIssues | number | Issues merged in the window |
fullyAutonomousIssues | number | Issues merged without human intervention |
ratePct | number | Autonomous merge rate (percentage, one decimal place) |
byDay | AutonomyDayEntry[] | Per-calendar-day breakdown |
byDay[].date | string | ISO date string (e.g. "2026-06-25") |
byDay[].merged | number | Issues merged that day |
byDay[].autonomous | number | Autonomously merged issues that day |
weekOverWeek | object | Comparison to the prior window of equal length |
weekOverWeek.current.ratePct | number | Autonomous rate for the current window |
weekOverWeek.previous.ratePct | number | Autonomous rate for the prior window |
weekOverWeek.deltaPp | number | Change in percentage points (current − previous) |
serverTimestamp | string | ISO 8601 server time when the query ran |
Errors:
| Status | Code | When |
|---|---|---|
| 400 | INVALID_INPUT | Invalid window value or malformed repo |
| 503 | STORE_UNAVAILABLE | Pipeline store not connected |
| 500 | INTERNAL_ERROR | Unexpected query failure |
GET /api/pipeline/throughput
Section titled “GET /api/pipeline/throughput”Returns the count of issues merged in a time window. Auth required.
Query parameters:
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
since | string | No | today | Lookback window. One of: today (since UTC midnight), 1h, 6h, 12h, 24h, 48h, 7d |
repo | string | No | — | Filter to a single repository (owner/repo) |
tenant | string | No | — | Filter to a tenant by external ID |
Response — ThroughputResponse
{ "since": "today", "count": 7, "windowStart": "2026-07-01T00:00:00.000Z", "serverTimestamp": "2026-07-01T10:00:00.000Z"}| Field | Type | Description |
|---|---|---|
since | string | Echo of the requested since parameter |
count | number | Number of issues merged in the window |
windowStart | string | ISO 8601 start of the counting window (UTC midnight for "today") |
serverTimestamp | string | ISO 8601 server time when the query ran |
Errors:
| Status | Code | When |
|---|---|---|
| 400 | INVALID_INPUT | Invalid since value or malformed repo |
| 503 | STORE_UNAVAILABLE | Pipeline store not connected |
| 500 | INTERNAL_ERROR | Unexpected query failure |
GET /api/pipeline-events
Section titled “GET /api/pipeline-events”Returns a paginated list of pipeline events across all agents. Auth required.
Query parameters:
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
repo | string | No | — | Filter to a repository (owner/repo) |
issue | integer | No | — | Filter to a specific issue number |
type | string | No | — | Filter by event type string |
limit | integer | No | 50 | Maximum events per page; capped at 200 |
offset | integer | No | 0 | Pagination offset |
Invalid limit and offset values are silently clamped to their defaults rather than returning a 400.
Response — PipelineEventsResponse
{ "items": [ { "id": 1001, "repoOwner": "acme", "repoName": "app", "issueNumber": 42, "eventType": "state_transition", "agent": "developer", "payload": { "from": "analyzing", "to": "ready-for-dev" }, "createdAt": "2026-07-01T09:30:00.000Z" } ], "page": { "total": 1234, "limit": 50, "offset": 0, "hasMore": true }}| Field | Type | Description |
|---|---|---|
items | PipelineEvent[] | Page of event records |
items[].id | number | Event row ID |
items[].repoOwner | string | Repository owner |
items[].repoName | string | Repository name |
items[].issueNumber | number | null | Issue number, or null for repo-level events |
items[].eventType | string | Event type string (e.g. "state_transition") |
items[].agent | string | Agent that emitted the event |
items[].payload | Record<string, unknown> | Event-specific structured data |
items[].createdAt | string | ISO 8601 event timestamp |
page.total | number | null | Total matching events across all pages |
page.limit | number | Effective page size |
page.offset | number | Current offset |
page.hasMore | boolean | Whether more pages follow |
Errors:
| Status | Code | When |
|---|---|---|
| 400 | INVALID_INPUT | Non-integer issue value or malformed repo |
| 503 | STORE_UNAVAILABLE | Pipeline store not connected |
| 500 | INTERNAL_ERROR | Unexpected query failure |
Event Stream
Section titled “Event Stream”GET /api/events
Section titled “GET /api/events”Opens a Server-Sent Events (SSE) stream of real-time pipeline events. Returns
text/event-stream (not JSON). Auth required.
Request headers:
| Header | Description |
|---|---|
Last-Event-ID | If provided, the server replays all buffered events with an ID greater than this value (up to 100 events retained) |
Response — text/event-stream
Each event follows the SSE wire format:
id: 42event: state-transitiondata: {"issueNumber":42,"repo":"acme/app","from":"analyzing","to":"ready-for-dev","agent":"worker-1","timestamp":"2026-01-15T08:05:00Z"}A heartbeat event is sent every 30 seconds to keep the connection alive.
Event types:
| Event type | Payload fields | Description |
|---|---|---|
state-transition | issueNumber, repo, from, to, agent, timestamp | An issue transitioned between pipeline states |
cost-update | issueNumber, repo, costUsd, agent, timestamp | Cost data changed for an in-flight issue |
worker-status | workerId, repo, status (idle/busy/draining/offline), issueNumber, taskType | A worker’s status changed |
alert | severity, message, detector, resolved | A failure-detector alert was raised or resolved |
issue-status | issueNumber, repo, status, actorType, state, timestamp | A new status message was emitted for an in-flight issue |
heartbeat | timestamp, uptime | Periodic keepalive (every 30 s) |
All data fields are JSON-encoded. See the SSEEvent discriminated union in
@runcolony/sdk for the complete typed payload shapes.
Errors:
| Status | Body | When |
|---|---|---|
| 503 | SSE_UNAVAILABLE | SSE manager is not running |
GET /api/workers/floor
Section titled “GET /api/workers/floor”Returns the worker floor health snapshot — one entry per live worker including its current task, utilization, and alert status. Auth required.
The response uses the UnpaginatedResponse<WorkerFloorEntry> envelope and always returns the full current worker set.
Response — WorkerFloorResponse
{ "items": [ { "id": "worker-1", "status": "running", "currentTask": { "id": "task-uuid", "type": "develop", "issueNumber": 42, "repo": "owner/repo", "pipelineState": "ready-for-dev", "etaMinutes": 12 }, "queueDepth": 3, "utilization8hPct": 87.5, "lastHeartbeat": "2026-07-01T12:00:00Z", "idle": false, "alert": null } ], "snapshot": true}| Field | Type | Description |
|---|---|---|
items | WorkerFloorEntry[] | One entry per live worker |
snapshot | true | Literal marker indicating the full current set is returned |
WorkerFloorEntry fields:
| Field | Type | Description |
|---|---|---|
id | string | Worker ID |
status | string | Worker process status (e.g. "running", "draining") |
currentTask | WorkerCurrentTask | null | Task currently claimed by this worker; null when idle |
queueDepth | number | Number of pending tasks queued for this worker’s repo |
utilization8hPct | number | Percentage of the last 8 hours this worker spent on active tasks |
lastHeartbeat | string | null | ISO timestamp of the worker’s most recent heartbeat |
idle | boolean | Whether the worker is currently idle (no active task) |
alert | WorkerFloorAlert | null | Escalation requiring human attention; null if none |
WorkerCurrentTask fields:
| Field | Type | Description |
|---|---|---|
id | string | Task UUID |
type | string | Task type (e.g. "develop", "review", "analyze") |
issueNumber | number | null | Associated issue number; null for repo-scoped tasks such as code-map-scan |
repo | string | Repository as owner/repo |
pipelineState | string | null | Current pipeline state of the associated issue |
etaMinutes | number | null | Estimated minutes until task completion; null if unknown |
WorkerFloorAlert fields:
| Field | Type | Description |
|---|---|---|
reason | string | Description of the escalation |
since | string | null | ISO timestamp when the alert was raised |
humanAssignee | string | null | Assigned human operator, if any |
Errors:
| Status | Code | When |
|---|---|---|
| 503 | STORE_UNAVAILABLE | Pipeline store (Postgres) not connected |
POST /api/workers/:id/reclaim
Section titled “POST /api/workers/:id/reclaim”Reclaims a stalled task from the named worker — resets the task to pending so another worker can claim it. Auth required. No request body.
The id path parameter is URL-decoded before being matched against the worker registry.
Path parameters:
| Param | Type | Description |
|---|---|---|
id | string | Worker ID (URL-encode if it contains special characters) |
Response — WorkerReclaimResponse
When a stalled task was found and reclaimed:
{ "ok": true, "reclaimed": true, "taskId": "task-uuid", "issueNumber": 42 }When no stalled task was found for this worker:
{ "ok": true, "reclaimed": false }| Field | Type | Description |
|---|---|---|
ok | true | Always true on success |
reclaimed | boolean | true if a stalled task was found and reset to pending |
taskId | string (optional) | ID of the reclaimed task; present only when reclaimed is true |
issueNumber | number (optional) | Issue number of the reclaimed task; present only when reclaimed is true |
Errors:
| Status | Code | When |
|---|---|---|
| 500 | RECLAIM_FAILED | Database error during the reclaim operation |
| 503 | STORE_UNAVAILABLE | Pipeline store (Postgres) not connected |
Strategy
Section titled “Strategy”GET /api/strategy/:owner/:repo/snapshots
Section titled “GET /api/strategy/:owner/:repo/snapshots”Returns strategy snapshots for a repository. Defaults to proposed and active snapshots. Auth required.
Query parameters:
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
status | string | No | proposed + active | Filter by status: proposed, approved, active, superseded |
Response — StrategySnapshotListResponse
{ "items": [{ "version": 1, "status": "proposed", "intent": "..." }], "snapshot": true }Errors:
| Status | Code | When |
|---|---|---|
| 400 | INVALID_INPUT | Invalid status value |
| 503 | STORE_UNAVAILABLE | Pipeline store not connected |
| 500 | INTERNAL_ERROR | Unexpected query failure |
GET /api/strategy/:owner/:repo/snapshots/:version
Section titled “GET /api/strategy/:owner/:repo/snapshots/:version”Returns a single strategy snapshot by version number. Auth required.
Path parameters: owner, repo, version (integer)
Response — StrategySnapshotDetailResponse
{ "snapshot": { "version": 1, "status": "proposed", "intent": "...", "rationale": "..." } }Errors:
| Status | Code | When |
|---|---|---|
| 400 | INVALID_INPUT | Non-integer version |
| 404 | SNAPSHOT_NOT_FOUND | Snapshot version not found |
| 503 | STORE_UNAVAILABLE | Pipeline store not connected |
| 500 | INTERNAL_ERROR | Unexpected query failure |
POST /api/strategy/:owner/:repo/snapshots/:version/approve
Section titled “POST /api/strategy/:owner/:repo/snapshots/:version/approve”Approves a proposed snapshot and applies it to tracks. Any authenticated monitor user may approve. Auth required.
Path parameters: owner, repo, version (integer)
Request body (optional JSON):
| Field | Type | Required | Description |
|---|---|---|---|
approver | string | No | Approver identity; defaults to operator |
Response — StrategyApproveResponse
{ "ok": true }Errors:
| Status | Code | When |
|---|---|---|
| 400 | INVALID_INPUT | Non-integer version |
| 422 | STRATEGY_APPLY_FAILED | Snapshot is stale (parent version mismatch) or violates the risk envelope |
| 503 | STORE_UNAVAILABLE | Pipeline store not connected |
| 500 | INTERNAL_ERROR | Unexpected failure |
POST /api/strategy/:owner/:repo/snapshots/:version/reject
Section titled “POST /api/strategy/:owner/:repo/snapshots/:version/reject”Rejects a proposed snapshot with an optional recorded reason. The snapshot moves to terminal status rejected. Auth required.
Path parameters: owner, repo, version (integer)
Request body (optional JSON):
| Field | Type | Required | Description |
|---|---|---|---|
reason | string | No | Human-readable reason for rejection |
Response — StrategyRejectResponse
{ "ok": true }Errors:
| Status | Code | When |
|---|---|---|
| 400 | INVALID_INPUT | Non-integer version |
| 503 | STORE_UNAVAILABLE | Pipeline store not connected |
| 500 | INTERNAL_ERROR | Unexpected failure |
GET /api/strategy/:owner/:repo/charter
Section titled “GET /api/strategy/:owner/:repo/charter”Returns the active strategy charter for a repository. Auth required.
Path parameters: owner, repo
Response — StrategyCharterResponse
{ "charter": { "version": 1, "goals": "...", "envelope": { "max_auto_size": "medium", ... } } }Returns { "charter": null } when no active charter exists.
Errors:
| Status | Code | When |
|---|---|---|
| 503 | STORE_UNAVAILABLE | Pipeline store not connected |
| 500 | INTERNAL_ERROR | Unexpected query failure |
PUT /api/strategy/:owner/:repo/charter
Section titled “PUT /api/strategy/:owner/:repo/charter”Creates or replaces the active strategy charter for a repository. Any authenticated monitor user may call this endpoint. Auth required.
Path parameters: owner, repo
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
goals | string | Yes | Human-readable strategic goals |
envelope | object | Yes | Risk envelope constraining auto-apply behaviour |
envelope.max_auto_size | string | Yes | Maximum issue size Strategist may auto-apply: small, medium, large, epic |
envelope.prefer | string[] | Yes | Track types the Strategist should favour |
envelope.avoid | string[] | Yes | Track types the Strategist should avoid |
envelope.escalate_when | string[] | Yes | Conditions that require human escalation |
author | string | No | Author identity; defaults to operator |
Response — StrategyCharterUpsertResponse
{ "ok": true, "version": 2 }Errors:
| Status | Code | When |
|---|---|---|
| 400 | INVALID_JSON | Malformed JSON body |
| 400 | INVALID_BODY | Missing goals, missing/invalid envelope, or invalid max_auto_size |
| 503 | STORE_UNAVAILABLE | Pipeline store not connected |
| 500 | INTERNAL_ERROR | Unexpected failure |
GET /api/strategy/:owner/:repo/run-state
Section titled “GET /api/strategy/:owner/:repo/run-state”Returns the latest strategize cycle run state for a repository. Auth required.
Path parameters: owner, repo
Response — StrategyRunStateResponse
{ "run": { "outcome": "failed", "failureReason": "LLM timeout", "costUsd": null, "nextDueAt": "2026-07-08T10:00:00.000Z", "createdAt": "2026-07-01T10:00:00.000Z", "consecutiveFailures": 3 } }Returns { "run": null } when no strategize cycles have run yet for the repository.
Errors:
| Status | Code | When |
|---|---|---|
| 503 | STORE_UNAVAILABLE | Pipeline store not connected |
| 500 | INTERNAL_ERROR | Unexpected query failure |
Metrics
Section titled “Metrics”GET /prometheus
Section titled “GET /prometheus”Returns Prometheus-format metrics. No auth required.
Response: text/plain; version=0.0.4; charset=utf-8
Metrics include agent health, alert counts, pipeline state gauges, tenant spend, per-repo pipeline metrics, and worker pool statistics.
For the full per-metric catalog (names, types, descriptions), see Prometheus Metrics Catalog.
Error Responses
Section titled “Error Responses”Most error responses follow the ApiError shape:
interface ApiError { error: string; // human-readable message code: string; // machine-readable error code status: number; // HTTP status code}All API error responses return the full ApiError shape — all three fields are always
present, except for the empty-body paths listed under Unmatched routes below.
Common HTTP status codes
Section titled “Common HTTP status codes”| Status | Meaning | When |
|---|---|---|
| 200 | OK | Successful request |
| 400 | Bad Request | Invalid input, malformed JSON, missing required fields |
| 401 | Unauthorized | Missing or invalid session cookie or Basic credentials |
| 404 | Not Found | Resource not found or unmatched route |
| 500 | Internal Server Error | Unexpected server-side failure |
| 503 | Service Unavailable | Pipeline store (Postgres) not connected |
Unmatched routes
Section titled “Unmatched routes”Unmatched API routes return 404 with a structured JSON body:
{ "error": "No matching route", "code": "NOT_FOUND", "status": 404 }.
One response path intentionally returns an empty body (no JSON):
/assets/*404s — directory traversal violations and missing static files.
Static Assets
Section titled “Static Assets”Serves the dashboard SPA (index.html). If the dashboard has not been built, returns
a placeholder HTML page. No auth required — the SPA shell must load so the in-page
login form can render before authentication.
GET /assets/*
Section titled “GET /assets/*”Serves static files from the dashboard build output directory. Includes directory traversal protection. No auth required — assets (JS, CSS) must load for the login form to function.