| 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 persists. Reads the model from the spine; writes the spine + grounding docs + decisions 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)
Compiled From
This play was compiled from the learn ICE (reference/ice.md) by play-editor (#466 Batch C).
Intent defines constraints (C1–C13) and failure conditions (F1–F13); 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 allowlisted apply,
the post-apply verify) through bundled scripts and an isolated judge. You never author the updates
yourself, you never advance a status without proving evidence, and you never persist 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 by any route other than scripts/apply_learn.py;
persisting before the checkpoint (C11).
Agent boundaries:
| Agent | Domain | Skill it invokes | Phases |
|---|
product-os-keeper | Read the unit's outcome evidence and author the proposed model updates (meaning fields, grounding-doc section rewrites, decisions), 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 |
Unit resolves + outcome evidence exists (check_ready_unit.py) | C1 | Hard halt (REC1) |
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 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.
[T0] start-change (injected — start head) blockedBy: []
[T1] Draft the updates blockedBy: [T0]
[T2] Validate the draft blockedBy: [T1]
[T3] Checkpoint (approval) blockedBy: [T2]
[T4] Persist blockedBy: [T3]
[T5] Verify persisted blockedBy: [T4]
[TE1] commit-change (injected — end #1) blockedBy: [T5]
[TE2] propose-change (injected — end #2) blockedBy: [TE1]
[TE3] review-change (injected — end #3) blockedBy: [TE2]
[TE4] merge-change (injected — end #4) blockedBy: [TE3]
[T6] Scenario Validation blockedBy: [TE4]
[T7] Close blockedBy: [T6]
Mark each task in-progress before its step and completed right after its eval passes.
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, sets up a worktree iff config calls for it, and initializes the STM workspace.
{ "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
Step 1 — Draft the updates · 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 draft the proposed updates as a human-reviewable learn-manifest.yaml plus the rewritten
grounding docs and any new decision — 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. 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>" },
"outputs": { "draft_dir": "<working>/draft/", "manifest": "<working>/draft/learn-manifest.yaml" }
}
The skill reads the model + outcomes read-only and writes only the draft (the manifest, the
rewritten grounding docs, any decision). No proposal advances a status without proving evidence
(C8); a fix_required becomes a grounding refinement, not a status advance.
Phase: Validate
Step 2 — Validate the draft · Owner: play · Depends on: Step 1
Run the guards over the draft before the checkpoint — shape first, then content, then grounding.
python3 scripts/lint_grounding.py --doc <each rewritten grounding doc under <working>/draft/product-os/...>
python3 scripts/validate_learn.py --draft <working>/draft --manifest <working>/draft/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, 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-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-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: Checkpoint
Step 3 — Human review (class: standard) · Owner: play · Depends on: Step 2
This is the single checkpoint (C11). It is a config switch per
standards/rules/gate-config.md (#466): 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 on the validated draft. When on (the default),
present every proposed update inline — 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 — and wait for the
typed response. Approve → persist; cancel → halt, nothing written.
SE-8 (F11/C11): the updates are persisted only after this gate completes — a typed approval, or
a recorded config skip — Step 4 is the sole writer and depends on this step; nothing is written
before the gate completes.
Phase: Apply
Step 4 — Persist · Owner: play · Depends on: Step 3
First snapshot the live spine and the touched grounding folders so Step 5 can verify (cp the spine
to <working>/spine-before.yaml; cp -R each touched doc folder to <working>/docs-before/).
Persist the approved updates on the fixed allowlist — only the manifest-named nodes' meaning fields,
the rewritten grounding docs, and new decisions (skip-if-exists, never edit in place):
python3 scripts/apply_learn.py --draft <working>/draft --manifest <working>/draft/learn-manifest.yaml --product-base <product_base> --decided-by /learn --date <YYYY-MM-DD> --out-manifest <working>/apply-manifest.json
apply_learn.py mutates only the allowlisted meaning fields (one_line, nfr_needs level
monotonic-up, earned status, appended decisions refs) on the nodes the manifest names, copies the
rewritten grounding docs, and writes new decision records — it refuses any skeleton change, any
slice/epic rewrite, and any edit to an accepted decision (C2/C7/C9). It writes the machine apply
record (C13/#464): apply-manifest.json carries applied: true alongside the written/changed
lists — this field, not prose, is what the close's stop-condition gate (D3) reads.
Phase: Verify
Step 5 — Verify persisted · Owner: play · Depends on: Step 4
Verify the persist was surgical:
python3 scripts/check_apply_learn.py --manifest <working>/apply-manifest.json --spine-before <working>/spine-before.yaml --spine-after <product_base>/product-os/_spine.yaml --docs-before <working>/docs-before
SE-9 (F2/F9/C2/C9): the only spine changes are the allowlisted meaning fields on the
manifest-named nodes — no domain/capability/functionality id, slug, or parent changed, no slice or
epic entry rewritten — every other node, field, and collection byte-identical.
SE-10 (F10/C10): the run is non-destructive — no non-allowlisted file changed, no accepted
decision edited in place, nothing removed.
SE-11 (F13/C13): the Done means holds as machine state — context/draft/learn-manifest.yaml
and context/apply-manifest.json exist and the apply record reads applied: true — and the
close's stop-condition verdict reads held before the run may close COMPLETED; a persist asserted
only in prose fails this eval (REC13: re-run apply_learn.py over the approved manifest, then
re-evaluate).
Phase: End sequence (injected — D2 position: end)
After the updates are persisted and verified, the D2 rule injects the close sequence — each a
sub-play dispatched with parent_run_id, resolving its own context — to commit the learn change,
raise the PR, take the verdict, and merge the refreshed model to main.
Step E1 — commit-change · blockedBy: Step 5
{ "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 6 — 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, and no product-model file was written before approval.
Phase: Evidence & Close
Step 7 — Close · Owner: play · Depends on: Step 6
Run the Standard Play Close. /learn opens its own learn issue (position start), so it is
project-scoped — record evidence per the D1 rule.
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_stamp=$(python3 scripts/session_stamp.py --phase close \
--marker "${stm_base}${issue}/status/session-stamp-learn.json")
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=$?
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. 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, the new decisions, the apply manifest, the
stop-condition verdict; the content-eval verdicts; step + scenario evals SE-1…SE-11 / 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), 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.
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 spine write changed more than the allowlisted fields | restore the spine and re-apply only the allowlisted mutations on the manifest-named nodes | human |
| F10 | a non-allowlisted file changed, or a decision edited in place | restore it and re-apply only the allowlisted writes, after a human confirms the restore | human |
| F11 | a model update persisted before the checkpoint completed (no approval, and no recorded config skip) | revert the premature write and re-present the checkpoint; persist only after the gate completes | human |
| F12 | a learning with no traceable outcome link | trace it to an outcome signal or drop it | autonomous |
| F13 | the run is about to close COMPLETED with the Done means unmet (a missing learnings manifest or apply record, or applied not true) | produce the missing artifact — re-run apply_learn.py over the approved manifest so the apply record carries the machine applied field — then re-evaluate the stop condition; the close stays HALTED until the verdict reads held | autonomous |
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.
Compilation Metadata
| Field | Value |
|---|
| fingerprint | sha256:d962e7dbd7c7ea7f07de741a1f44395ee3bde8395a854072488ca64b4c07da79 (of reference/ice.md) |
| compiled_by | 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) |
| stop_condition | stop-condition.yaml (D1–D3), gate live at Step C0 |
| model_writes | yes |
| domain_agents | 1 (product-os-keeper) |
| utility_agents | 0 |
| skills_used | author-learnings |
| scripts | 9 (preflight, check_ready_unit, lint_grounding, grounding_gate, validate_learn, apply_learn — writes the machine applied field, check_apply_learn, check_stop_condition — Done-means gate, session_stamp — #463 identity stamp) |
| step_evals | 11 (SE-1…SE-11) |
| scenario_evals | 6 (SCE-1…SCE-6) |
| recovery_entries | 13 (one per failure condition; 7 autonomous / 6 human) |