MKD (mkd) — Make Decision
MKD is a local decision-deck CLI the AI invokes mid-conversation. It renders a browser page where the user works through a deck of items one screen at a time (Slack Catch-Up style): decisions with justified options, questions with controls, report sections to react to, and answerable tables. The default flow is non-blocking copy-paste: the CLI opens the page and exits; the user answers whenever they want and pastes the Result JSON into the chat. That JSON is the execution contract — the AI executes exactly what it says.
This skill needs no install step: there is no compiled binary and nothing added to PATH. The AI runs the CLI from the skill directory with Bun. All output (rendered decks, results, images) lands under ~/.mkd/ — never the cwd — so running it inside any repo leaves zero footprint there.
Running it (no install)
bun "<skill-dir>/cli/index.ts" <specPath> [--wait] [--no-open] [--port <n>] [--timeout <min>]
<skill-dir> is this skill's install directory:
- project-level:
<repo>/.claude/skills/mkd
- user-level:
~/.claude/skills/mkd
Bun runs TypeScript directly; cold start is negligible. If Bun is missing, tell the user to install it (curl -fsSL https://bun.sh/install | bash) — do not substitute Node.
When to use vs AskUserQuestion vs inline prose
| Situation |
Use |
| 1-2 option pick, single yes/no, ≤3 simple decisions |
AskUserQuestion |
| >3 decision points, or decisions needing context + written tradeoffs |
MKD |
| A long multi-section report/plan the user should react to point by point |
MKD (report items) |
| Row-by-row verdicts over tabular data |
MKD (table item) |
| The reply must be ANCHORED to exact phrases |
MKD (highlight-to-quote) |
| Non-interactive / CI / no human at a browser |
neither — emit plain output |
The deck model
A spec is a list of items; each item is one screen. Four types:
| Type |
What the screen holds |
Answer captured |
decision |
Problem statement (plain PM language) + collapsible context balloons + options with written justification + optional custom option + note |
chosen key (or "CUSTOM" + customText), wasRecommended, note |
question |
Markdown content + single/multi/toggle control + free text |
controlAnswer, text, quotes |
report |
A report section to react to |
text, quotes |
table |
Answerable table, one answer per row (popover controls + text) |
rows[] each with controlAnswer/text/quotes, plus item note |
Deck chrome, always on: intro screen (headline + markdown + stat tiles), navigable progress rail, ← → arrow keys, per-item Skip for now (skipped = decide later, NOT a rejection), summary screen, live footer stats (answered / rec followed / changed / custom / skipped), light/dark theme, localStorage persistence (closing the tab loses nothing).
HARD RULE — justified options (this is the point of the tool)
When authoring decision items:
- Every option carries a written
justification: what it buys (value) and what it costs. The validator rejects any option without one (exit 2) — do not fight it, write the justification.
- At most one option is
recommended, and its justification must state why it is the recommendation explicitly (e.g. "Recommended because …").
- The
problem is written in plain language (PM voice) for someone without the full technical context; jargon and internal codenames go inside context balloons that explain how things work today.
- Every real tradeoff the user should know about goes in writing. An unexplained option list is exactly the failure mode this tool exists to eliminate.
Spec schema in brief
{
"session": "audit-skills-alignment", // required — result echo + persistence key
"source": ".session/.../audit.md", // optional pointer to the source artifact
"title": "Catch-Up: audit decisions", // required
"intro": { // optional intro screen
"headline": "8 decisions await you",
"body": "Markdown, PM voice",
"stats": [{ "n": "115", "label": "verified findings" }]
},
"items": [
{
"id": "D1", "type": "decision", "title": "…",
"severity": "high", // optional: high | medium | low (chip)
"scope": "test-documentation", // optional (chip)
"problem": "Plain-language markdown problem statement",
"context": [{ "title": "How does X work today?", "body": "markdown" }],
"options": [
{ "key": "A", "label": "…", "justification": "Value + cost. **Recommended** because …", "recommended": true },
{ "key": "B", "label": "…", "justification": "Value + cost." }
],
"allowCustom": true // default true
},
{ "id": "Q1", "type": "question", "title": "…", "content": "markdown",
"controls": { "type": "single", "required": true, "options": [{ "value": "keep", "label": "Keep" }] },
"text": { "placeholder": "Why?" } },
{ "id": "R1", "type": "report", "title": "…", "content": "markdown section" },
{ "id": "T1", "type": "table", "title": "…", "content": "optional intro",
"table": { "columns": ["Test", "Rate"], "rows": [{ "id": "r1", "cells": ["a", "b"] }],
"rowControls": { "type": "single", "options": [ … ] } } }
]
}
Full contract (every field, defaults, validation rules) → references/schema.md. Worked copy-pasteable specs → references/examples.md.
Exact invocation — default (copy mode, non-blocking)
- Write the spec to
~/.mkd/spec-<name>.json (that filename makes the page land at ~/.mkd/deck-<name>.html). Keeping it under ~/.mkd/ keeps the repo clean.
- Run:
bun "<skill-dir>/cli/index.ts" ~/.mkd/spec-<name>.json. The CLI validates, renders a self-contained page, opens the browser, and exits 0 immediately. Nothing lands on stdout.
- Tell the user the deck is open in their browser and that pressing Copy JSON (footer) and pasting it into the chat brings the answers back. Then continue with other work or end the turn — do NOT block or poll.
- When the pasted JSON arrives, parse it and treat it as the execution contract:
status: "skipped" items are "decide later" (re-ask later, never assume a rejection); chosen: "CUSTOM" means execute customText as stated (or ask if something does not add up); always read each item's note.
--wait (blocking, same-turn answer)
Use only when the AI genuinely needs the answer in the same turn to continue:
bun "<skill-dir>/cli/index.ts" <specPath> --wait serves the deck over loopback (--port, default 4747, auto-increments) with a per-run x-mkd-token submit gate, waits for the browser's submit (--timeout <min>, default 1440 = 24h), then prints the Result JSON to stdout (the ONLY thing on stdout; banners/errors go to stderr) and writes a backup to ~/.mkd/result-<name>.json.
- Image paste (clipboard → attachment) works only in
--wait mode: entries in images arrive as absolute file paths under ~/.mkd/ the AI can Read. Copy mode disables paste (no server to persist bytes).
Exit codes and absence protocol (--wait):
| Exit |
Meaning |
What the AI does |
| 0 |
submitted — Result JSON on stdout |
parse it, continue the same turn |
| 1 |
timeout with NO submission (or runtime error) |
user is AWAY, not an error — post a standby note, offer to relaunch |
| 2 |
bad spec (unreadable / failed validation) |
fix the spec at the reported path, re-run |
| 130 |
Ctrl-C |
the user cancelled — ask what they want next |
Reading the Result
{
"session": "audit-skills-alignment",
"source": ".session/.../audit.md",
"submittedAt": "2026-08-21T…",
"stats": { "total": 8, "answered": 6, "skipped": 1, "recFollowed": 4, "overridden": 1, "custom": 1 },
"items": [
{ "id": "D1", "type": "decision", "title": "…", "status": "answered",
"chosen": "A", "chosenLabel": "…", "wasRecommended": true, "customText": "", "note": "…" },
{ "id": "Q1", "type": "question", "status": "answered", "controlAnswer": "keep", "text": "…", "quotes": ["…"] },
{ "id": "R1", "type": "report", "status": "skipped", "controlAnswer": null, "text": "", "quotes": [] },
{ "id": "T1", "type": "table", "status": "answered", "note": "", "quotes": [],
"rows": [{ "id": "r1", "controlAnswer": "fix", "text": "", "quotes": [] }] }
]
}
controlAnswer decodes by control type: single → string|null, multi → string[], toggle → boolean.
quotes are exact phrases the user highlighted from that item's content (or that row's cells) — weight them when interpreting text.
- Match
items[].id (and rows[].id) back to the ids you authored to anchor every answer.
Notes
- UI chrome is English (repo-artifact rule);
title/problem/content/justifications are whatever language you author — write them in the user's language.
- The page loads its display fonts from Google Fonts with full system fallbacks; offline it degrades gracefully.
- The CLI is decoupled (Bun built-ins only, zero external deps) — see
cli/README.md.
1---2name: mkd3description: MKD (`mkd` — Make Decision) — a local, browser-based decision-deck CLI the AI drives to collect structured human feedback: decisions with justified options, questions, report reactions and answerable tables, one screen at a time (Slack Catch-Up style). WHEN to use: the AI has MORE THAN ~3 decision points or a long multi-section report/plan that needs the user's verdict point by point. It beats `AskUserQuestion` (capped at ~4 questions × ~4 options, terminal-bound, no rich free-text, cannot show reference content) and beats an inline prose questionnaire (unanchored replies the AI must guess-map back). HOW it works: the AI writes a spec JSON of `items` (`decision` | `question` | `report` | `table`), runs `bun <skill-dir>/cli/index.ts <specPath>` (no install, no binary), and the CLI renders a self-contained deck page under `~/.mkd/`, opens the browser and EXITS immediately — the user answers at their own pace (progress rail, arrow keys, skip = decide later, live stats, localStorage persistence) and pastes the co4license: MIT5---67# MKD (`mkd`) — Make Decision89MKD is a local decision-deck CLI the AI invokes mid-conversation. It renders a browser page where the user works through a deck of items **one screen at a time** (Slack Catch-Up style): decisions with justified options, questions with controls, report sections to react to, and answerable tables. The default flow is **non-blocking copy-paste**: the CLI opens the page and exits; the user answers whenever they want and pastes the Result JSON into the chat. That JSON is the **execution contract** — the AI executes exactly what it says.1011This skill needs **no install step**: there is no compiled binary and nothing added to PATH. The AI runs the CLI from the skill directory with Bun. All output (rendered decks, results, images) lands under `~/.mkd/` — never the cwd — so running it inside any repo leaves **zero footprint** there.1213## Running it (no install)1415```bash16bun "<skill-dir>/cli/index.ts" <specPath> [--wait] [--no-open] [--port <n>] [--timeout <min>]17```1819`<skill-dir>` is this skill's install directory:2021- project-level: `<repo>/.claude/skills/mkd`22- user-level: `~/.claude/skills/mkd`2324Bun runs TypeScript directly; cold start is negligible. If Bun is missing, tell the user to install it (`curl -fsSL https://bun.sh/install | bash`) — do not substitute Node.2526## When to use vs `AskUserQuestion` vs inline prose2728| Situation | Use |29| --- | --- |30| 1-2 option pick, single yes/no, ≤3 simple decisions | `AskUserQuestion` |31| >3 decision points, or decisions needing context + written tradeoffs | **MKD** |32| A long multi-section report/plan the user should react to point by point | **MKD** (`report` items) |33| Row-by-row verdicts over tabular data | **MKD** (`table` item) |34| The reply must be ANCHORED to exact phrases | **MKD** (highlight-to-quote) |35| Non-interactive / CI / no human at a browser | neither — emit plain output |3637## The deck model3839A spec is a list of **items**; each item is one screen. Four types:4041| Type | What the screen holds | Answer captured |42| --- | --- | --- |43| `decision` | Problem statement (plain PM language) + collapsible context balloons + options **with written justification** + optional custom option + note | `chosen` key (or `"CUSTOM"` + `customText`), `wasRecommended`, `note` |44| `question` | Markdown content + `single`/`multi`/`toggle` control + free text | `controlAnswer`, `text`, `quotes` |45| `report` | A report section to react to | `text`, `quotes` |46| `table` | Answerable table, one answer per row (popover controls + text) | `rows[]` each with `controlAnswer`/`text`/`quotes`, plus item `note` |4748Deck chrome, always on: intro screen (headline + markdown + stat tiles), navigable progress rail, ← → arrow keys, per-item **Skip for now** (skipped = decide later, NOT a rejection), summary screen, live footer stats (answered / rec followed / changed / custom / skipped), light/dark theme, localStorage persistence (closing the tab loses nothing).4950## HARD RULE — justified options (this is the point of the tool)5152When authoring `decision` items:53541. **Every option carries a written `justification`**: what it buys (value) and what it costs. The validator **rejects** any option without one (exit 2) — do not fight it, write the justification.552. **At most one option is `recommended`**, and its justification must state **why it is the recommendation** explicitly (e.g. "**Recommended** because …").563. The `problem` is written in **plain language** (PM voice) for someone without the full technical context; jargon and internal codenames go inside `context` balloons that explain how things work today.574. Every real tradeoff the user should know about goes in writing. An unexplained option list is exactly the failure mode this tool exists to eliminate.5859## Spec schema in brief6061```jsonc62{63 "session": "audit-skills-alignment", // required — result echo + persistence key64 "source": ".session/.../audit.md", // optional pointer to the source artifact65 "title": "Catch-Up: audit decisions", // required66 "intro": { // optional intro screen67 "headline": "8 decisions await you",68 "body": "Markdown, PM voice",69 "stats": [{ "n": "115", "label": "verified findings" }]70 },71 "items": [72 {73 "id": "D1", "type": "decision", "title": "…",74 "severity": "high", // optional: high | medium | low (chip)75 "scope": "test-documentation", // optional (chip)76 "problem": "Plain-language markdown problem statement",77 "context": [{ "title": "How does X work today?", "body": "markdown" }],78 "options": [79 { "key": "A", "label": "…", "justification": "Value + cost. **Recommended** because …", "recommended": true },80 { "key": "B", "label": "…", "justification": "Value + cost." }81 ],82 "allowCustom": true // default true83 },84 { "id": "Q1", "type": "question", "title": "…", "content": "markdown",85 "controls": { "type": "single", "required": true, "options": [{ "value": "keep", "label": "Keep" }] },86 "text": { "placeholder": "Why?" } },87 { "id": "R1", "type": "report", "title": "…", "content": "markdown section" },88 { "id": "T1", "type": "table", "title": "…", "content": "optional intro",89 "table": { "columns": ["Test", "Rate"], "rows": [{ "id": "r1", "cells": ["a", "b"] }],90 "rowControls": { "type": "single", "options": [ … ] } } }91 ]92}93```9495Full contract (every field, defaults, validation rules) → `references/schema.md`. Worked copy-pasteable specs → `references/examples.md`.9697## Exact invocation — default (copy mode, non-blocking)98991. **Write the spec** to `~/.mkd/spec-<name>.json` (that filename makes the page land at `~/.mkd/deck-<name>.html`). Keeping it under `~/.mkd/` keeps the repo clean.1002. **Run:** `bun "<skill-dir>/cli/index.ts" ~/.mkd/spec-<name>.json`. The CLI validates, renders a self-contained page, opens the browser, and **exits 0 immediately**. Nothing lands on stdout.1013. **Tell the user** the deck is open in their browser and that pressing **Copy JSON** (footer) and pasting it into the chat brings the answers back. Then continue with other work or end the turn — do NOT block or poll.1024. **When the pasted JSON arrives**, parse it and treat it as the execution contract: `status: "skipped"` items are "decide later" (re-ask later, never assume a rejection); `chosen: "CUSTOM"` means execute `customText` as stated (or ask if something does not add up); always read each item's `note`.103104## `--wait` (blocking, same-turn answer)105106Use only when the AI genuinely needs the answer in the same turn to continue:107108- `bun "<skill-dir>/cli/index.ts" <specPath> --wait` serves the deck over loopback (`--port`, default 4747, auto-increments) with a per-run `x-mkd-token` submit gate, waits for the browser's submit (`--timeout <min>`, default 1440 = 24h), then prints the Result JSON to **stdout** (the ONLY thing on stdout; banners/errors go to stderr) and writes a backup to `~/.mkd/result-<name>.json`.109- Image paste (clipboard → attachment) works **only** in `--wait` mode: entries in `images` arrive as absolute file paths under `~/.mkd/` the AI can `Read`. Copy mode disables paste (no server to persist bytes).110111Exit codes and absence protocol (`--wait`):112113| Exit | Meaning | What the AI does |114| --- | --- | --- |115| 0 | submitted — Result JSON on stdout | parse it, continue the same turn |116| 1 | timeout with NO submission (or runtime error) | user is AWAY, not an error — post a standby note, offer to relaunch |117| 2 | bad spec (unreadable / failed validation) | fix the spec at the reported path, re-run |118| 130 | Ctrl-C | the user cancelled — ask what they want next |119120## Reading the Result121122```jsonc123{124 "session": "audit-skills-alignment",125 "source": ".session/.../audit.md",126 "submittedAt": "2026-08-21T…",127 "stats": { "total": 8, "answered": 6, "skipped": 1, "recFollowed": 4, "overridden": 1, "custom": 1 },128 "items": [129 { "id": "D1", "type": "decision", "title": "…", "status": "answered",130 "chosen": "A", "chosenLabel": "…", "wasRecommended": true, "customText": "", "note": "…" },131 { "id": "Q1", "type": "question", "status": "answered", "controlAnswer": "keep", "text": "…", "quotes": ["…"] },132 { "id": "R1", "type": "report", "status": "skipped", "controlAnswer": null, "text": "", "quotes": [] },133 { "id": "T1", "type": "table", "status": "answered", "note": "", "quotes": [],134 "rows": [{ "id": "r1", "controlAnswer": "fix", "text": "", "quotes": [] }] }135 ]136}137```138139- `controlAnswer` decodes by control type: `single` → `string|null`, `multi` → `string[]`, `toggle` → `boolean`.140- `quotes` are exact phrases the user highlighted from that item's content (or that row's cells) — weight them when interpreting `text`.141- Match `items[].id` (and `rows[].id`) back to the ids you authored to anchor every answer.142143## Notes144145- UI chrome is English (repo-artifact rule); `title`/`problem`/`content`/justifications are whatever language you author — write them in the user's language.146- The page loads its display fonts from Google Fonts with full system fallbacks; offline it degrades gracefully.147- The CLI is decoupled (Bun built-ins only, zero external deps) — see `cli/README.md`.