dotagents-cli 0.7.0__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.0 → dotagents_cli-0.7.2}/CHANGELOG.md +26 -2
  2. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/PKG-INFO +3 -2
  3. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/README.md +2 -1
  4. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/guide/commands.md +5 -1
  5. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/guide/install.md +11 -1
  6. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/guide/overlays.md +11 -2
  7. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/AGENTS.md +18 -3
  8. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/__init__.py +1 -1
  9. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_context.py +47 -3
  10. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_overlay/dotagents/templates/AGENTS.md +6 -0
  11. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_sources.py +60 -6
  12. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/cli/_common.py +4 -1
  13. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_cmds_discovery.py +2 -2
  14. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_context.py +1 -1
  15. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_env_regressions.py +6 -6
  16. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_env_safety.py +1 -2
  17. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_findings_cmd.py +4 -4
  18. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_fix_adapters.py +1 -1
  19. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_fix_cli.py +1 -1
  20. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_fix_env.py +1 -2
  21. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_fix_hooks.py +1 -1
  22. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_fix_init.py +1 -1
  23. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_fix_overlays.py +2 -2
  24. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_fix_scope_context.py +1 -2
  25. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_fix_skills_adapters.py +2 -2
  26. dotagents_cli-0.7.2/tests/test_git_ssl_revoke.py +102 -0
  27. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_last_mile.py +3 -3
  28. dotagents_cli-0.7.2/tests/test_local_additions.py +86 -0
  29. dotagents_cli-0.7.2/tests/test_no_internal_labels.py +231 -0
  30. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_overlays_cli.py +5 -5
  31. dotagents_cli-0.7.0/tests/test_review_security.py → dotagents_cli-0.7.2/tests/test_quoting_injection_safety.py +3 -3
  32. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/.gitignore +0 -0
  33. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/LICENSE +0 -0
  34. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/api/agents.md +0 -0
  35. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/api/cli.md +0 -0
  36. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/api/commands.md +0 -0
  37. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/api/context.md +0 -0
  38. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/api/env.md +0 -0
  39. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/api/fs.md +0 -0
  40. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/api/hooks.md +0 -0
  41. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/api/merge.md +0 -0
  42. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/api/overlays.md +0 -0
  43. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/api/reference.md +0 -0
  44. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/api/resources.md +0 -0
  45. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/api/scope.md +0 -0
  46. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/api/skills.md +0 -0
  47. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/api/sources.md +0 -0
  48. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/api/wrappers.md +0 -0
  49. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/changelog.md +0 -0
  50. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/guide/authoring.md +0 -0
  51. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/guide/private-sync.md +0 -0
  52. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/index.md +0 -0
  53. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/install.py +0 -0
  54. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/mkdocs.yml +0 -0
  55. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/pyproject.toml +0 -0
  56. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/__main__.py +0 -0
  57. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_agents.py +0 -0
  58. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_env.py +0 -0
  59. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_fs.py +0 -0
  60. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_hooks.py +0 -0
  61. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_ignore.py +0 -0
  62. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_merge.py +0 -0
  63. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_overlay/dotagents/cmds/findings.py +0 -0
  64. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_overlay/dotagents/cmds/launch.py +0 -0
  65. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_overlay/dotagents/hooks/preinvocation_antigravity_context.py +0 -0
  66. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_overlay/dotagents/hooks/pretooluse_codex_env.py +0 -0
  67. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_overlay/dotagents/hooks/sessionstart_codex_context.py +0 -0
  68. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_overlay/dotagents/templates/PROJECT.md +0 -0
  69. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_overlays.py +0 -0
  70. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_resources.py +0 -0
  71. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_scope.py +0 -0
  72. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_skills.py +0 -0
  73. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_wrappers.py +0 -0
  74. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/cli/__init__.py +0 -0
  75. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/cli/about.py +0 -0
  76. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/cli/build_pyz.py +0 -0
  77. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/cli/context.py +0 -0
  78. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/cli/env.py +0 -0
  79. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/cli/init.py +0 -0
  80. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/cli/overlays.py +0 -0
  81. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/cli/path.py +0 -0
  82. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/py.typed +0 -0
  83. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/_helpers.py +0 -0
  84. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/_shell.py +0 -0
  85. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/conftest.py +0 -0
  86. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_about.py +0 -0
  87. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_agents.py +0 -0
  88. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_api_header.py +0 -0
  89. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_audit_leak.py +0 -0
  90. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_build_pyz.py +0 -0
  91. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_cli_main.py +0 -0
  92. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_cloud_setup.py +0 -0
  93. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_env.py +0 -0
  94. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_env_cli_scope.py +0 -0
  95. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_env_format.py +0 -0
  96. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_env_into.py +0 -0
  97. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_fix_packaging.py +0 -0
  98. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_hooks.py +0 -0
  99. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_ignore.py +0 -0
  100. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_init_base.py +0 -0
  101. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_launch_cmd.py +0 -0
  102. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_mcp.py +0 -0
  103. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_overlay_libs.py +0 -0
  104. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_overlays_command.py +0 -0
  105. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_path_timeout_frontmatter.py +0 -0
  106. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_powershell_env_hook.py +0 -0
  107. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_scope.py +0 -0
  108. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_sources.py +0 -0
  109. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_sources_uri.py +0 -0
  110. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_wire_hooks.py +0 -0
  111. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_wrappers.py +0 -0
  112. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tools/audit.py +0 -0
  113. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tools/cloud-setup.sh +0 -0
  114. {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tools/pyz_smoke.sh +0 -0
@@ -7,6 +7,28 @@ 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
+
22
+ ## [0.7.1] - 2026-10-01
23
+
24
+ ### Added
25
+
26
+ - Your own additions to an installed overlay's file live at the same path under the
27
+ store root (`~/.agents/kb/RUST.md` for the rust overlay's `kb/RUST.md`, or
28
+ `<project>/.agents/kb/RUST.md`), where `overlays sync` never touches them. The
29
+ base `AGENTS.md` tells agents to read them alongside the overlay's file, and
30
+ `dotagents context` lists the ones it finds (JSON: `local_additions`).
31
+
10
32
  ## [0.7.0] - 2026-10-01
11
33
 
12
34
  ### Changed
@@ -1347,7 +1369,7 @@ Patch release: the PATH/POSIX-conversion fix above (the only change since 0.3.0)
1347
1369
 
1348
1370
  ### Changed
1349
1371
 
1350
- - 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`
1351
1373
  split means commands are now `class X(LoggingArgs, Cmd)` with a `__call__` entrypoint
1352
1374
  (was a bare `LoggingArgs` with `__run__`) and the umbrella root is
1353
1375
  `class Dotagents(LoggingArgs, Cli)`. Field declarations (annotation + help string +
@@ -1362,7 +1384,9 @@ Patch release: the PATH/POSIX-conversion fix above (the only change since 0.3.0)
1362
1384
  affected module sources (`dotagents.cli`, `duho.presets`) to extracted temp files
1363
1385
  before dispatch; a no-op for a plain install.
1364
1386
 
1365
- [Unreleased]: https://github.com/jose-pr/dotagents/compare/v0.7.0...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
1389
+ [0.7.1]: https://github.com/jose-pr/dotagents/compare/v0.7.0...v0.7.1
1366
1390
  [0.7.0]: https://github.com/jose-pr/dotagents/compare/v0.6.3...v0.7.0
1367
1391
  [0.6.3]: https://github.com/jose-pr/dotagents/compare/v0.6.2...v0.6.3
1368
1392
  [0.6.2]: https://github.com/jose-pr/dotagents/compare/v0.6.1...v0.6.2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: dotagents-cli
3
- Version: 0.7.0
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
@@ -216,7 +216,11 @@ Assembles the effective context an agent should load — the overlay `CONTEXT.md
216
216
  and the project root's own `AGENTS.md` / `AGENTS.local.md` — minus whatever the harness already loads by itself (for Claude Code, whatever its
217
217
  `CLAUDE.md` files really `@`-include) — and prints it to **stdout** by default
