Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
814587c
Merge blueprint rework into the prototype branch
DavidLeuter Sep 13, 2026
0b7d841
Drop the duplicated event publisher the merge left behind
DavidLeuter Sep 13, 2026
aa77356
Let the buddy see the onboarding path, and walk the hire along it
DavidLeuter Sep 13, 2026
a98aa45
Give the hire a doorway into their path
DavidLeuter Sep 13, 2026
631fe30
Make the path something a hire and a mentor can point at
DavidLeuter Sep 13, 2026
de11f4b
Tell the two "where do I stand" chips apart
DavidLeuter Sep 13, 2026
5d62220
Fix the reviewer's path view, and say who added a step
DavidLeuter Sep 13, 2026
89ce5ae
Delete the path shape nothing should reach for again
DavidLeuter Sep 13, 2026
cee2512
Recheck path actions at confirm time, and migrate the step origin
DavidLeuter Sep 14, 2026
bbac44a
Point the buddy at steps it should close, and at refreshers after a miss
DavidLeuter Sep 14, 2026
b6c8285
Let the buddy file skip requests, and respect the ones waiting on a PM
DavidLeuter Sep 14, 2026
e6632bc
Show the buddy each phase as a graph, and let it place steps inside it
DavidLeuter Sep 14, 2026
21239c9
Never add a step with no way in when the mentor names only what it un…
DavidLeuter Sep 14, 2026
ff0a0b8
Merge remote-tracking branch 'origin/dev' into feature/311-buddy-onbo…
DavidLeuter Sep 14, 2026
b95aa35
Close three gaps the buddy's path actions opened up
DavidLeuter Sep 14, 2026
9ee365d
Retire the old onboarding: Task 0, the ramp and the path card
DavidLeuter Sep 15, 2026
7407c2b
Merge origin/dev into feature/311-buddy-onboarding-tutor
DavidLeuter Sep 21, 2026
a68d1d0
Restore the PM's Task-0 flag as a label on the pool
DavidLeuter Sep 21, 2026
d4da30f
Point the work chip at the pool, not at the path
DavidLeuter Sep 21, 2026
3af4acc
Merge origin/dev into feature/311-buddy-onboarding-tutor
DavidLeuter Sep 24, 2026
856445d
Stop the buddy reading a hire's path as a line of phases
DavidLeuter Sep 24, 2026
509b645
Drop the retired ramp from comments that still described it
DavidLeuter Sep 24, 2026
028a5c7
Show the buddy what a hire got wrong, and read a phase the page's way
DavidLeuter Sep 24, 2026
2efc836
Split where-the-hire-stands out of the path tool
DavidLeuter Sep 24, 2026
2024581
Tidy the path buddy after a self-review
DavidLeuter Sep 24, 2026
0020302
Let an explicit hire request be reason enough for buddy actions
DavidLeuter Sep 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
kotlin.daemon.jvmargs=-Xmx4g
org.gradle.parallel=true
org.gradle.caching=true
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,8 @@ class AssignedIssueReader(
*
* The tracker's version of a review asking for changes: the assignee said it was ready, and
* somebody who was not them moved it back. Counting every status change instead would count the
* normal flow of work as rework, and reporting a flat zero would hand every tracked issue the
* clean-run half of the autonomy signal without it having been earned.
* normal flow of work as rework, and reporting a flat zero would hand every tracked issue a
* clean run it never earned.
*/
private fun returnedCount(statusChanges: List<JiraIssueHistoryItem>, assignee: String): Int {
var returned = 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,9 +231,10 @@ class BuddyController(
@Operation(
summary = "Confirm a buddy-proposed action",
description = "Runs an action the buddy proposed, on the hire's explicit confirmation — for example " +
"start Task 0, open the task packet, flag a question to the PM, or keep, extend, tick or reword a " +
"checklist on the hire's board. The full set is `BuddyActionType`. The action is re-scoped to the " +
"caller server-side. Returns a single line to relay; a handled failure is `ok = false`, not an error.",
"claim a task, open the task packet, tick off a step of their path, flag a question to the PM, or " +
"keep, extend, tick or reword a checklist on the hire's board. The full set is `BuddyActionType`. " +
"The action is re-scoped to the caller server-side. Returns a single line to relay; a handled " +
"failure is `ok = false`, not an error.",
)
@ApiResponses(
value = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.sprintstart.sprintstartbackend.onboarding.model.request.competency.Re
import com.sprintstart.sprintstartbackend.onboarding.model.request.starterwork.ClaimGoalRequest
import com.sprintstart.sprintstartbackend.onboarding.model.request.starterwork.CreateStarterWorkTaskRequest
import com.sprintstart.sprintstartbackend.onboarding.model.request.starterwork.PromoteStarterWorkCandidateRequest
import com.sprintstart.sprintstartbackend.onboarding.model.request.starterwork.SetTaskZeroEligibilityRequest
import com.sprintstart.sprintstartbackend.onboarding.model.response.goal.GoalView
import com.sprintstart.sprintstartbackend.onboarding.model.response.starterwork.GenerateStarterWorkResponse
import com.sprintstart.sprintstartbackend.onboarding.model.response.starterwork.RankedStarterWorkTaskResponse
Expand Down Expand Up @@ -313,6 +314,36 @@ class StarterWorkController(
@PathVariable id: UUID,
): StarterWorkTaskProposalResponse = starterWorkTaskProposalService.markReviewed(id)

/**
* Flags a live task as a good first one for somebody.
*
* A note on the task, not an assignment: the pool shows it so a PM can see at a glance which
* tasks they consider gentle starts. Nothing hands a flagged task to a hire and nothing
* withholds an unflagged one -- hires claim their own work.
*/
@Operation(
summary = "Flag a starter-work task as a good first one (Task 0)",
description = "A PM's judgement that this live task is small and safe enough to be somebody's " +
"first one. A label the pool shows, never a gate or an assignment.",
)
@ApiResponses(
value = [
ApiResponse(responseCode = "200", description = "Flag updated"),
ApiResponse(responseCode = "401", description = "Authentication required"),
ApiResponse(responseCode = "403", description = "Insufficient role"),
ApiResponse(responseCode = "404", description = "No task found with the given id"),
ApiResponse(responseCode = "409", description = "The task is no longer in the pool"),
],
)
@ResponseStatus(HttpStatus.OK)
@PostMapping("/{id}/task-zero")
@PreAuthorize("hasAnyRole('ADMIN', 'PM')")
fun setTaskZeroEligibility(
@Parameter(description = "UUID of the starter-work task to flag")
@PathVariable id: UUID,
@RequestBody request: SetTaskZeroEligibilityRequest,
): StarterWorkTaskProposalResponse = starterWorkTaskProposalService.setTaskZeroEligibility(id, request.eligible)

/**
* Takes a starter-work task out of the pool for good.
*
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,12 @@ enum class BoardCardKind(
val placement: Placement,
val takesSubject: Boolean = false,
) {
/**
* The moments between joining and a first accepted piece of work, and which have happened.
*
* Composed from contributions, not pull requests, so the wording the board carries is what
* names one unit of accepted work.
*/
PATH_TO_FIRST_CONTRIBUTION(Placement.BASELINE),

/**
* What still has to be true before this hire can work: accounts, access, a machine that builds.
*
* Baseline rather than mentor-placed for the same reason the path card is: nobody should depend
* on a model noticing that somebody has been unable to clone the repository for a week. It is
* ensured on every board read and is the one card that is *most* useful on day one, when the
* board is otherwise thin.
* Baseline rather than mentor-placed: nobody should depend on a model noticing that somebody has
* been unable to clone the repository for a week. It is ensured on every board read and is the
* one card that is *most* useful on day one, when the board is otherwise thin.
*
* It shows outstanding work; it does not withhold anything. An unsettled step never
* stops a hire claiming a task, and nothing anywhere consults these rows before serving them.
Expand Down Expand Up @@ -59,8 +50,8 @@ enum class BoardCardKind(
* The task the hire is on, and where it came from.
*
* Not part of the baseline, because it is only true some of the time — somebody with no claimed
* goal and no Task 0 is not "between tasks", they simply have no task, and a card about nothing
* is worse than no card. The mentor places it, and confirming `claim_goal` places it too.
* goal is not "between tasks", they simply have no task, and a card about nothing is worse than
* no card. The mentor places it, and confirming `claim_goal` places it too.
*/
CURRENT_TASK(Placement.MENTOR),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ enum class BuddyActionType(
val label: String,
) {
FLAG_TO_PM("flag_to_pm", "Flag this to your PM"),
CLAIM_TASK_ZERO("claim_task_zero", "Start Task 0"),
OPEN_ORIENTATION("open_orientation", "Open the task packet"),
CLAIM_GOAL("claim_goal", "Work toward this task"),
REQUEST_ATTESTATION("request_attestation", "Ask them to confirm this"),
Expand Down Expand Up @@ -41,6 +40,54 @@ enum class BuddyActionType(
*/
RECORD_ASSESSMENT("record_assessment", "Save this placement"),

/**
* The path actions, from here to [REQUEST_SKIP]: the mentor moving the hire along the curriculum
* their PM wrote.
*
* They are what turns the buddy from a second onboarding mechanism into the tutor for the first
* one. One line decides how far that goes, and it is worth stating here rather than only in the
* tool descriptions: **these touch the hire's own copy of the path, never the blueprint.** The
* curriculum belongs to the PM; a mentor that could edit it is a mentor whose team stops
* trusting it. Everything here is reversible on the hire's own page, which is what makes
* proposing them reasonable at all.
*
* Their [label]s are fallbacks. Each proposal names the actual step, the actual answer or the
* actual title, because "Confirm" over a change to somebody's onboarding is not something
* anybody should have to click blind.
*/
COMPLETE_STEP("complete_step", "Mark this step as done"),

/**
* Sends the hire's own answer to a knowledge question.
*
* The hire's words, never the mentor's. The mentor is not told which option is correct (see
* `BuddyPathTools`), so it cannot answer for them even if it tried — and the button shows the
* answer that will be sent, because an attempt is recorded whether it is right or not.
*/
ANSWER_QUESTION("answer_question", "Send this answer"),

/** Adds a step the conversation produced to a phase of the hire's own path. */
ADD_PATH_STEP("add_path_step", "Add this step to your path"),

/**
* Ticks one line off the checklist of the step the hire is on.
*
* Separate from [COMPLETE_STEP] because they are different claims, and the product treats them as
* such: a step may be finished with lines still open, and unticking a line reopens a finished
* step. A mentor that could only make the coarse claim would either tick a whole step off for one
* line of progress or do nothing at all.
*/
COMPLETE_TASK("complete_task", "Tick this off"),

/**
* Asks the hire's PM to let them skip one step of their path, with the hire's reason.
*
* A *request*, and the PM's decision -- exactly the one the step page files. The mentor helps put
* the reason into words, and the button shows the whole of it, because it is sent to a person in
* the hire's name.
*/
REQUEST_SKIP("request_skip", "Ask your PM to skip this"),

/**
* Keeps a list the mentor just wrote as a checklist card on the hire's board.
*
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ package com.sprintstart.sprintstartbackend.onboarding.external.enums
*
* This is the same discipline [CompetencySource] already applies to the ledger
* ([CompetencySource.VERIFIED] outranks [CompetencySource.ASSESSED]), extended to the evidence
* stream the ramp and the metrics read.
* stream the metrics read.
*
* Ordered weakest-last so callers can compare: [OBSERVED] is the strongest.
*/
Expand All @@ -21,6 +21,6 @@ enum class Rigor {
/** A named accountable person, never the hire, confirmed the work happened and met the bar. */
ATTESTED,

/** The hire said so, with nothing behind it. Never counts toward autonomy. */
/** The hire said so, with nothing behind it. Never counts as evidence of the work. */
DECLARED,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.sprintstart.sprintstartbackend.onboarding.external.enums

/**
* Who put a step on somebody's onboarding path.
*
* The badge on a step card used to read this off `isAiAssisted`, which has exactly 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 later a step their buddy proposed, both arrived claiming their PM had
* prescribed it. A hire who cannot tell what their team requires of them from what they agreed to in
* a chat has lost the distinction the badge exists for.
*
* Stored rather than derived, because the endpoint a step was created through is the only thing that
* knows, and nothing keeps that afterwards.
*/
enum class StepOrigin {
/**
* Copied from the blueprint, or assembled for an AI-enhanced phase. The ordinary case. A copy of
* a blueprint step the PM wrote by hand is still this, and keeps `aiAssisted = false` from its
* blueprint -- which is what the badge reads to say the team authored it.
*/
GENERATED,

/** Written by a PM, HR or an admin on somebody else's path: what the team requires. */
PM,

/** Written by the hire on their own path. */
HIRE,

/** Proposed by the buddy and confirmed by the hire. Theirs, but not their idea. */
BUDDY,
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,22 @@ data class BuddyStreamEvent(
/** `record_assessment` confirm payload: which competency, and the level in words. */
@SerialName("competency_key") val competencyKey: String? = null,
val level: String? = null,
/**
* Path-action confirm payloads: which node of the hire's own onboarding path the action is aimed
* at, the answer `answer_question` will send in the hire's own words, and the description of a
* step `add_path_step` would add.
*/
@SerialName("step_id") val stepId: String? = null,
@SerialName("question_id") val questionId: String? = null,
@SerialName("phase_id") val phaseId: String? = null,
@SerialName("onboarding_task_id") val onboardingTaskId: String? = null,
val answer: String? = null,
val description: String? = null,
/** `request_skip` confirm payload: the reason that goes to the PM. */
val reason: String? = null,
/** `add_path_step` confirm payload: where the step goes in its phase's graph. */
@SerialName("waits_on_ids") val waitsOnIds: List<String>? = null,
@SerialName("unlocks_ids") val unlocksIds: List<String>? = null,
/**
* `place_checklist` confirm payload: the list the mentor offered to keep, as it was offered.
*
Expand Down
Loading
Loading