feat: add onWebhook handler for case-status events

Adds the onWebhook lifecycle hook to the definePlugin() call. When
legal-ai POSTs to /webhooks/case-status, the handler finds the linked
Paperclip issue (via plugin state scan), posts a Hebrew status comment,
and wakes the CEO agent on qa_failed. Hoists PluginContext and
CEO_AGENT_IDS to module scope so onWebhook can access them after setup().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-16 17:16:28 +00:00
parent bf639063f7
commit 6b856dbe85

View File

@@ -1,8 +1,19 @@
import { definePlugin, runWorker } from "@paperclipai/plugin-sdk";
import type { PluginContext, PluginWebhookInput } from "@paperclipai/plugin-sdk";
import { LegalApi } from "./legal-api.js";
// Hoisted so onWebhook can access the context after setup() completes.
let pluginCtx: PluginContext | null = null;
// Per-company CEO agent IDs (shared between setup and onWebhook).
const CEO_AGENT_IDS: Record<string, string> = {
"42a7acd0-30c5-4cbd-ac97-7424f65df294": "752cebdd-6748-4a04-aacd-c7ab0294ef33", // CMP (רישוי ובניה)
"8639e837-4c9d-47fa-a76b-95788d651896": "cdbfa8bc-3d61-41a4-a2e7-677ec7d34562", // CMPA (היטלי השבחה)
};
const plugin = definePlugin({
async setup(ctx) {
pluginCtx = ctx; // save for onWebhook
const config = (await ctx.config.get()) as {
legalApiBaseUrl?: string;
} | null;
@@ -517,8 +528,7 @@ const plugin = definePlugin({
}
});
// Route user comments through CEO agent
const CEO_AGENT_ID = "752cebdd-6748-4a04-aacd-c7ab0294ef33";
// Route user comments through CEO agent — per company
ctx.events.on("issue.comment.created", async (event) => {
// Only intercept human comments — not agent comments (prevents loops)
@@ -570,10 +580,18 @@ const plugin = definePlugin({
}
}
// Wake the CEO agent with the comment context
// Wake the CEO agent for this company
const ceoAgentId = CEO_AGENT_IDS[event.companyId];
if (!ceoAgentId) {
ctx.logger.warn("No CEO agent mapped for company", {
companyId: event.companyId,
});
return;
}
try {
const { runId } = await ctx.agents.invoke(
CEO_AGENT_ID,
ceoAgentId,
event.companyId,
{
prompt: [
@@ -694,6 +712,107 @@ const plugin = definePlugin({
async onHealth() {
return { status: "ok" as const };
},
async onWebhook(input: PluginWebhookInput): Promise<void> {
if (!pluginCtx) return; // not yet initialized
const { endpointKey, parsedBody } = input;
if (endpointKey !== "case-status") return;
const payload = parsedBody as {
caseNumber: string;
oldStatus: string;
newStatus: string;
companyId: string | null;
timestamp: string;
};
const { caseNumber, oldStatus, newStatus, companyId } = payload;
pluginCtx.logger.info(`Webhook: case ${caseNumber} ${oldStatus}${newStatus}`, {
companyId,
});
if (!companyId) {
pluginCtx.logger.warn("onWebhook: missing companyId in payload", { caseNumber });
return;
}
// Find the Paperclip issue linked to this case number by scanning plugin state.
// State stores: issue.id → case_number (scopeKind=issue, stateKey=legal-case-number)
const issues = await pluginCtx.issues.list({ companyId });
let linkedIssueId: string | null = null;
for (const issue of issues) {
const linkedCase = await pluginCtx.state.get({
scopeKind: "issue",
scopeId: issue.id,
stateKey: "legal-case-number",
});
if (linkedCase === caseNumber) {
linkedIssueId = issue.id;
break;
}
}
if (!linkedIssueId) {
pluginCtx.logger.warn(`onWebhook: no Paperclip issue linked to case ${caseNumber}`);
return;
}
// Status label map (Hebrew)
const statusLabels: Record<string, string> = {
new: "📂 תיק חדש",
uploading: "📤 העלאת מסמכים",
processing: "⚙️ עיבוד מסמכים",
documents_ready: "📁 מסמכים מוכנים",
outcome_set: "🎯 תוצאה הוזנה",
brainstorming: "💡 סיעור מוחות",
direction_approved: "✅ כיוון אושר",
drafting: "✍️ כתיבה בתהליך",
qa_review: "🔍 בדיקת איכות",
in_progress: "🔄 בעבודה",
drafted: "✍️ טיוטה מוכנה",
qa_failed: "❌ QA נכשל",
exported: "📄 יוצא ל-DOCX",
reviewed: "✅ נבדק",
final: "🎯 סופי",
};
const label = statusLabels[newStatus] ?? newStatus;
// Post a Hebrew status comment on the linked issue
await pluginCtx.issues.createComment(
linkedIssueId,
`**עדכון סטטוס תיק ${caseNumber}:** ${label} (היה: ${oldStatus})`,
companyId,
);
// Wake the CEO agent if QA failed
if (newStatus === "qa_failed") {
const ceoId = CEO_AGENT_IDS[companyId];
if (ceoId) {
try {
const { runId } = await pluginCtx.agents.invoke(ceoId, companyId, {
prompt: `תיק ${caseNumber} נכשל בבדיקת QA. עיין בתוצאות QA ותקן את הבעיות לפני שתמשיך.`,
reason: "qa_failed webhook",
});
pluginCtx.logger.info(`Invoked CEO agent for qa_failed`, {
caseNumber,
ceoId,
runId,
});
} catch (err) {
pluginCtx.logger.error("Failed to invoke CEO agent for qa_failed", {
caseNumber,
error: String(err),
});
}
} else {
pluginCtx.logger.warn("onWebhook: no CEO agent mapped for company", {
companyId,
});
}
}
},
});
export default plugin;