guild-cli 0.1.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 (54) hide show
  1. guild_cli-0.1.0/.claude/skills/cicd/SKILL.md +207 -0
  2. guild_cli-0.1.0/.claude/skills/cicd/scripts/_resolve-nick.sh +43 -0
  3. guild_cli-0.1.0/.claude/skills/cicd/scripts/portability-lint.sh +57 -0
  4. guild_cli-0.1.0/.claude/skills/cicd/scripts/pr-reply.sh +77 -0
  5. guild_cli-0.1.0/.claude/skills/cicd/scripts/pr-status.sh +163 -0
  6. guild_cli-0.1.0/.claude/skills/cicd/scripts/workflow.sh +157 -0
  7. guild_cli-0.1.0/.claude/skills/communicate/SKILL.md +335 -0
  8. guild_cli-0.1.0/.claude/skills/communicate/scripts/fetch-issues.sh +59 -0
  9. guild_cli-0.1.0/.claude/skills/communicate/scripts/mesh-message.sh +74 -0
  10. guild_cli-0.1.0/.claude/skills/communicate/scripts/post-comment.sh +65 -0
  11. guild_cli-0.1.0/.claude/skills/communicate/scripts/post-issue.sh +71 -0
  12. guild_cli-0.1.0/.claude/skills/communicate/scripts/templates/skill-new-brief.md +85 -0
  13. guild_cli-0.1.0/.claude/skills/communicate/scripts/templates/skill-update-brief.md +101 -0
  14. guild_cli-0.1.0/.claude/skills/doc-test-alignment/SKILL.md +55 -0
  15. guild_cli-0.1.0/.claude/skills/doc-test-alignment/scripts/check.sh +24 -0
  16. guild_cli-0.1.0/.claude/skills/pypi-maintainer/SKILL.md +75 -0
  17. guild_cli-0.1.0/.claude/skills/pypi-maintainer/scripts/switch-source.sh +102 -0
  18. guild_cli-0.1.0/.claude/skills/run-tests/SKILL.md +50 -0
  19. guild_cli-0.1.0/.claude/skills/run-tests/scripts/test.sh +52 -0
  20. guild_cli-0.1.0/.claude/skills/sonarclaude/SKILL.md +84 -0
  21. guild_cli-0.1.0/.claude/skills/sonarclaude/scripts/sonar.sh +263 -0
  22. guild_cli-0.1.0/.claude/skills/version-bump/SKILL.md +66 -0
  23. guild_cli-0.1.0/.claude/skills/version-bump/scripts/bump.py +178 -0
  24. guild_cli-0.1.0/.claude/skills.local.yaml.example +16 -0
  25. guild_cli-0.1.0/.flake8 +7 -0
  26. guild_cli-0.1.0/.github/workflows/publish.yml +88 -0
  27. guild_cli-0.1.0/.github/workflows/tests.yml +135 -0
  28. guild_cli-0.1.0/.gitignore +221 -0
  29. guild_cli-0.1.0/.markdownlint-cli2.yaml +19 -0
  30. guild_cli-0.1.0/CHANGELOG.md +35 -0
  31. guild_cli-0.1.0/CLAUDE.md +164 -0
  32. guild_cli-0.1.0/LICENSE +21 -0
  33. guild_cli-0.1.0/PKG-INFO +82 -0
  34. guild_cli-0.1.0/README.md +64 -0
  35. guild_cli-0.1.0/culture.yaml +3 -0
  36. guild_cli-0.1.0/guild/__init__.py +11 -0
  37. guild_cli-0.1.0/guild/__main__.py +8 -0
  38. guild_cli-0.1.0/guild/cli/__init__.py +86 -0
  39. guild_cli-0.1.0/guild/cli/_commands/__init__.py +22 -0
  40. guild_cli-0.1.0/guild/cli/_commands/explain.py +85 -0
  41. guild_cli-0.1.0/guild/cli/_commands/learn.py +75 -0
  42. guild_cli-0.1.0/guild/cli/_commands/whoami.py +57 -0
  43. guild_cli-0.1.0/guild/cli/_errors.py +37 -0
  44. guild_cli-0.1.0/guild/cli/_output.py +41 -0
  45. guild_cli-0.1.0/guild/cli/_repo.py +116 -0
  46. guild_cli-0.1.0/pyproject.toml +73 -0
  47. guild_cli-0.1.0/tests/__init__.py +0 -0
  48. guild_cli-0.1.0/tests/test_cli.py +78 -0
  49. guild_cli-0.1.0/tests/test_cli_explain.py +41 -0
  50. guild_cli-0.1.0/tests/test_cli_learn.py +39 -0
  51. guild_cli-0.1.0/tests/test_cli_whoami.py +55 -0
  52. guild_cli-0.1.0/tests/test_skills_convention.py +45 -0
  53. guild_cli-0.1.0/tests/test_version_fallback.py +10 -0
  54. guild_cli-0.1.0/uv.lock +471 -0
