import { createHash, randomUUID } from "node:crypto"; import type { Agent, AgentSessionEvent, Issue, IssueComment, PluginContext, PluginEvent, PluginLocalFolderEntry, Project, ToolResult } from "@paperclipai/plugin-sdk"; import type { IssueDocument, PluginIssueOriginKind, PluginManagedRoutineResolution, PluginManagedSkillResolution } from "@paperclipai/plugin-sdk/types"; import { DEFAULT_MAX_SOURCE_BYTES, DEFAULT_MAX_PAPERCLIP_CURSOR_WINDOW_CHARS, DEFAULT_MAX_PAPERCLIP_ISSUE_SOURCE_CHARS, DEFAULT_MAX_PAPERCLIP_ROUTINE_RUN_CHARS, DEFAULT_PAPERCLIP_COST_CENTS_PER_1K_CHARS, PLUGIN_ID, WIKI_MAINTAINER_AGENT_KEY, WIKI_MANAGED_SKILL_KEYS, WIKI_MAINTENANCE_ROUTINE_KEYS, WIKI_PROJECT_KEY, WIKI_ROOT_FOLDER_KEY, } from "../manifest.js"; import { BOOTSTRAP_FILES, LINT_PROMPT, QUERY_PROMPT, REQUIRED_WIKI_DIRECTORIES, REQUIRED_WIKI_FILES, } from "../templates.js"; export const DEFAULT_WIKI_ID = "default"; export const DEFAULT_SPACE_SLUG = "default"; export const OPERATION_ORIGIN_KIND = `plugin:${PLUGIN_ID}:operation` as PluginIssueOriginKind; const EVENT_INGESTION_STATE_NAMESPACE = "llm-wiki"; const EVENT_INGESTION_STATE_KEY = "event-ingestion"; const EVENT_INGESTION_DEDUP_NAMESPACE = "llm-wiki-event-ingestion"; const MAX_EVENT_SOURCE_CHARS = 20000; const MAX_PAPERCLIP_INGESTION_PROFILE_SOURCE_COUNT = 3; const MAX_PAPERCLIP_DISTILLATION_FAN_OUT = 25; const MAX_PAPERCLIP_PROFILE_SELECTED_PROJECTS = 25; const MAX_PAPERCLIP_PROFILE_ROOT_ISSUES = 25; const PROTECTED_WIKI_CONTROL_FILES = new Set(["AGENTS.md", "IDEA.md"]); export const PUBLIC_DISTILLATION_AUTO_APPLY_RESTRICTION = "Authenticated/public deployments always require manual review before wiki writes."; export type WikiEventIngestionSource = "issues" | "comments" | "documents"; export type PaperclipDistillationScope = "company" | "project" | "root_issue"; export type PaperclipDistillationWorkItemKind = "manual" | "retry" | "backfill" | "priority_override" | "review_patch"; export type WikiEventIngestionSettings = { enabled: boolean; sources: Record; wikiId: string; maxCharacters: number; }; export type WikiEventIngestionSettingsUpdate = Omit, "sources"> & { sources?: Partial>; }; export type PaperclipIngestionSourceScope = | { kind: "active_projects"; limit: number; statuses?: Array<"in_progress" | "todo" | "done"> } | { kind: "selected_projects"; projectIds: string[] } | { kind: "root_issues"; issueIds: string[] } | { kind: "company_all"; requiresBoardConfirmation: true }; export type PaperclipIngestionProfileV1 = { version: 1; enabled: boolean; sourceScopes: PaperclipIngestionSourceScope[]; sourceKinds: Record & { attachments: "off" | "metadata_only"; workProducts: "off" | "metadata_only"; }; cursor: { maxWindowCharacters: number; maxCharactersPerSource: number; minSourceAgeMinutes: number; maxWindowsPerRun: number; staleAfterHours: number; }; backfill: { defaultStartAt?: string | null; defaultEndAt?: string | null; requireManualQueue: boolean; }; }; export type PaperclipIngestionProfileEffectiveState = | "enabled" | "disabled" | "policy_blocked" | "pending_approval" | "enabled_no_scopes"; export type PaperclipIngestionProfileRead = { wikiId: string; space: Pick; profile: PaperclipIngestionProfileV1; effectiveState: PaperclipIngestionProfileEffectiveState; policyBlocks: string[]; historicalPageCount: number; overlapCount: number; }; export type DistillationAutoApplyRestriction = { autoApplyAllowed: boolean; autoApplyRestriction: string | null; deploymentMode: "local_trusted" | "authenticated" | null; deploymentExposure: "private" | "public" | null; }; type PaperclipIngestionPolicyPurpose = | "profile_read" | "profile_update" | "candidate_search" | "queue" | "execute" | "event_routing"; type PaperclipIngestionPolicyDecision = | { allowed: true; space: WikiSpace } | { allowed: false; space: WikiSpace; reason: "restricted_space" | "archived_space" | "profile_disabled" | "profile_empty"; message: string }; type PaperclipIngestionCandidatesInput = SpaceInput & { query?: string | null; }; export type EnableActiveProjectDistillationResult = { wikiId: string; spaceSlug: string; selectedProjects: Array<{ id: string; name: string; status: string; observedAt: string | null; cursorId: string; }>; eventIngestion: WikiEventIngestionSettings; }; export const DEFAULT_EVENT_INGESTION_SETTINGS: WikiEventIngestionSettings = { enabled: false, sources: { issues: false, comments: false, documents: false, }, wikiId: DEFAULT_WIKI_ID, maxCharacters: 12000, }; export type WikiOverview = { status: "ok"; checkedAt: string; wikiId: string; defaultSpace: WikiSpace; folder: Awaited>; managedAgent: WikiAgentResource; managedProject: WikiProjectResource; managedSkills: WikiSkillResource[]; operationCount: number; eventIngestion: WikiEventIngestionSettings; capabilities: readonly string[]; prompts: { query: string; lint: string; }; }; export type WikiAgentResource = { status: "missing" | "resolved" | "created" | "relinked" | "reset"; source: "managed" | "selected"; agentId: string | null; resourceKey: string; agent: Agent | null; details: { name: string; status: string; adapterType: string | null; icon?: string | null; urlKey?: string | null } | null; defaultDrift?: { entryFile: string; changedFiles: string[] } | null; }; export type WikiProjectResource = { status: "missing" | "resolved" | "created" | "relinked" | "reset"; source: "managed" | "selected"; projectId: string | null; resourceKey: string; project: Project | null; details: { name: string; status: string; color: string | null } | null; }; export type WikiSkillResource = { status: "missing" | "resolved" | "created" | "relinked" | "reset"; skillId: string | null; resourceKey: string; skill: PluginManagedSkillResolution["skill"]; details: { name: string; key: string; description: string | null } | null; defaultDrift?: { changedFiles: string[] } | null; }; export type WikiResourceOption = { id: string; name: string; status?: string | null; adapterType?: string | null; color?: string | null; icon?: string | null; urlKey?: string | null; }; export type WikiSpace = { id: string; companyId: string; wikiId: string; slug: string; displayName: string; spaceType: string; folderMode: string; rootFolderKey: string; pathPrefix: string | null; configuredRootPath: string | null; accessScope: string; ownerUserId: string | null; ownerAgentId: string | null; teamKey: string | null; settings: Record; status: "active" | "archived" | string; createdAt: string | null; updatedAt: string | null; }; export type WikiSpaceWithFolderStatus = WikiSpace & { relativeRoot: string; folder: Awaited>; }; type BootstrapInput = { companyId: string; path?: string | null; }; type SpaceInput = { companyId: string; wikiId?: string | null; spaceSlug?: string | null; }; type CreateSpaceInput = { companyId: string; wikiId?: string | null; slug?: string | null; displayName?: string | null; folderMode?: "managed_subfolder" | "existing_local_folder" | null; accessScope?: "shared" | "personal" | "team" | null; settings?: Record | null; }; type UpdateSpaceInput = SpaceInput & { displayName?: string | null; settings?: Record | null; status?: "active" | "archived" | null; }; type OperationInput = { companyId: string; wikiId?: string | null; spaceSlug?: string | null; operationType: "ingest" | "query" | "lint" | "file-as-page" | "index" | "distill" | "backfill"; title?: string | null; prompt?: string | null; useCheapModelProfile?: boolean; }; type OperationSpaceContext = { wikiId: string; space: WikiSpace; operationType: OperationInput["operationType"]; operationId: string; prompt?: string | null; }; type QuerySessionInput = { companyId: string; wikiId?: string | null; spaceSlug?: string | null; question: string; title?: string | null; }; type CaptureSourceInput = { companyId: string; wikiId?: string | null; spaceSlug?: string | null; sourceType?: string | null; title?: string | null; url?: string | null; contents: string; rawPath?: string | null; metadata?: Record | null; }; type PaperclipSourceBundleInput = { companyId: string; wikiId?: string | null; spaceSlug?: string | null; projectId?: string | null; rootIssueId?: string | null; maxCharacters?: number | null; maxCharactersPerSource?: number | null; backfillStartAt?: string | null; backfillEndAt?: string | null; routineRun?: boolean; includeComments?: boolean; includeDocuments?: boolean; workItemId?: string | null; operationIssueId?: string | null; }; type PaperclipSourceRef = { kind: "issue" | "comment" | "document"; issueId: string; issueIdentifier: string | null; projectId: string | null; title?: string | null; commentId?: string; documentId?: string; documentKey?: string; updatedAt?: string; createdAt?: string; redactionStatus?: "suppressed_sensitive_content"; redactionReasons?: string[]; }; type PaperclipSourceBundle = { markdown: string; sourceRefs: PaperclipSourceRef[]; sourceHash: string; sourceWindowStart: string | null; sourceWindowEnd: string | null; clipped: boolean; warnings: string[]; }; type PaperclipDistillationRunInput = PaperclipSourceBundleInput; type PaperclipDistillationOutcomeInput = { companyId: string; wikiId?: string | null; spaceSlug?: string | null; runId: string; cursorId?: string | null; status: "succeeded" | "failed" | "review_required"; sourceHash?: string | null; sourceWindowEnd?: string | null; warning?: string | null; costCents?: number | null; retryCount?: number | null; }; type PaperclipDistillationWorkItemInput = { companyId: string; wikiId?: string | null; spaceSlug?: string | null; kind: PaperclipDistillationWorkItemKind; projectId?: string | null; rootIssueId?: string | null; requestedByIssueId?: string | null; priority?: "critical" | "high" | "medium" | "low" | null; idempotencyKey?: string | null; metadata?: Record | null; }; type PaperclipProjectPageDistillationInput = PaperclipSourceBundleInput & { autoApply?: boolean; expectedProjectPageHash?: string | null; includeSupportingPages?: boolean; }; type WritePageInput = { companyId: string; wikiId?: string | null; spaceSlug?: string | null; path: string; contents: string; expectedHash?: string | null; summary?: string | null; sourceRefs?: unknown; operationId?: string | null; writer?: "agent_tool" | "board_ui" | "plugin_internal"; }; type FileQueryAnswerInput = { companyId: string; wikiId?: string | null; spaceSlug?: string | null; querySessionId?: string | null; question?: string | null; answer?: string | null; path: string; title?: string | null; contents?: string | null; expectedHash?: string | null; }; type ToolParams = Record; type WikiResourceKind = "agent" | "project"; type PaperclipDistillationPatchOperation = | "standup_update" | "project_page_distill" | "decision_distill" | "history_distill" | "index_refresh" | "log_append"; type PaperclipDistillationPatch = { pagePath: string; operationType: PaperclipDistillationPatchOperation; currentHash: string | null; proposedHash: string; proposedContents: string; sourceHash: string; sourceRefs: PaperclipSourceRef[]; cursorWindow: { start: string | null; end: string | null; }; confidence: "high" | "medium" | "low"; warnings: string[]; humanReviewRequired: boolean; }; type PaperclipEventIngestResult = | { status: "skipped"; reason: "disabled" | "source_disabled" | "unsupported_event" | "missing_issue" | "missing_comment" | "missing_document" | "plugin_operation" | "already_ingested" } | { status: "recorded"; sourceKind: WikiEventIngestionSource; sourceId: string; cursorId: string; issueId: string }; type WikiResourceBinding = { resolvedId: string | null; metadata: Record; }; function stringField(value: unknown): string | null { return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; } function requireString(value: unknown, name: string): string { const field = stringField(value); if (!field) throw new Error(`${name} is required`); return field; } function normalizeWikiId(value: unknown): string { return stringField(value) ?? DEFAULT_WIKI_ID; } export function normalizeSpaceSlug(value: unknown): string { const raw = stringField(value) ?? DEFAULT_SPACE_SLUG; const normalized = raw .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); if (!normalized) throw new Error("spaceSlug is required"); if (normalized.length > 64) throw new Error("spaceSlug must be 64 characters or fewer"); return normalized; } async function requirePaperclipIngestionPolicy( ctx: PluginContext, input: { companyId: string; wikiId: string; spaceSlug?: string | null }, purpose: PaperclipIngestionPolicyPurpose, options: { requireEnabledProfile?: boolean } = {}, ): Promise { const space = await resolveSpace(ctx, { companyId: input.companyId, wikiId: input.wikiId, spaceSlug: input.spaceSlug, }); const profile = await profileForSpace(ctx, input.companyId, space); const decision = evaluatePaperclipProfilePolicy({ space, profile, purpose, requireEnabledProfile: options.requireEnabledProfile, }); if (!decision.allowed) throw new Error(decision.message); return decision.space; } function assertPaperclipSourceScopePayload(input: { projectId?: string | null; rootIssueId?: string | null }) { if (input.projectId && input.rootIssueId) { throw new Error("Paperclip source scope must specify either projectId or rootIssueId, not both."); } } function assertRequestedCharacterLimit(name: string, value: unknown, max: number) { if (value == null) return; if (typeof value !== "number" || !Number.isFinite(value) || value < 1) { throw new Error(`${name} must be a positive number.`); } if (Math.floor(value) > max) { throw new Error(`${name} exceeds the hard Paperclip ingestion cap of ${max} characters.`); } } function stableSpaceId(input: { companyId: string; wikiId: string; slug: string }): string { const hex = createHash("md5") .update(`${input.companyId}:${input.wikiId}:${input.slug}`) .digest("hex"); return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`; } function normalizeLimit(value: unknown, fallback: number, max: number): number { if (typeof value !== "number" || !Number.isFinite(value)) return fallback; return Math.max(1, Math.min(max, Math.floor(value))); } function contentHash(contents: string): string { return createHash("sha256").update(contents, "utf8").digest("hex"); } function byteLength(contents: string): number { return Buffer.byteLength(contents, "utf8"); } function slugify(value: string): string { const slug = value .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); return slug || "source"; } function jsonParam(value: unknown): string { return JSON.stringify(value ?? {}); } function jsonArrayParam(value: unknown): string { return JSON.stringify(Array.isArray(value) ? value : []); } function isoString(value: unknown): string | null { if (value instanceof Date) return value.toISOString(); if (typeof value === "string" && value.trim().length > 0) { const parsed = new Date(value); return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString(); } return null; } function normalizeBoolean(value: unknown, fallback: boolean): boolean { return typeof value === "boolean" ? value : fallback; } function normalizeMaxSourceBytes(value: unknown): number { if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_MAX_SOURCE_BYTES; return Math.max(1, Math.floor(value)); } function normalizeBundleLimit(value: unknown, fallback: number): number { if (typeof value !== "number" || !Number.isFinite(value)) return fallback; return Math.max(1000, Math.floor(value)); } function normalizeCostRate(value: unknown): number { if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_PAPERCLIP_COST_CENTS_PER_1K_CHARS; return Math.max(0, value); } type PaperclipDistillationLimits = { maxCharacters: number; maxCharactersPerSource: number; maxRoutineRunCharacters: number; costCentsPerThousandSourceCharacters: number; }; const DISTILLATION_REDACTED_VALUE = "***REDACTED***"; const DISTILLATION_JSON_SECRET_FIELD_TEXT_RE = /((?:"|')?(?:api[-_]?key|access[-_]?token|auth(?:_?token)?|authorization|bearer|secret|passwd|password|credential|jwt|private[-_]?key|cookie|connectionstring)(?:"|')?\s*:\s*(?:"|'))[^"'`\r\n]+((?:"|'))/gi; const DISTILLATION_ESCAPED_JSON_SECRET_FIELD_TEXT_RE = /((?:\\")?(?:api[-_]?key|access[-_]?token|auth(?:_?token)?|authorization|bearer|secret|passwd|password|credential|jwt|private[-_]?key|cookie|connectionstring)(?:\\")?\s*:\s*(?:\\"))[^\\\r\n]+((?:\\"))/gi; const DISTILLATION_ENV_SECRET_ASSIGNMENT_RE = /(\b[A-Za-z0-9_]*(?:TOKEN|KEY|SECRET|PASSWORD|PASSWD|AUTHORIZATION|JWT)[A-Za-z0-9_]*\s*=\s*)[^\s"'`]+/gi; const DISTILLATION_AUTHORIZATION_BEARER_RE = /(\bAuthorization\s*:\s*Bearer\s+)[^\s"'`]+/gi; const DISTILLATION_OPENAI_KEY_RE = /\bsk-[A-Za-z0-9_-]{12,}\b/g; const DISTILLATION_GITHUB_TOKEN_RE = /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g; const DISTILLATION_JWT_RE = /\b[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}(?:\.[A-Za-z0-9_-]{8,})?\b/g; const DISTILLATION_CONNECTION_STRING_RE = /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqps?):\/\/[^\s"'`]+/gi; const DISTILLATION_PRIVATE_KEY_BLOCK_RE = /-----BEGIN(?:[A-Z ]+)?PRIVATE KEY-----[\s\S]*?-----END(?:[A-Z ]+)?PRIVATE KEY-----/gi; const DISTILLATION_PRIVATE_KEY_BLOCK_TEST_RE = /-----BEGIN(?:[A-Z ]+)?PRIVATE KEY-----[\s\S]*?-----END(?:[A-Z ]+)?PRIVATE KEY-----/i; type DistillationSourceProtectionResult = { body: string; warning: string | null; refPatch: Pick; }; function redactDistillationSensitiveText(input: string): string { return input .replace(DISTILLATION_PRIVATE_KEY_BLOCK_RE, DISTILLATION_REDACTED_VALUE) .replace(DISTILLATION_JSON_SECRET_FIELD_TEXT_RE, `$1${DISTILLATION_REDACTED_VALUE}$2`) .replace(DISTILLATION_ESCAPED_JSON_SECRET_FIELD_TEXT_RE, `$1${DISTILLATION_REDACTED_VALUE}$2`) .replace(DISTILLATION_ENV_SECRET_ASSIGNMENT_RE, `$1${DISTILLATION_REDACTED_VALUE}`) .replace(DISTILLATION_AUTHORIZATION_BEARER_RE, `$1${DISTILLATION_REDACTED_VALUE}`) .replace(DISTILLATION_CONNECTION_STRING_RE, DISTILLATION_REDACTED_VALUE) .replace(DISTILLATION_OPENAI_KEY_RE, DISTILLATION_REDACTED_VALUE) .replace(DISTILLATION_GITHUB_TOKEN_RE, DISTILLATION_REDACTED_VALUE) .replace(DISTILLATION_JWT_RE, DISTILLATION_REDACTED_VALUE); } function protectDistillationSourceBody(input: { issue: Issue; sourceKind: "comment" | "document"; sourceId: string; body: string; }): DistillationSourceProtectionResult { const redactedBody = redactDistillationSensitiveText(input.body); const reasons = [ DISTILLATION_PRIVATE_KEY_BLOCK_TEST_RE.test(input.body) ? "private_key_block" : null, redactedBody !== input.body ? "secret_like_token" : null, ].filter((reason): reason is string => Boolean(reason)); if (reasons.length === 0) { return { body: input.body, warning: null, refPatch: {}, }; } return { body: [ `[Suppressed by LLM Wiki distillation security policy for this ${input.sourceKind}.]`, "", `- Source ID: ${input.sourceId}`, `- Redaction reasons: ${reasons.join(", ")}`, "- Review the original Paperclip source directly if a human needs the unredacted material.", ].join("\n"), warning: `Suppressed ${input.sourceKind} content for ${sourceTitleForIssue(input.issue)} / ${input.sourceId}: ${reasons.join(", ")}.`, refPatch: { redactionStatus: "suppressed_sensitive_content", redactionReasons: reasons, }, }; } async function resolvePaperclipDistillationLimits( ctx: PluginContext, input: Pick, ): Promise { assertRequestedCharacterLimit("maxCharacters", input.maxCharacters, DEFAULT_MAX_PAPERCLIP_CURSOR_WINDOW_CHARS); assertRequestedCharacterLimit("maxCharactersPerSource", input.maxCharactersPerSource, DEFAULT_MAX_PAPERCLIP_ISSUE_SOURCE_CHARS); const config = await ctx.config.get() as Record; const maxCharactersPerSource = Math.min( normalizeBundleLimit(input.maxCharactersPerSource, DEFAULT_MAX_PAPERCLIP_ISSUE_SOURCE_CHARS), normalizeBundleLimit(config.maxPaperclipIssueSourceCharacters, DEFAULT_MAX_PAPERCLIP_ISSUE_SOURCE_CHARS), ); const cursorWindowCap = normalizeBundleLimit( config.maxPaperclipCursorWindowCharacters, DEFAULT_MAX_PAPERCLIP_CURSOR_WINDOW_CHARS, ); const routineRunCap = normalizeBundleLimit( config.maxPaperclipRoutineRunCharacters, DEFAULT_MAX_PAPERCLIP_ROUTINE_RUN_CHARS, ); const requestedMaxCharacters = normalizeBundleLimit(input.maxCharacters, cursorWindowCap); const hardCharacterCap = input.routineRun ? Math.min(cursorWindowCap, routineRunCap) : cursorWindowCap; return { maxCharacters: Math.min(requestedMaxCharacters, hardCharacterCap), maxCharactersPerSource, maxRoutineRunCharacters: routineRunCap, costCentsPerThousandSourceCharacters: normalizeCostRate(config.paperclipCostCentsPerThousandSourceCharacters), }; } async function resolvePaperclipDistillationLimitsForSpace( ctx: PluginContext, input: Pick & { space: WikiSpace }, ): Promise { const [base, profile] = await Promise.all([ resolvePaperclipDistillationLimits(ctx, input), profileForSpace(ctx, input.companyId, input.space), ]); return { ...base, maxCharacters: Math.min(base.maxCharacters, profile.cursor.maxWindowCharacters), maxCharactersPerSource: Math.min(base.maxCharactersPerSource, profile.cursor.maxCharactersPerSource), }; } function estimateSourceCostCents(characters: number, costCentsPerThousandSourceCharacters: number): number { if (characters <= 0 || costCentsPerThousandSourceCharacters <= 0) return 0; return Math.ceil((characters / 1000) * costCentsPerThousandSourceCharacters); } async function assertSourceWithinConfiguredLimit(ctx: PluginContext, contents: string) { const config = await ctx.config.get(); const maxSourceBytes = normalizeMaxSourceBytes(config.maxSourceBytes); const sourceBytes = byteLength(contents); if (sourceBytes > maxSourceBytes) { throw new Error(`Source content is ${sourceBytes} bytes, which exceeds the configured LLM Wiki source limit of ${maxSourceBytes} bytes.`); } } function normalizeEventIngestionSettings(value: unknown): WikiEventIngestionSettings { if (!value || typeof value !== "object" || Array.isArray(value)) { return { ...DEFAULT_EVENT_INGESTION_SETTINGS, sources: { ...DEFAULT_EVENT_INGESTION_SETTINGS.sources } }; } const record = value as Record; const sources = record.sources && typeof record.sources === "object" && !Array.isArray(record.sources) ? record.sources as Record : {}; const maxCharacters = typeof record.maxCharacters === "number" && Number.isFinite(record.maxCharacters) ? Math.max(1000, Math.min(MAX_EVENT_SOURCE_CHARS, Math.floor(record.maxCharacters))) : DEFAULT_EVENT_INGESTION_SETTINGS.maxCharacters; return { enabled: normalizeBoolean(record.enabled, DEFAULT_EVENT_INGESTION_SETTINGS.enabled), sources: { issues: normalizeBoolean(sources.issues, DEFAULT_EVENT_INGESTION_SETTINGS.sources.issues), comments: normalizeBoolean(sources.comments, DEFAULT_EVENT_INGESTION_SETTINGS.sources.comments), documents: normalizeBoolean(sources.documents, DEFAULT_EVENT_INGESTION_SETTINGS.sources.documents), }, wikiId: normalizeWikiId(record.wikiId), maxCharacters, }; } function defaultPaperclipIngestionProfile(input: { space: Pick; legacySettings?: WikiEventIngestionSettings | null; }): PaperclipIngestionProfileV1 { const legacy = input.space.slug === DEFAULT_SPACE_SLUG ? input.legacySettings : null; return { version: 1, enabled: legacy?.enabled ?? false, sourceScopes: legacy?.enabled ? [{ kind: "company_all", requiresBoardConfirmation: true }] : [], sourceKinds: { issues: legacy?.sources.issues ?? true, comments: legacy?.sources.comments ?? true, documents: legacy?.sources.documents ?? true, attachments: "off", workProducts: "off", }, cursor: { maxWindowCharacters: DEFAULT_MAX_PAPERCLIP_CURSOR_WINDOW_CHARS, maxCharactersPerSource: DEFAULT_MAX_PAPERCLIP_ISSUE_SOURCE_CHARS, minSourceAgeMinutes: 15, maxWindowsPerRun: 6, staleAfterHours: 72, }, backfill: { defaultStartAt: null, defaultEndAt: null, requireManualQueue: true, }, }; } function stringArray(value: unknown): string[] { if (!Array.isArray(value)) return []; return [...new Set(value.map((item) => stringField(item)).filter((item): item is string => Boolean(item)))]; } function normalizePaperclipIngestionSourceScope(value: unknown): PaperclipIngestionSourceScope | null { if (!value || typeof value !== "object" || Array.isArray(value)) return null; const record = value as Record; const kind = stringField(record.kind); if (kind === "active_projects") { const statuses = Array.isArray(record.statuses) ? record.statuses.filter((status): status is "in_progress" | "todo" | "done" => status === "in_progress" || status === "todo" || status === "done") : undefined; return { kind, limit: normalizeLimit(record.limit, 3, MAX_PAPERCLIP_PROFILE_SELECTED_PROJECTS), ...(statuses && statuses.length > 0 ? { statuses: [...new Set(statuses)] } : {}), }; } if (kind === "selected_projects") { return { kind, projectIds: stringArray(record.projectIds).slice(0, MAX_PAPERCLIP_PROFILE_SELECTED_PROJECTS) }; } if (kind === "root_issues") { return { kind, issueIds: stringArray(record.issueIds).slice(0, MAX_PAPERCLIP_PROFILE_ROOT_ISSUES) }; } if (kind === "company_all") { return { kind, requiresBoardConfirmation: true }; } return null; } function normalizePaperclipIngestionProfile( value: unknown, input: { space: Pick; legacySettings?: WikiEventIngestionSettings | null }, ): PaperclipIngestionProfileV1 { const fallback = defaultPaperclipIngestionProfile(input); if (!value || typeof value !== "object" || Array.isArray(value)) return fallback; const record = value as Record; const sourceKinds = record.sourceKinds && typeof record.sourceKinds === "object" && !Array.isArray(record.sourceKinds) ? record.sourceKinds as Record : {}; const cursor = record.cursor && typeof record.cursor === "object" && !Array.isArray(record.cursor) ? record.cursor as Record : {}; const backfill = record.backfill && typeof record.backfill === "object" && !Array.isArray(record.backfill) ? record.backfill as Record : {}; return { version: 1, enabled: normalizeBoolean(record.enabled, fallback.enabled), sourceScopes: Array.isArray(record.sourceScopes) ? record.sourceScopes.map(normalizePaperclipIngestionSourceScope).filter((scope): scope is PaperclipIngestionSourceScope => Boolean(scope)) : fallback.sourceScopes, sourceKinds: { issues: normalizeBoolean(sourceKinds.issues, fallback.sourceKinds.issues), comments: normalizeBoolean(sourceKinds.comments, fallback.sourceKinds.comments), documents: normalizeBoolean(sourceKinds.documents, fallback.sourceKinds.documents), attachments: sourceKinds.attachments === "metadata_only" ? "metadata_only" : "off", workProducts: sourceKinds.workProducts === "metadata_only" ? "metadata_only" : "off", }, cursor: { maxWindowCharacters: normalizeLimit(cursor.maxWindowCharacters, fallback.cursor.maxWindowCharacters, DEFAULT_MAX_PAPERCLIP_CURSOR_WINDOW_CHARS), maxCharactersPerSource: normalizeLimit(cursor.maxCharactersPerSource, fallback.cursor.maxCharactersPerSource, DEFAULT_MAX_PAPERCLIP_ISSUE_SOURCE_CHARS), minSourceAgeMinutes: normalizeLimit(cursor.minSourceAgeMinutes, fallback.cursor.minSourceAgeMinutes, 24 * 60), maxWindowsPerRun: normalizeLimit(cursor.maxWindowsPerRun, fallback.cursor.maxWindowsPerRun, 25), staleAfterHours: normalizeLimit(cursor.staleAfterHours, fallback.cursor.staleAfterHours, 24 * 30), }, backfill: { defaultStartAt: isoString(backfill.defaultStartAt), defaultEndAt: isoString(backfill.defaultEndAt), requireManualQueue: normalizeBoolean(backfill.requireManualQueue, fallback.backfill.requireManualQueue), }, }; } async function profileForSpace(ctx: PluginContext, companyId: string, space: WikiSpace): Promise { const legacySettings = space.slug === DEFAULT_SPACE_SLUG ? await getEventIngestionSettings(ctx, companyId) : null; return normalizePaperclipIngestionProfile(space.settings.paperclipIngestion, { space, legacySettings }); } function eventIngestionStateKey(companyId: string) { return { scopeKind: "company" as const, scopeId: companyId, namespace: EVENT_INGESTION_STATE_NAMESPACE, stateKey: EVENT_INGESTION_STATE_KEY, }; } function eventIngestionDedupKey(companyId: string, wikiId: string, spaceId: string, sourceKind: WikiEventIngestionSource, sourceId: string) { return { scopeKind: "company" as const, scopeId: companyId, namespace: EVENT_INGESTION_DEDUP_NAMESPACE, stateKey: `${wikiId}:${spaceId}:${sourceKind}:${sourceId}`, }; } export async function getEventIngestionSettings(ctx: PluginContext, companyId: string): Promise { return normalizeEventIngestionSettings(await ctx.state.get(eventIngestionStateKey(companyId))); } function evaluatePaperclipProfilePolicy(input: { space: WikiSpace; profile?: PaperclipIngestionProfileV1 | null; purpose: PaperclipIngestionPolicyPurpose; requireEnabledProfile?: boolean; }): PaperclipIngestionPolicyDecision { const { space, profile, purpose } = input; if (space.status !== "active") { return { allowed: false, space, reason: "archived_space", message: `Paperclip ingestion policy denied ${purpose}: space "${space.slug}" is ${space.status}.`, }; } if (space.accessScope !== "shared") { return { allowed: false, space, reason: "restricted_space", message: `Paperclip ingestion policy denied ${purpose}: ${space.accessScope} spaces cannot ingest Paperclip sources until host permissions are enforced.`, }; } if (input.requireEnabledProfile && space.slug !== DEFAULT_SPACE_SLUG && !profile?.enabled) { return { allowed: false, space, reason: "profile_disabled", message: `Paperclip ingestion policy denied ${purpose}: Paperclip ingestion is not enabled for space "${space.slug}".`, }; } if (input.requireEnabledProfile && space.slug !== DEFAULT_SPACE_SLUG && profile?.enabled && profile.sourceScopes.length === 0) { return { allowed: false, space, reason: "profile_empty", message: `Paperclip ingestion policy denied ${purpose}: space "${space.slug}" has no source scopes configured.`, }; } return { allowed: true, space }; } export async function getPaperclipIngestionProfile( ctx: PluginContext, input: { companyId: string; wikiId?: string | null; spaceSlug?: string | null }, ): Promise { const wikiId = normalizeWikiId(input.wikiId); const space = await resolveSpace(ctx, { companyId: input.companyId, wikiId, spaceSlug: input.spaceSlug }); const profile = await profileForSpace(ctx, input.companyId, space); const policy = evaluatePaperclipProfilePolicy({ space, profile, purpose: "profile_read" }); const historicalPageCount = await countPaperclipHistoricalPages(ctx, { companyId: input.companyId, wikiId, spaceId: space.id, }); const overlapCount = await countPaperclipProfileOverlaps(ctx, { companyId: input.companyId, wikiId, space, profile, }); const effectiveState: PaperclipIngestionProfileEffectiveState = !policy.allowed ? "policy_blocked" : profile.enabled && profile.sourceScopes.length === 0 ? "enabled_no_scopes" : profile.enabled ? "enabled" : "disabled"; return { wikiId, space: { id: space.id, slug: space.slug, displayName: space.displayName, accessScope: space.accessScope, status: space.status, }, profile, effectiveState, policyBlocks: policy.allowed ? [] : [policy.message], historicalPageCount, overlapCount, }; } async function countPaperclipHistoricalPages(ctx: PluginContext, input: { companyId: string; wikiId: string; spaceId: string }): Promise { const rows = await ctx.db.query<{ count: string | number }>( `SELECT count(*)::text AS count FROM ${pageBindingTable(ctx)} WHERE company_id = $1 AND wiki_id = $2 AND space_id = $3`, [input.companyId, input.wikiId, input.spaceId], ); return Number(rows[0]?.count ?? 0) || 0; } function scopeIdentity(scope: PaperclipIngestionSourceScope): string[] { if (scope.kind === "active_projects") return [`active_projects:${scope.limit}`]; if (scope.kind === "selected_projects") return scope.projectIds.map((id) => `project:${id}`); if (scope.kind === "root_issues") return scope.issueIds.map((id) => `root_issue:${id}`); return ["company_all"]; } async function countPaperclipProfileOverlaps(ctx: PluginContext, input: { companyId: string; wikiId: string; space: WikiSpace; profile: PaperclipIngestionProfileV1; }): Promise { if (!input.profile.enabled || input.profile.sourceScopes.length === 0) return 0; const own = new Set(input.profile.sourceScopes.flatMap(scopeIdentity)); if (own.size === 0) return 0; const { spaces } = await listSpaces(ctx, { companyId: input.companyId, wikiId: input.wikiId }); let overlaps = 0; for (const space of spaces) { if (space.id === input.space.id) continue; const profile = await profileForSpace(ctx, input.companyId, space); if (!profile.enabled) continue; for (const key of profile.sourceScopes.flatMap(scopeIdentity)) { if (own.has(key)) overlaps += 1; } } return overlaps; } async function validatePaperclipIngestionProfile(ctx: PluginContext, input: { companyId: string; space: WikiSpace; profile: PaperclipIngestionProfileV1; }) { const policy = evaluatePaperclipProfilePolicy({ space: input.space, profile: input.profile, purpose: "profile_update", requireEnabledProfile: input.profile.enabled && input.space.slug !== DEFAULT_SPACE_SLUG, }); if (!policy.allowed) throw new Error(policy.message); if (input.profile.enabled && input.profile.sourceScopes.length === 0) { throw new Error("Paperclip ingestion profile must include at least one source scope before it can be enabled."); } if (input.profile.sourceScopes.length > MAX_PAPERCLIP_INGESTION_PROFILE_SOURCE_COUNT) { throw new Error(`Paperclip ingestion profile sources exceed the hard cap of ${MAX_PAPERCLIP_INGESTION_PROFILE_SOURCE_COUNT}.`); } for (const scope of input.profile.sourceScopes) { if (scope.kind === "company_all" && input.space.slug !== DEFAULT_SPACE_SLUG) { throw new Error("Everything in the company is only available on the default wiki space."); } if (scope.kind === "selected_projects") { if (scope.projectIds.length > MAX_PAPERCLIP_PROFILE_SELECTED_PROJECTS) { throw new Error(`selected_projects exceeds the hard cap of ${MAX_PAPERCLIP_PROFILE_SELECTED_PROJECTS}.`); } for (const projectId of scope.projectIds) { const project = await ctx.projects.get(projectId, input.companyId); if (!project) throw new Error(`Project belongs to another company or does not exist: ${projectId}`); } } if (scope.kind === "root_issues") { if (scope.issueIds.length > MAX_PAPERCLIP_PROFILE_ROOT_ISSUES) { throw new Error(`root_issues exceeds the hard cap of ${MAX_PAPERCLIP_PROFILE_ROOT_ISSUES}.`); } for (const issueId of scope.issueIds) { const issue = await ctx.issues.get(issueId, input.companyId); if (!issue) throw new Error(`Issue belongs to another company or does not exist: ${issueId}`); } } } } export async function updatePaperclipIngestionProfile(ctx: PluginContext, input: { companyId: string; wikiId?: string | null; spaceSlug?: string | null; profile: unknown; }): Promise { const wikiId = normalizeWikiId(input.wikiId); const space = await resolveSpace(ctx, { companyId: input.companyId, wikiId, spaceSlug: input.spaceSlug }); const current = await profileForSpace(ctx, input.companyId, space); const profile = normalizePaperclipIngestionProfile(input.profile, { space, legacySettings: space.slug === DEFAULT_SPACE_SLUG ? await getEventIngestionSettings(ctx, input.companyId) : null }); await validatePaperclipIngestionProfile(ctx, { companyId: input.companyId, space, profile }); await updateSpace(ctx, { companyId: input.companyId, wikiId, spaceSlug: space.slug, settings: { paperclipIngestion: profile }, }); if (space.slug === DEFAULT_SPACE_SLUG) { await ctx.state.set(eventIngestionStateKey(input.companyId), { enabled: profile.enabled, wikiId, maxCharacters: profile.cursor.maxCharactersPerSource, sources: { issues: profile.sourceKinds.issues, comments: profile.sourceKinds.comments, documents: profile.sourceKinds.documents, }, }); } await ctx.activity.log({ companyId: input.companyId, message: `Updated Paperclip ingestion profile for ${space.displayName}`, entityType: "llm_wiki_space", entityId: space.id, metadata: { type: "plugin.llm_wiki.paperclip_ingestion_profile_updated", wikiId, spaceSlug: space.slug, beforeEnabled: current.enabled, afterEnabled: profile.enabled, sourceScopeKinds: profile.sourceScopes.map((scope) => scope.kind), sourceKinds: profile.sourceKinds, cursor: profile.cursor, }, }); return getPaperclipIngestionProfile(ctx, { companyId: input.companyId, wikiId, spaceSlug: space.slug }); } export async function listPaperclipIngestionCandidates(ctx: PluginContext, input: PaperclipIngestionCandidatesInput): Promise<{ projects: Array<{ id: string; name: string; status: string; updatedAt: string | null }>; rootIssues: Array<{ id: string; identifier: string | null; title: string; status: string; projectId: string | null }>; }> { const wikiId = normalizeWikiId(input.wikiId); await requirePaperclipIngestionPolicy(ctx, { companyId: input.companyId, wikiId, spaceSlug: input.spaceSlug }, "candidate_search"); const query = stringField(input.query)?.toLowerCase() ?? ""; const projects = (await ctx.projects.list({ companyId: input.companyId, limit: 200 })) .filter((project) => !project.archivedAt) .filter((project) => !query || project.name.toLowerCase().includes(query)) .slice(0, 50) .map((project) => ({ id: project.id, name: project.name, status: project.status, updatedAt: isoString(project.updatedAt), })); const issues = (await ctx.issues.list({ companyId: input.companyId, includePluginOperations: false, limit: 200, })) .filter((issue) => !issue.parentId) .filter((issue) => !query || issue.title.toLowerCase().includes(query) || issue.identifier?.toLowerCase().includes(query)) .slice(0, 50) .map((issue) => ({ id: issue.id, identifier: issue.identifier ?? null, title: issue.title, status: issue.status, projectId: issue.projectId ?? null, })); return { projects, rootIssues: issues }; } export async function updateEventIngestionSettings( ctx: PluginContext, input: { companyId: string; settings: WikiEventIngestionSettingsUpdate }, ): Promise { await requirePaperclipIngestionPolicy(ctx, { companyId: input.companyId, wikiId: normalizeWikiId(input.settings.wikiId), spaceSlug: DEFAULT_SPACE_SLUG, }, "profile_update"); const sourceKeys = Object.keys(input.settings.sources ?? {}); if (sourceKeys.length > MAX_PAPERCLIP_INGESTION_PROFILE_SOURCE_COUNT) { throw new Error(`Paperclip ingestion profile sources exceed the hard cap of ${MAX_PAPERCLIP_INGESTION_PROFILE_SOURCE_COUNT}.`); } assertRequestedCharacterLimit("maxCharacters", input.settings.maxCharacters, MAX_EVENT_SOURCE_CHARS); const current = await getEventIngestionSettings(ctx, input.companyId); const next = normalizeEventIngestionSettings({ ...current, ...input.settings, sources: { ...current.sources, ...(input.settings.sources ?? {}), }, }); await ctx.state.set(eventIngestionStateKey(input.companyId), next); const defaultSpace = await ensureDefaultSpace(ctx, { companyId: input.companyId, wikiId: next.wikiId }); const profile = normalizePaperclipIngestionProfile( { ...defaultPaperclipIngestionProfile({ space: defaultSpace, legacySettings: next }), enabled: next.enabled, sourceKinds: { issues: next.sources.issues, comments: next.sources.comments, documents: next.sources.documents, attachments: "off", workProducts: "off", }, cursor: { ...defaultPaperclipIngestionProfile({ space: defaultSpace, legacySettings: next }).cursor, maxCharactersPerSource: next.maxCharacters, }, }, { space: defaultSpace, legacySettings: next }, ); await updateSpace(ctx, { companyId: input.companyId, wikiId: next.wikiId, spaceSlug: DEFAULT_SPACE_SLUG, settings: { paperclipIngestion: profile }, }); return next; } function assertWikiPath(path: string, options: { allowMetadata?: boolean } = {}): string { const trimmed = path.trim().replace(/^\/+/, ""); if ( !trimmed || trimmed.includes("\\") || trimmed.split("/").some((segment) => segment === "" || segment === "." || segment === "..") ) { throw new Error(`Invalid wiki path: ${path}`); } if ( trimmed !== ".gitignore" && trimmed !== "WIKI.md" && trimmed !== "AGENTS.md" && trimmed !== "IDEA.md" && trimmed !== "index.md" && trimmed !== "log.md" && !trimmed.startsWith("raw/") && !trimmed.startsWith("wiki/") && !(options.allowMetadata && trimmed.startsWith(".paperclip/")) ) { throw new Error(`Wiki path must stay inside AGENTS.md, IDEA.md, raw/, or wiki/: ${path}`); } return trimmed; } function assertPagePath(path: string): string { const normalized = assertWikiPath(path); if (normalized !== "index.md" && normalized !== "log.md" && normalized !== "WIKI.md" && normalized !== "AGENTS.md" && normalized !== "IDEA.md" && !normalized.startsWith("wiki/")) { throw new Error(`Wiki page writes must target AGENTS.md, IDEA.md, or wiki/: ${path}`); } if (!normalized.endsWith(".md")) { throw new Error(`Wiki page path must be a markdown file: ${path}`); } return normalized; } function assertPageWriteAllowed(path: string, writer: WritePageInput["writer"] = "agent_tool"): void { if (writer !== "board_ui" && PROTECTED_WIKI_CONTROL_FILES.has(path)) { throw new Error(`Refusing to overwrite protected wiki control file ${path}; board-managed edits must use the wiki UI.`); } } function assertRawPath(path: string): string { const normalized = assertWikiPath(path); if (!normalized.startsWith("raw/")) { throw new Error(`Source path must stay inside raw/: ${path}`); } return normalized; } function tableName(namespace: string, table: string): string { return `${namespace}.${table}`; } function spaceTable(ctx: PluginContext): string { return tableName(ctx.db.namespace, "wiki_spaces"); } function bindingTable(ctx: PluginContext): string { return tableName(ctx.db.namespace, "wiki_resource_bindings"); } function distillationCursorTable(ctx: PluginContext): string { return tableName(ctx.db.namespace, "paperclip_distillation_cursors"); } function distillationRunTable(ctx: PluginContext): string { return tableName(ctx.db.namespace, "paperclip_distillation_runs"); } function sourceSnapshotTable(ctx: PluginContext): string { return tableName(ctx.db.namespace, "paperclip_source_snapshots"); } function distillationWorkItemTable(ctx: PluginContext): string { return tableName(ctx.db.namespace, "paperclip_distillation_work_items"); } function pageBindingTable(ctx: PluginContext): string { return tableName(ctx.db.namespace, "paperclip_page_bindings"); } function parseBindingMetadata(value: unknown): Record { if (value && typeof value === "object" && !Array.isArray(value)) return value as Record; if (typeof value === "string") { try { const parsed = JSON.parse(value); return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as Record : {}; } catch { return {}; } } return {}; } function parseJsonObject(value: unknown): Record { if (value && typeof value === "object" && !Array.isArray(value)) return value as Record; if (typeof value === "string") { try { const parsed = JSON.parse(value); return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as Record : {}; } catch { return {}; } } return {}; } type WikiSpaceRow = { id: string; company_id: string; wiki_id: string; slug: string; display_name: string; space_type: string; folder_mode: string; root_folder_key: string; path_prefix: string | null; configured_root_path: string | null; access_scope: string; owner_user_id: string | null; owner_agent_id: string | null; team_key: string | null; settings: unknown; status: string; created_at: string | null; updated_at: string | null; }; function wikiSpaceFromRow(row: WikiSpaceRow): WikiSpace { return { id: row.id, companyId: row.company_id, wikiId: row.wiki_id, slug: row.slug, displayName: row.display_name, spaceType: row.space_type, folderMode: row.folder_mode, rootFolderKey: row.root_folder_key, pathPrefix: row.path_prefix, configuredRootPath: row.configured_root_path, accessScope: row.access_scope, ownerUserId: row.owner_user_id, ownerAgentId: row.owner_agent_id, teamKey: row.team_key, settings: parseJsonObject(row.settings), status: row.status, createdAt: row.created_at, updatedAt: row.updated_at, }; } function fallbackDefaultSpace(input: { companyId: string; wikiId: string }): WikiSpace { return { id: stableSpaceId({ companyId: input.companyId, wikiId: input.wikiId, slug: DEFAULT_SPACE_SLUG }), companyId: input.companyId, wikiId: input.wikiId, slug: DEFAULT_SPACE_SLUG, displayName: DEFAULT_SPACE_SLUG, spaceType: "local_folder", folderMode: "managed_subfolder", rootFolderKey: WIKI_ROOT_FOLDER_KEY, pathPrefix: null, configuredRootPath: null, accessScope: "shared", ownerUserId: null, ownerAgentId: null, teamKey: null, settings: {}, status: "active", createdAt: null, updatedAt: null, }; } export async function ensureDefaultSpace(ctx: PluginContext, input: { companyId: string; wikiId?: string | null }): Promise { const wikiId = normalizeWikiId(input.wikiId); const id = stableSpaceId({ companyId: input.companyId, wikiId, slug: DEFAULT_SPACE_SLUG }); await ctx.db.execute( `INSERT INTO ${spaceTable(ctx)} AS wiki_spaces (id, company_id, wiki_id, slug, display_name, space_type, folder_mode, root_folder_key, path_prefix, access_scope, status, settings) VALUES ($1, $2, $3, 'default', 'default', 'local_folder', 'managed_subfolder', $4, NULL, 'shared', 'active', '{}'::jsonb) ON CONFLICT (company_id, wiki_id, slug) DO UPDATE SET updated_at = wiki_spaces.updated_at`, [id, input.companyId, wikiId, WIKI_ROOT_FOLDER_KEY], ); const rows = await ctx.db.query( `SELECT id, company_id, wiki_id, slug, display_name, space_type, folder_mode, root_folder_key, path_prefix, configured_root_path, access_scope, owner_user_id, owner_agent_id, team_key, settings, status, created_at::text AS created_at, updated_at::text AS updated_at FROM ${spaceTable(ctx)} WHERE company_id = $1 AND wiki_id = $2 AND slug = 'default' LIMIT 1`, [input.companyId, wikiId], ); return rows[0] ? wikiSpaceFromRow(rows[0]) : fallbackDefaultSpace({ companyId: input.companyId, wikiId }); } export async function resolveSpace(ctx: PluginContext, input: SpaceInput): Promise { const wikiId = normalizeWikiId(input.wikiId); const slug = normalizeSpaceSlug(input.spaceSlug); if (slug === DEFAULT_SPACE_SLUG) { return ensureDefaultSpace(ctx, { companyId: input.companyId, wikiId }); } const rows = await ctx.db.query( `SELECT id, company_id, wiki_id, slug, display_name, space_type, folder_mode, root_folder_key, path_prefix, configured_root_path, access_scope, owner_user_id, owner_agent_id, team_key, settings, status, created_at::text AS created_at, updated_at::text AS updated_at FROM ${spaceTable(ctx)} WHERE company_id = $1 AND wiki_id = $2 AND slug = $3 AND status <> 'archived' LIMIT 1`, [input.companyId, wikiId, slug], ); if (!rows[0]) throw new Error(`LLM Wiki space not found: ${slug}`); return wikiSpaceFromRow(rows[0]); } async function resolveSpaceAnyStatus(ctx: PluginContext, input: SpaceInput): Promise { const wikiId = normalizeWikiId(input.wikiId); const slug = normalizeSpaceSlug(input.spaceSlug); if (slug === DEFAULT_SPACE_SLUG) { return ensureDefaultSpace(ctx, { companyId: input.companyId, wikiId }); } const rows = await ctx.db.query( `SELECT id, company_id, wiki_id, slug, display_name, space_type, folder_mode, root_folder_key, path_prefix, configured_root_path, access_scope, owner_user_id, owner_agent_id, team_key, settings, status, created_at::text AS created_at, updated_at::text AS updated_at FROM ${spaceTable(ctx)} WHERE company_id = $1 AND wiki_id = $2 AND slug = $3 LIMIT 1`, [input.companyId, wikiId, slug], ); if (!rows[0]) throw new Error(`LLM Wiki space not found: ${slug}`); return wikiSpaceFromRow(rows[0]); } export async function listSpaces(ctx: PluginContext, input: { companyId: string; wikiId?: string | null }): Promise<{ spaces: WikiSpace[] }> { const wikiId = normalizeWikiId(input.wikiId); await ensureDefaultSpace(ctx, { companyId: input.companyId, wikiId }); const rows = await ctx.db.query( `SELECT id, company_id, wiki_id, slug, display_name, space_type, folder_mode, root_folder_key, path_prefix, configured_root_path, access_scope, owner_user_id, owner_agent_id, team_key, settings, status, created_at::text AS created_at, updated_at::text AS updated_at FROM ${spaceTable(ctx)} WHERE company_id = $1 AND wiki_id = $2 AND status <> 'archived' ORDER BY CASE WHEN slug = 'default' THEN 0 ELSE 1 END, display_name, slug`, [input.companyId, wikiId], ); const spaces = rows.length > 0 ? rows.map(wikiSpaceFromRow) : [fallbackDefaultSpace({ companyId: input.companyId, wikiId })]; return { spaces }; } export async function createSpace(ctx: PluginContext, input: CreateSpaceInput): Promise<{ status: "created"; space: WikiSpace }> { const wikiId = normalizeWikiId(input.wikiId); const displayName = stringField(input.displayName) ?? stringField(input.slug) ?? "New space"; const slug = normalizeSpaceSlug(input.slug ?? displayName); if (slug === DEFAULT_SPACE_SLUG) { return { status: "created", space: await ensureDefaultSpace(ctx, { companyId: input.companyId, wikiId }) }; } const folderMode = input.folderMode ?? "managed_subfolder"; if (folderMode !== "managed_subfolder") { throw new Error("Only managed_subfolder spaces are supported until dynamic local folder bindings are available."); } const accessScope = input.accessScope ?? "shared"; const id = randomUUID(); const pathPrefix = `spaces/${slug}`; await ctx.db.execute( `INSERT INTO ${spaceTable(ctx)} (id, company_id, wiki_id, slug, display_name, space_type, folder_mode, root_folder_key, path_prefix, access_scope, settings, status) VALUES ($1, $2, $3, $4, $5, 'local_folder', $6, $7, $8, $9, $10::jsonb, 'active')`, [ id, input.companyId, wikiId, slug, displayName, folderMode, WIKI_ROOT_FOLDER_KEY, pathPrefix, accessScope, jsonParam(input.settings ?? {}), ], ); const space: WikiSpace = { id, companyId: input.companyId, wikiId, slug, displayName, spaceType: "local_folder", folderMode, rootFolderKey: WIKI_ROOT_FOLDER_KEY, pathPrefix, configuredRootPath: null, accessScope, ownerUserId: null, ownerAgentId: null, teamKey: null, settings: input.settings ?? {}, status: "active", createdAt: null, updatedAt: null, }; await bootstrapSpaceFiles(ctx, input.companyId, space); await upsertWikiInstance(ctx, { companyId: input.companyId, wikiId }); return { status: "created", space }; } export async function updateSpace(ctx: PluginContext, input: UpdateSpaceInput): Promise<{ status: "ok"; space: WikiSpace }> { const nextStatus = input.status ?? null; if (nextStatus !== null && nextStatus !== "active" && nextStatus !== "archived") { throw new Error("LLM Wiki space status must be active or archived."); } const space = nextStatus === "active" ? await resolveSpaceAnyStatus(ctx, input) : await resolveSpace(ctx, input); const nextDisplayName = stringField(input.displayName); if (space.slug === DEFAULT_SPACE_SLUG && nextStatus === "archived") { throw new Error("The default LLM Wiki space cannot be archived."); } await ctx.db.execute( `UPDATE ${spaceTable(ctx)} SET display_name = COALESCE($4, display_name), settings = CASE WHEN $5::jsonb IS NULL THEN settings ELSE settings || $5::jsonb END, status = COALESCE($6, status), updated_at = now() WHERE company_id = $1 AND wiki_id = $2 AND slug = $3`, [ input.companyId, space.wikiId, space.slug, nextDisplayName, input.settings ? jsonParam(input.settings) : null, nextStatus ?? null, ], ); if (nextStatus === "archived") { return { status: "ok", space: { ...space, displayName: nextDisplayName ?? space.displayName, settings: input.settings ? { ...space.settings, ...input.settings } : space.settings, status: "archived", }, }; } return { status: "ok", space: await resolveSpace(ctx, { companyId: input.companyId, wikiId: space.wikiId, spaceSlug: space.slug }) }; } export async function archiveSpace(ctx: PluginContext, input: SpaceInput): Promise<{ status: "archived"; space: WikiSpace }> { const space = await resolveSpace(ctx, input); if (space.slug === DEFAULT_SPACE_SLUG) throw new Error("The default LLM Wiki space cannot be archived."); await ctx.db.execute( `UPDATE ${spaceTable(ctx)} SET status = 'archived', updated_at = now() WHERE company_id = $1 AND wiki_id = $2 AND slug = $3`, [input.companyId, space.wikiId, space.slug], ); return { status: "archived", space: { ...space, status: "archived" } }; } export function spaceRelativePath(space: Pick, path: string): string { const normalized = path.replace(/^\/+/, ""); return space.pathPrefix ? `${space.pathPrefix}/${normalized}` : normalized; } function logicalPathFromSpacePath(space: Pick, path: string): string { if (!space.pathPrefix) return path; const prefix = `${space.pathPrefix}/`; return path.startsWith(prefix) ? path.slice(prefix.length) : path; } export async function spaceFolderStatus(ctx: PluginContext, input: SpaceInput): Promise { const space = await resolveSpace(ctx, input); const folder = await ctx.localFolders.status(input.companyId, WIKI_ROOT_FOLDER_KEY); return { ...space, relativeRoot: space.pathPrefix ?? ".", folder, }; } async function getResourceBinding( ctx: PluginContext, input: { companyId: string; wikiId: string; resourceKind: WikiResourceKind; resourceKey: string }, ): Promise { const rows = await ctx.db.query<{ resolved_id: string | null; metadata: unknown }>( `SELECT resolved_id, metadata FROM ${bindingTable(ctx)} WHERE company_id = $1 AND wiki_id = $2 AND resource_kind = $3 AND resource_key = $4 LIMIT 1`, [input.companyId, input.wikiId, input.resourceKind, input.resourceKey], ); const row = rows[0]; return row ? { resolvedId: row.resolved_id, metadata: parseBindingMetadata(row.metadata) } : null; } async function upsertResourceBinding( ctx: PluginContext, input: { companyId: string; wikiId: string; resourceKind: WikiResourceKind; resourceKey: string; resolvedId: string; metadata?: Record }, ) { await ctx.db.execute( `INSERT INTO ${bindingTable(ctx)} AS wiki_resource_bindings (id, company_id, wiki_id, resource_kind, resource_key, resolved_id, metadata) VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb) ON CONFLICT (company_id, wiki_id, resource_kind, resource_key) DO UPDATE SET resolved_id = EXCLUDED.resolved_id, metadata = EXCLUDED.metadata, updated_at = now()`, [ randomUUID(), input.companyId, input.wikiId, input.resourceKind, input.resourceKey, input.resolvedId, jsonParam(input.metadata ?? {}), ], ); } function agentDetails(agent: Agent | null): WikiAgentResource["details"] { return agent ? { name: agent.name, status: agent.status, adapterType: agent.adapterType ?? null, icon: agent.icon ?? null, urlKey: agent.urlKey ?? null } : null; } function projectDetails(project: Project | null): WikiProjectResource["details"] { return project ? { name: project.name, status: project.status, color: project.color ?? null } : null; } function skillDetails(skill: PluginManagedSkillResolution["skill"]): WikiSkillResource["details"] { return skill ? { name: skill.name, key: skill.key, description: skill.description ?? null } : null; } function agentResource(input: { status: WikiAgentResource["status"]; source: WikiAgentResource["source"]; agent: Agent | null; defaultDrift?: WikiAgentResource["defaultDrift"]; }): WikiAgentResource { return { status: input.status, source: input.source, agentId: input.agent?.id ?? null, resourceKey: `${PLUGIN_ID}:agent:${WIKI_MAINTAINER_AGENT_KEY}`, agent: input.agent, details: agentDetails(input.agent), defaultDrift: input.defaultDrift ?? null, }; } function projectResource(input: { status: WikiProjectResource["status"]; source: WikiProjectResource["source"]; project: Project | null; }): WikiProjectResource { return { status: input.status, source: input.source, projectId: input.project?.id ?? null, resourceKey: `${PLUGIN_ID}:project:${WIKI_PROJECT_KEY}`, project: input.project, details: projectDetails(input.project), }; } function skillResource(resolved: PluginManagedSkillResolution): WikiSkillResource { return { status: resolved.status, skillId: resolved.skillId, resourceKey: resolved.resourceKey, skill: resolved.skill, details: skillDetails(resolved.skill), defaultDrift: resolved.defaultDrift ?? null, }; } async function resolveSelectedAgent(ctx: PluginContext, companyId: string, binding: WikiResourceBinding | null) { if (!binding?.resolvedId) return null; const agent = await ctx.agents.get(binding.resolvedId, companyId); return agent && agent.status !== "terminated" ? agent : null; } async function resolveSelectedProject(ctx: PluginContext, companyId: string, binding: WikiResourceBinding | null) { if (!binding?.resolvedId) return null; return ctx.projects.get(binding.resolvedId, companyId); } function inferTitle(path: string, contents: string): string { const heading = contents.match(/^#\s+(.+)$/m)?.[1]?.trim(); if (heading) return heading; const filename = path.split("/").pop()?.replace(/\.md$/i, "") ?? path; return filename.replace(/[-_]+/g, " "); } function inferPageType(path: string): string | null { if (/^wiki\/projects\/[^/]+\/standup\.md$/.test(path)) return "project-standup"; const match = path.match(/^wiki\/([^/]+)\//); return match?.[1] ?? (path === "index.md" || path === "wiki/index.md" ? "index" : path === "log.md" || path === "wiki/log.md" ? "log" : null); } function extractWikiLinks(contents: string): string[] { const links = new Set(); const markdownLinkPattern = /\[[^\]]+\]\(([^)]+)\)/g; for (const match of contents.matchAll(markdownLinkPattern)) { const target = match[1]?.split("#")[0]?.trim(); if (target && (target.startsWith("wiki/") || target === "index.md" || target === "log.md" || target === "AGENTS.md" || target === "IDEA.md")) { links.add(target); } } const wikiTokenPattern = /\bwiki\/[A-Za-z0-9._/-]+\.md\b/g; for (const match of contents.matchAll(wikiTokenPattern)) { links.add(match[0]); } return [...links].sort(); } async function readCurrentWithHash( ctx: PluginContext, companyId: string, path: string, space: Pick, ): Promise<{ contents: string | null; hash: string | null }> { try { const contents = await ctx.localFolders.readText(companyId, WIKI_ROOT_FOLDER_KEY, spaceRelativePath(space, path)); return { contents, hash: contentHash(contents) }; } catch { return { contents: null, hash: null }; } } async function filterReadableRows( ctx: PluginContext, companyId: string, space: Pick, rows: T[], pathForRow: (row: T) => string, ): Promise { const checks: Array = await Promise.all(rows.map(async (row): Promise => { try { await ctx.localFolders.readText(companyId, WIKI_ROOT_FOLDER_KEY, spaceRelativePath(space, pathForRow(row))); return row; } catch { return null; } })); return checks.filter((row): row is T => row != null); } async function listLocalFiles(ctx: PluginContext, input: { companyId: string; space: Pick; relativePath: "wiki" | "raw" }): Promise { try { const relativePath = spaceRelativePath(input.space, input.relativePath); const listing = await ctx.localFolders.list(input.companyId, WIKI_ROOT_FOLDER_KEY, { relativePath, recursive: true, maxEntries: LOCAL_BROWSE_FILE_LIMIT, }); return listing.entries .filter((entry) => entry.kind === "file") .map((entry) => ({ ...entry, path: logicalPathFromSpacePath(input.space, entry.path) })); } catch { return []; } } function mergeLocalPageRows(pages: WikiPageRow[], entries: PluginLocalFolderEntry[]): WikiPageRow[] { const byPath = new Map(pages.map((page) => [page.path, page])); for (const entry of entries) { if (!entry.path.endsWith(".md") || byPath.has(entry.path)) continue; byPath.set(entry.path, { path: entry.path, title: null, pageType: inferPageType(entry.path), backlinkCount: 0, sourceCount: 0, contentHash: null, updatedAt: entry.modifiedAt ?? new Date(0).toISOString(), }); } return [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path)); } function mergeLocalSourceRows(sources: WikiSourceRow[], entries: PluginLocalFolderEntry[]): WikiSourceRow[] { const byPath = new Map(sources.map((source) => [source.rawPath, source])); for (const entry of entries) { if (!entry.path.endsWith(".md") || byPath.has(entry.path)) continue; byPath.set(entry.path, { rawPath: entry.path, title: null, sourceType: "local_file", url: null, status: "present", createdAt: entry.modifiedAt ?? new Date(0).toISOString(), }); } return [...byPath.values()].sort((a, b) => a.rawPath.localeCompare(b.rawPath)); } function assertExpectedHash(expectedHash: string | null | undefined, currentHash: string | null, path: string): void { if (expectedHash && currentHash && expectedHash !== currentHash) { throw new Error(`Refusing to overwrite ${path}: expected hash ${expectedHash} but current hash is ${currentHash}`); } } async function upsertWikiInstance(ctx: PluginContext, input: { companyId: string; wikiId: string; rootPath?: string | null }) { await ctx.db.execute( `INSERT INTO ${tableName(ctx.db.namespace, "wiki_instances")} AS wiki_instances (id, company_id, wiki_id, root_folder_key, configured_root_path, schema_version, settings, managed_agent_key, managed_project_key) VALUES ($1, $2, $3, $4, $5, 1, '{}'::jsonb, $6, $7) ON CONFLICT (company_id, wiki_id) DO UPDATE SET configured_root_path = COALESCE(EXCLUDED.configured_root_path, wiki_instances.configured_root_path), managed_agent_key = EXCLUDED.managed_agent_key, managed_project_key = EXCLUDED.managed_project_key, updated_at = now()`, [ randomUUID(), input.companyId, input.wikiId, WIKI_ROOT_FOLDER_KEY, input.rootPath ?? null, WIKI_MAINTAINER_AGENT_KEY, WIKI_PROJECT_KEY, ], ); } async function upsertPageMetadata(ctx: PluginContext, input: { companyId: string; wikiId: string; spaceId: string; path: string; contents: string; summary?: string | null; sourceRefs?: unknown; operationId?: string | null; }) { const pageId = randomUUID(); const revisionId = randomUUID(); const hash = contentHash(input.contents); const title = inferTitle(input.path, input.contents); const pageType = inferPageType(input.path); const backlinks = extractWikiLinks(input.contents); const sourceRefs = Array.isArray(input.sourceRefs) ? input.sourceRefs : []; await ctx.db.execute( `INSERT INTO ${tableName(ctx.db.namespace, "wiki_pages")} (id, company_id, wiki_id, space_id, path, title, page_type, frontmatter, source_refs, backlinks, content_hash, current_revision_id) VALUES ($1, $2, $3, $11, $4, $5, $6, '{}'::jsonb, $7::jsonb, $8::jsonb, $9, $10) ON CONFLICT (company_id, wiki_id, space_id, path) DO UPDATE SET title = EXCLUDED.title, page_type = EXCLUDED.page_type, source_refs = EXCLUDED.source_refs, backlinks = EXCLUDED.backlinks, content_hash = EXCLUDED.content_hash, current_revision_id = EXCLUDED.current_revision_id, updated_at = now()`, [ pageId, input.companyId, input.wikiId, input.path, title, pageType, jsonParam(sourceRefs), jsonParam(backlinks), hash, revisionId, input.spaceId, ], ); await ctx.db.execute( `INSERT INTO ${tableName(ctx.db.namespace, "wiki_page_revisions")} (id, company_id, wiki_id, space_id, page_id, operation_id, path, content_hash, summary, metadata) VALUES ($1, $2, $3, $8, (SELECT id FROM ${tableName(ctx.db.namespace, "wiki_pages")} WHERE company_id = $2 AND wiki_id = $3 AND space_id = $8 AND path = $4), $7, $4, $5, $6, '{}'::jsonb)`, [revisionId, input.companyId, input.wikiId, input.path, hash, input.summary ?? null, input.operationId ?? null, input.spaceId], ); return { title, pageType, backlinks, hash, revisionId }; } export async function writeWikiPage(ctx: PluginContext, input: WritePageInput) { const wikiId = normalizeWikiId(input.wikiId); const space = await resolveSpace(ctx, { companyId: input.companyId, wikiId, spaceSlug: input.spaceSlug }); const path = assertPagePath(input.path); assertPageWriteAllowed(path, input.writer); const current = await readCurrentWithHash(ctx, input.companyId, path, space); assertExpectedHash(input.expectedHash, current.hash, path); await ctx.localFolders.writeTextAtomic(input.companyId, WIKI_ROOT_FOLDER_KEY, spaceRelativePath(space, path), input.contents); const metadata = await upsertPageMetadata(ctx, { companyId: input.companyId, wikiId, spaceId: space.id, path, contents: input.contents, summary: input.summary, sourceRefs: input.sourceRefs, operationId: input.operationId, }); await upsertWikiInstance(ctx, { companyId: input.companyId, wikiId }); return { status: "ok", wikiId, spaceSlug: space.slug, path, previousHash: current.hash, ...metadata }; } export async function captureWikiSource(ctx: PluginContext, input: CaptureSourceInput) { const wikiId = normalizeWikiId(input.wikiId); const space = await resolveSpace(ctx, { companyId: input.companyId, wikiId, spaceSlug: input.spaceSlug }); const title = stringField(input.title) ?? "Untitled source"; await assertSourceWithinConfiguredLimit(ctx, input.contents); const hash = contentHash(input.contents); const rawPath = input.rawPath ? assertRawPath(input.rawPath) : assertRawPath(`raw/${new Date().toISOString().slice(0, 10)}-${slugify(title)}-${hash.slice(0, 8)}.md`); await ctx.localFolders.writeTextAtomic(input.companyId, WIKI_ROOT_FOLDER_KEY, spaceRelativePath(space, rawPath), input.contents); await upsertWikiInstance(ctx, { companyId: input.companyId, wikiId }); const sourceId = randomUUID(); await ctx.db.execute( `INSERT INTO ${tableName(ctx.db.namespace, "wiki_sources")} (id, company_id, wiki_id, space_id, source_type, title, url, raw_path, content_hash, status, metadata) VALUES ($1, $2, $3, $10, $4, $5, $6, $7, $8, 'captured', $9::jsonb)`, [ sourceId, input.companyId, wikiId, stringField(input.sourceType) ?? "text", title, stringField(input.url), rawPath, hash, jsonParam(input.metadata ?? {}), space.id, ], ); return { status: "ok", sourceId, wikiId, spaceSlug: space.slug, rawPath, hash, title }; } export async function getOverview(ctx: PluginContext, companyId: string): Promise { const [defaultSpace, folder, managedAgent, managedProject, managedSkills] = await Promise.all([ ensureDefaultSpace(ctx, { companyId, wikiId: DEFAULT_WIKI_ID }), ctx.localFolders.status(companyId, WIKI_ROOT_FOLDER_KEY), resolveWikiAgentResource(ctx, companyId), resolveWikiProjectResource(ctx, companyId), resolveWikiSkillResources(ctx, companyId), ]); const operationRows = await ctx.db.query<{ count: string }>( `SELECT count(*)::text AS count FROM ${tableName(ctx.db.namespace, "wiki_operations")} WHERE company_id = $1`, [companyId], ); const operationCount = Number(operationRows[0]?.count ?? 0); const eventIngestion = await getEventIngestionSettings(ctx, companyId); return { status: "ok", checkedAt: new Date().toISOString(), wikiId: DEFAULT_WIKI_ID, defaultSpace, folder, managedAgent, managedProject, managedSkills, operationCount, eventIngestion, capabilities: ctx.manifest.capabilities, prompts: { query: QUERY_PROMPT, lint: LINT_PROMPT, }, }; } export async function resolveWikiAgentResource( ctx: PluginContext, companyId: string, options: { reconcileMissing?: boolean } = {}, ): Promise { const wikiId = DEFAULT_WIKI_ID; const binding = await getResourceBinding(ctx, { companyId, wikiId, resourceKind: "agent", resourceKey: WIKI_MAINTAINER_AGENT_KEY, }); const selectedAgent = await resolveSelectedAgent(ctx, companyId, binding); if (selectedAgent) { const source = binding?.metadata.source === "managed-default" ? "managed" : "selected"; const managedResolution = source === "managed" ? await ctx.agents.managed.get(WIKI_MAINTAINER_AGENT_KEY, companyId) : null; return agentResource({ status: "resolved", source, agent: selectedAgent, defaultDrift: managedResolution?.defaultDrift ?? null, }); } if (binding?.resolvedId && !options.reconcileMissing) { return agentResource({ status: "missing", source: "selected", agent: null }); } const resolved = options.reconcileMissing ? await ctx.agents.managed.reconcile(WIKI_MAINTAINER_AGENT_KEY, companyId) : await ctx.agents.managed.get(WIKI_MAINTAINER_AGENT_KEY, companyId); if (resolved.agentId && options.reconcileMissing) { await upsertResourceBinding(ctx, { companyId, wikiId, resourceKind: "agent", resourceKey: WIKI_MAINTAINER_AGENT_KEY, resolvedId: resolved.agentId, metadata: { source: "managed-default", updatedBy: "resolve" }, }); } return agentResource({ status: resolved.status, source: "managed", agent: resolved.agent, defaultDrift: resolved.defaultDrift ?? null }); } export async function resolveWikiProjectResource( ctx: PluginContext, companyId: string, options: { reconcileMissing?: boolean } = {}, ): Promise { const wikiId = DEFAULT_WIKI_ID; const binding = await getResourceBinding(ctx, { companyId, wikiId, resourceKind: "project", resourceKey: WIKI_PROJECT_KEY, }); const selectedProject = await resolveSelectedProject(ctx, companyId, binding); if (selectedProject) { return projectResource({ status: "resolved", source: binding?.metadata.source === "managed-default" ? "managed" : "selected", project: selectedProject, }); } if (binding?.resolvedId && !options.reconcileMissing) { return projectResource({ status: "missing", source: "selected", project: null }); } const resolved = options.reconcileMissing ? await ctx.projects.managed.reconcile(WIKI_PROJECT_KEY, companyId) : await ctx.projects.managed.get(WIKI_PROJECT_KEY, companyId); if (resolved.projectId && options.reconcileMissing) { await upsertResourceBinding(ctx, { companyId, wikiId, resourceKind: "project", resourceKey: WIKI_PROJECT_KEY, resolvedId: resolved.projectId, metadata: { source: "managed-default", updatedBy: "resolve" }, }); } return projectResource({ status: resolved.status, source: "managed", project: resolved.project }); } export async function resolveWikiSkillResources( ctx: PluginContext, companyId: string, options: { reconcileMissing?: boolean } = {}, ): Promise { return Promise.all( WIKI_MANAGED_SKILL_KEYS.map(async (skillKey) => { const resolved = options.reconcileMissing ? await ctx.skills.managed.reconcile(skillKey, companyId) : await ctx.skills.managed.get(skillKey, companyId); return skillResource(resolved); }), ); } export async function reconcileWikiAgentResource(ctx: PluginContext, companyId: string): Promise { const resolved = await ctx.agents.managed.reconcile(WIKI_MAINTAINER_AGENT_KEY, companyId); if (resolved.agentId) { await upsertResourceBinding(ctx, { companyId, wikiId: DEFAULT_WIKI_ID, resourceKind: "agent", resourceKey: WIKI_MAINTAINER_AGENT_KEY, resolvedId: resolved.agentId, metadata: { source: "managed-default", updatedBy: "reconcile" }, }); } return agentResource({ status: resolved.status, source: "managed", agent: resolved.agent, defaultDrift: resolved.defaultDrift ?? null }); } export async function resetWikiAgentResource(ctx: PluginContext, companyId: string): Promise { const resolved = await ctx.agents.managed.reset(WIKI_MAINTAINER_AGENT_KEY, companyId); if (resolved.agentId) { await upsertResourceBinding(ctx, { companyId, wikiId: DEFAULT_WIKI_ID, resourceKind: "agent", resourceKey: WIKI_MAINTAINER_AGENT_KEY, resolvedId: resolved.agentId, metadata: { source: "managed-default", updatedBy: "reset" }, }); } return agentResource({ status: resolved.status, source: "managed", agent: resolved.agent, defaultDrift: resolved.defaultDrift ?? null }); } export async function selectWikiAgentResource(ctx: PluginContext, input: { companyId: string; agentId: string }): Promise { const agent = await ctx.agents.get(input.agentId, input.companyId); if (!agent || agent.status === "terminated") { throw new Error("Selected Wiki Maintainer agent was not found or is terminated."); } await upsertResourceBinding(ctx, { companyId: input.companyId, wikiId: DEFAULT_WIKI_ID, resourceKind: "agent", resourceKey: WIKI_MAINTAINER_AGENT_KEY, resolvedId: agent.id, metadata: { source: "selected-existing", updatedBy: "settings" }, }); return agentResource({ status: "resolved", source: "selected", agent }); } export async function reconcileWikiProjectResource(ctx: PluginContext, companyId: string): Promise { const resolved = await ctx.projects.managed.reconcile(WIKI_PROJECT_KEY, companyId); if (resolved.projectId) { await upsertResourceBinding(ctx, { companyId, wikiId: DEFAULT_WIKI_ID, resourceKind: "project", resourceKey: WIKI_PROJECT_KEY, resolvedId: resolved.projectId, metadata: { source: "managed-default", updatedBy: "reconcile" }, }); } return projectResource({ status: resolved.status, source: "managed", project: resolved.project }); } export async function resetWikiProjectResource(ctx: PluginContext, companyId: string): Promise { const resolved = await ctx.projects.managed.reset(WIKI_PROJECT_KEY, companyId); if (resolved.projectId) { await upsertResourceBinding(ctx, { companyId, wikiId: DEFAULT_WIKI_ID, resourceKind: "project", resourceKey: WIKI_PROJECT_KEY, resolvedId: resolved.projectId, metadata: { source: "managed-default", updatedBy: "reset" }, }); } return projectResource({ status: resolved.status, source: "managed", project: resolved.project }); } export async function reconcileWikiSkillResources(ctx: PluginContext, companyId: string): Promise { return resolveWikiSkillResources(ctx, companyId, { reconcileMissing: true }); } export async function resetWikiSkillResources(ctx: PluginContext, companyId: string): Promise { return Promise.all( WIKI_MANAGED_SKILL_KEYS.map(async (skillKey) => { return skillResource(await ctx.skills.managed.reset(skillKey, companyId)); }), ); } export async function reconcileWikiRoutineResources( ctx: PluginContext, companyId: string, ): Promise<{ managedAgent: WikiAgentResource; managedProject: WikiProjectResource; managedRoutines: PluginManagedRoutineResolution[]; }> { const [managedAgent, managedProject] = await Promise.all([ reconcileWikiAgentResource(ctx, companyId), reconcileWikiProjectResource(ctx, companyId), ]); const managedRoutines = await Promise.all( WIKI_MAINTENANCE_ROUTINE_KEYS.map((routineKey) => ctx.routines.managed.reconcile(routineKey, companyId, { assigneeAgentId: managedAgent.agentId, projectId: managedProject.projectId, })), ); return { managedAgent, managedProject, managedRoutines }; } export async function selectWikiProjectResource(ctx: PluginContext, input: { companyId: string; projectId: string }): Promise { const project = await ctx.projects.get(input.projectId, input.companyId); if (!project) { throw new Error("Selected LLM Wiki project was not found."); } await upsertResourceBinding(ctx, { companyId: input.companyId, wikiId: DEFAULT_WIKI_ID, resourceKind: "project", resourceKey: WIKI_PROJECT_KEY, resolvedId: project.id, metadata: { source: "selected-existing", updatedBy: "settings" }, }); return projectResource({ status: "resolved", source: "selected", project }); } export async function listWikiAgentOptions(ctx: PluginContext, companyId: string): Promise { const agents = await ctx.agents.list({ companyId, limit: 200 }); return agents .filter((agent) => agent.status !== "terminated") .map((agent) => ({ id: agent.id, name: agent.name, status: agent.status, adapterType: agent.adapterType ?? null, icon: agent.icon ?? null, urlKey: agent.urlKey ?? null, })); } export async function listWikiProjectOptions(ctx: PluginContext, companyId: string): Promise { const projects = await ctx.projects.list({ companyId, limit: 200 }); return projects.map((project) => ({ id: project.id, name: project.name, status: project.status, color: project.color ?? null })); } export async function bootstrapWikiRoot(ctx: PluginContext, input: BootstrapInput) { const wikiId = DEFAULT_WIKI_ID; const defaultSpace = await ensureDefaultSpace(ctx, { companyId: input.companyId, wikiId }); const configureFolder = (path: string) => ctx.localFolders.configure({ companyId: input.companyId, folderKey: WIKI_ROOT_FOLDER_KEY, path, access: "readWrite", requiredDirectories: [...REQUIRED_WIKI_DIRECTORIES], requiredFiles: [...REQUIRED_WIKI_FILES], }); const currentFolder = input.path ? null : await ctx.localFolders.status(input.companyId, WIKI_ROOT_FOLDER_KEY); const folder = input.path ? await configureFolder(input.path) : currentFolder?.configured && currentFolder.path ? await configureFolder(currentFolder.path) : currentFolder ?? await ctx.localFolders.status(input.companyId, WIKI_ROOT_FOLDER_KEY); const writtenFiles: string[] = []; const preservedFiles: string[] = []; for (const file of BOOTSTRAP_FILES) { const path = assertWikiPath(file.path, { allowMetadata: true }); try { await ctx.localFolders.readText(input.companyId, WIKI_ROOT_FOLDER_KEY, path); preservedFiles.push(path); continue; } catch { // Missing files are initialized below. Existing files are intentionally preserved. } await ctx.localFolders.writeTextAtomic(input.companyId, WIKI_ROOT_FOLDER_KEY, file.path, file.contents); writtenFiles.push(path); } await upsertWikiInstance(ctx, { companyId: input.companyId, wikiId, rootPath: folder.path }); const managedSkills = await reconcileWikiSkillResources(ctx, input.companyId); const [managedAgent, managedProject] = await Promise.all([ reconcileWikiAgentResource(ctx, input.companyId), reconcileWikiProjectResource(ctx, input.companyId), ]); await ctx.state.set( { scopeKind: "company", scopeId: input.companyId, namespace: "llm-wiki", stateKey: "last-bootstrap", }, { at: new Date().toISOString(), path: folder.path }, ); return { status: "ok", folder, wikiId, space: defaultSpace, managedAgent, managedProject, managedSkills, writtenFiles, preservedFiles, }; } export async function bootstrapSpace(ctx: PluginContext, input: SpaceInput) { const wikiId = normalizeWikiId(input.wikiId); const space = await resolveSpace(ctx, { companyId: input.companyId, wikiId, spaceSlug: input.spaceSlug }); const { writtenFiles, preservedFiles } = await bootstrapSpaceFiles(ctx, input.companyId, space); await upsertWikiInstance(ctx, { companyId: input.companyId, wikiId }); return { status: "ok", wikiId, space, writtenFiles, preservedFiles, }; } async function bootstrapSpaceFiles(ctx: PluginContext, companyId: string, space: WikiSpace) { const writtenFiles: string[] = []; const preservedFiles: string[] = []; for (const file of BOOTSTRAP_FILES) { const path = assertWikiPath(file.path, { allowMetadata: true }); const physicalPath = spaceRelativePath(space, path); try { await ctx.localFolders.readText(companyId, WIKI_ROOT_FOLDER_KEY, physicalPath); preservedFiles.push(path); continue; } catch { // Missing files are initialized below. Existing files are intentionally preserved. } await ctx.localFolders.writeTextAtomic(companyId, WIKI_ROOT_FOLDER_KEY, physicalPath, file.contents); writtenFiles.push(path); } return { writtenFiles, preservedFiles }; } function operationSpaceRoot(space: WikiSpace): string { return space.pathPrefix ? `${space.rootFolderKey}/${space.pathPrefix}` : `${space.rootFolderKey} root`; } function operationBillingContext(wikiId: string, space: WikiSpace): string { return space.slug === DEFAULT_SPACE_SLUG ? `plugin-llm-wiki:${wikiId} (space ${space.slug})` : `plugin-llm-wiki:${wikiId}:${space.slug}`; } function operationBillingCode(wikiId: string, space: WikiSpace): string { return space.slug === DEFAULT_SPACE_SLUG ? `plugin-llm-wiki:${wikiId}` : `plugin-llm-wiki:${wikiId}:${space.slug}`; } function operationIssueOriginId(input: { wikiId: string; space: WikiSpace; operationId: string }): string { return input.space.slug === DEFAULT_SPACE_SLUG ? `wiki:${input.wikiId}:operation:${input.operationId}` : `wiki:${input.wikiId}:space:${input.space.slug}:operation:${input.operationId}`; } function operationTitleWithSpace(title: string, space: WikiSpace): string { return `${title} [space: ${space.displayName} / ${space.slug}]`; } function operationPromptWithSpaceContext(input: OperationSpaceContext): string { const paperclipDerived = input.operationType === "distill" || input.operationType === "backfill"; return [ `Plugin operation: ${input.operationType}`, `Wiki ID: ${input.wikiId}`, `Space: ${input.space.displayName} (${input.space.slug})`, `Space root: ${operationSpaceRoot(input.space)}`, `Billing context: ${operationBillingContext(input.wikiId, input.space)}`, "", "Space isolation requirement:", `- Pass wikiId \`${input.wikiId}\` and spaceSlug \`${input.space.slug}\` on every LLM Wiki tool call.`, "- Treat all paths in the prompt as relative to this space root.", paperclipDerived ? "- Paperclip-derived distill/backfill operations are default-space-only in Phase 1. Stop and comment if asked to write Paperclip-derived pages into a non-default space." : "- Manual ingest, query, lint, index, and file-as-page operations follow the named destination space. Do not cross into another space unless the operation explicitly asks for a multi-space sweep.", "", input.prompt ?? "Created by the LLM Wiki plugin.", ].join("\n"); } function operationMetadata(input: OperationSpaceContext) { return { operationType: input.operationType, operationId: input.operationId, wikiId: input.wikiId, spaceId: input.space.id, spaceSlug: input.space.slug, spaceName: input.space.displayName, spaceRootFolderKey: input.space.rootFolderKey, spacePathPrefix: input.space.pathPrefix, spaceRoot: operationSpaceRoot(input.space), }; } export async function createOperationIssue(ctx: PluginContext, input: OperationInput) { const wikiId = normalizeWikiId(input.wikiId); const space = input.operationType === "distill" || input.operationType === "backfill" ? await requirePaperclipIngestionPolicy(ctx, { companyId: input.companyId, wikiId, spaceSlug: input.spaceSlug }, "queue", { requireEnabledProfile: true }) : await resolveSpace(ctx, { companyId: input.companyId, wikiId, spaceSlug: input.spaceSlug }); const managedAgent = await resolveWikiAgentResource(ctx, input.companyId, { reconcileMissing: true }); const managedProject = await resolveWikiProjectResource(ctx, input.companyId, { reconcileMissing: true }); const operationId = randomUUID(); const title = operationTitleWithSpace(input.title ?? `LLM Wiki ${input.operationType}`, space); const originId = operationIssueOriginId({ wikiId, space, operationId }); const operationContext = { wikiId, space, operationType: input.operationType, operationId, prompt: input.prompt }; const assignableAgentId = managedAgent.agent && managedAgent.agent.status !== "pending_approval" && managedAgent.agent.status !== "terminated" ? managedAgent.agent.id : undefined; const issue = await ctx.issues.create({ companyId: input.companyId, projectId: managedProject.projectId ?? undefined, title, description: operationPromptWithSpaceContext(operationContext), status: "todo", priority: input.operationType === "query" ? "medium" : "low", assigneeAgentId: assignableAgentId, assigneeAdapterOverrides: input.useCheapModelProfile ? { modelProfile: "cheap" } : null, billingCode: operationBillingCode(wikiId, space), surfaceVisibility: "plugin_operation", originKind: `${OPERATION_ORIGIN_KIND}:${input.operationType}` as PluginIssueOriginKind, originId, }); await ctx.db.execute( `INSERT INTO ${tableName(ctx.db.namespace, "wiki_operations")} (id, company_id, wiki_id, space_id, operation_type, status, hidden_issue_id, project_id, run_ids, cost_cents, warnings, metadata) VALUES ($1, $2, $3, $8, $4, $5, $6, $7, '[]'::jsonb, 0, '[]'::jsonb, $9::jsonb)`, [ operationId, input.companyId, wikiId, input.operationType, "queued", issue.id, issue.projectId ?? null, space.id, jsonParam({ ...operationMetadata(operationContext), issueOriginId: originId, billingCode: operationBillingCode(wikiId, space), }), ], ); return { operationId, wikiId, spaceSlug: space.slug, issue }; } function isLlmWikiOperationIssue(issue: Issue): boolean { return typeof issue.originKind === "string" && issue.originKind.startsWith(OPERATION_ORIGIN_KIND); } function paperclipDistillationScope(input: { projectId?: string | null; rootIssueId?: string | null }): PaperclipDistillationScope { if (input.rootIssueId) return "root_issue"; if (input.projectId) return "project"; return "company"; } function paperclipCursorScopeMetadata(input: { projectId?: string | null; rootIssueId?: string | null }) { const sourceScope = paperclipDistillationScope(input); const projectId = sourceScope === "project" ? input.projectId ?? null : null; const rootIssueId = sourceScope === "root_issue" ? input.rootIssueId ?? null : null; return { sourceScope, scopeKey: rootIssueId ?? projectId ?? "company", projectId, rootIssueId, }; } async function upsertPaperclipDistillationCursor(ctx: PluginContext, input: { companyId: string; wikiId: string; spaceId: string; projectId?: string | null; rootIssueId?: string | null; observedAt?: string | null; metadata?: Record; }): Promise { const cursorId = randomUUID(); const scope = paperclipCursorScopeMetadata(input); await ctx.db.execute( `INSERT INTO ${distillationCursorTable(ctx)} AS paperclip_distillation_cursors (id, company_id, wiki_id, space_id, source_scope, scope_key, project_id, root_issue_id, source_kind, last_observed_at, pending_event_count, metadata) VALUES ($1, $2, $3, $11, $4, $5, $6, $7, 'paperclip_issue_history', $8::timestamptz, $9, $10::jsonb) ON CONFLICT (company_id, wiki_id, space_id, source_scope, scope_key, source_kind) DO UPDATE SET last_observed_at = GREATEST( COALESCE(paperclip_distillation_cursors.last_observed_at, EXCLUDED.last_observed_at), COALESCE(EXCLUDED.last_observed_at, paperclip_distillation_cursors.last_observed_at) ), pending_event_count = paperclip_distillation_cursors.pending_event_count + EXCLUDED.pending_event_count, metadata = paperclip_distillation_cursors.metadata || EXCLUDED.metadata, updated_at = now()`, [ cursorId, input.companyId, input.wikiId, scope.sourceScope, scope.scopeKey, scope.projectId, scope.rootIssueId, input.observedAt ?? null, input.observedAt ? 1 : 0, jsonParam(input.metadata ?? {}), input.spaceId, ], ); const rows = await ctx.db.query<{ id: string }>( `SELECT id FROM ${distillationCursorTable(ctx)} WHERE company_id = $1 AND wiki_id = $2 AND space_id = $3 AND source_scope = $4 AND scope_key = $5 AND source_kind = 'paperclip_issue_history' LIMIT 1`, [input.companyId, input.wikiId, input.spaceId, scope.sourceScope, scope.scopeKey], ); return rows[0]?.id ?? cursorId; } function isActiveDistillationProject(project: Project): boolean { if (project.status !== "in_progress") return false; if (project.archivedAt) return false; if (project.managedByPlugin?.pluginKey === PLUGIN_ID) return false; if (project.managedByPlugin?.resourceKey === WIKI_PROJECT_KEY) return false; return true; } function projectActivityTimestamp(project: Project): string { return isoString(project.updatedAt) ?? new Date().toISOString(); } export async function enableActiveProjectDistillation(ctx: PluginContext, input: { companyId: string; wikiId?: string | null; spaceSlug?: string | null; limit?: number | null; }): Promise { const wikiId = normalizeWikiId(input.wikiId); const space = await requirePaperclipIngestionPolicy(ctx, { companyId: input.companyId, wikiId, spaceSlug: input.spaceSlug }, "candidate_search", { requireEnabledProfile: true }); if (typeof input.limit === "number" && Number.isFinite(input.limit) && Math.floor(input.limit) > MAX_PAPERCLIP_DISTILLATION_FAN_OUT) { throw new Error(`Paperclip ingestion fan-out exceeds the hard cap of ${MAX_PAPERCLIP_DISTILLATION_FAN_OUT} enabled profiles.`); } const limit = normalizeLimit(input.limit ?? 3, 3, 25); const projects = await ctx.projects.list({ companyId: input.companyId, limit: 200 }); const activeProjects = projects .filter(isActiveDistillationProject) .sort((a, b) => projectActivityTimestamp(b).localeCompare(projectActivityTimestamp(a))) .slice(0, limit); const selectedProjects: EnableActiveProjectDistillationResult["selectedProjects"] = []; for (const project of activeProjects) { const observedAt = projectActivityTimestamp(project); const cursorId = await upsertPaperclipDistillationCursor(ctx, { companyId: input.companyId, wikiId, spaceId: space.id, projectId: project.id, rootIssueId: null, observedAt, metadata: { configuredBy: "enable-active-projects", projectName: project.name, projectStatus: project.status, }, }); selectedProjects.push({ id: project.id, name: project.name, status: project.status, observedAt, cursorId, }); } const eventIngestion = await updateEventIngestionSettings(ctx, { companyId: input.companyId, settings: { enabled: true, wikiId, sources: { issues: true, comments: true, documents: true, }, }, }); return { wikiId, spaceSlug: space.slug, selectedProjects, eventIngestion, }; } function appendBoundedSection(input: { lines: string[]; title: string; body: string; refs: PaperclipSourceRef[]; ref: PaperclipSourceRef; remaining: { value: number }; perSourceLimit: number; warnings: string[]; }) { if (input.remaining.value <= 0) { input.warnings.push(`Skipped ${input.title}: source bundle character limit reached.`); return; } const boundedBody = input.body.length > input.perSourceLimit ? `${input.body.slice(0, input.perSourceLimit)}\n\n[Clipped at ${input.perSourceLimit} characters for this source.]` : input.body; const section = [`## ${input.title}`, "", boundedBody.trim() || "_No content._", ""].join("\n"); const clippedSection = section.length > input.remaining.value ? `${section.slice(0, input.remaining.value)}\n\n[Source bundle clipped at configured limit.]\n` : section; input.lines.push(clippedSection); input.refs.push(input.ref); if (boundedBody.length !== input.body.length || clippedSection.length !== section.length) { input.warnings.push(`Clipped ${input.title}.`); } input.remaining.value -= clippedSection.length; } function issueSortKey(issue: Issue): string { return `${issue.identifier ?? ""}:${issue.title}:${issue.id}`; } function sourceRefUpdatedAt(ref: PaperclipSourceRef): string | null { return ref.updatedAt ?? ref.createdAt ?? null; } function issueInBackfillWindow(issue: Issue, input: Pick): boolean { const issueUpdatedAt = isoString(issue.updatedAt); if (!issueUpdatedAt) return true; const startAt = isoString(input.backfillStartAt); const endAt = isoString(input.backfillEndAt); if (startAt && issueUpdatedAt < startAt) return false; if (endAt && issueUpdatedAt > endAt) return false; return true; } async function listPaperclipBundleIssues(ctx: PluginContext, input: PaperclipSourceBundleInput): Promise { const filterAndSort = (issues: Issue[]) => issues .filter((issue) => !isLlmWikiOperationIssue(issue)) .filter((issue) => issueInBackfillWindow(issue, input)) .sort((a, b) => issueSortKey(a).localeCompare(issueSortKey(b))); if (input.rootIssueId) { const subtree = await ctx.issues.getSubtree(input.rootIssueId, input.companyId, { includeRoot: true, includeRelations: true, includeDocuments: true, includeAssignees: true, }); return filterAndSort(subtree.issues); } const issues = await ctx.issues.list({ companyId: input.companyId, projectId: input.projectId ?? undefined, includePluginOperations: false, limit: 500, }); return filterAndSort(issues); } export async function assemblePaperclipSourceBundle(ctx: PluginContext, input: PaperclipSourceBundleInput): Promise { const wikiId = normalizeWikiId(input.wikiId); assertPaperclipSourceScopePayload(input); const space = await requirePaperclipIngestionPolicy(ctx, { companyId: input.companyId, wikiId, spaceSlug: input.spaceSlug }, "execute", { requireEnabledProfile: true }); const limits = await resolvePaperclipDistillationLimitsForSpace(ctx, { ...input, space }); const maxCharacters = limits.maxCharacters; const perSourceLimit = limits.maxCharactersPerSource; const includeComments = input.includeComments !== false; const includeDocuments = input.includeDocuments !== false; const issues = await listPaperclipBundleIssues(ctx, input); const scope = paperclipCursorScopeMetadata(input); const sourceRefs: PaperclipSourceRef[] = []; const warnings: string[] = []; const lines = [ `# Paperclip source bundle`, "", "## Bundle Metadata", "", `- Company ID: ${input.companyId}`, `- Wiki ID: ${wikiId}`, `- Space: ${space.displayName} (${space.slug})`, `- Source scope: ${scope.sourceScope}`, scope.projectId ? `- Project ID: ${scope.projectId}` : null, scope.rootIssueId ? `- Root issue ID: ${scope.rootIssueId}` : null, input.backfillStartAt ? `- Backfill start: ${isoString(input.backfillStartAt) ?? input.backfillStartAt}` : null, input.backfillEndAt ? `- Backfill end: ${isoString(input.backfillEndAt) ?? input.backfillEndAt}` : null, `- Issue count: ${issues.length}`, `- Source caps: ${maxCharacters} characters per window; ${perSourceLimit} characters per source`, "", ].filter((line): line is string => line !== null); const remaining = { value: maxCharacters - lines.join("\n").length }; for (const issue of issues) { const issueBody = [ `- Issue ID: ${issue.id}`, issue.identifier ? `- Identifier: ${issue.identifier}` : null, `- Status: ${issue.status}`, `- Priority: ${issue.priority}`, issue.parentId ? `- Parent issue ID: ${issue.parentId}` : null, issue.projectId ? `- Project ID: ${issue.projectId}` : null, `- Updated at: ${isoString(issue.updatedAt) ?? "unknown"}`, "", issue.description?.trim() ? issue.description.trim() : "_No issue description._", ].filter((line): line is string => line !== null).join("\n"); appendBoundedSection({ lines, title: `Issue: ${sourceTitleForIssue(issue)}`, body: issueBody, refs: sourceRefs, ref: { kind: "issue", issueId: issue.id, issueIdentifier: issue.identifier ?? null, projectId: issue.projectId ?? null, title: issue.title, updatedAt: isoString(issue.updatedAt) ?? undefined, }, remaining, perSourceLimit, warnings, }); if (includeDocuments && remaining.value > 0) { const documentSummaries = await ctx.issues.documents.list(issue.id, input.companyId); for (const summary of [...documentSummaries].sort((a, b) => a.key.localeCompare(b.key))) { const document = await ctx.issues.documents.get(issue.id, summary.key, input.companyId); if (!document) continue; const protectedDocument = protectDistillationSourceBody({ issue, sourceKind: "document", sourceId: document.key, body: document.body, }); if (protectedDocument.warning) warnings.push(protectedDocument.warning); appendBoundedSection({ lines, title: `Document: ${sourceTitleForIssue(issue)} / ${document.key}`, body: [ `- Issue ID: ${issue.id}`, issue.identifier ? `- Issue identifier: ${issue.identifier}` : null, `- Document ID: ${document.id}`, `- Document key: ${document.key}`, `- Revision: ${document.latestRevisionNumber}`, `- Updated at: ${isoString(document.updatedAt) ?? "unknown"}`, "", protectedDocument.body, ].filter((line): line is string => line !== null).join("\n"), refs: sourceRefs, ref: { kind: "document", issueId: issue.id, issueIdentifier: issue.identifier ?? null, projectId: issue.projectId ?? null, documentId: document.id, documentKey: document.key, updatedAt: isoString(document.updatedAt) ?? undefined, ...protectedDocument.refPatch, }, remaining, perSourceLimit, warnings, }); } } if (includeComments && remaining.value > 0) { const comments = await ctx.issues.listComments(issue.id, input.companyId); for (const comment of [...comments].sort((a, b) => (isoString(a.createdAt) ?? "").localeCompare(isoString(b.createdAt) ?? ""))) { const protectedComment = protectDistillationSourceBody({ issue, sourceKind: "comment", sourceId: comment.id, body: comment.body, }); if (protectedComment.warning) warnings.push(protectedComment.warning); appendBoundedSection({ lines, title: `Comment: ${sourceTitleForIssue(issue)} / ${comment.id}`, body: [ `- Issue ID: ${issue.id}`, issue.identifier ? `- Issue identifier: ${issue.identifier}` : null, `- Comment ID: ${comment.id}`, `- Created at: ${isoString(comment.createdAt) ?? "unknown"}`, "", protectedComment.body, ].filter((line): line is string => line !== null).join("\n"), refs: sourceRefs, ref: { kind: "comment", issueId: issue.id, issueIdentifier: issue.identifier ?? null, projectId: issue.projectId ?? null, commentId: comment.id, createdAt: isoString(comment.createdAt) ?? undefined, ...protectedComment.refPatch, }, remaining, perSourceLimit, warnings, }); } } } const markdown = lines.join("\n").slice(0, maxCharacters); const sourceDates = sourceRefs.map(sourceRefUpdatedAt).filter((date): date is string => Boolean(date)).sort(); return { markdown, sourceRefs, sourceHash: contentHash(markdown), sourceWindowStart: sourceDates[0] ?? null, sourceWindowEnd: sourceDates[sourceDates.length - 1] ?? null, clipped: warnings.some((warning) => warning.includes("Clipped") || warning.includes("Skipped")) || lines.join("\n").length > maxCharacters, warnings, }; } export async function createPaperclipDistillationRun(ctx: PluginContext, input: PaperclipDistillationRunInput) { const wikiId = normalizeWikiId(input.wikiId); assertPaperclipSourceScopePayload(input); const space = await requirePaperclipIngestionPolicy(ctx, { companyId: input.companyId, wikiId, spaceSlug: input.spaceSlug }, "execute", { requireEnabledProfile: true }); const scope = paperclipCursorScopeMetadata(input); const limits = await resolvePaperclipDistillationLimitsForSpace(ctx, { ...input, space }); const cursorId = await upsertPaperclipDistillationCursor(ctx, { companyId: input.companyId, wikiId, spaceId: space.id, projectId: scope.projectId, rootIssueId: scope.rootIssueId, metadata: { source: "source-bundle" }, }); const bundle = await assemblePaperclipSourceBundle(ctx, input); const estimatedCostCents = estimateSourceCostCents( bundle.markdown.length, limits.costCentsPerThousandSourceCharacters, ); const runId = randomUUID(); const snapshotId = randomUUID(); await ctx.db.execute( `INSERT INTO ${distillationRunTable(ctx)} (id, company_id, wiki_id, space_id, cursor_id, work_item_id, project_id, root_issue_id, source_window_start, source_window_end, source_hash, status, operation_issue_id, retry_count, cost_cents, warnings, metadata) VALUES ($1, $2, $3, $15, $4, $5, $6, $7, $8::timestamptz, $9::timestamptz, $10, 'source_ready', $11, 0, $12, $13::jsonb, $14::jsonb)`, [ runId, input.companyId, wikiId, cursorId, input.workItemId ?? null, scope.projectId, scope.rootIssueId, bundle.sourceWindowStart, bundle.sourceWindowEnd, bundle.sourceHash, input.operationIssueId ?? null, estimatedCostCents, jsonArrayParam(bundle.warnings), jsonParam({ spaceSlug: space.slug, sourceScope: scope.sourceScope, limits, backfillStartAt: isoString(input.backfillStartAt), backfillEndAt: isoString(input.backfillEndAt), }), space.id, ], ); await ctx.db.execute( `INSERT INTO ${sourceSnapshotTable(ctx)} (id, company_id, wiki_id, space_id, distillation_run_id, project_id, root_issue_id, source_hash, max_characters, clipped, source_refs, bundle_markdown, metadata) VALUES ($1, $2, $3, $13, $4, $5, $6, $7, $8, $9, $10::jsonb, $11, $12::jsonb)`, [ snapshotId, input.companyId, wikiId, runId, scope.projectId, scope.rootIssueId, bundle.sourceHash, limits.maxCharacters, bundle.clipped, jsonParam(bundle.sourceRefs), bundle.markdown, jsonParam({ spaceSlug: space.slug, sourceScope: scope.sourceScope, estimatedCostCents, backfillStartAt: isoString(input.backfillStartAt), backfillEndAt: isoString(input.backfillEndAt), }), space.id, ], ); return { status: "source_ready" as const, wikiId, spaceSlug: space.slug, cursorId, runId, snapshotId, bundle, estimatedCostCents }; } export async function recordPaperclipDistillationOutcome(ctx: PluginContext, input: PaperclipDistillationOutcomeInput) { const wikiId = normalizeWikiId(input.wikiId); const space = await requirePaperclipIngestionPolicy(ctx, { companyId: input.companyId, wikiId, spaceSlug: input.spaceSlug }, "execute", { requireEnabledProfile: true }); const warnings = input.warning ? [input.warning] : []; await ctx.db.execute( `UPDATE ${distillationRunTable(ctx)} SET status = $4, warnings = CASE WHEN $5::jsonb = '[]'::jsonb THEN warnings ELSE warnings || $5::jsonb END, cost_cents = CASE WHEN $6::integer IS NULL THEN cost_cents ELSE $6::integer END, retry_count = CASE WHEN $7::integer IS NULL THEN retry_count ELSE $7::integer END, updated_at = now() WHERE company_id = $1 AND wiki_id = $2 AND space_id = $8 AND id = $3`, [ input.companyId, wikiId, input.runId, input.status, jsonArrayParam(warnings), input.costCents ?? null, input.retryCount ?? null, space.id, ], ); if (input.status === "succeeded" && input.cursorId && input.sourceHash && input.sourceWindowEnd) { await ctx.db.execute( `UPDATE ${distillationCursorTable(ctx)} SET last_processed_at = $4::timestamptz, last_successful_run_id = $3, last_source_hash = $5, pending_event_count = 0, updated_at = now() WHERE company_id = $1 AND wiki_id = $2 AND space_id = $7 AND id = $6`, [input.companyId, wikiId, input.runId, input.sourceWindowEnd, input.sourceHash, input.cursorId, space.id], ); } return { status: input.status, cursorAdvanced: input.status === "succeeded" && Boolean(input.cursorId && input.sourceHash && input.sourceWindowEnd), }; } export async function createPaperclipDistillationWorkItem(ctx: PluginContext, input: PaperclipDistillationWorkItemInput) { const wikiId = normalizeWikiId(input.wikiId); assertPaperclipSourceScopePayload(input); const space = await requirePaperclipIngestionPolicy(ctx, { companyId: input.companyId, wikiId, spaceSlug: input.spaceSlug }, "queue", { requireEnabledProfile: true }); const itemId = randomUUID(); const scope = paperclipCursorScopeMetadata(input); if (input.kind === "backfill" && !scope.projectId && !scope.rootIssueId) { throw new Error("Backfill work items must target a projectId or rootIssueId; whole-company backfill is not allowed."); } await ctx.db.execute( `INSERT INTO ${distillationWorkItemTable(ctx)} AS paperclip_distillation_work_items (id, company_id, wiki_id, space_id, work_item_kind, status, priority, project_id, root_issue_id, requested_by_issue_id, idempotency_key, metadata) VALUES ($1, $2, $3, $11, $4, 'pending', $5, $6, $7, $8, $9, $10::jsonb) ON CONFLICT (company_id, wiki_id, space_id, idempotency_key) DO UPDATE SET priority = EXCLUDED.priority, metadata = paperclip_distillation_work_items.metadata || EXCLUDED.metadata, updated_at = now()`, [ itemId, input.companyId, wikiId, input.kind, input.priority ?? "medium", scope.projectId, scope.rootIssueId, input.requestedByIssueId ?? null, input.idempotencyKey ?? null, jsonParam({ spaceSlug: space.slug, sourceScope: scope.sourceScope, ...(input.metadata ?? {}), }), space.id, ], ); return { status: "pending", workItemId: itemId, wikiId, spaceSlug: space.slug, kind: input.kind, sourceScope: scope.sourceScope }; } function sourceRefLabel(ref: PaperclipSourceRef): string { const issue = ref.issueIdentifier ? issueReference(ref.issueIdentifier) : (ref.title ?? "Paperclip source"); if (ref.kind === "document") return `${issue} document:${ref.documentKey ?? "unknown"}`; if (ref.kind === "comment") return `${issue} comment`; return issue; } function sourceRefMarkdown(ref: PaperclipSourceRef): string { const metadata = [ ref.redactionStatus ? `redaction=${ref.redactionStatus}` : null, ref.redactionReasons?.length ? `redaction_reasons=${ref.redactionReasons.join("|")}` : null, ].filter((part): part is string => Boolean(part)).join(", "); return `- ${sourceRefLabel(ref)}${metadata ? ` (${metadata})` : ""}`; } function issueSourceRef(issue: Issue): PaperclipSourceRef { return { kind: "issue", issueId: issue.id, issueIdentifier: issue.identifier ?? null, projectId: issue.projectId ?? null, title: issue.title, updatedAt: isoString(issue.updatedAt) ?? undefined, }; } function projectPageSlug(input: { project: Project | null; rootIssue: Issue | null }): string { return slugify(input.project?.name ?? input.rootIssue?.title ?? "paperclip-project"); } function issueDescription(issue: Issue): string { return issue.description?.trim() ?? ""; } function issueReference(identifier: string): string { const prefix = identifier.match(/^([A-Z]+)-\d+$/)?.[1]; return prefix ? `[${identifier}](/${prefix}/issues/${identifier})` : identifier; } function issueReferenceFor(issue: Issue): string { return issue.identifier ? issueReference(issue.identifier) : "source issue"; } function issueConcept(issue: Issue): string { const title = issue.title .replace(/^\s*(implement|add|update|fix|ship|write|create|publish|review|validate|investigate|design|refactor|support|make)\s+/i, "") .replace(/\s+/g, " ") .trim(); const words = title.split(" ").filter(Boolean).slice(0, 5).join(" "); return words || issue.title; } function issueNarrative(issue: Issue, maxLength = 260): string { const details = issueDescription(issue); return excerpt(details || issue.title, maxLength); } function conceptBullet(issue: Issue): string { return `- **${issueConcept(issue)}.** ${issueNarrative(issue)} (${issueReferenceFor(issue)})`; } function excerpt(value: string, maxLength = 240): string { const normalized = value.replace(/\s+/g, " ").trim(); if (normalized.length <= maxLength) return normalized; return `${normalized.slice(0, maxLength - 1).trimEnd()}...`; } function hasDecisionSignal(value: string): boolean { return /\b(accepted|approved|rejected|reversed|decided|decision|plan|proposal|approach|architecture|tradeoff)\b/i.test(value); } function hasRiskSignal(value: string): boolean { return /\b(blocked|blocker|risk|warning|stale|conflict|failed|failure|regression)\b/i.test(value); } function hasDurableSignal(bundle: PaperclipSourceBundle, issues: Issue[]): boolean { if (bundle.sourceRefs.some((ref) => ref.kind === "document" || ref.kind === "comment")) return true; if (issues.some((issue) => issue.status !== "todo" || issueDescription(issue).length > 0)) return true; return /\b(decision|approved|implemented|completed|blocked|risk|artifact|plan|handoff|merged|fixed)\b/i.test(bundle.markdown); } function standupPageContents(input: { project: Project | null; rootIssue: Issue | null; issues: Issue[]; bundle: PaperclipSourceBundle; pagePath: string; durablePagePath: string; }): string { const currentAsOf = input.bundle.sourceWindowEnd ?? new Date().toISOString(); const title = input.project?.name ?? input.rootIssue?.title ?? "Paperclip Project"; const activeIssues = input.issues.filter((issue) => !["done", "cancelled"].includes(issue.status)); const recentlyChanged = [...input.issues] .sort((a, b) => (isoString(b.updatedAt) ?? "").localeCompare(isoString(a.updatedAt) ?? "")) .slice(0, 6); const completedIssues = recentlyChanged.filter((issue) => issue.status === "done"); const advancedIssues = recentlyChanged.filter((issue) => issue.status !== "done" && issue.status !== "cancelled"); const decisionIssues = input.issues.filter((issue) => hasDecisionSignal(`${issue.title}\n${issueDescription(issue)}`)).slice(0, 6); const riskIssues = input.issues.filter((issue) => issue.status === "blocked" || hasRiskSignal(`${issue.title}\n${issueDescription(issue)}`)).slice(0, 6); const nextActionIssues = activeIssues.slice(0, 6); const lead = activeIssues[0] ?? recentlyChanged[0] ?? null; return [ "---", `title: ${JSON.stringify(`${title} Standup`)}`, "type: project-standup", `project: ${JSON.stringify(projectPageSlug(input))}`, `current_as_of: ${JSON.stringify(currentAsOf)}`, "sources: []", "---", "", `# ${title} Standup`, "", "## Executive Readout", "", lead ? `The current center of gravity is **${issueConcept(lead)}** (${issueReferenceFor(lead)}). ${input.bundle.clipped ? "The source window was clipped, so treat this as a bounded readout rather than the full live state." : "This is a high-level readout of the meaningful Paperclip work in the current source window."}` : "No meaningful project movement was present in this source window.", "", "## What Changed", "", ...(completedIssues.length ? completedIssues.map(conceptBullet) : advancedIssues.length ? advancedIssues.map(conceptBullet) : ["- No material completed or advanced work was identified in this source window."]), "", "## Decisions", "", ...(decisionIssues.length ? decisionIssues.map(conceptBullet) : ["- No decision changed the project direction in this source window."]), "", "## Blockers / Risks", "", ...(riskIssues.length ? riskIssues.map(conceptBullet) : ["- No active blocker or material risk surfaced in this source window."]), "", "## Next Actions", "", ...(nextActionIssues.length ? nextActionIssues.map((issue) => `- **${issueConcept(issue)}.** Continue the work represented by ${issueReferenceFor(issue)}; focus on the next concrete deliverable rather than routine status churn.`) : ["- No next action inferred from this source window."]), "", "## Links", "", `- Durable project overview: [[${input.durablePagePath}]]`, ...input.bundle.sourceRefs.slice(0, 12).map(sourceRefMarkdown), "", ].filter((line): line is string => line !== null).join("\n"); } function projectPageContents(input: { project: Project | null; rootIssue: Issue | null; issues: Issue[]; bundle: PaperclipSourceBundle; pagePath: string; }): string { const currentAsOf = input.bundle.sourceWindowEnd ?? new Date().toISOString(); const title = input.project?.name ?? input.rootIssue?.title ?? "Paperclip Project"; const description = input.project?.description?.trim() || input.rootIssue?.description?.trim() || ""; const activeIssues = input.issues.filter((issue) => !["done", "cancelled"].includes(issue.status)); const recentIssues = [...input.issues] .sort((a, b) => (isoString(b.updatedAt) ?? "").localeCompare(isoString(a.updatedAt) ?? "")) .slice(0, 8); const decisionIssues = input.issues.filter((issue) => hasDecisionSignal(`${issue.title}\n${issueDescription(issue)}`)).slice(0, 8); const riskIssues = input.issues.filter((issue) => issue.status === "blocked" || hasRiskSignal(`${issue.title}\n${issueDescription(issue)}`)).slice(0, 8); return [ "---", `title: ${JSON.stringify(title)}`, "type: project", `current_as_of: ${JSON.stringify(currentAsOf)}`, "sources: []", "---", "", `# ${title}`, "", "## Overview", "", description ? excerpt(description, 700) : `This page synthesizes Paperclip issue history into a stable project brief for ${title}.`, "", "## Current Direction", "", activeIssues.length ? `Work is currently organized around ${activeIssues.slice(0, 3).map((issue) => `**${issueConcept(issue)}** (${issueReferenceFor(issue)})`).join(", ")}. The useful project view is the concept being advanced, not the raw issue queue.` : "The current source window does not show active project work.", input.bundle.clipped ? "\nThe source window was clipped, so verify Paperclip before treating this as complete state." : null, "", "## Workstreams", "", ...(recentIssues.length ? recentIssues.map(conceptBullet) : ["- No meaningful workstream signal was identified in this source window."]), "", "## Decisions", "", ...(decisionIssues.length ? decisionIssues.map(conceptBullet) : ["- No durable decision signal was identified in this source window."]), "", "## Open Risks / Blockers", "", ...(riskIssues.length ? riskIssues.map(conceptBullet) : ["- No open risks or blockers identified in this source window."]), "", "## References", "", `- Current standup: [[${input.pagePath.replace(/\/index\.md$/, "/standup.md")}]]`, ...input.bundle.sourceRefs.slice(0, 12).map(sourceRefMarkdown), "", ].filter((line): line is string => line !== null).join("\n"); } function decisionsPageContents(input: { project: Project | null; rootIssue: Issue | null; issues: Issue[]; bundle: PaperclipSourceBundle }): string { const title = input.project?.name ?? input.rootIssue?.title ?? "Paperclip Project"; const decisionIssues = input.issues.filter((issue) => hasDecisionSignal(`${issue.title}\n${issueDescription(issue)}`)); return [ `# ${title} Decisions`, "", "Durable project decisions grouped by concept. Use this as an editorial memory of why the project changed direction, not as an issue log.", "", ...(decisionIssues.length ? decisionIssues.map((issue) => [ `## ${issueConcept(issue)}`, "", issueDescription(issue) ? excerpt(issueDescription(issue), 900) : "_No decision details beyond the issue title._", "", `Source: ${issueReferenceFor(issue)}`, "", ].join("\n")) : ["No durable decisions identified in this source window.", ""]), "## References", "", ...input.bundle.sourceRefs.slice(0, 40).map(sourceRefMarkdown), "", ].join("\n"); } function historyPageContents(input: { project: Project | null; rootIssue: Issue | null; issues: Issue[]; bundle: PaperclipSourceBundle }): string { const title = input.project?.name ?? input.rootIssue?.title ?? "Paperclip Project"; const timeline = [...input.issues] .sort((a, b) => (isoString(a.updatedAt) ?? "").localeCompare(isoString(b.updatedAt) ?? "")) .slice(-30); return [ `# ${title} History`, "", "Narrative history of meaningful project movement. Group by what changed in the work, not by dates or metadata.", "", "## Meaningful Project Movement", "", ...(timeline.length ? timeline.map(conceptBullet) : ["- No source issues in this window."]), "", "## References", "", ...input.bundle.sourceRefs.slice(0, 40).map(sourceRefMarkdown), "", ].join("\n"); } function updateProjectIndexContents(current: string | null, input: { pagePath: string; standupPath: string; title: string }): string { const base = current?.trimEnd() || "# Index\n\n## Sources\n\n_(none yet)_\n\n## Projects\n\n_(none yet)_\n\n## Entities\n\n_(none yet)_\n\n## Concepts\n\n_(none yet)_\n\n## Synthesis\n\n_(none yet)_"; const entry = `- [[${input.pagePath}]] — ${input.title} project overview. Current executive standup: [[${input.standupPath}]].`; const projectsMatch = base.match(/(^## Projects\n)([\s\S]*?)(?=^## |\s*$)/m); if (!projectsMatch || projectsMatch.index == null) { return `${base}\n\n## Projects\n\n${entry}\n`; } const start = projectsMatch.index + projectsMatch[1].length; const end = start + projectsMatch[2].length; const existingLines = projectsMatch[2] .split("\n") .map((line) => line.trimEnd()) .filter((line) => line.trim() && line.trim() !== "_(none yet)_" && !line.includes(input.pagePath) && !line.includes(input.standupPath)); const nextLines = [...existingLines, entry].sort((a, b) => a.localeCompare(b)); return `${base.slice(0, start)}${nextLines.join("\n")}\n\n${base.slice(end).replace(/^\n+/, "")}`.trimEnd() + "\n"; } function appendProjectLogContents(current: string | null, input: { standupPath: string; pagePath: string; runId: string; sourceHash: string; status: string; warnings: string[] }): string { const base = current?.trimEnd() || "# Log\n\nAppend-only chronological record of wiki operations."; const warningLines = input.warnings.length ? input.warnings.map((warning) => `- warning: ${warning}`) : ["- warnings: none"]; const entry = [ `## [${new Date().toISOString().slice(0, 10)}] paperclip-distill | ${input.status}`, `- standup: \`${input.standupPath}\``, `- page: \`${input.pagePath}\``, `- run: \`${input.runId}\``, `- source hash: \`${input.sourceHash}\``, ...warningLines, ].join("\n"); return `${base}\n\n${entry}\n`; } function patchForPage(input: { path: string; operationType: PaperclipDistillationPatchOperation; currentHash: string | null; contents: string; bundle: PaperclipSourceBundle; confidence: "high" | "medium" | "low"; warnings: string[]; humanReviewRequired: boolean; }): PaperclipDistillationPatch { return { pagePath: input.path, operationType: input.operationType, currentHash: input.currentHash, proposedHash: contentHash(input.contents), proposedContents: input.contents, sourceHash: input.bundle.sourceHash, sourceRefs: input.bundle.sourceRefs, cursorWindow: { start: input.bundle.sourceWindowStart, end: input.bundle.sourceWindowEnd, }, confidence: input.confidence, warnings: input.warnings, humanReviewRequired: input.humanReviewRequired, }; } async function readPageBinding(ctx: PluginContext, input: { companyId: string; wikiId: string; spaceId: string; pagePath: string }) { const rows = await ctx.db.query<{ last_applied_source_hash: string | null }>( `SELECT last_applied_source_hash FROM ${pageBindingTable(ctx)} WHERE company_id = $1 AND wiki_id = $2 AND space_id = $3 AND page_path = $4 LIMIT 1`, [input.companyId, input.wikiId, input.spaceId, input.pagePath], ); return rows[0] ?? null; } async function upsertPageBinding(ctx: PluginContext, input: { companyId: string; wikiId: string; spaceId: string; spaceSlug: string; projectId: string | null; rootIssueId: string | null; pagePath: string; sourceHash: string; runId: string; metadata?: Record; }) { await ctx.db.execute( `INSERT INTO ${pageBindingTable(ctx)} AS paperclip_page_bindings (id, company_id, wiki_id, space_id, project_id, root_issue_id, page_path, last_applied_source_hash, last_distillation_run_id, metadata) VALUES ($1, $2, $3, $10, $4, $5, $6, $7, $8, $9::jsonb) ON CONFLICT (company_id, wiki_id, space_id, page_path) DO UPDATE SET last_applied_source_hash = EXCLUDED.last_applied_source_hash, last_distillation_run_id = EXCLUDED.last_distillation_run_id, metadata = paperclip_page_bindings.metadata || EXCLUDED.metadata, updated_at = now()`, [ randomUUID(), input.companyId, input.wikiId, input.projectId, input.rootIssueId, input.pagePath, input.sourceHash, input.runId, jsonParam({ spaceSlug: input.spaceSlug, ...(input.metadata ?? {}) }), input.spaceId, ], ); } async function autoApplyEnabled(ctx: PluginContext, requested: boolean | undefined): Promise { if (getDistillationAutoApplyRestriction().autoApplyRestriction) { return false; } const config = await ctx.config.get(); const configured = (config as { autoApplyIngestPatches?: unknown }).autoApplyIngestPatches !== false; return configured && requested !== false; } export function getDistillationAutoApplyRestriction(): DistillationAutoApplyRestriction { const rawMode = process.env.PAPERCLIP_DEPLOYMENT_MODE; const rawExposure = process.env.PAPERCLIP_DEPLOYMENT_EXPOSURE; const deploymentMode = rawMode === "local_trusted" || rawMode === "authenticated" ? rawMode : null; const deploymentExposure = rawExposure === "private" || rawExposure === "public" ? rawExposure : null; const blocked = deploymentMode === "authenticated" && deploymentExposure === "public"; return { autoApplyAllowed: !blocked, autoApplyRestriction: blocked ? PUBLIC_DISTILLATION_AUTO_APPLY_RESTRICTION : null, deploymentMode, deploymentExposure, }; } export async function distillPaperclipProjectPage(ctx: PluginContext, input: PaperclipProjectPageDistillationInput) { if (!input.projectId && !input.rootIssueId) { throw new Error("projectId or rootIssueId is required"); } const wikiId = normalizeWikiId(input.wikiId); assertPaperclipSourceScopePayload(input); const space = await requirePaperclipIngestionPolicy(ctx, { companyId: input.companyId, wikiId, spaceSlug: input.spaceSlug }, "execute", { requireEnabledProfile: true }); const scope = paperclipCursorScopeMetadata(input); const issues = await listPaperclipBundleIssues(ctx, input); const project = scope.projectId ? await ctx.projects.get(scope.projectId, input.companyId) : null; const rootIssue = scope.rootIssueId ? await ctx.issues.get(scope.rootIssueId, input.companyId) : null; const slug = projectPageSlug({ project, rootIssue }); const projectDir = `wiki/projects/${slug}`; const standupPath = assertPagePath(`${projectDir}/standup.md`); const pagePath = assertPagePath(`${projectDir}/index.md`); const run = await createPaperclipDistillationRun(ctx, input); const bundle = run.bundle; const current = await readCurrentWithHash(ctx, input.companyId, pagePath, space); assertExpectedHash(input.expectedProjectPageHash, current.hash, pagePath); if (!hasDurableSignal(bundle, issues)) { await recordPaperclipDistillationOutcome(ctx, { companyId: input.companyId, wikiId, spaceSlug: space.slug, runId: run.runId, cursorId: run.cursorId, status: "succeeded", sourceHash: bundle.sourceHash, sourceWindowEnd: bundle.sourceWindowEnd, warning: "Skipped low-signal Paperclip source window.", }); return { status: "skipped", reason: "low_signal", wikiId, runId: run.runId, cursorId: run.cursorId, sourceHash: bundle.sourceHash, warnings: ["Skipped low-signal Paperclip source window."], patches: [] as PaperclipDistillationPatch[], }; } const existingBinding = await readPageBinding(ctx, { companyId: input.companyId, wikiId, spaceId: space.id, pagePath }); if (existingBinding?.last_applied_source_hash === bundle.sourceHash) { await recordPaperclipDistillationOutcome(ctx, { companyId: input.companyId, wikiId, spaceSlug: space.slug, runId: run.runId, cursorId: run.cursorId, status: "succeeded", sourceHash: bundle.sourceHash, sourceWindowEnd: bundle.sourceWindowEnd, warning: "Skipped unchanged Paperclip source hash.", }); return { status: "skipped", reason: "unchanged_source", wikiId, runId: run.runId, cursorId: run.cursorId, sourceHash: bundle.sourceHash, warnings: ["Skipped unchanged Paperclip source hash."], patches: [] as PaperclipDistillationPatch[], }; } const warnings = [...bundle.warnings]; const confidence: "high" | "medium" | "low" = bundle.clipped ? "medium" : "high"; const reviewRequired = bundle.clipped || warnings.length > 0; const title = project?.name ?? rootIssue?.title ?? "Paperclip Project"; const standupCurrent = await readCurrentWithHash(ctx, input.companyId, standupPath, space); const standupContents = standupPageContents({ project, rootIssue, issues, bundle, pagePath: standupPath, durablePagePath: pagePath }); const projectContents = projectPageContents({ project, rootIssue, issues, bundle, pagePath }); const indexCurrent = await readCurrentWithHash(ctx, input.companyId, "wiki/index.md", space); const logCurrent = await readCurrentWithHash(ctx, input.companyId, "wiki/log.md", space); const indexContents = updateProjectIndexContents(indexCurrent.contents, { pagePath, standupPath, title, }); const logContents = appendProjectLogContents(logCurrent.contents, { standupPath, pagePath, runId: run.runId, sourceHash: bundle.sourceHash, status: "proposed", warnings, }); const patches: PaperclipDistillationPatch[] = [ patchForPage({ path: standupPath, operationType: "standup_update", currentHash: standupCurrent.hash, contents: standupContents, bundle, confidence, warnings, humanReviewRequired: reviewRequired }), patchForPage({ path: pagePath, operationType: "project_page_distill", currentHash: current.hash, contents: projectContents, bundle, confidence, warnings, humanReviewRequired: reviewRequired }), patchForPage({ path: "wiki/index.md", operationType: "index_refresh", currentHash: indexCurrent.hash, contents: indexContents, bundle, confidence: "high", warnings: [], humanReviewRequired: false }), patchForPage({ path: "wiki/log.md", operationType: "log_append", currentHash: logCurrent.hash, contents: logContents, bundle, confidence: "high", warnings: [], humanReviewRequired: false }), ]; if (input.includeSupportingPages !== false) { const hasDecisions = issues.some((issue) => hasDecisionSignal(`${issue.title}\n${issueDescription(issue)}`)); if (hasDecisions) { const decisionsPath = assertPagePath(`${projectDir}/decisions.md`); const decisionsCurrent = await readCurrentWithHash(ctx, input.companyId, decisionsPath, space); patches.push(patchForPage({ path: decisionsPath, operationType: "decision_distill", currentHash: decisionsCurrent.hash, contents: decisionsPageContents({ project, rootIssue, issues, bundle }), bundle, confidence, warnings, humanReviewRequired: reviewRequired, })); } const historyPath = assertPagePath(`${projectDir}/history.md`); const historyCurrent = await readCurrentWithHash(ctx, input.companyId, historyPath, space); patches.push(patchForPage({ path: historyPath, operationType: "history_distill", currentHash: historyCurrent.hash, contents: historyPageContents({ project, rootIssue, issues, bundle }), bundle, confidence, warnings, humanReviewRequired: reviewRequired, })); } const autoApplyRestriction = getDistillationAutoApplyRestriction(); const canAutoApply = await autoApplyEnabled(ctx, input.autoApply); if (!canAutoApply || reviewRequired) { const autoApplyWarning = autoApplyRestriction.autoApplyRestriction ?? "Auto-apply policy disabled; proposed patches require review."; await recordPaperclipDistillationOutcome(ctx, { companyId: input.companyId, wikiId, spaceSlug: space.slug, runId: run.runId, cursorId: run.cursorId, status: "review_required", sourceHash: bundle.sourceHash, sourceWindowEnd: bundle.sourceWindowEnd, warning: canAutoApply ? "Human review required by patch warnings." : autoApplyWarning, }); return { status: "review_required", wikiId, runId: run.runId, cursorId: run.cursorId, sourceHash: bundle.sourceHash, patches, appliedPages: [] as string[], warnings: canAutoApply ? warnings : [autoApplyWarning, ...warnings], }; } const appliedPages: string[] = []; for (const patch of patches) { await writeWikiPage(ctx, { companyId: input.companyId, wikiId, spaceSlug: space.slug, path: patch.pagePath, contents: patch.proposedContents, expectedHash: patch.currentHash, summary: `Paperclip distillation ${patch.operationType} from ${bundle.sourceHash}`, sourceRefs: patch.sourceRefs, }); await upsertPageBinding(ctx, { companyId: input.companyId, wikiId, spaceId: space.id, spaceSlug: space.slug, projectId: scope.projectId, rootIssueId: scope.rootIssueId, pagePath: patch.pagePath, sourceHash: bundle.sourceHash, runId: run.runId, metadata: { operationType: patch.operationType }, }); appliedPages.push(patch.pagePath); } await recordPaperclipDistillationOutcome(ctx, { companyId: input.companyId, wikiId, spaceSlug: space.slug, runId: run.runId, cursorId: run.cursorId, status: "succeeded", sourceHash: bundle.sourceHash, sourceWindowEnd: bundle.sourceWindowEnd, }); return { status: "applied", wikiId, runId: run.runId, cursorId: run.cursorId, sourceHash: bundle.sourceHash, patches, appliedPages, warnings, }; } function truncateEventSource(contents: string, maxCharacters: number): string { if (contents.length <= maxCharacters) return contents; return `${contents.slice(0, maxCharacters)}\n\n[Truncated by LLM Wiki event ingestion policy at ${maxCharacters} characters.]\n`; } function eventPayload(event: PluginEvent): Record { return event.payload && typeof event.payload === "object" && !Array.isArray(event.payload) ? event.payload as Record : {}; } function sourceTitleForIssue(issue: Issue): string { return issue.identifier ? `${issue.identifier} ${issue.title}` : issue.title; } function rawPathForPaperclipEvent(input: { sourceKind: WikiEventIngestionSource; issue: Issue; label: string; contents: string; event: PluginEvent; }): string { const identifier = input.issue.identifier ?? input.issue.id.slice(0, 8); const eventDate = input.event.occurredAt.slice(0, 10); return assertRawPath(`raw/paperclip/${input.sourceKind}/${eventDate}-${slugify(identifier)}-${slugify(input.label)}-${contentHash(input.contents).slice(0, 8)}.md`); } function formatIssueEventSource(issue: Issue, event: PluginEvent, maxCharacters: number): string { return truncateEventSource([ `# Paperclip issue: ${sourceTitleForIssue(issue)}`, "", "## Provenance", "", `- Company ID: ${issue.companyId}`, `- Issue ID: ${issue.id}`, issue.identifier ? `- Issue identifier: ${issue.identifier}` : null, `- Event type: ${event.eventType}`, `- Event ID: ${event.eventId}`, `- Event occurred at: ${event.occurredAt}`, `- Status: ${issue.status}`, `- Priority: ${issue.priority}`, "", "## Issue", "", issue.description?.trim() ? issue.description.trim() : "_No issue description._", ].filter((line): line is string => line !== null).join("\n"), maxCharacters); } function formatCommentEventSource(issue: Issue, comment: IssueComment, event: PluginEvent, maxCharacters: number): string { return truncateEventSource([ `# Paperclip comment on ${sourceTitleForIssue(issue)}`, "", "## Provenance", "", `- Company ID: ${issue.companyId}`, `- Issue ID: ${issue.id}`, issue.identifier ? `- Issue identifier: ${issue.identifier}` : null, `- Comment ID: ${comment.id}`, `- Event type: ${event.eventType}`, `- Event ID: ${event.eventId}`, `- Event occurred at: ${event.occurredAt}`, "", "## Comment", "", comment.body, ].filter((line): line is string => line !== null).join("\n"), maxCharacters); } function formatDocumentEventSource(issue: Issue, document: IssueDocument, event: PluginEvent, maxCharacters: number): string { return truncateEventSource([ `# Paperclip document: ${document.title ?? document.key}`, "", "## Provenance", "", `- Company ID: ${issue.companyId}`, `- Issue ID: ${issue.id}`, issue.identifier ? `- Issue identifier: ${issue.identifier}` : null, `- Document ID: ${document.id}`, `- Document key: ${document.key}`, `- Event type: ${event.eventType}`, `- Event ID: ${event.eventId}`, `- Event occurred at: ${event.occurredAt}`, `- Format: ${document.format}`, `- Revision: ${document.latestRevisionNumber}`, "", "## Document", "", document.body, ].filter((line): line is string => line !== null).join("\n"), maxCharacters); } async function recordPaperclipCursorObservation(ctx: PluginContext, input: { companyId: string; wikiId: string; space: WikiSpace; sourceKind: WikiEventIngestionSource; sourceId: string; issue: Issue; event: PluginEvent; }): Promise> { const cursorId = await upsertPaperclipDistillationCursor(ctx, { companyId: input.companyId, wikiId: input.wikiId, spaceId: input.space.id, projectId: input.issue.projectId ?? null, rootIssueId: null, observedAt: input.event.occurredAt, metadata: { lastEventId: input.event.eventId, lastEventType: input.event.eventType, lastSourceKind: input.sourceKind, lastSourceId: input.sourceId, lastIssueId: input.issue.id, lastIssueIdentifier: input.issue.identifier ?? null, }, }); await ctx.state.set(eventIngestionDedupKey(input.companyId, input.wikiId, input.space.id, input.sourceKind, input.sourceId), { eventId: input.event.eventId, cursorId, issueId: input.issue.id, spaceSlug: input.space.slug, observedAt: new Date().toISOString(), }); return { status: "recorded", sourceKind: input.sourceKind, sourceId: input.sourceId, cursorId, issueId: input.issue.id, }; } async function paperclipProfileIncludesIssue(ctx: PluginContext, input: { companyId: string; issue: Issue; profile: PaperclipIngestionProfileV1; }): Promise { for (const scope of input.profile.sourceScopes) { if (scope.kind === "company_all") return true; if (scope.kind === "selected_projects" && input.issue.projectId && scope.projectIds.includes(input.issue.projectId)) { return true; } if (scope.kind === "active_projects" && input.issue.projectId) { const project = await ctx.projects.get(input.issue.projectId, input.companyId); const statuses = scope.statuses ?? ["in_progress"]; if (project && statuses.includes(project.status as "in_progress" | "todo" | "done") && isActiveDistillationProject(project)) { return true; } } if (scope.kind === "root_issues") { for (const rootIssueId of scope.issueIds) { if (input.issue.id === rootIssueId) return true; const subtree = await ctx.issues.getSubtree(rootIssueId, input.companyId, { includeRoot: true }); if (subtree.issues.some((issue) => issue.id === input.issue.id)) return true; } } } return false; } async function routePaperclipCursorObservation(ctx: PluginContext, input: { companyId: string; sourceKind: WikiEventIngestionSource; sourceId: string; issue: Issue; event: PluginEvent; }): Promise { const { spaces } = await listSpaces(ctx, { companyId: input.companyId, wikiId: DEFAULT_WIKI_ID }); const recorded: Array> = []; let eligibleProfileCount = 0; for (const space of spaces) { const profile = await profileForSpace(ctx, input.companyId, space); if (!profile.enabled) continue; const policy = evaluatePaperclipProfilePolicy({ space, profile, purpose: "event_routing", requireEnabledProfile: true }); if (!policy.allowed) continue; if (!profile.sourceKinds[input.sourceKind]) continue; if (!(await paperclipProfileIncludesIssue(ctx, { companyId: input.companyId, issue: input.issue, profile }))) continue; eligibleProfileCount += 1; if (eligibleProfileCount > MAX_PAPERCLIP_DISTILLATION_FAN_OUT) { throw new Error(`Paperclip ingestion fan-out exceeds the hard cap of ${MAX_PAPERCLIP_DISTILLATION_FAN_OUT} enabled profiles.`); } if (await ctx.state.get(eventIngestionDedupKey(input.companyId, space.wikiId, space.id, input.sourceKind, input.sourceId))) { continue; } recorded.push(await recordPaperclipCursorObservation(ctx, { ...input, wikiId: space.wikiId, space, })); } return recorded[0] ?? { status: "skipped", reason: "source_disabled" }; } export async function handlePaperclipEventIngestion(ctx: PluginContext, event: PluginEvent): Promise { const companyId = event.companyId; const issueId = stringField(event.entityId); if (!issueId) return { status: "skipped", reason: "unsupported_event" }; const issue = await ctx.issues.get(issueId, companyId); if (!issue) return { status: "skipped", reason: "missing_issue" }; if (isLlmWikiOperationIssue(issue)) return { status: "skipped", reason: "plugin_operation" }; const payload = eventPayload(event); if (event.eventType === "issue.created" || event.eventType === "issue.updated") { const sourceId = `${event.eventType}:${issue.id}:${event.eventId}`; return routePaperclipCursorObservation(ctx, { companyId, sourceKind: "issues", sourceId, issue, event, }); } if (event.eventType === "issue.comment.created") { const commentId = stringField(payload.commentId); if (!commentId) return { status: "skipped", reason: "missing_comment" }; const sourceId = `comment:${commentId}`; return routePaperclipCursorObservation(ctx, { companyId, sourceKind: "comments", sourceId, issue, event, }); } if (event.eventType === "issue.document.created" || event.eventType === "issue.document.updated") { const documentKey = stringField(payload.key) ?? stringField(payload.documentKey); if (!documentKey) return { status: "skipped", reason: "missing_document" }; const revision = stringField(payload.revisionId) ?? stringField(payload.latestRevisionId) ?? stringField(payload.revisionNumber) ?? event.eventId; const sourceId = `document:${issue.id}:${documentKey}:revision:${revision}`; return routePaperclipCursorObservation(ctx, { companyId, sourceKind: "documents", sourceId, issue, event, }); } return { status: "skipped", reason: "unsupported_event" }; } function queryStreamChannel(operationId: string): string { return `llm-wiki:query:${operationId}`; } function buildQueryPrompt(input: { companyId: string; wikiId: string; space: WikiSpace; question: string }): string { return [ QUERY_PROMPT, `Company ID: ${input.companyId}`, `Wiki ID: ${input.wikiId}`, `Space: ${input.space.displayName} (${input.space.slug})`, `Space root: ${operationSpaceRoot(input.space)}`, `Tool arguments: always pass wikiId \`${input.wikiId}\` and spaceSlug \`${input.space.slug}\`.`, "Use the LLM Wiki plugin tools against that space only. Read wiki/index.md first with wiki_read_page, then use wiki_search, wiki_read_page, wiki_list_sources, and wiki_read_source as needed.", "Cite the wiki page paths and raw source paths you used. If the wiki does not contain enough evidence, say that directly.", `Question: ${input.question}`, ].join("\n\n"); } async function markOperation(ctx: PluginContext, input: { companyId: string; operationId: string; status: string; runId?: string | null; warning?: string | null; affectedPages?: unknown[] | null; metadata?: Record | null; }) { await ctx.db.execute( `UPDATE ${tableName(ctx.db.namespace, "wiki_operations")} SET status = $3, run_ids = CASE WHEN $4::jsonb = '[]'::jsonb THEN run_ids ELSE run_ids || $4::jsonb END, warnings = CASE WHEN $5::jsonb = '[]'::jsonb THEN warnings ELSE warnings || $5::jsonb END, affected_pages = CASE WHEN $6::jsonb = '[]'::jsonb THEN affected_pages ELSE $6::jsonb END, metadata = metadata || $7::jsonb, updated_at = now() WHERE company_id = $1 AND id = $2`, [ input.companyId, input.operationId, input.status, jsonArrayParam(input.runId ? [input.runId] : []), jsonArrayParam(input.warning ? [input.warning] : []), jsonArrayParam(input.affectedPages ?? []), jsonParam(input.metadata ?? {}), ], ); } function isTerminalSessionEvent(event: AgentSessionEvent): boolean { return event.eventType === "done" || event.eventType === "error"; } export async function startWikiQuerySession(ctx: PluginContext, input: QuerySessionInput) { const question = requireString(input.question, "question"); const wikiId = normalizeWikiId(input.wikiId); const space = await resolveSpace(ctx, { companyId: input.companyId, wikiId, spaceSlug: input.spaceSlug }); const operation = await createOperationIssue(ctx, { companyId: input.companyId, wikiId, spaceSlug: space.slug, operationType: "query", title: input.title ?? `Query LLM Wiki: ${question.slice(0, 72)}`, prompt: question, }); const agentId = operation.issue.assigneeAgentId; const channel = queryStreamChannel(operation.operationId); if (!agentId) { const warning = "No configured Wiki Maintainer agent is available for this company."; await markOperation(ctx, { companyId: input.companyId, operationId: operation.operationId, status: "blocked", warning, }); await ctx.issues.update(operation.issue.id, { status: "blocked" }, input.companyId); await ctx.issues.createComment(operation.issue.id, warning, input.companyId); throw new Error(warning); } const agent = await ctx.agents.get(agentId, input.companyId); if (!agent || agent.status === "paused" || agent.status === "terminated" || agent.status === "pending_approval") { const warning = agent ? `Wiki Maintainer agent is not invokable while status is ${agent.status}.` : "Wiki Maintainer agent could not be loaded."; await markOperation(ctx, { companyId: input.companyId, operationId: operation.operationId, status: "blocked", warning, }); await ctx.issues.update(operation.issue.id, { status: "blocked" }, input.companyId); await ctx.issues.createComment(operation.issue.id, warning, input.companyId); throw new Error(warning); } const session = await ctx.agents.sessions.create(agentId, input.companyId, { taskKey: `plugin:${PLUGIN_ID}:session:wiki:${wikiId}:query:${operation.operationId}`, reason: "LLM Wiki query session", }); await ctx.db.execute( `INSERT INTO ${tableName(ctx.db.namespace, "wiki_query_sessions")} (id, company_id, wiki_id, space_id, hidden_issue_id, agent_session_id, status, filed_outputs) VALUES ($1, $2, $3, $6, $4, $5, 'active', '[]'::jsonb)`, [operation.operationId, input.companyId, wikiId, operation.issue.id, session.sessionId, space.id], ); const prompt = buildQueryPrompt({ companyId: input.companyId, wikiId, space, question }); ctx.streams.open(channel, input.companyId); ctx.streams.emit(channel, { type: "query.started", operationId: operation.operationId, querySessionId: operation.operationId, issueId: operation.issue.id, sessionId: session.sessionId, question, }); let answer = ""; const sendResult = await ctx.agents.sessions.sendMessage(session.sessionId, input.companyId, { prompt, reason: "LLM Wiki query", onEvent: (event) => { if (event.eventType === "chunk" && event.stream !== "stderr" && event.message) { answer += event.message; } ctx.streams.emit(channel, { type: "agent.event", operationId: operation.operationId, querySessionId: operation.operationId, eventType: event.eventType, stream: event.stream, message: event.message, payload: event.payload, runId: event.runId, seq: event.seq, }); if (isTerminalSessionEvent(event)) { const finalStatus = event.eventType === "done" ? "done" : "failed"; ctx.streams.emit(channel, { type: event.eventType === "done" ? "query.done" : "query.error", operationId: operation.operationId, querySessionId: operation.operationId, issueId: operation.issue.id, sessionId: session.sessionId, runId: event.runId, answer, message: event.message, }); ctx.streams.close(channel); void markOperation(ctx, { companyId: input.companyId, operationId: operation.operationId, status: finalStatus, runId: event.runId, warning: event.eventType === "error" ? event.message : null, metadata: { answerLength: answer.length }, }); void ctx.db.execute( `UPDATE ${tableName(ctx.db.namespace, "wiki_query_sessions")} SET status = $3, updated_at = now() WHERE company_id = $1 AND id = $2`, [input.companyId, operation.operationId, finalStatus === "done" ? "completed" : "failed"], ); void ctx.issues.createComment( operation.issue.id, event.eventType === "done" ? `Query completed.\n\n${answer.trim() || "_No answer text was streamed._"}` : `Query failed: ${event.message ?? "agent session ended with an error"}`, input.companyId, ); void ctx.issues.update( operation.issue.id, { status: event.eventType === "done" ? "done" : "blocked", originRunId: event.runId }, input.companyId, ); } }, }); await markOperation(ctx, { companyId: input.companyId, operationId: operation.operationId, status: "running", runId: sendResult.runId, }); await ctx.issues.update(operation.issue.id, { originRunId: sendResult.runId }, input.companyId); return { status: "running", wikiId, spaceSlug: space.slug, operationId: operation.operationId, querySessionId: operation.operationId, issue: operation.issue, sessionId: session.sessionId, runId: sendResult.runId, channel, }; } export async function fileQueryAnswerAsPage(ctx: PluginContext, input: FileQueryAnswerInput) { const wikiId = normalizeWikiId(input.wikiId); const space = await resolveSpace(ctx, { companyId: input.companyId, wikiId, spaceSlug: input.spaceSlug }); const path = assertPagePath(input.path); const title = stringField(input.title) ?? inferTitle(path, input.contents ?? input.answer ?? ""); const answer = stringField(input.answer); const contents = stringField(input.contents) ?? [ `# ${title}`, "", input.question ? `> Filed from query: ${input.question}` : null, "", answer ?? "", ].filter((line): line is string => line !== null).join("\n").trimEnd() + "\n"; const operation = await createOperationIssue(ctx, { companyId: input.companyId, wikiId, spaceSlug: space.slug, operationType: "file-as-page", title: `File LLM Wiki answer as ${path}`, prompt: input.question ?? answer ?? `Write ${path}`, }); const result = await writeWikiPage(ctx, { companyId: input.companyId, wikiId, spaceSlug: space.slug, path, contents, expectedHash: stringField(input.expectedHash), summary: `Filed query answer as ${path}`, sourceRefs: input.querySessionId ? [{ querySessionId: input.querySessionId }] : [], operationId: operation.operationId, }); const affectedPage = { path, title: result.title, pageType: result.pageType, revisionId: result.revisionId, }; await markOperation(ctx, { companyId: input.companyId, operationId: operation.operationId, status: "done", affectedPages: [affectedPage], metadata: { querySessionId: input.querySessionId ?? null }, }); await ctx.issues.update(operation.issue.id, { status: "done" }, input.companyId); await ctx.issues.createComment( operation.issue.id, `Filed query answer as \`${path}\`.`, input.companyId, ); if (input.querySessionId) { await ctx.db.execute( `UPDATE ${tableName(ctx.db.namespace, "wiki_query_sessions")} SET filed_outputs = filed_outputs || $3::jsonb, updated_at = now() WHERE company_id = $1 AND id = $2`, [input.companyId, input.querySessionId, jsonArrayParam([affectedPage])], ); } return { status: "ok", wikiId, spaceSlug: space.slug, path, operationId: operation.operationId, issue: operation.issue, page: affectedPage, }; } export async function registerWikiTools(ctx: PluginContext) { ctx.tools.register("wiki_search", { displayName: "Search Wiki", description: "Search indexed wiki page and source metadata.", parametersSchema: ctx.manifest.tools?.find((tool) => tool.name === "wiki_search")?.parametersSchema ?? { type: "object" }, }, async (params: unknown): Promise => { const input = params as ToolParams; const companyId = requireString(input.companyId, "companyId"); const wikiId = normalizeWikiId(input.wikiId); const space = await resolveSpace(ctx, { companyId, wikiId, spaceSlug: input.spaceSlug as string | null | undefined }); const query = requireString(input.query, "query"); const limit = normalizeLimit(input.limit, 20, 50); const rows = await ctx.db.query<{ kind: string; path: string; title: string | null; match_text: string | null }>( `SELECT 'page' AS kind, path, title, page_type AS match_text FROM ${tableName(ctx.db.namespace, "wiki_pages")} WHERE company_id = $1 AND wiki_id = $2 AND space_id = $5 AND (lower(path) LIKE lower($3) OR lower(coalesce(title, '')) LIKE lower($3)) UNION ALL SELECT 'source' AS kind, raw_path AS path, title, source_type AS match_text FROM ${tableName(ctx.db.namespace, "wiki_sources")} WHERE company_id = $1 AND wiki_id = $2 AND space_id = $5 AND (lower(raw_path) LIKE lower($3) OR lower(coalesce(title, '')) LIKE lower($3) OR lower(coalesce(url, '')) LIKE lower($3)) ORDER BY kind, path LIMIT $4`, [companyId, wikiId, `%${query}%`, limit, space.id], ); return { content: rows.length ? rows.map((row) => `${row.kind}: ${row.path}${row.title ? ` - ${row.title}` : ""}`).join("\n") : "No wiki matches found.", data: { companyId, wikiId, spaceSlug: space.slug, query, results: rows }, }; }); ctx.tools.register("wiki_read_page", { displayName: "Read Wiki Page", description: "Read a markdown wiki page from the configured local wiki root.", parametersSchema: ctx.manifest.tools?.find((tool) => tool.name === "wiki_read_page")?.parametersSchema ?? { type: "object" }, }, async (params: unknown): Promise => { const input = params as ToolParams; const companyId = requireString(input.companyId, "companyId"); const wikiId = normalizeWikiId(input.wikiId); const space = await resolveSpace(ctx, { companyId, wikiId, spaceSlug: input.spaceSlug as string | null | undefined }); const path = assertPagePath(requireString(input.path, "path")); const contents = await ctx.localFolders.readText(companyId, WIKI_ROOT_FOLDER_KEY, spaceRelativePath(space, path)); return { content: contents, data: { companyId, wikiId, spaceSlug: space.slug, path, hash: contentHash(contents) } }; }); ctx.tools.register("wiki_write_page", { displayName: "Write Wiki Page", description: "Atomically write a markdown wiki page after plugin path validation.", parametersSchema: ctx.manifest.tools?.find((tool) => tool.name === "wiki_write_page")?.parametersSchema ?? { type: "object" }, }, async (params: unknown): Promise => { const input = params as ToolParams; const result = await writeWikiPage(ctx, { companyId: requireString(input.companyId, "companyId"), wikiId: stringField(input.wikiId), spaceSlug: stringField(input.spaceSlug), path: requireString(input.path, "path"), contents: requireString(input.contents, "contents"), expectedHash: stringField(input.expectedHash), summary: stringField(input.summary), sourceRefs: input.sourceRefs, }); return { content: `Wrote ${result.path}`, data: result }; }); ctx.tools.register("wiki_propose_patch", { displayName: "Propose Wiki Patch", description: "Return a structured proposed page write without changing files.", parametersSchema: ctx.manifest.tools?.find((tool) => tool.name === "wiki_propose_patch")?.parametersSchema ?? { type: "object" }, }, async (params: unknown): Promise => { const input = params as ToolParams; const companyId = requireString(input.companyId, "companyId"); const wikiId = normalizeWikiId(input.wikiId); const space = await resolveSpace(ctx, { companyId, wikiId, spaceSlug: input.spaceSlug as string | null | undefined }); const path = assertPagePath(requireString(input.path, "path")); const contents = requireString(input.contents, "contents"); const current = await readCurrentWithHash(ctx, companyId, path, space); return { content: `Proposed patch for ${path}`, data: { companyId, wikiId, spaceSlug: space.slug, path, summary: stringField(input.summary), currentHash: current.hash, proposedHash: contentHash(contents), proposedContents: contents, }, }; }); ctx.tools.register("wiki_list_sources", { displayName: "List Wiki Sources", description: "Return captured raw source metadata from the plugin index.", parametersSchema: ctx.manifest.tools?.find((tool) => tool.name === "wiki_list_sources")?.parametersSchema ?? { type: "object" }, }, async (params: unknown): Promise => { const input = params as ToolParams; const companyId = requireString(input.companyId, "companyId"); const wikiId = normalizeWikiId(input.wikiId); const space = await resolveSpace(ctx, { companyId, wikiId, spaceSlug: input.spaceSlug as string | null | undefined }); const limit = normalizeLimit(input.limit, 50, 200); const rows = await ctx.db.query<{ raw_path: string; title: string | null; source_type: string; url: string | null; content_hash: string }>( `SELECT raw_path, title, source_type, url, content_hash FROM ${tableName(ctx.db.namespace, "wiki_sources")} WHERE company_id = $1 AND wiki_id = $2 AND space_id = $4 ORDER BY created_at DESC LIMIT $3`, [companyId, wikiId, limit, space.id], ); return { content: rows.length ? rows.map((row) => `${row.raw_path}${row.title ? ` - ${row.title}` : ""}`).join("\n") : "No sources captured yet.", data: { companyId, wikiId, spaceSlug: space.slug, sources: rows }, }; }); ctx.tools.register("wiki_read_source", { displayName: "Read Wiki Source", description: "Read a captured raw source from the configured local wiki root.", parametersSchema: ctx.manifest.tools?.find((tool) => tool.name === "wiki_read_source")?.parametersSchema ?? { type: "object" }, }, async (params: unknown): Promise => { const input = params as ToolParams; const companyId = requireString(input.companyId, "companyId"); const wikiId = normalizeWikiId(input.wikiId); const space = await resolveSpace(ctx, { companyId, wikiId, spaceSlug: input.spaceSlug as string | null | undefined }); const rawPath = assertRawPath(requireString(input.rawPath, "rawPath")); const contents = await ctx.localFolders.readText(companyId, WIKI_ROOT_FOLDER_KEY, spaceRelativePath(space, rawPath)); return { content: contents, data: { companyId, wikiId, spaceSlug: space.slug, rawPath, hash: contentHash(contents) } }; }); ctx.tools.register("wiki_append_log", { displayName: "Append Wiki Log", description: "Append a maintenance note to wiki/log.md.", parametersSchema: ctx.manifest.tools?.find((tool) => tool.name === "wiki_append_log")?.parametersSchema ?? { type: "object" }, }, async (params: unknown): Promise => { const input = params as ToolParams; const companyId = requireString(input.companyId, "companyId"); const wikiId = normalizeWikiId(input.wikiId); const space = await resolveSpace(ctx, { companyId, wikiId, spaceSlug: input.spaceSlug as string | null | undefined }); const entry = requireString(input.entry, "entry"); let current = ""; try { current = await ctx.localFolders.readText(companyId, WIKI_ROOT_FOLDER_KEY, spaceRelativePath(space, "wiki/log.md")); } catch { current = "# Log\n\nAppend-only chronological record of wiki operations.\n"; } const next = `${current.trimEnd()}\n\n- ${new Date().toISOString()} ${entry}\n`; await ctx.localFolders.writeTextAtomic(companyId, WIKI_ROOT_FOLDER_KEY, spaceRelativePath(space, "wiki/log.md"), next); await upsertPageMetadata(ctx, { companyId, wikiId, spaceId: space.id, path: "wiki/log.md", contents: next, summary: "Append log entry", }); return { content: "Appended log entry", data: { companyId, wikiId, spaceSlug: space.slug, hash: contentHash(next) } }; }); ctx.tools.register("wiki_update_index", { displayName: "Update Wiki Index", description: "Atomically replace wiki/index.md with optional hash conflict checks.", parametersSchema: ctx.manifest.tools?.find((tool) => tool.name === "wiki_update_index")?.parametersSchema ?? { type: "object" }, }, async (params: unknown): Promise => { const input = params as ToolParams; const result = await writeWikiPage(ctx, { companyId: requireString(input.companyId, "companyId"), wikiId: stringField(input.wikiId), spaceSlug: stringField(input.spaceSlug), path: "wiki/index.md", contents: requireString(input.contents, "contents"), expectedHash: stringField(input.expectedHash), summary: "Update index", }); return { content: "Updated wiki/index.md", data: result }; }); ctx.tools.register("wiki_list_backlinks", { displayName: "List Wiki Backlinks", description: "Return indexed backlinks for a wiki page.", parametersSchema: ctx.manifest.tools?.find((tool) => tool.name === "wiki_list_backlinks")?.parametersSchema ?? { type: "object" }, }, async (params: unknown): Promise => { const input = params as ToolParams; const companyId = requireString(input.companyId, "companyId"); const wikiId = normalizeWikiId(input.wikiId); const space = await resolveSpace(ctx, { companyId, wikiId, spaceSlug: input.spaceSlug as string | null | undefined }); const path = assertPagePath(requireString(input.path, "path")); const rows = await ctx.db.query<{ path: string; title: string | null }>( `SELECT path, title FROM ${tableName(ctx.db.namespace, "wiki_pages")} WHERE company_id = $1 AND wiki_id = $2 AND space_id = $4 AND backlinks ? $3 ORDER BY path LIMIT 200`, [companyId, wikiId, path, space.id], ); return { content: rows.length ? rows.map((row) => `${row.path}${row.title ? ` - ${row.title}` : ""}`).join("\n") : "No backlinks indexed.", data: { companyId, wikiId, spaceSlug: space.slug, path, backlinks: rows }, }; }); ctx.tools.register("wiki_list_pages", { displayName: "List Wiki Pages", description: "Return the known page index from plugin metadata.", parametersSchema: ctx.manifest.tools?.find((tool) => tool.name === "wiki_list_pages")?.parametersSchema ?? { type: "object" }, }, async (params: unknown): Promise => { const input = params as ToolParams; const companyId = requireString(input.companyId, "companyId"); const wikiId = normalizeWikiId(input.wikiId); const space = await resolveSpace(ctx, { companyId, wikiId, spaceSlug: input.spaceSlug as string | null | undefined }); const rows = await ctx.db.query<{ path: string; title: string | null; page_type: string | null }>( `SELECT path, title, page_type FROM ${tableName(ctx.db.namespace, "wiki_pages")} WHERE company_id = $1 AND wiki_id = $2 AND space_id = $3 ORDER BY path LIMIT 200`, [companyId, wikiId, space.id], ); return { content: rows.length ? rows.map((row) => `${row.path}${row.title ? ` - ${row.title}` : ""}`).join("\n") : "No pages indexed yet.", data: { companyId, wikiId, spaceSlug: space.slug, pages: rows }, }; }); } export function readCompanyIdFromParams(params: Record): string { return requireString(params.companyId, "companyId"); } const TEMPLATE_FILES = ["AGENTS.md", "IDEA.md"] as const; type WikiTemplateFile = (typeof TEMPLATE_FILES)[number]; function isTemplateFile(value: string): value is WikiTemplateFile { return (TEMPLATE_FILES as readonly string[]).includes(value); } export type WikiPageRow = { path: string; title: string | null; pageType: string | null; backlinkCount: number; sourceCount: number; contentHash: string | null; updatedAt: string; }; export type WikiSourceRow = { rawPath: string; title: string | null; sourceType: string; url: string | null; status: string; createdAt: string; }; const LOCAL_BROWSE_FILE_LIMIT = 2000; export type WikiOperationRow = { id: string; operationType: string; status: string; hiddenIssueId: string | null; hiddenIssueIdentifier: string | null; hiddenIssueTitle: string | null; hiddenIssueStatus: string | null; projectId: string | null; runIds: unknown[]; costCents: number; warnings: unknown[]; affectedPages: unknown[]; metadata: Record; createdAt: string; updatedAt: string; }; export async function listPages(ctx: PluginContext, input: { companyId: string; wikiId?: string | null; spaceSlug?: string | null; pageType?: string | null; includeRaw?: boolean; limit?: number | null; }): Promise<{ pages: WikiPageRow[]; sources: WikiSourceRow[] }> { const wikiId = normalizeWikiId(input.wikiId); const space = await resolveSpace(ctx, { companyId: input.companyId, wikiId, spaceSlug: input.spaceSlug }); const limit = normalizeLimit(input.limit, 500, LOCAL_BROWSE_FILE_LIMIT); const params: unknown[] = [input.companyId, wikiId, space.id]; let pageFilter = ""; if (input.pageType) { params.push(input.pageType); pageFilter = ` AND page_type = $${params.length}`; } params.push(limit); const limitIndex = params.length; const pageRows = await ctx.db.query<{ path: string; title: string | null; page_type: string | null; backlinks: unknown; source_refs: unknown; content_hash: string | null; updated_at: string; }>( `SELECT path, title, page_type, backlinks, source_refs, content_hash, updated_at::text AS updated_at FROM ${tableName(ctx.db.namespace, "wiki_pages")} WHERE company_id = $1 AND wiki_id = $2 AND space_id = $3${pageFilter} ORDER BY path LIMIT $${limitIndex}`, params, ); const readablePageRows = await filterReadableRows(ctx, input.companyId, space, pageRows, (row) => row.path); const pages: WikiPageRow[] = readablePageRows.map((row) => ({ path: row.path, title: row.title, pageType: row.page_type, backlinkCount: Array.isArray(row.backlinks) ? row.backlinks.length : 0, sourceCount: Array.isArray(row.source_refs) ? row.source_refs.length : 0, contentHash: row.content_hash, updatedAt: row.updated_at, })); let pagesWithLocalFiles = pages; if (!input.pageType) { const wikiFiles = await listLocalFiles(ctx, { companyId: input.companyId, space, relativePath: "wiki" }); pagesWithLocalFiles = mergeLocalPageRows(pages, wikiFiles); } let sources: WikiSourceRow[] = []; if (input.includeRaw) { sources = (await listSources(ctx, { companyId: input.companyId, wikiId, spaceSlug: space.slug, limit, onlyReadable: true })).sources; sources = mergeLocalSourceRows(sources, await listLocalFiles(ctx, { companyId: input.companyId, space, relativePath: "raw" })); } return { pages: pagesWithLocalFiles, sources }; } export async function listSources(ctx: PluginContext, input: { companyId: string; wikiId?: string | null; spaceSlug?: string | null; limit?: number | null; onlyReadable?: boolean; }): Promise<{ sources: WikiSourceRow[] }> { const wikiId = normalizeWikiId(input.wikiId); const space = await resolveSpace(ctx, { companyId: input.companyId, wikiId, spaceSlug: input.spaceSlug }); const limit = normalizeLimit(input.limit, 500, LOCAL_BROWSE_FILE_LIMIT); const rows = await ctx.db.query<{ raw_path: string; title: string | null; source_type: string; url: string | null; status: string; created_at: string }>( `SELECT raw_path, title, source_type, url, status, created_at::text AS created_at FROM ${tableName(ctx.db.namespace, "wiki_sources")} WHERE company_id = $1 AND wiki_id = $2 AND space_id = $4 ORDER BY created_at DESC LIMIT $3`, [input.companyId, wikiId, limit, space.id], ); const sourceRows = input.onlyReadable ? await filterReadableRows(ctx, input.companyId, space, rows, (row) => row.raw_path) : rows; return { sources: sourceRows.map((row) => ({ rawPath: row.raw_path, title: row.title, sourceType: row.source_type, url: row.url, status: row.status, createdAt: row.created_at, })), }; } export async function readWikiPage(ctx: PluginContext, input: { companyId: string; wikiId?: string | null; spaceSlug?: string | null; path: string }) { const wikiId = normalizeWikiId(input.wikiId); const space = await resolveSpace(ctx, { companyId: input.companyId, wikiId, spaceSlug: input.spaceSlug }); const path = assertWikiPath(input.path); const contents = await ctx.localFolders.readText(input.companyId, WIKI_ROOT_FOLDER_KEY, spaceRelativePath(space, path)); const meta = await ctx.db.query<{ title: string | null; page_type: string | null; backlinks: unknown; source_refs: unknown; updated_at: string }>( `SELECT title, page_type, backlinks, source_refs, updated_at::text AS updated_at FROM ${tableName(ctx.db.namespace, "wiki_pages")} WHERE company_id = $1 AND wiki_id = $2 AND space_id = $4 AND path = $3 LIMIT 1`, [input.companyId, wikiId, path, space.id], ); const row = meta[0] ?? null; return { wikiId, spaceSlug: space.slug, path, contents, title: row?.title ?? inferTitle(path, contents), pageType: row?.page_type ?? inferPageType(path), backlinks: Array.isArray(row?.backlinks) ? row?.backlinks : [], sourceRefs: Array.isArray(row?.source_refs) ? row?.source_refs : [], updatedAt: row?.updated_at ?? null, hash: contentHash(contents), }; } export async function readTemplate(ctx: PluginContext, input: { companyId: string; path: string }) { if (!isTemplateFile(input.path)) { throw new Error(`template path must be one of ${TEMPLATE_FILES.join(", ")}`); } try { const contents = await ctx.localFolders.readText(input.companyId, WIKI_ROOT_FOLDER_KEY, input.path); return { path: input.path, contents, hash: contentHash(contents), exists: true }; } catch (error) { return { path: input.path, contents: "", hash: null, exists: false, error: error instanceof Error ? error.message : String(error) }; } } export async function writeTemplate(ctx: PluginContext, input: { companyId: string; path: string; contents: string }) { if (!isTemplateFile(input.path)) { throw new Error(`template path must be one of ${TEMPLATE_FILES.join(", ")}`); } await ctx.localFolders.writeTextAtomic(input.companyId, WIKI_ROOT_FOLDER_KEY, input.path, input.contents); return { status: "ok", path: input.path, hash: contentHash(input.contents) }; } export type DistillationCursorRow = { id: string; sourceScope: string; scopeKey: string; projectId: string | null; projectName: string | null; projectColor: string | null; rootIssueId: string | null; rootIssueIdentifier: string | null; rootIssueTitle: string | null; lastProcessedAt: string | null; lastObservedAt: string | null; pendingEventCount: number; lastSourceHash: string | null; lastSuccessfulRunId: string | null; }; export type DistillationRunRow = { id: string; cursorId: string | null; workItemId: string | null; projectId: string | null; projectName: string | null; rootIssueId: string | null; rootIssueIdentifier: string | null; sourceWindowStart: string | null; sourceWindowEnd: string | null; sourceHash: string | null; status: string; costCents: number; retryCount: number; warnings: string[]; metadata: Record; operationIssueId: string | null; operationIssueIdentifier: string | null; operationIssueTitle: string | null; affectedPagePaths: string[]; createdAt: string; updatedAt: string; }; export type DistillationWorkItemRow = { id: string; workItemKind: string; status: string; priority: string; projectId: string | null; rootIssueId: string | null; metadata: Record; createdAt: string; updatedAt: string; }; export type DistillationPageBindingRow = { id: string; pagePath: string; projectId: string | null; projectName: string | null; rootIssueId: string | null; lastAppliedSourceHash: string | null; lastDistillationRunId: string | null; lastRunStatus: string | null; lastRunCompletedAt: string | null; lastRunSourceWindowEnd: string | null; lastRunSourceHash: string | null; metadata: Record; updatedAt: string; }; export type DistillationSourceSnapshotRow = { id: string; distillationRunId: string; sourceHash: string; maxCharacters: number; clipped: boolean; sourceRefs: unknown[]; metadata: Record; createdAt: string; }; export type DistillationOverview = { cursors: DistillationCursorRow[]; runs: DistillationRunRow[]; workItems: DistillationWorkItemRow[]; pageBindings: DistillationPageBindingRow[]; reviewWorkItems: DistillationWorkItemRow[]; counts: { cursors: number; runningRuns: number; failedRuns24h: number; reviewRequired: number; }; }; function jsonObject(value: unknown): Record { return value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : {}; } function jsonArray(value: unknown): unknown[] { return Array.isArray(value) ? value : []; } function affectedPagePathsFromRunMetadata(metadata: Record, fallbackBindings: DistillationPageBindingRow[], runId: string): string[] { const explicit = jsonArray(metadata.affectedPages ?? metadata.pagePaths ?? metadata.affected_pages) .map((entry) => { if (typeof entry === "string") return entry; if (entry && typeof entry === "object") { const path = (entry as Record).path; return typeof path === "string" ? path : null; } return null; }) .filter((value): value is string => Boolean(value)); if (explicit.length > 0) return Array.from(new Set(explicit)); const bindings = fallbackBindings .filter((binding) => binding.lastDistillationRunId === runId) .map((binding) => binding.pagePath); return Array.from(new Set(bindings)); } export async function getDistillationOverview(ctx: PluginContext, input: { companyId: string; wikiId?: string | null; spaceSlug?: string | null; limit?: number | null; }): Promise { const wikiId = normalizeWikiId(input.wikiId); const space = await resolveSpace(ctx, { companyId: input.companyId, wikiId, spaceSlug: input.spaceSlug }); const runLimit = normalizeLimit(input.limit ?? 25, 25, 200); const cursorRows = await ctx.db.query<{ id: string; source_scope: string; scope_key: string; project_id: string | null; project_name: string | null; project_color: string | null; root_issue_id: string | null; root_issue_identifier: string | null; root_issue_title: string | null; last_processed_at: string | null; last_observed_at: string | null; pending_event_count: number; last_source_hash: string | null; last_successful_run_id: string | null; }>( `SELECT cursor.id, cursor.source_scope, cursor.scope_key, cursor.project_id, project.name AS project_name, project.color AS project_color, cursor.root_issue_id, issue.identifier AS root_issue_identifier, issue.title AS root_issue_title, cursor.last_processed_at::text AS last_processed_at, cursor.last_observed_at::text AS last_observed_at, cursor.pending_event_count, cursor.last_source_hash, cursor.last_successful_run_id FROM ${distillationCursorTable(ctx)} cursor LEFT JOIN public.projects project ON project.id = cursor.project_id LEFT JOIN public.issues issue ON issue.id = cursor.root_issue_id WHERE cursor.company_id = $1 AND cursor.wiki_id = $2 AND cursor.space_id = $3 ORDER BY cursor.updated_at DESC LIMIT 200`, [input.companyId, wikiId, space.id], ); const runRows = await ctx.db.query<{ id: string; cursor_id: string | null; work_item_id: string | null; project_id: string | null; project_name: string | null; root_issue_id: string | null; root_issue_identifier: string | null; source_window_start: string | null; source_window_end: string | null; source_hash: string | null; status: string; cost_cents: number; retry_count: number; warnings: unknown; metadata: unknown; operation_issue_id: string | null; operation_issue_identifier: string | null; operation_issue_title: string | null; created_at: string; updated_at: string; }>( `SELECT run.id, run.cursor_id, run.work_item_id, run.project_id, project.name AS project_name, run.root_issue_id, root_issue.identifier AS root_issue_identifier, run.source_window_start::text AS source_window_start, run.source_window_end::text AS source_window_end, run.source_hash, run.status, run.cost_cents, run.retry_count, run.warnings, run.metadata, run.operation_issue_id, op_issue.identifier AS operation_issue_identifier, op_issue.title AS operation_issue_title, run.created_at::text AS created_at, run.updated_at::text AS updated_at FROM ${distillationRunTable(ctx)} run LEFT JOIN public.projects project ON project.id = run.project_id LEFT JOIN public.issues root_issue ON root_issue.id = run.root_issue_id LEFT JOIN public.issues op_issue ON op_issue.id = run.operation_issue_id WHERE run.company_id = $1 AND run.wiki_id = $2 AND run.space_id = $4 ORDER BY run.created_at DESC LIMIT $3`, [input.companyId, wikiId, runLimit, space.id], ); const workItemRows = await ctx.db.query<{ id: string; work_item_kind: string; status: string; priority: string; project_id: string | null; root_issue_id: string | null; metadata: unknown; created_at: string; updated_at: string; }>( `SELECT id, work_item_kind, status, priority, project_id, root_issue_id, metadata, created_at::text AS created_at, updated_at::text AS updated_at FROM ${distillationWorkItemTable(ctx)} WHERE company_id = $1 AND wiki_id = $2 AND space_id = $3 AND status IN ('pending', 'review_required', 'in_progress', 'failed') ORDER BY created_at DESC LIMIT 100`, [input.companyId, wikiId, space.id], ); const bindingRows = await ctx.db.query<{ id: string; page_path: string; project_id: string | null; project_name: string | null; root_issue_id: string | null; last_applied_source_hash: string | null; last_distillation_run_id: string | null; last_run_status: string | null; last_run_completed_at: string | null; last_run_source_window_end: string | null; last_run_source_hash: string | null; metadata: unknown; updated_at: string; }>( `SELECT binding.id, binding.page_path, binding.project_id, project.name AS project_name, binding.root_issue_id, binding.last_applied_source_hash, binding.last_distillation_run_id, run.status::text AS last_run_status, run.updated_at::text AS last_run_completed_at, run.source_window_end::text AS last_run_source_window_end, run.source_hash AS last_run_source_hash, binding.metadata, binding.updated_at::text AS updated_at FROM ${pageBindingTable(ctx)} binding LEFT JOIN public.projects project ON project.id = binding.project_id LEFT JOIN ${distillationRunTable(ctx)} run ON run.id = binding.last_distillation_run_id WHERE binding.company_id = $1 AND binding.wiki_id = $2 AND binding.space_id = $3 ORDER BY binding.updated_at DESC LIMIT 200`, [input.companyId, wikiId, space.id], ); const cursors: DistillationCursorRow[] = cursorRows.map((row) => ({ id: row.id, sourceScope: row.source_scope, scopeKey: row.scope_key, projectId: row.project_id, projectName: row.project_name, projectColor: row.project_color, rootIssueId: row.root_issue_id, rootIssueIdentifier: row.root_issue_identifier, rootIssueTitle: row.root_issue_title, lastProcessedAt: row.last_processed_at, lastObservedAt: row.last_observed_at, pendingEventCount: Number(row.pending_event_count ?? 0), lastSourceHash: row.last_source_hash, lastSuccessfulRunId: row.last_successful_run_id, })); const pageBindings: DistillationPageBindingRow[] = bindingRows.map((row) => ({ id: row.id, pagePath: row.page_path, projectId: row.project_id, projectName: row.project_name, rootIssueId: row.root_issue_id, lastAppliedSourceHash: row.last_applied_source_hash, lastDistillationRunId: row.last_distillation_run_id, lastRunStatus: row.last_run_status, lastRunCompletedAt: row.last_run_completed_at, lastRunSourceWindowEnd: row.last_run_source_window_end, lastRunSourceHash: row.last_run_source_hash, metadata: jsonObject(row.metadata), updatedAt: row.updated_at, })); const runs: DistillationRunRow[] = runRows.map((row) => { const metadata = jsonObject(row.metadata); return { id: row.id, cursorId: row.cursor_id, workItemId: row.work_item_id, projectId: row.project_id, projectName: row.project_name, rootIssueId: row.root_issue_id, rootIssueIdentifier: row.root_issue_identifier, sourceWindowStart: row.source_window_start, sourceWindowEnd: row.source_window_end, sourceHash: row.source_hash, status: row.status, costCents: Number(row.cost_cents ?? 0), retryCount: Number(row.retry_count ?? 0), warnings: jsonArray(row.warnings).map((entry) => (typeof entry === "string" ? entry : JSON.stringify(entry))), metadata, operationIssueId: row.operation_issue_id, operationIssueIdentifier: row.operation_issue_identifier, operationIssueTitle: row.operation_issue_title, affectedPagePaths: affectedPagePathsFromRunMetadata(metadata, pageBindings, row.id), createdAt: row.created_at, updatedAt: row.updated_at, }; }); const workItems: DistillationWorkItemRow[] = workItemRows.map((row) => ({ id: row.id, workItemKind: row.work_item_kind, status: row.status, priority: row.priority, projectId: row.project_id, rootIssueId: row.root_issue_id, metadata: jsonObject(row.metadata), createdAt: row.created_at, updatedAt: row.updated_at, })); const reviewWorkItems = workItems.filter((item) => item.status === "review_required" || item.workItemKind === "review"); const failedSince = Date.now() - 24 * 60 * 60 * 1000; const failedRuns24h = runs.filter((run) => { if (run.status !== "failed" && run.status !== "refused_cost_cap") return false; const updatedAt = run.updatedAt ? Date.parse(run.updatedAt) : Number.NaN; return Number.isFinite(updatedAt) ? updatedAt >= failedSince : true; }).length; return { cursors, runs, workItems, pageBindings, reviewWorkItems, counts: { cursors: cursors.length, runningRuns: runs.filter((run) => run.status === "running").length, failedRuns24h, reviewRequired: reviewWorkItems.length, }, }; } export async function getDistillationPageProvenance(ctx: PluginContext, input: { companyId: string; wikiId?: string | null; spaceSlug?: string | null; pagePath: string; }): Promise<{ binding: DistillationPageBindingRow | null; runs: DistillationRunRow[]; snapshot: DistillationSourceSnapshotRow | null; cursor: DistillationCursorRow | null; }> { const wikiId = normalizeWikiId(input.wikiId); const space = await resolveSpace(ctx, { companyId: input.companyId, wikiId, spaceSlug: input.spaceSlug }); const overview = await getDistillationOverview(ctx, { companyId: input.companyId, wikiId, spaceSlug: space.slug }); const binding = overview.pageBindings.find((row) => row.pagePath === input.pagePath) ?? null; if (!binding) { return { binding: null, runs: [], snapshot: null, cursor: null }; } const relatedRuns = overview.runs.filter((run) => { if (binding.lastDistillationRunId === run.id) return true; if (binding.projectId && run.projectId === binding.projectId) return true; if (binding.rootIssueId && run.rootIssueId === binding.rootIssueId) return true; return run.affectedPagePaths.includes(binding.pagePath); }); const cursor = overview.cursors.find((row) => { if (binding.rootIssueId && row.rootIssueId === binding.rootIssueId) return true; if (binding.projectId && row.projectId === binding.projectId) return true; return false; }) ?? null; let snapshot: DistillationSourceSnapshotRow | null = null; if (binding.lastDistillationRunId) { const snapshotRows = await ctx.db.query<{ id: string; distillation_run_id: string; source_hash: string; max_characters: number; clipped: boolean; source_refs: unknown; metadata: unknown; created_at: string; }>( `SELECT id, distillation_run_id, source_hash, max_characters, clipped, source_refs, metadata, created_at::text AS created_at FROM ${sourceSnapshotTable(ctx)} WHERE company_id = $1 AND wiki_id = $2 AND space_id = $4 AND distillation_run_id = $3 ORDER BY created_at DESC LIMIT 1`, [input.companyId, wikiId, binding.lastDistillationRunId, space.id], ); if (snapshotRows[0]) { const row = snapshotRows[0]; snapshot = { id: row.id, distillationRunId: row.distillation_run_id, sourceHash: row.source_hash, maxCharacters: Number(row.max_characters ?? 0), clipped: Boolean(row.clipped), sourceRefs: jsonArray(row.source_refs), metadata: jsonObject(row.metadata), createdAt: row.created_at, }; } } return { binding, runs: relatedRuns, snapshot, cursor }; } export async function listOperations(ctx: PluginContext, input: { companyId: string; wikiId?: string | null; spaceSlug?: string | null; operationType?: string | null; status?: string | null; limit?: number | null; }): Promise<{ operations: WikiOperationRow[] }> { const wikiId = normalizeWikiId(input.wikiId); const space = await resolveSpace(ctx, { companyId: input.companyId, wikiId, spaceSlug: input.spaceSlug }); const limit = normalizeLimit(input.limit, 50, 500); const params: unknown[] = [input.companyId, wikiId, space.id]; const filters: string[] = []; if (input.operationType && input.operationType !== "all") { params.push(input.operationType); filters.push(`op.operation_type = $${params.length}`); } if (input.status && input.status !== "all") { params.push(input.status); filters.push(`op.status = $${params.length}`); } params.push(limit); const filterSql = filters.length ? ` AND ${filters.join(" AND ")}` : ""; const rows = await ctx.db.query<{ id: string; operation_type: string; status: string; hidden_issue_id: string | null; hidden_issue_identifier: string | null; hidden_issue_title: string | null; hidden_issue_status: string | null; project_id: string | null; run_ids: unknown; cost_cents: number; warnings: unknown; affected_pages: unknown; metadata: unknown; created_at: string; updated_at: string; }>( `SELECT op.id, op.operation_type, op.status, op.hidden_issue_id, op.project_id, op.run_ids, op.cost_cents, op.warnings, op.affected_pages, op.metadata, op.created_at::text AS created_at, op.updated_at::text AS updated_at, issue.identifier AS hidden_issue_identifier, issue.title AS hidden_issue_title, issue.status::text AS hidden_issue_status FROM ${tableName(ctx.db.namespace, "wiki_operations")} op LEFT JOIN public.issues issue ON issue.id = op.hidden_issue_id WHERE op.company_id = $1 AND op.wiki_id = $2 AND op.space_id = $3${filterSql} ORDER BY op.created_at DESC LIMIT $${params.length}`, params, ); return { operations: rows.map((row) => ({ id: row.id, operationType: row.operation_type, status: row.status, hiddenIssueId: row.hidden_issue_id, hiddenIssueIdentifier: row.hidden_issue_identifier, hiddenIssueTitle: row.hidden_issue_title, hiddenIssueStatus: row.hidden_issue_status, projectId: row.project_id, runIds: Array.isArray(row.run_ids) ? row.run_ids : [], costCents: Number(row.cost_cents ?? 0), warnings: Array.isArray(row.warnings) ? row.warnings : [], affectedPages: Array.isArray(row.affected_pages) ? row.affected_pages : [], metadata: jsonObject(row.metadata), createdAt: row.created_at, updatedAt: row.updated_at, })), }; }