Skip to content

AGENTS.md

163 lines · 10.8 KB · Markdown
Raw

@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

ExportUse
./tickMain 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-sortFull 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: 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:<hash> 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)

# 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.