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 +11 -0
- steward/__main__.py +8 -0
- steward/cli/__init__.py +82 -0
- steward/cli/_commands/__init__.py +0 -0
- steward/cli/_commands/show.py +116 -0
- steward/cli/_errors.py +37 -0
- steward/cli/_output.py +41 -0
- steward_cli-0.1.2.dist-info/METADATA +57 -0
- steward_cli-0.1.2.dist-info/RECORD +12 -0
- steward_cli-0.1.2.dist-info/WHEEL +4 -0
- steward_cli-0.1.2.dist-info/entry_points.txt +2 -0
- steward_cli-0.1.2.dist-info/licenses/LICENSE +21 -0
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
steward/cli/__init__.py
ADDED
|
@@ -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,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.
|