Filing-time review of a bead's acceptance_criteria. Probes whether each criterion is concrete enough that the cheapest literal implementation would not embarrass the author. Emits a structured CONCRETE | UNDERSPEC verdict so `/create-bead` (the filing gate, contract §2) can refuse to emit a vague bead. Runs at filing time, inside /create-bead, before `bd create`; `summoner --preflight` also reads its operator_required[] field.
Installs into .claude/skills of the current project.
Are you the author of Ac Quality Review?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/kurofune-ac-quality-review)
---
# Generated by djinn-gen-adapters from theme=djinn. DO NOT EDIT — regenerate via djinn-gen-adapters.
# djinn-compat: 1
name: ac-quality-review
description: "Filing-time review of a bead's acceptance_criteria. Probes whether each criterion is concrete enough that the cheapest literal implementation would not embarrass the author. Emits a structured CONCRETE | UNDERSPEC verdict so `/create-bead` (the filing gate, contract §2) can refuse to emit a vague bead. Runs at filing time, inside /create-bead, before `bd create`; `summoner --preflight` also reads its operator_required[] field."
---
<!-- djinn-compat-check: begin -->
**Before anything else:** run `djinn skills check --here` in the repo you are working in. If it exits non-zero, stop: relay its output to the user verbatim — it names the fix — and run no other step of this skill. On exit 0, continue.
<!-- djinn-compat-check: end -->
# AC-Quality Review
## Overview
This skill is the **filing-time acceptance-criteria quality gate**. It answers one question per bead:
> "What is the cheapest implementation that literally satisfies every line of this bead's `acceptance_criteria`? Would shipping that be embarrassing?"
If the cheapest satisfying implementation would embarrass you, the criteria are under-specified. The skill emits an `UNDERSPEC` verdict and lists the specific criteria that admit a skin-deep implementation. `/create-bead` runs it on every draft before `bd create`, which is where the work contract (§2, in the `AGENTS.md` djinn block) settles acceptance-criteria quality; the summoner's `--preflight` reads the same sidecar.
This is a **single-lens** review — one agent, one pass, one verdict. It does not iterate, dedupe, or fan out. The cost model is "one call to refuse one draft before it is filed" because filing is the cheapest moment to refuse — every downstream step (implement, review, fix-review) is more expensive than tightening a few `acceptance_criteria` lines now.
**Scope boundary.** This skill judges the bead's `acceptance_criteria` text alone. It does NOT judge:
- The bead's `description` (intent prose is allowed to be conversational).
- The implementation (no code exists yet — this runs at filing).
- Whether the bead is the right work to do (that's `/plan-to-beads`'s job).
If the criteria are concrete but the bead is otherwise poorly-framed, the verdict is `CONCRETE`. If the bead is well-framed but the criteria are skin-deep, the verdict is `UNDERSPEC`. The two axes are independent.
**Second, orthogonal axis — mode-compatibility.** Separate from "are the criteria concrete?" is the question "can the *unattended autonomous harness* satisfy them at all?" A criterion can be perfectly concrete AND still require a human — interactive dogfooding with an operator continuing between steps, or an action outside the repository such as a login or a physical device. Only a genuine human step counts: a criterion that merely names where a result is recorded is the harness's own work. This axis is reported in the `operator_required[]` field and is **independent of the `verdict`**: a `CONCRETE` bead can carry a non-empty `operator_required[]`. `/create-bead` rewords such a criterion before filing — see Step 3b and the worked example in Example 4.
## Inputs
The invoker provides exactly one of two input forms:
1. **Draft** (filing time — `/create-bead`, before `bd create`, so no ID exists yet): the draft's `acceptance_criteria` text, plus its `description` and `type` when known. Review the supplied text directly; do not call `bd show`.
2. **`bead_id`** (an already-filed bead — `summoner --preflight`) — e.g. `phoenix-abcd`. The skill reads `bd show <bead_id> --json` to fetch `acceptance_criteria`, `description`, and `type`.
If the invoker supplies both, review the draft text and echo the `bead_id` in `bead`. If it supplies neither draft `acceptance_criteria` nor a `bead_id`, stop and ask.
## Process
### Step 1. Read the criteria
- **Draft input:** use the supplied `acceptance_criteria`, `description`, and `type` verbatim. A missing `type` is echoed as `""` in `bead_type`.
- **`bead_id` input:**
```bash
bd show <bead_id> --json | jq -r '.[0] | {type, acceptance_criteria, description}'
```
If `acceptance_criteria` is empty or null, emit `UNDERSPEC` with one row naming the empty field — a bead with zero criteria has nothing to verify and zero criteria cannot pass the gate.
### Step 2. Split criteria into segments
Each non-blank line of `acceptance_criteria` is one segment. Trim whitespace. Preserve order. Index segments 1..N.
### Step 3. Apply the rubric to each segment
For each criterion, ask the **Minimum-Output Simulation**:
> "If a junior implementer read this single line and wrote the cheapest code that literally satisfies it — no taste, no extra effort, just 'the words on the page' — would the result embarrass the bead author?"
Embarrassing minimum outputs are the failure mode this gate exists to catch. The plan-to-beads "Case files" appendix documents the canonical failure shapes (CSS-div charts, silent manifest drops, aggregate-where-a-series-was-wanted, picture-of-a-tool, list-where-a-workspace-was-wanted). Re-derive the same shapes here against the segment:
| Rubric question | Triggers `UNDERSPEC` when |
|---|---|
| **Named verification surface?** | An empirical threshold ("under 5s", "500 ticks", "10k rows") with no test fixture, file:line, env-var-gated integration test, or `"verified by dogfooding"` phrase. |
| **Concrete render primitive?** *(visualization)* | "bar chart" / "timeline" / "graph" with no SVG/Canvas/library mention and no hover/click payload. |
| **Named axes / dimensions?** *(visualization)* | "Per X" / "over time" claims with no "X-axis is X" line. |
| **Hover / click / keyboard payload?** *(interactive UI)* | "Interactive table" / "clickable rows" with no Interaction Inventory line naming the hover payload, click target, and keyboard binding. |
| **Filter / sort / selection at scale?** *(data-heavy UI)* | "Shows a list of N items" with no filter, sort, or pagination criterion and no Scalability Assumption. |
| **Empty / loading / error states named?** *(UI)* | Feature bead with no mention of any of the three states. |
| **Verifiable by the cheapest impl?** *(structural)* | "Idempotence contract" / "well-defined behavior" / "passes correctness" — abstract nouns with no observable signature. Rewrite as `Render(Render(F))` bit-identical to `Render(F)` on every Step (named operation, named equivalence, named scope). |
| **Precondition or trigger unstated?** *(behavioral)* | A behavioral criterion whose given-state or trigger is omitted, AND the omission changes the cheapest satisfying implementation. "Returns 401 for expired tokens" passes (the precondition "expired token" is stated); "Shows an error message" fails (error from what? the cheapest impl hard-codes one banner for one case and skips the rest). Rewrite in the compact form `Given X → Y` (`Given expired token on a protected route → 401`) or fold the precondition into the sentence. Do NOT flag criteria whose implicit precondition is unambiguous in the bead's context — this row exists for meaning-changing omissions, not ceremony. |
| **Test assertion masquerading as user-visible outcome?** | "Test coverage for all branches" / "100% coverage" — implementation artifacts, not user-visible outcomes. (Acceptable on plumbing beads; otherwise rewrite to the user-visible behavior the test would prove.) |
Plumbing beads (DB migrations, single API endpoints, type definitions) are held to a lower bar: structural criteria ("endpoint returns 200 for valid input", "migration adds column FOO with type BAR NOT NULL") are sufficient. Feature beads (UI capabilities, interactive workflows, anything user-facing) MUST pass the rubric above.
### Step 3b. Apply the mode-compatibility check to each segment
Independently of the embarrassing/UNDERSPEC rubric above, ask of each criterion:
> "Could the **unattended autonomous harness** — no human present, formula-autonomous mode (the harness owns terminal status: it records the evidence comment on the bead and writes `.djinn/last-status.json` itself) — satisfy this criterion on its own?"
If not, the criterion is **operator-required**. Flag it in `operator_required[]` (this is orthogonal to the criterion's `pass`/`embarrassing` tag — a criterion may be both concrete AND operator-required). There are exactly two trigger classes, and both turn on a **human being needed**:
| Trigger class | Fires when the criterion requires… | Canonical example |
|---|---|---|
| **(a) interactive-operator** | a human operator interacting mid-task — continuing between steps, answering a prompt, driving an `--interactive` workflow, making a live judgment call. | "Dogfood `/wish --interactive` end-to-end, continuing past each step." |
| **(b) external-action** | a human's action OUTSIDE the repository — a login, a physical device, an approval in another system, a console-only deploy with no API. | "Get the design partner to approve the layout in a screen-share." |
**Naming a recording surface is not a trigger.** A criterion that names the bead's evidence comment, `.djinn/last-status.json`, or the verify report as where a verification is recorded is satisfied by the harness writing it in formula mode, and is never operator-required — the write is the harness's job, so the criterion costs the agent nothing. Judge the VERIFICATION the criterion asks for, not the surface it names: authoring habits like "manual bd round trip" or "documented by hand in the evidence record" describe a record, not a human step, and flagging them defers beads a drain could have finished.
For each operator-required criterion, emit one `operator_required[]` entry naming its 1-based `index`, the verbatim `criterion`, the `reason` (which trigger class and why), and the `manual_step` (what the operator must do by hand). Most beads have an empty `operator_required[]` — flag a criterion ONLY when the autonomous harness genuinely cannot satisfy it, not merely when a human *could* do it faster.
No drain step re-checks this after filing, so an operator-required criterion that reaches a drain costs implementation iterations against work no unattended agent can finish. `/create-bead` rewords it at filing, and `summoner --preflight` reports it before a drain. The interactive contract treats it the same way — an operator-required criterion is worked by hand (e.g. `/wish --interactive`), not by the unattended harness.
### Step 4. Tag each segment
For each segment, assign a tag:
- `pass` — the cheapest literal implementation would not embarrass you.
- `embarrassing` — the cheapest literal implementation would ship something skin-deep, with a one-sentence "what it would look like" example and a "rewrite as" suggestion.
### Step 5. Emit the verdict
The verdict is structural:
- **All segments `pass`** → verdict **CONCRETE**.
- **Any segment `embarrassing`** → verdict **UNDERSPEC**.
There is no middle ground. A bead with one weak criterion among ten still rolls UNDERSPEC, because the agent will read that one weak criterion and produce the embarrassing minimum for that capability. The cheapest fix is to tighten the one line, not to negotiate the verdict.
## Output
Emit a fenced ```json``` sidecar matching the schema below. Optional Markdown prose may precede the fence (e.g., a one-paragraph summary), but the gate reads only the JSON. Do not wrap the JSON in additional code-fence noise, do not emit multiple JSON fences, and do not produce a JSON document outside a fenced block — the parser locates the first ```json``` fence and decodes its body.
### JSON sidecar schema
```json
{
"verdict": "CONCRETE" | "UNDERSPEC",
"bead": "phoenix-abcd",
"criteria_count": 10,
"embarrassing_count": 2,
"findings": [
{
"index": 3,
"criterion": "Bar chart shows revenue per period",
"minimum_output": "CSS div with style='width:43%' — technically a bar; no axes, no tooltip, no interactivity",
"rewrite_hint": "Name the render primitive (SVG via d3-scale or equivalent), the hover payload, and the click target"
},
{
"index": 7,
"criterion": "Scales to 500 ticks",
"minimum_output": "No test exercises 500 ticks; algorithmic reasoning passes the criterion in prose",
"rewrite_hint": "Name the verification surface: a unit fixture at testdata/500-ticks.json, an integration test gated on DJINN_INTEGRATION=1, or the literal phrase 'verified by dogfooding'"
}
],
"operator_required_count": 0,
"operator_required": [],
"bead_type": "feature",
"acceptance_criteria": "Activity shown as chronological timeline\nClicking any chip opens entity in right drawer\n..."
}
```
### Field rules
- **`verdict`** — exactly `CONCRETE` or `UNDERSPEC`. Callers branch on this token; any other token is a failed review.
- **`bead`** — the bead id this review covers. Mirrors the `bead_id` input; for a draft with no ID, the literal string `"draft"`.
- **`criteria_count`** — total non-blank acceptance_criteria segments. Equals `findings[]` length only when every criterion is embarrassing.
- **`embarrassing_count`** — count of segments tagged `embarrassing`. `0` when verdict is `CONCRETE`.
- **`findings[]`** — one entry per `embarrassing` segment. Empty when verdict is `CONCRETE`. Each entry names the segment `index` (1-based), quotes the `criterion` text verbatim, describes the cheapest `minimum_output` in one sentence, and offers a one-sentence `rewrite_hint` so the caller has a concrete handle.
- **`operator_required[]`** — one entry per criterion the **unattended autonomous harness cannot satisfy** (Step 3b). ORTHOGONAL to `verdict` and to `findings[]`: a `CONCRETE` bead can carry a non-empty `operator_required[]`, and a flagged criterion may also appear in `findings[]` if it is independently embarrassing. Empty (`[]`) for the common case. Each entry names the segment `index` (1-based), quotes the `criterion` verbatim, gives the `reason` (which trigger class — interactive-operator / external-action — and why), and the `manual_step` (what the operator does by hand). A non-empty array blocks `/create-bead` and marks the bead `OPERATOR-REQUIRED` in `summoner --preflight`.
- **`operator_required_count`** — count of `operator_required[]` entries. `0` for the common case; equals `len(operator_required)`.
- **`bead_type`** — the bead's `type` (`feature` | `task` | `chore` | `bug` | `epic`), echoed verbatim from `bd show` or the draft input. Echoed for the record so a reader of the sidecar sees the bead shape the review judged; not currently gated on by the check.
- **`acceptance_criteria`** — the bead's raw `acceptance_criteria` text (newline-separated segments), echoed verbatim, for the same record-keeping reason as `bead_type`.
### Required tokens (skill-prompt-contract gate)
The startup-time prompt-contract check scans this file for these literal strings to confirm the LLM instruction has not been silently dropped. Each token below MUST remain present as a substring in this SKILL.md file:
- `CONCRETE`
- `UNDERSPEC`
- `operator_required`
## Verdict mapping
`/create-bead` (filing time) is the caller that acts on the verdict; `summoner --preflight` reads only `operator_required[]`.
| Caller | When | Reads | Acts |
|---|---|---|---|
| `/create-bead` | on every draft, before `bd create`, passing the draft text (no `bead_id`) | `verdict`, `findings[]`, `operator_required[]` | `CONCRETE` with an empty `operator_required[]` → the verdict is not the end: `/create-bead` continues in the same turn to its `bd create --graph` filing call; `UNDERSPEC` or a non-empty `operator_required[]` → reword the flagged criteria and re-run, inside a 2-attempt tightening budget; still failing on the third pass → no `bd create` |
| `summoner --preflight` (`cmd/summoner/preflight_checker.go`) | over the scoped ready set, before a drain, dispatching nothing, passing each `bead_id` | `operator_required[]` (decoded as `checks.ACQualitySidecar`) | reports each bead as `OPERATOR-REQUIRED`, `autonomous-ready` or `CHECK-FAILED` |
The contract (`core/bead-work-contract.md` §2) settles acceptance-criteria quality at filing: `/create-bead` runs this skill on every draft and emits no `bd create` while the verdict is `UNDERSPEC`. No shipped formula runs it after the claim.
## Worked examples
### Example 1 — `CONCRETE` verdict (feature bead, all criteria pass)
Bead `phoenix-customer-dossier`:
```
acceptance_criteria:
Activity shown as chronological timeline with event icons and source labels
Open tickets listed with priority badges and age-in-days
All active tags shown as styled badges with context tooltip
Hovering a ticket row reveals full subject and assignee tooltip
Clicking any chip opens entity in right drawer
Arrow keys move ticket selection
Readable with 200 tickets and 50 tags per customer without layout breaking
```
Every criterion names a user-observable outcome and (where applicable) the interaction payload and scale. Verdict: `CONCRETE`.
### Example 2 — `UNDERSPEC` verdict (feature bead, weak criteria)
Bead `phoenix-revenue-chart`:
```
acceptance_criteria:
Bar chart shows revenue per period
Scales to large datasets
Test coverage for all branches
```
- Criterion 1 admits a CSS-div bar chart (Case 1 from plan-to-beads): no SVG primitive, no axes, no hover payload, no click target.
- Criterion 2's "large datasets" is not bound to a number or a verification surface.
- Criterion 3 is a test-coverage tautology that produces no user-visible outcome.
Verdict: `UNDERSPEC`. The skill emits one `findings[]` entry per embarrassing criterion with a `rewrite_hint`. `/create-bead` applies each hint to the draft and re-runs the review.
### Example 3 — `CONCRETE` verdict (plumbing bead, structural criteria are sufficient)
Bead `phoenix-token-endpoint`:
```
acceptance_criteria:
POST /api/tokens returns 201 with {token, expires_at} for valid {user_id}
Returns 400 with error code 'invalid_user' for unknown user_id
Returns 429 after 5 requests/minute per user_id
```
Plumbing beads (single API endpoint, DB migration, type definition) are scored against the structural bar: each criterion names an observable signature (status code, payload, rate-limit threshold). No interaction inventory or scalability assumption is required. Verdict: `CONCRETE`.
### Example 4 — `CONCRETE` verdict WITH a populated `operator_required[]` (orthogonality proof)
Bead `djinn-ipee.8` (the real bead that motivated this axis — djinn-ccyz.40):
```
acceptance_criteria:
/wish parses a goal string into an ordered list of formula steps
--dry-run prints the resolved step plan without executing
Each step's prompt is rendered from the goal + the step template
Errors in a step halt the wish and print the failing step id
Unit tests cover the parser, the renderer, and the halt path
Dogfood /wish --interactive end-to-end, continuing past every step prompt, and note each place the executed step order diverged from the --dry-run plan
```
Criteria 1–5 are concrete and autonomously satisfiable — every one names an observable signature or a named test surface, so none is `embarrassing`. Criterion 6 is ALSO concrete (it names the exact command and the exact observation) — but it is **operator-required** under class (a): `/wish --interactive` blocks on a human continuing past each step prompt, so no unattended agent can reach the end of the walkthrough at all. Note what does NOT contribute: the criterion could have added "and record it in the bead's evidence comment" and still be class-(a)-only, because the harness records that itself. So the verdict is `CONCRETE` (no criterion is embarrassing) AND `operator_required[]` is non-empty:
```json
{
"verdict": "CONCRETE",
"bead": "djinn-ipee.8",
"criteria_count": 6,
"embarrassing_count": 0,
"findings": [],
"operator_required_count": 1,
"operator_required": [
{
"index": 6,
"criterion": "Dogfood /wish --interactive end-to-end, continuing past every step prompt, and note each place the executed step order diverged from the --dry-run plan",
"reason": "Trigger (a) interactive-operator: /wish --interactive blocks on a human continuing past each step prompt, so an unattended agent never reaches the end of the walkthrough.",
"manual_step": "Operator runs /wish --interactive by hand and walks the steps, noting each divergence from the --dry-run plan."
}
]
}
```
`/create-bead` refuses to file this bead as written, and `summoner --preflight` reports it `OPERATOR-REQUIRED`, rather than letting a drain burn implementation iterations against a criterion no unattended agent can finish. This is exactly the failure the skill exists to catch cheaply: a `CONCRETE` bead that is nonetheless mode-incompatible.
## Anti-patterns the rubric MUST catch
The rubric must surface these as `UNDERSPEC` because the underlying failure mode has shipped before. Each row maps to a Case File entry in `~/.claude/commands/plan-to-beads.md` — the rubric here and the case files there are the two halves of the same defense.
| Pattern | Case File | Why it's UNDERSPEC |
|---|---|---|
| "Bar chart of X" with no SVG/Canvas/library, no hover payload, no click target | Case 1 | Admits `<div style="width:43%">` literal-percentage divs. |
| Plan manifest names component, no bead's `acceptance_criteria` mentions it | Case 2 | Component gets silently dropped from the implementation. |
| "Stacked bar of X per month" with no "X-axis is one bar per month" line | Case 3 | Admits a single aggregate stacked bar across all months. |
| "Gantt timeline with dependency lines" — no Reference Standard, no hover payload, no zoom | Case 4 | Admits a static SVG picture-of-a-tool with no interactivity. |
| "Shows a list of alerts" — no filter, sort, scale criterion | Case 5 | Admits a flat scrollable table that breaks past a few dozen rows. |
| Empirical threshold ("under 5s", "500 ticks") with no verification surface | bead-authoring rule | Algorithmic reasoning in prose passes the criterion; no test or dogfooding step ever exercises the number. |
When a finding matches one of these patterns, the `rewrite_hint` should reference the case file by number (e.g. "See Case 1") so `/create-bead` has a concrete vocabulary for the tightening.
## Failure-mode handling
- **Bead not found** (`bead_id` input). `bd show <bead_id>` exits non-zero. Emit verdict `UNDERSPEC` with one finding naming the lookup failure, so the gate halts and the operator triages a typo before any code runs.
- **`acceptance_criteria` is empty or null.** Emit `UNDERSPEC` with one finding `{index: 0, criterion: "(empty)", minimum_output: "Zero criteria means any implementation passes by vacuous truth", rewrite_hint: "File the criteria before implementing"}`.
- **`bd` binary unavailable** (`bead_id` input; a draft review needs no `bd`). Emit verdict `UNDERSPEC` with one finding naming the toolchain absence. The gate halts; the operator repairs `PATH` and retries.
## Universal constraints
- **Verify against the criteria text.** Read `acceptance_criteria` verbatim — from the draft input, or via `bd show` for a `bead_id`. Do not pattern-match against the title or description.
- **Read-only.** The skill never edits the bead. Tightening `acceptance_criteria` is the caller's job (`/create-bead` rewords the draft); this skill emits findings only.
- **Cap output at ~500 words of prose + the JSON sidecar.** Tokens go to the rubric application, not narration.