dotagents-cli 0.7.4__tar.gz → 0.7.5__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.4 → dotagents_cli-0.7.5}/CHANGELOG.md +26 -1
  2. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/PKG-INFO +2 -2
  3. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/pyproject.toml +1 -1
  4. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/src/dotagents/AGENTS.md +5 -1
  5. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/src/dotagents/__init__.py +1 -1
  6. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/src/dotagents/cli/__init__.py +26 -65
  7. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/src/dotagents/cli/build_pyz.py +1 -1
  8. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_cli_main.py +1 -1
  9. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_cmds_discovery.py +15 -2
  10. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_fix_cli.py +0 -25
  11. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_fix_init.py +3 -4
  12. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/.gitignore +0 -0
  13. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/LICENSE +0 -0
  14. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/README.md +0 -0
  15. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/docs/api/agents.md +0 -0
  16. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/docs/api/cli.md +0 -0
  17. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/docs/api/commands.md +0 -0
  18. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/docs/api/context.md +0 -0
  19. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/docs/api/env.md +0 -0
  20. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/docs/api/fs.md +0 -0
  21. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/docs/api/hooks.md +0 -0
  22. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/docs/api/merge.md +0 -0
  23. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/docs/api/overlays.md +0 -0
  24. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/docs/api/reference.md +0 -0
  25. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/docs/api/resources.md +0 -0
  26. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/docs/api/scope.md +0 -0
  27. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/docs/api/skills.md +0 -0
  28. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/docs/api/sources.md +0 -0
  29. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/docs/api/wrappers.md +0 -0
  30. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/docs/changelog.md +0 -0
  31. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/docs/guide/authoring.md +0 -0
  32. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/docs/guide/commands.md +0 -0
  33. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/docs/guide/install.md +0 -0
  34. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/docs/guide/overlays.md +0 -0
  35. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/docs/guide/private-sync.md +0 -0
  36. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/docs/index.md +0 -0
  37. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/install.py +0 -0
  38. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/mkdocs.yml +0 -0
  39. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/src/dotagents/__main__.py +0 -0
  40. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/src/dotagents/_agents.py +0 -0
  41. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/src/dotagents/_context.py +0 -0
  42. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/src/dotagents/_env.py +0 -0
  43. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/src/dotagents/_fs.py +0 -0
  44. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/src/dotagents/_hooks.py +0 -0
  45. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/src/dotagents/_ignore.py +0 -0
  46. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/src/dotagents/_merge.py +0 -0
  47. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/src/dotagents/_overlay/dotagents/cmds/findings.py +0 -0
  48. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/src/dotagents/_overlay/dotagents/cmds/launch.py +0 -0
  49. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/src/dotagents/_overlay/dotagents/hooks/preinvocation_antigravity_context.py +0 -0
  50. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/src/dotagents/_overlay/dotagents/hooks/pretooluse_codex_env.py +0 -0
  51. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/src/dotagents/_overlay/dotagents/hooks/sessionstart_codex_context.py +0 -0
  52. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/src/dotagents/_overlay/dotagents/templates/AGENTS.md +0 -0
  53. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/src/dotagents/_overlay/dotagents/templates/PROJECT.md +0 -0
  54. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/src/dotagents/_overlays.py +0 -0
  55. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/src/dotagents/_resources.py +0 -0
  56. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/src/dotagents/_scope.py +0 -0
  57. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/src/dotagents/_skills.py +0 -0
  58. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/src/dotagents/_sources.py +0 -0
  59. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/src/dotagents/_wrappers.py +0 -0
  60. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/src/dotagents/cli/_common.py +0 -0
  61. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/src/dotagents/cli/about.py +0 -0
  62. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/src/dotagents/cli/context.py +0 -0
  63. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/src/dotagents/cli/env.py +0 -0
  64. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/src/dotagents/cli/init.py +0 -0
  65. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/src/dotagents/cli/overlays.py +0 -0
  66. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/src/dotagents/cli/path.py +0 -0
  67. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/src/dotagents/py.typed +0 -0
  68. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/_helpers.py +0 -0
  69. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/_shell.py +0 -0
  70. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/conftest.py +0 -0
  71. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_about.py +0 -0
  72. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_agents.py +0 -0
  73. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_api_header.py +0 -0
  74. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_audit_leak.py +0 -0
  75. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_build_pyz.py +0 -0
  76. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_cloud_setup.py +0 -0
  77. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_context.py +0 -0
  78. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_env.py +0 -0
  79. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_env_cli_scope.py +0 -0
  80. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_env_format.py +0 -0
  81. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_env_into.py +0 -0
  82. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_env_regressions.py +0 -0
  83. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_env_safety.py +0 -0
  84. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_findings_cmd.py +0 -0
  85. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_fix_adapters.py +0 -0
  86. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_fix_env.py +0 -0
  87. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_fix_hooks.py +0 -0
  88. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_fix_overlays.py +0 -0
  89. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_fix_packaging.py +0 -0
  90. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_fix_scope_context.py +0 -0
  91. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_fix_skills_adapters.py +0 -0
  92. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_git_ssl_revoke.py +0 -0
  93. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_hooks.py +0 -0
  94. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_ignore.py +0 -0
  95. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_init_base.py +0 -0
  96. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_last_mile.py +0 -0
  97. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_launch_cmd.py +0 -0
  98. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_local_additions.py +0 -0
  99. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_mcp.py +0 -0
  100. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_no_internal_labels.py +0 -0
  101. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_overlay_libs.py +0 -0
  102. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_overlays_cli.py +0 -0
  103. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_overlays_command.py +0 -0
  104. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_path_timeout_frontmatter.py +0 -0
  105. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_powershell_env_hook.py +0 -0
  106. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_quoting_injection_safety.py +0 -0
  107. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_scope.py +0 -0
  108. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_sources.py +0 -0
  109. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_sources_uri.py +0 -0
  110. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_wire_hooks.py +0 -0
  111. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tests/test_wrappers.py +0 -0
  112. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tools/audit.py +0 -0
  113. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tools/cloud-setup.sh +0 -0
  114. {dotagents_cli-0.7.4 → dotagents_cli-0.7.5}/tools/pyz_smoke.sh +0 -0
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.7.5] - 2026-10-14
11
+
12
+ ### Changed
13
+
14
+ - `main()` runs through `duho.run(..., exit=False)`: it always returns the status
15
+ instead of raising `SystemExit` for a message, `--help` or a usage error (the
16
+ console script, `python -m dotagents` and `install.py` exit with it as before, with
17
+ the same statuses and the same text on standard error). Ctrl-C now ends with
18
+ status 130 and no traceback, and a closed pipe on standard output ends with
19
+ status 1, quietly.
20
+ - The `duho` floor moves to `>=0.7.5,<0.8` (from `>=0.6.0,<0.7`); the vendored
21
+ `.pyz` pin (`build-pyz --duho-version`) moves with it. No flag, exit status or
22
+ output changes.
23
+ - Command modules are loaded with duho's own `discover_commands(on_error=...)`
24
+ instead of a copy of its loop built on a private duho name. A module that fails
25
+ to import is still named in a warning and skipped, and the listing order is
26
+ unchanged. A command module may now import a sibling helper file by its bare
27
+ name.
28
+ - `kebab_name` delegates to `duho.text.kebabcase`; its results are unchanged.
29
+ - Discovered commands are listed in discovery order with `discover_commands(sort=False)`
30
+ instead of being re-sorted here, and a command that cannot build its parser is
31
+ found out with `duho.runtime.build_command_parser` instead of a private duho
32
+ helper. Output, exit statuses and which commands are skipped are unchanged.
33
+
10
34
  ## [0.7.4] - 2026-10-14
