Public: anyone can read this; the crew can change it.
Sentinel: shared:01m23vygyffx5hg2f5vm8r74sr:ky51wt3erj was degraded 156 minutes today: trevato/forge: "tools/list stays under 40 KB, with 3 KB of headroom (37,000 bytes)" failed on 3 pulls in the last hour … #6018
Check · Review · Land · Live done — Landed and live in production.
torque/forge:torque/6013-sentinel-shared-01m23vygyffx5hg2f5vm8r74 → main 7 commits fork 1 thread · 1 unresolved
29 files
changed +71
−60
docs/MCP.md +2
| @@ -162,6 +162,8 @@ a failure names the bytes over and the longest descriptions. | ||
| 162 | 162 | - A new door is an `action` or `kind` on the tool that already owns the |
| 163 | 163 | resource before it is a new tool. |
| 164 | 164 | - A new tool's description is one sentence. |
| 165 | - A `dryRun` argument is bare `{ type: "boolean" }`: the server's | |
| 166 | `instructions` say once that a tool with `dryRun` plans unless it is `false`. | |
| 165 | 167 | - The ceiling is the owner's: never raise it in a feature pull. |
| 166 | 168 | |
| 167 | 169 | Answers are JSON in a text block; most carry a `says` sentence short enough |
src/http/mcp.ts +2 −2
| @@ -97,7 +97,7 @@ function rpcOne( | ||
| 97 | 97 | : PROTOCOLS[0], |
| 98 | 98 | capabilities: { tools: {}, resources: {} }, |
| 99 | 99 | serverInfo: server(), |
| 100 | instructions: `This is a person's forge: their apps, and the crew that builds them. \`tell\` and \`say\` are how they direct it. Keep what you relay short — three sentences and the real options.${adminNote(ctx, user)}`, | |
| 100 | instructions: `This is a person's forge: their apps, and the crew that builds them. \`tell\` and \`say\` are how they direct it. Keep what you relay short — three sentences and the real options. A tool with \`dryRun\` plans unless it is \`false\`.${adminNote(ctx, user)}`, | |
| 101 | 101 | }); |
| 102 | 102 | } |
| 103 | 103 | case "ping": |
| @@ -183,7 +183,7 @@ function rpcOne( | ||
| 183 | 183 | const REPORT_TOOL = { |
| 184 | 184 | name: "report", |
| 185 | 185 | description: |
| 186 | "Report a problem in a public repository, without an account. Plain text; a person reads it first.", | |
| 186 | "Report a problem in a public repository, no account needed. Plain text; a person reads it first.", | |
| 187 | 187 | inputSchema: { |
| 188 | 188 | type: "object", |
| 189 | 189 | properties: { |
src/http/mcp/tools/account_role.ts +2 −2
| @@ -7,14 +7,14 @@ export const tool: Tool = { | ||
| 7 | 7 | name: "account_role", |
| 8 | 8 | doc: "`{ username, role, usdPerMonth?, dryRun? }`: make a person a `member`, or a `guest` again, with an optional starter allowance (`usdPerMonth`, 0.01 to 10000, promoting only); planned first (`dryRun` is true unless `false`)", |
| 9 | 9 | description: |
| 10 | "Make a person a member, or a guest again; promoting may give a monthly `usdPerMonth` allowance.", | |
| 10 | "Make a person a member, or a guest again; promoting may grant a monthly `usdPerMonth`.", | |
| 11 | 11 | inputSchema: { |
| 12 | 12 | type: "object", |
| 13 | 13 | properties: { |
| 14 | 14 | username: { type: "string" }, |
| 15 | 15 | role: { type: "string", enum: ["member", "guest"] }, |
| 16 | 16 | usdPerMonth: { type: ["number", "null"], description: "0.01–10000 or none." }, |
| 17 | dryRun: { type: "boolean", description: "false to apply" }, | |
| 17 | dryRun: { type: "boolean" }, | |
| 18 | 18 | }, |
| 19 | 19 | required: ["username", "role"], |
| 20 | 20 | }, |
src/http/mcp/tools/archive.ts +1 −4
| @@ -18,10 +18,7 @@ export const tool: Tool = { | ||
| 18 | 18 | description: "owner/name list, one operation.", |
| 19 | 19 | }, |
| 20 | 20 | archived: { type: "boolean" }, |
| 21 | dryRun: { | |
| 22 | type: "boolean", | |
| 23 | description: "false to apply", | |
| 24 | }, | |
| 21 | dryRun: { type: "boolean" }, | |
| 25 | 22 | }, |
| 26 | 23 | }, |
| 27 | 24 | writes: true, |
src/http/mcp/tools/asks_act.ts +1 −1
| @@ -11,7 +11,7 @@ export const tool: Tool = { | ||
| 11 | 11 | action: { type: "string", enum: ["dismiss", "decline", "mute", "ship", "close", "replan"] }, |
| 12 | 12 | items: { type: "array", items: { type: "string" }, maxItems: 100 }, |
| 13 | 13 | reason: { type: "string", maxLength: 300 }, |
| 14 | dryRun: { type: "boolean", description: "false to apply" }, | |
| 14 | dryRun: { type: "boolean" }, | |
| 15 | 15 | }, |
| 16 | 16 | required: ["action", "items"], |
| 17 | 17 | }, |
src/http/mcp/tools/attach_look.ts +1 −4
| @@ -18,10 +18,7 @@ export const tool: Tool = { | ||
| 18 | 18 | type: "string", |
| 19 | 19 | }, |
| 20 | 20 | image: { type: "string" }, |
| 21 | dryRun: { | |
| 22 | type: "boolean", | |
| 23 | description: "false to apply", | |
| 24 | }, | |
| 21 | dryRun: { type: "boolean" }, | |
| 25 | 22 | }, |
| 26 | 23 | required: ["app", "pull", "width", "says", "image"], |
| 27 | 24 | }, |
src/http/mcp/tools/capacity.ts +1 −1
| @@ -15,7 +15,7 @@ export const tool: Tool = { | ||
| 15 | 15 | enum: ["crew.concurrency", "checks.concurrency", "checks.shards", "cells.egress", "guard_new_accounts", "signin.edge"], |
| 16 | 16 | }, |
| 17 | 17 | value: { type: "string", maxLength: 16 }, |
| 18 | dryRun: { type: "boolean", description: "false to apply" }, | |
| 18 | dryRun: { type: "boolean" }, | |
| 19 | 19 | }, |
| 20 | 20 | }, |
| 21 | 21 | writes: true, |
src/http/mcp/tools/converge.ts +1 −1
| @@ -8,7 +8,7 @@ export const tool: Tool = { | ||
| 8 | 8 | name: "converge", |
| 9 | 9 | doc: "the fork's crew merges where both sides changed the same files; comes back as a pull", |
| 10 | 10 | description: |
| 11 | "A fork's crew merges the parent's updates where both changed the same files, as a pull.", | |
| 11 | "Have a fork's crew merge the parent's updates where both changed a file, as a pull.", | |
| 12 | 12 | inputSchema: { |
| 13 | 13 | type: "object", |
| 14 | 14 | properties: { repo: REPO_ARG }, |
src/http/mcp/tools/decisions_by.ts +1 −4
| @@ -13,10 +13,7 @@ export const tool: Tool = { | ||
| 13 | 13 | properties: { |
| 14 | 14 | repo: REPO_ARG, |
| 15 | 15 | by: { type: "string", enum: ["me", "decider", "decider_tell"] }, |
| 16 | dryRun: { | |
| 17 | type: "boolean", | |
| 18 | description: "false to apply", | |
| 19 | }, | |
| 16 | dryRun: { type: "boolean" }, | |
| 20 | 17 | }, |
| 21 | 18 | required: ["repo"], |
| 22 | 19 | }, |
src/http/mcp/tools/forge_proposals.ts +1 −1
| @@ -5,7 +5,7 @@ export const tool: Tool = { | ||
| 5 | 5 | name: "forge_proposals", |
| 6 | 6 | doc: "`{ repo }`: what this forge proposed to its parent, newest first; `reachable` is false if the parent did not answer", |
| 7 | 7 | description: |
| 8 | "What this forge proposed to its parent, newest first; `reachable` false if it did not answer.", | |
| 8 | "What this forge proposed to its parent, newest first; `reachable` false if it is silent.", | |
| 9 | 9 | inputSchema: { |
| 10 | 10 | type: "object", |
| 11 | 11 | properties: { repo: REPO_ARG }, |
src/http/mcp/tools/hold.ts +1 −4
| @@ -19,10 +19,7 @@ export const tool: Tool = { | ||
| 19 | 19 | release: { |
| 20 | 20 | type: "boolean", |
| 21 | 21 | }, |
| 22 | dryRun: { | |
| 23 | type: "boolean", | |
| 24 | description: "false to apply", | |
| 25 | }, | |
| 22 | dryRun: { type: "boolean" }, | |
| 26 | 23 | }, |
| 27 | 24 | required: ["app", "pull"], |
| 28 | 25 | }, |
src/http/mcp/tools/limb.ts +1 −4
| @@ -35,10 +35,7 @@ export const tool: Tool = { | ||
| 35 | 35 | operation: { |
| 36 | 36 | type: "string", |
| 37 | 37 | }, |
| 38 | dryRun: { | |
| 39 | type: "boolean", | |
| 40 | description: "false to apply", | |
| 41 | }, | |
| 38 | dryRun: { type: "boolean" }, | |
| 42 | 39 | }, |
| 43 | 40 | required: ["app", "action"], |
| 44 | 41 | }, |
src/http/mcp/tools/limits.ts +1 −1
| @@ -12,7 +12,7 @@ export const tool: Tool = { | ||
| 12 | 12 | properties: { |
| 13 | 13 | key: { type: "string", enum: [...LIMIT_KEYS] }, |
| 14 | 14 | value: { type: "string", maxLength: 16 }, |
| 15 | dryRun: { type: "boolean", description: "false to apply" }, | |
| 15 | dryRun: { type: "boolean" }, | |
| 16 | 16 | }, |
| 17 | 17 | }, |
| 18 | 18 | writes: true, |
src/http/mcp/tools/model_ceiling.ts +1 −1
| @@ -13,7 +13,7 @@ export const tool: Tool = { | ||
| 13 | 13 | username: { type: "string" }, |
| 14 | 14 | dailyUsd: { type: ["number", "null"], description: "0.01–10000; null clears." }, |
| 15 | 15 | perRunUsd: { type: ["number", "null"], description: "0.01–10000; null clears." }, |
| 16 | dryRun: { type: "boolean", description: "false to apply" }, | |
| 16 | dryRun: { type: "boolean" }, | |
| 17 | 17 | }, |
| 18 | 18 | required: ["username", "dailyUsd", "perRunUsd"], |
| 19 | 19 | }, |
src/http/mcp/tools/model_grant.ts +1 −1
| @@ -12,7 +12,7 @@ export const tool: Tool = { | ||
| 12 | 12 | properties: { |
| 13 | 13 | username: { type: "string" }, |
| 14 | 14 | usdPerMonth: { type: "number", description: "0.01–10000; 0 revokes." }, |
| 15 | dryRun: { type: "boolean", description: "false to apply" }, | |
| 15 | dryRun: { type: "boolean" }, | |
| 16 | 16 | }, |
| 17 | 17 | required: ["username", "usdPerMonth"], |
| 18 | 18 | }, |
src/http/mcp/tools/next_steps.ts +1 −1
| @@ -5,7 +5,7 @@ export const tool: Tool = { | ||
| 5 | 5 | name: "next_steps", |
| 6 | 6 | doc: "on a spawned forge, your first three steps and whether each is done; empty elsewhere or for anyone but its owner", |
| 7 | 7 | description: |
| 8 | "On a spawned forge, its owner's first three steps and whether each is done. Empty elsewhere.", | |
| 8 | "On a spawned forge, its owner's first three steps, each done or not. Empty elsewhere.", | |
| 9 | 9 | inputSchema: { type: "object", properties: {} }, |
| 10 | 10 | writes: false, |
| 11 | 11 | async run(ctx, user) { |
src/http/mcp/tools/outsiders.ts +1 −4
| @@ -13,10 +13,7 @@ export const tool: Tool = { | ||
| 13 | 13 | properties: { |
| 14 | 14 | repo: REPO_ARG, |
| 15 | 15 | on: { type: "boolean" }, |
| 16 | dryRun: { | |
| 17 | type: "boolean", | |
| 18 | description: "false to apply", | |
| 19 | }, | |
| 16 | dryRun: { type: "boolean" }, | |
| 20 | 17 | }, |
| 21 | 18 | required: ["repo"], |
| 22 | 19 | }, |
src/http/mcp/tools/pull.ts +1 −1
| @@ -12,7 +12,7 @@ export const tool: Tool = { | ||
| 12 | 12 | name: "pull", |
| 13 | 13 | doc: "`{ repo, number }`: a pull's title, state, head and base, and for an open pull whether it merges into the base's current head (`mergeable`) and the paths that stop it (`conflicts`), and `check: { state, baseOid }`: the latest check's state and the base tip it was merged with (empty when it merged nothing); `mergeable` and `conflicts` are the same fields as `GET …/pulls/:number`, `check` is `state` and `run.baseOid` from `GET …/pulls/:number/checks`; draws the `ui://forge/pull` card: `#N title`, state with check and conflicts while open, `head → base` linked to the pull, and the preview link when the pull has an environment", |
| 14 | 14 | description: |
| 15 | "A pull: title, state, head, base; open: `mergeable`, `conflicts`, `proposal`, `check`.", | |
| 15 | "A pull: title, state, head, base; open: `mergeable`, `conflicts`, `proposal`, `checkStale`, `check`.", | |
| 16 | 16 | inputSchema: { |
| 17 | 17 | type: "object", |
| 18 | 18 | properties: { |
src/http/mcp/tools/pulls.ts +1 −1
| @@ -8,7 +8,7 @@ export const tool: Tool = { | ||
| 8 | 8 | name: "pulls", |
| 9 | 9 | doc: "`{ repo, state?, limit? }`: a repository's pulls, newest first, one brief line each: `number`, `title`, `author`, `head`, `check` (state), `approvals`, `changesRequested`, `mergeable` (null once closed); `state` open (default) or closed, `limit` 1-50 (default 20); no bodies, no diffs", |
| 10 | 10 | description: |
| 11 | "A repository's pulls, newest first. `state` open (default) or closed; `limit` 1-50 (default 20).", | |
| 11 | "A repository's pulls, newest first. `state` open (default) or closed; `limit` 1-50.", | |
| 12 | 12 | inputSchema: { |
| 13 | 13 | type: "object", |
| 14 | 14 | properties: { |
src/http/mcp/tools/quotas.ts +1 −1
| @@ -12,7 +12,7 @@ export const tool: Tool = { | ||
| 12 | 12 | properties: { |
| 13 | 13 | key: { type: "string", enum: [...QUOTA_KEYS] }, |
| 14 | 14 | value: { type: "string", maxLength: 16 }, |
| 15 | dryRun: { type: "boolean", description: "false to apply" }, | |
| 15 | dryRun: { type: "boolean" }, | |
| 16 | 16 | }, |
| 17 | 17 | }, |
| 18 | 18 | writes: true, |
src/http/mcp/tools/runs.ts +1 −1
| @@ -6,7 +6,7 @@ export const tool: Tool = { | ||
| 6 | 6 | name: "runs", |
| 7 | 7 | doc: "`{ repo, limit? }`: the repository's recent runs, newest first (id, kind, state, issue or pull number, started, ended), 1–50, default 20; read as the runs page reads them", |
| 8 | 8 | description: |
| 9 | "A repository's runs, newest first: id, kind, state, the issue or pull number, started and ended.", | |
| 9 | "A repository's runs, newest first: id, kind, state, issue or pull, started and ended.", | |
| 10 | 10 | inputSchema: { |
| 11 | 11 | type: "object", |
| 12 | 12 | properties: { |
src/http/mcp/tools/search.ts +1 −1
| @@ -6,7 +6,7 @@ export const tool: Tool = { | ||
| 6 | 6 | name: "search", |
| 7 | 7 | doc: "`{ q, kind?, repo? }`: issues, pulls, repositories and code you can read, the same results as /search; `repo` keeps to one", |
| 8 | 8 | description: |
| 9 | "Find issues, pulls, repositories and code you can read; `kind` narrows, `repo` keeps to one.", | |
| 9 | "Find issues, pulls, repositories and code you can read; `kind` and `repo` narrow.", | |
| 10 | 10 | inputSchema: { |
| 11 | 11 | type: "object", |
| 12 | 12 | properties: { |
src/http/mcp/tools/signup.ts +1 −1
| @@ -12,7 +12,7 @@ export const tool: Tool = { | ||
| 12 | 12 | type: "object", |
| 13 | 13 | properties: { |
| 14 | 14 | mode: { type: "string", enum: [...SIGNUP_MODES] }, |
| 15 | dryRun: { type: "boolean", description: "false to apply" }, | |
| 15 | dryRun: { type: "boolean" }, | |
| 16 | 16 | }, |
| 17 | 17 | }, |
| 18 | 18 | writes: true, |
src/http/mcp/tools/sizes.ts +1 −1
| @@ -4,7 +4,7 @@ import { good, type Tool } from "../shared.ts"; | ||
| 4 | 4 | export const tool: Tool = { |
| 5 | 5 | name: "sizes", |
| 6 | 6 | description: |
| 7 | "What a child forge's sizes mean: small, medium and large, each with CPU, memory and disk.", | |
| 7 | "A child forge's sizes: small, medium and large, each with CPU, memory and disk.", | |
| 8 | 8 | inputSchema: { type: "object", properties: {} }, |
| 9 | 9 | writes: false, |
| 10 | 10 | run: () => good(sizesView()), |
src/http/mcp/tools/spawn.ts +2 −5
| @@ -5,7 +5,7 @@ export const tool: Tool = { | ||
| 5 | 5 | name: "spawn", |
| 6 | 6 | doc: "`{ name, apps?, crew?, dryRun? }`: a child forge, planned first (`dryRun` is true unless `false`); an admin's, and an `ro` token may plan but not make", |
| 7 | 7 | description: |
| 8 | "Spawn a child forge with the named apps and the crew's briefs. Plans first (dryRun false spawns).", | |
| 8 | "Spawn a child forge with the named apps and the crew's briefs. Plans first.", | |
| 9 | 9 | inputSchema: { |
| 10 | 10 | type: "object", |
| 11 | 11 | properties: { |
| @@ -45,10 +45,7 @@ export const tool: Tool = { | ||
| 45 | 45 | description: |
| 46 | 46 | "Stay an owner.", |
| 47 | 47 | }, |
| 48 | dryRun: { | |
| 49 | type: "boolean", | |
| 50 | description: "false to apply", | |
| 51 | }, | |
| 48 | dryRun: { type: "boolean" }, | |
| 52 | 49 | reveal: REVEAL_ARG, |
| 53 | 50 | }, |
| 54 | 51 | required: ["name"], |
src/http/mcp/tools/teams.ts +1 −1
| @@ -13,7 +13,7 @@ export const tool: Tool = { | ||
| 13 | 13 | charter: { type: "string", maxLength: 2000 }, |
| 14 | 14 | dial: { type: "integer", minimum: 0, maximum: 100 }, |
| 15 | 15 | members: { type: "array", maxItems: 500, items: { type: "string", maxLength: 64 } }, |
| 16 | dryRun: { type: "boolean", description: "false to apply" }, | |
| 16 | dryRun: { type: "boolean" }, | |
| 17 | 17 | }, |
| 18 | 18 | }, |
| 19 | 19 | writes: true, |
src/http/mcp/tools/thought.ts +1 −1
| @@ -6,7 +6,7 @@ export const tool: Tool = { | ||
| 6 | 6 | name: "thought", |
| 7 | 7 | doc: "`{ id }`: where one of your thoughts went (its id comes from `tell` without a repo): the placement, and the earlier thoughts it repeats or takes back and the later ones that did the same; not yours and not there answer alike, `not_found`", |
| 8 | 8 | description: |
| 9 | "Where one of your thoughts went: its id (from `tell` without a repo) answers placement and reply.", | |
| 9 | "Where a thought went: its id (from `tell` without a repo) answers placement and reply.", | |
| 10 | 10 | inputSchema: { |
| 11 | 11 | type: "object", |
| 12 | 12 | properties: { id: { type: "string", maxLength: 64 } }, |
src/http/mcp/tools/trail.ts +1 −1
| @@ -15,7 +15,7 @@ export const tool: Tool = { | ||
| 15 | 15 | properties: { |
| 16 | 16 | repo: REPO_ARG, |
| 17 | 17 | shown: { type: "boolean" }, |
| 18 | dryRun: { type: "boolean", description: "false to apply" }, | |
| 18 | dryRun: { type: "boolean" }, | |
| 19 | 19 | }, |
| 20 | 20 | required: ["repo"], |
| 21 | 21 | }, |
test/mcp-size.test.ts +39 −9
| @@ -22,9 +22,11 @@ const CEILING = 40_000; | ||
| 22 | 22 | // Headroom: a new tool should fit without another round of cuts. |
| 23 | 23 | // #4990 raised it to 37,500 to fit `threads`; the tool descriptions were cut |
| 24 | 24 | // instead (#5105), so it is 37,000 again: raising it is how the lane went red. |
| 25 | // #5433 raised it to 37,150 for `restore_drill` (124 bytes; main had 3 spare), | |
| 26 | // without trimming the safety wording of other tools' descriptions. | |
| 27 | const HEADROOM_CEILING = 37_150; | |
| 25 | // #5433 raised it to 37,150 for `restore_drill` (124 bytes; main had 3 spare). | |
| 26 | // At 36,741 bytes with 259 left, every pull adding a tool went red (#6013); | |
| 27 | // `dryRun`'s 17 descriptions moved to the instructions and the longest tool | |
| 28 | // descriptions were cut, so it is back at 37,000 with room for the next tool. | |
| 29 | const HEADROOM_CEILING = 37_000; | |
The ceiling is 37,000 now, but the test title at line 62 still says |
||
| 28 | 30 | // One sentence a tool: past this, a description is a paragraph and belongs in |
| 29 | 31 | // docs/MCP.md, which keeps the detail. |
| 30 | 32 | const DESCRIPTION_CEILING = 120; |
| @@ -59,7 +61,7 @@ afterAll(async () => { | ||
| 59 | 61 | await rm(dataDir, { recursive: true, force: true }); |
| 60 | 62 | }); |
| 61 | 63 | |
| 62 | test("tools/list stays under 40 KB, with 3 KB of headroom (37,150 bytes)", async () => { | |
| 64 | test(`tools/list stays under 40 KB, with 3 KB of headroom (${HEADROOM_CEILING.toLocaleString("en-US")} bytes)`, async () => { | |
| 63 | 65 | const res = await fetch(`${servers.url}/mcp`, { |
| 64 | 66 | method: "POST", |
| 65 | 67 | headers: { |
| @@ -90,11 +92,6 @@ test("tools/list stays under 40 KB, with 3 KB of headroom (37,150 bytes)", async | ||
| 90 | 92 | }; |
| 91 | 93 | for (const t of tools) walk(t.name, t.inputSchema, ""); |
| 92 | 94 | expect(long, `property descriptions over ${PROPERTY_MAX} bytes`).toEqual([]); |
| 93 | expect(bytes, "the argument descriptions were cut by 1,000 bytes").toBeLessThanOrEqual( | |
| 94 | // 36,700 before `ssh_keys` (#4795), which costs about 210 bytes. | |
| 95 | // 37,100 with `restore_drill` (#5433), 124 bytes. | |
| 96 | 37_100, | |
| 97 | ); | |
| 98 | 95 | // readOnlyHint defaults to false; spelling the default out on every write |
| 99 | 96 | // tool cost 2 KB, so only a read tool carries annotations. |
| 100 | 97 | expect(tools.filter((t) => t.annotations?.readOnlyHint === false).map((t) => t.name)).toEqual([]); |
| @@ -120,3 +117,36 @@ test("tools/list stays under 40 KB, with 3 KB of headroom (37,150 bytes)", async | ||
| 120 | 117 | `tool descriptions over ${DESCRIPTION_CEILING} bytes`, |
| 121 | 118 | ).toEqual([]); |
| 122 | 119 | }); |
| 120 | ||
| 121 | test("`dryRun` is explained once, in the instructions, not on every tool", async () => { | |
| 122 | const call = async (method: string) => | |
| 123 | (await ( | |
| 124 | await fetch(`${servers.url}/mcp`, { | |
| 125 | method: "POST", | |
| 126 | headers: { | |
| 127 | "content-type": "application/json", | |
| 128 | accept: "application/json, text/event-stream", | |
| 129 | authorization: `Bearer ${token}`, | |
| 130 | }, | |
| 131 | body: JSON.stringify({ | |
| 132 | jsonrpc: "2.0", | |
| 133 | id: 1, | |
| 134 | method, | |
| 135 | params: { protocolVersion: "2025-06-18" }, | |
| 136 | }), | |
| 137 | }) | |
| 138 | ).json()) as any; | |
| 139 | const tools = (await call("tools/list")).result.tools as { | |
| 140 | name: string; | |
| 141 | inputSchema: { properties?: Record<string, { description?: string }> }; | |
| 142 | }[]; | |
| 143 | const planners = tools.filter((t) => t.inputSchema.properties?.dryRun); | |
| 144 | expect(planners.length).toBeGreaterThan(10); | |
| 145 | // Seventeen copies of "false to apply" were 527 bytes of tools/list. | |
| 146 | expect( | |
| 147 | planners.filter((t) => t.inputSchema.properties!.dryRun!.description).map((t) => t.name), | |
| 148 | ).toEqual([]); | |
| 149 | expect((await call("initialize")).result.instructions).toContain( | |
| 150 | "A tool with `dryRun` plans unless it is `false`.", | |
| 151 | ); | |
| 152 | }); |
new above
offline
Low: the ceiling is back at 37,000, but the test that enforces it (line 62) is still titled "(37,150 bytes)". Sentinel failure issues quote that title, so rename it to "(37,000 bytes)" or build the title from
HEADROOM_CEILINGso the two can't drift apart again.