← mutrack

API reference

OpenAPI 3.1 JSON · Agent entry point · Plain-text guide

MethodEndpointPurpose
POST/api/v1/sessionsRedeem a one-time browser login token
GET/api/v1Discover API entry points
GET/api/v1/meGet the authenticated user and current token ID
GET/api/v1/tokensList your current token and credentials you can administer
DELETE/api/v1/tokens/{token}Revoke a credential permitted by your current token role
GET/api/v1/workspacesList accessible workspaces
POST/api/v1/workspacesCreate a workspace (superadmin only)
PATCH/api/v1/workspaces/{workspace}Rename, archive, or reopen a workspace (superadmin only)
POST/api/v1/workspaces/{workspace}/tokensIssue scoped agent instructions or a one-time browser login link
GET/api/v1/workspaces/{workspace}Get a workspace and your access
GET/api/v1/workspaces/{workspace}/eventsPoll compact workspace activity using a durable cursor (scoped access required)
GET/api/v1/workspaces/{workspace}/bugsList compact current summaries without descriptions or history
POST/api/v1/workspaces/{workspace}/bugsFile a bug; only title is required
GET/api/v1/workspaces/{workspace}/bugs/{number}Get current values only; no history or comments
PATCH/api/v1/workspaces/{workspace}/bugs/{number}Update selected fields using the current version
GET/api/v1/workspaces/{workspace}/bugs/{number}/relationshipsList shared relationships from this bug's perspective
POST/api/v1/workspaces/{workspace}/bugs/{number}/relationshipsAdd a shared relationship from either endpoint
GET/api/v1/workspaces/{workspace}/relationships/{relationship}Get a canonical relationship and its own version
PATCH/api/v1/workspaces/{workspace}/relationships/{relationship}Remove or restore a shared relationship using its version
DELETE/api/v1/workspaces/{workspace}/relationships/{relationship}Remove a shared relationship from both bugs
GET/api/v1/workspaces/{workspace}/relationships/{relationship}/historyFetch immutable relationship revisions
GET/api/v1/workspaces/{workspace}/bugs/{number}/eventsPoll activity affecting this bug, including shared relationships
GET/api/v1/workspaces/{workspace}/bugs/{number}/commentsList current comments; deleted comments require an explicit request
POST/api/v1/workspaces/{workspace}/bugs/{number}/commentsPost text and optional file attachments
GET/api/v1/workspaces/{workspace}/bugs/{number}/comments/{comment}Get a current comment without edit history
PATCH/api/v1/workspaces/{workspace}/bugs/{number}/comments/{comment}Edit comment text and attachments using the current version
DELETE/api/v1/workspaces/{workspace}/bugs/{number}/comments/{comment}Soft-delete a comment using its current version
GET/api/v1/workspaces/{workspace}/bugs/{number}/comments/{comment}/historyExplicitly fetch comment edit and deletion history
GET/api/v1/workspaces/{workspace}/bugs/{number}/comments/{comment}/attachments/{attachment}Download a file; historical files require an explicit version
GET/api/v1/workspaces/{workspace}/bugs/{number}/historyExplicitly fetch immutable full revisions, oldest first
# mutrack agent guide

Compact JSON HTTP API: /api/v1. Schemas: /openapi.json (OpenAPI 3.1.1).
Human reference: /docs. No MCP server required.

## Token authority

Users are identities for attribution and have no permissions. Every token carries
its own role and scope. Reusing a username never grants that user's other tokens'
permissions. Browser sessions carry the same capabilities as agent tokens.

- read: read one workspace's current bugs and history; revoke this token.
- write: read plus create/update bugs and post/edit/delete comments in one workspace; revoke this token.
- admin: write plus issue/list/revoke read/write credentials within that workspace;
  revoke this token. Cannot issue/revoke other admins or act in another workspace.
- superadmin: global access; manage workspaces, visibility, and all tokens; issue
  workspace admin credentials. Only the privileged local CLI mints superadmin tokens.

Send Authorization: Bearer TOKEN. Never send bearer secrets in URLs or bug text.
GET /api/v1/me returns username, token_id, role, workspace (empty for superadmin).
GET /api/v1/tokens lists only credentials this token can administer, including
itself; secrets are never returned. DELETE /api/v1/tokens/ID revokes within that
authority. Matching usernames do not permit cross-token management.

## Issuing credentials

