steward-cli 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
steward/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """steward — aligns and maintains resident agents across Culture projects."""
2
+
3
+ from importlib.metadata import PackageNotFoundError
4
+ from importlib.metadata import version as _v
5
+
6
+ try:
7
+ __version__ = _v("steward-cli")
8
+ except PackageNotFoundError:
9
+ __version__ = "0.0.0+local"
10
+
11
+ __all__ = ["__version__"]
steward/__main__.py ADDED
@@ -0,0 +1,8 @@
1
+ """Allow running steward as ``python -m steward``."""
2
+
3
+ import sys
4
+
5
+ from steward.cli import main
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(main())
@@ -0,0 +1,82 @@
1
+ """Unified CLI entry point for steward.
2
+
3
+ Every handler raises :class:`steward.cli._errors.StewardError` on failure;
4
+ ``main()`` catches it via :func:`_dispatch` and routes through
5
+ :mod:`steward.cli._output`. Argparse errors route through
6
+ ``_StewardArgumentParser`` so they share the same structured output.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import sys
13
+
14
+ from steward import __version__
15
+ from steward.cli._errors import EXIT_USER_ERROR, StewardError
16
+ from steward.cli._output import emit_error
17
+
18
+
19
+ class _StewardArgumentParser(argparse.ArgumentParser):
20
+ """ArgumentParser that routes errors through :func:`emit_error`."""
21
+
22
+ def error(self, message: str) -> None: # type: ignore[override]
23
+ err = StewardError(
24
+ code=EXIT_USER_ERROR,
25
+ message=message,
26
+ remediation=f"run '{self.prog} --help' to see valid arguments",
27
+ )
28
+ emit_error(err)
29
+ raise SystemExit(err.code)
30
+
31
+
32
+ def _build_parser() -> argparse.ArgumentParser:
33
+ # Deferred import to avoid coupling the parser module to the command modules
34
+ # at import time (matches afi-cli's pattern; cheap insurance).
35
+ from steward.cli._commands import show as _show_cmd
36
+
37
+ parser = _StewardArgumentParser(
38
+ prog="steward",
39
+ description="steward — align and maintain resident agents across Culture projects",
40
+ )
41
+ parser.add_argument(
42
+ "--version",
43
+ action="version",
44
+ version=f"%(prog)s {__version__}",
45
+ )
46
+ sub = parser.add_subparsers(dest="command", parser_class=_StewardArgumentParser)
47
+
48
+ _show_cmd.register(sub)
49
+
50
+ return parser
51
+
52
+
53
+ def _dispatch(args: argparse.Namespace) -> int:
54
+ try:
55
+ rc = args.func(args)
56
+ except StewardError as err:
57
+ emit_error(err)
58
+ return err.code
59
+ except Exception as err: # noqa: BLE001 - last-resort: wrap so no traceback leaks
60
+ wrapped = StewardError(
61
+ code=EXIT_USER_ERROR,
62
+ message=f"unexpected: {err.__class__.__name__}: {err}",
63
+ remediation="file a bug at https://github.com/agentculture/steward/issues",
64
+ )
65
+ emit_error(wrapped)
66
+ return wrapped.code
67
+ return rc if rc is not None else 0
68
+
69
+
70
+ def main(argv: list[str] | None = None) -> int:
71
+ parser = _build_parser()
72
+ args = parser.parse_args(argv)
73
+
74
+ if args.command is None:
75
+ parser.print_help()
76
+ return 0
77
+
78
+ return _dispatch(args)
79
+
80
+
81
+ if __name__ == "__main__":
82
+ sys.exit(main())
File without changes
@@ -0,0 +1,116 @@
1
+ """``steward show`` — wraps the agent-config skill's show.sh.
2
+
3
+ The skill (``.claude/skills/agent-config/scripts/show.sh``) is the canonical
4
+ implementation. The CLI is just a typed surface so people can run
5
+ ``steward show ../culture`` instead of remembering the script path.
6
+
7
+ If the skill script is missing (e.g. someone ``pip install``ed steward-cli
8
+ without cloning the repo), the command exits with a clear error pointing at
9
+ where the skill should live.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import subprocess
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ from steward.cli._errors import EXIT_ENV_ERROR, EXIT_USER_ERROR, StewardError
20
+
21
+
22
+ def _find_git_root(start: Path) -> Path | None:
23
+ """Return the nearest enclosing directory containing ``.git`` (or None)."""
24
+ for directory in (start, *start.parents):
25
+ if (directory / ".git").exists():
26
+ return directory
27
+ return None
28
+
29
+
30
+ def _resolve_skill_script() -> Path:
31
+ """Locate ``.claude/skills/agent-config/scripts/show.sh`` inside the current
32
+ git repo.
33
+
34
+ Walks up from cwd, but **stops at the git repository boundary** so
35
+ ``steward show`` never executes a script from an ancestor directory
36
+ outside the user's current checkout. If cwd isn't inside any git repo,
37
+ only cwd itself is checked. This eliminates the "search-path injection"
38
+ risk where an attacker-placed ancestor directory could supply the script.
39
+ """
40
+ start = Path.cwd().resolve()
41
+ repo_root = _find_git_root(start)
42
+
43
+ current = start
44
+ while True:
45
+ candidate = current / ".claude" / "skills" / "agent-config" / "scripts" / "show.sh"
46
+ if candidate.is_file():
47
+ return candidate
48
+ if current == repo_root or current.parent == current:
49
+ # Hit the git boundary or the filesystem root — stop walking.
50
+ break
51
+ if repo_root is None:
52
+ # Not inside a git repo: only inspect cwd itself, never ancestors.
53
+ break
54
+ current = current.parent
55
+
56
+ raise StewardError(
57
+ code=EXIT_ENV_ERROR,
58
+ message="agent-config skill script not found",
59
+ remediation=(
60
+ "run from inside a Steward git checkout that contains "
61
+ ".claude/skills/agent-config/scripts/show.sh"
62
+ ),
63
+ )
64
+
65
+
66
+ def register(sub: argparse._SubParsersAction) -> None:
67
+ parser = sub.add_parser(
68
+ "show",
69
+ help="Show a Culture agent's full configuration in one view.",
70
+ description=(
71
+ "Surface a Culture agent's CLAUDE.md, parallel culture.yaml, and "
72
+ ".claude/skills/ index. Wraps the agent-config skill script."
73
+ ),
74
+ )
75
+ parser.add_argument(
76
+ "target",
77
+ help="Path to a project directory or a registered agent suffix.",
78
+ )
79
+ parser.set_defaults(func=_handle)
80
+
81
+
82
+ def _handle(args: argparse.Namespace) -> int:
83
+ script = _resolve_skill_script()
84
+ # Capture and forward via Python streams so pytest's capsys/capfd both
85
+ # see the output. Going through sys.stdout/sys.stderr.write keeps the
86
+ # split (skill stdout → CLI stdout, skill stderr → CLI stderr).
87
+ #
88
+ # bandit S603: argv is a fixed list; the target is a positional string
89
+ # passed straight to the script (no shell, no expansion). Resolution
90
+ # of the script itself is constrained to the current git repo by
91
+ # _resolve_skill_script(), so an attacker can't substitute a different
92
+ # show.sh from an ancestor directory.
93
+ try:
94
+ completed = subprocess.run( # noqa: S603
95
+ [str(script), args.target],
96
+ check=False,
97
+ capture_output=True,
98
+ text=True,
99
+ )
100
+ except OSError as exc:
101
+ raise StewardError(
102
+ code=EXIT_ENV_ERROR,
103
+ message=f"could not execute {script}: {exc}",
104
+ remediation="ensure the script is executable (chmod +x)",
105
+ ) from exc
106
+ if completed.stdout:
107
+ sys.stdout.write(completed.stdout)
108
+ if completed.stderr:
109
+ sys.stderr.write(completed.stderr)
110
+ if completed.returncode != 0:
111
+ raise StewardError(
112
+ code=EXIT_USER_ERROR if completed.returncode == 2 else EXIT_ENV_ERROR,
113
+ message=f"agent-config script exited {completed.returncode}",
114
+ remediation=f"see stderr from {script.name}",
115
+ )
116
+ return 0
steward/cli/_errors.py ADDED
@@ -0,0 +1,37 @@
1
+ """StewardError and exit-code policy.
2
+
3
+ Every failure inside steward raises :class:`StewardError`. The top-level
4
+ ``main()`` catches it, formats via :mod:`steward.cli._output`, and exits with
5
+ :attr:`StewardError.code`. This guarantees:
6
+
7
+ * no Python traceback leaks to stderr;
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_SUCCESS = 0
17
+ EXIT_USER_ERROR = 1
18
+ EXIT_ENV_ERROR = 2
19
+
20
+
21
+ @dataclass
22
+ class StewardError(Exception):
23
+ """Structured error raised within steward; carries a remediation hint."""
24
+
25
+ code: int
26
+ message: str
27
+ remediation: str = ""
28
+
29
+ def __post_init__(self) -> None:
30
+ super().__init__(self.message)
31
+
32
+ def to_dict(self) -> dict[str, object]:
33
+ return {
34
+ "code": self.code,
35
+ "message": self.message,
36
+ "remediation": self.remediation,
37
+ }
steward/cli/_output.py ADDED
@@ -0,0 +1,41 @@
1
+ """stdout / stderr helpers with a strict split.
2
+
3
+ Rule: results go to stdout, diagnostics and errors go to stderr. Agents
4
+ parsing steward output can rely on this invariant.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import sys
10
+ from typing import Any, TextIO
11
+
12
+ from steward.cli._errors import StewardError
13
+
14
+
15
+ def emit_result(data: Any, *, stream: TextIO | None = None) -> None:
16
+ """Write a command result to stdout (default)."""
17
+ s = stream if stream is not None else sys.stdout
18
+ text = data if isinstance(data, str) else str(data)
19
+ s.write(text)
20
+ if not text.endswith("\n"):
21
+ s.write("\n")
22
+
23
+
24
+ def emit_error(err: StewardError, *, stream: TextIO | None = None) -> None:
25
+ """Write a :class:`StewardError` to stderr.
26
+
27
+ Renders as one or two lines::
28
+
29
+ error: <message>
30
+ hint: <remediation>
31
+ """
32
+ s = stream if stream is not None else sys.stderr
33
+ s.write(f"error: {err.message}\n")
34
+ if err.remediation:
35
+ s.write(f"hint: {err.remediation}\n")
36
+
37
+
38
+ def emit_diagnostic(message: str, *, stream: TextIO | None = None) -> None:
39
+ """Write a human diagnostic to stderr."""
40
+ s = stream if stream is not None else sys.stderr
41
+ s.write(message if message.endswith("\n") else message + "\n")
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.4
2
+ Name: steward-cli
3
+ Version: 0.1.2
4
+ Summary: Steward — aligns and maintains resident agents across Culture projects.
5
+ Project-URL: Homepage, https://github.com/agentculture/steward
6
+ Project-URL: Issues, https://github.com/agentculture/steward/issues
7
+ Author: AgentCulture
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Topic :: Software Development
15
+ Requires-Python: >=3.12
16
+ Description-Content-Type: text/markdown
17
+
18
+ # steward
19
+
20
+ Steward aligns and maintains resident agents across Culture projects.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pip install steward-cli
26
+ ```
27
+
28
+ Or, with [uv](https://github.com/astral-sh/uv):
29
+
30
+ ```bash
31
+ uv tool install steward-cli
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ```bash
37
+ steward --version
38
+ steward --help
39
+
40
+ # Show a Culture agent's full configuration in one view
41
+ # (CLAUDE.md + the parallel culture.yaml + .claude/skills/ index).
42
+ # Run from inside a Steward checkout — the command finds the agent-config
43
+ # skill script via a walk-up from the current working directory.
44
+ steward show ../culture
45
+ steward show ../daria
46
+ ```
47
+
48
+ `steward show` is a thin wrapper over the `agent-config` skill at
49
+ `.claude/skills/agent-config/scripts/show.sh`. The skill remains the canonical
50
+ implementation; the CLI is the typed entry point.
51
+
52
+ See [`CLAUDE.md`](CLAUDE.md) for project-shape, build/test/publish details, and
53
+ the skills convention.
54
+
55
+ ## License
56
+
57
+ MIT — see [`LICENSE`](LICENSE).
@@ -0,0 +1,12 @@
1
+ steward/__init__.py,sha256=Jji1ismBtgM9lXW3KygrlhthFuYHlo2o1G1yrEw0-ls,308
2
+ steward/__main__.py,sha256=MZEjt3hBuGmcoGEin37oQyBiS4XHVnmLZvuP1qgXH_g,145
3
+ steward/cli/__init__.py,sha256=61Q2haUoZN7T_aDCtzzIZOFtN1OelUMZDn2Pfx0zJzk,2466
4
+ steward/cli/_errors.py,sha256=3w024MCV-WuwkKU-p8GF_NBUfZXeqDHLbUZVgGtv8AU,971
5
+ steward/cli/_output.py,sha256=054l0nNt-uDUCwUL89NEWFpOdxd31JjRrphVGhZpm-Y,1236
6
+ steward/cli/_commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ steward/cli/_commands/show.py,sha256=TqLyq8Slh8zXgX9Kwvy832bU-09KpmNrtDtt-hfXhw0,4309
8
+ steward_cli-0.1.2.dist-info/METADATA,sha256=AKclOn8wTdCOGVWE0vueRMHEsnyZmwa9RHTfJdVYrzE,1567
9
+ steward_cli-0.1.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
10
+ steward_cli-0.1.2.dist-info/entry_points.txt,sha256=6mbfyrAed822bovEbJjnCowTdG_NeIB2LBAwgMf4PnQ,45
11
+ steward_cli-0.1.2.dist-info/licenses/LICENSE,sha256=d-o3g1Varo3ruZBbZUlkdoDVJBoIhutjarID0c0OIyU,1069
12
+ steward_cli-0.1.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ steward = steward.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 agentculture
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.