Skip to content

Make the buddy the tutor along the onboarding path - #267

Open
DavidLeuter wants to merge 23 commits into
devfrom
feature/311-buddy-onboarding-tutor
Open

DavidLeuter wants to merge 23 commits into
devfrom
feature/311-buddy-onboarding-tutor

Conversation

@DavidLeuter

@DavidLeuter DavidLeuter commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

The onboarding page and the buddy now point at the same things: buddy links land on the right step, question or phase, items carry the numbers the buddy uses, every step, question and phase can open the buddy, and the page refreshes after a confirmed buddy action. The board's second onboarding (path card, "Build my path", card blueprints) is gone.

Refs SprintStartProject/Wiki#311

Changes

Onboarding page

  • Buddy links /onboarding?step=…, ?question=…, ?phase=… open the phase, scroll to the item and highlight it briefly. They don't unfold or start anything. The highlight respects reduced motion.
  • #n numbers in the list and on graph nodes (itemNumbers), the same numbers the buddy uses.
  • "Ask your buddy" on steps, questions (with a wrong-answer variant), phase headers and the empty-generation state (buddyDrafts).
  • A step opened via /onboarding/<id> isn't started while a skip request on it is pending.

Buddy

  • Confirm payloads and proposal types for the path actions (complete_step, complete_task, answer_question, add_path_step, request_skip). The confirm shows the skip reason and the message a flag sends to the PM.
  • After a confirmed path action, announceBuddyPathChanged refreshes the onboarding page, the open step and the board's path strip. useBuddyPathSync invalidates the cached statuses and the board.
  • In-app links in buddy replies navigate in place instead of opening a new tab.
  • A draft put into the composer from outside (a chip, "Ask your buddy") takes the focus, so Enter sends it. Same fix in ChatComposer.

Step origin

  • StepOriginBadge reads the new origin: "Added with your buddy", "You added this", "Custom step by PM". Reviewers see "the hire" / "the buddy" instead of "you".

Retired from the board

  • The PATH_TO_FIRST_CONTRIBUTION card, BoardPathNotes and momentLabels.
  • "Build my path" and its generator (pathToCards, useGeneratedPathCards), the local-storage card blueprints and the "team" filter.
  • chosen on the current-task card, since nothing hands out a Task 0 any more.

Wording

  • "Onboarding metrics" reads "Contribution metrics" (texts only; routes and file names unchanged). The Task 0 toggle says it's a label, not an assignment.

Testing

  • npm run lint, npm run format:check, npm run build: green
  • npm run test: 2972 passed

Notes

Related PRs

🤖 Generated with Claude Code

DavidLeuter and others added 22 commits September 13, 2026 15:50
The buddy could not be reached from the one page it has most to say about. A hire stuck
on a step, or on a question they had now got wrong twice, was looking at the only page
in the product with nobody to ask — while a mentor sat in a dock that knew nothing
about any of it.

- `AskTheBuddy` on the phase, on every open step and on every unpassed question, plus
  the step detail page. The same mechanism the board cards use: the surface seeds a
  question, the mentor answers it with its own tools, so no surface needs action
  machinery of its own.
- The question modal hands off too, and closes on the way out — the dock renders under
  the modal, so opening it behind would have looked like nothing happening. Loudest
  under a wrong answer, which is where a second guess used to be the only thing on
  offer.
- The "no phases were generated" screen offers the conversation next to the retry.
  Generating again is the wrong hope when the corpus was what was thin; talking it
  through can actually produce something, and the mentor can offer to put the result on
  the path.
- Openings live in `buddyDrafts.ts`, in the hire's voice, pre-filled rather than sent,
  quoting a long title rather than pasting it.
- The path-changing actions announce themselves (`announceBuddyPathChanged`), so a step
  completed in the conversation is not still open on the page behind the dock.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four of the six things testing turned up were on this side.

- **Links are clickable and stay in the app.** The mentor is handed each item's path, so
  "want to take #3?" arrives as a link; `BuddyMarkdown` renders an app path through the
  router instead of opening a new tab, which would reload the SPA and lose the
  conversation. A question has no route of its own, so `?question=<id>` opens its modal
  and `?phase=<id>` selects a phase — derived from the URL rather than copied into state,
  so a link works whether it arrives on a fresh mount or on a click from the dock, and it
  stops winning as soon as the hire closes the modal or picks another phase.
- **Items carry the number the buddy uses**, from one rule (`itemNumbers`) that matches
  the Kotlin one: steps in position order, then questions. Not `position` itself — the two
  kinds carry their own, so a hire counting down one visible list would have been right
  while the data disagreed.
