dotagents-cli 0.7.1__tar.gz → 0.7.2__tar.gz
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.
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/CHANGELOG.md +15 -2
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/PKG-INFO +3 -2
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/README.md +2 -1
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/guide/install.md +11 -1
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/AGENTS.md +9 -1
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/__init__.py +1 -1
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_sources.py +60 -6
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/cli/_common.py +4 -1
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_cmds_discovery.py +2 -2
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_context.py +1 -1
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_env_regressions.py +6 -6
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_env_safety.py +1 -2
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_findings_cmd.py +4 -4
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_fix_adapters.py +1 -1
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_fix_cli.py +1 -1
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_fix_env.py +1 -2
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_fix_hooks.py +1 -1
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_fix_init.py +1 -1
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_fix_overlays.py +2 -2
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_fix_scope_context.py +1 -2
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_fix_skills_adapters.py +2 -2
- dotagents_cli-0.7.2/tests/test_git_ssl_revoke.py +102 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_last_mile.py +3 -3
- dotagents_cli-0.7.2/tests/test_no_internal_labels.py +231 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_overlays_cli.py +5 -5
- dotagents_cli-0.7.1/tests/test_review_security.py → dotagents_cli-0.7.2/tests/test_quoting_injection_safety.py +3 -3
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/.gitignore +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/LICENSE +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/api/agents.md +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/api/cli.md +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/api/commands.md +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/api/context.md +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/api/env.md +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/api/fs.md +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/api/hooks.md +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/api/merge.md +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/api/overlays.md +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/api/reference.md +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/api/resources.md +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/api/scope.md +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/api/skills.md +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/api/sources.md +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/api/wrappers.md +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/changelog.md +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/guide/authoring.md +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/guide/commands.md +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/guide/overlays.md +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/guide/private-sync.md +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/index.md +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/install.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/mkdocs.yml +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/pyproject.toml +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/__main__.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_agents.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_context.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_env.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_fs.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_hooks.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_ignore.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_merge.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_overlay/dotagents/cmds/findings.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_overlay/dotagents/cmds/launch.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_overlay/dotagents/hooks/preinvocation_antigravity_context.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_overlay/dotagents/hooks/pretooluse_codex_env.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_overlay/dotagents/hooks/sessionstart_codex_context.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_overlay/dotagents/templates/AGENTS.md +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_overlay/dotagents/templates/PROJECT.md +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_overlays.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_resources.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_scope.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_skills.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_wrappers.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/cli/__init__.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/cli/about.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/cli/build_pyz.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/cli/context.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/cli/env.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/cli/init.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/cli/overlays.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/cli/path.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/py.typed +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/_helpers.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/_shell.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/conftest.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_about.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_agents.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_api_header.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_audit_leak.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_build_pyz.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_cli_main.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_cloud_setup.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_env.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_env_cli_scope.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_env_format.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_env_into.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_fix_packaging.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_hooks.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_ignore.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_init_base.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_launch_cmd.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_local_additions.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_mcp.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_overlay_libs.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_overlays_command.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_path_timeout_frontmatter.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_powershell_env_hook.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_scope.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_sources.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_sources_uri.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_wire_hooks.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_wrappers.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tools/audit.py +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tools/cloud-setup.sh +0 -0
- {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tools/pyz_smoke.sh +0 -0
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.7.2] - 2026-10-03
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `git_ssl_revoke` in `<store>/dotagents/config.toml` sets how the git clones and
|
|
15
|
+
fetches of overlay repos check certificate revocation: `best-effort` (the default)
|
|
16
|
+
skips a revocation list that is missing or unreachable, `false` skips the check, as
|
|
17
|
+
git on Linux does, `true` enforces it. It is passed to every git call, so a stricter
|
|
18
|
+
git configuration elsewhere no longer fails an https clone on Windows, and a clone
|
|
19
|
+
that still fails on revocation says which setting to change. A git too old to know
|
|
20
|
+
`best-effort` (it rejects the value) runs the call again without the setting.
|
|
21
|
+
|
|
10
22
|
## [0.7.1] - 2026-10-01
|
|
11
23
|
|
|
12
24
|
### Added
|
|
@@ -1357,7 +1369,7 @@ Patch release: the PATH/POSIX-conversion fix above (the only change since 0.3.0)
|
|
|
1357
1369
|
|
|
1358
1370
|
### Changed
|
|
1359
1371
|
|
|
1360
|
-
- Migrate the CLI to `duho>=0.3.3` (was `>=0.1.1`). duho's
|
|
1372
|
+
- Migrate the CLI to `duho>=0.3.3` (was `>=0.1.1`). duho's `Args`/`Cmd`
|
|
1361
1373
|
split means commands are now `class X(LoggingArgs, Cmd)` with a `__call__` entrypoint
|
|
1362
1374
|
(was a bare `LoggingArgs` with `__run__`) and the umbrella root is
|
|
1363
1375
|
`class Dotagents(LoggingArgs, Cli)`. Field declarations (annotation + help string +
|
|
@@ -1372,7 +1384,8 @@ Patch release: the PATH/POSIX-conversion fix above (the only change since 0.3.0)
|
|
|
1372
1384
|
affected module sources (`dotagents.cli`, `duho.presets`) to extracted temp files
|
|
1373
1385
|
before dispatch; a no-op for a plain install.
|
|
1374
1386
|
|
|
1375
|
-
[Unreleased]: https://github.com/jose-pr/dotagents/compare/v0.7.
|
|
1387
|
+
[Unreleased]: https://github.com/jose-pr/dotagents/compare/v0.7.2...HEAD
|
|
1388
|
+
[0.7.2]: https://github.com/jose-pr/dotagents/compare/v0.7.1...v0.7.2
|
|
1376
1389
|
[0.7.1]: https://github.com/jose-pr/dotagents/compare/v0.7.0...v0.7.1
|
|
1377
1390
|
[0.7.0]: https://github.com/jose-pr/dotagents/compare/v0.6.3...v0.7.0
|
|
1378
1391
|
[0.6.3]: https://github.com/jose-pr/dotagents/compare/v0.6.2...v0.6.3
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: dotagents-cli
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: The dotagents CLI: install and manage a portable ~/.agents config for AI coding agents
|
|
5
5
|
Project-URL: Homepage, https://github.com/jose-pr/dotagents/
|
|
6
6
|
Project-URL: Repository, https://github.com/jose-pr/dotagents
|
|
@@ -270,7 +270,8 @@ config miss is a **finding** — `dotagents findings add -g` for the user store
|
|
|
270
270
|
triage closes each with its resolution, which is the record. How you keep any
|
|
271
271
|
decisions beyond that is up to you. A store's `dotagents/` directory exists only for
|
|
272
272
|
your own command modules (`dotagents/cmds/`), which you create when you add your first
|
|
273
|
-
one, and for `dotagents/config.toml`, which records an `init --from` base
|
|
273
|
+
one, and for `dotagents/config.toml`, which records an `init --from` base and,
|
|
274
|
+
if you set it, `git_ssl_revoke` (how git checks certificate revocation on Windows). This
|
|
274
275
|
repo follows the same rule: its working material lives in an **untracked** `.agents/`,
|
|
275
276
|
never committed — so what's public here is only the CLI, the base overlay, and the
|
|
276
277
|
opt-in overlays. If you fork, keep the tracked surface free of personal paths and
|
|
@@ -220,7 +220,8 @@ config miss is a **finding** — `dotagents findings add -g` for the user store
|
|
|
220
220
|
triage closes each with its resolution, which is the record. How you keep any
|
|
221
221
|
decisions beyond that is up to you. A store's `dotagents/` directory exists only for
|
|
222
222
|
your own command modules (`dotagents/cmds/`), which you create when you add your first
|
|
223
|
-
one, and for `dotagents/config.toml`, which records an `init --from` base
|
|
223
|
+
one, and for `dotagents/config.toml`, which records an `init --from` base and,
|
|
224
|
+
if you set it, `git_ssl_revoke` (how git checks certificate revocation on Windows). This
|
|
224
225
|
repo follows the same rule: its working material lives in an **untracked** `.agents/`,
|
|
225
226
|
never committed — so what's public here is only the CLI, the base overlay, and the
|
|
226
227
|
opt-in overlays. If you fork, keep the tracked surface free of personal paths and
|
|
@@ -101,7 +101,17 @@ python -m dotagents build-pyz --out dist/dotagents.pyz # or build it (needs a
|
|
|
101
101
|
[Authoring → Custom commands](authoring.md#custom-commands)).
|
|
102
102
|
- `dotagents/config.toml` — only after `init --from`: records that base, so a later
|
|
103
103
|
`init` and every `overlays add` / `remove` / `sync` compose over it. Delete its
|
|
104
|
-
`base` line to return to the bundled base.
|
|
104
|
+
`base` line to return to the bundled base. You may add one setting by hand:
|
|
105
|
+
`git_ssl_revoke` — how strictly the git clones of overlay repos check that the
|
|
106
|
+
server's certificate is not revoked. `best-effort` (the default) skips a
|
|
107
|
+
revocation list that is missing or unreachable; `false` skips the check, as git
|
|
108
|
+
on Linux always does; `true` enforces it. It matters on Windows, where git
|
|
109
|
+
checks revocation and an https clone otherwise fails when the list cannot be
|
|
110
|
+
fetched; a project store's setting wins over the user store's.
|
|
111
|
+
|
|
112
|
+
```toml
|
|
113
|
+
git_ssl_revoke = "false"
|
|
114
|
+
```
|
|
105
115
|
|
|
106
116
|
## What runs, and from where
|
|
107
117
|
|
|
@@ -306,7 +306,15 @@ a directory of overlays or a registry file (default: the root); for a **source**
|
|
|
306
306
|
- `redact(text) -> str` — URL userinfo removed, query values masked.
|
|
307
307
|
- `uri_path_class()` — `pathlib_next.uri.UriPath`, or `None` without the `uri` extra.
|
|
308
308
|
- `url_scheme(location) -> str` — the lower-cased scheme.
|
|
309
|
-
- `
|
|
309
|
+
- `GIT_SSL_REVOKE_KEY = "git_ssl_revoke"`, `GIT_SSL_REVOKE_VALUES = ("best-effort",
|
|
310
|
+
"true", "false")`, `GIT_SSL_REVOKE_DEFAULT = "best-effort"`;
|
|
311
|
+
`git_ssl_revoke(*stores) -> str` — the first store config (`config.toml`) that sets
|
|
312
|
+
the key, project store first; another value raises `SourceError` naming the file.
|
|
313
|
+
- `class SourceCache(root, logger=None, ssl_revoke="best-effort")` — every git call
|
|
314
|
+
runs as `git -c http.schannelCheckRevoke=<ssl_revoke> ...` (Git for Windows'
|
|
315
|
+
Schannel checks revocation; git with OpenSSL ignores it), and a failure that looks
|
|
316
|
+
like a revocation check names the key. A git that rejects the value as a bad
|
|
317
|
+
boolean (older than `best-effort`) is asked again without it. `repo_dir(location, ref=None) -> Path`
|
|
310
318
|
(`<root>/<slug>-<hash>`); `checkout(spec) -> Path` (clone or fetch, once per
|
|
311
319
|
process; a failed fetch of an existing checkout is a warning);
|
|
312
320
|
`materialize(spec) -> Path` (a `url` spec: `file://` in place, anything else
|
|
@@ -214,6 +214,48 @@ def url_scheme(location: str) -> str:
|
|
|
214
214
|
return location.split("://", 1)[0].lower()
|
|
215
215
|
|
|
216
216
|
|
|
217
|
+
#: The store-config key (``<store>/dotagents/config.toml``) for git's
|
|
218
|
+
#: certificate-revocation check on https clones and fetches, passed to every
|
|
219
|
+
#: git call as ``http.schannelCheckRevoke`` -- Git for Windows' Schannel
|
|
220
|
+
#: backend checks revocation; git elsewhere (OpenSSL) checks no CRLs and
|
|
221
|
+
#: ignores the setting. ``best-effort`` (the default, stated explicitly so a
|
|
222
|
+
#: stricter git config cannot fail dotagents' clones) skips a CRL that is
|
|
223
|
+
#: missing or unreachable; ``false`` skips the check, as on Linux; ``true``
|
|
224
|
+
#: enforces it.
|
|
225
|
+
GIT_SSL_REVOKE_KEY = "git_ssl_revoke"
|
|
226
|
+
GIT_SSL_REVOKE_VALUES = ("best-effort", "true", "false")
|
|
227
|
+
GIT_SSL_REVOKE_DEFAULT = "best-effort"
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def git_ssl_revoke(*stores: "Optional[Path]") -> str:
|
|
231
|
+
"""The first ``git_ssl_revoke`` set in ``stores``' config (the project
|
|
232
|
+
store before the user store), else the default. Any other value raises
|
|
233
|
+
:class:`SourceError` naming the file."""
|
|
234
|
+
from dotagents.cli._common import STORE_CONFIG, read_store_config
|
|
235
|
+
|
|
236
|
+
for store in stores:
|
|
237
|
+
if store is None:
|
|
238
|
+
continue
|
|
239
|
+
value = read_store_config(store).get(GIT_SSL_REVOKE_KEY)
|
|
240
|
+
if value is None:
|
|
241
|
+
continue
|
|
242
|
+
normalized = value.strip().lower()
|
|
243
|
+
if normalized not in GIT_SSL_REVOKE_VALUES:
|
|
244
|
+
raise SourceError(
|
|
245
|
+
"error: %s = %r in %s: expected one of %s"
|
|
246
|
+
% (GIT_SSL_REVOKE_KEY, value, Path(store) / STORE_CONFIG, ", ".join(GIT_SSL_REVOKE_VALUES))
|
|
247
|
+
)
|
|
248
|
+
return normalized
|
|
249
|
+
return GIT_SSL_REVOKE_DEFAULT
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
#: What a revocation-check failure looks like in git's stderr (Schannel).
|
|
253
|
+
_REVOCATION_FAILURE = re.compile(r"revocation|CRYPT_E_|SEC_E_UNTRUSTED", re.IGNORECASE)
|
|
254
|
+
#: A git too old to know ``best-effort`` reads it as a broken boolean and
|
|
255
|
+
#: fails every https call: the call is made again without the setting.
|
|
256
|
+
_UNKNOWN_REVOKE_VALUE = re.compile(r"bad boolean config value .* for 'http\.schannelcheckrevoke'", re.IGNORECASE)
|
|
257
|
+
|
|
258
|
+
|
|
217
259
|
class SourceCache(object):
|
|
218
260
|
"""What the commands fetch, under ``root``: git clones, one per repository
|
|
219
261
|
location AND ref (two registry entries naming the same repository at
|
|
@@ -223,9 +265,10 @@ class SourceCache(object):
|
|
|
223
265
|
(a remote directory synced down, a remote file copied), each refreshed
|
|
224
266
|
once per process."""
|
|
225
267
|
|
|
226
|
-
def __init__(self, root: Path, logger: Any = None):
|
|
268
|
+
def __init__(self, root: Path, logger: Any = None, ssl_revoke: str = GIT_SSL_REVOKE_DEFAULT):
|
|
227
269
|
self.root = Path(root)
|
|
228
270
|
self.logger = logger
|
|
271
|
+
self.ssl_revoke = ssl_revoke # git's http.schannelCheckRevoke (GIT_SSL_REVOKE_KEY)
|
|
229
272
|
self._fresh: "set[str]" = set() # locations fetched during this process
|
|
230
273
|
|
|
231
274
|
# --- pathlib_next paths ----------------------------------------------
|
|
@@ -314,17 +357,28 @@ class SourceCache(object):
|
|
|
314
357
|
def _git(self, args: "list[str]", cwd: "Optional[Path]", *, check: bool = True) -> "subprocess.CompletedProcess":
|
|
315
358
|
env = dict(os.environ)
|
|
316
359
|
env["GIT_TERMINAL_PROMPT"] = "0" # never hang on a credential prompt
|
|
360
|
+
setting = ["-c", "http.schannelCheckRevoke=%s" % self.ssl_revoke]
|
|
317
361
|
try:
|
|
318
362
|
proc = subprocess.run(
|
|
319
|
-
["git", *args], cwd=str(cwd) if cwd else None,
|
|
320
|
-
capture_output=True, text=True, env=env,
|
|
363
|
+
["git", *setting, *args], cwd=str(cwd) if cwd else None, capture_output=True, text=True, env=env,
|
|
321
364
|
)
|
|
365
|
+
if proc.returncode != 0 and _UNKNOWN_REVOKE_VALUE.search(proc.stderr or ""):
|
|
366
|
+
# An older git (no best-effort): its own default applies.
|
|
367
|
+
proc = subprocess.run(
|
|
368
|
+
["git", *args], cwd=str(cwd) if cwd else None, capture_output=True, text=True, env=env,
|
|
369
|
+
)
|
|
322
370
|
except OSError as exc:
|
|
323
371
|
raise SourceError("error: git is needed for a git overlay source: %s" % exc)
|
|
324
372
|
if check and proc.returncode != 0:
|
|
373
|
+
output = proc.stderr.strip() or proc.stdout.strip()
|
|
374
|
+
hint = ""
|
|
375
|
+
if _REVOCATION_FAILURE.search(output):
|
|
376
|
+
hint = (
|
|
377
|
+
"\n(the certificate's revocation check failed: set %s = \"false\" in"
|
|
378
|
+
" <store>/dotagents/config.toml to skip it, as git does on Linux)" % GIT_SSL_REVOKE_KEY
|
|
379
|
+
)
|
|
325
380
|
raise SourceError(
|
|
326
|
-
"error: git %s failed (exit %d): %s"
|
|
327
|
-
% (args[0], proc.returncode, redact(proc.stderr.strip() or proc.stdout.strip()))
|
|
381
|
+
"error: git %s failed (exit %d): %s%s" % (args[0], proc.returncode, redact(output), hint)
|
|
328
382
|
)
|
|
329
383
|
return proc
|
|
330
384
|
|
|
@@ -934,4 +988,4 @@ def resolve(
|
|
|
934
988
|
ordered += [Spec("dir", str(p)) for p in registry_files(*stores)]
|
|
935
989
|
if not ordered and not allow_empty:
|
|
936
990
|
raise SourceError(NO_SOURCE_MESSAGE)
|
|
937
|
-
return CompositeSource(ordered, SourceCache(cache_root, logger))
|
|
991
|
+
return CompositeSource(ordered, SourceCache(cache_root, logger, ssl_revoke=git_ssl_revoke(*stores)))
|
|
@@ -324,7 +324,10 @@ def _resolve_from(
|
|
|
324
324
|
if cache_root is None:
|
|
325
325
|
cache_root = resolve_user_store(None) / ".cache" / "overlays"
|
|
326
326
|
try:
|
|
327
|
-
|
|
327
|
+
cache = _sources.SourceCache(
|
|
328
|
+
Path(cache_root), logger, ssl_revoke=_sources.git_ssl_revoke(resolve_user_store(None))
|
|
329
|
+
)
|
|
330
|
+
target = _sources.locate(spec, cache)
|
|
328
331
|
except SystemExit as exc:
|
|
329
332
|
message = str(exc.code if exc.code is not None else exc)
|
|
330
333
|
if message.startswith("error: "):
|
|
@@ -297,8 +297,8 @@ def test_bad_source_is_skipped_not_fatal(monkeypatch, tmp_path):
|
|
|
297
297
|
# at import time must not crash: discovery runs before EVERY invocation,
|
|
298
298
|
# including `env`/`context` inside the SessionStart hooks, so one typo in a
|
|
299
299
|
# personal cmds module used to take the whole session's env and context
|
|
300
|
-
# down
|
|
301
|
-
#
|
|
300
|
+
# down. duho propagates SyntaxError on purpose; the dotagents layer is
|
|
301
|
+
# where it must stop.
|
|
302
302
|
good = tmp_path / "good"
|
|
303
303
|
_write(good / "toy.py", TOY)
|
|
304
304
|
broken = tmp_path / "broken"
|
|
@@ -116,7 +116,7 @@ def test_harness_loads_subtracts_user_agents_md(layout, monkeypatch):
|
|
|
116
116
|
|
|
117
117
|
# Fresh install, no include yet: NOTHING loads the store's AGENTS.md, so
|
|
118
118
|
# `context` must emit it (the old static assumption dropped it here, and
|
|
119
|
-
# the base rules never reached a session
|
|
119
|
+
# the base rules never reached a session at all).
|
|
120
120
|
text = _context.assemble_context(claude, S(dotagents_dir, project_root, True))
|
|
121
121
|
assert "# User rules" in text
|
|
122
122
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
"""Env-assembly regressions
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
"""Env-assembly regressions, each a repro turned into a test: malformed
|
|
2
|
+
env.py output, relative PATH entries, a session whose project root is the
|
|
3
|
+
home directory, and the PowerShell rendering of typographic quotes and
|
|
4
|
+
non-ASCII values through the real loader pipe.
|
|
5
5
|
|
|
6
|
-
A case the product still gets wrong is a strict xfail naming the
|
|
6
|
+
A case the product still gets wrong is a strict xfail naming the known
|
|
7
7
|
issue, so fixing it turns the test into an XPASS failure until the marker
|
|
8
8
|
goes. tmp dirs only, no network.
|
|
9
9
|
"""
|
|
@@ -22,7 +22,7 @@ from dotagents._scope import Scope
|
|
|
22
22
|
|
|
23
23
|
def _open(issue, why, condition=True):
|
|
24
24
|
return pytest.mark.xfail(
|
|
25
|
-
condition, strict=True, reason="open (
|
|
25
|
+
condition, strict=True, reason="open (%s): %s" % (issue, why)
|
|
26
26
|
)
|
|
27
27
|
|
|
28
28
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"""Env-chain safety: which files run, how a failed source is reported, and what
|
|
2
|
-
bash itself adds to a sourced file's output
|
|
3
|
-
the env/scope items).
|
|
2
|
+
bash itself adds to a sourced file's output.
|
|
4
3
|
|
|
5
4
|
tmp dirs only, no network. Run from repo root: ``python -m pytest tests/``.
|
|
6
5
|
"""
|
|
@@ -284,13 +284,13 @@ def test_subcommands_are_nested_not_module_level(findings_mod):
|
|
|
284
284
|
|
|
285
285
|
|
|
286
286
|
# --------------------------------------------------------------------------- #
|
|
287
|
-
# Encodings and reserved names
|
|
288
|
-
#
|
|
289
|
-
#
|
|
287
|
+
# Encodings and reserved names. Cases the module still gets wrong are strict
|
|
288
|
+
# xfails naming the issue: a fix flips them to XPASS failures until the
|
|
289
|
+
# marker goes.
|
|
290
290
|
# --------------------------------------------------------------------------- #
|
|
291
291
|
|
|
292
292
|
def _open(issue, why):
|
|
293
|
-
return pytest.mark.xfail(strict=True, reason="open (
|
|
293
|
+
return pytest.mark.xfail(strict=True, reason="open (%s): %s" % (issue, why))
|
|
294
294
|
|
|
295
295
|
|
|
296
296
|
def test_cmd_body_from_stdin_is_read_as_utf8_bytes(findings_mod, tmp_path, monkeypatch, capsys):
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Adapter fixes
|
|
1
|
+
"""Adapter fixes: adapters are told the scope,
|
|
2
2
|
Claude honours CLAUDE_CONFIG_DIR, `init` writes the store's AGENTS.md itself,
|
|
3
3
|
global harness configs are left alone by a project init, Cursor's rule file,
|
|
4
4
|
Claude's import parsing, identity from runtime signals only, include dedupe.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Regression tests for
|
|
1
|
+
"""Regression tests for CLI fixes: command discovery that survives a
|
|
2
2
|
command whose parser cannot be built, the `findings` queue's encoding, naming
|
|
3
3
|
and move-never-delete rules, and `launch`'s exit code, context file and
|
|
4
4
|
cmd.exe argument handling.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Hook fixes
|
|
1
|
+
"""Hook fixes: the PowerShell handlers' Git Bash
|
|
2
2
|
gate, UTF-8 through the PowerShell pipe, the PreToolUse matcher, merging that
|
|
3
3
|
keeps the user's keys, symlink-safe and non-ASCII-safe wrappers.
|
|
4
4
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
"""Regression tests for
|
|
2
|
-
|
|
1
|
+
"""Regression tests for "Overlays, sources and skills" fixes: typed source
|
|
2
|
+
errors, registry parsing, credential handling, the spec
|
|
3
3
|
grammar, manifest validation, overlay names, install records, and the
|
|
4
4
|
add/remove/sync lifecycle.
|
|
5
5
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
"""Regressions for the scope-resolution and context-assembly
|
|
2
|
-
(2026-09-23 review, "Scope resolution and context assembly").
|
|
1
|
+
"""Regressions for the scope-resolution and context-assembly fixes.
|
|
3
2
|
|
|
4
3
|
tmp dirs only. The user store is redirected with `$AGENTS_HOME`, the system
|
|
5
4
|
store with `$AGENTS_SYSTEM_ROOT` (a path that does not exist), and the project
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
"""Regression tests for
|
|
2
|
-
|
|
1
|
+
"""Regression tests for skills, adapter and hook-cost fixes: Claude's
|
|
2
|
+
per-skill links are refreshed and pruned, `overlays
|
|
3
3
|
add/sync/remove` keep a wired Claude skills dir current, project-scope skill
|
|
4
4
|
links warn when git would commit them, pi gets a pointer to a project store,
|
|
5
5
|
and the env loaders reuse a cached diff while the inputs are unchanged.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""`git_ssl_revoke` in `<store>/dotagents/config.toml`: git's
|
|
2
|
+
certificate-revocation check for overlay clones and fetches. Git for Windows'
|
|
3
|
+
Schannel backend fails an https clone whose CRL cannot be fetched; git with
|
|
4
|
+
OpenSSL (Linux) checks no CRLs. dotagents passes the setting to every git call
|
|
5
|
+
as `http.schannelCheckRevoke`, `best-effort` unless the store says otherwise."""
|
|
6
|
+
import subprocess
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from dotagents import _sources
|
|
11
|
+
from dotagents.cli._common import STORE_CONFIG
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _config(store, text):
|
|
15
|
+
path = store / STORE_CONFIG
|
|
16
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
path.write_text(text, encoding="utf-8")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.fixture()
|
|
21
|
+
def argv(monkeypatch):
|
|
22
|
+
seen = []
|
|
23
|
+
|
|
24
|
+
def run(cmd, **kwargs):
|
|
25
|
+
seen.append(cmd)
|
|
26
|
+
return subprocess.CompletedProcess(cmd, 0, "", "")
|
|
27
|
+
|
|
28
|
+
monkeypatch.setattr(_sources.subprocess, "run", run)
|
|
29
|
+
return seen
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_every_git_call_carries_the_setting_best_effort_by_default(tmp_path, argv):
|
|
33
|
+
cache = _sources.SourceCache(tmp_path / "cache")
|
|
34
|
+
cache._git(["ls-remote", "https://example.invalid/r.git"], None)
|
|
35
|
+
assert argv[-1][:3] == ["git", "-c", "http.schannelCheckRevoke=best-effort"]
|
|
36
|
+
_sources.SourceCache(tmp_path / "cache", ssl_revoke="false")._git(["fetch"], None)
|
|
37
|
+
assert argv[-1][:3] == ["git", "-c", "http.schannelCheckRevoke=false"]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_the_store_config_chooses_project_before_user(tmp_path):
|
|
41
|
+
user, project = tmp_path / "user", tmp_path / "project"
|
|
42
|
+
assert _sources.git_ssl_revoke(project, user) == "best-effort"
|
|
43
|
+
_config(user, 'git_ssl_revoke = "false"\n')
|
|
44
|
+
assert _sources.git_ssl_revoke(None, user) == "false"
|
|
45
|
+
_config(project, 'git_ssl_revoke = "TRUE"\n')
|
|
46
|
+
assert _sources.git_ssl_revoke(project, user) == "true"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_an_unknown_value_names_the_file(tmp_path):
|
|
50
|
+
_config(tmp_path, 'git_ssl_revoke = "maybe"\n')
|
|
51
|
+
with pytest.raises(_sources.SourceError, match="git_ssl_revoke = 'maybe'.*best-effort, true, false"):
|
|
52
|
+
_sources.git_ssl_revoke(tmp_path)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_a_revocation_failure_names_the_setting(tmp_path, monkeypatch):
|
|
56
|
+
stderr = ("fatal: unable to access 'https://example.invalid/r.git/': schannel: next InitializeSecurityContext "
|
|
57
|
+
"failed: CRYPT_E_NO_REVOCATION_CHECK (0x80092012) - The revocation function was unable to check "
|
|
58
|
+
"revocation for the certificate.")
|
|
59
|
+
monkeypatch.setattr(_sources.subprocess, "run",
|
|
60
|
+
lambda cmd, **kw: subprocess.CompletedProcess(cmd, 128, "", stderr))
|
|
61
|
+
with pytest.raises(_sources.SourceError) as caught:
|
|
62
|
+
_sources.SourceCache(tmp_path)._git(["clone", "https://example.invalid/r.git"], None)
|
|
63
|
+
message = str(caught.value)
|
|
64
|
+
assert "CRYPT_E_NO_REVOCATION_CHECK" in message and 'git_ssl_revoke = "false"' in message
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_a_git_without_best_effort_runs_again_without_it(tmp_path, monkeypatch):
|
|
68
|
+
seen = []
|
|
69
|
+
|
|
70
|
+
def run(cmd, **kwargs):
|
|
71
|
+
seen.append(cmd)
|
|
72
|
+
if "http.schannelCheckRevoke=best-effort" in cmd:
|
|
73
|
+
return subprocess.CompletedProcess(cmd, 128, "", "fatal: bad boolean config value 'best-effort' for "
|
|
74
|
+
"'http.schannelcheckrevoke'\n")
|
|
75
|
+
return subprocess.CompletedProcess(cmd, 0, "ok", "")
|
|
76
|
+
|
|
77
|
+
monkeypatch.setattr(_sources.subprocess, "run", run)
|
|
78
|
+
proc = _sources.SourceCache(tmp_path)._git(["ls-remote", "https://example.invalid/r.git"], None)
|
|
79
|
+
assert proc.returncode == 0 and len(seen) == 2
|
|
80
|
+
assert seen[1] == ["git", "ls-remote", "https://example.invalid/r.git"]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_any_other_failure_has_no_hint(tmp_path, monkeypatch):
|
|
84
|
+
monkeypatch.setattr(_sources.subprocess, "run",
|
|
85
|
+
lambda cmd, **kw: subprocess.CompletedProcess(cmd, 128, "", "fatal: repository not found"))
|
|
86
|
+
with pytest.raises(_sources.SourceError) as caught:
|
|
87
|
+
_sources.SourceCache(tmp_path)._git(["clone", "https://example.invalid/r.git"], None)
|
|
88
|
+
assert "git_ssl_revoke" not in str(caught.value)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@pytest.mark.skipif(not __import__("shutil").which("git"), reason="needs git on PATH")
|
|
92
|
+
def test_git_accepts_the_option_on_a_local_clone(tmp_path):
|
|
93
|
+
"""The option reaches a real git without breaking a clone (git ignores
|
|
94
|
+
it off Schannel; on Windows it is honoured)."""
|
|
95
|
+
work = tmp_path / "work"
|
|
96
|
+
work.mkdir()
|
|
97
|
+
for args in (["init", "-q", "-b", "main"], ["-c", "user.email=t@example.invalid", "-c", "user.name=t",
|
|
98
|
+
"commit", "-q", "--allow-empty", "-m", "x"]):
|
|
99
|
+
subprocess.run(["git", *args], cwd=str(work), check=True, capture_output=True)
|
|
100
|
+
cache = _sources.SourceCache(tmp_path / "cache", ssl_revoke="false")
|
|
101
|
+
cache._git(["clone", "-q", str(work), str(tmp_path / "copy")], None)
|
|
102
|
+
assert (tmp_path / "copy" / ".git").is_dir()
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""The last mile: does the config `init` writes actually reach a harness?
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
`init` wrote `<store>/CLAUDE.md`, which Claude Code never reads, and
|
|
4
|
+
`context` subtracted the store's AGENTS.md as "already loaded" on a static
|
|
5
|
+
assumption. These tests pin the fix -- the include `init`
|
|
6
6
|
writes where Claude reads it, the live include-based subtraction, and
|
|
7
7
|
`--write-agent` landing in the harness's real file as a managed block -- plus
|
|
8
8
|
the managed-block merge rules it relies on.
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Guard against internal working-artefact labels leaking into tracked files.
|
|
2
|
+
|
|
3
|
+
This repo's own working notes (a numbered plan, a phase/item inside one, a
|
|
4
|
+
work-item code, a review writeup dated and cited by day) live in the private,
|
|
5
|
+
untracked ``.agents/`` directory and must never show up in anything this
|
|
6
|
+
repository ships -- not source, not a comment or docstring, not a test, not
|
|
7
|
+
the changelog. Those labels are meaningful only to someone with that private
|
|
8
|
+
directory open; to everyone else (a user reading ``--help``, a contributor
|
|
9
|
+
reading a diff, a future maintainer years later) they are noise at best and a
|
|
10
|
+
dangling reference at worst.
|
|
11
|
+
|
|
12
|
+
**This repo is a special case.** Managing ``.agents/``, plans, findings and
|
|
13
|
+
decision logs *is* dotagents' product, so code, docs and tests that talk
|
|
14
|
+
about those as product features (a ``findings`` queue, an install path under
|
|
15
|
+
``~/.agents``, a decision-log id cited by convention, ``INDEX.md``) are
|
|
16
|
+
legitimate and are deliberately NOT scanned here -- ``tools/audit.py`` and the
|
|
17
|
+
project's own ``leak-check`` override (``.agents/dotagents/cmds/leak_check.py``)
|
|
18
|
+
already own that distinction, with the domain-aware exemptions it requires.
|
|
19
|
+
This test covers the other axis: labels that are never a dotagents product
|
|
20
|
+
concept under any reading -- a numbered plan/phase/item, a work-item code, or
|
|
21
|
+
a review cited by its date -- which are always internal-process leakage here,
|
|
22
|
+
exactly as in any other repo.
|
|
23
|
+
|
|
24
|
+
One exception has its own dedicated test below: decision ids (``D84``,
|
|
25
|
+
``[[D22]]``) are cited by convention throughout this repo's shipped notes and
|
|
26
|
+
code comments (an explicit, written repo rule -- see ``.agents/AGENTS.md``),
|
|
27
|
+
so the ban on them is scoped to ``CHANGELOG.md`` only, which is released
|
|
28
|
+
history and names no implementation detail by citing one.
|
|
29
|
+
|
|
30
|
+
This scans every ``git``-tracked text file for the label shapes and fails
|
|
31
|
+
naming every offending ``path:line``. A short, explicit allowlist covers the
|
|
32
|
+
rare genuine domain use that happens to match (each entry carries its own
|
|
33
|
+
one-line reason). Skipped entirely outside a git checkout (e.g. a built
|
|
34
|
+
sdist/wheel), since there is no ``git ls-files`` to run there.
|
|
35
|
+
|
|
36
|
+
The matching logic is exercised directly (not just against this repo's
|
|
37
|
+
current, clean state) by a second test that feeds it a planted offender per
|
|
38
|
+
label shape and asserts it is caught -- so a future edit that loosens a
|
|
39
|
+
pattern shows up as a test failure here, not as a silent gap. Those planted
|
|
40
|
+
strings are assembled from pieces at runtime rather than written literally,
|
|
41
|
+
so this file's own source never contains the shapes it is built to catch.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import re
|
|
47
|
+
import subprocess
|
|
48
|
+
from pathlib import Path
|
|
49
|
+
|
|
50
|
+
import pytest
|
|
51
|
+
|
|
52
|
+
_REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
53
|
+
_THIS_FILE = Path(__file__).resolve()
|
|
54
|
+
|
|
55
|
+
# --------------------------------------------------------------------------
|
|
56
|
+
# Label shapes
|
|
57
|
+
# --------------------------------------------------------------------------
|
|
58
|
+
# Each entry is (name, compiled pattern). Kept as data, not inlined into the
|
|
59
|
+
# scanner, so the planted-label test below can drive every shape generically.
|
|
60
|
+
#
|
|
61
|
+
# Deliberately NOT here: ``.agents`` path mentions and decision ids
|
|
62
|
+
# (``D\d{2}``) -- both are dotagents product concepts in this repo (the store
|
|
63
|
+
# path, the design log), already handled by `tools/audit.py` / `leak-check`
|
|
64
|
+
# with the domain-aware exemptions that distinction needs. Decision ids get
|
|
65
|
+
# their own narrower, CHANGELOG-scoped test below instead of a pattern here.
|
|
66
|
+
|
|
67
|
+
_PATTERNS = [
|
|
68
|
+
("numbered-plan", re.compile(r"\b[Pp]lan[ _-]?\d")),
|
|
69
|
+
("numbered-phase", re.compile(r"\b[Pp]hase[ _-]?\d")),
|
|
70
|
+
("numbered-item", re.compile(r"\b[Ii]tem[ _-]?\d")),
|
|
71
|
+
("task-range", re.compile(r"\bT\d+-T\d+\b")),
|
|
72
|
+
(
|
|
73
|
+
"work-item-code-paren",
|
|
74
|
+
re.compile(r"\((?:F|G|M|H|L|S|C|R|B|P|T)-?\d{1,3}\)"),
|
|
75
|
+
),
|
|
76
|
+
(
|
|
77
|
+
"work-item-code-bold",
|
|
78
|
+
re.compile(r"\*\*(?:F|G|M|H|L|S|C|R|B|P|T)\d{1,3}\*\*"),
|
|
79
|
+
),
|
|
80
|
+
("work-item-code-bare", re.compile(r"\b[FP]\d\b")),
|
|
81
|
+
("reviewer-reference", re.compile(r"\breviewer", re.IGNORECASE)),
|
|
82
|
+
("in-depth-review", re.compile(r"in-depth review", re.IGNORECASE)),
|
|
83
|
+
("review-finding", re.compile(r"review finding", re.IGNORECASE)),
|
|
84
|
+
(
|
|
85
|
+
"dated-review-reference",
|
|
86
|
+
re.compile(
|
|
87
|
+
r"\breview[ ,]+\d{4}-\d{2}-\d{2}\b|\b\d{4}-\d{2}-\d{2}[ ,]+review\b",
|
|
88
|
+
re.IGNORECASE,
|
|
89
|
+
),
|
|
90
|
+
),
|
|
91
|
+
("backlog-item", re.compile(r"\bbacklog\b", re.IGNORECASE)),
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
# (path, substring, reason) -- a match on `path` whose offending line
|
|
95
|
+
# contains `substring` is a genuine domain use, not an internal-artefact
|
|
96
|
+
# reference.
|
|
97
|
+
_ALLOWLIST = [
|
|
98
|
+
(
|
|
99
|
+
"tests/test_agents.py",
|
|
100
|
+
'AGENTS_AGENT": "reviewer"',
|
|
101
|
+
"`reviewer` here is a value of dotagents' own AGENTS_AGENT persona "
|
|
102
|
+
"variable (an agent-identity feature), not a reference to a human "
|
|
103
|
+
"reviewer or this repo's review process",
|
|
104
|
+
),
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
# Decision ids are cited by convention everywhere EXCEPT the changelog (a
|
|
108
|
+
# written repo rule -- see .agents/AGENTS.md, "the repo's own rule scopes the
|
|
109
|
+
# ban to CHANGELOG.md only"). Released history should describe what shipped,
|
|
110
|
+
# not point a reader at a private design-log entry they cannot open.
|
|
111
|
+
_DECISION_ID_RE = re.compile(r"\[\[D\d{2}\]\]|\bD\d{2}\b")
|
|
112
|
+
_CHANGELOG_PATH = "CHANGELOG.md"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _is_allowed(path: str, line: str) -> bool:
|
|
116
|
+
return any(
|
|
117
|
+
path == allowed_path and substring in line
|
|
118
|
+
for allowed_path, substring, _reason in _ALLOWLIST
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _scan_text(text: str):
|
|
123
|
+
"""Yield (pattern_name, line_no, line) for every label match in `text`."""
|
|
124
|
+
for line_no, line in enumerate(text.splitlines(), start=1):
|
|
125
|
+
for name, pattern in _PATTERNS:
|
|
126
|
+
if pattern.search(line):
|
|
127
|
+
yield name, line_no, line
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _git_tracked_files():
|
|
131
|
+
try:
|
|
132
|
+
result = subprocess.run(
|
|
133
|
+
["git", "ls-files", "-z"],
|
|
134
|
+
cwd=_REPO_ROOT,
|
|
135
|
+
capture_output=True,
|
|
136
|
+
timeout=30,
|
|
137
|
+
)
|
|
138
|
+
except (OSError, subprocess.SubprocessError):
|
|
139
|
+
return None
|
|
140
|
+
if result.returncode != 0:
|
|
141
|
+
return None
|
|
142
|
+
# NUL-separated (-z): plain output C-quotes and octal-escapes any path
|
|
143
|
+
# with a non-ASCII byte, which then cannot be opened.
|
|
144
|
+
return [p for p in result.stdout.decode("utf-8").split("\0") if p]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_no_internal_labels_in_tracked_files():
|
|
148
|
+
tracked = _git_tracked_files()
|
|
149
|
+
if tracked is None:
|
|
150
|
+
pytest.skip("not inside a git checkout -- nothing to scan")
|
|
151
|
+
|
|
152
|
+
offenses = []
|
|
153
|
+
for rel_path in tracked:
|
|
154
|
+
path = _REPO_ROOT / rel_path
|
|
155
|
+
if path.resolve() == _THIS_FILE:
|
|
156
|
+
continue # this file's own docstrings describe the label shapes
|
|
157
|
+
if not path.is_file():
|
|
158
|
+
continue # a tracked submodule/symlink target that isn't here
|
|
159
|
+
try:
|
|
160
|
+
text = path.read_text(encoding="utf-8")
|
|
161
|
+
except (UnicodeDecodeError, OSError):
|
|
162
|
+
continue # binary or unreadable -- not a text file we can scan
|
|
163
|
+
|
|
164
|
+
posix_path = Path(rel_path).as_posix()
|
|
165
|
+
for name, line_no, line in _scan_text(text):
|
|
166
|
+
if _is_allowed(posix_path, line):
|
|
167
|
+
continue
|
|
168
|
+
offenses.append(f"{posix_path}:{line_no}: [{name}] {line.strip()}")
|
|
169
|
+
|
|
170
|
+
assert not offenses, "internal-artefact labels found:\n" + "\n".join(offenses)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_no_decision_ids_in_changelog():
|
|
174
|
+
"""Decision ids are a legitimate dotagents citation everywhere else, but
|
|
175
|
+
CHANGELOG.md is released history: it should say what shipped, not point a
|
|
176
|
+
reader at a private design-log entry (`.agents/decisions/D<nn>.md`) they
|
|
177
|
+
cannot open. Scoped to this one file by explicit repo policy."""
|
|
178
|
+
tracked = _git_tracked_files()
|
|
179
|
+
if tracked is None:
|
|
180
|
+
pytest.skip("not inside a git checkout -- nothing to scan")
|
|
181
|
+
if _CHANGELOG_PATH not in tracked:
|
|
182
|
+
pytest.skip("no %s in this checkout" % _CHANGELOG_PATH)
|
|
183
|
+
|
|
184
|
+
text = (_REPO_ROOT / _CHANGELOG_PATH).read_text(encoding="utf-8")
|
|
185
|
+
offenses = [
|
|
186
|
+
f"{_CHANGELOG_PATH}:{line_no}: {line.strip()}"
|
|
187
|
+
for line_no, line in enumerate(text.splitlines(), start=1)
|
|
188
|
+
if _DECISION_ID_RE.search(line)
|
|
189
|
+
]
|
|
190
|
+
assert not offenses, "decision ids found in released history:\n" + "\n".join(
|
|
191
|
+
offenses
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_label_patterns_catch_a_planted_offender():
|
|
196
|
+
"""Each pattern above actually matches a realistic offending line.
|
|
197
|
+
|
|
198
|
+
Every sample is built from separate pieces and joined at the assertion,
|
|
199
|
+
so the literal offending text never appears in this file's own source.
|
|
200
|
+
"""
|
|
201
|
+
samples = {
|
|
202
|
+
"numbered-plan": " ".join(["See", "Pl" + "an", "7", "for context."]),
|
|
203
|
+
"numbered-phase": " ".join(["Start", "Ph" + "ase", "2", "now."]),
|
|
204
|
+
"numbered-item": " ".join(["Fixes", "it" + "em", "3", "from the list."]),
|
|
205
|
+
"task-range": "Measured after " + "T1" + "-" + "T7" + " landed.",
|
|
206
|
+
"work-item-code-paren": "Async support " + "(" + "F4" + ")" + " added.",
|
|
207
|
+
"work-item-code-bold": "- **" + "F4" + "** Async support added.",
|
|
208
|
+
"work-item-code-bare": "See " + "F4" + "/" + "P2" + " for the change.",
|
|
209
|
+
"reviewer-reference": "Mirrors the " + "review" + "er's fixture.",
|
|
210
|
+
"in-depth-review": "Findings from the " + "in-depth" + " review.",
|
|
211
|
+
"review-finding": "A " + "review find" + "ing about MCP.",
|
|
212
|
+
"dated-review-reference": "Fixed per " + "review" + " " + "2026-09-23" + ".",
|
|
213
|
+
"backlog-item": "Three fixes from the " + "backl" + "og.",
|
|
214
|
+
}
|
|
215
|
+
assert set(samples) == {
|
|
216
|
+
name for name, _ in _PATTERNS
|
|
217
|
+
}, "every pattern above must have a planted-offender sample"
|
|
218
|
+
for name, pattern in _PATTERNS:
|
|
219
|
+
matches = list(_scan_text(samples[name]))
|
|
220
|
+
assert (
|
|
221
|
+
matches and matches[0][0] == name
|
|
222
|
+
), f"pattern {name!r} failed to catch its own planted offender"
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_decision_id_pattern_catches_a_planted_offender():
|
|
226
|
+
"""Exercised separately from `_PATTERNS` since it only applies to one
|
|
227
|
+
file; same construction-from-pieces discipline as the test above."""
|
|
228
|
+
sample = "Per " + "D" + "01" + ", the SDK stays optional."
|
|
229
|
+
assert _DECISION_ID_RE.search(sample)
|
|
230
|
+
sample_wikilink = "See " + "[[D" + "22" + "]]" + " for the rationale."
|
|
231
|
+
assert _DECISION_ID_RE.search(sample_wikilink)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
"""`dotagents overlays` command behaviour
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
"""`dotagents overlays` command behaviour fixes: name normalization on
|
|
2
|
+
add/remove, remove's un-merge, skills published from the INSTALLED copy,
|
|
3
|
+
`sync --copy` and changed files, dry-run setup reporting, up-front
|
|
4
4
|
validation, `requires`, `show` and the manifest reader's comment handling --
|
|
5
5
|
plus the failure modes found since:
|
|
6
6
|
confinement of `requires`/`rules`, broken repos and registries, removing a
|
|
7
7
|
linked overlay, and what `sync` takes from where. Those still open in the
|
|
8
|
-
product are strict xfails naming the
|
|
8
|
+
product are strict xfails naming the known issue.
|
|
9
9
|
|
|
10
10
|
tmp dirs only, no network.
|
|
11
11
|
"""
|
|
@@ -492,7 +492,7 @@ def test_scratch_dir_is_one_dir_removed_at_exit():
|
|
|
492
492
|
def _open(issue, why):
|
|
493
493
|
"""A regression test for a confirmed, still-open product bug: strict, so
|
|
494
494
|
the fix turns it into an XPASS failure and the marker has to go."""
|
|
495
|
-
return pytest.mark.xfail(strict=True, reason="open (
|
|
495
|
+
return pytest.mark.xfail(strict=True, reason="open (%s): %s" % (issue, why))
|
|
496
496
|
|
|
497
497
|
|
|
498
498
|
def test_requires_cannot_install_outside_the_overlays_dir(world, tmp_path):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
"""Quoting and injection fixes
|
|
2
|
-
|
|
3
|
-
`
|
|
1
|
+
"""Quoting and injection fixes: the CwdChanged hook's path, PowerShell's
|
|
2
|
+
typographic quotes, the env-file path handed to bash, `DOTAGENTS_*` in
|
|
3
|
+
`env`'s full output, and git option injection through a spec.
|
|
4
4
|
|
|
5
5
|
tmp dirs only; HOME/USERPROFILE and the scope vars are redirected.
|
|
6
6
|
"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_overlay/dotagents/cmds/findings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_overlay/dotagents/templates/AGENTS.md
RENAMED
|
File without changes
|
{dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_overlay/dotagents/templates/PROJECT.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|