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.
Files changed (33) hide show
  1. steward_cli-0.1.2/.claude/skills/agent-config/SKILL.md +112 -0
  2. steward_cli-0.1.2/.claude/skills/agent-config/scripts/show.sh +113 -0
  3. steward_cli-0.1.2/.claude/skills/pr-review/SKILL.md +120 -0
  4. steward_cli-0.1.2/.claude/skills/pr-review/scripts/portability-lint.sh +57 -0
  5. steward_cli-0.1.2/.claude/skills/pr-review/scripts/pr-batch.sh +57 -0
  6. steward_cli-0.1.2/.claude/skills/pr-review/scripts/pr-comments.sh +100 -0
  7. steward_cli-0.1.2/.claude/skills/pr-review/scripts/pr-reply.sh +66 -0
  8. steward_cli-0.1.2/.claude/skills/pr-review/scripts/pr-status.sh +161 -0
  9. steward_cli-0.1.2/.claude/skills/pr-review/scripts/workflow.sh +99 -0
  10. steward_cli-0.1.2/.claude/skills/version-bump/SKILL.md +66 -0
  11. steward_cli-0.1.2/.claude/skills/version-bump/scripts/bump.py +178 -0
  12. steward_cli-0.1.2/.claude/skills.local.yaml.example +14 -0
  13. steward_cli-0.1.2/.flake8 +7 -0
  14. steward_cli-0.1.2/.github/workflows/publish.yml +88 -0
  15. steward_cli-0.1.2/.github/workflows/tests.yml +114 -0
  16. steward_cli-0.1.2/.gitignore +225 -0
  17. steward_cli-0.1.2/.markdownlint-cli2.yaml +19 -0
  18. steward_cli-0.1.2/CHANGELOG.md +53 -0
  19. steward_cli-0.1.2/CLAUDE.md +72 -0
  20. steward_cli-0.1.2/LICENSE +21 -0
  21. steward_cli-0.1.2/PKG-INFO +57 -0
  22. steward_cli-0.1.2/README.md +40 -0
  23. steward_cli-0.1.2/pyproject.toml +71 -0
  24. steward_cli-0.1.2/steward/__init__.py +11 -0
  25. steward_cli-0.1.2/steward/__main__.py +8 -0
  26. steward_cli-0.1.2/steward/cli/__init__.py +82 -0
  27. steward_cli-0.1.2/steward/cli/_commands/__init__.py +0 -0
  28. steward_cli-0.1.2/steward/cli/_commands/show.py +116 -0
  29. steward_cli-0.1.2/steward/cli/_errors.py +37 -0
  30. steward_cli-0.1.2/steward/cli/_output.py +41 -0
  31. steward_cli-0.1.2/tests/__init__.py +0 -0
  32. steward_cli-0.1.2/tests/test_cli.py +105 -0
  33. 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