DocsThe decisions page
The decisions page
desk decisions serves a page of the open questions of your issues. You read the options with their pros, cons and effects, answer, and send. The answer goes to the issue as a comment. The issue is the only source: a wrong question is corrected in the issue.
Run it
desk decisions # this repository, on http://127.0.0.1:8790/
desk decisions --repo acme/app --repo acme/api --port 8790
desk decisions --lan # listen on the network, for your phone
desk decisions --list # print the waiting questions and check the facts; exit 1 if a fact is missing
--lan lets anyone on the network open the page and post an answer. Use it on a network you trust.
Where the page reads and writes
The page reads and writes each repository through its Forge (src/forge.ts). The command desk decisions (and the decisions tool of the agents, which runs desk decisions --json) asks the desk server of the policy (server in ~/.config/desk/policy.json) for its repositories with DESK_TOKEN. A repository that the server lists is read and answered on the server. Any other repository uses the GitHub forge: it runs gh with the login of the page (ghUser, or the active account). With a policy server, DESK_TOKEN must be set unless every repository of the page is in serverExclude; else the command stops, because the GitHub copy of a moved repository holds stale issues. A code that builds the page with forgeFor gives it another forge, for example the server forge (src/server/forge.ts), so the same page reads the issues, the questions and the comments of a repository on the desk server and posts an answer there. The desk server serves that page for its repositories (#71).
On the server, a repository has one branch, main, and no pull requests yet. So the tab "To close" lists no issue there, and the facts of a question are checked against the files of main.
Settings
~/.config/desk/decisions.json (all fields optional):
| Field | Meaning |
|---|---|
repos |
owner/repo list. Without it, --repo or the repository of the current folder. |
ghUser |
The gh account that sees the repositories. |
labelOnDecided |
A label that an answered issue loses, for example needs design. |
onDecided |
A shell command that runs after an answer. {repo} and {issue} are replaced. |
board |
{ "owner": "acme", "project": 1 }: the GitHub Project that shows the status of each issue. |
allowedHosts |
Host names, besides the addresses of this machine, that the page answers to, for example the name of a proxy. |
The section in the issue
An issue gets the section ## Questions for the owner. desk issue check reads the section when it has ### blocks, and names each fault.
## Questions for the owner
Lane: now
Stake: One line on why you decide, and what is blocked.
After: What happens after the decision.
Facts:
- The classifier sends one string. [code: src/classifier.rs :: MAX_STATE_CHARS]
- A vendor documents it. [web: https://example.com/docs]
### What do apps call?
Recommend: A. The reason, in one sentence. Write `Recommend: none.` when you have no basis.
**A. Saved decisions** — what the option means.
- For: the gain.
- Against: the cost.
- Changes:
- new api: a route /v1/decisions
- change docs: the API reference
- remove api: the old route
**B. Classify only**
- For: one route.
- Against: no outcome route.
Diagrams and the business effect
A diagram shows what each option changes on a system map. Write the map once with Flow: (before the first question, or inside a question), then list the steps of each option.
Flow:
- app: App of the customer [customer]
- gw: Gateway [platform]
- jev: Jev [vendor]
- app -> gw: request
- gw -> jev: question
### Which way?
Recommend: A. Reason.
**A. Add an engine**
- For: gain.
- Against: cost.
- Business:
- What it means for customers, cost, risk or time.
- Diagram:
- add eng: Engine [platform]
- add gw -> eng: ask
- remove gw -> jev
- change gw
A node is
id: Label [lane]. The lane iscustomer,platform,vendororowner, and sets the colour.An edge is
from -> to: label.adddraws a new node or edge in green.changemarks a node in blue.removedraws a node or edge in red and dashed.change gw: the model listkeeps the note after the colon, and the side panel lists it under the diagram of the option.The steps can also stand on the
Diagram:line, apart by;:- Diagram: change gw: the model list; remove gw -> jev. A step that is notadd,changeorremoveof a node or an edge is a fault, and the check names it.The page draws the map as it is (the "today" figure), then once for each option with its steps. A question with its own
Flow:uses that map. A question withDiagram:steps and noFlow:is a fault.Business:lines show in a box under the diagram. Use them for the effect on customers, money, risk and time.Lane is
now(blocks work) orlater.Facts with
[code: file :: text]are checked on the default branch each minute. A missing text shows in red, because the issue is wrong.[web: url]shows a link. A fact with no tag shows as not checked.Each option has
For,Against, and optionalChanges. The page shows the changes as chips (new, change, remove) grouped by area, so you see what each choice does before you pick.A section with plain bullets and no
###block is not a card. The page lists it as unreadable.
Work of an option, short routes and your own options
An option can hold the block - Work:. It says what the option needs, in words that the issue can prove.
**A. Add a route**
- For: gain.
- Against: cost.
- Work:
- files: src/server.rs, src/decisions.rs (new)
- infra: one migration
- downtime: none
- tests: the round trip
- locks: the route /v1/decisions
- reversible: yes, the route can go with its migration
- debt: no outcome route yet
- revisit: 2026-12-01
fileslists paths, and only paths. Each token with the shape of a path counts, so a word such astheis a file too.- desk checks the paths on the default branch of the repository. It checks at most 300 paths of one issue.
- It makes at most 8 reads of paths from GitHub at one time, for all issues together.
- The page lists 200 names of one line, then the count of the other names, which desk does not check.
- desk reads
./, a line anchor (#L10,:12) and a glued(new). A backslash is a slash, and a URL is no path. - The page shows "in the repository", "not in the repository", or "not checked" when GitHub gives no answer.
- A path followed by
(new)is a file that does not exist yet, and the page does not check it.
infra,downtime,tests,locksandreversibleare free text. A value that nobody has written stays out of the issue. The page then shows "the issue does not state it".- A work line states work, never a time:
desk issue checkrefuses "2 weeks", "3 days", "a few days" or "10 min" in any work line.downtimenames what stops, in steps. debtandrevisitrecord a short route: what it leaves to do, and the fact that makes you decide again. An option needs both or neither. The fact is something code can check: a count, a metric value or a date. The plan above the questions shows them in amber when you pick the option.- When you choose a short route, the decision comment holds the record
### Short route of the ownerwith the question, the option, the debt and the fact. From the date in the fact on,desk statelists the issue as "short route due" and names the debt (docs/state.md). - The button "Enter the work" under each option writes this block into the issue. An empty field is left out. The rest of the issue stays byte for byte the same.
- The button "Add your own option" under each question adds an option with the next letter. It holds the For and Against lines that you write.
- Both forms change the text of the questions, so the options review runs again. Two edits of one issue on this page run one after the other. The second edit reads the body that the first one wrote. It stops with "changed after the page loaded" when the questions changed. A decided card refuses both forms, because its questions must stay as they were decided.
Tasks, cost and follow-up questions of an option (#129)
A pick on a card changes the plan before the owner sends anything. An option holds three more lines:
**A. Kernel bypass with a poll-mode driver**
- For: ...
- Against: ...
- Tasks:
- drv: Install the driver of the network card [infra: a kernel driver; downtime: the host restarts; tests: 1]
- rx: Rewrite the receive loop [after: drv; files: src/net/rx.rs; replaces: #12; tests: 2]
- Cost: +400 EUR a month [source: the price page of the host, 2026-10-26]
- Follow-up: 2
- A task line is
<id>: <title> [attributes]. The attributes, separated by;, areafter(task ids of the option, or issues#n),replaces(issues#n),files,infra,downtimeandtests. A task states work and what stops, never a time. Costis a number with its unit, ornone. It needs[source: ...]. The plan sums the costs of one unit.Follow-upnames questions of the section, numbered from 1. A question that an option names is asked only after the pick of that option.desk issue checkrefuses a task that comes after an unknown task, a time in a task, a cost with no source, and a follow-up that names its own question or a question that does not exist.
The server computes the plan (src/plan.ts): the tasks in an order where each follows the tasks it comes after, the issues that they replace, the questions that the picks open, and the totals of tasks, files, infra changes, stops, tests and cost. POST /api/plan (repo, issue, hash, picks) returns it and records nothing. The product shows it in the panel "What your choices change", and each option shows its own tasks and cost.
An answer needs a pick for each visible question only. The decision comment lists the answers of the visible questions (<!-- desk:picks {...} --> holds the picks) and the section "Tasks of the decision" with the tasks and the cost.
The roadmap. The call plan also returns roadmap: the goal that holds the card, with its steps and their status, and the tasks of the picks drawn into it (roadmapOf in src/plan.ts). A task with no task before it comes after the decision. The steps that waited for the decision wait for the last tasks (moved). An issue that a task replaces leaves the order, and the steps after it wait for the last tasks of its option. A card that no goal holds shows the decision and the issues that it holds up. The Decisions tab draws it top to bottom (ui/src/components/RoadmapSvg.tsx, ui/src/lib/roadmap.ts): green dashed boxes are the added tasks, a crossed box is a replaced issue, a violet dashed line is a step that now waits for the new tasks.
The issues of the tasks. A send creates one issue for each task, in the order of the plan, in the issue format. A task that comes after another names it (Needs #n.). An issue that a task replaces gets a comment that names the new issue. The card gets the comment "Issues of the decision" with a marker for each task (<!-- desk:task <hash> <plan id> <n> -->). If a create fails, the decision stands and the page offers "Create the missing issues" (POST /api/tasks); a second call creates only the tasks that have no marker.
An older option has no Tasks: lines. When it changes the design, names work or lists changes, desk treats the option itself as one task ("Do:
The pane of a card in the Decisions tab opens wide: the questions are on the left. On the right, and in view while the owner chooses: the design now next to the design after the picked options (each map is scaled to the height of the pane, with the new, changed and removed parts marked), the roadmap of the goal, the tasks and the cost, and for each picked option its design changes, its work lines and its effect on the business. A line above the questions says in words what the picks add. A long stake shows in two lines with "Show more".
desk questions review and desk requirements --issue <n> run on a repository on the desk server: the issue and the code come from the server. desk requirements --pr reads a PR of GitHub and stays refused there.
The trace of an issue
GET /api/trace?repo=<owner/repo>&issue=<n> returns what the issue holds for an agent or a person who reads it later: the issue (title, state, priority), the questions as they are now, the log of its comments that matter (the decisions with their answers, picks and note, the parked questions, the options reviews, the feedback of the owner and the issues that a decision made), whether the latest decision answers the questions as they are now, the plan that its picks made (with the roadmap), and the task issues with their state. The product shows it as the second tab of an issue (/r/<owner>/<repo>/issues/<n>/trace), with the changes that landed for the issue and for its tasks and the runs of each (docs/ui.md).
One place for the decisions
The Decisions tab of the product (/workspace?tab=decisions) is the place for the decisions. /decisions of the product redirects to it, and the header link opens it. The local page of desk decisions redirects / to the tab when the policy has a server; its /api/ calls keep answering. The page that the server serves at /v1/decisions/page has no link in the product.
The plan and the window
Above the questions of a waiting card, the plan shows the issues that wait for the card. An issue waits for the card when its text says blocked by, waits for, wait for, needs, depends on, requires or after followed by #<n>. The steps that the picked options add appear in the plan as new boxes.
The button "Open in a window" shows the card in a window, with the same options, maps and plan. The priority from the header (**Priority:** High, or the form field "Priority") shows on each card.
The tab "Overview"
The tab shows the goals of the chosen repositories. A goal is an open issue with **Type:** epic. Its steps are the issues that its body and its comments link, in their order. The open issues that no goal links form one more group, so every open issue is on the page.
Each step has one status:
- Blocked: your decision (red): a question of the issue waits for the owner. The button "Decide" opens its card.
- Waits for an issue: the issue names another open step of the goal with "needs #n", "after #n" or "waits for #n".
- In a PR: an open pull request names the issue.
- Ready for an agent: nothing blocks it.
- Done: the issue is closed.
A goal shows its priority, how many steps are done, and a bar of its progress. "Only goals with a blocker" and "Hide the done steps" narrow the view.
The tab "To close"
The tab lists the open issues that a merged pull request closes with a closing keyword (Closes #n). GitHub closes such an issue by itself when the pull request merges into the default branch. The tab lists the issues that stayed open. There are two cases. The pull request merged into another branch. The issue stayed open after a closing merge into the default branch. A mention such as "part of #49" does not list an issue. An issue that an open pull request also names is not listed, because its work is not done. A pull request on another branch is listed with that branch. The tab shows how many items of the section "Done when" are open and done. The button "Close this issue" asks first. It reads "Close anyway" when items of "Done when" are open. The server then closes the issue. desk issue close-check --issue <n> prints the same verdict and closes nothing.
The posts that edit or close an issue need a JSON body, the same origin, and a host name that the page serves. The API answers a request whose host is localhost, a loopback address, an address of this machine on the network, or the bind address. A machine name such as mybox.local is refused with the status 400, so a page of another site cannot reach the API through DNS. To serve a name, for example the name of a proxy, list it in allowedHosts in decisions.json: { "allowedHosts": ["decisions.example.com"] }. The page then answers to that name, with the port of the page or with no port (a proxy on port 443).
A repository on the desk server has no pull request. The change that lands names the issue with its intent, and desk issue close-ready --issues <n,...> closes the issue only when every line of "Done when" is ticked and every check, image and deploy of that landing passed. --all reads every open issue, --dry-run closes nothing, and a line that is not ticked is printed with the rest of "Done when". A fix that landed under another issue number leaves the issue with no change that names it, and --change <id> or --landing <seq> names the landing that the triage found in the log.
The server also sweeps by itself (src/server/close-sweep.ts), every 5 minutes and in one replica at a time through a lease: every open issue whose "Done when" is fully ticked, whose landing is not a revert and has no check, image or deploy that did not pass, and whose questions for the owner have a decision, gets the same record and closes. It never ticks a line, so an issue whose live check is not validated stays open. DESK_CLOSE_SWEEP=off turns the sweep off.
Two sessions can also hand an issue from one to the other. desk issue done --issue <n> reports the work with the landing that carries it, renews the claim of the session, and posts ## Work done (date) with the marker <!-- desk:done <change> <identity> <holder> -->; the issue stays open and desk state lists it as "reported done, waits for validation". desk issue validate --issue <n> in another session runs the same gate as close-ready against the landing of the report, posts ## Validated by <identity> (<holder>) (date) and closes the issue. The session that reported the work done cannot validate it, because it knows what it skipped; --self takes that on itself. --all reports what waits, and the close sweep leaves an issue with a report to the validator. A report that no session validated for issue.stallDays does not park the issue: desk state puts it back among the work to do, and the close sweep validates the landing and closes the issue, naming the report that waited. The key is in .desk/config.json and defaults to 3 days; 0 turns the guard off.
Feedback
An answer picks an option. Feedback is everything else the owner says about the work: a correction of a status report, what the owner expects, a change of direction. Each card has the button "Give feedback". The text is posted on the issue as the comment ## Feedback of the owner (date), in the owner's own words.
- Each open PR on the page has the button "Give my review". It posts the same comment on the PR, in the owner's words, and the page shows it under the PR.
desk decisions --jsonlists the feedback of each card underfeedback, and the open PRs with their feedback underprs. An agent reads both at the start of work.desk requirementsand the review packet show a comment marked "Feedback of the owner" as the owner's words. It wins over the body of the issue and over an older comment.- An agent that has the owner's feedback from a chat records it with
desk feedback --issue <n> --text "<words>", or--pr <n>for a PR (MCP toolfeedback). The command refuses words that no prompt of the owner holds, so an agent cannot write feedback in the name of the owner.
Swipe mode
The button "Swipe" (top right) shows one question at a time. The options stand side by side, each with its For and Against lines, its diagram and its business box. "How it works today" is folded until you tap it.
- Two options: swipe, or press the arrow key, to the option you choose (left is the first, right is the second). With three or more options, tap one or press its number.
Enteragrees with the recommendation.Sskips.Backspacegoes back. "Not now" parks the whole issue.- A choice moves to the next question. The last screen lists your answers. Tap a row to change it, then send. An issue is sent when every question of it has an answer. The rest stay waiting.
- The progress bar counts the questions. Blocking issues come first, then the issues with the most questions.
Several people
Type your name in the box at the top. It is saved in the browser and written into the decision comment as By: <name>, next to the GitHub login that posted it. The Decided tab shows who decided and when. Every answer stays on the issue, so a later answer does not hide an earlier one.
desk decisions --lan has no sign-in: anyone on the network can answer. Run it on a network you trust.
For agents and new sessions
The issue holds the state, so a new session loses nothing.
- MCP tool
decisions(CLI:desk decisions --json [--issue <n>]): every card with its status, the questions, and the decision comments. Call it at the start of work and before you ask the owner anything. A decided card tells you what to build. - MCP tool
questions_put(CLI:desk issue questions --issue <n> --body-file <file>): put the questions section into an issue after a format check. Do not ask the owner in chat. - Do the work that a decision does not block. Do not stop to ask how to proceed.
Questions at design time
A question that stops the work should be found when the issue is designed, not in the middle of the build. Two settings make the issue say so.
issue.requireQuestionsin.desk/config.jsonlists the issue types that must hold the section "Questions for the owner". The section lists the open questions (### blocks), or it says "No open questions." The default is none.desk issue check,desk issue create, the hook ongh issue createandgh issue edit, and the CI check all apply it.~/.config/desk/policy.jsonsets it for every repository under some folders, with no.desk/folder needed:
{ "paths": ["~/code/personal"], "issue": { "requireQuestions": ["design"] } }
The policy applies only to a repository under one of its paths, so a work repository is not touched. In a repository with no desk setup, the hook checks only this rule. It does not check the PR format or the rest of the issue.
If an agent is blocked later, the skill owner-questions says what to do: write the question in the issue, run the options review, and go on with other work.
The review gate
Another agent can check each question before you see it. In ~/.config/desk/decisions.json:
{ "review": { "required": true, "auto": true, "prs": true, "engine": "codex", "budgetUsd": 2, "maxPerDay": 20,
"workdir": "/path/for/review/worktrees", "env": { "GIT_SSH_COMMAND": "ssh -i ~/.ssh/key" },
"checkouts": { "owner/name": "/path/of/the/checkout" } } }
required: a card that has no review of its current questions waits in the list "Waiting for the options review". The swipe flow skips it. "Answer anyway" opens it for you.auto: the page starts the review itself, one at a time, withdesk questions reviewin the checkout of the repository (default~/code/personal/<name>). A failed review shows on the card and is tried again after 30 minutes.maxPerDaylimits how many start in 24 hours.prs: withauto, the page also reviews each open pull request, one at a time, withdesk review --pr N --profile all --engine <engine> --repo-access, when the head of the PR has no review yet. The review runs in a worktree of the default branch (workdir, one folder for each repository), so your own checkout is not touched. The In flight tab shows the verdict and the counts, and "Show the report" opens the report. The page does not post anything to the PR: you decide which findings go onto it. The result is kept in~/.local/state/desk/pr-reviews.json, so a restart does not repeat a review.envsets environment variables for the review commands, for exampleGIT_SSH_COMMANDwhen git needs a specific key.engineshould be an engine other than the one that wrote the questions, so the check is independent. The engines are claude, codex, opencode and openai.- Each review costs up to
budgetUsddollars. With many cards, the first pass costs the budget for each card.
What an answer does
- It posts the comment
## Decision of the owner (<date>), with the choices and your note. The comment holds a hash of the questions. - It removes the label of
labelOnDecidedand runsonDecided. - If the questions change later, the hash differs and the issue shows as waiting again. A page that loaded the old questions refuses to send, and asks you to reload.
A decision written by hand, before the comment held the hash, is still the owner's decision. The card reads the last ## Decision of the owner as decided. The page cannot tie it to the current questions, so the trace shows the decision and no plan under it. A change of the questions goes in a new issue.
"Not now" posts ## Parked by the owner. "Ask on the issue" posts ## Question of the owner, which the team and the agents read on the issue.