Skip to content

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

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

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

Conversation

@DavidLeuter

@DavidLeuter DavidLeuter commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The buddy can now read the hire's onboarding path and move them along it, always through a confirm button and never by editing the blueprint. The old onboarding next to it (Task 0 as an automatic assignment, the ramp, autonomy, the path-to-first-contribution card) is retired, so the path is the one plan.

Refs SprintStartProject/Wiki#311

Changes

Path read tool (BuddyPathTools, PathStanding, PhaseReadingOrder)

  • get_my_onboarding_path, mounted only for a hire who has a path. Shows the phase they are in with its steps, questions, dependencies and locks, the checklist of the step named next, what is left, and phases that came back empty.
  • Phases run side by side: the phase they are working in is the one they last touched, and several untouched open phases are a choice, not "phase 2 next".
  • The next item follows the page's rule: a step in progress, else the first open item in the graph's reading order. PhaseReadingOrder is a port of the frontend's arrangeRows, with a parity test.
  • Items carry the number the page shows, ids for the actions, and a link (/onboarding?step=…, ?question=…, ?phase=…).
  • Steps whose checklist is done but that were never closed are flagged READY TO CLOSE.
  • A question answered wrong carries the hire's own last answer. The correct answer is never on the hire-facing shape, so the buddy never sees it.
  • The greeting snapshot opens on the path.

Path actions (BuddyPathActions)

  • complete_step, complete_task, answer_question, add_path_step, request_skip. Each is checked against the hire's own path at propose time and again at confirm time. Refusals start with NOT PROPOSED.
  • add_path_step places the step in the phase graph (waits_on / unlocks, entry inferred when missing, loops and done targets refused) and never reopens a finished phase that others wait on.
  • Path actions sit outside the project gate: a path belongs to a person, not a project.

Step origin

  • New StepOrigin (GENERATED, PM, HIRE, BUDDY) on OnboardingStep and in every step response. Steps the buddy adds are BUDDY.

Retired

  • TaskZeroService, TaskZeroController (GET/DELETE /me/task-zero), TaskZeroAssignment, claim_task_zero.
  • RampService, RampStage, AutonomyMilestone, and taskZeroAssignedAt / autonomyReachedAt in the metrics. The ledger credit inside RampService already had no caller on dev.
  • The PATH_TO_FIRST_CONTRIBUTION board card. RetiredBoardCardCleanup deletes leftover rows on startup, because a row of a retired kind would fail every board read.
  • The Task 0 flag stays as a label on the pool: POST /starter-work/{id}/task-zero now lives in StarterWorkController, same path and body.

Other

  • Buddy chips: "Where am I on my path?" is new; the pool, metrics and ledger chips are renamed so they don't compete with it.
  • Onboarding metrics are worded as contribution metrics in tool texts and docs.

Testing

  • ./gradlew check (ktlint, detekt, tests): green, 3463 tests, 0 failures.
  • New tests: BuddyPathToolsTest, BuddyPathActionTest, BuddyPathStepActionTest, PhaseReadingOrderTest, plus updates for the retired services.

Notes

  • Breaking: /me/task-zero is gone. The frontend PR already stops using it.
  • Migrations V18__add_onboarding_step_origin.sql and V19__retire_legacy_onboarding.sql are idempotent and written for ddl-auto databases, like the others. V19 drops task_zero_assignments and autonomy_milestones.
  • Merge together with the AI and frontend PRs.

Related PRs

🤖 Generated with Claude Code

DavidLeuter and others added 25 commits September 13, 2026 15:52
Combines origin/dev with Aaron's feature/blueprint-rework so the buddy
prototype can be built on the reworked blueprint model.

Conflicts resolved:
- gradle.properties: keep the larger heap from the rework, keep dev's
  UTF-8 file encoding.
- AdminProjectService: both events are published now, so both imports stay.
- OnboardingPathServiceTest: the service takes the question-attempt
  repository *and* the real position reader.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both branches added an ApplicationEventPublisher to AdminProjectService, so
the automatic merge kept two identical constructor parameters and nothing
compiled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The mentor knew about arrival steps, pull requests, competencies and the board --
everything around the onboarding path -- and nothing about the phases, steps and
knowledge questions the hire's PM actually prescribed. So "what should I do next" was
answered out of the work pool while the hire sat on a page saying something else: two
plans, and nothing saying which was the plan.

Read side (BuddyPathTools):

- `get_my_onboarding_path` gives the phase the hire is standing in in full, one named
  next thing, the titles of what is ahead, and any phase that came back empty. Mounted
  only for a hire who has a path, and the greeting's state snapshot gains the same in
  two sentences.