11
35
 
12
36
  ### Added
@@ -1417,7 +1441,8 @@ Patch release: the PATH/POSIX-conversion fix above (the only change since 0.3.0)
1417
1441
  affected module sources (`dotagents.cli`, `duho.presets`) to extracted temp files
1418
1442
  before dispatch; a no-op for a plain install.
1419
1443
 
1420
- [Unreleased]: https://github.com/jose-pr/dotagents/compare/v0.7.4...HEAD
1444
+ [Unreleased]: https://github.com/jose-pr/dotagents/compare/v0.7.5...HEAD
1445
+ [0.7.5]: https://github.com/jose-pr/dotagents/compare/v0.7.4...v0.7.5
1421
1446
  [0.7.4]: https://github.com/jose-pr/dotagents/compare/v0.7.3...v0.7.4
1422
1447
  [0.7.3]: https://github.com/jose-pr/dotagents/compare/v0.7.2...v0.7.3
1423
1448
  [0.7.2]: https://github.com/jose-pr/dotagents/compare/v0.7.1...v0.7.2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: dotagents-cli
3
- Version: 0.7.4
3
+ Version: 0.7.5
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
@@ -23,7 +23,7 @@ Classifier: Programming Language :: Python :: 3.14
23
23
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
24
  Classifier: Typing :: Typed
