rollout-cli 0.2.1__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.
- rollout/__init__.py +13 -0
- rollout/__main__.py +10 -0
- rollout/cli/__init__.py +136 -0
- rollout/cli/_commands/__init__.py +1 -0
- rollout/cli/_commands/cli.py +43 -0
- rollout/cli/_commands/doctor.py +122 -0
- rollout/cli/_commands/explain.py +38 -0
- rollout/cli/_commands/learn.py +88 -0
- rollout/cli/_commands/overview.py +112 -0
- rollout/cli/_commands/whoami.py +106 -0
- rollout/cli/_errors.py +42 -0
- rollout/cli/_output.py +53 -0
- rollout/explain/__init__.py +24 -0
- rollout/explain/catalog.py +130 -0
- rollout_cli-0.2.1.dist-info/METADATA +75 -0
- rollout_cli-0.2.1.dist-info/RECORD +19 -0
- rollout_cli-0.2.1.dist-info/WHEEL +4 -0
- rollout_cli-0.2.1.dist-info/entry_points.txt +2 -0
- rollout_cli-0.2.1.dist-info/licenses/LICENSE +201 -0
rollout/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""rollout-cli — 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("rollout-cli")
|
|
10
|
+
except PackageNotFoundError: # pragma: no cover - editable install without metadata
|
|
11
|
+
__version__ = "0.0.0"
|
|
12
|
+
|
|
13
|
+
__all__ = ["__version__"]
|
rollout/__main__.py
ADDED
rollout/cli/__init__.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Unified CLI entry point for rollout-cli.
|
|
2
|
+
|
|
3
|
+
The agent-first global verbs (``whoami``, ``learn``, ``explain``, ``overview``,
|
|
4
|
+
``doctor``) are registered here under :mod:`rollout.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:`rollout.cli._errors.CliError` on
|
|
11
|
+
failure; ``main()`` catches it via :func:`_dispatch` and routes through
|
|
12
|
+
:mod:`rollout.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 rollout import __version__
|
|
28
|
+
from rollout.cli._errors import EXIT_USER_ERROR, CliError
|
|
29
|
+
from rollout.cli._output import emit_error
|
|
30
|
+
|
|
31
|
+
_ISSUES_URL = "https://github.com/agentculture/rollout-cli/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 rollout.cli._commands import cli as _cli_group
|
|
66
|
+
from rollout.cli._commands import doctor as _doctor_cmd
|
|
67
|
+
from rollout.cli._commands import explain as _explain_cmd
|
|
68
|
+
from rollout.cli._commands import learn as _learn_cmd
|
|
69
|
+
from rollout.cli._commands import overview as _overview_cmd
|
|
70
|
+
from rollout.cli._commands import whoami as _whoami_cmd
|
|
71
|
+
|
|
72
|
+
parser = _CliArgumentParser(
|
|
73
|
+
prog="rollout-cli",
|
|
74
|
+
description="rollout-cli — 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 rollout.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
|
+
"""``rollout-cli 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 rollout.cli._commands.overview import cli_sections, emit_overview
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def cmd_cli_overview(args: argparse.Namespace) -> int:
|
|
17
|
+
emit_overview(
|
|
18
|
+
"rollout-cli 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
|
+
# `rollout-cli 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 'rollout-cli 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 rollout-cli 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
|
+
"""``rollout-cli 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 rollout.cli._commands.whoami import find_culture_yaml, read_agent_fields
|
|
24
|
+
from rollout.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"rollout-cli 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
|
+
"""``rollout-cli 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:`rollout.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 rollout.cli._output import emit_result
|
|
13
|
+
from rollout.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 'rollout-cli').",
|
|
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
|
+
"""``rollout-cli 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 rollout import __version__
|
|
12
|
+
from rollout.cli._output import emit_result
|
|
13
|
+
|
|
14
|
+
_TEXT = """\
|
|
15
|
+
rollout-cli — 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
|
+
rollout-cli whoami Identity from culture.yaml.
|
|
27
|
+
rollout-cli learn This self-teaching prompt.
|
|
28
|
+
rollout-cli explain <path>... Markdown docs for any noun/verb path.
|
|
29
|
+
rollout-cli overview Descriptive snapshot of the agent.
|
|
30
|
+
rollout-cli doctor Check the agent-identity invariants.
|
|
31
|
+
rollout-cli 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
|
+
rollout-cli explain rollout-cli
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _as_json_payload() -> dict[str, object]:
|
|
52
|
+
return {
|
|
53
|
+
"tool": "rollout-cli",
|
|
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": "rollout-cli 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
|
+
"""``rollout-cli 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:`rollout.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 rollout.cli._commands.whoami import report
|
|
18
|
+
from rollout.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
|
+
"rollout-cli",
|
|
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
|
+
"""``rollout-cli 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 rollout import __version__
|
|
17
|
+
from rollout.cli._output import emit_result
|
|
18
|
+
|
|
19
|
+
_FALLBACK_NICK = "rollout-cli"
|
|
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)
|
rollout/cli/_errors.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""CliError and exit-code policy (stable-contract).
|
|
2
|
+
|
|
3
|
+
Every failure inside rollout-cli raises :class:`CliError`. The
|
|
4
|
+
top-level ``main()`` catches it, formats via :mod:`rollout.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 ``rollout-cli 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
|
+
}
|
rollout/cli/_output.py
ADDED
|
@@ -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 rollout.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 rollout.cli._errors import EXIT_USER_ERROR, CliError
|
|
9
|
+
from rollout.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: rollout-cli explain rollout-cli",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def known_paths() -> list[tuple[str, ...]]:
|
|
24
|
+
return list(ENTRIES.keys())
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Markdown catalog for ``rollout-cli explain <path>``.
|
|
2
|
+
|
|
3
|
+
Each entry is verbatim markdown. Keys are command-path tuples. The empty tuple
|
|
4
|
+
and ``("rollout-cli",)`` 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
|
+
# rollout-cli
|
|
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
|
+
- `rollout-cli whoami` — identity probe from `culture.yaml`.
|
|
24
|
+
- `rollout-cli learn` — structured self-teaching prompt.
|
|
25
|
+
- `rollout-cli explain <path>` — markdown docs for any noun/verb.
|
|
26
|
+
- `rollout-cli overview` — descriptive snapshot of the agent.
|
|
27
|
+
- `rollout-cli doctor` — check the agent-identity invariants.
|
|
28
|
+
- `rollout-cli 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
|
+
- `rollout-cli explain whoami`
|
|
40
|
+
- `rollout-cli explain doctor`
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
_WHOAMI = """\
|
|
44
|
+
# rollout-cli 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
|
+
rollout-cli whoami
|
|
52
|
+
rollout-cli whoami --json
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
_LEARN = """\
|
|
56
|
+
# rollout-cli 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
|
+
rollout-cli learn
|
|
64
|
+
rollout-cli learn --json
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
_EXPLAIN = """\
|
|
68
|
+
# rollout-cli 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
|
+
rollout-cli explain rollout-cli
|
|
76
|
+
rollout-cli explain whoami
|
|
77
|
+
rollout-cli explain --json <path>
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
_OVERVIEW = """\
|
|
81
|
+
# rollout-cli 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
|
+
rollout-cli overview
|
|
90
|
+
rollout-cli overview --json
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
_DOCTOR = """\
|
|
94
|
+
# rollout-cli 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
|
+
rollout-cli doctor
|
|
103
|
+
rollout-cli doctor --json
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
_CLI = """\
|
|
107
|
+
# rollout-cli 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
|
+
rollout-cli cli overview
|
|
115
|
+
rollout-cli cli overview --json
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
ENTRIES: dict[tuple[str, ...], str] = {
|
|
120
|
+
(): _ROOT,
|
|
121
|
+
("rollout-cli",): _ROOT,
|
|
122
|
+
("rollout",): _ROOT,
|
|
123
|
+
("whoami",): _WHOAMI,
|
|
124
|
+
("learn",): _LEARN,
|
|
125
|
+
("explain",): _EXPLAIN,
|
|
126
|
+
("overview",): _OVERVIEW,
|
|
127
|
+
("doctor",): _DOCTOR,
|
|
128
|
+
("cli",): _CLI,
|
|
129
|
+
("cli", "overview"): _CLI,
|
|
130
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rollout-cli
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Cross-repo propagation workflow built on refactor-cli — apply a transformation across many repos at once.
|
|
5
|
+
Project-URL: Homepage, https://github.com/agentculture/rollout-cli
|
|
6
|
+
Project-URL: Issues, https://github.com/agentculture/rollout-cli/issues
|
|
7
|
+
Author: Ori Nachum
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software 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
|
+
# rollout-cli
|
|
19
|
+
|
|
20
|
+
Cross-repo propagation workflow built on refactor-cli — apply a transformation across many repos at once.
|
|
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 rollout-cli whoami # identity from culture.yaml
|
|
39
|
+
uv run rollout-cli 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 `rollout/` and the `rollout-cli`
|
|
61
|
+
CLI/dist name throughout `pyproject.toml`, the package, `tests/`,
|
|
62
|
+
`sonar-project.properties`, and this `README.md`. The name is hard-coded in
|
|
63
|
+
~100 places, so list every occurrence first — see the `git grep` discovery
|
|
64
|
+
command in [`CLAUDE.md`](CLAUDE.md), the authoritative rename procedure.
|
|
65
|
+
2. Edit `culture.yaml` with your `suffix` and `backend`.
|
|
66
|
+
3. Rewrite `CLAUDE.md` for your agent and run `/init`.
|
|
67
|
+
4. Re-vendor only the skills you need from guildmaster (see
|
|
68
|
+
[`docs/skill-sources.md`](docs/skill-sources.md)).
|
|
69
|
+
|
|
70
|
+
See [`CLAUDE.md`](CLAUDE.md) for the full conventions (version-bump-every-PR,
|
|
71
|
+
the `cicd` PR lane, deploy setup).
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
Apache 2.0 — see [`LICENSE`](LICENSE).
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
rollout/__init__.py,sha256=xgecakTKsponNsY3Ev9rQsqTO_vC4hpVsmEKzLtxid8,404
|
|
2
|
+
rollout/__main__.py,sha256=ga2-VOptxJwLATptP6xp4B-xu3XjxBBJzYnWrZifrKM,172
|
|
3
|
+
rollout/cli/__init__.py,sha256=A8d0iLBQ2QGlTvYZufZDrDC8QkYWi1u8xqS8AAU9qLY,4947
|
|
4
|
+
rollout/cli/_errors.py,sha256=nfK0tFzJH_TsQacUIpw2EwV5l8LvT_yyNcya_xzfYqM,1300
|
|
5
|
+
rollout/cli/_output.py,sha256=PwyEiTqw7ztILjf765xPKY9kjS3LX_s0LLEHXZQbkUo,1706
|
|
6
|
+
rollout/cli/_commands/__init__.py,sha256=DqpfVkhBo7cj6eWOpSlwx1ag0nTbrYJkV8pnPgl2gyU,70
|
|
7
|
+
rollout/cli/_commands/cli.py,sha256=mEnyTvNXTWgbyacBQdBItcj-t3Z4edMwWsk4SXlVIFs,1679
|
|
8
|
+
rollout/cli/_commands/doctor.py,sha256=K92sKPeaTYIEWRXhLG6aacwqiQNclausnHL4hluY-c4,4391
|
|
9
|
+
rollout/cli/_commands/explain.py,sha256=DvzVJqM4VCP5dOI6uwWyTC5EXMopQMJxjeZWQTuP8S8,1220
|
|
10
|
+
rollout/cli/_commands/learn.py,sha256=x8o6iFvMM4AnVL-i6CMp7jIvwgnLong6XCJFDhSMqRA,3005
|
|
11
|
+
rollout/cli/_commands/overview.py,sha256=TFl2FOq25JYHKr76yaLPt93jIHjz2PokRXa5_VbSkAs,3944
|
|
12
|
+
rollout/cli/_commands/whoami.py,sha256=O2obpskMqzf-UT-9dsNMfIHZ71geOC3AiR59hJieVIs,3646
|
|
13
|
+
rollout/explain/__init__.py,sha256=kNBYT1v5baaJ2oOYUAXbR65kLpMEY30-FzAIJsOqU_0,700
|
|
14
|
+
rollout/explain/catalog.py,sha256=o2LeEc_CSNFIBW6AlGfMUGOWmLb6ZX6WBGBl2iV_S5U,3253
|
|
15
|
+
rollout_cli-0.2.1.dist-info/METADATA,sha256=vWx_i9lcyswrkhIhEDTGcMQT6iTt8B8JA7UqNI04wOw,3136
|
|
16
|
+
rollout_cli-0.2.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
17
|
+
rollout_cli-0.2.1.dist-info/entry_points.txt,sha256=cpZMUZM5oPj_P_iC2sjgIE7-m3-wuwHmWAc6vGv2Pi0,45
|
|
18
|
+
rollout_cli-0.2.1.dist-info/licenses/LICENSE,sha256=UTsio4-AMxNDoMcES3iMDOVbdrnTy1rQD7Be7zWjqcA,11340
|
|
19
|
+
rollout_cli-0.2.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright 2026 Ori Nachum
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|