From 0ca7831c53f68fd9281bfef7a464df9ca9bfb232 Mon Sep 17 00:00:00 2001 From: Chaim Marcus Date: Tue, 26 May 2026 13:28:43 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20adopt=20SDK=20525=20features=20?= =?UTF-8?q?=E2=80=94=20askUserQuestions=20+=20documents.upsert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new event types accepted on the existing case-status webhook (eventType discriminator added; legacy status_change still default): * **missing_precedent_created** → ``ctx.issues.askUserQuestions`` with a single-choice question {upload | irrelevant | defer}. continuationPolicy=wake_assignee_on_accept routes the chair's answer straight back to the CEO heartbeat without an extra hop. * **export_complete** → ``ctx.issues.documents.upsert`` with a markdown "final-decision" doc that links back to the DOCX on legal-ai. (The SDK's documents API stores text only — binary attachment isn't natively supported here.) Manifest: +issue.documents.write capability (issue.interactions.create was already declared in the previous SDK upgrade). Tested: plugin activates with all 18 capabilities, 8 tools + 3 jobs + 1 webhook + 2 event subs registered. --- src/manifest.ts | 1 + src/worker.ts | 149 ++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 144 insertions(+), 6 deletions(-) diff --git a/src/manifest.ts b/src/manifest.ts index 5a8e2ad..9b0ef47 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -15,6 +15,7 @@ export default { "issue.comments.read", "issue.comments.create", "issue.interactions.create", + "issue.documents.write", "agent.tools.register", "agents.invoke", "http.outbound", diff --git a/src/worker.ts b/src/worker.ts index a282432..b69a369 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -878,27 +878,47 @@ const plugin = definePlugin({ if (endpointKey !== "case-status") return; + // Webhook payload is a discriminated union by `eventType`: + // • undefined / "status_change" — legacy case-status update (default) + // • "missing_precedent_created" — ask Daphna to upload a missing citation + // • "export_complete" — attach a generated DOCX to the linked issue const payload = parsedBody as { + eventType?: string; caseNumber: string; - oldStatus: string; - newStatus: string; companyId: string | null; timestamp: string; + // status_change fields + oldStatus?: string; + newStatus?: string; + // missing_precedent_created fields + missingPrecedent?: { + id: string; + citation: string; + citedByParty?: string; + citedByPartyName?: string; + legalTopic?: string; + legalIssue?: string; + }; + // export_complete fields + docxBase64?: string; + docxFilename?: string; + docxTitle?: string; }; - const { caseNumber, oldStatus, newStatus, companyId } = payload; + const { caseNumber, companyId } = payload; + const eventType = payload.eventType ?? "status_change"; - if (!caseNumber || !newStatus || !companyId) { + if (!caseNumber || !companyId) { pluginCtx.logger.warn("onWebhook: malformed payload", { + eventType, caseNumber, - newStatus, companyId, }); return; } pluginCtx.logger.info( - `Webhook: case ${caseNumber} ${oldStatus} → ${newStatus}`, + `Webhook: case ${caseNumber} eventType=${eventType}`, { companyId, }, @@ -927,6 +947,123 @@ const plugin = definePlugin({ return; } + // ── Branch by eventType ──────────────────────────────────────── + + // EVENT: missing_precedent_created — ask Daphna to upload via SDK + // interaction (ask_user_questions). Avoids plain-text comment for + // a decision that has a clear set of choices. + if (eventType === "missing_precedent_created" && payload.missingPrecedent) { + const mp = payload.missingPrecedent; + const partyLabel = mp.citedByPartyName || mp.citedByParty || "אחד הצדדים"; + try { + await pluginCtx.issues.askUserQuestions( + linkedIssueId, + { + continuationPolicy: "wake_assignee_on_accept", + payload: { + version: 1, + title: `פסיקה חסרה בקורפוס: ${mp.citation}`, + questions: [ + { + id: `missing_precedent:${mp.id}`, + prompt: `יש להעלות את ${mp.citation}`, + helpText: [ + `הציטוט הוזכר על-ידי ${partyLabel} בתיק ${caseNumber}.`, + mp.legalTopic ? `נושא: ${mp.legalTopic}` : "", + mp.legalIssue ? `סוגיה: ${mp.legalIssue}` : "", + "", + "כדי שהמערכת תוכל לבחון את הטענה מול הפסיקה — יש להעלות את ה-PDF/DOCX לדף /missing-precedents.", + ] + .filter(Boolean) + .join("\n"), + selectionMode: "single", + required: true, + options: [ + { id: "upload", label: "אני מעלה PDF/DOCX" }, + { id: "irrelevant", label: "ההלכה לא רלוונטית" }, + { id: "defer", label: "אכריע מאוחר יותר" }, + ], + }, + ], + }, + }, + companyId, + ); + pluginCtx.logger.info( + "askUserQuestions: missing_precedent prompt sent", + { + caseNumber, + missingPrecedentId: mp.id, + }, + ); + } catch (err) { + pluginCtx.logger.error( + "askUserQuestions failed for missing_precedent", + { + caseNumber, + missingPrecedentId: mp.id, + error: String(err), + }, + ); + } + return; + } + + // EVENT: export_complete — attach a markdown "final decision" + // document to the issue, with a link back to the DOCX in legal-ai. + // The SDK's `documents.upsert` stores text (markdown), so we keep + // the binary DOCX in legal-ai and reference it; the issue page + // shows the document as a discoverable artifact. + if (eventType === "export_complete" && payload.docxFilename) { + const filename = payload.docxFilename; + const title = payload.docxTitle || `החלטה סופית — ${caseNumber}`; + const docxUrl = `https://legal-ai.nautilus.marcusgroup.org/api/cases/${encodeURIComponent(caseNumber)}/export/download?filename=${encodeURIComponent(filename)}`; + const markdownBody = [ + `# ${title}`, + "", + `**תיק:** ${caseNumber}`, + `**קובץ:** \`${filename}\``, + `**הופק:** ${payload.timestamp}`, + "", + `[הורדה](${docxUrl})`, + "", + "---", + "", + 'החלטה זו יוצאה אוטומטית ע"י legal-ai והוצמדה ל-issue זה.', + ].join("\n"); + try { + await pluginCtx.issues.documents.upsert({ + issueId: linkedIssueId, + companyId, + key: `final-decision:${caseNumber}`, + body: markdownBody, + title, + format: "markdown", + changeSummary: `Auto-attached final DOCX (${filename})`, + }); + pluginCtx.logger.info("documents.upsert: final-decision attached", { + caseNumber, + filename, + }); + } catch (err) { + pluginCtx.logger.error("documents.upsert failed for export_complete", { + caseNumber, + filename, + error: String(err), + }); + } + return; + } + + // EVENT: status_change (default) — legacy behavior. + const { oldStatus = "", newStatus = "" } = payload; + if (!newStatus) { + pluginCtx.logger.warn("onWebhook status_change: missing newStatus", { + caseNumber, + }); + return; + } + // Status label map (Hebrew) const statusLabels: Record = { new: "📂 תיק חדש",