{"record":{"id":"901bacf177e9b2aa","repo":"sipeed/picoclaw","slug":"resolve-lifecycle-delete-workspace-for-skill-q-w","errorCode":null,"errorMessage":"resolve lifecycle delete workspace for skill %q: workspace is required","messagePattern":"resolve lifecycle delete workspace for skill %q: workspace is required","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/evolution/lifecycle.go","lineNumber":53,"sourceCode":"\t\tif idle > 365*24*time.Hour && profile.RetentionScore < 0.1 {\n\t\t\treturn SkillStatusDeleted\n\t\t}\n\t}\n\n\treturn profile.Status\n}\n\nfunc ApplyLifecycleState(paths Paths, profile SkillProfile, next SkillStatus) error {\n\tif next != SkillStatusDeleted {\n\t\treturn nil\n\t}\n\n\tworkspace := profile.WorkspaceID\n\tif workspace == \"\" {\n\t\tworkspace = inferWorkspaceFromPaths(paths)\n\t}\n\tif workspace == \"\" {\n\t\treturn fmt.Errorf(\"resolve lifecycle delete workspace for skill %q: workspace is required\", profile.SkillName)\n\t}\n\tif err := skills.ValidateSkillName(profile.SkillName); err != nil {\n\t\treturn fmt.Errorf(\"resolve lifecycle delete skill name: %w\", err)\n\t}\n\n\tskillPath := filepath.Join(workspace, \"skills\", profile.SkillName, \"SKILL.md\")\n\terr := os.Remove(skillPath)\n\tif errors.Is(err, os.ErrNotExist) {\n\t\treturn nil\n\t}\n\treturn err\n}\n\nfunc RunLifecycleOnce(store *Store, paths Paths, workspace string, now time.Time) (LifecycleRunSummary, error) {\n\tif store == nil {\n\t\treturn LifecycleRunSummary{}, nil\n\t}\n","sourceCodeStart":35,"sourceCodeEnd":71,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/evolution/lifecycle.go#L35-L71","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Set WorkspaceID on stored profiles (or backfill it) before running the lifecycle","Construct Paths with evolution.NewPaths(workspace, \"\") so RootDir resolves to <workspace>/state/evolution","Re-layout the custom RootDir as <workspace>/state/evolution","Run profile sync first to populate workspace_id on legacy profiles"],"exampleFix":"// before\npaths := evolution.Paths{RootDir: \"/tmp/custom-state\"}\nerr := evolution.ApplyLifecycleState(paths, profile, evolution.SkillStatusDeleted)\n\n// after\npaths := evolution.NewPaths(workspace, \"\") // RootDir = <workspace>/state/evolution\nerr := evolution.ApplyLifecycleState(paths, profile, evolution.SkillStatusDeleted)","handlingStrategy":"validation","validationCode":"func lifecycleWorkspaceResolvable(paths evolution.Paths, profile evolution.SkillProfile) bool {\n\tif profile.WorkspaceID != \"\" {\n\t\treturn true\n\t}\n\troot := filepath.Clean(paths.RootDir)\n\treturn filepath.Base(root) == \"evolution\" && filepath.Base(filepath.Dir(root)) == \"state\"\n}\n\nif !lifecycleWorkspaceResolvable(paths, profile) {\n\t// set profile.WorkspaceID or rebuild paths via evolution.NewPaths(workspace, \"\")","typeGuard":null,"tryCatchPattern":"if err := evolution.ApplyLifecycleState(paths, profile, evolution.SkillStatusDeleted); err != nil {\n\tif strings.Contains(err.Error(), \"workspace is required\") {\n\t\t// populate WorkspaceID on profiles or fix the state-dir layout\n\t}\n}","preventionTips":["Always build Paths with NewPaths(workspace, \"\") instead of hand-assembling RootDir","Backfill workspace_id on profiles written by older versions before enabling lifecycle runs","Keep the state layout as <workspace>/state/evolution — inference depends on the literal path shape"],"tags":["go","lifecycle","workspace","path-layout","config"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}