Decisions
What it does
Section titled “What it does”When a PR or ticket needs you to choose something, it asks in one Decisions section with a fixed Markdown format: a <!-- marvin:decisions --> marker, one ### id: question per question, and - [ ] options under each. The dashboard turns that into option buttons, a note box per question and text boxes for written answers. Submit decisions ticks your choices in the description itself and posts a Decisions from the owner comment with the answers as JSON. Approve stays disabled until every required question is answered, and the merge server refuses the PR if anything tries to merge it first. A question asked in prose instead (“A, B or C?”, an “Open questions” heading) is called out as Too vague: needs options, refused by the merge server, and stopped earlier by the gh wrapper before it reaches GitHub. Once you’ve answered, the next move is MARVIN’s, and a scan files a follow-up ticket if nothing happens.
For a plain yes or no on the whole PR, use Approve or Deny. Decisions are for choices inside the work.
Why it exists
Section titled “Why it exists”A question buried in a PR’s prose is a question the owner never sees. Agents were told the format in their prompts and still wrote prose lists, so the dashboard showed nothing to answer and the owner was asked nothing (ADR 0080). The reverse gap hurt too: on PR #577 the owner submitted answers, the record comment asked an open question, and nothing reacted, because the pipeline only dispatches tickets (lib/owner_decisions.py). A rule in a prompt is advice. This one is held in code at the three places every path passes: the gh wrapper, the dashboard, and the merge server.
See it work
Section titled “See it work”A real answered block, from PR #839 (the owner’s choices on the test pipeline plan), read 2026-11-02, trimmed to two of four questions:
$ gh pr view 839 -R G-Eskayo/marvin --json body -q .body | awk '/marvin:decisions/{f=1} f'<!-- marvin:decisions -->### protect-main: Protect main on both repos so a website merge also needs the required checks?- [x] Yes, required checks on main, and admins (you) included (Recommended)- [ ] Yes, but let you override it as admin- [ ] No
### main-red: When main is already red, what happens to a PR that adds no new failures?- [ ] Merge it, and attach it to the open main-red ticket (Recommended)- [x] Keep refusing until main is fixed (today's behaviour)The gh wrapper’s rule run on a prose ask, 2026-11-02 (the body is a two-line sample; the message is the real one):
$ printf '## Open questions\n- Should the cache live in /tmp or ~/.cache?\n' > body.md$ venv/bin/python -c 'import sys; sys.path.insert(0,"lib"); import decisions_check as d; print(d.refusal(open("body.md").read()))'DECISIONS_NOT_ASKABLE: "## Open questions" asks the owner to choose, but there is no <!-- marvin:decisions -->block, so the dashboard shows no questions and he is asked nothing. Put the marker under the heading, each questionas "### some-id: question" and its options as "- [ ] option" lines (docs/agents/decisions-format.md). A heading thatonly reports ("## Decisions recorded") isn't an ask.And a real merge-server refusal, from the MacBook’s webhook error log:
$ grep DECISIONS ~/.claude/logs/dashboard-webhook-error.log2026-10-27T00:55:48.194Z approve failed: https://github.com/G-Eskayo/marvin/pull/688 DECISIONS_UNSTRUCTURED at request: this PR asks you to choose something but gives no options to answer ("## Decisions" asks for a choice)How it works
Section titled “How it works” agent writes PR/ticket ──► bin/gh (rule I2) ──refuse──► DECISIONS_NOT_ASKABLE / DECISIONS_UNPARSED │ ok ▼ dashboard parses the block ──► option buttons ──► Submit: ticks boxes in the body + record comment │ │ ▼ ▼ Approve disabled until answered hourly scan: answered, not acted on for 4 h? │ └─► follow-up ticket "Act on owner decisions on PR #N" ▼ merge server: DECISIONS_PENDING / DECISIONS_UNSTRUCTURED, else on to the merge gate- One parser.
dashboard/webhook-server/decisions.jsreads the block: ids, choice or(pick any)or(text),(optional), ticked boxes,> **Answer:**and> **Note:**lines. Everything else calls it, including Python throughdecisions_cli.js, so there’s no second copy to drift. - At write time.
bin/ghrunslib/decisions_check.pyon everypr create|edit|commentandissue create|edit|commentthat sets a body. A heading that asks with no block isDECISIONS_NOT_ASKABLE; a block that doesn’t parse isDECISIONS_UNPARSED. It fails open with a note if node isn’t there, because the merge server still holds the line. The pre-commit rule check also flags committed Markdown whose block doesn’t parse. - In the dashboard. MR Review shows Answer the decisions first while required questions are open, and Too vague: needs options for a prose ask, with one click that sends the PR back for options through the Deny feedback path (ADR 0025).
- On submit. The description is the source of truth: choices are ticked in place, then a Decisions from the owner comment is posted with a
marvin-decisionsJSON block. On a ticket labelledneeds-info, answering every required question moves it toready-for-agent. - At merge.
decisions_gate.jsreads the PR body. Required questions open:DECISIONS_PENDING. A prose ask:DECISIONS_UNSTRUCTURED. GitHub unreachable:DECISIONS_UNCHECKED. It fails closed, since a refused Approve costs one click. - After you answer.
lib/owner_decisions.pyruns inside the hourly ticket scan. A PR whose latest owner answers have no reply carrying<!-- marvin:decisions-addressed -->and no completed follow-up gets one follow-up ticket after the grace period, labelledready-for-agentandowner-decisions. It closes when the PR is addressed or closed. A commit alone doesn’t count as an answer.
Use it
Section titled “Use it”Write questions in this shape in a PR or issue body:
## Decisions<!-- marvin:decisions -->### storage: Where should the cache live?- [ ] /tmp- [ ] ~/.cache
### extras: Also cache thumbnails? (pick any) (optional)- [ ] Small- [ ] Large
### notes: Anything else? (text) (optional)No flag means one answer, required. Each choice question needs at least two options. Don’t tick boxes for the owner. The full rules are in docs/agents/decisions-format.md.
Common tasks:
# PRs whose decisions are answered and still waiting on MARVINvenv/bin/python lib/owner_decisions.py waiting# what the hourly scan would file or close, without doing itvenv/bin/python lib/owner_decisions.py scan --dry-runAfter acting on an answer, reply on the PR with <!-- marvin:decisions-addressed --> in the comment.
Configure
Section titled “Configure”| Setting | Where | What it does |
|---|---|---|
MARVIN_DECISIONS_OK="<reason>" |
environment, one gh call |
Lets one body through rule I2 when a person agrees it isn’t a question to the owner; logged to ~/.claude/logs/gh-calls.jsonl |
MARVIN_DECISIONS_GRACE_HOURS |
environment | Hours before an unaddressed answer becomes a follow-up ticket (default 4, accepted range above 0 up to 168) |
MARVIN_NODE |
environment | Path to node when it isn’t on PATH (the rule needs it to run the parser) |
| Format | docs/agents/decisions-format.md |
The block’s syntax and flags |
Requirements and limitations
Section titled “Requirements and limitations”- The
ghrule needs node to run the parser. Without it the rule lets the call through with a note. - The rule only sees bodies passed as
--bodyor--body-file. A body on stdin goes straight togh. - The dashboard merges through its own server, not
bin/gh, so the merge server has its own check (decisions_gate.js) built on the same parser. - The write-time rule can be overridden, and overrides do happen. On 2026-11-02 the
ghcall log on the MacBook held 5, all from one session adding a ticket link to PR bodies whose Decisions were unchanged (grep decisions_override ~/.claude/logs/gh-calls.jsonl; the log rotates at 5 MB, so older ones are gone). - The follow-up scan runs on the machine doing the hourly ticket scan. On a second Mac it only files when the primary is quiet.
Design and source
Section titled “Design and source”- ADR 0080: The work loop is one system: rule I2 (every question is machine-readable) and I3 (every answer is acted on and replied to). Rejected: keeping each rule on the surface where the problem was seen, which left copies that drifted.
- ADR 0025: Deny action, two buttons, structured feedback: the send-back path Too vague uses. Rejected: a third “adjust” button.
- ADR 0037: Answering a waiting ticket from the dashboard: the reply box and the
needs-infotoready-for-agentmove. - Source:
decisions.js,decisions_gate.js,decisions_check.py,bin/gh,owner_decisions.py,decisions_submit.js,Decisions.jsx - PR #608: Owner decisions on a PR no longer go unanswered: flag them, file a follow-up after N hours
- PR #744: bin/gh refuses Decisions the dashboard can’t read (rule I2)
- PR #760: Dashboard: back/forward history for two-finger swipes, Decisions before wiring
- PR #839: Test pipeline plan: your four decisions, the example above
Related features
Section titled “Related features”- Merge gate: the decisions check is one of its first refusals.
- Dashboard: MR Review and the ticket page, where you answer.
- Ticket pipeline: builds the follow-up tickets.
- GitHub gate (
bin/gh), failure breaker.