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.mdof 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¶
- 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. - 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.
- 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
queueduntil one of them is online. - 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
indexedbeforeverdict.indexed: falsemeans there was nothing to search: no ready index yet, or not your project. Averdictofnonethen says nothing about your code. verdictisexistswhen a symbol's name is at least 0.8 similar to your query,similarwhen something weaker matched (a looser name, or a doc-line hit), andnonewhen nothing did.matched_onsays which way each match was found:name,docorboth.truncated: truemeans the matches filledeffective_limit, so there may be more. Raiselimitto see them.limit_clamped: trueonly means yourlimitwas lowered to 50. It does not mean anything was left out.freshnessnames the commit and branch the answer comes from and when they were indexed. Ifindex_truncatedistrue, part of the repository was never read, and anoneis 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
}
statusisqueued,running,done,skippedorfailed.sourceiswebhook(a merge),poll(the five-minute check) ormanual.reason_classanddetailsay why a job was skipped or failed.preconditionis set when no job can be queued at all, because nothing exists yet to index. It carries the samereason_classanddetail.
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. |