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 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
@@ -0,0 +1,10 @@
1
+ """Entry point for ``python -m rollout``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from rollout.cli import main
8
+
9
+ if __name__ == "__main__":
10
+ sys.exit(main())
@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ rollout = rollout.cli:main
@@ -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.