{"record":{"id":"836bb600ce8351b8","repo":"gastownhall/beads","slug":"workspacegate-gate-parent-s-must-exist-w","errorCode":null,"errorMessage":"workspacegate: gate parent %s must exist: %w","messagePattern":"workspacegate: gate parent (.+?) must exist: %w","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/workspacegate/gate.go","lineNumber":189,"sourceCode":"\t// placing the gate beside it. So identity derives from the\n\t// canonicalized PARENT plus the literal base name, never from\n\t// resolving the guarded path itself: full-path resolution would\n\t// silently select a different gate once the directory appears as a\n\t// symlink, letting two exclusive holders coexist. The flip side is\n\t// that a guarded directory that IS a symlink has no stable identity\n\t// under this scheme, so it is refused outright rather than gated\n\t// ambiguously.\n\tif fi, err := os.Lstat(abs); err == nil && fi.Mode()&os.ModeSymlink != 0 {\n\t\treturn Gate{}, fmt.Errorf(\"workspacegate: %s is a symlink; gate the physical directory it points to\", abs)\n\t}\n\tparent, base := filepath.Split(abs)\n\tswitch base {\n\tcase \"\", \".\", \"..\":\n\t\treturn Gate{}, fmt.Errorf(\"workspacegate: cannot gate %q: the guarded path must be a named directory\", dir)\n\t}\n\tcanonParent, err := filepath.EvalSymlinks(filepath.Clean(parent))\n\tif err != nil {\n\t\treturn Gate{}, fmt.Errorf(\"workspacegate: gate parent %s must exist: %w\", parent, err)\n\t}\n\treturn Gate{path: filepath.Join(canonParent, gateFileName(base))}, nil\n}\n\n// ForWorkspace returns the gate guarding a workspace's .beads directory\n// (pass the .beads directory itself). The gate file is a sibling of\n// .beads; bd's project gitignore management covers \"*.gate.lock*\".\nfunc ForWorkspace(beadsDir string) (Gate, error) { return forDir(beadsDir) }\n\n// ForPhysicalRoot returns the gate guarding a physical database root (a\n// dolt server root such as .beads/dolt or ~/.beads/shared-server/dolt).\n// Distinct workspaces that point at the same physical root resolve to the\n// same gate file, which is the point: a workspace-level gate alone cannot\n// stop workspace B from restarting the server workspace A is draining.\n//\n// Cross-user shared roots are unsupported: the gate file is created 0o600\n// (see Acquire), so a second OS user attempting to gate a shared root such\n// as ~/.beads/shared-server/dolt hits EACCES on the sibling gate file, not","sourceCodeStart":171,"sourceCodeEnd":207,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/workspacegate/gate.go#L171-L207","documentation":"forDir canonicalizes the gate file's PARENT directory with filepath.EvalSymlinks; the parent must exist even though the guarded directory itself may not (bd init gates a workspace it is creating). If EvalSymlinks fails (ENOENT, ENOTDIR, permission), no stable gate location can be computed, so construction fails with the underlying OS error wrapped.","triggerScenarios":"Calling ForWorkspace(beadsDir) or ForPhysicalRoot(root) where the parent directory of the given path does not exist (e.g. ForWorkspace(\"/repo/.beads\") before /repo exists), the parent is actually a file (ENOTDIR), or a permission error blocks resolution.","commonSituations":"Running bd init in a not-yet-cloned repo path; a typo'd workspace root in config; mounting/renaming the workspace between path computation and gating; running as a user without traverse permission on the parent.","solutions":["Create the parent directory first: os.MkdirAll(filepath.Dir(beadsDir), 0o755) before calling ForWorkspace.","Verify the workspace root path spelling — the parent must exist, only the guarded .beads dir itself may be missing.","Check that no component of the parent is a regular file (ENOTDIR); fix or remove the offending entry.","If permissions are the cause, run as a user with execute/search permission on every ancestor directory."],"exampleFix":"// before\ng, err := workspacegate.ForWorkspace(beadsDir)\n// after\nif err := os.MkdirAll(filepath.Dir(filepath.Clean(beadsDir)), 0o755); err != nil {\n    return err\n}\ng, err := workspacegate.ForWorkspace(beadsDir)","handlingStrategy":"validation","validationCode":"func ensureGateParent(dir string) error {\n    parent := filepath.Dir(filepath.Clean(dir))\n    fi, err := os.Stat(parent)\n    if err != nil {\n        return fmt.Errorf(\"gate parent %s: %w\", parent, err)\n    }\n    if !fi.IsDir() {\n        return fmt.Errorf(\"gate parent %s is not a directory\", parent)\n    }\n    return nil\n}","typeGuard":null,"tryCatchPattern":"g, err := workspacegate.ForWorkspace(beadsDir)\nif err != nil {\n    var pe *fs.PathError\n    if errors.As(err, &pe) && errors.Is(pe.Err, fs.ErrNotExist) {\n        return fmt.Errorf(\"workspace root missing: %w\", err)\n    }\n    return err\n}","preventionTips":["MkdirAll the workspace root before initializing .beads.","Validate configured workspace paths at startup (stat the parent).","Remember only the guarded dir may be missing — its parent must exist.","Watch for ENOTDIR: a file where a directory is expected also triggers this."],"tags":["go","filesystem","missing-directory","workspacegate"],"backgroundTag":"parent-directory-missing","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}