convertible-cli 0.3.0__tar.gz → 0.5.0__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 (122) hide show
  1. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/CHANGELOG.md +13 -0
  2. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/PKG-INFO +36 -3
  3. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/README.md +35 -2
  4. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/cli/__init__.py +19 -0
  5. convertible_cli-0.5.0/convertible/cli/_banner-big.txt +37 -0
  6. convertible_cli-0.5.0/convertible/cli/_banner.py +56 -0
  7. convertible_cli-0.5.0/convertible/cli/_banner.txt +23 -0
  8. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/cli/_commands/drive.py +10 -2
  9. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/cli/_commands/session.py +5 -0
  10. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/explain/catalog.py +10 -5
  11. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/pyproject.toml +1 -1
  12. convertible_cli-0.5.0/tests/test_banner.py +145 -0
  13. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_cli.py +23 -1
  14. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/uv.lock +1 -1
  15. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/agent-config/SKILL.md +0 -0
  16. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/agent-config/data/backend-fingerprints.yaml +0 -0
  17. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/agent-config/scripts/show.sh +0 -0
  18. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/assign-to-workforce/SKILL.md +0 -0
  19. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/assign-to-workforce/scripts/assign-to-workforce.sh +0 -0
  20. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/cicd/SKILL.md +0 -0
  21. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/cicd/scripts/_resolve-nick.sh +0 -0
  22. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/cicd/scripts/portability-lint.sh +0 -0
  23. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/cicd/scripts/pr-reply.sh +0 -0
  24. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/cicd/scripts/pr-status.sh +0 -0
  25. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/cicd/scripts/workflow.sh +0 -0
  26. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/communicate/SKILL.md +0 -0
  27. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/communicate/scripts/fetch-issues.sh +0 -0
  28. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/communicate/scripts/mesh-message.sh +0 -0
  29. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/communicate/scripts/post-comment.sh +0 -0
  30. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/communicate/scripts/post-issue.sh +0 -0
  31. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/communicate/scripts/templates/skill-new-brief.md +0 -0
  32. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/communicate/scripts/templates/skill-update-brief.md +0 -0
  33. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/doc-test-alignment/SKILL.md +0 -0
  34. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/doc-test-alignment/scripts/check.sh +0 -0
  35. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/pypi-maintainer/SKILL.md +0 -0
  36. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/pypi-maintainer/scripts/switch-source.sh +0 -0
  37. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/run-tests/SKILL.md +0 -0
  38. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/run-tests/scripts/test.sh +0 -0
  39. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/sonarclaude/SKILL.md +0 -0
  40. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/sonarclaude/scripts/sonar.sh +0 -0
  41. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/spec-to-plan/SKILL.md +0 -0
  42. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/spec-to-plan/scripts/spec-to-plan.sh +0 -0
  43. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/think/SKILL.md +0 -0
  44. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/think/scripts/think.sh +0 -0
  45. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/version-bump/SKILL.md +0 -0
  46. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/version-bump/scripts/bump.py +0 -0
  47. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills.local.yaml.example +0 -0
  48. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.devague/current +0 -0
  49. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.devague/current_plan +0 -0
  50. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.devague/frames/convertible-gains-an-extensibility-layer-like-clau.json +0 -0
  51. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.devague/frames/convertible-v0-ships-point-it-at-a-repo-task-and-i.json +0 -0
  52. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.devague/plans/convertible-gains-an-extensibility-layer-like-clau.json +0 -0
  53. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.devague/plans/convertible-v0-ships-point-it-at-a-repo-task-and-i.json +0 -0
  54. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.flake8 +0 -0
  55. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.github/workflows/publish.yml +0 -0
  56. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.github/workflows/tests.yml +0 -0
  57. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.gitignore +0 -0
  58. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.markdownlint-cli2.yaml +0 -0
  59. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/CLAUDE.md +0 -0
  60. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/LICENSE +0 -0
  61. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/__init__.py +0 -0
  62. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/__main__.py +0 -0
  63. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/artifact.py +0 -0
  64. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/cli/_commands/__init__.py +0 -0
  65. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/cli/_commands/cli.py +0 -0
  66. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/cli/_commands/commands.py +0 -0
  67. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/cli/_commands/doctor.py +0 -0
  68. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/cli/_commands/explain.py +0 -0
  69. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/cli/_commands/hooks.py +0 -0
  70. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/cli/_commands/learn.py +0 -0
  71. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/cli/_commands/overview.py +0 -0
  72. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/cli/_commands/wheels.py +0 -0
  73. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/cli/_commands/whoami.py +0 -0
  74. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/cli/_errors.py +0 -0
  75. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/cli/_output.py +0 -0
  76. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/commands.py +0 -0
  77. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/config.py +0 -0
  78. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/configdir.py +0 -0
  79. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/contract.py +0 -0
  80. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/engine.py +0 -0
  81. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/engines/__init__.py +0 -0
  82. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/engines/mock.py +0 -0
  83. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/engines/vllm_openai.py +0 -0
  84. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/explain/__init__.py +0 -0
  85. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/handoff.py +0 -0
  86. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/hooks.py +0 -0
  87. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/loop.py +0 -0
  88. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/registry.py +0 -0
  89. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/tools.py +0 -0
  90. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/culture.yaml +0 -0
  91. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/docs/plans/2026-05-26-convertible-v0-ships-point-it-at-a-repo-task-and-i.md +0 -0
  92. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/docs/plans/2026-05-27-convertible-gains-an-extensibility-layer-like-clau.md +0 -0
  93. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/docs/skill-sources.md +0 -0
  94. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/docs/specs/2026-05-26-convertible-v0-ships-point-it-at-a-repo-task-and-i.md +0 -0
  95. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/docs/specs/2026-05-27-convertible-gains-an-extensibility-layer-like-clau.md +0 -0
  96. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/sonar-project.properties +0 -0
  97. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/__init__.py +0 -0
  98. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_artifact.py +0 -0
  99. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_boundary.py +0 -0
  100. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_cli_introspection.py +0 -0
  101. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_commands.py +0 -0
  102. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_commands_cli.py +0 -0
  103. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_config.py +0 -0
  104. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_configdir.py +0 -0
  105. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_contract.py +0 -0
  106. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_drive.py +0 -0
  107. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_e2e_extensibility.py +0 -0
  108. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_e2e_mock.py +0 -0
  109. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_engine.py +0 -0
  110. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_handoff.py +0 -0
  111. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_hooks.py +0 -0
  112. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_hooks_cli.py +0 -0
  113. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_loop.py +0 -0
  114. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_mock_engine.py +0 -0
  115. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_registry.py +0 -0
  116. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_review_fixes.py +0 -0
  117. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_session.py +0 -0
  118. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_tools.py +0 -0
  119. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_vllm_live.py +0 -0
  120. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_vllm_openai.py +0 -0
  121. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_wheels.py +0 -0
  122. {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_zero_deps.py +0 -0
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
5
5
  Format follows [Keep a Changelog](https://keepachangelog.com/). This project
6
6
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.5.0] - 2026-05-27
9
+
10
+ ### Changed
11
+
12
+ - Bare `convertible` (no subcommand) now opens the interactive harness (the `session` palette) when run at a terminal — the natural "get in and drive" gesture. Piped, redirected, or otherwise non-interactive, it still prints usage, preserving the discoverable surface for scripts and agents. `-h/--help` is unaffected.
13
+ - Reframed `convertible drive` help and `explain` text to lead with the goal/instruction ("drive toward a goal") rather than "run a repo task"; the repo is the target, not the headline. No behavior change.
14
+
15
+ ## [0.4.0] - 2026-05-27
16
+
17
+ ### Added
18
+
19
+ - Convertible ASCII banner on 'drive' and 'session' start (issue #15): decorative chrome shown only on an interactive TTY and suppressed in --json, so it never pollutes the stdout result stream or agent-parsed stderr.
20
+
8
21
  ## [0.3.0] - 2026-05-27
9
22
 
10
23
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: convertible-cli
3
- Version: 0.3.0
3
+ Version: 0.5.0
4
4
  Summary: Convertible CLI is a swappable coder-agent harness that turns different models into repo workers behind one shared task contract.
5
5
  Project-URL: Homepage, https://github.com/agentculture/convertible
6
6
  Project-URL: Issues, https://github.com/agentculture/convertible/issues
@@ -17,6 +17,27 @@ Description-Content-Type: text/markdown
17
17
 
18
18
  # convertible
19
19
 
20
+ ```text
21
+ ,"^,::,:::::I::::::^
22
+ ^!'` `"^,. ::`
23
+ `,;;i;,,' .!^'`",. "<," " ;,^
24
+ ^ '"^";"^```,":::','"'^`"."'.,:^,i::,"".
25
+ .I. ' I-l '",I;!ii,I.,,l;;IlIl:i!:::::,,,^^;",,'
26
+ '^ ""::``':. .`";,"```"^.. '^^'":,''.
27
+ ::,`;!"..^..'^"^ .' .."" ..`"^ ..`"`'"".
28
+ ;;l!i:' ^ '^"`. `"`.. ',^.. `` '^
29
+ :i>I!lII". .' .''^` '`^' .`". ^. :;lI.
30
+ ",l!`;"':l;,,`. .' '",'. ^...'^`'"...^l'":;Ii!!l:!';.
31
+ ^''^., '`^^`:l;"^"',!~ .^^^,""..'`^. .^l,I''^.:l!;;;i>|+,!IlI::^
32
+ `;. :>i":` "`, ',^^" "; I.'. :::"^:;iil;'":^; '"
33
+ '^"'"IlI,.:,::;:' .`;"!l:``,,"``',,,^,,:ll!,,l^
34
+ ":^:,I^:"!;:: :I>l:::"`""::,:I>!<i^ " ;.
35
+ ",;"Il;i^: ',":;"`'`'^, ><[il```'
36
+ " ;;I"l:^.^;^''lI" :^::",:,'
37
+ ,`:;,I.^ ''
38
+ `. " "
39
+ ```
40
+
20
41
  > Convertible CLI is a swappable coder-agent harness that turns different models
21
42
  > into repo workers behind one shared task contract.
22
43
  >
@@ -65,6 +86,10 @@ which one ran.
65
86
  - **Interactive palette** — `convertible session` opens a foreground command
66
87
  browser so operators can select templates and run ad-hoc instructions without
67
88
  leaving the shell.
89
+ - **Startup banner** — `convertible drive` and `convertible session` greet an
90
+ interactive terminal with an ASCII banner. It's decorative chrome: written to
91
+ stderr, shown only on a TTY, and suppressed under `--json`, so it never
92
+ pollutes the stdout result stream or agent-parsed output.
68
93
 
69
94
  **Not in v0** (by design): a multi-engine router/policy gearbox, an execution
70
95
  sandbox, a daemon mode, and Codex/Claude/Gemini drivers. The runtime package has
@@ -102,10 +127,13 @@ engine, so it binds equally to `mock`, `vllm-openai`, and any future wheel.
102
127
  uv sync
103
128
  uv run pytest -n auto # full suite, no network needed
104
129
 
130
+ # Open the interactive harness (the session palette) at a terminal:
131
+ uv run convertible
132
+
105
133
  # Discover the engines installed in this environment:
106
134
  uv run convertible wheels list
107
135
 
108
- # Drive a task with the deterministic mock engine (no model, no network):
136
+ # Drive toward a goal with the deterministic mock engine (no model, no network):
109
137
  uv run convertible drive "add a CONTRIBUTING.md stub" --repo . --engine mock --no-pr
110
138
  ```
111
139
 
@@ -293,6 +321,11 @@ loop, hooks, and artifact — no parallel code path):
293
321
  uv run convertible session --repo /path/to/repo --engine vllm-openai
294
322
  ```
295
323
 
324
+ Running `convertible` with no arguments **at a terminal** opens this same palette
325
+ (with the default engine and repo) — the natural "get in and drive" gesture.
326
+ Piped, redirected, or otherwise non-interactive, bare `convertible` prints usage
327
+ instead, so scripts and agents keep a discoverable surface.
328
+
296
329
  The session loops until the user enters `q`, `quit`, or an empty line. Any
297
330
  driver flags accepted by `drive` (`--engine`, `--no-pr`, `--base-url`, etc.)
298
331
  are also accepted by `session`.
@@ -329,7 +362,7 @@ rely on a non-existent flag.
329
362
 
330
363
  | Verb | What it does |
331
364
  |------|--------------|
332
- | `drive <instruction>` | Run a repo task through a coder engine; write the artifact; hand off. |
365
+ | `drive <goal>` | Drive toward a goal/instruction: work autonomously through a coder engine; write the artifact; hand off. |
333
366
  | `drive --command <name> [args…]` | Expand a saved command template and drive it. |
334
367
  | `commands list` | List discovered command templates for a repo. |
335
368
  | `commands overview` | Describe the commands surface. |
@@ -1,5 +1,26 @@
1
1
  # convertible
2
2
 
3
+ ```text
4
+ ,"^,::,:::::I::::::^
5
+ ^!'` `"^,. ::`
6
+ `,;;i;,,' .!^'`",. "<," " ;,^
7
+ ^ '"^";"^```,":::','"'^`"."'.,:^,i::,"".
8
+ .I. ' I-l '",I;!ii,I.,,l;;IlIl:i!:::::,,,^^;",,'
9
+ '^ ""::``':. .`";,"```"^.. '^^'":,''.
10
+ ::,`;!"..^..'^"^ .' .."" ..`"^ ..`"`'"".
11
+ ;;l!i:' ^ '^"`. `"`.. ',^.. `` '^
12
+ :i>I!lII". .' .''^` '`^' .`". ^. :;lI.
13
+ ",l!`;"':l;,,`. .' '",'. ^...'^`'"...^l'":;Ii!!l:!';.
14
+ ^''^., '`^^`:l;"^"',!~ .^^^,""..'`^. .^l,I''^.:l!;;;i>|+,!IlI::^
15
+ `;. :>i":` "`, ',^^" "; I.'. :::"^:;iil;'":^; '"
16
+ '^"'"IlI,.:,::;:' .`;"!l:``,,"``',,,^,,:ll!,,l^
17
+ ":^:,I^:"!;:: :I>l:::"`""::,:I>!<i^ " ;.
18
+ ",;"Il;i^: ',":;"`'`'^, ><[il```'
19
+ " ;;I"l:^.^;^''lI" :^::",:,'
20
+ ,`:;,I.^ ''
21
+ `. " "
22
+ ```
23
+
3
24
  > Convertible CLI is a swappable coder-agent harness that turns different models
4
25
  > into repo workers behind one shared task contract.
5
26
  >
@@ -48,6 +69,10 @@ which one ran.
48
69
  - **Interactive palette** — `convertible session` opens a foreground command
49
70
  browser so operators can select templates and run ad-hoc instructions without
50
71
  leaving the shell.
72
+ - **Startup banner** — `convertible drive` and `convertible session` greet an
73
+ interactive terminal with an ASCII banner. It's decorative chrome: written to
74
+ stderr, shown only on a TTY, and suppressed under `--json`, so it never
75
+ pollutes the stdout result stream or agent-parsed output.
51
76
 
52
77
  **Not in v0** (by design): a multi-engine router/policy gearbox, an execution
53
78
  sandbox, a daemon mode, and Codex/Claude/Gemini drivers. The runtime package has
@@ -85,10 +110,13 @@ engine, so it binds equally to `mock`, `vllm-openai`, and any future wheel.
85
110
  uv sync
86
111
  uv run pytest -n auto # full suite, no network needed
87
112
 
113
+ # Open the interactive harness (the session palette) at a terminal:
114
+ uv run convertible
115
+
88
116
  # Discover the engines installed in this environment:
89
117
  uv run convertible wheels list
90
118
 
91
- # Drive a task with the deterministic mock engine (no model, no network):
119
+ # Drive toward a goal with the deterministic mock engine (no model, no network):
92
120
  uv run convertible drive "add a CONTRIBUTING.md stub" --repo . --engine mock --no-pr
93
121
  ```
94
122
 
@@ -276,6 +304,11 @@ loop, hooks, and artifact — no parallel code path):
276
304
  uv run convertible session --repo /path/to/repo --engine vllm-openai
