Instructions for an agent
How this repository works binds everybody, and it lives in docs/. Each
evergreen page under docs/ and tools/ registers as a skill under
.agents/skills/.
When you do a task, consult the available skills. When a skill matches, load it.
A skill directs you to read its canonical page under docs/. Do not read
documentation for ad-hoc queries, inspection, or git branch maintenance.
docs/DOCUMENTATION.md states which pages
commit-review-against-rule-pages review judges. That page, docs/WRITING.md
and docs/GLOSSARY.md sit at the root of docs/ and bind every page below
them.
When you edit documentation, write prose, or prepare a pull request, read the
pages at the root of docs/.
docs/GOVERNANCE.md names the person who holds each role that a page names.
Binds: process
For an app deployment, read .agents/skills/loadbudget-deploy/SKILL.md. That
skill loads the current delivery procedure from docs/.
This page binds every agent that works here. CLAUDE.md states what Claude Code
adds on top of it, and no other agent takes instruction from that file.
commit-review-against-rule-pages review judges the change against every
applicable evergreen page, as well as the text. This file is one of them.
A heading is what selects a page, so write a heading that names its subject.
Check before you assert
Apple changes its health APIs every year, and training data represents them
badly. Before you state anything about these subjects, check docs/research/
and the open issues:
- HealthKit
- CloudKit
- SwiftData
- App Store review
- Xcode Cloud
If the answer is not there, look it up on developer.apple.com, then write it down.
Do not tamper with Xcode Cloud workflows through automation or GitHub workflows. Configure them directly in Xcode or App Store Connect.
Context that arrives by accident is a guess, and not a source.
“I do not know” is a valid answer here, and it is cheaper than a wrong one. A reader has no reason to doubt a statement about our own setup.
“Not documented” is a valid answer, and a useful one. A confidently wrong API name costs more than an open question, because the tech lead will build on it.
A pull request holds a reply in several places
GitHub splits a review across these records, and no one of them answers “did this person reply”. Read every one before you say that somebody answered nothing:
| Record | What it holds |
|---|---|
gh api repos/{owner}/{repo}/issues/{n}/comments |
The conversation, at the bottom of the page. |
gh api repos/{owner}/{repo}/pulls/{n}/comments |
A comment on a line of the diff. |
gh api repos/{owner}/{repo}/pulls/{n}/reviews |
Each submitted review, and its verdict. |
gh api repos/{owner}/{repo}/pulls/{n}/requested_reviewers |
Who still owes a review. |
gh pr view <n> --json reviewDecision |
The verdict of the whole pull request. |
A cloud session reads these records another way.
infra/docs/usage/CLOUD_SESSION.md states how.
The two comment lists are separate, and neither one holds the other. A reader who checks the inline list alone misses an answer that somebody typed into the conversation box.
An empty reviewDecision does not mean an empty review. The field carries a
verdict for the pull request, and it reads empty while an approval sits in
reviews.
A CODEOWNERS request expires. When the pull request opens, GitHub attaches
it. When the reviewer acts, it disappears. So a pull request can sit green with
nobody asked, and it looks the same as one that waits. Only reviewRequests
tells the two apart.
When you report that nothing came back, say “I read the conversation and the diff comments”. If you read one of them, say which.
Do not skip ahead to an implementation
Use docs/development/WORKFLOW.md, section “Planning uses known decisions”,
before you call a choice open. That section separates settled answers, discovery
work and choices that wait for their owner.
Name a new branch or worktree <type>/<scope>/<slug>. That page states the
conventional branch format.
Read docs/development/COMMITS.md before you write a commit message or a pull
request description. It carries the format that the merged commit takes.
commit-review-against-rule-pages review judges the text against the writing
rules that the first paragraph names.
When a choice remains for its owner, do these steps:
- Name the options.
- Say what each option costs.
- Cite the constraint that forces the answer.
- Let the tech lead decide. A product decision goes to the product lead, and
docs/development/LABELLING_AN_ISSUE.mdstates how.
Working code for the wrong decision costs more than no code.
Place each proposed feature in the product that owns that behavior. Read
docs/LAYOUT.md to learn what each product is for before you propose where a
feature lives. Keep generic capabilities in generic products. Keep app-specific
adaptations in ios-app or ios-app-tools. Do not propose a generic capability
in lb, ios-app or ios-app-tools. When you propose a new capability, cite
the product and the README.md under the docs/ directory of that product.
A plan reaches the operator as a briefing and questions
The operator reads a plan to make its decisions. A wall of text hides each decision, so the operator cannot correct the plan early.
Bring the operator up to speed first. In a few short lines, state the problem, what you found, and each tradeoff.
Then ask about each open choice, one at a time. Put your recommendation first, and state what each option costs. Let each answer shape the next question. Where the request or a rule settles a choice, do not ask about it.
Write the full plan after the answers. Keep it short. It holds the choices that the operator made, the files that change, and the check that proves the work.
Close what you finish, and ask where you cannot tell
An agent sees the issue that it read, and not the rest of the queue. So an issue with the same symptom stays open after the fix lands.
Search the open issues before the pull request opens. Search for the symptom
and for the code that the change touches, with
gh issue list --state open --search "<words>". Each hit gets Closes: or a
comment.
When you cannot tell whether the change finishes an issue, ask the operator.
The usual case is a symptom that no test shows, and only the app on a phone
shows. Health data, Health permissions and a rating from the Watch are such
cases. Ask before the pull request opens. The answer picks Closes: or the
comment. Do not guess.
Close an issue that landed work already finished, after the operator
confirms. The work already sits on main, so no pull request remains to carry
Closes:. docs/development/TAKING_AN_ISSUE.md, section “A close lands as a
commit that says why”, states the close by hand. Do not tell the operator to
close it. A report like that gives the step to the person who forgets it.
A task includes model processing and review
A repository task authorizes the configured coding agents and reviewers to process the repository content that the task requires. This includes sending the task’s diff, pull request description, rule pages and required tracked files to their model providers.
The tech lead authorizes cross-model review as part of the task. The reviewer can use a different model or provider from the coding session. Do not ask for separate approval solely because this processing uses a remote model or a different configured provider.
This authorization applies to future repository tasks as well. It covers the content required for the task and its review. It authorizes no publication.
Do not publish work without a request
A published page, a gist, a paste and an upload each put content on a server. A session sends none of them on its own.
A report, a screenshot and a comparison that the session makes go in a file
under .scratch/. A tool that owns a directory for them keeps its own. Open the
file with open. The operator reads it there.
When the operator wants a publish, the operator names it. A request for a report is not a request to publish it.
A pull request is not that publish. A comment that this session posts on a pull request it opened is not that publish either. An issue that this session opened is not that publish. A comment on that issue, or on an issue that the operator named by number, is not that publish either.
For every graphical change, follow the section “Graphical changes carry visual
evidence” in docs/development/WORKFLOW.md. That procedure authorizes
screenshots and recordings on this session’s pull request. The section “An issue
carries its images as attachments” in docs/development/FILING_AN_ISSUE.md
authorizes images, plots and recordings on those issues. Neither one authorizes
another upload host.
A publish is hard to undo. The tools of a session carry no delete for a published page. Only the operator can withdraw one.
A session writes its own files in .scratch/
A session writes each file that it makes for itself under .scratch/ at the
root of its worktree. A probe script, a log, a downloaded file, a report and a
comparison are such files.
Git ignores .scratch/. So git add skips a file there unless the command
forces it. Bazel reads no file in .scratch/. So a file there changes no
build. .gitignore and REPO.bazel each carry a line for it.
The sandbox of each agent already writes the worktree. So .scratch/ needs
no grant. In the home directory, the sandbox writes only the caches that
docs/development/tools/AGENT_HOOKS.md names. Every session shares /tmp. So a
name there can collide with the file of another session.
Name a file for the operator by its absolute path. Each session runs in a worktree of its own, so a path relative to the worktree does not say where the file is. A script for the operator to run, a report to open and a log to read each take the absolute path.
.scratch/ holds nothing that must outlive the session. When the session
ends, the worktree can go, and its files go with it. A fact that must outlive
the session goes in a commit, a note under docs/research/, or a comment on the
pull request of this session.
A rule that the operator states goes to its page, in the pull request of this session. A correction of the work and a preference about it are such rules. The memory of an agent, a file in the home directory and a scratch note reach no other session, no other account and no other agent.
The section “A session writes its own files in .scratch/” covers a file that
the session writes itself. A tool that owns a directory keeps it.
Screenshots/, Reports/, RunLogs/, Diagrams/ and .playwright-mcp/ are
such directories. .gitignore names the code that owns each one. A run of
commit-review-against-rule-pages keeps its files in the run directory that
tools/commit-review-against-rule-pages/docs/DEVELOPMENT.md names, under the
temporary directory of the user.
A request for the next work goes to a command of the operator
/top-of-backlog covers filed work. /work-seeker covers work that nobody
filed. Each one starts only through its command, so no agent sees it in a list
of skills.
When the operator asks in plain words for either kind of work, ask the operator to run the command.
A session owns the pull requests that it opened, and no others
Every session runs on the token of one person. gh pr list --author "@me"
returns the work of every session that person runs, and the numbers sit next to
each other. They do not form one queue.
A session on a laptop will run on the token of a machine user of its own.
Report the pull requests that this session opened. Name each one by number, and say what it carries. Leave the rest out of the report. A reader who counts them takes work from another session as this one.
Do not read, amend, review or merge a pull request that this session did not open. When the operator wants one, the operator names it by number. Without that number, the branch carries a decision that this session never saw. A change to it is a guess about work owned by somebody else.
Where such a pull request blocks this work, name it and say how it blocks. That is a fact about the queue. It is not a licence to touch the branch.
A conflict check reads the file names of a pull request, and nothing else of
it. gh pr list --state open --json number,title,files is that read. So a
session that plans new work learns what the open pull requests change.
The same rule covers an issue and a branch.
git commit and git push run the checks
The pre-commit hook runs every prek hook. One of them, bazel-test, runs
just test. The post-commit hook then runs just stamp-commit. That recipe
runs the hooks on every file of the commit, reviews the commit, and stamps the
commit that passes. The pre-push hook starts no review. It refuses a commit
whose stamp does not name its change, and it names just stamp-commit.
docs/development/tools/PREK.md and docs/development/tools/PR_GUARD.md state
the hooks.
just send --push runs the commit and the push. Stage the change, and write
the complete commit message to .scratch/commit-msg.txt. Then run
just send --push. It ends with the findings of the review, and the session
fixes each one and runs just send --amend --push.
docs/development/tools/PR_GUARD.md, section “just send commits, waits for
the review, and pushes”, states the command.
Do not run a check by hand before a commit or a push. A run of prek,
just test, a formatter or a validator repeats the work of the hook. Several
sessions share one machine, so each repeat slows every session.
When a hook fails, fix what its output names. The output names the hook and the file. Edit the file, then commit or push again. Where that output does not explain the failure, run one tool by hand.
Run a tool through the repository. uvx <tool> reads no setting of this
repository. A formatter with other settings rewrites a file, and the hooks then
refuse it.
A change to a tracked file ends in a pull request
A session that changes a tracked file opens a pull request before it reports. A commit on a branch is not delivery. Nobody reads the change until a pull request carries it.
Do not wait for a request. When the commit is ready, push the branch and run
gh pr create.
A revert is clean and fast. When the operator requests a revert, revert the commit cleanly. Keep it focused. Open the pull request immediately. Do not overcomplicate a revert with extra redesign or fixes.
A session that only reads, searches or plans touches no tracked file. It opens no pull request.
A session watches its pull request until the merge
After the pull request opens, start just lb pr watch <n>. Each line of the
watch is one finding: a merge, feedback, a failed check, or a conflict.
Answer each finding as it arrives. A fix goes out on the same branch. A line
that starts with Still open: repeats a finding that is not handled yet. The
watch has no timeout. When the watch stops, start it again. At the merge, the
watch prints the merged commit, and the report names it.
While the watch runs, ask nothing about a merge, because the watch prints the merge.
At the merge, release each slot of the session. Run
aspect worktree release <branch>. A slot that the session leased from a shell
command gets no WorktreeRemove, so nothing else ends its lease while the
session lives. The release removes the checkout, so release the slot that the
session runs in as the last step of the session.
Where aspect refuses the release, keep the slot. A bridge leases the slot
of a session before the session starts. The SessionStart hook moves that lease
to the session. A lease from before that hook still names the bridge, and
aspect refuses its release. Do not pass --force=all, because it removes the
checkout that the session runs in.
A task is done when its commit reaches main
An open pull request is work in progress, and docs/development/WORKFLOW.md,
section “An open pull request is work in progress”, says why. Nothing outside
main counts as delivered.
So a report on an open pull request names the merge, and who performs it. A report of a finished task names the commit that landed.
When the session waits for the operator to review or merge a pull request, the report names that wait. When the session waits for its own job, such as a push, a test run or a check, the report names that job.