@kentcdodds/ship-pr
Progressive ship-pr loop for agents: PR status, Bugbot, review sort, 7-day decisions, merge, deploy, wrap-up.
AGENTS.md
163 lines · 10.8 KB · Markdown@kentcdodds/ship-pr — agent notes
Human intent lives in README.md. Secrets by name only.
Never paste tokens. Never use hosted MCP execute. Run exports with the local
CLI. Use a one-line static-import passthrough, because @kodycodes/cli rejects
--local together with --invoke. (--invoke runs in Kody cloud: "--invoke
runs a saved package export in Kody; --local runs a module you provide".)
npx @kodycodes/cli execute --local \
--code 'import run from "kody:@kentcdodds/ship-pr/tick"; export default (p) => run(p)' \
--params '{"prUrl":"https://github.com/kentcdodds/kody/pull/123","risk":"medium","applySafeAutomations":true}'Every exampleInvokes[].command in tick output already uses this form. Copy it
and fill in the <placeholders>.
Auth (fresh Cloud Agents)
If you are already on the Kody MCP, call cliCredentialBootstrap through the
MCP api tool, then run the npx @kodycodes/cli auth bootstrap --code …
command it returns. No second kody login and no tokenCreate. Interactive
humans use kody login; CI without an MCP session uses a scoped
KODY_API_TOKEN. See local execute.
The first --local run downloads workerd (about 10 seconds) and logs progress
such as "Fetching stamped package modules…" on stderr. The JSON result is the
only thing on stdout, so add 2>/dev/null when you pipe it into a parser.
Exports
| Export | Use |
|---|---|
./tick | Main loop. Required: prUrl (or owner/repo/prNumber) + risk. Optional: applySafeAutomations, mergeAuthority, brief, expand (true/'all'/step ids), Discord fields agentId, model, title, difficulty, door, cleanup, cleanupNote, summary. |
./decide | `{ kind: 'thread' |
./reply-review | { commentId, body, resolve?, dryRun? }: reply on a review thread as kody-bot. |
./review-sort | Full Bugbot/Devin/Seer sort; applyInvalidReplies: true posts the short "invalid" replies. |
./merge | { risk, mergeAuthority?, mergeMethod?, dryRun? }: preflight (same status as tick) and then a squash merge pinned to the head SHA. |
./send-summary | `{ agentId, title, difficulty: 'Easy' |
. | Overview / export map. |
Every export rejects unknown keys and names the accepted keys in the error.
Enum params (risk, difficulty, door, cleanup, disposition, status) match case-insensitively; tick's step details.allowed lists the values (kept in brief output). Settle the friction step with decide kind: 'step', target: 'friction', disposition: 'accepted' (filed) or 'skipped' (nothing qualified).
Reading tick output
checklist: a one-line outline of the full process for this risk. Only on a full tick:brief: truedrops it, along with prose notes, and collapses hidden or already-handled items to counts. Re-tickexampleInvokespassbrief: true, so after the first tick you get compact output by default.done[]/remaining[]: progress. Only the focus step (and anyexpandsteps) carriesdetails+exampleInvokes.exit:{ done, status: 'waiting'|'needs_attention'|'action_available'|'ready_to_merge'|'done'|'blocked', pollAfterSeconds?, wakeHints?, reportStatus }.reportStatusisPendinguntil the PR merges (Shipped), you park it (Parked), or it closes unmerged (Blocked). Whenwaiting, do not tight-loop. End the turn only after arming a one-shot Cursorsubscribe_timerfromexit.wakeHints(required) and any relevant GitHub CI subscription. CI events can miss; without the timer a green PR can stall for hours (#3750). On timer wake, re-tick; if still waiting, re-arm from the newwakeHints.automations: whatapplySafeAutomationsdid (applied) or would do (available): mark ready, trigger Bugbot (medium/high only, once per head SHA, and only when the PR author is kentcdodds), and reply "invalid" on sorted invalid findings.activeDecisions: everything currently hidden, with its reason and expiry.cost:{ outboundFetches, automationCalls }for this tick. An open-PR tick costs 2 fetches (GraphQL snapshot + workflow runs); a merged kentcdodds/kody PR adds the health check and a compare. Automations add one call each, and tick re-reads the PR only when an automation actually changed something.quota(only near the cap): today'soutbound_fetches_per_dayusage with awarning. At 80%+ tick raisesexit.pollAfterSecondsto at least 300; at 95%+ to at least 900 and asks you to pause PRs that are only waiting so merges and review replies still fit. Usage is read at most every 5 minutes (shared across PRs) throughusageGet, which is not an outbound fetch.exit.status: 'blocked'withreason: 'plan_limit'andretryAt: the account already hit a daily plan cap, so every fetching call fails until 00:00 UTC. Stop ticking untilretryAt.
Fetch budget for multi-PR sessions
The account's daily outbound-fetch cap is shared by every job, webhook, and
package, not just ship-pr. When shepherding several PRs, honor
pollAfterSeconds, tick PRs that are only waiting less often, and watch for
quota in tick output.
Decisions (7-day packageStorage)
threadtarget: the rootcommentIdfrom tick, thePRRT_…thread id, or an outside-diffdecideTarget(cr-comment:v1:…from CodeRabbit markers, oroutside-diff:<hash>fallback). Passreplyonly with a numeric comment id (outside-diff findings have no inline thread — fix in code and/or decide).checktarget: the check name exactly as listed, or an AI reviewer name (bugbot,coderabbit,devin,seer) to stop waiting on it.steptarget:ready | ci | ai-review | merge | deploy | friction | report.merge+skipped= park (status becomes Parked).friction+accepted(filed …) orskipped(nothing qualified) completes the friction pass.deploy+accepted/skippedwhen verified another way or nothing deploys.
- A decision expires after
ttlDays(default 7, max 7). Re-recording the samekind+targetreplaces it.
Smoke tests (read-only / dry-run)
# status only (no automations)
npx @kodycodes/cli execute --local --code 'import run from "kody:@kentcdodds/ship-pr/tick"; export default (p) => run(p)' --params '{"prUrl":"https://github.com/kentcdodds/kody/pull/2943","risk":"medium"}'
# full sort without replies
npx @kodycodes/cli execute --local --code 'import run from "kody:@kentcdodds/ship-pr/review-sort"; export default (p) => run(p)' --params '{"prUrl":"https://github.com/kentcdodds/kody/pull/2937"}'
# merge preflight
npx @kodycodes/cli execute --local --code 'import run from "kody:@kentcdodds/ship-pr/merge"; export default (p) => run(p)' --params '{"prUrl":"https://github.com/kentcdodds/kody/pull/2943","risk":"medium","dryRun":true}'
# Discord preview
npx @kodycodes/cli execute --local --code 'import run from "kody:@kentcdodds/ship-pr/send-summary"; export default (p) => run(p)' --params '{"prUrl":"https://github.com/kentcdodds/kody/pull/2942","agentId":"bc-test","title":"Smoke","difficulty":"Easy","door":"two-way","cleanup":"needed","cleanupNote":"delete temp flag","summary":"dry run","dryRun":true}'Unit tests: npm test (Node ≥ 22, --experimental-strip-types). They cover the
classifier and the pure computeShip brain in src/status.ts.
Edge cases
- Door/cleanup on Discord reports: pass
door(two-way|one-way) andcleanup(none|needed|done, pluscleanupNotewhen needed) when known. Omit when unknown — never invent. No kind/category field.send-shipped-prmay also parseDoor:/Cleanup:lines from the PR body when omitted. - Bugbot posts as kentcdodds through
@kentcdodds/bugbot. Never commentbugbot runas kody-bot. When the PR author is not kentcdodds, tick reports Bugbotunavailableand does not wait on it. - CodeRabbit or Devin rate limits show up as
unavailable. Below high risk, tick never waits on them. - A reviewer status left pending is not trusted on its own. A non-empty-body
review on the head SHA counts as
completed; empty/whitespace-only CodeRabbit thread-reply reviews are ignored (they do not prove the status is stale). A pending status that has sat 15+ minutes after a substantive review on an earlier commit counts asstalledand is not waited on. Pending with no substantive review still waits; decide the checkskippedif it never posts. - CI is
doneonly when every check run, commit status, and GitHub Actions workflow run on the head SHA is terminal. Queued/in-progress workflow runs count as pending even before their jobs report check runs. After a push, PR open, or ready-for-review, tick waits up to 180s for CI to start before trusting the check list: draft-gated jobs reportskipped, and those skips are stale once the PR is marked ready (tick's ownmark_readyincluded). If nothing re-runs after ready, ci isneeds_attention(skippedWhileDraft). - Check names are deduped. A skipped twin from another workflow run loses to
the real run, and the newest re-run wins.
CANCELLEDcounts as a failure, with a note. - Resolved threads are hidden. Outdated threads stay visible with
outdated: true. - CodeRabbit "Outside diff range comments" live in the review body, not as
review threads. Tick/
review-sortsurface them inmustAddresswithsource: 'outside_diff',cannotReplyInline: true, anddecideTarget. Use that target with./decide(kindthread); do not call./reply-review. ./mergerefuses (refused: true, reasons) while ready / ci / ai-review are unsettled. Fix the issue, or decide it, if you judged it non-blocking.- For
kentcdodds/kody, tick adds repo hints: the control-kody preview for medium+, a single rebase on late conflicts, the Validate → Deploy skipped-run gotcha, and the health check. Other repos get a generic deploy hint. The deploy summary names running or failed runs with their URLs. - Deploy treats
cancelledruns on the merge SHA as superseded (a newer push in the same concurrency group), not failed. Forkentcdodds/kody, tick readshttps://kody.codes/healthand marks deploy done when the servedcommitShais the merge SHA or a descendant (compare API);details.productionshows it. - Friction filing goes to
kody:@kentcdodds/friction-log/filedirectly (tick shows the call). Whentarget.hostiskody, also statically importkody:@kentcdodds/grok-bot/wakeif you use--code.