paperclipai/paperclip · error · Error

projectId or rootIssueId is required

Error message

projectId or rootIssueId is required

What it means

Thrown by distillPaperclipProjectPage when neither input.projectId nor input.rootIssueId is provided. The function renders a project wiki page (index.md/standup.md) and needs a concrete scope to name the slug and directory. The guard fires before policy evaluation so the request fails fast.

Source

Thrown at packages/plugins/plugin-llm-wiki/src/wiki/core.ts:3338

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)) {

View on GitHub (pinned to 67001ec6eb)

Solutions

  1. Pass projectId (preferred) or rootIssueId on every distillPaperclipProjectPage input.
  2. For distillation triggered from an issue, resolve the issue's projectId from ctx.issues.get and forward it.
  3. Validate the scope at the caller (worker action) before invoking the core function so users get a clearer upstream error.

Example fix

// before
distillPaperclipProjectPage(ctx, { companyId, wikiId, spaceSlug });
// after
distillPaperclipProjectPage(ctx, { companyId, wikiId, spaceSlug, projectId });
Defensive patterns

Strategy: validation

Validate before calling

function assertProjectPageScope(input: { projectId?: string | null; rootIssueId?: string | null }) {
  if (!input.projectId && !input.rootIssueId) throw new Error('projectId or rootIssueId is required');
}

Type guard

function hasProjectPageScope(input: unknown): input is { projectId: string } | { rootIssueId: string } {
  return !!input && typeof input === 'object' && (
    !!(input as any).projectId || !!(input as any).rootIssueId
  );
}

Try / catch

try {
  await distillPaperclipProjectPage(ctx, input);
} catch (err) {
  if (err instanceof Error && err.message === 'projectId or rootIssueId is required') {
    // resolve projectId from the issue and retry
  } else throw err;
}

Prevention

When it happens

Trigger: Calling distillPaperclipProjectPage with both projectId and rootIssueId absent or empty, e.g. a distill worker action that did not forward the scope params.

Common situations: A worker action param mapping drops projectId; an issue-driven distillation run where the source issue is not attached to a project and has no root-issue lineage; misconfigured routine passing only companyId.

Related errors


AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12). Data as JSON: /api/errors/227fa6e6091ec101. Report an issue: GitHub.