25
25
  Requires-Python: >=3.9
26
- Requires-Dist: duho<0.7,>=0.6.0
26
+ Requires-Dist: duho<0.8,>=0.7.5
27
27
  Requires-Dist: pathlib-next<0.10,>=0.9.0
28
28
  Requires-Dist: tomli>=1.1.0; python_version < '3.11'
29
29
  Provides-Extra: dev
@@ -35,7 +35,7 @@ classifiers = [
35
35
  # `build-pyz` vendors the floor of each of these (src/dotagents/cli/build_pyz.py);
36
36
  # raise a floor and move its pin there in the same change.
37
37
  dependencies = [
38
- "duho>=0.6.0,<0.7",
38
+ "duho>=0.7.5,<0.8",
39
39
  "pathlib_next>=0.9.0,<0.10",
40
40
  # overlay.toml manifests and TOML registries; tomllib is stdlib from 3.11.
41
41
  'tomli>=1.1.0; python_version < "3.11"',
@@ -74,7 +74,11 @@ are skipped.
74
74
  ## `dotagents.cli`
75
75
 
76
76
  - `main(argv=None) -> int` — the entry point (`dotagents` console script,
77
- `python -m dotagents`, `install.py`).
77
+ `python -m dotagents`, `install.py`). It always returns the status and never
78
+ raises `SystemExit`: a `SystemExit` raised below it (a message, `--help`, a usage
79
+ error) comes back as its status, the message going to standard error. Ctrl-C
80
+ returns 130 and a closed pipe on standard output returns 1, both without a
81
+ traceback.
78
82
  - `class Dotagents(LoggingArgs, Cli)` — the umbrella; field `cmdspath: list[str]`.
79
83
  - `CMDS_PATH_ENV = "AGENTS_CMDS_PATH"`.
80
84
  - Re-exported for command modules: `DotAgentsArgs`, `_write_stdout` (see below).
@@ -1,3 +1,3 @@
1
1
  """dotagents: installable CLI for the dotagents agent-config payload."""
2
2
 
3
- __version__ = "0.7.4"
3
+ __version__ = "0.7.5"
@@ -30,6 +30,8 @@ from pathlib import Path
30
30
 
31
31
  import duho
32
32
  from duho import Cli, LoggingArgs
33
+ from duho.runtime import build_command_parser
34
+ from duho.text import kebabcase
33
35
 
34
36
  from dotagents import __version__
35
37
 
@@ -164,18 +166,7 @@ def _check_buildable(command) -> None:
164
166
  fields claiming the same flag, a flag the umbrella already owns) would
165
167
  otherwise fail every invocation. The parents are the umbrella's options,
