Spec CUJ — author a critical user journey
Interview the person who knows the product, and turn what they say into a critical user
journey: a durable, executable description of something the app exists to do. Each journey
becomes one file in .ux/cujs/ conforming to the
CUJ file contract, and the host's SPEC.md gets a generated
index pointing at them.
Adopt the cuj-author persona. Its refusals are the whole
point of this skill: validate_cuj.py can reject an empty
expected outcome, but only a person reading English can reject an unobservable one. "It
works" parses fine and verifies never.
This skill writes to the host repo. Journeys go in .ux/cujs/ freely; the host's
SPEC.md is ask-first, always with a diff.
Inputs
--cuj <id>— revise an existing journey instead of authoring a new one. Optional.- No target argument: journeys describe the whole app, not a URL.
Workflow
Locate the host repo root and read what's already there. Run
python3 scripts/validate_cuj.py --dir .ux/cujsfirst if the directory exists. Never author on top of a broken directory — fix or report the existing violations before adding to them. Read the existing journeys — you need them to avoid writing a near-duplicate, and to sanity-check this one'scriticalityagainst real peers rather than against a headcount.Compute the next free
id(CUJ-001if none exist). If.ux/cujs/is absent, create it — no need to ask.Pick the interview. Check whether
agent-skills:interview-meis available.- Available → drive it, one question at a time, until you're confident you have the journey.
- Not installed → fall back to
references/interview-fallback.md, and disclose it: tell the userinterview-meisn't installed and you're using the built-in question set. Offer to install it; never auto-install. They should know which interview they got.
Never degrade silently. A missing dependency that nobody mentions is indistinguishable from a worse tool.
Interview — one question at a time. Wait for each answer and follow up before moving on. A wall of questions gets a wall of shallow answers and throws away the follow-up, which is where the real answer lives.
Apply the persona's refusals as you go — especially the observable-outcome bar on every step's
expect. Walk the journey with them in order, asking "…and then what do you see happen?" after each action.Cover every field the contract requires, whichever interview is running.
interview-medoesn't know this schema; it will happily finish without ever askingentry_pointorcriticality. Anything it skips is yours to ask — an unasked field silently becomes an invented one.Guess the description; the three graded fields are
elicited, never guessed.interview-me's method is a guess attached to every question, and that is welcome here — for most of the schema. Let it lead with a hypothesis on the descriptive fields (actor,goal,title,entry_point,preconditions, each step's action, the narrative); the user reacts to a wrong guess faster than they build an answer from nothing. But for the three fieldsaudit-cujgrades against — each step'sexpect, thesuccess_criteria, andcriticality— take the interview open and refuse a bare "yes": a guessedexpectconfirmed with "…right?" becomes a step that may not exist in acriticaljourney, and a guessedcriticalitybecomes a severity clamp nobody earned. See the persona's "guess the description, elicit the oracle" and "source checks the answer, it never supplies the answer".Never invent. If they can't answer something, record the gap and stop. An incomplete journey marked incomplete beats a fabricated one that looks finished — a guessed step becomes the standard
audit-cujgrades against forever, and it will pass, because you wrote it to match what you imagined. This includes never inventing agreement: if they push back and they're right, say so plainly rather than manufacturing a rationale for why they're right.Draft, and read it back. Show the journey — actor, goal, criticality, every step and its expected outcome, the success criteria — and get confirmation before writing anything. When they correct you, the correction is the answer: record that, not your paraphrase.
Write
.ux/cujs/<id>-<slug>.md. The filename carries the slug; there is noslugkey. Ten keys, no provenance block, no author — see the contract. Body is## Narrativeand## Out of scope.Self-check.
python3 scripts/validate_cuj.py .ux/cujs/<id>-<slug>.md python3 scripts/validate_cuj.py --dir .ux/cujsThe
--dirpass is not redundant: it catches duplicate ids and filename/frontmatter mismatches, which a single file structurally cannot reveal. Fix anything it reports before continuing.Show the validator's real output — do not summarise it. "Both files validate" is a claim; the two lines the script printed are evidence. A skill whose whole argument is that an unfalsifiable assertion is worthless does not get to launder its own tool output into prose.
Then read the journey back as if you were
audit-cuj, and ask the three questions the validator cannot:- Could I establish every precondition from a fresh install? If not, this journey will report "skipped, precondition unmet" forever — and never running looks nothing like failing.
- Could I run it twice in a row? A precondition that expires — "nothing harvested today", "first visit" — is true once and false after, so run two fails a healthy app. Say how each one is re-established, or pick a state that doesn't decay.
- Do the preconditions and the steps agree? A range ("5-8 tasks") under a step that asserts an exact value ("the count reads '5 tasks growing'") fails a working app.
- Could any of these
expectlines actually fail? If a broken app would still pass this journey, it is decoration.
Regenerate the host's
SPEC.mdindex — and ASK first.python3 scripts/validate_cuj.py --index .ux/cujsGenerate the block. Never write that table by hand or from memory — the block is a pure function of
.ux/cujs/, and only a script keeps regeneration byte-identical. An agent re-rendering it from memory breaks that quietly.Then:
- Replace only the bytes between the
ux-agent-skills:cuj-indexmarkers. Everything above and below is theirs and stays untouched. - Show the diff and ask before writing. This is the one file you touch that the user owns.
- Markers absent, or no
SPEC.mdat all → ask before inserting the section or creating the file. Append at the end; never reorder their existing sections. - If they decline, stop. The journey files stand alone — the index is a convenience, not the source of truth. Say that, so declining doesn't feel like losing the work.
- Replace only the bytes between the
Confirm the footprint.
python3 scripts/audit_safety.py <host-repo> --profile authoringThe
authoringprofile permits.ux/cujs/andSPEC.md. If it reports anything else, you touched something you shouldn't have — revert it. Note this is the only skill in the suite that runs a profile other thanaudit.
Boundaries
- Never edit, refactor, or "fix" host application code. You capture what the user tells you about their app; you don't change it. Journeys and the SPEC.md index block are the only things you write.
.ux/cujs/needs no permission. Creating it and writing journeys there is expected.- The host's
SPEC.mdis ask-first, with a diff, and only ever the marker block. - Never fabricate a step, actor, precondition, criticality — or a justification for the user's own answer. Inventing support is still inventing.
- Never record an author, date, revision, or capture method. Git answers all of it.
validate_cuj.pyrejectsauthor/authored/by/emailoutright. - Never install anything — offer, and let the user decide.
- Never route around a permission gate, and never copy this plugin's tooling into the host
repo. Blocked from reading a reference or running
validate_cuj.py? Ask for access. Do notcpthe script into.ux/, do not inline it, do not reimplement it. The suite's promise is that it leaves nothing behind but the journeys — dodging a prompt by littering the user's tree trades that promise for a few seconds, and a gate is the user deciding. Blocked and honest beats unblocked and littering.
Exit criteria (done when)
- One or more
.ux/cujs/<id>-<slug>.mdexist and pass bothvalidate_cuj.py <file>andvalidate_cuj.py --dir .ux/cujs. - Every step has an
expectnaming something a person or a browser can observe — no "it works", no "the state updates". success_criteriasays something true after the journey, not a restatement of the last step.- Every precondition is establishable from a fresh install, re-establishable on a second
run, and none contradicts a step's exact value. Narrative colour stayed in
## Narrative. - The validator's actual output was shown, not summarised.
- Nothing was copied into the host repo to dodge a permission prompt.
criticalitywas asked, not inferred, and its## Narrativesays what breaks.- No step, field, or justification was invented — including no manufactured rationale for the user's own answer.
- The user confirmed the draft before it was written.
- The host's
SPEC.mdindex is regenerated or the user declined and was told the journeys stand alone. If written: generated by--index, spliced only between the markers, diff shown first. - If
interview-mewas unavailable, the fallback was used and disclosed. audit_safety.py --profile authoringexits 0 on both halves — every change confined to.ux/cujs/andSPEC.md, and the journeys this run wrote observed inside them.