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

  1. desk init writes .github/workflows/desk.yml. On each PR event it runs the munhq/desk action, which runs desk 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, unless size.newFilesFail or size.testsFail sets a limit for them. The size/override label turns the failure into a warning;
    • the mixed-scope gate (CHK-C4) and every check in config.json, on the added lines;
    • the PR format: <type>(<scope>): <summary>, **Type:**, Why, What, How tested, Review focus, and Split above the size warning. With pr.requireIssueState: true, a PR that links an issue (**Issue:** #n or a closing word) also needs ## Current state of the issue and ## Blockers and decisions, each with text; desk pr template writes both;
    • an annotation on each failed line, and a summary in the run.
  2. In the branch rules of the default branch, make desk / pr a required check. A PR then cannot merge until the gates and the format pass.
  3. pr-format: warn in 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:

  1. .github/ISSUE_TEMPLATE/task.yml is an issue form: Type, Priority and Size are drop-downs, and Why, Goal, Proposal and plan, and Done when are required fields. config.yml turns off blank issues on the web page.
  2. On each opened or edited issue, the workflow checks the body. An issue that does not follow the format gets the needs-format label 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 label type: <type>.
  3. desk issue create checks the issue before gh issue create, and the client hooks refuse gh issue create with 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:

  1. desk workspace fails 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.
  2. desk preflight adds the blocking check CHK-WT with the same text.
  3. The hook desk hook workspace refuses 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 install adds it to settings.json.
  4. The hook desk hook gh also 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 follows cd <folder>, git -C <folder>, a subshell and the text of sh -c "...", and it skips the options of git that take a value, such as -c. A leading ~, $HOME and ${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:

  1. The hook desk hook gh refuses every git and jj call, and gh issue, gh pr, gh label, gh api and gh 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 with desk checkout and works there.
  2. The hook finds a program in the place of a command: at the start, or after ;, &, |, (, a backtick or a line break, after any VAR=value. grep git is 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.
  3. The CLI refuses the commands that read GitHub: state, merge, threads, decisions, questions, learn, ci, requirements, and pr except pr template and pr check.
  4. desk review --pr <n>, desk report, desk verify and desk post run on a proposal of the server. docs/server.md gives the steps.
  5. The issue commands run on the server: desk issue view, list, create, edit, comment, questions and check. desk feedback --issue <n> posts its comment on the server, and desk feedback --pr <n> is refused, because a proposal on the server holds reviews and no comments. desk pr create, desk pr view and desk pr merge run on the server: a proposal is the PR of the server (#61).
  6. The skill desk-work gives the agents the steps: desk checkout, desk status, desk change, desk land and desk 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).

  1. The action installs OpenCode (the opencode-version input, default 1.18.33) and runs desk 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.
  2. If the OpenCode run fails, the action runs --engine openai against base-url (default OpenRouter) with the same model. That model gets the diff and the review packet as text only.
  3. engine: openai skips 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 -->):

  1. 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.
  2. The full text of each finding, folded under "Details".
  3. 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.