> ## Documentation Index
> Fetch the complete documentation index at: https://docs.oobo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Backend Integration

> How to read oobo anchor data from connected repositories

The backend ingests anchor data from connected repositories. It has **no direct git access** - it reads data exclusively through platform APIs (GitHub, GitLab, Bitbucket) triggered by webhooks.

## 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:

```bash theme={null}
oobo settings set api_url https://oobo.mycompany.com
```

Your backend implements endpoints under `/anchors`.

***

## POST `/anchors/search`

Search across anchors, sessions, and semantic memory. Returns full-text and memory hits, with an optional synthesized answer.

### Request

```json theme={null}
{
  "query": "why did we build the auth middleware?",
  "since": "7d",
  "project": { "kind": "global", "value": null },
  "tool": "cursor",
  "limit": 20
}
```

| 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

```json theme={null}
{
  "answer": "The auth middleware was built to handle JWT refresh tokens because session-based auth had scaling issues.",
  "hits": [
    {
      "source": "memory",
      "snippet": "Team decided JWT with refresh tokens because session-based auth had scaling issues",
      "anchor_sha": "def456",
      "score": 0.87,
      "memory_id": "mem_abc123",
      "author": "teddy",
      "session_ids": ["sess-3", "sess-4"],
      "project": { "name": "oobo-agent" },
      "timestamp": 1773100000
    },
    {
      "source": "fts",
      "project": { "id": "p1", "name": "staging-only" },
      "anchor_sha": "abc123",
      "session_id": "sess-1",
      "tool": "cursor",
      "tokens": 12000,
      "timestamp": 1773282899,
      "intent": "fix auth middleware",
      "snippet": "auth middleware token refresh",
      "score": 0.91
    }
  ]
}
```

| 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

```json theme={null}
{
  "anchor_sha": "abc123",
  "previous_sha": null,
  "repo_id": null,
  "git_remote": null,
  "full": false
}
```

| 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`)

```json theme={null}
{
  "current": {
    "sha": "abc123",
    "message": "feat: add JWT auth",
    "author": "teddy",
    "timestamp": "2026-05-05T10:00:00Z",
    "project": "oobo-agent",
    "headline": "Implemented JWT authentication with refresh token rotation",
    "category": "feature",
    "outcome": "success",
    "complexity": "moderate"
  },
  "previous": {
    "sha": "def789",
    "message": "refactor: extract auth middleware",
    "author": "teddy",
    "timestamp": "2026-05-04T16:00:00Z",
    "project": "oobo-agent",
    "headline": "Extracted auth logic into reusable middleware",
    "category": "refactor",
    "outcome": "success",
    "complexity": "low"
  },
  "changes": {
    "category_shift": { "from": "refactor", "to": "feature" },
    "complexity_shift": { "from": "low", "to": "moderate" },
    "new_areas": ["core/security", "redis"],
    "new_techniques": ["token rotation", "Redis TTL"],
    "files_continued": ["src/core/security/auth.py"],
    "files_new": ["src/core/security/refresh.py"],
    "narrative": "Built on the extracted middleware to add full JWT auth with refresh tokens..."
  }
}
```

When `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**. As of v2.0.0, new installs write to `oobo/anchors/v2`. The legacy `oobo/anchors/v1` branch is read-only and preserved for pre-2.0 data. Both branches have zero relationship to the repo's code history - they contain only structured JSON metadata about commits enriched with AI session data, attribution, and transcripts.

The CLI pushes to the `oobo/anchors/v2` branch on every `git push` (via a `pre-push` hook). No user action is required.

### Configurable Anchor Remote

<Warning>
  Anchors may not live in the same repository as the code. Users can configure a separate Git remote for anchor data via `.oobo/config`:

  ```toml theme={null}
  [anchors]
  remote = "git@github.com:org/repo-anchors.git"
  ```

  Or via the CLI: `oobo settings project set remote <value>`
</Warning>

When `[anchors].remote` is configured, the CLI pushes the `oobo/anchors/v2` branch to that remote instead of `origin`. Your backend must handle this by:

1. **Monitoring `.oobo/config` on the default branch** of connected repos for changes to `[anchors].remote`
2. **When `[anchors].remote` points to a separate repo**, register webhooks on that repo and listen for pushes to both `oobo/anchors/v2` and `oobo/anchors/v1` (legacy) there
3. **When unset or set to a named remote** (e.g. `origin`), anchors arrive via pushes to the same repository

The value can be either a named remote (e.g. `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 |

The user's `git push` is never blocked. Anchor push failures are warnings only. The local `oobo/anchors/v2` branch always has the complete data - the next successful push will include all pending anchors.

### Directory Layout (Sharding)

```
oobo/anchors/v2
├── README.md
├── c8/
│   └── e12fa9b3d4abcdef1234567890abcdef123456/
│       ├── metadata.json            ← Anchor (commit-level)
│       ├── timeline.json            ← Multi-agent interactions (optional)
│       ├── 1/
│       │   ├── metadata.json        ← SessionLink (per-session)
│       │   ├── transcript.json      ← Redacted transcript (if transparency=on)
│       │   └── subagents/
│       │       └── 1/
│       │           ├── metadata.json   ← Subagent type/ID
│       │           └── transcript.json
│       └── 2/
│           ├── metadata.json
│           └── transcript.json
└── ...
```

The commit hash (40-char hex SHA) is split: first 2 characters → top-level directory, remaining 38 → subdirectory name.

## Webhook Trigger

### Discovery: Where Do Anchors Live?

Before processing webhooks, the backend must determine where a project's anchors are pushed:

1. On repo connection, read `.oobo/config` from the default branch (if it exists)
2. Check for `[anchors].remote` - if set, anchors are pushed to that repo/remote
3. If unset, anchors are in the same repo on branch `oobo/anchors/v2`
4. Subscribe to pushes on the **default branch** (to detect config changes), `oobo/anchors/v2`, and `oobo/anchors/v1` (legacy)
5. If a push to the default branch modifies `.oobo/config`, re-read `[anchors].remote` and update webhook registration accordingly

### GitHub

Listen for **push events** where `ref` is `refs/heads/oobo/anchors/v2`. During the migration period, also monitor `refs/heads/oobo/anchors/v1` for legacy data:

```json theme={null}
{
  "ref": "refs/heads/oobo/anchors/v2",
  "before": "abc123...",
  "after": "def456...",
  "commits": [
    {
      "id": "def456...",
      "message": "oobo: add anchor for c8/e12fa9b3d4.../metadata.json",
      "added": ["c8/e12fa9b3d4.../metadata.json", "c8/e12fa9b3d4.../1/metadata.json"]
    }
  ]
}
```

### GitLab

Listen for **push events** where `ref` is `refs/heads/oobo/anchors/v2` (and `refs/heads/oobo/anchors/v1` for legacy data).

### Bitbucket

Listen for **repo:push** events. Filter `push.changes[].new.name == "oobo/anchors/v2"` (and `"oobo/anchors/v1"` for legacy data).

## Reading Files via Platform APIs

<Tabs>
  <Tab title="GitHub REST">
    **List all files (recursive tree):**

    ```
    GET /repos/{owner}/{repo}/git/trees/oobo/anchors/v2?recursive=1
    Authorization: Bearer {token}
    ```

    **Read a single file:**

    ```
    GET /repos/{owner}/{repo}/contents/{path}?ref=oobo/anchors/v2
    Authorization: Bearer {token}
    Accept: application/vnd.github.raw+json
    ```

    **Compare two commits (incremental processing):**

    ```
    GET /repos/{owner}/{repo}/compare/{before}...{after}
    Authorization: Bearer {token}
    ```
  </Tab>

  <Tab title="GitHub GraphQL">
    ```graphql theme={null}
    query {
      repository(owner: "org", name: "repo") {
        object(expression: "oobo/anchors/v2:c8/e12fa.../metadata.json") {
          ... on Blob { text }
        }
      }
    }
    ```

    Use GraphQL for bulk reads - you can fetch multiple objects in one query.
  </Tab>

  <Tab title="GitLab">
    **List tree:**

    ```
    GET /api/v4/projects/{id}/repository/tree?ref=oobo/anchors/v2&recursive=true
    ```

    **Read file:**

    ```
    GET /api/v4/projects/{id}/repository/files/{path}/raw?ref=oobo/anchors/v2
    ```
  </Tab>

  <Tab title="Bitbucket">
    **List directory:**

    ```
    GET /2.0/repositories/{workspace}/{repo}/src/oobo/anchors/v2/?max_depth=10
    ```

    **Read file:**

    ```
    GET /2.0/repositories/{workspace}/{repo}/src/oobo/anchors/v2/{path}
    ```
  </Tab>
</Tabs>

## Ingestion Strategy

### On Webhook Push

1. Extract `before` and `after` SHAs from the webhook payload
2. Get the diff between those two commits
3. For each newly added `XX/YYYY.../metadata.json` (3 segments) → new **anchor**
4. For each newly added `XX/YYYY.../N/metadata.json` (4 segments, N is numeric) → new **session link**
5. For each newly added `XX/YYYY.../N/transcript.json` → session transcript
6. For each newly added `XX/YYYY.../timeline.json` → multi-agent timeline

### Pattern Matching

```
XX/YYYY.../metadata.json              → Anchor metadata
XX/YYYY.../N/metadata.json            → SessionLink (N is 1-indexed)
XX/YYYY.../N/transcript.json          → Session transcript
XX/YYYY.../N/subagents/M/transcript.json → Subagent transcript
XX/YYYY.../timeline.json              → Multi-agent timeline
```

### Full Sync (Initial or Recovery)

1. Fetch the recursive tree of `oobo/anchors/v2` (and `oobo/anchors/v1` for legacy data)
2. Filter paths matching anchor metadata pattern
3. Bulk-fetch all metadata files
4. 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/v2` (or `oobo/anchors/v1` for legacy repos) |
| `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                               |
