Remove Briefs plugin from PR

This commit is contained in:
Dotta committed 2026-05-22 12:53:20 +00:00
1 parent 68a07334d3
commit 40cd83b53c
9 files changed
-1574

No files matched your search

@@ -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);
@@ -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"
}
}
File diff suppressed because it is too large. Load diff
@@ -1,2 +0,0 @@
export { default as manifest } from "./manifest.js";
export * from "./briefs.js";
@@ -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;
@@ -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<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
}
function readCompanyId(params: Record<string, unknown>): string {
const companyId = stringField(params.companyId);
if (!companyId) throw new Error("companyId is required");
return companyId;
}
function readUserId(params: Record<string, unknown>, 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<BriefSourceCollections> {
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<string[]> {
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<string, unknown>, 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);
@@ -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"]
}
@@ -1,8 +0,0 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
include: ["tests/**/*.spec.ts"],
environment: "node",
},
});