dotagents-cli 0.8.1__tar.gz → 0.8.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 (120) hide show
  1. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/CHANGELOG.md +23 -1
  2. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/PKG-INFO +1 -1
  3. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/docs/guide/private-sync.md +17 -10
  4. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/AGENTS.md +5 -2
  5. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/__init__.py +1 -1
  6. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/_overlay/dotagents/cmds/findings.py +21 -0
  7. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/_overlays.py +4 -3
  8. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/_scope.py +25 -2
  9. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/_wrappers.py +34 -23
  10. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_findings_cmd.py +14 -0
  11. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_fix_scope_context.py +27 -0
  12. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_wrappers.py +29 -3
  13. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/.gitignore +0 -0
  14. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/LICENSE +0 -0
  15. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/README.md +0 -0
  16. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/docs/api/agents.md +0 -0
  17. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/docs/api/cli.md +0 -0
  18. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/docs/api/commands.md +0 -0
  19. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/docs/api/context.md +0 -0
  20. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/docs/api/env.md +0 -0
  21. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/docs/api/fs.md +0 -0
  22. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/docs/api/hooks.md +0 -0
  23. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/docs/api/merge.md +0 -0
  24. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/docs/api/overlays.md +0 -0
  25. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/docs/api/reference.md +0 -0
  26. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/docs/api/resources.md +0 -0
  27. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/docs/api/scope.md +0 -0
  28. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/docs/api/skills.md +0 -0
  29. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/docs/api/sources.md +0 -0
  30. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/docs/api/wrappers.md +0 -0
  31. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/docs/changelog.md +0 -0
  32. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/docs/guide/authoring.md +0 -0
  33. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/docs/guide/commands.md +0 -0
  34. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/docs/guide/install.md +0 -0
  35. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/docs/guide/overlays.md +0 -0
  36. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/docs/index.md +0 -0
  37. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/install.py +0 -0
  38. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/mkdocs.yml +0 -0
  39. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/pyproject.toml +0 -0
  40. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/__main__.py +0 -0
  41. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/_agents.py +0 -0
  42. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/_context.py +0 -0
  43. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/_env.py +0 -0
  44. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/_fs.py +0 -0
  45. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/_hooks.py +0 -0
  46. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/_ignore.py +0 -0
  47. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/_launchers.py +0 -0
  48. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/_merge.py +0 -0
  49. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/_overlay/dotagents/cmds/launch.py +0 -0
  50. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/_overlay/dotagents/hooks/preinvocation_antigravity_context.py +0 -0
  51. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/_overlay/dotagents/hooks/pretooluse_codex_env.py +0 -0
  52. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/_overlay/dotagents/hooks/sessionstart_codex_context.py +0 -0
  53. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/_overlay/dotagents/templates/AGENTS.md +0 -0
  54. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/_overlay/dotagents/templates/PROJECT.md +0 -0
  55. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/_pipwheel.py +0 -0
  56. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/_pyvenv.py +0 -0
  57. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/_resources.py +0 -0
  58. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/_skills.py +0 -0
  59. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/_sources.py +0 -0
  60. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/cli/__init__.py +0 -0
  61. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/cli/_common.py +0 -0
  62. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/cli/about.py +0 -0
  63. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/cli/build_pyz.py +0 -0
  64. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/cli/context.py +0 -0
  65. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/cli/env.py +0 -0
  66. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/cli/init.py +0 -0
  67. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/cli/overlays.py +0 -0
  68. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/cli/path.py +0 -0
  69. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/src/dotagents/py.typed +0 -0
  70. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/_helpers.py +0 -0
  71. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/_shell.py +0 -0
  72. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/conftest.py +0 -0
  73. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_about.py +0 -0
  74. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_agents.py +0 -0
  75. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_api_header.py +0 -0
  76. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_audit_leak.py +0 -0
  77. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_build_pyz.py +0 -0
  78. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_cli_main.py +0 -0
  79. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_cloud_setup.py +0 -0
  80. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_cmds_discovery.py +0 -0
  81. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_context.py +0 -0
  82. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_env.py +0 -0
  83. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_env_cli_scope.py +0 -0
  84. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_env_format.py +0 -0
  85. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_env_into.py +0 -0
  86. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_env_regressions.py +0 -0
  87. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_env_safety.py +0 -0
  88. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_fix_adapters.py +0 -0
  89. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_fix_cli.py +0 -0
  90. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_fix_env.py +0 -0
  91. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_fix_hooks.py +0 -0
  92. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_fix_init.py +0 -0
  93. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_fix_overlays.py +0 -0
  94. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_fix_packaging.py +0 -0
  95. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_fix_skills_adapters.py +0 -0
  96. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_git_ssl_revoke.py +0 -0
  97. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_hooks.py +0 -0
  98. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_ignore.py +0 -0
  99. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_init_base.py +0 -0
  100. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_last_mile.py +0 -0
  101. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_launch_cmd.py +0 -0
  102. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_launchers.py +0 -0
  103. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_local_additions.py +0 -0
  104. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_mcp.py +0 -0
  105. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_no_internal_labels.py +0 -0
  106. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_overlay_libs.py +0 -0
  107. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_overlays_cli.py +0 -0
  108. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_overlays_command.py +0 -0
  109. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_path_timeout_frontmatter.py +0 -0
  110. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_pipwheel.py +0 -0
  111. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_powershell_env_hook.py +0 -0
  112. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_pyvenv_core.py +0 -0
  113. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_quoting_injection_safety.py +0 -0
  114. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_scope.py +0 -0
  115. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_sources.py +0 -0
  116. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_sources_uri.py +0 -0
  117. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tests/test_wire_hooks.py +0 -0
  118. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tools/audit.py +0 -0
  119. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tools/cloud-setup.sh +0 -0
  120. {dotagents_cli-0.8.1 → dotagents_cli-0.8.2}/tools/pyz_smoke.sh +0 -0
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.8.2] - 2026-10-25
11
+
12
+ ### Fixed
13
+
14
+ - `findings` (and `init` / `overlays`) run from inside a project's `.agents`, such as
15
+ `<project>/.agents/findings`, target that project. They used to write a second
16
+ `.agents` inside the store. Deeper than a top-level directory of the user store,
17
+ where no project can be named, they refuse instead of writing there.
18
+ - `findings` keeps a hand-written `INDEX.md`: before the first regeneration it is
19
+ copied to `INDEX.md.handwritten`, with a warning.
20
+ - The routing note above overlay paths no longer suggests `dotagents env` for
21
+ reading an overlay root (it prints the whole environment, secrets included); it
22
+ says to read just the variable.
23
+
24
+ - The `sh` wrapper `init` writes into `<store>/bin/dotagents` works from another
25
+ system that shares the store, such as WSL reading a Windows store. It ran the
26
+ interpreter recorded by `init`, a Windows path there; where that interpreter
27
+ does not run it now takes the store's agents' venv for that OS and machine
28
+ (`.pyvenv/<version>-<os>-<arch>`), else `python3`, and converts a recorded
29
+ `C:/...` pyz path with `wslpath`.
30
+
10
31
  ## [0.8.1] - 2026-10-21
