Skip to content

Milestone#162

Open
nanaabdul1172 wants to merge 2 commits intoboundlessfi:mainfrom
nanaabdul1172:milestone
Open

Milestone#162
nanaabdul1172 wants to merge 2 commits intoboundlessfi:mainfrom
nanaabdul1172:milestone

Conversation

@nanaabdul1172
Copy link
Copy Markdown

@nanaabdul1172 nanaabdul1172 commented Mar 27, 2026

closes #142

Implemented: Model 4 multi-winner milestone flow is now functional on the frontend, including join, submit, sponsor review, funnel advancement, and profile claim visibility.

Summary by CodeRabbit

  • New Features
    • Added milestone-based bounties with multi-stage progression for contributors and sponsors
    • Contributors can join milestone flows, submit work for each stage, and track completion status
    • Sponsors can review submissions and approve/reject milestone progress with feedback
    • New UI displays milestone progression, participant counts, and pending submissions queue
    • Updated bounty call-to-action to "Join Milestone Flow" for milestone-type bounties

@drips-wave
Copy link
Copy Markdown

drips-wave bot commented Mar 27, 2026

@nanaabdul1172 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 27, 2026

📝 Walkthrough

Walkthrough

This PR implements multi-winner milestone-based bounty flow with state persistence to localStorage. It introduces a new useMilestoneFlow hook for managing milestone progression, a milestone flow UI component for displaying and managing participant submissions, type definitions for the milestone domain, integration into the bounty detail page with conditional rendering, and profile page support for displaying milestone-based claims. The feature supports contributors joining milestone stages, submitting work, sponsor review/approval workflows, and participant advancement with capacity constraints.

Changes

Cohort / File(s) Summary
Type Definitions
types/milestone-flow.ts
New TypeScript interfaces and types defining milestone domain: MilestoneDefinition, MilestoneParticipant, MilestoneSubmissionEntry, MilestoneFlowState, and status union types (MilestoneParticipantStatus, MilestoneSubmissionReviewStatus).
Milestone Flow Hook & Tests
hooks/use-milestone-flow.ts, hooks/__tests__/use-milestone-flow.test.ts
New hook useMilestoneFlow managing milestone state via localStorage with operations for joining, submitting, and reviewing submissions. Hook computes derived state (pendingSubmissions, stageOccupancy, participantByContributor). Test suite validates participant lifecycle: joining, submission, approval/rejection, and advancement logic.
Milestone Flow UI Component
components/bounty-detail/bounty-detail-milestone-flow-card.tsx
New client component rendering multi-milestone bounty UI with contributor join/submit actions and sponsor review queue. Derives current user from auth session, validates organization membership, displays milestone progress, pending submissions, and status messages.
Bounty Detail Conditional Rendering
components/bounty-detail/bounty-detail-submissions-card.tsx, components/bounty-detail/bounty-detail-sidebar-cta.tsx
Updated BountyDetailSubmissionsCard to conditionally render BountyDetailMilestoneFlowCard when bounty.type === "MILESTONE_BASED", otherwise render standard submissions. Extracted standard logic to BountyDetailStandardSubmissionsCard. Updated CTA labels to show "Join Milestone Flow" for milestone-based bounties.
Profile Page Milestone Integration
app/profile/[userId]/page.tsx
Added getMilestoneClaimForUser helper to read milestone participation state from localStorage and derive claim status. Extended myClaims computation to merge milestone-based bounty claims via Map, overriding entries for bounties with type === "MILESTONE_BASED".
Dependency
package.json
Added optional dependency on @tailwindcss/oxide-linux-x64-gnu@4.2.0.

Sequence Diagram(s)

sequenceDiagram
    participant Contributor
    participant UI as Milestone Flow Card
    participant Hook as useMilestoneFlow
    participant Storage as localStorage
    
    Contributor->>UI: Click "Join Milestone Flow"
    UI->>Hook: joinFlow(contributorId, name)
    Hook->>Hook: Validate capacity & no duplicate
    Hook->>Hook: Create participant at stage 0
    Hook->>Storage: Persist state (milestone_flow_*)
    Hook-->>UI: Return updated state
    UI->>Contributor: Show "ACTIVE" status
    
    Contributor->>UI: Submit milestone with PR URL
    UI->>Hook: submitMilestone(contributorId, url)
    Hook->>Hook: Create PENDING submission
    Hook->>Storage: Persist updated state
    Hook-->>UI: Return pending submission
    UI->>Contributor: Show "SUBMITTED" status
