Corbits Triage triages pull requests on GitHub and Bitbucket Data Center. For every pull request on a repository with triage on, it gathers facts from the host, runs the repository's rules, decides Pass, Fail or Unsure, and suggests or runs the steps the repository's policy lists for that verdict.
Nothing runs by default. Connecting a host records repositories with triage off; a repository is triaged only after someone turns on Triage pull requests in this repository, and only once its policy has a rule. Drafts are skipped until they are marked ready for review, except for one run when a pull request is converted to draft. A pull request marked ready before any triage run wakes the policy as if it were opened.
How to use the portal, screen by screen, is in the user manual.
- Maintainers use the portal: verdicts, reasons and suggested actions for each open pull request.
- Teams share a workspace. Admins invite teammates by email; steps name users or teams for assignment and review.
- Authors see only what the repository's actions write on the pull request: labels, review requests, assignees, comments, and on Bitbucket a build status.
- Inbox: open pull requests across repositories with triage on, in three views, Needs you, Ready (passed on the latest commit, CI passed, no conflicts, oldest first) and All open. A row is a state mark and the title, then the number (with the repository's name unless grouped by repository), a short status, the author when grouped by repository, and its age. Each opens a reading pane: the status line, which says what the bar's main button does and which rules it is about (the row says the same, shortened), the problems (failed rules, unsure answers and rules the policy asks a maintainer about, rules that could not run, each named, then why), the description, and folded rows for the linked issue, the changed files and the rules that passed (Checked and fine). A bar at the bottom offers the next step; merging from the portal is pinned to the commit Triage checked. Pull requests waiting on the author, on CI or on Triage, and drafts, sit behind Waiting.
- Pulse: what changed since your last visit and what is stuck.
- Repositories: per repository, the triage switch and Triage open pull requests again, its policy file read back as sentences, where code review runs when the server has no place for it (the policy file turns code review on), the actions Triage takes on its own, and outcomes before and after Triage.
- Settings: Triage (sources, policy files, the decision model key, and one switch per action kind for every repository), Code review (the provider's key and where reviews run), people and invitations, the account, and each host's listener version for support. Roles and grants change only through the hub routes.
The portal shows the verdict, its score and the reasons. Evidence for a file
cites its changed lines (src/router.ts:40-58) and links them at the commit
that was judged. The pull request sees only the actions.
A rule answers one question about a pull request: pass, fail, or unconfirmed.
| Kind | Evaluates | Cost | Use it for |
|---|---|---|---|
| Rule | Host data only: files, size, linked issue, mergeability, reviews, CI, text. No inference. | Host API calls. | Anything the host already knows. |
| Decision model | Fixed-shape questions to a System One model (Jev), citing the pull request as evidence. | One model request per batch of questions, billed by the model provider. | Judgment a rule cannot make: is the change focused, does it need docs. |
The same pull request and policy give the same result. A rule is unconfirmed only while the host has not computed what it needs, such as mergeability or CI.
Built in: Max files changed, Max lines changed, Max lines changed per file, Max additions, Max deletions (lockfiles left out of the GitHub counts; on Bitbucket Data Center the per-file rule always passes, and lockfile lines stay in the line, addition, and deletion totals), Linked issue (GitHub, Linear or
either), Merge conflicts, CI green, Forbidden paths, Conventional commits
(the title and each of the first 100 commits' first line start
type(scope)!: , in any case; merges and reverts skipped), Hidden content
(what cleaning the title, description, commits and paths removed carried
meaning: two or more invisible characters together, tag characters outside
the three subdivision flags, variation selectors that select nothing (not a
lone emoji-style one), bidi overrides or isolates, or a link with no visible
content; on GitHub also hidden markup in the body's markdown, a comment,
reference definition, processing instruction, CDATA section or declaration
not exactly as a pull request template has it (the repository's, else its
organisation's .github repository's), that speaks to a model or
holds those characters; it names what it found and where, never the text, and
misses paraphrased, look-alike-letter, entity-encoded or split instructions),
Regression risk (a changed file with four or more commits titled as a fix or revert on the
default branch in the last 30 days, or a migration; labels by default), No duplicate
(an open pull request opened earlier on the same base branch, not a draft, has
the same latest commit, diff or commits, the commits alone where the host gives
no diff to compare; unconfirmed past 100 of them), Sensitive paths (named groups
of globs, high or medium. Its default onFail is ask, so a high group asks a
maintainer and adds no level label. A level label is written only when onFail
is label.) and Risk (low,
medium or high from sensitive, size, no-tests, hot-files and first-time; high
asks a maintainer by default, medium labels risk: medium). Two always run:
Draft, and Requested changes (a requested change fails and waits on
the author, then on the reviewer once the author pushes or replies). The
recommended forbidden paths are vendor/**, node_modules/** and
.github/**: an author who edits the workflows can change which checks run.
A text rule fails when the title, the description or any commit message (the
first 100) contains one of its phrases, or with not-contains when none
does, ignoring case. A contains rule also reads the description as written,
with its HTML comments and link addresses; on GitHub, with hidden-content
on, not the comments of a pull request template. A policy holds at most eight
rules of its own, text and decision-model together.
Each question has a fixed shape, so the answer is a probability or a choice, never free text:
- Is true: a yes or no claim about the whole pull request, each changed file, or each changed hunk, optionally only for paths matching globs.
- Score: which of 2 to 10 described levels, worst first, fits a subject. It fails only when fail below names a level.
- Choose: which of the policy's options best describes the pull request; it fails when that is one of the failing options.
Built in: Focused (each change is classified against the primary change), Docs (each changed file is asked whether it leaves the docs wrong or incomplete; changing a doc file satisfies the rule) and Duplicates an open pull request (up to five open pull requests opened earlier that share a changed path, or two title words of four or more letters, are each scored same change, overlaps or unrelated; one likely the same change fails it, and it asks a maintainer by default).
An answer decides only when it is clear of a band around a coin flip: 0.7 or above, or 0.3 or below (0.6 for Focused). Anything inside the band, a missing answer or a failed model call leaves the rule unconfirmed rather than guessed. A repository whose rules are all over host data never calls the model.
Focused is skipped on a branch another pull request was merged into, and says why. A pull request based on another open pull request's branch is checked on its own changes, and the comment says what it is stacked on.
The model reads the pull request and the repository's docs from its default
branch. Custom rules also get the repository's contribution rules
(AGENTS.md, CLAUDE.md, CONTRIBUTING, the pull request template).
DECISION_MODEL.md has how questions are asked, budgets,
cost and measured accuracy.
Each pull request head gets one verdict: Pass, Fail or Unsure.
- A failed rule over host data decides Fail without calling the model. So does a decision-model rule that fails, so nothing the pull request says can talk it into a pass.
- A policy with no decision-model rule never calls the model: every rule passing is Pass, and a rule the host has not settled yet, such as pending CI, makes it wait (see Waiting for CI).
- Otherwise one decision-model question decides: does this pull request pass this repository's policy? It reads every rule with its result and evidence and as much of the pull request as fits one request. Pass at 0.7 or above for pass, Fail at 0.7 or above for fail, Unsure otherwise. A judged rule left unconfirmed turns a pass into Unsure. The score shown is the probability of pass. An Unsure comment names each rule Triage could not confirm.
A rule's onFail changes what its failure does. ask makes it Unsure
for a maintainer instead of Fail, unless another rule fails it, and the
verdict question is not asked. label rules are left out of all three: they
never decide Fail or Unsure, and the verdict question neither asks about nor
sees them.
Draft pull requests are not decided. A closed or merged one gets no verdict, and Triage never writes to it. A model that does not answer leaves the verdict Unsure and Triage checks it again.
Triage keeps one comment on the pull request: the verdict, the reasons, and what Triage did or what waits on a maintainer. A re-triage edits it in place, or writes nothing when nothing changed. When only a policy change flips the verdict on the same head, Triage posts a new comment naming the policy versions and marks the earlier one superseded.
The policy holds one When (the events that wake it: opened, updated,
checks, ready; all four by default) and three ordered lists of steps:
On Pass, On Fail and On Unsure. The verdict picks the list. Each
step names an action and, optionally, when conditions.
-
Actions: approve, merge, close, comment, request changes, request review (from users, teams or code owners), assign, labels, or nothing.
-
Conditions, which must all hold for the step to run:
author: any of GitHub's author types (OWNER,MEMBER,COLLABORATOR,CONTRIBUTOR,FIRST_TIME_CONTRIBUTOR,FIRST_TIMER,NONE,MANNEQUIN),BOTfor any bot, the members of a team ({ team }), or named people ({ users }). The groupsmaintainers,external,first-timersandbotsstill read as those types.linkedIssue: true or false.ci: any ofpassing,failing,pendingornone.filesandlines:{ over: N },{ atMost: N }or both; lines are additions plus deletions.
A step whose conditions do not hold, or whose author the host cannot place, is skipped, and the verdict records why. An empty
whenis refused rather than read as no condition.
By default, Unsure asks the code owners for review.
actions:
on: [opened, updated, checks, ready]
pass:
- do: approve
- do: merge
when: { author: [OWNER, MEMBER, COLLABORATOR], ci: [passing] }
fail:
- do: request-changes
- do: close
when: { author: [BOT, FIRST_TIME_CONTRIBUTOR, FIRST_TIMER], lines: { over: 1000 }, linkedIssue: false }
unsure:
- do: request-review
target: { to: codeowners }Actions of the kinds Triage's own pr-triage role holds run during triage,
allowed per repository on its page, where one approval allows every kind its
policy uses; the rest wait in the Inbox for a person whose role holds that
kind. Turning triage on for a repository lets
Triage post its comment and verdict label there; everything else waits until
you allow it. Close never runs on its own on Unsure, and merge never on
anything but Pass.
Approve runs only on a Pass with CI green. A merge waits until CI passes on the head (or runs without CI only when the policy switches CI green off), and refuses while anyone has requested changes; Triage's own request for changes is taken back first. Branch protection stays the backstop. Request changes lists the failed rules, once per head, and is dismissed once none still fails.
When the policy depends on CI (CI green is on, a step has a ci condition, or
a step approves or merges), Triage waits for CI to finish on the head, or for
its first check to fail, before it triages it. A head with no checks after 2
minutes is taken as having no CI, and none waits longer than an hour; a run by
hand does not wait. Meanwhile the Inbox shows Waiting for CI. On GitHub a
pull request labelled Triage: Not Ready or Triage: Needs Human Review
keeps it, so a push that leaves the verdict as it was changes no label; any
other shows Triage: Waiting for CI until the verdict replaces it, or no
Triage label while the head has no checks. On Bitbucket the head's Corbits Triage build status is in progress until its verdict.
A head triaged anyway while the only rules left to confirm are the host's own
(CI still running, mergeability not computed yet) gets no verdict yet: it
shows the same marker, nothing is posted or run, and it is triaged again once
they are done. Past the hour, on Triage again, or on its last retry, it gets
its verdict anyway. A head triaged while CI still runs for a step's CI condition
has every action held until its checks finish.
Labels are the handoff: other tools, such as a coding agent, watch for them. Their names are stable:
| Label | When |
|---|---|
Triage: Ready |
The head passes. Taken off as soon as Triage sees a new head, until that head passes. |
Triage: Not Ready |
The head fails. |
Triage: Needs Human Review |
The head is unsure. |
Triage: Waiting for CI |
Triage is waiting for the head's CI before it decides, unless the pull request is Not Ready or Needs Human Review or the head has no checks. |
rule: <rule id> |
A rule that failed on the head, such as rule: docs, from a { do: labels, target: { from: rules } } step. |
A head carries one verdict label at a time, and Triage takes back the ones
that no longer apply, including the older Triage: Needs changes, Triage: Needs review, Triage: Ready to merge and triage:… labels. A policy may rename any verdict's label (up to 50
characters) under labels, for example labels: { pass: Ready to ship }.
Bitbucket Data Center has no labels; the Corbits Triage build status carries
the verdict there.
A repository's policy is .corbits-triage.yml at the root of its default
branch (.corbits-triage.yaml when there is no .yml), so policy changes are
reviewed like code. It lists rules, actions and labels, and may
extends another repository's policy file. It may also
hold the v4 keys groups, code-review, security, deslop, pace,
pauseLabel and decide. Any other key is refused, naming
the key it was likely meant to be. A step's when.author may name a group.
On GitHub, a pull request carrying pauseLabel starts no run, and while the
pull request cannot be read it is not triaged but retried, never run unpaused;
through a long GitHub outage it waits out a bounded number of retries and is
triaged on its next event. The code-review block runs a review whatever
provider it names. security, deslop and pace are accepted and run nothing. decide
is not carried out. A file that turns a block on, or writes its own decide,
holds approve and merge for a person in the Inbox. decide replaces actions,
so a file holds one or the other, and a file that uses extends holds none of
these keys.
Each rule is one sentence (text), how it is checked (check: a built-in
rule's id, model for a yes/no question about the whole pull request, or
text for a phrase match) and why it exists: a one-line reason and its
evidence (#44, owner/name#44, links or maintainer-stated). All four
are required. A model, text or code-patterns rule names its own id. A built-in rule takes
its setting as max (size limits), tracker (linked issue), paths
(forbidden paths) or types and fields (conventional commits). A
code-patterns rule lists shipped presets (convention) and its own ast-grep
patterns, and fails on a match in a line the pull request adds to a
TypeScript or JavaScript file (.ts, .tsx, .mts, .cts, .js, .jsx,
.mjs, .cjs); it checks nothing in other languages. Its own pattern wins on
a shared id in both languages; to keep checking JavaScript, add a
language: javascript pattern under the same id. A pattern's ast-grep files
and ignores lists scope it to paths by ast-grep's glob rules (strings only;
a negated class is [^a], and [!a] or an invalid glob refuses the policy),
and the preset's fallback patterns ignore test files. enabled: false keeps it
written down but off.
onFail says what a failing rule does: fail (the default, except label
for the size rules and regression risk, and ask for sensitive paths, risk
and the judged duplicate rule) fails the pull
request for its author, ask makes it Unsure and lists the rule for a
maintainer instead of the author, and label makes the rule label the pull
request instead of deciding its verdict. A top-level onFail is the default
for the file's rules. With label, when the rule fails or stays unsure, the
verdict is unchanged, the comment notes it, and an Add labels action adds its label (size: large,
docs: check, <id>: check, or the rule's own label), which stays until a
person removes it, except the level labels of sensitive paths and risk, which
Triage takes back when the level changes. A label starting Triage: or rule: , or a verdict label,
is refused. On Bitbucket Data Center it is only noted.
rules:
- check: max-lines-changed
text: A pull request changes at most 400 lines.
max: 400
reason: Large changes were split before review.
evidence: ["#44", "#57"]
- id: errors-are-handled
check: model
text: Every new call that can fail handles or propagates its error.
reason: Two outages came from swallowed errors.
evidence: ["acme/api#312"]
- id: no-attribution-trailers
check: text
text: Descriptions and commits carry no tool attribution.
fields: [description, commits] # any of title, description, commits
failWhen: contains # or not-contains
phrases: ["Co-Authored-By:", "Generated with"]
reason: Maintainer-stated.
evidence: [maintainer-stated]
- id: conventions
check: code-patterns
text: Added code follows our conventions.
presets: [convention]
patterns: # ast-grep rules; `rule` is an ast-grep rule object
- id: no-console
language: typescript # or javascript
message: Remove console logging.
ignores: ["scripts/**"] # ast-grep globs; `files` checks only the paths it names
rule: { pattern: "console.log($$$ARGS)" }
onFail: label
reason: Review time goes to design, not style.
evidence: [maintainer-stated]Only YAML is read, up to 64 KB and without anchors or aliases. Names, claims,
labels and the like are one line of at most 500 characters (50 for a label);
a comment keeps its line breaks. A list of labels, people or teams holds at
most 50, and a verdict at most 20 steps. The JSON Schema is policyFile at the hub's
/api/integrations/pack-schema, and POST /api/integrations/policy/validate
checks a file's text without signing in. The GitHub Action in
packages/policy-check-action
checks the file the same way on pull requests, and runs the base branch policy's
rules that need no model, except ci-green, against each one.
A push to the default branch that changes the file is read before the next
verdict. A file Triage refuses stops triage on the repository until it is
fixed; the portal shows each problem with its line. Score and choose rules and
file or hunk scope are written in the stored policy's shape, which a file
without rules is read in; see
@corbits/triage-contracts.
A repository without the file offers Start a policy file: a pull request adding the starter rules (at most 30 files and 3,000 lines changed, no merge conflicts, CI green, sensitive paths and risk; no actions). Rules saved in the portal by earlier builds keep running until they move into the file through a pull request the portal opens, and only their creator, the workspace's admins and its owners can change them; the manual walks through it. Wording, reasons and evidence never count as a difference between the two.
extends: owner/name (PROJECT/slug on Bitbucket) starts from that
repository's policy file on its default branch. It is a default, not a floor:
the extending file may switch off or replace any of its rules, its When, any
verdict's steps and any verdict's label. The repository must be connected to the
same workspace and its file must not extend another; otherwise triage refuses
the file and says why. A push or merge to that file reaches every repository
extending it from their next verdict. The pull request check validates the file
itself, not the policy it extends.
Code review reads a pull request for defects: correctness bugs in the changed lines and breakage the change causes elsewhere in the repository. Each defect names the line, why it is wrong, what triggers it and a fix, most severe and most agreed-on first. It is separate from the verdict and optional.
A workspace turns it on by saving an Anthropic key under Settings → Code review and switching it on per repository; each reviewed commit is billed to that Anthropic account. Every new commit of a non-draft pull request is reviewed, and Review again reviews the current one once more. A diff over 100,000 characters is cut; the review says how much it read and names the files it left out, and so does the review sent to GitHub.
Each review checks out the pull request's code, so it runs in an isolated place of its own; until the server has one, the repository page links Settings → Code review, which lists the places this server knows and how to set each up (provisioners), in place of the switch. Findings show in the reading pane. On GitHub, Send to GitHub posts the newest review as one review from the Triage App, inline where the diff shows the lines, once per commit and never on its own. On Bitbucket Data Center, Send to Bitbucket posts each finding the diff shows as a comment on its first shown line and one summary comment with the rest, once per commit. Sending needs only a finished review, so it works without an isolated place.
Turning triage on triages a GitHub repository's open pull requests; after that, events are triaged as they arrive, and the hub re-queues any open pull request whose latest commit has no verdict. A pull request merged into the default branch makes the repository's other open pull requests due again, each at most once per 15 minutes. A re-check reuses the decision model's earlier answers for anything unchanged.
- The hub does not run the repository scan, which measures closed pull requests.
- The Railway Sandboxes provisioner uses the operator's Railway token only; a workspace cannot bring its own.