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
- Supply the actual markdown body content for the skill
- Check the upstream generation step that produced the body for empty output
- 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
- Verify the generation step produced non-empty markdown before writing
- Check argument order — don't pass an empty string where body is expected
- Treat empty model output as a generation failure, not a valid skill
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
- Managed skill "${name}" needs a non-empty description.
- Report cannot be empty.
- Search scope entries must be non-empty paths or globs
- Empty report. ${reportIssueDeviceUsage()}
- Message must not be empty.
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/51c4290f17db8147.
Report an issue: GitHub.