# @kentcdodds/ship-pr — agent notes Human intent lives in [`README.md`](./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".) ```bash 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 ``. ## 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](https://kody.codes/docs/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'|'check'|'step', target, disposition: 'ignored'|'skipped'|'wontfix'|'accepted', reason, ttlDays?, reply? }` (`reply: true` posts `reason` on the thread; a string posts that text) or `{ kind, target, remove: true }`. | | `./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'|'Medium'|'Hard', summary, model?, door?, cleanup?, cleanupNote?, status?, extras?, dryRun? }`: Discord post that also marks `report` done. Pass door/cleanup explicitly when known; never invent. | | `.` | 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: true` drops it, along with prose notes, and collapses hidden or already-handled items to counts. Re-tick `exampleInvokes` pass `brief: true`, so after the first tick you get compact output by default. - `done[]` / `remaining[]`: progress. Only the **focus** step (and any `expand` steps) carries `details` + `exampleInvokes`. - `exit`: `{ done, status: 'waiting'|'needs_attention'|'action_available'|'ready_to_merge'|'done'|'blocked', pollAfterSeconds?, wakeHints?, reportStatus }`. `reportStatus` is `Pending` until the PR merges (`Shipped`), you park it (`Parked`), or it closes unmerged (`Blocked`). When `waiting`, do **not** tight-loop. End the turn only after arming a one-shot Cursor `subscribe_timer` from `exit.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 new `wakeHints`. - `automations`: what `applySafeAutomations` did (`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's `outbound_fetches_per_day` usage with a `warning`. At 80%+ tick raises `exit.pollAfterSeconds` to 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) through `usageGet`, which is not an outbound fetch. - `exit.status: 'blocked'` with `reason: 'plan_limit'` and `retryAt`: the account already hit a daily plan cap, so every fetching call fails until 00:00 UTC. Stop ticking until `retryAt`. ## 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) - `thread` target: the root `commentId` from tick, the `PRRT_…` thread id, or an outside-diff `decideTarget` (`cr-comment:v1:…` from CodeRabbit markers, or `outside-diff:` fallback). Pass `reply` only with a numeric comment id (outside-diff findings have no inline thread — fix in code and/or decide). - `check` target: the check name exactly as listed, or an AI reviewer name (`bugbot`, `coderabbit`, `devin`, `seer`) to stop waiting on it. - `step` target: `ready | ci | ai-review | merge | deploy | friction | report`. - `merge` + `skipped` = park (status becomes Parked). - `friction` + `accepted` (filed …) or `skipped` (nothing qualified) completes the friction pass. - `deploy` + `accepted`/`skipped` when verified another way or nothing deploys. - A decision expires after `ttlDays` (default 7, max 7). Re-recording the same `kind` + `target` replaces it. ## Smoke tests (read-only / dry-run) ```bash # 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`) and `cleanup` (`none`|`needed`|`done`, plus `cleanupNote` when needed) when known. Omit when unknown — never invent. No kind/category field. `send-shipped-pr` may also parse `Door:` / `Cleanup:` lines from the PR body when omitted. - Bugbot posts as kentcdodds through `@kentcdodds/bugbot`. Never comment `bugbot run` as kody-bot. When the PR author is not kentcdodds, tick reports Bugbot `unavailable` and 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 as `stalled` and is not waited on. Pending with no substantive review still waits; decide the check `skipped` if it never posts. - CI is `done` only 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 report `skipped`, and those skips are stale once the PR is marked ready (tick's own `mark_ready` included). If nothing re-runs after ready, ci is `needs_attention` (`skippedWhileDraft`). - Check names are deduped. A skipped twin from another workflow run loses to the real run, and the newest re-run wins. `CANCELLED` counts 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-sort` surface them in `mustAddress` with `source: 'outside_diff'`, `cannotReplyInline: true`, and `decideTarget`. Use that target with `./decide` (kind `thread`); do not call `./reply-review`. - `./merge` refuses (`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 `cancelled` runs on the merge SHA as superseded (a newer push in the same concurrency group), not failed. For `kentcdodds/kody`, tick reads `https://kody.codes/health` and marks deploy done when the served `commitSha` is the merge SHA or a descendant (compare API); `details.production` shows it. - Friction filing goes to `kody:@kentcdodds/friction-log/file` directly (tick shows the call). When `target.host` is `kody`, also statically import `kody:@kentcdodds/grok-bot/wake` if you use `--code`.