|
| 1 | +--- |
| 2 | +created: 2026-06-02 |
| 3 | +updated: 2026-06-02 |
| 4 | +author: builder |
| 5 | +--- |
| 6 | + |
| 7 | +# Skill: activity-feed-watcher |
| 8 | + |
| 9 | +The contract every per-feed activity watcher implements: event |
| 10 | +classification, subscription-union routing, deterministic reactji |
| 11 | +posting, and error escalation. Each |
| 12 | +[`scripts/watcher/<feed>/watcher.sh`](../../scripts/watcher/README.md) |
| 13 | +consults this skill on the LLM-substep paths it has (most paths are |
| 14 | +deterministic and skill-free). |
| 15 | + |
| 16 | +See [`designs/driver.md`](../../designs/driver.md) § Watcher |
| 17 | +subscription model and event routing for the design rationale; this |
| 18 | +skill is the playbook the implementation follows. |
| 19 | + |
| 20 | +## When to use |
| 21 | + |
| 22 | +When implementing a new feed watcher (a per-repo webhook stream, a |
| 23 | +poll-fallback loop, the review-queue endpoint, an assigned-issues |
| 24 | +feed). The skill is the contract; the watcher script is the |
| 25 | +implementation. |
| 26 | + |
| 27 | +## Inputs |
| 28 | + |
| 29 | +- A feed source the watcher polls or subscribes to (a webhook |
| 30 | + endpoint, a `gh api` poll loop, a kriskowal-side endpoint, etc.). |
| 31 | +- Read access to the union of all driver subscription files at |
| 32 | + `journal/drivers/<host>/<lane>.subscriptions`. |
| 33 | +- Write access to the journal for: |
| 34 | + - per-PR event logs at `journal/events/<repo>--<pr>.log`; |
| 35 | + - jobs posted to `journal/jobs/open/` for events with no |
| 36 | + subscribed driver; |
| 37 | + - gardener-inbox messages for unrecoverable feed failures. |
| 38 | + |
| 39 | +## State |
| 40 | + |
| 41 | +- **Last-seen-id cache** (per-feed, outside the dispatch root): |
| 42 | + `~/.garden-watchers/<feed>/last-seen-id` (or analogous |
| 43 | + per-feed-name path). The watcher reads it on every tick to know |
| 44 | + what events are new; updates it after the events for one tick |
| 45 | + are routed. |
| 46 | +- **ETag cache** (for `If-None-Match`-honoring HTTP feeds): same |
| 47 | + shape as the existing standing-monitor daemons' |
| 48 | + `/tmp/garden-monitor-*` caches; one ETag per polling endpoint. |
| 49 | +- **Subscription union**: read fresh from |
| 50 | + `journal/drivers/<host>/<lane>.subscriptions` on every tick; the |
| 51 | + watcher does not cache the union because subscriptions change |
| 52 | + with driver lifecycle. |
| 53 | + |
| 54 | +## Procedure |
| 55 | + |
| 56 | +A watcher's loop per tick: |
| 57 | + |
| 58 | +1. **Refresh subscriptions.** Read every |
| 59 | + `journal/drivers/<host>/<lane>.subscriptions` file. Build the |
| 60 | + union: a map from `<repo>:<pr>` to the set of subscribing |
| 61 | + lane numbers. The watcher uses this map for the routing |
| 62 | + decision in step 4. |
| 63 | + |
| 64 | +2. **Poll the feed.** Fetch the new events since |
| 65 | + `last-seen-id` (or `If-None-Match` against the cached ETag). |
| 66 | + On a transient error (network failure, 5xx), log and skip the |
| 67 | + tick; systemd's `Restart=on-failure` and the next tick's poll |
| 68 | + resolve the case. |
| 69 | + |
| 70 | +3. **Classify each event.** Each watcher implements a deterministic |
| 71 | + classifier keyed on the event-type field of its feed: |
| 72 | + |
| 73 | + | Event kind | Classification | |
| 74 | + | -------------------------------- | -------------------------- | |
| 75 | + | New comment on a PR | `comment` | |
| 76 | + | Review submission | `review` | |
| 77 | + | Push to a PR's head ref | `push` | |
| 78 | + | CI status change | `ci-status` | |
| 79 | + | Label change | `label` | |
| 80 | + | Assigned-issue update | `assigned-issue` | |
| 81 | + | Issue body mention of the bot | `issue-mention` | |
| 82 | + | Other | `other` (logged, not routed) | |
| 83 | + |
| 84 | + Classification is regex / structural; no LLM is involved. The |
| 85 | + classifier's escalation surface is empty by design. |
| 86 | + |
| 87 | +4. **Route the event.** For each classified event, decide its |
| 88 | + destination: |
| 89 | + |
| 90 | + - If the event names a `<repo>:<pr>` that appears in the |
| 91 | + subscription union, append a message to |
| 92 | + `journal/events/<repo>--<pr>.log` for every subscribed lane |
| 93 | + (a single append is enough; per-lane fan-out happens via the |
| 94 | + driver's read). |
| 95 | + - If the event names a `<repo>:<pr>` *not* in the subscription |
| 96 | + union, post a job to `journal/jobs/open/` with `kind: |
| 97 | + pr-creation` (so any eligible driver lane can claim and bind). |
| 98 | + - If the event is an `assigned-issue` event, post a job with |
| 99 | + `kind: issue-response`. |
| 100 | + - If the event is `other`, log to the watcher's transcript and |
| 101 | + drop. |
| 102 | + |
| 103 | +5. **Post the deterministic reactji.** For every `comment` event |
| 104 | + on any covered repo (subscribed PR or not), the watcher posts |
| 105 | + the `:eyes:` reactji synchronously, *before* the route in step |
| 106 | + 4 lands. The `(comment_id, reaction_id)` pair is recorded in |
| 107 | + the destination event log so downstream agents do not |
| 108 | + double-post. |
| 109 | + |
| 110 | +6. **Persist last-seen.** Update the per-feed `last-seen-id` cache |
| 111 | + so the next tick starts from this point. |
| 112 | + |
| 113 | +7. **Sleep until the next tick.** The polling cadence is per-feed |
| 114 | + (a webhook stream may have no sleep; a `gh api` poll has a 30- |
| 115 | + to 60-second tick budget). |
| 116 | + |
| 117 | +## Output |
| 118 | + |
| 119 | +- Per-PR event log entries at `journal/events/<repo>--<pr>.log` |
| 120 | + (one append per routed event). |
| 121 | +- Posted jobs at `journal/jobs/open/<...>.md` for unsubscribed |
| 122 | + events. |
| 123 | +- Reactji on PR comments (the `:eyes:` deterministic acknowledgment). |
| 124 | +- Updated per-feed `last-seen-id` / ETag caches outside the journal. |
| 125 | + |
| 126 | +## Error escalation |
| 127 | + |
| 128 | +The watcher inherits the uniform error-reporting pattern from |
| 129 | +[`skills/gardener-inbox-error-reporting/SKILL.md`](../gardener-inbox-error-reporting/SKILL.md): |
| 130 | + |
| 131 | +- A `-x` subshell captures the per-tick transcript. |
| 132 | +- An `EXIT` / `ERR` trap discriminates on `$?`. |
| 133 | +- On unexpected exit, the trap hashes the transcript via |
| 134 | + `git hash-object -w --stdin`, appends a section to |
| 135 | + `journal/inboxes/<host>/gardener.md` naming the feed slug, the |
| 136 | + transcript SHA, and a one-paragraph context, and exits non-zero. |
| 137 | +- systemd's `Restart=on-failure` (30-second backoff) brings the |
| 138 | + watcher back automatically. |
| 139 | + |
| 140 | +Persistent crash loops accumulate sections in the gardener inbox; |
| 141 | +the maintainer's next pass sees the pattern and triages. |
| 142 | + |
| 143 | +## Notes |
| 144 | + |
| 145 | +- **One watcher per feed, not per repo.** Multiple repos behind one |
| 146 | + webhook stream share a single watcher process; a repo with its |
| 147 | + own poll cadence runs its own watcher. |
| 148 | +- **No LLM in the routing path.** The watcher is fully |
| 149 | + deterministic. Classification, subscription union, reactji |
| 150 | + posting, and per-PR event-log appends are all bash / regex. |
| 151 | + Routing decisions are not gated on judgment. |
| 152 | +- **Subscription-union freshness.** A driver lane that just took |
| 153 | + on a new subscription must write its `.subscriptions` file |
| 154 | + *before* the watcher's next tick. The watcher does not lock or |
| 155 | + serialize; the race is "the driver missed this tick's event, |
| 156 | + the next tick will pick it up." |
| 157 | +- **Monitoring safety constraint.** Adding a new feed for a repo |
| 158 | + whose comments and PRs are not gated against untrusted |
| 159 | + contributors is gated on explicit maintainer authorization |
| 160 | + recorded in a journal `message` entry, per |
| 161 | + [the constraint in CLAUDE.md](../../CLAUDE.md#monitoring-safety-constraint). |
| 162 | + Watcher daemons are an event-level surveillance surface and |
| 163 | + inherit the same rule. |
| 164 | +- **Coalescence with existing standing monitors.** Phase 1 ships |
| 165 | + one stub feed (`endo-but-for-bots`). Phase 4-5 of the migration |
| 166 | + plan in `designs/driver.md` retires the per-repo standing |
| 167 | + monitors (`/tmp/garden-monitor-*.log` poll daemons) on a per- |
| 168 | + feed basis after observed equivalence. |
0 commit comments