218
218
  (POSIX convention); pass a path to write a file, or `--write-agent` to merge it into
219
- each agent's own instruction file.
219
+ each agent's own instruction file. It ends with the skills on offer and, when there are
220
+ any, your **local additions to overlay files** — a store file at the same path as an
221
+ overlay's (`~/.agents/kb/RUST.md` for the rust overlay's `kb/RUST.md`; see
222
+ [Overlays](overlays.md)); `--format json` carries both as their own keys (`skills`,
223
+ `local_additions`).
220
224
 
221
225
  ```bash
222
226
  dotagents context # print the active agent's context to stdout
@@ -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
 
@@ -79,8 +79,17 @@ up under `<store>/install_backup/` first. What the `.gitignore` / `.ignore` at t
79
79
  overlay's root match (an ignore file in a subdirectory is just a file it ships), and
80
80
  tool caches, belong to the install (a setup script's output, local
81
81
  state) and are left alone; `--prune` clears them too. A `requires` added upstream is
82
- installed. An installed overlay is upstream's copy, not a place for your edits: keep
83
- those in your own files (a `kb/` file, `AGENTS.local.md`).
82
+ installed. An installed overlay is upstream's copy, not a place for your edits.
83
+
84
+ ### Your additions to an overlay's files
85
+
86
+ Put your own text for an overlay file at the **same path under the store root**: an
87
+ addition to the rust overlay's `kb/RUST.md` goes in `~/.agents/kb/RUST.md` (or
88
+ `<project>/.agents/kb/RUST.md` for one project). `sync` never touches it, and agents
89
+ read it alongside the overlay's file, winning on conflict: the base `AGENTS.md` tells
90
+ them to, and `dotagents context` lists every addition it finds (for each `.md` an
91
+ overlay ships in a subdirectory other than `skills/`). Private names, local tool paths
92
+ and links into your own notes belong there, never in the overlay.
84
93
 
85
94
  Removing an overlay deletes only its directory and unpublishes only the skills **it**
86
95
  published (a copy the user edited is left alone). Its lines in `AGENTS.md`'s managed
@@ -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
@@ -433,7 +441,8 @@ API:
433
441
  `Overlay.sort_key`, then contract-A `AGENTS.md` (every store and the project root)
434
442
  and `AGENTS.local.md` (project store and project root), each prefixed
435
443
  `<!-- Source: <path> -->`. Then a skills listing (`- **name** (`<SKILL.md>`):
