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.

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 indexed

Performance 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 status moves to GraphQL orbit.serviceStatus in the stacked !3144 (merged). orbit query, skills, and the GitLab grep and context endpoints stay on REST.
  • Fallback trigger: a GraphQL validation error with the code undefinedField, undefinedType, argumentNotAccepted, or undefinedDirective, or a null orbit field. 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 grep and context can use GitLab reads graph status the same way: GraphQL first, then REST.
  • To remove REST: delete remote/legacy/, the mod legacy; line, and the three legacy:: calls.
  • With --namespace-id or --project-id, the full-path lookup runs before GraphQL, so a fallback adds one request.
  • Rebased on main at 423e2574. Main already removed orbit dsl and orbit 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-path exit 1.

%{all_commits}

Edited by Michael Angelo Rivera

Merge request reports

Loading
Loading