Stage 2.3: the Project board view and who may act on it (owner vs member) #1710
Copy link
Copy link
Closed
Labels
area:uiWeb/desktop UI and copyWeb/desktop UI and copyenhancementNew feature or requestNew feature or request
Description
Written-by: Chief
Part of #1550, Stage 2 third slice. Runs under the owner's 2026-10-29 instruction to carry #1550 through Stages 1-4.
Why
Stage 2.1 gave a Project an identity (d5347032) and Stage 2.2 gave it one board read model (#1703, merged as 554d939e): a Project id now resolves to rows from Bot boards, group boards and workflow runs, each with a stable id. Nothing shows them yet, and nothing decides who may act on what.
What to build
- One Project board view that renders the rows of the read model: grouped by source (
bot|group|workflow), showing column, assignee and the run/node when there is one. A row opens the task it comes from; the view never edits. - Role-gated actions. The Project's owner sees the management affordances Stage 2 names (Project settings, bindings, members). A session that is not the owner sees the same board read-only — no create, no reassign, no settings. The refusal is decided on the host, never by hiding a button alone.
- No second board. The view reads the aggregate; it does not keep its own task rows, and a change made on a Bot or group board shows up on the Project board without a copy.
Acceptance
- Tests in
core/test/: the board renders rows from all three sources; a non-owner session gets a refusal from the host action, not just a hidden control; the row id equals the underlying task id. - A Project with nothing under it renders an empty state, not an error.
- No second store and no new configuration key.
npm run verifygreen at the tip (TZ=UTC);core/client.jsregenerated ifcore/src/client/changed; the subsystem page updated or a validDocs-not-needed:in both the branch commit and the PR body.- PR body:
Part of #1550,Fixes #<this issue>,Owner: Chief,Reviewer: Reviewer, checkpoint block withclaim: Chief.
Out of scope
The Maintainer console and the Member role as separate shipped roles (they stay planned, as docs/governance.md §7 says); the Project Assistant (Stage 2.5).
Metadata
Metadata
Assignees
Labels
area:uiWeb/desktop UI and copyWeb/desktop UI and copyenhancementNew feature or requestNew feature or request
Activity
Birfy commentedon Oct 29, 2026
Written-by: Triage
Label set:
agent-ready(no previous state label to remove).Design conflict resolved. The owner approved the five-stage direction on 2026-10-29 (
decisions/2026-10-29-stage-1-approved.md, #1550 comment 11:32Z), and Stage 2's own acceptance already states the rule: "Owner can reach members, Bots, resources and governance controls; non-owners cannot perform those operations." Whose look the Project is, and that non-owners may not act, is therefore settled. Stage 2.1 (#1691) landedownerUserIdonstate.projects; Stage 2.2 (#1696, merged554d939e) landed the read-onlyproject-boardaggregate. Actor identity and host refusal already exist:core/src/host/channel.jsresolvesactorfromx-flybot-member-session, andcore/src/host/tasks.js:199-215is the precedent (requesterOf,mayAct,forbidden, fallback to the first member when no session exists). No new owner ruling is needed.Scope for the implementer:
core/src/client/project-board.js, one pureprojectBoardView({ project, rows, canManage })— group rows bysource(bot|group|workflow), each row shows column, assignee andrunId/nodeIdwhen present; the row id is the underlying task id and opening it opens that task; no rows → an empty state, never an error. It holds no task rows of its own (no second store, no new configuration key) and never edits, so a change on a Bot/group board shows up on the next read. Wire it into the existing settings/panes surface incore/src/client/index.js; regeneratecore/client.js(npm run buildincore/).actorintoprojectEndpointincore/src/host/store.js(same shapetaskEndpointalready uses), resolve the owner asproject.ownerUserIdwith thetasks.jsfallback (a single-human install with no sessions is the owner), return the host-computedcanManagefromproject-read, and refuseproject-bindfrom a non-owner withbot/forbidden; aproject-createby a signed-in actor stamps that actor asownerUserId. Hiding the button is allowed; the refusal must be the host's. Reads (project-read,project-board) stay available to any signed-in actor.project-boardis missing fromTASK_ENDPOINTSincore/src/host/authz.js, so once member sessions exist the board is refused asUnknown endpoint project-board; add it beside the other project endpoints.project-createandproject-bind, and the gate covers them; the view offers no move/reassign to anyone, which is what items 1 and 3 ("the view never edits", "no second board") require.core/test/project-board-view.test.js, bundled the waycore/test/board-card.test.jsbundles a client component — rows from all three sources, grouping,runId/nodeId, empty state, row id equals the underlying task id; extendcore/test/projects.test.js— non-ownerproject-bindrefused (bot/forbidden), owner accepted,project-readreportscanManagetrue/false, and a signed-in actor may readproject-board.Docs:
docs/subsystems/core-host.md(gate,canManage, endpoint list) anddocs/subsystems/core-client.md(the view), or a validDocs-not-needed:in both the branch commit and the PR body.Risk: T3.
core/src/host/channel.jsandcore/src/host/authz.jsare T3 paths in.github/agent/risk-rules.json;store.jsis T2 and the client is T1, highest wins. So the PR needs a PASS at the tip, greentests-okat that tip,npm run verifywithTZ=UTC, and the merge queue.Birfy commentedon Oct 29, 2026
Written-by: Claim 12f7
Claimed #1710 — branch
dev/1710-project-board. First step: the pureprojectBoardViewgrouped over the read model #1703 shipped.Add the Project board view and gate who may act on a Project (#1710)
Add the Project board view and gate who may act on a Project (ThinkFl…
core: paint only the Project cards `tasks` would let a member open (T…