convertible-cli 0.3.0__tar.gz → 0.4.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.4.0}/CHANGELOG.md +6 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/PKG-INFO +5 -1
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/README.md +4 -0
- convertible_cli-0.4.0/convertible/cli/_banner-big.txt +37 -0
- convertible_cli-0.4.0/convertible/cli/_banner.py +56 -0
- convertible_cli-0.4.0/convertible/cli/_banner.txt +23 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/convertible/cli/_commands/drive.py +5 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/convertible/cli/_commands/session.py +5 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/pyproject.toml +1 -1
- convertible_cli-0.4.0/tests/test_banner.py +145 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/uv.lock +1 -1
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/agent-config/SKILL.md +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/agent-config/data/backend-fingerprints.yaml +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/agent-config/scripts/show.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/assign-to-workforce/SKILL.md +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/assign-to-workforce/scripts/assign-to-workforce.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/cicd/SKILL.md +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/cicd/scripts/_resolve-nick.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/cicd/scripts/portability-lint.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/cicd/scripts/pr-reply.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/cicd/scripts/pr-status.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/cicd/scripts/workflow.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/communicate/SKILL.md +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/communicate/scripts/fetch-issues.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/communicate/scripts/mesh-message.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/communicate/scripts/post-comment.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/communicate/scripts/post-issue.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/communicate/scripts/templates/skill-new-brief.md +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/communicate/scripts/templates/skill-update-brief.md +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/doc-test-alignment/SKILL.md +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/doc-test-alignment/scripts/check.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/pypi-maintainer/SKILL.md +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/pypi-maintainer/scripts/switch-source.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/run-tests/SKILL.md +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/run-tests/scripts/test.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/sonarclaude/SKILL.md +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/sonarclaude/scripts/sonar.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/spec-to-plan/SKILL.md +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/spec-to-plan/scripts/spec-to-plan.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/think/SKILL.md +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/think/scripts/think.sh +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/version-bump/SKILL.md +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/version-bump/scripts/bump.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills.local.yaml.example +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.devague/current +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.devague/current_plan +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.devague/frames/convertible-gains-an-extensibility-layer-like-clau.json +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.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.4.0}/.devague/plans/convertible-gains-an-extensibility-layer-like-clau.json +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.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.4.0}/.flake8 +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.github/workflows/publish.yml +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.github/workflows/tests.yml +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.gitignore +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/.markdownlint-cli2.yaml +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/CLAUDE.md +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/LICENSE +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/convertible/__init__.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/convertible/__main__.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/convertible/artifact.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/convertible/cli/__init__.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/convertible/cli/_commands/__init__.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/convertible/cli/_commands/cli.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/convertible/cli/_commands/commands.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/convertible/cli/_commands/doctor.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/convertible/cli/_commands/explain.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/convertible/cli/_commands/hooks.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/convertible/cli/_commands/learn.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/convertible/cli/_commands/overview.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/convertible/cli/_commands/wheels.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/convertible/cli/_commands/whoami.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/convertible/cli/_errors.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/convertible/cli/_output.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/convertible/commands.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/convertible/config.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/convertible/configdir.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/convertible/contract.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/convertible/engine.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/convertible/engines/__init__.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/convertible/engines/mock.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/convertible/engines/vllm_openai.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/convertible/explain/__init__.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/convertible/explain/catalog.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/convertible/handoff.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/convertible/hooks.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/convertible/loop.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/convertible/registry.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/convertible/tools.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/culture.yaml +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.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.4.0}/docs/plans/2026-05-27-convertible-gains-an-extensibility-layer-like-clau.md +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/docs/skill-sources.md +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.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.4.0}/docs/specs/2026-05-27-convertible-gains-an-extensibility-layer-like-clau.md +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/sonar-project.properties +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/tests/__init__.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/tests/test_artifact.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/tests/test_boundary.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/tests/test_cli.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/tests/test_cli_introspection.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/tests/test_commands.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/tests/test_commands_cli.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/tests/test_config.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/tests/test_configdir.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/tests/test_contract.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/tests/test_drive.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/tests/test_e2e_extensibility.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/tests/test_e2e_mock.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/tests/test_engine.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/tests/test_handoff.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/tests/test_hooks.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/tests/test_hooks_cli.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/tests/test_loop.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/tests/test_mock_engine.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/tests/test_registry.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/tests/test_review_fixes.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/tests/test_session.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/tests/test_tools.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/tests/test_vllm_live.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/tests/test_vllm_openai.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/tests/test_wheels.py +0 -0
- {convertible_cli-0.3.0 → convertible_cli-0.4.0}/tests/test_zero_deps.py +0 -0
|
@@ -5,6 +5,12 @@ 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.4.0] - 2026-05-27
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- 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.
|
|
13
|
+
|
|
8
14
|
## [0.3.0] - 2026-05-27
|
|
9
15
|
|
|
10
16
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: convertible-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -65,6 +65,10 @@ which one ran.
|
|
|
65
65
|
- **Interactive palette** — `convertible session` opens a foreground command
|
|
66
66
|
browser so operators can select templates and run ad-hoc instructions without
|
|
67
67
|
leaving the shell.
|
|
68
|
+
- **Startup banner** — `convertible drive` and `convertible session` greet an
|
|
69
|
+
interactive terminal with an ASCII banner. It's decorative chrome: written to
|
|
70
|
+
stderr, shown only on a TTY, and suppressed under `--json`, so it never
|
|
71
|
+
pollutes the stdout result stream or agent-parsed output.
|
|
68
72
|
|
|
69
73
|
**Not in v0** (by design): a multi-engine router/policy gearbox, an execution
|
|
70
74
|
sandbox, a daemon mode, and Codex/Claude/Gemini drivers. The runtime package has
|
|
@@ -48,6 +48,10 @@ which one ran.
|
|
|
48
48
|
- **Interactive palette** — `convertible session` opens a foreground command
|
|
49
49
|
browser so operators can select templates and run ad-hoc instructions without
|
|
50
50
|
leaving the shell.
|
|
51
|
+
- **Startup banner** — `convertible drive` and `convertible session` greet an
|
|
52
|
+
interactive terminal with an ASCII banner. It's decorative chrome: written to
|
|
53
|
+
stderr, shown only on a TTY, and suppressed under `--json`, so it never
|
|
54
|
+
pollutes the stdout result stream or agent-parsed output.
|
|
51
55
|
|
|
52
56
|
**Not in v0** (by design): a multi-engine router/policy gearbox, an execution
|
|
53
57
|
sandbox, a daemon mode, and Codex/Claude/Gemini drivers. The runtime package has
|
|
@@ -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(
|
|
@@ -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),
|
|
@@ -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"
|
|
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.4.0}/.claude/skills/cicd/scripts/_resolve-nick.sh
RENAMED
|
File without changes
|
{convertible_cli-0.3.0 → convertible_cli-0.4.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.4.0}/.claude/skills/communicate/scripts/fetch-issues.sh
RENAMED
|
File without changes
|
{convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/communicate/scripts/mesh-message.sh
RENAMED
|
File without changes
|
{convertible_cli-0.3.0 → convertible_cli-0.4.0}/.claude/skills/communicate/scripts/post-comment.sh
RENAMED
|
File without changes
|
{convertible_cli-0.3.0 → convertible_cli-0.4.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.4.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.4.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|