import type { ToolDefinition } from "./types.js"; /** * `GET /api/v1/repository` as a tool — shipped today in the platform * (`specguard/config/routes.rb`, `Api::V1::RepositoriesController`). * * == Why this endpoint is the right second tool * * It is the agent-readable half of the repository dashboard, and its controller * says why it exists in the first line of its own comment: *without* it "an * agent can learn the suite's size only by running the suite and POSTing it — it * cannot ask". That is the cold-start requirement in Project Goals (SPGD-1) and * it is already met server-side; what was missing is a way for an agent to reach * it without writing HTTP and Bearer plumbing into its prompt. This tool is * exactly that gap and nothing more. * * One request answers what the suite is, what the last CI run cost, where the * time went (by file, by directory, by individual example), and how the suite * has grown — so the tool is described in those terms rather than as "get * repository", which is not a question anybody asks. * * == `repository` chooses WHICH REPOSITORY, and the credential changes with it * * Everything above answers about the one repository the configured `sgk_` key * RESOLVES TO — with five repositories in play, four of them are invisible to * every agent holding a single-repository key. `repository` is the ask that * fixes that: name one by its numeric id (the id `list_repositories` reports, * not the `org/repo` handle — the plural endpoint looks the id up inside the * calling credential's own read boundary) and the tool calls the PLURAL * endpoint, `GET /api/v1/repositories/:id`, which serves the same * `RepositoryOverview` body with every parameter below honoured exactly as the * singular one serves it — with ONE DELIBERATE OMISSION this branch owns: * `api_key` is ABSENT from the plural body, not null. That block describes the * credential that made the REQUEST, and the singular controller is the only * caller that has one to describe (`RepositoriesController#show` passes * `api_key_block: serialized_api_key`; `UserRepositoriesController#show` * passes none and says why at length: a block of nulls would be a sentence * about a credential that does not exist, because this request was made with * an agent or person key, and the repository's own `sgk_` keys are not the * caller's to describe). So the `api_key.last_used_at` paragraph below is * about the SINGULAR response; under `repository` the key is simply not there, * and its absence is the plural surface's shape — never this bridge dropping * a key. The two health blocks come from `RepositoryOverview` itself and DO * travel. The ladder moves; the subject moves; `api_key` does not. * * THE CREDENTIAL MOVES WITH IT, because the endpoints refuse each other's * tokens before any table is read: the singular path still authenticates with * `SPECGUARD_API_KEY` (`sgk_`, one repository), while the plural path answers * with EITHER member credential, whichever is set — `SPECGUARD_AGENT_API_KEY` * (`sga_`, the agent credential minted for exactly this: one key, many * repositories, each read bounded by the key's own granted set server-side) * preferred, and `SPECGUARD_USER_API_KEY` (`sgu_`, a person key whose * accessible set is the boundary ids are resolved inside instead) when no * agent key is set. With both set the agent key wins, so the plural answer * stays inside the same set `list_repositories` reports — scope consistency, * not preference. Omitting `repository` is byte-for-byte today's request, and * an operator who sets neither member variable never sees this half of the * tool exist — the blank-is-no-ask rule every argument here follows covers * `repository` too, so a blank value falls back to the singular path rather * than sending a guaranteed-404 id. * * == The response is passed through, not re-modelled * * Every figure in that body is annotated in the controller with the reason for * its exact shape, and several of those reasons are about honesty rather than * convenience: `null` where a value was not measured (never a zero that would * read as a measurement), counts served beside the figures they are the * denominator of, `tie_break_served: false` admitting the array's order is not * reproducible from the fields served. Any reshaping here — flattening, * defaulting a null to 0, re-sorting a list — would discard the distinction the * controller spent that care preserving. So the body goes back as it arrived. * * == `branch` narrows the history, and only the history * * That asymmetry is a documented property of the endpoint rather than a * surprise, and it is stated in the schema because an agent that has not read * the controller would otherwise read `latest_run` as belonging to the branch it * asked for. * * == `spec_directory` opens an area the ranking only names * * `latest_run.spec_directories` ranks the heaviest directories and is served * unconditionally, so an agent can already see WHERE the time went — but the * ranking is at the area grain and cannot say which files inside the area spent * it. `?spec_directory=` is the endpoint's answer to that, and the server has * served it since the controller took `RequestedSpecDirectoryParam`: the key * `latest_run.spec_directory_files` opens from `null` to a populated object the * moment the parameter is sent. This bridge withheld it by not offering the * parameter, which made the ranking a dead end for every agent that reached it * through MCP. * * The parameter is forwarded and nothing about it is interpreted here. The * server owns the whole meaning of the answer — `null` for "you did not ask", * `rows: []` for "asked, matched nothing" (a renamed or deleted directory is an * ordinary way to arrive, not an error), and a non-String shape read as no ask * at all. A blank one sends no parameter, exactly as `branch` does and for the * same reason: `getJson` omits an `undefined` value, so `optionalString` is the * whole of the blank-handling in both cases. * * == `spec_file` and `repeated_description` are the same argument, twice more * * At that point the endpoint served a FOUR-rung drill-down ladder and this * bridge forwarded two of them. The sentence above — "this bridge withheld it * by not offering the parameter, which made the ranking a dead end for every * agent that reached it through MCP" — was true verbatim of two further rungs, * and the block was doubled: `additionalProperties: false` REJECTED the * argument before the call, and `run()` would have dropped it anyway. Both * rankings are served unconditionally to every caller, so the agent was shown * the door and denied the handle: * * `latest_run.spec_files` → `spec_file_examples` (`spec_file`) * `latest_run.repeated_descriptions` → `repeated_description_examples` (`repeated_description`) * * Both follow `spec_directory` exactly — one schema property, one * `optionalString` call, one query key — because the pass-through above means * the drilled keys populate the moment the parameter is forwarded, with no * serializer or rendering work anywhere on this side. * * The fourth rung is worth naming separately: `repeated_description_examples` * is reachable from NO other key. `slowest_examples` is the run-wide top ten * and a group's members are usually absent from it entirely, and walking * `spec_file_examples` over each path in the group's `files_seen` is N * unrelated lists each cut at 50 BY DURATION, with no guarantee any of the * group's members survive the cut in any of them. Withholding this one * parameter withheld the capability, not merely a shortcut to it. * * == `unstable_test` is the fifth rung, and the same argument once more * * `unstable_tests.rows` → `unstable_tests.unstable_test_runs` (`unstable_test`) * * A flakiness row says `run_count: 30`, `failed_run_count: 4`, * `outcome_words: ["failed", "passed"]`. Those three figures are IDENTICAL for * two windows that call for opposite work: four failures in runs 27–30 is a * REGRESSION, and the work is to find the commit between run 26 and run 27; * four failures in runs 3, 11, 19 and 26 is genuine FLAKINESS, where there is * no culprit commit and the work is quarantine or shared state. The RUN * SEQUENCE is the only thing that separates them, and it is derivable from * nothing else this bridge returns — `history` has no per-test grain, and both * example drill-ins carry `outcome` for the LATEST RUN alone. So this is the * fourth rung's argument again: withholding the parameter withheld the * capability rather than a shortcut to it, and an agent told to "fix the flaky * tests" hunts nondeterminism in tests that fail deterministically. * * TWO THINGS DIFFER FROM ITS FOUR SIBLINGS, and both are stated in the schema * because neither is guessable from the ladder. The answer lands INSIDE the * flakiness block — `unstable_tests.unstable_test_runs`, not under * `latest_run.*` where the two example drill-ins live — and `branch` is a hard * PREREQUISITE rather than a suggestion: `unstable_tests` is served only for a * branch-narrowed window, so this parameter sent alone yields no block at all * to drill into. Every sibling works on a plain call; this one does not. * * THE SEQUENCE RUNS NEWEST RUN FIRST, and that is stated in the schema too, * because this is the one list on this tool where the direction IS the payload. * The window is `Repository#recent_test_runs`, ordered `created_at: :desc`, and * `SpecObservation.outcome_sequence_in` PRESERVES that order rather than * re-sorting it. Read front-to-back as run 1 → run N, the regression above * reads as four failures at the START of the window that have passed since — * a fixed flake, the exact inversion of the truth, with no error anywhere to * signal it. The consequence worth stating outright: the run a failure STARTED * at is the LAST row of the leading failed block, not the first. The 200 cap * takes rows off the OLD end for the same reason, so a truncated sequence is * still the recent runs. * * The run a row belongs to is read off its `commit_sha` / `test_run_id` and * NEVER off its index. `rows.length` is not the window's `run_count`: a run * that recorded nothing under the description contributes no row, and a * description carried by two examples in one run contributes two. Same * direction as `history` is not same index into it. * * == `commit_sha` is not a sixth rung — it moves the ladder * * The five parameters above narrow what is served ABOUT a run that * `Api::V1::RepositoriesController#latest_test_run` had already chosen. This * one CHOOSES THAT RUN, which the controller states in those terms: `?branch=` * asks about a SERIES, this asks WHICH RUN. It is read once, in that memo, so * every run-grain block moves together — `latest_run` and its rollups, the * five RUN-GRAIN drill-ins (`spec_directory_files`, `spec_file_examples`, * `repeated_description_examples`, `unannotated_examples` — the flag-style * rung documented below — and `slowest_examples`, which `layer` narrows), * `shards`, both growth windows and `previous_test_run`. * * That is five of the SIX drill-ins on this tool, and the excluded one is * worth naming because it is the composition an agent will actually try: * `unstable_test_runs` is read over the BRANCH WINDOW (`history_runs`), not off * the anchored run, so it does not move with this parameter. Sent together, * `?commit_sha=` and `?unstable_test=` answer about different things on * purpose — one run, and the window that run sits in. * * Withholding it here withheld a capability that the tool was ALREADY * DISCLOSING THE NEED FOR. `branch`'s own description names the failure: * "`latest_run` always names the repository's newest run, which on a busy repo * may be on another branch". And `unstable_test`'s teaches `commit_sha` as the * canonical run handle — read the run off each row's `commit_sha`, never off * its index — while every `unstable_test_runs` row carries one. The bridge * handed shas out and accepted none back, with `additionalProperties: false` * refusing the argument before a request was made, so the agent that most needs * it — one that edits tests, pushes, waits for CI and re-reads SpecGuard to * check its own work — could not work around it. On a repository where anything * else pushed in between, it was silently answered about another commit. * * `renderText` is the whole body verbatim, so this bridge has been SERVING the * `run_anchor` block since the API shipped it — with its only informative state * structurally unreachable. `requested_commit_sha` was always `nil` through the * bridge, so every MCP call read `source: "default"`, `resolved: true`. A * disclosure block cannot disclose a fallback to a client that cannot make the * ask that falls back. * * TWO THINGS ARE STATED IN THE SCHEMA because neither is guessable from the * ladder. `history` is NOT re-anchored — it stays the recent runs, narrowed * only by `branch` — so the `history[0] == latest_run` identity holds on a * default call and is NOT expected to hold under an explicit ask; that is the * contract, and a client needing the identity back omits the parameter. And an * unknown sha DOES NOT 404: a stale bookmark, a pruned run and a commit whose * CI never reported are ordinary ways to arrive, so the endpoint falls back to * the newest run and SAYS SO (`source: "requested"`, `resolved: false`, the raw * ask kept in `requested_commit_sha`, `commit_sha`/`branch` naming what was * actually served). Nothing else about the response looks unusual, which is why * the schema tells the agent to read `run_anchor.resolved`. * * == `unannotated_examples` is the one that is a FLAG rather than a name * * `latest_run.total_specs` − `annotated_specs` → `latest_run.unannotated_examples` * * The argument for forwarding it is the ladder's again — the parameter was * withheld by not being offered, `additionalProperties: false` refused it before * a request was made, and `renderText` has therefore been serving * `unannotated_examples: null` (the server's "you did not ask" spelling) to a * client structurally incapable of asking. What is new is WHOSE question it * answers. This is the adoption metric of Project Goals (SPGD-1): an agent told * to raise annotation coverage was served `annotated_ratio` and a `null`, so it * learned how far it had to go and could not name a single test to annotate. A * plain `curl` user could. * * ONE THING DIFFERS FROM ALL SIX SIBLINGS, and it is the reason this forward is * not a copy of the previous five. Every parameter above names a WHICH — which * branch, which commit, which area, which file, which description, which test — * because each opens the rows behind a LINE of a ranking the client had already * read. This one opens a POPULATION rather than a pick: the figure it drills out * of is a SUBTRACTION on the run itself, and a subtraction has no rows to have * keys, so there is nothing for the ask itself to NAME. So the server reads only * whether the parameter was NAMED, which `RequestedUnannotatedExamplesParam` * states outright — the value is not read, and THAT INCLUDES `false`: * `?unannotated_examples=false` opens the block exactly as `=true` does. * * That is a hazard on this side rather than a curiosity, and it is why the * argument is a BOOLEAN coerced with `optionalBoolean` and the query key is * built rather than stringified. `getJson` omits only `undefined`, so * `String(false)` would put `unannotated_examples=false` on the URL and open a * hundred-row block for the one caller who asked explicitly for it NOT to be * opened — the exact misreading the server's guard file exists to prevent, made * on the other side of the wire. The key is sent as `"true"` on an affirmative * ask and is `undefined` otherwise, so declining and omitting are the same wire * request. That matches how every other parameter here is declined: none of them * has an "off" value either. * * WHICH POPULATION IT OPENS IS NOT FIXED, and that is the half this file first * got wrong. `specguard` `55e3a09` made `?spec_file=` and `?spec_directory=` * narrow this block when either rides along with the flag — the same two * parameters that open their own drill-ins beside it — so the ask has FOUR * shapes rather than one: the whole run, one file, one area, or the AND of a * file and an area. The flag still names nothing, because the narrowing is named * by those two parameters and not by this one; what changed is that "the * population" is no longer a definite article. `SpecObservation.unannotated_in` * appends both predicates to the WHERE that the `COUNT(*) OVER ()` window of * `UNANNOTATED_POPULATION_COUNTS` rides, so `recorded_count` counts the NARROWED * population rather than the run's — and the controller echoes `spec_file` and * `spec_directory` back INSIDE the block, as the server read them and `null` * when not sent, for exactly that reason: `recorded_count` is the one figure * here a client reconciles against `total_specs - annotated_specs`, and a * silently narrowed count breaks that reconciliation. The echo is what makes the * count's population readable. * * ⭐ AND THE ONE ASK NOW OPENS TWO BLOCKS, THE SECOND OF WHICH IS A RANKING. * `specguard` `9df1b3d` added `latest_run.unannotated_directories` under this * SAME flag — no new parameter, no new value, nothing extra for a client to send * — so every call that already asks for the worklist is already being served the * map beside it. It answers what the worklist cannot: the worklist is WHICH * TESTS to go and annotate, and the map is WHERE THE DEBT IS, rolled up by area, * which is what a reader picks the next `?spec_directory=` narrowing FROM. That * is the same shape `spec_directory`'s own description states one parameter * over — one ask, several blocks, each in its own grain — so it is said here in * that form rather than in a new one. * * TWO CAPS UNDER ONE ASK, and the difference is the KIND of list rather than the * grain. `UNANNOTATED_EXAMPLES_LIMIT` is 100 and `UNANNOTATED_DIRECTORIES_LIMIT` * is 10, and the server's constant says why: the hundred caps a WORKLIST, sized * for a batch somebody opens, annotates and re-delivers in one sitting; the ten * caps a RANKING, which exists only to name where the debt is concentrated, and * a reader who cannot pick from ten areas is not helped by eighty. The ORDERS * differ for the same reason — the worklist is file-navigable, and the map is * `unannotated_count DESC, path ASC`: ranked by debt, with path as a tiebreak * only. A fully-annotated area is a REAL ROW here with `unannotated_count: 0` * against its real `recorded_count`, never an omission. Those rows sort last * COLLECTIVELY, so on a run with more areas than the cap they are cut and never * seen, but on a run inside the cap they ARE LISTED and listed is correct. So * `rows.size` is not a count of areas WITH debt — read each row's * `unannotated_count` for that; and `directory_count` counts EVERY area the run * touched, not every area with debt, and never `rows.size` either. * * ⭐ THE TWO KEYS OF THIS ONE BLOCK DISAGREE IN TWO PLACES, ON PURPOSE, and both * are counting traps rather than curiosities. `serialized_unannotated_directories` * discloses both at unusual length precisely because the machine-readable * consumer is the one that would otherwise discover them by arithmetic — and * this bridge IS that consumer. * * (a) SCOPE. `unannotated_examples.recorded_count` NARROWS with `?spec_file=` / * `?spec_directory=`; `unannotated_directories` stays WHOLE-RUN under both. So * under a narrowing the worklist's `recorded_count` is NOT the sum of the map's * `unannotated_count`s, AND NEITHER FIGURE IS WRONG: the first counts the one * area or file you named, the second ranks the whole run. The map is whole-run * BY DESIGN, because it is the thing a client picks a narrowing FROM and a map * that narrowed to the area you had already picked would answer nothing — one * row, echoing the parameter back. The sum is short of the run's total whenever * `directory_count > rows.size` besides, narrowing or no narrowing. This is why * the reconciliation rule above is scoped to the WORKLIST's count and to that * count alone. * * (b) NULL VERSUS EMPTY. On a run that recorded no per-example rows at all, with * the flag sent, `unannotated_examples` is a PRESENT block with `rows: []` and * `recorded_count: 0`, while `unannotated_directories` is `null`. That is not an * inconsistency to iron out. The sibling's zero is ambiguous by construction — * "fully annotated" and "recorded nothing at all" reach the same * `recorded_count: 0` there — and this key is how a client tells them apart: a * PRESENT map beside that zero means the run has a per-area grain and the zero * is the SUCCESS state; a `null` map means the run recorded nothing and the zero * is an ABSENCE of data. Serving `rows: []` here instead would spend a * distinction a client has no other way to make. * * THE `commit_sha` ROSTERS above and in README.md COUNT ONE KEY PER DRILL-IN PARAMETER (five, with `layer`), and * the reason is the roster's UNIT, not anything about this block's shape. That * roster carries ONE REPRESENTATIVE KEY PER DRILL-IN PARAMETER, not one entry * per response key: `spec_directory` opens THREE blocks (see its own * description below), yet only `spec_directory_files` is on the roster — * `directory_run_file_growth` and `directory_runtime_file_growth` are absent * from it for exactly this reason, and the guard in * `test/tools/repository-overview.test.ts` enforces it that way, deriving the * obligation from the schema's PARAMETERS and mapping each to the single key it * represents. `unannotated_directories` is a SECOND BLOCK OF AN EXISTING * PARAMETER'S ASK and adds no parameter, so it is not a roster entry. It is at * run grain and does re-anchor, and is covered there by "`latest_run` and its * rollups". * * FOUR THINGS ARE STATED IN THE SCHEMA. It is at RUN GRAIN, so it moves with * `commit_sha` like everything else under `latest_run` — unlike `unstable_test`, * which does not. A FULLY-ANNOTATED run answers `rows: []` / * `recorded_count: 0` with 200, never a 404 and never the no-ask `null`: that is * the state the metric exists to reach, so an agent walking a repository to * completion must see the block go empty rather than watch it vanish at the * moment it succeeded and be unable to tell that from its own parameter having * been dropped. The pair above — that `spec_file`/`spec_directory` narrow this * population when they ride along, and are echoed back so the client can tell * which population `recorded_count` is of. And the second block this one ask * opens, with its own cap, its own ranking order and both of the disagreements * above, because a pass-through `renderText` puts that key in front of every * agent whether or not anything here has named it. * * == `delivery_health` and `credential_health` are that rule applied to the * == blocks that say WHETHER TO BELIEVE THE REST * * The sentence directly above is the whole argument, and until now it was * unapplied at the top level of the very same body. `Api::V1::RepositoriesController` * serves both blocks UNCONDITIONALLY — it says "SERVED ON EVERY RESPONSE" in * capitals at both sites — so `renderText` has been handing them to every MCP * agent since they shipped, while this description enumerated the response in * exhaustive detail and named neither. * * NOTHING HERE OPENS THEM, which is exactly why nothing here caught the * omission. Every other block this file discusses arrived attached to a * parameter, and the roster guard in `test/tools/repository-overview.test.ts` * derives its obligation from `inputSchema.properties` — so a block that adds no * property is structurally invisible to it, as `unannotated_directories` was one * section up. The only other check on this string is a `length >= 80` floor. The * schema is UNTOUCHED by this change for that reason: the two are response * blocks, not asks, and a reader must not be able to infer a flag that does not * exist. * * WHAT THEY ANSWER IS "WHY IS THIS DATA LYING TO ME", which is the one question * an agent cannot answer from any other key here. `delivery_health` is the * staleness verdict — `refusing?`, `last_rejection_at`, and the endpoint's own * refusal reasons per retained delivery — and without it a `latest_run` that is * days old reads as a suite nobody ran rather than a suite the platform stopped * accepting. `credential_health` covers the one failure `delivery_health` * structurally cannot: a 401 resolves no repository and writes no * `IngestRejection` row, so an auth-broken pipeline leaves every rejection * figure at zero. It reports the state anyway because it need not observe the * 401 — it owns the key row and stamped the instant the token was retired. * * A QUIET ANSWER IS A POSITIVE FINDING, and that is stated outright rather than * left to be inferred, on the controller's own reasoning at both sites: an agent * that is served `refusing: false` must be able to tell "nothing was refused" * from "SpecGuard does not track that", and the difference is not visible in the * value. A human reads the dashboard panels for this; an agent reads only what * this string told it to look for. * * TWO FURTHER KEYS ARE NAMED HERE FOR THE SAME REASON, both found by taking the * membership question as a GREP over the endpoint's top-level keys rather than * as a reading of this file. `api_key.last_used_at` is the claim the two health * blocks exist to CORRECT — it is stamped on the way in, before the payload is * looked at, so a repository whose every delivery is refused serves its freshest * timestamp beside its stalest run, and the controller answers that with * `acceptance_reported_by` / `rotation_reported_by` naming the keys that answer * what it cannot. Naming the correction and not the claim would have been half a * sentence. (The claim itself is served by the SINGULAR body only — the plural * one under `repository` omits the whole `api_key` block, the deliberate * omission documented at the branch above — so the correction paragraph in the * description says which surface the instruction applies to rather than sending * a plural-path reader hunting for a key that is not there.) And the truncation * contract, which is NOT the uniform family it looks like from the * key names: only eight lists have a `*_window` sibling at all, MOST lists under `latest_run` * carry an inline `limit` beside `rows` instead, four of those windows serve no bound of their * own, `rejections_window` serves a bound, an order and a population (largest-bucket-first * `retained_clients` beside `retained_total` — both facts about the WINDOW its list is cut from, * never a restatement of the page), and the lists this census found carrying * no bound anywhere are `credential_health.keys` and BOTH `latest_run.shards` lists (`rows`, * ranked slowest-first off `TestRun#shard_durations`, and `per_shard`, in delivery order off * `#shard_reports`), each complete by construction. So the rule is stated in the direction that * stays true as the endpoint grows, and whose correctness does NOT depend on that list being * exhaustive: a bound BESIDE a list means a page, and no bound means the whole set. Quantifying * over the capped cases instead — "every ranking is capped, except..." — is what put a false * universal here twice, because the census that produced it counted lists that HAVE a bound and * never asked how many have none. It sends an agent looking for a disclosure that does not exist * and leaves it unable to tell "complete by construction" from "silently cut", which is the exact * misreading the two blocks above were named to prevent. * * == `suite_size_measured`, `shard_count` and `timed_shard_count` are that rule * == applied a FOURTH time, to the keys that say WHETHER TWO ROWS MAY BE * == DIFFERENCED AT ALL * * The same argument, the same blind spot, the same remedy — and the remaining * unapplied case. `serialized_history_row` in `Api::V1::RepositoriesController` * puts all three on EVERY `history[]` row, and `suite_size_measured` is served a * SECOND time on `latest_run`, deliberately from the same predicate so that a * single response body cannot describe one row twice and disagree with itself * (in the unfiltered window `history[0]` IS `latest_run`). Until now this string * named none of the three, while selling history differencing outright: the * `branch` parameter below tells an agent that consecutive all-branch rows "must * not be differenced", which teaches the differencing and names only the one * hazard that happens to be expressible as a parameter. * * THEY ARE ONE BLOCK BECAUSE THEY ARE ONE QUESTION. `suite_size_measured` says * whether a row is a measurement at all; `shard_count` is the denominator of * `total_specs` (a SUM over the shards RECORDED, and what `TestRun#assembled_like?` * reads to decide differenceability); `timed_shard_count` is the denominator of * `duration_seconds` (a MAX over the shards that REPORTED, whose absence lets a * client report the controller's "70% speedup produced entirely by telemetry * loss"). An agent that differences two rows without all three gets a number * wearing a SHA and a timestamp that make it read as a checked fact. * * THE LAST SENTENCE OF THE DESCRIPTION WAS ALSO WRONG IN ITS REACH, not merely * silent. "A null is 'not measured', never zero" routes a reader to NULLNESS as * the measured/not-measured signal, but `TestRun#suite_size_measured?` is * `total_specs_count.to_i.positive?` — so a run that reported zero tests serves a * NON-NULL `total_specs: 0` beside `suite_size_measured: false`. A reader obeying * the string's own stated rule reads that row as "measured, 0 tests" where the * server says "not a measurement". The sentence's true content about nulls is * kept; what is added is the bound, that the rule does not run backwards. The * controller serialized the boolean rather than leaving the client to re-derive * it from `total_specs` precisely so the two could not drift — and this bridge's * silence was forcing every MCP agent into exactly that re-derivation. * * THE SCHEMA IS UNTOUCHED, for the reason stated one section up: these are * RESPONSE keys, not asks, and a reader must not be able to infer a flag that * does not exist. And NO GUARD CAN CATCH A REGRESSION OF THIS CHANGE — every * roster guard in `test/tools/repository-overview.test.ts` opens with * `inputSchema.properties` and derives its obligation from a PARAMETER, so a * block that adds none is invisible to them by construction, and the only other * check on this string is a `length >= 80` floor. That is why the reasoning is * recorded here at this length: this comment is the only thing standing between * these three keys and a silent re-wording that drops them again. */ declare const getRepositoryOverview: ToolDefinition; export default getRepositoryOverview;