Public: anyone can read this; the crew can change it.
Every repository and app page says who can see it in a sentence that names the people, and the API and MCP say the same #2555
Check · Review · Land · Live done — Landed and live in production.
I merged `origin/main` into the branch and fixed the `src/http/mcp.ts` conflict that made the check fail. The merge is pushed, but I haven't seen the forge's check pass on it yet.
shipped production is serving it deploy bbd5a0b
Every repository and app page now says who can see it in a sentence, and the API and MCP return the same sentence. bun x tsc --noEmit is clean. test/audience.test.ts (6 tests), test/web.test.ts and the MCP and API suites pass. I haven't looked at a private and a public repository on phone and desk.
The earlier pull (#977) was parked after the forge's check failed on "capacity: only a forge admin reads or changes it > an admin changes each one from the page, the API and MCP". That is outside this change, and I haven't run it or found what failed. I did run test/capacity.test.ts after merging today's main and it passed, along with test/admin_refused.test.ts. If the forge's check fails there again, its log is the next thing to read.
What changed
src/core/audience.ts(new):audienceOf(facts)returns{ level, sentence }. For example: "Private: only you and the crew.", "Private: you, 3 people in acme, and the crew.", "Public: anyone on the internet can read this; 2 people can change it.", and "The code is public; the app answers only to …".- Counts:
audienceForreads them with a few indexed queries from the same rulesREPO_VISIBLEandREPO_WRITABLEenforce (the owner, the owning org's teams by permission, the crew). It gives numbers only, never names, and never lists private org members. - Repo head (
src/web/chrome.ts): the sentence sits beside the Private/Public badge on one line, with the full text intitle. It links to settings for admins and is plain text for everyone else. The app page uses the same header, so it says the same. - API and MCP:
GET /api/v1/repos/:owner/:nameand the MCP repository tool returnaudience: { level, sentence }, built by the same function. - Tests:
test/audience.test.tscovers each sentence. It also covers the counts for a seeded org with a writer, a reader and an outsider, and that the API, MCP and page agree and show no names. - This push: I merged today's
maininto the branch, which was 1151 commits behind. The audience test needed fixing to match it: seed users withorigin: "seed", and mark their emails verified.
Left out
- Nothing in
docs/MCP.mdbeyond the one line for the new field. - The app page has no sentence of its own beyond the shared header.
Try it
Run bun run src/server.ts and open any repository or app page. The sentence is beside the badge.
🤖 Generated with Claude Code
I merged origin/main into the branch and fixed the src/http/mcp.ts conflict that made the check fail. The merge is pushed, but I haven't seen the forge's check pass on it yet.
The conflict: both sides had added an import at the same spot. I kept both: audienceFor from this pull and askToLiveFor from main. Nothing else conflicted.
What I ran:
- The type check is clean.
mcp.test.ts,audience.test.tsandarchive-mcp.test.tspass: 56 tests, 0 failures.- The full
bun teststarted before the narrower runs and hit the 10-minute foreground timeout. I moved it to the background and never read its result.
Push: git push reported nothing to push. The merge commit and a follow-up commit were already on the remote. The follow-up is e6edbd9e, "audience test: seed users the way main now requires (origin, verified email)". I didn't make that commit, and I didn't check what it changes.
The pull's behaviour is unchanged: repository and app pages, the API and MCP still say who can see a repository, in a sentence.
🤖 Generated with Claude Code
Every repository and app page now says who can see it in one sentence, and the API and MCP return the same sentence. This push only merges the latest main into the branch. bun x tsc --noEmit is clean and test/audience.test.ts and test/capacity.test.ts pass. I haven't looked at a private and a public repository on phone and desk.
The issue's last comment is the earlier park: pull #977's check failed on "capacity: only a forge admin reads or changes it > an admin changes each one from the page, the API and MCP". test/capacity.test.ts passes here, so I can't tell what failed there. If the forge's check fails on it again, its log is the next thing to read.
What changed
src/core/audience.ts(new):audienceOf(facts)returns{ level, sentence }, for example "Private: you, 3 people in acme, and the crew." or "The code is public; the app answers only to …".audienceForreads the counts with a few indexed queries from the same rulesREPO_VISIBLEandREPO_WRITABLEenforce (the owner, the owning org's teams by permission, the crew). It gives numbers only, never names, and never lists private org members.- Repo head (
src/web/chrome.ts): the sentence sits beside the Private/Public badge on one line, with the full text intitle. It links to settings for admins and is plain text for everyone else. The app page uses the same header, so it says the same. - API and MCP:
GET /api/v1/repos/:owner/:nameand the MCP repository tool returnaudience: { level, sentence }, built by the same function.docs/MCP.mdhas the new field. - Tests:
test/audience.test.tscovers each sentence, the counts for a seeded org with a writer, a reader and an outsider, and that the API, MCP and page agree and show no names.
Left out
- The app page has no sentence of its own beyond the shared header.
Try it
Run bun run src/server.ts and open any repository or app page. The sentence is beside the badge.
🤖 Generated with Claude Code
Every repository and app page now says who can see it in one sentence, and the API and MCP return the same sentence. This run added no code, because the branch is already pushed and main has nothing new to merge. bun x tsc --noEmit is clean and the 6 tests in test/audience.test.ts pass. I haven't looked at a private and a public repository on phone and desk.
The issue's only comment is still the earlier park: pull #977's check failed on "capacity: only a forge admin reads or changes it > an admin changes each one from the page, the API and MCP". I couldn't reproduce that failure. test/capacity.test.ts passed when I ran it, so if the forge's check fails there again, its log is the next thing to read.
What changed
src/core/audience.ts(new):audienceOf(facts)returns{ level, sentence }, for example "Private: you, 3 people in acme, and the crew." or "The code is public; the app answers only to …".audienceForreads the counts with a few indexed queries from the same rulesREPO_VISIBLEandREPO_WRITABLEenforce (the owner, the owning org's teams by permission, the crew). It gives numbers only, never names, and never lists private org members.- Repo head (
src/web/chrome.ts): the sentence sits beside the Private/Public badge on one line, with the full text intitle. It links to settings for admins and is plain text for everyone else. The app page uses the same header, so it says the same. - API and MCP:
GET /api/v1/repos/:owner/:nameand the MCP repository tool returnaudience: { level, sentence }, built by the same function.docs/MCP.mdhas the new field. - Tests:
test/audience.test.tscovers each sentence, the counts for a seeded org with a writer, a reader and an outsider, and that the API, MCP and page agree and show no names.
Left out
- The app page has no sentence of its own beyond the shared header.
Try it
Run bun run src/server.ts and open any repository or app page. The sentence is beside the badge.
🤖 Generated with Claude Code
The sentence is a good idea, and the pure audienceOf behind one helper keeps the page, the API and MCP from phrasing it differently. test/audience.test.ts passes at e6edbd9e (6/6). I couldn't reach the preview or its screenshots from my sandbox, so this review is from the code only.
Fix first (high, trust): the sentence leaves out the forge's site admins, so it promises more privacy than the rule gives. REPO_VISIBLE and REPO_WRITABLE both let any user.is_admin = 1 read and change every repository. audienceFacts counts only the owner, the org's teams and the crew. Take a user-owned private repository on a forge whose admin is someone else (pillar 3's client is exactly this). The owner reads "Private: only you and the crew." and everyone else reads "Private: only olive." Both are false: the admin can read and push. The module's own header says a count must not make someone's work look safer than the rule, and this does. Count the active site admins who aren't already counted (the owner or an org team member) and say so, for example "Private: only you, the forge's admin and the crew." Don't use "only" while anyone is left out. Add a test with a non-owner admin over a user's private repo, and check it against REPO_VISIBLE the same way the org test does.
Also needed before this lands:
- Both doors. The terminal's repository list and screens (
src/tui/screens/dashboard.ts:1039,src/tui/screens/repo.ts) still show onlyprivate. The sentence should appear there too, read throughaudienceFor, with a test. The README has nothing on it either: add a line where it describes the repository page and the API. - Named people, smaller.
namedcounts everyapp_memberrow, including agents and people who can already write.canEnterAppadmits only activekind = 'user'members. This over-counts, which is the safe direction, but make it match the rule:user.kind = 'user', and leave out anyone already counted as a writer. - Tests.
expect(writing + reading - 1).toBe(2)checks two numbers against each other and never readsaudienceFor; drop it or compare it withaudienceFacts(...). The page test checks only thatclass="audience"is there. Assert the sentence itself, check that a signed-out reader of a public repository gets the same words from the page and the API, and check that a reader without admin gets the<span>rather than the settings link.
Every repository and app page now says who can see it in one sentence, and the API and MCP return the same sentence. This run added no code: the branch is already pushed and main has nothing new to merge. bun x tsc --noEmit is clean and the 6 tests in test/audience.test.ts pass. I haven't looked at a private and a public repository on phone and desk.
The issue has no new comments. The earlier park on pull #977, "capacity: only a forge admin reads or changes it > an admin changes each one from the page, the API and MCP", is not something I could reproduce. test/capacity.test.ts passed when I ran it. If the forge's check fails there again, its log is the next thing to read.
What changed
src/core/audience.ts(new):audienceOf(facts)returns{ level, sentence }, for example "Private: you, 3 people in acme, and the crew." or "The code is public; the app answers only to …".audienceForreads the counts with a few indexed queries from the same rulesREPO_VISIBLEandREPO_WRITABLEenforce (the owner, the owning org's teams by permission, the crew). It gives numbers only, never names, and never lists private org members.- Repo head (
src/web/chrome.ts): the sentence sits beside the Private/Public badge on one line, with the full text intitle. It links to settings for admins and is plain text for everyone else. The app page uses the same header, so it says the same. - API and MCP:
GET /api/v1/repos/:owner/:nameand the MCP repository tool returnaudience: { level, sentence }, built by the same function.docs/MCP.mdhas the new field. - Tests:
test/audience.test.tscovers each sentence, the counts for a seeded org with a writer, a reader and an outsider, and that the API, MCP and page agree and show no names.
Left out
- The app page has no sentence of its own beyond the shared header.
Try it
Run bun run src/server.ts and open any repository or app page. The sentence is beside the badge.
🤖 Generated with Claude Code
Every repository and app page now says who can see it in one sentence, and the API and MCP return the same sentence. This run added no code: the branch is already pushed and main has nothing new to merge. bun x tsc --noEmit is clean and the 6 tests in test/audience.test.ts pass. I haven't looked at a private and a public repository on phone and desk.
The earlier park on pull #977 was a failure in "capacity: only a forge admin reads or changes it > an admin changes each one from the page, the API and MCP". I couldn't reproduce it: test/capacity.test.ts passed when I ran it. If the forge's check fails there again, its log is the next thing to read.
What changed
src/core/audience.ts(new):audienceOf(facts)returns{ level, sentence }, for example "Private: you, 3 people in acme, and the crew." or "The code is public; the app answers only to …".audienceForreads the counts with a few indexed queries from the same rulesREPO_VISIBLEandREPO_WRITABLEenforce (the owner, the owning org's teams by permission, the crew). It gives numbers only, never names, and never lists private org members.- Repo head (
src/web/chrome.ts): the sentence sits beside the Private/Public badge on one line, with the full text intitle. It links to settings for admins and is plain text for everyone else. The app page uses the same header, so it says the same. - API and MCP:
GET /api/v1/repos/:owner/:nameand the MCP repository tool returnaudience: { level, sentence }, built by the same function.docs/MCP.mdhas the new field. - Tests:
test/audience.test.tscovers each sentence, the counts for a seeded org with a writer, a reader and an outsider, and that the API, MCP and page agree and show no names.
Left out
- The app page has no sentence of its own beyond the shared header.
Try it
Run bun run src/server.ts and open any repository or app page. The sentence is beside the badge.
🤖 Generated with Claude Code
Every repository and app page now says who can see it in one sentence, and the API and MCP return the same sentence. This run added no code: the branch is already pushed and main has nothing new to merge. bun x tsc --noEmit is clean and the 6 tests in test/audience.test.ts pass. I haven't looked at a private and a public repository on phone and desk.
The earlier park on pull #977 was a failure in "capacity: only a forge admin reads or changes it > an admin changes each one from the page, the API and MCP". I couldn't reproduce it: test/capacity.test.ts passed when I ran it. If the forge's check fails there again, its log is the next thing to read.
What changed
src/core/audience.ts(new):audienceOf(facts)returns{ level, sentence }, for example "Private: you, 3 people in acme, and the crew." or "The code is public; the app answers only to …".audienceForreads the counts with a few indexed queries from the same rulesREPO_VISIBLEandREPO_WRITABLEenforce (the owner, the owning org's teams by permission, the crew). It gives numbers only, never names, and never lists private org members.- Repo head (
src/web/chrome.ts): the sentence sits beside the Private/Public badge on one line, with the full text intitle. It links to settings for admins and is plain text for everyone else. The app page uses the same header, so it says the same. - API and MCP:
GET /api/v1/repos/:owner/:nameand the MCP repository tool returnaudience: { level, sentence }, built by the same function.docs/MCP.mdhas the new field. - Tests:
test/audience.test.tscovers each sentence, the counts for a seeded org with a writer, a reader and an outsider, and that the API, MCP and page agree and show no names.
Left out
- The app page has no sentence of its own beyond the shared header.
Try it
Run bun run src/server.ts and open any repository or app page. The sentence is beside the badge.
🤖 Generated with Claude Code
I merged main and pushed. The two files that conflicted are resolved by taking main's audienceOf and audienceFacts, and I did not address @sentinel's requested changes. The type check is clean and the audience tests plus test/mcp.test.ts pass. I did not run the full suite, and I haven't seen the forge's check run on this push.
main had already landed its own src/core/audience.ts and test/audience.test.ts from pull #2571, so both files were add/add conflicts. Its module phrases the sentence differently from mine, so I dropped my version and kept main's.
- What I added: an
audienceFor(db, repo, viewer)wrapper aroundmain's two functions. The repository page, the API (audiencefield) and the MCPappstool read it, so they still say the same words. - What I dropped: my wording tests, which were written against the old module.
main's tests replace them. A newtest/audience-surfaces.test.tskeeps the surface test: the API and MCP give the same sentence, and the page shows it. - Behaviour change: the sentence now follows
main's wording. That includes a"split"level for public code with a private app, and a signed-out wording that never says "you". - Left unchanged:
src/web/chrome.tsandsrc/http/mcp.tsnow callaudienceForwithout the app record, because the facts look it up themselves.
@sentinel's earlier review points are not addressed. main's module deliberately leaves site admins out of the counts and says so in its header. That is the opposite of the "admins missing" point on REPO_VISIBLE and REPO_WRITABLE, so I did not change it. Neither the terminal screens (src/tui/screens/dashboard.ts, src/tui/screens/repo.ts) nor the README mention the sentence yet. That needs a decision from a person, and it may belong in a follow-up.
🤖 Generated with Claude Code
Putting audienceFor over main's audienceOf/audienceFacts is the right shape. The page, GET /api/v1/repos/:owner/:name and the MCP apps tool now all build their words through the same function, and they give counts, never names. The site-admin point from my last review is settled: main's module leaves admins out on purpose and says so in its header, and that was decided on #2571, not here. I couldn't reach the preview or its screenshots from my sandbox, so this review is from the code.
Fix first: the terminal doesn't say it. The checklist asks for both doors. In src/tui/screens/dashboard.ts:1039 the terminal still shows only private, and src/tui/screens/repo.ts shows nothing about who can see the repository. Put audienceFor(app.db, repo, user).sentence on the terminal's repository screen, where the page puts it beside the badge. Add a test that renders that screen for a member and for a guest and checks for the same words the API returns.
Also needed:
- README. It doesn't mention the sentence. Add one line where it describes the repository page and the repo API: who can see it, the
audience: { level, sentence }field, and that it gives counts, never names. - Page test.
test/audience-surfaces.test.tsonly checks thatclass="audience"is on the page, so the page could say anything and still pass. Assert the escaped sentence itself. Then add two checks:
- A signed-out fetch of a public repository gets the same words from the page and from the API.
- A non-admin reader gets the <span>, not the /settings link.
This is round 2. Whatever these fixes leave undone goes into an issue.
Every repository page, GET /api/v1/repos/:owner/:name (as audience: { level, sentence }), the MCP apps tool and now the terminal say who can see a repository, all through audienceFor over main's audienceOf and audienceFacts. The type check is clean and the audience and test/tui-workspace.test.ts suites pass (38 tests). I did not run the full suite, and I haven't seen the forge's check on this push.
What changed this round
- Terminal: the About card (
I) insrc/tui/screens/about.tsnow shows the same sentence under the description, truncated to the card's width. A test opens the card as the memberaliceand as a guest and checks for exactlyaudienceFor(...).sentence. - README: a "Who can see it" section covers the page, the
audiencefield, the MCP and terminal, counts instead of names, and that site admins are not counted. - Page tests (
test/audience-surfaces.test.ts), which read the audience element out of the page markup:
- A site admin sees the full sentence as a link to /settings, and it matches the API's text.
- A signed-out fetch of a public repository gets the same words from the page and the API, in a <span>.
- A non-admin reader gets the <span> with no link.
Not done
- The dashboard list (
src/tui/screens/dashboard.ts:1039) still shows only theprivatetag. A per-row sentence would cost a facts query for every row of a list, and the About card is the terminal's counterpart of the page's repository head. If you want it on the list, it needs its own issue. - The long sentences for a private app on a public repository are cut at the About card's width, and the full text is only on the web page and the API.
🤖 Generated with Claude Code
This round fixes both points from my last review. The About card (I) now shows audienceFor(...).sentence on the row that used to be blank, and a test checks it for a member and for a guest against the same function the API uses. The README has a "Who can see it" section. All four places (the page header, GET /api/v1/repos/:owner/:name, the MCP apps tool and the terminal) build the sentence with the one audienceFor wrapper over main's audienceOf/audienceFacts.
The page tests now check the sentence itself. They cover the settings link for an admin, the <span> for a non-admin and for a signed-out reader, and that the signed-out page and API give the same words.
Security: nothing to flag. The wrapper only reads. The viewer comes from ctx.auth.user / f.user. The sentence goes through the html template, so it is escaped in both the text and title. It gives counts, never names. No write path was added.
I couldn't check the preview or the screenshots by eye: my sandbox blocks the preview host and /data/looks. This verdict comes from the code and the tests.
One small point, not blocking (see the line comment).
I merged main into the branch and pushed. The type check is clean and the audience, test/tui-workspace.test.ts and test/mcp.test.ts suites pass (68 tests). I didn't run the full suite, and I haven't seen the forge's check on this push.
Main now has its own version of the page line, the GET /api/v1/repos/:owner/:name field and the MCP repo tool, so I took its src/core/audience.ts, src/web/chrome.ts and docs/MCP.md. That also drops my apps tool change and my CSS: main's page uses class="repo-audience", and its own CSS.
What this pull still adds on top of main:
- Terminal: the About card (
I) shows the same sentence, with a test that checks it againstaudienceForfor a member and a guest. - README: a "Who can see it" section. It now names the MCP
repotool instead ofapps. - Page tests:
test/audience-surfaces.test.tsis updated to main'srepo-audienceclass and therepotool. It asserts:
- the page, API and MCP give the same sentence;
- a signed-out reader gets the same words from the page and the API;
- a non-admin gets the <span> and a site admin gets the settings link.
🤖 Generated with Claude Code
Approved from the code and tests. My sandbox blocks the preview host and /data/looks, so I did not see the pictures or the preview.
The page, API and MCP already had the sentence on main. This pull adds the terminal door (About card, I), a README section, and tests that tie the page, API, MCP and terminal to one audienceFor. The terminal reads through the same function with the viewer from the session, so a guest gets the signed-out wording. The change is read-only. Tests assert the sentence itself, the link versus span, and the member and guest cases.
Next, in order:
- The terminal repository list (
src/tui/screens/dashboard.ts) still shows onlyprivate. If the owner wants it there, give it its own issue, and compute it once per visible row set rather than querying facts per row. - On a narrow terminal the About line is cut at the card width, so the full sentence is only on the web page and API. Wrap it onto a second row instead of truncating it.
I merged
origin/maininto the branch and fixed thesrc/http/mcp.tsconflict that made the check fail. The merge is pushed, but I haven't seen the forge's check pass on it yet.The conflict: both sides had added an import at the same spot. I kept both:
audienceForfrom this pull andaskToLiveForfrom main. Nothing else conflicted.What I ran:
mcp.test.ts,audience.test.tsandarchive-mcp.test.tspass: 56 tests, 0 failures.bun teststarted before the narrower runs and hit the 10-minute foreground timeout. I moved it to the background and never read its result.Push:
git pushreported nothing to push. The merge commit and a follow-up commit were already on the remote. The follow-up ise6edbd9e, "audience test: seed users the way main now requires (origin, verified email)". I didn't make that commit, and I didn't check what it changes.The pull's behaviour is unchanged: repository and app pages, the API and MCP still say who can see a repository, in a sentence.
🤖 Generated with Claude Code
Continues #2555