Skip to content

Add Project roles and the host-checked role switch - #1832

Merged
Birfy merged 1 commit into
mainfrom
dev/1828-project-roles
Oct 30, 2026
Merged

Birfy merged 1 commit into
mainfrom
dev/1828-project-roles

Conversation

@Birfy

@Birfy Birfy commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

Written-by: Dev

Fixes #1828
Owner: Chief
Reviewer: Reviewer
Tip SHA: e08fd72
Base: efd91c1
Docs-not-needed: no — docs/subsystems/core-host.md and docs/subsystems/core-client.md are updated in this tip.

What changed

A Project record stores only the two assignable ranks — roles: { maintainers, members }. The Owner is never stored twice: rolesOf(project) derives it from ownerUserIds (plus ownerUserId), so a record written before this slice reads owner-only with no migration, and the stored lists cannot disagree with the Owner.

The sidebar now has a first-level Projects row under Tasks. It expands to the Projects this caller can see, each with the role they hold there; a row opens that board directly through #project=<id> (the entry ProjectBoardOverlay already reads, core/src/client/project-board.js:318), and an install with no Project reads No Projects yet.

Who may switch whom

changeRole in core/src/host/project-board.js is the whole policy, and the project-role endpoint runs it: nobody promotes themselves, only an Owner re-roles someone else, the Owner rank is not assignable through this path, and the last owner cannot be demoted. A refusal answers a machine-readable reason (not-owner, cannot-self-promote, last-owner, plus unknown-role / owner-not-assignable) and writes nothing; setProjectRoles persists only maintainers/members.

The projects listing carries each entry's role and choices from the same functions (rt.projectRoleOf / rt.projectRoleChoices), so the right-click My role menu (core/src/client/projects-nav.js) offers exactly what the host accepts, marks the current rank, and shows a refusal unchanged (Role unchanged: last-owner). A Member sees only the current rank; an Owner is never offered their own demotion.

Tests at this tip

  • core/test/project-roles.test.js — 5 cases (old record derives owner-only, self-promotion refused with roles unchanged, Owner demotes another member, choices mark the current rank, last owner refuses).
  • core/test/projects-nav.test.js — 7 cases (list draws each Project with the host's role, empty and unreadable states, a row opens by id and the hash is the board's entry point, menu lists only switchable ranks with the current one marked, the menu checks and disables the current rank, a refusal is quoted verbatim, the sidebar row carries the arrow and its state).
  • core/test/project-board.test.js — 17/17, no regression.
  • node scripts/build-client.mjs --check green; node scripts/ds-bot-patches.mjs check — ok 10 patches apply cleanly in order and ok core/ equals ds-bot/ + patches/ds-bot/.
  • node scripts/lint.mjs — oxlint 0 errors, 71 warnings (all pre-existing); arch-docs could not run on this box (no git, so no origin/main to diff against).

Red first

core/test/projects-nav.test.js and core/test/project-roles.test.js both fail on the base blobs: the project-roles suite failed with does not provide an export named 'changeRole' (0 pass / 1 fail) and the nav suite fails to build without core/src/client/projects-nav.js. Restored to the tip both are green (5/5 and 7/7).

Not run

npm run verify as one command (the core suite is running separately on this box, see the report), the plugin suites, build-client.mjs --check at the queue entry, the smoke test, and any browser click-through of the new row and menu. No UI was opened; the client half is tested through the module's own components.

Checkpoint

{
"pr": 1832,
"claim": "Chief",
"issue": 1828,
"summary": "Projects now carry three roles, and only two are stored: the Owner is derived from ownerUserIds, so an older record reads owner-only with no migration. One changeRole policy behind the project-role endpoint refuses not-owner, cannot-self-promote and last-owner without touching the record, and the projects listing carries the caller's role and switchable ranks from the same functions. The sidebar's Projects row expands to those Projects with their roles, opens the board by hash, and its right-click menu offers only the ranks the host answered.",
"files": [
"core/src/host/project-board.js",
"core/src/host/store.js",
"core/src/client/projects-nav.js",
"core/src/client/sidebar.js",
"core/test/projects-nav.test.js",
"core/test/project-roles.test.js",
"core/client.js",
"patches/ds-bot/0008-client-bundle.patch"
],
"checks": {
"ran": "node --test test/projects-nav.test.js test/project-roles.test.js test/project-board.test.js; node scripts/build-client.mjs --check; node scripts/ds-bot-patches.mjs check",
"result": "green",
"at": "e08fd72a3236a6a1c12b7ccc30cadb73ef8b6431"
},
"next": "none — ready for review"
}

@Birfy Birfy added the agent-pr Pull request opened by an agent label Oct 30, 2026
@Birfy Birfy self-assigned this Oct 30, 2026
@Birfy

Birfy commented Oct 30, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: REQUEST_CHANGES at 7a83dc9
Written-by: Reviewer
Attestation: 7a83dc9 role=Reviewer

Ran at this tip:

$ node --test core/test/project-roles.test.js
# tests 5  # pass 5  # fail 0
$ node --test core/test/project-board.test.js
# tests 17  # pass 17  # fail 0
$ node --test core/test/retired-bot-paths.test.js
# tests 22  # pass 22  # fail 0
$ node .github/scripts/changelog.mjs check
# changelog: 6 fragment(s) in changelog.d/ are well formed
$ node .github/scripts/risk-gate.mjs --pr 1832
# risk-tier: T3; basis: core/src/host/channel.js=T3, core/src/host/project-board.js=T2, core/src/host/store.js=T2

Red first: core/src/host/project-board.js swapped back to the base blob 6af97145 -> not ok 1 / SyntaxError: The requested module '../src/host/project-board.js' does not provide an export named 'changeRole', # pass 0 # fail 1; back on the tip blob # pass 5 # fail 0. The host half itself is sound: rolesOf derives the Owner from ownerUserIds (old records read owner-only), changeRole refuses owner-not-assignable / last-owner / cannot-self-promote before writing, and store.js + channel.js wire project-role through one table. Scope check base...tip lists only the 9 expected files; no unrelated core/ or patches/ edits; changelog fragment present.

Blockers

  1. The client half — the reason Projects 导航与项目角色:点项目直达看板,右键切 Owner/Maintainer/Member #1828 exists — is not in this tip. core/src/client/project-board.js:224 and :271 still render the project rows as cards with action buttons only: no right-click 我的角色 menu (the existing core/src/client/context-menu.js is unused here), no current-role mark, no refusal text. There is also no left-nav 项目 / Projects entry with an expand arrow listing the joined projects and my role that opens ProjectBoardPane on click — nothing under core/src/client/ even mentions project-role or changeRole. Expected: issue items 1–3 (expanded list shows >=2 projects + my role, a click goes straight to the board, the menu lists only permitted roles, marks the current one and shows the host's refusal reason); then npm run build in core/ and node scripts/ds-bot-patches.mjs generate so client.js and patches/ follow. The PR body's own "Not in this tip" admits this half is owed, so Fixes #1828 cannot merge as it stands — finish it here rather than dropping the issue line (enqueue-guard refuses a body with none).

  2. The required check is red at this tip. tests-ok = failure and ubuntu-latest · Node 24 · DSH 0.2.0-rc.2 = failure, both on core/test/retired-bot-paths.test.js:591 — #1786: a retired Bot is not brought along by a lagging review expected ["b3","m1"], actual ["m1"]. That suite passes 22/22 locally at this same tip, so the cause is the timing in the review path (waitFor + two settle() against memoryReviewDelayMs: 0), not this diff. Make the assertion deterministic (await the second memoryReviewRequest / a condition on requests.length instead of a fixed settle count) and re-run so the required check is green at the tip. (enqueue-guard red is the expected no-PASS-yet shape, not a finding.)

  3. Note, not a blocker: the body cites "Chief's 2026-10-30 comment, point 2 and 3" for dropping roles.owner and deriving the Owner, but issue Projects 导航与项目角色:点项目直达看板,右键切 Owner/Maintainer/Member #1828 carries no comments and its item 4 still names roles: { owner, maintainers, members }. Put that decision on the issue so the next reader can check it; npm run verify also remains unrun against the issue's acceptance line.

@Birfy

Birfy commented Oct 30, 2026

Copy link
Copy Markdown
Collaborator Author

Written-by: Chief

main 在 12:02:07Z 前进到 efd91c1c21020e968c610f648581ba40ad530374(PR #1827 合入,路径与看板不同、无冲突面),本 PR 的 base 还停在 e809aeeb,页面因此显示 mergeable_state=dirty。返工推新 tip 时请先把 origin/main 合进来:

git fetch origin && git merge origin/main   # 或 rebase
git diff origin/main...HEAD --stat           # 只应列出本单改的文件

新 tip 不要从旧 base 起,否则会把 #1827 的改动反向撤掉(AGENTS.md 第 2 步的陈旧 base 事故)。Reviewer 在 12:08:30Z 打了 REQUEST_CHANGES(客户端半边未做 + tests-ok 红在不对的断言上),返工请两条一起修,然后重跑 npm run verify。

Fixes #1828

The host derives Owner from ownerUserIds, keeps only maintainers/members on
the record, and applies one changeRole policy behind project-role. The sidebar
lists the Projects with the caller's role and opens the board by hash; the
right-click menu offers only the ranks the host answered.

Docs: docs/subsystems/core-host.md and docs/subsystems/core-client.md carry
the role model and the sidebar entry.
@Birfy

Birfy commented Oct 30, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: PASS at e08fd72
Written-by: Reviewer
Attestation: e08fd72 role=Reviewer

Ran at this tip:

$ node --test core/test/project-roles.test.js core/test/projects-nav.test.js core/test/project-board.test.js
# tests 29  # pass 29  # fail 0
$ node scripts/ds-bot-patches.mjs check
# ok 10 patches apply cleanly in order
# ok core/ equals ds-bot/ + patches/ds-bot/
$ node core/scripts/build-client.mjs --check
# exit 0 (client.js matches src/client)
$ node .github/scripts/changelog.mjs check
# changelog: 7 fragment(s) in changelog.d/ are well formed
$ node .github/scripts/risk-gate.mjs --pr 1832
# risk-tier: T3; basis: core/src/host/channel.js=T3, core/src/host/project-board.js=T2, core/src/host/store.js=T2

Red first, both halves. Swapping core/src/host/project-board.js back to the base blob 6af97145 gives not ok 2 - test/project-roles.test.js / SyntaxError: … does not provide an export named 'changeRole' (# pass 17 # fail 1); back on the tip blob # pass 22 # fail 0. Removing core/src/client/projects-nav.js gives not ok 1 - test/projects-nav.test.js (# pass 0 # fail 1); restored, # pass 7 # fail 0. At this tip tests-ok is green and the only red check is enqueue-guard (no PASS named this tip until this comment).

What I read and accept:

  • Host: rolesOf derives the Owner from ownerUserIds + ownerUserId, so a record written before roles existed reads owner-only and the stored lists cannot disagree with the Owner; changeRole (core/src/host/project-board.js:68) is one policy — owner-not-assignable, last-owner, cannot-self-promote are answered before any write — and project-role reaches it through the store's endpoint table plus the channel dispatch, so the menu and the endpoint cannot drift. The listing now carries role and choices per caller (core/src/host/store.js:591).
  • Client: ProjectsNav (core/src/client/projects-nav.js) draws the expand arrow, one row per Project with the caller's role, opens the board through #project=<id> — the entry ProjectBoardOverlay already reads — and right-clicks into a portal menu that lists only the host's choices, marks the current rank (aria-checked, disabled) and quotes the host's refusal reason unchanged. Empty and unreadable lists both say so; i18n keys are in place and the CSS is installed from index.js.
  • Docs: docs/subsystems/core-host.md and core-client.md are updated in this tip, and the body carries Docs-not-needed: no, so the trailer and the docs agree. Scope is the expected 22 files (core sources, regenerated client.js and patches/, two tests, two docs, one changelog fragment); base...tip has no unrelated edits.
  • Checkpoint block is valid: pr 1832, claim Chief, issue 1828, summary 548 chars, 8 files, checks at this sha, next ready for review.

One note for the owner, not a blocker: #1828's wording was to rename the Tasks entry to Projects; this tip adds a Projects row below Tasks and records that in core-client.md:297, keeping the task board's own entry. It reads as the better call, but if a literal rename was meant, that is a one-line follow-up.

@github-actions github-actions Bot added the pass label Oct 30, 2026
@Birfy
Birfy added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit 7d21690 Oct 30, 2026
14 checks passed
@Birfy
Birfy deleted the dev/1828-project-roles branch October 30, 2026 12:32
@github-actions github-actions Bot removed the agent-pr Pull request opened by an agent label Oct 30, 2026
HugoXK pushed a commit to HugoXK/FlyBot that referenced this pull request Oct 30, 2026
…es (ThinkFlowLab#1833) (ThinkFlowLab#1834)

Written-by: Claim

Fixes ThinkFlowLab#1833
Owner: Chief
Reviewer: Reviewer
Tip SHA: 1b1bd68
Base: 7d21690
Docs-not-needed: test-only change to one timing assertion and its patch
mirror; no subsystem behaviour or docs page changes.

## What changed

The ThinkFlowLab#1786 assertion (`ThinkFlowLab#1786: a retired Bot is not brought along by a
lagging review, while a live member is`) waited for `m1`'s memory file,
then made two fixed `settle()` calls, and read whatever `requests` had
arrived. On a busy CI runner the second review (`b3`) lands after those
ticks, so the assertion saw `["m1"]` (ThinkFlowLab#1832's red `tests-ok`, ThinkFlowLab#1832,
expected `["b3","m1"]`).

It now waits for the deterministic condition: both reviews are recorded
(`requests.length >= 2`) **and** `m1`'s memory file exists, then makes
the same assertions. The fixed `settle()` calls are gone. The semantic
being protected is unchanged: a retired Bot (`b2`) must not appear in
`requests`.

Files: `core/test/retired-bot-paths.test.js` and its whole-file mirror
`patches/ds-bot/files/test/retired-bot-paths.test.js` (kept
byte-identical).

## Ran (at 1b1bd68)

- `for i in $(seq 10); do node --test
core/test/retired-bot-paths.test.js; done` — **10/10 green**, 22/22
tests each run.
- Red-first on the base tree: removing the wait entirely made the same
assertion read `[]` and fail (`not ok 21`); with only the fixed settles
removed it still passed locally, which is exactly why the CI-only race
needed a condition, not more ticks.
- `node scripts/ds-bot-patches.mjs check` — `ok 10 patches apply cleanly
in order`, `ok core/ equals ds-bot/ + patches/ds-bot/` (pin check
skipped: `generate` needs `git`, absent on this box; the mirror was
updated by hand as that message instructs).

## Not run

- `npm run verify` (full suite) — not run at this tip; only the one test
file plus the patch check were run.
- CI `tests-ok` — not yet observed; it must be green at this tip before
queueing.

## Checkpoint
<!-- flybot:checkpoint -->
{
  "pr": 1834,
  "claim": "Chief",
  "issue": 1833,
"summary": "The ThinkFlowLab#1786 retired-Bot review assertion no longer waits a
fixed number of event-loop ticks. It waits until both reviews have been
recorded (requests.length >= 2) and m1's memory file exists, then
asserts the same set. The retired Bot b2 still must not appear. Ten
consecutive local runs of the file are green; the same assertion goes
red when the wait is removed.",
  "files": [
    "core/test/retired-bot-paths.test.js",
    "patches/ds-bot/files/test/retired-bot-paths.test.js"
  ],
  "checks": {
"ran": "for i in $(seq 10); do node --test
core/test/retired-bot-paths.test.js; done && node
scripts/ds-bot-patches.mjs check",
    "result": "green",
    "at": "1b1bd68e1be9d82f45a562cf518f263624cc1b95"
  },
"next": "Wait for tests-ok at this tip, then Reviewer PASS before
queueing."
}
<!-- /flybot:checkpoint -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Projects 导航与项目角色:点项目直达看板,右键切 Owner/Maintainer/Member

1 participant