gh-tendr — GitHub Issue-Driven Review
A workflow for asynchronous design review and implementation between the user and Claude. The user reviews decisions / proposals via the GitHub Issues UI and leaves comments. Claude polls for new comments on an interval, applies directed changes, posts status replies, and closes issues when work is done.
When to invoke
Trigger when the user says some variant of:
- "Let's run gh-tendr against
<repo>" - "Let's do issue-driven review"
- "Move the decision docs to issues and monitor them"
- "I'll comment on issues from work"
- "Set up the issue queue for the design docs"
The user has already authorized Claude to act autonomously on directed comments. Don't invoke if the user wants synchronous back-and-forth — that's normal interactive work.
Prerequisites
-
ghCLI authenticated (gh auth statusreturns OK) -
The repo exists and you can write issues to it
-
The decision documents (or other reviewable artifacts) live in the repo at known paths
-
The repo MUST be private. This skill is only safe against private repos — issue access controls are coarser than file access, and we routinely write security review notes, threat models, and unannounced product details into issue bodies. Verify visibility before any setup work:
gh repo view "$REPO" --json visibility --jq .visibility # Must return "PRIVATE". If "PUBLIC" or "INTERNAL", refuse to proceed.If the repo is public, halt the skill, tell the user the constraint, and stop. Never offer to switch the repo to private — visibility changes have legal/compliance implications (DMCA notices, contributor license assumptions, archive snapshots) that aren't mine to reverse. If the user wants to move review work to a private repo, that's a manual operator decision; instruct them to make the change in the GitHub UI themselves and re-invoke the skill afterward.
The loop
Step 1 — Setup (one-time)
-
Verify gh auth. If the token is stale, ask the user to run
gh auth loginin their prompt. Don't try to recover automatically. -
Create the standard label set. The kind labels below are sensible defaults; users can extend or rename via the
LABEL_PREFIXparameter (e.g.proposal:orrfc:) and by adding their own project-specific kinds.Kind labels (decision categories):
decision:design— architecture / UX / API decision requireddecision:policy— governance / legal / compliance decision requireddecision:strategy— business / market / IP / patent strategy decisiondecision:security— security-relevant decision required
Process labels (workflow state):
awaiting-review— needs operator eyesawaiting-implementation— decision made, ready to buildblocked-on-operator— explicit human input required
Use
gh label createwith--forcefor idempotency. Skip kind labels that don't apply to the project; add new ones (e.g.decision:performance,decision:data-model) as the workflow needs them. -
For each decision doc, create one issue. The body MUST be richly self-contained — last review-pass found that bare-minimum bodies (1-line summary + link) were cumbersome to triage from the GH UI alone, especially for items the user wanted to compare against the larger backlog. Required structure:
## Summary 2–4 sentences describing the feature/fix in concrete terms. State what changes about the user/operator/system experience after this ships. No "see the doc" — actually summarize. ## Why now 1–2 sentences on the trigger: what surfaced this (security review, user feedback, dependency change, regulatory requirement). Cite the source doc by name and date. ## Concrete proposal The plan in 3–6 bullets. File paths, schemas, function names, env vars. "Add `users.foo` column" not "track foo somewhere." The reader should be able to estimate the change without opening any other file. ## Open decisions The actual asks — bulleted, each phrased as a question with a recommended answer: - Should X be Y or Z? (Recommend Y because …) - Default value for the new env var? (Recommend "off" so existing deploys keep their current behavior) ## Effort + priority Restate the size estimate (S/M/L/XL) and priority tag (P0/P1/…) from the source doc. Repeating these in the issue body lets the user sort by glance without cross-referencing. ## Source - Backlog row: `<BACKLOG_FILE>:<line-or-section>` (e.g. `BACKLOG.md:142` or `ROADMAP.md#milestone-2`) - Decision doc (if separate): `<DOCS_DIR>/<slug>.md` - Permanent link to the doc on the default branch- Title: short feature name with a stable prefix that maps to the source doc/section. Examples:
[backlog/security] Rate limit signup endpoint,[admin/audit #6] Cron health dashboard,[rfc/0007] Multi-tenant data partitioning. The prefix lets the user see which family of work each issue belongs to without opening it. - Labels: apply both the kind label (
decision:design, etc.) AND a source label that mirrors the prefix (source:backlog,source:admin-audit,source:rfc, etc). Source labels help filter the backlog later. - Track the issue→doc mapping in a
.gh-review-state.jsonfile at the repo root (issue number ↔ source doc/section) so the monitor can resolve "which doc does this issue belong to" on every comment. The file is load-bearing for the polling loop — without it, a comment posted to issue #42 has no way to know whether to editdocs/decisions/0007.md,BACKLOG.md#rate-limit, or something else.
Minimum schema (see
examples/state-file.example.jsonfor a fully-populated reference):{ "repo": "owner/name", "issues": { "<issue-number>": { "doc": "<DOCS_DIR>/<slug>.md | <BACKLOG_FILE>", "section": "<anchor-or-null>", "title_prefix": "[backlog/security]", "kind_label": "decision:design", "source_label": "source:backlog", "created_at": "2026-05-13T..." } }, "processed_comment_ids": [/* every comment.id you've acted on */], "last_setup_at": "2026-05-13T..." }processed_comment_idsis the idempotency anchor — checked on every poll so a re-armed monitor or replayedsince=...query doesn't re-act on the same comment. - Title: short feature name with a stable prefix that maps to the source doc/section. Examples:
-
Post a single explanatory comment on each issue: "Comment on this issue with edits / questions / 'please implement' / 'close'. The full doc is at <link>; this issue body summarizes. New decisions you propose in comments will spawn their own issues. I'll respond on the next polling cycle."
Step 2 — Polling loop
Arm a Monitor script that polls for new comments since the last poll. The script structure:
LAST_SEEN_FILE=/tmp/gh-issue-poll-${REPO//\//_}.last
LAST=$(cat "$LAST_SEEN_FILE" 2>/dev/null || date -u +%Y-%m-%dT%H:%M:%SZ)
while true; do
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
gh api -X GET "repos/${REPO}/issues/comments" -f since="$LAST" --paginate --jq '.[] | "ISSUE_COMMENT|\(.issue_url|split("/issues/")[1])|\(.id)|\(.user.login)|\(.created_at)|\(.body | gsub("\n"; "⏎")[0:300])"' 2>/dev/null || true
echo "$NOW" > "$LAST_SEEN_FILE"
LAST="$NOW"
sleep ${INTERVAL:-240}
done
Critical: interval respects the prompt cache TTL (5 min). Pick from one of three zones:
INTERVAL | Behavior | When to use |
|---|---|---|
| 60–270s (under 5 min) | Cache stays warm; cheap + fast per poll | Active review session |
| 300–540s (5–9 min) | Worst of both — cache misses without amortizing | Avoid |
| 1200–1800s (20–30 min) | One cache miss amortized over a long wait | Idle / backgrounded / overnight |
Default to 240s during active review; bump to 1200s when the user signals stepping away ("back in a few hours") and 1800s for overnight. See docs/monitor-script.md for re-arm + rate