# repo-sync API Reference

**Service:** `repo-sync-service`  
**Base URL (local):** `http://localhost:6001`  
**Gateway integration:** [GATEWAY-REPO-SYNC-CONTRACT.md](../../../../api.lucos.com/docs/GATEWAY-REPO-SYNC-CONTRACT.md)

---

## Quick start (curl)

Tier 1 direct import (bypass gateway) — health check, import, poll status:

```bash
BASE_URL="http://localhost:6001"

# Health
curl -sS "${BASE_URL}/readyz"

# Import (replace credentials and repo details)
IMPORT_JOB_ID=$(uuidgen | tr '[:upper:]' '[:lower:]')
curl -sS -X POST "${BASE_URL}/repos/import" \
  -H "Content-Type: application/json" \
  -H "X-Org-Id: test-org-1" \
  -d "{
    \"importJobId\": \"${IMPORT_JOB_ID}\",
    \"orgId\": \"test-org-1\",
    \"connectionId\": \"test-conn-1\",
    \"provider\": \"github\",
    \"cloneAuth\": {
      \"username\": \"x-access-token\",
      \"password\": \"<YOUR_GITHUB_PAT>\",
      \"expiresAt\": \"$(date -u -v+1H +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d '+1 hour' +%Y-%m-%dT%H:%M:%SZ)\"
    },
    \"repos\": [{
      \"providerRepoId\": \"123456789\",
      \"fullName\": \"my-org/my-repo\",
      \"defaultBranch\": \"main\",
      \"cloneUrl\": \"https://github.com/my-org/my-repo.git\"
    }]
  }"

# Poll status (URL-encode: github:my-org/my-repo → github%3Amy-org%2Fmy-repo)
curl -sS "${BASE_URL}/repos/github%3Amy-org%2Fmy-repo/status"

# List all indexed repos
curl -sS "${BASE_URL}/indexed-repos"
```

---

## Conventions

### `repo_id` formats

| Format | Example | Used by |
|---|---|---|
| Legacy Bitbucket Server | `ad/my-repo` | Admin sync, webhooks |
| Gateway import | `github:my-org/my-repo` | Import path, status after import |
| Gateway import | `bitbucket_cloud:workspace/repo` | Import path |
| Gateway import | `bitbucket_server:PROJECT/slug` | Import path |

### URL encoding

Path segments must be a single URL segment. Encode:

- `/` → `%2F`
- `:` → `%3A`

Example: `github:acme/widget` → `github%3Aacme%2Fwidget`

### Error format

```json
{
  "error": "Human-readable error message"
}
```

Import validation errors may prefix with `invalid import payload:`.

---

## Endpoint index

| Method | Path | Description |
|---|---|---|
| GET | `/` | Service status |
| GET | `/healthz` | Liveness |
| GET | `/readyz` | Readiness (Mongo + Redis) |
| GET | `/version` | Build version |
| POST | `/repos/import` | Gateway batch import (v1.1) |
| POST | `/admin/repos/{repo_id}/sync` | Legacy Bitbucket admin sync |
| POST | `/webhooks/bitbucket` | Bitbucket Server push webhook |
| GET | `/repos/{repo_id}` | Repository metadata |
| GET | `/repos/{repo_id}/status` | Sync status (gateway polling) |
| GET | `/repos/{repo_id}/snapshots/{snapshot_id}` | Snapshot metadata |
| GET | `/repos/{repo_id}/indexing-runs/latest` | Latest pipeline run |
| GET | `/repos/{repo_id}/snapshots/{snapshot_id}/indexing-run` | Indexing run for snapshot |
| GET | `/repos/{repo_id}/snapshots/{snapshot_id}/indexing-run/diagnostics` | Pipeline diagnostics |
| GET | `/indexed-repos` | UI repo list with indexing status |

---

## Health & metadata

```bash
curl -sS http://localhost:6001/healthz
curl -sS http://localhost:6001/readyz
curl -sS http://localhost:6001/version
```

### `GET /`

**Response (200 OK):**

```json
{
  "service": "repo-sync-service",
  "status": "ok"
}
```

### `GET /healthz`

**Response (200 OK):**

```json
{
  "service": "repo-sync-service",
  "status": "healthy"
}
```

### `GET /readyz`

**Response (200 OK):** MongoDB and Redis reachable.

**Response (503):** `{ "service": "...", "status": "not_ready" }`

### `GET /version`

**Response (200 OK):**

```json
{
  "service": "repo-sync-service",
  "version": "0.1.0",
  "build_info": "..."
}
```

---

## Gateway import

### `POST /repos/import`

Accepts contract v1.1 payload from the gateway. Clones immediately with inline `cloneAuth`; credentials are **never persisted**.

