Skip to content
5 changes: 5 additions & 0 deletions .agents/memory/INBOX.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,8 @@ One note per PR that hit friction, four lines:
Would have prevented it: at the start, check for a simulator (xcrun, or an Android emulator with KVM) and, if there is none, ask the human up front to run e2e-device locally.
Cost: blocked
Seen: 2026-09-24

- 2026-09-24 #102 skill: work-issue
What went wrong: the full loop (tests-first implement, subagent review, fix rounds, E2E) ran on a PR that was only skill markdown and a permission allowlist; the implement subagent spent eleven minutes inventing dry runs to stand in for tests, and the human stopped the orchestration as overkill.
Would have prevented it: work-issue takes a light path for issues labelled area:tooling or area:docs with nothing under packages/: one implement round, one review read by the orchestrator, no E2E, ready.
Cost: blocked (human intervened), one wasted review round
2 changes: 2 additions & 0 deletions .agents/memory/LESSONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,5 @@ Caps: 10 entries per section, 40 in total. Over the cap, the next review merges
## cut-release

## work-issue

## steward-pr
7 changes: 7 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"Bash(git commit *)",
"Bash(git push origin *)",
"Bash(git push -u origin *)",
"Bash(git merge origin/*)",
"Bash(gh issue view *)",
"Bash(gh issue list *)",
"Bash(gh issue create *)",
Expand All @@ -31,6 +32,9 @@
"Bash(gh pr comment *)",
"Bash(gh pr review *)",
"Bash(gh pr ready *)",
"Bash(gh pr update-branch *)",
"Bash(gh api repos/{owner}/{repo}/pulls/*)",
"Bash(gh api repos/{owner}/{repo}/issues/*)",
"Bash(gh run list *)",
"Bash(gh run view *)",
"Bash(gh label list *)",
Expand All @@ -46,6 +50,9 @@
"Bash(git reset --hard*)",
"Bash(git branch -D *)",
"Bash(gh release *)",
"Bash(gh api repos/*/merge*)",
"Bash(gh api repos/*/git/refs*)",
"Bash(gh api repos/*/releases*)",
"Bash(gh repo delete *)",
"Bash(npm publish*)"
]
Expand Down
4 changes: 3 additions & 1 deletion .claude/skills/cut-release/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ Read the `cut-release` section of `.agents/memory/LESSONS.md` before starting, p
```bash
git switch -c "release/v$v" && git add -A && git commit -m "release: v$v"
git push -u origin "release/v$v"
gh pr create --title "release: v$v" --body "$(sed -n "/^## $v/,/^## /p" CHANGELOG.md | sed '$d')"
gh pr create --title "release: v$v" --body "$(sed -n "/^## $v/,/^## /p" CHANGELOG.md | sed '$d')

-- agent: cut-release"
```

Stop. Report the PR number, the version and the changelog section.
Expand Down
6 changes: 3 additions & 3 deletions .claude/skills/design-feature/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ Keep it under 500 words. A signature is worth a paragraph.

## 4. Post it and wait

Post the design as one issue comment headed `## Design`. Move the issue to
`status:blocked` with the first line "Question: approve this design?". Stop. Do not file
slices yet.
Post the design as one issue comment headed `## Design`, ending with the line
`-- agent: design-feature`. Move the issue to `status:blocked` with the first line
"Question: approve this design?". Stop. Do not file slices yet.

## 5. After approval

Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/e2e-device/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ criteria table and pick the ones a device can observe. Capture command and outpu

## Record

Fill the PR's "E2E evidence" section:
Fill the PR's "E2E evidence" section, keeping the body's `-- agent:` trailer as its last line:

```bash
gh pr edit <N> --body-file <updated body>
Expand Down
6 changes: 4 additions & 2 deletions .claude/skills/file-issue/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ gh issue list --state all --search "<three or four words>" --limit 10
gh issue list --state all --search "<literal identifier>" --limit 10
```

If one exists, comment on it with what you have and report its number.
If one exists, comment on it with what you have, ending with `-- agent: file-issue`, and
report its number.

## 3. Write it

Expand Down Expand Up @@ -71,7 +72,8 @@ missing`):
### Alternatives considered one line each, or "none"
```

Write the body to the session scratch directory, not into the repo.
End the body with the line `-- agent: file-issue`. Write it to the session scratch
directory, not into the repo.

## 4. Size it and create it

