specguard-mcp 0.1.38 → 0.1.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -33,9 +33,9 @@ refuses to boot and takes the tools that needed no configuration down with it.
33
33
  | Variable | Needed by | Default | What it is |
34
34
  | --- | --- | --- | --- |
35
35
  | `SPECGUARD_ENDPOINT` | `get_repository_overview`, `get_intent_schema`, `get_server_version`, `list_repositories`, `add_repository`, `registrable_repositories` | — | your SpecGuard instance's root URL, **including the scheme** — e.g. `https://specguard.example.com`, or `http://localhost:3000`. A value with no scheme is refused by name (`SPECGUARD_ENDPOINT is not a usable URL: "sg.example.com"`) rather than surfacing later as an opaque failure. `SPECGUARD_URL` is accepted as an alias, and is the name every message uses when it is the one you set. A blank value counts as unset, so leaving `SPECGUARD_ENDPOINT` empty in a templated config falls through to `SPECGUARD_URL` instead of suppressing it. `get_server_version` and `get_intent_schema` need only this variable — they send no API key, because the routes they read are unauthenticated by design |
36
- | `SPECGUARD_API_KEY` | `get_repository_overview`, `near_duplicate_clusters` (default calls) | — | an agent/CI API key (`sgk_…`) issued by that deployment — a **per-repository** key, which is the single repository those tools answer about by default |
37
- | `SPECGUARD_USER_API_KEY` | `list_repositories` (fallback), `add_repository`, `registrable_repositories`, `remove_repository` (fallback), `create_repository_api_key` (fallback), `revoke_repository_api_key` (fallback), `list_repository_api_keys` (fallback), `list_repository_agent_keys` (fallback), `revoke_repository_agent_key` (fallback), `list_repository_agent_keys_presented_revoked` (fallback), `list_repository_members` (fallback), `add_repository_member` (fallback), `update_repository_member_permissions` (fallback), `remove_repository_member` (fallback), `get_repository_overview` / `near_duplicate_clusters` **with** `repository` (fallback), `rename_repository` | — | a **user** API key (`sgu_…`), minted from that deployment's account page. A different credential from the one above, not a second place to put the same value: SpecGuard decides which of them a request may use from the token's prefix, before it reads anything, and answers `401` for the other one. Set whichever your tools need — both, if you use both |
38
- | `SPECGUARD_AGENT_API_KEY` | `list_repositories` (preferred), `remove_repository` (preferred), `create_repository_api_key` (preferred), `revoke_repository_api_key` (preferred), `list_repository_api_keys` (preferred), `list_repository_agent_keys` (preferred), `revoke_repository_agent_key` (preferred), `list_repository_agent_keys_presented_revoked` (preferred), `list_repository_members` (preferred), `add_repository_member` (preferred), `update_repository_member_permissions` (preferred), `remove_repository_member` (preferred), `get_repository_overview` / `near_duplicate_clusters` **with** `repository` (preferred) | — | an **agent** API key (`sga_…`), minted from that deployment's account page (Agent keys panel) with an explicit set of repositories and permissions. It speaks for nobody: its reach is exactly the set granted onto it, fixed at mint time, and every read is bounded by that set server-side. This is the credential to give an automated agent — one key, many repositories, none of a person's rights. When it and `SPECGUARD_USER_API_KEY` are both set, every tool that answers either key — `list_repositories`, `remove_repository`, the members tools, the key-lifecycle tools, and `get_repository_overview` / `near_duplicate_clusters` **with** `repository` — uses **this** one, so discovery stays inside the set the other tools can reach |
36
+ | `SPECGUARD_API_KEY` | `get_repository_overview`, `near_duplicate_clusters`, `find_tests_near_behavior` (default calls) | — | an agent/CI API key (`sgk_…`) issued by that deployment — a **per-repository** key, which is the single repository those tools answer about by default |
37
+ | `SPECGUARD_USER_API_KEY` | `list_repositories` (fallback), `add_repository`, `registrable_repositories`, `remove_repository` (fallback), `create_repository_api_key` (fallback), `revoke_repository_api_key` (fallback), `list_repository_api_keys` (fallback), `list_repository_agent_keys` (fallback), `revoke_repository_agent_key` (fallback), `list_repository_agent_keys_presented_revoked` (fallback), `list_repository_members` (fallback), `add_repository_member` (fallback), `update_repository_member_permissions` (fallback), `remove_repository_member` (fallback), `get_repository_overview` / `near_duplicate_clusters` / `find_tests_near_behavior` **with** `repository` (fallback), `rename_repository` | — | a **user** API key (`sgu_…`), minted from that deployment's account page. A different credential from the one above, not a second place to put the same value: SpecGuard decides which of them a request may use from the token's prefix, before it reads anything, and answers `401` for the other one. Set whichever your tools need — both, if you use both |
38
+ | `SPECGUARD_AGENT_API_KEY` | `list_repositories` (preferred), `remove_repository` (preferred), `create_repository_api_key` (preferred), `revoke_repository_api_key` (preferred), `list_repository_api_keys` (preferred), `list_repository_agent_keys` (preferred), `revoke_repository_agent_key` (preferred), `list_repository_agent_keys_presented_revoked` (preferred), `list_repository_members` (preferred), `add_repository_member` (preferred), `update_repository_member_permissions` (preferred), `remove_repository_member` (preferred), `get_repository_overview` / `near_duplicate_clusters` / `find_tests_near_behavior` **with** `repository` (preferred) | — | an **agent** API key (`sga_…`), minted from that deployment's account page (Agent keys panel) with an explicit set of repositories and permissions. It speaks for nobody: its reach is exactly the set granted onto it, fixed at mint time, and every read is bounded by that set server-side. This is the credential to give an automated agent — one key, many repositories, none of a person's rights. When it and `SPECGUARD_USER_API_KEY` are both set, every tool that answers either key — `list_repositories`, `remove_repository`, the members tools, the key-lifecycle tools, and `get_repository_overview` / `near_duplicate_clusters` / `find_tests_near_behavior` **with** `repository` — uses **this** one, so discovery stays inside the set the other tools can reach |
39
39
  | `SPECGUARD_LINT_COMMAND` | `lint_intent_annotations` | `specguard-lint` | the command that runs the linter — the one switch between the Ruby and the JS/TS client. Most Ruby projects need `bundle exec specguard-lint`; a JS/TS project sets `npx -p @yatfa/specguard specguard lint` (and needs the validator backend: a `validate-intent` binary whose path `SPECGUARD_VALIDATE_INTENT` names — today the only way to supply it) |
