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.
Files changed (122) hide show
  1. steward_cli-0.26.0/.claude/skills/agent-roster/SKILL.md +86 -0
  2. steward_cli-0.26.0/.claude/skills/agent-roster/scripts/roster.sh +39 -0
  3. {steward_cli-0.25.2 → steward_cli-0.26.0}/CHANGELOG.md +25 -0
  4. {steward_cli-0.25.2 → steward_cli-0.26.0}/CLAUDE.md +28 -8
  5. {steward_cli-0.25.2 → steward_cli-0.26.0}/PKG-INFO +1 -1
  6. {steward_cli-0.25.2 → steward_cli-0.26.0}/packaging/culture-lens/pyproject.toml +2 -2
  7. {steward_cli-0.25.2 → steward_cli-0.26.0}/pyproject.toml +1 -1
  8. {steward_cli-0.25.2 → steward_cli-0.26.0}/steward/cli/__init__.py +2 -0
  9. steward_cli-0.26.0/steward/cli/_commands/_roster.py +238 -0
  10. steward_cli-0.26.0/steward/cli/_commands/roster.py +214 -0
  11. steward_cli-0.26.0/tests/test_cli_roster.py +202 -0
  12. steward_cli-0.26.0/tests/test_roster.py +224 -0
  13. {steward_cli-0.25.2 → steward_cli-0.26.0}/uv.lock +1 -1
  14. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/agent-config/SKILL.md +0 -0
  15. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/agent-config/data/backend-fingerprints.yaml +0 -0
  16. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/agent-config/scripts/show.sh +0 -0
  17. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/ask-colleague/SKILL.md +0 -0
  18. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/ask-colleague/prompts/explore.md +0 -0
  19. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/ask-colleague/prompts/review.md +0 -0
  20. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/ask-colleague/prompts/write.md +0 -0
  21. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/ask-colleague/scripts/ask-colleague.sh +0 -0
  22. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/assign-to-workforce/SKILL.md +0 -0
  23. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/assign-to-workforce/scripts/assign-to-workforce.sh +0 -0
  24. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/cicd/SKILL.md +0 -0
  25. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/cicd/scripts/_resolve-nick.sh +0 -0
  26. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/cicd/scripts/portability-lint.sh +0 -0
  27. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/cicd/scripts/pr-reply.sh +0 -0
  28. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/cicd/scripts/pr-status.sh +0 -0
  29. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/cicd/scripts/workflow.sh +0 -0
  30. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/communicate/SKILL.md +0 -0
  31. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/communicate/scripts/fetch-issues.sh +0 -0
  32. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/communicate/scripts/mesh-message.sh +0 -0
  33. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/communicate/scripts/post-comment.sh +0 -0
  34. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/communicate/scripts/post-issue.sh +0 -0
  35. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/communicate/scripts/templates/skill-new-brief.md +0 -0
  36. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/communicate/scripts/templates/skill-update-brief.md +0 -0
  37. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/discord-notify/SKILL.md +0 -0
  38. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/discord-notify/scripts/send-discord.sh +0 -0
  39. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/doc-test-alignment/SKILL.md +0 -0
  40. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/doc-test-alignment/scripts/check.sh +0 -0
  41. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/jekyll-test/SKILL.md +0 -0
  42. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/jekyll-test/scripts/test-site.sh +0 -0
  43. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/notebooklm/SKILL.md +0 -0
  44. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/notebooklm/scripts/get-repo-sources.sh +0 -0
  45. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/org-overview/SKILL.md +0 -0
  46. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/org-overview/scripts/org-overview.sh +0 -0
  47. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/pypi-maintainer/SKILL.md +0 -0
  48. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/pypi-maintainer/scripts/switch-source.sh +0 -0
  49. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/run-tests/SKILL.md +0 -0
  50. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/run-tests/scripts/test.sh +0 -0
  51. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/sonarclaude/SKILL.md +0 -0
  52. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/sonarclaude/scripts/sonar.sh +0 -0
  53. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/spec-to-plan/SKILL.md +0 -0
  54. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/spec-to-plan/scripts/spec-to-plan.sh +0 -0
  55. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/think/SKILL.md +0 -0
  56. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/think/scripts/think.sh +0 -0
  57. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/version-bump/SKILL.md +0 -0
  58. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills/version-bump/scripts/bump.py +0 -0
  59. {steward_cli-0.25.2 → steward_cli-0.26.0}/.claude/skills.local.yaml.example +0 -0
  60. {steward_cli-0.25.2 → steward_cli-0.26.0}/.devague/current +0 -0
  61. {steward_cli-0.25.2 → steward_cli-0.26.0}/.devague/current_plan +0 -0
  62. {steward_cli-0.25.2 → steward_cli-0.26.0}/.devague/frames/steward-hands-its-agent-relationship-understanding.json +0 -0
  63. {steward_cli-0.25.2 → steward_cli-0.26.0}/.devague/plans/steward-hands-its-agent-relationship-understanding.json +0 -0
  64. {steward_cli-0.25.2 → steward_cli-0.26.0}/.flake8 +0 -0
  65. {steward_cli-0.25.2 → steward_cli-0.26.0}/.github/workflows/publish.yml +0 -0
  66. {steward_cli-0.25.2 → steward_cli-0.26.0}/.github/workflows/tests.yml +0 -0
  67. {steward_cli-0.25.2 → steward_cli-0.26.0}/.gitignore +0 -0
  68. {steward_cli-0.25.2 → steward_cli-0.26.0}/.markdownlint-cli2.yaml +0 -0
  69. {steward_cli-0.25.2 → steward_cli-0.26.0}/AGENTS.colleague.md +0 -0
  70. {steward_cli-0.25.2 → steward_cli-0.26.0}/LICENSE +0 -0
  71. {steward_cli-0.25.2 → steward_cli-0.26.0}/README.md +0 -0
  72. {steward_cli-0.25.2 → steward_cli-0.26.0}/culture.yaml +0 -0
  73. {steward_cli-0.25.2 → steward_cli-0.26.0}/docs/briefs/operator-cli-onboarding.md +0 -0
  74. {steward_cli-0.25.2 → steward_cli-0.26.0}/docs/perfect-patient.md +0 -0
  75. {steward_cli-0.25.2 → steward_cli-0.26.0}/docs/plans/2026-06-12-steward-hands-its-agent-relationship-understanding.md +0 -0
  76. {steward_cli-0.25.2 → steward_cli-0.26.0}/docs/sibling-pattern.md +0 -0
  77. {steward_cli-0.25.2 → steward_cli-0.26.0}/docs/skill-sources.md +0 -0
  78. {steward_cli-0.25.2 → steward_cli-0.26.0}/docs/specs/2026-06-12-steward-hands-its-agent-relationship-understanding.md +0 -0
  79. {steward_cli-0.25.2 → steward_cli-0.26.0}/docs/superpowers/plans/2026-05-21-org-overview-skill.md +0 -0
  80. {steward_cli-0.25.2 → steward_cli-0.26.0}/docs/superpowers/plans/2026-05-21-overview-cli-and-doc-relationships.md +0 -0
  81. {steward_cli-0.25.2 → steward_cli-0.26.0}/docs/superpowers/plans/2026-05-21-overview-markdown-digest.md +0 -0
  82. {steward_cli-0.25.2 → steward_cli-0.26.0}/docs/superpowers/plans/2026-05-21-steward-overview.md +0 -0
  83. {steward_cli-0.25.2 → steward_cli-0.26.0}/docs/superpowers/plans/2026-05-22-backend-aware-agents-lens.md +0 -0
  84. {steward_cli-0.25.2 → steward_cli-0.26.0}/docs/superpowers/specs/2026-05-21-org-overview-skill-design.md +0 -0
  85. {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
  86. {steward_cli-0.25.2 → steward_cli-0.26.0}/docs/superpowers/specs/2026-05-21-overview-digest-design.md +0 -0
  87. {steward_cli-0.25.2 → steward_cli-0.26.0}/docs/superpowers/specs/2026-05-21-steward-overview-design.md +0 -0
  88. {steward_cli-0.25.2 → steward_cli-0.26.0}/docs/superpowers/specs/2026-05-22-backend-aware-agents-lens-design.md +0 -0
  89. {steward_cli-0.25.2 → steward_cli-0.26.0}/packaging/culture-lens/README.md +0 -0
  90. {steward_cli-0.25.2 → steward_cli-0.26.0}/sonar-project.properties +0 -0
  91. {steward_cli-0.25.2 → steward_cli-0.26.0}/steward/__init__.py +0 -0
  92. {steward_cli-0.25.2 → steward_cli-0.26.0}/steward/__main__.py +0 -0
  93. {steward_cli-0.25.2 → steward_cli-0.26.0}/steward/cli/_commands/__init__.py +0 -0
  94. {steward_cli-0.25.2 → steward_cli-0.26.0}/steward/cli/_commands/_agents.py +0 -0
  95. {steward_cli-0.25.2 → steward_cli-0.26.0}/steward/cli/_commands/_corpus.py +0 -0
  96. {steward_cli-0.25.2 → steward_cli-0.26.0}/steward/cli/_commands/_graph.py +0 -0
  97. {steward_cli-0.25.2 → steward_cli-0.26.0}/steward/cli/_commands/_overview_html.py +0 -0
  98. {steward_cli-0.25.2 → steward_cli-0.26.0}/steward/cli/_commands/announce_skill_update.py +0 -0
  99. {steward_cli-0.25.2 → steward_cli-0.26.0}/steward/cli/_commands/doctor.py +0 -0
  100. {steward_cli-0.25.2 → steward_cli-0.26.0}/steward/cli/_commands/liveness.py +0 -0
  101. {steward_cli-0.25.2 → steward_cli-0.26.0}/steward/cli/_commands/overview.py +0 -0
  102. {steward_cli-0.25.2 → steward_cli-0.26.0}/steward/cli/_commands/show.py +0 -0
  103. {steward_cli-0.25.2 → steward_cli-0.26.0}/steward/cli/_errors.py +0 -0
  104. {steward_cli-0.25.2 → steward_cli-0.26.0}/steward/cli/_output.py +0 -0
  105. {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/__init__.py +0 -0
  106. {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/test_agents.py +0 -0
  107. {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/test_cli.py +0 -0
  108. {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/test_cli_announce_skill_update.py +0 -0
  109. {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/test_cli_doctor.py +0 -0
  110. {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/test_cli_doctor_siblings.py +0 -0
  111. {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/test_cli_liveness.py +0 -0
  112. {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/test_cli_overview.py +0 -0
  113. {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/test_cli_show.py +0 -0
  114. {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/test_corpus.py +0 -0
  115. {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/test_graph.py +0 -0
  116. {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/test_graph_seam.py +0 -0
  117. {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/test_org_overview_skill.py +0 -0
  118. {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/test_pr_reply_signature.py +0 -0
  119. {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/test_resolve_nick.py +0 -0
  120. {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/test_skills_convention.py +0 -0
  121. {steward_cli-0.25.2 → steward_cli-0.26.0}/tests/test_version_fallback.py +0 -0
  122. {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 three verbs today: `steward show`, `steward doctor`, and
154
- `steward overview`. Doctor runs in two modes single-repo diagnosis (the
155
- original "verify" flow, folded into doctor) and corpus mode (the
156
- agent-iteration flow). The `--apply` repair mode is the next layer on top.
157
-
158
- `doctor` and `overview` are complementary: `doctor` asks *"is this sibling
159
- healthy and aligned?"* (health / compliance); `overview` asks *"what exists,
160
- how does it fit together, and what seems missing?"* (sense-making).
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.25.2
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.25.2"
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.25.2",
23
+ "steward-cli==0.26.0",
24
24
  ]
25
25
 
26
26
  [project.urls]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "steward-cli"
3
- version = "0.25.2"
3
+ version = "0.26.0"
4
4
  description = "Steward — aligns and maintains resident agents across Culture projects."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -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
+ )