import type { ToolDefinition } from "./types.js"; /** * THE REGISTRY — the one file that changes when the toolset grows. * * SPGD-310 ships a bootstrap, not a tool contract: the initial set wraps * capabilities that are verified-shipped today, and the rest fills in as more * of SpecGuard lands. This array is the seam that makes that true. `server.ts` * iterates it and has no knowledge of any individual tool, so adding one is a * new file plus one line here — no new plumbing, no `switch` to extend, no * change to how the server starts, authenticates or reports errors. * * == What is in the bootstrap, and why only these two * * - `lint_intent_annotations` wraps `specguard-lint` (shipped: * `specguard-ruby/bin/specguard-lint`, SPGD-12 §1). * - `get_repository_overview` wraps `GET /api/v1/repository` (shipped: * `specguard/config/routes.rb`). * * Both were confirmed present before being wrapped, and the two chosen cover * the two halves of the surface — one local subprocess, one authenticated HTTP * call — so the shape is proven on both kinds of capability rather than on one. * * == The first tool that reads the OTHER credential * * - `list_repositories` wraps `GET /api/v1/repositories` (shipped: * `specguard/config/routes.rb`, `Api::V1::UserRepositoriesController`). * * It is the only tool here that answers to an `sga_` AGENT key first, falling * back to the `sgu_` USER key, rather than an `sgk_` repository key, and * SpecGuard refuses each credential in the other's place before it reads a * table. So this entry is also what proves the second * variable, the second `require*` helper and the credential-aware diagnostics * work end to end — one tool, over a real endpoint, rather than a seam nothing * exercises. * * == What is deliberately absent * * `/check-intent` is NOT here and must not be added until its backing endpoint * exists — `specguard/config/routes.rb` carries `/check-intent` as a comment * only, which is the evidence this forbid rests on. A tool advertised in * `tools/list` is a promise an agent will act on: wrapping an endpoint that * does not exist would produce a server that discovers cleanly and fails on * use, which is worse than not offering the tool, because the agent has * already committed to a plan by the time it finds out. * * == The first tool that WRITES * * - `add_repository` wraps `POST /api/v1/repositories` (shipped: * `specguard/config/routes.rb`, `Api::V1::UserRepositoriesController#create`). * * The endpoint had shipped for a while; what this bridge lacked was a way to * CALL it — `support/specguard-api.ts` offered only `getJson`, which hardcoded * `method: "GET"` and took no body. The reservation recorded here was that the * write transport should land WITH the first write tool, designed against a real * request body and a real 4xx surface rather than invented for a caller that did * not exist. That is what happened: `postJson`/`postJsonObject` arrived with * this entry, sharing `fetchWithTimeout` with the read path rather than standing * beside it, and `describeFailure` grew the `400` branch that surfaces * SpecGuard's own refusal sentence — the modal answer this endpoint gives. * * The standing rule is unchanged and still binding. What once kept `DELETE * /api/v1/repositories/:id` and the API-key endpoints out under it — "not on * `origin/main`, so they may not be wrapped" — stopped being true when SPGD-754 * shipped them, and the removal-and-key-lifecycle section below records their * wrapping. What moved was the platform, not the bar. * * == The read half of the registration gate * * - `registrable_repositories` wraps `GET /api/v1/repositories/registrable` * (shipped: `specguard/config/routes.rb`, * `Api::V1::UserRepositoriesController#registrable`). * * `list_repositories` says what IS registered; this says what COULD be — the * set the gate would consult, read out in advance, so an agent can pick a * `full_name` for `add_repository` from a real answer. Landing it also * generalised the 400 branch in `describeFailure` into a status-parameterised * extractor, because this endpoint's modal first answer is a 403 (`not_granted`) * carrying the same `{error, message}` contract — the identical defect, given * the identical remedy. * * The registration gate was the newest user-scoped entry when it landed; the * sections below record how the surface grew after it. The standing rule is * unchanged and still binding, and what it keeps out today is what has not * shipped — `/check-intent` has no backing endpoint (`routes.rb` mounts it as * a comment only), so it stays unwrapped. What moved was the platform, not * the bar. * * == Removal and the key lifecycle * * - `remove_repository` wraps `DELETE /api/v1/repositories/:id`, and * `create_repository_api_key` / `revoke_repository_api_key` wrap the two * `api_keys` endpoints (all shipped: SPGD-754, `specguard@origin/main`). * * The closing fence the two paragraphs above share — "the DELETE and API-key * endpoints are NOT on `origin/main`, so they may not be wrapped" — stopped * being true when SPGD-754 landed, and these three entries are what became * wrappable the moment it did. They also forced the transport's third verb: * both DELETE endpoints answer `204` with NO body, the one response in the * `sgu_` surface that is deliberately not JSON, which is why `deleteJson` * returns the raw body text instead of routing an empty 204 through * `requestJson`'s JSON parse. * * - `list_repository_api_keys` wraps the same controller's inventory read * (`GET repositories/:repository_id/api_keys`; shipped: SPGD-993, * specguard) — the verify step of the mint-and-replace rotation the pair * above prescribe, and the id source for every `sgk_` row this session * did not mint. * * The standing rule itself is unchanged and still binding — which still keeps * out `/check-intent`: it has no backing endpoint (`specguard/config/routes.rb` * mounts `/check-intent` as a comment only). A tool advertised in `tools/list` * remains a promise an agent will act on. * * == Near-duplicate clusters: the forbid that retired * * - `near_duplicate_clusters` wraps `GET /api/v1/repository?near_duplicates=` * (shipped: SPGD-703, `specguard` `c43dc19`, 2026-08-28), serving * `RepositoryOverview#serialized_near_duplicates` behind an opt-in ask. * * Duplicate clustering was once under the same forbid that still keeps * `/check-intent` out — the standing rule: no tool may wrap what has not * shipped. That half retired when the platform moved and this entry became * wrappable. What moved was the platform, not the bar. * * == Member management * * - `list_repository_members`, `add_repository_member`, * `update_repository_member_permissions` and `remove_repository_member` * wrap the four `user_repository_members` endpoints (all shipped: SPGD-875, * specguard@origin/main). * * They also forced the transport's fourth verb: `update` is a PATCH, which * arrived with `patchJson` in this same slice — 200 with a JSON body, so it * routes through `requestJson` like `postJson` rather than `deleteJson`'s * raw-body handling. * * PATCH and DELETE name a membership by id, and the platform serves that id on * every member response (SPGD-894): the list, the add 201 and the edit 200 all * carry it, so an agent that listed or added a member already holds it — the * edit/revoke tool descriptions say so rather than leaving it implicit. Lookup * stays scoped to the repository (a foreign id is refused 404), and there is * no name-based lookup. * * == Rename * * - `rename_repository` wraps `PATCH /api/v1/repositories/:id` (shipped: * SPGD-878, `specguard@origin/main` e026793, PR #266). * * This is the entry the forbid above once named as "the natural next slice" — * the platform moved (SPGD-878 shipped the endpoint), so the forbid retired * exactly as the duplicate-clustering one did. It reuses `patchJson` landed * with the member-management slice rather than minting a transport of its own. * The verb's one asymmetry is carried in the tool description rather than * papered over: rename authorizes OWNER-ONLY, while removal authorizes the * BROADER `repo.delete` capability — a member who may destroy the repository * may not rename it. Before this tool the bridge's only rename path was * remove-and-re-register, which destroys every key, run and intent; this one * keeps them. * * == The agent-key family * * - `list_repository_agent_keys` and `revoke_repository_agent_key` wrap the * inventory and revoke of `user_repository_agent_keys` (shipped: SPGD-1004, * specguard `dfb9892`), and `list_repository_agent_keys_presented_revoked` * wraps the still-presented triage over the same controller (shipped: * SPGD-1023, specguard `533e77b`). * * The `sga_` noun's first bridge tools, and the gap they close is the one the * platform controller's own header records: a `keys.manage` holder could * already mint and revoke `sgk_` keys through this bridge but could not even * LIST the agent keys covering a repository, so the offboarding arc — a * departed member's agent key authenticating indefinitely — was not * completable over the bridge at all. The family also forced the transport's * third DELETE shape: this revoke answers `200` WITH a JSON disclosure body * (the API's substitute for the web confirm dialog — the response that * performs the cut also names every repository it landed on), so it routes * through `deleteJsonObject` — `requestJson`'s parse — rather than * `deleteJson`'s raw-body handling, which the empty-204 endpoints keep * unchanged. And the id the revoke needs is learnable ONLY from the listing: * minting is deliberately web-only at /account, so unlike the `sgk_` pair * there is no create response to read an id from — the list and revoke * descriptions say so rather than leaving it implicit. * * == The first tool that needs NO credential * * - `get_server_version` wraps the server's root-level `GET /version` * (shipped: SPGD-1197, specguard `08ab408`). * * The platform's own doctrine placed this read OUTSIDE the credential seam * rather than inside with an exception: every controller under `api/` must * declare an accepted credential or answer 401 to everything, so the * unauthenticated identity read lives at the root, beside `/up` and the * schema mirror. The bridge meets it where it stands — a fourth `require*` * helper (`requireEndpointApiConfig`) demands an endpoint and no key, the * transport's `Authorization` header became conditional on a present key * (mirroring the conditional `Content-Type` one key below it), and * `describeFailure`'s canned 401 sentence — a diagnosis of a wrong-kind key — * falls to the generic sentence when no credential is bound, because a * credential-free ask cannot have a key problem. The standing rule is * unchanged and still binding: the endpoint was verified-shipped on * `origin/main` before this entry wrapped it. What moved was the platform, * not the bar. * * == The contract itself, readable at last * * - `get_intent_schema` wraps the root-level `GET * /schemas/open-test-intent.v1.json` (shipped: `specguard/config/routes.rb`, * `SchemasController`, spec-covered by * `spec/requests/open_test_intent_schema_spec.rb`). * * The second credential-free entry, and the one that gives this registry a * REFERENCE beside the JUDGE it opened with. `lint_intent_annotations` can * only say that an annotation already written is wrong, and its refusals are * an incomplete teacher — a wrong enumerated value names the legal set, an * omitted field does not, and the schema's OPTIONAL property is underivable * in principle from a closed-object refusal, which only ever reports a key * that WAS sent. Serving the document closes that hole without touching the * judge. * * It mirrors rather than vendors, deliberately: the schema's design is one * canonical source plus byte-identical mirrors, so a copy inside this bridge * would be a third one, drifting silently and answering about itself instead * of about the deployment the agent is judged by. That is also why it forced * the transport's raw-body READ (`getText`): `requestJson`'s not-JSON * sentence blames the endpoint for pointing at a proxy or a login page, which * is the wrong remedy for a response that arrived correctly — the same * argument `deleteJson`'s empty `204` made, generalised. The shared `Accept` * header was widened in the same slice to admit `application/schema+json`, the * media type this route actually serves: the header is a claim this client * makes, and it must not exclude a route this client calls. The standing rule * is unchanged and still binding — the route was verified-shipped, spec-covered * and root-level before this entry wrapped it, and `/check-intent` (a comment * in `routes.rb`) stays out. * * == Ask the suite map * * - `find_tests_near_behavior` wraps `GET /api/v1/repository?near=` * (and the plural `GET /api/v1/repositories/:id?near=`), serving the * `near` block `RepositoryOverview#serialized_near` builds through * `NearProbe` (shipped: roadmap SPGD-1102 slices 1–2, specguard * `2e18377` and `59b0d19`). * * The first tool here whose ask is LIVE and BILLED — each novel phrase costs * one embedding call — which is why it is a tool of its own rather than a * parameter on `get_repository_overview`: an overview call must never pay an * embed by accident. It ranks stored tests nearest a phrase and never answers * "is this already tested?"; the tool description carries that and the three * silences the block keeps apart. The standing rule is unchanged and still * binding — `/check-intent` stays out: it has no backing endpoint, and this * tool is deliberately not a stand-in for one (the platform's floor discloses * and filters a ranked read; it never gates a write and issues no verdict). */ export declare const TOOLS: readonly ToolDefinition[]; export type { ToolContext, ToolDefinition, ToolResult } from "./types.js";