dotagents-cli 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,366 @@
1
+ """`dotagents overlays` -- add / remove / list / sync, with skills sync.
2
+
3
+ Self-contained block (own module deps in `_scope.py` / `_skills.py` /
4
+ `_overlays.py`); the only umbrella touch is registering `Overlays` on
5
+ `Dotagents._subcommands_` (in `cli/__init__.py`). Discover-not-track: installed
6
+ overlays are the dirs under `<scope>/overlays/`.
7
+ """
8
+
9
+ import shutil
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+ from duho import Cli, Cmd, LoggingArgs
14
+
15
+ from dotagents.cli._common import (
16
+ BASE_ROOT,
17
+ _installed_overlay_dirs,
18
+ _run_overlay_setup,
19
+ )
20
+
21
+
22
+ class OverlayAdd(LoggingArgs, Cmd):
23
+ """Install overlay(s) by name into a scope, and publish their skills.
24
+
25
+ Resolves each ``<name>`` against the source (``--source`` /
26
+ ``$AGENTS_OVERLAYS_SRC``, default the bundled ``overlays/``), copies it into
27
+ ``<scope>/.agents/overlays/<name>/`` (discoverable), merges its D59
28
+ routing/rules into the installed ``AGENTS.md`` managed block (additive), and
29
+ publishes its ``skills/`` into the shared ``<scope>/.agents/skills/`` so every
30
+ agent sees them. ``--copy`` mirrors skills as real dirs instead of symlinks
31
+ (Windows / no-symlink)."""
32
+
33
+ _parsername_ = "add"
34
+
35
+ name: "list[str]" = []
36
+ "Overlay name(s) to install (resolved against the source)."
37
+ ("name",)
38
+
39
+ source: Optional[str] = None
40
+ "Overlay source directory (default: $AGENTS_OVERLAYS_SRC or the bundled overlays/)."
41
+ ("--source",)
42
+
43
+ global_scope: bool = False
44
+ "Install into the user scope (~/.agents) instead of the project scope."
45
+ ("--global", "-g")
46
+
47
+ agents_dir: Path = Path.home() / ".agents"
48
+ "User-scope agents dir (default: ~/.agents)."
49
+ ("--agents-dir",)
50
+
51
+ copy: bool = False
52
+ "Copy skills into the shared dir instead of symlinking (no-symlink fallback)."
53
+ ("--copy",)
54
+
55
+ no_setup: bool = False
56
+ "Skip running an overlay's idempotent `setup` script after install."
57
+ ("--no-setup",)
58
+
59
+ dry_run: bool = False
60
+ "Show what would happen without touching anything."
61
+ ("--dry-run",)
62
+
63
+ def __call__(self) -> int:
64
+ from dotagents import _overlays, _scope, _skills
65
+
66
+ if not self.name:
67
+ self._logger_.warning("no overlay name given; nothing to add")
68
+ return 0
69
+ # Validate + normalize each requested name with the shared overlay-name
70
+ # rule (D84), so `add My_Overlay` and `add my-overlay` resolve the same
71
+ # overlay and a bad name (`.git`, `README.md`, `2fast`) is rejected up
72
+ # front rather than creating a junk dir under overlays/.
73
+ names = []
74
+ for raw in self.name:
75
+ if not _scope.is_valid_overlay_name(raw):
76
+ raise SystemExit(
77
+ "error: %r is not a valid overlay name (must start with a "
78
+ "letter, then letters/digits/_/-)" % raw
79
+ )
80
+ names.append(_scope.normalize_overlay_name(raw))
81
+
82
+ source = _scope.resolve_source(self.source)
83
+ scope = _scope.resolve_scope(self.global_scope, agents_dir=self.agents_dir)
84
+ self._logger_.info("scope: %s (%s)", scope.level, scope.agents_root)
85
+ agents_md = scope.agents_root / "AGENTS.md"
86
+
87
+ for name in names:
88
+ overlay_src = source.overlay_dir(name)
89
+ dest_dir = scope.overlay_dir(name)
90
+ copied, skipped, lines = _overlays.install_overlay_dir(
91
+ overlay_src, dest_dir, self.dry_run
92
+ )
93
+ for line in lines:
94
+ self._logger_.info(line)
95
+ self._logger_.info(
96
+ "overlay %s: %d file(s) installed, %d skipped%s",
97
+ name, copied, skipped, " [dry-run]" if self.dry_run else "",
98
+ )
99
+ if not self.dry_run:
100
+ published = _skills.publish_overlay_skills(
101
+ overlay_src, scope.shared_skills_dir, copy=self.copy,
102
+ logger=self._logger_,
103
+ )
104
+ if published:
105
+ self._logger_.info("published %d skill(s) from %s", published, name)
106
+ rc = _run_overlay_setup(
107
+ dest_dir, name, scope=scope, no_setup=self.no_setup,
108
+ dry_run=self.dry_run, logger=self._logger_,
109
+ )
110
+ if rc:
111
+ raise SystemExit(
112
+ "error: setup for overlay %r failed (exit %d)" % (name, rc)
113
+ )
114
+
115
+ # Recompose the whole managed block from the pristine base over ALL installed
116
+ # overlays in (priority, name) order (plan 02 / D68), so a high-priority overlay
117
+ # lands last regardless of when it was added -- not merely appended after
118
+ # whatever was already in the block.
119
+ overlay_dirs = _installed_overlay_dirs(
120
+ scope, source, adding=names, dry_run=self.dry_run
121
+ )
122
+ base_block = (BASE_ROOT / "AGENTS.md").read_text(encoding="utf-8")
123
+ if _overlays.recompose_overlay_block(
124
+ agents_md, base_block, overlay_dirs, self.dry_run, self._logger_
125
+ ):
126
+ self._logger_.info("recomposed overlay rules/routing in AGENTS.md")
127
+
128
+ if self.dry_run:
129
+ self._logger_.info("dry-run: no files were written")
130
+ return 0
131
+
132
+
133
+ class OverlayRemove(LoggingArgs, Cmd):
134
+ """Remove installed overlay(s): delete the overlay dir + unpublish its skills.
135
+
136
+ Deletes only ``<scope>/.agents/overlays/<name>/`` and unpublishes only the
137
+ skills that overlay published (matched to its own ``skills/`` source) -- never a
138
+ file outside the overlay dir, never another overlay's skill. Broken skill
139
+ symlinks are then swept. The overlay's D59 rules/routing in ``AGENTS.md`` are
140
+ **not** auto-unmerged (a clean managed-block un-merge is deferred, see the
141
+ decision); a warning points at the manual prune when the overlay carried any."""
142
+
143
+ _parsername_ = "remove"
144
+
145
+ name: "list[str]" = []
146
+ "Overlay name(s) to remove."
147
+ ("name",)
148
+
149
+ global_scope: bool = False
150
+ "Operate on the user scope (~/.agents) instead of the project scope."
151
+ ("--global", "-g")
152
+
153
+ agents_dir: Path = Path.home() / ".agents"
154
+ "User-scope agents dir (default: ~/.agents)."
155
+ ("--agents-dir",)
156
+
157
+ dry_run: bool = False
158
+ "Show what would happen without touching anything."
159
+ ("--dry-run",)
160
+
161
+ def __call__(self) -> int:
162
+ from dotagents import _overlays, _scope, _skills
163
+
164
+ if not self.name:
165
+ self._logger_.warning("no overlay name given; nothing to remove")
166
+ return 0
167
+ scope = _scope.resolve_scope(self.global_scope, agents_dir=self.agents_dir)
168
+ self._logger_.info("scope: %s (%s)", scope.level, scope.agents_root)
169
+
170
+ for name in self.name:
171
+ overlay_dir = scope.overlay_dir(name)
172
+ if not overlay_dir.is_dir():
173
+ self._logger_.warning("overlay %r not installed at %s", name, overlay_dir)
174
+ continue
175
+ manifest = _overlays.read_manifest(overlay_dir)
176
+ has_rules = bool(manifest["routing"] or manifest["rules"])
177
+ if not self.dry_run:
178
+ removed = _skills.remove_overlay_skills(
179
+ overlay_dir, scope.shared_skills_dir, logger=self._logger_
180
+ )
181
+ if removed:
182
+ self._logger_.info("unpublished %d skill(s) from %s", removed, name)
183
+ shutil.rmtree(str(overlay_dir))
184
+ self._logger_.info(
185
+ "removed overlay %s (%s)%s",
186
+ name, overlay_dir, " [dry-run]" if self.dry_run else "",
187
+ )
188
+ if has_rules:
189
+ self._logger_.warning(
190
+ "overlay %s contributed rules/routing to AGENTS.md; those are "
191
+ "NOT auto-removed -- prune its lines from the managed block by "
192
+ "hand (or re-run `dotagents install` to regenerate it).", name,
193
+ )
194
+
195
+ if self.dry_run:
196
+ self._logger_.info("dry-run: no files were written")
197
+ return 0
198
+
199
+
200
+ class OverlayList(LoggingArgs, Cmd):
201
+ """List overlays: those installed in the scope, and those available from source.
202
+
203
+ ``installed`` is discovered by presence under ``<scope>/.agents/overlays/``; no
204
+ registry file. ``available`` is what the source offers. ``--json`` emits both as
205
+ a machine-readable object."""
206
+
207
+ _parsername_ = "list"
208
+
209
+ source: Optional[str] = None
210
+ "Overlay source directory (default: $AGENTS_OVERLAYS_SRC or the bundled overlays/)."
211
+ ("--source",)
212
+
213
+ global_scope: bool = False
214
+ "List the user scope (~/.agents) instead of the project scope."
215
+ ("--global", "-g")
216
+
217
+ agents_dir: Path = Path.home() / ".agents"
218
+ "User-scope agents dir (default: ~/.agents)."
219
+ ("--agents-dir",)
220
+
221
+ json: bool = False
222
+ "Emit JSON instead of plain text."
223
+ ("--json",)
224
+
225
+ def __call__(self) -> int:
226
+ import json as _json
227
+
228
+ from dotagents import _scope
229
+
230
+ scope = _scope.resolve_scope(self.global_scope, agents_dir=self.agents_dir)
231
+ installed = _scope.discover_overlays(scope)
232
+ try:
233
+ available = _scope.resolve_source(self.source).available()
234
+ except SystemExit:
235
+ available = []
236
+
237
+ if self.json:
238
+ print(_json.dumps({
239
+ "scope": scope.level,
240
+ "root": str(scope.overlay_root),
241
+ "installed": installed,
242
+ "available": available,
243
+ }, indent=2))
244
+ return 0
245
+
246
+ self._logger_.info("scope: %s (%s)", scope.level, scope.overlay_root)
247
+ print("installed (%s):" % scope.level)
248
+ if installed:
249
+ for n in installed:
250
+ print(" %s" % n)
251
+ else:
252
+ print(" (none)")
253
+ print("available (source):")
254
+ if available:
255
+ for n in available:
256
+ mark = " *" if n in installed else ""
257
+ print(" %s%s" % (n, mark))
258
+ else:
259
+ print(" (none)")
260
+ return 0
261
+
262
+
263
+ class OverlaySync(LoggingArgs, Cmd):
264
+ """Refresh installed overlays from source, and resync their skills.
265
+
266
+ Re-applies each installed overlay (additive: new files land, hand-edits stay),
267
+ re-merges its rules/routing, and refreshes its published skills. An optional
268
+ ``<glob>`` filters which installed overlays to sync (``sync 'py*'``)."""
269
+
270
+ _parsername_ = "sync"
271
+
272
+ pattern: Optional[str] = None
273
+ "Glob over installed overlay names to sync (default: all)."
274
+ ("pattern",)
275
+
276
+ source: Optional[str] = None
277
+ "Overlay source directory (default: $AGENTS_OVERLAYS_SRC or the bundled overlays/)."
278
+ ("--source",)
279
+
280
+ global_scope: bool = False
281
+ "Sync the user scope (~/.agents) instead of the project scope."
282
+ ("--global", "-g")
283
+
284
+ agents_dir: Path = Path.home() / ".agents"
285
+ "User-scope agents dir (default: ~/.agents)."
286
+ ("--agents-dir",)
287
+
288
+ copy: bool = False
289
+ "Copy skills into the shared dir instead of symlinking (no-symlink fallback)."
290
+ ("--copy",)
291
+
292
+ no_setup: bool = False
293
+ "Skip running each overlay's idempotent `setup` script after sync."
294
+ ("--no-setup",)
295
+
296
+ dry_run: bool = False
297
+ "Show what would happen without touching anything."
298
+ ("--dry-run",)
299
+
300
+ def __call__(self) -> int:
301
+ from dotagents import _overlays, _scope, _skills
302
+
303
+ scope = _scope.resolve_scope(self.global_scope, agents_dir=self.agents_dir)
304
+ source = _scope.resolve_source(self.source)
305
+ installed = _scope.discover_overlays(scope)
306
+ names = _scope.filter_names(installed, self.pattern)
307
+ if not names:
308
+ self._logger_.info(
309
+ "no installed overlays%s to sync",
310
+ "" if self.pattern in (None, "*") else " matching %r" % self.pattern,
311
+ )
312
+ return 0
313
+ self._logger_.info("scope: %s (%s)", scope.level, scope.agents_root)
314
+ agents_md = scope.agents_root / "AGENTS.md"
315
+
316
+ for name in names:
317
+ try:
318
+ overlay_src = source.overlay_dir(name)
319
+ except SystemExit:
320
+ self._logger_.warning("overlay %r not in source; skipping", name)
321
+ continue
322
+ dest_dir = scope.overlay_dir(name)
323
+ copied, skipped, lines = _overlays.install_overlay_dir(
324
+ overlay_src, dest_dir, self.dry_run
325
+ )
326
+ self._logger_.info(
327
+ "synced %s: %d new file(s), %d unchanged%s",
328
+ name, copied, skipped, " [dry-run]" if self.dry_run else "",
329
+ )
330
+ if not self.dry_run:
331
+ _skills.resync_overlay_skills(
332
+ overlay_src, scope.shared_skills_dir, logger=self._logger_
333
+ )
334
+ rc = _run_overlay_setup(
335
+ dest_dir, name, scope=scope, no_setup=self.no_setup,
336
+ dry_run=self.dry_run, logger=self._logger_,
337
+ )
338
+ if rc:
339
+ raise SystemExit(
340
+ "error: setup for overlay %r failed (exit %d)" % (name, rc)
341
+ )
342
+
343
+ # Recompose the managed block over ALL installed overlays in (priority, name)
344
+ # order (plan 02 / D68) -- not just the pattern-matched subset synced above, so
345
+ # priority ordering holds across the full installed set.
346
+ overlay_dirs = _installed_overlay_dirs(scope, source, dry_run=self.dry_run)
347
+ base_block = (BASE_ROOT / "AGENTS.md").read_text(encoding="utf-8")
348
+ if _overlays.recompose_overlay_block(
349
+ agents_md, base_block, overlay_dirs, self.dry_run, self._logger_
350
+ ):
351
+ self._logger_.info("recomposed overlay rules/routing in AGENTS.md")
352
+
353
+ if self.dry_run:
354
+ self._logger_.info("dry-run: no files were written")
355
+ return 0
356
+
357
+
358
+ class Overlays(LoggingArgs, Cli):
359
+ """Manage opt-in overlays by name: add / remove / list / sync (+ skills sync)."""
360
+
361
+ _parsername_ = "overlays"
362
+ _subcommands_ = [OverlayAdd, OverlayRemove, OverlayList, OverlaySync]
363
+
364
+ def __call__(self) -> int:
365
+ self._logger_.info("pick an overlays subcommand: add, remove, list, sync")
366
+ return 0
@@ -0,0 +1,291 @@
1
+ Metadata-Version: 2.4
2
+ Name: dotagents-cli
3
+ Version: 0.3.0
4
+ Summary: The dotagents CLI: install and manage a portable ~/.agents config for AI coding agents
5
+ Project-URL: Homepage, https://github.com/jose-pr/dotagents/
6
+ Project-URL: Repository, https://github.com/jose-pr/dotagents
7
+ Project-URL: Documentation, https://jose-pr.github.io/dotagents/
8
+ Author: Jose A.
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 Jose A.
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Classifier: Development Status :: 4 - Beta
32
+ Classifier: Environment :: Console
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Operating System :: OS Independent
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.9
38
+ Classifier: Programming Language :: Python :: 3.10
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Programming Language :: Python :: 3.13
42
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
43
+ Requires-Python: >=3.9
44
+ Requires-Dist: duho>=0.4.0
45
+ Requires-Dist: pathlib-next>=0.8.0
46
+ Provides-Extra: dev
47
+ Requires-Dist: pytest; extra == 'dev'
48
+ Provides-Extra: docs
49
+ Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
50
+ Requires-Dist: mkdocs>=1.5; extra == 'docs'
51
+ Requires-Dist: mkdocstrings[python]>=0.24; extra == 'docs'
52
+ Provides-Extra: http
53
+ Requires-Dist: pathlib-next[http]; extra == 'http'
54
+ Provides-Extra: s3
55
+ Requires-Dist: pathlib-next[s3]; extra == 's3'
56
+ Provides-Extra: sftp
57
+ Requires-Dist: pathlib-next[sftp]; extra == 'sftp'
58
+ Provides-Extra: uri
59
+ Requires-Dist: pathlib-next[uri]; extra == 'uri'
60
+ Description-Content-Type: text/markdown
61
+
62
+ # dotagents
63
+
64
+ [![Test](https://img.shields.io/github/actions/workflow/status/jose-pr/dotagents/test.yml?branch=main&label=tests)](https://github.com/jose-pr/dotagents/actions/workflows/test.yml)
65
+ [![Documentation](https://img.shields.io/badge/docs-online-blue.svg)](https://jose-pr.github.io/dotagents/)
66
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/jose-pr/dotagents/blob/main/LICENSE)
67
+
68
+ Like dotfiles, but for AI coding agents: a portable, token-budgeted `~/.agents`
69
+ configuration that works across agent runners (Claude Code, Antigravity, Copilot,
70
+ Codex, ...). dotagents is the **mechanism** — install a neutral base, then layer in
71
+ opt-in **overlays** that carry your standards (repo structure, CI/release discipline,
72
+ whatever workflows you want) — so you record them once instead of restating them every
73
+ session.
74
+
75
+ ## Design
76
+
77
+ - **Core + load-on-demand routing.** `AGENTS.md` is the only always-loaded file: a
78
+ handful of always-on rules plus a routing table. Task-specific detail lives in
79
+ `flows/` and `kb/` files that an agent reads only when the task matches. You pay for
80
+ what you use.
81
+ - **A neutral base + opt-in overlays.** `init` lays down a minimal, opinion-free
82
+ **base overlay** (just the `AGENTS.md` scaffolding + design-log convention).
83
+ Everything opinionated — workflows, language `kb/` files, repo templates, tools —
84
+ lives in composable **overlays** you layer in explicitly
85
+ (`dotagents overlays add <name>`), each contributing its own routing lines, rules,
86
+ skills, and commands. Additive-only: overlays never overwrite something you've
87
+ already customized.
88
+ - **Overlays carry the opinions, not the tool.** dotagents is the mechanism
89
+ (install, compose, discover); *what* your agents should do is an overlay concern.
90
+ The example overlays in this repo are a starting point — a planning/execution/review
91
+ workflow set, language conventions, a release helper — but they're payloads riding on
92
+ dotagents, swappable for your own. See the [docs](https://jose-pr.github.io/dotagents/)
93
+ for what each ships.
94
+
95
+ ## Layout
96
+
97
+ The config is a **base overlay** plus opt-in **overlays**; the `dotagents` CLI applies
98
+ them. Everything else is repo infrastructure.
99
+
100
+ | Path | What |
101
+ | --- | --- |
102
+ | `src/dotagents/` | The installable `dotagents` CLI (`init`/`overlays`/`context`/`env`/`build-pyz`) — that is the whole shipped surface; commands beyond it come from overlays or your own `cmds/` modules |
103
+ | `src/dotagents/_overlay/` | The **base overlay** `init` writes: `AGENTS.md` scaffolding, `CLAUDE.md`, `dotagents/DECISIONS.md` (empty design-log index), and an empty `dotagents/cmds/` dir — your drop-in point for your own command modules. Neutral — imposes no flows, ships no command |
104
+ | `tools/` | Required tooling (not an overlay): `cloud-setup.sh` (`leak-check` moved to the opt-in `leak-check` overlay, D84) |
105
+ | `install.py` | Thin shim over `dotagents.cli.main()`, kept at this filename for muscle memory |
106
+
107
+ The **example overlays** — the `flows` workflow set, per-language `kb/` + templates,
108
+ `references`, `release`, `private-sync`, `net`, `recovery`, `tools` — live on a separate
109
+ [`overlays` branch](https://github.com/jose-pr/dotagents/tree/overlays), not in `main`'s
110
+ tree: they are swappable payloads, not part of the tool. `dotagents overlays add <name>`
111
+ resolves them from there (or from any `--source`). See the
112
+ [docs](https://jose-pr.github.io/dotagents/) for what each ships.
113
+
114
+ Named-agent directives aren't a shipped overlay — a named agent (Claude, Antigravity,
115
+ …) just reads its own `~/.agents/<agent>.md` on top of the shared `AGENTS.md`, which the
116
+ base overlay's routing already states. This config's own design log lives **privately**
117
+ under its untracked `.agents/dotagents/` (`DECISIONS.md` + one file per decision) — like
118
+ every project, `.agents/` is never tracked or pushed.
119
+
120
+ Each overlay's `<name>/overlay.toml` carries a `name`/`description`/`requires`/`routing`
121
+ manifest read by the `dotagents overlays` subcommand, which manages overlays by name
122
+ (`add`/`remove`/`list`/`sync`) — see [Managing overlays](#managing-overlays) below.
123
+
124
+ ## Install
125
+
126
+ **`dotagents init`** lays down the neutral base config; a self-contained downloadable
127
+ `.pyz` needs no `pip install` at all.
128
+
129
+ `init` writes the `.agents/` scaffolding — the `AGENTS.md` managed block, the per-agent
130
+ `<CLAUDE|ANTIGRAVITY|...>.md → @AGENTS.md` pattern, the design-log convention — and
131
+ wires each supporting agent's hooks so `dotagents context` reaches it automatically at
132
+ session start (`--no-hooks` opts out) — but
133
+ imposes no opinions (those come from `overlays add`). Its `AGENTS.md`/`CLAUDE.md` are a
134
+ marker-delimited managed block, so re-running `init` never clobbers what you've added
135
+ around it. **Scope**: project by default (`<cwd>/.agents`), or the user store with
136
+ `-g`/`--global` (`~/.agents`).
137
+
138
+ ```bash
139
+ dotagents init # project: <cwd>/.agents
140
+ dotagents init -g # user store: ~/.agents
141
+ dotagents init --bin-dir ~/.local/bin # also write a `dotagents` command on PATH
142
+ dotagents init --dry-run # show what would happen
143
+ dotagents init --force # replace AGENTS.md/CLAUDE.md wholesale (backed up)
144
+ ```
145
+
146
+ `--from <path-or-uri>` selects the *base* source for a `pip install`-only environment
147
+ (a git checkout dir, `file:`, `http(s):`, `zip:`, `sftp:`, or `s3:` URI via
148
+ `pip install "dotagents-cli[uri]"`); `init`'s base ships inside the package, so it needs no
149
+ `--from`.
150
+
151
+ Overlays beyond the base are managed by name with `dotagents overlays add <name>` — it
152
+ installs into `<scope>/.agents/overlays/<name>/` (discoverable) and publishes the
153
+ overlay's skills into the shared skills dir. See below.
154
+
155
+ ### Managing overlays
156
+
157
+ `dotagents overlays` manages opt-in overlays **by name**, resolving each name against a
158
+ source directory of overlays — point `--source <dir>` (or `$AGENTS_OVERLAYS_SRC`) at
159
+ one, e.g. a checkout of the [`overlays` branch](https://github.com/jose-pr/dotagents/tree/overlays)
160
+ where the example overlays live. Installed overlays are *discovered* by their presence
161
+ under `<scope>/.agents/overlays/` — there is no registry file.
162
+
163
+ ```bash
164
+ dotagents overlays add python flows # install into the scope, publish skills, merge D59 rules/routing
165
+ dotagents overlays list # installed (discovered) + available (from source)
166
+ dotagents overlays sync 'py*' # refresh installed overlays matching a glob, resync their skills
167
+ dotagents overlays remove python # delete the overlay dir + unpublish its skills
168
+ ```
169
+
170
+ Scope is **project** by default (`<project>/.agents/`, when run inside one) or **user**
171
+ with `-g`/`--global` (`~/.agents/`, the configurable store). Each overlay installs as a
172
+ directory (kept, discoverable), its `routing`/`rules` merge additively into `AGENTS.md`'s
173
+ managed block, and its `skills/<name>/` are symlinked (or `--copy`'d, for Windows /
174
+ no-symlink) into the shared `<scope>/.agents/skills/` so every agent sees the same skills.
175
+ `add`/`sync` are additive and never clobber a file you hand-edited inside an installed
176
+ overlay. Removing an overlay deletes only its dir and unpublishes only the skills **it**
177
+ published; its lines in `AGENTS.md`'s managed block are not auto-pruned (a warning points
178
+ at the manual edit, or re-run `install`).
179
+
180
+ **Overlay setup scripts.** An overlay may ship an **idempotent** `setup.py` at its root
181
+ (the recommended form: it runs under the same Python that runs dotagents, so it works on
182
+ every OS — the bundled `net` overlay is the model). An extensionless `setup` (a POSIX
183
+ shell script) is still honored as a legacy fallback, but it is discouraged: a shell
184
+ script isn't portable to Windows without a shell. After `add`/`sync` copies the overlay
185
+ in, dotagents runs the script automatically — so anything a human would otherwise
186
+ hand-follow (PATH/lib wiring, self-registration) is one script the tool runs, not a doc.
187
+ When both are present, `setup.py` wins. Presence of a script is the opt-in; skip it with
188
+ `--no-setup`. The contract for authors:
189
+
190
+ - **Idempotent** — safe to run on every `add`/`sync`; check-then-act, never blindly append.
191
+ - **cwd** is the installed overlay dir (`<scope>/.agents/overlays/<name>/`), so reference
192
+ your own files by relative path.
193
+ - **Env** carries `AGENTS_HOME` (the resolved store path — never hardcode
194
+ `~/.agents`) and `AGENTS_OVERLAY_DIR` (your own installed dir).
195
+ - A **non-zero exit fails the install** with a clear error (not a silent skip). For any
196
+ outward or irreversible action the *script* must confirm first — the runner invokes a
197
+ script you chose to install; it does not second-guess it.
198
+
199
+ **Downloadable `dotagents.pyz`** — a self-contained zipapp with `duho`/
200
+ `pathlib_next` and the required `tools/` bundled in, so it needs no `pip install`:
201
+
202
+ ```bash
203
+ python -m dotagents build-pyz --out dist/dotagents.pyz # build it (needs this repo checkout)
204
+ python dist/dotagents.pyz init --bin-dir ~/.local/bin # lay down the base + a `dotagents` command, offline
205
+ ```
206
+
207
+ Then wire your runner to it — e.g. Claude Code: put `@AGENTS.md` in
208
+ `~/.claude/CLAUDE.md`... which is exactly what the installed `CLAUDE.md` contains.
209
+
210
+ **Or let your agent do it:** point it at this repo and say —
211
+ > Read README.md, run `python install.py init && python install.py overlays add flows -g`,
212
+ > and confirm `~/.agents/overlays/flows/flows/PLAN.md` exists.
213
+
214
+ ## Private sync (per-user + per-project, one private repo)
215
+
216
+ Keep your global config **and** every project's private `.agents` (plans, kb, findings)
217
+ in a single private git repo — synced across machines and cloud sessions — without ever
218
+ committing any of it into the (often public) project repos.
219
+
220
+ The idea: your global `~/.agents` **is** a private git repo. Its root is the per-user
221
+ config; a `projects/<name>/` tree holds each project's private `.agents` payload. For a
222
+ checked-out project, `<project>/.agents` is a **symlink** to `~/.agents/projects/<name>`
223
+ (the project's `.gitignore` already excludes `.agents/` per the Leakage rule, so the
224
+ link never lands in the public repo). `<name>` defaults to the project's basename, so a
225
+ local `~/code/app` and a cloud `/home/user/app` resolve to the same store.
226
+
227
+ The `link-project` / `sync-project` commands are **supplied by the `private-sync`
228
+ overlay**, not by dotagents itself — installing the overlay is what makes them exist
229
+ (it ships the commands, their logic, the `kb/` walkthrough and the cloud hooks
230
+ together). So `overlays add private-sync` comes first:
231
+
232
+ ```bash
233
+ dotagents init # base
234
+ dotagents overlays add private-sync --source <overlays-checkout> # commands + kb + hooks
235
+ dotagents link-project . # symlink this project's .agents into the private repo
236
+ # (an existing .agents/ is adopted in on the first link;
237
+ # --copy mirrors it as a real dir for no-symlink systems)
238
+ dotagents sync-project -m msg # git pull --rebase / commit / push the private repo
239
+ dotagents sync-project --remote git@github.com:<you>/.agents.git -m init # one-command bootstrap
240
+ ```
241
+
242
+ In cloud sessions, the installed `~/.agents/hooks/private-sync-{start,stop}.sh` clone/pull
243
+ the private repo and link/sync the project per session — register them in
244
+ `~/.claude/settings.json` (see `~/.agents/hooks/settings.snippet.json`). For a **fresh
245
+ container** (no `~/.agents` yet), point the web environment's **setup-script** field at the
246
+ self-contained bootstrap in this repo — it fetches the latest each start, so there's
247
+ nothing to re-paste:
248
+
249
+ ```bash
250
+ curl -fsSL https://raw.githubusercontent.com/<you>/dotagents/main/tools/cloud-setup.sh -o /tmp/dg-cloud-setup.sh && sh /tmp/dg-cloud-setup.sh
251
+ ```
252
+
253
+ Use `curl … -o file && sh file`, not `curl … | sh`: with a pipe the setup field's exit
254
+ code is `sh`'s (0 on empty stdin), so a failed fetch is silently logged as success; `&&`
255
+ propagates the curl failure instead.
256
+
257
+ It authenticates (bypassing a hosted-runner `github.com`→proxy git rewrite), clones/pulls
258
+ `~/.agents`, installs the CLI, and links the project — driven by `AGENTS_REMOTE`
259
+ / `DOTAGENTS_AGENTS_TOKEN` / `DOTAGENTS_CLI_INSTALL` env vars (token never committed). Full
260
+ walkthrough: `~/.agents/kb/PRIVATE_SYNC.md`.
261
+
262
+ ## Validate
263
+
264
+ ```bash
265
+ python tools/audit.py --root . # validate THIS REPO's layout (CI tooling)
266
+ python tools/audit.py --check-templates --root . # + template checks (needs 3.11+)
267
+ ```
268
+
269
+ ## Customize
270
+
271
+ Fork it — that's the point. Keep the base `AGENTS.md` small (the audit warns past
272
+ ~2.5KB); put opinionated content in overlays. Your `~/.agents/dotagents/DECISIONS.md`
273
+ is *your* private, per-install design log (index + `decisions/` files) — installed
274
+ empty, edited directly, never distributed. This repo follows the same rule: its own
275
+ design log and all working material live in an **untracked** `.agents/dotagents/`, never
276
+ committed — so what's public here is only the CLI, the base overlay, and the opt-in
277
+ overlays. If you fork, keep the tracked surface free of personal paths and private
278
+ project names. `dotagents audit` validates config *structure* only; personal-leak
279
+ scanning (machine paths, private plan names, session trailers) is a separate,
280
+ personal `leak-check` tool you run locally before a push — it lives in your private
281
+ `.agents/`, not shipped in this repo.
282
+
283
+ ## Documentation
284
+
285
+ Full docs — install modes, the overlay model, the CLI command surface, private sync,
286
+ authoring your own overlays and commands, and the API reference — are at
287
+ [jose-pr.github.io/dotagents](https://jose-pr.github.io/dotagents/).
288
+
289
+ ## License
290
+
291
+ MIT — see [LICENSE](LICENSE).