agtag 0.1.3__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 (74) hide show
  1. agtag-0.1.3/.claude/skills/cicd/SKILL.md +164 -0
  2. agtag-0.1.3/.claude/skills/cicd/scripts/_resolve-nick.sh +43 -0
  3. agtag-0.1.3/.claude/skills/cicd/scripts/create-pr-and-wait.sh +100 -0
  4. agtag-0.1.3/.claude/skills/cicd/scripts/poll-readiness.sh +222 -0
  5. agtag-0.1.3/.claude/skills/cicd/scripts/portability-lint.sh +57 -0
  6. agtag-0.1.3/.claude/skills/cicd/scripts/pr-batch.sh +57 -0
  7. agtag-0.1.3/.claude/skills/cicd/scripts/pr-comments.sh +141 -0
  8. agtag-0.1.3/.claude/skills/cicd/scripts/pr-reply.sh +77 -0
  9. agtag-0.1.3/.claude/skills/cicd/scripts/pr-status.sh +179 -0
  10. agtag-0.1.3/.claude/skills/cicd/scripts/wait-and-check.sh +63 -0
  11. agtag-0.1.3/.claude/skills/cicd/scripts/workflow.sh +211 -0
  12. agtag-0.1.3/.claude/skills/communicate/SKILL.md +250 -0
  13. agtag-0.1.3/.claude/skills/communicate/scripts/fetch-issues.sh +57 -0
  14. agtag-0.1.3/.claude/skills/communicate/scripts/mesh-message.sh +74 -0
  15. agtag-0.1.3/.claude/skills/communicate/scripts/templates/skill-update-brief.md +99 -0
  16. agtag-0.1.3/.claude/skills/pypi-maintainer/SKILL.md +75 -0
  17. agtag-0.1.3/.claude/skills/pypi-maintainer/scripts/switch-source.sh +102 -0
  18. agtag-0.1.3/.claude/skills/run-tests/SKILL.md +50 -0
  19. agtag-0.1.3/.claude/skills/run-tests/scripts/test.sh +52 -0
  20. agtag-0.1.3/.claude/skills/version-bump/SKILL.md +66 -0
  21. agtag-0.1.3/.claude/skills/version-bump/scripts/bump.py +178 -0
  22. agtag-0.1.3/.flake8 +3 -0
  23. agtag-0.1.3/.github/workflows/publish.yml +76 -0
  24. agtag-0.1.3/.github/workflows/tests.yml +114 -0
  25. agtag-0.1.3/.gitignore +221 -0
  26. agtag-0.1.3/.markdownlint-cli2.yaml +23 -0
  27. agtag-0.1.3/CHANGELOG.md +45 -0
  28. agtag-0.1.3/CLAUDE.md +32 -0
  29. agtag-0.1.3/LICENSE +21 -0
  30. agtag-0.1.3/PKG-INFO +65 -0
  31. agtag-0.1.3/README.md +47 -0
  32. agtag-0.1.3/agtag/__init__.py +13 -0
  33. agtag-0.1.3/agtag/__main__.py +10 -0
  34. agtag-0.1.3/agtag/cli/__init__.py +126 -0
  35. agtag-0.1.3/agtag/cli/_commands/__init__.py +0 -0
  36. agtag-0.1.3/agtag/cli/_commands/explain.py +33 -0
  37. agtag-0.1.3/agtag/cli/_commands/issue.py +45 -0
  38. agtag-0.1.3/agtag/cli/_commands/issue_fetch.py +45 -0
  39. agtag-0.1.3/agtag/cli/_commands/issue_post.py +37 -0
  40. agtag-0.1.3/agtag/cli/_commands/issue_reply.py +37 -0
  41. agtag-0.1.3/agtag/cli/_commands/learn.py +87 -0
  42. agtag-0.1.3/agtag/cli/_errors.py +41 -0
  43. agtag-0.1.3/agtag/cli/_output.py +53 -0
  44. agtag-0.1.3/agtag/explain/__init__.py +21 -0
  45. agtag-0.1.3/agtag/explain/catalog.py +152 -0
  46. agtag-0.1.3/agtag/issue/__init__.py +0 -0
  47. agtag-0.1.3/agtag/issue/_gh.py +43 -0
  48. agtag-0.1.3/agtag/issue/_sign.py +16 -0
  49. agtag-0.1.3/agtag/issue/fetch.py +54 -0
  50. agtag-0.1.3/agtag/issue/post.py +34 -0
  51. agtag-0.1.3/agtag/issue/reply.py +36 -0
  52. agtag-0.1.3/agtag/nick.py +44 -0
  53. agtag-0.1.3/culture.yaml +3 -0
  54. agtag-0.1.3/docs/commands.md +62 -0
  55. agtag-0.1.3/docs/culture.md +60 -0
  56. agtag-0.1.3/docs/features.md +50 -0
  57. agtag-0.1.3/docs/purpose.md +40 -0
  58. agtag-0.1.3/docs/skill-sources.md +41 -0
  59. agtag-0.1.3/docs/superpowers/plans/2026-05-09-agtag-cli-scaffold.md +3257 -0
  60. agtag-0.1.3/docs/superpowers/specs/2026-05-09-agtag-cli-scaffold-design.md +240 -0
  61. agtag-0.1.3/pyproject.toml +73 -0
  62. agtag-0.1.3/sonar-project.properties +17 -0
  63. agtag-0.1.3/tests/__init__.py +0 -0
  64. agtag-0.1.3/tests/test_cli.py +151 -0
  65. agtag-0.1.3/tests/test_cmd_handlers.py +305 -0
  66. agtag-0.1.3/tests/test_errors_output.py +56 -0
  67. agtag-0.1.3/tests/test_explain_catalog.py +48 -0
  68. agtag-0.1.3/tests/test_gh.py +40 -0
  69. agtag-0.1.3/tests/test_issue_fetch.py +41 -0
  70. agtag-0.1.3/tests/test_issue_post.py +66 -0
  71. agtag-0.1.3/tests/test_issue_reply.py +63 -0
  72. agtag-0.1.3/tests/test_nick.py +66 -0
  73. agtag-0.1.3/tests/test_version.py +10 -0
  74. agtag-0.1.3/uv.lock +471 -0