40
40
  | `SPECGUARD_TIMEOUT_MS` | HTTP tools | `30000` | how long a call to SpecGuard may take |
41
41
 
@@ -714,6 +714,50 @@ path's one deliberate omission: `api_key` is **absent** from the plural body rat
714
714
  because the block describes the credential that made the request and no member credential is a
715
715
  repository key (an absent key there is the surface's shape, never a dropped block).
716
716
 
717
+ ### `find_tests_near_behavior`
718
+
719
+ Asks a repository's stored test suite map which tests are **nearest a behavior phrase** you give
720
+ it. The server embeds the phrase and ranks the repository's stored test identities by similarity,
721
+ returning the top hits in the `near` block (`GET /api/v1/repository?near=<phrase>`, or the plural
722
+ `GET /api/v1/repositories/:id?near=<phrase>`) — each hit with its similarity, its `signal_source`,
723
+ its last-known path and the weight the latest run measured.
724
+
725
+ | argument | |
726
+ | --- | --- |
727
+ | `behavior` | **required** — the behavior phrase, in plain words (e.g. "rejects an expired password reset token"). Trimmed and sent as `near`. Blank or whitespace-only is refused before any request, and so is a phrase containing a NUL (`\u0000`) character: the server would read either as no ask and answer the whole overview with `near: null`, which would look like an answered ask |
728
+ | `repository` | ask THIS repository (its numeric id from `list_repositories`) under the **agent key** (`SPECGUARD_AGENT_API_KEY`, `sga_…`), or — when no agent key is set — the **user key** (`SPECGUARD_USER_API_KEY`, `sgu_…`), instead of the one the `sgk_…` key resolves to — omit it for the default, `sgk_`-keyed ask |
729
+
730
+ **What the answer is not.** It ranks *stored* tests nearest the phrase and **never answers "is this
731
+ already tested?"** — it never gates a write and never issues a verdict. A hit near the phrase is not
732
+ coverage, and an empty answer is not proof of absence. `similarity_floor` is the near-duplicate
733
+ census's redundancy bar, **not** the 0.95 matching threshold that decides whether two tests are the
734
+ same test. Read `similarity_basis` and `similarity_floor` before any figure.
735
+
736
+ **The three silences differ** — never collapse them, and `null` is never `[]`:
737
+
738
+ - `status` of `provider_unconfigured` or `embedding_failed` with `ranked: null` — no ranking was
739
+ attempted, or the provider refused (`error` carries its reason). This says nothing about the suite.
740
+ - `identity_count: 0` with `ranked: []` — the repository holds no identities; nothing has been
741
+ ingested.
742
+ - `ranked: []` with `identity_count` above zero and `best_below_floor_similarity` — identities exist,
743
+ the search ran, and none is near the phrase; the nearest one's similarity is served so "nothing
744
+ near" is a finding you can check.
745
+
746
+ `signal_sources` (the composition of the served page) and each hit's `signal_source` (matched on
747
+ declared intent, or on name) are different evidence — read the source before leaning on a hit.
748
+
749
+ **Cost.** Each *novel* phrase costs **one billed embedding call** at the provider; a repeated phrase
750
+ is served from the cache and costs nothing (`cache_served` says which happened). The ask is live —
751
+ computed on the request — unlike the stored census `near_duplicate_clusters` returns, and it is a
752
+ tool of its own so that a `get_repository_overview` call never pays an embed by accident.
753
+
754
+ Same endpoints and credentials as `get_repository_overview`: without `repository`, an `sgk_…`
755
+ repository key on `GET /api/v1/repository`; with `repository`, either member credential on the
756
+ plural endpoint — the agent key preferred, the user key when no agent key is set. A repository
757
+ outside the presented credential's grant answers 404 at the server. The response is that body with
758
+ the `near` block opened, passed through unmodified; on the plural path `api_key` is **absent** from
759
+ the body rather than nulled (the surface's shape, never a dropped block).
760
+
717
761
  ### `remove_repository`
718
762
 
719
763
  Removes a repository from SpecGuard — and with it **every key, run and intent on it**. This is the
@@ -780,7 +824,11 @@ serves live rows only). One row per key: `id`, `name`, `token_hint`, `created_at
780
824
  omitted — `rotated_at`, and `rotated_and_unused` (live-scoped: the stranded shape
781
825
  `get_repository_overview`'s `credential_health` reports, at row grain). `status` is
782
826
  `live`/`revoked`, with `revoked_at` present only on revoked rows — a revoked `sgk_` row is not a
783
- credential either, but the rotation's story lives in the population.
827
+ credential either, but the rotation's story lives in the population. A **revoked** row also
828
+ serves `last_refused_at`: `null` means offboarding took (nothing has presented the dead token
829
+ since the cut), a timestamp means the dead token is still arriving and that row's `token_hint`
830
+ is what to hunt in the secret stores that may still hold it. Live rows carry no
831
+ `last_refused_at` key.
784
832
 
785
833
  | argument | |
786
834
  | --- | --- |
@@ -0,0 +1,40 @@
1
+ import type { ToolDefinition } from "./types.js";
2
+ /**
3
+ * `GET /api/v1/repository?near=<behavior phrase>` as a tool — the bridge half
4
+ * of roadmap SPGD-1102 ("Ask the suite map"), slice 3. The platform half
5
+ * shipped first: `RequestedNearParam` guards the ask and `NearProbe` answers
6
+ * it, behind the `near` block of `RepositoryOverview`, on both overview doors
7
+ * (the singular `GET /api/v1/repository` under an `sgk_` key, the plural
8
+ * `GET /api/v1/repositories/:id` under an agent or user key).
9
+ *
10
+ * == Why a SEPARATE tool, and not a parameter on `get_repository_overview`
11
+ *
12
+ * Every other ask on the overview is free or stored. This one is LIVE and
13
+ * BILLED: a novel phrase costs one embedding call at the provider (a repeated
14
+ * phrase is served from the embedding cache and buys nothing). An agent
15
+ * calling the overview must never pay an embed by accident, and an agent
16
+ * calling this tool has asked for nothing else — the same line
17
+ * `near_duplicate_clusters` draws for the census, drawn here for a cost that
18
+ * is per-call rather than per-ingest.
19
+ *
20
+ * == The phrase is sent trimmed, and a NUL is refused HERE
21
+ *
22
+ * `requireString` trims and refuses blank. The NUL refusal is the bridge's to
23
+ * make, because the server's guard treats a NUL-containing, blank or
24
+ * non-String `near` as NO ASK and answers the plain overview with `near:
25
+ * null` — a 200 that would read, to an agent that sent a phrase, as an
26
+ * answered ask. Refusing client-side keeps "asked, nothing came back" and
27
+ * "never asked" from sharing one wire shape.
28
+ *
29
+ * == The response is passed through, not re-modelled
30
+ *
31
+ * Same rule as `near-duplicate-clusters.ts`. The `near` block's shape carries
32
+ * distinctions the serializer spent care on — `ranked: null` (no ranking was
33
+ * attempted: `provider_unconfigured` / `embedding_failed`) versus `ranked: []`
34
+ * (the search ran and found nothing near), and, inside the latter,
35
+ * `identity_count: 0` versus `identity_count > 0` with
36
+ * `best_below_floor_similarity`. Coalescing any of them would turn three
37
+ * different silences into one. The body goes back as it arrived.
38
+ */
39
+ declare const findTestsNearBehavior: ToolDefinition;
40
+ export default findTestsNearBehavior;
@@ -0,0 +1,135 @@
1
+ import { ArgumentError } from "../errors.js";
2
+ import { getJsonObject, repositoryTarget } from "../support/specguard-api.js";
3
+ import { optionalString, requireString } from "./args.js";
4
+ /**
5
+ * `GET /api/v1/repository?near=<behavior phrase>` as a tool — the bridge half
6
+ * of roadmap SPGD-1102 ("Ask the suite map"), slice 3. The platform half
7
+ * shipped first: `RequestedNearParam` guards the ask and `NearProbe` answers
8
+ * it, behind the `near` block of `RepositoryOverview`, on both overview doors
9
+ * (the singular `GET /api/v1/repository` under an `sgk_` key, the plural
10
+ * `GET /api/v1/repositories/:id` under an agent or user key).
11
+ *
12
+ * == Why a SEPARATE tool, and not a parameter on `get_repository_overview`
13
+ *
14
+ * Every other ask on the overview is free or stored. This one is LIVE and
15
+ * BILLED: a novel phrase costs one embedding call at the provider (a repeated
16
+ * phrase is served from the embedding cache and buys nothing). An agent
17
+ * calling the overview must never pay an embed by accident, and an agent
18
+ * calling this tool has asked for nothing else — the same line
19
+ * `near_duplicate_clusters` draws for the census, drawn here for a cost that
20
+ * is per-call rather than per-ingest.
21
+ *
22
+ * == The phrase is sent trimmed, and a NUL is refused HERE
23
+ *
24
+ * `requireString` trims and refuses blank. The NUL refusal is the bridge's to
25
+ * make, because the server's guard treats a NUL-containing, blank or
26
+ * non-String `near` as NO ASK and answers the plain overview with `near:
27
+ * null` — a 200 that would read, to an agent that sent a phrase, as an
28
+ * answered ask. Refusing client-side keeps "asked, nothing came back" and
29
+ * "never asked" from sharing one wire shape.
30
+ *
31
+ * == The response is passed through, not re-modelled
32
+ *
33
+ * Same rule as `near-duplicate-clusters.ts`. The `near` block's shape carries
34
+ * distinctions the serializer spent care on — `ranked: null` (no ranking was
35
+ * attempted: `provider_unconfigured` / `embedding_failed`) versus `ranked: []`
36
+ * (the search ran and found nothing near), and, inside the latter,
37
+ * `identity_count: 0` versus `identity_count > 0` with
38
+ * `best_below_floor_similarity`. Coalescing any of them would turn three
39
+ * different silences into one. The body goes back as it arrived.
40
+ */
41
+ const findTestsNearBehavior = {
42
+ name: "find_tests_near_behavior",
43
+ title: "Find tests near a behavior",
44
+ description: "Ask a repository's stored test suite map: which tests are nearest a behavior phrase you give it? " +
45
+ "The server embeds your phrase and ranks the repository's stored test identities by similarity, " +
46
+ "returning the top hits in the `near` block — each with its similarity, `signal_source`, last-known " +
47
+ "path and the weight the latest run measured. " +
48
+ "WHAT THE ANSWER IS NOT: it ranks STORED tests nearest the phrase and NEVER answers \"is this " +
49
+ "already tested?\". It never gates a write and never issues a verdict — a hit near the phrase is not " +
50
+ "coverage, and an empty answer is not proof of absence. `similarity_floor` is the near-duplicate " +
51
+ "census's redundancy bar (how alike two tests must read for the census to pair them), NOT the 0.95 " +
52
+ "matching threshold that decides whether two tests are the same test; do not read it as a pass mark. " +
53
+ "READ `similarity_basis` AND `similarity_floor` BEFORE ANY FIGURE: a similarity without the statement " +
54
+ "of what it measures is a confident number over nothing. " +
55
+ "THE THREE SILENCES ARE DIFFERENT AND MUST NOT BE COLLAPSED: (1) `status` of `provider_unconfigured` " +
56
+ "or `embedding_failed` with `ranked: null` — no ranking was attempted or the provider refused (an " +
57
+ "`error` carries the provider's own reason); this says NOTHING about the suite. (2) `identity_count: " +
58
+ "0` with `ranked: []` — the repository holds no identities, nothing has been ingested. (3) `ranked: " +
59
+ "[]` with `identity_count` above zero and `best_below_floor_similarity` — identities exist and the " +
60
+ "search ran, and none is near the phrase; the nearest one's similarity is served so that \"nothing " +
61
+ "near\" is a finding you can check. `null` and `[]` are never interchangeable here. " +
62
+ "`signal_sources` (the composition of the served page) and each hit's `signal_source` (whether the " +
63
+ "hit matched on declared intent or on its name) are DIFFERENT EVIDENCE: a name match and an intent " +
64
+ "match are not the same claim, so read the source before leaning on a hit. " +
65
+ "COST: each NOVEL phrase costs ONE BILLED EMBEDDING CALL at the provider; repeating a phrase is " +
66
+ "served from the cache and costs nothing (`cache_served` says which happened). The ask is LIVE — " +
67
+ "computed on the request — unlike the stored census `near_duplicate_clusters` returns; it is its " +
68
+ "own tool so that an overview call never pays an embed by accident. Send a considered phrase, not " +
69
+ "a sweep of guesses. " +
70
+ "`behavior` is REQUIRED: a behavior phrase in plain words (trimmed; blank is refused here before " +
71
+ "any request, and so is a NUL character — the server would read it as no ask and answer the whole " +
72
+ "overview with `near: null`, which would look like an answered ask). " +
73
+ "WHICH repository is asked is optional: pass `repository` (a numeric id from `list_repositories`) " +
74
+ "to ask that named repository under either member credential — the agent key preferred, the person " +
75
+ "key when no agent key is set — or omit it to ask the repository the configured sgk_… key resolves " +
76
+ "to. Same endpoints and credentials as `get_repository_overview`: without `repository`, an `sgk_` " +
77
+ "repository key (SPECGUARD_API_KEY) on `GET /api/v1/repository`; with `repository`, EITHER member " +
78
+ "credential on `GET /api/v1/repositories/:id` — SPECGUARD_AGENT_API_KEY (an sga_… agent key) " +
79
+ "preferred, and, when that is not set, SPECGUARD_USER_API_KEY (an sgu_… person key); with both set " +
80
+ "the agent key wins. The server owns the refusals on that path: a repository outside the presented " +
81
+ "credential's grant answers 404, person and agent alike. " +
82
+ "The response is the endpoint's full body with the `near` block OPENED, passed through unmodified — " +
83
+ "with the one surface difference `get_repository_overview` documents for its own `repository` ask: " +
84
+ "on the plural path the `api_key` block is ABSENT from the body rather than nulled.",
85
+ inputSchema: {
86
+ type: "object",
87
+ properties: {
88
+ behavior: {
89
+ type: "string",
90
+ description: "REQUIRED. The behavior phrase to look for — plain words describing what a test would " +
91
+ "check, e.g. \"rejects an expired password reset token\". Trimmed and sent as `near`. " +
92
+ "Blank or whitespace-only is refused before any request, and so is a phrase containing a " +
93
+ "NUL character (U+0000): the server reads those shapes as no ask at all and would answer " +
94
+ "the whole overview with `near: null`. NOTE: every NOVEL phrase costs one billed " +
95
+ "embedding call; a repeated phrase is cache-served.",
96
+ },
97
+ repository: {
98
+ type: "string",
99
+ description: "Ask THIS repository instead of the one the configured sgk_… key resolves to. The " +
100
+ "value is the repository's NUMERIC ID, exactly as served in `list_repositories` " +
101
+ "entries' `id` — not the `org/repo` handle. " +
102
+ "The credential changes with it: the call authenticates with EITHER member credential, " +
103
+ "whichever is set — SPECGUARD_AGENT_API_KEY (an sga_… agent key; the set of " +
104
+ "repositories granted onto it at mint time is the boundary the id is resolved " +
105
+ "inside) and, when that is not set, SPECGUARD_USER_API_KEY (an sgu_… key — a PERSON " +
106
+ "key, whose accessible set is the boundary instead); with both set the agent key " +
107
+ "wins. A repository outside the presented credential's grant answers 404, " +
108
+ "indistinguishable from one that does not exist. " +
109
+ "Omit it — or pass a blank — and the call is the singular one under SPECGUARD_API_KEY.",
110
+ },
111
+ },
112
+ required: ["behavior"],
113
+ additionalProperties: false,
114
+ },
115
+ async run(args, context) {
116
+ // Argument checks come FIRST, before config resolution or any request:
117
+ // a malformed phrase must not cost a credential lookup, let alone a
118
+ // billed embed.
119
+ const behavior = requireString(args["behavior"], "behavior");
120
+ if (behavior.includes("\u0000")) {
121
+ throw new ArgumentError("`behavior` must not contain a NUL (\\u0000) character: the server reads a NUL-containing " +
122
+ "`near` as no ask and would answer the whole overview with `near: null`, which would look " +
123
+ "like an answered ask. Remove the character and send the phrase again.");
124
+ }
125
+ const repository = optionalString(args["repository"], "repository");
126
+ const { api, path } = repositoryTarget(context.config, repository);
127
+ const overview = await getJsonObject(api, path, { near: behavior }, context.fetch);
128
+ return {
129
+ text: JSON.stringify(overview, null, 2),
130
+ structured: overview,
131
+ };
132
+ },
133
+ };
134
+ export default findTestsNearBehavior;
135
+ //# sourceMappingURL=find-tests-near-behavior.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"find-tests-near-behavior.js","sourceRoot":"","sources":["../../../src/tools/find-tests-near-behavior.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC9E,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAG1D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,MAAM,qBAAqB,GAAmB;IAC5C,IAAI,EAAE,0BAA0B;IAEhC,KAAK,EAAE,4BAA4B;IAEnC,WAAW,EACT,mGAAmG;QACnG,iGAAiG;QACjG,qGAAqG;QACrG,+CAA+C;QAC/C,+FAA+F;QAC/F,sGAAsG;QACtG,kGAAkG;QAClG,oGAAoG;QACpG,sGAAsG;QACtG,uGAAuG;QACvG,0DAA0D;QAC1D,sGAAsG;QACtG,mGAAmG;QACnG,sGAAsG;QACtG,qGAAqG;QACrG,oGAAoG;QACpG,oGAAoG;QACpG,qFAAqF;QACrF,oGAAoG;QACpG,oGAAoG;QACpG,4EAA4E;QAC5E,iGAAiG;QACjG,kGAAkG;QAClG,kGAAkG;QAClG,mGAAmG;QACnG,sBAAsB;QACtB,kGAAkG;QAClG,mGAAmG;QACnG,sEAAsE;QACtE,mGAAmG;QACnG,oGAAoG;QACpG,oGAAoG;QACpG,mGAAmG;QACnG,mGAAmG;QACnG,8FAA8F;QAC9F,oGAAoG;QACpG,oGAAoG;QACpG,0DAA0D;QAC1D,qGAAqG;QACrG,oGAAoG;QACpG,oFAAoF;IAEtF,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE;YACV,QAAQ,EAAE;gBACR,IAAI,EAAE,QAAQ;gBACd,WAAW,EACT,uFAAuF;oBACvF,uFAAuF;oBACvF,0FAA0F;oBAC1F,0FAA0F;oBAC1F,kFAAkF;oBAClF,oDAAoD;aACvD;YACD,UAAU,EAAE;gBACV,IAAI,EAAE,QAAQ;gBACd,WAAW,EACT,mFAAmF;oBACnF,iFAAiF;oBACjF,6CAA6C;oBAC7C,wFAAwF;oBACxF,6EAA6E;oBAC7E,+EAA+E;oBAC/E,qFAAqF;oBACrF,kFAAkF;oBAClF,2EAA2E;oBAC3E,kDAAkD;oBAClD,uFAAuF;aAC1F;SACF;QACD,QAAQ,EAAE,CAAC,UAAU,CAAC;QACtB,oBAAoB,EAAE,KAAK;KAC5B;IAED,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO;QACrB,uEAAuE;QACvE,oEAAoE;QACpE,gBAAgB;QAChB,MAAM,QAAQ,GAAG,aAAa,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC,CAAC;QAC7D,IAAI,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;YAChC,MAAM,IAAI,aAAa,CACrB,2FAA2F;gBACzF,2FAA2F;gBAC3F,uEAAuE,CAC1E,CAAC;QACJ,CAAC;QACD,MAAM,UAAU,GAAG,cAAc,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,YAAY,CAAC,CAAC;QACpE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,gBAAgB,CAAC,OAAO,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;QAEnE,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;QAEnF,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;YACvC,UAAU,EAAE,QAAQ;SACrB,CAAC;IACJ,CAAC;CACF,CAAC;AAEF,eAAe,qBAAqB,CAAC"}
@@ -231,6 +231,24 @@ import type { ToolDefinition } from "./types.js";
231
231
  * is unchanged and still binding — the route was verified-shipped, spec-covered