- Full detail for the current phase, titles only for what is ahead: a mentor that
  recites sixteen phases has produced a table of contents, and the hire has one
  already. It also bounds the prompt.
- No correct answers, because the hire-facing shape carries none -- the mentor cannot
  leak an answer it was never given. And no blended progress figure, following the rule
  the arrival tool states at length.

Write side (BuddyPathActions), each a proposal the hire confirms:

- `complete_step` -- asked for, never announced; only the hire knows they did it.
- `answer_question` -- the hire's own answer, matched to an option server-side from the
  same input at propose and confirm time, so what is recorded is what they read on the
  button. A wrong answer is a recorded attempt, not a failed action.
- `add_path_step` -- for a phase that came back empty, or something real the path does
  not mention. It writes to the hire's own copy; the PM's blueprint is untouched, which
  is the line that makes any of this safe to offer.

All three sit outside the project gate, because a path belongs to a person: a hire
onboarding on two projects still has exactly one. Nodes are resolved through the hire's
own path, which makes the lookup the authorization check too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A chip beside the composer, derived from the path tool like every other chip is derived
from its own: mounted only for a hire who has a path, so nobody is offered a way into a
plan they have not got.

It asks where they are rather than for the plan itself -- the plan is a page they
already have, and what a hire looking at an empty composer wants is the phase they are
standing in and one next thing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Five things testing turned up, all of them about the same gap: the tool said enough for
the mentor to describe the path and not enough for the two of them to act on it
together.

- **Numbers.** Each item carries the number its own page prints — steps in position
  order, then questions, which is the order the page lists them. "Let's do 3" now means
  one thing on both sides.
- **Links.** Each step, question and phase carries the path that opens it, so "want to
  take the check?" can arrive as something clickable. They are a contract with the
  router, so the constants are named and asserted rather than spelled inline.
- **Locked, with a reason.** A locked item now says what it waits on, by number and
  title, and says not to offer it. Told only "locked", the mentor agreed a hire could go
  ahead with a step their own page refuses to open.
- **The checklist of the step they are on**, with its lines and ids — the level a
  conversation actually happens at ("I've done the first two"). Only that one step, or
  the prompt would bury the path it describes.
- **`complete_task`**, so part of a step can be ticked off without claiming the step. A
  step may be finished with lines still open, and the tool text says so rather than
  letting the mentor invent a rule the product does not have.

And every path-action refusal now starts with NOT PROPOSED: a refusal written as advice
reads, from inside the model, like the offer having been made, and hires were being told
to click buttons that were never rendered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Next to the path chip the competency one was a coin toss, and they lead to different
halves of the product: one is the plan, the other is the ledger. The chips now say which.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
**The team page crashed when a PM opened a member.** `GET /onboarding/users/{id}/path`
answered with the summary shape — phases and nothing inside them — so reviewer screens
rebuilt the rest client-side, one request per phase for its steps, and could not get the
questions at all: no endpoint hands out a member's questions with their status. The
moment questions became first-class members of a phase, three surfaces read
`phase.questions` on phases that had never carried any, and the page went down with a
TypeError.

It now answers with the path *as its owner has it* — the same lock states, step statuses
and per-question marks the hire sees. Computing that once here rather than in three
clients is what makes the reviewer's view and the hire's view incapable of disagreeing.

**A step now records who put it there.** The badge read this off `isAiAssisted`, which has
two values and three answers to give: anything not AI-generated was labelled "Custom step
by PM", so a step the hire wrote themselves — and then a step their buddy proposed — both
arrived claiming their team required it. `StepOrigin` (GENERATED / PM / HIRE / BUDDY) is
set by the endpoint the step came through, which is the only thing that knows. Rows
written before the column stay recognisable by `isAiAssisted`, and the badge still falls
back to it for them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`GetOnboardingPathResponse` and its mapper had one caller, and that caller is now the
reason the team page crashed: phases with nothing inside them, which every reviewer screen
then tried to rebuild. Leaving the shape lying around is an invitation to answer with it
a second time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A proposal can sit in a conversation while the path around it changes. The
completion route does not check locks and the attempt route grades whatever it
is sent, so a late click could finish a locked step or record a second attempt
on a question already passed on the page. Both are refused at confirm now.

Adds the idempotent V18 migration for onboarding_steps.origin, matching how the
other schema changes are recorded, and corrects the origin docs: a copy of a
hand-written blueprint step is GENERATED with aiAssisted false, which is what
keeps its "Custom step by PM" badge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The path tool now names steps whose checklist is fully ticked while the step is
still open, and what each one holds up. That is the quiet way a hire gets stuck:
the work is done, the step's own button was never pressed, and whatever waits on
it stays locked. The mentor raises it and offers complete_step; the greeting
can open on it too.

