From 97d628d7847d78557605951f32141ac8ef3e6868 Mon Sep 17 00:00:00 2001 From: teknium1 Date: Tue, 10 Mar 2026 23:12:13 -0700 Subject: [PATCH 01/18] feat: add Hermes Agent adapter (hermes_local) Adds support for Hermes Agent (https://github.com/NousResearch/hermes-agent) as a managed employee in Paperclip companies. Hermes Agent is a full-featured AI agent by Nous Research with 30+ native tools, persistent memory, session persistence, 80+ skills, MCP support, and multi-provider model access. Changes: - Add 'hermes_local' to AGENT_ADAPTER_TYPES (packages/shared) - Add @nousresearch/paperclip-adapter-hermes dependency (server) - Register hermesLocalAdapter in the adapter registry (server) The adapter package is maintained at: https://github.com/NousResearch/hermes-paperclip-adapter --- packages/shared/src/constants.ts | 1 + server/package.json | 1 + server/src/adapters/registry.ts | 18 ++++++++++++++++++ 3 files changed, 20 insertions(+) diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index ba75dc8e..c78d1dd0 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -30,6 +30,7 @@ export const AGENT_ADAPTER_TYPES = [ "pi_local", "cursor", "openclaw_gateway", + "hermes_local", ] as const; export type AgentAdapterType = (typeof AGENT_ADAPTER_TYPES)[number]; diff --git a/server/package.json b/server/package.json index aeb09944..63585fae 100644 --- a/server/package.json +++ b/server/package.json @@ -40,6 +40,7 @@ "@paperclipai/adapter-opencode-local": "workspace:*", "@paperclipai/adapter-pi-local": "workspace:*", "@paperclipai/adapter-openclaw-gateway": "workspace:*", + "@nousresearch/paperclip-adapter-hermes": "github:NousResearch/hermes-paperclip-adapter", "@paperclipai/adapter-utils": "workspace:*", "@paperclipai/db": "workspace:*", "@paperclipai/shared": "workspace:*", diff --git a/server/src/adapters/registry.ts b/server/src/adapters/registry.ts index 9fe536a0..571d8131 100644 --- a/server/src/adapters/registry.ts +++ b/server/src/adapters/registry.ts @@ -45,6 +45,14 @@ import { import { agentConfigurationDoc as piAgentConfigurationDoc, } from "@paperclipai/adapter-pi-local"; +import { + execute as hermesExecute, + testEnvironment as hermesTestEnvironment, +} from "@nousresearch/paperclip-adapter-hermes/server"; +import { + agentConfigurationDoc as hermesAgentConfigurationDoc, + models as hermesModels, +} from "@nousresearch/paperclip-adapter-hermes"; import { processAdapter } from "./process/index.js"; import { httpAdapter } from "./http/index.js"; @@ -111,6 +119,15 @@ const piLocalAdapter: ServerAdapterModule = { agentConfigurationDoc: piAgentConfigurationDoc, }; +const hermesLocalAdapter: ServerAdapterModule = { + type: "hermes_local", + execute: hermesExecute, + testEnvironment: hermesTestEnvironment, + models: hermesModels, + supportsLocalAgentJwt: false, + agentConfigurationDoc: hermesAgentConfigurationDoc, +}; + const adaptersByType = new Map( [ claudeLocalAdapter, @@ -119,6 +136,7 @@ const adaptersByType = new Map( piLocalAdapter, cursorLocalAdapter, openclawGatewayAdapter, + hermesLocalAdapter, processAdapter, httpAdapter, ].map((a) => [a.type, a]), From 4e354ad00d7a1592e5e8d800dababc0bc3db73af Mon Sep 17 00:00:00 2001 From: teknium1 Date: Thu, 12 Mar 2026 17:03:49 -0700 Subject: [PATCH 02/18] =?UTF-8?q?fix:=20address=20review=20feedback=20?= =?UTF-8?q?=E2=80=94=20pin=20dependency=20and=20add=20sessionCodec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Pin @nousresearch/paperclip-adapter-hermes to v0.1.0 tag for reproducible builds and supply-chain safety - Import and wire hermesSessionCodec into the adapter registration for structured session parameter validation (matching claude_local, codex_local, and other adapters that support session persistence) --- server/package.json | 2 +- server/src/adapters/registry.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/server/package.json b/server/package.json index 63585fae..e3a9b821 100644 --- a/server/package.json +++ b/server/package.json @@ -40,7 +40,7 @@ "@paperclipai/adapter-opencode-local": "workspace:*", "@paperclipai/adapter-pi-local": "workspace:*", "@paperclipai/adapter-openclaw-gateway": "workspace:*", - "@nousresearch/paperclip-adapter-hermes": "github:NousResearch/hermes-paperclip-adapter", + "@nousresearch/paperclip-adapter-hermes": "github:NousResearch/hermes-paperclip-adapter#v0.1.0", "@paperclipai/adapter-utils": "workspace:*", "@paperclipai/db": "workspace:*", "@paperclipai/shared": "workspace:*", diff --git a/server/src/adapters/registry.ts b/server/src/adapters/registry.ts index 571d8131..f112f788 100644 --- a/server/src/adapters/registry.ts +++ b/server/src/adapters/registry.ts @@ -48,6 +48,7 @@ import { import { execute as hermesExecute, testEnvironment as hermesTestEnvironment, + sessionCodec as hermesSessionCodec, } from "@nousresearch/paperclip-adapter-hermes/server"; import { agentConfigurationDoc as hermesAgentConfigurationDoc, @@ -123,6 +124,7 @@ const hermesLocalAdapter: ServerAdapterModule = { type: "hermes_local", execute: hermesExecute, testEnvironment: hermesTestEnvironment, + sessionCodec: hermesSessionCodec, models: hermesModels, supportsLocalAgentJwt: false, agentConfigurationDoc: hermesAgentConfigurationDoc, From e84c0e8df2f76f3d843d7bd4824bfbc195ae1142 Mon Sep 17 00:00:00 2001 From: teknium1 Date: Thu, 12 Mar 2026 17:23:24 -0700 Subject: [PATCH 03/18] fix: use npm package instead of GitHub URL dependency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Published hermes-paperclip-adapter@0.1.0 to npm registry - Replaced github:NousResearch/hermes-paperclip-adapter with hermes-paperclip-adapter ^0.1.0 (proper semver, reproducible builds) - Updated imports from @nousresearch/paperclip-adapter-hermes to hermes-paperclip-adapter - Wired in hermesSessionCodec for structured session validation Addresses both review items from greptile-apps: 1. Unpinned GitHub dependency → now a proper npm package with semver 2. Missing sessionCodec → now imported and registered --- server/package.json | 2 +- server/src/adapters/registry.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/package.json b/server/package.json index e3a9b821..dfdb46fb 100644 --- a/server/package.json +++ b/server/package.json @@ -40,7 +40,7 @@ "@paperclipai/adapter-opencode-local": "workspace:*", "@paperclipai/adapter-pi-local": "workspace:*", "@paperclipai/adapter-openclaw-gateway": "workspace:*", - "@nousresearch/paperclip-adapter-hermes": "github:NousResearch/hermes-paperclip-adapter#v0.1.0", + "hermes-paperclip-adapter": "^0.1.0", "@paperclipai/adapter-utils": "workspace:*", "@paperclipai/db": "workspace:*", "@paperclipai/shared": "workspace:*", diff --git a/server/src/adapters/registry.ts b/server/src/adapters/registry.ts index f112f788..35d407d3 100644 --- a/server/src/adapters/registry.ts +++ b/server/src/adapters/registry.ts @@ -49,11 +49,11 @@ import { execute as hermesExecute, testEnvironment as hermesTestEnvironment, sessionCodec as hermesSessionCodec, -} from "@nousresearch/paperclip-adapter-hermes/server"; +} from "hermes-paperclip-adapter/server"; import { agentConfigurationDoc as hermesAgentConfigurationDoc, models as hermesModels, -} from "@nousresearch/paperclip-adapter-hermes"; +} from "hermes-paperclip-adapter"; import { processAdapter } from "./process/index.js"; import { httpAdapter } from "./http/index.js"; From 93faf6d361a1f92c174953a9a0297de8830d2dcf Mon Sep 17 00:00:00 2001 From: teknium1 Date: Fri, 13 Mar 2026 20:26:27 -0700 Subject: [PATCH 04/18] =?UTF-8?q?fix:=20address=20review=20feedback=20?= =?UTF-8?q?=E2=80=94=20pin=20version,=20enable=20JWT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Pin hermes-paperclip-adapter to exact version 0.1.1 (was ^0.1.0). Avoids auto-pulling potentially breaking patches from a 0.x package. - Enable supportsLocalAgentJwt (was false). The adapter uses buildPaperclipEnv which passes the JWT to the child process, matching the pattern of all other local adapters. --- server/package.json | 2 +- server/src/adapters/registry.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/package.json b/server/package.json index dfdb46fb..745abfd8 100644 --- a/server/package.json +++ b/server/package.json @@ -40,7 +40,7 @@ "@paperclipai/adapter-opencode-local": "workspace:*", "@paperclipai/adapter-pi-local": "workspace:*", "@paperclipai/adapter-openclaw-gateway": "workspace:*", - "hermes-paperclip-adapter": "^0.1.0", + "hermes-paperclip-adapter": "0.1.1", "@paperclipai/adapter-utils": "workspace:*", "@paperclipai/db": "workspace:*", "@paperclipai/shared": "workspace:*", diff --git a/server/src/adapters/registry.ts b/server/src/adapters/registry.ts index 35d407d3..f2ca65ed 100644 --- a/server/src/adapters/registry.ts +++ b/server/src/adapters/registry.ts @@ -126,7 +126,7 @@ const hermesLocalAdapter: ServerAdapterModule = { testEnvironment: hermesTestEnvironment, sessionCodec: hermesSessionCodec, models: hermesModels, - supportsLocalAgentJwt: false, + supportsLocalAgentJwt: true, agentConfigurationDoc: hermesAgentConfigurationDoc, }; From 61fd5486e88bdb66d899a4b57e8b60b17c7dcead Mon Sep 17 00:00:00 2001 From: HD Date: Mon, 16 Mar 2026 02:10:10 +0700 Subject: [PATCH 05/18] fix: wire plugin event subscriptions from worker to host MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plugin workers register event handlers via `ctx.events.on()` in the SDK, but these subscriptions were never forwarded to the host process. The host sends events via `notifyWorker("onEvent", ...)` which produces a JSON-RPC notification (no `id`), but the worker only dispatched `onEvent` as a request handler — notifications were silently dropped. Changes: - Add `events.subscribe` RPC method so workers can register subscriptions on the host-side event bus during setup - Handle `onEvent` notifications in the worker notification dispatcher (previously only `agents.sessions.event` was handled) - Add `events.subscribe` to HostServices interface, capability map, and host client handler - Add `subscribe` handler in host services that registers on the scoped plugin event bus and forwards matched events to the worker --- packages/plugins/sdk/src/host-client-factory.ts | 7 ++++++- packages/plugins/sdk/src/protocol.ts | 4 ++++ packages/plugins/sdk/src/worker-rpc-host.ts | 15 +++++++++++++++ server/src/services/plugin-host-services.ts | 11 +++++++++++ 4 files changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/plugins/sdk/src/host-client-factory.ts b/packages/plugins/sdk/src/host-client-factory.ts index f7829ad7..e445cc0b 100644 --- a/packages/plugins/sdk/src/host-client-factory.ts +++ b/packages/plugins/sdk/src/host-client-factory.ts @@ -103,9 +103,10 @@ export interface HostServices { list(params: WorkerToHostMethods["entities.list"][0]): Promise; }; - /** Provides `events.emit`. */ + /** Provides `events.emit` and `events.subscribe`. */ events: { emit(params: WorkerToHostMethods["events.emit"][0]): Promise; + subscribe(params: WorkerToHostMethods["events.subscribe"][0]): Promise; }; /** Provides `http.fetch`. */ @@ -261,6 +262,7 @@ const METHOD_CAPABILITY_MAP: Record { return services.events.emit(params); }), + "events.subscribe": gated("events.subscribe", async (params) => { + return services.events.subscribe(params); + }), // HTTP "http.fetch": gated("http.fetch", async (params) => { diff --git a/packages/plugins/sdk/src/protocol.ts b/packages/plugins/sdk/src/protocol.ts index 8330f680..61228b53 100644 --- a/packages/plugins/sdk/src/protocol.ts +++ b/packages/plugins/sdk/src/protocol.ts @@ -482,6 +482,10 @@ export interface WorkerToHostMethods { params: { name: string; companyId: string; payload: unknown }, result: void, ]; + "events.subscribe": [ + params: { eventPattern: string; filter?: Record | null }, + result: void, + ]; // HTTP "http.fetch": [ diff --git a/packages/plugins/sdk/src/worker-rpc-host.ts b/packages/plugins/sdk/src/worker-rpc-host.ts index 8242c261..2dbb8196 100644 --- a/packages/plugins/sdk/src/worker-rpc-host.ts +++ b/packages/plugins/sdk/src/worker-rpc-host.ts @@ -387,6 +387,13 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost registration = { name, filter: filterOrFn, fn: maybeFn }; } eventHandlers.push(registration); + // Register subscription on the host so events are forwarded to this worker + void callHost("events.subscribe", { eventPattern: name, filter: registration.filter ?? null }).catch((err) => { + notifyHost("log", { + level: "warn", + message: `Failed to subscribe to event "${name}" on host: ${err instanceof Error ? err.message : String(err)}`, + }); + }); return () => { const idx = eventHandlers.indexOf(registration); if (idx !== -1) eventHandlers.splice(idx, 1); @@ -1107,6 +1114,14 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost const event = notif.params as AgentSessionEvent; const cb = sessionEventCallbacks.get(event.sessionId); if (cb) cb(event); + } else if (notif.method === "onEvent" && notif.params) { + // Plugin event bus notifications — dispatch to registered event handlers + handleOnEvent(notif.params as OnEventParams).catch((err) => { + notifyHost("log", { + level: "error", + message: `Failed to handle event notification: ${err instanceof Error ? err.message : String(err)}`, + }); + }); } } } diff --git a/server/src/services/plugin-host-services.ts b/server/src/services/plugin-host-services.ts index 6be022e2..fd945413 100644 --- a/server/src/services/plugin-host-services.ts +++ b/server/src/services/plugin-host-services.ts @@ -556,6 +556,17 @@ export function buildHostServices( } await scopedBus.emit(params.name, params.companyId, params.payload); }, + async subscribe(params: { eventPattern: string; filter?: Record }) { + scopedBus.subscribe( + params.eventPattern as any, + params.filter as any ?? {}, + async (event) => { + if (notifyWorker) { + notifyWorker("onEvent", { event }); + } + }, + ); + }, }, http: { From 8468d347be46cb2a9e033055e039564467dfa780 Mon Sep 17 00:00:00 2001 From: HD Date: Mon, 16 Mar 2026 02:25:03 +0700 Subject: [PATCH 06/18] =?UTF-8?q?fix:=20address=20review=20feedback=20?= =?UTF-8?q?=E2=80=94=20subscription=20cleanup,=20filter=20nullability,=20s?= =?UTF-8?q?tale=20diagram?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add scopedBus.clear() in dispose() to prevent subscription accumulation on worker crash/restart cycles - Use two-arg subscribe() overload when filter is null instead of passing empty object; fix filter type to include null - Update ASCII flow diagram: onEvent is a notification, not request/response --- packages/plugins/sdk/src/worker-rpc-host.ts | 3 +-- server/src/services/plugin-host-services.ts | 25 ++++++++++++--------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/packages/plugins/sdk/src/worker-rpc-host.ts b/packages/plugins/sdk/src/worker-rpc-host.ts index 2dbb8196..1e8d5591 100644 --- a/packages/plugins/sdk/src/worker-rpc-host.ts +++ b/packages/plugins/sdk/src/worker-rpc-host.ts @@ -19,8 +19,7 @@ * |--- request(initialize) -------------> | → calls plugin.setup(ctx) * |<-- response(ok:true) ---------------- | * | | - * |--- request(onEvent) ----------------> | → dispatches to registered handler - * |<-- response(void) ------------------ | + * |--- notification(onEvent) -----------> | → dispatches to registered handler * | | * |<-- request(state.get) --------------- | ← SDK client call from plugin code * |--- response(result) ----------------> | diff --git a/server/src/services/plugin-host-services.ts b/server/src/services/plugin-host-services.ts index fd945413..2a3f5ecc 100644 --- a/server/src/services/plugin-host-services.ts +++ b/server/src/services/plugin-host-services.ts @@ -556,16 +556,17 @@ export function buildHostServices( } await scopedBus.emit(params.name, params.companyId, params.payload); }, - async subscribe(params: { eventPattern: string; filter?: Record }) { - scopedBus.subscribe( - params.eventPattern as any, - params.filter as any ?? {}, - async (event) => { - if (notifyWorker) { - notifyWorker("onEvent", { event }); - } - }, - ); + async subscribe(params: { eventPattern: string; filter?: Record | null }) { + const handler = async (event: import("@paperclipai/plugin-sdk").PluginEvent) => { + if (notifyWorker) { + notifyWorker("onEvent", { event }); + } + }; + if (params.filter) { + scopedBus.subscribe(params.eventPattern as any, params.filter as any, handler); + } else { + scopedBus.subscribe(params.eventPattern as any, handler); + } }, }, @@ -1071,6 +1072,10 @@ export function buildHostServices( dispose() { disposed = true; + // Clear event bus subscriptions to prevent accumulation on worker restart. + // Without this, each crash/restart cycle adds duplicate subscriptions. + scopedBus.clear(); + // Snapshot to avoid iterator invalidation from concurrent sendMessage() calls const snapshot = Array.from(activeSubscriptions); activeSubscriptions.clear(); From aea133ff9fe23d5b06a4d76ba0cfef8893f56927 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sat, 14 Mar 2026 17:47:53 -0500 Subject: [PATCH 07/18] Add archive project button and filter archived projects from selectors - Add "Archive project" / "Unarchive project" button in the project configuration danger zone (ProjectProperties) - Filter archived projects from the Projects listing page - Filter archived projects from NewIssueDialog project selector - Filter archived projects from IssueProperties project picker (keeps current project visible even if archived) - Filter archived projects from CommandPalette - SidebarProjects already filters archived projects Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- ui/src/components/CommandPalette.tsx | 6 +++- ui/src/components/IssueProperties.tsx | 6 +++- ui/src/components/NewIssueDialog.tsx | 6 +++- ui/src/components/ProjectProperties.tsx | 45 +++++++++++++++++++++++-- ui/src/pages/ProjectDetail.tsx | 17 ++++++++++ ui/src/pages/Projects.tsx | 12 ++++--- 6 files changed, 83 insertions(+), 9 deletions(-) diff --git a/ui/src/components/CommandPalette.tsx b/ui/src/components/CommandPalette.tsx index 3defb0e6..9d84be52 100644 --- a/ui/src/components/CommandPalette.tsx +++ b/ui/src/components/CommandPalette.tsx @@ -75,11 +75,15 @@ export function CommandPalette() { enabled: !!selectedCompanyId && open, }); - const { data: projects = [] } = useQuery({ + const { data: allProjects = [] } = useQuery({ queryKey: queryKeys.projects.list(selectedCompanyId!), queryFn: () => projectsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId && open, }); + const projects = useMemo( + () => allProjects.filter((p) => !p.archivedAt), + [allProjects], + ); function go(path: string) { setOpen(false); diff --git a/ui/src/components/IssueProperties.tsx b/ui/src/components/IssueProperties.tsx index cf4b6a43..4781aea5 100644 --- a/ui/src/components/IssueProperties.tsx +++ b/ui/src/components/IssueProperties.tsx @@ -131,8 +131,12 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp queryFn: () => projectsApi.list(companyId!), enabled: !!companyId, }); + const activeProjects = useMemo( + () => (projects ?? []).filter((p) => !p.archivedAt || p.id === issue.projectId), + [projects, issue.projectId], + ); const { orderedProjects } = useProjectOrder({ - projects: projects ?? [], + projects: activeProjects, companyId, userId: currentUserId, }); diff --git a/ui/src/components/NewIssueDialog.tsx b/ui/src/components/NewIssueDialog.tsx index dc2d73c7..5a9ce792 100644 --- a/ui/src/components/NewIssueDialog.tsx +++ b/ui/src/components/NewIssueDialog.tsx @@ -288,8 +288,12 @@ export function NewIssueDialog() { queryFn: () => authApi.getSession(), }); const currentUserId = session?.user?.id ?? session?.session?.userId ?? null; + const activeProjects = useMemo( + () => (projects ?? []).filter((p) => !p.archivedAt), + [projects], + ); const { orderedProjects } = useProjectOrder({ - projects: projects ?? [], + projects: activeProjects, companyId: effectiveCompanyId, userId: currentUserId, }); diff --git a/ui/src/components/ProjectProperties.tsx b/ui/src/components/ProjectProperties.tsx index 9237f5e3..38dc1a33 100644 --- a/ui/src/components/ProjectProperties.tsx +++ b/ui/src/components/ProjectProperties.tsx @@ -13,7 +13,7 @@ import { Separator } from "@/components/ui/separator"; import { Button } from "@/components/ui/button"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; -import { AlertCircle, Check, ExternalLink, Github, Loader2, Plus, Trash2, X } from "lucide-react"; +import { AlertCircle, Archive, ArchiveRestore, Check, ExternalLink, Github, Loader2, Plus, Trash2, X } from "lucide-react"; import { ChoosePathButton } from "./PathInstructionsModal"; import { DraftInput } from "./agent-config-primitives"; import { InlineEditor } from "./InlineEditor"; @@ -34,6 +34,8 @@ interface ProjectPropertiesProps { onUpdate?: (data: Record) => void; onFieldUpdate?: (field: ProjectConfigFieldKey, data: Record) => void; getFieldSaveState?: (field: ProjectConfigFieldKey) => ProjectFieldSaveState; + onArchive?: (archived: boolean) => void; + archivePending?: boolean; } export type ProjectFieldSaveState = "idle" | "saving" | "saved" | "error"; @@ -152,7 +154,7 @@ function ProjectStatusPicker({ status, onChange }: { status: string; onChange: ( ); } -export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSaveState }: ProjectPropertiesProps) { +export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSaveState, onArchive, archivePending }: ProjectPropertiesProps) { const { selectedCompanyId } = useCompany(); const queryClient = useQueryClient(); const [goalOpen, setGoalOpen] = useState(false); @@ -954,6 +956,45 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa )} + + {onArchive && ( + <> + +
+
+ Danger Zone +
+
+

+ {project.archivedAt + ? "Unarchive this project to restore it in the sidebar and project selectors." + : "Archive this project to hide it from the sidebar and project selectors."} +

+ +
+
+ + )} ); } diff --git a/ui/src/pages/ProjectDetail.tsx b/ui/src/pages/ProjectDetail.tsx index 42bb5b86..5134c22b 100644 --- a/ui/src/pages/ProjectDetail.tsx +++ b/ui/src/pages/ProjectDetail.tsx @@ -274,6 +274,21 @@ export function ProjectDetail() { onSuccess: invalidateProject, }); + const archiveProject = useMutation({ + mutationFn: (archived: boolean) => + projectsApi.update( + projectLookupRef, + { archivedAt: archived ? new Date().toISOString() : null }, + resolvedCompanyId ?? lookupCompanyId, + ), + onSuccess: (_, archived) => { + invalidateProject(); + if (archived) { + navigate("/projects"); + } + }, + }); + const uploadImage = useMutation({ mutationFn: async (file: File) => { if (!resolvedCompanyId) throw new Error("No company selected"); @@ -476,6 +491,8 @@ export function ProjectDetail() { onUpdate={(data) => updateProject.mutate(data)} onFieldUpdate={updateProjectField} getFieldSaveState={(field) => fieldSaveStates[field] ?? "idle"} + onArchive={(archived) => archiveProject.mutate(archived)} + archivePending={archiveProject.isPending} /> )} diff --git a/ui/src/pages/Projects.tsx b/ui/src/pages/Projects.tsx index 6fe80ada..886a2b60 100644 --- a/ui/src/pages/Projects.tsx +++ b/ui/src/pages/Projects.tsx @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import { useEffect, useMemo } from "react"; import { useQuery } from "@tanstack/react-query"; import { projectsApi } from "../api/projects"; import { useCompany } from "../context/CompanyContext"; @@ -22,11 +22,15 @@ export function Projects() { setBreadcrumbs([{ label: "Projects" }]); }, [setBreadcrumbs]); - const { data: projects, isLoading, error } = useQuery({ + const { data: allProjects, isLoading, error } = useQuery({ queryKey: queryKeys.projects.list(selectedCompanyId!), queryFn: () => projectsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); + const projects = useMemo( + () => (allProjects ?? []).filter((p) => !p.archivedAt), + [allProjects], + ); if (!selectedCompanyId) { return ; @@ -47,7 +51,7 @@ export function Projects() { {error &&

{error.message}

} - {projects && projects.length === 0 && ( + {!isLoading && projects.length === 0 && ( )} - {projects && projects.length > 0 && ( + {projects.length > 0 && (
{projects.map((project) => ( Date: Sun, 15 Mar 2026 10:48:27 -0500 Subject: [PATCH 08/18] Restyle markdown code blocks: dark background, smaller font, compact padding - Switch code block background from transparent accent to dark (#1e1e2e) with light text (#cdd6f4) for better readability in both light and dark modes - Reduce code font size from 0.84em to 0.78em - Compact padding and margins on pre blocks - Hide MDXEditor code block toolbar by default, show on hover/focus to prevent overlap with code content on mobile - Use horizontal scroll instead of word-wrap for code blocks to preserve formatting Co-Authored-By: Paperclip --- ui/src/components/MarkdownBody.tsx | 2 +- ui/src/index.css | 46 +++++++++++++++++++++++------- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/ui/src/components/MarkdownBody.tsx b/ui/src/components/MarkdownBody.tsx index ca9624c0..1242fa8a 100644 --- a/ui/src/components/MarkdownBody.tsx +++ b/ui/src/components/MarkdownBody.tsx @@ -117,7 +117,7 @@ export function MarkdownBody({ children, className }: MarkdownBodyProps) { return (
Date: Sun, 15 Mar 2026 10:49:24 -0500 Subject: [PATCH 09/18] Fix sidebar scrollbar: hide track background when not hovering The scrollbar track background was still visible as a colored "well" even when the thumb was hidden. Now both track and thumb are fully transparent by default, only appearing on container hover. Co-Authored-By: Paperclip --- ui/src/index.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/src/index.css b/ui/src/index.css index 3b8a5af6..a2a3b794 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -178,12 +178,13 @@ background: oklch(0.5 0 0); } -/* Auto-hide scrollbar: fully transparent by default, visible on container hover */ +/* Auto-hide scrollbar: fully invisible by default, visible on container hover */ .scrollbar-auto-hide::-webkit-scrollbar-track { background: transparent !important; } .scrollbar-auto-hide::-webkit-scrollbar-thumb { background: transparent !important; + transition: background 150ms ease; } .scrollbar-auto-hide:hover::-webkit-scrollbar-track { background: oklch(0.205 0 0) !important; From d7f45eac14845555c4c92b179ea4f3047b517c6a Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 15 Mar 2026 10:55:53 -0500 Subject: [PATCH 10/18] Add doc-maintenance skill for periodic documentation accuracy audits Skill detects documentation drift by scanning git history since last review, cross-referencing shipped features against README, SPEC, and PRODUCT docs, and opening PRs with minimal fixes. Includes audit checklist and section map references. Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- .agents/skills/doc-maintenance/SKILL.md | 201 ++++++++++++++++++ .../references/audit-checklist.md | 85 ++++++++ .../doc-maintenance/references/section-map.md | 22 ++ 3 files changed, 308 insertions(+) create mode 100644 .agents/skills/doc-maintenance/SKILL.md create mode 100644 .agents/skills/doc-maintenance/references/audit-checklist.md create mode 100644 .agents/skills/doc-maintenance/references/section-map.md diff --git a/.agents/skills/doc-maintenance/SKILL.md b/.agents/skills/doc-maintenance/SKILL.md new file mode 100644 index 00000000..a597e90c --- /dev/null +++ b/.agents/skills/doc-maintenance/SKILL.md @@ -0,0 +1,201 @@ +--- +name: doc-maintenance +description: > + Audit top-level documentation (README, SPEC, PRODUCT) against recent git + history to find drift — shipped features missing from docs or features + listed as upcoming that already landed. Proposes minimal edits, creates + a branch, and opens a PR. Use when asked to review docs for accuracy, + after major feature merges, or on a periodic schedule. +--- + +# Doc Maintenance Skill + +Detect documentation drift and fix it via PR — no rewrites, no churn. + +## When to Use + +- Periodic doc review (e.g. weekly or after releases) +- After major feature merges +- When asked "are our docs up to date?" +- When asked to audit README / SPEC / PRODUCT accuracy + +## Target Documents + +| Document | Path | What matters | +|----------|------|-------------| +| README | `README.md` | Features table, roadmap, quickstart, "what is" accuracy, "works with" table | +| SPEC | `doc/SPEC.md` | No false "not supported" claims, major model/schema accuracy | +| PRODUCT | `doc/PRODUCT.md` | Core concepts, feature list, principles accuracy | + +Out of scope: DEVELOPING.md, DATABASE.md, CLI.md, doc/plans/, skill files, +release notes. These are dev-facing or ephemeral — lower risk of user-facing +confusion. + +## Workflow + +### Step 1 — Detect what changed + +Find the last review cursor: + +```bash +# Read the last-reviewed commit SHA +CURSOR_FILE=".doc-review-cursor" +if [ -f "$CURSOR_FILE" ]; then + LAST_SHA=$(cat "$CURSOR_FILE" | head -1) +else + # First run: look back 60 days + LAST_SHA=$(git log --format="%H" --after="60 days ago" --reverse | head -1) +fi +``` + +Then gather commits since the cursor: + +```bash +git log "$LAST_SHA"..HEAD --oneline --no-merges +``` + +### Step 2 — Classify changes + +Scan commit messages and changed files. Categorize into: + +- **Feature** — new capabilities (keywords: `feat`, `add`, `implement`, `support`) +- **Breaking** — removed/renamed things (keywords: `remove`, `breaking`, `drop`, `rename`) +- **Structural** — new directories, config changes, new adapters, new CLI commands + +**Ignore:** refactors, test-only changes, CI config, dependency bumps, doc-only +changes, style/formatting commits. These don't affect doc accuracy. + +For borderline cases, check the actual diff — a commit titled "refactor: X" +that adds a new public API is a feature. + +### Step 3 — Build a change summary + +Produce a concise list like: + +``` +Since last review (, ): +- FEATURE: Plugin system merged (runtime, SDK, CLI, slots, event bridge) +- FEATURE: Project archiving added +- BREAKING: Removed legacy webhook adapter +- STRUCTURAL: New .agents/skills/ directory convention +``` + +If there are no notable changes, skip to Step 7 (update cursor and exit). + +### Step 4 — Audit each target doc + +For each target document, read it fully and cross-reference against the change +summary. Check for: + +1. **False negatives** — major shipped features not mentioned at all +2. **False positives** — features listed as "coming soon" / "roadmap" / "planned" + / "not supported" / "TBD" that already shipped +3. **Quickstart accuracy** — install commands, prereqs, and startup instructions + still correct (README only) +4. **Feature table accuracy** — does the features section reflect current + capabilities? (README only) +5. **Works-with accuracy** — are supported adapters/integrations listed correctly? + +Use `references/audit-checklist.md` as the structured checklist. +Use `references/section-map.md` to know where to look for each feature area. + +### Step 5 — Create branch and apply minimal edits + +```bash +# Create a branch for the doc updates +BRANCH="docs/maintenance-$(date +%Y%m%d)" +git checkout -b "$BRANCH" +``` + +Apply **only** the edits needed to fix drift. Rules: + +- **Minimal patches only.** Fix inaccuracies, don't rewrite sections. +- **Preserve voice and style.** Match the existing tone of each document. +- **No cosmetic changes.** Don't fix typos, reformat tables, or reorganize + sections unless they're part of a factual fix. +- **No new sections.** If a feature needs a whole new section, note it in the + PR description as a follow-up — don't add it in a maintenance pass. +- **Roadmap items:** Move shipped features out of Roadmap. Add a brief mention + in the appropriate existing section if there isn't one already. Don't add + long descriptions. + +### Step 6 — Open a PR + +Commit the changes and open a PR: + +```bash +git add README.md doc/SPEC.md doc/PRODUCT.md .doc-review-cursor +git commit -m "docs: update documentation for accuracy + +- [list each fix briefly] + +Co-Authored-By: Paperclip " + +git push -u origin "$BRANCH" + +gh pr create \ + --title "docs: periodic documentation accuracy update" \ + --body "$(cat <<'EOF' +## Summary +Automated doc maintenance pass. Fixes documentation drift detected since +last review. + +### Changes +- [list each fix] + +### Change summary (since last review) +- [list notable code changes that triggered doc updates] + +## Review notes +- Only factual accuracy fixes — no style/cosmetic changes +- Preserves existing voice and structure +- Larger doc additions (new sections, tutorials) noted as follow-ups + +🤖 Generated by doc-maintenance skill +EOF +)" +``` + +### Step 7 — Update the cursor + +After a successful audit (whether or not edits were needed), update the cursor: + +```bash +git rev-parse HEAD > .doc-review-cursor +``` + +If edits were made, this is already committed in the PR branch. If no edits +were needed, commit the cursor update to the current branch. + +## Change Classification Rules + +| Signal | Category | Doc update needed? | +|--------|----------|-------------------| +| `feat:`, `add`, `implement`, `support` in message | Feature | Yes if user-facing | +| `remove`, `drop`, `breaking`, `!:` in message | Breaking | Yes | +| New top-level directory or config file | Structural | Maybe | +| `fix:`, `bugfix` | Fix | No (unless it changes behavior described in docs) | +| `refactor:`, `chore:`, `ci:`, `test:` | Maintenance | No | +| `docs:` | Doc change | No (already handled) | +| Dependency bumps only | Maintenance | No | + +## Patch Style Guide + +- Fix the fact, not the prose +- If removing a roadmap item, don't leave a gap — remove the bullet cleanly +- If adding a feature mention, match the format of surrounding entries + (e.g. if features are in a table, add a table row) +- Keep README changes especially minimal — it shouldn't churn often +- For SPEC/PRODUCT, prefer updating existing statements over adding new ones + (e.g. change "not supported in V1" to "supported via X" rather than adding + a new section) + +## Output + +When the skill completes, report: + +- How many commits were scanned +- How many notable changes were found +- How many doc edits were made (and to which files) +- PR link (if edits were made) +- Any follow-up items that need larger doc work diff --git a/.agents/skills/doc-maintenance/references/audit-checklist.md b/.agents/skills/doc-maintenance/references/audit-checklist.md new file mode 100644 index 00000000..9c13a437 --- /dev/null +++ b/.agents/skills/doc-maintenance/references/audit-checklist.md @@ -0,0 +1,85 @@ +# Doc Maintenance Audit Checklist + +Use this checklist when auditing each target document. For each item, compare +against the change summary from git history. + +## README.md + +### Features table +- [ ] Each feature card reflects a shipped capability +- [ ] No feature cards for things that don't exist yet +- [ ] No major shipped features missing from the table + +### Roadmap +- [ ] Nothing listed as "planned" or "coming soon" that already shipped +- [ ] No removed/cancelled items still listed +- [ ] Items reflect current priorities (cross-check with recent PRs) + +### Quickstart +- [ ] `npx paperclipai onboard` command is correct +- [ ] Manual install steps are accurate (clone URL, commands) +- [ ] Prerequisites (Node version, pnpm version) are current +- [ ] Server URL and port are correct + +### "What is Paperclip" section +- [ ] High-level description is accurate +- [ ] Step table (Define goal / Hire team / Approve and run) is correct + +### "Works with" table +- [ ] All supported adapters/runtimes are listed +- [ ] No removed adapters still listed +- [ ] Logos and labels match current adapter names + +### "Paperclip is right for you if" +- [ ] Use cases are still accurate +- [ ] No claims about capabilities that don't exist + +### "Why Paperclip is special" +- [ ] Technical claims are accurate (atomic execution, governance, etc.) +- [ ] No features listed that were removed or significantly changed + +### FAQ +- [ ] Answers are still correct +- [ ] No references to removed features or outdated behavior + +### Development section +- [ ] Commands are accurate (`pnpm dev`, `pnpm build`, etc.) +- [ ] Link to DEVELOPING.md is correct + +## doc/SPEC.md + +### Company Model +- [ ] Fields match current schema +- [ ] Governance model description is accurate + +### Agent Model +- [ ] Adapter types match what's actually supported +- [ ] Agent configuration description is accurate +- [ ] No features described as "not supported" or "not V1" that shipped + +### Task Model +- [ ] Task hierarchy description is accurate +- [ ] Status values match current implementation + +### Extensions / Plugins +- [ ] If plugins are shipped, no "not in V1" or "future" language +- [ ] Plugin model description matches implementation + +### Open Questions +- [ ] Resolved questions removed or updated +- [ ] No "TBD" items that have been decided + +## doc/PRODUCT.md + +### Core Concepts +- [ ] Company, Employees, Task Management descriptions accurate +- [ ] Agent Execution modes described correctly +- [ ] No missing major concepts + +### Principles +- [ ] Principles haven't been contradicted by shipped features +- [ ] No principles referencing removed capabilities + +### User Flow +- [ ] Dream scenario still reflects actual onboarding +- [ ] Steps are achievable with current features diff --git a/.agents/skills/doc-maintenance/references/section-map.md b/.agents/skills/doc-maintenance/references/section-map.md new file mode 100644 index 00000000..4ec64f83 --- /dev/null +++ b/.agents/skills/doc-maintenance/references/section-map.md @@ -0,0 +1,22 @@ +# Section Map + +Maps feature areas to specific document sections so the skill knows where to +look when a feature ships or changes. + +| Feature Area | README Section | SPEC Section | PRODUCT Section | +|-------------|---------------|-------------|----------------| +| Plugins / Extensions | Features table, Roadmap | Extensions, Agent Model | Core Concepts | +| Adapters (new runtimes) | "Works with" table, FAQ | Agent Model, Agent Configuration | Employees & Agents, Agent Execution | +| Governance / Approvals | Features table, "Why special" | Board Governance, Board Approval Gates | Principles | +| Budget / Cost Control | Features table, "Why special" | Budget Delegation | Company (revenue & expenses) | +| Task Management | Features table | Task Model | Task Management | +| Org Chart / Hierarchy | Features table | Agent Model (reporting) | Employees & Agents | +| Multi-Company | Features table, FAQ | Company Model | Company | +| Heartbeats | Features table, FAQ | Agent Execution | Agent Execution | +| CLI Commands | Development section | — | — | +| Onboarding / Quickstart | Quickstart, FAQ | — | User Flow | +| Skills / Skill Injection | "Why special" | — | — | +| Company Templates | "Why special", Roadmap (ClipMart) | — | — | +| Mobile / UI | Features table | — | — | +| Project Archiving | — | — | — | +| OpenClaw Integration | "Works with" table, FAQ | Agent Model | Agent Execution | From 41e03bae61d1ad5c96bf250043076b682cd25d5e Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 15 Mar 2026 14:08:37 -0500 Subject: [PATCH 11/18] Fix org chart canvas height to fit viewport without scrolling The height calc subtracted only 4rem but the actual overhead is ~6rem (3rem breadcrumb bar + 3rem main padding). Also use dvh for better mobile support. Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- ui/src/pages/OrgChart.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/pages/OrgChart.tsx b/ui/src/pages/OrgChart.tsx index 981545c0..7eb0f0d9 100644 --- a/ui/src/pages/OrgChart.tsx +++ b/ui/src/pages/OrgChart.tsx @@ -269,7 +269,7 @@ export function OrgChart() { return (
Date: Sun, 15 Mar 2026 14:18:56 -0500 Subject: [PATCH 12/18] Add Docker setup for untrusted PR review in isolated containers Adds a dedicated Docker environment for reviewing untrusted pull requests with codex/claude, keeping CLI auth state in volumes and using a separate scratch workspace for PR checkouts. Co-Authored-By: Claude Opus 4.6 (1M context) --- doc/DEVELOPING.md | 4 + doc/DOCKER.md | 6 + doc/UNTRUSTED-PR-REVIEW.md | 135 ++++++++++++++++++ docker-compose.untrusted-review.yml | 33 +++++ docker/untrusted-review/Dockerfile | 44 ++++++ .../untrusted-review/bin/review-checkout-pr | 65 +++++++++ 6 files changed, 287 insertions(+) create mode 100644 doc/UNTRUSTED-PR-REVIEW.md create mode 100644 docker-compose.untrusted-review.yml create mode 100644 docker/untrusted-review/Dockerfile create mode 100644 docker/untrusted-review/bin/review-checkout-pr diff --git a/doc/DEVELOPING.md b/doc/DEVELOPING.md index e3668516..b39839c1 100644 --- a/doc/DEVELOPING.md +++ b/doc/DEVELOPING.md @@ -89,6 +89,10 @@ docker compose -f docker-compose.quickstart.yml up --build See `doc/DOCKER.md` for API key wiring (`OPENAI_API_KEY` / `ANTHROPIC_API_KEY`) and persistence details. +## Docker For Untrusted PR Review + +For a separate review-oriented container that keeps `codex`/`claude` login state in Docker volumes and checks out PRs into an isolated scratch workspace, see `doc/UNTRUSTED-PR-REVIEW.md`. + ## Database in Dev (Auto-Handled) For local development, leave `DATABASE_URL` unset. diff --git a/doc/DOCKER.md b/doc/DOCKER.md index 82559bf8..6f6ca374 100644 --- a/doc/DOCKER.md +++ b/doc/DOCKER.md @@ -93,6 +93,12 @@ Notes: - Without API keys, the app still runs normally. - Adapter environment checks in Paperclip will surface missing auth/CLI prerequisites. +## Untrusted PR Review Container + +If you want a separate Docker environment for reviewing untrusted pull requests with `codex` or `claude`, use the dedicated review workflow in `doc/UNTRUSTED-PR-REVIEW.md`. + +That setup keeps CLI auth state in Docker volumes instead of your host home directory and uses a separate scratch workspace for PR checkouts and preview runs. + ## Onboard Smoke Test (Ubuntu + npm only) Use this when you want to mimic a fresh machine that only has Ubuntu + npm and verify: diff --git a/doc/UNTRUSTED-PR-REVIEW.md b/doc/UNTRUSTED-PR-REVIEW.md new file mode 100644 index 00000000..0061a581 --- /dev/null +++ b/doc/UNTRUSTED-PR-REVIEW.md @@ -0,0 +1,135 @@ +# Untrusted PR Review In Docker + +Use this workflow when you want Codex or Claude to inspect a pull request that you do not want touching your host machine directly. + +This is intentionally separate from the normal Paperclip dev image. + +## What this container isolates + +- `codex` auth/session state in a Docker volume, not your host `~/.codex` +- `claude` auth/session state in a Docker volume, not your host `~/.claude` +- `gh` auth state in the same container-local home volume +- review clones, worktrees, dependency installs, and local databases in a writable scratch volume under `/work` + +By default this workflow does **not** mount your host repo checkout, your host home directory, or your SSH agent. + +## Files + +- `docker/untrusted-review/Dockerfile` +- `docker-compose.untrusted-review.yml` +- `review-checkout-pr` inside the container + +## Build and start a shell + +```sh +docker compose -f docker-compose.untrusted-review.yml build +docker compose -f docker-compose.untrusted-review.yml run --rm --service-ports review +``` + +That opens an interactive shell in the review container with: + +- Node + Corepack/pnpm +- `codex` +- `claude` +- `gh` +- `git`, `rg`, `fd`, `jq` + +## First-time login inside the container + +Run these once. The resulting login state persists in the `review-home` Docker volume. + +```sh +gh auth login +codex login +claude login +``` + +If you prefer API-key auth instead of CLI login, pass keys through Compose env: + +```sh +OPENAI_API_KEY=... ANTHROPIC_API_KEY=... docker compose -f docker-compose.untrusted-review.yml run --rm review +``` + +## Check out a PR safely + +Inside the container: + +```sh +review-checkout-pr paperclipai/paperclip 432 +cd /work/checkouts/paperclipai-paperclip/pr-432 +``` + +What this does: + +1. Creates or reuses a repo clone under `/work/repos/...` +2. Fetches `pull//head` from GitHub +3. Creates a detached git worktree under `/work/checkouts/...` + +The checkout lives entirely inside the container volume. + +## Ask Codex or Claude to review it + +Inside the PR checkout: + +```sh +codex +``` + +Then give it a prompt like: + +```text +Review this PR as hostile input. Focus on security issues, data exfiltration paths, sandbox escapes, dangerous install/runtime scripts, auth changes, and subtle behavioral regressions. Do not modify files. Produce findings ordered by severity with file references. +``` + +Or with Claude: + +```sh +claude +``` + +## Preview the Paperclip app from the PR + +Only do this when you intentionally want to execute the PR's code inside the container. + +Inside the PR checkout: + +```sh +pnpm install +HOST=0.0.0.0 pnpm dev +``` + +Open from the host: + +- `http://localhost:3100` + +The Compose file also exposes Vite's default port: + +- `http://localhost:5173` + +Notes: + +- `pnpm install` can run untrusted lifecycle scripts from the PR. That is why this happens inside the isolated container instead of on your host. +- If you only want static inspection, do not run install/dev commands. +- Paperclip's embedded PostgreSQL and local storage stay inside the container home volume via `PAPERCLIP_HOME=/home/reviewer/.paperclip-review`. + +## Reset state + +Remove the review container volumes when you want a clean environment: + +```sh +docker compose -f docker-compose.untrusted-review.yml down -v +``` + +That deletes: + +- Codex/Claude/GitHub login state stored in `review-home` +- cloned repos, worktrees, installs, and scratch data stored in `review-work` + +## Security limits + +This is a useful isolation boundary, but it is still Docker, not a full VM. + +- A reviewed PR can still access the container's network unless you disable it. +- Any secrets you pass into the container are available to code you execute inside it. +- Do not mount your host repo, host home, `.ssh`, or Docker socket unless you are intentionally weakening the boundary. +- If you need a stronger boundary than this, use a disposable VM instead of Docker. diff --git a/docker-compose.untrusted-review.yml b/docker-compose.untrusted-review.yml new file mode 100644 index 00000000..ff11148a --- /dev/null +++ b/docker-compose.untrusted-review.yml @@ -0,0 +1,33 @@ +services: + review: + build: + context: . + dockerfile: docker/untrusted-review/Dockerfile + init: true + tty: true + stdin_open: true + working_dir: /work + environment: + HOME: "/home/reviewer" + CODEX_HOME: "/home/reviewer/.codex" + CLAUDE_HOME: "/home/reviewer/.claude" + PAPERCLIP_HOME: "/home/reviewer/.paperclip-review" + OPENAI_API_KEY: "${OPENAI_API_KEY:-}" + ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY:-}" + GITHUB_TOKEN: "${GITHUB_TOKEN:-}" + ports: + - "${REVIEW_PAPERCLIP_PORT:-3100}:3100" + - "${REVIEW_VITE_PORT:-5173}:5173" + volumes: + - review-home:/home/reviewer + - review-work:/work + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + tmpfs: + - /tmp:mode=1777,size=1g + +volumes: + review-home: + review-work: diff --git a/docker/untrusted-review/Dockerfile b/docker/untrusted-review/Dockerfile new file mode 100644 index 00000000..c8b1f432 --- /dev/null +++ b/docker/untrusted-review/Dockerfile @@ -0,0 +1,44 @@ +FROM node:lts-trixie-slim + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + bash \ + ca-certificates \ + curl \ + fd-find \ + gh \ + git \ + jq \ + less \ + openssh-client \ + procps \ + ripgrep \ + && rm -rf /var/lib/apt/lists/* + +RUN ln -sf /usr/bin/fdfind /usr/local/bin/fd + +RUN corepack enable \ + && npm install --global --omit=dev @anthropic-ai/claude-code@latest @openai/codex@latest + +RUN useradd --create-home --shell /bin/bash reviewer + +ENV HOME=/home/reviewer \ + CODEX_HOME=/home/reviewer/.codex \ + CLAUDE_HOME=/home/reviewer/.claude \ + PAPERCLIP_HOME=/home/reviewer/.paperclip-review \ + PNPM_HOME=/home/reviewer/.local/share/pnpm \ + PATH=/home/reviewer/.local/share/pnpm:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + +WORKDIR /work + +COPY --chown=reviewer:reviewer docker/untrusted-review/bin/review-checkout-pr /usr/local/bin/review-checkout-pr + +RUN chmod +x /usr/local/bin/review-checkout-pr \ + && mkdir -p /work \ + && chown -R reviewer:reviewer /work + +USER reviewer + +EXPOSE 3100 5173 + +CMD ["bash", "-l"] diff --git a/docker/untrusted-review/bin/review-checkout-pr b/docker/untrusted-review/bin/review-checkout-pr new file mode 100644 index 00000000..abca98ad --- /dev/null +++ b/docker/untrusted-review/bin/review-checkout-pr @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: review-checkout-pr [checkout-dir] + +Examples: + review-checkout-pr paperclipai/paperclip 432 + review-checkout-pr https://github.com/paperclipai/paperclip.git 432 +EOF +} + +if [[ $# -lt 2 || $# -gt 3 ]]; then + usage >&2 + exit 1 +fi + +normalize_repo_slug() { + local raw="$1" + raw="${raw#git@github.com:}" + raw="${raw#ssh://git@github.com/}" + raw="${raw#https://github.com/}" + raw="${raw#http://github.com/}" + raw="${raw%.git}" + printf '%s\n' "${raw#/}" +} + +repo_slug="$(normalize_repo_slug "$1")" +pr_number="$2" + +if [[ ! "$repo_slug" =~ ^[^/]+/[^/]+$ ]]; then + echo "Expected GitHub repo slug like owner/repo or a GitHub repo URL, got: $1" >&2 + exit 1 +fi + +if [[ ! "$pr_number" =~ ^[0-9]+$ ]]; then + echo "PR number must be numeric, got: $pr_number" >&2 + exit 1 +fi + +repo_key="${repo_slug//\//-}" +mirror_dir="/work/repos/${repo_key}" +checkout_dir="${3:-/work/checkouts/${repo_key}/pr-${pr_number}}" +pr_ref="refs/remotes/origin/pr/${pr_number}" + +mkdir -p "$(dirname "$mirror_dir")" "$(dirname "$checkout_dir")" + +if [[ ! -d "$mirror_dir/.git" ]]; then + if command -v gh >/dev/null 2>&1; then + gh repo clone "$repo_slug" "$mirror_dir" -- --filter=blob:none + else + git clone --filter=blob:none "https://github.com/${repo_slug}.git" "$mirror_dir" + fi +fi + +git -C "$mirror_dir" fetch --force origin "pull/${pr_number}/head:${pr_ref}" + +if [[ -e "$checkout_dir" ]]; then + printf '%s\n' "$checkout_dir" + exit 0 +fi + +git -C "$mirror_dir" worktree add --detach "$checkout_dir" "$pr_ref" >/dev/null +printf '%s\n' "$checkout_dir" From 597c4b1d457d2207dee01863c6ade1078d7f6c83 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 15 Mar 2026 14:27:34 -0500 Subject: [PATCH 13/18] Fix code block styles with robust prose overrides Previous attempt was being overridden by Tailwind prose/prose-invert CSS variables. This fix: - Overrides --tw-prose-pre-bg and --tw-prose-invert-pre-bg CSS variables on .paperclip-markdown to force dark background in both modes - Uses .paperclip-markdown pre with \!important for bulletproof overrides - Removes conflicting prose-pre: utility classes from MarkdownBody - Adds explicit pre code reset (inherit color/size, no background) - Verified visually with Playwright at desktop and mobile viewports Co-Authored-By: Paperclip --- ui/src/components/MarkdownBody.tsx | 2 +- ui/src/index.css | 36 +++++++++++++++++++++--------- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/ui/src/components/MarkdownBody.tsx b/ui/src/components/MarkdownBody.tsx index 1242fa8a..683adc53 100644 --- a/ui/src/components/MarkdownBody.tsx +++ b/ui/src/components/MarkdownBody.tsx @@ -117,7 +117,7 @@ export function MarkdownBody({ children, className }: MarkdownBodyProps) { return (
Date: Sun, 15 Mar 2026 14:33:22 -0500 Subject: [PATCH 14/18] Remove duplicate @paperclipai/adapter-openclaw-gateway in server/package.json Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- server/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/server/package.json b/server/package.json index a2387fe9..1887d64c 100644 --- a/server/package.json +++ b/server/package.json @@ -41,7 +41,6 @@ "@paperclipai/adapter-openclaw-gateway": "workspace:*", "@paperclipai/adapter-opencode-local": "workspace:*", "@paperclipai/adapter-pi-local": "workspace:*", - "@paperclipai/adapter-openclaw-gateway": "workspace:*", "hermes-paperclip-adapter": "0.1.1", "@paperclipai/adapter-utils": "workspace:*", "@paperclipai/db": "workspace:*", From 16ab8c830325d36cb532e3e8701d9f6c59c683a6 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 15 Mar 2026 14:39:09 -0500 Subject: [PATCH 15/18] Dark theme for CodeMirror code blocks in MDXEditor The code blocks users see in issue documents are rendered by CodeMirror (via MDXEditor's codeMirrorPlugin), not by MarkdownBody. MDXEditor bundles cm6-theme-basic-light which gives them a white background. Added dark overrides for all CodeMirror elements: - .cm-editor: dark background (#1e1e2e), light text (#cdd6f4) - .cm-gutters: darker gutter with muted line numbers - .cm-activeLine, .cm-selectionBackground: subtle dark highlights - .cm-cursor: light cursor for visibility - Language selector dropdown: dark-themed to match - Reduced pre padding to 0 since CodeMirror handles its own spacing Uses \!important to beat CodeMirror's programmatically-injected theme styles (EditorView.theme generates high-specificity scoped selectors). Co-Authored-By: Paperclip --- ui/src/index.css | 50 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/ui/src/index.css b/ui/src/index.css index 1172f103..c9ee652f 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -417,7 +417,7 @@ .paperclip-mdxeditor-content pre { margin: 0.4rem 0; - padding: 0.5rem 0.65rem; + padding: 0; border: 1px solid color-mix(in oklab, var(--foreground) 12%, transparent); border-radius: calc(var(--radius) - 3px); background: #1e1e2e; @@ -425,7 +425,46 @@ overflow-x: auto; } -/* MDXEditor code block language selector – keep it out of the way on small screens */ +/* Dark theme for CodeMirror code blocks inside the MDXEditor. + Overrides the default cm6-theme-basic-light that MDXEditor bundles. */ +.paperclip-mdxeditor .cm-editor { + background-color: #1e1e2e !important; + color: #cdd6f4 !important; + font-size: 0.78em; +} + +.paperclip-mdxeditor .cm-gutters { + background-color: #181825 !important; + color: #585b70 !important; + border-right: 1px solid #313244 !important; +} + +.paperclip-mdxeditor .cm-activeLineGutter { + background-color: #1e1e2e !important; +} + +.paperclip-mdxeditor .cm-activeLine { + background-color: color-mix(in oklab, #cdd6f4 5%, transparent) !important; +} + +.paperclip-mdxeditor .cm-cursor, +.paperclip-mdxeditor .cm-dropCursor { + border-left-color: #cdd6f4 !important; +} + +.paperclip-mdxeditor .cm-selectionBackground { + background-color: color-mix(in oklab, #89b4fa 25%, transparent) !important; +} + +.paperclip-mdxeditor .cm-focused .cm-selectionBackground { + background-color: color-mix(in oklab, #89b4fa 30%, transparent) !important; +} + +.paperclip-mdxeditor .cm-content { + caret-color: #cdd6f4; +} + +/* MDXEditor code block language selector – show on hover only */ .paperclip-mdxeditor-content [class*="_codeMirrorWrapper_"] { position: relative; } @@ -440,6 +479,13 @@ transition: opacity 150ms ease; } +.paperclip-mdxeditor-content [class*="_codeMirrorToolbar_"] select, +.paperclip-mdxeditor-content [class*="_codeBlockToolbar_"] select { + background-color: #313244; + color: #cdd6f4; + border-color: #45475a; +} + .paperclip-mdxeditor-content [class*="_codeMirrorWrapper_"]:hover [class*="_codeMirrorToolbar_"], .paperclip-mdxeditor-content [class*="_codeMirrorWrapper_"]:hover [class*="_codeBlockToolbar_"], .paperclip-mdxeditor-content [class*="_codeMirrorWrapper_"]:focus-within [class*="_codeMirrorToolbar_"], From c5cc191a08fbd9da2e1b68b525ffd48c14160bc0 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 15 Mar 2026 14:44:01 -0500 Subject: [PATCH 16/18] chore: ignore superset artifacts --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 066fcc68..06303bd0 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,5 @@ tmp/ # Playwright tests/e2e/test-results/ -tests/e2e/playwright-report/ \ No newline at end of file +tests/e2e/playwright-report/ +.superset/ \ No newline at end of file From 3bffe3e479c65987ab8f91cf356b00fc8debdbcc Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 15 Mar 2026 20:30:54 -0500 Subject: [PATCH 17/18] docs: update documentation for accuracy after plugin system launch - README: mark plugin system as shipped in roadmap - SPEC: update adapter table with openclaw_gateway, gemini-local, hermes_local - SPEC: update plugin architecture section to reflect shipped status - Add .doc-review-cursor for future maintenance runs Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- .doc-review-cursor | 1 + README.md | 2 +- doc/SPEC.md | 15 +++++++++------ 3 files changed, 11 insertions(+), 7 deletions(-) create mode 100644 .doc-review-cursor diff --git a/.doc-review-cursor b/.doc-review-cursor new file mode 100644 index 00000000..789f2092 --- /dev/null +++ b/.doc-review-cursor @@ -0,0 +1 @@ +16ab8c8 diff --git a/README.md b/README.md index 70ddee5f..391a0feb 100644 --- a/README.md +++ b/README.md @@ -239,7 +239,7 @@ See [doc/DEVELOPING.md](doc/DEVELOPING.md) for the full development guide. - ⚪ ClipMart - buy and sell entire agent companies - ⚪ Easy agent configurations / easier to understand - ⚪ Better support for harness engineering -- ⚪ Plugin system (e.g. if you want to add a knowledgebase, custom tracing, queues, etc) +- 🟢 Plugin system (e.g. if you want to add a knowledgebase, custom tracing, queues, etc) - ⚪ Better docs
diff --git a/doc/SPEC.md b/doc/SPEC.md index 33c24b3a..44cfe8a8 100644 --- a/doc/SPEC.md +++ b/doc/SPEC.md @@ -188,12 +188,15 @@ The heartbeat is a protocol, not a runtime. Paperclip defines how to initiate an Agent configuration includes an **adapter** that defines how Paperclip invokes the agent. Initial adapters: -| Adapter | Mechanism | Example | -| --------- | ----------------------- | --------------------------------------------- | -| `process` | Execute a child process | `python run_agent.py --agent-id {id}` | -| `http` | Send an HTTP request | `POST https://openclaw.example.com/hook/{id}` | +| Adapter | Mechanism | Example | +| -------------------- | ----------------------- | --------------------------------------------- | +| `process` | Execute a child process | `python run_agent.py --agent-id {id}` | +| `http` | Send an HTTP request | `POST https://openclaw.example.com/hook/{id}` | +| `openclaw_gateway` | OpenClaw gateway API | Managed OpenClaw agent via gateway | +| `gemini-local` | Gemini CLI process | Local Gemini CLI with sandbox and approval | +| `hermes_local` | Hermes agent process | Local Hermes agent | -The `process` and `http` adapters ship as defaults. Additional adapters can be added via the plugin system (see Plugin / Extension Architecture). +The `process` and `http` adapters ship as defaults. Additional adapters have been added for specific agent runtimes (see list above), and new adapter types can be registered via the plugin system (see Plugin / Extension Architecture). ### Adapter Interface @@ -429,7 +432,7 @@ The core Paperclip system must be extensible. Features like knowledge bases, ext - **Agent Adapter plugins** — new Adapter types can be registered via the plugin system - Plugin-registrable UI components (future) -This isn't a V1 deliverable (we're not building a plugin framework upfront), but the architecture should not paint us into a corner. Keep boundaries clean so extensions are possible. +The plugin framework has shipped. Plugins can register new adapter types, hook into lifecycle events, and contribute UI components (e.g. global toolbar buttons). A plugin SDK and CLI commands (`paperclipai plugin`) are available for authoring and installing plugins. --- From 52b12784a04706efce9a6013bc7f665126c849b2 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 15 Mar 2026 21:08:19 -0500 Subject: [PATCH 18/18] =?UTF-8?q?docs:=20fix=20documentation=20drift=20?= =?UTF-8?q?=E2=80=94=20adapters,=20plugins,=20tech=20stack?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix gemini adapter name: `gemini-local` → `gemini_local` (matches registry.ts) - Move .doc-review-cursor to .gitignore (tooling state, not source) Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- .doc-review-cursor | 1 - .gitignore | 3 +++ doc/SPEC.md | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) delete mode 100644 .doc-review-cursor diff --git a/.doc-review-cursor b/.doc-review-cursor deleted file mode 100644 index 789f2092..00000000 --- a/.doc-review-cursor +++ /dev/null @@ -1 +0,0 @@ -16ab8c8 diff --git a/.gitignore b/.gitignore index 066fcc68..ecb6e9a7 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,9 @@ tmp/ .claude/settings.local.json .paperclip-local/ +# Doc maintenance cursor +.doc-review-cursor + # Playwright tests/e2e/test-results/ tests/e2e/playwright-report/ \ No newline at end of file diff --git a/doc/SPEC.md b/doc/SPEC.md index 44cfe8a8..82315bce 100644 --- a/doc/SPEC.md +++ b/doc/SPEC.md @@ -193,7 +193,7 @@ Agent configuration includes an **adapter** that defines how Paperclip invokes t | `process` | Execute a child process | `python run_agent.py --agent-id {id}` | | `http` | Send an HTTP request | `POST https://openclaw.example.com/hook/{id}` | | `openclaw_gateway` | OpenClaw gateway API | Managed OpenClaw agent via gateway | -| `gemini-local` | Gemini CLI process | Local Gemini CLI with sandbox and approval | +| `gemini_local` | Gemini CLI process | Local Gemini CLI with sandbox and approval | | `hermes_local` | Hermes agent process | Local Hermes agent | The `process` and `http` adapters ship as defaults. Additional adapters have been added for specific agent runtimes (see list above), and new adapter types can be registered via the plugin system (see Plugin / Extension Architecture).