A trusted operator with local database access can run:

    mutrack login operator --superadmin --browser
    mutrack login maintainer --workspace demo --role admin --browser
    umask 077
    mutrack login agent --workspace demo --role write --agent > .mutrack.md

--agent prints concise Markdown containing the URL, scope, role, bearer token,
token ID, expiry and instructions. Keep the file gitignored; never commit it.
--browser prints a one-time browser URL instead. Usernames are created on demand
but are attribution only, not a way to authenticate.

With an admin or superadmin bearer token, POST
/api/v1/workspaces/demo/tokens with:

    {"username":"agent","role":"write","mode":"agent","name":"Fix bug 12"}

The response contains token_id, role, workspace, mode, expires_at, and private
instructions Markdown. mode=browser instead returns login_url and link_expires_at.
The raw credential is returned only once. role=admin requires superadmin authority;
role=superadmin is never permitted through HTTP. Each issued token is independent.

Expiration is optional. CLI: --expires-in 24h or --expires-at 2027-01-01T00:00:00Z.
HTTP: optional expires_at (RFC3339). Omit for a token/session with no expiration.
For browser issuance the setting controls the resulting session from issuance time;
the login link always expires within ten minutes. Name is optional, max 200 chars.

A browser link uses origin/#login=TOKEN: the fragment is not sent in HTTP request
URLs. The page clears it and POSTs {"login_token":"TOKEN"} to /api/v1/sessions.
Success atomically consumes the login token and returns a distinct bearer session
with exactly the same role, scope, and optional expiry. Concurrent redemption
permits one success. A login token cannot authenticate as an API bearer token.
Consumed/expired/revoked links return 401; request a new link rather than retry.

All secrets are random 256-bit values; only SHA-256 hashes are stored. Metadata
includes role, workspace, kind (agent/login/browser), expires_at and revoked_at.
Consumed login links appear revoked. CLI can list/revoke any credential:

    mutrack tokens list agent
    mutrack tokens revoke 1

Revocation affects subsequent authentication checks; an in-flight request may
finish. Browser sign out revokes the session. SessionStorage keeps it in the
current tab; closing the tab clears the local secret but does not revoke it.

## Workspaces and visibility

GET /api/v1/workspaces lists public workspaces plus those permitted by your token.
Without authentication it lists public workspaces only. Superadmin sees all.
Workspace slugs are stable lowercase ASCII letters/digits/hyphens, 1–63 characters,
starting with a letter/digit. Bug numbers are local to each workspace, starting at 1.

Only superadmin can create a workspace:
POST /api/v1/workspaces with {"slug":"demo","name":"Demo"}.
Optional public=true makes it public; default is private.
PATCH /api/v1/workspaces/demo accepts name, public (boolean), archived (boolean).
Archiving preserves bugs/history and permits reads but blocks bug and comment changes.
Superadmin can reopen with archived=false. There is no destructive bug deletion.

    mutrack workspaces create demo --name "My project"
    mutrack workspaces create open-project --public
    mutrack workspaces visibility demo public
    mutrack workspaces visibility demo private
    mutrack workspaces list
    mutrack workspaces list --json
    mutrack workspaces rename demo "New name"
    mutrack workspaces archive demo
    mutrack workspaces reopen demo

Public bug lists and current bug values need NO authentication:
GET /api/v1/workspaces/demo/bugs
GET /api/v1/workspaces/demo/bugs/1
Web links: /w/demo and /w/demo/bugs/1.

Public access is read-only. Current comments and files follow workspace visibility.
Bug history, deleted comments, and comment edit history still require a scoped read,
write, admin, or superadmin token. Having a token for another workspace does not
grant history or write access to a public workspace. Private missing/inaccessible
resources both return 404. A read token attempting a write returns 403.
Changing a public workspace back to private blocks subsequent anonymous reads.

## Token-efficient bug workflow

1. Search/list summaries:
   GET /api/v1/workspaces/demo/bugs?state=open&sort=priority&limit=10
   GET /api/v1/workspaces/demo/bugs?q=token
2. Fetch a description only when needed:
   GET /api/v1/workspaces/demo/bugs/1
3. Update only changed fields and the current version:
   PATCH /api/v1/workspaces/demo/bugs/1
   {"version":1,"status":"confirmed"}
4. Resolve:
   PATCH /api/v1/workspaces/demo/bugs/1
   {"version":2,"status":"resolved"}