```bash
IMPORT_JOB_ID=$(uuidgen | tr '[:upper:]' '[:lower:]')
curl -sS -X POST http://localhost:6001/repos/import \
  -H "Content-Type: application/json" \
  -H "X-Org-Id: test-org-1" \
  -d "{
    \"importJobId\": \"${IMPORT_JOB_ID}\",
    \"orgId\": \"test-org-1\",
    \"connectionId\": \"test-conn-1\",
    \"provider\": \"github\",
    \"cloneAuth\": {
      \"username\": \"x-access-token\",
      \"password\": \"<short-lived-token>\",
      \"expiresAt\": \"2026-06-18T15:30:00.000Z\"
    },
    \"repos\": [{
      \"providerRepoId\": \"123456789\",
      \"fullName\": \"my-org/my-repo\",
      \"defaultBranch\": \"main\",
      \"cloneUrl\": \"https://github.com/my-org/my-repo.git\"
    }]
  }"
```

**Headers (from gateway):**

| Header | Description |
|---|---|
| `Content-Type` | `application/json` |
| `X-Org-Id` | Optional fallback if `orgId` omitted in body |

**Request body:**

```json
{
  "importJobId": "550e8400-e29b-41d4-a716-446655440000",
  "orgId": "507f1f77bcf86cd799439011",
  "connectionId": "507f1f77bcf86cd799439012",
  "provider": "github",
  "cloneAuth": {
    "username": "x-access-token",
    "password": "<short-lived-token>",
    "expiresAt": "2026-06-18T15:30:00.000Z"
  },
  "repos": [
    {
      "providerRepoId": "123456789",
      "fullName": "my-org/my-repo",
      "defaultBranch": "main",
      "cloneUrl": "https://github.com/my-org/my-repo.git"
    }
  ]
}
```

**Request schema:**

| Field | Type | Required | Description |
|---|---|---|---|
| `importJobId` | string (UUID) | yes | Idempotency key |
| `orgId` | string | yes | Organization ID |
| `connectionId` | string | yes | Gateway connection ID |
| `provider` | string | yes | `github` \| `bitbucket_cloud` \| `bitbucket_server` |
| `cloneAuth` | object | yes | Short-lived git credentials |
| `cloneAuth.username` | string | yes | Git HTTP username |
| `cloneAuth.password` | string | yes | Short-lived token |
| `cloneAuth.expiresAt` | string (ISO 8601) | yes | Credential expiry |
| `repos` | array | yes | Non-empty list |
| `repos[].providerRepoId` | string | yes | Provider-native repo ID |
| `repos[].fullName` | string | yes | `owner/repo` or `PROJECT/slug` |
| `repos[].defaultBranch` | string | no | Defaults to `main` |
| `repos[].cloneUrl` | string | yes | HTTPS clone URL (no embedded creds) |

**Provider clone credentials:**

| Provider | `cloneAuth.username` | `cloneAuth.password` |
|---|---|---|
| `github` | `x-access-token` | Installation or PAT token |
| `bitbucket_cloud` | `x-token-auth` | OAuth access token |
| `bitbucket_server` | Bitbucket username | HTTP access token |

**Response (202 Accepted):**

```json
{
  "importJobId": "550e8400-e29b-41d4-a716-446655440000",
  "status": "accepted",
  "jobs": [
    {
      "jobId": "job_a1b2c3d4e5f67890",
      "providerRepoId": "123456789",
      "fullName": "my-org/my-repo",
      "repoId": "github:my-org/my-repo",
      "status": "pending"
    }
  ]
}
```

**Response schema:**

| Field | Type | Description |
|---|---|---|
| `importJobId` | string | Echo of request idempotency key |
| `status` | string | `accepted` on success |
| `jobs` | array | Per-repo sync jobs |
| `jobs[].jobId` | string | Sync job ID |
| `jobs[].providerRepoId` | string | Provider repo ID |
| `jobs[].fullName` | string | `owner/repo` |
| `jobs[].repoId` | string | Canonical repo-sync ID (`provider:fullName`) |
| `jobs[].status` | string | Initial: `pending` |

**Response (409 Conflict)** — duplicate `importJobId`:

Same body shape as 202, with previously stored job snapshot.

**Errors:**

| HTTP | Body | Cause |
|---|---|---|
| 400 | `{ "error": "invalid import payload: ..." }` | Validation failure |
| 409 | ImportAcceptedResponse | Duplicate `importJobId` |
| 500 | `{ "error": "failed to accept import" }` | Internal error |

**Max body size:** 1 MiB

---

## Admin sync (legacy Bitbucket Server)

### `POST /admin/repos/{repo_id}/sync`

First-time clone or incremental sync using **global** `BITBUCKET_USERNAME` / `BITBUCKET_TOKEN` env credentials.

