Skip to content

Commit 5238968

Browse files
endolinbotclaude
andcommitted
feat(skills): driver state machine, prompt-on-failure capture, activity-feed watcher (#3)
Rename skills/driver-state-machine to skills/driver-pr-creation-state-machine to fit the per-workflow naming convention (every workflow lives at skills/driver-<kind>-state-machine/SKILL.md). The body is preserved verbatim apart from the renamed title and a redirect note explaining the move. Updated the two callers that referenced the prior name (skills/cleaner/SKILL.md, skills/driver-design-only-pr-workflow/SKILL.md). Two new skill files: skills/prompt-on-failure-capture/SKILL.md the git hash-object / cat-file blob capture pattern, prompt template, claude invocation, and known-SHA short-circuit (per designs/driver.md § Prompt-on-failure capture pattern). skills/activity-feed-watcher/SKILL.md the contract every per-feed watcher daemon implements: event classification, subscription-union routing, deterministic reactji posting, error escalation (per designs/driver.md § Watcher subscription model and event routing). Phase 1 scope of the migration plan. The other named skills (gardener-inbox-error-reporting, driver-pre-ci-validation, and per- workflow state machines for observed-error / issue-response / build-request / design-request / retcon-rebase / ci-recovery) are deferred to Phase 2-5. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 564372a commit 5238968

5 files changed

Lines changed: 359 additions & 9 deletions

File tree

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
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.

skills/cleaner/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Phase 3 lands the real coverage-driven-testing body inside this script; phase 1
1515

1616
## When to use
1717

18-
- The driver posts a `clean` job (per `skills/driver-state-machine/SKILL.md` § `clean` state).
18+
- The driver posts a `clean` job (per `skills/driver-pr-creation-state-machine/SKILL.md` § `clean` state).
1919
- A worker daemon polling either the flat board (`jobs/open/` with `eligible_roles:` containing `cleaner`) or the per-role board (`jobs/cleaner/open/`) claims the job and invokes this script with the claimed path.
2020
- The script does the work (phase 1: no-op stub; phase 3: coverage-driven-testing pass) and moves the job to `done/` or `abandoned/`.
2121

skills/driver-design-only-pr-workflow/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ A design-only PR touches only paths under `<project>/designs/` (typically
1111
`designs/<slug>.md` plus minor README updates); it has no source-touching
1212
CI to wait on, no cleaner pass, and no fixer-loop. The state machine is
1313
the strict subset of
14-
[`skills/driver-state-machine/SKILL.md`](../driver-state-machine/SKILL.md)
14+
[`skills/driver-pr-creation-state-machine/SKILL.md`](../driver-pr-creation-state-machine/SKILL.md)
1515
that the design-only branch needs.
1616

1717
This is the simplest workflow shape and the natural Phase-2 target per

skills/driver-state-machine/SKILL.md renamed to skills/driver-pr-creation-state-machine/SKILL.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
---
22
created: 2026-06-01
3-
updated: 2026-06-01
3+
updated: 2026-06-02
44
author: builder
55
---
66

7-
# Skill: driver-state-machine
7+
# Skill: driver-pr-creation-state-machine
88

99
The PR-creation workflow's states and transition predicates. The driver
10-
loads this skill on entry to a PR-creation-bound subscription. Each
11-
state is a predicate the driver evaluates against live GitHub state plus
12-
its per-lane state file; each transition is either a deterministic
13-
action (run in-process) or a job-board post (delegated to a
14-
role-specific worker).
10+
loads this skill on entry to a PR-creation-bound subscription (or when
11+
it claims a job whose `kind:` is `pr-creation`). Each state is a
12+
predicate the driver evaluates against live GitHub state plus its
13+
per-lane state file; each transition is either a deterministic action
14+
(run in-process) or a job-board post (delegated to a role-specific
15+
worker).
16+
17+
This file was renamed from `skills/driver-state-machine/SKILL.md` on
18+
2026-06-02 to fit the per-workflow naming convention introduced by the
19+
[scripts/ layout pivot](../../designs/driver.md) (every workflow lives
20+
at `skills/driver-<kind>-state-machine/SKILL.md`).
1521

1622
For the smaller design-only PR variant (Phase 2 of the migration), see
1723
the sibling skill at
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
---
2+
created: 2026-06-02
3+
updated: 2026-06-02
4+
author: builder
5+
---
6+
7+
# Skill: prompt-on-failure-capture
8+
9+
The capture-by-SHA pattern that lets a deterministic bash script
10+
(driver, watcher, helper) escalate an unresolvable step to an
11+
ephemeral `claude -p` subagent without inlining a large failure log
12+
into the prompt. The log is hashed into the journal's git object
13+
database; the prompt names the SHA; the subagent reads the blob on
14+
demand via `git cat-file blob`.
15+
16+
See [`designs/driver.md`](../../designs/driver.md) § Prompt-on-failure
17+
capture pattern for the design rationale and the four-slot brief
18+
shape.
19+
20+
## When to use
21+
22+
- A driver tick reaches a state whose deterministic predicate cannot
23+
resolve the next transition (the canonical example: a panel
24+
verdict body that says "almost-approve with one footnote" and the
25+
predicate cannot tell if that is `approve` or `must-fix-loop`).
26+
- A watcher hits a feed event whose classification the regex
27+
filter cannot resolve.
28+
- A helper script (cleaner skeleton, fixer's retcon wrapper) fails
29+
in a way the surrounding bash cannot interpret.
30+
31+
When the deterministic predicate succeeds, this skill is not used:
32+
the script advances on its own. The skill is the escape hatch, not
33+
the default.
34+
35+
## Inputs
36+
37+
- A failure context (a log file, a JSON payload, a diff, a verdict
38+
body) the script captured during its own run.
39+
- The four-slot brief metadata (PR identifier, design path, role,
40+
state machine state).
41+
- A one-paragraph human-authored reason for the escalation (the
42+
context line the prompt template fills).
43+
44+
## State
45+
46+
The capture writes a blob to `$GARDEN_JOURNAL`'s object database
47+
via `git hash-object -w --stdin`. The blob is unreferenced; `git gc`
48+
collects it after the journal's grace window (default 14 days)
49+
unless an operator or agent anchors it:
50+
51+
```sh
52+
git -C "$GARDEN_JOURNAL" update-ref refs/captures/<role>/<pr>/<short-id> <sha>
53+
```
54+
55+
Captures under `refs/captures/` survive indefinitely until explicitly
56+
pruned. Most one-off failures do not need this. Anchoring is
57+
appropriate when:
58+
59+
- The failure shape is one we want to revisit (a recurring CI
60+
flake we want to round-trip into a regression test).
61+
- The escalation produced a non-trivial classification we want to
62+
reuse on identical SHAs (see *Known-SHA short-circuit* below).
63+
- An operator's triage flagged it.
64+
65+
A small lookup table at
66+
`journal/drivers/<host>/<lane>.classifications` may also map
67+
capture SHA → classification verdict so identical bodies reuse the
68+
prior verdict without re-invoking the LLM.
69+
70+
## Procedure
71+
72+
1. **Capture.** Write the relevant output to the journal as a blob:
73+
74+
```sh
75+
LOG_SHA=$(some_command 2>&1 | git -C "$GARDEN_JOURNAL" hash-object -w --stdin)
76+
```
77+
78+
The blob's content is content-addressable: identical inputs
79+
produce identical SHAs. This is the foundation of the known-SHA
80+
short-circuit below.
81+
82+
2. **Promote (optional).** When the failure is one we want to
83+
survive `git gc`, anchor it:
84+
85+
```sh
86+
SHORT_ID=$(printf '%s' "$LOG_SHA" | head -c 7)
87+
git -C "$GARDEN_JOURNAL" update-ref \
88+
"refs/captures/<role>/<pr>/${SHORT_ID}" "$LOG_SHA"
89+
```
90+
91+
3. **Known-SHA short-circuit.** Before constructing the prompt,
92+
consult the per-lane classifications table:
93+
94+
```sh
95+
CLASSIFICATIONS="$GARDEN_JOURNAL/drivers/$GARDEN_HOST/$LANE.classifications"
96+
if [ -f "$CLASSIFICATIONS" ]; then
97+
KNOWN=$(grep "^$LOG_SHA " "$CLASSIFICATIONS" | head -1 | cut -d' ' -f2-)
98+
if [ -n "$KNOWN" ]; then
99+
# apply the known disposition and skip the LLM call
100+
apply_classification "$KNOWN"
101+
return 0
102+
fi
103+
fi
104+
```
105+
106+
The match is exact; near-misses (a CI log with one timestamp
107+
different) hash to a different SHA and re-escalate.
108+
109+
4. **Prompt construct.** Write a prompt file that fills the four
110+
named slots plus the capture SHA and the failure context:
111+
112+
```
113+
You are a subagent classifying an escalation from driver lane $LANE.
114+
115+
PR: $DRIVER_PR
116+
Design: $DESIGN_PATH # or "(none)"
117+
Role: $ESCALATING_ROLE # builder, fixer, etc.
118+
State: $DRIVER_STATE
119+
Capture: $LOG_SHA
120+
121+
Context: $FAILURE_CONTEXT_PARAGRAPH
122+
123+
Read the capture on demand via:
124+
git -C "$GARDEN_JOURNAL" cat-file blob $LOG_SHA
125+
126+
Reply with a structured JSON verdict naming the classification and,
127+
when applicable, the action the driver should take next.
128+
```
129+
130+
The four-slot brief is the minimum. Some workflows add a fifth
131+
slot (the design dependency walk's prior-PR list, the conflict
132+
resolver's parent SHAs).
133+
134+
5. **Invoke.** Pipe the prompt to `claude -p`:
135+
136+
```sh
137+
RESPONSE=$(printf '%s' "$PROMPT" | claude -p --output-format json)
138+
```
139+
140+
The prompt is small (the four-slot brief plus the context
141+
paragraph); the LLM pulls the capture on demand. Token cost
142+
stays bounded by the brief, not the capture's length.
143+
144+
6. **Apply.** Read the response (a patch, a directive, or a
145+
classification), apply it, and record the SHA → verdict mapping
146+
in the per-lane classifications table so the next identical
147+
capture short-circuits.
148+
149+
## Output
150+
151+
- The capture blob's SHA (written to the journal's object DB).
152+
- (Optional) An anchored ref under `refs/captures/<role>/<pr>/<short-id>`.
153+
- The LLM's classification or patch.
154+
- An updated `journal/drivers/<host>/<lane>.classifications` row when
155+
the SHA was unseen.
156+
157+
## Notes
158+
159+
- **Identical SHAs short-circuit.** The whole point of the
160+
capture-by-SHA pattern is that recurring operational flakes (a
161+
test-xs esvu-download failure, an esbuild bundling error with a
162+
stable error trail) hash to the same blob and reuse the prior
163+
classification.
164+
- **The prompt stays small.** Inlining the log defeats the
165+
capture's purpose and reintroduces the per-cycle token cost the
166+
driver design eliminates. The prompt names the SHA and lets the
167+
subagent decide if it needs to read the capture.
168+
- **Sensitive content.** Captures persist in the journal repo
169+
until `git gc` collects them. Secrets that leaked into a
170+
transcript stay readable until the grace window passes. Do not
171+
anchor a capture (`refs/captures/...`) unless its content is
172+
safe to retain indefinitely.
173+
- **No promotion to a tracked file.** Captures live as blobs only;
174+
there is no `journal/captures/` tree of committed snapshots.
175+
The `git hash-object -w` plus optional `update-ref` shape is
176+
the contract.

0 commit comments

Comments
 (0)