- **The step page refreshes itself** when the buddy changes something on it. Ticking a
  line off in the dock left the checklist behind it unchanged — the hire's own click
  looking like it had done nothing. Silent: the page is already on screen, and a spinner
  over it is a worse answer than a stale tick box.
- `complete_task` wired through the proposal payloads.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- **The member's path is normalised where it comes off the wire.** A phase without
  `questions` took the whole team page down with a TypeError: three surfaces read it
  because the type promised it, and the endpoint did not deliver. The backend now sends
  it, and the default here means no caller downstream has to defend itself against the
  same shape drift again.
- **The badge says who added a step**: the buddy, the hire, or a PM. It used to call all
  three "Custom step by PM", which is the one of the three that matters — it is what the
  team requires — so a step agreed to in a chat was arriving as an instruction from above.
- **Numbers in the graph view and on the step page**, from the same `itemNumbers` rule as
  the list. A number the buddy uses and the hire cannot see on the page they are looking
  at is worse than no number; the step page pays one path read for it, because a step
  cannot know its own place among its siblings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The origin badge also sits on the team page, where "You added this" and
"Added with your buddy" read as being about the reviewer. The reviewer view
now says "Added by the hire" and "Added with the buddy".

Also formats the branch's onboarding files with Prettier.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Picking a suggestion filled the composer but left focus on the chip, so Enter
sent nothing. Both composers now take the caret whenever text arrives from
outside rather than from typing -- chips, hand-offs from "Ask your buddy", any
of them.

A step or question link from the buddy now opens the item's phase, scrolls to
its card and lights it up briefly, instead of opening a question modal. Starting
the step or answering the question stays the hire's own click.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
request_skip's reason rides the proposal and the confirm payload like every
other action field, and the proposal shows it in full under the button, since
it goes to the PM in the hire's name. Confirming refreshes the path and the
step page, which now picks up the pending reason too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
waits_on_ids and unlocks_ids ride the proposal and the confirm payload like the
other action fields, so a step the buddy adds lands connected in its phase
graph.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The class string had the space inside the interpolated value, which the
Tailwind Prettier plugin trims when it sorts, and the className test caught it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The onboarding is the path on the Onboarding page. The board carried a
second one: a "Your path here" rail from joining to a first accepted
contribution that announced when "onboarding ended", and a "Build my
path" button that copied the path's steps into checklists once, matched
by title, and then drifted from it.

- Removed the path-to-first-contribution card and rail, and their
  moment labels.
- Removed "Build my path", pathToCards, useGeneratedPathCards and
  applyPlan, and the card blueprints it read (a localStorage
  placeholder with no editor). Cards it already made stay; their
  invisible title markers are still stripped (layout/cardNames.ts).
- The current-task card no longer tells a picked task from a handed
  one: nothing hands out tasks any more.
- The empty board points at the Onboarding page instead.
- Test fixtures moved off Task 0 and the removed card kind.

Refs #311

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
They measure joining to a first accepted contribution and review
waits, not onboarding progress, which is the path. Only visible wording
changes; the route and file names still say onboarding.

Refs #311

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dev has moved a long way: PageShell chrome, lazy routes with page
transitions, react-query for the hire's onboarding status, the Hire
Setup rework, chat filters. This branch carries the blueprint path and
the buddy that tutors along it, so the resolutions keep dev's structure
and this branch's onboarding:

- AppRouter takes dev's lazy/transition version, with the blueprint
  pages added in the same shape (dev has no /blueprints yet).
- OnBoardingPage keeps this branch's page (graph view, per-question
  modal, buddy links) inside dev's PageShell, and invalidates the
  dashboard's status query where it used to refresh the path alone.
- OnBoardingItemPage is dev's PageShell rewrite with this branch's work
  re-applied: the step's number, "ask your buddy about this step", the
  refresh after a buddy action, and "next" resolved from the shared
  resolver, which knows questions rather than phase checks.
- Phase checks and the review pool are gone here -- the blueprint
  replaced them with per-phase questions -- so dev's PhaseCheckModal,
  ReviewCheckModal and review-pool reads are not carried over.
- ChatComposer keeps both sides: dev's filter popover and this branch's
  focus handling for a value set from outside.
- The PM metrics page keeps its "contribution metrics" wording on top of
  dev's react-query refactor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two things the hire or the PM was asked to take on trust.