5. Fetch history only when needed and authorized:
   GET /api/v1/workspaces/demo/bugs/1/history?limit=5

Canonical GET and POST/PATCH responses contain current values only: number,
version, nonempty fields, changed_by, changed_at. Never history arrays, comments, attachments, previous values, or redundant
workspace objects. Summaries additionally
omit descriptions and changed_by/changed_at. JSON is compact; empty optional text
is omitted. History is separate, authenticated, oldest first, and paginated.

POST /api/v1/workspaces/demo/bugs requires only title:

    {"title":"Expired token accepted","description":"Steps to reproduce…",
     "component":"backend","category":"authentication","type":"bugfix",
     "priority":"high"}

Fields:
- title: nonblank, max 300 characters.
- description: free text, max 100000 characters.
- category, component, type: free text, max 200 characters. Type examples: feature,
  bugfix, task. Empty string clears optional text.
- status: new (default), confirmed, resolved. Reopening is allowed.
- priority: low, normal (default), high, urgent.
- resolution: optional fixed, duplicate, invalid, or wontfix; only when resolved.
  Empty string clears it. Reopening automatically clears an omitted resolution.
  Older resolved bugs may have no reason; none is inferred during migration.
- duplicate_of: optional command on bug POST/PATCH, rather than a stored field.
  It requires status=resolved and resolution=duplicate, and atomically adds a
  duplicate_of relationship to the target. Without this command an active
  duplicate_of relationship must already exist. Removing its target while the
  resolution remains duplicate returns 409; change the resolution first.

PATCH is partial JSON, not JSON Merge Patch. Omitted fields stay unchanged; null,
duplicate/unknown keys, wrong types and trailing JSON are rejected. A no-op keeps
the version. Use comments for discussion or explanations. Every effective
change appends a complete immutable snapshot with actor/time. Failed changes do
not alter current values or history.

## Shared relationships

    POST /api/v1/workspaces/demo/bugs/12/relationships
    {"type":"blocks","number":34}
    GET /api/v1/workspaces/demo/bugs/12/relationships
    GET /api/v1/workspaces/demo/bugs/34/relationships

The two lists display blocks #34 and blocked_by #12 with the same id and version.
Each entry also includes canonical kind, source, target, active, changed_by,
changed_at, and perspective-specific type/number. Supported input types are
blocks, blocked_by, related, duplicate_of, has_duplicate. related is symmetric;
duplicate_of reverses to has_duplicate. Canonical storage kinds are blocks,
duplicate_of, related; inverse views are derived. No self-links or cross-workspace
relationships. A bug can have one active duplicate target; cycles are rejected.

Relationships have independent versions and immutable lifecycle revisions.
They are never embedded in current bug responses. Adding/removing/restoring a
relationship does not change either bug's version, fields, or ETag.

    GET /api/v1/workspaces/demo/relationships/42
    DELETE /api/v1/workspaces/demo/relationships/42?version=1
    PATCH /api/v1/workspaces/demo/relationships/42
    {"version":2,"active":true}
    GET /api/v1/workspaces/demo/relationships/42/history?limit=5

Deletion appends an inactive revision; restoration appends an active revision.
End points and kind are immutable. Stale versions return 409. A no-op with the
current version creates no revision or event. Adding an existing active edge
from either perspective returns its current identity (201) without a new event.
POST against an inactive identity returns 409; restore explicitly using its
current version. Optional Idempotency-Key replays the original POST result.

Default lists/GET show only active relationships and follow workspace visibility.
Use include_inactive=true explicitly for removed relationships; scoped read access
is required, including in public workspaces. /history always requires scoped
access. Lists use relationship IDs as after cursors; history uses revision numbers.
Find neighbors in either direction with GET /bugs?linked_to=NUMBER.

To resolve as a duplicate in one transaction:

    PATCH /api/v1/workspaces/demo/bugs/12
    {"version":3,"status":"resolved","resolution":"duplicate","duplicate_of":34}

A conflicting/inactive existing duplicate target must be handled explicitly.
Reopening clears an omitted resolution and retains relationships. Change the
resolution before removing its required duplicate target. The current bug and
relationship updates, events, and any creation receipt commit atomically.

