| name | pr-onboard |
| description | One-time repository onboarding for PR review — builds a RepoProfile (conventions, hot files, security surfaces, nit catalogue, test command) stored as workspace learnings. Re-runs automatically when the profile is stale (>30 days or >30% of files changed). |
| version | 1.0.0 |
| author | kai-agent |
| metadata | {"kai":{"tags":["github","pr-review","onboarding","conventions","code-quality"],"related_skills":["github-code-review","self-onboard"]}} |
PR Review — Repository Onboarding
Builds the RepoProfile artifact for a repository. This is called by the github-code-review skill before the first review and when the profile is stale. It is never called directly by the user.
The output is a set of workspace learnings keyed by pr_review:<field>:<repoId>. The reviewing agent reads these learnings on every PR to ground its review in this team's actual conventions instead of generic best practices.
This is the single biggest quality lever. A reviewer that knows this team's patterns cuts false positives dramatically. Run it once, run it well.
Prerequisites
kai_activate_category(category="integrations") must have been called in this session.
workspaceId and repoId are known (passed by the caller).
Phase 1: Gather Raw Signals
Collect the data that conventions, hot files, and nit catalogue are derived from. Do all reads in parallel where possible.
1a. Recent commit history
kai_list_commits(workspaceId, repoId, limit=200)
Extract:
- Top 20 most-frequently-changed files (commit count in last 200 commits) →
hot_files
- Authors with > 5 commits →
team_members (used for owner map)
- Commit message style: imperative ("fix: …"), conventional commits, free-form? →
commit_style
1b. Recent merged PRs with review threads
kai_list_pull_requests(workspaceId, repoId, state="closed", limit=50)
For each PR that has merged=true, fetch both the PR detail AND its review comments in parallel:
kai_get_pull_request(workspaceId, repoId, number=<n>)
kai_list_pull_request_review_comments(workspaceId, repoId, number=<n>)
Read the review comments (not just PR body). Classify each review comment into one of:
correctness | security | performance | maintainability | style | test | docs | nitpick
Track per-category:
- Count of comments in that category
- Estimated acceptance rate: did the file change in a follow-up commit on the same PR? (proxy: PR has > 1 commit and the commented file appears in later commits)
- Example verbatim quotes (top 2 per category)
Nit catalogue extraction: Comments matching patterns like "use X instead of Y", "missing null check", "prefer const over let", "add a test for the edge case" are high-value nits. Cluster by structural similarity. Discard clusters with < 2 occurrences.
Stop if > 15 PR detail calls would be needed — sample the 15 most recent merged PRs only.
1c. Representative source files
Browse the top-level and key subdirectories to identify the primary source layout:
kai_browse_repository_files(workspaceId, repoId, path="")
kai_browse_repository_files(workspaceId, repoId, path="src") # if exists
kai_browse_repository_files(workspaceId, repoId, path="tests") # or __tests__, test, spec
Read 8–12 representative files spanning different layers (entry points, models/types, utilities, tests):
kai_read_repository_files(workspaceId, repoId, paths=[...8–12 files...])
Choose files from different parts of the repo, not all from one directory.
1d. Config and tooling files
kai_read_repository_files(workspaceId, repoId, paths=[
"package.json", # or pyproject.toml, go.mod, Cargo.toml, pom.xml
".eslintrc*", # or .eslintrc.json, eslint.config.*
"tsconfig.json",
".prettierrc*",
"pyproject.toml", # Python
"mypy.ini", # Python
".golangci.yml", # Go
"Makefile",
"biome.json",
])
Only read files that exist (skip 404s silently).
Phase 2: Extract Conventions
Using the files you read in Phase 1c + 1d, follow the extraction rules in prompts/convention_extractor.md.
The output of this phase is a single conventions JSON object conforming to the schema in that prompt. Every field must be a concrete, falsifiable fact. Unknown fields must be null — never guess.
Phase 3: Identify Security Surfaces
From the file tree and commit history, flag paths that are historically high-risk. These paths get a mandatory security specialist pass during review regardless of diff size.
Authentication and authorization: files matching auth*, session*, token*, jwt*, oauth*, permission*, role*, middleware/auth*
Cryptography: files matching crypto*, hash*, encrypt*, sign*, verify*, secret*, key*
Data boundaries: files matching api*, route*, handler*, controller*, webhook*, request*
Persistence: files matching db*, database*, query*, model*, schema*, migration*
Execution: files matching exec*, spawn*, eval*, shell*, command*, subprocess*
External IO: files matching http*, fetch*, client*, request*, upload*, download*
Record the matched paths (not patterns) — these are what matter.
Phase 4: Assemble and Save the RepoProfile
Call workspace_learnings_add once per field. Use category="pr_review" and repoId=repoId on all of them. Include a _type discriminator as the first field in the content JSON — the review skill uses this to identify each learning when listing by (category, repoId).
# Freshness anchor — checked by github-code-review to decide if re-onboard needed
workspace_learnings_add(
workspaceId,
content=JSON.stringify({ _type: "profile_sha", sha: "<latest commit SHA from Phase 1a>" }),
category="pr_review",
repoId=repoId
)
workspace_learnings_add(
workspaceId,
content=JSON.stringify({
_type: "conventions",
naming: { functions: "camelCase", files: "kebab-case", tests: "*.test.ts", constants: "UPPER_SNAKE" },
errorHandling: "throws + try/catch at route boundaries; custom error classes in src/errors/",
async: "async/await throughout; never .then() chaining",
logging: "pino structured logger; info for requests, warn for recoverable errors, error for thrown",
imports: "barrel exports via index.ts; @/ alias for src/",
commitStyle: "conventional commits (feat:/fix:/chore:)"
# ... actual values from Phase 2
}),
category="pr_review",
repoId=repoId
)
workspace_learnings_add(
workspaceId,
content=JSON.stringify({
_type: "hot_files",
files: [
{ path: "src/auth/session.ts", commitCount: 47, note: "historically bug-prone; 3 security fixes in last 6 months" },
{ path: "src/payments/stripe.ts", commitCount: 31, note: "high churn; treat changes here carefully" },
# top 20 files
]
}),
category="pr_review",
repoId=repoId
)
workspace_learnings_add(
workspaceId,
content=JSON.stringify({
_type: "nit_catalogue",
clusters: [
{
id: "nit_001",
category: "correctness",
pattern: "missing null check before .id access on optional relation",
examples: ["'user.role should be checked before accessing user.role.permissions'"],
frequency: 8,
estimatedAcceptanceRate: 0.87
},
# all clusters with frequency >= 2
]
}),
category="pr_review",
repoId=repoId
)
workspace_learnings_add(
workspaceId,
content=JSON.stringify({
_type: "security_surfaces",
auth: ["src/auth/session.ts", "src/middleware/auth.ts", "src/lib/jwt.ts"],
crypto: ["src/utils/crypto.ts"],
dataBoundaries: ["src/routes/webhooks/", "src/routes/user/auth/"],
persistence: ["src/models/"],
execution: [],
externalIO: ["src/clients/"]
}),
category="pr_review",
repoId=repoId
)
workspace_learnings_add(
workspaceId,
content=JSON.stringify({ _type: "test_command", command: "bun test" }), # or "npm test", "pytest", "go test ./...", null
category="pr_review",
repoId=repoId
)
Phase 5: Report to Caller
Return a single summary — this is called programmatically, not by the user:
RepoProfile built for <repo>:
- Conventions: <3 most distinctive facts>
- Hot files: <top 3 paths>
- Nit catalogue: <N clusters, top category>
- Security surfaces: <N paths flagged>
- Test command: <command or "not detected">
- Profile SHA: <sha>
Freshness Rules (enforced by the caller, documented here for reference)
To check freshness, call:
workspace_learnings_list(workspaceId, category="pr_review", repoId=repoId)
Re-run this skill if any of these are true:
- No learning with
content._type === "profile_sha" exists in the returned list
- Latest commit SHA from
kai_list_commits does not match content.sha in the profile_sha learning AND > 30 days since the learning's createdAt
- The current PR touches > 30% of the files in the
hot_files learning's files array (major refactor signal)
Do NOT re-run on every PR — the profile is designed to be stable across dozens of PRs.