@@ -0,0 +1,164 @@
1
+ ---
2
+ name: cicd
3
+ description: >
4
+ agtag's CI/CD lane: open PR (auto-wait for Qodo/Copilot), push fixes
5
+ (re-poll bots), triage feedback, reply, resolve. Adds a portability lint
6
+ (no absolute /home paths, no per-user dotfile refs in committed docs),
7
+ an alignment-delta check when CLAUDE.md or culture.yaml change, and
8
+ greenfield-aware test/version-bump steps. Use when: creating PRs in
9
+ agtag, handling review feedback, polling CI status, or the user says
10
+ "create PR", "review comments", "address feedback", "resolve threads".
11
+ Vendored from steward (renamed from `pr-review` in steward 0.7.0).
12
+ ---
13
+
14
+ # CI/CD — agtag edition (vendored from steward)
15
+
16
+ agtag's PRs touch agent prompts, `culture.yaml` configs, and cross-project
17
+ guidance once they begin landing. The generic `pr-review` skills don't know
18
+ that, so they miss two classes of bugs the AgentCulture stack keeps
19
+ producing in PRs of this shape:
20
+
21
+ - **Path leaks** — committing absolute home-directory paths that work only on
22
+ the author's machine.
23
+ - **Per-user config dependencies** — referencing a dotfile under the user's
24
+ home directory in repo guidance, breaking reproducibility for other
25
+ contributors and CI.
26
+
27
+ This skill specializes Culture's `pr-review` flow to catch both up front, plus
28
+ an alignment-delta step when agtag-affecting files (`CLAUDE.md`,
29
+ `culture.yaml`, vendored skills) change. The workflow is
30
+ encapsulated in `scripts/workflow.sh` — follow that, not a manual checklist.
31
+
32
+ ## Prerequisites
33
+
34
+ Hard requirements: `gh` (GitHub CLI), `jq`, `bash`, `python3` (stdlib only),
35
+ `curl` (used by `pr-status.sh`).
36
+
37
+ Soft requirement: `PyYAML` is needed **only for suffix mode** of the sibling
38
+ `agent-config` skill, where it parses Culture's server manifest. Path mode
39
+ and every `cicd` script work without it. If suffix mode runs without
40
+ PyYAML it exits with a clear install hint.
41
+
42
+ Per-machine paths (sibling-project layout) live in
43
+ `.claude/skills.local.yaml`; see the committed `.example` for the schema.
44
+
45
+ ## How to run
46
+
47
+ `scripts/workflow.sh` is the entry point. Subcommands:
48
+
49
+ | Command | Purpose |
50
+ |---------|---------|
51
+ | `workflow.sh lint` | Portability lint on the current diff (staged + unstaged). |
52
+ | `workflow.sh open-pr --title T [--body-file F] [--wait SECS] [...]` | `gh pr create` then sleep 180s (or `--wait SECS`) and fetch reviewer comments in one shot. Use after pushing the initial branch. |
53
+ | `workflow.sh poll <PR>` | Fetch and display all review comments. |
54
+ | `workflow.sh poll-readiness <PR> [--max-iters N] [--interval SECS] [--require LIST]` | Loop until all required reviewers are ready (default `qodo`; pass `--require qodo,copilot` to also gate on Copilot) — or the PR closes / iteration cap hits. Headline on stdout, per-iteration diagnostics on stderr. Direct wrapper around `scripts/poll-readiness.sh`. |
55
+ | `workflow.sh wait-after-push <PR> [--wait SECS]` | Sleep 180s (or `--wait SECS`) then re-fetch comments. Use after pushing fixes. |
56
+ | `workflow.sh await <PR>` | Poll for reviewer readiness (default: 30 × 60s ≈ 30 min cap, requires qodo only; tune with `STEWARD_PR_AWAIT_ITERS`, `STEWARD_PR_AWAIT_INTERVAL`, and `STEWARD_PR_REVIEWERS`), then run `pr-status.sh` (CI checks + SonarCloud quality gate, OPEN issues, hotspots) and `pr-comments.sh` (inline / issue / top-level / SonarCloud-new-issues sections). Exits non-zero on SonarCloud `ERROR` or unresolved threads. Setting the legacy `STEWARD_PR_AWAIT_SECONDS=<n>` falls back to a fixed sleep with a deprecation warning. |
57
+ | `workflow.sh delta` | Dump each sibling project's `CLAUDE.md` head + `culture.yaml`. |
58
+ | `workflow.sh reply <PR>` | Batch reply (JSONL on stdin) and resolve threads. |
59
+ | `workflow.sh help` | Print this list. |
60
+
61
+ The vendored single-comment helpers — `pr-reply.sh`, `pr-status.sh` — live
62
+ next to `workflow.sh` and are usable directly when batching isn't appropriate.
63
+
64
+ ## Polling for reviewer readiness
65
+
66
+ `scripts/poll-readiness.sh` watches a PR until its required reviewers post
67
+ real (not placeholder) feedback, the PR closes, or an iteration cap fires.
68
+ It fetches `gh api` JSON directly — never `pr-comments.sh` output — so
69
+ truncation can't bias the gate. Default required set is qodo only
70
+ (see header comments and `--help` for tunables, env vars, and the `qodo` /
71
+ `copilot` heuristics; Copilot is detected but not required because its
72
+ review bot is silent on agentculture repos in 2026). Heartbeats stream to
73
+ stderr; the final headline is the only thing on stdout.
74
+
75
+ Two ways to drive it:
76
+
77
+ - **Synchronous** — `workflow.sh await <PR>` after `gh pr create`. The
78
+ main session burns context during the wait; fine up to ~5 minutes.
79
+ - **Asynchronous** — for longer waits, run the looper inside a background
80
+ subagent (Agent tool, `run_in_background: true`) so the main session
81
+ only pays the cache cost when readiness fires. The subagent's only job
82
+ is to invoke `poll-readiness.sh` and echo its headline back. The
83
+ parent triages with `workflow.sh await <PR>` when the notification
84
+ arrives. The user can interrupt with TaskStop.
85
+
86
+ This pattern is borrowed from sibling repo
87
+ [`agentculture/cfafi`](https://github.com/agentculture/cfafi)'s `poll`
88
+ skill — Steward vendors only the looper here rather than promoting `poll`
89
+ to its own first-class skill until other Steward verbs need the same
90
+ primitive.
91
+
92
+ ## End-to-end flow
93
+
94
+ ```text
95
+ git checkout -b <type>/<desc>
96
+ # ... edit ...
97
+ .claude/skills/cicd/scripts/workflow.sh lint
98
+ git commit -am "..." && git push -u origin <branch>
99
+ gh pr create --title "..." --body "..." # title <70 chars, body signed "- <nick> (Claude)"
100
+ .claude/skills/cicd/scripts/workflow.sh await <PR> # readiness loop, then CI + SonarCloud + all comments
101
+ # triage; if CLAUDE.md/culture.yaml/.claude/skills changed:
102
+ .claude/skills/cicd/scripts/workflow.sh delta
103
+ # fix, re-lint, push
104
+ .claude/skills/cicd/scripts/workflow.sh reply <PR> < replies.jsonl
105
+ gh pr checks <PR>
106
+ # Wait for human merge — never merge yourself.
107
+ ```
108
+
109
+ Branch naming: `fix/<desc>`, `feat/<desc>`, `docs/<desc>`, `skill/<name>`.
110
+ PR / comment signature: `- <nick> (Claude)`, where `<nick>` comes from
111
+ the agent's own `culture.yaml` — first agent's `suffix` — falling back
112
+ to the git-repo basename when no `culture.yaml` is present. The reply
113
+ script resolves this via `scripts/_resolve-nick.sh` and auto-appends the
114
+ signature only when the body isn't already signed, so JSONL reply
115
+ entries can include or omit it. Hand-rolled `gh pr create` and
116
+ `gh issue comment` calls should follow the same convention.
117
+
118
+ ## Triage rules
119
+
120
+ For every comment, decide **FIX** or **PUSHBACK** with reasoning.
121
+
122
+ Default to **FIX** for: portability complaints (always valid for AgentCulture
123
+ repos — recurring bug class), test or doc requests, style nits aligned with
124
+ workspace conventions.
125
+
126
+ Default to **PUSHBACK** for: architecture opinions that conflict with workspace
127
+ `CLAUDE.md` or the all-backends rule; greenfield false-positives (e.g. "add
128
+ tests" before there's any source — defer to a later PR, don't refuse).
129
+
130
+ ### Alignment-delta rule
131
+
132
+ If the PR touches `CLAUDE.md`, `culture.yaml`, or anything under
133
+ `.claude/skills/`, run `workflow.sh delta` **before** declaring FIX or
134
+ PUSHBACK on each comment. The script dumps the head of every sibling
135
+ project's `CLAUDE.md` plus the full `culture.yaml`, using `sibling_projects`
136
+ from `skills.local.yaml`. Note any sibling that needs a follow-up PR and
137
+ mention it in your reply.
138
+
139
+ ## Greenfield-aware steps
140
+
141
+ The lint and the workflow script are always-on. Stack-specific steps are
142
+ conditional and currently no-op (greenfield repo):
143
+
144
+ ```bash
145
+ [ -d tests ] && [ -f pyproject.toml ] && uv run pytest tests/ -x -q
146
+ [ -f pyproject.toml ] && bump_version_per_project_convention # see project README
147
+ [ -f .markdownlint-cli2.yaml ] && markdownlint-cli2 "$(git diff --name-only --cached '*.md')"
148
+ ```
149
+
150
+ Revisit each line as the corresponding stack element actually lands.
151
+
152
+ ## Reply etiquette
153
+
154
+ Every comment must get a reply — no silent fixes. Always pass `--resolve`
155
+ when batch-replying so threads close automatically. Reference the
156
+ review-comment IDs in the fix-up commit message.
157
+
158
+ SonarCloud is queried in two places: `pr-status.sh` (quality gate, OPEN
159
+ issues, hotspots) and the Section-4 dump in `pr-comments.sh` (new-issue
160
+ list). Both derive the project key as `<owner>_<repo>`; override with
161
+ `SONAR_PROJECT_KEY=<key>` for non-standard naming, and they silently skip
162
+ when the project isn't on SonarCloud. agtag isn't yet a registered mesh
163
+ agent, so the post-merge IRC ping that Culture's `pr-review` includes is
164
+ still skipped — that returns when agtag 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,100 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Create a PR, wait for automated reviewers (qodo, copilot, sonarcloud) to
5
+ # post comments, then dump the feedback. Wraps the manual `gh pr create →
6
+ # sleep 300 → pr-comments.sh` sequence into one invocation.
7
+ #
8
+ # Usage:
9
+ # create-pr-and-wait.sh --title "Title" --body-file PATH [--wait SECS] [extra gh pr create flags...]
10
+ # create-pr-and-wait.sh --title "Title" [--wait SECS] [extra gh pr create flags...] < body-on-stdin
11
+ #
12
+ # Flags:
13
+ # --title TITLE PR title (required)
14
+ # --body-file PATH Read the body from a file. If omitted, reads stdin.
15
+ # --wait SECS How long to sleep after `gh pr create` before fetching
16
+ # comments. Default: 180 (3 min). qodo/copilot/sonarcloud
17
+ # usually post within this window. Use `wait-and-check.sh`
18
+ # to extend by another 3 min if reviewers haven't finished.
19
+ # Any other flags pass through to `gh pr create` (e.g. --base, --reviewer).
20
+ #
21
+ # Behavior:
22
+ # 1. Stage the body to a tempfile and `gh pr create --body-file …` so
23
+ # large self-contained briefs don't hit the OS argv length limit.
24
+ # 2. `sleep $WAIT_SECS` to give reviewers time to post.
25
+ # 3. Run the sibling `pr-comments.sh <PR_NUMBER>` (vendored next to this
26
+ # script) to dump inline + issue + review comments.
27
+ #
28
+ # Exit codes:
29
+ # 0 PR created and feedback fetched (no judgment about whether feedback
30
+ # is clean — caller decides what to do with it).
31
+ # 2 Bad usage (missing --title, or no body source on a TTY).
32
+ # 3 Could not parse PR number from `gh pr create` output.
33
+ # * Whatever `gh pr create` or the comment-fetch step returns.
34
+
35
+ usage() {
36
+ echo "Usage: create-pr-and-wait.sh --title TITLE [--body-file PATH | < stdin] [--wait SECS] [gh pr create flags...]" >&2
37
+ exit 2
38
+ }
39
+
40
+ require_value() {
41
+ if [[ $# -lt 2 ]]; then
42
+ echo "Missing value for $1" >&2
43
+ usage
44
+ fi
45
+ }
46
+
47
+ WAIT_SECS=180
48
+ TITLE=""
49
+ BODY_FILE=""
50
+ PASSTHROUGH=()
51
+
52
+ while [[ $# -gt 0 ]]; do
53
+ case "$1" in
54
+ --title) require_value "$@"; TITLE="$2"; shift 2 ;;
55
+ --body-file) require_value "$@"; BODY_FILE="$2"; shift 2 ;;
56
+ --wait) require_value "$@"; WAIT_SECS="$2"; shift 2 ;;
57
+ -h|--help) usage ;;
58
+ *) PASSTHROUGH+=("$1"); shift ;;
59
+ esac
60
+ done
61
+
62
+ if [[ -z "$TITLE" ]]; then
63
+ usage
64
+ fi
65
+
66
+ # Stage the body to a tempfile and pass `--body-file` to gh. Large
67
+ # self-contained briefs can otherwise hit the OS argv length limit
68
+ # (~128 KB) when passed via `--body "$BODY"`.
69
+ TMP_BODY=$(mktemp -t cicd-create-pr-body.XXXXXX)
70
+ trap 'rm -f "$TMP_BODY"' EXIT
71
+
72
+ if [[ -n "$BODY_FILE" ]]; then
73
+ cat "$BODY_FILE" > "$TMP_BODY"
74
+ elif [[ ! -t 0 ]]; then
75
+ cat > "$TMP_BODY"
76
+ else
77
+ echo "No --body-file given and stdin is a TTY — refusing to hang on cat." >&2
78
+ echo "Pass --body-file PATH or pipe the body in." >&2
79
+ exit 2
80
+ fi
81
+
82
+ # Step 1: create the PR
83
+ PR_URL=$(gh pr create --title "$TITLE" --body-file "$TMP_BODY" "${PASSTHROUGH[@]}")
84
+ echo "Created: $PR_URL"
85
+
86
+ PR_NUM=$(echo "$PR_URL" | grep -oE '[0-9]+$' || true)
87
+ if [[ -z "$PR_NUM" ]]; then
88
+ echo "Could not parse PR number from gh pr create output: $PR_URL" >&2
89
+ exit 3
90
+ fi
91
+
92
+ # Step 2: wait for reviewers
93
+ echo "Waiting ${WAIT_SECS}s for automated reviewers (qodo, copilot, sonarcloud)..."
94
+ sleep "$WAIT_SECS"
95
+
96
+ # Step 3: dump feedback. The cicd skill always vendors pr-comments.sh
97
+ # next to this script — no per-user $HOME fallback (would violate the
98
+ # skills-portability rule).
99
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
100
+ bash "$SCRIPT_DIR/pr-comments.sh" "$PR_NUM"
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # poll-readiness.sh — wait until both automated PR reviewers (qodo + Copilot)
5
+ # have posted their full reviews, OR the PR is merged/closed, OR an iteration
6
+ # cap is hit.
7
+ #
8
+ # Designed to be invoked two ways:
9
+ # (a) Synchronously by `workflow.sh await` (stdin/stdout local to the
10
+ # caller — main session burns context during the wait).
11
+ # (b) Asynchronously by a background subagent (Agent tool with
12
+ # run_in_background:true). The subagent owns the wait so the main
13
+ # session pays the cache cost only once, at completion.
14
+ #
15
+ # Usage:
16
+ # poll-readiness.sh [--repo OWNER/REPO] [--max-iters N] [--interval SECS]
17
+ # [--require LIST] PR_NUMBER
18
+ #
19
+ # Defaults: --max-iters 30, --interval 60, --require qodo (≈30-minute hard cap)
20
+ #
21
+ # --require accepts a comma-separated subset of {qodo,copilot}; the loop
22
+ # exits 0 only when every listed reviewer is "ready" (or PR state flips
23
+ # to MERGED/CLOSED). Override the default via STEWARD_PR_REVIEWERS=...
24
+ # Copilot is *not* required by default — its automated PR-review bot
25
+ # stopped posting top-level reviews on agentculture repos in 2026, so
26
+ # requiring it would cause every wait to TIMEOUT. Re-add `--require
27
+ # qodo,copilot` if Copilot starts posting again.
28
+ #
29
+ # Exit codes:
30
+ # 0 All required reviewers ready, OR PR state is MERGED / CLOSED.
31
+ # 1 TIMEOUT after --max-iters with at least one required reviewer pending.
32
+ # 2 Bad usage.
33
+ #
34
+ # Output:
35
+ # stdout — final headline (≤10 lines), suitable for capture.
36
+ # stderr — per-iteration diagnostics ("still waiting (qodo: …, copilot: …)").
37
+ #
38
+ # Detection heuristics (mirror cfafi's `poll` skill):
39
+ # qodo ready = ISSUE COMMENTS section contains "Code Review by Qodo"
40
+ # AND does NOT contain "Looking for bugs?" (qodo's
41
+ # "still analysing" placeholder).
42
+ # Copilot ready = TOP-LEVEL REVIEWS header reports a count > 0.
43
+ #
44
+ # Dependencies: gh, jq, bash, curl (same as the rest of the skill).
45
+
46
+ usage() {
47
+ cat >&2 <<'EOF'
48
+ Usage: poll-readiness.sh [--repo OWNER/REPO] [--max-iters N] [--interval SECS]
49
+ [--require LIST] PR_NUMBER
50
+
51
+ Defaults: --max-iters 30, --interval 60, --require qodo
52
+ (set STEWARD_PR_REVIEWERS to override the default --require list)
53
+
54
+ --require LIST comma-separated subset of {qodo,copilot}. Exit 0 only
55
+ when every listed reviewer is ready, or PR closes.
56
+ Copilot is not required by default — its bot stopped
57
+ posting top-level reviews on agentculture repos in 2026.
58
+
59
+ Exit 0 when all required reviewers ready (or PR closed),
60
+ exit 1 on TIMEOUT, exit 2 on bad usage.
61
+ EOF
62
+ exit 2
63
+ }
64
+
65
+ require_value() {
66
+ if [[ $# -lt 2 ]]; then
67
+ echo "Missing value for $1" >&2
68
+ usage
69
+ fi
70
+ }
71
+
72
+ REPO=""
73
+ MAX_ITERS=30
74
+ INTERVAL=60
75
+ PR_NUMBER=""
76
+ REQUIRE="${STEWARD_PR_REVIEWERS:-qodo}"
77
+
78
+ while [[ $# -gt 0 ]]; do
79
+ case "$1" in
80
+ --repo) require_value "$@"; REPO="$2"; shift 2 ;;
81
+ --max-iters) require_value "$@"; MAX_ITERS="$2"; shift 2 ;;
82
+ --interval) require_value "$@"; INTERVAL="$2"; shift 2 ;;
83
+ --require) require_value "$@"; REQUIRE="$2"; shift 2 ;;
84
+ -h|--help) usage ;;
85
+ --) shift; break ;;
86
+ -*) echo "Unknown flag: $1" >&2; usage ;;
87
+ *) PR_NUMBER="$1"; shift ;;
88
+ esac
89
+ done
90
+
91
+ [[ -z "$PR_NUMBER" ]] && usage
92
+ [[ "$PR_NUMBER" =~ ^[0-9]+$ ]] || { echo "PR_NUMBER must be a positive integer" >&2; exit 2; }
93
+ [[ "$MAX_ITERS" =~ ^[0-9]+$ ]] || { echo "--max-iters must be a positive integer" >&2; exit 2; }
94
+ [[ "$INTERVAL" =~ ^[0-9]+$ ]] || { echo "--interval must be a positive integer" >&2; exit 2; }
95
+
96
+ # Parse --require into REQUIRE_QODO / REQUIRE_COPILOT booleans. Reject
97
+ # unknown reviewers so a typo (e.g. "qoda") doesn't silently make the
98
+ # loop exit on iteration 1.
99
+ REQUIRE_QODO=0
100
+ REQUIRE_COPILOT=0
101
+ IFS=',' read -ra _REQ <<<"$REQUIRE"
102
+ for r in "${_REQ[@]}"; do
103
+ r="${r// /}"
104
+ case "$r" in
105
+ ""|qodo) REQUIRE_QODO=1 ;;
106
+ copilot) REQUIRE_COPILOT=1 ;;
107
+ *) echo "--require: unknown reviewer '$r' (valid: qodo, copilot)" >&2; exit 2 ;;
108
+ esac
109
+ done
110
+ if [[ $REQUIRE_QODO -eq 0 && $REQUIRE_COPILOT -eq 0 ]]; then
111
+ echo "--require: at least one reviewer must be required" >&2
112
+ exit 2
113
+ fi
114
+
115
+ if [[ -z "$REPO" ]]; then
116
+ REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
117
+ fi
118
+
119
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
120
+ PR_URL="https://github.com/${REPO}/pull/${PR_NUMBER}"
121
+
122
+ emit_headline() {
123
+ # $1 final state, $2 qodo status, $3 copilot status, $4 iter count, $5 next-step hint
124
+ cat <<EOF
125
+ PR URL: ${PR_URL}
126
+ Final state: $1
127
+ qodo: $2
128
+ Copilot: $3
129
+ Iterations: $4 / ${MAX_ITERS}
130
+ Next step: $5
131
+ EOF
132
+ }
133
+
134
+ iter=0
135
+ qodo_status="not-posted"
136
+ copilot_status="not-posted"
137
+
138
+ while (( iter < MAX_ITERS )); do
139
+ iter=$((iter + 1))
140
+
141
+ # 1. Short-circuit on closed/merged.
142
+ pr_state=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json state -q .state 2>/dev/null || echo "UNKNOWN")
143
+ if [[ "$pr_state" == "MERGED" || "$pr_state" == "CLOSED" ]]; then
144
+ emit_headline "$pr_state" "$qodo_status" "$copilot_status" "$iter" \
145
+ "PR was ${pr_state,,} before reviewers finished — no triage needed."
146
+ exit 0
147
+ fi
148
+
149
+ # 2. Fetch raw, untruncated bodies directly from the GitHub API.
150
+ # Reasoning: pr-comments.sh truncates comment bodies for human display,
151
+ # so its output isn't a reliable input for readiness logic — a long
152
+ # qodo comment can have its "Code Review by Qodo" marker fall past the
153
+ # truncation, and a stale "Looking for bugs?" placeholder elsewhere in
154
+ # the dump can flip a real review back to "placeholder-only". Using
155
+ # `gh api` here also avoids the SonarCloud round-trip pr-comments.sh
156
+ # now does on every iteration (qodo PR #17 review, perf bug).
157
+ issue_json=$(gh api "repos/$REPO/issues/$PR_NUMBER/comments" --paginate 2>/dev/null) || {
158
+ echo "iter ${iter}: gh api issues/comments failed; will retry" >&2
159
+ sleep "$INTERVAL"
160
+ continue
161
+ }
162
+ reviews_json=$(gh api "repos/$REPO/pulls/$PR_NUMBER/reviews" --paginate 2>/dev/null) || {
163
+ echo "iter ${iter}: gh api pulls/reviews failed; will retry" >&2
164
+ sleep "$INTERVAL"
165
+ continue
166
+ }
167
+
168
+ # 3. qodo readiness — match the structural <h3>Code Review by Qodo</h3>
169
+ # header, which appears only in the done-state body. Cfafi's bare-text
170
+ # heuristic ("contains 'Code Review by Qodo' AND NOT 'Looking for
171
+ # bugs?'") false-positives when qodo's done review *quotes* either
172
+ # string while reporting bugs about polling code (PR #17 hit this).
173
+ # The placeholder comment uses a different layout (no <h3> wrapper),
174
+ # so the H3 marker alone is enough.
175
+ qodo_real=$(echo "$issue_json" | jq '[
176
+ .[] | select(.user.login == "qodo-code-review[bot]")
177
+ | select(.body | contains("<h3>Code Review by Qodo</h3>"))
178
+ ] | length')
179
+ qodo_any=$(echo "$issue_json" | jq '[
180
+ .[] | select(.user.login == "qodo-code-review[bot]")
181
+ ] | length')
182
+ if (( qodo_real > 0 )); then
183
+ qodo_status="ready"
184
+ elif (( qodo_any > 0 )); then
185
+ qodo_status="placeholder-only"
186
+ else
187
+ qodo_status="not-posted"
188
+ fi
189
+
190
+ # 4. Copilot readiness — at least one top-level review with a non-empty
191
+ # body. Excludes "review whose only content is inline comments".
192
+ copilot_count=$(echo "$reviews_json" | jq '[
193
+ .[] | select((.user.login // "") | startswith("copilot"))
194
+ | select((.body // "") != "")
195
+ ] | length')
196
+ if (( copilot_count > 0 )); then
197
+ copilot_status="ready"
198
+ else
199
+ copilot_status="not-posted"
200
+ fi
201
+
202
+ # 5. Done?
203
+ qodo_ok=1
204
+ copilot_ok=1
205
+ [[ $REQUIRE_QODO -eq 1 && "$qodo_status" != "ready" ]] && qodo_ok=0
206
+ [[ $REQUIRE_COPILOT -eq 1 && "$copilot_status" != "ready" ]] && copilot_ok=0
207
+ if [[ $qodo_ok -eq 1 && $copilot_ok -eq 1 ]]; then
208
+ emit_headline "OPEN" "$qodo_status" "$copilot_status" "$iter" \
209
+ "Required reviewers ready (require=${REQUIRE}); run pr-status.sh and triage."
210
+ exit 0
211
+ fi
212
+
213
+ echo "iter ${iter}/${MAX_ITERS}: qodo=${qodo_status}, copilot=${copilot_status} (require=${REQUIRE}); sleeping ${INTERVAL}s" >&2
214
+ if (( iter < MAX_ITERS )); then
215
+ sleep "$INTERVAL"
216
+ fi
217
+ done
218
+
219
+ # Fell off the loop — TIMEOUT.
220
+ emit_headline "TIMEOUT" "$qodo_status" "$copilot_status" "$MAX_ITERS" \
221
+ "Hit ${MAX_ITERS}-iteration cap; re-run poll-readiness.sh or check PR manually."
222
+ exit 1
@@ -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"