Remote API Surface
The CLI calls these endpoints. There is no cloud upload/ingest pipeline - team sync is Git-first via the orphan branch.| Endpoint | Method | Auth | Required | Purpose |
|---|---|---|---|---|
/anchors/search | POST | Bearer token | Yes | Search anchors/sessions with memory + answer synthesis |
/anchors/delta | POST | Bearer token | Yes | Compare two anchors (what changed between them) |
/anchors/health | GET | None | No | Health check (recommended convention, not called by CLI) |
Self-Hosting
Point your CLI at your own server:/anchors.
POST /anchors/search
Search across anchors, sessions, and semantic memory. Returns full-text and memory hits, with an optional synthesized answer.
Request
| Field | Type | Required | Description |
|---|---|---|---|
query | string | Yes | Natural-language search query |
since | string? | No | Time filter (e.g. 24h, 7d, ISO-8601) |
project | object? | No | Scope: {"kind": "global"}, {"kind": "project_name", "value": "..."}, or {"kind": "project_id", "value": "..."} |
tool | string? | No | Filter by tool name |
limit | number | No | Max results (default 20) |
Response
| Field | Type | When present | Description |
|---|---|---|---|
answer | string? | When memory hits exist and synthesis succeeds | Synthesized 1-3 sentence answer to the query |
hits[].source | string | Always | "fts" (full-text search) or "memory" (semantic memory). Defaults to "fts" if absent |
hits[].memory_id | string? | Memory hits only | Unique memory identifier |
hits[].session_ids | string[]? | Memory hits only | Sessions that contributed to this memory |
hits[].author | string? | Memory hits only | Who created the anchor this memory came from |
hits[].anchor_sha | string? | Both | Commit SHA (may be short) |
hits[].session_id | string? | FTS hits | Single session ID |
hits[].tool | string? | FTS hits | Tool name |
hits[].tokens | number? | FTS hits | Token count |
hits[].intent | string? | FTS hits | Commit intent |
hits[].snippet | string? | Both | Matching text excerpt |
hits[].score | number? | Both | Relevance score (0-1) |
hits[].project | object | Both | { "id": "...", "name": "..." } |
hits[].timestamp | number? | Both | Unix timestamp |
POST /anchors/delta
Compare two anchors to see what changed: category shifts, complexity changes, new areas, new techniques, and a narrative summary.
Request
| Field | Type | Required | Description |
|---|---|---|---|
anchor_sha | string | Yes | Commit hash of the anchor to inspect |
previous_sha | string? | No | Explicit previous anchor to compare against. If null, auto-finds the previous anchor |
repo_id | string? | No | Filter by repo ID |
git_remote | string? | No | Filter by git remote URL |
full | bool | No | If true, includes detailed narrative, decisions, techniques, sessions |
Response (compact, full: false)
full: true, the response adds current_detail and previous_detail objects with: narrative, key_decisions, techniques, areas_affected, blockers, intent, reasoning, and session info.
Error Responses
| Status | Body | Description |
|---|---|---|
| 404 | {"error": "anchor_not_found", "message": "..."} | The specified anchor SHA was not found |
| 500 | {"error": "internal_error", "message": "..."} | Server error |
The Orphan Branch
All anchor data lives on a Git orphan branch namedoobo/anchors/v1. This branch has zero relationship to the repo’s code history. It contains only structured JSON metadata about commits enriched with AI session data, attribution, and transcripts.
The CLI pushes to this branch on every git push (via a pre-push hook). No user action is required.
Configurable Anchor Remote
When[anchors].remote is configured, the CLI pushes the oobo/anchors/v1 branch to that remote instead of origin. Your backend must handle this by:
- Monitoring
.oobo/configon the default branch of connected repos for changes to[anchors].remote - When
[anchors].remotepoints to a separate repo, register webhooks on that repo and listen for pushes tooobo/anchors/v1there - When unset or set to a named remote (e.g.
origin), anchors arrive via pushes to the same repository
oobo) or a full URL (e.g. git@github.com:org/repo-anchors.git). Your backend should resolve named remotes via the repository’s Git config or treat direct URLs as the target.
Push Behavior and Data Safety
| Scenario | User-Visible Behavior | Data Loss? |
|---|---|---|
| Push succeeds | Silent - no output | No |
| Non-fast-forward (contention) | Auto-retries up to 5 times with fetch+reconcile | No |
| Permission denied | Warning on stderr: failed to push oobo anchors: ... | No - local branch intact |
| Remote doesn’t exist | Warning on stderr | No - local branch intact |
| Network failure | Warning on stderr | No - local branch intact |
git push is never blocked. Anchor push failures are warnings only. The local oobo/anchors/v1 branch always has the complete data - the next successful push will include all pending anchors.
Directory Layout (Sharding)
Webhook Trigger
Discovery: Where Do Anchors Live?
Before processing webhooks, the backend must determine where a project’s anchors are pushed:- On repo connection, read
.oobo/configfrom the default branch (if it exists) - Check for
[anchors].remote- if set, anchors are pushed to that repo/remote - If unset, anchors are in the same repo on branch
oobo/anchors/v1 - Subscribe to pushes on both the default branch (to detect config changes) and
oobo/anchors/v1 - If a push to the default branch modifies
.oobo/config, re-read[anchors].remoteand update webhook registration accordingly
GitHub
Listen for push events whereref is refs/heads/oobo/anchors/v1:
GitLab
Listen for push events whereref is refs/heads/oobo/anchors/v1.
Bitbucket
Listen for repo:push events. Filterpush.changes[].new.name == "oobo/anchors/v1".
Reading Files via Platform APIs
- GitHub REST
- GitHub GraphQL
- GitLab
- Bitbucket
List all files (recursive tree):Read a single file:Compare two commits (incremental processing):
Ingestion Strategy
On Webhook Push
- Extract
beforeandafterSHAs from the webhook payload - Get the diff between those two commits
- For each newly added
XX/YYYY.../metadata.json(3 segments) → new anchor - For each newly added
XX/YYYY.../N/metadata.json(4 segments, N is numeric) → new session link - For each newly added
XX/YYYY.../N/transcript.json→ session transcript - For each newly added
XX/YYYY.../timeline.json→ multi-agent timeline
Pattern Matching
Full Sync (Initial or Recovery)
- Fetch the recursive tree of
oobo/anchors/v1 - Filter paths matching anchor metadata pattern
- Bulk-fetch all metadata files
- Process in parallel - anchors are independent
Rate Limits
- GitHub REST: 5,000 requests/hour (authenticated)
- GitHub GraphQL: 5,000 points/hour
- Use blob SHAs for deduplication
Key Invariants
| Rule | Detail |
|---|---|
| One anchor per commit | Directory path is unique per commit hash |
| Sessions are 1-indexed | Subdirectories are 1/, 2/, 3/… |
| Append-only | Anchors are never deleted from the branch |
| Idempotent | Re-pushing the same anchor overwrites with identical content |
| Consistency | ai_added + human_added == added always holds |
| Consistency | ai_deleted + human_deleted == deleted always holds |
| Ordering | Sessions are ordered chronologically (1 = earliest) |
Edge Cases
| Scenario | Behavior |
|---|---|
| Branch doesn’t exist yet | No oobo data - skip until first push to oobo/anchors/v1 |
session_ids is empty | Pure human commit (author_type = "human" or "automated") |
ai_percentage is null | No AI involvement - treat as 0% |
is_estimated is true | Token counts were estimated via tiktoken (~90% accuracy) |
link_type is "inferred" | Session linked via time-window heuristic (±5min of commit) |
| Multiple anchors in one push | Process all - user committed multiple times locally then pushed |
| Transparency mode mixed | Check transparency_mode per anchor - some have transcripts, some don’t |
[anchors].remote configured | Anchors arrive via a different repo - backend must follow the remote |
[anchors].remote changes | Re-read .oobo/config on default-branch pushes, update webhook targets |
| Push failures (permissions etc.) | Data stays on user’s local branch - next successful push catches up |