232
232
  * and root-level before this entry wrapped it, and `/check-intent` (a comment
233
233
  * in `routes.rb`) stays out.
234
+ *
235
+ * == Ask the suite map
236
+ *
237
+ * - `find_tests_near_behavior` wraps `GET /api/v1/repository?near=<phrase>`
238
+ * (and the plural `GET /api/v1/repositories/:id?near=`), serving the
239
+ * `near` block `RepositoryOverview#serialized_near` builds through
240
+ * `NearProbe` (shipped: roadmap SPGD-1102 slices 1–2, specguard
241
+ * `2e18377` and `59b0d19`).
242
+ *
243
+ * The first tool here whose ask is LIVE and BILLED — each novel phrase costs
244
+ * one embedding call — which is why it is a tool of its own rather than a
245
+ * parameter on `get_repository_overview`: an overview call must never pay an
246
+ * embed by accident. It ranks stored tests nearest a phrase and never answers
247
+ * "is this already tested?"; the tool description carries that and the three
248
+ * silences the block keeps apart. The standing rule is unchanged and still
249
+ * binding — `/check-intent` stays out: it has no backing endpoint, and this
250
+ * tool is deliberately not a stand-in for one (the platform's floor discloses
251
+ * and filters a ranked read; it never gates a write and issues no verdict).
234
252
  */
