Merge pull request #1708 from paperclipai/pr/pap-817-onboarding-goal-context

Seed onboarding project and issue goal context
This commit is contained in:
Dotta 2026-03-24 12:38:19 -05:00 committed by GitHub
commit 03f44d0089
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 556 additions and 34 deletions

View file

@ -8,6 +8,7 @@ import { companiesApi } from "../api/companies";
import { goalsApi } from "../api/goals";
import { agentsApi } from "../api/agents";
import { issuesApi } from "../api/issues";
import { projectsApi } from "../api/projects";
import { queryKeys } from "../lib/queryKeys";
import { Dialog, DialogPortal } from "@/components/ui/dialog";
import {
@ -24,6 +25,11 @@ import {
import { getUIAdapter } from "../adapters";
import { defaultCreateValues } from "./agent-config-defaults";
import { parseOnboardingGoalInput } from "../lib/onboarding-goal";
import {
buildOnboardingIssuePayload,
buildOnboardingProjectPayload,
selectDefaultCompanyGoalId
} from "../lib/onboarding-launch";
import {
DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX,
DEFAULT_CODEX_LOCAL_MODEL
@ -144,7 +150,11 @@ export function OnboardingWizard() {
const [createdCompanyPrefix, setCreatedCompanyPrefix] = useState<
string | null
>(null);
const [createdCompanyGoalId, setCreatedCompanyGoalId] = useState<string | null>(
null
);
const [createdAgentId, setCreatedAgentId] = useState<string | null>(null);
const [createdProjectId, setCreatedProjectId] = useState<string | null>(null);
const [createdIssueRef, setCreatedIssueRef] = useState<string | null>(null);
useEffect(() => {
@ -160,6 +170,10 @@ export function OnboardingWizard() {
setStep(effectiveOnboardingOptions.initialStep ?? 1);
setCreatedCompanyId(cId);
setCreatedCompanyPrefix(null);
setCreatedCompanyGoalId(null);
setCreatedProjectId(null);
setCreatedAgentId(null);
setCreatedIssueRef(null);
}, [
effectiveOnboardingOpen,
effectiveOnboardingOptions.companyId,
@ -284,7 +298,9 @@ export function OnboardingWizard() {
setTaskDescription(DEFAULT_TASK_DESCRIPTION);
setCreatedCompanyId(null);
setCreatedCompanyPrefix(null);
setCreatedCompanyGoalId(null);
setCreatedAgentId(null);
setCreatedProjectId(null);
setCreatedIssueRef(null);
}
@ -371,7 +387,7 @@ export function OnboardingWizard() {
if (companyGoal.trim()) {
const parsedGoal = parseOnboardingGoalInput(companyGoal);
await goalsApi.create(company.id, {
const goal = await goalsApi.create(company.id, {
title: parsedGoal.title,
...(parsedGoal.description
? { description: parsedGoal.description }
@ -379,9 +395,12 @@ export function OnboardingWizard() {
level: "company",
status: "active"
});
setCreatedCompanyGoalId(goal.id);
queryClient.invalidateQueries({
queryKey: queryKeys.goals.list(company.id)
});
} else {
setCreatedCompanyGoalId(null);
}
setStep(2);
@ -522,16 +541,38 @@ export function OnboardingWizard() {
setLoading(true);
setError(null);
try {
let goalId = createdCompanyGoalId;
if (!goalId) {
const goals = await goalsApi.list(createdCompanyId);
goalId = selectDefaultCompanyGoalId(goals);
setCreatedCompanyGoalId(goalId);
}
let projectId = createdProjectId;
if (!projectId) {
const project = await projectsApi.create(
createdCompanyId,
buildOnboardingProjectPayload(goalId)
);
projectId = project.id;
setCreatedProjectId(projectId);
queryClient.invalidateQueries({
queryKey: queryKeys.projects.list(createdCompanyId)
});
}
let issueRef = createdIssueRef;
if (!issueRef) {
const issue = await issuesApi.create(createdCompanyId, {
title: taskTitle.trim(),
...(taskDescription.trim()
? { description: taskDescription.trim() }
: {}),
assigneeAgentId: createdAgentId,
status: "todo"
});
const issue = await issuesApi.create(
createdCompanyId,
buildOnboardingIssuePayload({
title: taskTitle,
description: taskDescription,
assigneeAgentId: createdAgentId,
projectId,
goalId
})
);
issueRef = issue.identifier ?? issue.id;
setCreatedIssueRef(issueRef);
queryClient.invalidateQueries({

View file

@ -0,0 +1,131 @@
import { describe, expect, it } from "vitest";
import {
buildOnboardingIssuePayload,
buildOnboardingProjectPayload,
selectDefaultCompanyGoalId,
} from "./onboarding-launch";
describe("selectDefaultCompanyGoalId", () => {
it("prefers the earliest active root company goal", () => {
expect(
selectDefaultCompanyGoalId([
{
id: "team-goal",
companyId: "company-1",
title: "Nested",
description: null,
level: "team",
status: "active",
parentId: null,
ownerAgentId: null,
createdAt: new Date("2026-03-04T00:00:00Z"),
updatedAt: new Date("2026-03-04T00:00:00Z"),
},
{
id: "goal-2",
companyId: "company-1",
title: "Later active root",
description: null,
level: "company",
status: "active",
parentId: null,
ownerAgentId: null,
createdAt: new Date("2026-03-03T00:00:00Z"),
updatedAt: new Date("2026-03-03T00:00:00Z"),
},
{
id: "goal-1",
companyId: "company-1",
title: "Earliest active root",
description: null,
level: "company",
status: "active",
parentId: null,
ownerAgentId: null,
createdAt: new Date("2026-03-02T00:00:00Z"),
updatedAt: new Date("2026-03-02T00:00:00Z"),
},
]),
).toBe("goal-1");
});
it("falls back to the earliest root company goal when none are active", () => {
expect(
selectDefaultCompanyGoalId([
{
id: "goal-2",
companyId: "company-1",
title: "Cancelled root",
description: null,
level: "company",
status: "cancelled",
parentId: null,
ownerAgentId: null,
createdAt: new Date("2026-03-03T00:00:00Z"),
updatedAt: new Date("2026-03-03T00:00:00Z"),
},
{
id: "goal-1",
companyId: "company-1",
title: "Earliest root",
description: null,
level: "company",
status: "planned",
parentId: null,
ownerAgentId: null,
createdAt: new Date("2026-03-02T00:00:00Z"),
updatedAt: new Date("2026-03-02T00:00:00Z"),
},
]),
).toBe("goal-1");
});
});
describe("onboarding launch payloads", () => {
it("links the onboarding project and first issue to the selected goal", () => {
expect(buildOnboardingProjectPayload("goal-1")).toEqual({
name: "Onboarding",
status: "in_progress",
goalIds: ["goal-1"],
});
expect(
buildOnboardingIssuePayload({
title: " Hire your first engineer ",
description: " Kick off the hiring plan ",
assigneeAgentId: "agent-1",
projectId: "project-1",
goalId: "goal-1",
}),
).toEqual({
title: "Hire your first engineer",
description: "Kick off the hiring plan",
assigneeAgentId: "agent-1",
projectId: "project-1",
goalId: "goal-1",
status: "todo",
});
});
it("omits goal links when no default company goal exists", () => {
expect(buildOnboardingProjectPayload(null)).toEqual({
name: "Onboarding",
status: "in_progress",
});
expect(
buildOnboardingIssuePayload({
title: "Task",
description: "",
assigneeAgentId: "agent-1",
projectId: "project-1",
goalId: null,
}),
).toEqual({
title: "Task",
assigneeAgentId: "agent-1",
projectId: "project-1",
status: "todo",
});
});
});

View file

@ -0,0 +1,53 @@
import type { Goal } from "@paperclipai/shared";
export const ONBOARDING_PROJECT_NAME = "Onboarding";
function goalCreatedAt(goal: Goal) {
const createdAt = goal.createdAt instanceof Date ? goal.createdAt : new Date(goal.createdAt);
return Number.isNaN(createdAt.getTime()) ? 0 : createdAt.getTime();
}
function pickEarliestGoal(goals: Goal[]) {
return [...goals].sort((a, b) => goalCreatedAt(a) - goalCreatedAt(b))[0] ?? null;
}
export function selectDefaultCompanyGoalId(goals: Goal[]): string | null {
const companyGoals = goals.filter((goal) => goal.level === "company");
const rootGoals = companyGoals.filter((goal) => !goal.parentId);
const activeRootGoals = rootGoals.filter((goal) => goal.status === "active");
return (
pickEarliestGoal(activeRootGoals)?.id ??
pickEarliestGoal(rootGoals)?.id ??
pickEarliestGoal(companyGoals)?.id ??
null
);
}
export function buildOnboardingProjectPayload(goalId: string | null) {
return {
name: ONBOARDING_PROJECT_NAME,
status: "in_progress" as const,
...(goalId ? { goalIds: [goalId] } : {}),
};
}
export function buildOnboardingIssuePayload(input: {
title: string;
description: string;
assigneeAgentId: string;
projectId: string;
goalId: string | null;
}) {
const title = input.title.trim();
const description = input.description.trim();
return {
title,
...(description ? { description } : {}),
assigneeAgentId: input.assigneeAgentId,
projectId: input.projectId,
...(input.goalId ? { goalId: input.goalId } : {}),
status: "todo" as const,
};
}