| name | learn |
| position | both |
| description | Close the loop: after a unit of work ships, read what actually happened — the measure lens (baseline/target/realized), the validate verdicts and fix reports, the run lens, and the delivered epic/slice status — and update the LIVING product model to match reality. Refreshes only the model's MEANING (capability/functionality one_line, nfr_needs levels, status promotions, the grounding-doc sections learning changed) and records each material learning as an append-only decision — never the tree skeleton. Replaces the old capture/codify/distill/enrich/reap loop. One delivered unit per run; one human checkpoint before anything is committed. Writes the spine + grounding docs + decisions directly on the live model, through the change pipeline. |
| user-invocable | true |
learn
Close the loop. After a unit of work ships, read what actually happened and update the LIVING
product model — the spine and the grounding docs — to match reality. /learn reads the outcome
signals the pipeline already produced — the measure lens (each metric's baseline → target →
realized value and proof), the validate verdicts and fix reports (which gates truly cleared; a
fix_required is a model-was-wrong signal), the run lens (production actuals vs planned), and
the delivered epic/slice status — and proposes precise updates to the model's meaning: a
refined one_line, a raised nfr_needs level, an earned status promotion, the refreshed
grounding-doc sections the learning changed, and a new append-only decision per material
learning. It REPLACES the old five-play learning loop (capture / codify / distill / enrich / reap),
which staged proposals into a separate knowledge base and wrote a retired shape; /learn writes the
new spine + grounding model directly. It never touches the tree skeleton.
Pipeline position: both. The product model is the source of truth on main, so a model update
rides the change pipeline like any other change. The D2 rule prepends start-change (opens the
learn issue, cuts a fresh branch off main, inits STM) and, after the updates are persisted and
verified, appends the close sequence commit-change → propose-change → review-change → merge-change, merging the refreshed model to main. (#434 ProductOS command model)
Write discipline (ADR 026, standards/rules/direct-model-write.md). The LLM authoring skill
(author-learnings) writes ONLY the per-node grounding docs (capability.md, functionality.md, and
the slice lens/{measure|run|quality}.md) straight to the live model; every shared-file mutation
(the spine _spine.yaml meaning fields, the profile block inside it, the new decisions/ records)
is done by the deterministic keyed persist script (persist_learn.py), in place, keyed to the
manifest-named nodes and the meaning-field whitelist so it cannot touch a node the manifest does not
name, the tree skeleton, a slice/epic entry, or an accepted decision. The model tree is asserted
clean at entry (F14) and the play commits its own feat(model) delta after the checkpoint (C14),
before the injected close sequence runs — so the working-tree diff vs the branch base is exactly this
run's delta. Containment is a post-write scoped guard (scoped_write_guard.py), not a draft. There
is no draft model copy and no apply/promote step.
Compiled From
This play was compiled from the learn ICE (reference/ice.md) by play-editor (#466 Batch C; #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–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 —
reading the outcome evidence and authoring the proposed model updates — to the product-os-keeper
agent via a JSON contract over files on disk, and you run the mechanical checks (unit + outcome
resolution, the shape linter, the content-quality eval, outcome grounding, the keyed in-place
persist, and the post-write scoped guard) through bundled scripts and an isolated judge. You never
author the updates yourself, you never advance a status without proving evidence, and you never
COMMIT the model delta before the human approves the single checkpoint (C11).
Forbidden: hand-authoring the updates or a decision; writing anything other than the model's
meaning fields, the grounding-doc sections, and new decisions (C2); changing the tree skeleton,
rewriting a slice or epic entry, or editing an accepted decision in place (C2/C7); advancing a
status without proving evidence (C8); persisting the shared files (spine/profile/decisions) by any
route other than scripts/persist_learn.py; committing the model delta before the checkpoint
resolves (C11); running against a dirty product-os tree (C14/F14); closing COMPLETED without the
stop-condition verdict reading held (C13/F13).
Agent boundaries:
| Agent | Domain | Skill it invokes | Phases |
|---|
product-os-keeper | Read the unit's outcome evidence and author the proposed model updates — writing the rewritten grounding docs in place on the live model, and emitting the spine meaning-field deltas and the decision records into the manifest — each carrying its outcome citation | author-learnings | 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 |
Clean model tree — git status --porcelain -- <product_base>product-os is empty | C14/F14 | Hard halt (REC14) |
Unit resolves + outcome evidence exists (check_ready_unit.py) | C1 | Hard halt (REC1) |
Clean-tree assertion (C14/F14, ADR 026). Before any work — and before the injected
start-change cuts the fresh branch — assert the product-os tree carries no uncommitted edits, so
the branch base is a correct reference for the scoped guard and the working-tree diff:
test -z "$(git status --porcelain -- <product_base>product-os)" || { echo "HALT: dirty product-os tree (REC14)"; exit 1; }
If dirty, halt at pre-flight and ask for a clean model tree (commit or revert the pending model
edits) before /learn proceeds.
Resolve the pre-flight facts mechanically with the bundled resolver:
python3 scripts/preflight.py --play learn --config .garura/core/config.yaml
Then resolve the delivered unit and gather its outcome evidence from the spine + STM:
python3 scripts/check_ready_unit.py --product-base <product_base> --issue <issue> --stm-base <stm_base>
It asserts the unit (its issue and the slice it shipped) resolves, and that at least one outcome
signal exists — the measure lens, a validate verdict, the run lens, or a delivered epic/slice
status. If the unit is absent or nothing has delivered, hard halt (C1/REC1).
<working> is the issue's STM context dir ({stm_base}{issue}/context/) — the learn-manifest.yaml,
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 is written IN PLACE under <product_base>product-os/, never into <working>. The baked stop
condition's relative paths resolve against the STM root ({stm_base}{issue}/).
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}{issue}/status/session-stamp-learn.json" \
--cwd "$(pwd)" --branch "$(git branch --show-current)"
Resume check: if {stm_base}{issue}/status/learn.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 docs AND the keyed persist's shared-file
mutations — is written to the live model BEFORE the checkpoint, so the guard and the human both see
the real delta. Nothing is COMMITTED before the gate resolves; cancel reverts the uncommitted writes.
[T0] start-change (injected — start head) blockedBy: []
[T1] Draft the updates (docs to live) blockedBy: [T0]
[T2] Validate the live docs blockedBy: [T1]
[T3] Persist (keyed, in place — shared files) blockedBy: [T2]
[T4] Guard the full delta blockedBy: [T3]
[T5] Checkpoint (approval over the full git diff) blockedBy: [T4]
[T6] Commit the model delta 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: 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 learn issue, cuts a
fresh branch off main against the clean product-os tree the pre-flight asserted, sets up a worktree
iff config calls for it, and initializes the STM workspace. The branch tip it leaves is the base the
scoped guard and the working-tree diff reference.
{ "play": "start-change", "parent_run_id": "<this run id>", "inputs": { "title": "learn: <unit>" }, "outputs": { "result": "{stm_base}{issue}/evidence/learn/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.
Phase: Draft (write the grounding docs to the live model)
Step 1 — Draft the updates (docs to live) · Owner: product-os-keeper · Depends on: Step 0
The agent invokes author-learnings to read the unit's outcome evidence (the measure lens, the
validate verdicts + fix reports, the run lens, the delivered epic/slice status) against the current
model, and author the proposed updates. Per ADR 026 the skill writes the rewritten grounding docs
straight to the live model and emits the spine meaning-field deltas + the decision records as
structured data in learn-manifest.yaml (it never writes _spine.yaml, the profile block, or a
decision record) — each proposed change carrying its outcome citation and a confidence tier:
{
"task": "read this delivered unit's outcomes (measure/validate/run/delivered status) against the current model; propose ONLY meaning updates — one_line, nfr_needs level (monotonic-up), earned status promotions, the grounding-doc sections the learning changed, and a decision per material learning — each citing the outcome that justifies it and tiered by confidence. Write the rewritten grounding docs in place on the live model; emit the spine deltas + decision records into the manifest. Never touch the tree skeleton, a slice/epic entry, or an accepted decision.",
"inputs": { "issue": "<issue>",
"unit": "<from check_ready_unit>",
"outcomes": "<outcome evidence paths from check_ready_unit>",
"spine": "<product_base>/product-os/_spine.yaml",
"product_base": "<product_base>",
"manifest_path": "<working>/learn-manifest.yaml" },
"outputs": { "manifest": "<working>/learn-manifest.yaml" }
}
The skill writes the rewritten grounding docs (capability.md, functionality.md, slice lens docs)
IN PLACE under <product_base>product-os/ and writes learn-manifest.yaml under <working> (STM)
with the spine meaning-field deltas as changes: and the full decision records as decisions:. It
writes NO shared model file. No proposal advances a status without proving evidence (C8); a
fix_required becomes a grounding refinement, not a status advance.
SE-1 (F1/C1): check_ready_unit.py passed at pre-flight — the unit resolves and at least one
outcome signal exists; an undelivered unit halted (REC1).
SE-12 (F14/C14): the product-os tree was clean at entry — the pre-flight assertion
(git status --porcelain -- <product_base>product-os empty) passed before start-change, so the
branch base and the scoped guard reflect only this run's delta; a dirty model tree halted (REC14).
Phase: Validate
Step 2 — Validate the live docs · Owner: play · Depends on: Step 1
Run the guards over the LIVE docs before the checkpoint — shape first, then content, then grounding.
Under direct-model-write the docs are already written in place, and the spine has NOT yet been
mutated for this run (the keyed persist runs in Step 3):
python3 scripts/lint_grounding.py --doc <each rewritten grounding doc at its live path under <product_base>/product-os/...>
python3 scripts/validate_learn.py --manifest <working>/learn-manifest.yaml --spine <product_base>/product-os/_spine.yaml
Then run the content-quality eval over every rewritten grounding doc: spawn an isolated,
clean-context sub-agent handed the judge prompt (standards/rules/grounding-eval.md), the doc at its
live path, and that kind's per-section guidance, on the model from grounding-eval.judge.model. Gate
each verdict:
python3 scripts/grounding_gate.py --verdict <verdict.json>
SE-2 (F3/C3): lint_grounding.py exits 0 — every rewritten grounding doc conforms to its
template, no missing/extra/empty section.
SE-3 (F4/C4): the content-quality eval gate (grounding_gate.py) passes for every rewritten
doc — each is self-explaining and clears the stranger test.
SE-4 (F5/C5, F12/C12): validate_learn.py — every proposed change carries an outcome citation
that traces to a real outcome signal; an uncited change fails (REC5/REC12).
SE-5 (F8/C8): validate_learn.py — no status is advanced without proving evidence; a
fix_required is carried as a grounding refinement, not a status advance.
SE-6 (F6/C6): validate_learn.py — every nfr_needs change is monotonic-up and any box-move it
forces carries a decision.
SE-7 (F7/C7): validate_learn.py — every material learning is a NEW decision (status accepted);
a learning that overturns a prior decision names what it supersedes; no accepted decision is edited.
On any GAP, apply the matching recovery (REC3–REC8, REC12) and re-run before the checkpoint.
Phase: Persist (write the full delta first, ADR 026 write-then-review)
Step 3 — Persist (keyed, in place — shared files) · 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 and the human both see the real delta. The docs are already on the live model (Step 1).
persist_learn.py now writes the SHARED files in place, keyed to the manifest-named nodes: it reads
the approved learn-manifest.yaml and mutates only the allowlisted meaning fields (one_line,
nfr_needs level monotonic-up, earned status, appended decisions refs) on the nodes the manifest
names, and writes new decision records (decisions/<id>.yaml, skip-if-exists) — and it REFUSES any
node the manifest does not name, any non-meaning field, any skeleton change, any slice/epic rewrite,
any nfr fall, and any edit to an accepted decision (this is the node-level containment the file-level
guard cannot provide). 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_learn.py --manifest <working>/learn-manifest.yaml \
--product-base <product_base> --decided-by /learn --date "$(date -u +%Y-%m-%d)" \
--out-manifest <working>/persist-manifest.json
persist_learn.py writes the machine persist record (C13/#464): persist-manifest.json carries
applied: true alongside the written/changed lists — this field, not prose, is what the close's
stop-condition gate (D2) reads.
SE-8 (F2/F9/C2/C9): the keyed persist mutated only the allowlisted meaning fields on the
manifest-named nodes — no domain/capability/functionality id, slug, or parent changed, no slice or
epic entry rewritten, no accepted decision edited in place; the persist refuses any other mutation by
construction.
Phase: Guard (over the full delta)
Step 4 — Guard the full delta · Owner: play · Depends on: Step 3
The run's write scope (the per-play guard policy, ADR 026). The old fixed allowlist in
apply_learn.py is now the scoped_write_guard.py policy. /learn legitimately touches meaning across
many nodes, so the scope is broader than a single-capability play: the shared spine, the rewritable
grounding-doc kinds, and add-only decision records:
--allow 'product-os/_spine.yaml' # the shared spine + profile block (persist writes it)
--allow 'product-os/*/capability.md' # rewritten capability grounding docs
--allow 'product-os/*/functionality.md' # rewritten functionality grounding docs
--allow 'product-os/*/lens/*.md' # rewritten slice lens docs (measure/run/quality)
--add-only 'product-os/decisions/*' # new decision records (added, never modified — append-only, C7)
(fnmatch * crosses /, so product-os/*/capability.md matches a nested {domain}/{cap}/capability.md
and product-os/*/lens/*.md matches {domain}/slices/{slice}/lens/measure.md.) Node-level containment
inside the shared spine — only the manifest-named nodes, meaning fields only, no skeleton/slice/epic,
monotonic-up — is kept by the keyed persist (Step 3), not the guard.
Guard ONCE over the full delta (C9/C10, F10). After ALL writes (the LLM docs from Step 1 and the
keyed persist's shared-file writes from Step 3), run the scoped guard a single time over the whole
delta against the branch base. Capture its report — its ok field is the stop condition's D3 input:
python3 scripts/scoped_write_guard.py --product-base <product_base> --base-ref HEAD \
--allow 'product-os/_spine.yaml' --allow 'product-os/*/capability.md' \
--allow 'product-os/*/functionality.md' --allow 'product-os/*/lens/*.md' \
--add-only 'product-os/decisions/*' \
--out <working>/guard-report.json
If the guard exits non-zero, re-run with --restore to revert the offending paths, apply REC10 (an
out-of-scope path changed, or an accepted decision file was modified), and re-persist before the
checkpoint.
SE-9 (F10/C10): the scoped-write guard report reads ok: true — every changed model path fell
inside the declared write scope, and no accepted decision file was modified (add-only held); the run
is non-destructive.
Phase: Checkpoint
Step 5 — Human review (class: standard) · Owner: play · Depends on: Step 4
This is the single checkpoint (C11). It is a config switch per
standards/rules/gate-config.md (#466) — NOT a #467 conditional learned gate, so there is no
change-shape classification. Resolve gates.plays.learn → gates.classes.standard →
gates.default (absent ⇒ on). When off, record gate skipped by config (<resolution path>) as a
Checkpoint Decisions row in the evidence and proceed to Step 6. When on (the default), present every
proposed update inline over the real model git diff — the spine meaning-field changes, the
grounding-doc section rewrites, and the new decisions — each with its outcome citation, tiered by
confidence: high-confidence batched for a single approval, low-confidence surfaced one by one —
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 remove the new ones (byte-clean back to the branch base),
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
SE-10 (F11/C11): the model delta is COMMITTED (Step 6) only after this gate completes — a typed
approval, or a recorded config skip; nothing is committed before the gate completes, and on cancel the
whole delta is reverted so nothing the run wrote becomes durable. The checkpoint presented every
change with its outcome citation, tiered by confidence, over the full written delta.
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 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, run BEFORE the
injected close sequence so the subsequent commit-change handles only what remains uncommitted (STM
evidence, ADRs), not the model delta this play already committed. 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): learn from <unit> — meaning refresh, decisions (#<issue>)"
SE-11 (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 keyed persist or scoped guard did
not land closes HALTED, never COMPLETED (REC13).
Phase: End sequence (injected — D2 position: end)
After the model delta is committed, the D2 rule injects the close sequence — each a sub-play
dispatched with parent_run_id, resolving its own context — to commit any remaining change, raise the
PR, take the verdict, and merge the refreshed model to main.
Step E1 — commit-change · blockedBy: Step 6
{ "play": "commit-change", "parent_run_id": "<this run id>", "inputs": {}, "outputs": { "result": "{stm_base}{issue}/evidence/learn/end/commit-change.json" } }
Step E2 — propose-change · blockedBy: E1
{ "play": "propose-change", "parent_run_id": "<this run id>", "inputs": {}, "outputs": { "result": "{stm_base}{issue}/evidence/learn/end/propose-change.json" } }
Step E3 — review-change · blockedBy: E2
{ "play": "review-change", "parent_run_id": "<this run id>", "inputs": {}, "outputs": { "result": "{stm_base}{issue}/evidence/learn/end/review-change.json" } }
Step E4 — merge-change · blockedBy: E3
{ "play": "merge-change", "parent_run_id": "<this run id>", "inputs": {}, "outputs": { "result": "{stm_base}{issue}/evidence/learn/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 — learning analyst, first run): the proposed updates land — every rewritten grounding
doc clears the linter + content eval, the spine meaning fields update, and each material learning is
a decision; every change carries an outcome citation.
- SCE-2 (S2 — product owner, an nfr box-move): a capability's
nfr_needs rose (monotonic-up), the
profile box-move is a recorded decision, and that is the only structural change.
- SCE-3 (S3 — reviewer, a fix-required signal): a
fix_required became a grounding refinement
citing the fix report; the functionality status is unchanged.
- SCE-4 (S4 — architect, outcome-grounded): every proposed change cites the outcome that
justified it; none stands without evidence.
- SCE-5 (S5 — product owner, re-run): a re-run re-derives only the proposed updates; everything
else byte-identical; no accepted decision edited in place.
- SCE-6 (S6 — reviewer, the checkpoint): the checkpoint showed every change with its citation,
tiered by confidence, over the full written delta, and no product-model file was COMMITTED before
approval — the full delta shows as the branch diff and is reverted byte-clean on cancel.
Phase: Evidence & Close
Step 8 — Close · Owner: play · Depends on: Step 7
Run the Standard Play Close. /learn opens its own learn issue (position start), 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/learn/" ; slug="#${issue}"
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}${issue}/status/session-stamp-learn.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 STM as the close's
# authoritative input.
python3 scripts/check_stop_condition.py \
--manifest "<play-dir>/stop-condition.yaml" \
--base "${stm_base}${issue}/" \
--out "${stm_base}${issue}/status/stop-condition-learn.yaml"
sc_exit=$? # 0 held · 1 unmet · 2 error
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
persist_learn.py over the approved manifest, re-capture the scoped-guard report) and
re-evaluate; the close stays HALTED until the verdict reads held. An unevaluable verdict is
never a pass.
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 learn,
run_id learn-${ts}, issue, started/completed, status per C0, exit_reason; artifacts: the
learn-manifest.yaml, the rewritten grounding docs at their live paths, the new decisions, the
persist manifest (persist-manifest.json), the captured guard-report.json, the model-delta commit
sha, the stop-condition verdict; the content-eval verdicts; step + scenario evals SE-1…SE-12 /
SCE-1…SCE-6; checkpoint decision (incl. any gate skipped by config row); 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: ## learn Delivered — ${slug}, the Run Summary table, the Pipeline Steps table, the Artifacts Produced table (the updated
grounding docs + meaning-field changes + new decisions + the model-delta commit), Next Steps (the
model now reflects the delivered unit; run /next for the highest-value next action), and a pointer to
$evidence_dest. Always emitted.
# --- end Standard Play Close ---
Scenario Validation
| Scenario | Persona | Eval |
|---|
| S1 — first run | learning analyst | SCE-1 |
| S2 — an nfr box-move | product owner | SCE-2 |
| S3 — a fix-required signal | reviewer | SCE-3 |
| S4 — outcome-grounded | architect | SCE-4 |
| S5 — re-run | product owner | SCE-5 |
| S6 — the checkpoint | reviewer | SCE-6 |
Recovery
| For | Trigger | Direction | Handoff |
|---|
| F1 | the unit is absent or nothing has delivered | halt and route to the delivery plays (/implement → /validate → /measure) before /learn runs | human |
| F2 | a write touched the skeleton, a slice/epic entry, or an accepted decision | revert it; /learn writes only meaning fields, grounding sections, and new decisions | human |
| F3 | a rewritten grounding doc fails the template/shape | re-emit the doc to its template (no missing/extra/empty section) | autonomous |
| F4 | a rewritten grounding doc fails the content eval | rewrite the failing section to the judge's cited fixes and re-judge until the gate passes | autonomous |
| F5 | a proposed change cites no outcome | attach the outcome that justifies it or drop the change | autonomous |
| F6 | an nfr level was lowered, or a box-move lacks a decision | restore the level (monotonic-up only) and record the box-move decision | autonomous |
| F7 | an accepted decision was edited, or a supersede did not name its target | restore the decision and write a NEW record naming what it supersedes | human |
| F8 | a status advanced without evidence, or a fix_required was silently advanced | revert the status and refine the grounding from the gap instead | autonomous |
| F9 | the keyed persist changed more than the allowlisted meaning fields on the manifest-named nodes | restore the spine and re-run persist_learn.py so only the allowlisted mutations on the manifest-named nodes are applied | human |
| F10 | the scoped-write guard reports an out-of-scope path, or a decision file was modified rather than added | the guard's --restore already reverted the offending paths; re-run writing only the allowlisted scope, after a human confirms the restore | human |
| F11 | a model delta was committed before the checkpoint completed (no approval, and no recorded config skip) | revert the premature commit and re-present the checkpoint; commit only after the gate completes | human |
| F12 | a learning with no traceable outcome link | trace it to an outcome signal or drop it |
Pause and Resume
Steps run top to bottom. On entry, resolve config, resolve the target unit, check the status marker,
skip completed steps, reset any in-progress step to pending, and continue. A fresh start with no
marker runs everything. The pre-flight clean-tree assertion (F14) is scoped to a FRESH start — a
resume that already wrote model docs continues its own in-progress delta.
Compilation Metadata
| Field | Value |
|---|
| fingerprint | sha256:61a011a3d94e08d4b0aea6eb213b7b004eca097e39878837124ad9f23b6f4500 (of reference/ice.md) |
| compiled_by | play-editor (#500 direct-model-write, ADR 026); prior: play-editor (#466 Batch C) |
| pipeline_position | both (injects start-change head and commit → propose → review → merge close) |
| workflow_structure | A (single checkpoint, class standard, config-gated per gate-config.md; direct-model-write WRITE-THEN-REVIEW per ADR 026 — persist + guard before the gate, commit after; stop-condition gated close) |
| model_writes | yes |
| domain_agents | 1 (product-os-keeper) |
| utility_agents | 0 |
| skills_used | author-learnings |
| scripts | 8 (preflight, check_ready_unit, lint_grounding, grounding_gate, validate_learn, persist_learn — keyed in-place persist writing the machine applied field, scoped_write_guard — post-write containment, check_stop_condition — Done-means gate, session_stamp — #463 identity stamp) |
| step_evals | 12 (SE-1…SE-12) |
| scenario_evals | 6 (SCE-1…SCE-6) |
| recovery_entries | 14 (one per failure condition; 8 autonomous / 6 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_learn.py / check_apply_learn.py
promotion + verify scripts are removed; the authoring skill (author-learnings) writes the per-node
grounding docs straight to the live model and emits the spine deltas + decision records as manifest
data; the new keyed persist_learn.py writes the shared files (spine meaning fields, profile,
decisions) in place, keyed to the manifest-named nodes and the meaning-field whitelist; containment
is the post-write scoped_write_guard.py (its guard-report.json is D3). Order is
write-then-review (ADR 026 "Order of operations"): the full delta — LLM docs AND the keyed
persist's shared-file mutations — is written to the live model FIRST (Steps 1+3), then guarded ONCE
over the full delta (Step 4), then the gate resolves over the real git diff (Step 5), and only an
approved (or config-skipped) gate COMMITS (Step 6). Nothing is COMMITTED before the gate; cancel
reverts the uncommitted writes via scoped_write_guard.py --restore. The play asserts a clean
product-os tree at entry (F14) and commits its own feat(model) delta after the checkpoint (C14),
BEFORE the injected commit-change — which then handles only what remains uncommitted (STM evidence,
ADRs). /learn's checkpoint is a #466 default-on config gate, NOT a #467 conditional learned gate, so
no classify_change.py and no gate ledger/policy are used. See standards/rules/direct-model-write.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 here, 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 — before this pattern is relied on in production.