235
253
  export declare const TOOLS: readonly ToolDefinition[];
236
254
  export type { ToolContext, ToolDefinition, ToolResult } from "./types.js";
@@ -1,6 +1,7 @@
1
1
  import addRepository from "./add-repository.js";
2
2
  import addRepositoryMember from "./add-repository-member.js";
3
3
  import createRepositoryApiKey from "./create-repository-api-key.js";
4
+ import findTestsNearBehavior from "./find-tests-near-behavior.js";
4
5
  import getIntentSchema from "./get-intent-schema.js";
5
6
  import getServerVersion from "./get-server-version.js";
6
7
  import lintIntentAnnotations from "./lint-intent-annotations.js";
@@ -250,6 +251,24 @@ import updateRepositoryMemberPermissions from "./update-repository-member-permis
250
251
  * is unchanged and still binding — the route was verified-shipped, spec-covered
251
252
  * and root-level before this entry wrapped it, and `/check-intent` (a comment
252
253
  * in `routes.rb`) stays out.
254
+ *
255
+ * == Ask the suite map
256
+ *
257
+ * - `find_tests_near_behavior` wraps `GET /api/v1/repository?near=<phrase>`
258
+ * (and the plural `GET /api/v1/repositories/:id?near=`), serving the
259
+ * `near` block `RepositoryOverview#serialized_near` builds through
260
+ * `NearProbe` (shipped: roadmap SPGD-1102 slices 1–2, specguard
261
+ * `2e18377` and `59b0d19`).
262
+ *
263
+ * The first tool here whose ask is LIVE and BILLED — each novel phrase costs
264
+ * one embedding call — which is why it is a tool of its own rather than a
265
+ * parameter on `get_repository_overview`: an overview call must never pay an
266
+ * embed by accident. It ranks stored tests nearest a phrase and never answers
267
+ * "is this already tested?"; the tool description carries that and the three
268
+ * silences the block keeps apart. The standing rule is unchanged and still
269
+ * binding — `/check-intent` stays out: it has no backing endpoint, and this
270
+ * tool is deliberately not a stand-in for one (the platform's floor discloses
271
+ * and filters a ranked read; it never gates a write and issues no verdict).
253
272
  */
