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 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/.claude/settings.json b/.claude/settings.json index 263aee1..8b6d155 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,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 *)", @@ -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*)" ] 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 56ac2c9..6c13378 100644 --- a/.claude/skills/review-memory/SKILL.md +++ b/.claude/skills/review-memory/SKILL.md @@ -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 "" +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/steward-pr/SKILL.md b/.claude/skills/steward-pr/SKILL.md new file mode 100644 index 0000000..6c6a37b --- /dev/null +++ b/.claude/skills/steward-pr/SKILL.md @@ -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 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: ` +(`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 --json headRefName,headRefOid,mergeStateStatus,closingIssuesReferences +gh pr checks +``` + +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//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 +``` + +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 +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= +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 `, 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 --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 --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 + +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//comments//replies -f body='> + +-- agent: steward-pr' # a review thread +gh pr comment --body ', answering '"'"'s review of > + +-- 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 ..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 --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 --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: +``` 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 73066ee..dcf39cd 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/`. @@ -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/`) |