watch-pr-feedback · v1.3.0 · 2026-10-15 · sha256 381595c4a917d619
watch-pr-feedback v1.3.0A
Immutable. This exact content never changes and is served at /api/v1/blob/381595c4a917d619.
---
name: watch-pr-feedback
description: >
Watch the user's open GitHub PRs for normalized feedback, validate each new or edited
actionable item once, and render a bounded decision queue. Read-only by default; attended
mode pauses only when the queue needs acknowledgment.
allowed-tools: "Read,Grep,Glob,Bash(~/.agents/skills/watch-telemetry/scripts/watch_telemetry.py record:*),Bash(~/.agents/skills/pr-status/scripts/gh-pr-list-open.sh:*),Bash(~/.agents/skills/pr-status/scripts/gh-pr-details.sh:*),Bash(~/.agents/skills/pr-status/scripts/gh-pr-feedback.py:*),Bash(~/.agents/skills/pr-status/scripts/gh-pr-checkout.py:*),Bash(gh pr view:*),Bash(gh pr diff:*),Bash(gh pr checks:*),Bash(date:*),mcp__jira__jira_get,AskUserQuestion"
model-tier: standard
model: sonnet
effort: high
version: "1.3.0"
author: "flurdy"
---
# Watch PR Feedback
Watch open PRs for normalized feedback events, independently validate new actionable claims, and
render a concise queue. This skill composes `/pr-status` discovery and inventory behavior with the
read-only validation boundary from `/review-comments`; it does not replace either skill and does
not create another scheduler.
## Usage
```text
/watch-pr-feedback # adaptive, read-only, stop at 18:00
/watch-pr-feedback attended # adaptive, attended, stop at 18:00
/watch-pr-feedback 10m 17 # fixed 10m, read-only, stop at 17:00
/watch-pr-feedback attended 5m 17 # fixed 5m, attended, stop at 17:00
/watch-pr-feedback reset # clear this session's seen/retry state
/watch-pr-feedback recheck # one read-only validation pass, ignoring seen state
/watch-pr-feedback status # show runtime and bounded state summary
```
Parse at most one mode (`attended`; absent means read-only), one positive `\d+m` fixed interval,
and one stop hour from `0` through `23` (default `18`). No interval means adaptive mode. Reject
unknown or duplicate arguments. Resolve today's stop hour in local time and do not start at or past
it.
## Execution telemetry
Only for a normal valid execution request, not when reading this file as context, record once:
```text
~/.agents/skills/watch-telemetry/scripts/watch_telemetry.py record watch-pr-feedback {harness} invocation
```
Bind `{harness}` to `pi` or `claude` from the known current harness, never a model name or shell
probe; otherwise skip. Use only already-permitted recording. Never enable collection, change
permissions, or wait for telemetry approval. Missing/denied/failed recording must not block work.
Do not count `status`, `reset`, `recheck`, `disposition`, or internal ticks as invocations. The
self-contained tick prompt owns tick recording; do not record again on a nested skill read.
See [the counter contract](../watch-telemetry/SKILL.md) for opt-in, partial coverage and retention.
## Safety boundary
The named execution-counter helper above is the sole local-write exception and is not a preflight
probe. It never stores feedback, queue state, or review content. All other write prohibitions below
remain unchanged; optional counters do not authorize persisted watcher state.
Default read-only mode never prompts and never edits code, changes the working tree, runs commands
that may create build artifacts, publishes GitHub content, changes thread state, or changes Git
history. It may read local files and metadata, inspect a PR diff, existing tests, and CI. Attended
mode has the same mutation boundary; its question only controls acknowledgment and watcher state.
A semantic classification, validation outcome, or answer is never permission to mutate code or
GitHub. Stop the watcher and invoke the appropriate attended workflow separately for any action.
Do not switch branches, fetch remotes, or create a checkout. Never assume the current directory is
the checkout for a PR merely because repository names look related. Do not run shell, Git,
filesystem, workspace, or authentication preflight probes when starting the watcher. In ticks,
never improvise a shell loop or workspace scan; use only the allowlisted checkout helper below.
## Session-local state
Maintain bounded state in this conversation, keyed by `repository`, PR number, and `identity`.
For each key retain its latest `updateKey`, latest `stateKey`, acknowledgment status, and observation
order. Also retain a consecutive failure count per repository/source and an adaptive quiet streak.
- Keep at most 500 identities. Prune oldest terminal or suppressed entries first, then oldest
acknowledged entries. Never add unseen overflow to a full ledger of unacknowledged items; report
`state-capacity` partial and drain it only after acknowledgment frees capacity. If pruning could
make an old actionable item appear new, label the state `state-pruned` and treat any replay as a
recheck rather than fresh feedback.
- The first tick in a new session is a visible baseline: validate and queue current actionable
records once. A no-change poll after that emits no decision items.
- Restarting a stopped watcher in the same session reuses state and does not replay acknowledged
items. A new session has no state and announces a fresh baseline.
- `reset` clears identities, failure counts, and quiet streak, confirms the reset visibly, and makes
the next tick a fresh baseline. It does not start or stop a runtime watcher.
- `recheck` runs one immediate read-only tick against all current actionable records, ignoring seen
state for validation, but does not change acknowledgment state or start another scheduler.
- `status` reports `/watch-status` when protocol v1 exists, plus state size, pending attended items,
prune status, failure streaks, and whether the next poll is a baseline.
Carry the bounded ledger forward as structured turn state rather than inferring it from prose or
counts. If the ledger is unavailable after context loss, announce a fresh baseline instead of
claiming continuity. State is session-local only. Do not write a cache, repository file, GitHub
marker, comment, label, or Beads item to remember feedback.
### Event comparison
For every normalized record:
1. **New** — the full repository/PR/`identity` key is absent.
2. **Materially edited** — the key exists but `updateKey` changed. Validate and queue it again once.
3. **Lifecycle-only** — `updateKey` is unchanged but `stateKey` changed. Render the transition under
suppressed updates; resolved, outdated, dismissed, or self-authored state never prompts.
4. **Duplicate/no-change** — both keys match. Update observation order only; do not validate, queue,
or prompt again.
Never use thread counts, record counts, body text, author names, or list position as identity.
## Start behavior
Normal invocations start a recurring watcher. `reset`, `recheck`, `status`, and the internal `tick`
mode are commands, not starts. Before starting, state the selected read-only/attended mode, cadence,
and local deadline. The first tick lands after about one minute.
### Pi protocol v1
This section is Pi-only. In Claude Code, skip directly to **Claude Code fallback** without probing,
searching for, or discussing Pi. Harness selection comes from the current tool surface; never use
the shell to detect another harness or executable.
If the current harness directly exposes `watch_loop`, use this branch before Claude scheduling:
1. Call `watch_loop` with `action: status` and require `protocolVersion: 1`. If another watch is
`armed`, `running`, or `paused`, do not replace it; show status and point to `/watch-status`,
`/watch-stop`, or `/watch-resume`. Stop on a protocol mismatch.
2. Convert the local deadline to an ISO-8601 `stopAt` value preserving its timezone offset.
3. Start with this self-contained prompt, substituting `{interaction_mode}` and `{cadence_mode}`:
```text
When already permitted, first run `~/.agents/skills/watch-telemetry/scripts/watch_telemetry.py record watch-pr-feedback pi tick` once; telemetry failure must not block the watch. Then Load and follow the skill named `watch-pr-feedback` now in `tick` mode. Interaction mode is `{interaction_mode}` and cadence mode is `{cadence_mode}`. This is one feedback tick, not a watcher start. Re-run open-PR discovery and the normalized inventory, compare bounded session-local identity/update/lifecycle state, and independently validate only new or materially edited actionable records. Use `--compact` for inventory polls; fetch bodies only for new/edited records or explicit rechecks, after factual suppression and within the skill's 20-fetch bound. Never filter or dump full bodies on unchanged polls. For feedback with two to five materially distinct sub-claims, validate each claim and use `mixed — see claim breakdown` only when outcomes differ; never mark the whole record stale unless every actionable claim is stale. For more than five claims, use `unable to validate` and recommend the attended workflow. Render the complete bounded decision queue, but keep routine suppressions silent, render only user-relevant lifecycle/disposition suppressions and actual failures, mention partial status only when partial, and render State summary only for pending attended feedback, pruning/capacity state, a non-zero failure streak, or lost ledger continuity. Only a non-baseline, non-recheck tick with complete inventories, exclusively unchanged duplicate records, and no lifecycle transitions, pending candidates, capacity/pruning notices, or failures renders the compact quiet-tick summary required by the skill. In read-only mode never ask a question. In attended mode ask exactly once only when the actionable queue is non-empty, and wait for the answer. Do not mutate code, Git, GitHub, or tracking state. Never run ad-hoc shell/workspace probes; use only the allowlisted checkout helper for optional local evidence. Never call `gh api` directly; use only the allowlisted feedback helper for comment content. Keep healthy internal ledger and fetch state silent: do not print `inventories complete`, `partial: false`, or empty errors. Never add a prose recap after the final cadence line. Finish only after visible output with the matching protocol-v1 `watch_loop` action: complete. In adaptive mode pass the numeric N from the final `next-tick:` line as `delaySeconds`; in fixed mode omit it. On the third consecutive partial failure for the same repository/source, stop instead of scheduling another retry.
```
4. Adaptive read-only start:
```yaml
action: start
protocolVersion: 1
label: PR feedback
mode: adaptive
initialDelaySeconds: 60
missedCompletionPolicy: retry
stopAt: <today's local deadline as ISO-8601>
tickPrompt: <prompt above with read-only and adaptive>
```
5. Adaptive attended start uses the same fields except:
```yaml
mode: adaptive
initialDelaySeconds: 60
missedCompletionPolicy: pause
```
6. Fixed mode converts minutes to seconds and includes:
```yaml
mode: fixed
initialDelaySeconds: <interval seconds>
intervalSeconds: <interval seconds>
missedCompletionPolicy: <retry for read-only; pause for attended>
stopAt: <today's local deadline as ISO-8601>
```
The runtime clamps scheduling to 60–3600 seconds. `retry` is safe only for the non-interactive
read-only tick. `pause` prevents an unanswered or interrupted attended question from replaying by
itself. A successful start terminates the initiating turn.
### Claude Code fallback
In Claude Code, enter this branch directly without PATH, filesystem, process, binary, or Pi
capability probes or commentary. Use existing Claude scheduling capability; never imitate the Pi
tool. If neither `ScheduleWakeup` nor `/loop` is available, explain that recurring watches are
unsupported and stop.
Before starting the Claude adaptive path, apply the established session-model guard from
`/watch-prs`: if the session uses a Fable model, do not start because a trailing `ScheduleWakeup`
would discard the tick's visible output. Recommend a Sonnet/Opus session or fixed mode instead.
For adaptive mode, schedule a 60-second first wake with a self-contained prompt equivalent to the
Pi tick prompt: replace the Pi recorder call with `~/.agents/skills/watch-telemetry/scripts/watch_telemetry.py record watch-pr-feedback claude tick` before scheduling. Never retain Pi attribution in a Claude wake. Carry the adapted prompt verbatim into every later wake.
Each completed tick computes N, renders all skill-authored content including the
final `next-tick:` cadence line, then calls `ScheduleWakeup` last with N and emits no further
skill-authored text. Stop rather than reschedule past the local deadline or after the third
consecutive failure for one repository/source. In attended mode, ask and wait before the trailing
schedule call; an unanswered question must not create another wake.
For fixed mode, use this self-contained prompt and state the local stop hour:
```text
/loop {interval} When already permitted, first run `~/.agents/skills/watch-telemetry/scripts/watch_telemetry.py record watch-pr-feedback claude tick` once; telemetry failure must not block the watch. Then invoke the watch-pr-feedback skill with exactly the arguments `tick {read-only|attended}`. Render its complete output and preserve its session-local state and safety boundaries. Do not start another watcher or record another invocation.
```
Fixed ticks ignore `next-tick:` for scheduling but still emit it as their final
skill-authored line without another watcher summary. Every fallback tick must load this skill,
enter `tick` mode, render visible output, and preserve the same session-local state and safety
boundary.
Claude Code may append its built-in `※ recap:` after a tick; that harness output is outside this
skill's rendering contract and can be disabled by the user in `/config`. Never duplicate it inside
the watcher or change user configuration automatically.
## Tick mode
### 1. Discover and fetch once
Re-run open-PR discovery every tick:
```bash
~/.agents/skills/pr-status/scripts/gh-pr-list-open.sh
```
Group results by owner/repository. Fetch the normalized schema-v1 inventory using one call per `owner/repo` group,
even when multiple repositories are present:
```bash
~/.agents/skills/pr-status/scripts/gh-pr-feedback.py {owner} {repo} {number1} {number2} ... --compact
```
The `recordFormat: compact` envelope retains completeness/errors/caps and body-free records:
`identity`, `updateKey`, `stateKey`, `pr`, `source`, `lifecycle`, `selfAuthored`,
`hasLaterSelfReply` when present, `authorKind`, `reviewState`, `semanticType`, and `actionability`.
Use the envelope's `repository` with each row's `pr` and `identity` as the ledger key.
Bodies, gists, author names, locations, and response targets are intentionally absent, not empty.
This reduces tool output and context, not the collector's GitHub API requests. Do not use `--pretty`.
Do not pipe the inventory through ad-hoc Python or jq filters or request full inventory bodies as a
fallback. An unsupported compact mode is a fetch failure, not permission to dump the full inventory.
Compare keys before fetching any bodies. Unchanged duplicate rows need no focused calls; lifecycle-only
transitions use factual fields without revalidation. Baseline, recheck, and pending attended rechecks
still follow the bounded body-fetch procedure below. Do not mark an item seen merely because its compact row was fetched;
only factual suppression or completed classification/validation advances its ledger state.
Do not issue overlapping inventory queries; the bounded focused body calls in step 4 are the only
follow-up comment reads. Keep records associated with their repository and PR throughout comparison and rendering. Existing org/workspace discovery behavior belongs to
the shared list script and must remain unchanged. This normalized helper owns GitHub API access for
feedback inventory: never invoke `gh api` directly, use `curl`, or call a bulk compatibility
comments wrapper from this watcher.
### 2. Handle partial data safely
Inspect each envelope's `partial` and `errors` before comparing records.
- Render available records and name every failed repository/source.
- Do not mark absent records as handled, resolved, deleted, or acknowledged.
- Increment only the matching repository/source failure streak. Clear that streak after a complete
fetch for that source.
- Retry incomplete sources on a warm cadence without replaying already acknowledged records.
- After three consecutive partial fetches for the same repository/source, render the retained
queue and stop safely. In Pi use matching `action: complete` with `outcome: stop`; in Claude do
not schedule another wake. Default mode still never prompts.
A missing or malformed envelope counts as a partial failure, not an empty inventory.
### 3. Suppress factual non-candidates
Retain factual state but do not validate or place these records in the decision queue:
- resolved, outdated, or dismissed lifecycle;
- self-authored records or later self-replies that already handle the item;
- unchanged duplicate polls;
- approvals, informational-only notes, and non-actionable bot automated-status noise, but only after
reading the focused body for a new or edited record.
Never suppress a new or edited record solely from semantic hints in compact output. `semanticType`
and `actionability` help order body fetches; they are not enough to dismiss a claim. Lifecycle and
self-authorship/self-reply facts can suppress a record without a body fetch.
Treat unchanged duplicates, routine acknowledgments, informational notes, and non-actionable bot
status noise as silent ledger updates. Render **Suppressed updates** only for a lifecycle-only transition
or when suppression changes the disposition of previously queued or pending feedback; include the
stable identity and transition in those cases. CI annotations remain fix-only findings with no reply
or resolution target.
### 4. Validate new candidates independently
Treat collector `semanticType`, `actionability`, author kind, and model/bot output as triage hints,
not truth. Make at most 20 focused body fetches per tick for new or materially edited records
remaining after factual suppression (or explicit rechecks). Order by blocking or security hint,
human request, question, suggestion/change request, then other CI/bot findings. Include records hinted
as informational or noise: inspect their bodies before semantic suppression. This budget includes
bodies subsequently classified as non-actionable and failed/stale selections; validate at most 20
candidates. Leave overflow unseen and on a warm cadence so the next tick drains it rather than losing it.
For each selected compact record, fetch only that stable identity through the same normalized helper
before semantic classification or validation; compact records never contain validation-ready bodies:
```bash
~/.agents/skills/pr-status/scripts/gh-pr-feedback.py OWNER REPO PR_NUMBER \
--identity IDENTITY --expected-update-key UPDATE_KEY
```
Make at most one focused call per identity/update key per tick. Use the returned body only when
`partial` is false, `selection.status` is `matched`, the selected `updateKey` still matches, and
`bodyTruncated` is false. Recheck lifecycle and self-reply flags on the focused result before
classifying or validating: they may have changed since the poll even if `updateKey` still matches.
Apply factual suppression to the fresh result and retain its fresh `stateKey`.
A `stale` or `not-found` selection is retained for the next normal inventory comparison rather than
validated from obsolete text. A partial or still-truncated result means `unable to validate` and
remains pending on a warm cadence; partial focused fetches follow the same per-source failure bound
as inventories. Never bypass the helper with direct API access. Never acknowledge a failed/stale
selection as handled. No second focused call for the same identity/update key in this tick.
After reading the body, suppress informational/non-actionable content; for remaining candidates gather:
1. PR metadata and head SHA, description, changed paths, and CI state.
2. The PR diff and referenced path/line.
3. Linked requirements from the PR body and available Jira/project documentation.
4. Relevant implementation and existing tests from a matching clean checkout when one is already
available. Resolve it once per repository/head only through:
```bash
~/.agents/skills/pr-status/scripts/gh-pr-checkout.py OWNER/REPO HEAD_SHA \
--timeout REMAINING_SECONDS
```
The helper alone may enumerate registered workspace members and Git worktrees. Never run `for`,
`ls`, `find`, `git -C`, `git worktree`, remote, or HEAD probes. Use local `Read`, `Grep`, and
`Glob` only when `checkout.available` is true, anchored under its returned path.
5. CI results and test changes. Do not run tests in watcher mode; execution can create artifacts and
belongs to an explicitly attended implementation workflow.
If the helper reports no clean exact-head checkout, continue with PR diff, requirements, existing
tests visible in the diff, and CI evidence, then lower confidence. Never substitute an unrelated
or stale checkout. If evidence is missing or contradictory, use `unable to validate` rather than guessing.
For each candidate or materially distinct sub-claim, choose exactly one validation outcome:
- `confirmed defect`
- `valid improvement`
- `question needing an answer`
- `subjective/trade-off decision`
- `false positive/already handled`
- `stale/outdated`
- `out of scope`
- `unable to validate`
`confirmed defect` requires concrete evidence that current behavior violates code intent,
requirements, or a testable invariant. `valid improvement` is worthwhile but not required for
correctness. Questions and subjective decisions are not defects. A security or blocking label does
not raise confidence by itself.
When one feedback record contains two to five materially distinct claims, validate each claim and
render a compact claim breakdown with its own allowed outcome and evidence. Use
`mixed — see claim breakdown` for the record-level outcome when claim outcomes differ; this is the
only aggregate exception to the outcome list. Never label the whole record `stale/outdated` unless
every actionable claim is stale. If a record contains more than five distinct claims, use
`unable to validate` and recommend the attended workflow rather than silently dropping claims.
### 5. Render the bounded decision queue
A tick is **quiet** only when it is not a baseline or recheck, every inventory is complete, every
observed record is an unchanged duplicate, and there are no lifecycle transitions, pending
candidates, capacity/pruning notices, or failures. Render a quiet tick as exactly one summary line
before the cadence line:
```text
Quiet tick — checked {N} PRs; {M} tracked feedback records unchanged.
```
Do not render empty **Decision queue**, **Suppressed updates**, **Fetch failures**, or **State
summary** sections on quiet ticks. Do not list identities or expose `unchanged duplicate poll` as a
user-facing reason; that phrase is internal ledger terminology. The quiet line confirms the poll
without narrating empty categories.
For every non-quiet tick, render a timestamp, mode, baseline/recheck status, and repositories and
PRs checked. Show partial status only when data is partial; do not print healthy defaults such as
`inventories complete`, `partial: false`, or empty errors.
Then show no more than 20 decision rows:
| PR | Author/source | Feedback type | Lifecycle | Validation outcome | Evidence | Confidence | Recommended response |
|---|---|---|---|---|---|---|---|
Evidence cites concrete requirements, diff/path/line, implementation, existing tests, or CI; state
what is unavailable. Confidence is `high`, `medium`, or `low` with concise uncertainty. The
recommended response is read-only guidance such as answer, discuss, fix later, no change, inspect
manually, or recheck. Include the stable identity in a short detail beneath each row so edits and
rechecks are auditable.
On non-quiet ticks with no new or edited candidates, render
`Decision queue: No new actionable feedback.` Do not replace it with a count delta. Render only
non-empty, user-relevant **Suppressed updates** and **Fetch failures** sections. Render **State
summary** only for pending attended feedback, pruning/capacity state, a non-zero failure streak, or
lost ledger continuity. Omit normal identity counts, acknowledgment totals, healthy limits, and
empty sections instead of printing `None`, `No failures`, or equivalent filler.
### 6. Handle interaction mode
**Default read-only mode:** after the full queue is visible, acknowledge its displayed records in
session state except failed/stale body selections, which remain pending as required above, and never prompts. Suppressed factual updates may update their `stateKey` without
becoming actionable.
**Attended mode:** if and only if at least one new or materially edited actionable item is in the
queue, asks exactly once for the whole queue. Use `AskUserQuestion` with one single-select question
and these choices:
- **Open attended workflow (Recommended)** — leave displayed records pending, group their stable
IDs by repository/PR, stop this watcher, and render `/review-comments owner/repo#number id...`
for explicit item selection, validation, local fixes, and later remote gates.
- **Acknowledge** — mark the displayed queue seen and continue, except failed/stale body selections,
which remain pending.
- **Recheck next tick** — leave displayed actionable records unacknowledged so they return once.
- **Stop watcher** — leave them pending and stop after the tick.
If there is no actionable queue, do not ask. A question remains open until answered; never call
`action: complete` or `ScheduleWakeup` while it is open. **Open attended workflow** is a handoff,
not mutation permission: the tick performs no repository or GitHub action, and stops before the
user invokes `/review-comments`. A custom answer may refine read-only validation or acknowledgment,
but refuse any repository or GitHub mutation and point to the attended workflow after stopping this
watcher.
### 7. Complete and pace
End skill-authored visible output with exactly one cadence line. It must be the final watcher text:
do not follow it with a completion sentence, decision recap, or another watcher summary.
```text
next-tick: {hot|warm|cold} (~{N}s) — {reason}
```
- **hot (~180s):** new or materially edited actionable feedback was rendered.
- **warm (~600s):** partial data, an attended recheck, unresolved candidates, lifecycle transitions,
or capacity/pruning notices remain.
- **cold (1200 → 1500 → 1800s):** complete ticks with no new or edited candidates, lifecycle
transitions, pending candidates, capacity/pruning notices, or failures; increment the quiet streak
and reset it on any hot/warm tick. A non-baseline cold tick that meets the stricter quiet predicate
uses the compact quiet rendering above.
In an injected Pi tick, call the matching `action: complete` only after the cadence line and any
attended answer. Use `outcome: continue` and `delaySeconds: N` for adaptive mode; omit the delay for
fixed mode. Use `outcome: stop` for the failure bound, **Open attended workflow**, or **Stop
watcher**. In Claude adaptive mode, render the cadence line before calling `ScheduleWakeup`; scheduling is the
final action and no skill-authored text follows it. Fixed `/loop` owns scheduling and the cadence
remains the final watcher-authored line. Claude Code's optional built-in recap may appear afterward
outside the skill.