A question answered wrong now carries a hint to go through the material and, if
what was missed is bigger than one explanation, offer one refresher step in that
phase -- never the answer. add_path_step's description names this third use.

Step links now land on the onboarding page (/onboarding?step=) instead of the
step's own page, matching questions and phases.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds request_skip: the buddy helps the hire put their reason into words and
proposes the same skip request the step page files, with the reason on the
confirm payload. The PM accepts or declines it through the existing review flow.
A step that already has a pending request is refused with a pointer to the step
page; asking again after a decline puts the PM's comment in front of the mentor.

A pending skip is now visible and honoured on the path tool: the step is marked
SKIP REQUESTED, never named as the next thing or as ready to close, and the PM's
review comment is shown. complete_step on such a step says on the button and to
the mentor that finishing it withdraws the request, which the completion route
does silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The path read now gives every item of the current phase what it comes after
and what it opens, lists what is open right now, and says outright that a phase
is a dependency graph, not a sequence. Told only about locks, the mentor read
the page's numbering as an order and could not place a new step anywhere but
the end.

add_path_step takes waits_on and unlocks. OnboardingStepPlacementService creates
the step and connects it in one transaction: it waits on waits_on, every item in
unlocks waits on it instead of on those, and it is drawn between its neighbours.
Loops, items from other phases and placements in front of finished items are
refused, before the button and again at confirm. A missed question's refresher
line names the exact placement in front of that question.

Also: the next item follows the page's rule (a step before a question) and puts
a ready-to-close step first; that section now gives the answer an order --
where they are, what closing it opens, then a light question with the button --
instead of leading with the button. Options are never to be narrowed down.
BuddyPathActionTest is split for size.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…locks

Testing: the hire finished #1 and asked for a step next. The mentor passed
unlocks (the question) but no waits_on, so the question was locked behind a new
step that nothing led into -- open from the start, unconnected at the end of the
phase.

PathStepPlacement.inferred fills the entry when waits_on is missing: first what
the unlocked items waited on until now (A -> B becomes A -> new -> B), else where
the hire is (the step started, or the one finished most recently). The button
names what it comes after, so an inference they did not mean is visible before
the click, and the mentor is told it was inferred.

The path read also spells out both halves of "add a step as the next thing" for
the step they are on, and the refresher line for a missed question uses the
same inference.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deleting a step left the edges that pointed at it: the join rows outlived the
step, so the delete failed or the items after it stayed locked behind a step
nobody could finish. Steps the buddy adds now sit inside the graph and the hire
is told they can delete them, so both delete paths bridge over the step instead
(A -> X -> B becomes A -> B).

Adding a step to a finished phase would reopen it and lock every phase waiting
on it, including the one the hire is working in; add_path_step refuses that and
points at the current phase. complete_task refuses a line of a locked step at
proposal and at confirm, since the task route does not check locks.

A blank expected outcome is no longer returned as [""], which rendered as an
empty bullet on the step page for every step a hire or their buddy added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Onboarding is the path a PM's blueprint prescribes, and the buddy is the
tutor along it. Beside it the backend still ran a second onboarding: a
Task 0 handed out on first read, a ramp that ended onboarding at
"autonomy", and a board card from joining to a first accepted
contribution that said "onboarding ended" on its own terms.

- Task 0 is gone: TaskZeroService and its controller, the assignment
  entity, the claim_task_zero buddy action. The current task is only
  ever the goal the hire claimed, and the task packet now reads that
  (it used to work for Task 0 alone).
- RampService, the autonomy milestone and the ramp responses are gone.
  Nothing called the ramp; autonomy was only ever read, never written.
- The PATH_TO_FIRST_CONTRIBUTION board card is gone. Stored rows of it
  would fail every board read, so RetiredBoardCardCleanup deletes them
  on startup.
- The buddy reads the path first, then setup, then work. Metrics are
  described as how their work is going, not their onboarding, and the
  metrics chip asks that.
- V19 documents the cleanup. task_zero_eligible stays mapped until the
  column has a default everywhere, or new proposals fail to insert.

Arrival steps, claiming work, the task packet, metrics and competencies
stay; they are just no longer called the onboarding.

Refs #311

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Conflicts were all about Task 0, which dev extended (starter-work
improvements) and this branch retired:

- StarterWorkTaskProposalResponse keeps dev's reviewed /
  sourceHasAssignee / sourceCheckedAt and stays without
  taskZeroEligible; the same three fields land in the test fixtures.
