feat(orbit-cli): read schema and scoped status from GraphQL first, with a REST fallback
What does this MR do and why?
The orbit schema and scoped orbit status commands, and their aliases, now read the GitLab GraphQL API first. They use typed queries from the new GraphQL crate. GitLab can then remove the REST schema and graph status endpoints.
On a GitLab without the Orbit GraphQL fields, the CLI falls back to those deprecated REST endpoints. The CLI keeps that choice in memory for the rest of the process. The REST code is in remote/legacy/, so we can delete it later in one step.
On GraphQL, the JSON output of both commands uses the GraphQL field names. The output before and after is under Testing.
Related Issues
Relates to #263. Replaces the CLI part of !3036 (closed).
Testing
On gitlab.com, the new output matches REST for the same scopes. That covers 33 nodes, 60 edges, 18 User properties, and 31 entity counts for a project and a group.
Mock tests cover each fallback code, a null orbit field, and no fallback on HTTP 401. A live run went through a proxy that rejects the GraphQL fields. It passed 9 of 9 checks on the REST path, and its output equals REST byte for byte. The CI e2e jobs call the REST endpoints directly and do not run the CLI.
Output before and after
Real gitlab.com JSON output, cut to one item per list.
orbit schema User:
// before (REST)
{
"schema_version": "0.1",
"domains": [{ "name": "agents", "description": "Entities related to AI agent sessions.", "node_names": ["AgentSession"] }],
"nodes": [{
"name": "User", "domain": "core", "description": "A GitLab user account",
"primary_key": "id", "label_field": "username",
"properties": [{ "name": "id", "data_type": "Int", "nullable": false, "enum_values": [] }],
"style": { "size": 32, "color": "#10B981" }, "outgoing_edges": ["APPROVED", "ASSIGNED", "AUTHORED"], "incoming_edges": []
}],
"edges": [{ "name": "AUTHORED", "description": "Authorship relationship between users and entities",
"variants": [{ "source_type": "User", "target_type": "Note" }] }]
}
// after (GraphQL)
{
"nodes": [{
"name": "User", "description": "A GitLab user account", "domain": { "name": "core" },
"primaryKey": "id", "labelProperty": "username",
"properties": { "nodes": [{ "name": "id", "dataType": "Int", "nullable": false, "enumValues": [] }] }
}],
"edges": [{ "name": "AUTHORED", "description": "Authorship relationship between users and entities",
"endpoints": { "nodes": [{ "source": { "name": "User" }, "target": { "name": "Note" } }] } }]
}orbit status --full-path gitlab-org/orbit/knowledge-graph --response-format raw:
// before (REST)
{
"projects": { "indexed": 1, "total_known": 1, "gaps": 0 },
"domains": [{ "name": "agents", "items": [{ "name": "AgentSession", "count": 4830, "state": "indexed" }] }],
"indexing": { "state": "indexed", "last_completed_at": "2026-10-25T18:59:47Z", "last_error": null },
"sdlc_indexing": { "state": "indexed" },
"code_indexing": { "state": "indexed" }
}
// after (GraphQL)
{
"indexing": {
"state": "READY",
"areas": [{ "name": "agents", "state": "READY" }],
"projects": { "indexed": 1, "totalKnown": 1, "gaps": 0 }
},
"itemCounts": { "domains": [{ "name": "agents", "entities": [{ "name": "AgentSession", "count": "4830" }] }] }
}Counts are now strings, because GraphQL sends BigInt as a string. On GraphQL, the terminal summary shows one row per indexing area. The rows below show the layout of each summary for gitlab-org/orbit. They are not a copy of one run:
before: SDLC data indexed
Code indexed
Projects 97 of 97 indexed
after: agents indexed
ci indexed
...
source_code indexed
Projects 97 of 97 indexedPerformance Analysis
A cold orbit-cli build takes 2:02 instead of 1:57, and clippy takes 1:26 instead of 1:24. Clippy peak memory goes from 691 MB to 1,195 MB.
On a GitLab without the Orbit GraphQL fields, the first call sends one more request. When GitLab rejects the fields, later calls in the same process go to REST directly.
- This merge request does not introduce any performance regression. If a performance regression is expected, explain why.
Agent context: long-form analysis, file-by-file walkthroughs, profiler output, alternatives considered
- Unscoped
orbit statusmoves to GraphQLorbit.serviceStatusin the stacked !3144 (merged).orbit query, skills, and the GitLabgrepandcontextendpoints stay on REST. - Fallback trigger: a GraphQL validation error with the code
undefinedField,undefinedType,argumentNotAccepted, orundefinedDirective, or a nullorbitfield. Network errors, HTTP errors, and other GraphQL errors do not fall back. Only the validation error sets the cached choice. - The probe that decides if
grepandcontextcan use GitLab reads graph status the same way: GraphQL first, then REST. - To remove REST: delete
remote/legacy/, themod legacy;line, and the threelegacy::calls. - With
--namespace-idor--project-id, the full-path lookup runs before GraphQL, so a fallback adds one request. - Rebased on main at 423e2574. Main already removed
orbit dslandorbit tools, and the docs no longer list them. - Follow-ups: the ID flags still look up the full path over REST; an unknown indexing state makes
status --full-pathexit 1.
%{all_commits}