concepts

Backend contract

Pheebs holds a base URL and a token.

Pheebs is backend-agnostic. The client holds a base URL and a per-developer token, and nothing else. Anything that answers the four required routes is a Pheebs backend, and openapi.yaml at the repo root is the normative schema.

This page covers what a schema cannot express: why each route behaves the way it does, and the failure semantics a correct implementation has to honour.

Point a client at it

pheebs config set base-url https://pheebs.acme.dev
pheebs config set token <token>

Both, or nothing is sent. config set token blocks on /validate-token, so that route has to answer before a developer can finish setup. Everything after that runs on the hook path and never blocks the agent.

The routes

RouteAuthBlocking?
POST /ingest Bearer <token>No, fire and forget
POST /validate-token noneYes, at token-set time
POST /classify-prompt Bearer <token>Yes, roughly a 2.5s budget
POST /otel/v1/{traces,metrics,logs} Bearer <token>No
GET /insights Bearer <token>Optional

POST /ingest

One event envelope per request. Seven fields are always present, and everything else is per-event and open-ended.

FieldTypeNotes
timestamp stringRFC 3339 date-time
source stringAlways hook
event stringOne of 17 names, from session_started through artifact_found
codebase stringPHEEBS_CODEBASE_ID if set, else org/repo from the git remote, else local/<folder>, else the literal unknown
ai_tool stringclaude_code, cursor, or codex
session_id stringThe agent's own session id. See the warning below
version stringThe Pheebs client version that produced the row

Status codes that mean something

Only two matter to the client:

  • 401 and 403: the token was rejected. The client writes ~/.pheebs/.last-transport-error, which pheebs doctor surfaces to tell the developer to re-set their token.
  • 422: unprocessable envelope. The client writes one line to stderr and moves on.

Everything else, including 5xx, is swallowed. A connection-level failure prints one stderr line and is otherwise ignored. The client does not retry and does not queue: the local JSONL is the durable copy, and a hook must never block the agent. Do not design around client-side retry, because there is none.

POST /validate-token

The one unauthenticated route, because the token being validated travels in the body: { "token": "..." }. Return allowed plus, when allowed, id, developer, tenant, opt_out, and prompt_collection. On rejection, return a reason.

{
  "allowed": true,
  "id": "dev_01H8X",
  "developer": "octocat",
  "tenant": "acme",
  "opt_out": false,
  "prompt_collection": true
}

Three important notes:

opt_out is advisory in the current client. It changes a message printed by pheebs config set token and nothing else. It does not suppress ingest and it does not gate classification. If you mean to stop capture for a developer, enforce it server-side.

prompt_collection is the consent gate for the next route. It is cached locally when setting up the token and never refreshed automatically, because nothing on the hook path revalidates it. Revoking consent server-side does not reach a machine until its developer re-runs pheebs config set token, so enforce consent again on every /classify-prompt call. That is the only gate that actually revokes.

POST /classify-prompt

Takes { "prompt": "..." } and returns a verdict plus a single intent label:

{
  "allowed": true,
  "result": {
    "label": "pushback",
    "requests_verification": true,
    "classifier_version": "2026-09-01"
  }
}

label is the only required field inside result. It lands on the event as prompt_intent. The client copies label, requests_verification, and classifier_version, and drops everything else.

This is the only route that receives raw developer content, and it carries four obligations the others do not:

  • Answer within 2500 ms. The client aborts at that point and degrades.
  • Enforce consent again, server-side. The client checks the token's prompt_collection before sending, but a client check is not a control. Return allowed: false and the client records unclassified.

Every degraded path lands identically, with no error surfaced and nothing blocked: a non-2xx, an allowed: false, a missing result.label, or a timeout all record prompt_intent: "unclassified".

result.task_scope

Optional and additive: mechanical, bounded, cross_cutting, open_ended, or n/a. It sizes the work a prompt asks for, and only task-shaped and repair-shaped prompts get sized. A backend that does not size tasks omits the field and stays conformant.

POST /otel/v1/{signal}

An OTLP/HTTP passthrough, where {signal} is traces, metrics, or logs. The agent's native OpenTelemetry exporter points here with the Pheebs token as the bearer, and the proxy swaps in the real upstream credential before forwarding.

GET /insights (optional)

What one developer can see about their own work. The response is a map of named sections which follow the AI Proficiency Model: repertoire is Layer 1, judgement_signals is Layer 2.

Identity comes from the token, as everywhere else. The route returns the caller's own data and never anyone else's.

Build one

Start from examples/backend-node/server.js. It implements all four required routes in around 150 lines with no dependencies, answers /insights with enabled: false, appends to JSONL, and runs with node server.js:

node examples/backend-node/server.js

pheebs config set base-url http://localhost:8787
pheebs config set token pheebs_local

The full prose rationale lives in docs/backend-contract.md, and the schema that governs every field is openapi.yaml.