From a9d7f00766f86cf604c6de1bca8d054a870e86d2 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Thu, 24 Sep 2026 12:24:17 +0200 Subject: [PATCH 1/6] test: route PR stewardship to a steward-pr skill that does not exist yet (#101) --- .agents/memory/LESSONS.md | 2 ++ AGENTS.md | 1 + 2 files changed, 3 insertions(+) diff --git a/.agents/memory/LESSONS.md b/.agents/memory/LESSONS.md index e923da5..9e74001 100644 --- a/.agents/memory/LESSONS.md +++ b/.agents/memory/LESSONS.md @@ -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 diff --git a/AGENTS.md b/AGENTS.md index 7a0303c..b5e7738 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -84,4 +84,5 @@ Load the skill before starting the matching task. They live in `.claude/skills/` | Cutting a release | `cut-release` | | Curating agent memory (monthly, 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/`) | From 965212d926be4f5a682da4d7d8480f9fc7f191ee Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Thu, 24 Sep 2026 12:26:21 +0200 Subject: [PATCH 2/6] chore: end every agent post to GitHub with an -- agent: trailer (#101) criteria 2 of 9 unmet -> 1 of 9 unmet --- .claude/skills/cut-release/SKILL.md | 4 +++- .claude/skills/design-feature/SKILL.md | 6 +++--- .claude/skills/e2e-device/SKILL.md | 2 +- .claude/skills/file-issue/SKILL.md | 6 ++++-- .claude/skills/implement-issue/SKILL.md | 6 ++++-- .claude/skills/review-memory/SKILL.md | 4 +++- .claude/skills/review-pr/SKILL.md | 3 ++- .claude/skills/triage-issue/SKILL.md | 2 ++ .claude/skills/work-issue/SKILL.md | 8 +++++--- AGENTS.md | 10 ++++++++++ 10 files changed, 37 insertions(+), 14 deletions(-) diff --git a/.claude/skills/cut-release/SKILL.md b/.claude/skills/cut-release/SKILL.md index 6cced02..2b3738f 100644 --- a/.claude/skills/cut-release/SKILL.md +++ b/.claude/skills/cut-release/SKILL.md @@ -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. diff --git a/.claude/skills/design-feature/SKILL.md b/.claude/skills/design-feature/SKILL.md index ca41fc5..f0ce9d7 100644 --- a/.claude/skills/design-feature/SKILL.md +++ b/.claude/skills/design-feature/SKILL.md @@ -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 diff --git a/.claude/skills/e2e-device/SKILL.md b/.claude/skills/e2e-device/SKILL.md index 89b9b58..5f008ef 100644 --- a/.claude/skills/e2e-device/SKILL.md +++ b/.claude/skills/e2e-device/SKILL.md @@ -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 --body-file diff --git a/.claude/skills/file-issue/SKILL.md b/.claude/skills/file-issue/SKILL.md index 4c0d396..2e1651e 100644 --- a/.claude/skills/file-issue/SKILL.md +++ b/.claude/skills/file-issue/SKILL.md @@ -40,7 +40,8 @@ gh issue list --state all --search "" --limit 10 gh issue list --state all --search "" --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 @@ -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 diff --git a/.claude/skills/implement-issue/SKILL.md b/.claude/skills/implement-issue/SKILL.md index f75d12d..3ea9b94 100644 --- a/.claude/skills/implement-issue/SKILL.md +++ b/.claude/skills/implement-issue/SKILL.md @@ -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 @@ -68,7 +69,8 @@ gh pr create --draft --title ": (#$n)" --body-file ``` 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 diff --git a/.claude/skills/review-memory/SKILL.md b/.claude/skills/review-memory/SKILL.md index 60944c1..907ec4f 100644 --- a/.claude/skills/review-memory/SKILL.md +++ b/.claude/skills/review-memory/SKILL.md @@ -49,7 +49,9 @@ Empty the inbox down to its header. 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 "" +gh pr create --title "chore(memory): review $(date +%Y-%m-%d)" --body " + +-- 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)" ``` diff --git a/.claude/skills/review-pr/SKILL.md b/.claude/skills/review-pr/SKILL.md index e6279c7..0174c56 100644 --- a/.claude/skills/review-pr/SKILL.md +++ b/.claude/skills/review-pr/SKILL.md @@ -92,7 +92,8 @@ gh pr review --comment --body "" # should-fix only gh pr review --approve --body "" # 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 diff --git a/.claude/skills/triage-issue/SKILL.md b/.claude/skills/triage-issue/SKILL.md index 5f86f15..eb1c26f 100644 --- a/.claude/skills/triage-issue/SKILL.md +++ b/.claude/skills/triage-issue/SKILL.md @@ -46,6 +46,8 @@ documented contract. State the question as the first line of the comment. Otherw **Rejected:** , one line each **Fix:** **Risks:** + +-- agent: triage-issue ``` ## Labels diff --git a/.claude/skills/work-issue/SKILL.md b/.claude/skills/work-issue/SKILL.md index e4ad181..66e325b 100644 --- a/.claude/skills/work-issue/SKILL.md +++ b/.claude/skills/work-issue/SKILL.md @@ -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 `, 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 @@ -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 diff --git a/AGENTS.md b/AGENTS.md index b5e7738..ba0f962 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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: +``` + +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/`. From dc2f38790a10c2692ad75dacbf5c2a04add0eab1 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Thu, 24 Sep 2026 12:29:24 +0200 Subject: [PATCH 3/6] chore: add the steward-pr skill and allowlist the commands it runs (#101) 8 of 9 criteria unmet -> 0 unmet pending the dry runs --- .claude/settings.json | 3 + .claude/skills/steward-pr/SKILL.md | 172 +++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 .claude/skills/steward-pr/SKILL.md diff --git a/.claude/settings.json b/.claude/settings.json index 263aee1..4050d3c 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -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 *)", @@ -31,6 +32,8 @@ "Bash(gh pr comment *)", "Bash(gh pr review *)", "Bash(gh pr ready *)", + "Bash(gh pr update-branch *)", + "Bash(gh api repos/*)", "Bash(gh run list *)", "Bash(gh run view *)", "Bash(gh label list *)", diff --git a/.claude/skills/steward-pr/SKILL.md b/.claude/skills/steward-pr/SKILL.md new file mode 100644 index 0000000..c4928ad --- /dev/null +++ b/.claude/skills/steward-pr/SKILL.md @@ -0,0 +1,172 @@ +--- +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 and 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, +and reason over its report. Subagents: in Claude Code the Agent tool with a fresh +general-purpose agent per step, in OpenCode the task tool; the prompt names the PR, the +branch, the worktree, the one thing to do, "load the `` skill and follow it" and "end +with that skill's report and nothing else". + +Never `gh pr merge`, never rebase, never force push, never resolve a review thread. The +history of a branch 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 body carries the +`work-issue` ledger: + +```bash +gh pr list --state open --json number,isDraft,body \ + -q '.[] | select(.isDraft | not) | select(.body | contains("### Status")) | .number' +``` + +With a PR number, that PR alone; when it does not qualify, report `skipped: ` +(`not open`, `draft`, `no ledger`) 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, in ascending number order, steps 2 to 7 each. + +## 2. Take stock + +```bash +gh pr view --json headRefName,headRefOid,mergeStateStatus,closingIssuesReferences +gh pr checks +``` + +Note `headRefOid`; the report and the E2E decision compare 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` (GitHub has not computed it yet; re-read once after a few seconds). + +Read every piece of review activity through REST, never GraphQL; cloud sessions allow only a +pinned set of GraphQL operations: + +```bash +gh api repos/{owner}/{repo}/pulls//comments --paginate # review threads: id, in_reply_to_id, path, line, body, created_at +gh api repos/{owner}/{repo}/pulls//reviews --paginate # id, state, body, submitted_at +gh api repos/{owner}/{repo}/issues//comments --paginate # id, body, created_at +``` + +A comment, review or body is **agent** activity when one of its lines is `-- agent: `; +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 whose state is `CHANGES_REQUESTED` or whose body is not empty, with no + agent issue comment posted after it; +- a human issue comment with no agent issue comment posted after it. + +An approval with an empty body needs nothing. Nothing to do when the status is `CLEAN`, the +checks are green and nothing needs an answer: report `nothing to do` and go to the next PR. +Do not touch the ledger for a PR you did nothing to. + +Otherwise get a workspace. The worktree may already exist from the `work-issue` run: + +```bash +branch= +dir=.worktrees/$branch; [ -d "$dir" ] || dir=$(.agents/scripts/worktree.sh "$branch") +cd "$dir" && git fetch origin && git merge "origin/$branch" --ff-only +``` + +## 3. Bring the branch up to date + +- `BEHIND`: `gh pr update-branch `, 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`. When it stops 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". A clean merge you + push yourself: `git push origin "$branch"`. + +Then `gh pr checks --watch` and re-read the status. It must now be `CLEAN` or +`UNSTABLE`; `BEHIND` or `DIRTY` again means main moved while you worked, so go round once +more. + +## 4. Get the checks green + +For each failing check, the log excerpt is what the fixer needs: + +```bash +gh pr checks --json name,state,link # the link ends in /job/ +gh run view --job --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 --watch` again. + +## 5. Answer the humans + +Delegate 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. +Ask for "address each item; when an item is wrong, change nothing for it and say why in the +report; push". Then reply on each item, whether it was fixed or declined: + +```bash +# a review thread: reply under its top comment +gh api repos/{owner}/{repo}/pulls//comments//replies -f body='> + +-- agent: steward-pr' +# a review body or an issue comment: one PR comment naming it +gh pr comment --body ', answering '"'"'s review of > + +-- agent: steward-pr' +``` + +One sentence on what changed and the commit, or one sentence on why not. The thread stays +open; the human who opened it resolves it, and their next reply makes the thread human +again for the next sweep. + +## 6. Review and E2E what the sweep pushed + +When `implement-issue` produced commits in this sweep, delegate one `review-pr` round on the +PR. Findings above nit go back to `implement-issue` as a further round (step 7 counts it), +then replies for anything a human raised go out as in step 5. + +When `git diff --name-only ..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 (conflicts, +checks, review items, review findings, E2E). 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 --add-label status:blocked`; leave the +PR as it is and move to the next one. + +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 --json body -q .body > /body.md # edit the ### Status line +gh pr edit --body-file /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: PRs: of +#N: updated from main | merged main ( conflicts) | checks green (round ) | answered items | e2e pass | blocked: | nothing to do | skipped: +Posted: Pushed: +``` From 7b37f1092a644182cb1f6b9df8a5686bef3d0ff4 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Thu, 24 Sep 2026 12:33:12 +0200 Subject: [PATCH 4/6] refactor: tighten the steward-pr skill to its siblings' length (#101) --- .claude/skills/steward-pr/SKILL.md | 116 +++++++++++++---------------- 1 file changed, 51 insertions(+), 65 deletions(-) diff --git a/.claude/skills/steward-pr/SKILL.md b/.claude/skills/steward-pr/SKILL.md index c4928ad..536e2f8 100644 --- a/.claude/skills/steward-pr/SKILL.md +++ b/.claude/skills/steward-pr/SKILL.md @@ -7,26 +7,24 @@ description: Keep every open, non-draft agent PR mergeable, green and answered u `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 and every human comment answered, then a report. A sweep with +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, -and reason over its report. Subagents: in Claude Code the Agent tool with a fresh -general-purpose agent per step, in OpenCode the task tool; the prompt names the PR, the -branch, the worktree, the one thing to do, "load the `` skill and follow it" and "end -with that skill's report and nothing else". +`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 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. The -history of a branch only grows: `gh pr update-branch` and `git merge origin/main` add merge -commits, and the squash merge a human performs makes them disappear. +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 body carries the -`work-issue` ledger: +Without an argument, every open PR that is not a draft and carries the `work-issue` ledger: ```bash gh pr list --state open --json number,isDraft,body \ @@ -38,7 +36,7 @@ With a PR number, that PR alone; when it does not qualify, report `skipped: --json headRefName,headRefOid,mergeStateStatus,closingIssuesRefer gh pr checks ``` -Note `headRefOid`; the report and the E2E decision compare 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` (GitHub has not computed it yet; re-read once after a few seconds). +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 every piece of review activity through REST, never GraphQL; cloud sessions allow only a -pinned set of GraphQL operations: +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//comments --paginate # review threads: id, in_reply_to_id, path, line, body, created_at +gh api repos/{owner}/{repo}/pulls//comments --paginate # review threads: id, in_reply_to_id, path, line, body gh api repos/{owner}/{repo}/pulls//reviews --paginate # id, state, body, submitted_at gh api repos/{owner}/{repo}/issues//comments --paginate # id, body, created_at ``` A comment, review or body is **agent** activity when one of its lines is `-- agent: `; -everything else is **human**, whatever login posted it. Group review comments into threads -by `in_reply_to_id`. Needing an answer: +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. -- a thread whose last comment is human; -- a human review whose state is `CHANGES_REQUESTED` or whose body is not empty, with no - agent issue comment posted after it; -- a human issue comment with no agent issue comment posted after it. - -An approval with an empty body needs nothing. Nothing to do when the status is `CLEAN`, the -checks are green and nothing needs an answer: report `nothing to do` and go to the next PR. -Do not touch the ledger for a PR you did nothing to. - -Otherwise get a workspace. The worktree may already exist from the `work-issue` run: +`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= @@ -86,22 +79,19 @@ cd "$dir" && git fetch origin && git merge "origin/$branch" --ff-only - `BEHIND`: `gh pr update-branch `, 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`. When it stops 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". A clean merge you - push yourself: `git push origin "$branch"`. +- `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 --watch` and re-read the status. It must now be `CLEAN` or -`UNSTABLE`; `BEHIND` or `DIRTY` again means main moved while you worked, so go round once -more. +`UNSTABLE`; `BEHIND` or `DIRTY` again means main moved meanwhile, so go round once more. ## 4. Get the checks green -For each failing check, the log excerpt is what the fixer needs: - ```bash -gh pr checks --json name,state,link # the link ends in /job/ +gh pr checks --json name,state,link # the link ends in /job/ gh run view --job --log-failed | tail -n 80 ``` @@ -111,46 +101,42 @@ Then `gh pr checks --watch` again. ## 5. Answer the humans -Delegate 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. -Ask for "address each item; when an item is wrong, change nothing for it and say why in the -report; push". Then reply on each item, whether it was fixed or declined: +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 -# a review thread: reply under its top comment gh api repos/{owner}/{repo}/pulls//comments//replies -f body='> --- agent: steward-pr' -# a review body or an issue comment: one PR comment naming it +-- agent: steward-pr' # a review thread gh pr comment --body ', answering '"'"'s review of > --- agent: steward-pr' +-- agent: steward-pr' # a review body or issue comment ``` -One sentence on what changed and the commit, or one sentence on why not. The thread stays -open; the human who opened it resolves it, and their next reply makes the thread human -again for the next sweep. +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 in this sweep, delegate one `review-pr` round on the -PR. Findings above nit go back to `implement-issue` as a further round (step 7 counts it), -then replies for anything a human raised go out as in step 5. +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 ..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. +When `git diff --name-only ..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 (conflicts, -checks, review items, review findings, E2E). 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 --add-label status:blocked`; leave the -PR as it is and move to the next one. +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 --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: +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 --json body -q .body > /body.md # edit the ### Status line From 1d54e8184fa9d583e0df504d9a3dc864e04e25b5 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Thu, 24 Sep 2026 12:42:29 +0200 Subject: [PATCH 5/6] fix: select on Ready: yes, narrow the gh api allowlist, unquote the merge ref (#101) review dry run: 1 blocker, 2 should-fix -> 0 --- .claude/settings.json | 6 +++++- .claude/skills/steward-pr/SKILL.md | 17 ++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index 4050d3c..8b6d155 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -33,7 +33,8 @@ "Bash(gh pr review *)", "Bash(gh pr ready *)", "Bash(gh pr update-branch *)", - "Bash(gh api repos/*)", + "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 *)", @@ -49,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*)" ] diff --git a/.claude/skills/steward-pr/SKILL.md b/.claude/skills/steward-pr/SKILL.md index 536e2f8..6c6a37b 100644 --- a/.claude/skills/steward-pr/SKILL.md +++ b/.claude/skills/steward-pr/SKILL.md @@ -24,15 +24,18 @@ Read the `steward-pr` section of `.agents/memory/LESSONS.md` before starting, pl ## 1. Select -Without an argument, every open PR that is not a draft and carries the `work-issue` ledger: +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 --json number,isDraft,body \ - -q '.[] | select(.isDraft | not) | select(.body | contains("### Status")) | .number' +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: ` -(`not open`, `draft`, `no ledger`) and stop. A draft belongs to its orchestrator: a stale +(`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. @@ -54,7 +57,7 @@ Read review activity through REST, never GraphQL; cloud sessions allow only a pi GraphQL operations. Flags go after the path so the permission allowlist matches: ```bash -gh api repos/{owner}/{repo}/pulls//comments --paginate # review threads: id, in_reply_to_id, path, line, body +gh api repos/{owner}/{repo}/pulls//comments --paginate # review threads: id, in_reply_to_id, path, line, body; {owner}/{repo} literally, gh fills them gh api repos/{owner}/{repo}/pulls//reviews --paginate # id, state, body, submitted_at gh api repos/{owner}/{repo}/issues//comments --paginate # id, body, created_at ``` @@ -72,13 +75,13 @@ next PR. Otherwise get a workspace; the worktree may still exist from the `work- ```bash branch= dir=.worktrees/$branch; [ -d "$dir" ] || dir=$(.agents/scripts/worktree.sh "$branch") -cd "$dir" && git fetch origin && git merge "origin/$branch" --ff-only +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 `, then in the worktree `git fetch origin && git merge - "origin/$branch" --ff-only` so the local branch has the merge commit. + 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' From 6e4382f231f5e861a5187bcc000e960091493ebf Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Thu, 24 Sep 2026 12:43:51 +0200 Subject: [PATCH 6/6] chore(memory): note that work-issue overshoots on tooling-only issues (#101) --- .agents/memory/INBOX.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.agents/memory/INBOX.md b/.agents/memory/INBOX.md index c3b4009..9a27b26 100644 --- a/.agents/memory/INBOX.md +++ b/.agents/memory/INBOX.md @@ -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