diff --git a/src/app/(dashboard)/projects/[uuid]/ideas/idea-detail-panel.tsx b/src/app/(dashboard)/projects/[uuid]/ideas/idea-detail-panel.tsx
index b174a8a..e4166eb 100644
--- a/src/app/(dashboard)/projects/[uuid]/ideas/idea-detail-panel.tsx
+++ b/src/app/(dashboard)/projects/[uuid]/ideas/idea-detail-panel.tsx
@@ -228,9 +228,8 @@ export function IdeaDetailPanel({
const canAssign = idea.status !== "completed" && idea.status !== "closed";
const elaborationResolved = idea.elaborationStatus === "resolved";
const canCreateProposal =
- idea.status === "elaborating" &&
- elaborationResolved &&
- !isUsedInProposal;
+ (idea.status === "elaborating" || idea.status === "proposal_created" || idea.status === "completed") &&
+ elaborationResolved;
const canSkipElaboration =
idea.status === "elaborating" &&
(!idea.elaborationStatus || idea.elaborationStatus !== "resolved") &&
diff --git a/src/app/(dashboard)/projects/[uuid]/proposals/actions.ts b/src/app/(dashboard)/projects/[uuid]/proposals/actions.ts
index 893c0f2..8f11d7a 100644
--- a/src/app/(dashboard)/projects/[uuid]/proposals/actions.ts
+++ b/src/app/(dashboard)/projects/[uuid]/proposals/actions.ts
@@ -4,7 +4,6 @@ import { revalidatePath } from "next/cache";
import { getServerAuthContext } from "@/lib/auth-server";
import {
createProposal,
- checkIdeasAvailability,
checkIdeasAssignee,
type DocumentDraftInput,
type TaskDraftInput,
@@ -58,18 +57,7 @@ export async function createProposalAction(
};
}
- // Validate whether these Ideas have already been used by another Proposal
- const availabilityCheck = await checkIdeasAvailability(
- auth.companyUuid,
- data.inputUuids
- );
- if (!availabilityCheck.available) {
- const usedIdea = availabilityCheck.usedIdeas[0];
- return {
- success: false,
- error: `One of the selected ideas is already used in proposal "${usedIdea.proposalTitle}"`,
- };
- }
+ // Note: Ideas can be reused across multiple Proposals (no availability check blocking)
}
const proposal = await createProposal({
diff --git a/src/app/(dashboard)/projects/[uuid]/proposals/new/page.tsx b/src/app/(dashboard)/projects/[uuid]/proposals/new/page.tsx
index ac86162..532cc9a 100644
--- a/src/app/(dashboard)/projects/[uuid]/proposals/new/page.tsx
+++ b/src/app/(dashboard)/projects/[uuid]/proposals/new/page.tsx
@@ -6,7 +6,6 @@ import { getTranslations } from "next-intl/server";
import { getServerAuthContext } from "@/lib/auth-server";
import { projectExists } from "@/services/project.service";
import { listIdeas } from "@/services/idea.service";
-import { checkIdeasAvailability } from "@/services/proposal.service";
import { CreateProposalForm } from "./create-proposal-form";
interface PageProps {
@@ -41,14 +40,8 @@ export default async function NewProposalPage({ params, searchParams }: PageProp
actorType: auth.type,
});
- // Filter out ideas that have not been used yet
- const ideaUuids = ideas.map(idea => idea.uuid);
- const availabilityCheck = ideaUuids.length > 0
- ? await checkIdeasAvailability(auth.companyUuid, ideaUuids)
- : { usedIdeas: [] };
-
- const usedIdeaUuids = new Set(availabilityCheck.usedIdeas.map(u => u.uuid));
- const availableIdeas = ideas.filter(idea => !usedIdeaUuids.has(idea.uuid));
+ // All ideas with resolved elaboration are available (ideas can be reused across proposals)
+ const availableIdeas = ideas;
return (
diff --git a/src/mcp/tools/pm.ts b/src/mcp/tools/pm.ts
index 130a8ae..d4981ab 100644
--- a/src/mcp/tools/pm.ts
+++ b/src/mcp/tools/pm.ts
@@ -184,7 +184,8 @@ export function registerPmTools(server: McpServer, auth: AgentAuthContext) {
return { content: [{ type: "text", text: "Project not found" }], isError: true };
}
- // If input type is idea, validate assignee and uniqueness
+ // If input type is idea, validate assignee
+ let reusedWarning = "";
if (inputType === "idea") {
const assigneeCheck = await proposalService.checkIdeasAssignee(
auth.companyUuid,
@@ -199,17 +200,14 @@ export function registerPmTools(server: McpServer, auth: AgentAuthContext) {
};
}
+ // Check if ideas are already used by other proposals (informational only, not blocking)
const availabilityCheck = await proposalService.checkIdeasAvailability(
auth.companyUuid,
inputUuids
);
- if (!availabilityCheck.available) {
- const usedIdea = availabilityCheck.usedIdeas[0];
- return {
- content: [{ type: "text", text: `Idea is already used by Proposal "${usedIdea.proposalTitle}"` }],
- isError: true,
- };
- }
+ reusedWarning = !availabilityCheck.available
+ ? `\nNote: Idea is also referenced by existing Proposal(s): ${availabilityCheck.usedIdeas.map(u => `"${u.proposalTitle}"`).join(", ")}`
+ : "";
}
const proposal = await proposalService.createProposal({
@@ -224,7 +222,7 @@ export function registerPmTools(server: McpServer, auth: AgentAuthContext) {
});
return {
- content: [{ type: "text", text: JSON.stringify({ uuid: proposal.uuid, title: proposal.title, status: proposal.status }, null, 2) }],
+ content: [{ type: "text", text: JSON.stringify({ uuid: proposal.uuid, title: proposal.title, status: proposal.status }, null, 2) + reusedWarning }],
};
}
);
diff --git a/src/services/__tests__/proposal.service.test.ts b/src/services/__tests__/proposal.service.test.ts
index 5fe6318..d924b7e 100644
--- a/src/services/__tests__/proposal.service.test.ts
+++ b/src/services/__tests__/proposal.service.test.ts
@@ -1711,3 +1711,108 @@ describe("approveProposal - edge cases", () => {
expect(mockCreateTasks).toHaveBeenCalled();
});
});
+
+// ===== Idea Reuse across Proposals =====
+describe("Idea reuse - submitProposal with proposal_created Idea", () => {
+ it("should not error when Idea is already in proposal_created status", async () => {
+ const { submitProposal } = await import("@/services/proposal.service");
+
+ const now = new Date();
+ const proposal = {
+ uuid: "proposal-reuse",
+ companyUuid: COMPANY_UUID,
+ projectUuid: PROJECT_UUID,
+ status: "draft",
+ inputType: "idea",
+ inputUuids: ["idea-already-used"],
+ documentDrafts: [
+ { uuid: "doc-1", type: "prd", title: "PRD", content: "This is a comprehensive PRD document that describes the feature requirements in detail for the Idea reuse feature across multiple proposals." },
+ { uuid: "doc-2", type: "tech_design", title: "Tech Design", content: "This is a comprehensive tech design document that describes the implementation approach for the Idea reuse feature across multiple proposals." },
+ ],
+ taskDrafts: [{
+ uuid: "task-1", title: "Task", description: "desc", storyPoints: 1, priority: "medium",
+ acceptanceCriteria: null, acceptanceCriteriaItems: [{ description: "AC1" }], dependsOnDraftUuids: [],
+ }],
+ project: { uuid: PROJECT_UUID, name: "Test" },
+ description: "Test proposal for Idea reuse scenario",
+ createdByUuid: ACTOR_UUID,
+ createdByType: "agent",
+ reviewedByUuid: null,
+ reviewNote: null,
+ reviewedAt: null,
+ createdAt: now,
+ updatedAt: now,
+ };
+
+ // E5 check: ideas must have resolved elaboration
+ mockPrisma.idea.findMany.mockResolvedValue([
+ { uuid: "idea-already-used", title: "Test Idea", elaborationStatus: "resolved" },
+ ]);
+
+ mockPrisma.proposal.findFirst.mockResolvedValue(proposal);
+ mockPrisma.proposal.update.mockResolvedValue({ ...proposal, status: "pending" });
+ // Idea is already proposal_created - updateMany should match 0 rows (no error)
+ mockPrisma.idea.updateMany.mockResolvedValue({ count: 0 });
+
+ const result = await submitProposal("proposal-reuse", COMPANY_UUID);
+
+ expect(result.status).toBe("pending");
+ // updateMany was called with status: "elaborating" filter, which won't match proposal_created
+ expect(mockPrisma.idea.updateMany).toHaveBeenCalledWith({
+ where: { uuid: { in: ["idea-already-used"] }, companyUuid: COMPANY_UUID, status: "elaborating" },
+ data: { status: "proposal_created" },
+ });
+ });
+});
+
+describe("Idea reuse - approveProposal with completed Idea", () => {
+ it("should not error when Idea is already in completed status", async () => {
+ const { approveProposal } = await import("@/services/proposal.service");
+
+ const now = new Date();
+ const proposal = {
+ uuid: "proposal-reuse-2",
+ companyUuid: COMPANY_UUID,
+ projectUuid: PROJECT_UUID,
+ status: "pending",
+ inputType: "idea",
+ inputUuids: ["idea-completed"],
+ documentDrafts: [{ uuid: "doc-1", type: "prd", title: "PRD", content: "content" }],
+ taskDrafts: [],
+ project: { uuid: PROJECT_UUID, name: "Test" },
+ createdByUuid: ACTOR_UUID,
+ createdByType: "agent",
+ reviewedByUuid: null,
+ reviewNote: null,
+ reviewedAt: null,
+ createdAt: now,
+ updatedAt: now,
+ };
+
+ mockPrisma.proposal.findFirst.mockResolvedValue(proposal);
+
+ mockPrisma.$transaction.mockImplementation(async (callback) => {
+ const txMock = {
+ proposal: { update: vi.fn().mockResolvedValue({
+ ...proposal, status: "approved",
+ reviewedByUuid: "reviewer-uuid", reviewNote: "Approved", reviewedAt: now,
+ project: { uuid: PROJECT_UUID, name: "Test" },
+ }) },
+ taskDependency: { create: vi.fn() },
+ acceptanceCriterion: { createMany: vi.fn() },
+ };
+ return callback(txMock);
+ });
+ mockCreateTasks.mockResolvedValue({ draftToTaskUuidMap: new Map() });
+ // Idea is already completed - updateMany should match 0 rows (no error)
+ mockPrisma.idea.updateMany.mockResolvedValue({ count: 0 });
+
+ await approveProposal("proposal-reuse-2", COMPANY_UUID, "reviewer-uuid", "Approved");
+
+ // updateMany was called with status: "proposal_created" filter, which won't match completed
+ expect(mockPrisma.idea.updateMany).toHaveBeenCalledWith({
+ where: { uuid: { in: ["idea-completed"] }, companyUuid: COMPANY_UUID, status: "proposal_created" },
+ data: { status: "completed" },
+ });
+ });
+});