| name | ux |
| position | start |
| description | Write a SLICE's UX lens as a grounding doc (ux.md) — the screens (each with a low-fidelity layout) that make the slice's functionalities visible, the states each holds, and the product's visual core (color + typography) — directly in place on the live model. The START of the FUNCTIONAL realize pipe (ux → agentic → marketing), run on a shaped slice. Accessibility is not here (it lives in the marketing lens); flows are the build's to derive. Reads the hub from the spine (functionality grounding + profile), never another lens. Writes only the slice's ux lens and its visual-core decision. |
| user-invocable | true |
ux
Write a shaped slice's UX lens as the grounding doc ux.md: the screens that make every
functionality visible, the states each holds, and the product's visual core. Just enough to
anchor the intended experience and let the build figure the rest. /ux reads the slice's hub
— its functionalities' grounding docs plus the profile (both from the spine) — and never another
realize lens.
Pipeline position: start. /ux OPENS the functional realize pipe (ux → agentic → marketing):
the D2 rule prepends start-change — resolve or create the slice-realize issue, cut the branch
off fresh main, optional worktree, init STM — so /agentic and /marketing run on this
already-started branch. No pipeline close sequence (no end PR) is injected here; the functional
pipe closes at /marketing. It writes the persistent product model directly, in place (the
slice's ux lens and the visual-core decision) on the started branch — there is no draft copy and
no apply/promote step; review is the branch git diff and the pipeline's end PR. (#437, #500, ADR 026)
Write discipline (ADR 026, standards/rules/direct-model-write.md). The LLM authoring skill
writes ONLY the per-node grounding doc ux.md straight to the live model; the one shared-file
mutation — the visual-core decisions/*.yaml (slice-level, skip-if-exists) — is done by the
deterministic keyed persist script, in place, keyed to the target slice so it cannot touch another
slice's decisions and never edits an accepted decision in place. The model tree is asserted clean
once start-change has cut the branch (F14) and the play commits its own model delta after approval
(C13), so the working-tree diff vs HEAD is exactly this run's delta. Containment is a post-write
scoped guard (scoped_write_guard.py), not a draft.
Compiled From
This play was compiled from the ux ICE (reference/ice.md) by play-editor (#466 Batch C; #467
Batch B — the checkpoint upgraded to a conditional learned gate, see
standards/rules/gate-config.md; #500 — migrated to direct-model-write per ADR 026 and
standards/rules/direct-model-write.md). Intent defines constraints (C1–C13) and failure
conditions (F1–F14); the expectation defines success scenarios (S1–S6), a Done means (D1–D3, baked
to stop-condition.yaml), and one recovery entry per failure condition. To modify this play,
update reference/ice.md and recompile with play-editor. Do NOT edit this file manually — it is a
compiled artifact.
Role
You are the orchestrator. You own the workflow and step order. You delegate the domain work —
authoring the UX lens grounding doc — to the product-os-keeper agent via a JSON contract over
files on disk, and you run the mechanical work (readiness/hub resolution, the shape linter, the
content-quality eval, grounding + coverage, KB grounding, the keyed in-place persist, and the
post-write scoped guard) through bundled scripts and an isolated judge. You never write the lens
yourself, and you never persist the shared decision or commit the model delta before the single
checkpoint (C11) resolves — a typed approval, a recorded config skip, or a recorded policy
auto-pass.
Forbidden: hand-writing the lens or a decision; writing anything other than this slice's
ux.md (by the skill) and its visual-core decision (by the keyed persist) (C2); reading or
grounding on another realize lens (C7); writing the visual-core decision by any route other than
scripts/persist_ux.py; committing the model delta before the Step 5 gate resolves; editing an
accepted decision in place (additive-only — F9); running the lens writes against a dirty
product-os tree (C13/F14); closing COMPLETED without the stop-condition verdict held (C12/F12).
Agent boundaries:
| Agent | Domain | Skill it invokes | Phases |
|---|
product-os-keeper | Author the slice's UX lens (screens + states + visual core) from the hub + KB pattern grounding — the ux.md written in place on the live model, and the visual-core decision emitted into the manifest | kb-search, author-ux-lens | Author |
product-os-keeper is the single domain agent this play uses (1 of the ≤5 budget). The
utility work — cutting the branch and resolving the issue — is start-change (injected head), not
a domain agent. The content-quality judge always runs as an isolated, clean-context sub-agent
(optionally on a configured different model) — never the orchestrator's own context.
Pre-flight
| Check | Constraint | Action on Failure |
|---|
Resolve config + product_base (.garura/core/config.yaml) | — | Hard halt |
Resolve grounding-eval.judge (optional model override) | C4 | Default: sub-agent on the session model |
Slice ready + hub resolves (check_ready_slice.py) | C1 | Hard halt (REC1) |
Clean model tree — after start-change (Step 0), git status --porcelain -- <product_base>product-os is empty | C13/F14 | Hard halt (REC14) |
Resolve the pre-flight facts mechanically with the bundled resolver:
python3 scripts/preflight.py --play ux --config .garura/core/config.yaml
Then resolve the slice and its hub from the spine — the readiness gate that every realize lens
shares:
python3 scripts/check_ready_slice.py --product-base <product_base> --slice <slice-id>
It asserts the profile is set (from the spine), resolves the slice record, and resolves every
functionality_ref through the spine to its functionality.md grounding doc — the hub. If the
slice is absent, a functionality does not resolve, or the profile is not firmed, hard halt
(C1/REC1).
The run's working root (<working> below) is {stm_base}_realize/ux/<slice>/ — the routing, the
ux manifest (ux-manifest.yaml), the change-shape, the persist record (persist-manifest.json),
and the captured scoped-guard report (guard-report.json) all live under it. These are STM,
non-model artifacts (ADR 008/017) — the model itself (the ux.md and the visual-core decision) is
written IN PLACE under <product_base>product-os/, never into <working>. The stop condition
evaluates against <working>. Status markers live at {stm_base}_realize/ux/status/.
Clean-tree assertion (C13/F14, ADR 026). start-change cuts the branch off fresh main, so the
product-os tree is clean by construction; still assert it right after Step 0 — HEAD is only a
correct base for the scoped guard and the change-shape if the tree is clean before the lens writes:
test -z "$(git status --porcelain -- <product_base>product-os)" || { echo "HALT: dirty product-os tree (REC14)"; exit 1; }
If dirty, halt and ask for a clean model tree (commit or revert the pending model edits) before
/ux writes the lens.
Right after the resolver, record the session identity stamp's start marker (#463 — soft-fail, never
a halt):
python3 scripts/session_stamp.py --phase start \
--marker "{stm_base}_realize/ux/status/session-stamp-ux.json" \
--cwd "$(pwd)" --branch "$(git branch --show-current)"
Resume check: if {stm_base}_realize/ux/status/<slice-id>.json exists, resume — skip
completed steps, reset any in-progress step to pending, continue.
Task DAG
Create ALL tasks immediately after resolving config — before any domain work.
The play owns this DAG; the agent must not edit its top-level tasks.
Write-then-review (ADR 026): the FULL model delta — the LLM's ux.md AND the keyed persist's
visual-core decision — is written to the live model BEFORE the checkpoint, so the guard, the
change-shape, and the human all see the real delta. Nothing is COMMITTED before the gate resolves;
cancel reverts the uncommitted writes.
[T0] start-change (injected — start, head) blockedBy: []
[T1] Author the lens (ux.md to live) blockedBy: [T0]
[T2] Validate the live doc blockedBy: [T1]
[T3] Persist (keyed, in place — decision) blockedBy: [T2]
[T4] Guard the full delta + classify the shape blockedBy: [T3]
[T5] Checkpoint (approval over the full git diff) blockedBy: [T4]
[T6] Commit the model delta blockedBy: [T5]
[T7] Scenario Validation blockedBy: [T6]
[T8] Close blockedBy: [T7]
Mark each task in-progress before its step and completed right after its eval passes.
No runtime reordering. On resume, skip completed and reset in-progress to pending.
Workflow
Phase: Start (injected — D2 position: start)
Step 0 — start-change · Owner: start-change (sub-play) · Depends on: pre-flight
Run the start-of-pipeline member as a sub-play, dispatched with parent_run_id so it emits only
its own C1 evidence and this play's close absorbs it. It resolves or creates the slice-realize
issue, cuts the branch off fresh main, sets up a worktree iff config calls for it, and initializes
STM. /agentic and /marketing run on this branch; /marketing closes it.
{
"play": "start-change",
"parent_run_id": "<this run id>",
"inputs": { "title": "<realize ux: the slice>" },
"outputs": { "result": "{stm_base}_realize/ux/start/start-change.json" }
}
start-change owns its own evals (issue anchored, branch off latest main, worktree per config, STM
initialized); they are not re-checked here. Immediately after it returns, run the clean-tree
assertion (pre-flight, C13/F14) so HEAD is a correct base for the guard and the change-shape.
SE-13 (F14/C13): the product-os tree was clean once start-change had cut the branch — the
assertion (git status --porcelain -- <product_base>product-os empty) passed before any lens
write; a dirty model tree halts (REC14), so the change-shape and the scoped guard reflect only
this run's delta.
Phase: Author (write the doc to the live model, ADR 026)
Step 1 — Author the lens (ux.md to live) · Owner: product-os-keeper · Depends on: Step 0
The agent invokes author-ux-lens to write the slice's ux.md (screens + states + visual core,
per the UX lens template) from the hub (the functionality grounding docs + the profile) and KB
pattern grounding. Per ADR 026 the skill writes the per-node doc straight to the live model in
place, and emits the visual-core decision (with the grounding map) as structured data in
ux-manifest.yaml — it writes NO shared model file (_spine.yaml, the profile, or a
decisions/*.yaml):
{
"task": "author the slice's UX lens (screens/states/visual core) from its hub; write ux.md in place on the live model; ground screens to functionalities and the visual core to the KB or a decision; emit the visual-core decision delta into the manifest",
"inputs": { "slice_ref": "<domain>/<slice>",
"slice_file": "<slice record>",
"functionality_groundings": "<from check_ready_slice>",
"profile": "<spine profile>", "product_base": "<product_base>",
"lens_rel": "product-os/<domain>/slices/<slice>/lens/ux.md",
"manifest_path": "<working>/ux-manifest.yaml" },
"outputs": { "manifest": "<working>/ux-manifest.yaml" }
}
The skill reads the hub read-only and writes the ux.md IN PLACE under <product_base>product-os/.
It writes ux-manifest.yaml under <working> (STM) with the grounding map and the visual-core
decision_delta (omitted when it reuses an existing product decision). It writes NO shared model
file. It returns the contract with the output paths on disk — never inline content.
SE-1 (F1/C1): check_ready_slice.py passed at pre-flight — the slice is ready and its hub
resolves; an unready slice halted (REC1).
Step 2 — Validate the live doc · Owner: play · Depends on: Step 1
Run the guards over the LIVE ux.md this run wrote, before the checkpoint — shape first, then
content, then grounding. Under direct-model-write the doc is already written in place, and the
visual-core decision has NOT yet been written for this run (the keyed persist runs after Step 2):
python3 scripts/lint_grounding.py --doc <product_base>/product-os/<domain>/slices/<slice>/lens/ux.md
python3 scripts/validate_ux.py --manifest <working>/ux-manifest.yaml --slice-file <product_base>/<slice_file> --product-base <product_base>
python3 scripts/check_kb_grounding.py --manifest <working>/ux-manifest.yaml --kb-root <kb_root> --proposals-dir <working>/proposals
Then run the content-quality eval over ux.md: spawn an isolated, clean-context sub-agent
handed the judge prompt (standards/rules/grounding-eval.md), the doc (at its live path), and the
UX lens per-section guidance (it sees neither the brief nor the author's reasoning), on the model
from grounding-eval.judge.model. Gate the verdict:
python3 scripts/grounding_gate.py --verdict <verdict.json>
SE-2 (F3/C3): lint_grounding.py exits 0 — ux.md conforms to the UX lens template
(Intent/Screens/States/Visual core), no missing/extra/empty section.
SE-3 (F4/C4): the content-quality eval gate (grounding_gate.py) passes — ux.md is
self-explaining and clears the stranger test.
SE-4 (F5/C5): validate_ux.py — every screen in the manifest grounds to a functionality or a
persona/journey; the visual core grounds to the KB or a decision.
SE-5 (F6/C6): validate_ux.py — every functionality the slice bundles is visualized by at
least one screen (coverage).
SE-6 (F7/C7): validate_ux.py — no screen grounds on another realize lens.
SE-7 (F8/C8): validate_ux.py — the visual core names a decision (the manifest's
decision_delta or a reused product decision) that resolves; the keyed persist (Step 3) writes it.
SE-8 (F10/C10): check_kb_grounding.py exits 0 — the visual core, navigation, and responsive
choices trace to a KB learning or a recorded proposal.
On any GAP, apply the matching recovery (REC2–REC10) and re-run before the checkpoint — a
content-eval fail (SE-3) is REC4: rewrite the failing section to the judge's cited fixes and
re-judge until the gate passes.
Phase: Persist (write the full delta first, ADR 026 write-then-review)
Step 3 — Persist (keyed, in place — decision) · Owner: play · Depends on: Step 2
Write-then-review (ADR 026): the FULL model delta is written to the live model BEFORE the
checkpoint, so the guard, the change-shape, and the human all see the real delta. The ux.md is
already on the live model (Step 1). persist_ux.py now writes the SHARED file — the visual-core
decision — in place, keyed to --slice-ref: it reads the manifest's decision_delta and writes
the decision skip-if-exists under the target slice's decisions/ folder ONLY, and it REFUSES a
decision path outside that folder (the node-level containment the file-level guard cannot provide)
and never edits an accepted decision in place. It also confirms the live ux.md landed. No draft,
no doc copy. Nothing is COMMITTED yet — the commit (Step 6) happens only after the gate approves;
on cancel the whole delta is reverted (Step 5):
python3 scripts/persist_ux.py --ux-manifest <working>/ux-manifest.yaml \
--product-base <product_base> --slice-ref <domain>/<slice> \
--lens-rel product-os/<domain>/slices/<slice>/lens/ux.md \
--out-manifest <working>/persist-manifest.json
The persist record stamps applied: true (the live ux.md is confirmed and the decision handled)
— D1/D2's input.
Phase: Guard + Classify (over the full delta)
Step 4 — Guard the full delta + classify the shape · Owner: play · Depends on: Step 3
The run's write scope (the per-play guard policy, ADR 026). The old apply_ux.py encoded /ux's
write scope by construction — the lens re-derive (overwrite) and decisions skip-if-exists; under
direct-model-write that same scope is the scoped_write_guard.py policy. Resolve <slice-dir> as
<domain>/slices/<slice>:
--allow 'product-os/<slice-dir>/lens/ux.md' # the ux lens (re-derive; overwrite allowed)
--add-only 'product-os/<slice-dir>/decisions/*' # the visual-core decision (added, never modified)
Guard ONCE over the full delta (C9). After ALL writes (the LLM's ux.md from Step 1 and the
keyed persist's decision from Step 3), run the scoped guard a single time over the whole delta.
Capture its report — its ok field is the stop condition's D3 input (this replaces the old
before/after verify):
python3 scripts/scoped_write_guard.py --product-base <product_base> --base-ref HEAD \
--allow 'product-os/<slice-dir>/lens/ux.md' \
--add-only 'product-os/<slice-dir>/decisions/*' \
--out <working>/guard-report.json
If the guard exits non-zero (a path outside the slice's lens/decisions scope changed, or an
existing decision was overwritten), re-run with --restore to revert the offending paths, apply
REC9, and re-persist before the checkpoint.
Classify the full working-tree delta (C11). Classify the model tree's diff vs HEAD — now the
FULL delta (the ux.md + the visual-core decision), per ADR 026 write-then-review (no draft dir):
python3 scripts/classify_change.py --play ux \
--product-base <product_base> --base-ref HEAD --out <working>/shape.json
SE-9 (F2/F9/C2/C9): the scoped-write guard report reads ok: true — the model delta is
confined to the slice's ux.md (re-derive) and its decisions/* (add-only); the spine, the slice
record, the profile, and the other lenses are byte-unchanged, and no accepted decision was edited
in place.
Phase: Checkpoint (conditional gate, C11)
Step 5 — Human review (class: standard, conditional) · Owner: play · Depends on: Step 4
This is the single checkpoint (C11) — the agent never skips it on its own judgment. It is a
conditional gate (#467) per standards/rules/gate-config.md — /ux is one of the eleven
conditional document plays. Resolve it first match wins: pinned (n/a here) → gates.plays.ux →
the learned policy → gates.classes.standard → gates.default (absent ⇒ on). For the policy
lookup, use the shape key classified in Step 4.
Look the shape key up in the config-resolved policy (gates.conditional.policy, default
.garura/core/gate-policy.yaml): auto-pass iff the shape is in the policy's auto: block AND
not in never_auto: AND Step 2 + Step 4 stand with no blocking finding (a lint_grounding.py gap,
a grounding_gate.py content-eval fail, or a guard violation). On auto-pass, do NOT wait: record
gate auto-passed by learned policy (shape: <shape-key>, policy v<version>) as a Checkpoint
Decisions row, include the working-tree diff summary in the run record, append the crossing's
live-eval ledger line, and proceed to Step 6 (commit):
python3 scripts/gate_eval.py append --ledger <gates.conditional.ledger> --play ux \
--issue <issue> --shape <shape-key> --predicted auto --human auto_pass \
--policy-version <policy version> --ts <run ts>
Anything else resolves the gate on (an explicit gates.plays.ux: off instead records gate skipped by config (<resolution path>) as a Checkpoint Decisions row and proceeds). When on,
present the proposed screens (with layouts), states, and visual core, plus the decision, inline
over the real model git diff — render the approval prompt
(standards/templates/approval-prompt.md) and wait for the typed response. Approve → continue to
Step 6 (commit). Cancel → revert the working tree (ADR 026 step 6): the full delta is already
on disk, so run the guard with --restore and an EMPTY allow set to git restore the modified
model paths and git clean/remove the new ones (byte-clean back to HEAD), then halt — nothing was
committed, and cancel means "revert what was written" (the branch, issue, and STM start-change
created are its own committed side effects and are left as-is):
python3 scripts/scoped_write_guard.py --product-base <product_base> --base-ref HEAD \
--restore --out <working>/guard-report.json # empty --allow ⇒ every model path reverted
Then append the crossing's live-eval ledger line with the human's real action:
python3 scripts/gate_eval.py append --ledger <gates.conditional.ledger> --play ux \
--issue <issue> --shape <shape-key> --predicted gate \
--human <approved_clean|approved_edited|rejected> --ts <run ts>
<issue> is the slice-realize issue Step 0 resolved. <gates.conditional.ledger> /
<gates.conditional.policy> resolve from config gates.conditional (defaults
.garura/core/gate-evals.jsonl / .garura/core/gate-policy.yaml); <policy version> is the
policy file's version: field. <run ts> is the run's own UTC timestamp, derived the same way the
close derives ts (date -u), passed by the orchestrator.
SE-10 (F11/C11): the model delta was written to the live model by Steps 1 + 3 but is COMMITTED
(made durable) only at Step 6 on approval — so no product-model change is COMMITTED before the gate
resolves (a typed approval, a recorded config skip, or a recorded policy auto-pass); on cancel the
whole working-tree delta is reverted before any commit, so nothing is left on the tree.
SE-11 (F13): every crossing of this gate appended exactly one live-eval ledger line (shape,
predicted gate|auto, the human's real action or auto_pass), and an auto-pass fired only for a
shape the policy lists in auto: (and not in never_auto:) with no blocking finding standing.
Phase: Commit (make the delta durable, ADR 026 step 7)
Step 6 — Commit the model delta · Owner: play · Depends on: Step 5
The gate approved (or auto-passed / was skipped by config). Commit the full model delta on the
branch (C13, ADR 026 step 7) — a lightweight persist step that makes the writes durable and
advances HEAD so the next pipeline play (/agentic) enters a clean tree; it is NOT the pipeline end
sequence (no end PR — that closes at /marketing). A cancelled checkpoint never reaches this step —
its tree was already restored in Step 5:
git add -- <product_base>product-os
git commit -m "feat(model): ux lens <slice> — screens, states, visual core (#<issue>)"
SE-12 (F12/C12): the close is stop-condition gated — check_stop_condition.py over the baked
stop-condition.yaml (D1 the persist record persist-manifest.json exists; D2 it stamps
applied: true; D3 the captured guard-report.json reads ok: true) must read held before
any COMPLETED close, and the model delta is committed (C13); a run whose persist or guard did not
land closes HALTED, never COMPLETED (REC12).
Phase: Scenario Validation
Step 7 — Scenario evals · Owner: play · Depends on: Step 6
- SCE-1 (S1 — product designer):
ux.md is a valid UX Lens doc clearing the linter + the
content eval, written in place on the live model, and the spine/slice/profile/other lenses are
byte-identical (the scoped-guard report reads ok); the stop-condition verdict reads held.
- SCE-2 (S2 — product owner): every functionality the slice bundles maps to ≥1 screen.
- SCE-3 (S3 — ux researcher): every screen traces to a functionality or persona/journey; the
visual core to a decision that resolves.
- SCE-4 (S4 — architect): no other realize lens was read or written.
- SCE-5 (S5 — product owner, re-run): a re-run re-derives only
ux.md; everything else
byte-identical; no accepted decision edited in place (the keyed persist skipped the existing
decision).
- SCE-6 (S6 — reviewer): the checkpoint showed the screens, states, and visual core inline
over the real model git diff, and no product-model change was COMMITTED before approval — on
cancel the working tree returns byte-clean to HEAD — or, on the auto-pass path, the change shape
is policy-listed and a recorded auto-pass + live-eval ledger line + diff summary exist, with no
wait.
Phase: Evidence & Close
Step 8 — Close · Owner: play · Depends on: Step 7
Run the Standard Play Close. /ux opened a slice-realize issue via start-change, so it is
project-scoped — record evidence per the D1 rule.
# --- Standard Play Close (canonical; see standards/rules/play-close.md) ---
# Path tokens resolved at pre-flight (resolve here if not already):
# ltm_project_target = yq '.ltm.project-target' .garura/core/config.yaml
# evidence_base, slug:
# project-scoped play : evidence_base="${stm_base}${issue}/evidence/ux/" ; slug="#${issue}"
# product-scoped play : evidence_base="${product_base}_evidence/ux/" ; slug="${slice_slug}"
evidence_template=$(cat "${ltm_project_target}standards/templates/evidence-file.md")
delivery_template=$(cat "${ltm_project_target}standards/templates/delivery-report.md")
ts=$(date -u +%Y%m%d-%H%M%S)
evidence_dest="${evidence_base}${ts}.md"
mkdir -p "$(dirname "$evidence_dest")"
# Session identity stamp (#463) — close phase; start phase ran at pre-flight
session_stamp=$(python3 scripts/session_stamp.py --phase close \
--marker "${stm_base}_realize/ux/status/session-stamp-ux.json")
# Stop-condition gate (#464) — Step C0: this play carries a baked manifest, so the
# gate is LIVE. Evaluate the Done means against the run's working root as the
# close's authoritative input.
python3 scripts/check_stop_condition.py \
--manifest "<play-dir>/stop-condition.yaml" \
--base "${stm_base}_realize/ux/<slice>/" \
--out "${stm_base}_realize/ux/status/stop-condition-ux.yaml"
sc_exit=$? # 0 held · 1 unmet · 2 error
# Conditional-gate policy refresh (#467) — soft: a distill failure never blocks the close
python3 scripts/distill_gate_policy.py \
--ledger "$(yq '.gates.conditional.ledger' .garura/core/config.yaml)" \
--policy "$(yq '.gates.conditional.policy' .garura/core/config.yaml)" \
--streak "$(yq '.gates.conditional.streak' .garura/core/config.yaml)" \
--project "$(yq '.project.name' .garura/core/config.yaml)" || true
Step C0 — bind the verdict. sc_exit == 0 (held) permits status: COMPLETED.
Anything else closes HALTED with exit_reason: stop_condition_unmet and the evidence's
Stop Condition section names every unmet clause — fix the state per REC12 (re-run the keyed
persist, re-capture the scoped-write guard report, or make the model-delta commit) and
re-evaluate; the close stays HALTED until the verdict reads held. An unevaluable verdict is never
a pass.
/ux opened a slice-realize issue via start-change, so it is project-scoped:
evidence_base="${stm_base}${issue}/evidence/ux/" and slug="#${issue}".
Step C1 — Write evidence file. Gated by the resolved evidence.record flag. When false, skip
and record evidence skipped (record=false). Otherwise fill the evidence-file.md slots (play
ux, run_id ux-${ts}, slice slug, started/completed, status per C0, exit_reason; artifacts: the
slice's ux.md, the manifest, the visual-core decision, the persist manifest
(persist-manifest.json), the captured guard-report.json, the model-delta commit sha, the
stop-condition verdict; the content-eval verdict; step + scenario evals SE-1…SE-13 / SCE-1…SCE-6;
checkpoint decision from Step 5 (incl. any gate skipped by config or gate auto-passed by learned policy row) plus the gate ledger line(s) appended this run; the session identity stamp fields from
$session_stamp (#463): session_id, ledger_file, ledger_start_offset, ledger_end_offset (null when
unresolved — never blocks the close); and stop_condition per C0 with the Stop Condition section
filled) and write to $evidence_dest. Do NOT hand-author the body.
Step C2 — Render delivery report. Also render the Next line: resolve this play in
standards/rules/pipeline-next.md and emit **Next:** /<command> — <why>. Or run /next to see all recommended actions. (only /next pointer, or omit, when the mapped command is null), per
play-close.md. Fill the delivery-report.md slots: ## ux Delivered — ${slug}, the Run Summary
table (incl. the stop-condition verdict), the Pipeline Steps table, the Artifacts Produced table
(the ux lens + the visual-core decision + the model-delta commit sha), Next Steps (run /agentic on
this slice), and a pointer to $evidence_dest. Always emitted.
# --- end Standard Play Close ---
Scenario Validation
| Scenario | Persona | Eval |
|---|
| S1 — first run | product designer | SCE-1 |
| S2 — validates the shape | product owner | SCE-2 |
| S3 — grounded | ux researcher | SCE-3 |
| S4 — hub-only | architect | SCE-4 |
| S5 — re-run | product owner | SCE-5 |
| S6 — the checkpoint | reviewer | SCE-6 |
Recovery
| For | Trigger | Direction | Handoff |
|---|
| F1 | the slice is absent, a functionality does not resolve, or the profile is not firmed | halt and route to /shape or /understand before /ux runs | human |
| F2 | a write touched something beyond this slice's ux.md or its visual-core decision | the guard's --restore already reverted the out-of-scope write; re-run writing only the slice's ux.md and its decision | autonomous |
| F3 | ux.md fails the template/shape or carries out-of-scope content | re-emit to the UX lens template (Intent/Screens/States/Visual core only) | autonomous |
| F4 | ux.md fails the content-quality eval | rewrite the failing section to the judge's cited fixes and re-judge until the gate passes | autonomous |
| F5 | an invented/ungrounded element | drop it, or re-tie the screen to a functionality or persona/journey, and the visual core to a decision | autonomous |
| F6 | a functionality is covered by no screen | add the screen(s) that visualize the missing functionality | autonomous |
| F7 | /ux read or depended on another lens | remove the dependency; /ux derives only from the slice's hub | autonomous |
| F8 | the visual core was set with no decision | record the slice-level visual-core decision in the manifest (reuse the product one if it exists) before the keyed persist | autonomous |
| F9 | the scoped-write guard report is not ok — a non-lens/non-decision path changed, or an accepted decision was edited in place | the guard's --restore already reverted the offending paths; re-run writing only the slice's ux.md and its visual-core decision, after a human confirms the restore | human |
| F10 | a UX pattern choice with no KB learning and no recorded proposal | search the KB via kb-search and ground the choice, or raise a KB-learning-gap proposal | autonomous |
| F11 | the model delta was committed before the checkpoint resolved, or a cancelled checkpoint left writes on the working tree | revert the premature commit and the working-tree writes (guard --restore, empty allow set) and re-present the checkpoint; commit only after the gate resolves |
Pause and Resume
Steps run top to bottom. On entry, resolve config, resolve the target slice, check the status
marker, skip completed steps, reset any in-progress step to pending, and continue. A fresh start
with no marker runs everything and creates the marker at Step 1. Resuming a run that already wrote
the model doc enters a dirty tree; the pre-flight clean-tree assertion (F14) is scoped to a FRESH
start — a resume continues its own in-progress delta.
Compilation Metadata
| Field | Value |
|---|
| fingerprint | sha256:0d18d26db5ffd9253213d30716f46e3caac8652d25cf8ef743a1e0b91a1805a7 (of reference/ice.md) |
| compiled_by | play-editor (#500 direct-model-write, ADR 026); prior: play-editor (#467 Batch B, #466 Batch C) |
| pipeline_position | start (functional pipe head; the functional pipe closes at /marketing) |
| workflow_structure | A (single checkpoint — class: standard, conditional learned gate per gate-config.md #467; direct-model-write WRITE-THEN-REVIEW per ADR 026 — persist + guard + classify before the gate, commit after; stop-condition gated close) |
| stop_condition | stop-condition.yaml (D1–D3), gate live at Step C0 |
| domain_agents | 1 (product-os-keeper) |
| utility_agents | 0 |
| skills_used | kb-search, author-ux-lens |
| scripts | 12 (preflight, check_ready_slice, lint_grounding, grounding_gate, validate_ux, check_kb_grounding, persist_ux — keyed in-place persist, scoped_write_guard — post-write containment, check_stop_condition — Done-means gate, session_stamp — #463 identity stamp, classify_change + gate_eval + distill_gate_policy — #467 conditional gate) |
| step_evals | 13 (SE-1…SE-13) |
| scenario_evals | 6 (SCE-1…SCE-6) |
| recovery_entries | 14 (one per failure condition; 11 autonomous / 3 human) |
Recompiled note (#500, direct-model-write / ADR 026): migrated from draft-then-apply to
direct-model-write. The old draft model tree and the apply_ux.py/check_ux.py promotion+verify
scripts are removed; the authoring skill writes ux.md straight to the live model; the new keyed
persist_ux.py writes the visual-core decision in place (skip-if-exists, keyed to the slice);
containment is the post-write scoped_write_guard.py (its guard-report.json is D3);
classify_change.py reads the working-tree git diff (--product-base/--base-ref HEAD);
checkpoint cancel reverts the working tree via the guard --restore; the play asserts a clean
product-os tree at entry (F14) and commits its own feat(model) delta after approval (C13). Order
is write-then-review (ADR 026 "Order of operations"): the full delta — the LLM's ux.md AND
the keyed persist's decision — is written to the live model FIRST (Steps 1+3), then guarded ONCE
and classified over the full delta (Step 4), then the gate resolves over the real git diff
(Step 5), and only an approved gate COMMITS (Step 6). Nothing is COMMITTED before approval; cancel
reverts the uncommitted writes. See standards/rules/direct-model-write.md.
Recompiled note (#467 Batch B): checkpoint upgraded to a conditional learned gate; see
gate-config.md.
Direct-edit deviation note (#500) — INTENT CHANGE, HAND-COMPILED, CONVERGENCE UNVERIFIED:
This SKILL was updated to the direct-model-write write-then-review shape (ADR 026) by a
hand-compile from reference/ice.md, NOT by a /play-editor run. This is an intent change
(it alters the write path, the containment guarantee, the checkpoint cancel semantics, and the
step order), so the sanctioned path is recompile-via-/play-editor; play-editor is
interactive-only (fully gated, human-checkpoint) and cannot run headless in this environment, so
the compiled output was produced by hand to match what play-editor would emit from the current
reference/ice.md (fingerprint above). The compiled_by line names play-editor for provenance
intent, but no play-editor run actually occurred and convergence is UNVERIFIED. An interactive
/play-editor convergence run against reference/ice.md is REQUIRED — confirming the emitted
SKILL matches this hand-compiled body and refreshing the fingerprint. This migration mirrors the
merged /understand reference implementation (#498) and the just-migrated /vision, /grill, /learn
on this branch. Note also: reference/ice.md C13 distinguishes the model-delta commit from the
Standard Play Close (evidence + delivery report) that /ux still runs; the close anchor block is
retained (required by lint-components structural.js and the play-creator G12 emit on every play),
and the feat(model) commit is the separate lightweight persist step, so both coexist as C13
describes.