How the AIDD browser decision gate works — the options.json schema, the submission payload contract, theming rules, and the serve-and-wait loop. Load when building, theming, serving, or debugging the research gate page.
Scanned 9/6/2026
Install to Claude Code
npx -y skills add rchase999/aidd --skill decision-gate --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Decision Gate?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/rchase999-decision-gate)More formats (shields.io, HTML) on the badges page.
---
name: decision-gate
description: How the AIDD browser decision gate works — the options.json schema, the submission payload contract, theming rules, and the serve-and-wait loop. Load when building, theming, serving, or debugging the research gate page.
---
# The decision gate
A browser page cannot hand data to a terminal agent. So the gate ships its own server: Claude launches `gate-server.mjs` in the background, the user chooses in a real browser, and the server writes the contract and **exits**. That exit is how Claude learns the gate passed.
```
options.json ──► gate-server ──► themed page in browser
▲ │
└──── POST /submit ◄─┘ (only on Submit)
│
writes decisions.md + decisions.json
sets phase = execute, exits 0
```
## options.json
```jsonc
{
"project": { "name": "…", "tagline": "…" },
// Brownfield only — what the existing codebase already fixes.
// Rendered as a panel above the tabs and copied into the contract.
"context": {
"mode": "brownfield",
"summary": "two sentences a newcomer could act on",
"stack": [
{ "name": "Django 5.0", "detail": "DRF for the API", "evidence": "pyproject.toml" }
],
"constraints": ["Python 3.11 — pinned by the deployment image"],
"doNotBreak": ["The nightly invoicing job — it is the revenue path"]
},
"theme": {
"palette": { "bg","surface","surface2","line","ink","muted",
"accent","accent2","apply","infer","innovate" },
"fontDisplay": "\"Fraunces\", Georgia, serif",
"fontBody": "\"Inter\", system-ui, sans-serif",
"fontImport": "https://fonts.googleapis.com/css2?…",
"heroTexture": "radial-gradient(…)",
"heroSvg": "<svg …>…</svg>", // inline, authored locally
"heroImage": "assets/hero.webp" // relative to .aidd/02-research/
},
"segments": [
{
"id": "kebab-slug",
"title": "Runtime & Language",
"summary": "what this decision governs and what it locks in downstream",
"question": "the question to hold in mind while choosing", // optional
// Already settled by the existing codebase. Renders read-only: no stance
// buttons, no defer, excluded from the submit requirement, reaches the
// contract as LOCKED. Requires lockedReason. Show exactly one option —
// the thing that is actually in place.
"locked": true,
"lockedReason": "Celery already runs nightly invoicing (app/tasks/invoicing.py)",
"options": [
{
"id": "kebab-slug",
"name": "SvelteKit on Node",
"kind": "Full-stack JS", // optional
"maturity": "stable", // optional
"recommended": true, // max one per segment
"blurb": "one sentence",
"pros": ["…"], // required, non-empty
"cons": ["…"], // required, non-empty
"sources": [{ "title": "…", "url": "https://…", "note": "…" }]
}
]
}
]
}
```
`validate` enforces slugs, uniqueness, non-empty pros and cons, and at least one `http(s)` source per option. A gate that hides costs is not a gate, so the schema will not let you ship one.
## The page contract
`aidd.mjs scaffold-gate` writes a working, data-driven page. Restyle it freely; four things must survive:
1. `<!-- AIDD:INJECT -->` stays in `<head>` — the server replaces it with the options data and the submit shim.
2. The page reads `window.AIDD_OPTIONS`. Never inline a static copy of the options.
3. Submission goes through `window.AIDD.submit(payload)`.
4. Every option keeps three stance buttons, its pros, its cons, and its source links.
The stylesheet is split by a marker: custom properties above `THEME LAYER ENDS`, mechanism below. Rewrite above it freely; below it, keep `.tab`, `.card`, `.stance`, `.defer`, `.progress`, `#submit`, `.toast`, `.done` working.
## Submission payload
```json
{
"version": 1,
"gate": "research",
"project": "Tidepool",
"globalNote": "free text from the bottom of the page",
"segments": [
{
"id": "runtime",
"title": "Runtime & Language",
"defer": false,
"note": "per-segment free text",
"choices": [
{ "optionId": "expo-rn", "stance": "apply" },
{ "optionId": "sveltekit-node","stance": "infer" }
]
}
]
}
```
Server-side rules: option and segment ids must exist; stance ∈ `apply|infer|innovate`; **at most one `apply` per segment**; a segment with no choices is normalised to `defer: true`; missing segments are added as deferred with a warning. Locked segments carry `locked: true` and a note only — any stance submitted for one is dropped with a warning, and they never count toward `resolved` or `deferred`. Rejections come back as `422` with a list, and the page shows them in a toast — the user fixes and resubmits without losing state.
## Interaction rules the page already enforces
- Clicking the active stance again clears it.
- Choosing `Apply` clears any other `Apply` in that segment.
- Setting any stance clears that segment's "Claude decides".
- Submit stays disabled until every **open** segment is resolved or deferred; locked segments are excluded from the count and the progress bar reads "N / M open segments".
- After a `200`, the page shows a confirmation and closes; the server exits `0`.
## Theming
Derive the palette from the **domain**, never from habit. Tide app: cold teal, wet slate, wave motif, humanist serif. Warehouse tool: industrial contrast, safety amber, dense grotesk. If you can't say why a colour is there, it's wrong.
- Prefer authored inline SVG for the motif — always renders, no network, cannot break the page.
- A real image may be downloaded into `.aidd/02-research/` and referenced relatively; the server serves that directory. Never hotlink, never embed an image you have not looked at.
- Body text must clear WCAG AA against its background. The user is reading trade-offs, not admiring a mood board.
- Works at 380px and at 1600px. No external JS.
## Serving
```bash
node "${CLAUDE_PLUGIN_ROOT}/scripts/gate-server.mjs" --timeout 90
```
Run it in the **background** and then wait. Exit `0` submitted, `2` timed out or interrupted, `3` bad inputs. A timeout is not a pass — re-serve it.
While the gate is open: no product code, no scaffolding, no guessing the outcome.
## When the page is opened without the server
The page detects the missing shim, downloads `aidd-decisions.json`, and tells the user to run:
```
/aidd:gate --ingest <path to that file>
```
which validates and writes the same contract. Same rules, same output — only the transport differs.
## Debugging
| Symptom | Cause |
| --- | --- |
| "No research data" | Page opened directly from disk, or `AIDD:INJECT` was removed |
| Server exits `3` | `options.json` missing or failing validation — run `validate` |
| `422` on submit | Two `apply`s in a segment, or an id that isn't in `options.json` |
| Submit stays disabled | A segment has no stance and no defer |
| Server exits `2` | Nobody submitted before the timeout |
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!