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

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

torque/forge:torque/6013-sentinel-shared-01m23vygyffx5hg2f5vm8r74 → main 7 commits fork 1 thread · 1 unresolved

  • Merged. torque/6013-sentinel-shared-01m23vygyffx5hg2f5vm8r74 was merged into main as f7d1b0f.

Review

Sign in to review this change.
29 files changed +71 −60
docs/MCP.md +2
@@ -162,6 +162,8 @@ a failure names the bytes over and the longest descriptions.
162162 - A new door is an `action` or `kind` on the tool that already owns the
163163 resource before it is a new tool.
164164 - 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`.
165167 - The ceiling is the owner's: never raise it in a feature pull.
166168
167169 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(
9797 : PROTOCOLS[0],
9898 capabilities: { tools: {}, resources: {} },
9999 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)}`,
101101 });
102102 }
103103 case "ping":
@@ -183,7 +183,7 @@ function rpcOne(
183183 const REPORT_TOOL = {
184184 name: "report",
185185 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.",
187187 inputSchema: {
188188 type: "object",
189189 properties: {
src/http/mcp/tools/account_role.ts +2 −2
@@ -7,14 +7,14 @@ export const tool: Tool = {
77 name: "account_role",
88 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`)",
99 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`.",
1111 inputSchema: {
1212 type: "object",
1313 properties: {
1414 username: { type: "string" },
1515 role: { type: "string", enum: ["member", "guest"] },
1616 usdPerMonth: { type: ["number", "null"], description: "0.01–10000 or none." },
17 dryRun: { type: "boolean", description: "false to apply" },
17 dryRun: { type: "boolean" },
1818 },
1919 required: ["username", "role"],
2020 },
src/http/mcp/tools/archive.ts +1 −4
@@ -18,10 +18,7 @@ export const tool: Tool = {
1818 description: "owner/name list, one operation.",
1919 },
2020 archived: { type: "boolean" },
21 dryRun: {
22 type: "boolean",
23 description: "false to apply",
24 },
21 dryRun: { type: "boolean" },
2522 },
2623 },
2724 writes: true,
src/http/mcp/tools/asks_act.ts +1 −1
@@ -11,7 +11,7 @@ export const tool: Tool = {
1111 action: { type: "string", enum: ["dismiss", "decline", "mute", "ship", "close", "replan"] },
1212 items: { type: "array", items: { type: "string" }, maxItems: 100 },
1313 reason: { type: "string", maxLength: 300 },
14 dryRun: { type: "boolean", description: "false to apply" },
14 dryRun: { type: "boolean" },
1515 },
1616 required: ["action", "items"],
1717 },
src/http/mcp/tools/attach_look.ts +1 −4
@@ -18,10 +18,7 @@ export const tool: Tool = {
1818 type: "string",
1919 },
2020 image: { type: "string" },
21 dryRun: {
22 type: "boolean",
23 description: "false to apply",
24 },
21 dryRun: { type: "boolean" },
2522 },
2623 required: ["app", "pull", "width", "says", "image"],
2724 },
src/http/mcp/tools/capacity.ts +1 −1
@@ -15,7 +15,7 @@ export const tool: Tool = {
1515 enum: ["crew.concurrency", "checks.concurrency", "checks.shards", "cells.egress", "guard_new_accounts", "signin.edge"],
1616 },
1717 value: { type: "string", maxLength: 16 },
18 dryRun: { type: "boolean", description: "false to apply" },
18 dryRun: { type: "boolean" },
1919 },
2020 },
2121 writes: true,
src/http/mcp/tools/converge.ts +1 −1
@@ -8,7 +8,7 @@ export const tool: Tool = {
88 name: "converge",
99 doc: "the fork's crew merges where both sides changed the same files; comes back as a pull",
1010 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.",
1212 inputSchema: {
1313 type: "object",
1414 properties: { repo: REPO_ARG },
src/http/mcp/tools/decisions_by.ts +1 −4
@@ -13,10 +13,7 @@ export const tool: Tool = {
1313 properties: {
1414 repo: REPO_ARG,
1515 by: { type: "string", enum: ["me", "decider", "decider_tell"] },
16 dryRun: {
17 type: "boolean",
18 description: "false to apply",
19 },
16 dryRun: { type: "boolean" },
2017 },
2118 required: ["repo"],
2219 },
src/http/mcp/tools/forge_proposals.ts +1 −1
@@ -5,7 +5,7 @@ export const tool: Tool = {
55 name: "forge_proposals",
66 doc: "`{ repo }`: what this forge proposed to its parent, newest first; `reachable` is false if the parent did not answer",
77 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.",
99 inputSchema: {
1010 type: "object",
1111 properties: { repo: REPO_ARG },
src/http/mcp/tools/hold.ts +1 −4
@@ -19,10 +19,7 @@ export const tool: Tool = {
1919 release: {
2020 type: "boolean",
2121 },
22 dryRun: {
23 type: "boolean",
24 description: "false to apply",
25 },
22 dryRun: { type: "boolean" },
2623 },
2724 required: ["app", "pull"],
2825 },
src/http/mcp/tools/limb.ts +1 −4
@@ -35,10 +35,7 @@ export const tool: Tool = {
3535 operation: {
3636 type: "string",
3737 },
38 dryRun: {
39 type: "boolean",
40 description: "false to apply",
41 },
38 dryRun: { type: "boolean" },
4239 },
4340 required: ["app", "action"],
4441 },
src/http/mcp/tools/limits.ts +1 −1
@@ -12,7 +12,7 @@ export const tool: Tool = {
1212 properties: {
1313 key: { type: "string", enum: [...LIMIT_KEYS] },
1414 value: { type: "string", maxLength: 16 },
15 dryRun: { type: "boolean", description: "false to apply" },
15 dryRun: { type: "boolean" },
1616 },
1717 },
1818 writes: true,
src/http/mcp/tools/model_ceiling.ts +1 −1
@@ -13,7 +13,7 @@ export const tool: Tool = {
1313 username: { type: "string" },
1414 dailyUsd: { type: ["number", "null"], description: "0.01–10000; null clears." },
1515 perRunUsd: { type: ["number", "null"], description: "0.01–10000; null clears." },
16 dryRun: { type: "boolean", description: "false to apply" },
16 dryRun: { type: "boolean" },
1717 },
1818 required: ["username", "dailyUsd", "perRunUsd"],
1919 },
src/http/mcp/tools/model_grant.ts +1 −1
@@ -12,7 +12,7 @@ export const tool: Tool = {
1212 properties: {
1313 username: { type: "string" },
1414 usdPerMonth: { type: "number", description: "0.01–10000; 0 revokes." },
15 dryRun: { type: "boolean", description: "false to apply" },
15 dryRun: { type: "boolean" },
1616 },
1717 required: ["username", "usdPerMonth"],
1818 },
src/http/mcp/tools/next_steps.ts +1 −1
@@ -5,7 +5,7 @@ export const tool: Tool = {
55 name: "next_steps",
66 doc: "on a spawned forge, your first three steps and whether each is done; empty elsewhere or for anyone but its owner",
77 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.",
99 inputSchema: { type: "object", properties: {} },
1010 writes: false,
1111 async run(ctx, user) {
src/http/mcp/tools/outsiders.ts +1 −4
@@ -13,10 +13,7 @@ export const tool: Tool = {
1313 properties: {
1414 repo: REPO_ARG,
1515 on: { type: "boolean" },
16 dryRun: {
17 type: "boolean",
18 description: "false to apply",
19 },
16 dryRun: { type: "boolean" },
2017 },
2118 required: ["repo"],
2219 },
src/http/mcp/tools/pull.ts +1 −1
@@ -12,7 +12,7 @@ export const tool: Tool = {
1212 name: "pull",
1313 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",
1414 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`.",
1616 inputSchema: {
1717 type: "object",
1818 properties: {
src/http/mcp/tools/pulls.ts +1 −1
@@ -8,7 +8,7 @@ export const tool: Tool = {
88 name: "pulls",
99 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",
1010 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.",
1212 inputSchema: {
1313 type: "object",
1414 properties: {
src/http/mcp/tools/quotas.ts +1 −1
@@ -12,7 +12,7 @@ export const tool: Tool = {
1212 properties: {
1313 key: { type: "string", enum: [...QUOTA_KEYS] },
1414 value: { type: "string", maxLength: 16 },
15 dryRun: { type: "boolean", description: "false to apply" },
15 dryRun: { type: "boolean" },
1616 },
1717 },
1818 writes: true,
src/http/mcp/tools/runs.ts +1 −1
@@ -6,7 +6,7 @@ export const tool: Tool = {
66 name: "runs",
77 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",
88 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.",
1010 inputSchema: {
1111 type: "object",
1212 properties: {
src/http/mcp/tools/search.ts +1 −1
@@ -6,7 +6,7 @@ export const tool: Tool = {
66 name: "search",
77 doc: "`{ q, kind?, repo? }`: issues, pulls, repositories and code you can read, the same results as /search; `repo` keeps to one",
88 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.",
1010 inputSchema: {
1111 type: "object",
1212 properties: {
src/http/mcp/tools/signup.ts +1 −1
@@ -12,7 +12,7 @@ export const tool: Tool = {
1212 type: "object",
1313 properties: {
1414 mode: { type: "string", enum: [...SIGNUP_MODES] },
15 dryRun: { type: "boolean", description: "false to apply" },
15 dryRun: { type: "boolean" },
1616 },
1717 },
1818 writes: true,
src/http/mcp/tools/sizes.ts +1 −1
@@ -4,7 +4,7 @@ import { good, type Tool } from "../shared.ts";
44 export const tool: Tool = {
55 name: "sizes",
66 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.",
88 inputSchema: { type: "object", properties: {} },
99 writes: false,
1010 run: () => good(sizesView()),
src/http/mcp/tools/spawn.ts +2 −5
@@ -5,7 +5,7 @@ export const tool: Tool = {
55 name: "spawn",
66 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",
77 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.",
99 inputSchema: {
1010 type: "object",
1111 properties: {
@@ -45,10 +45,7 @@ export const tool: Tool = {
4545 description:
4646 "Stay an owner.",
4747 },
48 dryRun: {
49 type: "boolean",
50 description: "false to apply",
51 },
48 dryRun: { type: "boolean" },
5249 reveal: REVEAL_ARG,
5350 },
5451 required: ["name"],
src/http/mcp/tools/teams.ts +1 −1
@@ -13,7 +13,7 @@ export const tool: Tool = {
1313 charter: { type: "string", maxLength: 2000 },
1414 dial: { type: "integer", minimum: 0, maximum: 100 },
1515 members: { type: "array", maxItems: 500, items: { type: "string", maxLength: 64 } },
16 dryRun: { type: "boolean", description: "false to apply" },
16 dryRun: { type: "boolean" },
1717 },
1818 },
1919 writes: true,
src/http/mcp/tools/thought.ts +1 −1
@@ -6,7 +6,7 @@ export const tool: Tool = {
66 name: "thought",
77 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`",
88 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.",
1010 inputSchema: {
1111 type: "object",
1212 properties: { id: { type: "string", maxLength: 64 } },
src/http/mcp/tools/trail.ts +1 −1
@@ -15,7 +15,7 @@ export const tool: Tool = {
1515 properties: {
1616 repo: REPO_ARG,
1717 shown: { type: "boolean" },
18 dryRun: { type: "boolean", description: "false to apply" },
18 dryRun: { type: "boolean" },
1919 },
2020 required: ["repo"],
2121 },
test/mcp-size.test.ts +39 −9
@@ -22,9 +22,11 @@ const CEILING = 40_000;
2222 // Headroom: a new tool should fit without another round of cuts.
2323 // #4990 raised it to 37,500 to fit `threads`; the tool descriptions were cut
2424 // 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;

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_CEILING so the two can't drift apart again.

The ceiling is 37,000 now, but the test title at line 62 still says (37,150 bytes), and the 37,100 ratchet near line 97 is looser than this ceiling, so it can never fail. Bring both in line with 37,000.

2830 // One sentence a tool: past this, a description is a paragraph and belongs in
2931 // docs/MCP.md, which keeps the detail.
3032 const DESCRIPTION_CEILING = 120;
@@ -59,7 +61,7 @@ afterAll(async () => {
5961 await rm(dataDir, { recursive: true, force: true });
6062 });
6163
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 () => {
6365 const res = await fetch(`${servers.url}/mcp`, {
6466 method: "POST",
6567 headers: {
@@ -90,11 +92,6 @@ test("tools/list stays under 40 KB, with 3 KB of headroom (37,150 bytes)", async
9092 };
9193 for (const t of tools) walk(t.name, t.inputSchema, "");
9294 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 );
9895 // readOnlyHint defaults to false; spelling the default out on every write
9996 // tool cost 2 KB, so only a read tool carries annotations.
10097 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
120117 `tool descriptions over ${DESCRIPTION_CEILING} bytes`,
121118 ).toEqual([]);
122119 });
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 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs