Skip to content

Scope

dotagents._scope

Scope and overlay-source resolution for dotagents overlays.

Two orthogonal axes the overlays command needs, kept out of cli.py (which only wires args) to match _overlays.py / _skills.py / _sync.py:

  • Scope -- where installed overlays live. user is <agents_dir>/ (the configurable store, default ~/.agents); project is <project>/.agents/. An overlay installs into <scope>/overlays/<name>/ and skills publish into the shared <scope>/skills/. There is no registry file: installed overlays are discovered by their presence under overlays/ (the locked "discover, don't track" decision). A system tier (/etc/agents) is designed-for but not built.

  • Source -- where an overlay to install comes from. resolve_source returns an OverlaySource whose .root is a local directory of <name>/ overlay dirs. The default is the bundled overlays/ (resolved .pyz-safe via importlib.resources, mirroring cli._package_data_dir); an explicit --source / $AGENTS_OVERLAYS_SRC overrides it. A git/URI source is a later swap of the resolver's default -- resolve_source is the single extension point (clone/pull into a cache, then hand back a local .root), so a repo source drops in with no change to the command classes.

Never print DOTAGENTS_* values (Leakage): this module reads the env var but only ever reports the resolved path, never the raw value.

SOURCE_ENV = 'AGENTS_OVERLAYS_SRC' module-attribute

SOURCE_ENV_LEGACY = 'DOTAGENTS_OVERLAYS_SRC' module-attribute

OverlaySource(root)

A resolved place overlays are fetched from for add/sync.

root is a local directory holding <name>/ overlay dirs. available() lists them; overlay_dir(name) resolves one (raising if absent). This is the seam a future git/URI source slots into: a GitOverlaySource would clone/pull into a cache in __init__ and set root to that checkout -- the command classes only ever touch this interface, so they need no change.

Source code in src/dotagents/_scope.py
180
181
def __init__(self, root: Path):
    self.root = Path(root)

root = Path(root) instance-attribute

__repr__()

Source code in src/dotagents/_scope.py
201
202
def __repr__(self) -> str:
    return "OverlaySource(%s)" % self.root

available()

Source code in src/dotagents/_scope.py
183
184
185
186
187
188
189
190
def available(self) -> "list[str]":
    if not self.root.is_dir():
        return []
    return sorted(
        p.name
        for p in self.root.iterdir()
        if p.is_dir() and not p.name.startswith(".")
    )

overlay_dir(name)

Source code in src/dotagents/_scope.py
192
193
194
195
196
197
198
199
def overlay_dir(self, name: str) -> Path:
    candidate = self.root / name
    if not candidate.is_dir():
        raise SystemExit(
            "error: overlay %r not found in source %s (available: %s)"
            % (name, self.root, ", ".join(self.available()) or "none")
        )
    return candidate

Scope(level, agents_root)

A resolved install scope: the roots overlays and skills live under.

user -> <agents_dir>/ (the configurable store, default ~/.agents). project -> <project_root>/.agents/. The overlays/ and skills/ subdirs beneath agents_root are the discover-and-publish surfaces.

Source code in src/dotagents/_scope.py
68
69
70
def __init__(self, level: str, agents_root: Path):
    self.level = level
    self.agents_root = Path(agents_root)

agents_root = Path(agents_root) instance-attribute

cmds_dir property

Directory of discovered command modules for this scope (D76).

<agents_root>/dotagents/cmds -- a seam alongside overlays/skills. init/install lay the bundled command modules here, and dotagents.cli._discover runs duho.discover_commands over it (per scope, user + project) so a user's own *.py command modules dropped beside them are picked up with zero config.

level = level instance-attribute

overlay_root property

shared_skills_dir property

__repr__()

Source code in src/dotagents/_scope.py
94
95
def __repr__(self) -> str:
    return "Scope(%s, root=%s)" % (self.level, self.agents_root)

overlay_dir(name)

Source code in src/dotagents/_scope.py
91
92
def overlay_dir(self, name: str) -> Path:
    return self.overlay_root / name

bundled_overlays_root()

Locate the bundled example overlays/ directory, .pyz-safe.

Two homes, tried in order: the packaged copy (importlib.resources under the installed dotagents package -- extracted from a zipapp when needed, exactly like cli._package_data_dir), then a repo checkout's top-level overlays/ (dev use, mirroring BuildPyz's parents[2] reach). Returns None if neither exists -- a plain pip install that bundled no overlays.

Source code in src/dotagents/_scope.py
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
def bundled_overlays_root() -> "Path | None":
    """Locate the bundled example ``overlays/`` directory, ``.pyz``-safe.

    Two homes, tried in order: the packaged copy (``importlib.resources`` under the
    installed ``dotagents`` package -- extracted from a zipapp when needed, exactly
    like ``cli._package_data_dir``), then a repo checkout's top-level ``overlays/``
    (dev use, mirroring ``BuildPyz``'s ``parents[2]`` reach). Returns ``None`` if
    neither exists -- a plain ``pip install`` that bundled no overlays.
    """
    # Prefer the shared resolver in cli so zipapp extraction is cached once.
    try:
        from dotagents.cli import _package_data_dir

        packaged = _package_data_dir("_overlays_src")
        if packaged is not None and packaged.is_dir():
            return packaged
    except Exception:
        pass

    repo_overlays = Path(__file__).resolve().parents[2] / "overlays"
    if repo_overlays.is_dir():
        return repo_overlays
    return None

discover_overlays(scope)

Installed overlay names in this scope -- the presence of overlays/<name>/.

No registry: a directory under <scope>/overlays/ is an installed overlay (manifest or not) as long as its name is a valid overlay name (:func:is_valid_overlay_name -- so .git/__pycache__/dotfiles are skipped). Returns sorted names; empty if the root is absent.

Source code in src/dotagents/_scope.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
def discover_overlays(scope: Scope) -> "list[str]":
    """Installed overlay names in this scope -- the presence of ``overlays/<name>/``.

    No registry: a directory under ``<scope>/overlays/`` *is* an installed overlay
    (manifest or not) as long as its name is a valid overlay name
    (:func:`is_valid_overlay_name` -- so ``.git``/``__pycache__``/dotfiles are
    skipped). Returns sorted names; empty if the root is absent.
    """
    root = scope.overlay_root
    if not root.is_dir():
        return []
    return sorted(
        p.name for p in root.iterdir() if p.is_dir() and is_valid_overlay_name(p.name)
    )

filter_names(names, pattern)

Glob-filter overlay names (sync 'py*'). None/'*' keep all.

Source code in src/dotagents/_scope.py
163
164
165
166
167
def filter_names(names: "list[str]", pattern: "Optional[str]") -> "list[str]":
    """Glob-filter overlay names (``sync 'py*'``). ``None``/``'*'`` keep all."""
    if not pattern or pattern == "*":
        return list(names)
    return [n for n in names if fnmatch.fnmatch(n, pattern)]

is_valid_overlay_name(name)

A dir under overlays/ is an overlay iff its name matches: a leading ASCII letter, then ASCII letters/digits/_/./-. Dots are allowed mid-name (foo.bar, v1.2) but NOT as the first char, so .git/.hidden and __pycache__ (leading _) and 2fast (leading digit) are excluded.

Source code in src/dotagents/_scope.py
45
46
47
48
49
50
def is_valid_overlay_name(name: str) -> bool:
    """A dir under ``overlays/`` is an overlay iff its name matches: a leading ASCII
    letter, then ASCII letters/digits/``_``/``.``/``-``. Dots are allowed mid-name
    (``foo.bar``, ``v1.2``) but NOT as the first char, so ``.git``/``.hidden`` and
    ``__pycache__`` (leading ``_``) and ``2fast`` (leading digit) are excluded."""
    return bool(_OVERLAY_NAME_RE.match(name))

normalize_overlay_name(name)

Overlay names normalize to lowercase-dash for matching (precursor rule): name.lower().replace('_', '-'). So My_Overlay and my-overlay are the same overlay.

Source code in src/dotagents/_scope.py
53
54
55
56
57
def normalize_overlay_name(name: str) -> str:
    """Overlay names normalize to lowercase-dash for matching (precursor rule):
    ``name.lower().replace('_', '-')``. So ``My_Overlay`` and ``my-overlay`` are
    the same overlay."""
    return name.lower().replace("_", "-")

project_root_default()

The project root when none is passed explicitly, in precedence order: $AGENTS_PROJECT_ROOT (dotagents' canonical var) -> a known agent-native var (:data:_HARNESS_PROJECT_ROOT_VARS, e.g. Claude Code's CLAUDE_PROJECT_DIR) -> the current working directory.

Emitting AGENTS_PROJECT_ROOT (see dotagents env) lets every command and subprocess agree on one root regardless of the cwd it happens to run in.