11
32
 
12
33
  ### Added
@@ -1546,7 +1567,8 @@ Patch release: the PATH/POSIX-conversion fix above (the only change since 0.3.0)
1546
1567
  affected module sources (`dotagents.cli`, `duho.presets`) to extracted temp files
1547
1568
  before dispatch; a no-op for a plain install.
1548
1569
 
1549
- [Unreleased]: https://github.com/jose-pr/dotagents/compare/v0.8.1...HEAD
1570
+ [Unreleased]: https://github.com/jose-pr/dotagents/compare/v0.8.2...HEAD
1571
+ [0.8.2]: https://github.com/jose-pr/dotagents/compare/v0.8.1...v0.8.2
1550
1572
  [0.8.1]: https://github.com/jose-pr/dotagents/compare/v0.8.0...v0.8.1
1551
1573
  [0.8.0]: https://github.com/jose-pr/dotagents/compare/v0.7.6...v0.8.0
1552
1574
  [0.7.6]: https://github.com/jose-pr/dotagents/compare/v0.7.5...v0.7.6
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: dotagents-cli
3
- Version: 0.8.1
3
+ Version: 0.8.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
@@ -11,13 +11,19 @@ overlay is what makes them exist; a plain dotagents has no private-sync surface
11
11
 
12
12
  ## The idea
