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.
Files changed (114) hide show
  1. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/CHANGELOG.md +15 -2
  2. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/PKG-INFO +3 -2
  3. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/README.md +2 -1
  4. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/guide/install.md +11 -1
  5. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/AGENTS.md +9 -1
  6. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/__init__.py +1 -1
  7. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_sources.py +60 -6
  8. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/cli/_common.py +4 -1
  9. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_cmds_discovery.py +2 -2
  10. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_context.py +1 -1
  11. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_env_regressions.py +6 -6
  12. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_env_safety.py +1 -2
  13. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_findings_cmd.py +4 -4
  14. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_fix_adapters.py +1 -1
  15. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_fix_cli.py +1 -1
  16. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_fix_env.py +1 -2
  17. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_fix_hooks.py +1 -1
  18. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_fix_init.py +1 -1
  19. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_fix_overlays.py +2 -2
  20. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_fix_scope_context.py +1 -2
  21. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_fix_skills_adapters.py +2 -2
  22. dotagents_cli-0.7.2/tests/test_git_ssl_revoke.py +102 -0
  23. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_last_mile.py +3 -3
  24. dotagents_cli-0.7.2/tests/test_no_internal_labels.py +231 -0
  25. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_overlays_cli.py +5 -5
  26. dotagents_cli-0.7.1/tests/test_review_security.py → dotagents_cli-0.7.2/tests/test_quoting_injection_safety.py +3 -3
  27. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/.gitignore +0 -0
  28. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/LICENSE +0 -0
  29. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/api/agents.md +0 -0
  30. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/api/cli.md +0 -0
  31. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/api/commands.md +0 -0
  32. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/api/context.md +0 -0
  33. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/api/env.md +0 -0
  34. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/api/fs.md +0 -0
  35. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/api/hooks.md +0 -0
  36. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/api/merge.md +0 -0
  37. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/api/overlays.md +0 -0
  38. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/api/reference.md +0 -0
  39. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/api/resources.md +0 -0
  40. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/api/scope.md +0 -0
  41. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/api/skills.md +0 -0
  42. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/api/sources.md +0 -0
  43. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/api/wrappers.md +0 -0
  44. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/changelog.md +0 -0
  45. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/guide/authoring.md +0 -0
  46. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/guide/commands.md +0 -0
  47. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/guide/overlays.md +0 -0
  48. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/guide/private-sync.md +0 -0
  49. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/docs/index.md +0 -0
  50. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/install.py +0 -0
  51. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/mkdocs.yml +0 -0
  52. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/pyproject.toml +0 -0
  53. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/__main__.py +0 -0
  54. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_agents.py +0 -0
  55. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_context.py +0 -0
  56. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_env.py +0 -0
  57. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_fs.py +0 -0
  58. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_hooks.py +0 -0
  59. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_ignore.py +0 -0
  60. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_merge.py +0 -0
  61. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_overlay/dotagents/cmds/findings.py +0 -0
  62. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_overlay/dotagents/cmds/launch.py +0 -0
  63. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_overlay/dotagents/hooks/preinvocation_antigravity_context.py +0 -0
  64. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_overlay/dotagents/hooks/pretooluse_codex_env.py +0 -0
  65. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_overlay/dotagents/hooks/sessionstart_codex_context.py +0 -0
  66. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_overlay/dotagents/templates/AGENTS.md +0 -0
  67. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_overlay/dotagents/templates/PROJECT.md +0 -0
  68. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_overlays.py +0 -0
  69. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_resources.py +0 -0
  70. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_scope.py +0 -0
  71. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_skills.py +0 -0
  72. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/_wrappers.py +0 -0
  73. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/cli/__init__.py +0 -0
  74. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/cli/about.py +0 -0
  75. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/cli/build_pyz.py +0 -0
  76. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/cli/context.py +0 -0
  77. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/cli/env.py +0 -0
  78. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/cli/init.py +0 -0
  79. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/cli/overlays.py +0 -0
  80. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/cli/path.py +0 -0
  81. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/src/dotagents/py.typed +0 -0
  82. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/_helpers.py +0 -0
  83. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/_shell.py +0 -0
  84. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/conftest.py +0 -0
  85. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_about.py +0 -0
  86. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_agents.py +0 -0
  87. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_api_header.py +0 -0
  88. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_audit_leak.py +0 -0
  89. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_build_pyz.py +0 -0
  90. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_cli_main.py +0 -0
  91. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_cloud_setup.py +0 -0
  92. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_env.py +0 -0
  93. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_env_cli_scope.py +0 -0
  94. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_env_format.py +0 -0
  95. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_env_into.py +0 -0
  96. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_fix_packaging.py +0 -0
  97. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_hooks.py +0 -0
  98. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_ignore.py +0 -0
  99. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_init_base.py +0 -0
  100. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_launch_cmd.py +0 -0
  101. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_local_additions.py +0 -0
  102. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_mcp.py +0 -0
  103. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_overlay_libs.py +0 -0
  104. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_overlays_command.py +0 -0
  105. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_path_timeout_frontmatter.py +0 -0
  106. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_powershell_env_hook.py +0 -0
  107. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_scope.py +0 -0
  108. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_sources.py +0 -0
  109. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_sources_uri.py +0 -0
  110. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_wire_hooks.py +0 -0
  111. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tests/test_wrappers.py +0 -0
  112. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tools/audit.py +0 -0
  113. {dotagents_cli-0.7.1 → dotagents_cli-0.7.2}/tools/cloud-setup.sh +0 -0
  114. {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 Plan-13 `Args`/`Cmd`
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.1...HEAD
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.1
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. This
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. This
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
- - `class SourceCache(root, logger=None)` — `repo_dir(location, ref=None) -> Path`
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
@@ -1,3 +1,3 @@
1
1
  """dotagents: installable CLI for the dotagents agent-config payload."""
2
2
 
3
- __version__ = "0.7.1"
3
+ __version__ = "0.7.2"
@@ -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
- target = _sources.locate(spec, _sources.SourceCache(Path(cache_root), logger))
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 (review 2026-09-09). duho propagates SyntaxError on purpose; the
301
- # dotagents layer is where it must stop.
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 -- review 2026-09-09, 1.5).
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 from the 2026-09-23 review, each a repro turned
2
- into a test: malformed env.py output, relative PATH entries, a session whose
3
- project root is the home directory, and the PowerShell rendering of
4
- typographic quotes and non-ASCII values through the real loader pipe.
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 review
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 (review 2026-09-23 %s): %s" % (issue, why)
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 (review 2026-09-09, findings 1.6 and
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 (review 2026-09-23). Cases the module still
288
- # gets wrong are strict xfails naming the issue: a fix flips them to XPASS
289
- # failures until the marker goes.
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 (review 2026-09-23 %s): %s" % (issue, why))
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 from the 2026-09-23 review: adapters are told the scope,
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 the CLI review fixes: command discovery that survives a
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,5 +1,4 @@
1
- """Regression tests for the env assembly and rendering review fixes
2
- (review 2026-09-23, "Env assembly and rendering").
1
+ """Regression tests for env assembly and rendering fixes.
3
2
 
4
3
  tmp dirs only, no network. Run from repo root: ``python -m pytest tests/``.
5
4
  """
@@ -1,4 +1,4 @@
1
- """Hook fixes from the 2026-09-23 review: the PowerShell handlers' Git Bash
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,4 +1,4 @@
1
- """Regression tests for the 2026-09-23 review's `init` / `launch` fixes:
1
+ """Regression tests for `init` / `launch` fixes:
2
2
 
3
3
  - `init --from` takes the documented URI, git and checkout forms (it crashed
4
4
  on every remote scheme and on a checkout root);
@@ -1,5 +1,5 @@
1
- """Regression tests for the 2026-09-23 review's "Overlays, sources and skills"
2
- findings: typed source errors, registry parsing, credential handling, the spec
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 review fixes
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 the 2026-09-23 review's skills, adapter and hook-cost
2
- findings: Claude's per-skill links are refreshed and pruned, `overlays
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
- Review 2026-09-09 (finding 1.5): `init` wrote `<store>/CLAUDE.md`, which Claude
4
- Code never reads, and `context` subtracted the store's AGENTS.md as "already
5
- loaded" on a static assumption. These tests pin the fix -- the include `init`
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 fixed in the 2026-09-09 review:
2
- name normalization on add/remove, remove's un-merge, skills published from the
3
- INSTALLED copy, `sync --copy` and changed files, dry-run setup reporting, up-front
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 review issue.
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 (review 2026-09-23 %s): %s" % (issue, why))
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 from the 2026-09-23 review: the CwdChanged hook's
2
- path, PowerShell's typographic quotes, the env-file path handed to bash,
3
- `DOTAGENTS_*` in `env`'s full output, and git option injection through a spec.
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