436
- description`, from each skill's leading frontmatter; a project skill wins).
444
+ description`, from each skill's leading frontmatter; a project skill wins), then
445
+ a "Local additions to overlay files" section when `local_additions` finds any.
437
446
  `<PROJECT_ROOT>` and `<NAME_OVERLAY_ROOT>` placeholders expand; with
438
447
  `expand_vars` so do `$NAME_OVERLAY_ROOT` / `${NAME_OVERLAY_ROOT}`. `inline`
439
448
  appends the on-demand `.md` files the sources reference, each resolved against
@@ -441,7 +450,13 @@ API:
441
450
  system source never into the project).
442
451
  - `assemble_context_data(agent, scope, *, inline=False, expand_vars=True) -> dict`
443
452
  — `{"agent", "harness", "sources", "context", "skills": [{"name",
444
- "description", "path"}]}`; `context` without the skills listing.
453
+ "description", "path"}], "local_additions": [{"overlay_file", "local": [...]}]}`;
454
+ `context` without the skills and local-additions listings.
455
+ - `local_additions(scope) -> list[tuple[Path, list[Path]]]` — the user's own
456
+ additions to installed overlay files: for each `.md` an overlay in `scope.overlays`
457
+ ships in a subdirectory other than `skills/`, every store's file at the same
458
+ relative path (`<store>/kb/RUST.md` for the overlay's `kb/RUST.md`), in
459
+ `scope.stores` order. A store's root files never match.
445
460
 
446
461
  ## `dotagents._agents`
447
462
 
@@ -1,3 +1,3 @@
1
1
  """dotagents: installable CLI for the dotagents agent-config payload."""
2
2
 
3
- __version__ = "0.7.0"
3
+ __version__ = "0.7.2"
@@ -309,6 +309,44 @@ def _get_skills_listing(scope: _scope.Scope) -> str:
309
309
  return "\n\n## Available Skills (Opt-in)\n" + "\n".join(lines) + "\n"
310
310
 
311
311
 
312
+ def local_additions(scope: _scope.Scope) -> "list[tuple[Path, list[Path]]]":
313
+ """The user's own additions to installed overlay files: for each ``.md``
314
+ an overlay ships in a subdirectory (``kb/``, ``flows/``, ``rules/`` ... --
315
+ not ``skills/``, whose published copies sit at the same path in a store),
316
+ every store's file at the same relative path (``~/.agents/kb/RUST.md`` for
317
+ an overlay's ``kb/RUST.md``), in store order. An overlay file is upstream's
318
+ copy, replaced by ``overlays sync``; the addition is where the user's own
319
+ text lives. A store's root files (``AGENTS.md``, ``env.py``) never match:
320
+ only a file in a subdirectory counts."""
321
+ stores = list(scope.stores)
322
+ out: "list[tuple[Path, list[Path]]]" = []
323
+ for overlay in scope.overlays:
324
+ for path in overlay.files():
325
+ rel = path.relative_to(overlay.path)
326
+ if path.suffix.lower() != ".md" or len(rel.parts) < 2 or rel.parts[0] == "skills":
327
+ continue
328
+ found = [store / rel for store in stores if (store / rel).is_file()]
329
+ if found:
330
+ out.append((path, found))
331
+ return out
332
+
333
+
334
+ def _get_local_additions_listing(scope: _scope.Scope) -> str:
335
+ """The local additions (:func:`local_additions`) as a markdown section, or ''."""
336
+ additions = local_additions(scope)
337
+ if not additions:
338
+ return ""
339
+ lines = [
340
+ "- `%s` -- also read %s" % (path, ", ".join("`%s`" % p for p in found))
341
+ for path, found in additions
342
+ ]
343
+ return (
344
+ "\n\n## Local additions to overlay files\n"
345
+ "The user's own additions: read each with the overlay file it extends; it wins on conflict.\n"
346
+ + "\n".join(lines) + "\n"
347
+ )
348
+
349
+
312
350
  def _resolve_and_filter_sources(
313
351
  agent: _agents.Agent, scope: _scope.Scope
314
352
  ) -> "tuple[list[tuple[str, Path, Path | None]], list[Path]]":
@@ -424,6 +462,7 @@ def assemble_context(
424
462
  if not source_paths:
425
463
  return ""
426
464
  text += _get_skills_listing(scope)
465
+ text += _get_local_additions_listing(scope)
427
466
  return text
428
467
 
429
468
 
@@ -440,11 +479,12 @@ def assemble_context_data(
440
479
  "sources": [<absolute source path>, ...], # after harness subtraction
441
480
  "context": <assembled markdown text, minus the skills listing>,
442
481
  "skills": [{"name": ..., "description": ..., "path": <SKILL.md>}, ...],
482
+ "local_additions": [{"overlay_file": ..., "local": [<path>, ...]}, ...],
443
483
  }
444
484
  ``context`` is the same assembled(+inlined, with ``inline=True``) text the
445
- markdown format emits, but WITHOUT the skills listing appended -- skills are
446
- their own structured field so a consumer can render them separately and
447
- keep the opt-in distinction."""
485
+ markdown format emits, but WITHOUT the skills and local-additions listings
486
+ appended -- both are their own structured fields so a consumer can render
487
+ them separately."""
448
488
  text, source_paths = _assemble(agent, scope, inline, expand_vars)
449
489
 
450
490
  skills = [
@@ -457,4 +497,8 @@ def assemble_context_data(
457
497
  "sources": source_paths,
458
498
  "context": text,
459
499
  "skills": skills,
500
+ "local_additions": [
501
+ {"overlay_file": str(path), "local": [str(p) for p in found]}
502
+ for path, found in local_additions(scope)
503
+ ],
460
504
  }
@@ -16,6 +16,12 @@ Startup: annotate that you read `{{AGENTS_MD}}`.
16
16
  config). These files say what lives there and how to work in it, so reading them
17
17
  first is cheaper than rediscovering it from the source. How `.agents/` is
18
18
  populated — real directory, symlink, committed or not — is yours to decide.
19
+ - **Local additions to overlay files**: an overlay file (`$<NAME>_OVERLAY_ROOT/<path>`)
20
+ is upstream's copy, and `dotagents overlays sync` replaces it. The user's own
21
+ additions live at the same `<path>` under this config directory and the project's
22
+ `.agents/`: when you read an overlay file, read those too — they extend it and win on
23
+ conflict (`dotagents context` lists the ones it finds). Never write user-specific text
24
+ into an overlay file; put it there.
19
25
  - **Global-config misses**: if these instructions caused a mistake or rework, or you
20
26
  have an improvement idea, record it and move on — don't edit the config:
21
27
  `dotagents findings add -g "<one line>" -b "<what happened, evidence>"`. Read the
@@ -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()