mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 11:01:16 +01:00
Include plan comments in accepted plan handoff
This commit is contained in:
1 parent
4a3174e032
commit
a5b3db58dc
3 files changed
+161
-5
No files matched your search
@@ -72,6 +72,34 @@ describe("buildPaperclipTaskMarkdown", () => {
|
||||
expect(acceptedConfirmation).not.toContain("- Work mode: \"planning\"");
|
||||
});
|
||||
|
||||
it("includes confirmed-plan comments in accepted-plan continuation task context", () => {
|
||||
const acceptedConfirmation = buildPaperclipTaskMarkdown({
|
||||
issue: {
|
||||
id: "issue-1",
|
||||
identifier: "PAP-3404",
|
||||
title: "Plan first",
|
||||
workMode: "planning",
|
||||
description: null,
|
||||
},
|
||||
interaction: {
|
||||
kind: "request_confirmation",
|
||||
status: "accepted",
|
||||
},
|
||||
acceptedPlanComments: [
|
||||
{
|
||||
id: "comment-1",
|
||||
authorType: "user",
|
||||
body: "Please keep the migration backwards compatible.",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(acceptedConfirmation).toContain("Create child issues from the approved plan only");
|
||||
expect(acceptedConfirmation).toContain("Comments included with the confirmed plan:");
|
||||
expect(acceptedConfirmation).toContain("Comment 1 - user - comment-1:");
|
||||
expect(acceptedConfirmation).toContain("Please keep the migration backwards compatible.");
|
||||
});
|
||||
|
||||
it("prefers ordinary comment planning guidance over stale accepted confirmation state", () => {
|
||||
const commentWake = buildPaperclipTaskMarkdown({
|
||||
issue: {
|
||||
|
||||
@@ -175,6 +175,69 @@ describeEmbeddedPostgres("deleted issue comment redaction", () => {
|
||||
expect(JSON.stringify(wakePayload)).not.toContain("secret metadata");
|
||||
});
|
||||
|
||||
it("adds recent issue comments to accepted-plan continuation wake payloads", async () => {
|
||||
const { companyId, issueId } = await seedIssue();
|
||||
const firstCommentId = randomUUID();
|
||||
const secondCommentId = randomUUID();
|
||||
const deletedCommentId = randomUUID();
|
||||
await db.insert(issueComments).values([
|
||||
{
|
||||
id: firstCommentId,
|
||||
companyId,
|
||||
issueId,
|
||||
authorUserId: "board-user-1",
|
||||
body: "Keep the migration backwards compatible.",
|
||||
createdAt: new Date("2026-06-03T12:00:00.000Z"),
|
||||
},
|
||||
{
|
||||
id: deletedCommentId,
|
||||
companyId,
|
||||
issueId,
|
||||
authorUserId: "board-user-1",
|
||||
body: "Do not pass this deleted text downstream.",
|
||||
deletedAt: new Date("2026-06-03T12:01:00.000Z"),
|
||||
deletedByType: "user",
|
||||
deletedByUserId: "board-user-1",
|
||||
createdAt: new Date("2026-06-03T12:01:00.000Z"),
|
||||
},
|
||||
{
|
||||
id: secondCommentId,
|
||||
companyId,
|
||||
issueId,
|
||||
authorUserId: "board-user-1",
|
||||
body: "Split the rollout into a verification child task.",
|
||||
createdAt: new Date("2026-06-03T12:02:00.000Z"),
|
||||
},
|
||||
]);
|
||||
|
||||
const wakePayload = await buildPaperclipWakePayload({
|
||||
db,
|
||||
companyId,
|
||||
contextSnapshot: {
|
||||
issueId,
|
||||
interactionKind: "request_confirmation",
|
||||
interactionStatus: "accepted",
|
||||
workspaceRefreshReason: "accepted_plan_confirmation",
|
||||
},
|
||||
issueSummary: {
|
||||
id: issueId,
|
||||
identifier: "RED-1",
|
||||
title: "Deleted comment redaction",
|
||||
status: "in_progress",
|
||||
priority: "medium",
|
||||
workMode: "planning",
|
||||
},
|
||||
});
|
||||
|
||||
expect(wakePayload?.commentContextSource).toBe("accepted_plan_confirmation");
|
||||
expect(wakePayload?.commentIds).toEqual([firstCommentId, secondCommentId]);
|
||||
expect(wakePayload?.comments.map((comment) => comment.body)).toEqual([
|
||||
"Keep the migration backwards compatible.",
|
||||
"Split the rollout into a verification child task.",
|
||||
]);
|
||||
expect(JSON.stringify(wakePayload)).not.toContain("Do not pass this deleted text downstream.");
|
||||
});
|
||||
|
||||
it("excludes deleted comment bodies from company search", async () => {
|
||||
const { companyId, issueId } = await seedIssue();
|
||||
await db.insert(issueComments).values({
|
||||
|
||||
@@ -2397,7 +2397,7 @@ export async function buildPaperclipWakePayload(input: {
|
||||
exposeLowTrustRaw?: boolean;
|
||||
}) {
|
||||
const executionStage = parseObject(input.contextSnapshot.executionStage);
|
||||
const commentIds = extractWakeCommentIds(input.contextSnapshot);
|
||||
let commentIds = extractWakeCommentIds(input.contextSnapshot);
|
||||
const annotationCommentId = readNonEmptyString(input.contextSnapshot.annotationCommentId);
|
||||
const issueId = readNonEmptyString(input.contextSnapshot.issueId);
|
||||
const continuationSummary = input.continuationSummary ?? null;
|
||||
@@ -2417,6 +2417,31 @@ export async function buildPaperclipWakePayload(input: {
|
||||
.where(and(eq(issues.id, issueId), eq(issues.companyId, input.companyId)))
|
||||
.then((rows) => rows[0] ?? null)
|
||||
: null);
|
||||
let acceptedPlanCommentWindowTruncated = false;
|
||||
const acceptedPlanContinuationWake =
|
||||
readNonEmptyString(input.contextSnapshot.workspaceRefreshReason) === "accepted_plan_confirmation" ||
|
||||
(
|
||||
issueSummary?.workMode === "planning" &&
|
||||
readNonEmptyString(input.contextSnapshot.interactionKind) === "request_confirmation" &&
|
||||
readNonEmptyString(input.contextSnapshot.interactionStatus) === "accepted"
|
||||
);
|
||||
if (commentIds.length === 0 && acceptedPlanContinuationWake && issueSummary?.id) {
|
||||
const recentPlanCommentRows = await input.db
|
||||
.select({ id: issueComments.id })
|
||||
.from(issueComments)
|
||||
.where(and(
|
||||
eq(issueComments.companyId, input.companyId),
|
||||
eq(issueComments.issueId, issueSummary.id),
|
||||
isNull(issueComments.deletedAt),
|
||||
))
|
||||
.orderBy(desc(issueComments.createdAt), desc(issueComments.id))
|
||||
.limit(MAX_INLINE_WAKE_COMMENTS + 1);
|
||||
acceptedPlanCommentWindowTruncated = recentPlanCommentRows.length > MAX_INLINE_WAKE_COMMENTS;
|
||||
commentIds = recentPlanCommentRows
|
||||
.slice(0, MAX_INLINE_WAKE_COMMENTS)
|
||||
.reverse()
|
||||
.map((comment) => comment.id);
|
||||
}
|
||||
if (commentIds.length === 0 && Object.keys(executionStage).length === 0 && !issueSummary) return null;
|
||||
|
||||
const commentRows =
|
||||
@@ -2451,7 +2476,7 @@ export async function buildPaperclipWakePayload(input: {
|
||||
const commentsById = new Map(commentRows.map((comment) => [comment.id, comment]));
|
||||
const comments: Array<Record<string, unknown>> = [];
|
||||
let remainingBodyChars = MAX_INLINE_WAKE_COMMENT_BODY_TOTAL_CHARS;
|
||||
let truncated = false;
|
||||
let truncated = acceptedPlanCommentWindowTruncated;
|
||||
let missingCommentCount = 0;
|
||||
const safeContinuationSummary =
|
||||
continuationSummary && !input.exposeLowTrustRaw
|
||||
@@ -2589,6 +2614,9 @@ export async function buildPaperclipWakePayload(input: {
|
||||
: null,
|
||||
interactionKind: readNonEmptyString(input.contextSnapshot.interactionKind),
|
||||
interactionStatus: readNonEmptyString(input.contextSnapshot.interactionStatus),
|
||||
commentContextSource: acceptedPlanContinuationWake && commentIds.length > 0
|
||||
? "accepted_plan_confirmation"
|
||||
: null,
|
||||
checkedOutByHarness: input.contextSnapshot[PAPERCLIP_HARNESS_CHECKOUT_KEY] === true,
|
||||
dependencyBlockedInteraction: input.contextSnapshot.dependencyBlockedInteraction === true,
|
||||
treeHoldInteraction: input.contextSnapshot.treeHoldInteraction === true,
|
||||
@@ -2663,6 +2691,11 @@ export function buildPaperclipTaskMarkdown(input: {
|
||||
kind?: string | null;
|
||||
status?: string | null;
|
||||
} | null;
|
||||
acceptedPlanComments?: Array<{
|
||||
id?: string | null;
|
||||
authorType?: string | null;
|
||||
body?: string | null;
|
||||
}> | null;
|
||||
acceptedPlanContinuation?: boolean;
|
||||
}) {
|
||||
const quoteTaskScalar = (value: string) => JSON.stringify(value);
|
||||
@@ -2723,6 +2756,28 @@ export function buildPaperclipTaskMarkdown(input: {
|
||||
if (wakeComment?.body.trim()) {
|
||||
lines.push("", "Latest wake comment:", fenceTaskText(wakeComment.body.trim()));
|
||||
}
|
||||
if (acceptedPlanContinuation) {
|
||||
const acceptedPlanComments = (input.acceptedPlanComments ?? [])
|
||||
.map((comment) => ({
|
||||
...comment,
|
||||
body: comment.body?.trim() ?? "",
|
||||
}))
|
||||
.filter((comment) => comment.body.length > 0)
|
||||
.slice(0, MAX_INLINE_WAKE_COMMENTS);
|
||||
if (acceptedPlanComments.length > 0) {
|
||||
lines.push("", "Comments included with the confirmed plan:");
|
||||
for (const [index, comment] of acceptedPlanComments.entries()) {
|
||||
const authorType = comment.authorType?.trim();
|
||||
const commentId = comment.id?.trim();
|
||||
const labelParts = [
|
||||
`Comment ${index + 1}`,
|
||||
...(authorType ? [authorType] : []),
|
||||
...(commentId ? [commentId] : []),
|
||||
];
|
||||
lines.push("", `${labelParts.join(" - ")}:`, fenceTaskText(comment.body));
|
||||
}
|
||||
}
|
||||
}
|
||||
lines.push("", "Use this task context as the current assignment.");
|
||||
return lines.join("\n");
|
||||
}
|
||||
@@ -7894,6 +7949,17 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
} else {
|
||||
delete context[PAPERCLIP_WAKE_PAYLOAD_KEY];
|
||||
}
|
||||
const acceptedPlanContinuationForTask =
|
||||
readNonEmptyString(context.workspaceRefreshReason) === "accepted_plan_confirmation"
|
||||
&& !parseObject(context.acceptedPlanWakeRouting);
|
||||
const acceptedPlanCommentsForTask =
|
||||
acceptedPlanContinuationForTask && Array.isArray(paperclipWakePayload?.comments)
|
||||
? paperclipWakePayload.comments.map((comment) => ({
|
||||
id: readNonEmptyString(comment.id),
|
||||
authorType: readNonEmptyString(comment.authorType),
|
||||
body: readNonEmptyString(comment.body),
|
||||
}))
|
||||
: null;
|
||||
const taskMarkdown = buildPaperclipTaskMarkdown({
|
||||
issue: issueRef
|
||||
? {
|
||||
@@ -7909,9 +7975,8 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
kind: readNonEmptyString(context.interactionKind),
|
||||
status: readNonEmptyString(context.interactionStatus),
|
||||
},
|
||||
acceptedPlanContinuation:
|
||||
readNonEmptyString(context.workspaceRefreshReason) === "accepted_plan_confirmation"
|
||||
&& !parseObject(context.acceptedPlanWakeRouting),
|
||||
acceptedPlanComments: acceptedPlanCommentsForTask,
|
||||
acceptedPlanContinuation: acceptedPlanContinuationForTask,
|
||||
});
|
||||
if (issueRef) {
|
||||
context.paperclipIssue = {
|
||||
|
||||
Reference in new issue
Block a user