Include plan comments in accepted plan handoff

This commit is contained in:
Dotta committed 2026-06-07 00:43:07 +00:00
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({
+70 -5
View File
@@ -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 = {