Code index

When a project has a GitHub repository attached, OPVS keeps a code index of it: every function, class, method and type it can find, with its file, its line range, its signature and the first line of its doc comment. The index is rebuilt after each merge to the attached branch, so it describes the code as it is now, not as it was when the project was set up.

The index is structural. A parser reads the tree, and nothing else happens to your code: no source is sent to a model provider, and the index holds no summaries and no embeddings.

Two things read it:

  • A build desk, when it is set up. If your repository has no CLAUDE.md of its own, the desk gets a generated map of the repository at its root, naming the branch and commit it describes. If your repository has one, OPVS writes nothing there, and your file stays the only briefing.
  • Your agents, through the endpoints on this page. Before an agent writes a function, it can ask whether one already exists, and where.

Base path: /api/v1/code. Authentication: a brand-scoped PAT or a dashboard session. See Authentication. Every route on this page needs the code:read scope, and every route also answers ?format=yaml or ?format=md with the same content.

How the index is built

  1. Attach a GitHub repository to a project, with a branch. A brand admin does this with PUT /api/v1/builders/projects/{project_id}/repo. The attached branch is the one that is indexed. A repository attached without a branch is not indexed.
  2. A merge queues an index job. A push to the attached branch queues a job within seconds, and a check every five minutes queues one for any commit that was missed.
  3. One of your workspace's build machines runs the job. It downloads that commit's archive, reads it and sends back the result. Jobs are only ever run by your own workspace's machines, so a job waits in queued until one of them is online.
  4. The new index takes over when it is ready. Until then, queries answer from the previous index, and every answer names the commit it describes.

Only repositories on GitHub are indexed. A repository on any other host is skipped with the reason unsupported_host.

What the index reads. Python is read with a full parser. TypeScript, JavaScript and Go are read by matching declaration patterns, which finds most declarations but not every one. Each symbol says which of the two found it, in derivation (parsed or regex). SQL, shell, YAML, JSON, Markdown, CSS, HTML, Rust, Ruby, Java and PHP files count toward the file and line totals but add no symbols. Files your .gitignore excludes are not read.

Size limit. One index holds up to 50,000 symbols. In a larger repository the limit is shared across directories, so every directory is read and each one is under-counted, rather than some being skipped. The index then reports index_truncated: true with the reason, and every count it gives is a lower bound.

Another workspace's project answers exactly as a project with no index does: indexed: false, or an empty job list. The API never confirms that someone else's project exists.

GET /api/v1/code/{project_id}/context

Ask whether something already exists in the project's code. It matches two ways at once: on symbol names by similarity, so resolveInvoice finds resolve_invoice, and on the first line of each symbol's doc comment by keyword, so "refund a payment" finds a function whose doc says so.

Parameter Type Default Notes
query string required The name or phrase to look for, 1 to 512 characters.
kind string none Only this kind of symbol: function, method, class, interface, type, enum, struct or const.
branch string the most recently indexed branch Answer from this branch's index.
limit integer 10 The most matches to return, 1 or more. A value above 50 is lowered to 50.
format string JSON yaml or md.
curl -s "https://api.opvs.ai/api/v1/code/$PROJECT_ID/context?query=resolve_invoice&limit=5" \
  -H "Authorization: Bearer $OPVS_PAT"
# → 200 {"indexed": true, "verdict": "exists", "match_count": 1, "truncated": false}
# → 200 {"indexed": false, "verdict": "none", "match_count": 0} when the project has no ready index yet
# → 403 when the token does not carry code:read

Response:

{
  "project_id": "5de4fb58-fda9-4c19-bbdb-5aa43874854e",
  "query": "resolve_invoice",
  "kind": null,
  "indexed": true,
  "verdict": "exists",
  "matches": [
    {
      "name": "resolve_invoice_id",
      "kind": "function",
      "file_path": "billing/invoices.py",
      "line_start": 212,
      "line_end": 240,
      "signature": "def resolve_invoice_id(ref: str) -> Optional[str]",
      "doc_first_line": "Resolve an invoice reference, number or id, to an invoice id.",
      "derivation": "parsed",
      "name_similarity": 0.8235,
      "matched_on": "both"
    }
  ],
  "match_count": 1,
  "truncated": false,
  "truncated_reason": null,
  "limit_clamped": false,
  "effective_limit": 5,
  "freshness": {
    "commit_hash": "4625c8481d0321d507b2ed26240688bbb378c8bf",
    "branch": "main",
    "indexed_at": "2026-09-28T13:39:43Z",
    "age_seconds": 759,
    "symbol_count": 8412,
    "tool_version": "0.2.66",
    "index_truncated": false,
    "index_truncated_reason": null,
    "derivation_counts": {"parsed": 5120, "regex": 3292}
  }
}

How to read the answer:

  • Read indexed before verdict. indexed: false means there was nothing to search: no ready index yet, or not your project. A verdict of none then says nothing about your code.
  • verdict is exists when a symbol's name is at least 0.8 similar to your query, similar when something weaker matched (a looser name, or a doc-line hit), and none when nothing did. matched_on says which way each match was found: name, doc or both.
  • truncated: true means the matches filled effective_limit, so there may be more. Raise limit to see them. limit_clamped: true only means your limit was lowered to 50. It does not mean anything was left out.
  • freshness names the commit and branch the answer comes from and when they were indexed. If index_truncated is true, part of the repository was never read, and a none is weaker than it looks.

Errors

Error When Resolution
400 project_id is not a UUID. Pass the project's id, not its name or ref.
401 The token is missing, malformed, expired or revoked. Send a valid PAT as Authorization: Bearer.
403 The token does not carry code:read. Request a token that includes code:read.
422 query is missing, limit is below 1, or format is not yaml or md. Fix the parameter the response's detail names.
503 The index database could not be reached. Retry with backoff.

GET /api/v1/code/{project_id}/summary

The repository at a glance: its languages, entry points and test command, how many symbols of each kind it has, and the files that hold the most symbols. It is the same data the desk map is written from, returned as data so an agent can read the facts rather than a rendering of them.

Parameter Type Default Notes
branch string the most recently indexed branch Summarise this branch's index.
format string JSON yaml or md.
curl -s "https://api.opvs.ai/api/v1/code/$PROJECT_ID/summary?format=md" \
  -H "Authorization: Bearer $OPVS_PAT"
# → 200 a Markdown summary of the indexed commit
# → 200 {"indexed": false} when the project has no ready index yet
# → 401 when the token is missing or expired

Response (JSON):

{
  "project_id": "5de4fb58-fda9-4c19-bbdb-5aa43874854e",
  "indexed": true,
  "repo_url": "https://github.com/acme/checkout-service.git",
  "freshness": {"commit_hash": "4625c8481d0321d507b2ed26240688bbb378c8bf", "branch": "main"},
  "languages": {
    "python": {"files": 214, "lines": 38120, "symbols": 5120},
    "typescript": {"files": 188, "lines": 24410, "symbols": 3292},
    "sql": {"files": 41, "lines": 2210, "symbols": 0}
  },
  "entry_points": [{"kind": "container", "path": "docker-compose.yml", "detail": "docker-compose.yml"}],
  "test_command": "make test",
  "kind_counts": {"function": 4980, "method": 1720, "class": 610, "type": 540, "interface": 390},
  "top_files": [{"file_path": "billing/invoices.py", "symbol_count": 96}]
}

freshness carries the same fields as it does on /context; it is shortened here. test_command is null when the index found no test command.

Errors

Error When Resolution
400 project_id is not a UUID. Pass the project's id.
401 The token is missing, malformed, expired or revoked. Send a valid PAT.
403 The token does not carry code:read. Request a token that includes code:read.
422 format is not yaml or md. Omit it for JSON, or use one of the two.
503 The index database could not be reached. Retry with backoff.

GET /api/v1/code/{project_id}/jobs

The project's index jobs, newest first: what was asked for, and what happened to it. Read it when an index is missing or older than you expect.

Parameter Type Default Notes
limit integer 25 1 to 100.
offset integer 0 For the next page, add limit to it while has_more is true.
format string JSON yaml or md.
curl -s "https://api.opvs.ai/api/v1/code/$PROJECT_ID/jobs?limit=10" \
  -H "Authorization: Bearer $OPVS_PAT"
# → 200 {"total": 32, "limit": 10, "offset": 0, "has_more": true, "precondition": null}
# → 422 when limit is above 100

Response:

{
  "project_id": "5de4fb58-fda9-4c19-bbdb-5aa43874854e",
  "jobs": [
    {
      "job_id": "eb049864-3b0b-4d44-a6e9-052a9090b673",
      "commit_hash": "4625c8481d0321d507b2ed26240688bbb378c8bf",
      "branch": "main",
      "commit_time": "2026-09-28T13:33:07Z",
      "source": "webhook",
      "status": "done",
      "attempts": 1,
      "reason_class": null,
      "detail": null,
      "index_id": "3115f041-da29-407f-af67-6ced02fee166",
      "noop": false,
      "notified": false,
      "created_at": "2026-09-28T13:32:53Z",
      "started_at": "2026-09-28T13:37:52Z",
      "finished_at": "2026-09-28T13:39:46Z"
    }
  ],
  "total": 32,
  "limit": 10,
  "offset": 0,
  "has_more": true,
  "precondition": null
}
  • status is queued, running, done, skipped or failed.
  • source is webhook (a merge), poll (the five-minute check) or manual.
  • reason_class and detail say why a job was skipped or failed.
  • precondition is set when no job can be queued at all, because nothing exists yet to index. It carries the same reason_class and detail.

The reasons you can act on:

reason_class Means What to do
no_branch The attached repository names no branch. Attach it again with a branch.
unsupported_host The repository is not on GitHub. Only GitHub repositories are indexed.
no_credential The project's GitHub credential no longer works for this repository. Attach a working credential to the project's repository.
repo_unreachable GitHub refused the request: access was revoked, or the repository or branch was deleted. Restore access or re-attach.
retries_exhausted A temporary fault lasted through three attempts. Nothing. It is asked for again automatically.
superseded_by_newer A newer commit has its own job. Nothing. The newer commit's index replaces this one.

A job that failed, or was skipped for a reason you can fix, is asked for again at most once an hour for the same commit, so fixing access is enough and you do not need to push again. A failed index also posts one in-app notification, Repo index failed. A run of failures posts it once, and it is never emailed. notified says whether a job was the one that posted it.

Errors

Error When Resolution
400 project_id is not a UUID. Pass the project's id.
401 The token is missing, malformed, expired or revoked. Send a valid PAT.
403 The token does not carry code:read. Request a token that includes code:read.
422 limit is outside 1 to 100, offset is negative, or format is not yaml or md. Fix the parameter the response's detail names.
503 The index database could not be reached. Retry with backoff.
Powered by OPVS