@@ -0,0 +1,207 @@
1
+ ---
2
+ name: cicd
3
+ description: >
4
+ guildmaster's CI/CD lane, layered on `agex pr`. Delegates lint / open /
5
+ read / reply / delta to agex; adds two extensions — `status`
6
+ (SonarCloud quality gate + hotspots + unresolved-thread tally) and
7
+ `await` (read --wait + status with non-zero exit on Sonar ERROR or
8
+ unresolved threads). Use when: creating PRs in guildmaster, handling
9
+ review feedback, polling CI status, or the user says "create PR",
10
+ "review comments", "address feedback", "resolve threads". Renamed
11
+ from `pr-review` in steward 0.7.0; rebased on agex in 0.12.0.
12
+ ---
13
+
14
+ # CI/CD — guildmaster edition
15
+
16
+ `agex pr` (in `agentculture/agex-cli`) is the upstream for the
17
+ five core PR-lifecycle verbs — `lint`, `open`, `read`, `reply`,
18
+ `delta`. Steward used to vendor parallel scripts for each; in 0.12.0
19
+ those vendored copies were dropped in favor of delegating to `agex`.
20
+ What's left in this skill is **the steward-specific gating layer**:
21
+
22
+ - `status` — SonarCloud quality gate, OPEN issues, hotspots, deploy
23
+ preview URL, unresolved-inline-thread tally.
24
+ - `await` — composes `agex pr read --wait` with `status` and gates on
25
+ Sonar `ERROR` / unresolved threads. The single command to run after
26
+ pushing a fix when you want "wake me when this PR is triage-able."
27
+
28
+ Those two are the steward unique surface today. The `await` combo verb
29
+ landed natively in agex
30
+ ([agex-cli#41](https://github.com/agentculture/agex-cli/issues/41), now
31
+ closed); the gate extras that aren't yet native — SonarCloud hotspots,
32
+ deploy-preview URL, an explicit resolved/unresolved thread tally — are
33
+ tracked upstream in
34
+ [agex-cli#52](https://github.com/agentculture/agex-cli/issues/52) and
35
+ migrate out of this skill once they land.
36
+
37
+ The workflow is encapsulated in `scripts/workflow.sh` — follow that
38
+ (or call `agex pr` directly).
39
+
40
+ ## The agex-cli inversion (upstream-as-consumer)
41
+
42
+ One consumer is special: **agex-cli itself**, the repo that owns `agex pr`.
43
+ Vendoring this skill there verbatim would re-vendor bash that just wraps the
44
+ Python agex-cli already ships, so agex-cli vendors it **adapted-thin**
45
+ ([agex-cli#53](https://github.com/agentculture/agex-cli/pull/53)):
46
+ `workflow.sh` is the only script and it forwards
47
+ `lint | open | read | reply | delta | await` straight to the native
48
+ `agex pr <verb>` — including the native `agex pr await` combo verb (agex-cli
49
+ 0.21.0). The steward `status` / `await` shell extensions and the vendored
50
+ helpers (`pr-reply.sh`, `_resolve-nick.sh`, `portability-lint.sh`) are all
51
+ redundant there, each superseded by a native verb. For that one consumer the
52
+ skill collapses to a **pure delegate**.
53
+
54
+ The only gate bits not yet native are SonarCloud **hotspots**, the
55
+ **deploy-preview URL**, and an explicit **resolved/unresolved thread tally** —
56
+ tracked upstream in
57
+ [agex-cli#52](https://github.com/agentculture/agex-cli/issues/52). Once those
58
+ land, steward retires `pr-status.sh` too and `workflow.sh status/await`
59
+ delegates to native `agex pr` everywhere.
60
+
61
+ **For broadcasts:** a skill-update brief to agex-cli should expect this thin
62
+ `workflow.sh`-only shape, not steward's five-file layout. (Ref:
63
+ [steward#53](https://github.com/agentculture/steward/issues/53).)
64
+
65
+ ## Prerequisites
66
+
67
+ Hard requirements: `agex` (>=0.1), `gh` (GitHub CLI), `jq`, `bash`,
68
+ `python3` (stdlib only), `curl` (used by `pr-status.sh`).
69
+
70
+ Install agex once:
71
+
72
+ ```bash
73
+ uv tool install agex-cli # or: pip install --user agex-cli
74
+ ```
75
+
76
+ Soft requirement: `PyYAML` is needed **only for suffix mode** of the
77
+ sibling `agent-config` skill, where it parses Culture's server
78
+ manifest. Every `cicd` script works without it; suffix mode prints a
79
+ clear install hint when invoked without it.
80
+
81
+ Per-machine paths (sibling-project layout) live in
82
+ `.claude/skills.local.yaml`; see the committed `.example` for the
83
+ schema. `agex pr delta` reads the same file.
84
+
85
+ ## How to run
86
+
87
+ `scripts/workflow.sh` is the entry point. Subcommands:
88
+
89
+ | Command | What it does |
90
+ |---------|--------------|
91
+ | `workflow.sh lint` | `agex pr lint --exit-on-violation` — portability + alignment-trigger check. |
92
+ | `workflow.sh open [gh-flags]` | `agex pr open --delayed-read`. Creates the PR, then polls 180s for an initial briefing. `--title TITLE` required; body via `--body-file PATH` or stdin. |
93
+ | `workflow.sh read [PR] [--wait N]` | `agex pr read`. One-shot briefing (CI checks, SonarCloud gate + new issues, all comments, next-step footer). Pass `--wait N` to poll up to N seconds for required reviewers. |
94
+ | `workflow.sh reply <PR>` | `agex pr reply <PR>` — batch JSONL replies (stdin) + thread resolve. agex auto-signs from `culture.yaml`. |
95
+ | `workflow.sh delta` | `agex pr delta` — sibling alignment dump. |
96
+ | `workflow.sh status <PR>` | **Steward extension.** `pr-status.sh` — Sonar gate, OPEN issues, hotspots, unresolved-thread breakdown, deploy preview URL. Authoritative gate for `await`. |
97
+ | `workflow.sh await <PR>` | **Steward extension.** `agex pr read --wait` then `status`. Exits non-zero on Sonar ERROR or unresolved threads. Tunables: `STEWARD_PR_AWAIT_WAIT` (default 1800s passed to `--wait`), `STEWARD_PR_AWAIT_SECONDS` (legacy fixed pre-sleep, deprecated). |
98
+ | `workflow.sh help` | Print the list. |
99
+
100
+ You can also call `agex pr <verb>` directly — `workflow.sh` is a
101
+ typing-saver around the same verbs. The steward `status` and `await`
102
+ extensions only have shell entry points.
103
+
104
+ The vendored single-comment helper `pr-reply.sh` (plus its
105
+ `_resolve-nick.sh` dependency) is still shipped — pinned by
106
+ `tests/test_pr_reply_signature.py` and `tests/test_resolve_nick.py`,
107
+ and useful when a one-off reply doesn't merit batch JSONL. It is not
108
+ called by `workflow.sh` anymore. The vendored `portability-lint.sh`
109
+ is also still shipped — `steward doctor`'s portability check runs it
110
+ directly against target repos. Both are scheduled for follow-up
111
+ migration to agex.
112
+
113
+ ## Long waits (background polling)
114
+
115
+ `agex pr read --wait N` polls in-session for up to N seconds. The
116
+ Anthropic prompt cache has a 5-minute TTL; sleeping past it burns
117
+ context every cache miss. Two ways to drive the wait:
118
+
119
+ - **Synchronous** — `workflow.sh await <PR>` after `gh pr create` /
120
+ `workflow.sh open`. Fine when readiness is expected within ~5
121
+ minutes.
122
+ - **Asynchronous** — for longer waits, run `agex pr read --wait NNN`
123
+ inside a background subagent (Agent tool, `run_in_background: true`)
124
+ so the main session only pays the cache cost when readiness fires.
125
+ The subagent's only job is to invoke `agex pr read --wait` and echo
126
+ its headline back. The parent triages with `workflow.sh await`
127
+ when the notification arrives. The user can interrupt with
128
+ TaskStop.
129
+
130
+ This pattern was originally borrowed from sibling repo
131
+ [`agentculture/cfafi`](https://github.com/agentculture/cfafi)'s `poll`
132
+ skill. The async guidance is also filed upstream
133
+ ([agex-cli#41](https://github.com/agentculture/agex-cli/issues/41)).
134
+
135
+ ## Conventions
136
+
137
+ `agex pr` emits a **"Next step:"** footer at the end of every command
138
+ that names the right next verb (the same chain `agex learn cicd`
139
+ documents) — follow that rather than memorizing an order. `workflow.sh
140
+ help` mirrors the verb table when you need the steward-flavored
141
+ extensions (`status`, `await`) on top.
142
+
143
+ Branch naming: `fix/<desc>`, `feat/<desc>`, `docs/<desc>`,
144
+ `skill/<name>`. PR / comment signature: `- <nick> (Claude)`, where
145
+ `<nick>` is resolved by `agex` from the agent's own `culture.yaml`
146
+ (first agent's `suffix`), falling back to the git-repo basename. agex
147
+ auto-appends the signature on `pr open` and `pr reply` only when the
148
+ body isn't already signed.
149
+
150
+ ## Finishing a branch
151
+
152
+ When implementation on a branch is complete and tests pass, go straight
153
+ to `workflow.sh open` (push the branch + open the PR). Do **not** stop to
154
+ present a *merge / PR / keep / discard* menu and wait for a choice — in
155
+ AgentCulture the standing default is **always "push and create a Pull
156
+ Request."** This overrides the Superpowers `finishing-a-development-branch`
157
+ skill's interactive menu, which otherwise pauses the flow at the moment
158
+ work is ready to integrate. Merge-locally, keep-as-is, and discard happen
159
+ only when the user explicitly asks for one of them.
160
+
161
+ ## Triage rules
162
+
163
+ For every comment, decide **FIX** or **PUSHBACK** with reasoning.
164
+
165
+ Default to **FIX** for: portability complaints (always valid for
166
+ Steward — recurring bug class), test or doc requests, style nits
167
+ aligned with workspace conventions.
168
+
169
+ Default to **PUSHBACK** for: architecture opinions that conflict with
170
+ workspace `CLAUDE.md` or the all-backends rule; greenfield
171
+ false-positives (e.g. "add tests" before there's any source — defer
172
+ to a later PR, don't refuse).
173
+
174
+ ### Alignment-delta rule
175
+
176
+ If the PR touches `CLAUDE.md`, `culture.yaml`, or anything under
177
+ `.claude/skills/`, run `workflow.sh delta` **before** declaring FIX or
178
+ PUSHBACK on each comment. Note any sibling that needs a follow-up PR
179
+ and mention it in your reply.
180
+
181
+ ## Greenfield-aware steps
182
+
183
+ The lint and the workflow script are always-on. Stack-specific steps
184
+ are conditional and currently no-op (greenfield repo):
185
+
186
+ ```bash
187
+ [ -d tests ] && [ -f pyproject.toml ] && uv run pytest tests/ -x -q
188
+ [ -f pyproject.toml ] && bump_version_per_project_convention # see project README
189
+ [ -f .markdownlint-cli2.yaml ] && markdownlint-cli2 "$(git diff --name-only --cached '*.md')"
190
+ ```
191
+
192
+ Revisit each line as the corresponding stack element actually lands.
193
+ A `pr lint --extra=tests,version,markdown` ask is filed upstream
194
+ ([agex-cli#41](https://github.com/agentculture/agex-cli/issues/41)).
195
+
196
+ ## Reply etiquette
197
+
198
+ Every comment must get a reply — no silent fixes. `agex pr reply`
199
+ includes thread-resolve by default. Reference the review-comment IDs
200
+ in the fix-up commit message.
201
+
202
+ The `status` extension queries SonarCloud directly (it predates the
203
+ upstream Sonar integration in `agex pr read`). Both surfaces are
204
+ trustworthy — `agex pr read` for display in the briefing, `status` for
205
+ the gate. Steward isn't yet a registered mesh agent, so the
206
+ post-merge IRC ping that Culture's `pr-review` includes is still
207
+ skipped — that returns when Steward joins the mesh.
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Resolve the agent's nick for GitHub message signing.
5
+ # Order: first agent's `suffix` in <repo-root>/culture.yaml,
6
+ # then basename of the git repo root.
7
+ # Prints the nick to stdout. Always exits 0 — pr-reply.sh needs *some*
8
+ # nick to sign with — but if a culture.yaml exists and we couldn't
9
+ # extract a suffix from it, emits a stderr warning so a misconfigured
10
+ # manifest doesn't silently mask itself behind the basename fallback.
11
+
12
+ repo_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
13
+ if [[ -z "$repo_root" ]]; then
14
+ repo_root="$PWD"
15
+ fi
16
+
17
+ manifest="$repo_root/culture.yaml"
18
+
19
+ if [[ -f "$manifest" ]]; then
20
+ if ! command -v python3 >/dev/null 2>&1; then
21
+ echo "_resolve-nick: python3 not found; cannot parse $manifest, falling back to repo basename" >&2
22
+ else
23
+ nick="$(python3 - "$manifest" <<'PY' 2>/dev/null || true
24
+ import re, sys
25
+ path = sys.argv[1]
26
+ with open(path, encoding="utf-8") as f:
27
+ for raw in f:
28
+ line = raw.rstrip("\n")
29
+ m = re.match(r"^[\s-]*\s*suffix:\s*(\S+)", line)
30
+ if m:
31
+ print(m.group(1).strip("'\""))
32
+ break
33
+ PY
34
+ )"
35
+ if [[ -n "$nick" ]]; then
36
+ printf '%s\n' "$nick"
37
+ exit 0
38
+ fi
39
+ echo "_resolve-nick: $manifest exists but no suffix could be parsed; falling back to repo basename" >&2
40
+ fi
41
+ fi
42
+
43
+ basename "$repo_root"
@@ -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,77 @@
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
+ PRINT_BODY=false
10
+
11
+ while [[ $# -gt 0 ]]; do
12
+ case "$1" in
13
+ --repo) REPO="$2"; shift 2 ;;
14
+ --resolve) RESOLVE=true; shift ;;
15
+ --print-body) PRINT_BODY=true; shift ;;
16
+ *) break ;;
17
+ esac
18
+ done
19
+
20
+ PR_NUMBER="${1:?Usage: pr-reply.sh [--repo OWNER/REPO] [--resolve] [--print-body] PR_NUMBER COMMENT_ID \"body\"}"
21
+ COMMENT_ID="${2:?Missing COMMENT_ID}"
22
+ BODY="${3:?Missing reply body}"
23
+
24
+ if [[ "$PRINT_BODY" != true && -z "$REPO" ]]; then
25
+ REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
26
+ fi
27
+
28
+ # Sign with the agent's nick. Resolved per invocation so siblings that
29
+ # vendor this skill pick up their own culture.yaml suffix automatically.
30
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
31
+ NICK="$("$SCRIPT_DIR/_resolve-nick.sh")"
32
+ SIG="- ${NICK} (Claude)"
33
+ if ! printf '%s' "$BODY" | grep -qFx -- "$SIG"; then
34
+ BODY="${BODY}
35
+
36
+ ${SIG}"
37
+ fi
38
+
39
+ if [[ "$PRINT_BODY" == true ]]; then
40
+ printf '%s\n' "$BODY"
41
+ exit 0
42
+ fi
43
+
44
+ # Post reply
45
+ REPLY_URL=$(gh api "repos/$REPO/pulls/$PR_NUMBER/comments/$COMMENT_ID/replies" \
46
+ -f body="$BODY" \
47
+ --jq '.html_url')
48
+ echo "Replied: $REPLY_URL"
49
+
50
+ # Resolve thread if requested
51
+ if [[ "$RESOLVE" == true ]]; then
52
+ # Find the thread ID for this comment
53
+ THREAD_ID=$(gh api graphql -f query="
54
+ {
55
+ repository(owner: \"${REPO%%/*}\", name: \"${REPO##*/}\") {
56
+ pullRequest(number: $PR_NUMBER) {
57
+ reviewThreads(first: 100) {
58
+ nodes {
59
+ id
60
+ comments(first: 100) {
61
+ nodes { databaseId }
62
+ }
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }" --jq ".data.repository.pullRequest.reviewThreads.nodes[] | select(any(.comments.nodes[]; .databaseId == $COMMENT_ID)) | .id")
68
+
69
+ if [[ -n "$THREAD_ID" ]]; then
70
+ RESOLVED=$(gh api graphql -f query="
71
+ mutation { resolveReviewThread(input: {threadId: \"$THREAD_ID\"}) { thread { isResolved } } }
72
+ " --jq '.data.resolveReviewThread.thread.isResolved')
73
+ echo "Resolved: $RESOLVED (thread $THREAD_ID)"
74
+ else
75
+ echo "Warning: could not find thread for comment $COMMENT_ID"
76
+ fi
77
+ fi
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env bash
2
+ # pr-status.sh — one-shot status overview for a Steward PR.
3
+ #
4
+ # Combines five things review feedback usually scatters across:
5
+ # 1. PR state (open / merged / closed) + branch + author
6
+ # 2. CI checks (build / lint / unit / sonarcloud / cf-pages / etc.)
7
+ # 3. Review-bot pipeline status (Copilot, qodo, SonarCloud, Cloudflare)
8
+ # 4. SonarCloud quality gate + open-issue count
9
+ # 5. Inline-thread resolved-vs-unresolved tally
10
+ #
11
+ # Usage: scripts/pr-status.sh [--repo OWNER/REPO] [--sonar-key KEY] PR_NUMBER
12
+ #
13
+ # Defaults:
14
+ # --repo auto-detected via `gh repo view`
15
+ # --sonar-key derived from repo as `<owner>_<name>` (SonarCloud convention)
16
+ #
17
+ # Requires: gh, jq, curl, python3.
18
+
19
+ set -euo pipefail
20
+
21
+ REPO=""
22
+ SONAR_KEY=""
23
+
24
+ while [[ $# -gt 0 ]]; do
25
+ case "$1" in
26
+ --repo) REPO="$2"; shift 2 ;;
27
+ --sonar-key) SONAR_KEY="$2"; shift 2 ;;
28
+ *) break ;;
29
+ esac
30
+ done
31
+
32
+ PR_NUMBER="${1:?Usage: pr-status.sh [--repo OWNER/REPO] [--sonar-key KEY] PR_NUMBER}"
33
+
34
+ if [[ -z "$REPO" ]]; then
35
+ REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
36
+ fi
37
+ # Sonar key precedence: explicit --sonar-key flag > SONAR_PROJECT_KEY env >
38
+ # `<owner>_<repo>` derivation. Mirrors pr-comments.sh so SKILL.md's claim
39
+ # that the env var works for both scripts is true.
40
+ if [[ -z "$SONAR_KEY" ]]; then
41
+ SONAR_KEY="${SONAR_PROJECT_KEY:-${REPO%%/*}_${REPO##*/}}"
42
+ fi
43
+
44
+ # ── 1. PR header ──────────────────────────────────────────────────────────
45
+ PR_JSON=$(gh pr view "$PR_NUMBER" --json \
46
+ number,title,state,isDraft,mergedAt,mergedBy,baseRefName,headRefName,author,url)
47
+
48
+ echo "════════════════════════════════════════════════════════════════════"
49
+ echo "$PR_JSON" | jq -r '
50
+ "PR #\(.number) — \(.title)",
51
+ " \(.url)",
52
+ " Author: \(.author.login)",
53
+ " Branch: \(.headRefName) → \(.baseRefName)",
54
+ " State: \(if .state == "MERGED" then "MERGED at \(.mergedAt) by \(.mergedBy.login)" elif .state == "OPEN" and .isDraft then "OPEN (draft)" else .state end)"
55
+ '
56
+ echo "════════════════════════════════════════════════════════════════════"
57
+
58
+ # ── 2. CI checks ──────────────────────────────────────────────────────────
59
+ echo
60
+ echo "── CI checks ─────────────────────────────────────────────────────────"
61
+ # `gh pr checks` exits non-zero when checks are still pending/failing.
62
+ # We don't care about its exit code here; capture and pretty-print.
63
+ CHECKS=$(gh pr checks "$PR_NUMBER" 2>/dev/null || true)
64
+ if [[ -z "$CHECKS" ]]; then
65
+ echo " (no checks reported)"
66
+ else
67
+ echo "$CHECKS" | awk -F'\t' '
68
+ {
69
+ name = $1
70
+ state = $2
71
+ dur = $3
72
+ sym = "?"
73
+ if (state == "pass") sym = "✅"
74
+ else if (state == "fail") sym = "❌"
75
+ else if (state == "skipping") sym = "⏭"
76
+ else if (state == "pending" || state == "queued" || state == "in_progress") sym = "…"
77
+ printf " %s %-22s %-10s %s\n", sym, name, state, dur
78
+ }
79
+ '
80
+ fi
81
+
82
+ # ── 3. Review bots & comment pipeline ────────────────────────────────────
83
+ echo
84
+ echo "── Review pipeline ───────────────────────────────────────────────────"
85
+
86
+ # Inline-thread tally via GraphQL (resolved vs unresolved).
87
+ THREADS_JSON=$(gh api graphql -f query="
88
+ {
89
+ repository(owner: \"${REPO%%/*}\", name: \"${REPO##*/}\") {
90
+ pullRequest(number: $PR_NUMBER) {
91
+ reviewThreads(first: 100) {
92
+ nodes { id isResolved comments(first: 1) { nodes { author { login } } } }
93
+ }
94
+ }
95
+ }
96
+ }" --jq '.data.repository.pullRequest.reviewThreads.nodes')
97
+
98
+ INLINE_TOTAL=$(echo "$THREADS_JSON" | jq 'length')
99
+ INLINE_RESOLVED=$(echo "$THREADS_JSON" | jq '[.[] | select(.isResolved)] | length')
100
+ INLINE_PENDING=$((INLINE_TOTAL - INLINE_RESOLVED))
101
+
102
+ # Per-bot inline counts.
103
+ COPILOT_INLINE=$(echo "$THREADS_JSON" | jq '[.[] | select((.comments.nodes[0].author.login // "") | startswith("Copilot"))] | length')
104
+ QODO_INLINE=$(echo "$THREADS_JSON" | jq '[.[] | select((.comments.nodes[0].author.login // "") | startswith("qodo"))] | length')
105
+
106
+ # Issue-level comments (qodo summary, sonarcloud quality-gate body, cf-pages preview, etc.).
107
+ # Skip --paginate to avoid array concatenation; per_page=100 covers typical PRs.
108
+ ISSUE=$(gh api "repos/$REPO/issues/$PR_NUMBER/comments?per_page=100")
109
+ QODO_ISSUE=$(echo "$ISSUE" | jq '[.[] | select((.user.login // "") | startswith("qodo"))] | length')
110
+ SONARQUBE_ISSUE=$(echo "$ISSUE" | jq '[.[] | select((.user.login // "") | startswith("sonarqubecloud"))] | length')
111
+ CFPAGES_ISSUE=$(echo "$ISSUE" | jq '[.[] | select((.user.login // "") | test("cloudflare"))] | length')
112
+ COPILOT_TOPLEVEL=$(gh api "repos/$REPO/pulls/$PR_NUMBER/reviews?per_page=100" \
113
+ | jq '[.[] | select((.user.login // "") | startswith("copilot")) | select((.body // "") != "")] | length')
114
+
115
+ # Cloudflare deploy URL hidden in issue-comment bodies (look for pages.dev).
116
+ CF_URL=$(echo "$ISSUE" | jq -r '[.[].body // "" | scan("https?://[a-z0-9.-]+\\.pages\\.dev[^\\s)\"<]*")] | first // ""')
117
+
118
+ printf " %-12s %s\n" "Copilot" "$([[ "$COPILOT_TOPLEVEL" -gt 0 || "$COPILOT_INLINE" -gt 0 ]] && echo "✅ overview×$COPILOT_TOPLEVEL, inline×$COPILOT_INLINE" || echo "— no posts yet")"
119
+ printf " %-12s %s\n" "qodo" "$([[ "$QODO_ISSUE" -gt 0 || "$QODO_INLINE" -gt 0 ]] && echo "✅ summary×$QODO_ISSUE, inline×$QODO_INLINE" || echo "— no posts yet")"
120
+ printf " %-12s %s\n" "Cloudflare" "$([[ -n "$CF_URL" ]] && echo "✅ $CF_URL" || ([[ "$CFPAGES_ISSUE" -gt 0 ]] && echo "✅ ($CFPAGES_ISSUE comments)" || echo "— no deploy preview"))"
121
+
122
+ # ── 4. SonarCloud quality gate + open issues ─────────────────────────────
123
+ SONAR_QG=$(curl -s "https://sonarcloud.io/api/qualitygates/project_status?projectKey=${SONAR_KEY}&pullRequest=${PR_NUMBER}")
124
+ SONAR_QG_STATUS=$(echo "$SONAR_QG" | jq -r '.projectStatus.status // "UNKNOWN"')
125
+ SONAR_OPEN=$(curl -s "https://sonarcloud.io/api/issues/search?componentKeys=${SONAR_KEY}&pullRequest=${PR_NUMBER}&statuses=OPEN,CONFIRMED&ps=1" \
126
+ | jq -r '.total // 0')
127
+ SONAR_HOTSPOTS=$(curl -s "https://sonarcloud.io/api/hotspots/search?projectKey=${SONAR_KEY}&pullRequest=${PR_NUMBER}&status=TO_REVIEW&ps=1" \
128
+ | jq -r '.paging.total // 0')
129
+
130
+ case "$SONAR_QG_STATUS" in
131
+ OK) SONAR_SYM="✅" ;;
132
+ ERROR) SONAR_SYM="❌" ;;
133
+ WARN) SONAR_SYM="⚠ " ;;
134
+ *) SONAR_SYM="?" ;;
135
+ esac
136
+ printf " %-12s %s Quality Gate %s, %d OPEN issue(s), %d hotspot(s)\n" \
137
+ "SonarCloud" "$SONAR_SYM" "$SONAR_QG_STATUS" "$SONAR_OPEN" "$SONAR_HOTSPOTS"
138
+
139
+ # When SonarCloud has OPEN issues, list them — saves a follow-up curl.
140
+ if [[ "$SONAR_OPEN" != "0" ]]; then
141
+ echo
142
+ echo " SonarCloud OPEN issues:"
143
+ curl -s "https://sonarcloud.io/api/issues/search?componentKeys=${SONAR_KEY}&pullRequest=${PR_NUMBER}&statuses=OPEN,CONFIRMED&ps=20" \
144
+ | jq -r '.issues[] | " • [\(.rule)] \(.component | sub("^[^:]+:"; ""))(:\(.line // "?")) (\(.severity)) — \(.message)"'
145
+ fi
146
+
147
+ # ── 5. Tally + summary ────────────────────────────────────────────────────
148
+ echo
149
+ echo "── Inline threads ────────────────────────────────────────────────────"
150
+ printf " Total: %d Resolved: %d Unresolved: %d\n" \
151
+ "$INLINE_TOTAL" "$INLINE_RESOLVED" "$INLINE_PENDING"
152
+
153
+ if [[ "$INLINE_PENDING" -gt 0 ]]; then
154
+ echo
155
+ echo " Unresolved threads:"
156
+ echo "$THREADS_JSON" | jq -r '
157
+ .[] | select(.isResolved == false) |
158
+ " • \(.comments.nodes[0].author.login): thread \(.id)"
159
+ '
160
+ fi
161
+
162
+ echo
163
+ echo "(For full comment bodies: agex pr read --agent claude-code $PR_NUMBER)"