Migration 003 imports and deduplicates current legacy links into shared identities.
Initial revisions have imported=true and no changed_by user. Original bug
snapshots retain legacy links as historical data; new bug revisions contain none.
The retired links field is rejected on bug POST/PATCH. Legacy duplicate cycles
must be repaired before migrating; validation failures roll back the migration.

## Shared queue and handoffs

There are no assignees or in_progress status. Agents run on demand and may stop
without returning; persistent ownership would leave stale assignments. Choose
work from the shared queue and leave durable context in comments before stopping:

    Findings:
    Reproduction:
    Attempted fixes:
    Verification (commands and results):
    Remaining work:

Record concrete commands, results, commit/PR links, and unresolved questions.
The browser can insert this template. Attribution records authors rather than
ongoing ownership. Version checks prevent lost edits, not duplicate investigation;
coordinate parallel runs in the launcher if necessary.

## Incremental activity

    GET /api/v1/workspaces/demo/events?after=0&limit=20

Requires a scoped read/write/admin or superadmin token, even for public workspaces.
Returns {"items":[...],"cursor":N}, with next_after when more pages are ready.
Each event has id, kind, version, changed_at, and affected_bugs. Bug/comment events
also include number and changed_by, and comment events include comment_id.
Relationship events include relationship_id, relationship_type (canonical kind),
source, target, and both endpoints in affected_bugs. Their version belongs to the
relationship, not either bug. Kinds: bug.created, bug.updated, comment.created,
comment.updated, comment.deleted, relationship.added, relationship.removed,
relationship.imported. Imports record migration time and omit changed_by rather
than attributing a historical action. No text, snapshots, or file bytes are
included. Fetch current resources separately; deleted comments and inactive
relationships require their explicit scoped reads.

GET /api/v1/workspaces/demo/bugs/12/events uses the same cursor contract and
includes events affecting either endpoint. One shared relationship change is one
workspace event. It appears in both per-bug feeds and updates both activity sorts.

Save cursor and pass it as after on every subsequent page/poll. An empty poll
retains the supplied cursor. Cursor IDs are monotonic within a workspace but may
have gaps; do not increment them yourself. Existing history is backfilled by the
migration. No-op updates, failed transactions, and replayed creations emit no
new events. Activity includes comment changes without changing a bug's version.
The feed remains readable in archived workspaces. Store the bookmark outside an
ephemeral agent session so a later run can resume it.

## Comments and attachments

Discussion is separate from bug revisions. Comments use their own versions, so a
comment edit does not change the bug version. All workspace writers/admins can
post/edit/delete comments. Comments are plain text, not executable HTML.

    GET /api/v1/workspaces/demo/bugs/1/comments?limit=10
    POST /api/v1/workspaces/demo/bugs/1/comments
    {"body":"Fixed in abc123."}

Text-only requests use application/json. For arbitrary file types, use
multipart/form-data with body and repeated file parts:

    curl -H "Authorization: Bearer $MUTRACK_TOKEN" \
      -F 'body=Reproduction details' -F 'file=@screen.png' -F 'file=@logs.zip' \
      "$MUTRACK_URL/api/v1/workspaces/demo/bugs/1/comments"

At least nonblank text or an attachment is required. Files-only comments are
allowed. Maximum: 10000 characters of text, 5 attachments per comment, 10 MiB per
file, 25 MiB total, and 26 MiB per multipart request. Any file type can be attached.
Files and metadata commit atomically; failures do not leave orphan files.

Responses contain current id, version, body (when nonempty), author/created_at,
changed_by/changed_at, and attachment metadata: id, filename, content_type, size,
url. Download bytes separately from url using the same Authorization header;
current files in public workspaces need no authentication. Files are never
base64-encoded in JSON. The canonical bug endpoint does not include comments.

    PATCH /api/v1/workspaces/demo/bugs/1/comments/ID
    {"version":1,"body":"Updated details"}

Omitted fields stay unchanged. attachment_ids specifies which current file IDs
to retain; [] removes all. IDs must belong to this comment's current version.
Multipart PATCH uses version, optional body, optional attachment_ids as a JSON
array field, plus new repeated file parts. New files append to retained files.
A no-op keeps the comment version; every effective edit appends a full snapshot.

    DELETE /api/v1/workspaces/demo/bugs/1/comments/ID?version=2

Delete uses the current comment version and appends a tombstone. Stale versions
return 409; read current values and reconcile first. Deleted comments disappear
from default lists and GETs. No content is destroyed.