```bash
curl -sS -X POST "http://localhost:6001/admin/repos/ad%2Fmy-repo/sync?ref=master"
```

**Path:** `repo_id` = `{project_key}/{repo_slug}` (URL-encode `/`).

**Query:**

| Param | Default | Description |
|---|---|---|
| `ref` | `master` | Branch name or commit SHA |

**Response (202 Accepted):**

```json
{
  "repo_id": "ad/my-repo",
  "ref": "master",
  "job_id": "job_a1b2c3d4e5f67890",
  "status": "accepted"
}
```

**Errors:** `400` invalid `repo_id`; `500` enqueue failure.

---

## Webhooks

### `POST /webhooks/bitbucket`

Bitbucket Server `repo:refs_changed` events. Only **master** branch pushes trigger sync.

**Headers:**

| Header | Description |
|---|---|
| `X-Event-Key` | Must be `repo:refs_changed` |
| `X-Hub-Signature` | `sha256=<hmac-hex>` of raw body |
| `Content-Type` | `application/json` |

**Accepted (202):**

```json
{
  "status": "accepted",
  "repo_id": "ad/my-repo",
  "ref": "master",
  "job_id": "job_a1b2c3d4e5f67890"
}
```

**Ignored (202):**

```json
{
  "status": "ignored",
  "reason": "not master branch",
  "repo_id": "ad/example-repo"
}
```

**Errors:** `401` invalid signature; `400` malformed payload.

---

## Repository read APIs

### `GET /repos/{repo_id}`

```bash
curl -sS "http://localhost:6001/repos/github%3Amy-org%2Fmy-repo"
```

**Response (200 OK):**

```json
{
  "repo_id": "github:my-org/my-repo",
  "clone_url": "https://github.com/my-org/my-repo.git",
  "default_ref": "main",
  "last_commit_sha": "178864a7d521b6f5e720b386b2c2b0ef8563e0dc",
  "last_sync_at": "2026-06-18T12:00:00Z",
  "status": "active",
  "updated_at": "2026-06-18T12:00:00Z"
}
```

**Errors:** `404` not found; `400` invalid `repo_id`.

### `GET /repos/{repo_id}/status`

Lightweight status for gateway polling after import.

```bash
curl -sS "http://localhost:6001/repos/github%3Amy-org%2Fmy-repo/status"
```

**Response (200 OK):**

```json
{
  "repo_id": "github:my-org/my-repo",
  "status": "active",
  "last_commit_sha": "178864a7d521b6f5e720b386b2c2b0ef8563e0dc",
  "last_sync_at": "2026-06-18T12:00:00Z",
  "default_ref": "main"
}
```

| Field | Type | Description |
|---|---|---|
| `repo_id` | string | Canonical repo ID |
| `status` | string | e.g. `active` |
| `last_commit_sha` | string | HEAD at last sync |
| `last_sync_at` | string | ISO 8601 UTC |
| `default_ref` | string | Default branch |

### `GET /repos/{repo_id}/snapshots/{snapshot_id}`

```bash
curl -sS "http://localhost:6001/repos/ad%2Fmy-repo/snapshots/snap_a1b2c3d4e5f67890"
```

**Response (200 OK):**

```json
{
  "snapshot_id": "snap_a1b2c3d4e5f67890",
  "repo_id": "github:my-org/my-repo",
  "commit_sha": "178864a7d521b6f5e720b386b2c2b0ef8563e0dc",
  "ref": "main",
  "file_count": 128,
  "status": "ready",
  "created_at": "2026-06-18T12:00:00Z"
}
```

---

## Indexing pipeline APIs

### `GET /repos/{repo_id}/indexing-runs/latest`

```bash
curl -sS "http://localhost:6001/repos/github%3Amy-org%2Fmy-repo/indexing-runs/latest"
```

**Response (200 OK):**

```json
{
  "indexing_run_id": "idxrun_abc123",
  "job_id": "job_a1b2c3d4e5f67890",
  "repo_id": "github:my-org/my-repo",
  "snapshot_id": "snap_a1b2c3d4e5f67890",
  "commit_sha": "178864a7d521b6f5e720b386b2c2b0ef8563e0dc",
  "ref": "main",
  "mode": "fresh",
  "sync_status": "completed",
  "pipeline_status": "running",
  "expected_commits": 42,
  "failed_files": 0,
  "stages": {
    "repo_sync": "completed",
    "docs_parse": "running",
    "code_parse": "pending",
    "graph_finalize": "pending",
    "commit_intel": "pending",
    "embedding": "pending"
  },
  "progress": {
    "repo_sync": {
      "expected": 1,
      "processed": 1,
      "percent_done": 100,
      "status": "completed"
    },
    "code_parse": {
      "expected": 80,
      "processed": 12,
      "failed": 0,
      "failure_rate": 0,
      "percent_done": 15,
      "status": "running"
    }
  },
  "started_at": "2026-06-18T12:00:00Z",
  "completed_at": ""
}
```

