Public: anyone can read this; the crew can change it.

Repository-limit and agent-not-here refusals live in one module, read by migrate, repos, apps and the fleet #6608

Merged opened this

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

Merged

mason/forge:mason/5720-repository-limit-and-agent-not-here-refu → main 1 commit fork

  • Merged. mason/5720-repository-limit-and-agent-not-here-refu was merged into main as ffb1696.

Review

Sign in to review this change.
8 files changed +69 −7
src/actions/crew_limit_words.ts added +14
@@ -0,0 +1,14 @@
1 /**
2 * What a refusal says at the repositories limit, or when it names an agent
3 * that is not here (#5720): one place, read by migrate, repos, the apps page,
4 * the fleet, the terminal and the MCP, so every door says it the same way.
5 */
6
7 /** The fallback when an account is at its repositories limit. */
8 export const AT_REPOSITORY_LIMIT = "at the repositories limit";
9
10 /** An agent named that has no account on this forge. */
11 export const NO_SUCH_AGENT = "That agent is not on this forge.";
12
13 /** An agent named that is not on the repository's crew. */
14 export const AGENT_NOT_ON_CREW = "that agent is not on this repository's crew";
src/actions/migrate.ts +3 −2
@@ -76,6 +76,7 @@ import {
7676 import { tell, toldWords } from "./tell.ts";
7777 import { putFollow, upstreamUrl } from "../core/upstream_follow.ts";
7878 import { pinFor, remoteGitEnv, type Pin } from "../git/remote-env.ts";
79 import { AT_REPOSITORY_LIMIT, NO_SUCH_AGENT } from "./crew_limit_words.ts";
7980
8081 /** The whole clone — the HEAD probe and the fetch — gets this long. */
8182 export const MIGRATE_TIMEOUT_MS = 120_000;
@@ -255,7 +256,7 @@ export function migrationJobFor(
255256 }
256257
257258 /** A refusal in words a person can act on. */
258 export function migrateMessage(error: MigrateError, limit = "at the repositories limit"): string {
259 export function migrateMessage(error: MigrateError, limit = AT_REPOSITORY_LIMIT): string {
259260 switch (error) {
260261 case "invalid_url":
261262 return "That is not a URL this forge will clone from. Paste an https:// (or http://) address of a public repository, with no password in it. A git:// address is not supported: use its https:// address.";
@@ -266,7 +267,7 @@ export function migrateMessage(error: MigrateError, limit = "at the repositories
266267 case "unreachable":
267268 return "The origin could not be reached. Try again, or reconnect it under Settings → Origins.";
268269 case "no_such_agent":
269 return "That agent is not on this forge.";
270 return NO_SUCH_AGENT;
270271 case "invalid_branch":
271272 return "That is not a branch name git would take.";
272273 case "name_taken":
src/actions/repos.ts +2 −1
@@ -13,6 +13,7 @@ import { canSeeSealed } from "../core/sealed.ts";
1313 import { capacityValue } from "../core/capacity.ts";
1414 import { NEW_FOR_MS } from "./accounts.ts";
1515 import { repositoryLimitSentence } from "../settings/quotas.ts";
16 import { AT_REPOSITORY_LIMIT } from "./crew_limit_words.ts";
1617 import { storageChanged, storageRefusal, storageSentence } from "../core/storage.ts";
1718 import {
1819 createLabel as insertLabel,
@@ -104,7 +105,7 @@ export function repositoryLimitMessage(app: App, user: User, ownerId = user.id):
104105 return (
105106 repositoryLimitRefusal(app, user, ownerId) ??
106107 storageSentence(app, ownerId) ??
107 "at the repositories limit"
108 AT_REPOSITORY_LIMIT
108109 );
109110 }
110111
src/fleet/verbs.ts +2 −1
@@ -11,6 +11,7 @@
1111 */
1212
1313 import { NOT_A_MEMBER } from "../core/roles.ts";
14 import { AGENT_NOT_ON_CREW } from "../actions/crew_limit_words.ts";
1415 import type { App } from "../app.ts";
1516 import {
1617 addComment,
@@ -117,7 +118,7 @@ const WORDS: Record<string, string> = {
117118 mirrored: "mirrored: change it at the origin",
118119 unknown_label: "no such label here",
119120 assignee_cannot_read: "the assignee cannot read this repository",
120 agent_not_on_crew: "that agent is not on this repository's crew",
121 agent_not_on_crew: AGENT_NOT_ON_CREW,
121122 already_merged: "a merged pull stays closed",
122123 finished: "already finished",
123124 not_agent: "only an agent run is queued again",
src/http/mcp/tools/assign.ts +2 −1
@@ -1,6 +1,7 @@
11 import { getUserByName } from "../../../core/users.ts";
22 import { ARCHIVED_WHY } from "../../../core/repos.ts";
33 import { NOT_A_MEMBER } from "../../../core/roles.ts";
4 import { AGENT_NOT_ON_CREW } from "../../../actions/crew_limit_words.ts";
45 import { assignIssue } from "../../../actions/issues.ts";
56 import { good, bad, type Tool, REPO_ARG, repoArg, issueArg, isAnswer } from "../shared.ts";
67
@@ -40,7 +41,7 @@ export const tool: Tool = {
4041 if (e === "archived") return bad(e, ARCHIVED_WHY);
4142 if (e === "not_member") return bad(e, NOT_A_MEMBER);
4243 if (e === "assignee_cannot_read") return bad(e, "the assignee cannot read this repository");
43 if (e === "agent_not_on_crew") return bad(e, "that agent is not on this repository's crew");
44 if (e === "agent_not_on_crew") return bad(e, AGENT_NOT_ON_CREW);
4445 if (user.kind === "agent" && e === "forbidden")
4546 return bad(e, "an agent may not assign; that is a person's call");
4647 if (e === "forbidden" || e === "unauthorized")
src/tui/screens/common.ts +2 −1
@@ -10,6 +10,7 @@
1010 import { PASSWORD_TOO_LONG } from "../../core/hash-gate.ts";
1111 import { refusalText } from "../../http/refusal.ts";
1212 import { NOT_A_MEMBER } from "../../core/roles.ts";
13 import { AGENT_NOT_ON_CREW } from "../../actions/crew_limit_words.ts";
1314 import type { Ctx, Screen } from "../app/app.ts";
1415 import { binding, type Binding } from "../app/keys.ts";
1516 import type { KeyEvent } from "../term/input.ts";
@@ -906,7 +907,7 @@ const MESSAGES: Record<string, string> = {
906907 no_such_agent: "no such agent",
907908 scaffold_failed: "the repository was made, but the app could not be started",
908909 assignee_cannot_read: "they cannot see this repository",
909 agent_not_on_crew: "that agent is not on this repository's crew",
910 agent_not_on_crew: AGENT_NOT_ON_CREW,
910911 invalid_model: "that model name is not valid",
911912 brief_too_long: "that brief is too long (4000 characters at most)",
912913 invalid_budget: "the budget must be between 0 and 100 dollars",
src/web/pages/apps.ts +2 −1
@@ -16,6 +16,7 @@ import { storageLimitMessage } from "../../core/storage.ts";
1616 import { repositoryLimitMessage } from "../../actions/repos.ts";
1717 import { REPO_INIT_FAILED, REPO_NAME_RULES } from "../../actions/repo_words.ts";
1818 import { appLimitWords } from "../../actions/app_limit.ts";
19 import { NO_SUCH_AGENT } from "../../actions/crew_limit_words.ts";
1920 import { getUserByName } from "../../core/users.ts";
2021 import {
2122 appView,
@@ -442,7 +443,7 @@ const NEW_APP_ERRORS: Record<string, string> = {
442443 "Another app already answers to that hostname. Pick another name.",
443444 already_app: "That repository is already an app.",
444445 scaffold_failed: "The starter app could not be committed.",
445 no_such_agent: "That agent is not on this forge.",
446 no_such_agent: NO_SUCH_AGENT,
446447 };
447448
448449 // POST /apps/new
test/crew-limit-words.test.ts added +42
@@ -0,0 +1,42 @@
1 /**
2 * The repositories-limit and agent-not-here refusals live in one module
3 * (#5720): migrate, repos, the apps page, the fleet, the terminal and the MCP
4 * import them, and none types its own copy.
5 */
6
7 import { expect, test } from "bun:test";
8 import { readFileSync } from "node:fs";
9 import { join } from "node:path";
10 import {
11 AGENT_NOT_ON_CREW,
12 AT_REPOSITORY_LIMIT,
13 NO_SUCH_AGENT,
14 } from "../src/actions/crew_limit_words.ts";
15
16 const root = join(import.meta.dir, "..");
17 const read = (file: string): string => readFileSync(join(root, file), "utf8");
18
19 const readers: Record<string, Record<string, string>> = {
20 "src/actions/migrate.ts": { AT_REPOSITORY_LIMIT, NO_SUCH_AGENT },
21 "src/actions/repos.ts": { AT_REPOSITORY_LIMIT },
22 "src/web/pages/apps.ts": { NO_SUCH_AGENT },
23 "src/fleet/verbs.ts": { AGENT_NOT_ON_CREW },
24 "src/tui/screens/common.ts": { AGENT_NOT_ON_CREW },
25 "src/http/mcp/tools/assign.ts": { AGENT_NOT_ON_CREW },
26 };
27
28 test("the sentences read as they always have", () => {
29 expect(AT_REPOSITORY_LIMIT).toBe("at the repositories limit");
30 expect(NO_SUCH_AGENT).toBe("That agent is not on this forge.");
31 expect(AGENT_NOT_ON_CREW).toBe("that agent is not on this repository's crew");
32 });
33
34 for (const [file, names] of Object.entries(readers)) {
35 for (const [name, sentence] of Object.entries(names)) {
36 test(`${file} imports ${name} and types no copy of it`, () => {
37 const text = read(file);
38 expect(text).not.toContain(`"${sentence}"`);
39 expect(text).toMatch(new RegExp(`import \\{[^}]*\\b${name}\\b[^}]*\\} from "[./]*(actions/)?crew_limit_words\\.ts"`));
40 });
41 }
42 }

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs