feat: add crew members page with per-member DM threads - #6210
Conversation
UX Review (Fable 5) — 🟡 CONCERNSUX-level review of UX-Verdict: CONCERNS Solid roster→DM flow with honest states, but the thread header and composer speak the wrong vocabulary for a page whose whole premise is "who am I talking to." Watch
Suggestions
[UX-REVIEWED] 7153cda |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of All mechanical checks are done. Writing the review. First-Principles-Verdict: CONCERNS Two files are reformatted wholesale with zero behavioral change riding in a feature commit, and the resume path rebuilds hydration the new thread endpoint already owns. What this change shipsIntent: give each crew member one durable, pinned DM thread on a dedicated page — an ADDITION.
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] 7153cda |
Design Review (Fable 5) — 🟡 CONCERNSDesign-level review of Design-Verdict: CONCERNS Sound feature, but the pin invariant is hand-replicated at every surface, and the lossy slug key manufactures most of the failure modes the PR then defends. Watch
Suggestions
[DESIGN-REVIEWED] 7153cda |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsFINDING -- src/kiro_crew/dashboard/handlers/members.py:38 -- function-local False positive or not applicable? A repository writer can comment: |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
c155bb0 to
502a746
Compare
502a746 to
fe3dd4d
Compare
|
UX disposition (fixes landed in fe3dd4d: single-pane narrow layout + back button, drawer overlays and starts closed below md, empty-state CTA to the crew manager, header Edit now uses the full 'Edit in crew manager' label, narrow capture now exercises the selected state — see 03b frame):
|
|
Design disposition (fix landed in fe3dd4d: GET /api/members now serializes an explicit four-field allowlist — the dataclass spread is gone, pinned by test):
|
|
First-principles disposition (subtractions landed in fe3dd4d:
|
fe3dd4d to
072d02b
Compare
|
First-principles disposition (fixes landed in 072d02b:
|
|
GPT non-blocking findings disposition (blockers fixed in 072d02b: registry-drift guard on member sends returns
|
217c330 to
fdbb886
Compare
|
GPT disposition — the single blocker from the fdbb886 round is fixed in 6db7a54: POST /api/members/{slug}/thread now rehydrates a dormant thread's canonical transcript (via |
|
GPT disposition — the single blocker from the 82e2b06 round is fixed in b253873: |
|
GPT disposition — the blocker from the b253873 round is fixed in eeb4f89: the member-thread validation now runs EARLY, before |
|
GPT disposition — the blocker from the eeb4f89 round is fixed in 7b45ada: the member-slot send guard now validates the on-disk binding alongside the existing registry-drift check. A live slot whose binding was deleted or corrupted while the tab stayed open refuses the send with a coded 409 ( |
|
GPT disposition — the blocker from the 7b45ada round is fixed in 2adf711: the OpenAI-compat send path now mirrors chat_send's binding-drift guard. A completion targeting a live member slot whose dm.json was deleted or corrupted refuses with a coded 409 ( |
|
GPT disposition — the blocker from the 2adf711 round is fixed in 7a19ded (a regression my previous fix introduced — correctly caught). A legitimate CLOSED member-thread resume passed the new early guard, |
|
GPT disposition — both blockers from the 7a19ded round are fixed in 21338a9:
Pinned by |
|
GPT disposition — the blocker from the 21338a9 round is fixed in 2185c0b, and the fix is a SWEEP, not the one site: every member-pin permission denial across the PR now emits a denied SEL event ( |
|
GPT disposition — the blocker from the 2185c0b round is fixed in 512fc75: the constructor's member-key reservation now casefolds before the prefix check, so a mixed-case squatter ( |
|
GPT disposition — the blocker from the 86f9616 round is fixed in cf91f87 (a correct consequence-catch of the previous round's casefolded constructor reservation): all four NAME-based member-prefix guards in chat_handlers now casefold to match — the send auto-create refusal, the resume app-isolation 404, and both resume pin guards (early + late barrier). A mixed-case key like |
|
GPT disposition — the blocker from the cf91f87 round is fixed in 55a9f5c: the header Edit button is now |
|
GPT disposition — the blocker from the 55a9f5c round is fixed in 2eef6ab: the agent-switch veto branch now sets |
|
GPT disposition — the blocker from the 2eef6ab round is fixed in 7153cda: |
Open PR relationship auditThis is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion. Relationship findings
No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit. |
Problem / Motivation
Crew members exist as configuration (crews in the manager, per-member spaces under
$KIROCREW_HOME/members/<slug>/since #2768), but there is no place to talk to one. Conversations with a crew member are ordinary sessions scattered through the sidebar: nothing is durable, nothing is pinned to the member's identity, and a member-initiated report has no fixed inbox.Why it matters
The autonomous-crew direction makes "a named member you converse with" the primary object. Without a per-member thread, users cannot build a working relationship with a member: every conversation starts cold, an agent switch mid-thread silently changes who is speaking, and there is no surface where a member's escalations can reliably land.
What changed (motivation → approach → change)
Goal: one durable, pinned DM thread per crew member, on a dedicated page, with configuration writes staying in the existing crew manager (this page adds no second editor).
Approach: extend the existing per-member space (
src/kiro_crew/members.py) with adm.jsonbinding, give member threads a reserved slot namespace (member-<slug>,mode="member"), and enforce the agent pin structurally rather than by convention.What was built:
Backend
members.py:dm.jsonbinding (atomic write, canonicalslot_keyenforced on both write and read — a tampered or stale binding reads as absent),member_slot_key(slug)derivation.handlers/members.py:GET /api/members(roster: crew record fields + binding + O(1)running) andPOST /api/members/{slug}/thread(idempotent get-or-create; the ONLY birthplace of member slots). Registry drift (crew renamed/deleted with a same-slug successor) fails closed withmember_pin_mismatch— keyed off the binding, so it holds even when no live slot exists after a restart.state.get_or_create_slotrefuses to mint amember-*key withoutmode="member", so every creation surface (send auto-create, slot-create, openai-compat, resume, channels) is covered by one gate instead of per-endpoint guards.slot.agent/slot.modewriter (enumerated and verified in review): send path, agent-switch endpoint, mode PATCH (member_mode_locked), openai-compat, fork (member mode never inherited), and the mid-turnEVENT_AGENT_SWITCHEDveto (visiblenoticerow + forced session reset).dm.jsonbefore slot construction; transcript metadata (an operator-editable file) never overrides the pin. A member key without a binding is skipped on restore and refused on resume. Resume additionally requires the member key to resume only its own canonical history (structural check, no metadata trust) and hydrates from the single guard-validated metadata snapshot (no validate-A/hydrate-B window).Frontend
/membersroute + sidebar entry (slotMode: 'member'claims these slots' unread counts;isChatPageSurfacekeeps them out of Sessions, now pinned by a test).MembersPage: member list (live presence via the already-subscribed WSslotsframes), the selected member's pinned thread hosted on the realChatPane(newagentLockedprop suppresses the agent picker — every selection would 409 against the pin), and a read-only detail drawer (config summary, shared-memory disclosure, Edit jumps to/capabilities?tab=crews).boundflag is never trusted as mountable).Docs:
docs/system-specs/modules/learn-cron-dashboard.mdgains thehandlers/members.pyentry.Adversarial review (pre-push)
5 rounds × two blind cross-model seats (gpt-5.6-sol + claude-opus-5), fresh context per round, ~40 findings, ~25 fixed — including four structural refactors (constructor-central reservation; fail-closed replacing an in-endpoint re-pin that skipped the real switch endpoint's invariants; binding-first restore; structural resume validation). Round-5 seats verified the full writer enumeration and every creation surface. The three round-5 fixes (bindingless drift fail-closed, openai-compat 409 mapping,
read_dm_bindingtotality) are pinned by tests but did not receive a further blind round (5-round cap).Tests
test/test_members_dm_thread.py(46 tests): binding round-trip/canonicality/totality, roster contract, thread idempotency, foreign-slot refusal (legacy squatter simulated), colliding-slug first-bound-wins, registry-drift fail-closed with and without a live slot (mutation-checked: nothing moves, binding untouched), constructor reservation (unit + slot-create endpoint + send auto-create, with an existing-thread control), resume guards (foreign transcript 409 with no landmine slot, member-mode-on-ordinary-key 409, own-transcript 200 pinned from the binding against tampered metadata), persistence restore identity (binding wins over wrong metadata; bindingless key skipped), pin enforcement at send/switch/runner-event/openai with mutation verification, mode-PATCH lock + ordinary-slot control.website/src/pages/members/MembersPage.test.tsx(11 tests): roster render/empty/error, thread open mounts the member slot withagentLocked+ pin chip, bound rows still POST, collision surfaced not mounted, stale completion isolation, error retry, drawer content + toggle, edit-jump destinations (mutation-checked).website/src/utils/channelOrigin.test.ts:isChatPageSurfacemember-exclusion pin.Manual verification
Playwright capture harness (
website/scripts/capture-members-page.mjs, self-checking: every frame asserts its state before writing). Frames below are the committed output.Screenshots / video
Roster with live presence (dark):
Pinned thread + detail drawer (dark) — pin chip, real chat stack, config summary, shared-memory disclosure:
390px narrow viewport:
Light theme parity:
Related Issues
no linked issue: built from the Crew Members design session (per-member DM threads); design doc and mockups live in the session workspace.
Known follow-ups (deliberately out of scope)
member-are skipped on restore (visible in History only), not migrated.Checklist
learn-cron-dashboard.md)main