can1357/oh-my-pi · error

Managed skill "${name}" needs a non-empty body.

Error message

Managed skill "${name}" needs a non-empty body.

What it means

writeManagedSkill rejects a skill body that is empty after trimming. A managed skill without body content is useless and would produce an empty SKILL.md document.

Source

Thrown at packages/coding-agent/src/autolearn/managed-skills.ts:163

			throw new Error(`Managed skill "${name}" SKILL.md is a symlink; refusing to overwrite it.`);
		}
		throw err;
	}
}

/** Create or update a managed `SKILL.md`. Returns the resolved file path. */
export async function writeManagedSkill(input: WriteManagedSkillInput): Promise<{ path: string }> {
	const name = sanitizeSkillName(input.name);
	const description = sanitizeManagedDescription(input.description);
	const body = input.body.trim();
	// Reject empty content: an all-whitespace/control description sanitizes to ""
	// and the `requireDescription` discovery scan then silently drops the skill,
	// so the tool would report success for a skill that never appears.
	if (!description) {
		throw new Error(`Managed skill "${name}" needs a non-empty description.`);
	}
	if (!body) {
		throw new Error(`Managed skill "${name}" needs a non-empty body.`);
	}
	const content = `${toSkillFrontmatter(name, description)}\n${body}\n`;
	// Cap the UTF-8 byte size of the FINAL file (body + description + frontmatter),
	// not the UTF-16 code-unit length of the body alone.
	const bytes = Buffer.byteLength(content, "utf8");
	if (bytes > MAX_MANAGED_SKILL_BYTES) {
		throw new Error(
			`Managed skill is ${bytes} bytes; the limit is ${MAX_MANAGED_SKILL_BYTES}. Trim the body or description.`,
		);
	}
	return serializeSkillMutation(name, async () => {
		await assertManagedRootSafe();
		const dir = path.join(getManagedSkillsDir(), name);
		const file = path.join(dir, "SKILL.md");
		// Reject a symlinked skill directory: an intermediate symlink would let the
		// write escape the isolated managed root. lstat does not follow the final
		// component, so a symlinked `dir` is caught here.
		const dirStat = await fs.lstat(dir).catch(err => {

View on GitHub (pinned to 9690622007)

Solutions

  1. Supply the actual markdown body content for the skill
  2. Check the upstream generation step that produced the body for empty output
  3. Swap arguments if body and description were passed in the wrong order

Example fix

// before
writeManagedSkill({ name: "foo", description: "d", body: "   " });
// after
writeManagedSkill({ name: "foo", description: "d", body: "# Fix flaky tests\n\n1. Run the suite twice..." });
Defensive patterns

Strategy: validation

Validate before calling

if (!input.body || !input.body.trim()) throw new Error("skill body is empty");

Try / catch

try {
  await writeManagedSkill(input);
} catch (err) {
  if (String((err as Error).message).includes("non-empty body")) {
    // regenerate content or abort the skill creation
  } else throw err;
}

Prevention

When it happens

Trigger: Calling writeManagedSkill with body set to "", whitespace-only, or a string that trims to nothing.

Common situations: The generating agent produced frontmatter but no content; a template or upstream stream failed and left the body blank; the caller passed the wrong variable (e.g. description into body).

Understand the failure class

Background: "Missing required field" and "field is required" errors: why libraries reject payloads that omit mandatory fields — this error's family across 20 libraries.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/51c4290f17db8147. Report an issue: GitHub.