166
168
  as in `duho.app`, so a clash with them surfaces here too."""
167
- import argparse
168
-
169
- base = Dotagents._parser_(add_help=False)
170
- subparsers = argparse.ArgumentParser(add_help=False).add_subparsers()
171
- if isinstance(command, type):
172
- command._parser_(subparsers, parents=[base])
173
- return
174
- try:
175
- from duho.runtime import _register_module_command
176
- except ImportError: # pragma: no cover -- a later duho without this helper
177
- return
178
- _register_module_command(subparsers, command, base, None, Dotagents)
169
+ build_command_parser(command, Dotagents)
179
170
 
180
171
 
181
172
  def _source_of(command) -> str:
@@ -207,18 +198,14 @@ def _buildable(commands, file=None) -> "list":
207
198
  def kebab_name(class_name: str) -> str:
208
199
  """A command class's default subcommand name: ``LeakCheck`` ->
209
200
  ``leak-check``, ``HTTPServer`` -> ``http-server``, ``my_cmd`` -> ``my-cmd``."""
210
- import re
211
-
212
- spaced = re.sub(r"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])", "-", class_name)
213
- return re.sub(r"[-_]+", "-", spaced).strip("-").lower()
201
+ return kebabcase(class_name)
214
202
 
215
203
 
216
204
  def _named(commands) -> "list":
217
205
  """``commands``, each discovered class that declares no ``_parsername_``
218
- of its own given the kebab-case of its class name (:func:`kebab_name`).
219
- ``_parsername_`` is optional in a command module: duho < 0.6 derived that
220
- name itself, and from 0.6.0 it uses the class name as it is (``LeakCheck``),
221
- which broke ``dotagents leak-check`` for a module that relied on it."""
206
+ of its own given the kebab-case of its class name (:func:`kebab_name`),
207
+ which is also the name duho gives it. Setting it makes the name readable as
208
+ an attribute, which the command-set keys and the tests rely on."""
222
209
  for command in commands:
223
210
  if isinstance(command, type) and "_parsername_" not in vars(command):
224
211
  command._parsername_ = kebab_name(command.__name__)
@@ -228,46 +215,19 @@ def _named(commands) -> "list":
228
215
  def _discover_modules(directory: Path) -> "list":
