learn

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

SKILL.md

GitHub

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 (shared; see ../_shared/play-close.md) ---
# Run the close in ../_shared/play-close.md. Its Step L loads the templates through
# template-manager and sets ts and evidence_dest; then run this play's own lines below;
# then Steps C0-C2.
#   evidence_base, slug (project-scoped play):
#     evidence_base="${stm_base}${issue}/evidence/learn/"   ; slug="#${issue}"
# 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 the Next-line map ($next_map, from template-manager) 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 autonomous
F13 the run is about to close COMPLETED with the Done means unmet (a missing persist record, applied not true, or the guard report absent or not ok) produce the missing artifact — re-run persist_learn.py over the approved manifest so the persist record carries the machine applied field, re-capture the guard report — then re-evaluate the stop condition; the close stays HALTED until the verdict reads held autonomous
F14 the product-os tree is dirty at entry (uncommitted model edits present) halt at pre-flight and ask for a clean model tree — commit or revert the pending model edits — before /learn proceeds human

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.