steward-cli 0.25.2__tar.gz → 0.26.0__tar.gz
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_cli-0.26.0/.claude/skills/agent-roster/SKILL.md +86 -0
- steward_cli-0.26.0/.claude/skills/agent-roster/scripts/roster.sh +39 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/CHANGELOG.md +25 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/CLAUDE.md +28 -8
- {steward_cli-0.25.2 → steward_cli-0.26.0}/PKG-INFO +1 -1
- {steward_cli-0.25.2 → steward_cli-0.26.0}/packaging/culture-lens/pyproject.toml +2 -2
- {steward_cli-0.25.2 → steward_cli-0.26.0}/pyproject.toml +1 -1
- {steward_cli-0.25.2 → steward_cli-0.26.0}/steward/cli/__init__.py +2 -0
- steward_cli-0.26.0/steward/cli/_commands/_roster.py +238 -0
- steward_cli-0.26.0/steward/cli/_commands/roster.py +214 -0
- steward_cli-0.26.0/tests/test_cli_roster.py +202 -0
- steward_cli-0.26.0/tests/test_roster.py +224 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/uv.lock +1 -1
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/agent-config/SKILL.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/agent-config/data/backend-fingerprints.yaml +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/agent-config/scripts/show.sh +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/ask-colleague/SKILL.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/ask-colleague/prompts/explore.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/ask-colleague/prompts/review.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/ask-colleague/prompts/write.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/ask-colleague/scripts/ask-colleague.sh +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/assign-to-workforce/SKILL.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/assign-to-workforce/scripts/assign-to-workforce.sh +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/cicd/SKILL.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/cicd/scripts/_resolve-nick.sh +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/cicd/scripts/portability-lint.sh +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/cicd/scripts/pr-reply.sh +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/cicd/scripts/pr-status.sh +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/cicd/scripts/workflow.sh +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/communicate/SKILL.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/communicate/scripts/fetch-issues.sh +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/communicate/scripts/mesh-message.sh +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/communicate/scripts/post-comment.sh +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/communicate/scripts/post-issue.sh +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/communicate/scripts/templates/skill-new-brief.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/communicate/scripts/templates/skill-update-brief.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/discord-notify/SKILL.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/discord-notify/scripts/send-discord.sh +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/doc-test-alignment/SKILL.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/doc-test-alignment/scripts/check.sh +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/jekyll-test/SKILL.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/jekyll-test/scripts/test-site.sh +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/notebooklm/SKILL.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/notebooklm/scripts/get-repo-sources.sh +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/org-overview/SKILL.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/org-overview/scripts/org-overview.sh +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/pypi-maintainer/SKILL.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/pypi-maintainer/scripts/switch-source.sh +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/run-tests/SKILL.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/run-tests/scripts/test.sh +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/sonarclaude/SKILL.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/sonarclaude/scripts/sonar.sh +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/spec-to-plan/SKILL.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/spec-to-plan/scripts/spec-to-plan.sh +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/think/SKILL.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/think/scripts/think.sh +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/version-bump/SKILL.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/version-bump/scripts/bump.py +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills.local.yaml.example +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.devague/current +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.devague/current_plan +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.devague/frames/steward-hands-its-agent-relationship-understanding.json +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.devague/plans/steward-hands-its-agent-relationship-understanding.json +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.flake8 +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.github/workflows/publish.yml +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.github/workflows/tests.yml +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.gitignore +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/.markdownlint-cli2.yaml +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/AGENTS.colleague.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/LICENSE +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/README.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/culture.yaml +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/docs/briefs/operator-cli-onboarding.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/docs/perfect-patient.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/docs/plans/2026-06-12-steward-hands-its-agent-relationship-understanding.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/docs/sibling-pattern.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/docs/skill-sources.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/docs/specs/2026-06-12-steward-hands-its-agent-relationship-understanding.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/docs/superpowers/plans/2026-05-21-org-overview-skill.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/docs/superpowers/plans/2026-05-21-overview-cli-and-doc-relationships.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/docs/superpowers/plans/2026-05-21-overview-markdown-digest.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/docs/superpowers/plans/2026-05-21-steward-overview.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/docs/superpowers/plans/2026-05-22-backend-aware-agents-lens.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/docs/superpowers/specs/2026-05-21-org-overview-skill-design.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/docs/superpowers/specs/2026-05-21-overview-cli-and-doc-relationships-design.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/docs/superpowers/specs/2026-05-21-overview-digest-design.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/docs/superpowers/specs/2026-05-21-steward-overview-design.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/docs/superpowers/specs/2026-05-22-backend-aware-agents-lens-design.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/packaging/culture-lens/README.md +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/sonar-project.properties +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/steward/__init__.py +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/steward/__main__.py +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/steward/cli/_commands/__init__.py +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/steward/cli/_commands/_agents.py +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/steward/cli/_commands/_corpus.py +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/steward/cli/_commands/_graph.py +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/steward/cli/_commands/_overview_html.py +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/steward/cli/_commands/announce_skill_update.py +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/steward/cli/_commands/doctor.py +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/steward/cli/_commands/liveness.py +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/steward/cli/_commands/overview.py +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/steward/cli/_commands/show.py +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/steward/cli/_errors.py +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/steward/cli/_output.py +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/__init__.py +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/test_agents.py +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/test_cli.py +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/test_cli_announce_skill_update.py +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/test_cli_doctor.py +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/test_cli_doctor_siblings.py +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/test_cli_liveness.py +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/test_cli_overview.py +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/test_cli_show.py +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/test_corpus.py +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/test_graph.py +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/test_graph_seam.py +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/test_org_overview_skill.py +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/test_pr_reply_signature.py +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/test_resolve_nick.py +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/test_skills_convention.py +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/test_version_fallback.py +0 -0
- {steward_cli-0.25.2 → steward_cli-0.26.0}/tools/overview_html.py +0 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-roster
|
|
3
|
+
description: >
|
|
4
|
+
Inventory every Culture agent declared across the workspace's culture.yaml
|
|
5
|
+
files and reconcile it against the local Culture server's enrollment — one
|
|
6
|
+
flat table answering "which agents exist, which are actually registered with
|
|
7
|
+
the server, and what channels do they join?" Shows backend, model, channels,
|
|
8
|
+
and an enrolled (yes/no) column per agent, then flags enrolled-but-undeclared
|
|
9
|
+
suffixes (stale or out-of-workspace registrations). Use when the user asks to
|
|
10
|
+
list/roster/map the agents, "what agents do we have", "which agents are
|
|
11
|
+
enrolled / on the server", "what channels is each agent in", or "is anything
|
|
12
|
+
registered that we don't declare". Read-only; no graph, no LLM, no repo
|
|
13
|
+
writes — that is `org-overview`'s job. Steward-specific.
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Agent Roster — list every culture.yaml agent and its enrollment
|
|
17
|
+
|
|
18
|
+
A roster is the plainest sense-making question in steward's lane: *who is on
|
|
19
|
+
the mesh, and who is actually wired up?* It sits below the two existing verbs:
|
|
20
|
+
|
|
21
|
+
- `steward doctor` asks *"is this agent healthy and aligned?"* (compliance).
|
|
22
|
+
- `steward overview` / `org-overview` asks *"how do agents relate?"* (graph).
|
|
23
|
+
- **`agent-roster` (this skill) asks *"what exists and what's enrolled?"*** — a
|
|
24
|
+
flat inventory, no graph, no interpretation.
|
|
25
|
+
|
|
26
|
+
It joins two deterministic sources by agent `suffix`:
|
|
27
|
+
|
|
28
|
+
1. **Declarations** — every `<workspace>/*/culture.yaml` agent (the same corpus
|
|
29
|
+
walk `doctor`/`overview` use).
|
|
30
|
+
2. **Enrollment** — the `agents:` table in the local Culture server manifest
|
|
31
|
+
(`server.yaml`), which says which suffixes are *registered* with the running
|
|
32
|
+
server. Enrollment is **dir-aware**: when the server records a repo dir, a
|
|
33
|
+
suffix is only counted enrolled for the repo that dir resolves to, so a
|
|
34
|
+
suffix collision across two repos (e.g. two `daria` declarations) is
|
|
35
|
+
attributed to the one the server actually registered.
|
|
36
|
+
|
|
37
|
+
## When to use
|
|
38
|
+
|
|
39
|
+
- "List / roster / map the agents." / "What agents do we have?"
|
|
40
|
+
- "Which agents are enrolled?" / "What's running on the server?"
|
|
41
|
+
- "What channels is each agent in?"
|
|
42
|
+
- "Is anything registered that no culture.yaml declares?" (stale enrollment)
|
|
43
|
+
|
|
44
|
+
For "how do they relate / overlap / who's overloaded", use `org-overview`
|
|
45
|
+
instead — that is the graph + interpretation layer.
|
|
46
|
+
|
|
47
|
+
## How to run
|
|
48
|
+
|
|
49
|
+
One script wrapping `steward roster`:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Every declared agent + enrollment, as a table (default)
|
|
53
|
+
.claude/skills/agent-roster/scripts/roster.sh
|
|
54
|
+
|
|
55
|
+
# Only agents registered with the server
|
|
56
|
+
.claude/skills/agent-roster/scripts/roster.sh --enrolled-only
|
|
57
|
+
|
|
58
|
+
# Machine-readable for precise reasoning ({agents, unmatched_enrollments, totals})
|
|
59
|
+
.claude/skills/agent-roster/scripts/roster.sh --json
|
|
60
|
+
|
|
61
|
+
# Non-default checkout layout / explicit server manifest (passthrough)
|
|
62
|
+
.claude/skills/agent-roster/scripts/roster.sh --workspace-root /path/to/workspace
|
|
63
|
+
.claude/skills/agent-roster/scripts/roster.sh --server-yaml /path/to/server.yaml
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The table has one row per declared agent: `AGENT REPO BACKEND MODEL CHANNELS
|
|
67
|
+
ENROLLED`. Below it, an "Enrolled but no culture.yaml declares them" list names
|
|
68
|
+
any suffix the server registered that the workspace doesn't declare — a stale
|
|
69
|
+
registration, or a repo outside the scanned workspace. A diagnostic line (on
|
|
70
|
+
stderr) gives the declared / enrolled / unmatched totals, and notes when no
|
|
71
|
+
server manifest was readable (every agent then shows `no`).
|
|
72
|
+
|
|
73
|
+
## Server manifest resolution
|
|
74
|
+
|
|
75
|
+
The enrollment source is resolved in order: `--server-yaml` →
|
|
76
|
+
`culture_server_yaml` in `.claude/skills.local.yaml` → its committed
|
|
77
|
+
`.example` → `~/.culture/server.yaml`. A missing manifest is **not** an error —
|
|
78
|
+
the roster still lists every declared agent, just with enrollment blank.
|
|
79
|
+
|
|
80
|
+
## Reflect-only
|
|
81
|
+
|
|
82
|
+
This skill reports facts and stops. Reconciling a stale enrollment (server
|
|
83
|
+
knows an agent the workspace doesn't declare, or vice versa) is a separate,
|
|
84
|
+
explicit step — start a server agent, add the missing `culture.yaml`, or
|
|
85
|
+
deregister — none of which this skill does. Output is the chat conversation; it
|
|
86
|
+
writes nothing to disk and mutates no repo.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# agent-roster — inventory every culture.yaml agent + its server enrollment.
|
|
3
|
+
#
|
|
4
|
+
# Steward-the-agent (or the user) runs this to answer "what agents exist and
|
|
5
|
+
# which are enrolled?". Deterministic glue only: it resolves how to invoke
|
|
6
|
+
# steward and delegates to `steward roster`. No interpretation, no repo writes.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# roster.sh # every declared agent + enrollment (table)
|
|
10
|
+
# roster.sh --enrolled-only # only server-registered agents
|
|
11
|
+
# roster.sh --json # machine-readable evidence
|
|
12
|
+
# roster.sh --workspace-root DIR # non-default layout (passthrough)
|
|
13
|
+
# roster.sh --server-yaml FILE # explicit server manifest (passthrough)
|
|
14
|
+
#
|
|
15
|
+
# All arguments pass through to `steward roster`.
|
|
16
|
+
#
|
|
17
|
+
# Exit codes:
|
|
18
|
+
# 0 success (delegates to `steward roster`)
|
|
19
|
+
# 1 environment error (no way to invoke steward)
|
|
20
|
+
|
|
21
|
+
set -euo pipefail
|
|
22
|
+
|
|
23
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
24
|
+
SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
25
|
+
REPO_ROOT="$(cd "$SKILL_DIR/../../.." && pwd)"
|
|
26
|
+
|
|
27
|
+
# Resolve how to invoke steward: installed console script, then uv, then module.
|
|
28
|
+
if command -v steward >/dev/null 2>&1; then
|
|
29
|
+
STEWARD=(steward)
|
|
30
|
+
elif [ -f "$REPO_ROOT/pyproject.toml" ] && command -v uv >/dev/null 2>&1; then
|
|
31
|
+
STEWARD=(uv run --project "$REPO_ROOT" steward)
|
|
32
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
33
|
+
STEWARD=(python3 -m steward)
|
|
34
|
+
else
|
|
35
|
+
echo "agent-roster: cannot invoke steward (need 'steward', 'uv', or 'python3' on PATH)" >&2
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
exec "${STEWARD[@]}" roster "$@"
|
|
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
Format follows [Keep a Changelog](https://keepachangelog.com/). This project
|
|
6
6
|
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.26.0] - 2026-06-13
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`steward roster`** — flat inventory of every `culture.yaml` agent in the
|
|
13
|
+
workspace, joined by `suffix` against the local Culture server manifest's
|
|
14
|
+
`agents:` enrollment table. One row per declared agent (backend, model,
|
|
15
|
+
channels, enrolled yes/no), plus an "enrolled but no `culture.yaml` declares
|
|
16
|
+
them" list for stale or out-of-workspace registrations. Enrollment is
|
|
17
|
+
dir-aware, so a suffix collision across two repos (e.g. two `daria`
|
|
18
|
+
declarations) is attributed to the repo the server actually registered.
|
|
19
|
+
Table by default; `--json` for `{agents, unmatched_enrollments, totals}`;
|
|
20
|
+
`--enrolled-only` restricts to registered agents. Server manifest resolved
|
|
21
|
+
from `--server-yaml` → `culture_server_yaml` in `skills.local.yaml(.example)`
|
|
22
|
+
→ `~/.culture/server.yaml`; a missing manifest is not an error. Read-only —
|
|
23
|
+
no graph, no LLM, no repo writes. New helper module `_roster.py`
|
|
24
|
+
(`load_enrollment` / `build_report`) with unit + CLI tests.
|
|
25
|
+
- **`agent-roster` skill** (`.claude/skills/agent-roster/`) — chat wrapper over
|
|
26
|
+
`steward roster` for "list/roster/map the agents", "which are enrolled", and
|
|
27
|
+
"what channels is each agent in". Reflect-only.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
8
33
|
## [0.25.2] - 2026-06-13
|
|
9
34
|
|
|
10
35
|
### Added
|
|
@@ -150,14 +150,18 @@ rule applied to skills.
|
|
|
150
150
|
|
|
151
151
|
## Roadmap (CLI surface)
|
|
152
152
|
|
|
153
|
-
The CLI ships
|
|
154
|
-
`steward overview
|
|
155
|
-
original "verify" flow, folded into
|
|
156
|
-
agent-iteration flow). The `--apply` repair mode
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
153
|
+
The CLI ships these verbs today: `steward show`, `steward doctor`,
|
|
154
|
+
`steward overview`, `steward roster`, and `steward liveness`. Doctor runs in
|
|
155
|
+
two modes — single-repo diagnosis (the original "verify" flow, folded into
|
|
156
|
+
doctor) and corpus mode (the agent-iteration flow). The `--apply` repair mode
|
|
157
|
+
is the next layer on top.
|
|
158
|
+
|
|
159
|
+
`doctor`, `overview`, and `roster` are complementary cuts at the corpus:
|
|
160
|
+
`doctor` asks *"is this sibling healthy and aligned?"* (health / compliance);
|
|
161
|
+
`overview` asks *"what exists, how does it fit together, and what seems
|
|
162
|
+
missing?"* (sense-making via a typed graph); `roster` asks the plainest
|
|
163
|
+
question — *"what agents exist, which are enrolled with the server, and what
|
|
164
|
+
channels do they join?"* (a flat inventory, no graph).
|
|
161
165
|
|
|
162
166
|
- `steward doctor <path>` (default `--scope self`) — score a target repo
|
|
163
167
|
against `docs/sibling-pattern.md`. Aggregates findings across all
|
|
@@ -203,6 +207,22 @@ how does it fit together, and what seems missing?"* (sense-making).
|
|
|
203
207
|
repo mutation. That narration is driven by this repo's `org-overview` skill
|
|
204
208
|
(`.claude/skills/org-overview/`), which runs `steward overview` and speaks
|
|
205
209
|
the three layers in chat, reflect-only.
|
|
210
|
+
- `steward roster` — flat inventory of every `culture.yaml` agent in the
|
|
211
|
+
workspace (`<workspace-root>/*/culture.yaml`), joined by `suffix` against
|
|
212
|
+
the local Culture server manifest's `agents:` enrollment table (resolved
|
|
213
|
+
from `--server-yaml` → `culture_server_yaml` in `skills.local.yaml(.example)`
|
|
214
|
+
→ `~/.culture/server.yaml`). One row per declared agent — backend, model,
|
|
215
|
+
channels, and an enrolled (yes/no) column — plus an "enrolled but no
|
|
216
|
+
`culture.yaml` declares them" list for stale or out-of-workspace
|
|
217
|
+
registrations. Enrollment is **dir-aware**: when the manifest records a
|
|
218
|
+
repo dir, a suffix only counts as enrolled for the repo that dir resolves
|
|
219
|
+
to, so a suffix collision across two repos (e.g. two `daria` declarations)
|
|
220
|
+
is attributed to the one the server registered. Table by default,
|
|
221
|
+
`--json` for `{agents, unmatched_enrollments, totals}`, `--enrolled-only`
|
|
222
|
+
to restrict to registered agents. A missing manifest is not an error
|
|
223
|
+
(every agent then shows `no`). Read-only; no graph, no LLM, no repo writes.
|
|
224
|
+
Driven in chat by this repo's `agent-roster` skill
|
|
225
|
+
(`.claude/skills/agent-roster/`).
|
|
206
226
|
- `steward doctor --apply` *(planned)* — repair what diagnosis flagged,
|
|
207
227
|
where the repair is unambiguous (missing `scripts/` directory, missing
|
|
208
228
|
`.markdownlint-cli2.yaml`, missing `.claude/skills.local.yaml.example`,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: steward-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.26.0
|
|
4
4
|
Summary: Steward — aligns and maintains resident agents across Culture projects.
|
|
5
5
|
Project-URL: Homepage, https://github.com/agentculture/steward
|
|
6
6
|
Project-URL: Issues, https://github.com/agentculture/steward/issues
|
|
@@ -4,7 +4,7 @@ name = "culture-lens"
|
|
|
4
4
|
# source of truth). The placeholder below is overwritten in CI before build;
|
|
5
5
|
# it stays a valid PEP 440 string so a local `uv build packaging/culture-lens`
|
|
6
6
|
# still works for smoke-testing the build mechanics.
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.26.0"
|
|
8
8
|
description = "culture-lens — parallel console name for steward-cli (thin wrapper; installs steward-cli and exposes the `culture-lens` command)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -20,7 +20,7 @@ classifiers = [
|
|
|
20
20
|
# The `==<version>` pin is injected at build time to match this wrapper's own
|
|
21
21
|
# version, keeping culture-lens and steward-cli in lockstep.
|
|
22
22
|
dependencies = [
|
|
23
|
-
"steward-cli==0.
|
|
23
|
+
"steward-cli==0.26.0",
|
|
24
24
|
]
|
|
25
25
|
|
|
26
26
|
[project.urls]
|
|
@@ -36,6 +36,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
36
36
|
from steward.cli._commands import doctor as _doctor_cmd
|
|
37
37
|
from steward.cli._commands import liveness as _liveness_cmd
|
|
38
38
|
from steward.cli._commands import overview as _overview_cmd
|
|
39
|
+
from steward.cli._commands import roster as _roster_cmd
|
|
39
40
|
from steward.cli._commands import show as _show_cmd
|
|
40
41
|
|
|
41
42
|
parser = _StewardArgumentParser(
|
|
@@ -54,6 +55,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
54
55
|
_asu_cmd.register(sub)
|
|
55
56
|
_overview_cmd.register(sub)
|
|
56
57
|
_liveness_cmd.register(sub)
|
|
58
|
+
_roster_cmd.register(sub)
|
|
57
59
|
|
|
58
60
|
return parser
|
|
59
61
|
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Roster helpers for ``steward roster``.
|
|
2
|
+
|
|
3
|
+
Pure helpers, no CLI surface. The ``roster`` command imports from here to:
|
|
4
|
+
|
|
5
|
+
1. Read the local Culture server's enrollment map (:func:`load_enrollment`)
|
|
6
|
+
— the ``agents:`` table in ``server.yaml`` (``suffix → repo dir``) that
|
|
7
|
+
says which declared agents are actually *registered* with the running
|
|
8
|
+
server.
|
|
9
|
+
2. Cross-reference declared agents (from :func:`steward.cli._commands._corpus.discover_agents`)
|
|
10
|
+
against that enrollment to build a flat inventory (:func:`build_report`):
|
|
11
|
+
one row per declared agent — backend, model, channels, enrollment — plus
|
|
12
|
+
the enrolled-but-undeclared suffixes that have no matching ``culture.yaml``.
|
|
13
|
+
|
|
14
|
+
Why a separate module: ``_corpus.py`` already owns the doctor/baseline
|
|
15
|
+
scoring surface. The roster is a different question ("what exists and what's
|
|
16
|
+
wired up?", not "is it healthy?"), so its data shaping lives on its own and
|
|
17
|
+
stays testable without argparse.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
import yaml
|
|
26
|
+
|
|
27
|
+
from steward.cli._commands._corpus import Agent
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class RosterRow:
|
|
32
|
+
"""One declared agent, cross-referenced with server enrollment."""
|
|
33
|
+
|
|
34
|
+
suffix: str
|
|
35
|
+
repo: str
|
|
36
|
+
backend: str
|
|
37
|
+
model: str | None
|
|
38
|
+
channels: list[str]
|
|
39
|
+
tags: list[str]
|
|
40
|
+
inline_prompt: bool # culture.yaml carries an inline ``system_prompt:``
|
|
41
|
+
enrolled: bool # suffix present in the server's ``agents:`` map
|
|
42
|
+
enrolled_dir: str | None # the dir the server registered for this suffix
|
|
43
|
+
|
|
44
|
+
def to_dict(self) -> dict:
|
|
45
|
+
return {
|
|
46
|
+
"suffix": self.suffix,
|
|
47
|
+
"repo": self.repo,
|
|
48
|
+
"backend": self.backend,
|
|
49
|
+
"model": self.model,
|
|
50
|
+
"channels": self.channels,
|
|
51
|
+
"tags": self.tags,
|
|
52
|
+
"inline_prompt": self.inline_prompt,
|
|
53
|
+
"enrolled": self.enrolled,
|
|
54
|
+
"enrolled_dir": self.enrolled_dir,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class UnmatchedEnrollment:
|
|
60
|
+
"""A suffix registered with the server that no ``culture.yaml`` declares."""
|
|
61
|
+
|
|
62
|
+
suffix: str
|
|
63
|
+
enrolled_dir: str
|
|
64
|
+
|
|
65
|
+
def to_dict(self) -> dict:
|
|
66
|
+
return {"suffix": self.suffix, "enrolled_dir": self.enrolled_dir}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class RosterReport:
|
|
71
|
+
"""The full inventory: declared agents + enrollment reconciliation."""
|
|
72
|
+
|
|
73
|
+
rows: list[RosterRow] = field(default_factory=list)
|
|
74
|
+
unmatched_enrollments: list[UnmatchedEnrollment] = field(default_factory=list)
|
|
75
|
+
server_yaml: str | None = None # resolved path, or None when no manifest found
|
|
76
|
+
server_present: bool = False # the manifest existed and parsed
|
|
77
|
+
|
|
78
|
+
def to_dict(self) -> dict:
|
|
79
|
+
return {
|
|
80
|
+
"agents": [r.to_dict() for r in self.rows],
|
|
81
|
+
"unmatched_enrollments": [u.to_dict() for u in self.unmatched_enrollments],
|
|
82
|
+
"server_yaml": self.server_yaml,
|
|
83
|
+
"server_present": self.server_present,
|
|
84
|
+
"totals": {
|
|
85
|
+
"declared": len(self.rows),
|
|
86
|
+
"enrolled": sum(1 for r in self.rows if r.enrolled),
|
|
87
|
+
"unmatched_enrollments": len(self.unmatched_enrollments),
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def load_enrollment(server_yaml: Path) -> tuple[dict[str, str], bool]:
|
|
93
|
+
"""Return ``(enrollment, present)`` from a Culture ``server.yaml``.
|
|
94
|
+
|
|
95
|
+
``enrollment`` maps each registered agent ``suffix`` to the repo dir the
|
|
96
|
+
server recorded for it (the ``agents:`` table). ``present`` is True iff the
|
|
97
|
+
manifest existed and parsed as a mapping — when False, enrollment is ``{}``
|
|
98
|
+
and the caller renders every agent as "not enrolled" rather than crashing.
|
|
99
|
+
|
|
100
|
+
A missing or malformed manifest is not an error: a roster is still useful
|
|
101
|
+
without a running server (it just can't say what's enrolled).
|
|
102
|
+
"""
|
|
103
|
+
try:
|
|
104
|
+
text = server_yaml.read_text(encoding="utf-8")
|
|
105
|
+
except OSError:
|
|
106
|
+
return {}, False
|
|
107
|
+
try:
|
|
108
|
+
data = yaml.safe_load(text)
|
|
109
|
+
except yaml.YAMLError:
|
|
110
|
+
return {}, False
|
|
111
|
+
if not isinstance(data, dict):
|
|
112
|
+
return {}, False
|
|
113
|
+
agents = data.get("agents")
|
|
114
|
+
if not isinstance(agents, dict):
|
|
115
|
+
# Manifest parsed but has no agents table — treat as present-but-empty.
|
|
116
|
+
return {}, True
|
|
117
|
+
enrollment: dict[str, str] = {}
|
|
118
|
+
for suffix, repo_dir in agents.items():
|
|
119
|
+
enrollment[str(suffix)] = _entry_dir(repo_dir)
|
|
120
|
+
return enrollment, True
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _entry_dir(repo_dir) -> str:
|
|
124
|
+
"""Normalize a server.yaml ``agents:`` value to a repo-dir string.
|
|
125
|
+
|
|
126
|
+
Culture's manifest registers each suffix as either a bare path string or a
|
|
127
|
+
mapping with a ``directory`` key — the same two shapes the ``agent-config``
|
|
128
|
+
skill handles (``show.sh``: ``entry['directory'] if isinstance(entry, dict)
|
|
129
|
+
else entry``). Mirror that here so a mapping-form entry resolves to its
|
|
130
|
+
path instead of stringifying to a dict repr that could never match a repo.
|
|
131
|
+
A null value (or a mapping missing ``directory``) becomes ``""`` — present
|
|
132
|
+
but undisambiguatable, enrolled on suffix alone.
|
|
133
|
+
"""
|
|
134
|
+
if repo_dir is None:
|
|
135
|
+
return ""
|
|
136
|
+
if isinstance(repo_dir, dict):
|
|
137
|
+
directory = repo_dir.get("directory")
|
|
138
|
+
return "" if directory is None else str(directory)
|
|
139
|
+
return str(repo_dir)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _channels(raw: dict) -> list[str]:
|
|
143
|
+
value = raw.get("channels")
|
|
144
|
+
if not isinstance(value, list):
|
|
145
|
+
return []
|
|
146
|
+
return [str(c) for c in value]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _tags(raw: dict) -> list[str]:
|
|
150
|
+
value = raw.get("tags")
|
|
151
|
+
if not isinstance(value, list):
|
|
152
|
+
return []
|
|
153
|
+
return [str(t) for t in value]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _same_dir(a: str, b: Path) -> bool:
|
|
157
|
+
"""True if registered dir string *a* resolves to repo path *b*.
|
|
158
|
+
|
|
159
|
+
Best-effort: a registration whose dir can't be resolved (e.g. a relative
|
|
160
|
+
path against an unknown cwd) falls back to False so a mismatch never
|
|
161
|
+
silently claims enrollment.
|
|
162
|
+
"""
|
|
163
|
+
if not a:
|
|
164
|
+
return False
|
|
165
|
+
try:
|
|
166
|
+
return Path(a).expanduser().resolve() == b.resolve()
|
|
167
|
+
except OSError:
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _is_enrolled(agent: Agent, enrollment: dict[str, str]) -> bool:
|
|
172
|
+
"""Whether *agent* is registered with the server.
|
|
173
|
+
|
|
174
|
+
Suffix match is necessary; when the server also records a dir, it must
|
|
175
|
+
resolve to this agent's repo (so a suffix collision across two repos —
|
|
176
|
+
e.g. two ``daria`` declarations — is attributed to the one the server
|
|
177
|
+
actually registered). A blank registered dir can't disambiguate, so it
|
|
178
|
+
is accepted on the suffix alone.
|
|
179
|
+
"""
|
|
180
|
+
if agent.suffix not in enrollment:
|
|
181
|
+
return False
|
|
182
|
+
registered_dir = enrollment[agent.suffix]
|
|
183
|
+
if not registered_dir:
|
|
184
|
+
return True
|
|
185
|
+
return _same_dir(registered_dir, agent.repo_path)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _row(agent: Agent, enrollment: dict[str, str]) -> RosterRow:
|
|
189
|
+
raw = agent.raw
|
|
190
|
+
model = raw.get("model")
|
|
191
|
+
enrolled = _is_enrolled(agent, enrollment)
|
|
192
|
+
return RosterRow(
|
|
193
|
+
suffix=agent.suffix,
|
|
194
|
+
repo=agent.repo_name,
|
|
195
|
+
backend=agent.backend or "",
|
|
196
|
+
model=str(model) if model else None,
|
|
197
|
+
channels=_channels(raw),
|
|
198
|
+
tags=_tags(raw),
|
|
199
|
+
inline_prompt=bool(raw.get("system_prompt")),
|
|
200
|
+
enrolled=enrolled,
|
|
201
|
+
enrolled_dir=enrollment.get(agent.suffix) if enrolled else None,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def build_report(
|
|
206
|
+
agents: list[Agent],
|
|
207
|
+
enrollment: dict[str, str],
|
|
208
|
+
*,
|
|
209
|
+
server_yaml: str | None = None,
|
|
210
|
+
server_present: bool = False,
|
|
211
|
+
) -> RosterReport:
|
|
212
|
+
"""Turn discovered agents + an enrollment map into a :class:`RosterReport`.
|
|
213
|
+
|
|
214
|
+
Rows are sorted by ``(repo, suffix)`` for stable output. Any enrolled
|
|
215
|
+
suffix that no discovered agent claims becomes an
|
|
216
|
+
:class:`UnmatchedEnrollment` — the server knows about an agent the corpus
|
|
217
|
+
doesn't declare (a stale registration, or a repo outside the scanned
|
|
218
|
+
workspace).
|
|
219
|
+
"""
|
|
220
|
+
rows = sorted(
|
|
221
|
+
(_row(a, enrollment) for a in agents),
|
|
222
|
+
key=lambda r: (r.repo, r.suffix),
|
|
223
|
+
)
|
|
224
|
+
# A registration is "matched" when some declared agent claimed it (enrolled
|
|
225
|
+
# row). Anything left over — suffix never declared, or declared only in a
|
|
226
|
+
# different dir than the server registered — is an unmatched enrollment.
|
|
227
|
+
matched_suffixes = {r.suffix for r in rows if r.enrolled}
|
|
228
|
+
unmatched = [
|
|
229
|
+
UnmatchedEnrollment(suffix=s, enrolled_dir=d)
|
|
230
|
+
for s, d in sorted(enrollment.items())
|
|
231
|
+
if s not in matched_suffixes
|
|
232
|
+
]
|
|
233
|
+
return RosterReport(
|
|
234
|
+
rows=rows,
|
|
235
|
+
unmatched_enrollments=unmatched,
|
|
236
|
+
server_yaml=server_yaml,
|
|
237
|
+
server_present=server_present,
|
|
238
|
+
)
|