Loading
sequenceDiagram
    participant Sponsor
    participant UI as Milestone Flow Card
    participant Hook as useMilestoneFlow
    participant Storage as localStorage
    
    Sponsor->>UI: View pending submissions queue
    UI->>Hook: Access pendingSubmissions
    Hook-->>UI: Return filtered submissions
    UI->>Sponsor: Display review list
    
    Sponsor->>UI: Review submission (APPROVED)
    UI->>Hook: reviewSubmission(submissionId, decision)
    Hook->>Hook: Update submission with decision
    Hook->>Hook: Advance participant to next stage<br/>(or mark COMPLETED if final)
    Hook->>Storage: Persist updated state
    Hook-->>UI: Return updated participant
    UI->>Sponsor: Confirm advancement
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • Benjtalkshow

🐰 Milestone by milestone, we hop along,
Where contributors join the bounty throng,
From stage to stage with effort true,
Parallel winners all chase the view,
A funnel refined—completion in sight!

🚥 Pre-merge checks | ✅ 1 | ❌ 4

❌ Failed checks (3 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR implements core milestone flow functionality (hook, state management, profile claims, UI card, sidebar label) but omits several required components: milestone-funnel, multi-winner-apply, milestone-progress-tracker, and multi-winner-management. Implement the missing components (milestone-funnel.tsx, multi-winner-apply.tsx, milestone-progress-tracker.tsx, multi-winner-management.tsx) specified in issue #142 to complete the feature.
Out of Scope Changes check ⚠️ Warning Changes are within scope but include an out-of-scope optional dependency addition (@tailwindcss/oxide-linux-x64-gnu) that is unrelated to the milestone flow feature. Remove the tailwindcss oxide optional dependency from package.json unless explicitly required for the milestone flow feature; it should be a separate PR.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title 'Milestone' is extremely vague and does not convey meaningful information about the changeset, which implements a complete multi-winner milestone-based bounty flow. Consider renaming to a more descriptive title such as 'Implement multi-winner milestone flow UI and state management' to clearly summarize the primary change.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 27, 2026

@nanaabdul1172 is attempting to deploy a commit to the Threadflow Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/bounty-detail/bounty-detail-milestone-flow-card.tsx`:
- Around line 289-358: The sponsor queue currently only calls
handleReview(submission.id, "APPROVED"/"REJECTED") and lacks a per-stage
payout/escrow-release step; add a release/capture flow that runs when a sponsor
approves. Update the UI in bounty-detail-milestone-flow-card to either replace
or supplement the Approve button with an "Approve & Release" action that calls a
new handler (e.g., handleSponsorApprove or extend handleReview to accept a
"RELEASE" action), and implement that handler to: 1) call the backend endpoint
that performs the escrow release/transaction capture (suggested function names:
releaseMilestonePayout, captureTransaction, or markMilestonePaid) using
submission.id and submission.milestoneIndex, 2) update local state.milestones
and pendingSubmissions to reflect paid/audited status, and 3) surface errors via
the existing error/logging flow; keep the Reject path unchanged. Ensure you
reference submission.id, submission.milestoneIndex, pendingSubmissions,
state.milestones, and handleReview when making the changes.

In `@components/bounty-detail/bounty-detail-sidebar-cta.tsx`:
- Around line 38-40: The CTA label change for MILESTONE_BASED bounties is
correct, but the click handlers still open bounty.githubIssueUrl; update the
primary and secondary click handlers in bounty-detail-sidebar-cta.tsx so that
when bounty.type === "MILESTONE_BASED" they invoke the milestone flow handler
instead of navigating to bounty.githubIssueUrl — i.e., replace calls that open
or link to bounty.githubIssueUrl with the existing milestone flow entry point
(e.g., openMilestoneFlow / joinMilestoneFlow / onOpenMilestoneModal) used
elsewhere in the app, and apply this change for all instances noted (the other
CTA handlers in the same file). Ensure the conditional uses bounty.type ===
"MILESTONE_BASED" to select the milestone handler and leaves non-milestone
behavior unchanged.

In `@hooks/use-milestone-flow.ts`:
- Around line 13-45: The hook hardcodes milestone definitions via
createDefaultMilestones and createInitialState so every bounty gets the same
8→4→2 and 30/30/40 payout regardless of real bounty config; change the API to
accept milestone definitions from the caller: update useMilestoneFlow to accept
an optional initialMilestones parameter (or a full config object), modify
createInitialState to accept that milestones array (falling back to
createDefaultMilestones()), and update createDefaultMilestones to support
parameterized overrides if needed; ensure all places constructing the state (and
tests) pass the real milestone defs so UI and capacity checks use the
bounty-specific funnel.
- Around line 64-67: The milestone flow state is stored client-side with
useLocalStorage
(`useLocalStorage<MilestoneFlowState>(\`${FLOW_KEY_PREFIX}${bountyId}\`,
createInitialState(bountyId))`), which makes joins/reviews/submissions
local-only and user-editable; change this to a shared, server-backed state:
replace the useLocalStorage usage for MilestoneFlowState with API-backed
persistence (e.g., load/save via your milestones endpoints using bountyId) and
add real-time sync (websocket/subscribe) so updates to the state are written
through the server and broadcast to other clients; update functions that call
setState to call the server save/update methods and reconcile incoming server
events to update the local state.
- Around line 223-241: Advancement currently consumes next-stage slots on
approval; change it to perform a top-N selection instead: when computing
advancement in use-milestone-flow (around submission.milestoneIndex /
current.milestones and isStageOccupied usage), gather all candidates for
nextMilestone (existing current.participants whose currentMilestoneIndex ===
nextMilestoneIndex plus this submission), sort them by the canonical score field
(e.g., submission.score or participant.score) and permit advancement only if
this submission ranks within nextMilestone.maxWinners; otherwise mark the
submission as waitlisted/review-pending instead of immediately claiming a slot.
Update the slot-check logic that uses occupiedNextStage and set
updatedParticipant.status and currentMilestoneIndex only for selected winners.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a69f2681-e24e-4b2d-9d72-a53196890ab6

📥 Commits

Reviewing files that changed from the base of the PR and between 3aad250 and 0dce5ed.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (8)
  • app/profile/[userId]/page.tsx
  • components/bounty-detail/bounty-detail-milestone-flow-card.tsx
  • components/bounty-detail/bounty-detail-sidebar-cta.tsx
  • components/bounty-detail/bounty-detail-submissions-card.tsx
  • hooks/__tests__/use-milestone-flow.test.ts
  • hooks/use-milestone-flow.ts
  • package.json
  • types/milestone-flow.ts

Comment on lines +289 to +358
{isOrgMember && (
<div className="p-5 rounded-xl border border-gray-800 bg-background-card space-y-4">
<h4 className="text-sm font-semibold text-gray-200">
Sponsor Review Queue
</h4>

{pendingSubmissions.length === 0 && (
<p className="text-xs text-gray-400">
No pending milestone submissions.
</p>
)}

{pendingSubmissions.length > 0 && (
<div className="space-y-3">
{pendingSubmissions.map((submission) => {
const milestone = state.milestones[submission.milestoneIndex];
return (
<div
key={submission.id}
className="p-3 rounded-lg border border-gray-700 bg-gray-900/30 space-y-2"
>
<div className="flex items-start justify-between gap-3">
<div className="space-y-1 min-w-0">
<p className="text-sm text-gray-200 font-medium">
{submission.contributorName}
</p>
<p className="text-xs text-gray-400">
{milestone?.title}
</p>
{isSafeHttpUrl(submission.githubPullRequestUrl) ? (
<a
className="text-xs text-primary hover:underline break-all inline-flex items-center gap-1"
href={submission.githubPullRequestUrl}
target="_blank"
rel="noopener noreferrer"
>
<GitBranch className="size-3" />
{submission.githubPullRequestUrl}
</a>
) : (
<span className="text-xs text-gray-500 break-all">
{submission.githubPullRequestUrl}
</span>
)}
</div>
<div className="flex gap-2 shrink-0">
<Button
size="sm"
variant="outline"
onClick={() =>
handleReview(submission.id, "REJECTED")
}
>
Reject
</Button>
<Button
size="sm"
onClick={() =>
handleReview(submission.id, "APPROVED")
}
>
Approve
</Button>
</div>
</div>
</div>
);
})}
</div>
)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

This flow still has no per-stage payout or escrow-release action.

The sponsor queue stops at approve/reject. Unlike the standard submissions flow, there is no mark-paid / transaction-capture step here, so approved milestone work cannot actually be released or audited per stage.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/bounty-detail/bounty-detail-milestone-flow-card.tsx` around lines
289 - 358, The sponsor queue currently only calls handleReview(submission.id,
"APPROVED"/"REJECTED") and lacks a per-stage payout/escrow-release step; add a
release/capture flow that runs when a sponsor approves. Update the UI in
bounty-detail-milestone-flow-card to either replace or supplement the Approve
button with an "Approve & Release" action that calls a new handler (e.g.,
handleSponsorApprove or extend handleReview to accept a "RELEASE" action), and
implement that handler to: 1) call the backend endpoint that performs the escrow
release/transaction capture (suggested function names: releaseMilestonePayout,
captureTransaction, or markMilestonePaid) using submission.id and
submission.milestoneIndex, 2) update local state.milestones and
pendingSubmissions to reflect paid/audited status, and 3) surface errors via the
existing error/logging flow; keep the Reject path unchanged. Ensure you
reference submission.id, submission.milestoneIndex, pendingSubmissions,
state.milestones, and handleReview when making the changes.

Comment on lines +38 to +40
if (bounty.type === "MILESTONE_BASED") {
return "Join Milestone Flow";
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Wire the milestone CTA to the milestone flow, not the GitHub issue.

For MILESTONE_BASED bounties the label changes, but both click handlers still open bounty.githubIssueUrl. That makes the primary action navigate away from the new in-page flow instead of joining or focusing it.

Also applies to: 85-88, 147-149, 159-162

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/bounty-detail/bounty-detail-sidebar-cta.tsx` around lines 38 - 40,
The CTA label change for MILESTONE_BASED bounties is correct, but the click
handlers still open bounty.githubIssueUrl; update the primary and secondary
click handlers in bounty-detail-sidebar-cta.tsx so that when bounty.type ===
"MILESTONE_BASED" they invoke the milestone flow handler instead of navigating
to bounty.githubIssueUrl — i.e., replace calls that open or link to
bounty.githubIssueUrl with the existing milestone flow entry point (e.g.,
openMilestoneFlow / joinMilestoneFlow / onOpenMilestoneModal) used elsewhere in
the app, and apply this change for all instances noted (the other CTA handlers
in the same file). Ensure the conditional uses bounty.type === "MILESTONE_BASED"
to select the milestone handler and leaves non-milestone behavior unchanged.

Comment on lines +13 to +45
function createDefaultMilestones(): MilestoneDefinition[] {
return [
{
id: "m1",
title: "Milestone 1 - Build",
description: "Ship an initial implementation and open a first PR.",
maxWinners: 8,
rewardPercentage: 30,
},
{
id: "m2",
title: "Milestone 2 - Harden",
description:
"Refine implementation quality based on review feedback and testing.",
maxWinners: 4,
rewardPercentage: 30,
},
{
id: "m3",
title: "Milestone 3 - Finalize",
description:
"Deliver the production-ready version and final documentation.",
maxWinners: 2,
rewardPercentage: 40,
},
];
}

function createInitialState(bountyId: string): MilestoneFlowState {
return {
bountyId,
milestones: createDefaultMilestones(),
participants: [],
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Parameterize milestone definitions instead of hardcoding them in the hook.

Every milestone bounty gets the same 8 → 4 → 2 stages and 30/30/40 payouts here because useMilestoneFlow only accepts bountyId. The UI and capacity checks will be wrong for any bounty whose actual funnel differs.

Also applies to: 63-67

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-milestone-flow.ts` around lines 13 - 45, The hook hardcodes
milestone definitions via createDefaultMilestones and createInitialState so
every bounty gets the same 8→4→2 and 30/30/40 payout regardless of real bounty
config; change the API to accept milestone definitions from the caller: update
useMilestoneFlow to accept an optional initialMilestones parameter (or a full
config object), modify createInitialState to accept that milestones array
(falling back to createDefaultMilestones()), and update createDefaultMilestones
to support parameterized overrides if needed; ensure all places constructing the
state (and tests) pass the real milestone defs so UI and capacity checks use the
bounty-specific funnel.

Comment on lines +64 to +67
const [state, setState] = useLocalStorage<MilestoneFlowState>(
`${FLOW_KEY_PREFIX}${bountyId}`,
createInitialState(bountyId),
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

localStorage cannot back a shared milestone workflow.

This state lives only in the current browser and is user-editable. Contributors, sponsors, and profile pages on other devices or sessions will not see the same joins, submissions, reviews, or occupancy, so the multi-user flow cannot function reliably.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-milestone-flow.ts` around lines 64 - 67, The milestone flow state
is stored client-side with useLocalStorage
(`useLocalStorage<MilestoneFlowState>(\`${FLOW_KEY_PREFIX}${bountyId}\`,
createInitialState(bountyId))`), which makes joins/reviews/submissions
local-only and user-editable; change this to a shared, server-backed state:
replace the useLocalStorage usage for MilestoneFlowState with API-backed
persistence (e.g., load/save via your milestones endpoints using bountyId) and
add real-time sync (websocket/subscribe) so updates to the state are written
through the server and broadcast to other clients; update functions that call
setState to call the server save/update methods and reconcile incoming server
events to update the local state.

Comment on lines +86 to +119
const joinFlow = (contributorId: string, contributorName: string) => {
setState((current) => {
const existing = current.participants.find(
(participant) => participant.contributorId === contributorId,
);
if (existing) return current;

const firstMilestone = current.milestones[0];
const occupied = current.participants.filter((participant) =>
isStageOccupied(participant, 0),
).length;

if (occupied >= firstMilestone.maxWinners) {
throw new Error("Milestone 1 is full. Try again later.");
}

const timestamp = nowIso();
const newParticipant: MilestoneParticipant = {
id: `mp_${bountyId}_${Date.now()}`,
bountyId,
contributorId,
contributorName,
currentMilestoneIndex: 0,
status: "ACTIVE",
joinedAt: timestamp,
updatedAt: timestamp,
};

return {
...current,
participants: [...current.participants, newParticipant],
updatedAt: timestamp,
};
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

joinFlow only supports a blind join, not a slot application.

The transition records only contributor identity at milestone 0. There is no requested-slot or proposal payload here, so contributors cannot apply for a specific slot and sponsors have nothing to use for slot assignment or reassignment decisions.

Comment on lines +223 to +241
const nextMilestoneIndex = submission.milestoneIndex + 1;
const nextMilestone = current.milestones[nextMilestoneIndex];

const occupiedNextStage = current.participants.filter((item) =>
isStageOccupied(item, nextMilestoneIndex),
).length;

if (occupiedNextStage >= nextMilestone.maxWinners) {
throw new Error(
`${nextMilestone.title} has no open slots. Review queue before advancing.`,
);
}

updatedParticipant = {
...participant,
currentMilestoneIndex: nextMilestoneIndex,
status: "ACTIVE",
updatedAt: timestamp,
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Advancement is currently "first approved wins", not top-N.

Each approval immediately consumes a slot in the next stage until maxWinners is full. There is no score/ranking or explicit selection step, so later stronger submissions can be blocked purely by review order.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-milestone-flow.ts` around lines 223 - 241, Advancement currently
consumes next-stage slots on approval; change it to perform a top-N selection
instead: when computing advancement in use-milestone-flow (around
submission.milestoneIndex / current.milestones and isStageOccupied usage),
gather all candidates for nextMilestone (existing current.participants whose
currentMilestoneIndex === nextMilestoneIndex plus this submission), sort them by
the canonical score field (e.g., submission.score or participant.score) and
permit advancement only if this submission ranks within
nextMilestone.maxWinners; otherwise mark the submission as
waitlisted/review-pending instead of immediately claiming a slot. Update the
slot-check logic that uses occupiedNextStage and set updatedParticipant.status
and currentMilestoneIndex only for selected winners.

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.

Implement Multi-Winner Milestone-Based Bounty Flow

1 participant