From 40cd83b53cbbd09f9e047dfd22531339a3afebab Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 22 May 2026 12:53:20 +0000 Subject: [PATCH] Remove Briefs plugin from PR --- Dockerfile | 1 - .../plugin-briefs/migrations/001_briefs.sql | 103 -- packages/plugins/plugin-briefs/package.json | 37 - packages/plugins/plugin-briefs/src/briefs.ts | 1043 ----------------- packages/plugins/plugin-briefs/src/index.ts | 2 - .../plugins/plugin-briefs/src/manifest.ts | 71 -- packages/plugins/plugin-briefs/src/worker.ts | 291 ----- packages/plugins/plugin-briefs/tsconfig.json | 18 - .../plugins/plugin-briefs/vitest.config.ts | 8 - 9 files changed, 1574 deletions(-) delete mode 100644 packages/plugins/plugin-briefs/migrations/001_briefs.sql delete mode 100644 packages/plugins/plugin-briefs/package.json delete mode 100644 packages/plugins/plugin-briefs/src/briefs.ts delete mode 100644 packages/plugins/plugin-briefs/src/index.ts delete mode 100644 packages/plugins/plugin-briefs/src/manifest.ts delete mode 100644 packages/plugins/plugin-briefs/src/worker.ts delete mode 100644 packages/plugins/plugin-briefs/tsconfig.json delete mode 100644 packages/plugins/plugin-briefs/vitest.config.ts diff --git a/Dockerfile b/Dockerfile index 8aabd5fd57f..03f26942b40 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,7 +35,6 @@ COPY packages/adapters/pi-local/package.json packages/adapters/pi-local/ COPY packages/plugins/sdk/package.json packages/plugins/sdk/ COPY --parents packages/plugins/sandbox-providers/./*/package.json packages/plugins/sandbox-providers/ COPY packages/plugins/paperclip-plugin-fake-sandbox/package.json packages/plugins/paperclip-plugin-fake-sandbox/ -COPY packages/plugins/plugin-briefs/package.json packages/plugins/plugin-briefs/ COPY packages/plugins/plugin-llm-wiki/package.json packages/plugins/plugin-llm-wiki/ COPY packages/plugins/plugin-workspace-diff/package.json packages/plugins/plugin-workspace-diff/ COPY patches/ patches/ diff --git a/packages/plugins/plugin-briefs/migrations/001_briefs.sql b/packages/plugins/plugin-briefs/migrations/001_briefs.sql deleted file mode 100644 index 4a3a1824f7a..00000000000 --- a/packages/plugins/plugin-briefs/migrations/001_briefs.sql +++ /dev/null @@ -1,103 +0,0 @@ -CREATE TABLE plugin_briefs_eacdc5f4df.briefs_cards ( - id uuid PRIMARY KEY, - company_id uuid NOT NULL REFERENCES public.companies(id) ON DELETE CASCADE, - user_id text NOT NULL, - slug text NOT NULL, - title text NOT NULL, - grouping_description text NOT NULL, - grouping_hash text NOT NULL, - root_issue_id uuid REFERENCES public.issues(id) ON DELETE SET NULL, - state text NOT NULL CHECK (state IN ('error', 'blocked', 'waiting-user', 'waiting-reviewer', 'live', 'done', 'stale')), - summary_status text NOT NULL CHECK (summary_status IN ('ok', 'pending', 'fallback')), - pinned boolean NOT NULL DEFAULT false, - hidden boolean NOT NULL DEFAULT false, - stale_at timestamptz NOT NULL, - expires_at timestamptz, - latest_snapshot_id uuid, - last_meaningful_event_at timestamptz NOT NULL, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - UNIQUE (company_id, user_id, grouping_hash), - UNIQUE (company_id, user_id, slug) -); - -CREATE TABLE plugin_briefs_eacdc5f4df.briefs_card_sources ( - id uuid PRIMARY KEY, - company_id uuid NOT NULL REFERENCES public.companies(id) ON DELETE CASCADE, - card_id uuid NOT NULL REFERENCES plugin_briefs_eacdc5f4df.briefs_cards(id) ON DELETE CASCADE, - source_kind text NOT NULL CHECK (source_kind IN ('issue_tree', 'issue', 'run', 'comment', 'document', 'work_product', 'interaction', 'activity_event', 'approval')), - source_id text NOT NULL, - source_issue_id uuid REFERENCES public.issues(id) ON DELETE SET NULL, - source_run_id uuid REFERENCES public.heartbeat_runs(id) ON DELETE SET NULL, - identifier text, - title_line text NOT NULL, - right_tag text NOT NULL, - link_path text NOT NULL, - is_intra_tree_blocked boolean, - event_at timestamptz NOT NULL, - metadata jsonb NOT NULL DEFAULT '{}'::jsonb, - created_at timestamptz NOT NULL DEFAULT now(), - UNIQUE (card_id, source_kind, source_id) -); - -CREATE TABLE plugin_briefs_eacdc5f4df.briefs_card_snapshots ( - id uuid PRIMARY KEY, - company_id uuid NOT NULL REFERENCES public.companies(id) ON DELETE CASCADE, - card_id uuid NOT NULL REFERENCES plugin_briefs_eacdc5f4df.briefs_cards(id) ON DELETE CASCADE, - summary_paragraph text, - summary_status text NOT NULL CHECK (summary_status IN ('ok', 'pending', 'fallback')), - summary_model text, - summary_tokens_in integer, - summary_tokens_out integer, - summary_failure_reason text CHECK (summary_failure_reason IS NULL OR summary_failure_reason IN ('model_error', 'truncation_failed', 'budget_capped', 'safety_block')), - task_rows jsonb NOT NULL DEFAULT '[]'::jsonb, - evidence_source_ids jsonb NOT NULL DEFAULT '[]'::jsonb, - generated_by_agent_id uuid REFERENCES public.agents(id) ON DELETE SET NULL, - generated_by_run_id uuid REFERENCES public.heartbeat_runs(id) ON DELETE SET NULL, - created_at timestamptz NOT NULL DEFAULT now() -); - -ALTER TABLE plugin_briefs_eacdc5f4df.briefs_cards - ADD CONSTRAINT briefs_cards_latest_snapshot_fk - FOREIGN KEY (latest_snapshot_id) - REFERENCES plugin_briefs_eacdc5f4df.briefs_card_snapshots(id) - ON DELETE SET NULL; - -CREATE TABLE plugin_briefs_eacdc5f4df.briefs_user_preferences ( - id uuid PRIMARY KEY, - company_id uuid NOT NULL REFERENCES public.companies(id) ON DELETE CASCADE, - user_id text NOT NULL, - cadence text NOT NULL DEFAULT 'hourly', - discovery_window_days integer NOT NULL DEFAULT 14 CHECK (discovery_window_days > 0), - retention_days integer NOT NULL DEFAULT 7 CHECK (retention_days > 0), - done_retention_hours integer NOT NULL DEFAULT 72 CHECK (done_retention_hours > 0), - stale_after_days integer NOT NULL DEFAULT 7 CHECK (stale_after_days > 0), - max_unpinned_cards integer NOT NULL DEFAULT 50 CHECK (max_unpinned_cards > 0), - settings jsonb NOT NULL DEFAULT '{}'::jsonb, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - UNIQUE (company_id, user_id) -); - -CREATE TABLE plugin_briefs_eacdc5f4df.briefs_cursors ( - id uuid PRIMARY KEY, - company_id uuid NOT NULL REFERENCES public.companies(id) ON DELETE CASCADE, - user_id text NOT NULL, - cursor_key text NOT NULL, - last_event_at timestamptz, - last_event_id text, - overlap_window_seconds integer NOT NULL DEFAULT 3600 CHECK (overlap_window_seconds >= 0), - dedupe_keys jsonb NOT NULL DEFAULT '[]'::jsonb, - metadata jsonb NOT NULL DEFAULT '{}'::jsonb, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - UNIQUE (company_id, user_id, cursor_key) -); - -CREATE INDEX briefs_cards_company_user_state_idx ON plugin_briefs_eacdc5f4df.briefs_cards (company_id, user_id, hidden, state, pinned, last_meaningful_event_at DESC); -CREATE INDEX briefs_cards_company_root_idx ON plugin_briefs_eacdc5f4df.briefs_cards (company_id, root_issue_id); -CREATE INDEX briefs_cards_expiry_idx ON plugin_briefs_eacdc5f4df.briefs_cards (company_id, expires_at) WHERE expires_at IS NOT NULL AND pinned = false; -CREATE INDEX briefs_card_sources_card_event_idx ON plugin_briefs_eacdc5f4df.briefs_card_sources (card_id, event_at DESC); -CREATE INDEX briefs_card_sources_company_source_idx ON plugin_briefs_eacdc5f4df.briefs_card_sources (company_id, source_kind, source_id); -CREATE INDEX briefs_snapshots_card_created_idx ON plugin_briefs_eacdc5f4df.briefs_card_snapshots (card_id, created_at DESC); -CREATE INDEX briefs_cursors_company_user_idx ON plugin_briefs_eacdc5f4df.briefs_cursors (company_id, user_id, updated_at DESC); diff --git a/packages/plugins/plugin-briefs/package.json b/packages/plugins/plugin-briefs/package.json deleted file mode 100644 index fc77bfb5e85..00000000000 --- a/packages/plugins/plugin-briefs/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "@paperclipai/plugin-briefs", - "version": "0.1.0", - "type": "module", - "private": true, - "description": "Briefing cards plugin for deterministic Paperclip work summaries.", - "files": [ - "dist", - "migrations", - "README.md" - ], - "scripts": { - "postinstall": "node ../../../scripts/link-plugin-dev-sdk.mjs", - "prebuild": "pnpm --filter @paperclipai/plugin-sdk ensure-build-deps", - "build": "tsc", - "test": "vitest run --config ./vitest.config.ts", - "typecheck": "pnpm --filter @paperclipai/plugin-sdk ensure-build-deps && tsc --noEmit" - }, - "paperclipPlugin": { - "manifest": "./dist/manifest.js", - "worker": "./dist/worker.js" - }, - "keywords": [ - "paperclip", - "plugin", - "briefs", - "briefing" - ], - "author": "Paperclip", - "license": "MIT", - "devDependencies": { - "@paperclipai/plugin-sdk": "workspace:*", - "@types/node": "^24.6.0", - "typescript": "^5.7.3", - "vitest": "^3.0.5" - } -} diff --git a/packages/plugins/plugin-briefs/src/briefs.ts b/packages/plugins/plugin-briefs/src/briefs.ts deleted file mode 100644 index b675cdb7fb6..00000000000 --- a/packages/plugins/plugin-briefs/src/briefs.ts +++ /dev/null @@ -1,1043 +0,0 @@ -import { createHash, randomUUID } from "node:crypto"; -import type { - Issue, - IssueComment, - IssueDocumentSummary, - PluginDatabaseClient, - PluginIssueApprovalSummary, - PluginIssueRunSummary, - PluginIssueSubtree, -} from "@paperclipai/plugin-sdk"; - -export const BRIEF_CARD_STATES = [ - "error", - "blocked", - "waiting-user", - "waiting-reviewer", - "live", - "done", - "stale", -] as const; - -export type BriefCardState = (typeof BRIEF_CARD_STATES)[number]; - -export const BRIEF_SUMMARY_STATUSES = ["ok", "pending", "fallback"] as const; -export type BriefSummaryStatus = (typeof BRIEF_SUMMARY_STATUSES)[number]; - -export type BriefSummaryFailureReason = - | "model_error" - | "truncation_failed" - | "budget_capped" - | "safety_block"; - -export type BriefSourceKind = - | "issue_tree" - | "issue" - | "run" - | "comment" - | "document" - | "work_product" - | "interaction" - | "activity_event" - | "approval"; - -export interface BriefUserPreferences { - discoveryWindowDays: number; - retentionDays: number; - doneRetentionHours: number; - staleAfterDays: number; - maxUnpinnedCards: number; -} - -export const DEFAULT_BRIEF_USER_PREFERENCES: BriefUserPreferences = { - discoveryWindowDays: 14, - retentionDays: 7, - doneRetentionHours: 72, - staleAfterDays: 7, - maxUnpinnedCards: 50, -}; - -export interface BriefInteraction { - id: string; - companyId: string; - issueId: string; - kind: "suggest_tasks" | "ask_user_questions" | "request_confirmation" | string; - status: string; - title?: string | null; - summary?: string | null; - createdByUserId?: string | null; - resolvedByUserId?: string | null; - resolvedAt?: Date | string | null; - createdAt: Date | string; - updatedAt: Date | string; -} - -export interface BriefWorkProduct { - id: string; - companyId: string; - issueId: string; - title: string; - type: string; - status: string; - url?: string | null; - createdAt: Date | string; - updatedAt: Date | string; -} - -export interface BriefActivityEvent { - id: string; - companyId: string; - action: string; - entityType: string; - entityId: string; - createdAt: Date | string; - details?: Record | null; -} - -export interface BriefSourceCollections { - commentsByIssueId?: Record; - documentsByIssueId?: Record; - activeRunsByIssueId?: Record; - runs?: PluginIssueRunSummary[]; - approvals?: PluginIssueApprovalSummary[]; - interactionsByIssueId?: Record; - workProductsByIssueId?: Record; - activityEvents?: BriefActivityEvent[]; -} - -export interface BriefTaskRow { - kind: Exclude | "work_product"; - sourceId: string; - identifier: string | null; - titleLine: string; - rightTag: string; - linkPath: string; - isIntraTreeBlocked?: boolean; -} - -export interface BriefCardSourceDraft extends Omit { - id?: string; - companyId: string; - cardId?: string; - kind: BriefSourceKind; - sourceIssueId: string | null; - sourceRunId?: string | null; - eventAt: string; - metadata?: Record; -} - -export interface BriefCardDraft { - id?: string; - companyId: string; - userId: string; - slug: string; - title: string; - groupingDescription: string; - groupingHash: string; - rootIssueId: string | null; - state: BriefCardState; - summaryStatus: BriefSummaryStatus; - summaryParagraph: string | null; - summaryModel?: string | null; - summaryTokensIn?: number | null; - summaryTokensOut?: number | null; - summaryFailureReason?: BriefSummaryFailureReason | null; - pinned: boolean; - hidden: boolean; - staleAt: string; - expiresAt: string | null; - lastMeaningfulEventAt: string; - sources: BriefCardSourceDraft[]; - taskRows: BriefTaskRow[]; - evidenceSourceIds: string[]; -} - -export interface ExistingBriefCardState { - id?: string; - slug?: string | null; - pinned?: boolean | null; - hidden?: boolean | null; -} - -export interface BuildBriefCardInput { - companyId: string; - userId: string; - subtree: PluginIssueSubtree; - sources?: BriefSourceCollections; - now?: Date | string; - preferences?: Partial; - companyIssuePrefix?: string | null; - existingCard?: ExistingBriefCardState | null; - forceRelevant?: boolean; - summary?: { - status: BriefSummaryStatus; - paragraph?: string | null; - model?: string | null; - tokensIn?: number | null; - tokensOut?: number | null; - failureReason?: BriefSummaryFailureReason | null; - evidenceSourceIds?: string[]; - }; -} - -export interface BriefRelevanceResult { - relevant: boolean; - reasons: string[]; - lastMeaningfulEventAt: string; -} - -export interface BriefCursorState { - lastEventAt: string | null; - lastEventId: string | null; - dedupeKeys: string[]; -} - -export interface BriefCursorEvent { - id: string; - eventAt: Date | string; - dedupeKey?: string | null; -} - -export interface BriefCursorAdvanceResult { - acceptedEvents: TEvent[]; - nextCursor: BriefCursorState; -} - -const MS_PER_HOUR = 60 * 60 * 1000; -const MS_PER_DAY = 24 * MS_PER_HOUR; -const ACTIVE_RUN_STATUSES = new Set(["queued", "starting", "running"]); -const FAILED_RUN_STATUSES = new Set(["failed", "error"]); -const PENDING_INTERACTION_STATUSES = new Set(["pending"]); -const NON_TERMINAL_ISSUE_STATUSES = new Set(["backlog", "todo", "in_progress", "in_review", "blocked"]); - -function toDate(value: Date | string | null | undefined): Date | null { - if (!value) return null; - const date = value instanceof Date ? value : new Date(value); - return Number.isNaN(date.getTime()) ? null : date; -} - -function toIso(value: Date | string): string { - const date = toDate(value); - if (!date) throw new Error(`Invalid date: ${String(value)}`); - return date.toISOString(); -} - -function addMs(value: Date, amountMs: number): string { - return new Date(value.getTime() + amountMs).toISOString(); -} - -function maxDate(values: Array, fallback: Date): Date { - let result = fallback; - for (const value of values) { - const date = toDate(value); - if (date && date.getTime() > result.getTime()) result = date; - } - return result; -} - -function truncate(value: string, max: number): string { - const normalized = value.replace(/\s+/g, " ").trim(); - if (normalized.length <= max) return normalized; - return `${normalized.slice(0, Math.max(0, max - 1)).trimEnd()}…`; -} - -function slugify(value: string): string { - const slug = value - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-+|-+$/g, "") - .replace(/-+/g, "-") - .slice(0, 56); - return slug || "brief"; -} - -function hash(value: string): string { - return createHash("sha256").update(value).digest("hex"); -} - -function issuePath(issue: Pick, companyIssuePrefix?: string | null): string { - const ref = issue.identifier ?? issue.id; - return companyIssuePrefix ? `/${companyIssuePrefix}/issues/${ref}` : `/issues/${ref}`; -} - -function runPath(run: Pick, companyIssuePrefix?: string | null): string { - const path = `/agents/${run.agentId}/runs/${run.id}`; - return companyIssuePrefix ? `/${companyIssuePrefix}${path}` : path; -} - -function issueById(subtree: PluginIssueSubtree): Map { - return new Map(subtree.issues.map((issue) => [issue.id, issue])); -} - -function assertCompanyScoped(input: BuildBriefCardInput): void { - if (input.subtree.companyId !== input.companyId) { - throw new Error("Brief subtree company does not match requested company"); - } - for (const issue of input.subtree.issues) { - if (issue.companyId !== input.companyId) { - throw new Error(`Brief source issue ${issue.id} belongs to another company`); - } - } - for (const comments of Object.values(input.sources?.commentsByIssueId ?? {})) { - for (const comment of comments) { - if (comment.companyId !== input.companyId) { - throw new Error(`Brief source comment ${comment.id} belongs to another company`); - } - } - } - for (const docs of Object.values(input.sources?.documentsByIssueId ?? {})) { - for (const doc of docs) { - if (doc.companyId !== input.companyId) { - throw new Error(`Brief source document ${doc.id} belongs to another company`); - } - } - } - for (const interactions of Object.values(input.sources?.interactionsByIssueId ?? {})) { - for (const interaction of interactions) { - if (interaction.companyId !== input.companyId) { - throw new Error(`Brief source interaction ${interaction.id} belongs to another company`); - } - } - } - for (const products of Object.values(input.sources?.workProductsByIssueId ?? {})) { - for (const product of products) { - if (product.companyId !== input.companyId) { - throw new Error(`Brief source work product ${product.id} belongs to another company`); - } - } - } - for (const event of input.sources?.activityEvents ?? []) { - if (event.companyId !== input.companyId) { - throw new Error(`Brief source activity event ${event.id} belongs to another company`); - } - } -} - -function sourceDates(subtree: PluginIssueSubtree, sources: BriefSourceCollections | undefined): Array { - return [ - ...subtree.issues.flatMap((issue) => [issue.updatedAt, issue.startedAt, issue.completedAt, issue.cancelledAt]), - ...Object.values(sources?.commentsByIssueId ?? {}).flat().map((comment) => comment.createdAt), - ...Object.values(sources?.documentsByIssueId ?? subtree.documents ?? {}).flat().map((doc) => doc.updatedAt), - ...Object.values(sources?.activeRunsByIssueId ?? subtree.activeRuns ?? {}).flat().flatMap((run) => [ - run.startedAt, - run.finishedAt, - run.createdAt, - ]), - ...(sources?.runs ?? []).flatMap((run) => [run.startedAt, run.finishedAt, run.createdAt]), - ...(sources?.approvals ?? []).flatMap((approval) => [approval.decidedAt, approval.createdAt]), - ...Object.values(sources?.interactionsByIssueId ?? {}).flat().flatMap((interaction) => [ - interaction.resolvedAt, - interaction.updatedAt, - interaction.createdAt, - ]), - ...Object.values(sources?.workProductsByIssueId ?? {}).flat().map((product) => product.updatedAt), - ...(sources?.activityEvents ?? []).map((event) => event.createdAt), - ]; -} - -export function resolveBriefRelevance(input: { - companyId: string; - userId: string; - subtree: PluginIssueSubtree; - sources?: BriefSourceCollections; - now?: Date | string; - preferences?: Partial; -}): BriefRelevanceResult { - const preferences = { ...DEFAULT_BRIEF_USER_PREFERENCES, ...input.preferences }; - const now = toDate(input.now) ?? new Date(); - const root = input.subtree.issues.find((issue) => issue.id === input.subtree.rootIssueId) ?? input.subtree.issues[0]; - const reasons: string[] = []; - - if (root?.createdByUserId === input.userId) reasons.push("root-created-by-user"); - if (input.subtree.issues.some((issue) => issue.createdByUserId === input.userId)) reasons.push("issue-created-by-user"); - if (input.subtree.issues.some((issue) => issue.assigneeUserId === input.userId)) reasons.push("issue-assigned-to-user"); - if ( - input.subtree.issues.some((issue) => { - const participant = issue.executionState?.currentParticipant; - const returnAssignee = issue.executionState?.returnAssignee; - return ( - (participant?.type === "user" && participant.userId === input.userId) || - (returnAssignee?.type === "user" && returnAssignee.userId === input.userId) - ); - }) - ) { - reasons.push("user-execution-participant"); - } - if (Object.values(input.sources?.commentsByIssueId ?? {}).flat().some((comment) => comment.authorUserId === input.userId)) { - reasons.push("user-authored-comment"); - } - if ( - Object.values(input.sources?.documentsByIssueId ?? input.subtree.documents ?? {}) - .flat() - .some((doc) => doc.createdByUserId === input.userId || doc.updatedByUserId === input.userId) - ) { - reasons.push("user-authored-document"); - } - if ( - Object.values(input.sources?.interactionsByIssueId ?? {}) - .flat() - .some((interaction) => interaction.createdByUserId === input.userId || interaction.resolvedByUserId === input.userId) - ) { - reasons.push("user-touched-interaction"); - } - if ( - (input.sources?.approvals ?? []).some( - (approval) => approval.requestedByUserId === input.userId || approval.decidedByUserId === input.userId, - ) - ) { - reasons.push("user-touched-approval"); - } - - const lastMeaningfulEvent = maxDate(sourceDates(input.subtree, input.sources), root ? toDate(root.updatedAt) ?? now : now); - const fresh = now.getTime() - lastMeaningfulEvent.getTime() <= preferences.discoveryWindowDays * MS_PER_DAY; - - return { - relevant: reasons.length > 0 && fresh, - reasons, - lastMeaningfulEventAt: lastMeaningfulEvent.toISOString(), - }; -} - -function failedRunsByIssueWithin(runs: PluginIssueRunSummary[], since: Date): Map { - const counts = new Map(); - for (const run of runs) { - const issueId = run.issueId; - if (!issueId || !FAILED_RUN_STATUSES.has(run.status)) continue; - const eventAt = toDate(run.finishedAt) ?? toDate(run.createdAt); - if (!eventAt || eventAt.getTime() < since.getTime()) continue; - counts.set(issueId, (counts.get(issueId) ?? 0) + 1); - } - return counts; -} - -function hasOutOfTreeBlocker(issueId: string, treeIssueIds: Set, relations: PluginIssueSubtree["relations"]): boolean { - const blockedBy = relations?.[issueId]?.blockedBy ?? []; - return blockedBy.some((blocker) => !treeIssueIds.has(blocker.id) && blocker.status !== "done"); -} - -function hasAttentionBlocker(issue: Issue): boolean { - const attention = issue.blockerAttention; - if (!attention) return false; - return attention.state === "stalled" || attention.state === "needs_attention" || attention.reason === "stalled_review" || attention.reason === "attention_required"; -} - -export function resolveBriefState(input: { - subtree: PluginIssueSubtree; - sources?: BriefSourceCollections; - userId: string; - now?: Date | string; - preferences?: Partial; -}): BriefCardState { - const preferences = { ...DEFAULT_BRIEF_USER_PREFERENCES, ...input.preferences }; - const now = toDate(input.now) ?? new Date(); - const treeIssueIds = new Set(input.subtree.issueIds); - const allRuns = [ - ...Object.values(input.sources?.activeRunsByIssueId ?? input.subtree.activeRuns ?? {}).flat(), - ...(input.sources?.runs ?? []), - ]; - - if (input.subtree.issues.some((issue) => issue.activeRecoveryAction)) return "error"; - const failedCounts = failedRunsByIssueWithin(allRuns, new Date(now.getTime() - 24 * MS_PER_HOUR)); - if ([...failedCounts.values()].some((count) => count >= 3)) return "error"; - - if ( - input.subtree.issues.some((issue) => ( - issue.status === "blocked" && - (hasOutOfTreeBlocker(issue.id, treeIssueIds, input.subtree.relations) || hasAttentionBlocker(issue)) - )) - ) { - return "blocked"; - } - - const pendingInteractions = Object.values(input.sources?.interactionsByIssueId ?? {}) - .flat() - .filter((interaction) => PENDING_INTERACTION_STATUSES.has(interaction.status)); - if (pendingInteractions.some((interaction) => ( - interaction.kind === "request_confirmation" || - interaction.kind === "ask_user_questions" || - interaction.kind === "suggest_tasks" - ))) { - return "waiting-user"; - } - if ((input.sources?.approvals ?? []).some((approval) => approval.status === "pending" && approval.decidedByUserId == null)) { - return "waiting-user"; - } - - const hasUserReview = input.subtree.issues.some((issue) => { - const participant = issue.executionState?.currentParticipant; - return issue.status === "in_review" && participant?.type === "user" && participant.userId === input.userId; - }); - if (hasUserReview) return "waiting-user"; - - const hasReviewerWait = input.subtree.issues.some((issue) => { - if (issue.status !== "in_review") return false; - const participant = issue.executionState?.currentParticipant; - return participant !== null || (input.sources?.approvals ?? []).some((approval) => approval.issueId === issue.id && approval.status === "pending"); - }); - if (hasReviewerWait) return "waiting-reviewer"; - - const lastMeaningfulEvent = maxDate(sourceDates(input.subtree, input.sources), now); - const liveWindowOpen = now.getTime() - lastMeaningfulEvent.getTime() <= 6 * MS_PER_HOUR; - const hasActiveRun = allRuns.some((run) => ACTIVE_RUN_STATUSES.has(run.status)); - const hasInProgress = input.subtree.issues.some((issue) => issue.status === "in_progress"); - if ((hasActiveRun || hasInProgress) && liveWindowOpen) return "live"; - - const root = input.subtree.issues.find((issue) => issue.id === input.subtree.rootIssueId) ?? input.subtree.issues[0]; - const completedAt = toDate(root?.completedAt); - if ( - root?.status === "done" && - completedAt && - now.getTime() - completedAt.getTime() <= preferences.doneRetentionHours * MS_PER_HOUR - ) { - return "done"; - } - - return "stale"; -} - -function lifecycle(input: { - pinned: boolean; - state: BriefCardState; - lastMeaningfulEventAt: string; - preferences: BriefUserPreferences; -}): { staleAt: string; expiresAt: string | null } { - const last = toDate(input.lastMeaningfulEventAt) ?? new Date(); - const staleAt = addMs(last, input.preferences.staleAfterDays * MS_PER_DAY); - if (input.pinned) return { staleAt, expiresAt: null }; - const retentionMs = input.state === "done" - ? input.preferences.doneRetentionHours * MS_PER_HOUR - : input.preferences.retentionDays * MS_PER_DAY; - return { staleAt, expiresAt: addMs(last, retentionMs) }; -} - -function issueRightTag(issue: Issue, treeIssueIds: Set, relations: PluginIssueSubtree["relations"]): { tag: string; isIntraTreeBlocked?: boolean } { - if (issue.status === "blocked") { - const blockers = relations?.[issue.id]?.blockedBy ?? []; - const isIntraTreeBlocked = blockers.length > 0 && blockers.every((blocker) => treeIssueIds.has(blocker.id)); - return { tag: "blocked", isIntraTreeBlocked }; - } - return { tag: issue.status }; -} - -function sourcePriority(source: BriefCardSourceDraft): number { - if (source.rightTag === "blocked") return source.isIntraTreeBlocked ? 2 : 0; - if (source.rightTag === "pending" || source.rightTag === "asked you" || source.kind === "approval") return 1; - if (source.rightTag === "failed" || source.rightTag === "error") return 2; - if (source.rightTag === "running" || source.rightTag === "in_progress") return 3; - if (source.rightTag === "in_review") return 4; - return 5; -} - -export function buildBriefSources(input: { - companyId: string; - subtree: PluginIssueSubtree; - sources?: BriefSourceCollections; - companyIssuePrefix?: string | null; -}): BriefCardSourceDraft[] { - const sources: BriefCardSourceDraft[] = []; - const issues = issueById(input.subtree); - const treeIssueIds = new Set(input.subtree.issueIds); - const root = issues.get(input.subtree.rootIssueId) ?? input.subtree.issues[0]; - const rootEventAt = toIso(root?.updatedAt ?? new Date()); - - if (root) { - sources.push({ - companyId: input.companyId, - kind: "issue_tree", - sourceId: root.id, - sourceIssueId: root.id, - identifier: root.identifier, - titleLine: truncate(root.title, 60), - rightTag: "tree", - linkPath: issuePath(root, input.companyIssuePrefix), - eventAt: rootEventAt, - metadata: { issueCount: input.subtree.issueIds.length }, - }); - } - - for (const issue of input.subtree.issues) { - const { tag, isIntraTreeBlocked } = issueRightTag(issue, treeIssueIds, input.subtree.relations); - const shouldShow = issue.status === "blocked" || issue.status === "in_review" || issue.status === "in_progress" || issue.activeRecoveryAction || issue.id === input.subtree.rootIssueId; - if (!shouldShow) continue; - sources.push({ - companyId: input.companyId, - kind: "issue", - sourceId: issue.id, - sourceIssueId: issue.id, - identifier: issue.identifier, - titleLine: truncate(issue.title, 60), - rightTag: issue.activeRecoveryAction ? "error" : tag, - linkPath: issuePath(issue, input.companyIssuePrefix), - isIntraTreeBlocked, - eventAt: toIso(issue.updatedAt), - metadata: { priority: issue.priority, assigneeAgentId: issue.assigneeAgentId }, - }); - } - - for (const [issueId, comments] of Object.entries(input.sources?.commentsByIssueId ?? {})) { - const issue = issues.get(issueId); - if (!issue) continue; - for (const comment of comments.slice(-3)) { - sources.push({ - companyId: input.companyId, - kind: "comment", - sourceId: comment.id, - sourceIssueId: issue.id, - identifier: issue.identifier, - titleLine: truncate(comment.body, 60), - rightTag: comment.authorType, - linkPath: `${issuePath(issue, input.companyIssuePrefix)}#comment-${comment.id}`, - eventAt: toIso(comment.createdAt), - }); - } - } - - for (const [issueId, docs] of Object.entries(input.sources?.documentsByIssueId ?? input.subtree.documents ?? {})) { - const issue = issues.get(issueId); - if (!issue) continue; - for (const doc of docs.slice(-3)) { - sources.push({ - companyId: input.companyId, - kind: "document", - sourceId: doc.id, - sourceIssueId: issue.id, - identifier: issue.identifier, - titleLine: truncate(doc.title ?? doc.key, 60), - rightTag: "document", - linkPath: `${issuePath(issue, input.companyIssuePrefix)}#document-${encodeURIComponent(doc.key)}`, - eventAt: toIso(doc.updatedAt), - }); - } - } - - const activeRunsByIssue = input.sources?.activeRunsByIssueId ?? input.subtree.activeRuns ?? {}; - for (const [issueId, runs] of Object.entries(activeRunsByIssue)) { - const issue = issues.get(issueId); - if (!issue) continue; - for (const run of runs) { - sources.push({ - companyId: input.companyId, - kind: "run", - sourceId: run.id, - sourceIssueId: issue.id, - sourceRunId: run.id, - identifier: issue.identifier, - titleLine: truncate(`${issue.title} run`, 60), - rightTag: run.status, - linkPath: runPath(run, input.companyIssuePrefix), - eventAt: toIso(run.startedAt ?? run.createdAt), - }); - } - } - - for (const run of input.sources?.runs ?? []) { - if (!run.issueId || activeRunsByIssue[run.issueId]?.some((activeRun) => activeRun.id === run.id)) continue; - const issue = issues.get(run.issueId); - if (!issue) continue; - sources.push({ - companyId: input.companyId, - kind: "run", - sourceId: run.id, - sourceIssueId: issue.id, - sourceRunId: run.id, - identifier: issue.identifier, - titleLine: truncate(`${issue.title} run`, 60), - rightTag: run.status, - linkPath: runPath(run, input.companyIssuePrefix), - eventAt: toIso(run.finishedAt ?? run.startedAt ?? run.createdAt), - }); - } - - for (const approval of input.sources?.approvals ?? []) { - const issue = issues.get(approval.issueId); - if (!issue) continue; - sources.push({ - companyId: input.companyId, - kind: "approval", - sourceId: approval.id, - sourceIssueId: issue.id, - identifier: issue.identifier, - titleLine: truncate(`${approval.type} approval`, 60), - rightTag: approval.status === "pending" ? "approval" : approval.status, - linkPath: input.companyIssuePrefix ? `/${input.companyIssuePrefix}/approvals/${approval.id}` : `/approvals/${approval.id}`, - eventAt: toIso(approval.decidedAt ?? approval.createdAt), - }); - } - - for (const [issueId, interactions] of Object.entries(input.sources?.interactionsByIssueId ?? {})) { - const issue = issues.get(issueId); - if (!issue) continue; - for (const interaction of interactions) { - sources.push({ - companyId: input.companyId, - kind: "interaction", - sourceId: interaction.id, - sourceIssueId: issue.id, - identifier: issue.identifier, - titleLine: truncate(interaction.title ?? interaction.summary ?? interaction.kind, 60), - rightTag: interaction.status === "pending" ? "asked you" : interaction.status, - linkPath: `${issuePath(issue, input.companyIssuePrefix)}#interaction-${interaction.id}`, - eventAt: toIso(interaction.updatedAt), - }); - } - } - - for (const [issueId, products] of Object.entries(input.sources?.workProductsByIssueId ?? {})) { - const issue = issues.get(issueId); - if (!issue) continue; - for (const product of products) { - sources.push({ - companyId: input.companyId, - kind: "work_product", - sourceId: product.id, - sourceIssueId: issue.id, - identifier: issue.identifier, - titleLine: truncate(product.title, 60), - rightTag: product.status, - linkPath: product.url ?? `${issuePath(issue, input.companyIssuePrefix)}#work-product-${product.id}`, - eventAt: toIso(product.updatedAt), - metadata: { type: product.type }, - }); - } - } - - for (const event of input.sources?.activityEvents ?? []) { - sources.push({ - companyId: input.companyId, - kind: "activity_event", - sourceId: event.id, - sourceIssueId: event.entityType === "issue" ? event.entityId : null, - identifier: null, - titleLine: truncate(event.action, 60), - rightTag: "activity", - linkPath: input.companyIssuePrefix ? `/${input.companyIssuePrefix}/activity?event=${event.id}` : `/activity?event=${event.id}`, - eventAt: toIso(event.createdAt), - metadata: event.details ?? {}, - }); - } - - return sources.sort((a, b) => { - const byPriority = sourcePriority(a) - sourcePriority(b); - if (byPriority !== 0) return byPriority; - return (toDate(b.eventAt)?.getTime() ?? 0) - (toDate(a.eventAt)?.getTime() ?? 0); - }); -} - -function toTaskRow(source: BriefCardSourceDraft): BriefTaskRow | null { - if (source.kind === "issue_tree" || source.kind === "activity_event") return null; - return { - kind: source.kind, - sourceId: source.sourceId, - identifier: source.identifier, - titleLine: source.titleLine, - rightTag: source.rightTag, - linkPath: source.linkPath, - ...(source.isIntraTreeBlocked === undefined ? {} : { isIntraTreeBlocked: source.isIntraTreeBlocked }), - }; -} - -export function buildBriefCardDraft(input: BuildBriefCardInput): BriefCardDraft | null { - assertCompanyScoped(input); - const preferences = { ...DEFAULT_BRIEF_USER_PREFERENCES, ...input.preferences }; - const root = input.subtree.issues.find((issue) => issue.id === input.subtree.rootIssueId) ?? input.subtree.issues[0]; - if (!root) return null; - const relevance = resolveBriefRelevance(input); - if (!input.forceRelevant && !relevance.relevant) return null; - const state = resolveBriefState(input); - const pinned = input.existingCard?.pinned === true; - const hidden = input.existingCard?.hidden === true; - const { staleAt, expiresAt } = lifecycle({ - pinned, - state, - lastMeaningfulEventAt: relevance.lastMeaningfulEventAt, - preferences, - }); - const groupingDescription = `Issue tree ${root.identifier ?? root.id}: ${root.title}`; - const groupingHash = hash(`${input.companyId}:${input.userId}:root:${root.id}`); - const slugBase = input.existingCard?.slug || slugify(root.identifier ? `${root.identifier}-${root.title}` : root.title); - const slug = truncate(slugBase, 64).replace(/…$/, ""); - const sourceDrafts = buildBriefSources(input); - const taskRows = sourceDrafts.map(toTaskRow).filter((row): row is BriefTaskRow => row !== null).slice(0, 3); - const summary = input.summary ?? { status: "pending" as const }; - - return { - id: input.existingCard?.id, - companyId: input.companyId, - userId: input.userId, - slug, - title: truncate(root.title, 70), - groupingDescription, - groupingHash, - rootIssueId: root.id, - state, - summaryStatus: summary.status, - summaryParagraph: summary.paragraph ?? null, - summaryModel: summary.model ?? null, - summaryTokensIn: summary.tokensIn ?? null, - summaryTokensOut: summary.tokensOut ?? null, - summaryFailureReason: summary.failureReason ?? null, - pinned, - hidden, - staleAt, - expiresAt, - lastMeaningfulEventAt: relevance.lastMeaningfulEventAt, - sources: sourceDrafts, - taskRows, - evidenceSourceIds: summary.evidenceSourceIds ?? [], - }; -} - -export function sortBriefCards>(cards: T[]): T[] { - const stateOrder = new Map(BRIEF_CARD_STATES.map((state, index) => [state, index])); - return [...cards].sort((a, b) => { - if (a.pinned !== b.pinned) return a.pinned ? -1 : 1; - const stateDelta = (stateOrder.get(a.state) ?? 99) - (stateOrder.get(b.state) ?? 99); - if (stateDelta !== 0) return stateDelta; - return (toDate(b.lastMeaningfulEventAt)?.getTime() ?? 0) - (toDate(a.lastMeaningfulEventAt)?.getTime() ?? 0); - }); -} - -export function advanceBriefCursor(input: { - cursor: BriefCursorState | null; - events: TEvent[]; - overlapWindowMs?: number; - dedupeLimit?: number; -}): BriefCursorAdvanceResult { - const overlapMs = input.overlapWindowMs ?? MS_PER_HOUR; - const dedupeLimit = input.dedupeLimit ?? 500; - const previousLastAt = toDate(input.cursor?.lastEventAt ?? null); - const windowStart = previousLastAt ? new Date(previousLastAt.getTime() - overlapMs) : null; - const seen = new Set(input.cursor?.dedupeKeys ?? []); - const ordered = [...input.events].sort((a, b) => { - const aTime = toDate(a.eventAt)?.getTime() ?? 0; - const bTime = toDate(b.eventAt)?.getTime() ?? 0; - if (aTime !== bTime) return aTime - bTime; - return a.id.localeCompare(b.id); - }); - const acceptedEvents: TEvent[] = []; - const nextDedupeKeys = [...seen]; - - for (const event of ordered) { - const eventAt = toDate(event.eventAt); - if (!eventAt) continue; - const dedupeKey = event.dedupeKey ?? event.id; - if (windowStart && eventAt.getTime() < windowStart.getTime()) continue; - if (seen.has(dedupeKey)) continue; - seen.add(dedupeKey); - nextDedupeKeys.push(dedupeKey); - acceptedEvents.push(event); - } - - const lastAccepted = acceptedEvents[acceptedEvents.length - 1] ?? ordered[ordered.length - 1] ?? null; - const lastAcceptedAt = lastAccepted ? toDate(lastAccepted.eventAt) : previousLastAt; - return { - acceptedEvents, - nextCursor: { - lastEventAt: lastAcceptedAt ? lastAcceptedAt.toISOString() : input.cursor?.lastEventAt ?? null, - lastEventId: lastAccepted?.id ?? input.cursor?.lastEventId ?? null, - dedupeKeys: nextDedupeKeys.slice(-dedupeLimit), - }, - }; -} - -function table(namespace: string, name: string): string { - return `${namespace}.${name}`; -} - -export async function getBriefPreferences(db: PluginDatabaseClient, input: { - companyId: string; - userId: string; -}): Promise { - const rows = await db.query<{ - discovery_window_days: number; - retention_days: number; - done_retention_hours: number; - stale_after_days: number; - max_unpinned_cards: number; - }>( - `SELECT discovery_window_days, retention_days, done_retention_hours, stale_after_days, max_unpinned_cards - FROM ${table(db.namespace, "briefs_user_preferences")} - WHERE company_id = $1 AND user_id = $2`, - [input.companyId, input.userId], - ); - const row = rows[0]; - return row - ? { - discoveryWindowDays: Number(row.discovery_window_days), - retentionDays: Number(row.retention_days), - doneRetentionHours: Number(row.done_retention_hours), - staleAfterDays: Number(row.stale_after_days), - maxUnpinnedCards: Number(row.max_unpinned_cards), - } - : DEFAULT_BRIEF_USER_PREFERENCES; -} - -export async function listBriefCards(db: PluginDatabaseClient, input: { - companyId: string; - userId: string; - includeHidden?: boolean; - limit?: number; -}): Promise>> { - return db.query( - `SELECT c.*, s.task_rows, s.summary_paragraph, s.summary_failure_reason, s.created_at AS snapshot_created_at - FROM ${table(db.namespace, "briefs_cards")} c - LEFT JOIN ${table(db.namespace, "briefs_card_snapshots")} s ON s.id = c.latest_snapshot_id - WHERE c.company_id = $1 AND c.user_id = $2 AND ($3::boolean = true OR c.hidden = false) - ORDER BY c.pinned DESC, c.last_meaningful_event_at DESC, c.id ASC - LIMIT $4`, - [input.companyId, input.userId, input.includeHidden === true, input.limit ?? 100], - ); -} - -export async function findExistingBriefCard(db: PluginDatabaseClient, input: { - companyId: string; - userId: string; - groupingHash: string; -}): Promise { - const rows = await db.query<{ - id: string; - slug: string; - pinned: boolean; - hidden: boolean; - }>( - `SELECT id, slug, pinned, hidden - FROM ${table(db.namespace, "briefs_cards")} - WHERE company_id = $1 AND user_id = $2 AND grouping_hash = $3 - LIMIT 1`, - [input.companyId, input.userId, input.groupingHash], - ); - return rows[0] ?? null; -} - -export async function upsertBriefCard(db: PluginDatabaseClient, draft: BriefCardDraft): Promise<{ cardId: string; snapshotId: string }> { - const cardId = draft.id ?? randomUUID(); - const snapshotId = randomUUID(); - - await db.execute( - `INSERT INTO ${table(db.namespace, "briefs_cards")} AS cards - (id, company_id, user_id, slug, title, grouping_description, grouping_hash, root_issue_id, state, summary_status, - pinned, hidden, stale_at, expires_at, last_meaningful_event_at, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13::timestamptz, $14::timestamptz, $15::timestamptz, now(), now()) - ON CONFLICT (company_id, user_id, grouping_hash) DO UPDATE SET - title = EXCLUDED.title, - grouping_description = EXCLUDED.grouping_description, - root_issue_id = EXCLUDED.root_issue_id, - state = EXCLUDED.state, - summary_status = EXCLUDED.summary_status, - stale_at = EXCLUDED.stale_at, - expires_at = CASE WHEN cards.pinned THEN NULL ELSE EXCLUDED.expires_at END, - last_meaningful_event_at = EXCLUDED.last_meaningful_event_at, - updated_at = now()`, - [ - cardId, - draft.companyId, - draft.userId, - draft.slug, - draft.title, - draft.groupingDescription, - draft.groupingHash, - draft.rootIssueId, - draft.state, - draft.summaryStatus, - draft.pinned, - draft.hidden, - draft.staleAt, - draft.expiresAt, - draft.lastMeaningfulEventAt, - ], - ); - - const existing = await findExistingBriefCard(db, { - companyId: draft.companyId, - userId: draft.userId, - groupingHash: draft.groupingHash, - }); - const resolvedCardId = existing?.id ?? cardId; - - await db.execute( - `INSERT INTO ${table(db.namespace, "briefs_card_snapshots")} - (id, company_id, card_id, summary_paragraph, summary_status, summary_model, summary_tokens_in, - summary_tokens_out, summary_failure_reason, task_rows, evidence_source_ids, generated_by_agent_id, generated_by_run_id, created_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb, $11::jsonb, NULL, NULL, now())`, - [ - snapshotId, - draft.companyId, - resolvedCardId, - draft.summaryParagraph, - draft.summaryStatus, - draft.summaryModel ?? null, - draft.summaryTokensIn ?? null, - draft.summaryTokensOut ?? null, - draft.summaryFailureReason ?? null, - JSON.stringify(draft.taskRows), - JSON.stringify(draft.evidenceSourceIds), - ], - ); - - await db.execute( - `DELETE FROM ${table(db.namespace, "briefs_card_sources")} WHERE card_id = $1`, - [resolvedCardId], - ); - - for (const source of draft.sources) { - await db.execute( - `INSERT INTO ${table(db.namespace, "briefs_card_sources")} - (id, company_id, card_id, source_kind, source_id, source_issue_id, source_run_id, identifier, title_line, - right_tag, link_path, is_intra_tree_blocked, event_at, metadata, created_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13::timestamptz, $14::jsonb, now())`, - [ - randomUUID(), - draft.companyId, - resolvedCardId, - source.kind, - source.sourceId, - source.sourceIssueId, - source.sourceRunId ?? null, - source.identifier, - source.titleLine, - source.rightTag, - source.linkPath, - source.isIntraTreeBlocked ?? null, - source.eventAt, - JSON.stringify(source.metadata ?? {}), - ], - ); - } - - await db.execute( - `UPDATE ${table(db.namespace, "briefs_cards")} - SET latest_snapshot_id = $1, updated_at = now() - WHERE id = $2 AND company_id = $3 AND user_id = $4`, - [snapshotId, resolvedCardId, draft.companyId, draft.userId], - ); - - return { cardId: resolvedCardId, snapshotId }; -} - -export async function setBriefCardPinned(db: PluginDatabaseClient, input: { - companyId: string; - userId: string; - cardId: string; - pinned: boolean; -}): Promise { - await db.execute( - `UPDATE ${table(db.namespace, "briefs_cards")} - SET pinned = $4, expires_at = CASE WHEN $4 THEN NULL ELSE expires_at END, updated_at = now() - WHERE company_id = $1 AND user_id = $2 AND id = $3`, - [input.companyId, input.userId, input.cardId, input.pinned], - ); -} - -export async function setBriefCardHidden(db: PluginDatabaseClient, input: { - companyId: string; - userId: string; - cardId: string; - hidden: boolean; -}): Promise { - await db.execute( - `UPDATE ${table(db.namespace, "briefs_cards")} - SET hidden = $4, updated_at = now() - WHERE company_id = $1 AND user_id = $2 AND id = $3`, - [input.companyId, input.userId, input.cardId, input.hidden], - ); -} diff --git a/packages/plugins/plugin-briefs/src/index.ts b/packages/plugins/plugin-briefs/src/index.ts deleted file mode 100644 index f8526b71966..00000000000 --- a/packages/plugins/plugin-briefs/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as manifest } from "./manifest.js"; -export * from "./briefs.js"; diff --git a/packages/plugins/plugin-briefs/src/manifest.ts b/packages/plugins/plugin-briefs/src/manifest.ts deleted file mode 100644 index fee06fb65a2..00000000000 --- a/packages/plugins/plugin-briefs/src/manifest.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { PaperclipPluginManifestV1 } from "@paperclipai/plugin-sdk"; - -export const PLUGIN_ID = "paperclipai.briefs"; -export const BRIEFING_ANALYST_AGENT_KEY = "briefing-analyst"; -export const BRIEFING_PROJECT_KEY = "briefing-operations"; -export const BRIEFS_DISCOVER_ROUTINE_KEY = "briefs-discover-cards"; -export const BRIEFS_UPDATE_ROUTINE_KEY = "briefs-update-cards"; - -const manifest: PaperclipPluginManifestV1 = { - id: PLUGIN_ID, - apiVersion: 1, - version: "0.1.0", - displayName: "Briefs", - description: "Maintains source-linked Briefing cards for user-relevant Paperclip work.", - author: "Paperclip", - categories: ["automation", "ui"], - capabilities: [ - "api.routes.register", - "database.namespace.migrate", - "database.namespace.read", - "database.namespace.write", - "companies.read", - "issues.read", - "issue.subtree.read", - "issue.relations.read", - "issue.comments.read", - "issue.documents.read", - "issues.orchestration.read", - "agents.read", - "activity.log.write", - "plugin.state.read", - "plugin.state.write", - ], - entrypoints: { - worker: "./dist/worker.js", - }, - database: { - namespaceSlug: "briefs", - migrationsDir: "migrations", - coreReadTables: [ - "companies", - "issues", - "issue_relations", - "issue_comments", - "issue_documents", - "heartbeat_runs", - "approvals", - "issue_approvals", - ], - }, - apiRoutes: [ - { - routeKey: "cards", - method: "GET", - path: "/cards", - auth: "board-or-agent", - capability: "api.routes.register", - companyResolution: { from: "query", key: "companyId" }, - }, - { - routeKey: "refresh-tree", - method: "POST", - path: "/trees/:rootIssueId/refresh", - auth: "board-or-agent", - capability: "api.routes.register", - companyResolution: { from: "issue", param: "rootIssueId" }, - }, - ], -}; - -export default manifest; diff --git a/packages/plugins/plugin-briefs/src/worker.ts b/packages/plugins/plugin-briefs/src/worker.ts deleted file mode 100644 index fbf3e29ff99..00000000000 --- a/packages/plugins/plugin-briefs/src/worker.ts +++ /dev/null @@ -1,291 +0,0 @@ -import { - definePlugin, - runWorker, - type PluginApiRequestInput, - type PluginContext, - type PluginIssueSubtree, -} from "@paperclipai/plugin-sdk"; -import { - buildBriefCardDraft, - findExistingBriefCard, - getBriefPreferences, - listBriefCards, - setBriefCardHidden, - setBriefCardPinned, - upsertBriefCard, - type BriefSourceCollections, -} from "./briefs.js"; - -function stringField(value: unknown): string | null { - if (typeof value === "string" && value.trim().length > 0) return value.trim(); - if (Array.isArray(value)) return stringField(value[0]); - return null; -} - -function boolField(value: unknown): boolean { - return value === true || value === "true"; -} - -function objectBody(value: unknown): Record { - return value && typeof value === "object" && !Array.isArray(value) ? value as Record : {}; -} - -function readCompanyId(params: Record): string { - const companyId = stringField(params.companyId); - if (!companyId) throw new Error("companyId is required"); - return companyId; -} - -function readUserId(params: Record, fallback?: string | null): string { - const userId = stringField(params.userId) ?? fallback; - if (!userId) throw new Error("userId is required"); - return userId; -} - -async function collectTreeSources(ctx: PluginContext, companyId: string, subtree: PluginIssueSubtree): Promise { - const [commentPairs, orchestration] = await Promise.all([ - Promise.all( - subtree.issueIds.map(async (issueId) => [ - issueId, - await ctx.issues.listComments(issueId, companyId), - ] as const), - ), - ctx.issues.summaries.getOrchestration({ - issueId: subtree.rootIssueId, - companyId, - includeSubtree: true, - }), - ]); - - return { - commentsByIssueId: Object.fromEntries(commentPairs), - documentsByIssueId: subtree.documents, - activeRunsByIssueId: subtree.activeRuns, - runs: orchestration.runs, - approvals: orchestration.approvals, - }; -} - -async function refreshTreeCard(ctx: PluginContext, input: { - companyId: string; - userId: string; - rootIssueId: string; - companyIssuePrefix?: string | null; - forceRelevant?: boolean; -}) { - const preferences = await getBriefPreferences(ctx.db, input); - const subtree = await ctx.issues.getSubtree(input.rootIssueId, input.companyId, { - includeRoot: true, - includeRelations: true, - includeDocuments: true, - includeActiveRuns: true, - includeAssignees: true, - }); - const sources = await collectTreeSources(ctx, input.companyId, subtree); - const initialDraft = buildBriefCardDraft({ - ...input, - subtree, - sources, - preferences, - }); - if (!initialDraft) { - return { refreshed: false, reason: "not-relevant-or-stale", card: null }; - } - const existingCard = await findExistingBriefCard(ctx.db, { - companyId: input.companyId, - userId: input.userId, - groupingHash: initialDraft.groupingHash, - }); - const draft = existingCard - ? buildBriefCardDraft({ - ...input, - subtree, - sources, - preferences, - existingCard, - }) ?? initialDraft - : initialDraft; - const result = await upsertBriefCard(ctx.db, draft); - await ctx.activity.log({ - companyId: input.companyId, - message: "briefs.card_refreshed", - entityType: "brief", - entityId: result.cardId, - metadata: { - rootIssueId: input.rootIssueId, - userId: input.userId, - state: draft.state, - summaryStatus: draft.summaryStatus, - sourceCount: draft.sources.length, - }, - }); - return { refreshed: true, cardId: result.cardId, snapshotId: result.snapshotId, card: draft }; -} - -async function discoverCandidateRootIds(ctx: PluginContext, companyId: string, sinceIso: string, limit: number): Promise { - const rows = await ctx.db.query<{ root_id: string }>( - `WITH RECURSIVE recent AS ( - SELECT id, parent_id - FROM public.issues - WHERE company_id = $1 AND hidden_at IS NULL AND updated_at >= $2::timestamptz - ORDER BY updated_at DESC - LIMIT $3 - ), - chain AS ( - SELECT id, parent_id, id AS leaf_id - FROM recent - UNION ALL - SELECT parent.id, parent.parent_id, chain.leaf_id - FROM public.issues parent - INNER JOIN chain ON chain.parent_id = parent.id - WHERE parent.company_id = $1 AND parent.hidden_at IS NULL - ) - SELECT DISTINCT id AS root_id - FROM chain - WHERE parent_id IS NULL - ORDER BY id`, - [companyId, sinceIso, limit], - ); - return rows.map((row) => row.root_id); -} - -let currentContext: PluginContext | null = null; - -const plugin = definePlugin({ - async setup(ctx) { - currentContext = ctx; - ctx.logger.info("Briefs plugin setup"); - - ctx.data.register("cards", async (params) => { - const companyId = readCompanyId(params); - const userId = readUserId(params); - return { - cards: await listBriefCards(ctx.db, { - companyId, - userId, - includeHidden: boolField(params.includeHidden), - limit: Number(params.limit ?? 100), - }), - }; - }); - - ctx.actions.register("refresh-tree", async (params) => { - const companyId = readCompanyId(params); - const userId = readUserId(params); - const rootIssueId = stringField(params.rootIssueId); - if (!rootIssueId) throw new Error("rootIssueId is required"); - return refreshTreeCard(ctx, { - companyId, - userId, - rootIssueId, - companyIssuePrefix: stringField(params.companyIssuePrefix), - forceRelevant: boolField(params.forceRelevant), - }); - }); - - ctx.actions.register("discover-cards", async (params) => { - const companyId = readCompanyId(params); - const userId = readUserId(params); - const preferences = await getBriefPreferences(ctx.db, { companyId, userId }); - const since = new Date(Date.now() - preferences.discoveryWindowDays * 24 * 60 * 60 * 1000).toISOString(); - const rootIssueIds = await discoverCandidateRootIds(ctx, companyId, since, Number(params.limit ?? 100)); - const results = []; - for (const rootIssueId of rootIssueIds.slice(0, preferences.maxUnpinnedCards)) { - results.push(await refreshTreeCard(ctx, { - companyId, - userId, - rootIssueId, - companyIssuePrefix: stringField(params.companyIssuePrefix), - })); - } - return { rootIssueIds, results }; - }); - - ctx.actions.register("pin-card", async (params) => { - const companyId = readCompanyId(params); - const userId = readUserId(params); - const cardId = stringField(params.cardId); - if (!cardId) throw new Error("cardId is required"); - const pinned = boolField(params.pinned); - await setBriefCardPinned(ctx.db, { companyId, userId, cardId, pinned }); - await ctx.activity.log({ - companyId, - message: pinned ? "briefs.card_pinned" : "briefs.card_unpinned", - entityType: "brief", - entityId: cardId, - metadata: { userId }, - }); - return { cardId, pinned }; - }); - - ctx.actions.register("hide-card", async (params) => { - const companyId = readCompanyId(params); - const userId = readUserId(params); - const cardId = stringField(params.cardId); - if (!cardId) throw new Error("cardId is required"); - const hidden = boolField(params.hidden); - await setBriefCardHidden(ctx.db, { companyId, userId, cardId, hidden }); - await ctx.activity.log({ - companyId, - message: hidden ? "briefs.card_hidden" : "briefs.card_unhidden", - entityType: "brief", - entityId: cardId, - metadata: { userId }, - }); - return { cardId, hidden }; - }); - }, - - async onApiRequest(input: PluginApiRequestInput) { - if (input.routeKey === "cards") { - const userId = readUserId(input.query as Record, input.actor.userId ?? input.actor.agentId ?? null); - return { - body: { - cards: await listBriefCards(activeContext(input).db, { - companyId: input.companyId, - userId, - includeHidden: boolField(input.query.includeHidden), - limit: Number(input.query.limit ?? 100), - }), - }, - }; - } - - if (input.routeKey === "refresh-tree") { - const body = objectBody(input.body); - const rootIssueId = stringField(input.params.rootIssueId); - if (!rootIssueId) throw new Error("rootIssueId is required"); - const userId = readUserId(body, input.actor.userId ?? input.actor.agentId ?? null); - return { - status: 202, - body: await refreshTreeCard(activeContext(input), { - companyId: input.companyId, - userId, - rootIssueId, - companyIssuePrefix: stringField(body.companyIssuePrefix), - forceRelevant: boolField(body.forceRelevant), - }), - }; - } - - return { status: 404, body: { error: `Unknown Briefs route: ${input.routeKey}` } }; - }, - - async onHealth() { - return { - status: "ok", - message: "Briefs worker is running", - details: { - surfaces: ["database", "deterministic-cards", "scoped-api-route"], - }, - }; - }, -}); - -function activeContext(_input?: PluginApiRequestInput): PluginContext { - if (!currentContext) throw new Error("Briefs plugin has not been set up"); - return currentContext; -} - -export default plugin; -runWorker(plugin, import.meta.url); diff --git a/packages/plugins/plugin-briefs/tsconfig.json b/packages/plugins/plugin-briefs/tsconfig.json deleted file mode 100644 index 2ea2eede8d2..00000000000 --- a/packages/plugins/plugin-briefs/tsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "lib": ["ES2022", "DOM"], - "types": ["node"], - "strict": true, - "skipLibCheck": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "outDir": "dist", - "rootDir": "src" - }, - "include": ["src"], - "exclude": ["dist", "node_modules"] -} diff --git a/packages/plugins/plugin-briefs/vitest.config.ts b/packages/plugins/plugin-briefs/vitest.config.ts deleted file mode 100644 index 649a293ef95..00000000000 --- a/packages/plugins/plugin-briefs/vitest.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - include: ["tests/**/*.spec.ts"], - environment: "node", - }, -});