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.
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/CHANGELOG.md +13 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/PKG-INFO +36 -3
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/README.md +35 -2
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/cli/__init__.py +19 -0
- convertible_cli-0.5.0/convertible/cli/_banner-big.txt +37 -0
- convertible_cli-0.5.0/convertible/cli/_banner.py +56 -0
- convertible_cli-0.5.0/convertible/cli/_banner.txt +23 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/cli/_commands/drive.py +10 -2
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/cli/_commands/session.py +5 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/explain/catalog.py +10 -5
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/pyproject.toml +1 -1
- convertible_cli-0.5.0/tests/test_banner.py +145 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_cli.py +23 -1
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/uv.lock +1 -1
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/agent-config/SKILL.md +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/agent-config/data/backend-fingerprints.yaml +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/agent-config/scripts/show.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/assign-to-workforce/SKILL.md +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/assign-to-workforce/scripts/assign-to-workforce.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/cicd/SKILL.md +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/cicd/scripts/_resolve-nick.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/cicd/scripts/portability-lint.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/cicd/scripts/pr-reply.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/cicd/scripts/pr-status.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/cicd/scripts/workflow.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/communicate/SKILL.md +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/communicate/scripts/fetch-issues.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/communicate/scripts/mesh-message.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/communicate/scripts/post-comment.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/communicate/scripts/post-issue.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/communicate/scripts/templates/skill-new-brief.md +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/communicate/scripts/templates/skill-update-brief.md +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/doc-test-alignment/SKILL.md +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/doc-test-alignment/scripts/check.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/pypi-maintainer/SKILL.md +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/pypi-maintainer/scripts/switch-source.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/run-tests/SKILL.md +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/run-tests/scripts/test.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/sonarclaude/SKILL.md +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/sonarclaude/scripts/sonar.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/spec-to-plan/SKILL.md +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/spec-to-plan/scripts/spec-to-plan.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/think/SKILL.md +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/think/scripts/think.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/version-bump/SKILL.md +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/version-bump/scripts/bump.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills.local.yaml.example +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.devague/current +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.devague/current_plan +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.devague/frames/convertible-gains-an-extensibility-layer-like-clau.json +0 -0
- {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
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.devague/plans/convertible-gains-an-extensibility-layer-like-clau.json +0 -0
- {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
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.flake8 +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.github/workflows/publish.yml +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.github/workflows/tests.yml +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.gitignore +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/.markdownlint-cli2.yaml +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/CLAUDE.md +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/LICENSE +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/__init__.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/__main__.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/artifact.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/cli/_commands/__init__.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/cli/_commands/cli.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/cli/_commands/commands.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/cli/_commands/doctor.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/cli/_commands/explain.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/cli/_commands/hooks.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/cli/_commands/learn.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/cli/_commands/overview.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/cli/_commands/wheels.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/cli/_commands/whoami.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/cli/_errors.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/cli/_output.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/commands.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/config.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/configdir.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/contract.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/engine.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/engines/__init__.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/engines/mock.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/engines/vllm_openai.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/explain/__init__.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/handoff.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/hooks.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/loop.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/registry.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/convertible/tools.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/culture.yaml +0 -0
- {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
- {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
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/docs/skill-sources.md +0 -0
- {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
- {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
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/sonar-project.properties +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/__init__.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_artifact.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_boundary.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_cli_introspection.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_commands.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_commands_cli.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_config.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_configdir.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_contract.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_drive.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_e2e_extensibility.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_e2e_mock.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_engine.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_handoff.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_hooks.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_hooks_cli.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_loop.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_mock_engine.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_registry.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_review_fixes.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_session.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_tools.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_vllm_live.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_vllm_openai.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.5.0}/tests/test_wheels.py +0 -0
- {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
|
+
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
|
|
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 <
|
|
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
|
|
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 <
|
|
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=
|
|
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
|
-
"
|
|
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 <
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
change as a branch + PR. The
|
|
128
|
-
`--
|
|
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
|
|
|
@@ -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
|
|
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"])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/cicd/scripts/_resolve-nick.sh
RENAMED
|
File without changes
|
{convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/cicd/scripts/portability-lint.sh
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/communicate/scripts/fetch-issues.sh
RENAMED
|
File without changes
|
{convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/communicate/scripts/mesh-message.sh
RENAMED
|
File without changes
|
{convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/communicate/scripts/post-comment.sh
RENAMED
|
File without changes
|
{convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/communicate/scripts/post-issue.sh
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/doc-test-alignment/scripts/check.sh
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{convertible_cli-0.3.0 → convertible_cli-0.5.0}/.claude/skills/spec-to-plan/scripts/spec-to-plan.sh
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|