| name | marketing |
| position | end |
| description | Write a SLICE's marketing lens as a grounding doc (marketing.md) — how the slice is found and reached (SEO / AEO / GEO), the accessibility bar it meets, and the reach signals worth capturing — written directly in place on the live model. The END of the FUNCTIONAL realize pipe (ux → agentic → marketing): it closes the functional pipe (commit → propose → review → merge). An internal tool answers discoverability "not applicable", plainly. Reads the hub from the spine (functionality grounding + profile), never another lens. Writes only the slice's marketing lens. |
| user-invocable | true |
marketing
Write a shaped slice's marketing lens as the grounding doc marketing.md: how the slice is
found and reached, the accessibility bar it meets, and the reach signals worth capturing. The lens
is four things and only four — intent, discoverability (SEO / AEO / GEO), accessibility (moved here
from the profile), and marketing analytics. An internal tool behind auth answers discoverability
"not applicable", plainly and with the reason. /marketing reads the slice's hub — its
functionalities' grounding docs plus the profile (both from the spine) — and never another realize
lens.
Pipeline position: end. /marketing is the END of the functional realize pipe (ux → agentic →
marketing): it runs on the branch /ux already started, injects no start-change head, and — after
the lens is persisted and verified — injects the close sequence commit-change → propose-change → review-change → merge-change, committing the functional pipe's lenses, opening the PR, taking the
verdict, and merging to main. It writes the persistent product model directly, in place (the
slice's marketing lens) on the already-started branch — there is no draft copy and no apply/promote
step; review is the branch git diff and the injected end PR. (#437; 3-pipe realize 2026-06-26; #500,
ADR 026)
Write discipline (ADR 026, standards/rules/direct-model-write.md). The LLM authoring skill
writes ONLY the per-node lens doc (marketing.md) straight to the live model — a re-derive that
overwrites a prior marketing lens for this slice; every shared-file mutation (the slice's
decisions/) 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
(add-only). The authoring skill emits any material decision as structured data in its manifest; the
keyed persist writes the decision file. The model tree is asserted clean at entry (F15) and the play
commits its own model delta after approval (C14) — a feat(model) commit distinct from the injected
commit-change — so the working-tree diff vs HEAD is exactly this run's delta and the injected end
sequence carries a committed model change. Containment is a post-write scoped guard
(scoped_write_guard.py), not a draft.
Compiled From
This play was compiled from the marketing 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–C14) and failure conditions
(F1–F15); 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 marketing 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, you never write a shared model file (a decision) by any route other than
scripts/persist_marketing.py, and you never commit the model delta before the human approves the
single checkpoint (C12).
Forbidden: hand-writing the lens or a decision; writing anything other than this slice's
marketing.md and its decisions (C2); reading or grounding on another realize lens (C7);
manufacturing public-marketing reach where the functionalities and profile don't warrant it (C8);
deferring accessibility to the profile (C9); writing a decision by any route other than
scripts/persist_marketing.py; committing the model delta before the checkpoint gate resolves;
running against a dirty product-os tree (C14/F15); closing COMPLETED without the stop-condition
verdict held (C13).
Agent boundaries:
| Agent | Domain | Skill it invokes | Phases |
|---|
product-os-keeper | Author the slice's marketing lens (intent / discoverability / accessibility / analytics) from the hub + KB pattern grounding, written in place on the live model, and emit any decision into the manifest | kb-search, author-marketing-lens | Draft |
product-os-keeper is the single domain agent this play uses (1 of the ≤5 budget). 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 — git status --porcelain -- <product_base>product-os is empty | C14/F15 | Hard halt (REC15) |
Resolve the pre-flight facts mechanically with the bundled resolver:
python3 scripts/preflight.py --play marketing --config .garura/core/config.yaml
Then resolve the slice and its hub from the spine — the readiness gate 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).
Clean-tree assertion (C14/F15, ADR 026). /marketing runs on the branch the functional realize
pipe already started (ux → agentic → marketing); the prior lenses commit their own model deltas, so
the tree should be clean when /marketing enters. Assert it before any work — HEAD is only a correct
base for the scoped guard and the change-shape if the tree is clean at entry:
test -z "$(git status --porcelain -- <product_base>product-os)" || { echo "HALT: dirty product-os tree (REC15)"; exit 1; }
If dirty, halt at pre-flight and ask for a clean model tree (commit or revert the pending model
edits, or run the prior functional-pipe play to its persist) before /marketing proceeds.
The run's working root is <working> = {stm_base}_realize/marketing/ — the marketing manifest, the
change-shape, the persist record (persist-manifest.json), the captured scoped-guard report
(guard-report.json), and status markers all live under it. These are STM, non-model artifacts (ADR
008/017) — the model itself is written IN PLACE under <product_base>product-os/, never into
<working>. The stop condition evaluates against <working>.
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/marketing/status/session-stamp-marketing.json" \
--cwd "$(pwd)" --branch "$(git branch --show-current)"
Resume check: if {stm_base}_realize/marketing/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.
Write-then-review (ADR 026): the FULL model delta — the LLM lens doc AND the keyed persist's
decisions — 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.
[T1] Draft the lens (doc to live) blockedBy: []
[T2] Validate the live doc blockedBy: [T1]
[T3] Persist (keyed, in place — decisions) 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 (feat(model)) blockedBy: [T5]
[TE1] commit-change (injected — end #1) blockedBy: [T6]
[TE2] propose-change (injected — end #2) blockedBy: [TE1]
[TE3] review-change (injected — end #3) blockedBy: [TE2]
[TE4] merge-change (injected — end #4) blockedBy: [TE3]
[T7] Scenario Validation blockedBy: [TE4]
[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: Draft (write the doc to the live model, ADR 026)
Step 1 — Draft the lens (doc to live) · Owner: product-os-keeper · Depends on: pre-flight
The agent invokes author-marketing-lens to write the slice's marketing.md (intent /
discoverability / accessibility / marketing analytics, per the Marketing lens template) from the hub
(the functionality grounding docs + the profile) and KB pattern grounding — straight to the live
model (a re-derive, skip-nothing overwrite for this slice) — and to emit a marketing-manifest.yaml
(the grounding map plus any material decision as a full record). Per ADR 026 the skill writes NO
shared model file (_spine.yaml, the profile, the slice record, another lens, or a decisions/*.yaml
file) — the keyed persist (Step 3) writes any decision:
{
"task": "author the slice's marketing lens (intent/discoverability/accessibility/analytics) from its hub; ground the reach in the functionalities + profile and the patterns in the KB; write marketing.md in place on the live model and emit any decision 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/marketing.md",
"manifest_path": "<working>/marketing-manifest.yaml",
"proposals_dir": "<working>/proposals/" },
"outputs": { "manifest": "<working>/marketing-manifest.yaml" }
}
The skill reads the hub read-only and writes only the live marketing.md plus the STM manifest (any
decision as a full record whose node_ref is the slice) and any KB proposals. 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).
SE-14 (F15/C14): the product-os tree was clean at entry — the pre-flight assertion
(git status --porcelain -- <product_base>product-os empty) passed on a fresh start; a dirty model
tree halted at pre-flight (REC15), so the change-shape and the scoped guard reflect only this run's
delta.
Phase: Validate (over the live doc)
Step 2 — Validate the live doc · Owner: play · Depends on: Step 1
Run the guards over the LIVE marketing.md this run wrote, before the checkpoint — shape first, then
content, then grounding. Under direct-model-write the doc is already written in place; the linter and
the validators run over the live tree.
python3 scripts/lint_grounding.py --doc <product_base>/product-os/<domain>/slices/<slice>/lens/marketing.md
python3 scripts/validate_marketing.py --manifest <working>/marketing-manifest.yaml --slice-file <product_base>/<slice_file>
python3 scripts/check_kb_grounding.py --manifest <working>/marketing-manifest.yaml --kb-root <kb_root> --proposals-dir <working>/proposals
Then run the content-quality eval over marketing.md at its live path: spawn an isolated,
clean-context sub-agent handed the judge prompt (standards/rules/grounding-eval.md), the doc, and
the Marketing 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 — marketing.md conforms to the Marketing lens template
("Intent", "Discoverability", "Accessibility", "Marketing analytics"), no missing/extra/empty section.
SE-3 (F4/C4): the content-quality eval gate (grounding_gate.py) passes — marketing.md is
self-explaining and clears the stranger test.
SE-4 (F5/C5): validate_marketing.py — the reach assessment grounds in the slice's
functionalities and the profile; any material marketing choice names a decision emitted in the
manifest.
SE-5 (F6/C6): validate_marketing.py — the assessment considers every functionality the slice
bundles (coverage).
SE-6 (F7/C7): validate_marketing.py — the assessment grounds on no other realize lens.
SE-7 (F8/C8): validate_marketing.py — reach is honest: a slice with no public surface answers
discoverability "not applicable", and public-marketing reach is not manufactured.
SE-8 (F9/C9): validate_marketing.py — accessibility is set concretely in the lens (a bar + the
means), not deferred to the profile or omitted.
SE-9 (F11/C11): check_kb_grounding.py exits 0 — the discoverability/accessibility patterns trace
to a KB learning or a recorded proposal.
On any GAP, apply the matching recovery (REC3–REC11) 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 — decisions) · 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 lens is already on the live
model (Step 1). persist_marketing.py writes any material decision in place, keyed to the target
slice: it reads the manifest, writes each decision to the slice's decisions/ add-only (an id
already present is left byte-untouched, so an accepted decision is superseded, never edited in place),
and REFUSES any decision whose node_ref names another slice (this is the node-level containment the
file-level guard cannot see across slices). 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_marketing.py --marketing-manifest <working>/marketing-manifest.yaml \
--product-base <product_base> --slice-ref <domain>/<slice> \
--decisions-rel product-os/<domain>/slices/<slice>/decisions \
--out-manifest <working>/persist-manifest.json
The persist manifest stamps applied: true on a completed run (even when the slice carries no
decision) — this is the Done-means record the close's stop-condition gate reads (D1/D2).
SE-11 (F12/C12): 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; Step 6 is the sole committer and depends on the checkpoint.
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_marketing.py encoded
/marketing's write scope by construction — the slice's marketing.md (re-derive) and its decisions
(skip-if-exists); under direct-model-write that same scope is the scoped_write_guard.py policy,
keyed to the target slice so no other slice's lens or decision can slip through:
--allow 'product-os/*/slices/<slice>/lens/marketing.md' # the slice's lens (re-derive — the LLM overwrites it)
--add-only 'product-os/*/slices/<slice>/decisions/*.yaml' # the slice's decisions (added by the keyed persist, never modified)
Guard ONCE over the full delta (C10). After ALL writes (the LLM lens from Step 1 and the keyed
persist's decisions 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
check_marketing.py verify):
python3 scripts/scoped_write_guard.py --product-base <product_base> --base-ref HEAD \
--allow 'product-os/*/slices/<slice>/lens/marketing.md' \
--add-only 'product-os/*/slices/<slice>/decisions/*.yaml' \
--out <working>/guard-report.json
If the guard exits non-zero (a path outside the slice's write scope changed, or an accepted decision
was modified), re-run with --restore to revert the offending paths, apply REC2 (or REC10 for a
decision edited in place), and re-persist before the checkpoint.
Classify the full working-tree delta (C12). Classify the model tree's diff vs HEAD — now the FULL
delta (the lens + any decision), per ADR 026 write-then-review (no draft dir):
python3 scripts/classify_change.py --play marketing \
--product-base <product_base> --base-ref HEAD --out <working>/shape.json
SE-10 (F2/F10/C2/C10): the scoped-write guard report reads ok: true — the model delta is
confined to the slice's marketing.md (re-derived) and its decisions (added, never edited in place);
the spine, the slice record, the profile, the other lenses, and the other slices are byte-identical,
and no path outside the slice's write scope changed.
Phase: Checkpoint (conditional gate, C12)
Step 5 — Human review (class: standard, conditional) · Owner: play · Depends on: Step 4
This is the single checkpoint (C12) — the agent never skips it on its own judgment. It is a
conditional gate (#467) per standards/rules/gate-config.md — /marketing is one of the eleven
conditional document plays. Resolve it first match wins: pinned (n/a here) → gates.plays.marketing →
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 marketing --issue <issue> \
--shape <shape_key> --predicted auto --human auto_pass --policy-version <policy version> --ts <ts>
Anything else resolves the gate on (an explicit gates.plays.marketing: off instead records
gate skipped by config (<resolution path>) as a Checkpoint Decisions row and proceeds). When on,
present the proposed intent, discoverability, accessibility, and analytics inline over the real
model git diff, plus any decision — 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":
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 marketing --issue <issue> \
--shape <shape_key> --predicted gate --human <approved_clean|approved_edited|rejected> --ts <ts>
<issue> is the functional-pipe issue the run's branch carries (opened by /ux's start-change).
<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. <ts> is the run's own UTC timestamp, derived the same way the
close derives ts (date -u +%Y%m%d-%H%M%S), passed by the orchestrator.
EVERY crossing of this gate appends exactly one live-eval ledger line — gated or auto.
SE-13 (F14/C12): the crossing left exactly one live-eval ledger line, and any auto-pass fired
only on a shape the policy lists in auto: (and not never_auto:) with no blocking finding standing.
Phase: Commit (make the delta durable, ADR 026 step 7)
Step 6 — Commit the model delta (feat(model)) · Owner: play · Depends on: Step 5
The gate approved (or auto-passed / was skipped by config). Commit the full model delta on the branch
(C14, ADR 026 step 7) — a lightweight persist step that makes the writes durable and advances HEAD, so
the injected end sequence carries a committed model change. It is NOT the injected commit-change
(which then commits only what remains — STM evidence). 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): marketing lens for <slice-id> (#<issue>)"
SE-12 (F13/C13): 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 (C14); a run whose persist or guard did not land
closes HALTED, never COMPLETED (REC13).
Phase: End sequence (injected — D2 position: end)
After the lens is committed as a feat(model) delta (Step 6), the D2 rule injects the close sequence —
each a sub-play dispatched with parent_run_id, resolving its own context from the branch + config —
to commit whatever remains of the functional pipe, raise the PR, take the verdict, and merge to main.
The durable model change did not sit waiting on a manual /commit-change.
Step E1 — commit-change · blockedBy: Step 6 — commit any work still uncommitted after the
feat(model) model-delta commit (STM evidence, other lenses' residue); the marketing lens and any
decision were already committed in Step 6.
{ "play": "commit-change", "parent_run_id": "<this run id>", "inputs": {}, "outputs": { "result": "{stm_base}_realize/marketing/end/commit-change.json" } }
Step E2 — propose-change · blockedBy: E1
{ "play": "propose-change", "parent_run_id": "<this run id>", "inputs": {}, "outputs": { "result": "{stm_base}_realize/marketing/end/propose-change.json" } }
Step E3 — review-change · blockedBy: E2
{ "play": "review-change", "parent_run_id": "<this run id>", "inputs": {}, "outputs": { "result": "{stm_base}_realize/marketing/end/review-change.json" } }
Step E4 — merge-change · blockedBy: E3
{ "play": "merge-change", "parent_run_id": "<this run id>", "inputs": {}, "outputs": { "result": "{stm_base}_realize/marketing/end/merge-change.json" } }
Each end member owns its own evals (commit grouped by concern, PR opened, verdict posted, branch
merged + cleaned); they are not re-checked here. A review-change reject stops the chain before merge.
Phase: Scenario Validation
Step 7 — Scenario evals · Owner: play · Depends on: the end sequence
- SCE-1 (S1 — growth lead, first run): the live
marketing.md is a valid Marketing Lens doc
clearing the linter + the content eval, and the scoped-guard report reads ok, so the
spine/slice/profile/other lenses are byte-identical; the stop-condition verdict reads held.
- SCE-2 (S2 — product owner, honest reach): an internal tool's discoverability reads "not
applicable" with the reason rather than an invented SEO/AEO/GEO claim.
- SCE-3 (S3 — accessibility reviewer): accessibility names a concrete bar and how the slice meets
it.
- SCE-4 (S4 — architect, hub-only): no other realize lens was read or written (the scoped-guard
report confirms it).
- SCE-5 (S5 — product owner, re-run): a re-run re-derives only
marketing.md; the scoped-guard
report reads ok with everything else byte-identical; no accepted decision edited in place.
- SCE-6 (S6 — reviewer, the checkpoint): the checkpoint showed the lens 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 (a policy-listed shape), the gate resolved
with no wait and a recorded auto-pass + live-eval ledger line + diff summary exist.
Phase: Evidence & Close
Step 8 — Close · Owner: play · Depends on: Step 7
Run the Standard Play Close. /marketing is a slice-realize play — record evidence per the D1 rule.
SE-12 (F13/C13): the close is stop-condition gated — check_stop_condition.py over the baked
stop-condition.yaml (D1–D3) reads held before the run closes COMPLETED, and the model delta is
committed (C14); anything else closes HALTED with the unmet clauses named.
# --- 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/marketing/" ; slug="#${issue}"
# product-scoped play : evidence_base="${product_base}_evidence/marketing/" ; 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/marketing/status/session-stamp-marketing.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/marketing/" \
--out "${stm_base}_realize/marketing/status/stop-condition-marketing.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 REC13 (re-run the keyed persist,
re-capture the scoped-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.
/marketing runs on the slice-realize issue /ux opened, so it is project-scoped:
evidence_base="${stm_base}${issue}/evidence/marketing/" 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
marketing, run_id marketing-${ts}, slice slug, started/completed, status per C0, exit_reason;
artifacts: the live marketing.md, the marketing manifest, any 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-14 / SCE-1…SCE-6;
checkpoint decision (incl. any gate skipped by config or gate auto-passed by learned policy row)
plus the gate ledger line(s) appended this run; the end-sequence results; 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: ## marketing Delivered — ${slug}, the Run
Summary table (incl. the stop-condition verdict and the model-delta commit sha), the Pipeline Steps
table, the Artifacts Produced table (the marketing lens + any decision), Next Steps (the functional
pipe is closed — run the non-functional pipe: /architecture), and a pointer to $evidence_dest.
Always emitted.
# --- end Standard Play Close ---
Scenario Validation
| Scenario | Persona | Eval |
|---|
| S1 — first run | growth lead | SCE-1 |
| S2 — honest reach | product owner | SCE-2 |
| S3 — accessibility | accessibility reviewer | 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 /marketing runs | human |
| F2 | a model path outside the run's write scope changed (the scoped-guard report is not ok) | the guard's --restore already reverted the offending paths; re-run writing only the slice's marketing.md and any decision, after a human confirms the restore | human |
| F3 | marketing.md fails the template/shape or carries out-of-scope content | re-emit to the Marketing lens template (the four sections only) | autonomous |
| F4 | marketing.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 reach assessment, or a material choice with no decision | re-tie the assessment to the functionalities and the profile, and emit the marketing decision in the manifest | autonomous |
| F6 | a functionality was not considered | extend the assessment to consider the missing functionality | autonomous |
| F7 | /marketing read or depended on another lens | remove the dependency; /marketing derives only from the slice's hub | autonomous |
| F8 | public-marketing reach was manufactured for a slice with no public surface | reset discoverability to the honest answer ("not applicable" with the reason) | autonomous |
| F9 | accessibility was deferred to the profile or omitted | set the concrete accessibility bar and the means in the lens | autonomous |
| F10 | an accepted decision was edited in place rather than superseded (add-only breach) | restore the decision to its accepted content and record the change as a new superseding decision, after a human confirms the restore | human |
| F11 | a discoverability/accessibility pattern 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 |
| F12 | the model delta was committed before the checkpoint resolved, or a cancelled checkpoint left writes on the working tree |
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. Resuming a run that already wrote the lens enters a dirty tree; the pre-flight
clean-tree assertion (F15) is scoped to a FRESH start — a resume continues its own in-progress delta.
Compilation Metadata
| Field | Value |
|---|
| fingerprint | sha256:5c87cf97b0171de982224d994450d8c925ebd8681b325458aeaa3e9b0c39897e (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 | end (closes the functional realize pipe; injects commit → propose → review → merge) |
| position_exception | model-writing end play — writes the model on the already-started branch and commits its own feat(model) model delta after approval (C14); the injected end sequence then raises/reviews/lands it (#437) |
| 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, feat(model) commit after; 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-marketing-lens |
| scripts | 12 (preflight, check_ready_slice, lint_grounding, grounding_gate, validate_marketing, check_kb_grounding, persist_marketing — keyed decision persist, scoped_write_guard — post-write containment, classify_change — git-mode change shape, check_stop_condition — Done-means gate, session_stamp — #463 identity stamp, gate_eval + distill_gate_policy — #467 conditional gate) |
| step_evals | 14 (SE-1…SE-14) |
| scenario_evals | 6 (SCE-1…SCE-6) |
| recovery_entries | 15 (one per failure condition; 10 autonomous / 5 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_marketing.py/check_marketing.py
promotion + before/after verify path are removed; the authoring skill (author-marketing-lens)
writes the slice's marketing.md straight to the live model (a re-derive) and emits any decision as
manifest data; the new keyed persist_marketing.py writes the decisions in place, keyed to the slice,
add-only (never editing an accepted decision in place); containment is the post-write
scoped_write_guard.py (the lens --allow, decisions --add-only; 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 (F15) and commits its own feat(model) delta after approval (C14) before the injected end
sequence. Order is write-then-review (ADR 026 "Order of operations"): the full delta — the LLM lens
AND the keyed persist's decisions — 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); the injected end sequence then raises/reviews/lands it.
Nothing is COMMITTED before approval; cancel reverts the uncommitted writes. Done means moved from
"draft lens + manifest exist + lens_applied" to the direct-write records "persist record exists +
applied: true + guard-report.json ok" (mirroring /vision and /understand). Honesty note on the
lens-landed guarantee: unlike /understand or /vision — whose keyed persist writes the primary
artifact (the spine) so applied: true is real proof-of-work — /marketing's keyed persist writes
only decisions (often none), so D1/D2/D3 can pass without asserting the lens doc itself is on disk;
the guard's ok: true proves nothing changed OUTSIDE scope, not that the lens was written. The
lens-landed guarantee therefore rests on Step 2 — lint_grounding.py --doc <live path> and the
content-quality eval run over the live doc, both of which fail (and stop the run before close) if the
lens is missing — not on the stop-condition. This matches the task's Done-means instruction and the
/vision reference; a future intent revision could add a read-only lens-presence stamp to the persist
if a stop-condition-level guarantee is wanted (NOT the before/after diff that was removed). See
.
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 mirrors the same caveat on /understand's and /vision's #498
migrations (the ratified reference implementation and its fan-out): /marketing is a further fan-out of
that pattern — a REALIZE lens (position end) that writes only its slice lens + decisions and injects
the functional-pipe close — and remains convergence-unverified until an interactive play-editor run
confirms it.