- MyTaskZeroResponse and TaskZeroControllerTest stay deleted.
- OnboardingPathServiceTest keeps this branch's real
  OnboardingPositionReader over dev's mock.
- gradle.properties keeps the UTF-8 file encoding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#311 removed Task 0 whole. That was right about the half that mattered: a first
task handed out automatically, with the hire's path nowhere in it, was a second
onboarding competing with the one their PM's blueprint prescribes.

It was wrong about the other half. While that work was in flight the pool UI grew
around the flag -- a "Task 0" filter, a badge on the card and a toggle in the
drawer -- and removing the field and the endpoint under it left a PM clicking a
switch that 404s.

So the flag comes back as what it now is: a PM's note that a task is small and
safe enough to start on. Nothing assigns a flagged task to anybody, nothing
withholds an unflagged one, and hires still claim their own work from the whole
live pool. The assignment, the ramp and the autonomy milestone stay gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"What should I work on?" sat next to "Where am I on my path?" and the two were a
coin toss. The mentor read it as the path's question, which is how a hire with
work waiting in the pool got told there was nothing for them.

The chip now asks for the pool and nothing else, and the tool's own description
says the pool is open however far along the path somebody is -- the same rule the
persona holds, stated where the reasoner reads it.

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 path tool put the hire in "the first phase with anything open, by
position". Blueprints open several phases at once, and the journey page
lets the hire pick: a hire who finished phase 1 and chose phase 3 was
told to start phase 2, and told again after correcting the buddy.

Where they stand now follows the page's own rule (`resolveNextAction`):
the phase they have started, most recently touched first; else the only
open phase; else a choice between the open phases, each with where it
starts. Other open phases are named as open alongside, "still left"
includes lower-numbered phases, and the greeting says the same. A phase
picked on the page but not started yet is not stored anywhere, so the
choice text tells the buddy to take the hire's word for it.

Refs #311

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comments only. #311 removed the ramp and the idea that onboarding ends
at a first accepted contribution; five comments still pointed at it.

Refs #311

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two things the path read told the mentor too little or differently from
the page:

- A missed knowledge question said only "answered wrong before". It now
  carries the hire's own last answer -- their words, or the options they
  picked -- so the tutoring can start from what did not land. Never the
  correct answer: that still is not on the hire-facing shape.
- The next thing in a phase was every open step by position before any
  question. The page takes a step in progress first, then the first open
  item in graph reading order, questions included. `PhaseReadingOrder`
  ports that order from the frontend's `arrangeRows`, and a test pins it
  to an ordering the frontend produced.

Refs #311

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
detekt flagged `BuddyPathTools` as too large once it learned phases run
side by side. Deciding where the hire stands and what comes next in a
phase moves to `PathStanding.kt`, next to `PhaseReadingOrder`; the path
tool keeps turning that into text. No behaviour change.

Refs #311

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The checklist the path tool shows is now the one of the step it names
next (same reading-order rule as the page) rather than the first open
step by position. Path actions reuse hasPendingSkip, dispatch explicitly
instead of through an else branch, and a few stale doc references
(BuddyActionService -> BuddyPathActions, "three" path actions) and
awkward wraps are fixed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
flag_to_pm was described only as a last resort for questions the docs do
not cover, so the mentor refused a hire's explicit "flag this to my PM" as
not being a PM matter. Its description now names the hire asking as its
own case, and accepts a problem or feedback as well as a question.

add_path_step, place_checklist and place_note had the same shape (only
the mentor's judgement could trigger them) and now also say that the
hire asking is enough.

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

Copy link
Copy Markdown
Contributor Author

Follow-up fix (0020302): an explicit hire request is now enough for buddy actions

While testing, the buddy refused a direct "flag this to my PM" and replied that it didn't sound like something for the PM. The cause was the flag_to_pm tool description. It only allowed the tool "as the last resort when you genuinely cannot ground an answer", and the parameter was described as a question only. Nothing said the hire could simply ask for it.

Changes:

  • flag_to_pm now lists the hire asking as its own case: "their asking is the reason", and the model must never decide for them that something isn't a PM matter. The question field now also covers a problem or feedback, not just a question.
  • add_path_step, place_checklist and place_note had the same pattern, where only the model's own judgement could trigger them. Each now also says that the hire asking is enough.
  • New test in BuddyActionServiceTest pins the flag_to_pm wording.

The persona side is in SprintStartProject/sprintstart-ai#208 (8f6adc3). Verified manually: the buddy now offers the flag button when asked. ktlint/detekt are clean. The full suite shows only the two known local OnDiskOperationsTest failures.

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.

1 participant