create-bead
Skill4 starsChanged 6 days agoMITView on GitHub
File an ask as beads. Invoke when the user wants to file a bead, create an issue, turn an ask, plan, PRD or design doc into tracked work, or run `bd create`. Writes each bead at product level — what is wanted, why, the goal beyond it, the constraints and exclusions that bind it, and how we will know it worked — and files one bead per ask, cut across every layer, splitting only at a real boundary (independent tracks, a wide refactor, an operator decision, the per-bead spend cap). Runs /ac-quality-review on every draft with a 2-iteration tightening budget and files nothing while it still returns UNDERSPEC. Files every gate-approved bead through one atomic `bd create --graph` plan (a lone bead is a one-node plan). Args mode (a structured bead spec or a plain-language ask, for programmatic callers) and interactive mode (asks only what the ask leaves open).
In this repository14 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- ac-quality-reviewSkill
- beadsSkill
- consult-councilSkill
- freshness-triageSkill
- playwright-cliSkill
- reaperSkill
- reviewSkill
- rubSkill
- verify-acceptanceSkill
- wishSkill
- create-beadSkill
- wishSkill
The file
We split this skill into the rules it gives an agent
9 rules. About 0 are common, 3 are distinctive, and 6 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
6
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 9
Before anything else: run djinn skills check --here in the repo you are working in.
- Very specific: line 24
Quality by construction: Before filing, the skill runs /ac-quality-review on each draft's acceptance criteria and tightens what it flags, …
- Distinctive: line 69
Ask only what the ask and the repo leave open, in this order:
- Very specific: line 84
When the ask is to move a number rather than reach a done-state, run the crucible interview (crucible below) instead of questions 8-10.
- Very specific: line 90
Before drafting, run bd --version.
- Very specific: line 94
Read the code, docs and DECISIONS.md rulings the ask touches before drafting, so the bead names what exists and binds to what is settled.
- Distinctive: line 156
Do not force a wide refactor into vertical slices.
- Distinctive: line 233
The interview: Ask the operator only these four, in order:
- Very specific: line 319
Before filing, run /ac-quality-review on each node's drafted acceptance_criteria, passing the draft text, its description and its type.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
<!-- 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 -->Create-Bead
Overview
This skill turns an ask into filed beads. It is the only authoring path: the always-loaded rules (the repo's AGENTS.md) carry only the invariants.
One ask, one bead. The operator states an ask once; it lands as one coherent change. A bead cuts across every layer the ask needs — backend, frontend, tests, docs — because a worker split from its other half loses the context that makes the pieces fit. The bead splits only at a real boundary (One bead per ask, below). There is no cap on acceptance lines or description words, and no target bead count.
Product level, never the steps. A bead says what is wanted and how we will know it worked. It never tells the worker how to build it: a spec that prescribes implementation passes its mistakes downstream, and the worker reading the repo knows the code better than the author guessing at it.
One transaction per filing. Every gate-approved bead is a node in one bd create --graph <plan.json> call — a lone bead is a one-node plan, and its parent and blockers ride the same plan. bd applies a plan in one transaction, so a bead never exists without its blocker edges: a loaded workshop drains a bead the moment it is ready.
Quality by construction. Before filing, the skill runs /ac-quality-review on each draft's acceptance criteria and tightens what it flags, inside a 2-attempt budget (Internal AC-quality gate, below). No step after the claim re-checks acceptance quality (work contract §2).
Invocation modes
Args mode (programmatic)
The invoker passes one of two inputs.
A bead spec. Required keys: title, type, priority, description, acceptance, touches. Optional: key, parent, design, notes, labels, blocked_by, ac_labels, short_name, agent, crucible.
key— the node's plan-local key (defaultbead).parent— an EXISTING bead id; becomes the node'sparent_id.blocked_by— EXISTING bead ids this bead waits on; each becomes an edge{"from_key": <key>, "to_id": <id>, "type": "blocks"}.ac_labels— one short label per non-blankacceptanceline, in order. Absent, the skill authors them (ac_labelsbelow).short_name— the bead's name in narrow columns. Absent, the skill authors it from the title (short_namebelow).
title: "Expired sessions sign the user out cleanly"
type: feature
priority: 2
parent: djinn-auth1
blocked_by: ["djinn-abc12"]
description: |
**What is wanted:** a user whose session expires mid-task is signed out with their place kept...
acceptance: |
Given an expired token on any protected route → 401 with error code session_expired
Given a 401 session_expired in the app → the sign-in page opens with the user's unsaved draft still in the editor after sign-in
design: "Tokens are issued in internal/auth/; the app's request layer is web/src/lib/api/."
notes: "Source: docs/auth-design.md §Expiry"
labels: "auth"
touches: ["internal/auth/", "web/src/lib/api/"]
ac_labels: ["401 on an expired session", "Draft kept across sign-in"] # optional; authored when absent
short_name: "Clean session expiry" # optional; authored from the title when absent
agent: "codex gpt-6-astra high" # optional; see `agent` below
# crucible: {...} # only on a formula:crucible bead; see `crucible` belowA payload missing a required key returns a structured error naming the missing keys; never invent a title, priority, type or path list.
An ask. ask: — the operator's words, verbatim — plus any of parent, priority, labels, blocked_by, agent. The skill reads the repo, drafts the nodes the ask needs (usually one), and gates each.
Either way, without --execute the skill files nothing and returns the gate-approved plan (Output); with --execute it files it.
Interactive mode (bare invocation)
Ask only what the ask and the repo leave open, in this order:
- Title — a plain descriptive phrase naming the change the operator would notice. Beside it, show the
short_nameyou authored, for the operator to correct. - Type —
epic/feature/task/chore/bug. - Priority — P0-P4, as urgency (0=critical, 4=backlog).
- Parent — bead id, or
none. - Blocked by — existing bead ids this bead waits on, or
none. - The ask — what is wanted, why, and the goal beyond it.
- Constraints and exclusions — what must stay true, and what is out.
- Done — how we will know it worked. Draft
acceptance_criteriafrom this and show theac_labelsyou authored beside it, one per line. - Design hints (optional) — binding constraints and where things live.
- Touches — the paths this bead will change. Derive the default from the repo and the answer to 9 and offer it back rather than asking cold; directories when unsure.
- Source / context (optional) — plan reference, sibling beads.
- Agent (optional, operator's call) — ask only when the operator named who should do the work. See
agentbelow.
When the ask is to move a number rather than reach a done-state, run the crucible interview (crucible below) instead of questions 8-10.
Draft each field, run the gate, show the assembled plan for confirmation, then file it.
Preflight — bd 1.3.0 floor
Before drafting, run bd --version. Below 1.3.0, bd create --graph does not carry every bead field — acceptance_criteria, design, notes, labels and metadata are lost. Stop before any filing with an error naming both versions: bd <installed> is below the 1.3.0 floor bd create --graph needs; upgrade bd and retry. Do not fall back to one-at-a-time bd create.
Writing the bead
Read the code, docs and DECISIONS.md rulings the ask touches before drafting, so the bead names what exists and binds to what is settled. Then write each field under its own key — never concatenate fields:
| Field | Holds |
description | What is wanted — the change, in the operator's terms. Why — the problem it solves. The goal beyond the goal — what the operator is really after, so the worker can make the calls the bead does not. Constraints — what must stay true (a compatibility promise, a performance floor, an interface others read). Exclusions — what is out, named. |
acceptance_criteria | How we will know it worked: observable outcomes, one per line (Acceptance criteria, below). |
design | Binding constraints and pointers to where things live: the files, rulings and docs that bear on the work. Never a numbered step list, a prescribed order or a file-by-file plan — unless order is itself the requirement. |
notes | Source and context: where the ask came from (Source: <file path> §<section>, or the operator and the date), sibling beads, an operator go. |
metadata | touches, ac_labels, short_name, and agent or crucible when set — each below. Read by the harness and the ops room, not by the worker. |
Every constraint and every exclusion the ask names appears in the description; each one a reviewer can check also gets its own acceptance line. When the ask names deliverables by name (files, endpoints, commands, screens), each appears in an acceptance line or the description says why it is out — absorbed into prose, it gets dropped.
The description is a brief, not a build instruction: "a user whose session expires mid-task should lose nothing" lets the worker build depth; "add a 401 handler to api.ts" gets exactly that handler. Use markdown structure (bolded labels, bullets) when the content is long enough to benefit. Prefer a pointer to a committed doc (Read docs/support-workspace.md §3.3) over pasting its prose: inline text is paid by every agent that loads the bead.
A child of an epic carries its own delta; the shared brief lives once in the epic's description, and a notes line names it: Context: parent epic <id> carries the shared brief. The worker reads the parent before coding (work contract §2). A standalone bead carries its whole intent inline.
Priority is urgency, project-wide: the follow-up floor, the reaper and triage all read it that way. Never encode order as priority; order is edges.
Labels are free-form, 2-4 per bead, for filtering. Stamp no formula:* label by default: the default iter formula reviews every bead the same way. formula:claude (implement on Claude when codex is unavailable) and formula:codex (every agent step on codex) select a provider and are the operator's call, stamped only when asked for by name. formula:crucible changes what the bead is (crucible below).
Acceptance criteria
One criterion per line. No semicolons separating criteria, no paragraph prose, no markdown bullets. Each non-blank line is one independently checkable outcome; blank lines may group lines and do not count. Every downstream reader counts lines: the harness writes one evidence row per non-blank line (work contract §6), and /verify-acceptance checks each.
An outcome, never a step. Each line says what is true when the work is done, in terms a reviewer can check by running, using or reading the result: what the user sees and can do, what a command prints, what an endpoint returns, what no longer exists. It names no technology, library, file layout or function unless that name is itself the requirement (a public command, an endpoint others call, a file another tool reads). An observable behaviour is its own verification surface: the line needs no test name or file:line.
Every number names where it is measured. A threshold ("under 5 s", "500 rows", "at most 28 cells") names the test, fixture, command or "verified by dogfooding" step that checks it. Prefer a structural fact when the number cannot be tested: "imposes no cap on criterion count" beats "handles a dozen criteria".
State the precondition when it changes the meaning. Use the compact form Given X → Y on one line: "Shows an error message" admits one hard-coded banner; "Given a 5xx from the tickets API → the list shows a retry banner with the error code" does not. Never split a Given onto its own line, and skip the form where plain prose already states the precondition ("Returns 401 for expired tokens").
No abstract nouns. "Works well", "idempotence contract", "well-defined behaviour", "scales well" each admit any implementation. Name the operation, the equivalence and the scope: "Render(Render(F)) is byte-identical to Render(F) for every step".
Fit the lines to the ask. A technical ask — an API, infrastructure, a developer tool, a refactor — takes structural lines ("POST /api/tokens returns 201 with {token, expires_at}", "no reference to old_column remains under internal/") and carries no empty, loading or error-state, keyboard or other UI line. An ask with a user interface gets a line for each action the user takes and its result ("hovering a bar shows its period and revenue", "Enter opens the selected ticket") and one for the data volume it must stay usable at. The work contract §4 visual rules (empty, loading and error states, keyboard reach, readable at 3x volume, tooltips that wrap) are already implicit criteria for every visual change: add a line only where the ask fixes something specific, such as what an empty list says. Named visual patterns to omit are exclusion lines (No pill-shaped buttons), never "avoid a generic look".
Test assertions are not outcomes. "100% branch coverage" or "component renders with props" describe the build; write what the test would prove. A test-suite line belongs only on an ask about the tests themselves, or as one line that the repo's gate passes.
Before (prescribes the build):
- Add
RevenueChart.svelteusing d3-scale with an SVG<g>per bar- X-axis via
d3.axisBottom, y-axis viad3.axisLeft
After (what the user sees and can do):
- The revenue view shows one bar per month for the last 20 months, ranked by revenue, with month and revenue labels readable at 20 bars
- Hovering a bar shows its month, revenue, order count and change on the year before
- Clicking a bar filters the orders table below to that month
One bead per ask — split only at a real boundary
File the whole ask as one bead, across every layer it touches. A sequential chain over the same code (struct, then loader, then validator) is one bead: the chain buys no parallelism and pays a review pass per link. Split only where one of these holds, and name the boundary in each node's notes:
- Independent tracks — two parts share no paths and can land in either order. Each is its own node, with no edge between them.
- A wide refactor — the procedure below.
- An operator decision between halves — the second half cannot be specified until the operator rules on the first. The first half is a node; the second waits on it with a blocking edge, and its open question goes to the operator.
- The spend and review budget — the ask cannot plausibly land inside one bead's spend cap (
.djinn/config.jsonmax_cost_usd_per_bead) and the review loop's 3 attempts (core/iter.formula.tomlreview-loop). Split along the line where each part ships on its own.
Neither size, nor layer, nor line count is a boundary: an ask with backend and frontend halves and a dozen outcomes is one bead. Nodes that share a brief (a wide refactor, a decision between halves, a budget split) go under one epic — the owning open epic, or a new epic node in the same plan whose description carries the shared brief. Independent tracks are separate asks: each goes under the invoker's parent or the epic that owns it, never under a new epic made only to group them.
Wide refactors — the exception to vertical slicing
One class of work breaks the rule above. A wide refactor is a single mechanical change — rename a column, retype a shared symbol, move a package — whose blast radius fans across the codebase: one edit breaks call sites in every corner of the tree at once, so no vertical slice can land green and a planner who forces one emits beads that cannot pass their own gate. Recognize it by blast radius, not by size — the change itself is trivial; what makes it wide is how many callers it breaks simultaneously.
Do not force a wide refactor into vertical slices. Sequence it as expand → migrate → contract, and wire the phases with real blocking edges in the plan:
- Expand — one bead that adds the new form beside the old one, touching no call sites. Nothing breaks, because nothing has moved yet. This bead blocks every bead below it.
- Migrate — call sites move in batches sized by blast radius (per package, per directory, per module), and each batch is its own bead, blocked by the expand bead. The suite stays green batch to batch because the old form still exists — that is the whole reason to expand first. Batches run in parallel unless two of them touch the same file, where the Step 4 sibling-file check still applies.
- Contract — one bead that deletes the old form once no caller remains, blocked by every migrate batch. Miss one of those edges and
bd readywill surface the delete while a caller still points at the old form.
Fallback when the batches cannot stay green alone. Some refactors have no expand form that keeps each batch green in isolation — an exhaustive-match retype, a schema change the ORM validates at boot. Keep the same expand → migrate → contract sequence, but let the migrate batches share an integration branch and all block a final integrate-and-verify bead. Green is promised only at that integration bead: each batch's acceptance criteria describe its own call-site conversion, and the integrate-and-verify bead carries the suite-green criterion for the whole chain. Say so in the batch beads' notes — an executing agent that expects a green suite from its own bead will otherwise thrash against a failure the plan already accounted for.
The sibling-file check: two batches whose touches share a file are merged into one batch or ordered by an edge. Wide-refactor beads take structural acceptance lines ("no reference to old_column remains under internal/") and no UI lines.
Edges
A blocking edge only where one node cannot land before another: expand before each migrate batch, every batch before contract, a decision before the half that waits on it. Independent tracks get none, and a node's position in the plan never makes one. An edge reads from the waiting node to the node it waits on: {"from_key": "B", "to_key": "A", "type": "blocks"} keeps B out of bd ready until A closes. Non-blocking links ride the same list as "type": "related".
touches — the file-overlap path list
touches is the list of paths a bead is expected to change, in the node's metadata. The summoner reads it at dispatch: two ready beads whose lists overlap are never worked side by side, and a bead without it is never held, so it can collide with a bead editing the same file. It is required on every node; the judgment belongs here, at filing, where the design is in front of the author.
Shape. A JSON array of strings. Paths are repo-relative — no leading ./, no absolute paths, no globs. A directory ends in a trailing slash (internal/djinn/drainstate/) and covers everything beneath it; anything else is one file (cmd/summoner/spawner.go):
"metadata": {"touches": ["cmd/summoner/", "internal/djinn/drainstate/"]}What to list. The paths the work will edit, at the coarsest honest granularity: the directory when the bead owns the package, the file when it touches one corner of a shared one. When unsure, take the directory: a lost parallel slot is cheaper than a lost worktree. Never emit an empty array: "this bead overlaps nothing" is a claim no bead that changes the repo can make.
Never CONTEXT_MAP.md. Workers never edit the root CONTEXT_MAP.md (work contract §7): the gardener seat maintains it, and a landing that changes it is deferred. A touches entry equal to CONTEXT_MAP.md — the exact root path, nothing else — plans a change no worker may make. In args mode, return a structured error naming the entry and contract §7 (route the map change to the gardener seat as a duty) and build no node. In interactive mode, drop the entry, say why, and file the rest. Only the exact entry triggers this: docs/CONTEXT_MAP.md is an ordinary file, and a description that mentions CONTEXT_MAP.md is no reason to refuse.
Never .djinn/seats/. Workers never edit a seat's files (work contract §7; docs/adr/2026-09-28-seat-run-commits-seat-files.md): only djinn seat run writes them, and a landing that changes one is deferred. A touches entry equal to .djinn/seats/ or beginning with it plans a change no worker may make; its owning seat is the path segment after .djinn/seats/ (.djinn/seats/lead/duties.toml belongs to lead). In args mode, return a structured error naming the entry, the owning seat and contract §7, citing that ADR, and build no node; name every such entry when there are several. In interactive mode, drop the entry, say why, and file the rest. Route the change to that seat as a duty, or to the lead for an attended landing on main; for a shipped seat the drain path is its template under internal/djinn/seat/templates/ — list internal/djinn/seat/templates/<seat>/ in touches, and the change reaches .djinn/seats/<seat>/ through djinn init --resync-seats. Only a touches entry with that exact prefix triggers this: .djinn/seatsx/, docs/.djinn/seats/x, .djinn/lead/file.md, internal/djinn/seat/templates/lead/duties.toml and a .djinn/ directory entry are ordinary paths, and a description that mentions a seat file is no reason to refuse.
ac_labels — a name per criterion
ac_labels names each acceptance criterion in a few words. The ops room's bead detail lists a bead's criteria in a narrow column, one row each, with a status tick; a criterion's own text is a sentence that does not fit there. The label is that row. The acceptance text itself is untouched: the AC gate, the evidence writer and verify-acceptance read only acceptance_criteria.
Shape. A JSON array of strings under metadata.ac_labels, aligned by position with the non-blank acceptance_criteria lines: label i names line i, and the array is exactly as long as the non-blank line count.
Who writes it. Args mode takes an optional ac_labels list. Absent, the skill authors one label per criterion, in args and interactive mode alike, so every node carries the key. Author the labels from the acceptance text the gate approved, after the gate: a tightening pass rewrites lines. Passed labels keep their lines when the gate left the line count unchanged; when tightening changed the count, author the whole list again.
The authoring rule. Each label is a noun phrase of 2 to 6 words naming the criterion's outcome — what is true when the line holds, not how it is checked — at most 32 cells, distinct within the bead. It opens on the thing the line is about (a noun), never on a verb or participle: Narrow ruling line cut short, not Cut to width, never wrapped. It contains none of Given, →, pinned, verified, a test name (a word beginning with a capital Test, such as TestDetailLogDigest), a path (no /) or a file:line. Good: Header: state, id, priority, round; 401 on expired tokens; Fast suite and audit green. Not Given a bead → the row reads x (a sentence), not TestDetailLogDigest passes (how it is checked), not ops/cockpit.go reads labels (a path).
Wrong length. In args mode, an ac_labels list whose length differs from the non-blank acceptance line count returns a structured error naming both counts (ac_labels has 3 labels; acceptance has 4 non-blank lines) and builds no node: guessing which label belongs to which line would put a wrong name on a criterion. The error proposes no label and no corrected list, not even as a hint: the invoker re-sends a list of the right length, or omits ac_labels. In interactive mode, say both counts and author the list again.
short_name — the bead's name in narrow columns
short_name is the bead's name wherever a whole title does not fit: the ops room lists beads in narrow columns, and a title cut mid-word reads as noise. The room shows the one this skill wrote; the title itself is untouched.
Shape. A JSON string under metadata.short_name: at most 28 cells, made of whole words of plain prose, with no …, no trailing punctuation (no ., ,, :, ;, - or — at the end), no line break and no leading or trailing space. A cell is one terminal column: each ASCII character is one cell, a wide character (CJK, emoji) two. Every node carries the key.
Who writes it. Args mode takes an optional short_name string. A passed value is checked against the rules here and, when it holds, carried unchanged — never shortened, reworded or "improved". Absent, the skill authors one from the title, in args and interactive mode alike.
The authoring rule. When the whole title fits in 28 cells, the title is the name (minus any trailing punctuation). Otherwise name what the bead is about in a noun phrase of whole words that fits: the title's leading words, cut only at a space, when they read as a phrase on their own; a fresh phrase from the title's own words when they do not. Never end on a dangling article, preposition or conjunction (a, the, with, and), never cut inside a word, never mark the cut with …. Good: JWT validation middleware for "Add JWT validation middleware" (29 cells); Short names at filing for "Every bead files with a short name the skill writes". Not Every bead files with a (dangling article), not Every bead files with a sho (cut inside a word), not Every bead files with a… (an ellipsis).
A bad passed value. In args mode, refuse a passed short_name that is empty, is over 28 cells, carries … or trailing punctuation, or is a prefix of the title that ends inside one of the title's words (the title's next character after the prefix is a letter or digit, not a space). Return a structured error naming the value and its fault — its cell count and the 28-cell limit, or the word it cuts ("Every bead files with a short na" cuts the title's word "name") — and build no node. Name every fault the value has. The error proposes no replacement name, not even as a hint: the invoker re-sends a valid name, or omits short_name. A value of at most 28 cells that ends at a word boundary of the title is valid even when the title's next word would not fit: carry it unchanged. In interactive mode, say what is wrong and author the name again.
agent — who does the agent work
agent names the adapter, model and effort that run a bead's agent steps, on whatever formula the bead runs; it beats the formula's own per-step pins. It is the operator's call: set it only when the operator named the agent. Operators say it as agent: <adapter> <model> <effort> — e.g. agent: codex gpt-6-astra high.
Bead-wide form — overrides every agent step:
"metadata": {"agent": {"adapter": "codex", "model": "gpt-6-astra", "effort": "high"}}Per-step form — agent.steps.<step-id> overrides one step and beats the bead-wide form on that step:
"metadata": {"agent": {"adapter": "codex", "model": "gpt-6-astra", "effort": "high",
"steps": {"review": {"adapter": "claude-code", "model": "opus[1m]", "effort": "high"}}}}steps may appear without the bead-wide keys; unnamed steps then keep the formula's pins. Every selection names all three knobs. Valid values are the adapter's own: claude-code models opus[1m], opus, sonnet[1m], sonnet, haiku, fable and the full claude-* ids, efforts low|medium|high|xhigh|max; codex models gpt-6-astra, gpt-6-sol, gpt-6-luna, efforts none|minimal|low|medium|high|xhigh|max. A step id names an agent step of the bead's formula (default: implement, fix-review, review, fix-regression; crucible: try, review, fix-regression). An invalid override defers the bead needs-operator before any step runs.
crucible — a bead that moves a number
Use it when the operator wants a number a script can measure to get better — slow tests faster, a binary smaller — rather than a done-state. Label the bead formula:crucible: an agent makes one change per try, the harness measures it, keeps a real gain, resets the rest, and stops at the target, after flat tries in a row are not kept, or at the dollar cap. The kept chain then gets one review pass whose findings are filed as follow-up beads, then the regression loop; if anything committed past the last kept try, the harness re-measures the final tree and lands it only if it still beats the baseline by more than the baseline spread.
The interview. Ask the operator only these four, in order:
- What number? — what is being measured, in their words ("slow-tier wall time").
- Which direction? —
lowerorhigheris better. - Target — the value that means done, or
none. - Budget — the dollar cap on the tries. Required: nothing else bounds the loop. The steps after the loop add to the run's spend.
Draft the rest from the repo, then show the whole spec back for confirmation:
measure— the command whose LAST stdout line is the number. Find an existing script and run it once to confirm the last line parses as a number. When none exists, file a blocker bead that writes it, and block this bead on it.runs— how many times each measurement runs; the score is the median. Default3.protect— every path a try could edit to move the number without improving the thing: the measure script, the fixtures it reads, the guard's config. An entry may be a glob (**crosses directories):**/*_test.gomakes every test file read-only. A glob covers its whole match set — adding or deleting a matching file is a violation too.guard— the command every try must keep green, e.g.go test -short ./....touches— the code the improvement may change. The guard enforces it as an allowlist: a try whose diff changes any path outside it is rejected unmeasured. No literalprotectpath may sit inside it; a protect glob may match inside it.stop.flat— consecutive non-kept tries before giving up. Default5.
Shape. crucible {measure, better, runs, protect, guard, stop {target, flat, usd}} in metadata, beside touches. measure and stop.usd are required: a bead missing either defers needs-operator at claim. Omit stop.target for no target.
Acceptance criteria name the number and its record, e.g. The kept chain lands with the slow-tier median at or below 120 s, with one row per try in .djinn/crucible/<bead-id>.tsv on the branch and the bead's djinn-crucible summary comment reading baseline and best.
Worked example — "make the slow tier faster, under 120 seconds, $25":
"labels": ["formula:crucible"],
"metadata": {"touches": ["internal/", "cmd/"],
"crucible": {"measure": "scripts/bench-slow.sh", "better": "lower", "runs": 3,
"protect": ["scripts/bench-slow.sh", "**/*_test.go"],
"guard": "go test -short ./...",
"stop": {"target": 120, "flat": 5, "usd": 25}}}Field mapping — bead field to plan key
A plan is {"commit_message": ..., "nodes": [...], "edges": [...]}. bd drops an unknown key with only a warning — acceptance is NOT a plan key, and a node carrying it files a bead with no acceptance criteria.
| Bead field (args key) | Plan key | Shape |
| title | title | string |
| type | type | epic / feature / task / chore / bug |
| priority | priority | integer 0-4 |
| description | description | string (markdown) |
| design | design | string (markdown); omit when absent |
| acceptance | acceptance_criteria | string, one criterion per line |
| notes | notes | string (markdown); omit when absent |
| labels | labels | array of strings |
| touches | metadata.touches | "metadata": {"touches": [...]}; always present, never empty |
| ac_labels | metadata.ac_labels | beside touches; always present, one string per non-blank acceptance_criteria line |
| short_name | metadata.short_name | beside touches and ac_labels; always present |
| agent | metadata.agent | beside touches; omit when absent |
| crucible | metadata.crucible | beside touches; only with the formula:crucible label |
| key | key | plan-local node key, unique in the plan |
| parent in the same plan | parent_key | the parent node's key |
| parent that already exists | parent_id | the existing bead id |
| blocked by a node in the same plan | edge from_key / to_key | {"from_key": <blocked>, "to_key": <blocker>, "type": "blocks"} |
| blocked by an existing bead | edge from_key / to_id | {"from_key": <blocked>, "to_id": <blocker id>, "type": "blocks"} |
A lone bead is a one-node plan:
{
"commit_message": "file: Expired sessions sign the user out cleanly",
"nodes": [
{
"key": "bead",
"title": "Expired sessions sign the user out cleanly",
"type": "feature",
"priority": 2,
"description": "**What is wanted:** ...",
"acceptance_criteria": "Given an expired token on any protected route → 401 with error code session_expired\nGiven a 401 session_expired in the app → the sign-in page opens with the user's unsaved draft still in the editor after sign-in",
"design": "Tokens are issued in internal/auth/; the app's request layer is web/src/lib/api/.",
"notes": "Source: docs/auth-design.md §Expiry",
"labels": ["auth"],
"metadata": {"touches": ["internal/auth/", "web/src/lib/api/"],
"ac_labels": ["401 on an expired session", "Draft kept across sign-in"],
"short_name": "Clean session expiry"},
"parent_id": "djinn-auth1"
}
],
"edges": [
{"from_key": "bead", "to_id": "djinn-abc12", "type": "blocks"}
]
}Internal AC-quality gate — iteration budget
Before filing, run /ac-quality-review on each node's drafted acceptance_criteria, passing the draft text, its description and its type. This is the only place the gate runs: the bead-work contract (§2) settles acceptance-criteria quality at filing.
<!-- djinn-dispatch: codex -->Invoking the gate: Codex has no slash command for a skill. Wherever this skill says to invoke /ac-quality-review, run the $ac-quality-review skill (.agents/skills/ac-quality-review/SKILL.md, else ~/.agents/skills/ac-quality-review/SKILL.md) in this session on the draft, and read its verdict block before going on.
<!-- djinn-dispatch: end -->A CONCRETE verdict does not end this skill. /ac-quality-review returns a verdict and hands control back. In the same turn, continue to the bd create --graph call (Filing) and report the new bead id(s) — or, without --execute, return the gate-approved plan. A run that stops at the verdict has filed nothing.
Iteration protocol — a 2-iteration tightening budget, so at most 3 review passes per node:
- Invoke
/ac-quality-reviewwith the draft. - CONCRETE with an empty
operator_required[]→ the node is approved. - UNDERSPEC, or a non-empty
operator_required[]→ apply eachfindings[i].rewrite_hintto its line, reword eachoperator_required[i].criterion(below), keep unflagged lines in order, and invoke the gate again. At most twice. - Still UNDERSPEC on the third pass → HALT. Do NOT emit
bd create. Return the third pass'sfindings[]as an actionable error naming each line'sminimum_outputandrewrite_hint: the intent is not clear enough yet to state as outcomes, and a person decides whether to restate the ask.
operator_required[] is the second half of the gate. A non-empty operator_required[] is handled exactly like UNDERSPEC: reword the flagged line so the unattended harness can satisfy it — drop the interactive step, replace an external action with the observable it would produce, or split the human part into its own separately filed bead — inside the SAME 2-iteration tightening budget. If operator_required[] is still non-empty on the third pass, HALT: do NOT emit bd create. Surface its entries the same way, naming each manual_step. Filing time is where this is decided because no drain step re-checks an operator-required criterion: a bead filed with one spends implementation iterations on work no unattended agent can finish.
One HALT holds the whole plan: file nothing until every node is approved.
Drafting in a helper
A caller that must keep its own context lean — the lead on a long source document, a seat — may hand the whole ask to the djinn-planner agent djinn init writes. The helper runs this skill without --execute and returns only the gate-approved plan or {HALT, findings[]}; it never files, and the caller files the plan it returns. When the agent file is missing, run djinn init --resync (in a home realm, djinn skills install --global) first.
<!-- djinn-dispatch: codex -->Starting the helper: a fresh codex exec run is Codex's context-isolated helper. Write the prompt to a file — its first line Act as the djinn-planner agent: read .codex/agents/djinn-planner.toml (else ~/.codex/agents/djinn-planner.toml) and follow its developer_instructions as your own., then the ask verbatim — and read the plan from last-message.md:
OUT=$(mktemp -d)
MODEL=$(djinn formula describe --embedded iter-codex --json | jq -r '.steps[] | select(.id == "implement") | .model')
codex exec --skip-git-repo-check -C "$(git rev-parse --show-toplevel)" -m "$MODEL" \
-c model_reasoning_effort=high -o "$OUT/last-message.md" - < "$OUT/prompt.txt" > "$OUT/stdout.log" 2> "$OUT/stderr.log"codex exec needs the network and bd its database: run it outside this session's sandbox. A run that exits non-zero or leaves last-message.md empty returned nothing: re-dispatch once, then report it as failed.
<!-- djinn-dispatch: end -->Filing — one bd create --graph call
Filing runs only after every node is approved by the gate and the bd 1.3.0 preflight passed.
- Resolve every existing id. For each
parentandblocked_byid, runbd show <id> --json. Stop if any is missing: bd accepts aparent_idorto_idthat names no bead, and a dangling blocker edge leaves the bead ready. - Stage every field in a per-invocation scratch dir and assemble the plan with
jq --rawfile(the quoting gotcha) — never hand-write multi-line markdown into a JSON string, and never retype an approved line. - Pre-filing check — only when a node carries the
proposedlabel (below). - Dry-run.
bd create --graph "$d/plan.json" --dry-run --json. Stop on any error or anyunknown field(s)warning. - File.
bd create --graph "$d/plan.json" --jsonreturns{"ids": {"<key>": "<new-id>"}}. Whole plan or nothing: on an error no bead was created, so fix the plan and re-run — never file the nodes one at a time. - Read back (Post-filing verification).
d="$(mktemp -d)"
cat > "$d/description.md" <<'EOF'
**What is wanted:** ...
EOF
cat > "$d/acceptance.txt" <<'EOF'
Given an expired token on any protected route → 401 with error code session_expired
EOF
jq -n --rawfile description "$d/description.md" --rawfile acceptance "$d/acceptance.txt" \
'def t: rtrimstr("\n");
{commit_message: "file: Expired sessions sign the user out cleanly",
nodes: [{key: "bead", title: "Expired sessions sign the user out cleanly", type: "feature", priority: 2,
description: ($description|t), acceptance_criteria: ($acceptance|t), labels: ["auth"],
metadata: {touches: ["internal/auth/", "web/src/lib/api/"],
ac_labels: ["401 on an expired session"], short_name: "Clean session expiry"},
parent_id: "djinn-auth1"}],
edges: [{from_key: "bead", to_id: "djinn-abc12", type: "blocks"}]}' > "$d/plan.json"
bd create --graph "$d/plan.json" --dry-run --json # stop on any error or unknown-field warning
bd create --graph "$d/plan.json" --json # → {"ids": {"bead": "<new-id>"}}Pre-filing check — proposed plans only
When any node's labels hold proposed (seat-originated work), ask a judge of the other model family whether the plan is worth building before it is filed, so its objections arrive while fixing them is still an edit. With no proposed label, skip it and record nothing. It runs once per plan and never blocks filing.
<!-- djinn-dispatch: codex -->The judge is fable: pass --only fable.
<!-- djinn-dispatch: end -->djinn triage preview --plan "$d/plan.json" --only <judge> --json > "$d/prefile.json"
# → [{"key": "<top-level key>", "votes": [{"vote": "go|no-go|abstain", "hard_rule": null|"<name>", "reason": "<one sentence>"}]}]Each top-level node (no parent_key) gets one vote; append one prefile: line to that node's notes, then take one branch:
no-go, or a non-nullhard_rule— fix each finding you agree with; a changed node goes back through the gate. For each finding you reject, appendprefile rejected: <finding> — <why>. Then appendprefile: <judge> <vote>[ [<hard-rule>]] — <reason>; fixed <n>, rejected <m>. Do not run the preview again.go— appendprefile: <judge> go — <reason>.abstain, or a non-zero exit — appendprefile: unavailable — <reason>.
Rebuild plan.json from the staged files so the prefile: lines reach the plan, then continue to the dry-run.
Gotcha: multiline quoting
Write each multi-line field to a file with a quoted heredoc (<<'EOF') and read it into the plan with jq --rawfile, which does all JSON escaping. Heredoc-in-$(...) breaks on [[ patterns and apostrophes, and hand-escaped JSON breaks on the first stray quote. Stage under mktemp -d, never fixed /tmp/bead-* names: concurrent invocations would clobber each other's fields.
Output
Args mode
- Without
--execute: file nothing. Return the gate-approved plan as one fencedjsonblock,{"nodes": [...], "edges": [...]}— each node exactly as the Field mapping builds it (key, every present field,acceptance_criteriaas the gate approved it,metadata.touches,metadata.ac_labelsandmetadata.short_namealways), and an edge for everyblocked_byid and every boundary edge. Most asks return one node and no edges besides its blockers. - With
--execute: file the plan (Filing), run the post-filing verification, and return{bead_ids, short_names}. - On HALT or a structured error: one fenced
jsonblock with the error (the third pass'sfindings[], the missing keys, or the refused value and its fault); build no node and file nothing.
Interactive mode
Show the assembled plan for review. When the ask split into more than one node, show each node's title and the boundary that split it, and wait for a yes: the plan's shape is the operator's call. File it, run the post-filing verification, then report each new bead id with its title.
Post-filing verification — every returned id
For each id in the returned ids map, run bd show <id> --json and confirm on .[0]:
acceptance_criteriais non-empty and equals the gate-approved text.metadata.touchesequals the drafted list.metadata.ac_labelsequals the drafted labels, as many as the non-blankacceptance_criterialines —bd show <id> --json | jq '.[0].metadata.ac_labels'.metadata.short_nameequals the drafted name: present, at most 28 cells, whole words —bd show <id> --json | jq '.[0].metadata.short_name'.metadata.agentandmetadata.crucibleequal the drafted values, or are absent when none was drafted.parentequals the drafted parent, anddependenciesholds adependency_type: "blocks"entry for every drafted blocker.
Then run bd ready --exclude-type=epic: a node that waits on another must not be listed. Fix a wrong field with bd update <id>; an acceptance_criteria repair passes /ac-quality-review before it is written.
Failure-mode handling
- bd below 1.3.0 — stop before any filing (Preflight).
- Missing required key — args mode: a structured error naming the keys; interactive mode: ask.
ac_labelsof the wrong length, a badshort_name, atouchesentry that isCONTEXT_MAP.mdor under.djinn/seats/— args mode: the structured error each section names; build no node.- Parent epic unclear — run
bd list --type=epic --status=open --no-parent; ask which one owns the ask, or file a new epic node in the same plan with the bead'sparent_keypointing at it. - Existing id not found — stop filing; ask, or return the error.
/ac-quality-revieworbdunavailable — surface the error. Never file without the gate.
Universal constraints
- Read-only on existing beads. Tightening happens on the draft, before filing.
- No silent fields. An optional field the invoker omits stays omitted; never invent design notes, priorities or paths.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

