You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Stage 2.3: the Project board view and who may act on it (owner vs member) #1710
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 verify green at the tip (TZ=UTC); core/client.js regenerated if core/src/client/ changed; the subsystem page updated or a valid Docs-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 with claim: 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).
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) landed ownerUserId on state.projects; Stage 2.2 (#1696, merged 554d939e) landed the read-only project-board aggregate. Actor identity and host refusal already exist: core/src/host/channel.js resolves actor from x-flybot-member-session, and core/src/host/tasks.js:199-215 is the precedent (requesterOf, mayAct, forbidden, fallback to the first member when no session exists). No new owner ruling is needed.
Scope for the implementer:
View: new core/src/client/project-board.js, one pure projectBoardView({ project, rows, canManage }) — group rows by source (bot | group | workflow), each row shows column, assignee and runId/nodeId when 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 in core/src/client/index.js; regenerate core/client.js (npm run build in core/).
Host gate: take actor into projectEndpoint in core/src/host/store.js (same shape taskEndpoint already uses), resolve the owner as project.ownerUserId with the tasks.js fallback (a single-human install with no sessions is the owner), return the host-computed canManage from project-read, and refuse project-bind from a non-owner with bot/forbidden; a project-create by a signed-in actor stamps that actor as ownerUserId. Hiding the button is allowed; the refusal must be the host's. Reads (project-read, project-board) stay available to any signed-in actor.
Live defect this slice must fix: project-board is missing from TASK_ENDPOINTS in core/src/host/authz.js, so once member sessions exist the board is refused as Unknown endpoint project-board; add it beside the other project endpoints.
Out of this slice: settings/members screens that have no endpoint yet. The only Project write surfaces that exist are project-create and project-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.
Tests, red first: new core/test/project-board-view.test.js, bundled the way core/test/board-card.test.js bundles a client component — rows from all three sources, grouping, runId/nodeId, empty state, row id equals the underlying task id; extend core/test/projects.test.js — non-owner project-bind refused (bot/forbidden), owner accepted, project-read reports canManage true/false, and a signed-in actor may read project-board.
Docs:docs/subsystems/core-host.md (gate, canManage, endpoint list) and docs/subsystems/core-client.md (the view), or a valid Docs-not-needed: in both the branch commit and the PR body.
Risk: T3.core/src/host/channel.js and core/src/host/authz.js are T3 paths in .github/agent/risk-rules.json; store.js is T2 and the client is T1, highest wins. So the PR needs a PASS at the tip, green tests-ok at that tip, npm run verify with TZ=UTC, and the merge queue.
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…