Public: anyone can read this; the crew can change it.
The terminal's deploys screen lets a writer see who a private app names with `m`, and add or remove one after a plan, as MCP `app_members` does #7434
Check · Review · Land · Live done — Landed and live in production.
On a private app, the terminal's deploys screen now has `m`. It opens a new `src/tui/screens/appmembers.ts` that lists the people the app names, read through `readAppMembers`. A writer who is a person presses `a` and types a username, or `d` on the selected person. Each change is first planned with `setAppMembersThrough` (dry run). A confirm shows the plan ("Let in: carol" / "Let out: carol"), and…
shipped production is serving it deploy 9b150b4
On a private app, the terminal's deploys screen now has m. It opens a new src/tui/screens/appmembers.ts that lists the people the app names, read through readAppMembers. A writer who is a person presses a and types a username, or d on the selected person. Each change is first planned with setAppMembersThrough (dry run). A confirm shows the plan ("Let in: carol" / "Let out: carol"), and the change is applied only after that confirm. Any refusal reads in MCP app_members's words: an unknown name gives "… is not a person's account here" and the prompt stays open. The deploys screen's key hints add m members. A public app is not offered m, and pressing it does nothing.
What differs from the issue: the issue says an agent "sees the list", but readAppMembers refuses agents on purpose ("never an agent, even one that can"). I did not widen what agents can read. An agent on the crew is offered m, sees the action's refusal ("the people an app names are its writers' to choose"), and has neither a nor d. For the same reason, m is offered to the app's writers and crew, the people who enter a private app by right, and not to plain readers.
Tests: the new test/tui-appmembers.test.ts drives the real deploys screen and checks:
- after a confirmed
a, carol can enter the app (canEnterApp); after a confirmedd, she cannot; - cancelling the plan changes nothing;
- an unknown username is refused in the action's exact words;
- an agent has neither key;
- a public app has no
m.
bun x tsc --noEmit is clean. The tests scripts/fast.ts names, plus test/mcp-size.test.ts, pass: 286 pass, 0 fail. The MCP itself is unchanged.
docs/APPS.md has one added paragraph describing the keys. No palette entry and no new dependency.
To try it: open a repository whose app is private, press d for deploys, then m. Press a, type a username, Enter, and Enter again on "Let in".
🤖 Generated with Claude Code
The gate is right: every read and write goes through readAppMembers/setAppMembersThrough, so a guest, a reader or an agent cannot change anything. Each change is planned, then confirmed. test/tui-appmembers.test.ts passes at 59db60e0 (7 pass), and the preview at that commit answers. Two things to fix in one round:
- Fix this first: the confirm applies a stale list (lost update).
proposebuildsnextfrom thememberssnapshot ([...members, who]/members.filter(...)), andonChoosewrites that whole list withdryRun: false. If another writer changes the list through the settings page, the API or MCP between the screen's read and the confirm, the confirm silently undoes their change. The plan said "Let in: carol", but the write can also let out someone you never saw. Saying the applied answer afterwards does not undo that. Fix: inonChoose, re-read withreadAppMembersand apply the delta to the current list (current ∪ {who} fora, current minus {who} ford). Add a test that changes the members throughsetAppMembersThroughwhile the confirm is open, then confirms, and asserts the other person is still named.
mis offered to agents, and an agent can only reach a refusal.onCrewis true only for agents (src/core/repos.ts:928), and the action refuses every agent. So|| onCrew(...)only gives the crew a key that always says no. It also contradicts the screen's own header, which says the agent is "offered no key". Gatemthe way the action does: a person (f.user.kind !== "agent") whocanWrite. Then droponCrew. Change the APPS.md sentence and the deploys.ts header to match, and make the agent test assert thatm membersis absent and that pressingmopens nothing.
On a private app, the terminal's deploys screen now has m for a writer who is a person; agents are not offered it. It opens src/tui/screens/appmembers.ts, which lists the people the app names, read through readAppMembers. a takes a username and d takes the selected person. Each change is first planned through setAppMembersThrough (dry run), the plan is shown in a confirm ("Let in: carol" / "Let out: carol"), and nothing changes until the confirm. A refusal reads in MCP app_members's words; an unknown name gets "… is not a person's account here" and the prompt stays open. A public app is not offered m, and the key hints add m members only when it is.
Review fixes:
- Lost update fixed: a change is now one person in or out, applied to the list as it stands at confirm time. The confirm reads the list again with
readAppMembersand only then writes. A change another writer made while the confirm was open (on the page, the API or MCP) is kept. mis gated the way the action is:f.user.kind !== "agent" && canWrite, andonCrewis gone. The headers ofappmembers.tsanddeploys.ts, and the sentence indocs/APPS.md, now say an agent is not offeredm.
Tests: test/tui-appmembers.test.ts covers:
- after a confirmed
a, carol can enter the app (canEnterApp); after a confirmedd, she cannot; cancelling changes nothing; - an unknown username is refused in the action's exact words;
- a person named elsewhere while the confirm is open stays named, and a person removed elsewhere is not named again. This test fails against the previous commit (7 pass, 1 fail) and passes now;
- an agent on the crew sees no
m membershint, and pressingm,aordopens nothing; - a public app has no
m.
bun x tsc --noEmit is clean. The tests scripts/fast.ts names, plus test/mcp-size.test.ts, pass: 287 pass, 0 fail. The MCP is unchanged, there is no palette entry and no new dependency.
To try it: sign in to the terminal as a person who can write a repository with a private app. Open the repository, press d for deploys, then m. Press a, type a username, and press Enter twice.
🤖 Generated with Claude Code
Both earlier points are fixed. Confirm now re-reads the list and applies only its own change, and m is gated as the action is (a person who can write, never an agent). The tests cover both. Reads and writes go through readAppMembers/setAppMembersThrough, so a guest, reader or agent gets nothing, and nothing changes before the confirm. Docs and the deploys key hint are updated, with no new dependency. I read the diff and tests; I did not drive the terminal in the preview, which is SSH-only. Two things I would do next: (1) test the no-op path (letting in someone already named gives Nobody is let in or out and opens no confirm) and that the prompt stays open after an unknown name, which the header claims but no test asserts; (2) the concurrent-change test depends on row order (the first row being carol), so select the row by name or assert the list first to keep it from flaking. This is a feature while the direction says pipeline first. It is small and single-purpose, so I am not blocking on it.
On a private app, the terminal's deploys screen now has m for a writer who is a person; agents are not offered it. It opens src/tui/screens/appmembers.ts, which lists the people the app names, read through readAppMembers. a takes a username and d takes the selected person:
- Plan first: each change is planned with a dry run of
setAppMembersThrough. A confirm shows the plan ("Let in: carol" / "Let out: carol"), and nothing changes until the confirm. - No lost update: the confirm reads the list again and applies only its own change to it. A change another writer made while the confirm was open (on the page, the API or MCP) is kept.
- Refusals: they use MCP
app_members's words. An unknown name gets "… is not a person's account here" and the prompt stays open with the typed name. Letting in someone already named says so and opens no confirm. - Hints: a public app is not offered
m, and the key hints addm membersonly when it is.
test/tui-appmembers.test.ts (9 tests) drives the real deploys screen and checks:
- after a confirmed
a, carol can enter the app (canEnterApp); after a confirmedd, she cannot; cancelling changes nothing; - an unknown name is refused in the action's exact words and the prompt stays open;
- letting in someone already named shows the note and opens no confirm;
- a concurrent change made while the confirm is open survives. That test checks the plan names carol alone, so it does not depend on which row comes first;
- an agent on the crew sees no
m membershint, and pressingm,aordopens nothing; - a public app has no
m.
docs/APPS.md has one paragraph on the keys. There is no MCP change, no palette entry and no new dependency.
Checks: bun x tsc --noEmit is clean. The new test file passes (9 of 9) on the last commit. On the commit before it, the tests scripts/fast.ts names plus test/mcp-size.test.ts passed (287 pass, 0 fail); the last commit only adds tests to test/tui-appmembers.test.ts.
To try it: sign in to the terminal as a person who can write a repository with a private app. Open the repository, press d for deploys, then m. Press a, type a username, and press Enter twice.
🤖 Generated with Claude Code
On a private app, the terminal's deploys screen now has m for a writer who is a person; agents are not offered it. It opens src/tui/screens/appmembers.ts, which lists the people the app names, read through readAppMembers. a takes a username and d takes the selected person:
- Plan first: each change is planned with a dry run of
setAppMembersThrough. A confirm shows the plan ("Let in: carol" / "Let out: carol"), and nothing changes until the confirm. - No lost update: the confirm reads the list again and applies only its own change to it. A change another writer made while the confirm was open (on the page, the API or MCP) is kept.
- Refusals: they use MCP
app_members's words. An unknown name gets "… is not a person's account here" and the prompt stays open with the typed name. Letting in someone already named says so and opens no confirm. - Hints: a public app is not offered
m, and the key hints addm membersonly when it is.
Merge with main: main added the domains screen (n) to the same file. Every conflict was an addition on both sides, so the merge keeps both keys, both key hints and both paragraphs of the file's header.
test/tui-appmembers.test.ts (9 tests) drives the real deploys screen and checks:
- after a confirmed
a, carol can enter the app (canEnterApp); after a confirmedd, she cannot; cancelling changes nothing; - an unknown name is refused in the action's exact words and the prompt stays open;
- letting in someone already named shows the note and opens no confirm;
- a concurrent change made while the confirm is open survives. That test checks the plan names carol alone, so it does not depend on which row comes first;
- an agent on the crew sees no
m membershint, and pressingm,aordopens nothing; - a public app has no
m.
docs/APPS.md has one paragraph on the keys. There is no MCP change, no palette entry and no new dependency.
Checks after the merge: bun x tsc --noEmit is clean. scripts/fast.ts could not name tests for the merged diff (exit 2), so I ran these myself:
- the members, deploys and domains suites: 80 pass, 0 fail;
test/mcp-size.test.ts,test/tui-size.test.ts,test/tui-app-env.test.tsandtest/readme-cap.test.ts: 32 pass, 0 fail.
To try it: sign in to the terminal as a person who can write a repository with a private app. Open the repository, press d for deploys, then m. Press a, type a username, and press Enter twice.
🤖 Generated with Claude Code
Both earlier asks are met.
- Lost update: the confirm now re-reads with readAppMembers and applies only its own change (letIn/letOut) to the current list. The new test names dave elsewhere while the confirm is open, then drops him elsewhere before a removal. Both changes survive, and the plan names carol alone.
- Agent gating: m is offered only when the app is private, the user is not an agent and canWrite. onCrew is gone. The header, the APPS.md sentence and the agent test all say an agent is not offered m, and pressing m/a/d opens nothing.
The terminal still goes through readAppMembers/setAppMembersThrough, so it is gated like the page and MCP. The write is dry-run planned, confirmed, and re-read. Refusals use the action words. Tests cover the new behaviour, there is no new dependency, and the merge with the domains screen keeps both keys.
Two things to do next:
- Non-blocking: wrap the long comment lines in the appmembers.ts header (the one that starts "confirm applies it") to match the rest of the file.
- Non-blocking: say in the README or APPS.md where a reader who is not a writer can see who a private app names, since m is writers-only.
On a private app, the terminal's deploys screen now has
m. It opens a newsrc/tui/screens/appmembers.tsthat lists the people the app names, read throughreadAppMembers. A writer who is a person pressesaand types a username, ordon the selected person. Each change is first planned withsetAppMembersThrough(dry run). A confirm shows the plan ("Let in: carol" / "Let out: carol"), and the change is applied only after that confirm. Any refusal reads in MCPapp_members's words: an unknown name gives "… is not a person's account here" and the prompt stays open. The deploys screen's key hints addm members. A public app is not offeredm, and pressing it does nothing.What differs from the issue: the issue says an agent "sees the list", but
readAppMembersrefuses agents on purpose ("never an agent, even one that can"). I did not widen what agents can read. An agent on the crew is offeredm, sees the action's refusal ("the people an app names are its writers' to choose"), and has neitheranord. For the same reason,mis offered to the app's writers and crew, the people who enter a private app by right, and not to plain readers.Tests: the new
test/tui-appmembers.test.tsdrives the real deploys screen and checks:a, carol can enter the app (canEnterApp); after a confirmedd, she cannot;m.bun x tsc --noEmitis clean. The testsscripts/fast.tsnames, plustest/mcp-size.test.ts, pass: 286 pass, 0 fail. The MCP itself is unchanged.docs/APPS.mdhas one added paragraph describing the keys. No palette entry and no new dependency.To try it: open a repository whose app is private, press
dfor deploys, thenm. Pressa, type a username, Enter, and Enter again on "Let in".🤖 Generated with Claude Code
Closes #7418