can1357/oh-my-pi · error

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

Error message

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

What it means

writeManagedSkill rejects a skill whose description sanitizes to an empty string. Managed descriptions must be non-empty because the discovery scan (requireDescription) silently drops skills without descriptions — the tool would otherwise report success for a skill that never appears.

Source

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

		return await fs.open(file, UPDATE_FILE_OPEN_FLAGS);
	} catch (err) {
		if ((err as { code?: string }).code === "ELOOP") {
			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

View on GitHub (pinned to 9690622007)

Solutions

  1. Provide a plain-text, one-line description with meaningful content
  2. Remove angle brackets, control characters, and backtick/tilde fences from the generated description before writing
  3. Check the sanitized result (sanitizeManagedDescription) is non-empty before calling

Example fix

// before
writeManagedSkill({ name: "foo", description: "<skills>...", body });
// after
writeManagedSkill({ name: "foo", description: "Repairs failing unit tests in the build pipeline", body });
Defensive patterns

Strategy: validation

Validate before calling

const desc = sanitizeManagedDescription(input.description);
if (!desc) throw new Error("description is empty after sanitization");

Try / catch

try {
  await writeManagedSkill(input);
} catch (err) {
  if (String((err as Error).message).includes("non-empty description")) {
    // regenerate or supply a plain-text description, then retry
  } else throw err;
}

Prevention

When it happens

Trigger: Calling writeManagedSkill with an empty/whitespace description, or a description made only of characters stripped by sanitizeManagedDescription (control chars, angle brackets, backticks/~~~ fences).

Common situations: Auto-learn generates a description consisting entirely of markup like `<system>` tags or code fences, which sanitization removes; the caller passes an empty description field.

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/37e1c7ac7b8d57be. Report an issue: GitHub.