DocsEnforcement: PRs, issues and reviews
Enforcement: PRs, issues and reviews
The kit checks three things: the PR (size, scope, pattern checks, title and body), the issue (title and body), and the review (the kit writes the review body itself). The same code runs in three places. Only the CI check blocks a merge for everyone.
| Where | What it covers | Who it covers | Can it be skipped? |
|---|---|---|---|
CI: the desk workflow |
PR gates and PR format (a required check); issue format (a label and a comment) | Everyone: every client, gh, the GitHub web page |
No, when the branch rule requires desk / pr |
| Client hooks | gh pr create, gh pr edit --body, gh issue create, gh issue edit from an AI agent |
A person who ran the install, in Claude Code, Codex, Cursor, Antigravity or Hermes | Yes: another tool, or the web page |
| The issue form | The fields of a new issue on the web page | Everyone who uses the web page | No: blank_issues_enabled: false turns off blank issues |
git push hook |
The gates, before a push | A person who ran desk install --git-hook |
Yes: git push --no-verify |
PRs
desk initwrites.github/workflows/desk.yml. On each PR event it runs themunhq/deskaction, which runsdesk ci:- the size gate (
CHK-C3): the changed lines in existing files, warning at 1,500 and failure at 5,000 by default. New files and tests do not count, unlesssize.newFilesFailorsize.testsFailsets a limit for them. Thesize/overridelabel turns the failure into a warning; - the mixed-scope gate (
CHK-C4) and every check inconfig.json, on the added lines; - the PR format:
<type>(<scope>): <summary>,**Type:**, Why, What, How tested, Review focus, and Split above the size warning. Withpr.requireIssueState: true, a PR that links an issue (**Issue:** #nor a closing word) also needs## Current state of the issueand## Blockers and decisions, each with text;desk pr templatewrites both; - an annotation on each failed line, and a summary in the run.
- the size gate (
- In the branch rules of the default branch, make
desk / pra required check. A PR then cannot merge until the gates and the format pass. pr-format: warnin the workflow reports the format and does not block, for a first trial.
A maintainer adds the size/override label to a PR that cannot be split; the check runs again on the label event.
Issues
GitHub cannot refuse an issue, so the kit makes the format the easy path and marks the rest:
.github/ISSUE_TEMPLATE/task.ymlis an issue form: Type, Priority and Size are drop-downs, and Why, Goal, Proposal and plan, and Done when are required fields.config.ymlturns off blank issues on the web page.- On each opened or edited issue, the workflow checks the body. An issue that does not follow the format gets the
needs-formatlabel and one comment that lists the problems. When the author fixes the issue, the label goes away and the comment says that the format is right. A correct issue gets the labeltype: <type>. desk issue createchecks the issue beforegh issue create, and the client hooks refusegh issue createwith a bad body.
Four settings of issue in config.json make the check stricter. Each one lists the issue types that it applies to, and all of them default to none:
| Key | An issue of these types needs |
|---|---|
requirePriority |
**Priority:** High|Medium|Low in the header (or the Priority field of the form), so that desk state can rank it |
requireEpic |
its goal: **Epic:** <name> in the header, a field Epic, or a line Part of #<n> |
requireResearch |
the section ## Research with a source (a link, an issue or PR number, [code: file :: symbol]), or the line No research needed: <reason> |
gatePrs |
(true or false) a PR that links an open issue with a fault fails its own check, and names the issue |
The owner's policy file takes the first three keys under issue, for every repository under its paths. desk init writes requirePriority for feature, bug and design issues and gatePrs: true into a new config. issue.stallDays is the days after which a report of work done that no session validated comes back among the work to do in desk state, and the close sweep validates the landing and closes the issue: 3 by default, and 0 turns the guard off.
desk issue audit [--repo <owner/repo>] [--json] [--fail] lists the open issues that do not follow the format, with the faults and the number of issues for each fault. desk state marks such an issue "not ready", with its rank kept, and an agent fixes the issue before it builds. The MCP tool is issue_audit.
The format follows the owner's backlog format: **Type:** (or the Type field), Why, Goal, Options, Proposal and plan, Done when, Links. issue.types and issue.sections in config.json change the types and the required sections.
Reviews
The kit writes the review body, so it is always in one format: the verdict, the counts, the checks, and each finding with severity, rule id, file and line, proof, and the verification result. post sends only that body and its inline comments, as a COMMENT review that never approves or blocks. Human review comments are not checked.
The owner's words
With review.requirements.enforce on, the PR check and the hook require the section ## Owner's request in a PR of a type in review.requirements.prTypes, and they check each quote against the owner's prompts on the machine. A CI runner has no prompts, so it checks the section and not the quotes. The review then checks the change against the owner's prompts, the linked issues and the specs. See requirements.md.
The merge
desk merge --pr <n> (MCP tool merge) merges a PR only when all of these hold, and it names each rule that fails:
| Rule | What it checks |
|---|---|
review |
A review run exists for the current head of the PR, and its reviewer is not the author: a headless engine, or a name in the findings that is not the PR author and not "author", "self", "session", "main", "main session", "pr author" or "me". A new commit makes the run stale. |
verdict |
The verdict of the run is "Ready to merge", with the decisions of the owner applied. |
findings |
No finding is open. Decide each one with desk decide <id> --accept or --reject --reason <text>. |
quotes |
The PR body has the section "Owner's request" when its type needs one, and each quote in it is found in the owner's words (the check of desk pr check). |
checks |
The checks of the PR passed or were skipped, and none is running. A check that ran more than once counts by its newest run, so a cancelled or failed older run does not block the merge; when two runs start together, the worse one counts. A PR with no checks fails, because its checks may not have started; merge.requireChecks: false allows it for a repository that has no CI. |
The run folder is searched in .desk/out/pr-<n>/ of this checkout and of the main checkout. --run <dir> names a run folder in another place. The merge uses --match-head-commit <head>, so GitHub refuses it when a commit arrived after the review. merge.method (merge, squash or rebase; default merge) sets the method.
A repository on the desk server has no pull requests and no desk merge. desk land puts a change on main, the checks with stage: "land" run before it, and the other checks run after it; a check that fails on main starts a bisect and an automatic revert. docs/runs.md describes the flow.
desk merge reads .desk/config.json at the base branch (origin/main by default) and not in the working tree, so a PR cannot turn off a rule of its own merge or add a command that runs after it. A base branch with no config gives the defaults: no command after the merge.
After a merge, the commands of merge.after in .desk/config.json run, one after the other, in the main checkout of the repository. For desk itself they update the checkout and run ./install.sh. A command that fails stops the rest and gives exit code 1; the PR stays merged. The command writes merge.json into the run folder with what ran. --dry-run checks the rules and merges nothing.
The workspace
A repository can ask that all work happens in linked git worktrees, so that a session never writes into the checkout that other sessions read from. Set workspace.worktrees in .desk/config.json:
| Value | Rule |
|---|---|
off |
No check. This is the default. |
auto |
The rule applies when the repository has a linked worktree already. |
required |
The rule applies to every checkout of the repository. |
The owner's policy file ~/.config/desk/policy.json takes the same key, {"workspace": {"worktrees": "auto"}}, for every repository under its paths. The stricter value of the config and the policy applies.
When the rule applies:
desk workspacefails on a detached HEAD, except while a rebase or a bisect is in progress: git detaches HEAD in both, and the commands that continue them must run. In the primary checkout it also fails when the checkout holds work: a branch other than the base branch, a change in a tracked file, or commits that the base does not have. Untracked files do not count.desk preflightadds the blocking checkCHK-WTwith the same text.- The hook
desk hook workspacerefuses the tools Write, Edit, MultiEdit and NotebookEdit of Claude Code for a file in a detached checkout (outside a rebase or a bisect) or in the primary checkout.desk installadds it tosettings.json. - The hook
desk hook ghalso refuses a shell command that changes a checkout with git (commit,add,merge,rebase,cherry-pick,am,apply,stash,revert,restore,rm,mv,reset --hard,checkout -b,switch -c,branch -f) when the command runs in such a checkout. It followscd <folder>,git -C <folder>, a subshell and the text ofsh -c "...", and it skips the options of git that take a value, such as-c. A leading~,$HOMEand${HOME}in a folder are written out.
If .desk/config.json does not load, the file hook lets the edit through, so that the file can be repaired; desk doctor and desk preflight report the error. In the primary checkout desk workspace prints "workspace ok" when the checkout holds no work, and the hooks still refuse an edit and a git commit there.
Each refusal names the command that makes the worktree: git worktree add -b <new-branch> <folder> origin/main. The hook reads the folder of the command. It does not read a folder that a variable or a command substitution builds.
A repository on the desk server
A repository with "store": "server" in .desk/config.json, and a work folder of the desk client (a folder with .desk-work), keep their code and issues on the desk server. GitHub holds only a copy. The rules:
- The hook
desk hook ghrefuses everygitandjjcall, andgh issue,gh pr,gh label,gh apiandgh project. Each refusal names the desk command to use. A git checkout of such a repository is a copy: the agent makes a work folder withdesk checkoutand works there. - The hook finds a program in the place of a command: at the start, or after
;,&,|,(, a backtick or a line break, after anyVAR=value.grep gitis not a git call. The hook reads the folder where the command starts, so a command that starts there and runs git in another folder is refused too: start it in that folder. - The CLI refuses the commands that read GitHub:
state,merge,threads,decisions,questions,learn,ci,requirements, andprexceptpr templateandpr check. desk review --pr <n>,desk report,desk verifyanddesk postrun on a proposal of the server.docs/server.mdgives the steps.- The issue commands run on the server:
desk issue view,list,create,edit,comment,questionsandcheck.desk feedback --issue <n>posts its comment on the server, anddesk feedback --pr <n>is refused, because a proposal on the server holds reviews and no comments.desk pr create,desk pr viewanddesk pr mergerun on the server: a proposal is the PR of the server (#61). - The skill
desk-workgives the agents the steps:desk checkout,desk status,desk change,desk landanddesk sync.
The AI review in CI
.github/workflows/desk-ai.yml runs the munhq/desk/review action on each ready PR, with the secret OPENROUTER_API_KEY and the variable DESK_MODEL (an OpenRouter model id).
- The action installs OpenCode (the
opencode-versioninput, default1.18.33) and runsdesk review --pr <n> --profile all --engine opencode --repo-access --model openrouter/<model>. Each profile and each part of the diff gets its own OpenCode reviewer, and a verifier checks each blocker and important finding. The reviewers can use the read, grep, glob and list tools in a read-only checkout of the PR head. The shell, edits, web fetch and MCP tools are off. - If the OpenCode run fails, the action runs
--engine openaiagainstbase-url(default OpenRouter) with the same model. That model gets the diff and the review packet as text only. engine: openaiskips OpenCode.
The judge. Before the review, one call to a decision model (judge-model, default typesafe/jev-1.13 on OpenRouter's Decisions API, about $0.00002 a call) reads the PR title, the body and the changed paths with their line counts. It gets no code. It rates the risk (none, low, medium, high) and gives each profile except full the probability that the PR needs it. The judge input sets what happens next:
judge |
What runs |
|---|---|
shadow (default) |
full and every profile whose paths match. The review shows the judge's choice under the title, and judge.json in the run folder records it, so its choices can be compared with the reviews. |
decide |
full and each profile with a probability of 0.5 or more. A PR with a probability of 0.85 or more for "no risk" gets no model review. |
off |
No judge call. full and every profile whose paths match. |
If the judge call fails, every matching profile runs. Use shadow until the recorded choices show that decide drops no profile that found a real problem.
The job writes the report to the job summary, uploads the run folder as the desk-ai-review artifact, and runs desk post --ai. That posts one GitHub review (event COMMENT, marked <!-- desk:ai-review -->):
- One numbered line for each finding, blockers first: the severity, a title of at most 150 characters, a link to the line, and a link to the full report.
- The full text of each finding, folded under "Details".
- An inline comment on each finding in the diff.
A finding that an earlier review of the kit holds is not posted again, and a push with no new finding posts nothing. OpenCode gets small_model set to the review model, so it calls no other model. Findings do not fail the job. A failed model call does. The job is not a required check.
The GitHub setting for the action
munhq/desk is private. For other repositories of the organization to use its action, its Actions settings must allow access from repositories in the organization (Settings → Actions → General → Access). An organization admin sets it once.