Source code in src/dotagents/_scope.py
132
133
134
135
136
137
138
139
140
141
142
143
144
def project_root_default() -> Path:
    """The project root when none is passed explicitly, in precedence order:
    ``$AGENTS_PROJECT_ROOT`` (dotagents' canonical var) -> a known agent-native var
    (:data:`_HARNESS_PROJECT_ROOT_VARS`, e.g. Claude Code's ``CLAUDE_PROJECT_DIR``)
    -> the current working directory.

    Emitting ``AGENTS_PROJECT_ROOT`` (see ``dotagents env``) lets every command and
    subprocess agree on one root regardless of the cwd it happens to run in."""
    for var in ("AGENTS_PROJECT_ROOT", *_HARNESS_PROJECT_ROOT_VARS):
        value = os.environ.get(var)
        if value:
            return Path(value).expanduser()
    return Path.cwd()

resolve_scope(global_scope=False, *, agents_dir=None, project_root=None)

Pick the install scope.

-g/--global forces the user scope (agents_dir, default ~/.agents). Otherwise the scope is project, rooted at (in precedence order): an explicit project_root argument, else $AGENTS_PROJECT_ROOT if set, else the current directory. $AGENTS_PROJECT_ROOT lets a harness (or dotagents env) pin the project root once so every command agrees on it regardless of the cwd a subprocess happens to run in; <root>/.agents/ is where this project's overlays live. The store location is configurable (D58): agents_dir comes from the caller (--agents-dir) or defaults to ~/.agents; never hardcoded past that default.

Source code in src/dotagents/_scope.py
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
def resolve_scope(
    global_scope: bool = False,
    *,
    agents_dir: "str | os.PathLike | None" = None,
    project_root: "str | os.PathLike | None" = None,
) -> Scope:
    """Pick the install scope.

    ``-g/--global`` forces the **user** scope (``agents_dir``, default ``~/.agents``).
    Otherwise the scope is **project**, rooted at (in precedence order): an explicit
    ``project_root`` argument, else ``$AGENTS_PROJECT_ROOT`` if set, else the current
    directory. ``$AGENTS_PROJECT_ROOT`` lets a harness (or ``dotagents env``) pin the
    project root once so every command agrees on it regardless of the cwd a subprocess
    happens to run in; ``<root>/.agents/`` is where this project's overlays live. The
    store location is configurable (D58): ``agents_dir`` comes from the caller
    (``--agents-dir``) or defaults to ``~/.agents``; never hardcoded past that default.
    """
    root = Path(agents_dir).expanduser() if agents_dir else (Path.home() / ".agents")
    if global_scope:
        return Scope("user", root)
    proj = Path(project_root).expanduser() if project_root else project_root_default()
    return Scope("project", proj / ".agents")

resolve_source(source=None)

Resolve the overlay source: explicit --source / $AGENTS_OVERLAYS_SRC, else the bundled overlays/.

Today every branch yields a local directory OverlaySource. The single extension point for a git/URI source is here: when raw looks like a URI (a later change), construct a GitOverlaySource instead -- callers are unaffected because they only use the returned object's available/overlay_dir.

Source code in src/dotagents/_scope.py
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
def resolve_source(source: "Optional[str]" = None) -> OverlaySource:
    """Resolve the overlay source: explicit ``--source`` / ``$AGENTS_OVERLAYS_SRC``,
    else the bundled ``overlays/``.

    Today every branch yields a **local directory** ``OverlaySource``. The single
    extension point for a git/URI source is here: when ``raw`` looks like a URI (a
    later change), construct a ``GitOverlaySource`` instead -- callers are unaffected
    because they only use the returned object's ``available``/``overlay_dir``.
    """
    raw = (
        source
        or os.environ.get(SOURCE_ENV)
        or os.environ.get(SOURCE_ENV_LEGACY)
        or None
    )
    if raw:
        # (extension point) a URI/git ``raw`` would branch to a cached clone here.
        root = Path(raw).expanduser()
        if not root.is_dir():
            raise SystemExit("error: --source path is not a directory: %s" % raw)
        return OverlaySource(root)

    bundled = bundled_overlays_root()
    if bundled is None:
        raise SystemExit(
            "error: no overlay source. This build bundles no overlays; pass "
            "--source <dir> or set %s to a directory of overlays." % SOURCE_ENV
        )
    return OverlaySource(bundled)