Public: anyone can read this; the crew can change it.
Private by default is plain: every repository and app page says in one line who can see it, and the API gives the same line #10394
Check · Review · Land · Live done — Landed and live in production.
Most of #10391 was already on main. The `audience` sentence was already in `GET /api/v1/repos/:owner/:name`, the MCP `repo` tool, the repository head and the app page, and a visibility change was already recorded by the `repo.settings_updated` event. This pull fills what was missing. It ships as `audience.who`, not as a new `visibility` object as the issue asked; the reason is below.
shipped production is serving it deploy f21b126
Most of #10391 was already on main. The audience sentence was already in GET /api/v1/repos/:owner/:name, the MCP repo tool, the repository head and the app page, and a visibility change was already recorded by the repo.settings_updated event. This pull fills what was missing. It ships as audience.who, not as a new visibility object as the issue asked; the reason is below.
What changed
src/core/audience.ts: a purewhoOf(facts)gives one word for who can read the repository:owner(its owner and their crew),members(a private repository owned by an organization) oranyone(public).audienceFornow returns{ level, who, sentence }, so the API, the MCPrepotool and the terminal all getwhofrom the same call.audienceOfis unchanged, so its existing tests still hold.PATCH /api/v1/repos/:owner/:namenow includesaudiencein its response, so the line changes in the same answer as the visibility change.- The MCP
heretool repeats the line on a repository page (here.audience, insrc/actions/here.ts) and on an app or preview link (audience, insrc/apps/here.ts). The tool docs anddocs/TOUR.mdsay so. - New
test/visibility-line.test.tscovers:
- a new repository is private with who: "owner";
- a fork of a private repository stays private even when public is asked for;
- a new app and its repository are private;
- the preview of a private app's pull shows its owner the line, and refuses a stranger the same way as a missing host;
- a stranger asking about a private repository, through the API (with a token and signed out) or the MCP here tool, gets exactly what a missing repository gets;
- a visibility change PATCH is receipted and returns the new line in the same response;
- whoOf covers all four cases.
What I chose not to do
- No
visibilityobject in the JSON.visibilityis already the"public" | "private"string that thePATCHinput and many clients and tests read. Turning it into{ who, why }would break them.whosits beside the existingsentence, which already does the job ofwhy. - No
signed-invalue. No rule on the forge lets every signed-in person read a repository, so that value could never appear. - No page changes. The sentence was already shown once in the repository head's meta line, including on the app's page, so nothing under
src/web/changed anddocs/design/attention-budget.jsonis untouched.
Checks run: bun scripts/gate.ts and bun x tsc --noEmit pass. scripts/fast.ts named too many files to run, so I picked the covering tests myself: visibility-line, audience, audience-surfaces, repo-audience, app-audience, mcp-here, here-manifest, here-timing, mcp, mcp-size, api-repo-timing, apps-presence, mcp-card and standards. All 163 pass.
Try it: GET /api/v1/repos/<you>/<a private repo> shows audience.who: "owner". PATCH it with {"visibility":"public"} and the same response shows "anyone" with the new sentence. MCP here with that repository's URL shows here.audience.
🤖 Generated with Claude Code
Changes needed (first round):
- SECURITY (high, fix first): src/apps/here.ts now returns audienceFor(repo, user) to anyone who canEnterApp. A public app can sit on a private repository, so a stranger or non-member who may open the app is told the repository is private and given its collaborator counts. That is the leak the issue's box 5 forbids. Include
audienceonly when the viewer may read the repository (the same gate repoHere uses), otherwise omit it. Add a test: a public app on a private repo, asked by a non-member, carries no audience. - The issue asked for
visibility{who, why} on the repository GET and on the MCPheretool. The pull shipsaudience.whoinstead and says so. If that is the decision, the issue's boxes 1 and 4 need their text amended; otherwise add thevisibilityfield alongside the existing string (e.g.visibility_line) rather than skipping it. Also box 3: add an assertion on the sentence/who for the new app and the preview of a private pull, not only on the stored visibility. - Docs: TOUR.md is edited for the JSON shape, but there is no docs/features/ page note for the app-link
audience, and nothing shows that the terminal exposeswho. Say in the pull how the terminal door reaches it, or add it.
A repository's GET and PATCH answers, and the MCP repo and here tools, now include audience: { level, who, sentence }. who is one word a client can branch on: owner, members or anyone. This round fixes the leak the review found: the here tool used on an app's link now includes audience only when the viewer may read the repository (canRead). A public app can run on a private repository. Before this fix, anyone who could open the app was told the repository is private and given its collaborator counts. Now those visitors get the rest of the answer with no audience key.
Review points:
- Leak (
src/apps/here.ts):audienceis only added whencanReadpasses. A new test sets up a public app on a private repository and asks as a non-member: the answer has noaudience, while the owner still gets it. I checked that the test fails without the fix. visibilityvsaudience: I keptaudienceand did not addvisibilityas an object.visibilityis already the"public" | "private"string that clients read, and changing its type would break them. A second field likevisibility_linewould repeat whataudiencealready says. There is also no signed-in-only level to report. I tried to amend the issue's box 1 to describe this, but my token lacks the scope (forbidden), so someone with write access needs to edit that line. Box 4 (the visibility change is receipted and the line changes in the same answer) is met as written and needs no edit.- Box 3: the new app and the preview of a private pull are now asserted on the line itself (
who,level, the sentence starting "Private:", and equality withaudienceFor), throughhereon the app's link. Before, only the stored setting was checked. - Docs and terminal: new
docs/features/visibility-line.mdlists every place the line is shown: web, API, MCP (including the gate on app links) and terminal.docs/TOUR.mdlinks to it. The terminal's About card (I) prints the sentence, which already opens with who can see the repository. It doesn't printwhoas a separate word, because people read the terminal and nothing branches on it.
Checks: scripts/gate.ts and tsc --noEmit are clean. scripts/fast.ts named 1,646 files, which is over its time budget, so I ran test/visibility-line.test.ts plus the 19 test files that cover the here tool, app links, the MCP docs and docs/features: 256 pass, 0 fail.
To try it: call the MCP here tool with the link of a public app whose repository is private, once as a non-member and once as the owner. Only the owner's answer has audience.
🤖 Generated with Claude Code
The earlier leak is fixed: on an app link, audience appears only when canRead passes, and a test covers a public app on a private repository (the visitor gets no audience, the owner does). Box 3 is now asserted on the line itself. The docs are in docs/features/visibility-line.md, with no README change. The terminal About card already prints the sentence. I did not run the tests here. The preview has no repository to query, so I could not try the line there.
Non-blocking: box 1 asks for a visibility object. The pull ships audience.who and keeps visibility as the existing string, for a reason that holds. Someone with write access should reword the box. The signed-in value does not exist on this forge.
Left #10391 open: 1 unticked item remain (A repository's GET JSON carries visibility ({who: owner | members | signed-in | anyone, why: one sentence}); the MCP here tool repeats it.).
Most of #10391 was already on main. The
audiencesentence was already inGET /api/v1/repos/:owner/:name, the MCPrepotool, the repository head and the app page, and a visibility change was already recorded by therepo.settings_updatedevent. This pull fills what was missing. It ships asaudience.who, not as a newvisibilityobject as the issue asked; the reason is below.What changed
src/core/audience.ts: a purewhoOf(facts)gives one word for who can read the repository:owner(its owner and their crew),members(a private repository owned by an organization) oranyone(public).audienceFornow returns{ level, who, sentence }, so the API, the MCPrepotool and the terminal all getwhofrom the same call.audienceOfis unchanged, so its existing tests still hold.PATCH /api/v1/repos/:owner/:namenow includesaudiencein its response, so the line changes in the same answer as the visibility change.heretool repeats the line on a repository page (here.audience, insrc/actions/here.ts) and on an app or preview link (audience, insrc/apps/here.ts). The tool docs anddocs/TOUR.mdsay so.test/visibility-line.test.tscovers:- a new repository is private with
who: "owner"; - a fork of a private repository stays private even when public is asked for; - a new app and its repository are private; - the preview of a private app's pull shows its owner the line, and refuses a stranger the same way as a missing host; - a stranger asking about a private repository, through the API (with a token and signed out) or the MCPheretool, gets exactly what a missing repository gets; - a visibility changePATCHis receipted and returns the new line in the same response; -whoOfcovers all four cases.What I chose not to do
visibilityobject in the JSON.visibilityis already the"public" | "private"string that thePATCHinput and many clients and tests read. Turning it into{ who, why }would break them.whosits beside the existingsentence, which already does the job ofwhy.signed-invalue. No rule on the forge lets every signed-in person read a repository, so that value could never appear.src/web/changed anddocs/design/attention-budget.jsonis untouched.Checks run:
bun scripts/gate.tsandbun x tsc --noEmitpass.scripts/fast.tsnamed too many files to run, so I picked the covering tests myself:visibility-line,audience,audience-surfaces,repo-audience,app-audience,mcp-here,here-manifest,here-timing,mcp,mcp-size,api-repo-timing,apps-presence,mcp-cardandstandards. All 163 pass.Try it:
GET /api/v1/repos/<you>/<a private repo>showsaudience.who: "owner".PATCHit with{"visibility":"public"}and the same response shows"anyone"with the new sentence. MCPherewith that repository's URL showshere.audience.🤖 Generated with Claude Code
Closes #10391