nexu-io/open-design · error

proposal patch.after markdown is required

Error message

proposal patch.after markdown is required

What it means

Thrown by proposalAfterMarkdown() when proposal.patch.after is not a non-empty string. This helper backs applyDesignSystemProposal and applySkillProposal, both of which write the after value verbatim as DESIGN.md / SKILL.md content. An empty markdown body would create an empty file, so it is rejected.

Source

Thrown at apps/daemon/src/automation-proposals.ts:231

  return proposal.metadata && typeof proposal.metadata === 'object' && !Array.isArray(proposal.metadata)
    ? proposal.metadata as Record<string, unknown>
    : {};
}

function targetSlugFor(proposal: AutomationEvolutionProposal, kind: 'design-system' | 'skill'): string {
  const expected = kind === 'design-system'
    ? /^design-systems\/([^/]+)\/DESIGN\.md$/
    : /^skills\/([^/]+)\/SKILL\.md$/;
  const fromRef = typeof proposal.targetRef === 'string' ? expected.exec(proposal.targetRef)?.[1] : '';
  if (fromRef && SAFE_SLUG.test(fromRef)) return fromRef;
  const metadata = metadataRecord(proposal);
  if (typeof metadata.slug === 'string' && SAFE_SLUG.test(metadata.slug)) return metadata.slug;
  return slugifyTarget(proposal.title);
}

function proposalAfterMarkdown(proposal: AutomationEvolutionProposal): string {
  if (typeof proposal.patch.after !== 'string' || !proposal.patch.after.trim()) {
    throw new Error('proposal patch.after markdown is required');
  }
  return proposal.patch.after.trimEnd() + '\n';
}

async function applyDesignSystemProposal(dataDir: string, proposal: AutomationEvolutionProposal) {
  const slug = targetSlugFor(proposal, 'design-system');
  const dir = path.join(dataDir, 'design-systems', slug);
  const file = path.join(dir, 'DESIGN.md');
  if (proposal.action === 'delete') {
    await fsp.rm(dir, { recursive: true, force: true });
    return { designSystemId: slug, action: 'delete', path: `design-systems/${slug}/DESIGN.md` };
  }
  await fsp.mkdir(dir, { recursive: true });
  await fsp.writeFile(file, proposalAfterMarkdown(proposal), 'utf8');
  return { designSystemId: slug, action: proposal.action, path: `design-systems/${slug}/DESIGN.md` };
}

async function applySkillProposal(dataDir: string, proposal: AutomationEvolutionProposal) {

View on GitHub (pinned to 5be4028344)

Solutions

  1. Set patch.after to the full markdown body for DESIGN.md / SKILL.md before applying.
  2. Reject the proposal if no real content exists yet.
  3. Mirror the markdown shape produced by buildDesignSystemProposalMarkdown / buildSkillProposalMarkdown in automation-ingestions.ts.

Example fix

// before
{ targetKind: 'skill', action: 'create', patch: { format: 'markdown', after: '' } }
// after
{ targetKind: 'skill', action: 'create', patch: { format: 'markdown', after: '# My Skill\n\n...body...' } }
Defensive patterns

Strategy: validation

Validate before calling

if (typeof proposal.patch.after !== 'string' || !proposal.patch.after.trim()) {
  throw new Error('proposal patch.after markdown is required');
}

Type guard

function hasMarkdownAfter(patch: unknown): patch is { after: string } {
  return !!patch && typeof (patch as any).after === 'string' && (patch as any).after.trim().length > 0;
}

Prevention

When it happens

Trigger: Applying a design-system or skill create/update proposal whose patch.format is 'markdown' but patch.after is missing, empty, whitespace-only, or not a string. (For these target kinds, format is expected to be 'markdown'.)

Common situations: Agent created the proposal with a placeholder after it intended to fill later; patch.after was cleared during an edit; proposal built from a template whose body field was empty.

Related errors


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/7bac77d37bbe5d47. Report an issue: GitHub.