sipeed/picoclaw · error

cannot create skill %q: skill already exists

Error message

cannot create skill %q: skill already exists

What it means

renderAppliedBody (pkg/evolution/apply.go:180) refuses change_kind "create" when backupCurrentSkill found an existing <workspace>/skills/<name>/SKILL.md. This guards idempotency: a create must never silently overwrite an already-deployed skill. The error is raised before any file write, so there are no side effects.

Source

Thrown at pkg/evolution/apply.go:180

	}
	if name != targetSkillName {
		return fmt.Errorf("skill frontmatter name %q does not match target skill %q", name, targetSkillName)
	}
	if strings.TrimSpace(fields["description"]) == "" {
		return fmt.Errorf("skill frontmatter description is required")
	}
	return nil
}

func allowsExistingFrontmatterFields(kind ChangeKind, hadOriginal bool) bool {
	return hadOriginal && (kind == ChangeKindAppend || kind == ChangeKindMerge)
}

func renderAppliedBody(draft SkillDraft, existingBody string, hadOriginal bool) (string, error) {
	switch draft.ChangeKind {
	case ChangeKindCreate:
		if hadOriginal {
			return "", fmt.Errorf("cannot create skill %q: skill already exists", draft.TargetSkillName)
		}
		return renderDeployableSkillBody(draft.BodyOrPatch), nil
	case ChangeKindReplace:
		if !hadOriginal {
			return "", fmt.Errorf("cannot replace skill %q: skill does not exist", draft.TargetSkillName)
		}
		return renderDeployableSkillBody(draft.BodyOrPatch), nil
	case ChangeKindAppend:
		patch, err := renderDeployablePatchBody(draft.BodyOrPatch, draft.TargetSkillName)
		if err != nil {
			return "", err
		}
		if !hadOriginal || strings.TrimSpace(existingBody) == "" {
			return renderDeployableSkillBody(draft.BodyOrPatch), nil
		}
		return strings.TrimRight(existingBody, "\n") + "\n\n" + strings.TrimLeft(patch, "\n"), nil
	case ChangeKindMerge:
		patch, err := renderDeployablePatchBody(draft.BodyOrPatch, draft.TargetSkillName)

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Change the draft's ChangeKind to replace (overwrite intent) or append/merge (additive intent)
  2. Delete or rename the existing <workspace>/skills/<name>/SKILL.md if a fresh create is truly intended
  3. Pick a different TargetSkillName for the new skill
  4. Filter out create drafts whose target already exists before running apply

Example fix

// before
draft.ChangeKind = evolution.ChangeKindCreate // target skill already on disk

// after (choose one intent)
draft.ChangeKind = evolution.ChangeKindReplace  // overwrite
draft.ChangeKind = evolution.ChangeKindAppend   // add a section
Defensive patterns

Strategy: validation

Validate before calling

func skillExists(workspace, name string) bool {
	_, err := os.Stat(filepath.Join(workspace, "skills", name, "SKILL.md"))
	return err == nil
}

if draft.ChangeKind == evolution.ChangeKindCreate && skillExists(ws, draft.TargetSkillName) {
	// choose replace/append, rename, or skip — do not call ApplyDraft

Try / catch

if err := applier.ApplyDraft(ctx, ws, draft); err != nil {
	if strings.Contains(err.Error(), "skill already exists") {
		// flip ChangeKind to replace/append or pick a new TargetSkillName
	}
}

Prevention

When it happens

Trigger: ApplyDraft with ChangeKindCreate when the target skill file exists — re-running an evolution pass after an earlier accepted create; a manually authored skill with the same name; two evolution runs racing on the same workspace.

Common situations: Retrying a pipeline run that already applied; name collisions between generated and hand-written skills; drafts generated while the skill was concurrently created by another process.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/f3baf5246b44de25. Report an issue: GitHub.