Coaching Programs and Group Challenges
Lets a coach create a named coaching space scoped to a squad roster, and opens each program's live board: its shared topic sprint (research, quests, task routing, progress, notes), its friendly group-challenge standings, and (once a member has a flowed practice round) their generated drill set.
- Route:
/coaching-programs - Nav: the Tools page's Coaching & Analytics group; the Reason Editor's
Workspace menu (
t programsin Ctrl/Cmd-Shift-Space's command palette) - Package:
debate-round
What it shows
A form to create a coaching space (name + comma-separated squad-roster member
IDs), then every persisted CoachingProgramConfig with a roster badge list
and a "Remove" action. Each program also has a "View board" action that opens
a topic input and a "Member flows" roster; once a topic is entered, the
program's live board renders as a status block:
- The coaching space's name and member count
- The topic sprint's status line (quest progress, task routing, active contributors, open follow-up notes)
- Each open group challenge's live standings line
- How many members currently have a generated drill set
The "Member flows" roster lets a coach record, per roster member, the round
workspace's currently selected flow as that member's practice round: enter a
side key (e.g. A/N) and click "Save current flow". A member with a
recorded flow shows a "Flow recorded" badge and gets a "Clear" action; the
board's per-member drill count updates the next time it's composed.
Data flow
state/coachingPrograms.ts (localStorage: coachingPrograms)
→ buildCoachingProgramsPanelView() — every persisted CoachingProgramConfig,
name-sorted
→ panels/CoachingProgramsPanel.tsx — renders the config form + list
→ apps/debate-ai.com/app/coaching-programs/page.tsx — mounts the panel as a route
Opening a program's board (topic entered in the panel):
panels/CoachingProgramsPanel.tsx
→ buildPersistedCoachingProgramBoard(programId, topic, now) — state/persistedCoachingProgramBoard.ts
→ getCoachingProgram(programId) — state/coachingPrograms.ts
→ readPersistedTopicSprintInputs(topic) — debate-team-collaboration's state/topicSprints.ts
→ listGroupChallenges() — debate-team-collaboration's state/groupChallenges.ts
→ listContributions() (filtered to a submittedAt timestamp) — debate-research-evidence's state/contributions.ts
→ listChallengeWinEvents() — debate-team-collaboration's state/challengeWinEvents.ts
→ buildCoachingProgramMemberFlows(program.memberIds) — state/roundContributorFlows.ts (default; overridable)
→ buildCoachingProgramMemberPracticeRounds(program.memberIds) — state/roundContributorFlows.ts (default; overridable)
→ buildCoachingProgramBoard({ program, topicSprint, challenges, contributions, winEvents, memberFlows, memberPracticeRounds })
— round/coaching-program.ts
→ buildCoachingProgramSummaryText(board) — round/coaching-program.ts
→ panel renders it as a status block, plus a per-member Practice Round Simulator badge
Recording a member's flow ("Save current flow" in the panel's Member flows
roster):
panels/CoachingProgramsPanel.tsx (reads state/store.ts's useFlowStore directly)
→ buildAndSaveRoundContributorFlow(flow, roundId, contributorId, sideKey)
— state/roundContributorFlows.ts
→ board recomposed via buildPersistedCoachingProgramBoard aboveThis closes the "(b-continued)" follow-up named under idea #13 ("Coaching
Programs and Group Challenges") in TODO.md: "wiring CoachingProgramsPanel
(in debate-round) to render a program's full buildCoachingProgramBoard
off this and the topic-sprint composition." state/persistedCoachingProgramBoard.ts
composes every one of buildCoachingProgramBoard's inputs directly from its
own persisted store, mirroring debate-team-collaboration's state/topicSprints.ts
and state/prepRooms.ts "compose every input from its own store"
convention, so the panel doesn't need to assemble a topic sprint, challenge
roster, contribution feed, win-event list, or member-flow roster itself. The
new state/roundContributorFlows.ts supplies that last piece — a
contributorId-keyed store of each member's currently recorded, already-flowed
practice round (roundId + sideKey + Flow), which
buildPersistedCoachingProgramBoard reads by default (still overridable by
an explicit memberFlows argument, e.g. for tests). The panel's "Save
current flow" action is the one place this package reads the live round
workspace's useFlowStore directly (every other panel here is otherwise
self-contained), recording that flow against a chosen roster member and a
free-form side key.
state/roundContributorFlows.ts's buildCoachingProgramMemberPracticeRounds
closes idea #13's remaining "(c) wiring a member's practice-round
setup/feedback (Practice Round Simulator) into the space" follow-up: a
roster member's recorded roundId already names the same id
state/practiceRounds.ts keys its PracticeRoundRecords by, so this just
joins the two stores — no separate contributorId-keyed practice-round store
was needed. buildPersistedCoachingProgramBoard reads it by default (again
overridable by an explicit memberPracticeRounds argument), and
round/coaching-program.ts's buildCoachingProgramBoard composes the result
into the board's new memberPracticeRounds map. The panel shows a
"Practice round recorded" (or "Practice round + feedback" once feedback has
been generated) badge per roster member alongside the existing "Flow
recorded" badge — a member starts a Practice Round Simulator session
separately at /practice-round, then records that same round's flow here to
surface it on the board.
Roster analytics dashboard
Closes idea #13's own follow-up in TODO.md: "A coach-facing roster
analytics dashboard (completion rates, streaks, standings in one place)."
Before this, a coach had to visit debate-team-collaboration's Group
Challenges panel (/cards/group-challenges) for a squad's challenge
standings and debate-community's Quest Streaks panel (/cards/streaks)
for a contributor's daily-quest streak separately. The /coaching-programs
route now also renders a Roster Analytics section below the existing
program list/board panel: pick one of the coach's persisted coaching
programs and see every roster member's group-challenge standing (challenges
completed/participated, how many they're currently leading) and daily-quest
streak (current, longest, milestone badges) in one table, ranked by total
challenge-matching activity, then current streak length.
panels/CoachingProgramRosterAnalyticsPanel.tsx (debate-community package)
→ buildCoachingProgramsPanelView() — debate-team-collaboration's state/coachingPrograms.ts (program picker)
→ buildPersistedCoachingProgramRosterAnalytics(programId, now) — state/coachingProgramRosterAnalytics.ts
→ getCoachingProgram(programId) — debate-team-collaboration's state/coachingPrograms.ts
→ buildPersistedGroupChallengeBoard(now) — debate-team-collaboration's state/challengeWinEvents.ts
→ buildCoachingProgramRosterAnalytics(memberIds, challengeBoard, missionResultsForContributor, dayKey)
— lib/coaching-program-roster-analytics.ts
→ summarizeMemberChallengeStanding(contributorId, challengeBoard)
→ buildContributorQuestStreak(contributorId, results, dayKey) — lib/gamified-quests.ts
→ listDailyMissionResultsForContributor(contributorId) — state/dailyMissionResults.tsThis composition lives in the debate-community package
(packages/debate-contributor-progress), not debate-team-collaboration
(the package the rest of Coaching Programs lives in), because
debate-community already depends on debate-team-collaboration (for
daily-quests.ts) — the reverse dependency would be circular. The pure
lib/coaching-program-roster-analytics.ts slice reuses
debate-team-collaboration's already-computed GroupChallengeProgress
(no separate standings computation) and this package's own
gamified-quests.ts streak logic directly, mirroring this same package's
existing unlock-streak-status.ts "tie two ideas' pure slices together"
precedent. apps/debate-ai.com/app/coaching-programs/page.tsx mounts the
new panel alongside the existing CoachingProgramsPanel, so both are
reachable from the same already-linked /coaching-programs tool page — no
new nav/catalog entry was needed.
Also subscribes to the browser's storage event via debate-research-evidence's
state/live-update.ts's new isCoachingProgramRosterAnalyticsLiveUpdateStorageEvent,
so a challenge created/completed, a win recorded, or a mission result saved
in another browser tab refreshes the rendered roster here too.
Roster challenge digest
Closes idea #13's own further follow-up in TODO.md: "A digest notification
summarizing challenge results instead of requiring a panel visit." Before
this, seeing a squad's own completed group challenges meant either visiting
debate-team-collaboration's Group Challenges panel directly or spotting the
one-line announcement state/newsStream.ts's groupChallengeNews() already
auto-posts to the Community feed for every completed challenge across
every roster — not scoped to any one coaching program. The Roster Analytics
section now also renders a Recent challenge results list below the
roster table: every completed group challenge whose own roster overlaps the
selected program's roster, newest first, each line naming the challenge and
its MVP (buildChallengeCompletionAnnouncementText, reused directly from
group-challenges.ts rather than a second summary-text formatter), capped to
the 10 most recent with a "showing N of M" note once there are more.
panels/CoachingProgramRosterAnalyticsPanel.tsx (debate-community package)
→ buildPersistedCoachingProgramChallengeDigest(programId) — state/coachingProgramRosterAnalytics.ts
→ getCoachingProgram(programId) — debate-team-collaboration's state/coachingPrograms.ts
→ buildCompletedGroupChallengeEvents() — debate-team-collaboration's state/challengeWinEvents.ts
(feed-wide; each event now also carries the challenge's own memberIds)
→ buildCoachingProgramChallengeDigest(memberIds, completedEvents)
— lib/coaching-program-roster-analytics.ts
(keeps only events whose memberIds overlap the roster)
→ buildChallengeCompletionAnnouncementText(entry) — debate-team-collaboration's lib/group-challenges.tsCompletedGroupChallengeEvent (debate-team-collaboration's
state/challengeWinEvents.ts) gained a memberIds field — the completed
challenge's own roster — since the feed-wide event record otherwise had no
way to tell a coaching-program-scoped digest which roster it belonged to;
state/newsStream.ts's groupChallengeNews() is unaffected by the added
field. The digest composition lives alongside the existing roster analytics
in debate-community for the same circular-dependency reason described
above, and refreshes on the same storage-event subscription (the digest's
own two source keys, groupChallenges/challengeWinEvents, were already in
COACHING_PROGRAM_ROSTER_ANALYTICS_LIVE_UPDATE_STORAGE_KEYS, so no changes
were needed there).
Program calendar
Closes idea #13's own further follow-up in TODO.md: "A calendar/schedule
view across a program's drills, sprints, and challenges." Before this, seeing
a program's actual dated events meant piecing together the Group Challenges
panel's own start/end dates and a topic sprint's note thread separately — the
Roster Analytics section's own digest above only ever showed completed
challenges, not the still-open windows a coach might want to plan around. The
Roster Analytics section now also renders a Program calendar section
below the challenge digest: a day-grouped, chronological list of every
roster-scoped group challenge's start and end date, the signed-in coach's own
scheduled drill review reminders, plus — once a topic is typed into the
section's own topic field — that topic's sprint notes, dated by when they
were logged.
app/coaching-programs/CoachingProgramRosterAnalyticsWithDrills.tsx (apps/debate-ai.com, "use client")
→ useDrillSets() — debate-practice-rounds' hooks/useDrillSets.ts
→ buildDrillReviewCalendarEvents(drillSets) — debate-practice-rounds' state/drillSets.ts
(one { dayKey, label, detail } per scheduled
drill review — dependency-free of debate-community)
→ <CoachingProgramRosterAnalyticsPanel drillReviewEvents={...} />
panels/CoachingProgramRosterAnalyticsPanel.tsx (debate-community package)
→ buildPersistedCoachingProgramCalendar(programId, topic, drillReviewEvents) — state/coachingProgramCalendar.ts
→ getCoachingProgram(programId) — debate-team-collaboration's state/coachingPrograms.ts
→ listGroupChallenges() — debate-team-collaboration's state/groupChallenges.ts
→ listSprintNotesForTopic(topic) — debate-team-collaboration's state/sprintNotes.ts (only when topic is non-blank)
→ buildCoachingProgramCalendarEvents(memberIds, challenges, sprintNotes, drillReviews)
— lib/coaching-program-calendar.ts
(challenges narrowed to ones whose own
memberIds overlaps the roster, same rule
buildCoachingProgramChallengeDigest uses;
drillReviews merged in as-is, unfiltered by
roster — a drill set has no roster concept)
→ groupCoachingProgramCalendarEventsByDay(events) — lib/coaching-program-calendar.ts
→ panel renders one heading per day, a badge per event kindA topic sprint isn't itself dated the way a challenge window is (startsAt/
endsAt), so the calendar's sprint-note events come from a topic the coach
types into the section — leaving it blank still shows the program's
challenge-window and drill-review events, just with no note events. The
composition lives alongside the existing roster analytics/digest in
debate-community for the same circular-dependency reason described above,
and refreshes on the same storage-event subscription — sprintNotes was
added to COACHING_PROGRAM_ROSTER_ANALYTICS_LIVE_UPDATE_STORAGE_KEYS so a
note logged in another tab (or from debate-team-collaboration's own
TopicSprintPanel) refreshes the calendar here too.
Per-drill scheduled review reminders (debate-practice-rounds'
state/drillSets.ts#scheduledReviewAt) — the "drills" part the original
follow-up named alongside sprints and challenges — are folded in via the
panel's optional drillReviewEvents prop rather than read inside
debate-community directly: debate-practice-rounds already depends on
debate-community (for Progress Unlocks tiers), so importing it back here
would be circular. Instead CoachingProgramRosterAnalyticsWithDrills.tsx (the
app/page layer, which already depends on both packages) resolves the
signed-in coach's own drill sets via useDrillSets() and
buildDrillReviewCalendarEvents, into a dependency-free
{ dayKey, label, detail } shape debate-community's calendar types accept
without knowing anything about Drill/DrillSetRecord. A drill set has no
roster/membership concept the way a GroupChallenge does, so these events
are the viewing coach's own scheduled reviews, not roster-wide — every
other calendar entry (challenges, sprint notes) is roster/topic-scoped, so a
coach viewing a program they're not personally drilling for sees no
drill-review events, which is expected.
Per-member drill/practice-round status
Closes the first of the two items this doc's Known gaps used to name here:
"the roster analytics table only covers group-challenge standings and
daily-quest streaks — it doesn't yet fold in drill-completion rate or
practice-round counts." The Roster Analytics table now has two more columns
per member: Drills completed (N/M from that member's persisted drill
set, or "—" with none generated yet) and Practice round ("Recorded" /
"+ feedback" / "—"), reusing the exact same roundId-to-contributor mapping
(debate-team-collaboration's state/roundContributorFlows.ts) that already
drives the "Flow recorded"/"Practice round recorded" badges on
CoachingProgramsPanel's own board above.
app/coaching-programs/CoachingProgramRosterAnalyticsWithDrills.tsx (apps/debate-ai.com, "use client")
→ listRoundContributorFlows() — debate-team-collaboration's state/roundContributorFlows.ts
(every contributor's currently recorded { contributorId, roundId })
→ buildContributorDrillCompletionStats(contributorRoundIds, drillSets)
— debate-practice-rounds' state/drillSets.ts
(joins each contributorId's roundId against that
store's own persisted DrillSetRecords by roundId)
→ buildCoachingProgramMemberPracticeRounds(contributorIds) — debate-team-collaboration's state/roundContributorFlows.ts
(same join CoachingProgramsPanel's own board already uses)
→ <CoachingProgramRosterAnalyticsPanel memberDrillPracticeStatus={...} />
panels/CoachingProgramRosterAnalyticsPanel.tsx (debate-community package)
→ renders "Drills completed"/"Practice round" columns from the
caller-resolved memberDrillPracticeStatus map, keyed by contributorIdLike drillReviewEvents above, this composition can't live inside
debate-community itself — debate-practice-rounds already depends on this
package (for Progress Unlocks tiers), so importing drill types back here
would be circular — so the app/page layer, which already depends on both
debate-practice-rounds and debate-team-collaboration, resolves it instead
into a dependency-free memberDrillPracticeStatus prop
({ completedDrills, totalDrills, practiceRoundRecorded, practiceRoundHasFeedback },
keyed by contributorId) the panel just renders. Unlike the ad hoc Drill[]
CoachingProgramsPanel's own board generates fresh from a member's flow on
every render, "Drills completed" reads the actually-persisted, completion-tracked
DrillSetRecord a member gets by separately generating (and practicing
against) their drill set at /drills — a member with a recorded flow but no
persisted drill set for that same roundId still shows "—" here. Every
contributor's recorded round is resolved regardless of which coaching
program's roster the viewer currently has selected (the roster filtering
happens implicitly — the table only ever renders rows for the selected
program's own memberIds), so this isn't scoped to the viewing coach's own
account the way drillReviewEvents is.
Cross-tab live update
CoachingProgramsPanel (the program list/board panel this doc otherwise
covers, as opposed to the roster analytics panel above, which already had its
own storage-event subscription) only refreshed its program list, roster
"Flow recorded" badges, and open board on mount or right after its own
create/remove/record/clear actions — a coach creating or removing a coaching
program, or recording/clearing a roster member's flow, in another open tab
never showed up here without a manual reload. This is the recurring "every
other localStorage-backed panel in this repo still has no cross-tab
live-update mechanism" gap tracked in shared-flow-sync.md.
New state/live-update.ts (the first in this package) adds
COACHING_PROGRAMS_PANEL_LIVE_UPDATE_STORAGE_KEYS/
isCoachingProgramsPanelLiveUpdateStorageEvent, covering the two stores the
panel reads directly regardless of which board is open: coachingPrograms
(the persisted program-config list) and roundContributorFlows (each roster
member's recorded-flow badge). CoachingProgramsPanel.tsx now subscribes to
window's storage event and, when the predicate matches, refreshes the
program list, re-derives the roster's recorded-contributor set, and — if a
board is currently open — recomposes it.
An open board also transitively depends on several other packages' stores
(topic-sprint inputs, the group-challenge roster, the contribution feed, win
events, and practice-round records, all read through
state/persistedCoachingProgramBoard.ts) that this predicate deliberately
leaves out, matching every other panel's "cover the store(s) this panel reads
directly, not everything a composed view transitively depends on" convention
(e.g. JudgeDecisionPanel's hook-scoped predicate). A change to one of those
deeper stores in another tab still shows up the next time the board is
reopened (toggle "Hide board" then "View board" again) or the topic field is
re-entered — it just doesn't force a live recompose while already open.
Vitest-covered by packages/debate-team-collaboration/test/live-update.test.ts
(every backing-store key, the null-key clear-all case, and
unrelated/substring-matching keys staying ignored, mirroring every other
package's live-update.test.ts cases).
Known gaps
- Programs sync to a signed-in user's account as the
coachingProgramscollection (see Tool Data Sync), so they follow that coach across devices — but they are still per-user, so a roster is not yet shared with the members on it. - The program calendar's drill-review events are scoped to the viewing coach's own drill sets, not the roster's — there's no per-member drill data a coach can see for teammates today. Widening this to a real roster-wide view would need drill sets to carry an owning contributor id and a way to look them up across the roster, neither of which exists yet.
- An open program board does not live-update against the topic-sprint,
group-challenge, contribution, win-event, or practice-round stores it
transitively composes (see "Cross-tab live update" above) — only the
program list and roster "Flow recorded" badges do. Widening the predicate
to cover the full composed board would need
state/live-update.tsto import every one of those stores' own keys (several from other packages), left as a follow-up.
