PR Review Checklist
Internal checklist for reviewing iris-core PRs — architecture fit first, then invariant-specific correctness checks.
Internal doc, not part of the public docs site navigation — a working reference for reviewing PRs against this repo's actual invariants, not generic style advice.
Fast pass — architecture and approach
Check these before reading line-by-line. Failing any of these means redirect the PR (comment with the concern) rather than proceeding to a detailed review — a correctly-implemented wrong approach still needs to be rejected.
- Engine stays transport-agnostic. Does this PR add any Slack- or
Telegram-specific code, types, or string literals to
src/engine/agent.tsorsrc/engine/index.ts? Both should only referencetransport/types.tsshapes (MessageContext,TransportEvent,ChannelTransport,TransportPromptProfile). A new "if transport === slack" branch in either file is the wrong layer — it belongs inslack.ts/telegram.ts(src/transports/) or on the transport'sTransportPromptProfile. - Does this duplicate
ChannelTransport? A new transport (or a transport-shaped helper) should implement the interface intransport/types.tsand plug intocreateEngine's dispatch — not grow a parallel bespoke integration path bolted ontomain.ts. - Does this duplicate per-channel state?
src/engine/index.ts'schannelStates: Map<string, ChannelState>(viagetState/getOrCreateRunner) is the one place run/store/stop state lives. A PR that adds its own channel-keyedMapfor similar bookkeeping should instead extendChannelStateor route through the engine. - Skill boundary respected. Core skills (
skills/) must be platform-operation skills usable by any install; anything client/domain-specific (a particular API integration, a client's business logic) belongs in an overlay (docs/overlay.md), not committed to core. - No new fork-shaped divergence. If the PR reimplements something that
already has a near-identical counterpart elsewhere in the file (a second
near-copy of a handler, a second copy of a chunking/splitting function), a
shared helper is very likely already there or should be — this is
literally the bug class Phase 2 exists to kill (see
src/engine/index.ts's history: it replaced two duplicated Slack/Telegram handlers). - Stacked-PR check. If the PR branches off another open PR, confirm it's
a real functional dependency (the diff needs types/functions the base PR
introduces) and not just "I happened to branch here." A convenience stack
needlessly blocks unrelated work — flag it and suggest rebasing onto
main.
Detail pass — invariant-specific correctness
Each item below is checkable against a specific file, not aspirational.
- Slack envelopes ack exactly once. Every
socketClient.on("app_mention" | "message", ...)handler insrc/transports/slack/slack.tsmust callack()exactly once per invocation, including on the error path. The existing pattern islet acked = false+ackOnce()wrapper +try/catch/finally { ackOnce() }(seesetupEventHandlers()around line 787). A new handler or an edit that adds an earlyreturnbeforeackOnce()breaks this — a missed ack causes Slack to redeliver and repeat the failure. - Channel directory resolution goes through
resolveChannelDir/resolveChannelPath. Nothing should hand-build a channel's on-disk path (workingDir + channelId,slack/${id}, etc.) — always callresolveChannelDir(workingDir, channelId)(src/engine/store.ts). It encodes the Slack/Telegram/virtual-channel (SESSION-,BRIDGE-,ESCALATE-,SELFHEAL-,WEBUI) split; a hand-built path silently drifts from it the next time that split changes. -
TransportEvent/MessageContextfields aren't skipped. If a PR adds or touches a context factory (createSlackContext/createTelegramContextinslack.ts/telegram.ts), the returned object must satisfyMessageContextintransport/types.tsin full — in particulartransportIdmust be stamped correctly ("slack" | "telegram" | "bridge" | "web"), sinceagent.tsuses it to look up the prompt profile viagetPromptProfile(ctx.transportId)and throws if none is registered. - New transport-specific prompt text lives on
TransportPromptProfile, not inagent.ts.buildSystemPromptinsrc/engine/agent.tsmust stay free of hardcoded Slack/Telegram strings — formatting rules, identity lines, attachment tag names, etc. all come from theprofileparameter.grep -ri slack src/engine/agent.tsshould return nothing (this was IRIS-49's acceptance bar and should stay true). - Wildcard channel config resolves through one path. Any new
per-channel setting in
channels.jsonmust be read viaresolveChannelConfig/its wrapper getters inslack.ts(exact match, else longest matching wildcard prefix) — not a second ad hocchannelConfigs.get(id)lookup that skips wildcard resolution. This exact bug (requireMentionForTopLevelworking for exact IDs but not wildcards) was fixed in PR #37 and is covered byiris-runtime/test/dispatch.test.mjs. - Unknown/malformed
channels.jsonentries fail closed, not half-open.loadChannelModes()must reject an entry with an unrecognizedmodeoutright (skip it, log a warning) rather than applying some fields (e.g.requireMentionForTopLevel) while ignoring others. Covered by the"config: unknown mode entry is skipped entirely"test — if a PR touchesloadChannelModes(), that test must still pass. - API endpoints requiring auth check
IRIS_API_TOKENviabearerTokenMatches. Any new endpoint instartApiServer(src/engine/api.ts) other thanGET /healthis gated by the existingapiToken && !bearerTokenMatches(...)check near the top of the request handler — don't add a second, parallel auth check, and don't add an endpoint before that gate. - Secrets never land in a channel/session log or a passthrough error
path.
resolvePassthroughKey(src/transports/slack/slack.ts) resolves viaPASSTHROUGH_API_KEYor theget-secretskill and must never write the resolved value intolog.jsonl,last_prompt.jsonl, or a posted Slack/ Telegram message. If a PR adds a new secret-bearing config field, check it isn't logged anywhere the waytext/payloadfields are. - Every behavior-changing PR updates
iris-runtime/CHANGELOG.mdunder[Unreleased]and the relevantdocs/page, or carries thechangelog-not-needed/docs-not-neededlabel. This is enforced by.github/workflows/docs-guard.ymlagainst paths matchingiris-runtime/src/,skills/,scripts/,agents/,bootstrap.sh,install.sh— but the CI gate only checks a file was touched, not that the content is adequate. Review the changelog entry for accuracy, not just presence.
Leave to tooling
Don't manually check what CI already checks — spend review time on the invariants above instead:
- Formatting, lint, unused imports — not currently enforced by a dedicated lint step; if one gets added, defer to it entirely rather than commenting on style.
- Type correctness —
npm run build(tsc -p tsconfig.build.json) is strict-mode TypeScript; if it compiles, don't re-derive type errors by eye. - Dispatch regression coverage —
npm test(iris-runtime/test/) exercises the channel-mode × message-path matrix end-to-end against the compiled output. If a PR touches dispatch logic and this suite passes, trust it over re-tracing every mode by hand; if a PR should have added a test case here and didn't, that's a detail-pass finding, not a re-derivation of the existing ones. - Smoke boot —
scripts/smoke.shverifies the bridge-only boot path stays alive; don't manually re-verify basic startup if it's green. - Secret scanning —
gitleaksruns on every PR against full git history; don't manually grep for committed secrets, but do still apply the "secrets never land in a log" invariant above, which gitleaks can't see (it's about runtime behavior, not committed content). - Static analysis —
.github/workflows/codeql.ymlruns CodeQL on every PR and push tomain, plus a weekly schedule (catches newly-added query-pack rules against code no PR touched). Scheduled runs have no originating PR, so GitHub's Copilot Autofix opens a standalonealert-autofix-NPR againstmainfor each finding — review these like any other PR (correctness of the fix, plus the changelog/docs-guard requirement above), not as noise to wave through.