254
273
  export const TOOLS = [
255
274
  lintIntentAnnotations,
@@ -272,5 +291,6 @@ export const TOOLS = [
272
291
  listRepositoryAgentKeys,
273
292
  listRepositoryAgentKeysPresentedRevoked,
274
293
  revokeRepositoryAgentKey,
294
+ findTestsNearBehavior,
275
295
  ];
276
296
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,MAAM,qBAAqB,CAAC;AAChD,OAAO,mBAAmB,MAAM,4BAA4B,CAAC;AAC7D,OAAO,sBAAsB,MAAM,gCAAgC,CAAC;AACpE,OAAO,eAAe,MAAM,wBAAwB,CAAC;AACrD,OAAO,gBAAgB,MAAM,yBAAyB,CAAC;AACvD,OAAO,qBAAqB,MAAM,8BAA8B,CAAC;AACjE,OAAO,gBAAgB,MAAM,wBAAwB,CAAC;AACtD,OAAO,uBAAuB,MAAM,iCAAiC,CAAC;AACtE,OAAO,uCAAuC,MAAM,mDAAmD,CAAC;AACxG,OAAO,qBAAqB,MAAM,+BAA+B,CAAC;AAClE,OAAO,qBAAqB,MAAM,8BAA8B,CAAC;AACjE,OAAO,qBAAqB,MAAM,8BAA8B,CAAC;AACjE,OAAO,qBAAqB,MAAM,0BAA0B,CAAC;AAC7D,OAAO,uBAAuB,MAAM,+BAA+B,CAAC;AACpE,OAAO,gBAAgB,MAAM,wBAAwB,CAAC;AACtD,OAAO,sBAAsB,MAAM,+BAA+B,CAAC;AACnE,OAAO,gBAAgB,MAAM,wBAAwB,CAAC;AACtD,OAAO,wBAAwB,MAAM,kCAAkC,CAAC;AACxE,OAAO,sBAAsB,MAAM,gCAAgC,CAAC;AACpE,OAAO,iCAAiC,MAAM,2CAA2C,CAAC;AAG1F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwOG;AACH,MAAM,CAAC,MAAM,KAAK,GAA8B;IAC9C,qBAAqB;IACrB,eAAe;IACf,qBAAqB;IACrB,gBAAgB;IAChB,gBAAgB;IAChB,aAAa;IACb,uBAAuB;IACvB,gBAAgB;IAChB,sBAAsB;IACtB,sBAAsB;IACtB,qBAAqB;IACrB,qBAAqB;IACrB,qBAAqB;IACrB,mBAAmB;IACnB,iCAAiC;IACjC,sBAAsB;IACtB,gBAAgB;IAChB,uBAAuB;IACvB,uCAAuC;IACvC,wBAAwB;CACzB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,MAAM,qBAAqB,CAAC;AAChD,OAAO,mBAAmB,MAAM,4BAA4B,CAAC;AAC7D,OAAO,sBAAsB,MAAM,gCAAgC,CAAC;AACpE,OAAO,qBAAqB,MAAM,+BAA+B,CAAC;AAClE,OAAO,eAAe,MAAM,wBAAwB,CAAC;AACrD,OAAO,gBAAgB,MAAM,yBAAyB,CAAC;AACvD,OAAO,qBAAqB,MAAM,8BAA8B,CAAC;AACjE,OAAO,gBAAgB,MAAM,wBAAwB,CAAC;AACtD,OAAO,uBAAuB,MAAM,iCAAiC,CAAC;AACtE,OAAO,uCAAuC,MAAM,mDAAmD,CAAC;AACxG,OAAO,qBAAqB,MAAM,+BAA+B,CAAC;AAClE,OAAO,qBAAqB,MAAM,8BAA8B,CAAC;AACjE,OAAO,qBAAqB,MAAM,8BAA8B,CAAC;AACjE,OAAO,qBAAqB,MAAM,0BAA0B,CAAC;AAC7D,OAAO,uBAAuB,MAAM,+BAA+B,CAAC;AACpE,OAAO,gBAAgB,MAAM,wBAAwB,CAAC;AACtD,OAAO,sBAAsB,MAAM,+BAA+B,CAAC;AACnE,OAAO,gBAAgB,MAAM,wBAAwB,CAAC;AACtD,OAAO,wBAAwB,MAAM,kCAAkC,CAAC;AACxE,OAAO,sBAAsB,MAAM,gCAAgC,CAAC;AACpE,OAAO,iCAAiC,MAAM,2CAA2C,CAAC;AAG1F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0PG;AACH,MAAM,CAAC,MAAM,KAAK,GAA8B;IAC9C,qBAAqB;IACrB,eAAe;IACf,qBAAqB;IACrB,gBAAgB;IAChB,gBAAgB;IAChB,aAAa;IACb,uBAAuB;IACvB,gBAAgB;IAChB,sBAAsB;IACtB,sBAAsB;IACtB,qBAAqB;IACrB,qBAAqB;IACrB,qBAAqB;IACrB,mBAAmB;IACnB,iCAAiC;IACjC,sBAAsB;IACtB,gBAAgB;IAChB,uBAAuB;IACvB,uCAAuC;IACvC,wBAAwB;IACxB,qBAAqB;CACtB,CAAC"}
@@ -75,6 +75,10 @@ const listRepositoryApiKeys = {
75
75
  "`get_repository_overview`'s `credential_health` reports, at row grain. " +
76
76
  "`token_hint` is a hint, never the token — the plaintext existed for exactly one response " +
77
77
  "at mint time and nothing persisted it. " +
78
+ "The post-cut verify: a REVOKED row also serves `last_refused_at`. `null` means offboarding " +
79
+ "took — nothing has presented the dead token since the cut; a timestamp means the dead " +
80
+ "token is still arriving, and that row's `token_hint` is what to hunt in the secret stores " +
81
+ "that may still hold it. Live rows carry no `last_refused_at` key. " +
78
82
  "Takes `repository_id` (the numeric id `list_repositories` reports, not the `org/repo` handle). " +
79
83
  "It authenticates with EITHER of this server's two key-administration credentials, whichever " +
80
84
  "is set: SPECGUARD_AGENT_API_KEY (an sga_… key — the call then reaches only the repositories " +
@@ -1 +1 @@
1
- {"version":3,"file":"list-repository-api-keys.js","sourceRoot":"","sources":["../../../src/tools/list-repository-api-keys.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,2BAA2B,EAAE,MAAM,6BAA6B,CAAC;AACzF,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAG1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,MAAM,qBAAqB,GAAmB;IAC5C,IAAI,EAAE,0BAA0B;IAChC,KAAK,EAAE,0BAA0B;IACjC,WAAW,EACT,qFAAqF;QACrF,0FAA0F;QAC1F,6FAA6F;QAC7F,4FAA4F;QAC5F,uEAAuE;QACvE,uFAAuF;QACvF,uFAAuF;QACvF,yFAAyF;QACzF,+BAA+B;QAC/B,oFAAoF;QACpF,4FAA4F;QAC5F,2FAA2F;QAC3F,uFAAuF;QACvF,2FAA2F;QAC3F,wFAAwF;QACxF,yEAAyE;QACzE,2FAA2F;QAC3F,yCAAyC;QACzC,iGAAiG;QACjG,8FAA8F;QAC9F,8FAA8F;QAC9F,+FAA+F;QAC/F,4FAA4F;QAC5F,wEAAwE;QACxE,4FAA4F;QAC5F,wFAAwF;QACxF,mDAAmD;QACnD,kGAAkG;QAClG,oDAAoD;IACtD,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE;YACV,aAAa,EAAE;gBACb,IAAI,EAAE,QAAQ;gBACd,WAAW,EACT,8EAA8E;oBAC9E,qEAAqE;aACxE;SACF;QACD,QAAQ,EAAE,CAAC,eAAe,CAAC;QAC3B,qEAAqE;QACrE,6EAA6E;QAC7E,uBAAuB;QACvB,oBAAoB,EAAE,KAAK;KAC5B;IAED,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO;QACrB,MAAM,YAAY,GAAG,aAAa,CAAC,IAAI,CAAC,eAAe,CAAC,EAAE,eAAe,CAAC,CAAC;QAE3E,MAAM,GAAG,GAAG,2BAA2B,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAExD,MAAM,IAAI,GAAG,MAAM,aAAa,CAC9B,GAAG,EACH,wBAAwB,kBAAkB,CAAC,YAAY,CAAC,WAAW,EACnE,EAAE,EACF,OAAO,CAAC,KAAK,CACd,CAAC;QAEF,2EAA2E;QAC3E,2EAA2E;QAC3E,yEAAyE;QACzE,0DAA0D;QAC1D,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YACnC,UAAU,EAAE,IAAI;SACjB,CAAC;IACJ,CAAC;CACF,CAAC;AAEF,eAAe,qBAAqB,CAAC"}
1
+ {"version":3,"file":"list-repository-api-keys.js","sourceRoot":"","sources":["../../../src/tools/list-repository-api-keys.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,2BAA2B,EAAE,MAAM,6BAA6B,CAAC;AACzF,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAG1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,MAAM,qBAAqB,GAAmB;IAC5C,IAAI,EAAE,0BAA0B;IAChC,KAAK,EAAE,0BAA0B;IACjC,WAAW,EACT,qFAAqF;QACrF,0FAA0F;QAC1F,6FAA6F;QAC7F,4FAA4F;QAC5F,uEAAuE;QACvE,uFAAuF;QACvF,uFAAuF;QACvF,yFAAyF;QACzF,+BAA+B;QAC/B,oFAAoF;QACpF,4FAA4F;QAC5F,2FAA2F;QAC3F,uFAAuF;QACvF,2FAA2F;QAC3F,wFAAwF;QACxF,yEAAyE;QACzE,2FAA2F;QAC3F,yCAAyC;QACzC,6FAA6F;QAC7F,wFAAwF;QACxF,4FAA4F;QAC5F,oEAAoE;QACpE,iGAAiG;QACjG,8FAA8F;QAC9F,8FAA8F;QAC9F,+FAA+F;QAC/F,4FAA4F;QAC5F,wEAAwE;QACxE,4FAA4F;QAC5F,wFAAwF;QACxF,mDAAmD;QACnD,kGAAkG;QAClG,oDAAoD;IACtD,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE;YACV,aAAa,EAAE;gBACb,IAAI,EAAE,QAAQ;gBACd,WAAW,EACT,8EAA8E;oBAC9E,qEAAqE;aACxE;SACF;QACD,QAAQ,EAAE,CAAC,eAAe,CAAC;QAC3B,qEAAqE;QACrE,6EAA6E;QAC7E,uBAAuB;QACvB,oBAAoB,EAAE,KAAK;KAC5B;IAED,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO;QACrB,MAAM,YAAY,GAAG,aAAa,CAAC,IAAI,CAAC,eAAe,CAAC,EAAE,eAAe,CAAC,CAAC;QAE3E,MAAM,GAAG,GAAG,2BAA2B,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAExD,MAAM,IAAI,GAAG,MAAM,aAAa,CAC9B,GAAG,EACH,wBAAwB,kBAAkB,CAAC,YAAY,CAAC,WAAW,EACnE,EAAE,EACF,OAAO,CAAC,KAAK,CACd,CAAC;QAEF,2EAA2E;QAC3E,2EAA2E;QAC3E,yEAAyE;QACzE,0DAA0D;QAC1D,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YACnC,UAAU,EAAE,IAAI;SACjB,CAAC;IACJ,CAAC;CACF,CAAC;AAEF,eAAe,qBAAqB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specguard-mcp",
3
- "version": "0.1.38",
3
+ "version": "0.1.39",
4
4
  "description": "MCP server exposing SpecGuard suite intelligence to AI coding agents",
5
5
  "license": "ISC",
6
6
  "type": "module",