steward-cli 0.1.2__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.1.2/.claude/skills/agent-config/SKILL.md +112 -0
- steward_cli-0.1.2/.claude/skills/agent-config/scripts/show.sh +113 -0
- steward_cli-0.1.2/.claude/skills/pr-review/SKILL.md +120 -0
- steward_cli-0.1.2/.claude/skills/pr-review/scripts/portability-lint.sh +57 -0
- steward_cli-0.1.2/.claude/skills/pr-review/scripts/pr-batch.sh +57 -0
- steward_cli-0.1.2/.claude/skills/pr-review/scripts/pr-comments.sh +100 -0
- steward_cli-0.1.2/.claude/skills/pr-review/scripts/pr-reply.sh +66 -0
- steward_cli-0.1.2/.claude/skills/pr-review/scripts/pr-status.sh +161 -0
- steward_cli-0.1.2/.claude/skills/pr-review/scripts/workflow.sh +99 -0
- steward_cli-0.1.2/.claude/skills/version-bump/SKILL.md +66 -0
- steward_cli-0.1.2/.claude/skills/version-bump/scripts/bump.py +178 -0
- steward_cli-0.1.2/.claude/skills.local.yaml.example +14 -0
- steward_cli-0.1.2/.flake8 +7 -0
- steward_cli-0.1.2/.github/workflows/publish.yml +88 -0
- steward_cli-0.1.2/.github/workflows/tests.yml +114 -0
- steward_cli-0.1.2/.gitignore +225 -0
- steward_cli-0.1.2/.markdownlint-cli2.yaml +19 -0
- steward_cli-0.1.2/CHANGELOG.md +53 -0
- steward_cli-0.1.2/CLAUDE.md +72 -0
- steward_cli-0.1.2/LICENSE +21 -0
- steward_cli-0.1.2/PKG-INFO +57 -0
- steward_cli-0.1.2/README.md +40 -0
- steward_cli-0.1.2/pyproject.toml +71 -0
- steward_cli-0.1.2/steward/__init__.py +11 -0
- steward_cli-0.1.2/steward/__main__.py +8 -0
- steward_cli-0.1.2/steward/cli/__init__.py +82 -0
- steward_cli-0.1.2/steward/cli/_commands/__init__.py +0 -0
- steward_cli-0.1.2/steward/cli/_commands/show.py +116 -0
- steward_cli-0.1.2/steward/cli/_errors.py +37 -0
- steward_cli-0.1.2/steward/cli/_output.py +41 -0
- steward_cli-0.1.2/tests/__init__.py +0 -0
- steward_cli-0.1.2/tests/test_cli.py +105 -0
- steward_cli-0.1.2/uv.lock +467 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-config
|
|
3
|
+
description: >
|
|
4
|
+
Show a Culture agent's full configuration in one view: CLAUDE.md, the parallel
|
|
5
|
+
culture.yaml, and the agent's local skills. Use when reviewing an agent for
|
|
6
|
+
alignment, before changing a system_prompt, when triaging a PR that touches
|
|
7
|
+
agent definitions, or when the user says "show agent <name>" / "what does
|
|
8
|
+
<agent> look like" / "audit agent config". Steward-specific.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Agent Config — surface a Culture agent in one view
|
|
12
|
+
|
|
13
|
+
Steward's job is keeping resident agents aligned across Culture projects. To
|
|
14
|
+
reason about alignment you need to see, in one place, the three artifacts that
|
|
15
|
+
together define an agent:
|
|
16
|
+
|
|
17
|
+
1. **`CLAUDE.md`** — prompt-side guidance (what Claude Code sessions in that
|
|
18
|
+
repo are told).
|
|
19
|
+
2. **`culture.yaml`** — runtime-side config (`agents:` list with `suffix`,
|
|
20
|
+
`backend`, `model`, `system_prompt`, `channels`, `tags`, `acp_command`,
|
|
21
|
+
`extras`). Lives parallel to `CLAUDE.md` at the project root.
|
|
22
|
+
3. **`.claude/skills/*/SKILL.md`** — per-project skills the agent can invoke.
|
|
23
|
+
|
|
24
|
+
If any of these drifts away from the others (prompt says one thing,
|
|
25
|
+
`system_prompt` says another, skills assume a third), the agent is misaligned.
|
|
26
|
+
That's what Steward is supposed to catch.
|
|
27
|
+
|
|
28
|
+
## When to use
|
|
29
|
+
|
|
30
|
+
- Before editing a `system_prompt` in any sibling project's `culture.yaml`.
|
|
31
|
+
- When a PR diff touches `CLAUDE.md` or `culture.yaml` in any sibling project.
|
|
32
|
+
- During a Steward alignment audit (cross-project consistency check).
|
|
33
|
+
- Before answering a question about what an agent does — read it, don't guess.
|
|
34
|
+
|
|
35
|
+
## How to run
|
|
36
|
+
|
|
37
|
+
One script, two ways to call it:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Path mode — point at any directory containing CLAUDE.md + culture.yaml
|
|
41
|
+
.claude/skills/agent-config/scripts/show.sh ../culture
|
|
42
|
+
|
|
43
|
+
# Suffix mode — resolves a registered agent suffix via the Culture server's
|
|
44
|
+
# manifest (location set by culture_server_yaml in skills.local.yaml)
|
|
45
|
+
.claude/skills/agent-config/scripts/show.sh daria
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Output is three sections: CLAUDE.md, culture.yaml, and a one-line summary
|
|
49
|
+
per local skill (name + description, truncated to 120 chars).
|
|
50
|
+
|
|
51
|
+
## What to look at in `culture.yaml`
|
|
52
|
+
|
|
53
|
+
| Field | Why it matters |
|
|
54
|
+
|-------|----------------|
|
|
55
|
+
| `suffix` | Identifies the agent on the mesh. |
|
|
56
|
+
| `backend` | One of `claude` / `codex` / `copilot` / `acp`. The all-backends rule means a feature in one must land in all four. |
|
|
57
|
+
| `model` | Drift here changes behavior silently. |
|
|
58
|
+
| `system_prompt` | Must not contradict `CLAUDE.md`. |
|
|
59
|
+
| `channels` | Where the agent listens. Must match what `CLAUDE.md` claims it does. |
|
|
60
|
+
| `tags`, `extras`, `acp_command` | Backend-specific; check against the canonical example in `culture/packages/agent-harness/culture.yaml`. |
|
|
61
|
+
|
|
62
|
+
## Alignment checks
|
|
63
|
+
|
|
64
|
+
After `show.sh`, ask three questions about the agent:
|
|
65
|
+
|
|
66
|
+
1. **Does `system_prompt` (in `culture.yaml`) contradict `CLAUDE.md`?** If
|
|
67
|
+
`CLAUDE.md` says "this agent runs the deploy" but `system_prompt` says "you
|
|
68
|
+
are a casual chat assistant," that's drift.
|
|
69
|
+
2. **Are the `channels` listed in `culture.yaml` the channels `CLAUDE.md`
|
|
70
|
+
claims the agent participates in?** Mismatch = silent absence on the mesh.
|
|
71
|
+
3. **Is the `backend` consistent with the all-backends rule?** If the agent is
|
|
72
|
+
`claude` only and a sibling agent doing the same job is `codex` only,
|
|
73
|
+
feature drift between the two is inevitable.
|
|
74
|
+
|
|
75
|
+
Report findings as a short bulleted list — not a pass/fail. Drift is for
|
|
76
|
+
humans to decide on; this skill is read-only.
|
|
77
|
+
|
|
78
|
+
## Inventory of skills already present in the workspace
|
|
79
|
+
|
|
80
|
+
Captured during Steward's first audit (2026-04). Use this to recognize what
|
|
81
|
+
already exists before scaffolding something new — duplication is a Steward
|
|
82
|
+
anti-goal. Append discoveries here so the next session inherits them.
|
|
83
|
+
|
|
84
|
+
**PR / code review** — `pr-review` (multiple copies across culture,
|
|
85
|
+
culture-sonar-cli, daria, codex-guide, plus the user-level canonical version);
|
|
86
|
+
`review-and-fix`; `superpowers:receiving-code-review`,
|
|
87
|
+
`superpowers:requesting-code-review`.
|
|
88
|
+
|
|
89
|
+
**Agent lifecycle / mesh ops** — `culture` (multiple copies); `culture-irc` /
|
|
90
|
+
`irc` (multiple copies); `daria`.
|
|
91
|
+
|
|
92
|
+
**Build / verify** — `run-tests` (culture, culture-sonar-cli).
|
|
93
|
+
|
|
94
|
+
**Introspection** — `claude-code-guide:introspect`,
|
|
95
|
+
`codex-guide:codex-guide-introspect`.
|
|
96
|
+
|
|
97
|
+
**Onboarding / docs** — `ask`, `onboard`, `codex-guide-ask`,
|
|
98
|
+
`codex-guide-onboarding`.
|
|
99
|
+
|
|
100
|
+
**Gamification** — `game-mode`, `level-up`, `visualize-setup`,
|
|
101
|
+
`migrate-to-claude`.
|
|
102
|
+
|
|
103
|
+
## Notes
|
|
104
|
+
|
|
105
|
+
- `show.sh` is read-only. It never edits agent files. Drift is reported, not
|
|
106
|
+
auto-fixed.
|
|
107
|
+
- The canonical `culture.yaml` parser lives at
|
|
108
|
+
`culture/culture/config.py:load_culture_yaml(directory, suffix)` returning
|
|
109
|
+
`AgentConfig`. If parsing gets non-trivial in this skill, shell out to a
|
|
110
|
+
`culture` CLI command instead of re-parsing.
|
|
111
|
+
- The script handles both `directory` keys and bare-string values in the
|
|
112
|
+
server manifest's `agents:` mapping.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Show a Culture agent's full configuration in one view:
|
|
3
|
+
# CLAUDE.md, the parallel culture.yaml, and the .claude/skills/ index.
|
|
4
|
+
#
|
|
5
|
+
# Usage: show.sh <path-or-agent-suffix>
|
|
6
|
+
#
|
|
7
|
+
# Path mode: show.sh ../culture
|
|
8
|
+
# Suffix mode: show.sh daria (resolved via culture_server_yaml in skills.local.yaml)
|
|
9
|
+
#
|
|
10
|
+
# Exit codes:
|
|
11
|
+
# 0 success
|
|
12
|
+
# 1 environment error (missing manifest, missing PyYAML for suffix mode)
|
|
13
|
+
# 2 user error (no target given, unknown suffix, target path doesn't exist)
|
|
14
|
+
|
|
15
|
+
set -euo pipefail
|
|
16
|
+
|
|
17
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
18
|
+
SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
19
|
+
REPO_ROOT="$(cd "$SKILL_DIR/../../.." && pwd)"
|
|
20
|
+
|
|
21
|
+
CFG="$REPO_ROOT/.claude/skills.local.yaml"
|
|
22
|
+
[ -f "$CFG" ] || CFG="$REPO_ROOT/.claude/skills.local.yaml.example"
|
|
23
|
+
|
|
24
|
+
# Read a top-level YAML scalar from CFG. Schema is intentionally tiny:
|
|
25
|
+
# key: value (with optional surrounding quotes / trailing comment)
|
|
26
|
+
# No PyYAML dependency.
|
|
27
|
+
read_cfg() {
|
|
28
|
+
awk -v key="$1" '
|
|
29
|
+
$0 ~ ("^" key ":[[:space:]]*") {
|
|
30
|
+
sub("^" key ":[[:space:]]*", "")
|
|
31
|
+
sub(/[[:space:]]*#.*$/, "")
|
|
32
|
+
sub(/^[[:space:]]+/, ""); sub(/[[:space:]]+$/, "")
|
|
33
|
+
sub(/^["\047]/, ""); sub(/["\047]$/, "")
|
|
34
|
+
print
|
|
35
|
+
exit
|
|
36
|
+
}
|
|
37
|
+
' "$CFG"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
target="${1:-}"
|
|
41
|
+
if [ -z "$target" ]; then
|
|
42
|
+
echo "Usage: $(basename "$0") <path-or-agent-suffix>" >&2
|
|
43
|
+
exit 2
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
if [ -d "$target" ]; then
|
|
47
|
+
DIR="$target"
|
|
48
|
+
else
|
|
49
|
+
SERVER_YAML_RAW="$(read_cfg culture_server_yaml)"
|
|
50
|
+
SERVER_YAML="${SERVER_YAML_RAW/#\~/$HOME}"
|
|
51
|
+
if [ ! -f "$SERVER_YAML" ]; then
|
|
52
|
+
echo "no server manifest at $SERVER_YAML — set culture_server_yaml in $CFG" >&2
|
|
53
|
+
echo "or pass an explicit path instead of suffix '$target'" >&2
|
|
54
|
+
exit 1
|
|
55
|
+
fi
|
|
56
|
+
# Suffix mode parses Culture's server manifest, whose schema is dictated by
|
|
57
|
+
# Culture (not by us) and includes nested mappings — too rich for awk.
|
|
58
|
+
# We use python+PyYAML here, with a friendly install hint if it's missing.
|
|
59
|
+
if ! python3 -c 'import yaml' 2>/dev/null; then
|
|
60
|
+
echo "suffix mode needs Python + PyYAML to parse $SERVER_YAML" >&2
|
|
61
|
+
echo " install: pip install --user pyyaml (or: uv pip install pyyaml)" >&2
|
|
62
|
+
echo " or pass an explicit path instead of suffix '$target'" >&2
|
|
63
|
+
exit 1
|
|
64
|
+
fi
|
|
65
|
+
# Use a dedicated exit code (2) for "unknown suffix" so the steward CLI
|
|
66
|
+
# wrapper can distinguish user errors (typo'd suffix) from env errors
|
|
67
|
+
# (missing manifest / PyYAML).
|
|
68
|
+
if ! DIR=$(python3 - "$SERVER_YAML" "$target" <<'PY'
|
|
69
|
+
import sys, yaml, pathlib
|
|
70
|
+
manifest_path, suffix = sys.argv[1], sys.argv[2]
|
|
71
|
+
m = yaml.safe_load(pathlib.Path(manifest_path).read_text()) or {}
|
|
72
|
+
agents = m.get('agents', {})
|
|
73
|
+
entry = agents.get(suffix)
|
|
74
|
+
if entry is None:
|
|
75
|
+
print(f"no agent registered with suffix {suffix!r} in {manifest_path}", file=sys.stderr)
|
|
76
|
+
sys.exit(2)
|
|
77
|
+
print(entry['directory'] if isinstance(entry, dict) else entry)
|
|
78
|
+
PY
|
|
79
|
+
); then
|
|
80
|
+
exit 2
|
|
81
|
+
fi
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
DIR="${DIR/#\~/$HOME}"
|
|
85
|
+
|
|
86
|
+
echo "=== $DIR/CLAUDE.md ==="
|
|
87
|
+
if [ -f "$DIR/CLAUDE.md" ]; then cat "$DIR/CLAUDE.md"; else echo "(missing)"; fi
|
|
88
|
+
echo
|
|
89
|
+
echo "=== $DIR/culture.yaml ==="
|
|
90
|
+
if [ -f "$DIR/culture.yaml" ]; then cat "$DIR/culture.yaml"; else echo "(missing)"; fi
|
|
91
|
+
echo
|
|
92
|
+
echo "=== $DIR/.claude/skills/ ==="
|
|
93
|
+
found=0
|
|
94
|
+
for s in "$DIR"/.claude/skills/*/SKILL.md; do
|
|
95
|
+
[ -f "$s" ] || continue
|
|
96
|
+
found=1
|
|
97
|
+
name=$(awk '/^name:/{print $2; exit}' "$s")
|
|
98
|
+
desc=$(awk '
|
|
99
|
+
/^description:/ {
|
|
100
|
+
sub(/^description:[[:space:]]*/, "")
|
|
101
|
+
buf = $0
|
|
102
|
+
flag = 1
|
|
103
|
+
next
|
|
104
|
+
}
|
|
105
|
+
flag && /^[a-z_-]+:/ { flag = 0 }
|
|
106
|
+
flag { buf = buf " " $0 }
|
|
107
|
+
END { gsub(/^[[:space:]]+|[[:space:]]+$/, "", buf); print buf }
|
|
108
|
+
' "$s")
|
|
109
|
+
printf " %-30s %s\n" "$name" "${desc:0:120}"
|
|
110
|
+
done
|
|
111
|
+
if [ "$found" -eq 0 ]; then
|
|
112
|
+
echo " (no skills)"
|
|
113
|
+
fi
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pr-review
|
|
3
|
+
description: >
|
|
4
|
+
Steward-specific PR workflow: branch, commit, push, PR, wait for Qodo/Copilot,
|
|
5
|
+
triage, fix, reply, resolve. Adds a portability lint (no absolute /home paths,
|
|
6
|
+
no per-user dotfile refs in committed docs), an alignment-delta check when
|
|
7
|
+
CLAUDE.md or culture.yaml change, and greenfield-aware test/version-bump
|
|
8
|
+
steps. Use when: creating PRs in steward, handling review feedback, or the
|
|
9
|
+
user says "create PR", "review comments", "address feedback", "resolve threads".
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# PR Review — Steward edition
|
|
13
|
+
|
|
14
|
+
Steward's PRs touch agent prompts, `culture.yaml` configs, and cross-project
|
|
15
|
+
guidance. The generic `pr-review` skills don't know that, so they miss two
|
|
16
|
+
classes of bugs Steward keeps producing:
|
|
17
|
+
|
|
18
|
+
- **Path leaks** — committing absolute home-directory paths that work only on
|
|
19
|
+
the author's machine. (PR #1 had four of these.)
|
|
20
|
+
- **Per-user config dependencies** — referencing a dotfile under the user's
|
|
21
|
+
home directory in repo guidance, breaking reproducibility for other
|
|
22
|
+
contributors and CI.
|
|
23
|
+
|
|
24
|
+
This skill specializes Culture's `pr-review` to catch both up front, plus an
|
|
25
|
+
alignment-delta step when Steward-affecting files change. The workflow is
|
|
26
|
+
encapsulated in `scripts/workflow.sh` — follow that, not a manual checklist.
|
|
27
|
+
|
|
28
|
+
## Prerequisites
|
|
29
|
+
|
|
30
|
+
Hard requirements: `gh` (GitHub CLI), `jq`, `bash`, `python3` (stdlib only),
|
|
31
|
+
`curl` (used by `pr-status.sh`).
|
|
32
|
+
|
|
33
|
+
Soft requirement: `PyYAML` is needed **only for suffix mode** of the sibling
|
|
34
|
+
`agent-config` skill, where it parses Culture's server manifest. Path mode
|
|
35
|
+
and every `pr-review` script work without it. If suffix mode runs without
|
|
36
|
+
PyYAML it exits with a clear install hint.
|
|
37
|
+
|
|
38
|
+
Per-machine paths (sibling-project layout) live in
|
|
39
|
+
`.claude/skills.local.yaml`; see the committed `.example` for the schema.
|
|
40
|
+
|
|
41
|
+
## How to run
|
|
42
|
+
|
|
43
|
+
`scripts/workflow.sh` is the entry point. Subcommands:
|
|
44
|
+
|
|
45
|
+
| Command | Purpose |
|
|
46
|
+
|---------|---------|
|
|
47
|
+
| `workflow.sh lint` | Portability lint on the current diff (staged + unstaged). |
|
|
48
|
+
| `workflow.sh poll <PR>` | Fetch and display all review comments. |
|
|
49
|
+
| `workflow.sh delta` | Dump each sibling project's `CLAUDE.md` head + `culture.yaml`. |
|
|
50
|
+
| `workflow.sh reply <PR>` | Batch reply (JSONL on stdin) and resolve threads. |
|
|
51
|
+
| `workflow.sh help` | Print this list. |
|
|
52
|
+
|
|
53
|
+
The vendored single-comment helpers — `pr-reply.sh`, `pr-status.sh` — live
|
|
54
|
+
next to `workflow.sh` and are usable directly when batching isn't appropriate.
|
|
55
|
+
|
|
56
|
+
## End-to-end flow
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
git checkout -b <type>/<desc>
|
|
60
|
+
# ... edit ...
|
|
61
|
+
.claude/skills/pr-review/scripts/workflow.sh lint
|
|
62
|
+
git commit -am "..." && git push -u origin <branch>
|
|
63
|
+
gh pr create --title "..." --body "..." # title <70 chars, body signed "- Claude"
|
|
64
|
+
sleep 300 # wait for Qodo + Copilot
|
|
65
|
+
.claude/skills/pr-review/scripts/workflow.sh poll <PR>
|
|
66
|
+
# triage; if CLAUDE.md/culture.yaml/.claude/skills changed:
|
|
67
|
+
.claude/skills/pr-review/scripts/workflow.sh delta
|
|
68
|
+
# fix, re-lint, push
|
|
69
|
+
.claude/skills/pr-review/scripts/workflow.sh reply <PR> < replies.jsonl
|
|
70
|
+
gh pr checks <PR>
|
|
71
|
+
# Wait for human merge — never merge yourself.
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Branch naming: `fix/<desc>`, `feat/<desc>`, `docs/<desc>`, `skill/<name>`.
|
|
75
|
+
Commit/PR signature: `- Claude` (workspace convention). The reply script
|
|
76
|
+
auto-appends `- Claude` only if the body isn't already signed, so JSONL
|
|
77
|
+
entries can include or omit it.
|
|
78
|
+
|
|
79
|
+
## Triage rules
|
|
80
|
+
|
|
81
|
+
For every comment, decide **FIX** or **PUSHBACK** with reasoning.
|
|
82
|
+
|
|
83
|
+
Default to **FIX** for: portability complaints (always valid for Steward —
|
|
84
|
+
recurring bug class), test or doc requests, style nits aligned with workspace
|
|
85
|
+
conventions.
|
|
86
|
+
|
|
87
|
+
Default to **PUSHBACK** for: architecture opinions that conflict with workspace
|
|
88
|
+
`CLAUDE.md` or the all-backends rule; greenfield false-positives (e.g. "add
|
|
89
|
+
tests" before there's any source — defer to a later PR, don't refuse).
|
|
90
|
+
|
|
91
|
+
### Alignment-delta rule
|
|
92
|
+
|
|
93
|
+
If the PR touches `CLAUDE.md`, `culture.yaml`, or anything under
|
|
94
|
+
`.claude/skills/`, run `workflow.sh delta` **before** declaring FIX or
|
|
95
|
+
PUSHBACK on each comment. The script dumps the head of every sibling
|
|
96
|
+
project's `CLAUDE.md` plus the full `culture.yaml`, using `sibling_projects`
|
|
97
|
+
from `skills.local.yaml`. Note any sibling that needs a follow-up PR and
|
|
98
|
+
mention it in your reply.
|
|
99
|
+
|
|
100
|
+
## Greenfield-aware steps
|
|
101
|
+
|
|
102
|
+
The lint and the workflow script are always-on. Stack-specific steps are
|
|
103
|
+
conditional and currently no-op (greenfield repo):
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
[ -d tests ] && [ -f pyproject.toml ] && uv run pytest tests/ -x -q
|
|
107
|
+
[ -f pyproject.toml ] && bump_version_per_project_convention # see project README
|
|
108
|
+
[ -f .markdownlint-cli2.yaml ] && markdownlint-cli2 "$(git diff --name-only --cached '*.md')"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Revisit each line as the corresponding stack element actually lands.
|
|
112
|
+
|
|
113
|
+
## Reply etiquette
|
|
114
|
+
|
|
115
|
+
Every comment must get a reply — no silent fixes. Always pass `--resolve`
|
|
116
|
+
when batch-replying so threads close automatically. Reference the
|
|
117
|
+
review-comment IDs in the fix-up commit message. Steward currently has no
|
|
118
|
+
SonarCloud integration and isn't a registered mesh agent, so skip the
|
|
119
|
+
sonarclaude check and the post-merge IRC ping that Culture's `pr-review`
|
|
120
|
+
includes — those will return when Steward joins those systems.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Portability lint: catch path leaks and per-user config dependencies in
|
|
3
|
+
# committed docs/configs before they ship in a PR. Steward's recurring bug
|
|
4
|
+
# class.
|
|
5
|
+
#
|
|
6
|
+
# Usage: portability-lint.sh [--all]
|
|
7
|
+
# default: lint files modified vs HEAD (staged + unstaged)
|
|
8
|
+
# --all: lint all tracked files
|
|
9
|
+
#
|
|
10
|
+
# Exits 0 if clean, 1 if any leak is found.
|
|
11
|
+
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
|
|
14
|
+
mode="${1:-diff}"
|
|
15
|
+
case "$mode" in
|
|
16
|
+
--all) files=$(git ls-files -- ':(exclude)*.lock') ;;
|
|
17
|
+
diff|--diff) files=$(git diff --diff-filter=AMR --name-only HEAD -- ':(exclude)*.lock') ;;
|
|
18
|
+
*) echo "Usage: $(basename "$0") [--all]" >&2; exit 2 ;;
|
|
19
|
+
esac
|
|
20
|
+
|
|
21
|
+
[ -z "$files" ] && { echo "(no files to check)"; exit 0; }
|
|
22
|
+
|
|
23
|
+
# ----- Check 1: hard-coded /home/<user>/... paths -----
|
|
24
|
+
hits1=$(echo "$files" | xargs -r grep -nE '/home/[a-z][a-z0-9_-]+/' 2>/dev/null || true)
|
|
25
|
+
|
|
26
|
+
# ----- Check 2: per-user dotfile *config* refs in committed docs/configs -----
|
|
27
|
+
# Carve-outs (allowed, NOT flagged):
|
|
28
|
+
# - ~/.claude/skills/<x>/scripts/ vendored tool calls
|
|
29
|
+
# - ~/.culture/ Culture mesh data this skill is supposed to read
|
|
30
|
+
md_yaml=$(echo "$files" | grep -E '\.(md|ya?ml|toml|json|jsonc)$' || true)
|
|
31
|
+
if [ -n "$md_yaml" ]; then
|
|
32
|
+
hits2=$(echo "$md_yaml" | xargs -r grep -nE '~/\.[A-Za-z]' 2>/dev/null \
|
|
33
|
+
| grep -vE '~/\.claude/skills/[^[:space:]"]+/scripts/' \
|
|
34
|
+
| grep -vE '~/\.culture/' \
|
|
35
|
+
|| true)
|
|
36
|
+
else
|
|
37
|
+
hits2=""
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
fail=0
|
|
41
|
+
if [ -n "$hits1" ]; then
|
|
42
|
+
echo "❌ Hard-coded /home/<user>/ paths:"
|
|
43
|
+
echo "$hits1" | sed 's/^/ /'
|
|
44
|
+
echo " Fix: use ../sibling, repo URL, or \$WORKSPACE/sibling instead."
|
|
45
|
+
fail=1
|
|
46
|
+
fi
|
|
47
|
+
if [ -n "$hits2" ]; then
|
|
48
|
+
[ "$fail" -eq 1 ] && echo
|
|
49
|
+
echo "❌ Per-user ~/.<dotfile> config refs in committed doc/config:"
|
|
50
|
+
echo "$hits2" | sed 's/^/ /'
|
|
51
|
+
echo " Allowed carve-outs: ~/.claude/skills/.../scripts/ (tool calls), ~/.culture/ (mesh data)."
|
|
52
|
+
echo " Otherwise: commit a repo-local config or document a portable lookup."
|
|
53
|
+
fail=1
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
[ "$fail" -eq 0 ] && echo "✓ portability lint clean ($(echo "$files" | wc -l | tr -d ' ') files checked)"
|
|
57
|
+
exit $fail
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Batch reply to PR review comments from JSONL on stdin.
|
|
5
|
+
# Each line: {"comment_id": 123, "body": "reply text"}
|
|
6
|
+
# Usage: pr-batch.sh [--repo OWNER/REPO] [--resolve] PR_NUMBER < input.jsonl
|
|
7
|
+
|
|
8
|
+
REPO=""
|
|
9
|
+
RESOLVE=false
|
|
10
|
+
|
|
11
|
+
while [[ $# -gt 0 ]]; do
|
|
12
|
+
case "$1" in
|
|
13
|
+
--repo) REPO="$2"; shift 2 ;;
|
|
14
|
+
--resolve) RESOLVE=true; shift ;;
|
|
15
|
+
*) break ;;
|
|
16
|
+
esac
|
|
17
|
+
done
|
|
18
|
+
|
|
19
|
+
PR_NUMBER="${1:?Usage: pr-batch.sh [--repo OWNER/REPO] [--resolve] PR_NUMBER < input.jsonl}"
|
|
20
|
+
|
|
21
|
+
if [[ -z "$REPO" ]]; then
|
|
22
|
+
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
26
|
+
RESOLVE_FLAG=""
|
|
27
|
+
if [[ "$RESOLVE" == true ]]; then
|
|
28
|
+
RESOLVE_FLAG="--resolve"
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
SUCCESS=0
|
|
32
|
+
FAIL=0
|
|
33
|
+
|
|
34
|
+
while IFS= read -r line; do
|
|
35
|
+
# Skip empty lines
|
|
36
|
+
[[ -z "$line" ]] && continue
|
|
37
|
+
|
|
38
|
+
COMMENT_ID=$(echo "$line" | jq -r '.comment_id')
|
|
39
|
+
BODY=$(echo "$line" | jq -r '.body')
|
|
40
|
+
|
|
41
|
+
if [[ "$COMMENT_ID" == "null" || "$BODY" == "null" ]]; then
|
|
42
|
+
echo "SKIP: invalid line: $line"
|
|
43
|
+
((FAIL++)) || true
|
|
44
|
+
continue
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
echo "--- Comment $COMMENT_ID ---"
|
|
48
|
+
if bash "$SCRIPT_DIR/pr-reply.sh" --repo "$REPO" $RESOLVE_FLAG "$PR_NUMBER" "$COMMENT_ID" "$BODY"; then
|
|
49
|
+
((SUCCESS++)) || true
|
|
50
|
+
else
|
|
51
|
+
echo "FAILED: comment $COMMENT_ID"
|
|
52
|
+
((FAIL++)) || true
|
|
53
|
+
fi
|
|
54
|
+
done
|
|
55
|
+
|
|
56
|
+
echo ""
|
|
57
|
+
echo "Done: $SUCCESS succeeded, $FAIL failed"
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Fetch and display all PR feedback in one pass:
|
|
5
|
+
# 1. Inline review comments (with thread resolve status)
|
|
6
|
+
# 2. Issue comments (qodo summaries, sonarcloud, etc.)
|
|
7
|
+
# 3. Top-level reviews with a non-empty body (copilot overview, etc.)
|
|
8
|
+
#
|
|
9
|
+
# Usage: pr-comments.sh [--repo OWNER/REPO] PR_NUMBER
|
|
10
|
+
|
|
11
|
+
REPO=""
|
|
12
|
+
|
|
13
|
+
while [[ $# -gt 0 ]]; do
|
|
14
|
+
case "$1" in
|
|
15
|
+
--repo) REPO="$2"; shift 2 ;;
|
|
16
|
+
*) break ;;
|
|
17
|
+
esac
|
|
18
|
+
done
|
|
19
|
+
|
|
20
|
+
PR_NUMBER="${1:?Usage: pr-comments.sh [--repo OWNER/REPO] PR_NUMBER}"
|
|
21
|
+
|
|
22
|
+
if [[ -z "$REPO" ]]; then
|
|
23
|
+
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# ── Section 1: inline review comments ─────────────────────────────────────
|
|
27
|
+
THREADS_JSON=$(gh api graphql -f query="
|
|
28
|
+
{
|
|
29
|
+
repository(owner: \"${REPO%%/*}\", name: \"${REPO##*/}\") {
|
|
30
|
+
pullRequest(number: $PR_NUMBER) {
|
|
31
|
+
reviewThreads(first: 100) {
|
|
32
|
+
nodes {
|
|
33
|
+
id
|
|
34
|
+
isResolved
|
|
35
|
+
comments(first: 100) {
|
|
36
|
+
nodes { databaseId }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}" --jq '.data.repository.pullRequest.reviewThreads.nodes')
|
|
43
|
+
|
|
44
|
+
# Build a map from every comment ID in every thread → its thread metadata,
|
|
45
|
+
# so replies in a thread also show resolved status (not just the first comment).
|
|
46
|
+
THREAD_MAP=$(echo "$THREADS_JSON" | jq -r '
|
|
47
|
+
[.[] as $t | $t.comments.nodes[] | {
|
|
48
|
+
comment_id: .databaseId,
|
|
49
|
+
thread_id: $t.id,
|
|
50
|
+
resolved: $t.isResolved
|
|
51
|
+
}]
|
|
52
|
+
')
|
|
53
|
+
|
|
54
|
+
INLINE=$(gh api "repos/$REPO/pulls/$PR_NUMBER/comments" --paginate)
|
|
55
|
+
INLINE_COUNT=$(echo "$INLINE" | jq 'length')
|
|
56
|
+
|
|
57
|
+
echo "════════════════ INLINE REVIEW COMMENTS ($INLINE_COUNT) ════════════════"
|
|
58
|
+
echo "$INLINE" | jq -r --argjson threads "$THREAD_MAP" '
|
|
59
|
+
.[] | . as $c |
|
|
60
|
+
($threads | map(select(.comment_id == $c.id)) | first // {resolved: "unknown", thread_id: "?"}) as $t |
|
|
61
|
+
"──────────────────────────────────────────────────",
|
|
62
|
+
"ID: \($c.id) | Thread: \(if $t.resolved == true then "RESOLVED" elif $t.resolved == false then "UNRESOLVED" else "?" end) | Reply-to: \($c.in_reply_to_id // "none")",
|
|
63
|
+
"File: \($c.path):\($c.original_line // $c.line // "?")",
|
|
64
|
+
"Thread ID: \($t.thread_id)",
|
|
65
|
+
"Author: \($c.user.login)",
|
|
66
|
+
"",
|
|
67
|
+
($c.body | split("\n") | if length > 10 then .[:10] + ["... (truncated)"] else . end | join("\n")),
|
|
68
|
+
""
|
|
69
|
+
'
|
|
70
|
+
|
|
71
|
+
# ── Section 2: issue comments (general PR comments) ───────────────────────
|
|
72
|
+
ISSUE=$(gh api "repos/$REPO/issues/$PR_NUMBER/comments" --paginate)
|
|
73
|
+
ISSUE_COUNT=$(echo "$ISSUE" | jq 'length')
|
|
74
|
+
|
|
75
|
+
echo ""
|
|
76
|
+
echo "════════════════ ISSUE COMMENTS ($ISSUE_COUNT) ════════════════"
|
|
77
|
+
echo "$ISSUE" | jq -r '
|
|
78
|
+
.[] |
|
|
79
|
+
"──────────────────────────────────────────────────",
|
|
80
|
+
"ID: \(.id) | Author: \(.user.login) | Created: \(.created_at)",
|
|
81
|
+
"",
|
|
82
|
+
(.body | split("\n") | if length > 10 then .[:10] + ["... (truncated)"] else . end | join("\n")),
|
|
83
|
+
""
|
|
84
|
+
'
|
|
85
|
+
|
|
86
|
+
# ── Section 3: top-level reviews with a body ──────────────────────────────
|
|
87
|
+
REVIEWS=$(gh api "repos/$REPO/pulls/$PR_NUMBER/reviews" --paginate)
|
|
88
|
+
REVIEWS_WITH_BODY=$(echo "$REVIEWS" | jq '[.[] | select((.body // "") != "")]')
|
|
89
|
+
REVIEW_COUNT=$(echo "$REVIEWS_WITH_BODY" | jq 'length')
|
|
90
|
+
|
|
91
|
+
echo ""
|
|
92
|
+
echo "════════════════ TOP-LEVEL REVIEWS ($REVIEW_COUNT) ════════════════"
|
|
93
|
+
echo "$REVIEWS_WITH_BODY" | jq -r '
|
|
94
|
+
.[] |
|
|
95
|
+
"──────────────────────────────────────────────────",
|
|
96
|
+
"Review ID: \(.id) | Author: \(.user.login) | State: \(.state) | Submitted: \(.submitted_at)",
|
|
97
|
+
"",
|
|
98
|
+
(.body | split("\n") | if length > 10 then .[:10] + ["... (truncated)"] else . end | join("\n")),
|
|
99
|
+
""
|
|
100
|
+
'
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Reply to a PR review comment, optionally resolve its thread.
|
|
5
|
+
# Usage: pr-reply.sh [--repo OWNER/REPO] [--resolve] PR_NUMBER COMMENT_ID "body"
|
|
6
|
+
|
|
7
|
+
REPO=""
|
|
8
|
+
RESOLVE=false
|
|
9
|
+
|
|
10
|
+
while [[ $# -gt 0 ]]; do
|
|
11
|
+
case "$1" in
|
|
12
|
+
--repo) REPO="$2"; shift 2 ;;
|
|
13
|
+
--resolve) RESOLVE=true; shift ;;
|
|
14
|
+
*) break ;;
|
|
15
|
+
esac
|
|
16
|
+
done
|
|
17
|
+
|
|
18
|
+
PR_NUMBER="${1:?Usage: pr-reply.sh [--repo OWNER/REPO] [--resolve] PR_NUMBER COMMENT_ID \"body\"}"
|
|
19
|
+
COMMENT_ID="${2:?Missing COMMENT_ID}"
|
|
20
|
+
BODY="${3:?Missing reply body}"
|
|
21
|
+
|
|
22
|
+
if [[ -z "$REPO" ]]; then
|
|
23
|
+
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# Append signature only if the body isn't already signed.
|
|
27
|
+
if ! printf '%s' "$BODY" | grep -qE '(^|\n)[[:space:]]*-[[:space:]]+Claude[[:space:]]*$'; then
|
|
28
|
+
BODY="${BODY}
|
|
29
|
+
|
|
30
|
+
- Claude"
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
# Post reply
|
|
34
|
+
REPLY_URL=$(gh api "repos/$REPO/pulls/$PR_NUMBER/comments/$COMMENT_ID/replies" \
|
|
35
|
+
-f body="$BODY" \
|
|
36
|
+
--jq '.html_url')
|
|
37
|
+
echo "Replied: $REPLY_URL"
|
|
38
|
+
|
|
39
|
+
# Resolve thread if requested
|
|
40
|
+
if [[ "$RESOLVE" == true ]]; then
|
|
41
|
+
# Find the thread ID for this comment
|
|
42
|
+
THREAD_ID=$(gh api graphql -f query="
|
|
43
|
+
{
|
|
44
|
+
repository(owner: \"${REPO%%/*}\", name: \"${REPO##*/}\") {
|
|
45
|
+
pullRequest(number: $PR_NUMBER) {
|
|
46
|
+
reviewThreads(first: 100) {
|
|
47
|
+
nodes {
|
|
48
|
+
id
|
|
49
|
+
comments(first: 100) {
|
|
50
|
+
nodes { databaseId }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}" --jq ".data.repository.pullRequest.reviewThreads.nodes[] | select(any(.comments.nodes[]; .databaseId == $COMMENT_ID)) | .id")
|
|
57
|
+
|
|
58
|
+
if [[ -n "$THREAD_ID" ]]; then
|
|
59
|
+
RESOLVED=$(gh api graphql -f query="
|
|
60
|
+
mutation { resolveReviewThread(input: {threadId: \"$THREAD_ID\"}) { thread { isResolved } } }
|
|
61
|
+
" --jq '.data.resolveReviewThread.thread.isResolved')
|
|
62
|
+
echo "Resolved: $RESOLVED (thread $THREAD_ID)"
|
|
63
|
+
else
|
|
64
|
+
echo "Warning: could not find thread for comment $COMMENT_ID"
|
|
65
|
+
fi
|
|
66
|
+
fi
|