Expand Down
6 changes: 4 additions & 2 deletions .claude/skills/implement-issue/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ them, each as an observable outcome a test can assert through a public API, an e
a port fake's state or CLI output.

If a criterion cannot be phrased that way, post the list as an issue comment with the open
question under it, apply `status:blocked`, and stop. Do not guess.
question under it and `-- agent: implement-issue` as its last line, apply `status:blocked`,
and stop. Do not guess.

Add one criterion of your own when the change is user-visible: the docs. Name the surfaces
from the `writing-user-docs` skill's table (README, `docs/`, website, the shipped skill, CLI
Expand Down Expand Up @@ -68,7 +69,8 @@ gh pr create --draft --title "<type>: <issue title> (#$n)" --body-file <scratch>
```

The PR body follows `.github/PULL_REQUEST_TEMPLATE.md`: the criteria table with the test
that covers each, everything else marked pending.
that covers each, everything else marked pending, and `-- agent: implement-issue` as the last
line.

## 4. Green in checkpoints

Expand Down
4 changes: 3 additions & 1 deletion .claude/skills/review-memory/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ Empty the inbox down to its header plus the notes kept in step 2. Then:
```bash
git add .agents/memory && git commit -m "chore(memory): review $(date +%Y-%m-%d)"
git push -u origin HEAD
gh pr create --title "chore(memory): review $(date +%Y-%m-%d)" --body "<promoted: n, kept: n, dropped: n, pruned: n, mechanisms filed: #...>"
gh pr create --title "chore(memory): review $(date +%Y-%m-%d)" --body "<promoted: n, kept: n, dropped: n, pruned: n, mechanisms filed: #...>

-- agent: review-memory"
gh pr view --json files -q '.files[].path' | grep -v '^\.agents/memory/' && exit 1 # memory only
gh pr merge --squash --delete-branch "memory/$(date +%Y-%m-%d)"
```
Expand Down
3 changes: 2 additions & 1 deletion .claude/skills/review-pr/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ gh pr review <N> --comment --body "<summary>" # should-fix only
gh pr review <N> --approve --body "<summary>" # nothing above nit
```

The summary is three lines at most: verdict with counts, which spec was used, the one thing
The summary and every inline comment end with the line `-- agent: review-pr`, in a dry run
too. The summary is three lines at most: verdict with counts, which spec was used, the one thing
to fix first. Do not list what you checked and found fine.

## Report
Expand Down
161 changes: 161 additions & 0 deletions .claude/skills/steward-pr/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
---
name: steward-pr
description: Keep every open, non-draft agent PR mergeable, green and answered until a human merges it - update the branch when main moves, merge main in when it conflicts, delegate failing checks and human review comments to implement-issue, reply on every thread, record a Steward entry in the ledger. Use on a schedule, when asked to watch, babysit or sweep PRs, or with one PR number to handle that PR alone.
---

# Steward ready PRs

`work-issue` stops when a PR is ready for review. From then on main moves, checks go stale
and humans leave comments nobody reads. You pick that up: one sweep over the ready agent PRs,
each brought back to mergeable with every human comment answered, then a report. A sweep with
nothing to do posts nothing, pushes nothing and edits nothing.

