# Decision Board

> Collect many per-item decisions (keep/drop/check, accept/reject, yes/no) or answers to a long question list through one interactive HTML page (an Artifact with shared storage), then read the answers back and reply per item. Use when a task needs more than a handful of user judgments and one-by-one CLI questions would be tedious. Triggers - "make a list I can decide on", "let me pick which ones to keep", "too many questions, give me a page", "read the decision board", "check the board answers"; 日本語 - 「一覧にして判断したい」「いる／いらないを選ばせて」「質問が多いので画面で答えたい」「decision-board を読んで」「ボードの回答を確認して」.

- Skill: `ie3jp/decision-board` (Agent Skill, multi-file: 7 files)
- Install (CLI): `npx skillmds@latest add ie3jp/decision-board`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ie3jp/decision-board/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: ie3jp (https://skillmd.com/u/ie3jp)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ie3jp/decision-board

---


# decision-board

A loop for **many decisions or answers at once**, where the CLI's one-question-at-a-time flow would be tedious:

1. Claude writes `board.json` (the items and their choices).
2. `build.mjs` turns it into a single self-contained HTML page.
3. The page is published as an Artifact with `capabilities: {db: {}, artifact: {}}`. The user decides, comments, and asks questions on the page. Everything is saved to the Artifact's database automatically.
4. The user presses **Notify Claude** on the page (the page republishes itself, which reaches the publishing session as a republish notification), or says "read the decision board". Claude reads the answers back with `read_db`, verifies against real code/files, and writes replies with `write_db`. Replies appear under each item as a purple callout, live.

The same HTML also works as a plain local file. Without the database it saves to `localStorage`, and the user copies Markdown/JSON from the "Send to Claude" button into the chat; Claude's replies go back as a JSON block the user pastes into "Import".

## When to use it

- 20+ items to judge, or fewer items but each needs a comment or question.
- Mixed granularity: "drop this whole group, except these two".
- Several rounds of question and answer per item.
- Requirements gathering with dozens of questions, where free-text answers are needed.

Do **not** use it for one question with four or fewer options. `AskUserQuestion` is enough there.

## Files in this skill

| File | Role |
| --- | --- |
| `SKILL.md` | This guide |
| `template.html` | The page. UI strings switch between `ja` and `en` via `board.lang` |
| `build.mjs` | `node build.mjs <board.json> <out.html>` — validates and generates |
| `examples/*.json` | Three ready-to-adapt boards: triage, hearing (free text), review findings |
| `examples/extract-strings.mjs` | Reproducible extractor: user-facing strings from a TS/JS codebase → items |

When this skill is loaded you are told its base directory. Use that path for `build.mjs`; do not assume `~/.claude/skills/decision-board`.

## Step 1 — write board.json

```jsonc
{
  "id": "copy-review-2026-09",       // optional; localStorage key ([A-Za-z0-9_-])
  "lang": "ja",                      // "ja" (default) or "en" — UI language
  "title": "SKIN MATE Copy Review",  // page name: 2–4 words, specific
  "description": "One sentence shown under the title",
  "intro": "What to decide, what was already excluded, what happens next",
  "options": [                       // board-wide choices; default is yes / no / check
    { "value": "keep",  "label": "Keep",  "tone": "good" },
    { "value": "drop",  "label": "Drop",  "tone": "bad"  },
    { "value": "check", "label": "Check", "tone": "warn" }
  ],
  "items": [
    {
      "id": "components.chat.ChatInput.tsx:189:0", // becomes the DB doc id: [A-Za-z0-9_.~:@+-], unique, stable
      "group": "3. Chat",                          // section; navigation and group-wide bulk decision
      "subgroup": "app/components/chat/ChatInput.tsx", // optional sub-section (file, screen, topic)
      "subgroupNote": "optional note shown once under the subgroup header",
      "text": "Type a message",                    // the thing to decide on
      "kind": "visible text",                      // optional category chip; a select filter appears when items carry 2+ kinds
      "meta": ["L189"],                            // optional chips
      "note": "optional context, prefixed with ※",
      "question": "optional question from Claude to the user, shown as Q.",
      "options": [],                               // per-item override; [] = free text only
      "free": true                                 // do not wrap text in quotes (use for question-style items)
    }
  ]
}
```

Rules that matter:

- **Keep `id` stable across regenerations** (file + line, a ticket key, a slug). Answers are keyed by id; a changed id orphans its answer.
- Groups appear in first-seen order. Prefix with numbers to pin the order.
- Up to a few hundred items per board. The database holds 5,000 documents; one item uses one.
- Write `intro` from the user's side: what they are deciding, what is excluded, what happens after.
- Use `tone` for meaning: `good` = positive, `bad` = negative, `warn` = needs attention. Labels are free.

### Authoring checklist (the board is only as good as its items)

The page cannot show what was never listed, and reviewers cannot see why something is classified the way it is. Before building:

- **State the scope in `intro`**: what is included, what was deliberately excluded and why, and what happens after the decisions.
- **Make exclusions visible.** Put items you excluded with a judgment call (not by a mechanical rule) into a final group such as `"Z. Excluded (check the reasoning)"` with `"options": []` and the reason in `note`, so the user can pull one back in.
- **Extract mechanically when the items come from code or files.** `examples/extract-strings.mjs` walks a TS/JS codebase with the compiler API (no comments, stable ids); adapt it rather than grepping by hand.
- **Put the evidence in `note`** for any classification that is not obvious from the text (e.g. "aria-label on an icon button", "only rendered when the video fails").
- **Turn low confidence into a `question`** instead of a silent guess.
- **Self-check before publishing**: pick 10 random items and verify text, location and kind against the source; compare the item count with the raw extraction and account for the difference; run the build and read its warnings.

## Step 2 — build

```bash
node <skill-dir>/build.mjs board.json out/board.html
```

The build fails on a missing title, empty items, an invalid or duplicate id, or a missing text. Fix and rerun.

## Step 3 — publish

Call the `Artifact` tool with the generated file, `capabilities: {"db": {}, "artifact": {}}`, and a `favicon` on the first publish. `db` stores the answers; `artifact` lets the page republish itself so the **Notify Claude** button can wake this session. Forgetting `capabilities` silently degrades the page to local-only mode; omitting `artifact` hides the button (the user must then say "read the decision board").

Then tell the user:

- the URL;
- decisions are saved automatically; press **Notify Claude** when done (or say "read the decision board" if the session that published the page is no longer running);
- "Send to Claude" is only needed for another chat or for keeping a copy.

If the user wants a local file instead, hand over the HTML as is and explain the copy-and-paste loop.

## Step 4 — read the answers back

Start this step when either happens:

- a notification says the artifact was republished from elsewhere and the new version carries `<meta name="decision-board-notified">` — that is the page's **Notify Claude** button (each press adds one version; the content is otherwise unchanged);
- the user asks you to read the board.

After a page-triggered republish, the artifact's live version is newer than the file you published. Before your next `Artifact` publish of the same file, `read` the artifact (or accept the conflict guidance) so the publish is not refused as stale; the HTML content itself is unchanged, so republishing your local file is safe.

```
Artifact action:read_db url:<URL> db_op:list collection:answers query:{"limit":1000}
Artifact action:read_db url:<URL> db_op:get  collection:board doc_id:meta
```

For large boards add `out_dir` and aggregate with a script instead of reading inline.

- `answers/<id>` = `{choice, comment, updatedAt}`. Empty `choice` means undecided.
- `board/meta.memo` = the board-wide notes and questions.

Then:

- Tally by `choice`; read every non-empty `comment`.
- A comment that is a question gets answered **after checking the real code or files**. Never guess.
- For anything marked with a `bad`-tone choice (drop, reject), check what would break if removed: references, tests, accessibility text, CSS that assumes it.

## Step 5 — reply

Per item: `claude/<id>` with `{note, updatedAt}`. Board-wide: `board/claude` with `{note, updatedAt}`. The page subscribes to both, so writes appear immediately.

```
Artifact action:write_db url:<URL> db_op:batch writes:[
  {"op":"set","collection":"claude","doc_id":"<item id>","data":{"note":"...","updatedAt":"<ISO 8601>"}}
]
```

Batches take up to 50 writes; split larger sets. Keep notes short, conclusion first. Every item the user marked with a `warn`-tone choice gets a reply (a finding, or the question you need answered to decide).

To ask new questions, add `question` to the items in `board.json`, rebuild, and republish to the same URL. Answers survive as long as ids are unchanged.

## Without the database (local file, or pasted export)

The Markdown export ends each line with `<!-- <id> -->`; the JSON export has `answers: {id: {choice, comment}}`. Match on id.

Reply with a JSON block the user pastes into "Import":

```json
{
  "claude": { "<item id>": { "note": "..." } },
  "claudeGlobal": "board-wide reply (optional)"
}
```

## Design assumptions

- Writes are last-writer-wins. The page is built for one deciding user, though anyone in the organization with the link can view it. A `db` Artifact cannot be shared publicly.
- **Notify Claude** only reaches a session that is still running and watching the artifact. It is a convenience, not a queue: a press after the session ended is lost, and the fallback is always "read the decision board" in a new session (pass the URL).
- Users write `answers`, Claude writes `claude`. Neither overwrites the other.
- Bulk decisions (group or subgroup) overwrite existing decisions in that scope.
- The page always mirrors state to `localStorage`. On connecting to the database, local-only answers are pushed up; answers present in both prefer the database copy.

