| name | propose-change |
| position | end |
| description | Raise the current change for review — run a scope-and-quality self-review from the project's standards, push the branch, and open a pull request carrying that review and the issue reference. The second step of the end sequence in the ProductOS command model. Use when a committed change is ready to raise as a PR. |
| user-invocable | true |
propose-change
Raise the current change for review: run a self-review on its scope and quality, push the
branch, and open a pull request that carries that review — so the change is ready for
review-change.
Pipeline position: end (second step of the end sequence: commit-change →
propose-change → review-change → merge-change). The D2 pipeline-position rule injects this
sequence into any play declared position: end. The plays are independent and invokable;
ordering is enforced by each one's pre-flight, not by welding them together.
Compiled From
This play was compiled from the propose-change ICE (reference/ice.md) by play-editor
(#466 Batch A, Level 3 rollout per ADR 025; #467 Batch C gate ruling). Intent defines
constraints (C1–C9) and failure conditions (F1–F9); the expectation defines success
scenarios (S1–S4), a Done means (D1–D3, baked to stop-condition.yaml), and one
recovery entry per failure condition. This play is SINGLE-PASS: no loop, no iteration cap — the Done means is
proven once, at the gated close.
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 the step order. The self-review is
genuine judgment, so it goes to a utility agent (analyze-pr). The mechanical push +
PR-open is a fixed git/gh sequence, so it runs in the bundled submit_pr.py, not an agent
(C9, #484). You never reason out git/gh inline in play prose.
Forbidden: running the mechanical push/PR-open through an agent dispatch (the #484 bug);
ad-hoc gh/git in play prose; inlining the self-review rules (they are resolved from a
standards file — C6).
Agent boundaries:
| Agent | Domain | Skill it invokes | Phases |
|---|
repo-orchestrator | Self-review checklist (judgment) | analyze-pr | Execution (Step 1) |
repo-orchestrator is a utility agent (the self-review judgment), exempt from the
domain-agent budget. The push/PR-open is the bundled submit_pr.py — no agent (C9). This
play uses zero domain agents.
Pre-flight
| Check | Constraint | Action on Failure |
|---|
Resolve working base + config (.garura/core/config.yaml) | — | Hard halt |
On a feature branch (not main) | C2 | Hard halt |
| Working tree is clean (change is committed) | C3 | Hard halt |
Platform CLI available (gh) | C2 | Hard halt |
Resolve self-review rules file via standards_order (project override → base standards/rules/self-review.md) | C6 | Hard halt |
| Existing-PR check for the branch | C5 | Resume / update |
Resolve the deterministic pre-flight facts with the bundled resolver — config tokens,
branch, issue (from the branch name), on_default_branch, evidence_record:
python3 scripts/preflight.py --play propose-change \
--config .garura/core/config.yaml --branch "$(git branch --show-current)"
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-propose-change.json" \
--cwd "$(pwd)" --branch "$(git branch --show-current)"
The resolver does NOT pick the self-review standards file (that stays with
resolve_standard.py below — live precedence) and does NOT run the clean-tree or open-PR
checks (live git/gh, kept in the table). Resolve the self-review rules path mechanically:
read the standards_order base dirs from config (most-specific first) and run
python3 scripts/resolve_standard.py --file self-review.md --dir <most-specific> … --dir <base>
which returns the resolved path and an is_override flag — record both for the run (this
is the C6 resolution; the play does not pick the file in prose).
Resume / update (C5, F5): if an open PR already exists for the branch, this run updates
it (refreshes the self-review and body) instead of opening a second one.
Task DAG
Create ALL tasks immediately after resolving the working base — before any domain work.
The play owns this DAG; agents must not edit its top-level tasks.
[T1] Self-Review (scope + quality) blockedBy: []
[T2] Confirm Raise blockedBy: [T1]
[T3] Push + Open/Update PR blockedBy: [T2]
[T4] Scenario Validation blockedBy: [T3]
[T5] Close blockedBy: [T4]
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: Execution
Step 1 — Self-Review (scope + quality) · Owner: repo-orchestrator · Depends on: pre-flight
The agent invokes analyze-pr against the diff, driven by the rules file resolved at
pre-flight, and writes the checklist to disk. The contract moves only paths:
{
"task": "run the scope+quality self-review using the resolved rules file",
"inputs": { "rules_file": "<resolved self-review.md path>",
"issue": "<issue number>" },
"outputs": { "self_review": "<working>/self-review.md",
"resolved_rules": "<working>/resolved-rules.json" }
}
resolved-rules.json is the output of scripts/resolve_standard.py from pre-flight (the
resolved path + is_override); the agent passes that path to analyze-pr, it does not
re-resolve.
SE-1 (F1/C1): self-review.md exists and is produced before any PR is opened.
SE-2 (F6/C6): resolved-rules.json names the project override when one exists under
standards_order, else the base file — the rules were read from the standards file, not
inlined in the play.
Phase: Approval
Step 2 — Confirm Raise · Owner: play · Depends on: Step 1 · Checkpoint (class: standard)
Opening or updating a PR is outward-facing. The checkpoint is a config switch per
standards/rules/gate-config.md (C8; first match wins: gates.plays.propose-change →
gates.classes.standard → gates.default; absent ⇒ on) — per the #467 ruling
gates.plays.propose-change is off, so the resolved default is the off path; flipping
the config back to on restores the wait unchanged.
- On — present the self-review and the PR draft (title, body, base = main, issue
reference) and wait for a typed approval. Approve → continue; cancel → halt without
pushing or opening.
- Off — do not wait: record
gate skipped by config (gates.plays.propose-change) as
a Checkpoint Decisions row in the evidence and proceed. The skip is never silent.
Gate on or off, the human's blocking scan is replaced by a machine check, not dropped:
Step 3 runs scripts/check_self_review.py BEFORE any push or PR-open, and a blocking
finding halts to a human (C8, F8). Tree-clean (C3, pre-flight) and the issue reference
(C4, SE-4) are the other two legs of the raise precondition — machine walls, never gated
by this switch.
Phase: Execution
Step 3 — Push + Open/Update PR · Owner: play (script) · Depends on: Step 2
Pre-push self-review check (C8/F8) — gate on or off, always first:
python3 scripts/check_self_review.py --self-review <working>/self-review.md
Exit 0 (clean) → proceed. Non-zero → HALT to a human before any push or PR-open: surface
the blocking findings the script printed, and do not push or open until they are fixed
and the check passes (REC8). Record the executed command and its exit code for the
evidence file.
Then run the bundled submit_pr.py — it pushes the branch and opens (or updates, C5) a PR
against main carrying the pre-rendered body, and writes pr.json (via platform_adapter.py).
No agent (C9). Assemble the body first (the self-review embedded + the issue reference),
and commit the run artifacts BEFORE the push so the tree stays clean and the PR carries the
record (C7): (1) render the PR body to <working>/pr-body.md; (2) git add the issue's
context dir and commit chore(stm): record propose-change run artifacts (#<issue>)
(self-review + resolved-rules); (3) run the script:
python3 scripts/submit_pr.py --config .garura/core/config.yaml \
--title "<conventional title (#<issue>)>" --base main \
--body-file "<working>/pr-body.md" --out "<working>/pr.json"
then (4) commit pr.json under the same message — never an amend — and push, so the PR
carries the play's full record.
SE-6 (F3/C3): the working tree was clean (no uncommitted changes) immediately before
the push — nothing was left behind.
SE-3 (F2/C2): after the step, local HEAD matches the pushed remote branch and a PR is
open against main for the branch.
SE-4 (F4/C4): the PR body references the issue (#<issue>).
SE-5 (F5/C5): the open-PR count for the branch is exactly one — a pre-existing PR was
updated, not duplicated.
SE-8 (F8/C8): check_self_review.py was executed BEFORE the push, and its recorded
exit code is 0 — no push or PR-open happened while the self-review carried a blocking
finding.
SE-9 (F9/C9): the push + PR-open ran as the bundled submit_pr.py (via
platform_adapter.py), not an agent dispatch — no repo-orchestrator/submit-pr was
invoked for the push/PR work.
Phase: Scenario Validation
Step 4 — Scenario Evals · Owner: play · Depends on: Step 3
- SCE-1 (S1 — developer, clean change): a PR exists for the branch whose body carries
the self-review checklist and references the issue; the run artifacts were committed
before the push; the stop-condition verdict reads held.
- SCE-2 (S2 — developer, project override): when the project supplies its own rules
file,
resolved-rules.json shows the override was used.
- SCE-3 (S3 — developer, re-raise): a second run left the open-PR count unchanged and
reused the same PR number.
- SCE-4 (S4 — developer, dirty tree): with uncommitted changes, the run halted at
pre-flight — no push and no PR were made.
Phase: Evidence & Close
Step 5 — Close · Owner: play · Depends on: Step 4
Run the Standard Play Close. Evidence recording is play-only and config-gated per the D1
evidence rule (standards/rules/evidence-recording.md). The close is GATED (the
stop-condition step below, #464): the verdict decides COMPLETED vs HALTED.
SE-7 (F7/C7): the stop-condition verdict read held before COMPLETED was stamped; the
run artifacts are committed and on the remote branch — the PR carries them.
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-propose-change.json")
python3 scripts/check_stop_condition.py \
--manifest "<play-dir>/stop-condition.yaml" \
--base "${stm_base}${issue}/" \
--out "${stm_base}${issue}/status/stop-condition-propose-change.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 (global +
per-play evidence.plays.propose-change; first match wins, absent ⇒ record). When false,
skip the write and record evidence skipped (record=false) in the report's pointer line.
Otherwise fill the evidence-file.md slots (play propose-change, run_id
propose-change-${ts}, issue, started_at/completed_at, status per C0, exit_reason,
artifacts produced: self-review.md, resolved-rules.json, pr.json (the PR
number/URL), the stop-condition verdict; step/scenario eval results;
checkpoint decision from Step 2 (or its config-skip row); commit reference = the
run-artifact commits from Step 3; 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 and output the
report: ## propose-change Delivered — #${issue}, the Run Summary table, the Pipeline
Steps table from the task DAG, the Artifacts Produced table (incl. the PR URL), Next Steps
(run review-change on the PR), and a pointer to $evidence_dest. Always emitted; never
gated.
Scenario Validation
| Scenario | Persona | Eval |
|---|
| S1 — clean change | developer | SCE-1 |
| S2 — project override | developer | SCE-2 |
| S3 — re-raise | developer | SCE-3 |
| S4 — dirty tree | developer | SCE-4 |
Recovery
| For | Trigger | Direction | Handoff |
|---|
| F1 | a PR would open without the self-review | run the self-review from the resolved rules file and attach it before opening | autonomous |
| F2 | the push or PR-open failed | retry the push/open and surface the platform error | autonomous |
| F3 | uncommitted changes at raise time | halt and have the change committed (e.g. via commit-change) before raising | human |
| F4 | the PR has no issue reference | add the issue reference to the PR | autonomous |
| F5 | a second run would open a duplicate PR | detect the existing PR for the branch and update it instead | autonomous |
| F6 | the self-review ran on hardcoded rules or ignored a project override | re-resolve the rules file via standards_order (project override → base) and re-run the self-review | autonomous |
| F7 | the close would stamp COMPLETED without the stop-condition verdict held | evaluate the stop condition, surface the unmet clauses, and close HALTED (stop_condition_unmet) until they are fixed — an unevaluable verdict is never a pass | autonomous |
| F8 | a push or PR-open happened (or would happen) while the self-review carried a blocking finding | halt to a human — fix the blocking findings, then re-run so check_self_review.py passes before any push or PR-open | human |
| F9 | the mechanical push or PR-open ran through an agent instead of submit_pr.py | route it through submit_pr.py (via platform_adapter.py) and remove the agent dispatch | autonomous |
Pause and Resume
Steps run top to bottom. On entry, resolve the issue from the branch name, check the
status marker and whether an open PR already exists for the branch, skip completed steps,
reset any in-progress step to pending, and continue. A re-run with an existing PR updates
it rather than opening a new one (C5, F5).
Compilation Metadata
| Field | Value |
|---|
| fingerprint | sha256:325f03623f5569755f2991fcae46c1f18e4cf081505a114ffa5674d2c94c0a94 (of reference/ice.md) |
| compiled_by | play-editor (#466 Batch A; #467 Batch C; #484 scripted chain) |
| pipeline_position | end |
| workflow_structure | B (fast execution flow, single-pass — no loop, no cap; gated close) |
| stop_condition | stop-condition.yaml (D1–D3), gate live at Step C0 |
| domain_agents | 0 |
| utility_agents | 1 (repo-orchestrator — self-review judgment, Step 1 only) |
| skills_reused | analyze-pr |
| standards_consumed | standards/rules/self-review.md (overrideable per project); standards/rules/gate-config.md (Step 2 switch) |
| scripts | 7 (resolve_standard.py — standards-file precedence; preflight.py — pre-flight facts; session_stamp.py — session identity stamp; check_stop_condition.py — Done-means gate; check_self_review.py — pre-push blocking-finding check; platform_adapter.py — code-host adapter; submit_pr.py — push + PR-open) |
| step_evals | 9 (SE-1…SE-9) |
| scenario_evals | 4 (SCE-1…SCE-4) |
| recovery_entries | 9 (one per failure condition; 7 autonomous / 2 human) |
Recompile note (#484, scripted chain)
Intent change via reference/ice.md → recompile. Added C9 (mechanical push + PR-open runs
in the bundled submit_pr.py, never agent dispatch), F9, REC9, SE-9. Step 3 no longer
dispatches repo-orchestrator → submit-pr; it runs submit_pr.py via platform_adapter.py
(canonical copies in play-creator/references/, stamped here). The self-review (Step 1)
keeps repo-orchestrator → analyze-pr — it is genuine judgment, not mechanical. Scripts
5 → 7; skills_reused drops submit-pr. Guarantees (self-review attached, tree clean before
push, issue referenced, no duplicate PR, blocker check) unchanged. Fingerprint recomputed
over the edited ICE.
Direct-edit deviation note (#434, harness-led sweep)
Non-intent edit: the C6 self-review file resolution was moved from prose into a called
script (scripts/resolve_standard.py, pure filesystem precedence — no git/gh). No
constraint/failure/scenario/eval text changed; reference/ice.md and the fingerprint are
unchanged. play-creator step 3 is taught the same discipline so a rebuild reproduces it.
Direct-edit deviation note (#434, pre-flight resolver)
Also non-intent: pre-flight resolution moved from orchestrator inference to the bundled
scripts/preflight.py (config/branch/issue/changeset facts as JSON; the play keeps the halt
policy). It does NOT resolve the self-review standards file (that stays with
resolve_standard.py) nor the clean-tree/open-PR checks (live git/gh). The script is the
canonical resolver stamped from play-creator/references/preflight.py; a rebuild reproduces
it (play-creator step 4). reference/ice.md and the fingerprint are unchanged. Direct edit —
recompiling would clobber this play's hand-added scripts, so the wiring is hand-added here.
Recompile note (#467 Batch C, chain gates off)
Intent change via reference/ice.md → play-editor: the Confirm Raise checkpoint gained
C8/F8 — the gate resolves per gate-config (per-play gates.plays.propose-change now off;
skips recorded), and gate on or off the play runs the bundled
scripts/check_self_review.py on self-review.md BEFORE any push or PR-open, halting to
a human on any blocking finding (non-zero exit). Tree-clean (C3) and the issue reference
(C4) are named as the other two legs of the raise precondition. Checkpoint machinery is
kept, not removed: flipping the config back to on restores the human wait unchanged.
check_self_review.py is stamped from the canonical
play-creator/references/check_self_review.py. The fingerprint reflects the updated ICE.
Direct-edit deviation note (#463, session identity stamp)
Non-intent change: the Standard Play Close gained the session identity stamp — scripts/session_stamp.py (canonical copy: play-creator/references/session_stamp.py) runs --phase start at pre-flight and --phase close in the close block; the evidence frontmatter carries session_id / ledger_file / ledger_start_offset / ledger_end_offset. Source of truth: standards/rules/play-close.md; play-creator emits the same lines so a rebuild converges. No constraint, failure, scenario, or eval changed; the fingerprint stands.