Asking the buddy to flag something to the PM produced a button that said "Flag
this to your PM" and nothing else. The question it composed -- the whole of what
lands in that person's inbox, in the hire's name -- was already on the proposal
and simply never rendered. A skip request has shown its reason under the button
since it existed; this is the same rule applied to the other action that leaves
the product.

The Task-0 toggle, meanwhile, still told a PM the task would be "handed to a new
hire as their very first task". It is not handed to anybody any more: the flag is
a note on the pool entry, and hires claim their own work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Keeps both sides: the path tutor from #311 and what dev brought in
(team-mode buddy, board checklist actions, PATH_STEP card, the reworked
onboarding journey). Task 0, the ramp and the path-to-first-contribution
card stay retired; dev code that still reached for them was adapted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The buddy writes `/onboarding?step=`, `?question=` and `?phase=` links
into its replies. The journey rework (#228) only knew `/onboarding/:stepId`,
so these fell through to the overview.

They are now an arrival like the others, with one difference kept on
purpose: a link lands instead of starting. The owning phase opens in the
list, the row scrolls into view and lights up once, and nothing is
unfolded or started -- following a link is finding something, starting
it is the hire's own click. The handled parameter is dropped from the
address, and following the same link again lands again.

Refs #311

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The buddy names items "#3" and a hire answers "let's do 3". The reworked
list and phase graph showed no numbers, so neither side could point at
anything. Both now carry `itemNumbers` -- steps first, then questions,
the rule `BuddyPathTools` numbers by. The list reads in graph order, so
the numbers are labels rather than a count down it.

Also keeps the link highlight working: the Tailwind Prettier plugin
trimmed the space after `app-link-highlight` and glued it to the next
class, so it now sits in its own interpolation at the end.

Refs #311

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The step page, the question dialog and the old phase header each had a
way into the conversation, and the journey rework (#228) replaced all
three. The ways in come back where the things now live:

- A step still open offers "Stuck? Ask your buddy about this step".
- A question offers the material before an attempt (louder on one
  already answered wrong), and right after a wrong answer offers to go
  through it -- the buddy is not given the answer, so it never promises
  one.
- The phase header offers a walkthrough, and an empty phase offers to
  work out with the buddy what it should contain; so does the screen
  shown when no phase could be generated.

Each only pre-fills the composer; the hire sends it.

Refs #311

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The dock confirms path actions (tick a step or a line off, answer, add a
step, request a skip) over whatever page is open, and announces it with
`announceBuddyPathChanged`. After the journey rework nothing listened
any more, so the page behind the dock kept showing the old state.

- The onboarding page re-reads the path; an open step re-reads its
  tasks and status.
- The board's "where you are" strip reads the path again.
- `useBuddyPathSync`, mounted once in the app, marks the board and the
  onboarding status queries stale, which covers the PATH_STEP cards and
  the dashboard's next-step card.

Refs #311

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The step origin badge says "You added this" to the hire. The journey
rework's member view reused it without `viewer="reviewer"`, so a PM read
a hire's own step as one they had added themselves.

Refs #311

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The buddy links a step with a pending skip request to `/onboarding/<id>`
so the hire can change or withdraw the reason, which lives in the
unfolded step. The journey page starts a waiting step when it unfolds
it, so following that link began the very step the hire asked to skip.
A step with a pending skip is now opened without being started; its own
"Start" button still starts it.

Refs #311

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A dead question link now says it was a question, the reduced-motion link
highlight goes away like the animated one instead of staying for good,
and the linkedCardId and DependencySource doc comments sit where they
belong.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@daniilperkin

Copy link
Copy Markdown
Collaborator

PR Review — Make the buddy the tutor along the onboarding path (#267)

Head faac83d1 → dev (up to date with dev, no conflicts). Gates: npm run try green — format:check, build, lint clean; unit 2909/2909, a11y 63/63 (= the 2972 in the description). Every changed file matches Prettier as committed.
Cross-checked against backend #261: numbering (steps by position, then questions by position), the ?step=/?question=/?phase= link constants, the BuddyActionType tool names vs BUDDY_PATH_ACTIONS, answer_question returning ok: true on a wrong answer, and migration V19 deleting the PATH_TO_FIRST_CONTRIBUTION board cards. All consistent.

🔴 Blocking

None.

🟠 Should fix before merge

  1. A buddy link opened from graph view never scrolls to the item. The link arrival switches the view to list (OnBoardingPage.tsx:405) and sets linkHighlight in the same batch. But the list lives inside SlidingTabPanel (AnimatePresence mode="wait"), so it only mounts after the graph's exit animation. The scroll effect (OnBoardingPage.tsx:475-480) runs on that commit, before the card exists. getElementById returns null, and nothing in the effect's deps changes afterwards to retry. The same happens on a cold load of /onboarding?step=… for a hire whose saved view is the graph. The page already has the fix for this exact case: scrollToChooser waits SLIDING_PANEL_EXIT_MS. Tests can't catch it because framer-motion is mocked globally.

  2. linkHighlight is never cleared, which causes three side effects:

    • The scroll effect depends on selectedPhaseId. Leaving the linked item's phase and coming back scrolls to that item again, long after the link.
    • The <li> keeps app-link-highlight (PhaseItemList.tsx:96), so the pulse replays every time the list remounts (phase switch, graph→list).
    • The animation uses fill-mode both (index.css:968, :984), so its last frame (box-shadow: 0 0 0 0 transparent) stays applied and beats the class styles. When the hire expands the linked item, it never gets its shadow-lg.
      Fix: clear the highlight on animationend (or on a timer), or drop both and use backwards so it doesn't stick after it ends.
  3. Confirming any buddy path action wipes unsaved text in the open step. StepWorkspace re-reads the step on every onBuddyPathChanged (StepWorkspace.tsx:104, :151). The re-read calls setSkipReason(detail.skip?.reason ?? "") and setComment(detail.feedback.comment ?? "") (:138, :141). So a half-typed skip reason or feedback comment is replaced by the server value, or "", when the hire confirms something unrelated in the dock (e.g. "answer_question" in another phase). Either refresh only tasks and status on a buddy change, or leave the draft fields alone when their value differs from the last server value. It also fetches twice: once from buddyChanges, once more when the page's refreshPath changes stepStatus.

🟡 Minor

  1. A link to the item that's already open collapses it. The link arrival always calls setExpandedItemId(null) (OnBoardingPage.tsx:444, :416 for phases). The hire is most likely to click "you're on [#3]" while #3 is open, and that throws away a typed answer or skip reason. The page protects against exactly this elsewhere (swipe is disabled while an item is expanded). Skip the collapse when expandedItemId === arrival.id.
  2. The skip-pending guard applies to every way of opening a step, not just /onboarding/<id> (OnBoardingPage.tsx:543). The list's primary button still says Start (nodeLabels.ts primaryActionLabel), so for a step with a pending skip, clicking it opens the step without starting it, and the hire has to press "Start step" again inside. Either word the button differently for that state, or limit the guard to link and route arrivals. The PR description only mentions /onboarding/<id>.
  3. Following a link while a graph item is open leaves graphItemId set. Swipe stays disabled (enabled needs graphItemId === null), and switching back to the graph reopens the old item.
  4. isInAppPath accepts /\evil.example (BuddyMarkdown.tsx:16). Browsers read a backslash as a slash, so this is protocol-relative. A normal click only throws in pushState, but opening it in a new tab (middle-click or context menu) goes off-site. The comment promises the model output is handled carefully; also reject \ in the second position. External links already opened in a new tab before this PR, so the exposure is small.
  5. The empty-generation state offers the buddy only for the first issue: askAboutEmptyPhase(generationIssues[0].title) (OnBoardingPage.tsx:861). With several empty phases, the draft names one of them.

🔵 Nits (non-blocking)

  • List numbers can appear out of order. The list is in graph order (orderedPhaseItems) but the numbers follow position order, so it can read #1, #4, #2. This is deliberate and documented, but a hire may take it for a bug.
  • Wrong icon on "You added this". StepOriginBadge.tsx:44 uses Sparkles, which the app uses for AI. That's the wrong glyph for a step the hire wrote; PenLine or UserRound would fit.
  • Unneeded fallback. (phase.questions ?? []) at OnBoardingPage.tsx:1130 guards a field the type marks required, and itemNumbers / phaseItems read it without the guard. Pick one approach; getUserOnboardingPath already normalises it at the service boundary.
  • Stale comment. boardGroups.ts:82 still says "What the generator calls the area…" — the generator is gone. The unsplitTeamArea migration is still worth keeping; only the tense is off.
  • Old localStorage data. Card blueprints saved by cardBlueprintService stay in users' browsers forever. Harmless, but a one-line cleanup is cheap.
  • Focus-grabbing effect in ChatComposer. The new effect focuses the box whenever value changes without typing. That includes a restored draft after a session switch, and on touch devices that pops the keyboard. Worth a quick check.

✅ Checked and sound

  • Deleting the old board path is complete. No references remain to PathToFirstContributionCard, BoardPathNotes, momentLabels, pathToCards, useGeneratedPathCards, card-blueprints, isFromTeam or applyPlan. readableTitle moved to layout/cardNames.ts and still strips legacy U+2060 and U+2063 characters from titles. DependencySource TEAM/BUDDY are kept on purpose for boards arranged before the change.
  • BoardCardView still renders a visible fallback for an unknown kind, and backend V19 deletes the retired card kind anyway.
  • The path-changed event: announceBuddyPathChanged fires only on result.ok for path actions. useBuddyPathSync is mounted once in AppContent and invalidates myStatuses and board.all. BoardPathWindow subscribes and unsubscribes correctly, with its cancelled guard.
  • New confirm payload fields go through all four layers: stream chunk, onActionProposal, the proposal type and performAction. Skip reason and flag-to-PM text are shown before confirming, and only for non-stored proposals.
  • The arrival key includes location.key for link kinds, so the same link clicked twice lands twice. clearLinkRef is assigned before the microtask reads it.
  • StepOriginBadge: the isAiAssisted === false fallback covers blueprint copies and rows written before origin existed. Reviewer views pass viewer="reviewer" at both call sites.
  • getUserOnboardingPath now fills in questions at the boundary, which fixes the TypeError on the team page.
  • The composer focus handoff (typedRef) doesn't fire on mount (focusOnMount handles that) or on the post-send clear.

Verdict: approve after 1–3. 4 and 5 are small and worth doing in the same pass.

@daniilperkin daniilperkin self-assigned this Sep 25, 2026
- Scroll to a linked card once the list has slid in, and only once per link
- Clear the link highlight when it has played or the phase changes; key rows by item
  so a link no longer remounts an open step, and let the pulse end with `backwards`
- Keep a half-typed skip reason or feedback comment when the buddy changes the path,
  and skip the second step read when the path catches up
- Keep the item open when a link points at it; let go of an open graph item on a link
- Hold back starting a skip-pending step only for `/onboarding/<id>` arrivals
- Reject `/\host` links; offer the buddy for every empty phase, not just the first
- PenLine icon for hire-added steps, drop an unneeded `questions` fallback, fix a stale comment

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@daniilperkin daniilperkin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed cbbe63b7 against dev (0fdb2cdd) — source diff read in full, cross-repo contract checked by hand (CI proves nothing here, it runs against current dev).

Retirement is clean. PATH_TO_FIRST_CONTRIBUTION, BoardPathNotes, momentLabels, pathToCards, useGeneratedPathCards, card-blueprints/ have zero remaining references in src/ and tests/; the kind is gone from BoardCardKind, so every exhaustive map/switch is compiler-checked. The single Build my path hit is the comment in cardNames.ts describing the retired generator.

Wire contract matches backend #261 exactly: POST /api/v1/onboarding/me/buddy/actions (hasRole('USER')), camelCase request fields (stepId, questionId, phaseId, onboardingTaskId, answer, description, reason, waitsOnIds, unlocksIds), snake_case stream fields, and the five action names byte-identical to BuddyActionType. The same DTO on dev carries a subset, i.e. the actions genuinely do not exist there yet.

Merge order: backend #261 first — merged alone against dev, the path actions would vanish silently and confirm would be rejected. AI #208 is behaviour-only (persona/prompt, step budget) and order-independent. Task-0 endpoint, reviewer path reads and phase questions are unchanged. origin on step responses is optional and StepOriginBadge falls back, so it degrades rather than crashes.

Two points worth deciding, neither a blocker:

  1. useBuddyPathSync invalidates the board only for BUDDY_PATH_ACTIONS. Confirmable board-writing actions (claim_goal, the checklist actions) still don't announce, so the stale-card symptom this PR fixes for path actions remains for those — pre-existing and one-line fixable, but it is the same class of bug.
  2. BuddyMarkdown is also used for GitHub issue excerpts (CorpusIssueBrowser.tsx:434), so the new root-relative in-app-link rule now renders an external text's /… link as an SPA navigation instead of a new tab.

Also: the branch conflicts with the current dev tip in exactly one file, BuddyComposer.tsx — #184's caret/dino handoff and this PR's external-draft caret rule overlap, so it needs a backmerge before merging. I can do that pass and resolve it by keeping both behaviours.

Approving on the code as reviewed; the ordering constraint above is the thing to respect at merge time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants