sipeed/picoclaw · error

resolve lifecycle delete workspace for skill %q: workspace i

Error message

resolve lifecycle delete workspace for skill %q: workspace is required

What it means

ApplyLifecycleState (pkg/evolution/lifecycle.go:53) must build <workspace>/skills/<name>/SKILL.md to delete a skill transitioning to "deleted". The workspace root comes from profile.WorkspaceID, falling back to inferWorkspaceFromPaths, which only succeeds when paths.RootDir is laid out literally as <workspace>/state/evolution. When both are empty the delete is refused instead of guessing a path.

Source

Thrown at pkg/evolution/lifecycle.go:53

		if idle > 365*24*time.Hour && profile.RetentionScore < 0.1 {
			return SkillStatusDeleted
		}
	}

	return profile.Status
}

func ApplyLifecycleState(paths Paths, profile SkillProfile, next SkillStatus) error {
	if next != SkillStatusDeleted {
		return nil
	}

	workspace := profile.WorkspaceID
	if workspace == "" {
		workspace = inferWorkspaceFromPaths(paths)
	}
	if workspace == "" {
		return fmt.Errorf("resolve lifecycle delete workspace for skill %q: workspace is required", profile.SkillName)
	}
	if err := skills.ValidateSkillName(profile.SkillName); err != nil {
		return fmt.Errorf("resolve lifecycle delete skill name: %w", err)
	}

	skillPath := filepath.Join(workspace, "skills", profile.SkillName, "SKILL.md")
	err := os.Remove(skillPath)
	if errors.Is(err, os.ErrNotExist) {
		return nil
	}
	return err
}

func RunLifecycleOnce(store *Store, paths Paths, workspace string, now time.Time) (LifecycleRunSummary, error) {
	if store == nil {
		return LifecycleRunSummary{}, nil
	}

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Set WorkspaceID on stored profiles (or backfill it) before running the lifecycle
  2. Construct Paths with evolution.NewPaths(workspace, "") so RootDir resolves to <workspace>/state/evolution
  3. Re-layout the custom RootDir as <workspace>/state/evolution
  4. Run profile sync first to populate workspace_id on legacy profiles

Example fix

// before
paths := evolution.Paths{RootDir: "/tmp/custom-state"}
err := evolution.ApplyLifecycleState(paths, profile, evolution.SkillStatusDeleted)

// after
paths := evolution.NewPaths(workspace, "") // RootDir = <workspace>/state/evolution
err := evolution.ApplyLifecycleState(paths, profile, evolution.SkillStatusDeleted)
Defensive patterns

Strategy: validation

Validate before calling

func lifecycleWorkspaceResolvable(paths evolution.Paths, profile evolution.SkillProfile) bool {
	if profile.WorkspaceID != "" {
		return true
	}
	root := filepath.Clean(paths.RootDir)
	return filepath.Base(root) == "evolution" && filepath.Base(filepath.Dir(root)) == "state"
}

if !lifecycleWorkspaceResolvable(paths, profile) {
	// set profile.WorkspaceID or rebuild paths via evolution.NewPaths(workspace, "")

Try / catch

if err := evolution.ApplyLifecycleState(paths, profile, evolution.SkillStatusDeleted); err != nil {
	if strings.Contains(err.Error(), "workspace is required") {
		// populate WorkspaceID on profiles or fix the state-dir layout
	}
}

Prevention

When it happens

Trigger: profile.WorkspaceID == "" (profiles written before the field existed or cleared by a sync) combined with Paths built with a custom RootDir — its base directory is not `evolution` under a `state` directory (temp dirs, relocated state, test fixtures).

Common situations: Embedding RunLifecycleOnce with a custom state layout; profiles migrated from an older store schema; unit tests using t.TempDir() roots; running the lifecycle against a copy of the state directory.

Related errors


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