**Stage status values:** `pending` \| `running` \| `completed` \| `failed`

**Pipeline status values:** `pending` \| `running` \| `completed` \| `failed` \| `degraded`

### `GET /repos/{repo_id}/snapshots/{snapshot_id}/indexing-run`

Same response schema as latest run, scoped to the snapshot.

### `GET /repos/{repo_id}/snapshots/{snapshot_id}/indexing-run/diagnostics`

**Query:**

| Param | Default | Description |
|---|---|---|
| `limit` | 100 | Max diagnostics (1–500) |

**Response (200 OK):**

```json
{
  "diagnostics": [
    {
      "diagnostic_id": "diag_abc123",
      "repo_id": "github:my-org/my-repo",
      "snapshot_id": "snap_a1b2c3d4e5f67890",
      "stage": "code_parse",
      "source_type": "file",
      "source_id": "src/main.go",
      "code": "PARSE_ERROR",
      "severity": "warning",
      "message": "Skipped unsupported syntax",
      "created_at": "2026-06-18T12:05:00Z"
    }
  ]
}
```

---

## Indexed repositories (UI)

### `GET /indexed-repos`

**Auth:** `X-Internal-Secret: <GATEWAY_INTERNAL_SERVICE_SECRET>` (same value as gateway). Returns **401** without it. Not for browser or public clients — use gateway `GET /api/v1/indexed-repos` with JWT.

```bash
# Gateway proxy only — not for direct UI use
curl -sS http://localhost:6001/indexed-repos \
  -H "X-Internal-Secret: ${GATEWAY_INTERNAL_SERVICE_SECRET}" \
  -H "X-Gateway-Indexed-Repos: 1" \
  -H "X-Org-Id: 507f1f77bcf86cd799439011"
```

Org-scoped listing (gateway imports only; legacy `ad/slug` excluded):

**Response (200 OK):**

```json
{
  "repos": [
    {
      "repo_id": "github:my-org/my-repo",
      "repo_name": "my-repo",
      "git_url": "https://github.com/my-org/my-repo.git",
      "status": "ready",
      "last_indexed_at": "2026-06-18T14:00:00Z"
    },
    {
      "repo_id": "ad/legacy-repo",
      "repo_name": "legacy-repo",
      "git_url": "https://bit.admedia.com/scm/ad/legacy-repo.git",
      "status": "indexing",
      "last_indexed_at": null
    }
  ]
}
```

**Status values:**

| Status | Meaning |
|---|---|
| `indexing` | Pipeline in progress or no run yet |
| `ready` | All required stages complete (including embedding) |
| `failed` | Run or critical stage failed |

Sorted newest-first by `last_indexed_at`.

---

## Redis events (post-sync)

After a successful sync, repo-sync publishes to Redis streams:

| Stream | Event | Consumer |
|---|---|---|
| `repo.snapshot.ready` | Snapshot metadata | Code parser |
| `files.changed` | Per-file changes | Code parser, docs ingestion |
| `commits.changed` | Commit delta | Code parser |

See [events.md](./events.md) for payload schemas.

---

## Environment variables

| Variable | Default (local) | Description |
|---|---|---|
| `HOST` | `0.0.0.0` | Bind host |
| `PORT` | `6001` | Bind port |
| `ENVIRONMENT` | `local` | `local` relaxes required-field validation |
| `REDIS_URL` | `redis://localhost:6379` | Redis connection |
| `MONGO_URI` | `mongodb://localhost:27018/adpilot_repo_sync` | MongoDB |
| `MONGODB_DATABASE` | `adpilot_repo_sync` | Database name |
| `GIT_WORKSPACE_PATH` | `./data/repos` | Clone workspace |
| `BITBUCKET_BASE_URL` | `https://bit.admedia.com` | Bitbucket Server URL |
| `BITBUCKET_USERNAME` | — | Global BB creds (admin sync / webhooks) |
| `BITBUCKET_TOKEN` | — | Global BB token |
| `BITBUCKET_WEBHOOK_SECRET` | — | Webhook HMAC secret |

---

## Related documents

- [api.md](./api.md) — operational guide and curl examples
- [GATEWAY-REPO-SYNC-CONTRACT.md](../../../../api.lucos.com/docs/GATEWAY-REPO-SYNC-CONTRACT.md) — gateway ↔ repo-sync contract v1.1
- [Gateway API reference](../../../../api.lucos.com/docs/API-REFERENCE.md) — how to obtain `providerRepoIds`