229
216
  """duho's per-directory discovery, made resilient per MODULE.
230
217
 
231
- duho's own loop catches only ImportError/NotImplementedError per file and
232
- lets anything else propagate. Discovery runs before EVERY dotagents
233
- invocation (including `env`/`context` inside the SessionStart hooks), so
234
- one broken user module must not take down every command: the loop is
235
- mirrored here with a catch-all per file -- the bad module is named, with
236
- its exception, and skipped; the rest of the directory still loads.
237
- SystemExit is included because a module that `sys.exit()`s at import is
238
- not an Exception. A command that imports but cannot build its parser is
239
- skipped the same way (`_buildable`). Falls back to duho's own
240
- per-directory unit, with a warning, if a duho release moves these
241
- helpers.
242
-
243
- Uses duho's PUBLIC `import_from_path` (duho >= 0.6.0) for the file
244
- import; `_commands_in_module` has no public counterpart yet, so it stays
245
- a private import (see `duho.discovery.import_from_path`'s own docstring:
246
- it is the public counterpart of the `_unique_module_name` +
247
- `_import_from_path` pair this module used to call directly)."""
248
- try:
249
- from duho.discovery import _commands_in_module, import_from_path
250
- except ImportError: # pragma: no cover -- a later duho without these internals
251
- from duho.discovery import discover_commands
252
-
253
- _LOGGER.warning(
254
- "duho's discovery internals moved; a broken module in %s now skips "
255
- "the whole directory, not just itself",
256
- directory,
257
- )
258
- return _named(_buildable(discover_commands(directory)))
259
- commands = []
260
- for file in sorted(directory.glob("*.py")):
261
- if file.name.startswith("_"):
262
- continue
263
- try:
264
- module = import_from_path("duho._discovered." + file.stem, file)
265
- found = _commands_in_module(module, stem=file.stem)
266
- except (Exception, SystemExit) as exc: # noqa: BLE001 -- see docstring
267
- _LOGGER.warning("skipping command module %s: %s", file, _describe(exc))
268
- continue
269
- commands.extend(_named(_buildable(found, file)))
270
- return commands
218
+ Discovery runs before EVERY dotagents invocation (including `env`/`context`
219
+ inside the SessionStart hooks), so one broken user module must not take
220
+ down every command. `on_error` is called for any exception raised importing
221
+ one file, `SystemExit` included (a module that `sys.exit()`s at import is
222
+ not an Exception): the bad module is named, with its exception, and skipped;
223
+ the rest of the directory still loads. A command that imports but cannot
224
+ build its parser is skipped the same way (`_buildable`)."""
225
+
226
+ def skip(source, exc) -> None:
227
+ _LOGGER.warning("skipping command module %s: %s", source, _describe(exc))
228
+
229
+ found = duho.discover_commands(directory, on_error=skip, sort=False)
230
+ return _named(_buildable(found))
271
231
 
272
232
 
273
233
  def _discover_dir(source, by_name: dict) -> None:
@@ -522,13 +482,14 @@ def _repoint_zipapp_sources() -> None:
522
482
 
523
483
  def main(argv=None) -> int:
524
484
  _repoint_zipapp_sources()
525
- # `duho.app` (not `duho.main`) so command discovery runs; default dispatch
526
- # calls each command's `__call__`. `commands=` is the resolved built-ins +
527
- # discovered set.
528
- return duho.app(
485
+ # `commands=` makes `run` use `duho.app` (not `duho.main`) so the resolved
486
+ # built-ins + discovered set are the commands; dispatch calls each command's
487
+ # `__call__`. `exit=False` returns the status, whatever ended the run.
488
+ return duho.run(
529
489
  Dotagents,
490
+ argv,
491
+ exit=False,
530
492
  commands=_discover(argv),
531
- argv=argv,
532
493
  name="dotagents",
533
494
  )
534
495
 
@@ -73,7 +73,7 @@ class BuildPyz(LoggingArgs, Cmd):
73
73
  # stale pin here ships an artifact `pip install dotagents-cli` would refuse.
74
74
  # Pin the FLOOR of each declared range, not the latest patch -- the .pyz then
75
75
  # exercises the minimum the metadata promises.
76
- duho_version: str = "0.6.0"
76
+ duho_version: str = "0.7.5"
77
77
  "Pinned duho version to vendor."
78
78
  ("--duho-version",)
79
79
 
@@ -130,7 +130,7 @@ def test_reinit_after_overlays_add_keeps_the_overlay_routing(tmp_path):
130
130
 
131
131
 
132
132
  def test_init_with_an_unknown_agent_is_a_usage_error(tmp_path):
133
- assert _main(["init", "--dest", str(tmp_path / "s"), "--agents", "claud", "--no-hooks"]) == 2
133
+ assert _main(["init", "--dest", str(tmp_path / "s"), "--agents", "claud", "--no-hooks"]) == 1
134
134
 
135
135
 
136
136
  # --------------------------------------------------------------------------
@@ -224,8 +224,7 @@ class LeakCheck(LoggingArgs, Cmd):
224
224
 
225
225
 
226
226
  def test_a_command_without_parsername_gets_its_kebab_class_name(monkeypatch, tmp_path, capsys):
227
- """`_parsername_` is optional: duho >= 0.6.0 names such a class `LeakCheck`,
228
- dotagents gives it `leak-check` (what duho < 0.6 derived)."""
227
+ """`_parsername_` is optional: such a class is named `leak-check`."""
229
228
  cmds = tmp_path / "extra"
230
229
  _write(cmds / "leak_check.py", UNNAMED)
231
230
  _write(cmds / "toy.py", TOY)
@@ -436,3 +435,17 @@ def test_a_module_failing_at_parser_build_does_not_take_the_cli_down(monkeypatch
436
435
  except SystemExit as exc:
437
436
  rc = exc.code
438
437
  assert rc == 0
438
+
439
+
440
+ def test_a_directory_lists_its_commands_by_file_then_definition_order(tmp_path):
441
+ """The `--help` listing follows the files, not the subcommand names."""
442
+ template = (
443
+ "from duho import Cmd, LoggingArgs\n\n"
444
+ "class {a}(LoggingArgs, Cmd):\n _parsername_ = '{a}'\n\n"
445
+ " def __call__(self) -> int:\n return 0\n\n"
446
+ "class {b}(LoggingArgs, Cmd):\n _parsername_ = '{b}'\n\n"
447
+ " def __call__(self) -> int:\n return 0\n"
448
+ )
449
+ _write(tmp_path / "aa_first.py", template.format(a="zulu", b="alpha"))
450
+ _write(tmp_path / "bb_second.py", template.format(a="mike", b="bravo"))
451
+ assert _names(cli._discover_modules(tmp_path)) == ["zulu", "alpha", "mike", "bravo"]
@@ -211,31 +211,6 @@ def test_a_command_clashing_with_an_umbrella_flag_is_skipped(monkeypatch, tmp_pa
211
211
  assert cli.main(["about"]) == 0
212
212
 
213
213
 
214
- def test_moved_duho_internals_are_reported_not_silent(monkeypatch, tmp_path, caplog):
215
- import builtins
216
-
217
- cmds = tmp_path / "cmds"
218
- cmds.mkdir()
219
- (cmds / "good.py").write_text(GOOD, encoding="utf-8")
220
- real_import = builtins.__import__
221
-
222
- def renamed(name, globals=None, locals=None, fromlist=(), level=0):
223
- # What a duho release that renamed its remaining private helper looks
224
- # like to `from duho.discovery import _commands_in_module, ...` --
225
- # and only to it. `import_from_path` is duho's public counterpart
226
- # (duho >= 0.6.0) and stays importable; `_commands_in_module` has no
227
- # public counterpart yet, so it is the one still exposed to this risk.
228
- if name == "duho.discovery" and "_commands_in_module" in (fromlist or ()):
229
- raise ImportError("cannot import name '_commands_in_module'")
230
- return real_import(name, globals, locals, fromlist, level)
231
-
232
- monkeypatch.setattr(builtins, "__import__", renamed)
233
- with caplog.at_level(logging.WARNING):
234
- commands = cli._discover_modules(cmds)
235
- assert "good" in _names(commands)
236
- assert any("internals moved" in r.getMessage() for r in caplog.records)
237
-
238
-
239
214
  # --------------------------------------------------------------------------- #
240
215
  # The zipapp-repointed module list is derived, not hand-kept
241
216
  # --------------------------------------------------------------------------- #
@@ -246,16 +246,15 @@ def test_a_bom_in_settings_json_is_accepted_and_not_written_back(tmp_path):
246
246
 
247
247
 
248
248
  @pytest.mark.parametrize("content", [b"{not json", b"\xff\xfe{}"])
249
- def test_a_corrupt_settings_file_stops_init_before_anything_is_written(content):
249
+ def test_a_corrupt_settings_file_stops_init_before_anything_is_written(content, capsys):
250
250
  """The error fired after AGENTS.md and the CLAUDE.md include were written,
251
251
  leaving a half-initialised store."""
252
252
  settings = Path.home() / ".claude" / "settings.json"
253
253
  settings.parent.mkdir(parents=True)
254
254
  settings.write_bytes(content)
255
255
 
256
- with pytest.raises(SystemExit) as exc:
257
- cli.main(["init", "-g", "--agents", "claude"])
258
- assert str(settings) in str(exc.value)
256
+ assert cli.main(["init", "-g", "--agents", "claude"]) == 1
257
+ assert str(settings) in capsys.readouterr().err
259
258
  assert not (Path.home() / ".agents").exists()
260
259
  assert not (Path.home() / ".claude" / "CLAUDE.md").exists()
261
260
  assert settings.read_bytes() == content
File without changes
File without changes
File without changes
File without changes
File without changes