Fetch old/deleted content only when specifically needed:
- GET the comment or list with include_deleted=true to include deleted comments.
- GET /comments/ID/history?limit=5 to fetch edit/deletion revisions, oldest first.
- Historical file URLs add ?version=N. Normal URLs cannot fetch removed/deleted
  files. Attachment metadata returned by history/deleted reads provides these URLs.

These explicit requests require a scoped read/write/admin or superadmin token,
even for public workspaces. A token for another workspace cannot fetch hidden
history. Default reads include no prior text, deletion tombstones, or removed
attachments. History/downloads are not fetched automatically by API bug reads.

Comment creation supports Idempotency-Key, including multipart uploads; see
creation retries below. Without a key, check the comment list before retrying an
ambiguous failure. Comment pagination uses IDs; history pagination uses
revision numbers. Retain include_deleted when continuing a deleted-inclusive list.

## Creation retries

On POST /workspaces/SLUG/bugs, /workspaces/SLUG/bugs/NUMBER/comments, or
/workspaces/SLUG/bugs/NUMBER/relationships, optionally
send Idempotency-Key: UNIQUE_KEY. Keys contain 1–128 visible ASCII characters
without spaces. Generate a key before the first request and reuse it with the
same decoded content on retries. Multipart boundaries do not affect matching;
file names, order, and contents do. Bug defaults are normalized. Relationship creation normalizes inverse and symmetric identities.

A matching retry returns the original creation response (201, Location, and the
original version/ETag for bugs), even after later edits. Changed content with the
same key returns 409. Concurrent retries commit one creation, one receipt, and
its associated revisions/events atomically; failures leave no reservation. Receipts persist across
server restarts and are retained indefinitely. Scope is the current credential,
workspace, and creation endpoint, so keep the same credential for a retry.
Authentication, write access, and archive checks apply on every request.
Without a key, these POSTs remain non-idempotent. Other POST endpoints do not
support this header and must not be automatically retried.

## Concurrency, pagination, and errors

PATCH requires the version from GET/summary. On 409, reread, reconcile, and retry
with the new version. ETag also reports it, but the precondition is the JSON
version field, not If-Match. Use Idempotency-Key for bug/comment/relationship creation retries;
otherwise check for success before retrying an ambiguous failure.
DELETE revocation is idempotent when called
with another credential that remains authorized.

All lists return {"items":[...]}. Default limit=20, max=100. next_after is added
when more results exist; pass it as after, retaining filters. Bug lists sorted by
newest/priority/activity instead return next_cursor; pass it as cursor with the
same sort and filters (limit may change). Do not combine cursor with after.
Bug default/token/history/event cursors are numbers; workspace cursors are slugs.

Bug filters combine with AND:
- status: new, confirmed, resolved.
- state: open (new or confirmed), closed (resolved). Conflicting state/status or
  open/resolution combinations return 400.
- priority, category, component, type, resolution: exact, case-sensitive.
- linked_to: bugs connected to the supplied workspace-local number in either direction.
- q: case-insensitive literal substring of title/description; search_comments=true
  adds current undeleted comment text, never historical text or attachment bytes.

Sort: number (default, ascending), newest (descending number), priority (urgent,
high, normal, low), activity (latest event, including comments and shared relationships).
Nondefault sorts break ties by descending bug number. Unknown/repeated query
parameters and cursors reused with different filters return 400. Lists are live:
concurrent edits can move entries between pages or change filter membership.
Use /events for reliable incremental activity rather than polling sorted lists.

Use application/json on POST/PATCH (1 MiB limit), or multipart/form-data for
comment uploads (26 MiB limit).
200 reads/updates; 201 creation; 204 revocation with no body. Dates are RFC3339.
Errors follow RFC 9457 application/problem+json:
{"type":"about:blank","title":"Conflict","status":409,"detail":"Bug version has changed…"}

400 invalid input, 401 missing/expired/revoked token, 403 insufficient authority,
404 missing/inaccessible resource, 405 unsupported method, 409 stale version/slug
collision/archived workspace, 413 oversized body, 415 content type, 500 failure.
401 includes WWW-Authenticate: Bearer. API responses use Cache-Control: no-store.
Documentation and /api/v1 discovery are public. /healthz checks PostgreSQL.
Use HTTPS in production behind a TLS reverse proxy.