277
305
  ```
278
306
 
307
+ Running `convertible` with no arguments **at a terminal** opens this same palette
308
+ (with the default engine and repo) — the natural "get in and drive" gesture.
309
+ Piped, redirected, or otherwise non-interactive, bare `convertible` prints usage
310
+ instead, so scripts and agents keep a discoverable surface.
311
+
279
312
  The session loops until the user enters `q`, `quit`, or an empty line. Any
280
313
  driver flags accepted by `drive` (`--engine`, `--no-pr`, `--base-url`, etc.)
281
314
  are also accepted by `session`.
@@ -312,7 +345,7 @@ rely on a non-existent flag.
312
345
 
313
346
  | Verb | What it does |
314
347
  |------|--------------|
315
- | `drive <instruction>` | Run a repo task through a coder engine; write the artifact; hand off. |
348
+ | `drive <goal>` | Drive toward a goal/instruction: work autonomously through a coder engine; write the artifact; hand off. |
316
349
  | `drive --command <name> [args…]` | Expand a saved command template and drive it. |
317
350
  | `commands list` | List discovered command templates for a repo. |
318
351
  | `commands overview` | Describe the commands surface. |
@@ -61,6 +61,17 @@ def _argv_has_json(argv: list[str] | None) -> bool:
61
61
  return any(t == "--json" or t.startswith("--json=") for t in tokens)
62
62
 
63
63
 
64
+ def _stdio_is_interactive() -> bool:
65
+ """Whether stdin and stdout are both interactive terminals.
66
+
67
+ Bare ``convertible`` opens the interactive harness only at a real terminal;
68
+ isolated as a module function so tests can force the interactive branch
69
+ without a TTY (mirrors :func:`convertible.cli._banner._isatty`). Both streams
70
+ must be a TTY: the palette reads from stdin and renders its chrome to stdout.
71
+ """
72
+ return sys.stdin.isatty() and sys.stdout.isatty()
73
+
74
+
64
75
  def _build_parser() -> argparse.ArgumentParser:
65
76
  from convertible.cli._commands import cli as _cli_group
66
77
  from convertible.cli._commands import commands as _commands_group
@@ -136,6 +147,14 @@ def main(argv: list[str] | None = None) -> int:
136
147
  args = parser.parse_args(argv)
137
148
 
138
149
  if args.command is None:
150
+ # Bare `convertible` opens the interactive harness at a terminal; piped /
151
+ # redirected / non-interactive it prints usage so scripts and agents keep
152
+ # a discoverable surface. `-h/--help` is handled by argparse before here,
153
+ # so the help surface (and the teken rubric, which probes --help) stay
154
+ # available either way. Re-parsing ["session"] reuses the session
155
+ # subparser's defaults and func wiring — no parallel code path.
156
+ if _stdio_is_interactive():
157
+ return _dispatch(parser.parse_args(["session"]))
139
158
  parser.print_help()
140
159
  return 0
141
160
 
@@ -0,0 +1,37 @@
1
+ ,. ..^I+[1z}{r]>1_}}]__+___+~++~".";
2
+ "; ...;^` | _.!
3
+ l, Ii < .. !).''
4
+ n1' 1. ;--l"-` C-.~
5
+ ..^ ''}|{??-J .. ."'l-,!}m\l .i.`,'. r; '.
6
+ '. l..ij~_ ;l` .... c~?> ; | `t<t .' .
7
+ ] .+I `' ' ,^ ..:,^.. "! Il'{> ii '`. .`>1;:]. n .~^v. :l!^ `'
8
+ r ._+ "it "1`<:'c} ~: '` . 1< `> :,. ,_n+\|)`{f| _^ '
9
+ '] l'. ?I .I`:l. ..;^ l<' '. .zQx)i?tri'^ .':' .. . `.`., .Q.>U. .
10
+ v{ . . 'j\ . "<.:< { Q|<'I?< j I.*Cj!;1?[>`..,';]`( `" ;? .)".^~' ;(;
11
+ i| ..]. ^' ] . :!....Cch?> ... '. . 'c[. .' <(
12
+ ;l ^" "" |1.>i :;U'" +. .I .+.!^ l+ l .! I~
13
+ t .. . ?<.. .. `'.|i. ,I ` }! <\ ` .}. '.'f:
14
+ . ....'.n.}<" .l "~."i Y._. ^.^: `. - t'.
15
+ 0|[.,^ J. ? ` ' ^ i? . ':\. ;. ^ ."`t .
16
+ . ~':!.r . ^<>.. Y'l' ~- i . .-
17
+ .> .I1."" .I.<` ^, "".t `^+ !`' ';` .`
18
+ '.'+/. ''x.^+" [i' ^: .."` ~< fc ,.'(Iv ."_
19
+ ;:_:+!. i~. ;,^]:?...l` '^ 'i.| ^,j i .c ,. '-j .lu} `_.!!. ^.
20
+ ..i;n~'.j ^x^ .!>i]`. c.. . ...' `'.i^.. . .,w . )i '`. .'<):' ;j,"..`']^^ '. .' "
21
+ `^ ii } ;`n .,l]+:- ~+` :l .?.lQ. _` .I+ <. >i[ i!?^'|"' I ^i. ^l-;j^,.~I:. 1~?]`'Qi .~..-It^'
22
+ . .... :~' x;"u: C.. Y. . . .c, ' '. . ' .._tn:` . .` i,," ..`' `(.p' 'z
23
+ `!.[ /^lv,i <.+ .. ," +._^ < - 1. :` ]`?}!!! .^/1 .``J!t .f.. . `! ^.
24
+ . ?. If"Y.~...| .^ ^` f. ,] t . /'. .h ^' 0 .:}h?``io['. . .+<.. ._^ ' .[
25
+ ;>`,: fl)'l_'(" . .- . ]I'".^. `^`'{t ..'l . .~l` .^;(,. ) i| +'`'`_.
26
+ ^.) ^:". ` f`'^ ? ^'z-l. 1]:.. Xn" . .:^`.. .":Z:. ^"c:' , ;x^|' |m`.
27
+ <~ ` . . `!. .x`;<`?`; ].'.^?.I; (`.. <?i>lI^ . .`;"... [j: .I]: !,)I.. <. [ +;
28
+ ". .. .<,} '`(,j`..`.> n. "" l` `tO- .`<1X.)^xC`' `}( ]~ '.
29
+ . ..+: ;^f_, `c '. :~."v.J|!I` '/x^ "+b '".?"(XYX _ ``..
30
+ ...^+.`I".^.."m .x . . ^" " "\` .`: }.;
31
+ ., [^| ]^i Y.Y . " .: "^.-' ?; `. '!.'t:. ./,
32
+ , `+`- j .~;[. :' .^ ..t"^ ... "tj," ^[],
33
+ !: )<r.("~^1^ ' ,
34
+ ? `il"!~< `" [
35
+ .; r. z^
36
+ ``l 1 ".
37
+ . .
@@ -0,0 +1,56 @@
1
+ """Convertible ASCII banner shown at drive/session start (decorative chrome).
2
+
3
+ The art lives in ``_banner.txt`` next to this module so the wide, trailing-
4
+ whitespace-laden lines never have to satisfy ``black``/``flake8``. It is loaded
5
+ once (``lru_cache``) via :mod:`importlib.resources`, which resolves correctly for
6
+ both editable installs and built wheels — and uses only the standard library, so
7
+ the ``dependencies = []`` rule is preserved.
8
+
9
+ The banner is purely decorative, so :func:`emit_banner` shows it **only on an
10
+ interactive terminal** and **never in ``--json`` mode**. convertible is itself an
11
+ agent harness: agents (and CI) parse stderr for the ``error:``/``hint:`` rubric,
12
+ so the art must never prepend to machine-read output — only a human at a TTY sees
13
+ it.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import sys
19
+ from functools import lru_cache
20
+ from importlib import resources
21
+ from typing import Callable
22
+
23
+
24
+ @lru_cache(maxsize=1)
25
+ def banner() -> str:
26
+ """Return the convertible ASCII banner, loaded once from the data file."""
27
+ return resources.files(__package__).joinpath("_banner.txt").read_text(encoding="utf-8")
28
+
29
+
30
+ def _isatty() -> bool:
31
+ """Whether the diagnostics stream (stderr) is an interactive terminal.
32
+
33
+ Isolated as a module function so tests can force the interactive branch
34
+ without a real TTY (``monkeypatch.setattr`` this name).
35
+ """
36
+ return sys.stderr.isatty()
37
+
38
+
39
+ def emit_banner(emit: Callable[[str], None], *, json_mode: bool) -> None:
40
+ """Emit the banner via ``emit`` — only on an interactive TTY, never in ``--json``.
41
+
42
+ The banner is decorative, so two robustness rules apply:
43
+
44
+ * A missing/unreadable resource is swallowed — a packaging glitch must never
45
+ break a real drive (only the art is lost, the task still runs).
46
+ * Trailing newlines are stripped and each sink adds its own, so rendering is
47
+ identical whether ``emit`` always appends a newline (``print`` in
48
+ ``session``) or only when absent (``emit_diagnostic`` in ``drive``).
49
+ """
50
+ if json_mode or not _isatty():
51
+ return
52
+ try:
53
+ art = banner()
54
+ except OSError:
55
+ return
56
+ emit(art.rstrip("\n"))
@@ -0,0 +1,23 @@
1
+
2
+
3
+
4
+
5
+
6
+ ,"^,::,:::::I::::::^
7
+ ^!'` `"^,. ::`
8
+ `,;;i;,,' .!^'`",. "<," " ;,^
9
+ ^ '"^";"^```,":::','"'^`"."'.,:^,i::,"".
10
+ .I. ' I-l '",I;!ii,I.,,l;;IlIl:i!:::::,,,^^;",,'
11
+ '^ ""::``':. .`";,"```"^.. '^^'":,''.
12
+ ::,`;!"..^..'^"^ .' .."" ..`"^ ..`"`'"".
13
+ ;;l!i:' ^ '^"`. `"`.. ',^.. `` '^
14
+ :i>I!lII". .' .''^` '`^' .`". ^. :;lI.
15
+ ",l!`;"':l;,,`. .' '",'. ^...'^`'"...^l'":;Ii!!l:!';.
16
+ ^''^., '`^^`:l;"^"',!~ .^^^,""..'`^. .^l,I''^.:l!;;;i>|+,!IlI::^
17
+ `;. :>i":` "`, ',^^" "; I.'. :::"^:;iil;'":^; '"
18
+ '^"'"IlI,.:,::;:' .`;"!l:``,,"``',,,^,,:ll!,,l^
19
+ ":^:,I^:"!;:: :I>l:::"`""::,:I>!<i^ " ;.
20
+ ",;"Il;i^: ',":;"`'`'^, ><[il```'
21
+ " ;;I"l:^.^;^''lI" :^::",:,'
22
+ ,`:;,I.^ ''
23
+ `. " "
@@ -26,6 +26,7 @@ from pathlib import Path
26
26
 
27
27
  from convertible import registry
28
28
  from convertible.artifact import artifact_dir, failed_result, write
29
+ from convertible.cli._banner import emit_banner
29
30
  from convertible.cli._errors import EXIT_ENV_ERROR, EXIT_USER_ERROR, CliError
30
31
  from convertible.cli._output import emit_diagnostic, emit_result
31
32
  from convertible.commands import CommandError, expand_command
@@ -140,6 +141,10 @@ def execute_drive(
140
141
  def cmd_drive(args: argparse.Namespace) -> int:
141
142
  json_mode = bool(getattr(args, "json", False))
142
143
 
144
+ # Decorative startup banner — interactive TTY only, suppressed in --json so
145
+ # neither stdout (the result stream) nor agent-parsed stderr is polluted (issue #15).
146
+ emit_banner(emit_diagnostic, json_mode=json_mode)
147
+
143
148
  repo = Path(args.repo).expanduser()
144
149
  if not repo.is_dir():
145
150
  raise CliError(
@@ -219,7 +224,10 @@ def cmd_drive(args: argparse.Namespace) -> int:
219
224
  def register(sub: argparse._SubParsersAction) -> None:
220
225
  p = sub.add_parser(
221
226
  "drive",
222
- help="Run a repo task through a coder engine and hand off the result.",
227
+ help=(
228
+ "Drive toward a goal: work autonomously on a request or instruction "
229
+ "through a coder engine, then hand off the result."
230
+ ),
223
231
  )
224
232
  # ``instruction`` is now zero-or-more positional tokens (nargs="*") so
225
233
  # ``--command`` can be the sole input without argparse raising an error.
@@ -227,7 +235,7 @@ def register(sub: argparse._SubParsersAction) -> None:
227
235
  "instruction",
228
236
  nargs="*",
229
237
  help=(
230
- "What the engine should do in the repo. "
238
+ "A goal or instruction to pursue autonomously. "
231
239
  "Mutually exclusive with --command. "
232
240
  "When --command is used, any positional tokens are passed as template arguments."
233
241
  ),
@@ -24,6 +24,7 @@ import sys
24
24
  from pathlib import Path
25
25
  from typing import Callable, Iterator, Optional
26
26
 
27
+ from convertible.cli._banner import emit_banner
27
28
  from convertible.cli._commands.drive import execute_drive as _default_drive
28
29
  from convertible.cli._errors import CliError
29
30
  from convertible.commands import CommandError, discover_commands, expand_command, load_command
@@ -130,6 +131,10 @@ def run_session(
130
131
  # mode, but to stderr in --json mode so stdout carries only JSON results.
131
132
  chrome: Callable[..., None] = err if json_mode else out
132
133
 
134
+ # Decorative startup banner — interactive TTY only, suppressed in --json (issue #15).
135
+ # Printed once here (not in the loop) so it greets the session, not each prompt.
136
+ emit_banner(err, json_mode=json_mode)
137
+
133
138
  config = EngineConfig.resolve(
134
139
  base_url=getattr(args, "base_url", None),
135
140
  model=getattr(args, "model", None),
@@ -18,9 +18,13 @@ A clonable template for AgentCulture mesh agents. It carries an agent-first CLI
18
18
  buildable/deployable package baseline. Clone it, rename the package, edit
19
19
  `culture.yaml`, and you have a new agent.
20
20
 
21
+ Run `convertible` with no verb at a terminal to open the interactive harness (the
22
+ `session` palette); piped or non-interactive, it prints this usage instead.
23
+
21
24
  ## Verbs
22
25
 
23
- - `convertible drive <instruction>` — run a repo task through a coder engine.
26
+ - `convertible drive <goal>` — drive toward a goal/instruction; work autonomously
27
+ through a coder engine and hand off the result.
24
28
  - `convertible session` — foreground interactive palette over the drive path.
25
29
  - `convertible wheels list` — list discovered engine wheels.
26
30
  - `convertible whoami` — identity probe from `culture.yaml`.
@@ -122,10 +126,11 @@ itself (distinct from the global `overview`, which describes the agent).
122
126
  _DRIVE = """\
123
127
  # convertible drive
124
128
 
125
- Run a repo task through a coder engine: select an engine wheel, run the bounded
126
- agentic tool-loop against the repo, write a result artifact, and hand off the
127
- change as a branch + PR. The same invocation works for every engine — only
128
- `--engine` changes.
129
+ Drive toward a goal: hand convertible a request or instruction and it works
130
+ autonomously selecting an engine wheel, running the bounded agentic tool-loop,
131
+ writing a result artifact, and handing off the change as a branch + PR. The repo
132
+ is the target (`--repo`, default cwd); the same invocation works for every
133
+ engine — only `--engine` changes.
129
134
 
130
135
  ## Usage
131
136
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "convertible-cli"
3
- version = "0.3.0"
3
+ version = "0.5.0"
4
4
  description = "Convertible CLI is a swappable coder-agent harness that turns different models into repo workers behind one shared task contract."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -0,0 +1,145 @@
1
+ """The convertible startup banner — decorative chrome on drive/session start (issue #15).
2
+
3
+ Contract under test: the banner is written to **stderr**, shown **only on an
4
+ interactive TTY**, and **suppressed in ``--json`` mode** — in both ``drive`` and
5
+ ``session``. So it never pollutes the stdout result stream nor the agent-parsed
6
+ ``error:``/``hint:`` stderr that convertible (an agent harness) emits.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ from pathlib import Path
14
+
15
+ import pytest
16
+
17
+ from convertible.cli import main
18
+ from convertible.cli._banner import banner, emit_banner
19
+
20
+
21
+ def _force_tty(monkeypatch: pytest.MonkeyPatch) -> None:
22
+ """Make the banner think stderr is interactive (capsys/pipes are not)."""
23
+ monkeypatch.setattr("convertible.cli._banner._isatty", lambda: True)
24
+
25
+
26
+ def _art() -> str:
27
+ """The banner as ``emit_banner`` renders it (trailing newlines normalized off)."""
28
+ return banner().rstrip("\n")
29
+
30
+
31
+ class _CollectingOut:
32
+ """Fake output sink that collects all emitted lines (mirrors test_session)."""
33
+
34
+ def __init__(self) -> None:
35
+ self.lines: list[str] = []
36
+
37
+ def __call__(self, *args: object, **kwargs: object) -> None:
38
+ self.lines.append(" ".join(str(a) for a in args))
39
+
40
+ def text(self) -> str:
41
+ return "\n".join(self.lines)
42
+
43
+
44
+ def _session_args(tmp_path: Path, *, json_mode: bool) -> argparse.Namespace:
45
+ return argparse.Namespace(
46
+ repo=str(tmp_path),
47
+ engine="mock",
48
+ no_pr=True,
49
+ base="main",
50
+ base_url=None,
51
+ model=None,
52
+ api_key=None,
53
+ max_steps=None,
54
+ json=json_mode,
55
+ )
56
+
57
+
58
+ def _run_session(args: argparse.Namespace) -> tuple[_CollectingOut, _CollectingOut]:
59
+ from convertible.cli._commands.session import run_session
60
+
61
+ out, err = _CollectingOut(), _CollectingOut()
62
+ rc = run_session(args, input_fn=iter(["q"]), out=out, err=err)
63
+ assert rc == 0
64
+ return out, err
65
+
66
+
67
+ def test_banner_loads_nonempty_art() -> None:
68
+ """The data file loads and yields the multi-line ASCII art."""
69
+ art = banner()
70
+ assert art.strip(), "banner should not be empty"
71
+ assert art.count("\n") >= 5, "expected a multi-line art block"
72
+ assert banner() is art, "banner() is cached"
73
+
74
+
75
+ def test_drive_banner_on_tty(
76
+ tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
77
+ ) -> None:
78
+ _force_tty(monkeypatch)
79
+ rc = main(["drive", "do work", "--repo", str(tmp_path), "--engine", "mock", "--no-pr"])
80
+ assert rc == 0
81
+ captured = capsys.readouterr()
82
+ assert _art() in captured.err, "banner should print to stderr on an interactive drive"
83
+ assert _art() not in captured.out, "banner must never reach the stdout result stream"
84
+
85
+
86
+ def test_drive_no_banner_when_not_a_tty(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
87
+ """Without a TTY (pipes, CI, agents), stderr stays clean for the error rubric."""
88
+ rc = main(["drive", "do work", "--repo", str(tmp_path), "--engine", "mock", "--no-pr"])
89
+ assert rc == 0
90
+ captured = capsys.readouterr()
91
+ assert _art() not in captured.err
92
+ assert _art() not in captured.out
93
+
94
+
95
+ def test_drive_json_suppresses_banner(
96
+ tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
97
+ ) -> None:
98
+ _force_tty(monkeypatch) # even on a TTY, --json suppresses it
99
+ rc = main(
100
+ ["drive", "do work", "--repo", str(tmp_path), "--engine", "mock", "--no-pr", "--json"]
101
+ )
102
+ assert rc == 0
103
+ captured = capsys.readouterr()
104
+ assert _art() not in captured.err, "--json must suppress the banner entirely"
105
+ assert _art() not in captured.out
106
+ assert json.loads(captured.out)["status"] == "ok", "stdout is still pure JSON"
107
+
108
+
109
+ def test_session_banner_on_tty(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
110
+ _force_tty(monkeypatch)
111
+ out, err = _run_session(_session_args(tmp_path, json_mode=False))
112
+ assert _art() in err.text(), "banner should greet the session on stderr"
113
+ assert _art() not in out.text()
114
+
115
+
116
+ def test_session_json_suppresses_banner(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
117
+ _force_tty(monkeypatch)
118
+ out, err = _run_session(_session_args(tmp_path, json_mode=True))
119
+ assert _art() not in err.text(), "--json must suppress the banner in session too"
120
+ assert _art() not in out.text()
121
+
122
+
123
+ def test_drive_and_session_banner_render_identically(
124
+ tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
125
+ ) -> None:
126
+ """No trailing-blank-line mismatch between drive (emit_diagnostic) and session (print)."""
127
+ _force_tty(monkeypatch)
128
+ main(["drive", "do work", "--repo", str(tmp_path), "--engine", "mock", "--no-pr"])
129
+ drive_err = capsys.readouterr().err
130
+ # Drive emits the art followed by exactly one newline (no trailing blank line).
131
+ assert _art() + "\n" in drive_err
132
+ assert _art() + "\n\n" not in drive_err
133
+
134
+
135
+ def test_emit_banner_swallows_missing_resource(monkeypatch: pytest.MonkeyPatch) -> None:
136
+ """A missing/unreadable resource must never break a drive — the banner is decorative."""
137
+ _force_tty(monkeypatch)
138
+
139
+ def _boom() -> str:
140
+ raise FileNotFoundError("_banner.txt")
141
+
142
+ monkeypatch.setattr("convertible.cli._banner.banner", _boom)
143
+ emitted: list[str] = []
144
+ emit_banner(emitted.append, json_mode=False) # must not raise
145
+ assert emitted == [], "no art emitted when the resource is missing"
@@ -18,12 +18,34 @@ def test_version_flag(capsys: pytest.CaptureFixture[str]) -> None:
18
18
  assert __version__ in capsys.readouterr().out
19
19
 
20
20
 
21
- def test_no_args_prints_help(capsys: pytest.CaptureFixture[str]) -> None:
21
+ def test_no_args_non_tty_prints_help(
22
+ capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
23
+ ) -> None:
24
+ # Force the non-interactive branch so this is deterministic regardless of
25
+ # pytest's capture mode: under `pytest -s` from a real terminal stdin/stdout
26
+ # would be TTYs and bare invocation would otherwise open the session loop
27
+ # (and block on input()). Non-interactive must fall back to usage, preserving
28
+ # the discoverable surface for scripts and agents.
29
+ monkeypatch.setattr("convertible.cli._stdio_is_interactive", lambda: False)
22
30
  rc = main([])
23
31
  assert rc == 0
24
32
  assert "usage: convertible" in capsys.readouterr().out
25
33
 
26
34
 
35
+ def test_no_args_tty_opens_session(
36
+ capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
37
+ ) -> None:
38
+ # At an interactive terminal, bare `convertible` opens the session harness.
39
+ # Force the interactive branch via the isolated seam, and stub input() to a
40
+ # quit token so the session renders its palette header then exits cleanly.
41
+ monkeypatch.setattr("convertible.cli._stdio_is_interactive", lambda: True)
42
+ monkeypatch.setattr("builtins.input", lambda *a, **k: "q")
43
+ rc = main([])
44
+ assert rc == 0
45
+ out = capsys.readouterr().out
46
+ assert "convertible session" in out
47
+
48
+
27
49
  def test_unknown_command_errors(capsys: pytest.CaptureFixture[str]) -> None:
28
50
  with pytest.raises(SystemExit) as exc:
29
51
  main(["bogus"])
@@ -72,7 +72,7 @@ wheels = [
72
72
 
73
73
  [[package]]
74
74
  name = "convertible-cli"
75
- version = "0.3.0"
75
+ version = "0.5.0"
76
76
  source = { editable = "." }
77
77
 
78
78
  [package.dev-dependencies]
File without changes
File without changes