mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 16:27:18 +02:00
Fix mission decision log revisions
This commit is contained in:
1 parent
1b9800907e
commit
e551b857ca
6 files changed
+80
-26
No files matched your search
@@ -487,20 +487,33 @@ async function appendDecisionLog(
|
||||
ctx: PluginContext,
|
||||
companyId: string,
|
||||
issueId: string,
|
||||
existingBody: string,
|
||||
entry: { marker: string; body: string },
|
||||
) {
|
||||
const nextBody = appendDecisionLogEntry(existingBody, entry);
|
||||
if (nextBody === existingBody.trim()) return nextBody;
|
||||
await ctx.issues.documents.upsert({
|
||||
issueId,
|
||||
companyId,
|
||||
key: "decision-log",
|
||||
title: "Decision Log",
|
||||
body: nextBody,
|
||||
changeSummary: "Recorded mission decision",
|
||||
});
|
||||
return nextBody;
|
||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||
const existing = await ctx.issues.documents.get(issueId, "decision-log", companyId);
|
||||
const existingBody = existing?.body ?? "";
|
||||
const nextBody = appendDecisionLogEntry(existingBody, entry);
|
||||
if (nextBody === existingBody.trim()) return nextBody;
|
||||
|
||||
try {
|
||||
await ctx.issues.documents.upsert({
|
||||
issueId,
|
||||
companyId,
|
||||
key: "decision-log",
|
||||
title: "Decision Log",
|
||||
body: nextBody,
|
||||
changeSummary: "Recorded mission decision",
|
||||
baseRevisionId: existing?.latestRevisionId ?? null,
|
||||
});
|
||||
return nextBody;
|
||||
} catch (error) {
|
||||
const message = stringifyError(error);
|
||||
if (attempt === 0 && message.includes("updated by someone else")) continue;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Unable to append mission decision log entry");
|
||||
}
|
||||
|
||||
async function ensureFixIssue(
|
||||
@@ -590,7 +603,7 @@ export async function advanceMission(
|
||||
actorRunId: input.actorRunId ?? null,
|
||||
};
|
||||
const actorLabel = buildActorLabel(actor);
|
||||
const { mission, summary, featuresDocument, decisionLogBody } = await buildSummaryInternal(
|
||||
const { mission, summary, featuresDocument } = await buildSummaryInternal(
|
||||
ctx,
|
||||
input.companyId,
|
||||
input.issueId,
|
||||
@@ -626,7 +639,6 @@ export async function advanceMission(
|
||||
ctx,
|
||||
input.companyId,
|
||||
input.issueId,
|
||||
decisionLogBody,
|
||||
buildMissionEventEntry({
|
||||
markerKey: `advance:paused:${stopReason}:${summary.latestValidationRound}`,
|
||||
title: `Advance Paused ${summary.missionIdentifier ?? input.issueId}`,
|
||||
@@ -655,13 +667,11 @@ export async function advanceMission(
|
||||
createdFixIssues.push(fixIssue);
|
||||
}
|
||||
|
||||
let decisionLogBodyAfterFixes = decisionLogBody;
|
||||
if (createdFixIssues.length > 0) {
|
||||
decisionLogBodyAfterFixes = await appendDecisionLog(
|
||||
await appendDecisionLog(
|
||||
ctx,
|
||||
input.companyId,
|
||||
input.issueId,
|
||||
decisionLogBodyAfterFixes,
|
||||
buildMissionEventEntry({
|
||||
markerKey: `advance:fixes:${createdFixIssues.map((issue) => issue.id).join(",")}`,
|
||||
title: `Advance Created Fixes ${summary.missionIdentifier ?? input.issueId}`,
|
||||
@@ -699,7 +709,6 @@ export async function advanceMission(
|
||||
ctx,
|
||||
input.companyId,
|
||||
input.issueId,
|
||||
decisionLogBodyAfterFixes,
|
||||
buildMissionEventEntry({
|
||||
markerKey: `advance:wakeup:${wokenIssueIds.join(",")}`,
|
||||
title: `Advance Woke Issues ${summary.missionIdentifier ?? input.issueId}`,
|
||||
@@ -765,7 +774,7 @@ export async function waiveMissionFinding(
|
||||
actorRunId: input.actorRunId ?? null,
|
||||
};
|
||||
const actorLabel = buildActorLabel(actor);
|
||||
const { summary, decisionLogBody } = await buildSummaryInternal(ctx, input.companyId, input.issueId);
|
||||
const { summary } = await buildSummaryInternal(ctx, input.companyId, input.issueId);
|
||||
const finding = summary.findings.find((candidate) => candidate.id === input.findingId);
|
||||
if (!finding) {
|
||||
throw new Error(`Mission finding not found: ${input.findingId}`);
|
||||
@@ -778,7 +787,7 @@ export async function waiveMissionFinding(
|
||||
actorLabel,
|
||||
createdAt,
|
||||
});
|
||||
await appendDecisionLog(ctx, input.companyId, input.issueId, decisionLogBody, waiverEntry);
|
||||
await appendDecisionLog(ctx, input.companyId, input.issueId, waiverEntry);
|
||||
await ctx.state.set(
|
||||
{ scopeKind: "issue", scopeId: input.issueId, stateKey: `waiver:${input.findingId}` },
|
||||
{
|
||||
|
||||
@@ -527,6 +527,19 @@ describe("missions plugin worker", () => {
|
||||
});
|
||||
}
|
||||
|
||||
const originalUpsert = harness.ctx.issues.documents.upsert;
|
||||
const documentUpdates: Array<{ key: string; baseRevisionId: string | null | undefined }> = [];
|
||||
harness.ctx.issues.documents.upsert = async (input) => {
|
||||
const existing = await harness.ctx.issues.documents.get(input.issueId, input.key, input.companyId);
|
||||
if (existing && input.baseRevisionId !== existing.latestRevisionId) {
|
||||
throw new Error(
|
||||
input.baseRevisionId ? "Document was updated by someone else" : "Document update requires baseRevisionId",
|
||||
);
|
||||
}
|
||||
documentUpdates.push({ key: input.key, baseRevisionId: input.baseRevisionId });
|
||||
return originalUpsert(input);
|
||||
};
|
||||
|
||||
const first = await harness.performAction<{ createdFixIssueIds: string[] }>("advance", {
|
||||
companyId,
|
||||
issueId: rootIssueId,
|
||||
@@ -542,12 +555,35 @@ describe("missions plugin worker", () => {
|
||||
const generated = await harness.ctx.issues.list({ companyId, limit: 50 });
|
||||
expect(generated.filter((item) => item.originKind === "plugin:paperclip.missions:fix")).toHaveLength(1);
|
||||
|
||||
await expect(harness.performAction("waive", {
|
||||
const summaryResponse = await plugin.definition.onApiRequest?.({
|
||||
routeKey: "summary",
|
||||
method: "GET",
|
||||
path: `/issues/${rootIssueId}/missions/summary`,
|
||||
params: { issueId: rootIssueId },
|
||||
query: {},
|
||||
body: null,
|
||||
actor: { actorType: "agent", actorId: workerAgentId, agentId: workerAgentId, runId: "run-test" },
|
||||
companyId,
|
||||
issueId: rootIssueId,
|
||||
findingId: "FINDING-MISSION-001",
|
||||
rationale: "Accepted temporarily for test coverage.",
|
||||
})).resolves.toMatchObject({
|
||||
headers: {},
|
||||
});
|
||||
expect(summaryResponse?.body).toMatchObject({
|
||||
validationSummary: expect.objectContaining({
|
||||
findings: [expect.objectContaining({ id: "FINDING-MISSION-001", computedStatus: "fix_created" })],
|
||||
}),
|
||||
});
|
||||
|
||||
const waiverResponse = await plugin.definition.onApiRequest?.({
|
||||
routeKey: "waive",
|
||||
method: "POST",
|
||||
path: `/issues/${rootIssueId}/missions/findings/FINDING-MISSION-001/waive`,
|
||||
params: { issueId: rootIssueId, findingKey: "FINDING-MISSION-001" },
|
||||
query: {},
|
||||
body: { rationale: "Accepted temporarily for test coverage." },
|
||||
actor: { actorType: "agent", actorId: workerAgentId, agentId: workerAgentId, runId: "run-test" },
|
||||
companyId,
|
||||
headers: {},
|
||||
});
|
||||
expect(waiverResponse?.body).toMatchObject({
|
||||
findingId: "FINDING-MISSION-001",
|
||||
waived: true,
|
||||
});
|
||||
@@ -555,5 +591,10 @@ describe("missions plugin worker", () => {
|
||||
const decisionLog = await harness.ctx.issues.documents.get(rootIssueId, "decision-log", companyId);
|
||||
expect(decisionLog?.body).toContain("paperclip:mission-finding-waiver:FINDING-MISSION-001");
|
||||
expect(decisionLog?.body).toContain("Accepted temporarily for test coverage.");
|
||||
expect(documentUpdates.filter((update) => update.key === "decision-log")).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ baseRevisionId: expect.any(String) }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -776,6 +776,7 @@ export interface WorkerToHostMethods {
|
||||
title?: string;
|
||||
format?: string;
|
||||
changeSummary?: string;
|
||||
baseRevisionId?: string | null;
|
||||
},
|
||||
result: IssueDocument,
|
||||
];
|
||||
|
||||
@@ -878,7 +878,7 @@ export interface PluginIssueDocumentsClient {
|
||||
*
|
||||
* Requires the `issue.documents.write` capability.
|
||||
*
|
||||
* @param input - Document data including issueId, key, body, and optional title/format/changeSummary
|
||||
* @param input - Document data including issueId, key, body, and optional title/format/changeSummary/baseRevisionId
|
||||
*/
|
||||
upsert(input: {
|
||||
issueId: string;
|
||||
@@ -888,6 +888,7 @@ export interface PluginIssueDocumentsClient {
|
||||
title?: string;
|
||||
format?: string;
|
||||
changeSummary?: string;
|
||||
baseRevisionId?: string | null;
|
||||
}): Promise<IssueDocument>;
|
||||
|
||||
/**
|
||||
|
||||
@@ -775,6 +775,7 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost
|
||||
title: input.title,
|
||||
format: input.format,
|
||||
changeSummary: input.changeSummary,
|
||||
baseRevisionId: input.baseRevisionId,
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -1558,6 +1558,7 @@ export function buildHostServices(
|
||||
title: params.title ?? null,
|
||||
format: params.format ?? "markdown",
|
||||
changeSummary: params.changeSummary ?? null,
|
||||
baseRevisionId: params.baseRevisionId ?? null,
|
||||
});
|
||||
await logPluginActivity({
|
||||
companyId,
|
||||
|
||||
Reference in new issue
Block a user