convertible-cli 0.1.2__py3-none-any.whl
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/__init__.py +13 -0
- convertible/__main__.py +10 -0
- convertible/cli/__init__.py +136 -0
- convertible/cli/_commands/__init__.py +1 -0
- convertible/cli/_commands/cli.py +43 -0
- convertible/cli/_commands/doctor.py +122 -0
- convertible/cli/_commands/explain.py +38 -0
- convertible/cli/_commands/learn.py +88 -0
- convertible/cli/_commands/overview.py +112 -0
- convertible/cli/_commands/whoami.py +106 -0
- convertible/cli/_errors.py +42 -0
- convertible/cli/_output.py +53 -0
- convertible/explain/__init__.py +24 -0
- convertible/explain/catalog.py +129 -0
- convertible_cli-0.1.2.dist-info/METADATA +73 -0
- convertible_cli-0.1.2.dist-info/RECORD +19 -0
- convertible_cli-0.1.2.dist-info/WHEEL +4 -0
- convertible_cli-0.1.2.dist-info/entry_points.txt +2 -0
- convertible_cli-0.1.2.dist-info/licenses/LICENSE +21 -0
convertible/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""convertible — agent-first CLI for an AgentCulture mesh agent."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib.metadata import PackageNotFoundError
|
|
6
|
+
from importlib.metadata import version as _pkg_version
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
__version__ = _pkg_version("convertible-cli")
|
|
10
|
+
except PackageNotFoundError: # pragma: no cover - editable install without metadata
|
|
11
|
+
__version__ = "0.0.0"
|
|
12
|
+
|
|
13
|
+
__all__ = ["__version__"]
|
convertible/__main__.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Unified CLI entry point for convertible.
|
|
2
|
+
|
|
3
|
+
The agent-first global verbs (``whoami``, ``learn``, ``explain``, ``overview``,
|
|
4
|
+
``doctor``) are registered here under :mod:`convertible.cli._commands`,
|
|
5
|
+
alongside the ``cli`` noun group. Future noun groups register via their own
|
|
6
|
+
``register()`` functions following the same pattern.
|
|
7
|
+
|
|
8
|
+
Error propagation contract
|
|
9
|
+
--------------------------
|
|
10
|
+
Every handler raises :class:`convertible.cli._errors.CliError` on
|
|
11
|
+
failure; ``main()`` catches it via :func:`_dispatch` and routes through
|
|
12
|
+
:mod:`convertible.cli._output`. Unknown exceptions are wrapped into a
|
|
13
|
+
``CliError`` so no Python traceback leaks to stderr.
|
|
14
|
+
|
|
15
|
+
Argparse errors (unknown verb, missing arg) also route through the structured
|
|
16
|
+
format — ``_CliArgumentParser`` overrides ``.error()`` and the subparsers are
|
|
17
|
+
built with ``parser_class=_CliArgumentParser``. Whether errors render as text or
|
|
18
|
+
JSON depends on whether ``--json`` appears in the raw argv (:func:`main` sets
|
|
19
|
+
``_json_hint`` before ``parse_args``).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import sys
|
|
26
|
+
|
|
27
|
+
from convertible import __version__
|
|
28
|
+
from convertible.cli._errors import EXIT_USER_ERROR, CliError
|
|
29
|
+
from convertible.cli._output import emit_error
|
|
30
|
+
|
|
31
|
+
_ISSUES_URL = "https://github.com/agentculture/convertible/issues"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class _CliArgumentParser(argparse.ArgumentParser):
|
|
35
|
+
"""ArgumentParser that routes errors through :func:`emit_error`.
|
|
36
|
+
|
|
37
|
+
Argparse's default error handler writes ``prog: error: <msg>`` to stderr
|
|
38
|
+
and exits 2, skipping the CliError plumbing (and the ``hint:`` line agents
|
|
39
|
+
look for). This subclass emits the structured format and exits with
|
|
40
|
+
:attr:`EXIT_USER_ERROR`.
|
|
41
|
+
|
|
42
|
+
JSON mode: parse-time errors happen before ``args.json`` exists, so we rely
|
|
43
|
+
on a class-level ``_json_hint`` that :func:`main` pre-populates by scanning
|
|
44
|
+
raw argv for ``--json``. Shared across all subparser instances.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
_json_hint: bool = False
|
|
48
|
+
|
|
49
|
+
def error(self, message: str) -> None: # type: ignore[override]
|
|
50
|
+
err = CliError(
|
|
51
|
+
code=EXIT_USER_ERROR,
|
|
52
|
+
message=message,
|
|
53
|
+
remediation=f"run '{self.prog} --help' to see valid arguments",
|
|
54
|
+
)
|
|
55
|
+
emit_error(err, json_mode=type(self)._json_hint)
|
|
56
|
+
raise SystemExit(err.code)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _argv_has_json(argv: list[str] | None) -> bool:
|
|
60
|
+
tokens = argv if argv is not None else sys.argv[1:]
|
|
61
|
+
return any(t == "--json" or t.startswith("--json=") for t in tokens)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
65
|
+
from convertible.cli._commands import cli as _cli_group
|
|
66
|
+
from convertible.cli._commands import doctor as _doctor_cmd
|
|
67
|
+
from convertible.cli._commands import explain as _explain_cmd
|
|
68
|
+
from convertible.cli._commands import learn as _learn_cmd
|
|
69
|
+
from convertible.cli._commands import overview as _overview_cmd
|
|
70
|
+
from convertible.cli._commands import whoami as _whoami_cmd
|
|
71
|
+
|
|
72
|
+
parser = _CliArgumentParser(
|
|
73
|
+
prog="convertible",
|
|
74
|
+
description="convertible — a clonable template for AgentCulture mesh agents.",
|
|
75
|
+
)
|
|
76
|
+
parser.add_argument(
|
|
77
|
+
"--version",
|
|
78
|
+
action="version",
|
|
79
|
+
version=f"%(prog)s {__version__}",
|
|
80
|
+
)
|
|
81
|
+
# parser_class propagates to every subparser so their .error() routes
|
|
82
|
+
# through _CliArgumentParser too.
|
|
83
|
+
sub = parser.add_subparsers(dest="command", parser_class=_CliArgumentParser)
|
|
84
|
+
|
|
85
|
+
_whoami_cmd.register(sub)
|
|
86
|
+
_learn_cmd.register(sub)
|
|
87
|
+
_explain_cmd.register(sub)
|
|
88
|
+
_overview_cmd.register(sub)
|
|
89
|
+
_doctor_cmd.register(sub)
|
|
90
|
+
_cli_group.register(sub)
|
|
91
|
+
# Register your own noun groups here:
|
|
92
|
+
# from convertible.cli._commands import my_noun as _my_noun_group
|
|
93
|
+
# _my_noun_group.register(sub)
|
|
94
|
+
|
|
95
|
+
return parser
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _dispatch(args: argparse.Namespace) -> int:
|
|
99
|
+
"""Invoke the registered handler and translate exceptions to exit codes.
|
|
100
|
+
|
|
101
|
+
A handler may return ``None`` (success, exit 0) or an ``int`` exit code.
|
|
102
|
+
Failures MUST raise :class:`CliError`; any other exception is wrapped into
|
|
103
|
+
one so no Python traceback leaks.
|
|
104
|
+
"""
|
|
105
|
+
json_mode = bool(getattr(args, "json", False))
|
|
106
|
+
try:
|
|
107
|
+
rc = args.func(args)
|
|
108
|
+
except CliError as err:
|
|
109
|
+
emit_error(err, json_mode=json_mode)
|
|
110
|
+
return err.code
|
|
111
|
+
except Exception as err: # noqa: BLE001 - last-resort; wrap and route cleanly
|
|
112
|
+
wrapped = CliError(
|
|
113
|
+
code=EXIT_USER_ERROR,
|
|
114
|
+
message=f"unexpected: {err.__class__.__name__}: {err}",
|
|
115
|
+
remediation=f"file a bug at {_ISSUES_URL}",
|
|
116
|
+
)
|
|
117
|
+
emit_error(wrapped, json_mode=json_mode)
|
|
118
|
+
return wrapped.code
|
|
119
|
+
return rc if rc is not None else 0
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def main(argv: list[str] | None = None) -> int:
|
|
123
|
+
# Pre-parse peek so argparse-level errors honour --json.
|
|
124
|
+
_CliArgumentParser._json_hint = _argv_has_json(argv)
|
|
125
|
+
parser = _build_parser()
|
|
126
|
+
args = parser.parse_args(argv)
|
|
127
|
+
|
|
128
|
+
if args.command is None:
|
|
129
|
+
parser.print_help()
|
|
130
|
+
return 0
|
|
131
|
+
|
|
132
|
+
return _dispatch(args)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
if __name__ == "__main__":
|
|
136
|
+
sys.exit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI command modules. Each exposes a ``register(sub)`` function."""
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""``convertible cli`` — noun grouping CLI-surface introspection.
|
|
2
|
+
|
|
3
|
+
Exists to satisfy the agent-first rubric's ``overview_cli_noun_exists`` check:
|
|
4
|
+
any noun with action-verbs must also expose ``overview``. There are no
|
|
5
|
+
action-verbs under ``cli`` today, but ``cli overview`` describes the CLI surface
|
|
6
|
+
(distinct from the global ``overview``, which describes the agent).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
|
|
13
|
+
from convertible.cli._commands.overview import cli_sections, emit_overview
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def cmd_cli_overview(args: argparse.Namespace) -> int:
|
|
17
|
+
emit_overview(
|
|
18
|
+
"convertible cli",
|
|
19
|
+
cli_sections(),
|
|
20
|
+
json_mode=bool(getattr(args, "json", False)),
|
|
21
|
+
)
|
|
22
|
+
return 0
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _no_verb(args: argparse.Namespace) -> int:
|
|
26
|
+
# `convertible cli` with no sub-verb prints the noun's overview.
|
|
27
|
+
return cmd_cli_overview(args)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
31
|
+
p = sub.add_parser(
|
|
32
|
+
"cli",
|
|
33
|
+
help="CLI-surface introspection (see 'convertible cli overview').",
|
|
34
|
+
)
|
|
35
|
+
p.add_argument("--json", action="store_true", help="Emit structured JSON.")
|
|
36
|
+
p.set_defaults(func=_no_verb, json=False)
|
|
37
|
+
# `p` is a _CliArgumentParser (the top-level subparsers were built with that
|
|
38
|
+
# parser_class); propagate it so `cli overview` parse errors route through
|
|
39
|
+
# the structured error contract instead of argparse's default stderr/exit 2.
|
|
40
|
+
noun_sub = p.add_subparsers(dest="cli_command", parser_class=type(p))
|
|
41
|
+
ov = noun_sub.add_parser("overview", help="Describe the convertible CLI surface.")
|
|
42
|
+
ov.add_argument("--json", action="store_true", help="Emit structured JSON.")
|
|
43
|
+
ov.set_defaults(func=cmd_cli_overview)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""``convertible doctor`` — check the agent-identity invariants.
|
|
2
|
+
|
|
3
|
+
Mirrors the two invariants ``steward doctor`` verifies for a mesh agent:
|
|
4
|
+
|
|
5
|
+
* **prompt-file-present** — the repo declares an agent in ``culture.yaml`` and
|
|
6
|
+
has the matching prompt file on disk;
|
|
7
|
+
* **backend-consistency** — the declared ``backend`` matches the prompt file
|
|
8
|
+
(``claude`` → ``CLAUDE.md``, ``acp`` → ``AGENTS.md``, ``gemini`` → ``GEMINI.md``).
|
|
9
|
+
|
|
10
|
+
Plus a **skills-present** check (the vendored ``.claude/skills/`` kit). Read-only.
|
|
11
|
+
|
|
12
|
+
Reports the rubric-shaped contract
|
|
13
|
+
``{healthy, checks: [{id, passed, severity, message, remediation}]}`` so the
|
|
14
|
+
agent-first rubric's bundle 7 passes. When run from a wheel install (no
|
|
15
|
+
``culture.yaml`` alongside the package), it reports a single info check and
|
|
16
|
+
exits 0 — there is nothing to diagnose.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
|
|
23
|
+
from convertible.cli._commands.whoami import find_culture_yaml, read_agent_fields
|
|
24
|
+
from convertible.cli._output import emit_result
|
|
25
|
+
|
|
26
|
+
# backend → required prompt file (the backend-consistency mapping).
|
|
27
|
+
_PROMPT_FILE = {
|
|
28
|
+
"claude": "CLAUDE.md",
|
|
29
|
+
"acp": "AGENTS.md",
|
|
30
|
+
"gemini": "GEMINI.md",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _diagnose() -> dict[str, object]:
|
|
35
|
+
cfg = find_culture_yaml()
|
|
36
|
+
if cfg is None:
|
|
37
|
+
check = {
|
|
38
|
+
"id": "source_checkout",
|
|
39
|
+
"passed": True,
|
|
40
|
+
"severity": "info",
|
|
41
|
+
"message": "no culture.yaml found alongside the package; identity checks skipped",
|
|
42
|
+
"remediation": "",
|
|
43
|
+
}
|
|
44
|
+
return {"healthy": True, "checks": [check]}
|
|
45
|
+
|
|
46
|
+
root = cfg.parent
|
|
47
|
+
fields = read_agent_fields()
|
|
48
|
+
backend = fields["backend"]
|
|
49
|
+
checks: list[dict[str, object]] = []
|
|
50
|
+
|
|
51
|
+
# 1. backend-consistency: the prompt file for the declared backend exists.
|
|
52
|
+
expected = _PROMPT_FILE.get(backend)
|
|
53
|
+
if expected is None:
|
|
54
|
+
checks.append(
|
|
55
|
+
{
|
|
56
|
+
"id": "backend_consistency",
|
|
57
|
+
"passed": False,
|
|
58
|
+
"severity": "error",
|
|
59
|
+
"message": f"unknown backend '{backend}' in culture.yaml",
|
|
60
|
+
"remediation": f"set backend to one of: {', '.join(sorted(_PROMPT_FILE))}",
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
present = (root / expected).is_file()
|
|
65
|
+
checks.append(
|
|
66
|
+
{
|
|
67
|
+
"id": "prompt_file_present",
|
|
68
|
+
"passed": present,
|
|
69
|
+
"severity": "error",
|
|
70
|
+
"message": (
|
|
71
|
+
f"backend '{backend}' requires {expected} — "
|
|
72
|
+
+ ("present" if present else "missing")
|
|
73
|
+
),
|
|
74
|
+
"remediation": "" if present else f"create {expected} at the repo root",
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# 2. skills-present: the vendored skill kit is on disk.
|
|
79
|
+
skills_dir = root / ".claude" / "skills"
|
|
80
|
+
has_skills = skills_dir.is_dir() and any(skills_dir.iterdir())
|
|
81
|
+
checks.append(
|
|
82
|
+
{
|
|
83
|
+
"id": "skills_present",
|
|
84
|
+
"passed": has_skills,
|
|
85
|
+
"severity": "warning",
|
|
86
|
+
"message": (
|
|
87
|
+
".claude/skills/ vendored" if has_skills else ".claude/skills/ missing or empty"
|
|
88
|
+
),
|
|
89
|
+
"remediation": (
|
|
90
|
+
"" if has_skills else "vendor the skill kit (see docs/skill-sources.md)"
|
|
91
|
+
),
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
healthy = all(c["passed"] for c in checks)
|
|
96
|
+
return {"healthy": healthy, "checks": checks}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def cmd_doctor(args: argparse.Namespace) -> int:
|
|
100
|
+
report = _diagnose()
|
|
101
|
+
json_mode = bool(getattr(args, "json", False))
|
|
102
|
+
if json_mode:
|
|
103
|
+
emit_result(report, json_mode=True)
|
|
104
|
+
else:
|
|
105
|
+
status = "healthy" if report["healthy"] else "unhealthy"
|
|
106
|
+
lines = [f"convertible doctor: {status}", ""]
|
|
107
|
+
for check in report["checks"]:
|
|
108
|
+
mark = "ok" if check["passed"] else "FAIL"
|
|
109
|
+
lines.append(f"[{mark}] {check['id']}: {check['message']}")
|
|
110
|
+
if not check["passed"] and check["remediation"]:
|
|
111
|
+
lines.append(f" hint: {check['remediation']}")
|
|
112
|
+
emit_result("\n".join(lines), json_mode=False)
|
|
113
|
+
return 0 if report["healthy"] else 1
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
117
|
+
p = sub.add_parser(
|
|
118
|
+
"doctor",
|
|
119
|
+
help="Check the agent-identity invariants (prompt-file-present, backend-consistency).",
|
|
120
|
+
)
|
|
121
|
+
p.add_argument("--json", action="store_true", help="Emit structured JSON.")
|
|
122
|
+
p.set_defaults(func=cmd_doctor)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""``convertible explain <path>...`` — global markdown catalog lookup (stable-contract).
|
|
2
|
+
|
|
3
|
+
``explain`` is global (not nested under a noun). It takes zero or more path
|
|
4
|
+
tokens and resolves them via the catalog in :mod:`convertible.explain`.
|
|
5
|
+
Unknown paths raise :class:`CliError` with a remediation hint.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
|
|
12
|
+
from convertible.cli._output import emit_result
|
|
13
|
+
from convertible.explain import resolve
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def cmd_explain(args: argparse.Namespace) -> int:
|
|
17
|
+
path = tuple(args.path) if args.path else ()
|
|
18
|
+
markdown = resolve(path)
|
|
19
|
+
json_mode = bool(getattr(args, "json", False))
|
|
20
|
+
if json_mode:
|
|
21
|
+
emit_result({"path": list(path), "markdown": markdown}, json_mode=True)
|
|
22
|
+
else:
|
|
23
|
+
emit_result(markdown, json_mode=False)
|
|
24
|
+
return 0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
28
|
+
p = sub.add_parser(
|
|
29
|
+
"explain",
|
|
30
|
+
help="Print markdown docs for a noun/verb path. Supports --json.",
|
|
31
|
+
)
|
|
32
|
+
p.add_argument(
|
|
33
|
+
"path",
|
|
34
|
+
nargs="*",
|
|
35
|
+
help="Command path tokens; empty = root (same as 'convertible').",
|
|
36
|
+
)
|
|
37
|
+
p.add_argument("--json", action="store_true", help="Emit structured JSON.")
|
|
38
|
+
p.set_defaults(func=cmd_explain)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""``convertible learn`` — the learnability affordance.
|
|
2
|
+
|
|
3
|
+
Prints a structured self-teaching prompt. Must satisfy the agent-first rubric:
|
|
4
|
+
>=200 chars and mention purpose, command map, exit codes, --json, and explain.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
|
|
11
|
+
from convertible import __version__
|
|
12
|
+
from convertible.cli._output import emit_result
|
|
13
|
+
|
|
14
|
+
_TEXT = """\
|
|
15
|
+
convertible — a clonable template for AgentCulture mesh agents.
|
|
16
|
+
|
|
17
|
+
Purpose
|
|
18
|
+
-------
|
|
19
|
+
Scaffold for a new Culture mesh agent: an agent-first CLI (cited from the teken
|
|
20
|
+
`python-cli` reference), an identity (culture.yaml + CLAUDE.md), the canonical
|
|
21
|
+
guildmaster skill kit under .claude/skills/, and a deploy/CI baseline. Clone it,
|
|
22
|
+
rename the package, and edit culture.yaml to mint a new agent.
|
|
23
|
+
|
|
24
|
+
Commands
|
|
25
|
+
--------
|
|
26
|
+
convertible whoami Identity from culture.yaml.
|
|
27
|
+
convertible learn This self-teaching prompt.
|
|
28
|
+
convertible explain <path>... Markdown docs for any noun/verb path.
|
|
29
|
+
convertible overview Descriptive snapshot of the agent.
|
|
30
|
+
convertible doctor Check the agent-identity invariants.
|
|
31
|
+
convertible cli overview Describe the CLI surface itself.
|
|
32
|
+
|
|
33
|
+
Machine-readable output
|
|
34
|
+
-----------------------
|
|
35
|
+
Every command supports --json. Errors in JSON mode emit
|
|
36
|
+
{"code", "message", "remediation"} to stderr. Stdout and stderr never mix.
|
|
37
|
+
|
|
38
|
+
Exit-code policy
|
|
39
|
+
----------------
|
|
40
|
+
0 success
|
|
41
|
+
1 user-input error (bad flag, bad path, missing arg)
|
|
42
|
+
2 environment / setup error
|
|
43
|
+
3+ reserved
|
|
44
|
+
|
|
45
|
+
More detail
|
|
46
|
+
-----------
|
|
47
|
+
convertible explain convertible
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _as_json_payload() -> dict[str, object]:
|
|
52
|
+
return {
|
|
53
|
+
"tool": "convertible",
|
|
54
|
+
"version": __version__,
|
|
55
|
+
"purpose": "Clonable scaffold for a new AgentCulture mesh agent.",
|
|
56
|
+
"commands": [
|
|
57
|
+
{"path": ["whoami"], "summary": "Identity probe from culture.yaml."},
|
|
58
|
+
{"path": ["learn"], "summary": "Self-teaching prompt."},
|
|
59
|
+
{"path": ["explain"], "summary": "Markdown docs by path."},
|
|
60
|
+
{"path": ["overview"], "summary": "Descriptive snapshot of the agent."},
|
|
61
|
+
{"path": ["doctor"], "summary": "Check the agent-identity invariants."},
|
|
62
|
+
{"path": ["cli", "overview"], "summary": "Describe the CLI surface."},
|
|
63
|
+
],
|
|
64
|
+
"exit_codes": {
|
|
65
|
+
"0": "success",
|
|
66
|
+
"1": "user-input error",
|
|
67
|
+
"2": "environment/setup error",
|
|
68
|
+
},
|
|
69
|
+
"json_support": True,
|
|
70
|
+
"explain_pointer": "convertible explain <path>",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def cmd_learn(args: argparse.Namespace) -> int:
|
|
75
|
+
if getattr(args, "json", False):
|
|
76
|
+
emit_result(_as_json_payload(), json_mode=True)
|
|
77
|
+
else:
|
|
78
|
+
emit_result(_TEXT, json_mode=False)
|
|
79
|
+
return 0
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
83
|
+
p = sub.add_parser(
|
|
84
|
+
"learn",
|
|
85
|
+
help="Print a structured self-teaching prompt for agent consumers.",
|
|
86
|
+
)
|
|
87
|
+
p.add_argument("--json", action="store_true", help="Emit structured JSON.")
|
|
88
|
+
p.set_defaults(func=cmd_learn)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""``convertible overview`` — read-only descriptive snapshot of the agent.
|
|
2
|
+
|
|
3
|
+
Describes the agent to an agent reader: identity (from culture.yaml), the verb
|
|
4
|
+
surface, and the sibling-pattern artifacts this template carries. The shared
|
|
5
|
+
section/render helpers here are reused by the ``cli`` noun's ``overview`` (see
|
|
6
|
+
:mod:`convertible.cli._commands.cli`).
|
|
7
|
+
|
|
8
|
+
Descriptive verbs never hard-fail on a missing target path — an optional
|
|
9
|
+
positional ``target`` is accepted and ignored (overview describes this agent,
|
|
10
|
+
not an external target), so ``overview <bogus-path>`` still exits 0.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
|
|
17
|
+
from convertible.cli._commands.whoami import report
|
|
18
|
+
from convertible.cli._output import emit_result
|
|
19
|
+
|
|
20
|
+
_ARTIFACTS = [
|
|
21
|
+
"culture.yaml + CLAUDE.md — mesh identity (suffix + backend)",
|
|
22
|
+
".claude/skills/ — the canonical guildmaster skill kit (cite-don't-import)",
|
|
23
|
+
"docs/skill-sources.md — skill provenance ledger",
|
|
24
|
+
"pyproject.toml + .github/workflows/ — buildable, deployable package baseline",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
_VERBS = [
|
|
28
|
+
"whoami — identity probe (nick, version, backend, model)",
|
|
29
|
+
"learn — structured self-teaching prompt",
|
|
30
|
+
"explain <path> — markdown docs for a topic",
|
|
31
|
+
"overview — this descriptive snapshot",
|
|
32
|
+
"doctor — check the agent-identity invariants",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def agent_sections() -> list[dict[str, object]]:
|
|
37
|
+
"""Sections describing the agent (used by the global verb)."""
|
|
38
|
+
ident = report()
|
|
39
|
+
return [
|
|
40
|
+
{
|
|
41
|
+
"title": "Identity",
|
|
42
|
+
"items": [
|
|
43
|
+
f"nick: {ident['nick']}",
|
|
44
|
+
f"version: {ident['version']}",
|
|
45
|
+
f"backend: {ident['backend']}",
|
|
46
|
+
f"model: {ident['model']}",
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
{"title": "Verbs", "items": list(_VERBS)},
|
|
50
|
+
{"title": "Sibling-pattern artifacts", "items": list(_ARTIFACTS)},
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def cli_sections() -> list[dict[str, object]]:
|
|
55
|
+
"""Sections describing the CLI surface itself (used by `cli overview`)."""
|
|
56
|
+
return [
|
|
57
|
+
{
|
|
58
|
+
"title": "Verbs",
|
|
59
|
+
"items": list(_VERBS) + ["cli overview — describe the CLI surface (this command)"],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"title": "Conventions",
|
|
63
|
+
"items": [
|
|
64
|
+
"every command supports --json",
|
|
65
|
+
"results to stdout, errors/diagnostics to stderr (never mixed)",
|
|
66
|
+
"exit codes: 0 success, 1 user error, 2 environment error, 3+ reserved",
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def render_text(subject: str, sections: list[dict[str, object]]) -> str:
|
|
73
|
+
lines = [f"# {subject}", ""]
|
|
74
|
+
for section in sections:
|
|
75
|
+
lines.append(f"## {section['title']}")
|
|
76
|
+
for item in section["items"]:
|
|
77
|
+
lines.append(f"- {item}")
|
|
78
|
+
lines.append("")
|
|
79
|
+
return "\n".join(lines).rstrip()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def emit_overview(subject: str, sections: list[dict[str, object]], *, json_mode: bool) -> None:
|
|
83
|
+
if json_mode:
|
|
84
|
+
emit_result({"subject": subject, "sections": sections}, json_mode=True)
|
|
85
|
+
else:
|
|
86
|
+
emit_result(render_text(subject, sections), json_mode=False)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def cmd_overview(args: argparse.Namespace) -> int:
|
|
90
|
+
# `target` is accepted for rubric compatibility (descriptive verbs must not
|
|
91
|
+
# hard-fail on a missing path) but overview describes this agent itself.
|
|
92
|
+
emit_overview(
|
|
93
|
+
"convertible",
|
|
94
|
+
agent_sections(),
|
|
95
|
+
json_mode=bool(getattr(args, "json", False)),
|
|
96
|
+
)
|
|
97
|
+
return 0
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
101
|
+
p = sub.add_parser(
|
|
102
|
+
"overview",
|
|
103
|
+
help="Read-only descriptive snapshot of the agent (identity, verbs, artifacts).",
|
|
104
|
+
)
|
|
105
|
+
p.add_argument(
|
|
106
|
+
"target",
|
|
107
|
+
nargs="?",
|
|
108
|
+
help="Ignored — overview always describes this agent itself. Accepted so a "
|
|
109
|
+
"stray path argument never hard-fails.",
|
|
110
|
+
)
|
|
111
|
+
p.add_argument("--json", action="store_true", help="Emit structured JSON.")
|
|
112
|
+
p.set_defaults(func=cmd_overview)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""``convertible whoami`` — the smallest identity probe.
|
|
2
|
+
|
|
3
|
+
Reports the agent's identity as declared in ``culture.yaml``: its nick
|
|
4
|
+
(``suffix``), the backend it runs on, and the served model (if any) — plus the
|
|
5
|
+
package version. Read-only; touches nothing but its own ``culture.yaml``.
|
|
6
|
+
|
|
7
|
+
When you clone this template, rename the package and update ``culture.yaml`` —
|
|
8
|
+
``whoami`` then reflects your new agent's identity with no code change.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from convertible import __version__
|
|
17
|
+
from convertible.cli._output import emit_result
|
|
18
|
+
|
|
19
|
+
_FALLBACK_NICK = "convertible"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def find_culture_yaml() -> Path | None:
|
|
23
|
+
"""Locate this agent's own ``culture.yaml`` by walking up from this module.
|
|
24
|
+
|
|
25
|
+
The identity must be the agent's own, not whatever ``culture.yaml`` happens
|
|
26
|
+
to sit in the caller's current working directory. In an editable / source
|
|
27
|
+
install, walking up from ``__file__`` finds the repo root; in a wheel
|
|
28
|
+
install no ``culture.yaml`` ships alongside the package and the caller falls
|
|
29
|
+
back to the literal defaults.
|
|
30
|
+
"""
|
|
31
|
+
here = Path(__file__).resolve()
|
|
32
|
+
for parent in here.parents:
|
|
33
|
+
candidate = parent / "culture.yaml"
|
|
34
|
+
if candidate.is_file():
|
|
35
|
+
return candidate
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def read_agent_fields() -> dict[str, str]:
|
|
40
|
+
"""Return ``suffix``/``backend``/``model`` from the first agent block.
|
|
41
|
+
|
|
42
|
+
Parsed without a YAML dependency to keep the runtime deps empty. Reads
|
|
43
|
+
top-level ``key: value`` lines within the first agent entry; anything
|
|
44
|
+
fancier than the documented shape falls back to the defaults below.
|
|
45
|
+
"""
|
|
46
|
+
fields = {"nick": _FALLBACK_NICK, "backend": "unknown", "model": "unknown"}
|
|
47
|
+
cfg = find_culture_yaml()
|
|
48
|
+
if cfg is None:
|
|
49
|
+
return fields
|
|
50
|
+
try:
|
|
51
|
+
text = cfg.read_text(encoding="utf-8")
|
|
52
|
+
except OSError:
|
|
53
|
+
return fields
|
|
54
|
+
seen_agent = False
|
|
55
|
+
for line in text.splitlines():
|
|
56
|
+
stripped = line.strip()
|
|
57
|
+
if stripped.startswith(("- suffix:", "suffix:")):
|
|
58
|
+
if seen_agent: # second agent block — stop at the first
|
|
59
|
+
break
|
|
60
|
+
seen_agent = True
|
|
61
|
+
fields["nick"] = _scalar(stripped, "suffix")
|
|
62
|
+
elif seen_agent and stripped.startswith("backend:"):
|
|
63
|
+
fields["backend"] = _scalar(stripped, "backend")
|
|
64
|
+
elif seen_agent and stripped.startswith("model:"):
|
|
65
|
+
fields["model"] = _scalar(stripped, "model")
|
|
66
|
+
return fields
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _scalar(line: str, key: str) -> str:
|
|
70
|
+
"""Extract the scalar after ``key:`` from a ``culture.yaml`` line."""
|
|
71
|
+
_, _, value = line.partition(f"{key}:")
|
|
72
|
+
return value.strip().strip("'\"") or "unknown"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def report() -> dict[str, object]:
|
|
76
|
+
fields = read_agent_fields()
|
|
77
|
+
return {
|
|
78
|
+
"nick": fields["nick"],
|
|
79
|
+
"version": __version__,
|
|
80
|
+
"backend": fields["backend"],
|
|
81
|
+
"model": fields["model"],
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def cmd_whoami(args: argparse.Namespace) -> None:
|
|
86
|
+
identity = report()
|
|
87
|
+
json_mode = bool(getattr(args, "json", False))
|
|
88
|
+
if json_mode:
|
|
89
|
+
emit_result(identity, json_mode=True)
|
|
90
|
+
return
|
|
91
|
+
text = (
|
|
92
|
+
f"nick: {identity['nick']}\n"
|
|
93
|
+
f"version: {identity['version']}\n"
|
|
94
|
+
f"backend: {identity['backend']}\n"
|
|
95
|
+
f"model: {identity['model']}"
|
|
96
|
+
)
|
|
97
|
+
emit_result(text, json_mode=False)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
101
|
+
p = sub.add_parser(
|
|
102
|
+
"whoami",
|
|
103
|
+
help="Report this agent's nick, version, backend, and served model.",
|
|
104
|
+
)
|
|
105
|
+
p.add_argument("--json", action="store_true", help="Emit structured JSON.")
|
|
106
|
+
p.set_defaults(func=cmd_whoami)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""CliError and exit-code policy (stable-contract).
|
|
2
|
+
|
|
3
|
+
Every failure inside convertible raises :class:`CliError`. The
|
|
4
|
+
top-level ``main()`` catches it, formats via :mod:`convertible.cli._output`,
|
|
5
|
+
and exits with :attr:`CliError.code`. This guarantees:
|
|
6
|
+
|
|
7
|
+
* no Python traceback leaks to stderr (the agent-first error contract);
|
|
8
|
+
* every error has a structured shape ``{code, message, remediation}``;
|
|
9
|
+
* the exit-code policy is centralised in one place.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
# Exit-code policy. Documented in ``convertible learn`` output.
|
|
17
|
+
# 0 = success
|
|
18
|
+
# 1 = user-input error (bad flag, missing required arg, unknown path)
|
|
19
|
+
# 2 = environment / setup error (tool not installed, file unreadable)
|
|
20
|
+
# 3+ = reserved for future categorisation
|
|
21
|
+
EXIT_SUCCESS = 0
|
|
22
|
+
EXIT_USER_ERROR = 1
|
|
23
|
+
EXIT_ENV_ERROR = 2
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class CliError(Exception):
|
|
28
|
+
"""Structured error raised within the CLI; carries a remediation hint for agents."""
|
|
29
|
+
|
|
30
|
+
code: int
|
|
31
|
+
message: str
|
|
32
|
+
remediation: str = ""
|
|
33
|
+
|
|
34
|
+
def __post_init__(self) -> None:
|
|
35
|
+
super().__init__(self.message)
|
|
36
|
+
|
|
37
|
+
def to_dict(self) -> dict[str, object]:
|
|
38
|
+
return {
|
|
39
|
+
"code": self.code,
|
|
40
|
+
"message": self.message,
|
|
41
|
+
"remediation": self.remediation,
|
|
42
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""stdout / stderr helpers with a strict split (stable-contract).
|
|
2
|
+
|
|
3
|
+
Rule: **results go to stdout, diagnostics and errors go to stderr.** Agents
|
|
4
|
+
parsing output can rely on this invariant. JSON mode routes structured
|
|
5
|
+
payloads to the same streams — never mixes them.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
from typing import Any, TextIO
|
|
13
|
+
|
|
14
|
+
from convertible.cli._errors import CliError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def emit_result(data: Any, *, json_mode: bool, stream: TextIO | None = None) -> None:
|
|
18
|
+
"""Write a command result to stdout (or ``stream``)."""
|
|
19
|
+
s = stream if stream is not None else sys.stdout
|
|
20
|
+
if json_mode:
|
|
21
|
+
json.dump(data, s, ensure_ascii=False)
|
|
22
|
+
s.write("\n")
|
|
23
|
+
return
|
|
24
|
+
text = data if isinstance(data, str) else str(data)
|
|
25
|
+
s.write(text)
|
|
26
|
+
if not text.endswith("\n"):
|
|
27
|
+
s.write("\n")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def emit_error(err: CliError, *, json_mode: bool, stream: TextIO | None = None) -> None:
|
|
31
|
+
"""Write a :class:`CliError` to stderr.
|
|
32
|
+
|
|
33
|
+
Text mode renders as two lines when a remediation is present::
|
|
34
|
+
|
|
35
|
+
error: <message>
|
|
36
|
+
hint: <remediation>
|
|
37
|
+
|
|
38
|
+
The ``hint:`` prefix is required by the agent-first error rubric.
|
|
39
|
+
"""
|
|
40
|
+
s = stream if stream is not None else sys.stderr
|
|
41
|
+
if json_mode:
|
|
42
|
+
json.dump(err.to_dict(), s, ensure_ascii=False)
|
|
43
|
+
s.write("\n")
|
|
44
|
+
return
|
|
45
|
+
s.write(f"error: {err.message}\n")
|
|
46
|
+
if err.remediation:
|
|
47
|
+
s.write(f"hint: {err.remediation}\n")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def emit_diagnostic(message: str, *, stream: TextIO | None = None) -> None:
|
|
51
|
+
"""Write a human diagnostic (progress, summary) to stderr."""
|
|
52
|
+
s = stream if stream is not None else sys.stderr
|
|
53
|
+
s.write(message if message.endswith("\n") else message + "\n")
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Explain catalog — markdown keyed by command-path tuples (stable-contract).
|
|
2
|
+
|
|
3
|
+
Every noun/verb registered in the CLI should have a catalog entry.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from convertible.cli._errors import EXIT_USER_ERROR, CliError
|
|
9
|
+
from convertible.explain.catalog import ENTRIES
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def resolve(path: tuple[str, ...]) -> str:
|
|
13
|
+
if path in ENTRIES:
|
|
14
|
+
return ENTRIES[path]
|
|
15
|
+
display = " ".join(path) if path else "<root>"
|
|
16
|
+
raise CliError(
|
|
17
|
+
code=EXIT_USER_ERROR,
|
|
18
|
+
message=f"no explain entry for: {display}",
|
|
19
|
+
remediation="list entries with: convertible explain convertible",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def known_paths() -> list[tuple[str, ...]]:
|
|
24
|
+
return list(ENTRIES.keys())
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Markdown catalog for ``convertible explain <path>``.
|
|
2
|
+
|
|
3
|
+
Each entry is verbatim markdown. Keys are command-path tuples. The empty tuple
|
|
4
|
+
and ``("convertible",)`` both resolve to the root entry.
|
|
5
|
+
|
|
6
|
+
Keep bodies self-contained: an agent reading one entry should get enough
|
|
7
|
+
context without chaining reads.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
_ROOT = """\
|
|
13
|
+
# convertible
|
|
14
|
+
|
|
15
|
+
A clonable template for AgentCulture mesh agents. It carries an agent-first CLI
|
|
16
|
+
(cited from the teken `python-cli` reference), a mesh identity (`culture.yaml` +
|
|
17
|
+
`CLAUDE.md`), the canonical guildmaster skill kit under `.claude/skills/`, and a
|
|
18
|
+
buildable/deployable package baseline. Clone it, rename the package, edit
|
|
19
|
+
`culture.yaml`, and you have a new agent.
|
|
20
|
+
|
|
21
|
+
## Verbs
|
|
22
|
+
|
|
23
|
+
- `convertible whoami` — identity probe from `culture.yaml`.
|
|
24
|
+
- `convertible learn` — structured self-teaching prompt.
|
|
25
|
+
- `convertible explain <path>` — markdown docs for any noun/verb.
|
|
26
|
+
- `convertible overview` — descriptive snapshot of the agent.
|
|
27
|
+
- `convertible doctor` — check the agent-identity invariants.
|
|
28
|
+
- `convertible cli overview` — describe the CLI surface.
|
|
29
|
+
|
|
30
|
+
## Exit-code policy
|
|
31
|
+
|
|
32
|
+
- `0` success
|
|
33
|
+
- `1` user-input error
|
|
34
|
+
- `2` environment / setup error
|
|
35
|
+
- `3+` reserved
|
|
36
|
+
|
|
37
|
+
## See also
|
|
38
|
+
|
|
39
|
+
- `convertible explain whoami`
|
|
40
|
+
- `convertible explain doctor`
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
_WHOAMI = """\
|
|
44
|
+
# convertible whoami
|
|
45
|
+
|
|
46
|
+
Reports the agent's identity from `culture.yaml`: nick (`suffix`), backend,
|
|
47
|
+
served model, and the package version. Read-only.
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
convertible whoami
|
|
52
|
+
convertible whoami --json
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
_LEARN = """\
|
|
56
|
+
# convertible learn
|
|
57
|
+
|
|
58
|
+
Prints a structured self-teaching prompt covering purpose, command map,
|
|
59
|
+
exit-code policy, `--json` support, and the `explain` pointer.
|
|
60
|
+
|
|
61
|
+
## Usage
|
|
62
|
+
|
|
63
|
+
convertible learn
|
|
64
|
+
convertible learn --json
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
_EXPLAIN = """\
|
|
68
|
+
# convertible explain <path>
|
|
69
|
+
|
|
70
|
+
Prints markdown documentation for any noun/verb path. Unlike `--help` (terse,
|
|
71
|
+
positional), `explain` is global and addressable by path.
|
|
72
|
+
|
|
73
|
+
## Usage
|
|
74
|
+
|
|
75
|
+
convertible explain convertible
|
|
76
|
+
convertible explain whoami
|
|
77
|
+
convertible explain --json <path>
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
_OVERVIEW = """\
|
|
81
|
+
# convertible overview
|
|
82
|
+
|
|
83
|
+
Read-only descriptive snapshot of the agent: identity (from `culture.yaml`), the
|
|
84
|
+
verb surface, and the sibling-pattern artifacts the template carries. Accepts an
|
|
85
|
+
ignored `target` so a stray path never hard-fails.
|
|
86
|
+
|
|
87
|
+
## Usage
|
|
88
|
+
|
|
89
|
+
convertible overview
|
|
90
|
+
convertible overview --json
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
_DOCTOR = """\
|
|
94
|
+
# convertible doctor
|
|
95
|
+
|
|
96
|
+
Checks the agent-identity invariants `steward doctor` verifies:
|
|
97
|
+
prompt-file-present and backend-consistency (`claude` → `CLAUDE.md`), plus a
|
|
98
|
+
skills-present check. Exits 1 when unhealthy.
|
|
99
|
+
|
|
100
|
+
## Usage
|
|
101
|
+
|
|
102
|
+
convertible doctor
|
|
103
|
+
convertible doctor --json
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
_CLI = """\
|
|
107
|
+
# convertible cli
|
|
108
|
+
|
|
109
|
+
Noun group for CLI-surface introspection. `cli overview` describes the CLI
|
|
110
|
+
itself (distinct from the global `overview`, which describes the agent).
|
|
111
|
+
|
|
112
|
+
## Usage
|
|
113
|
+
|
|
114
|
+
convertible cli overview
|
|
115
|
+
convertible cli overview --json
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
ENTRIES: dict[tuple[str, ...], str] = {
|
|
120
|
+
(): _ROOT,
|
|
121
|
+
("convertible",): _ROOT,
|
|
122
|
+
("whoami",): _WHOAMI,
|
|
123
|
+
("learn",): _LEARN,
|
|
124
|
+
("explain",): _EXPLAIN,
|
|
125
|
+
("overview",): _OVERVIEW,
|
|
126
|
+
("doctor",): _DOCTOR,
|
|
127
|
+
("cli",): _CLI,
|
|
128
|
+
("cli", "overview"): _CLI,
|
|
129
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: convertible-cli
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Convertible CLI is a swappable coder-agent harness that turns different models into repo workers behind one shared task contract.
|
|
5
|
+
Project-URL: Homepage, https://github.com/agentculture/convertible
|
|
6
|
+
Project-URL: Issues, https://github.com/agentculture/convertible/issues
|
|
7
|
+
Author: AgentCulture
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Topic :: Software Development
|
|
15
|
+
Requires-Python: >=3.12
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# convertible
|
|
19
|
+
|
|
20
|
+
Convertible CLI is a swappable coder-agent harness that turns different models into repo workers behind one shared task contract.
|
|
21
|
+
|
|
22
|
+
## What you get
|
|
23
|
+
|
|
24
|
+
- **An agent-first CLI** cited from [teken](https://github.com/agentculture/teken)
|
|
25
|
+
(`afi-cli`) — the runtime package has no third-party dependencies.
|
|
26
|
+
- **A mesh identity** — `culture.yaml` (`suffix` + `backend`) and the matching
|
|
27
|
+
prompt file (`CLAUDE.md` for `backend: claude`).
|
|
28
|
+
- **The canonical guildmaster skill kit** (11 skills) under `.claude/skills/`,
|
|
29
|
+
vendored cite-don't-import. See [`docs/skill-sources.md`](docs/skill-sources.md).
|
|
30
|
+
- **A build + deploy baseline** — pytest, lint, the agent-first rubric gate, and
|
|
31
|
+
PyPI Trusted Publishing wired into GitHub Actions.
|
|
32
|
+
|
|
33
|
+
## Quickstart
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
uv sync
|
|
37
|
+
uv run pytest -n auto # run the test suite
|
|
38
|
+
uv run convertible whoami # identity from culture.yaml
|
|
39
|
+
uv run convertible learn # self-teaching prompt (add --json)
|
|
40
|
+
uv run teken cli doctor . --strict # the agent-first rubric gate CI runs
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## CLI
|
|
44
|
+
|
|
45
|
+
| Verb | What it does |
|
|
46
|
+
|------|--------------|
|
|
47
|
+
| `whoami` | Report this agent's nick, version, backend, and model from `culture.yaml`. |
|
|
48
|
+
| `learn` | Print a structured self-teaching prompt. |
|
|
49
|
+
| `explain <path>` | Markdown docs for any noun/verb path. |
|
|
50
|
+
| `overview` | Read-only descriptive snapshot of the agent. |
|
|
51
|
+
| `doctor` | Check the agent-identity invariants (prompt-file-present, backend-consistency). |
|
|
52
|
+
| `cli overview` | Describe the CLI surface itself. |
|
|
53
|
+
|
|
54
|
+
Every command supports `--json`. Results go to stdout, errors/diagnostics to
|
|
55
|
+
stderr (never mixed). Exit codes: `0` success, `1` user error, `2` environment
|
|
56
|
+
error, `3+` reserved.
|
|
57
|
+
|
|
58
|
+
## Make it your own
|
|
59
|
+
|
|
60
|
+
1. Rename the package `convertible/` and the `convertible`
|
|
61
|
+
CLI/dist name throughout `pyproject.toml`, the package, `tests/`, and
|
|
62
|
+
`sonar-project.properties`.
|
|
63
|
+
2. Edit `culture.yaml` with your `suffix` and `backend`.
|
|
64
|
+
3. Rewrite `CLAUDE.md` for your agent and run `/init`.
|
|
65
|
+
4. Re-vendor only the skills you need from guildmaster (see
|
|
66
|
+
[`docs/skill-sources.md`](docs/skill-sources.md)).
|
|
67
|
+
|
|
68
|
+
See [`CLAUDE.md`](CLAUDE.md) for the full conventions (version-bump-every-PR,
|
|
69
|
+
the `cicd` PR lane, deploy setup).
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
MIT — see [`LICENSE`](LICENSE).
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
convertible/__init__.py,sha256=JYtnSOfHnF_Mi1-X2zf4UrFykjHuvZFT-0zkt6NEB4A,408
|
|
2
|
+
convertible/__main__.py,sha256=WHuLyDe2jxViy3egr8ULsKn1mbEEah8vhjyy4OiHcxQ,180
|
|
3
|
+
convertible/cli/__init__.py,sha256=WAqekKV1Ynt-Qaw0RcDDNnjh0CiKZtwTPvwzEKeGN6A,4999
|
|
4
|
+
convertible/cli/_errors.py,sha256=U-yoWAPzhcC189gA2d88-4Lit7b_hHeoPYQwc_NpNgE,1304
|
|
5
|
+
convertible/cli/_output.py,sha256=p-SAa8yucaWtid13Yb6QUDrQeTn3ZboFyeOY-cpX5LU,1710
|
|
6
|
+
convertible/cli/_commands/__init__.py,sha256=DqpfVkhBo7cj6eWOpSlwx1ag0nTbrYJkV8pnPgl2gyU,70
|
|
7
|
+
convertible/cli/_commands/cli.py,sha256=9N7uRfJkA9UMiPtsg4MRbsEE-SdnaTIItjT9hV_mk8s,1683
|
|
8
|
+
convertible/cli/_commands/doctor.py,sha256=ZT-Gml3X0en3pQjpW36KM1cprCcpG2GsL4BM7oloE3I,4399
|
|
9
|
+
convertible/cli/_commands/explain.py,sha256=IlXlP94TT92Ne8nPqll2WIaPcKFTsikAslfNF8jYH4M,1232
|
|
10
|
+
convertible/cli/_commands/learn.py,sha256=tcAh2oUYTr2m2a4L8oALgLgFCC-joNHX56ocHGf1Qyg,3013
|
|
11
|
+
convertible/cli/_commands/overview.py,sha256=6oN1PLHsGkdaRAaoNg69gl8AJ1Wq7WLIAZzopVoMRVA,3956
|
|
12
|
+
convertible/cli/_commands/whoami.py,sha256=je7WAjb4SzBH6DdGMP5DzUvANFsr-0WhiK4ARUaFdsk,3654
|
|
13
|
+
convertible/explain/__init__.py,sha256=YPZcsSN_YPXzAUbPajnqVawEU2sdt5Q0oyfi0ZHj7H4,708
|
|
14
|
+
convertible/explain/catalog.py,sha256=evKCfcLxbUVACBKpwitNlqLXHArbZFX1OutPwU8Thio,3228
|
|
15
|
+
convertible_cli-0.1.2.dist-info/METADATA,sha256=syXr0mAFesi8UpBO5ytXNpPxS4HREv2BhuVLuCFcYwU,2964
|
|
16
|
+
convertible_cli-0.1.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
17
|
+
convertible_cli-0.1.2.dist-info/entry_points.txt,sha256=ZZ5pg_AcVNlbDlKrT5oe9J8UqSn_YLaj7sXSKb1wlts,53
|
|
18
|
+
convertible_cli-0.1.2.dist-info/licenses/LICENSE,sha256=wCcdPywGtFXx1P8N0j0eEDINSWfSjrIsU7ds1YZl-MA,1069
|
|
19
|
+
convertible_cli-0.1.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AgentCulture
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|