13
13
 
14
- Your global config store **is** a private git repo. Its root is the per-user config;
15
- a `projects/<name>/` tree inside it holds each project's private payload. For a
16
- checked-out project, its `.agents` directory is a **symlink** into the store (the
17
- project's `.gitignore` already excludes `.agents` per the Leakage rule, so the link
14
+ Your global config store **is** a private git repo, and its `main` branch is the
15
+ per-user config. Each project's private payload is a **branch** of it, named after
16
+ the project, and the project's `.agents` directory is a git worktree on that branch
17
+ (the project's `.gitignore` already excludes `.agents` per the Leakage rule, so it
18
18
  never lands in the public repo). `<name>` defaults to the project's basename, so a
19
- local checkout and a cloud checkout of the same project resolve to the same store
20
- entry.
19
+ local checkout and a cloud checkout of the same project resolve to the same branch.
20
+
21
+ That branch can live elsewhere: `link-project --store-repo self` keeps it on the
22
+ project's own repository (branch `agents`), and `--store-repo <url>` on any other,
23
+ such as a team account's (cloned into `.agents`); `--store-branch` names the branch.
24
+ The choice is recorded in the config repo, so every machine links the project the
25
+ same way. A store kept as a `projects/<name>/` directory, symlinked into the
26
+ project, still works and is kept by a project that already has one.
21
27
 
22
28
  The store's installed overlays can travel two ways:
23
29
 
@@ -39,11 +45,12 @@ dotagents overlays add private-sync -g \
39
45
  --repo "https://github.com/jose-pr/dotagents.git@repo#overlays" # commands + kb + hooks
40
46
 
41
47
  # link-project / sync-project come FROM that overlay -- run `overlays add` first.
42
- dotagents link-project . # symlink this project's .agents into its store
48
+ dotagents link-project . # check this project's store branch out at .agents
43
49
  # (an existing real .agents/ is adopted on the
44
- # first link; --copy mirrors it as a real dir
45
- # for no-symlink systems)
46
- dotagents sync-project -m "msg" # git pull --rebase / commit / push the store
50
+ # first link)
51
+ dotagents link-project . --store-repo self # ...on the project's own repo
52
+ dotagents link-project . --store-repo <team-repo-url> # ...on another repo
53
+ dotagents sync-project --project . -m "msg" # commit / push the project's branch, then the config
47
54
  dotagents sync-project --remote <url> -m init # one-command bootstrap
48
55
  ```
49
56
 
@@ -722,8 +722,11 @@ objects `{"matcher"?: str, "hooks": [{"type": "command", "command": str, ...}]}`
722
722
 
723
723
  ## `dotagents._wrappers`
724
724
 
725
- - `POSIX_TEMPLATE`, `POSIX_TEMPLATE_REL` (pyz path relative to the wrapper),
726
- `POSIX_TEMPLATE_MODULE` (`"<python>" -m dotagents`) — the `sh` wrapper bodies.
725
+ - `POSIX_TEMPLATE` — the `sh` wrapper body, for a `.pyz` (absolute, or relative
726
+ to the wrapper) or `-m dotagents`. It runs the recorded interpreter; where that
727
+ does not run (a Windows store read from WSL) it takes the store's agents' venv
728
+ for this OS and machine (`.pyvenv/*-<os>-<arch>/bin/python`), else `python3`,
729
+ and turns a recorded `C:/...` pyz path into a local one with `wslpath`.
727
730
  - `write_wrappers(bin_dir, pyz_path, python=None, *, relative=False) -> list[Path]` —
728
731
  `dotagents` (sh) and `dotagents.cmd`, both always, running the `.pyz`.
729
732
  - `write_module_wrappers(bin_dir, python=None) -> list[Path]` — both forms running
@@ -1,3 +1,3 @@
1
1
  """dotagents: installable CLI for the dotagents agent-config payload."""
2
2
 
3
- __version__ = "0.8.1"
3
+ __version__ = "0.8.2"
@@ -493,11 +493,32 @@ class FindingsStore:
493
493
  return "\n".join(lines) + "\n"
494
494
 
495
495
  def write_index(self) -> Path:
496
+ _keep_handwritten(self.index_path, "Regenerated by `dotagents findings`")
496
497
  # Atomic: an agent may be reading the index while a change rewrites it.
497
498
  write_text_lf(self.index_path, self.render_index(), atomic=True)
498
499
  return self.index_path
499
500
 
500
501
 
502
+ def _keep_handwritten(index: Path, marker: str) -> None:
503
+ """Copy an index this command did not write (it lacks ``marker``) to
504
+ ``INDEX.md.handwritten`` before it is regenerated, saying so. The backup is
505
+ not ``*.md``, so it is never read as an entry."""
506
+ try:
507
+ text = index.read_text(encoding="utf-8")
508
+ except OSError:
509
+ return
510
+ if marker in text:
511
+ return
512
+ backup = index.with_name(index.name + ".handwritten")
513
+ n = 1
514
+ while backup.exists():
515
+ n += 1
516
+ backup = index.with_name("%s.handwritten-%d" % (index.name, n))
517
+ write_text_lf(backup, text)
518
+ logging.getLogger("dotagents").warning(
519
+ "%s was written by hand; it is kept as %s and regenerated", index, backup.name)
520
+
521
+
501
522
  # --------------------------------------------------------------------------- #
502
523
  # The command
503
524
  # --------------------------------------------------------------------------- #
@@ -1166,9 +1166,10 @@ def _compose_block(base_text: str, overlays, logger) -> str:
1166
1166
  #: Emitted once above overlay routing lines that use the per-overlay root vars.
1167
1167
  OVERLAY_ROOT_NOTE = (
1168
1168
  "Overlay paths below use `$<NAME>_OVERLAY_ROOT` -- an environment variable "
1169
- "`dotagents env` exports per installed overlay (its install dir); resolve it "
1170
- "in a shell (`echo $ENGINEERING_OVERLAY_ROOT`; PowerShell `$env:ENGINEERING_OVERLAY_ROOT`) "
1171
- "before opening the file with a file tool."
1169
+ "already set in your shell for each installed overlay (its install dir); read "
1170
+ "just that one (`echo $ENGINEERING_OVERLAY_ROOT`; PowerShell "
1171
+ "`$env:ENGINEERING_OVERLAY_ROOT`) before opening the file with a file tool, "
1172
+ "never the whole environment (`dotagents env` prints secrets)."
1172
1173
  )
1173
1174
 
1174
1175
 
@@ -505,6 +505,27 @@ def _project_containing(start: Path) -> Path:
505
505
  return start
506
506
 
507
507
 
508
+ def _outside_store(cwd: Path) -> Path:
509
+ """``cwd``, or -- when it lies inside a ``.agents`` directory -- the project
510
+ that ``.agents`` belongs to (the parent of the outermost one): a command run
511
+ from ``<project>/.agents/findings`` means that project, never a second
512
+ ``.agents`` inside its store. Deeper than a top-level directory of the user
513
+ store (a linked project store reached through its target) there is no
514
+ project to name, so that is refused rather than written into the store."""
515
+ parts = cwd.parts
516
+ if ".agents" not in parts:
517
+ return cwd
518
+ index = parts.index(".agents")
519
+ root = Path(*parts[:index]) if index else cwd
520
+ store = resolve_user_store()
521
+ if _same_path(root / ".agents", store) and len(parts) > index + 2:
522
+ raise SystemExit(
523
+ "error: %s is inside the agents store %s, not a project; run from the "
524
+ "project's root (or pass --dir / --agents-dir)" % (cwd, store)
525
+ )
526
+ return root
527
+
528
+
508
529
  def install_project_root() -> Path:
509
530
  """The project root the WRITING commands (``init``, ``overlays``,
510
531
  ``findings``) target when none is passed explicitly.
@@ -519,8 +540,10 @@ def install_project_root() -> Path:
519
540
 
520
541
  ``env`` / ``context`` keep :func:`project_root_default`, where the pin wins
521
542
  regardless of the cwd: what they read must stay stable across the
522
- subdirectories a session's commands run in."""
523
- cwd = Path.cwd()
543
+ subdirectories a session's commands run in.
544
+
545
+ A cwd inside a ``.agents`` counts as its project's root (:func:`_outside_store`)."""
546
+ cwd = _outside_store(Path.cwd())
524
547
  for var in ("AGENTS_PROJECT_ROOT", *_HARNESS_PROJECT_ROOT_VARS):
525
548
  value = os.environ.get(var)
526
549
  if not value:
@@ -6,20 +6,39 @@ import stat
6
6
  import sys
7
7
  from pathlib import Path
8
8
 
9
- POSIX_TEMPLATE = '#!/bin/sh\nexec "{python}" "{pyz}" "$@"\n'
10
-
11
- # Relative forms: resolve the pyz from the wrapper's own directory, so moving or
12
- # copying the scope dir does not break the command.
13
- # `readlink -f` so a wrapper run through a symlink (into ~/.local/bin, say)
14
- # still finds the pyz beside the real file; plain `$0` where readlink -f fails.
15
- POSIX_TEMPLATE_REL = (
16
- '#!/bin/sh\nd=$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")")\n'
17
- 'exec "{python}" "$d/{pyz}" "$@"\n'
9
+ # The sh wrapper. `{target}` is what the interpreter runs: the pyz (absolute,
10
+ # or `"$d/..."` relative to the wrapper's own directory, so moving the scope dir
11
+ # does not break it) or `-m dotagents` for a plain install. `readlink -f` so a
12
+ # wrapper run through a symlink still finds what is beside the real file.
13
+ #
14
+ # The interpreter recorded is the one of the system that ran `init`. A store
15
+ # shared with another system -- WSL reading a Windows store -- reaches a
16
+ # recorded path that does not run there; the wrapper then takes that system's
17
+ # own: the store's agents' venv for this OS and machine (`.pyvenv/*-<os>-<arch>`,
18
+ # the naming `dotagents._pyvenv` uses), else `python3`, and a recorded
19
+ # `C:/...` pyz path goes through `wslpath`.
20
+ POSIX_TEMPLATE = (
21
+ '#!/bin/sh\n'
22
+ 'd=$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")")\n'
23
+ 'py="{python}"\n'
24
+ 'set -- {target} "$@"\n'
25
+ 'if [ ! -x "$py" ]; then\n'
26
+ ' case "$(uname -s)" in Darwin) os=darwin ;; *) os=posix ;; esac\n'
27
+ ' py=\n'
28
+ ' for v in "$d"/../.pyvenv/*-"$os"-"$(uname -m)"/bin/python; do\n'
29
+ ' [ -x "$v" ] && py=$v\n'
30
+ ' done\n'
31
+ ' [ -n "$py" ] || py=$(command -v python3 || command -v python) || {{\n'
32
+ ' echo "dotagents: no Python interpreter found" >&2; exit 127; }}\n'
33
+ ' case "$1" in [A-Za-z]:/*) if command -v wslpath >/dev/null 2>&1; then\n'
34
+ ' p=$(wslpath -u "$1"); shift; set -- "$p" "$@"; fi ;; esac\n'
35
+ 'fi\n'
36
+ 'exec "$py" "$@"\n'
18
37
  )
19
38
 
20
- # Module mode: a plain (pip / editable) install has no pyz to point at, so the
21
- # wrapper runs the package through the interpreter that has it installed.
22
- POSIX_TEMPLATE_MODULE = '#!/bin/sh\nexec "{python}" -m dotagents "$@"\n'
39
+
40
+ def _posix_wrapper(python: str, target: str) -> str:
41
+ return POSIX_TEMPLATE.format(python=Path(python).as_posix(), target=target)
23
42
 
24
43
 
25
44
  def _cmd_safe(path: str) -> str:
@@ -88,7 +107,7 @@ def write_module_wrappers(bin_dir: Path, python: "str | None" = None) -> "list[P
88
107
  python = python or sys.executable
89
108
  sh_path, cmd_path = bin_dir / "dotagents", bin_dir / "dotagents.cmd"
90
109
  with open(sh_path, "w", encoding="utf-8", newline="") as f:
91
- f.write(POSIX_TEMPLATE_MODULE.format(python=Path(python).as_posix()))
110
+ f.write(_posix_wrapper(python, "-m dotagents"))
92
111
  if os.name != "nt":
93
112
  sh_path.chmod(sh_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
94
113
  _write_cmd(cmd_path, '"%s" -m dotagents %%*' % _cmd_safe(str(python)))
@@ -149,17 +168,9 @@ def write_wrappers(
149
168
  sh_path = bin_dir / "dotagents"
150
169
  with open(sh_path, "w", encoding="utf-8", newline="") as f:
151
170
  if rel_pyz is not None:
152
- f.write(
153
- POSIX_TEMPLATE_REL.format(
154
- python=Path(python).as_posix(), pyz=rel_pyz.as_posix()
155
- )
156
- )
171
+ f.write(_posix_wrapper(python, '"$d/%s"' % rel_pyz.as_posix()))
157
172
  else:
158
- f.write(
159
- POSIX_TEMPLATE.format(
160
- python=Path(python).as_posix(), pyz=pyz_path.as_posix()
161
- )
162
- )
173
+ f.write(_posix_wrapper(python, '"%s"' % pyz_path.as_posix()))
163
174
  if os.name != "nt":
164
175
  mode = sh_path.stat().st_mode
165
176
  sh_path.chmod(mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
@@ -417,3 +417,17 @@ def test_index_requote_changes_only_the_values_yaml_cannot_read(findings_mod, tm
417
417
  assert "— [low][api] a: b" in (root / "INDEX.md").read_text(encoding="utf-8")
418
418
  # A second pass has nothing left to do.
419
419
  assert findings_mod.FindingsStore(root).requote() == []
420
+
421
+
422
+ def test_a_hand_written_index_is_kept_before_it_is_regenerated(findings_mod, tmp_path):
423
+ root = tmp_path / "findings"
424
+ root.mkdir()
425
+ (root / "INDEX.md").write_text("# My own index\n\n- evidence line\n", encoding="utf-8")
426
+ store = findings_mod.FindingsStore(root)
427
+ store.add("First problem", body="b")
428
+ kept = root / "INDEX.md.handwritten"
429
+ assert kept.read_text(encoding="utf-8") == "# My own index\n\n- evidence line\n"
430
+ assert "Regenerated by `dotagents findings`" in (root / "INDEX.md").read_text(encoding="utf-8")
431
+ store.add("Second problem", body="b") # a generated index is not backed up again
432
+ assert sorted(p.name for p in root.iterdir() if "handwritten" in p.name) == ["INDEX.md.handwritten"]
433
+ assert [f.name for f in store.active()] == ["first-problem", "second-problem"]
@@ -97,6 +97,33 @@ def test_a_store_that_appears_twice_is_walked_once(tmp_path):
97
97
  # scope-context-03: a pin the cwd has left is reported by the writing commands
98
98
  # --------------------------------------------------------------------------
99
99
 
100
+ def test_a_cwd_inside_a_project_store_means_that_project(tmp_path, monkeypatch):
101
+ """`dotagents findings index` run from `<project>/.agents/findings` wrote a
102
+ second `.agents` inside the store; the project is the one holding it."""
103
+ project = tmp_path / "proj"
104
+ (project / ".agents" / "findings").mkdir(parents=True)
105
+ monkeypatch.chdir(project / ".agents" / "findings")
106
+ assert _scope.resolve_scope(False).agents_root == project / ".agents"
107
+ # Pinned elsewhere, the same.
108
+ monkeypatch.setenv("AGENTS_PROJECT_ROOT", str(tmp_path / "other"))
109
+ assert _scope.resolve_scope(False).agents_root == project / ".agents"
110
+
111
+
112
+ def test_a_cwd_deep_in_the_user_store_is_refused(tmp_path, monkeypatch):
113
+ """Reached through a linked store's target, the cwd names no project; it is
114
+ refused rather than written into the store as `.agents/.agents`."""
115
+ store = tmp_path / "home" / ".agents"
116
+ monkeypatch.setenv("AGENTS_HOME", str(store))
117
+ deep = store / "projects" / "app" / "findings"
118
+ deep.mkdir(parents=True)
119
+ monkeypatch.chdir(deep)
120
+ with pytest.raises(SystemExit, match="inside the agents store"):
121
+ _scope.resolve_scope(False)
122
+ # A top-level directory of the store is the store itself: the user scope.
123
+ monkeypatch.chdir(store / "projects")
124
+ assert _scope.resolve_scope(False).agents_root == store
125
+
126
+
100
127
  def test_writing_outside_the_pinned_root_warns(tmp_path, monkeypatch, caplog):
101
128
  proj_a, proj_b = tmp_path / "projA", tmp_path / "projB"
102
129
  (proj_a / "sub").mkdir(parents=True)
@@ -74,8 +74,7 @@ def test_relative_declines_a_pyz_outside_the_scope(tmp_path):
74
74
  write_wrappers(tmp_path / "scope" / "bin", pyz, relative=True)
75
75
 
76
76
  sh = (tmp_path / "scope" / "bin" / "dotagents").read_text(encoding="utf-8")
77
- assert ".." not in sh, "must not emit a ../ chain"
78
- assert pyz.as_posix() in sh
77
+ assert 'set -- "%s"' % pyz.as_posix() in sh, "the pyz must be absolute, not a ../ chain"
79
78
 
80
79
 
81
80
  def test_absolute_by_default(tmp_path):
@@ -83,7 +82,7 @@ def test_absolute_by_default(tmp_path):
83
82
  write_wrappers(tmp_path / "bin", pyz)
84
83
  sh = (tmp_path / "bin" / "dotagents").read_text(encoding="utf-8")
85
84
  assert pyz.as_posix() in sh
86
- assert "dirname" not in sh
85
+ assert '"$d/' not in sh, "the pyz path is absolute, not relative to the wrapper"
87
86
 
88
87
 
89
88
  def test_crlf_in_cmd_and_lf_in_sh(tmp_path):
@@ -159,6 +158,33 @@ def test_relative_falls_back_when_no_relative_path_exists(tmp_path, monkeypatch)
159
158
  assert pyz.as_posix() in sh, "must fall back to an absolute path, not crash"
160
159
 
161
160
 
161
+ @pytest.mark.skipif(os.name == "nt", reason="the fallback is for a system where the recorded interpreter does not run")
162
+ def test_a_recorded_interpreter_that_does_not_run_falls_back_to_the_store_venv(tmp_path):
163
+ """A store shared with another system (WSL reading a Windows store): the
164
+ recorded interpreter is not there, so the store's agents' venv for this OS
165
+ and machine runs the pyz."""
166
+ import platform
167
+
168
+ store = tmp_path / "store"
169
+ store.mkdir()
170
+ pyz = store / "dotagents.pyz"
171
+ pyz.write_text(PROBE, encoding="utf-8")
172
+ write_wrappers(store / "bin", pyz, python="C:/Not/Here/python.exe", relative=True)
173
+ os_name = "darwin" if sys.platform == "darwin" else "posix"
174
+ venv_python = store / ".pyvenv" / ("3.99.0-%s-%s" % (os_name, platform.machine())) / "bin" / "python"
175
+ venv_python.parent.mkdir(parents=True)
176
+ venv_python.symlink_to(sys.executable)
177
+ _run_sh(store / "bin" / "dotagents", tmp_path)
178
+
179
+
180
+ @pytest.mark.skipif(os.name == "nt", reason="the fallback is for a system where the recorded interpreter does not run")
181
+ def test_with_no_store_venv_the_fallback_is_python3(tmp_path):
182
+ pyz = tmp_path / "dotagents.pyz"
183
+ pyz.write_text(PROBE, encoding="utf-8")
184
+ write_wrappers(tmp_path / "bin", pyz, python="C:/Not/Here/python.exe")
185
+ _run_sh(tmp_path / "bin" / "dotagents", tmp_path)
186
+
187
+
162
188
  def test_module_wrappers_run_the_installed_package(tmp_path):
163
189
  r"""A plain install has no pyz: `init` wrote no wrappers at all, and the
164
190
  PowerShell hooks called a `<store>\bin\dotagents.cmd` that never existed."""
File without changes
File without changes
File without changes
File without changes
File without changes