Like `work-issue`, you never edit files, run builds or post review comments yourself. You run
`gh` and `git`, delegate anything that changes a file to a subagent with the matching skill
(Agent tool in Claude Code, task tool in OpenCode; the prompt names the PR, the branch, the
worktree, the one thing to do, "load the `<skill>` skill and follow it" and "end with that
skill's report and nothing else"), and reason over the report.

Never `gh pr merge`, never rebase, never force push, never resolve a review thread. A branch's
history only grows: `gh pr update-branch` and `git merge origin/main` add merge commits, and
the squash merge a human performs makes them disappear.

Read the `steward-pr` section of `.agents/memory/LESSONS.md` before starting, plus General.

## 1. Select

Without an argument, every open PR that is not a draft and whose ledger says `Ready: yes`.
The PR template ships the `### Status` heading with `Ready: no`, so the heading alone would
select every human PR opened from the template; `Ready: yes` is written only by
`work-issue` step 5 or by a human handing a PR over.

```bash
gh pr list --state open --limit 100 --json number,isDraft,body \
-q '.[] | select(.isDraft | not) | select(.body | contains("### Status")) | select(.body | contains("Ready: yes")) | .number'
```

With a PR number, that PR alone; when it does not qualify, report `skipped: <reason>`
(`not open`, `draft`, `no ledger`, `not ready`) and stop. A draft belongs to its orchestrator: a stale
draft is an unfinished `work-issue` run, resumed by running `work-issue` on its issue. A PR
without the ledger (a human's, dependabot's) is never touched.

Handle the selected PRs one at a time, ascending, steps 2 to 7 each.

## 2. Take stock

```bash
gh pr view <N> --json headRefName,headRefOid,mergeStateStatus,closingIssuesReferences
gh pr checks <N>
```

Note `headRefOid`; step 6 compares against it. `mergeStateStatus` is `BEHIND` (main moved,
no conflict), `DIRTY` (conflict), `CLEAN`, `UNSTABLE` (mergeable, a non-required check
failing), `BLOCKED` (a required check or review is missing) or `UNKNOWN` (not computed yet;
re-read once after a few seconds).

Read review activity through REST, never GraphQL; cloud sessions allow only a pinned set of
GraphQL operations. Flags go after the path so the permission allowlist matches:

```bash
gh api repos/{owner}/{repo}/pulls/<N>/comments --paginate # review threads: id, in_reply_to_id, path, line, body; {owner}/{repo} literally, gh fills them
gh api repos/{owner}/{repo}/pulls/<N>/reviews --paginate # id, state, body, submitted_at
gh api repos/{owner}/{repo}/issues/<N>/comments --paginate # id, body, created_at
```

A comment, review or body is **agent** activity when one of its lines is `-- agent: <skill>`;
everything else is **human**, whatever login posted it. Group review comments into threads by
`in_reply_to_id`. Needing an answer: a thread whose last comment is human; a human review
with state `CHANGES_REQUESTED` or a non-empty body, with no agent issue comment after it; a
human issue comment with no agent issue comment after it. An approval with an empty body
needs nothing.

`CLEAN`, checks green, nothing to answer: report `nothing to do`, do not touch the ledger,
next PR. Otherwise get a workspace; the worktree may still exist from the `work-issue` run:

```bash
branch=<headRefName>
dir=.worktrees/$branch; [ -d "$dir" ] || dir=$(.agents/scripts/worktree.sh "$branch")
cd "$dir" && git fetch origin && git merge origin/$branch --ff-only # unquoted: the allowlist matches command text
```

## 3. Bring the branch up to date

- `BEHIND`: `gh pr update-branch <N>`, then in the worktree `git fetch origin && git merge
origin/$branch --ff-only` so the local branch has the merge commit.
- `DIRTY`: in the worktree `git merge origin/main --no-edit`. A clean merge you push yourself
with `git push origin "$branch"`. On conflicts, delegate to `implement-issue`: the worktree,
the conflicting files from `git status`, "resolve the merge in progress keeping both sides'
intent, run `pnpm build && pnpm test`, commit the merge as it is, push; do not rebase, do
not widen scope".

Then `gh pr checks <N> --watch` and re-read the status. It must now be `CLEAN` or
`UNSTABLE`; `BEHIND` or `DIRTY` again means main moved meanwhile, so go round once more.

## 4. Get the checks green

```bash
gh pr checks <N> --json name,state,link # the link ends in /job/<job id>
gh run view --job <job id> --log-failed | tail -n 80
```

Delegate to `implement-issue`: the worktree, the check name, the excerpt, "make this check
pass on the branch, tests-first for any behaviour you change, push, do not widen scope".
Then `gh pr checks <N> --watch` again.

## 5. Answer the humans

One `implement-issue` round with every item from step 2 pasted verbatim: the thread's top
comment id, `path`, `line` and body, or the review or comment id and body; "address each
item; when an item is wrong, change nothing for it and say why in the report; push". Then
reply on each item, fixed or declined, one sentence plus the commit:

```bash
gh api repos/{owner}/{repo}/pulls/<N>/comments/<top comment id>/replies -f body='<what changed, in <sha>>

-- agent: steward-pr' # a review thread
gh pr comment <N> --body '<what changed, in <sha>, answering <login>'"'"'s review of <date>>

-- agent: steward-pr' # a review body or issue comment
```

The thread stays open. The human who opened it resolves it, and their next reply makes it
human again for the next sweep.

## 6. Review and E2E what the sweep pushed

When `implement-issue` produced commits, delegate one `review-pr` round on the PR. Findings
above nit go back to `implement-issue` as one more round, with replies as in step 5 for
anything a human raised.

When `git diff --name-only <headRefOid>..HEAD` in the worktree touches `packages/native`,
`packages/react-native`, `playground` or `playground-native`, delegate `e2e-device` on the
PR. A failure is one more `implement-issue` round, then E2E once more.

## 7. Loop limit and the ledger

At most two `implement-issue` rounds per PR per sweep, whatever the reason. Past that, or when
a subagent reports blocked: one comment on the linked issue with what is stuck, what was tried
and the decision needed, ending with `-- agent: steward-pr`; `gh issue edit <I> --add-label
status:blocked`; leave the PR as it is, next PR.

After every PR you did something to, update its ledger: replace the previous `Steward:` entry
if there is one, keep the body's trailer as its last line.

```bash
gh pr view <N> --json body -q .body > <scratch>/body.md # edit the ### Status line
gh pr edit <N> --body-file <scratch>/body.md
```

```
Implement: done (5/5 green) Review: round 2, approve E2E: pass (iOS) Ready: yes Steward: 2026-09-24 merged main (2 conflicts), answered 3 threads, checks green
```

## Report

End with exactly this, one line per selected PR:

```
Steward sweep: <date> PRs: <selected> of <open>
#N: updated from main | merged main (<k> conflicts) | checks green (round <r>) | answered <k> items | e2e pass | blocked: <why> | nothing to do | skipped: <reason>
Posted: <comments and replies> Pushed: <PRs>
```
2 changes: 2 additions & 0 deletions .claude/skills/triage-issue/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ documented contract. State the question as the first line of the comment. Otherw
**Rejected:** <hypothesis: why>, one line each
**Fix:** <module; call or event; what the test asserts; what stays the same>
**Risks:** <what else touches this path>

-- agent: triage-issue
```

## Labels
Expand Down
8 changes: 5 additions & 3 deletions .claude/skills/work-issue/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ Each prompt has the same shape: the issue number, the branch, the one thing to d
it fails, delegate the failure to `implement-issue` with the evidence pasted in, then
re-run E2E. At most two rounds.
5. **Ready.** `gh pr ready <M>`, then update the PR body's status lines. Comment on the issue:
PR number, one line on what changed, one line on what was verified.
PR number, one line on what changed, one line on what was verified, then the trailer
`-- agent: work-issue`.
6. **Friction gate.** If any of these happened, append one note to `.agents/memory/INBOX.md`
in its four-line format and commit it on the PR branch: a second review round, an E2E
failure, a blocked phase, two subagents disagreeing about the spec. The note names the
Expand All @@ -60,12 +61,13 @@ Each prompt has the same shape: the issue number, the branch, the one thing to d

When a loop limit is hit, when two subagents disagree about the spec, or when a report says
something that contradicts the issue, stop. Post one comment on the issue with what is stuck,
what you tried, and the decision needed. Apply `status:blocked`. Leave the draft PR as is.
what you tried, and the decision needed, ending with `-- agent: work-issue`. Apply
`status:blocked`. Leave the draft PR as is.

## 4. Keep the ledger current

After each phase, update the PR body's status section so a fresh session can resume from
GitHub alone:
GitHub alone. The body's `-- agent:` trailer stays its last line:

```
### Status
Expand Down
11 changes: 11 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,16 @@ outcome. Plain everyday English, colleague to colleague. Say only what bears on
hand; skip what you checked and found fine unless it changes a decision. No filler, no hedging,
no restating the question. Commit subjects: conventional prefix, imperative, under 72 chars.

Everything an agent posts to GitHub (issue body or comment, PR body, review, review comment)
ends with one trailer line naming the skill that posted it:

```
-- agent: <skill-name>
```

The `steward-pr` skill tells agent activity from human activity by that line, whatever login
posted it. Editing a body someone else wrote keeps its trailer.

## Skills

Load the skill before starting the matching task. They live in `.claude/skills/`.
Expand All @@ -84,4 +94,5 @@ Load the skill before starting the matching task. They live in `.claude/skills/`
| Cutting a release | `cut-release` |
| Curating agent memory (weekly, or when the inbox has notes) | `review-memory` |
| Taking an issue from `status:ready` to a reviewed, tested PR | `work-issue` (orchestrator) |
| Watching open agent PRs: keeping them mergeable, green and answered until a human merges | `steward-pr` |
| Driving an Appduct-enabled app as a user of Appduct | `appduct` (in `skills/`) |
Loading