| name | commit-change |
| position | end |
| description | Commit all uncommitted work on the feature branch, grouped by concern, with conventional messages that reference the issue — leaving a clean tree ready to raise. Commits only; does not push. The first step of the end sequence in the ProductOS change pipeline. Use when work is done and needs committing before propose-change. |
| user-invocable | true |
commit-change
Commit all uncommitted work on the feature branch, grouped by concern, with conventional
messages that reference the tracked issue — leaving a clean tree ready to raise. This play
commits only; it does not push (propose-change pushes when it opens the PR).
Pipeline position: end. commit-change is the first step of the end sequence
(commit-change → propose-change → review-change → merge-change) that the D2 pipeline-position
rule injects into any play declared position: end. Positions are start and end only —
there is no middle.
The pilot (#465). This is the first play compiled on the Level 3 model (ADR 025):
the skeleton — pre-flight, one loop, conditional checkpoint, gated close — is deterministic
and guaranteed; the interior of the loop is the agent's, bounded by four walls: the baked
stop condition (stop-condition.yaml), the bundled scripts as the checker, an iteration
cap of 5, and the run's evidence. The play ends by PROVING its Done means (C9), never by a
step list running out.
Compiled From
This play was compiled from the commit-change ICE (reference/ice.md) by play-editor
(#465, Level 3 pilot recompile; #467 Batch C gate-off recompile). Intent defines
constraints (C1–C9) and failure conditions (F1–F9); the expectation defines success
scenarios (S1–S6), a Done means (D1–D4, 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 skeleton — pre-flight, the loop's walls, the
checkpoint policy, the gated close — and the agent owns the interior of each round.
Mechanical work runs as bundled scripts (C8): the changeset scan and triviality
classification (scripts/analyze_changeset.py), commit execution over the decided plan
(scripts/execute_commits.py), and the stop-condition evaluation
(scripts/check_stop_condition.py). Inference is reserved for the one genuine judgment —
grouping a multi-concern changeset — delegated to repo-orchestrator (invoking
analyze-changes) via a JSON contract over files on disk. Issue resolution, when not
derivable from the branch, goes to project-orchestrator (resolve-issues,
manage-issue). You never push — that is propose-change's job (C7).
Forbidden: ad-hoc git/gh commands reasoned out inline — git runs only inside the
bundled scripts or the dispatched skills; spawning an analyze agent for a changeset the
script classified trivial (C8/F7); any push (git push, submit-pr, or equivalent — the
execution script contains no push path); closing COMPLETED without the stop condition
held (C9); exceeding the iteration cap (F8).
Agent boundaries:
| Agent | Domain | Skill it invokes | Phases | Dispatched when |
|---|
repo-orchestrator | Multi-concern grouping judgment | analyze-changes | Loop | analyze_changeset.py reports needs_judgment: true |
project-orchestrator | Resolve change groups to the tracked issue | resolve-issues, manage-issue | Loop | issue not derivable from the branch |
Both are utility agents, exempt from the domain-agent budget. This play uses zero
domain agents, and on a trivial changeset it spawns zero agents at all (S5).
The plan contract (C3, #465 defect 1). The plan (analysis.yaml) carries each group's
subject BARE — no type prefix, no scope, no issue reference; those live in the group's
separate commit_type / scope / issue fields. The executor renders the conventional
message and defensively normalizes a subject that arrives pre-formatted (strips a doubled
prefix or trailing issue ref) instead of doubling it.
Pre-flight
| Check | Constraint | Action on Failure |
|---|
Resolve working base + config (.garura/core/config.yaml) | — | Hard halt |
Branch guard (not on main/default) | C1 | Hard halt |
Changes exist (git status --porcelain non-empty) | C5 | Graceful exit |
Sensitive-file scan (loop's scan script, analyze_changeset.py patterns) | C6 | Hard block |
Issue resolvable (from the feature/<issue>-* branch, else open issues) | C3 | Hard halt |
Resolve the pre-flight facts mechanically with the bundled resolver — do not derive them by
inference. The orchestrator captures the two live reads and hands them to the script:
git branch --show-current # current branch
git status --porcelain > <working>/porcelain.txt # changeset
python3 scripts/preflight.py --play commit-change \
--config .garura/core/config.yaml \
--branch "<current branch>" \
--porcelain-file <working>/porcelain.txt
It returns one JSON object of facts: stm_base, ltm_project_target, evidence_record,
platform, branch, issue, on_default_branch, changes_present. The orchestrator reads
those and applies the policy in the table above (the script resolves facts, the play
decides halts): on_default_branch == true → hard halt (C1); changes_present == false →
graceful exit (C5); a non-null issue short-circuits issue resolution
(auto_issue_resolved = true) and skips the checkpoint (C3). The sensitive-file scan runs
inside the loop's scan script (same porcelain file), not the resolver.
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-commit-change.json" \
--cwd "$(pwd)" --branch "$(git branch --show-current)"
Resume check: if {stm_base}/{issue}/status/commit-change.json exists, resume — the
loop re-enters at the next round with the recorded round count; the cap counts all rounds
across resumes.
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. The DAG is the skeleton:
the loop is ONE task, however many rounds it takes (C9 — the interior is the agent's).
[T1] Commit Loop (≤5 rounds) blockedBy: []
[T2] Scenario Validation blockedBy: [T1]
[T3] Close (stop-condition gated) blockedBy: [T2]
Mark each task in-progress before its phase and completed right after its eval passes. On
resume, the loop task re-enters with its round counter.
Workflow
Phase: The Loop
Loop — Commit until Done means holds · Owner: play (scripts) + agent on judgment ·
Depends on: pre-flight · Cap: 5 rounds (C9/F8)
Each round the agent decides its own moves toward the goal — every change committed or
excluded-with-reason, tree clean, nothing pushed — using the bundled scripts as its hands.
A round typically:
- Scan — always the script first:
python3 scripts/analyze_changeset.py \
--porcelain-file <working>/porcelain.txt \
--issue {issue} --repo-root . \
--out <working>/analysis.yaml
Policy on its summary: sensitive (blocking) non-empty → hard block (C6/F5),
human recovery (REC5) — the block is a machine wall, never gated by the checkpoint
switch. warnings non-empty (content-pattern matches in prose/standards
artifacts — pattern carriers, #438) → never a block; surfaced at the checkpoint when
the gate is on, carried into the delivery report's Run Summary when it is off (F9).
needs_judgment: false → the script's plan IS the plan; a subject_source: script-draft
subject may be reworded in place (one line, no agent); no agent is dispatched (C8).
needs_judgment: true → dispatch repo-orchestrator → analyze-changes for the
grouping judgment, standard JSON contract (subjects BARE per the plan contract):
{
"task": "group the changeset by concern; flag sensitive/risky files",
"inputs": { "scan": "<working>/analysis.yaml" },
"outputs": { "analysis": "<working>/analysis.yaml" }
}
-
Resolve issue — skip when derived from the branch (auto_issue_resolved);
otherwise project-orchestrator invokes manage-issue + resolve-issues and writes
issue-mappings.yaml.
-
Checkpoint — only when the skip condition fails (a sensitive flag, scanner
warnings, or low-confidence mapping) (class: standard). When it fires, resolve the
gate switch per standards/rules/gate-config.md FIRST:
- off (this project:
gates.plays.commit-change: off) → do not wait; record
gate skipped by config (gates.plays.commit-change) as a Checkpoint Decisions row
in evidence and proceed to execute. The off path drops nothing silently (C9/F9):
scanner warnings are carried into the delivery report's Run Summary (a visible
surface); a low-confidence mapping proceeds with the mapping recorded as
low-confidence in issue-mappings.yaml and the run record — never silently
upgraded. The sensitive-file block is untouched by the switch — it already
hard-halted at the scan (a machine wall, never a gate).
- on → present the brief — groups, mapping + confidence, risk flags, warnings —
and wait for typed approval. Approve → execute; cancel → halt.
-
Execute — the decided plan, mechanically, run-state finalization included
(#465 defects 2+3 — ignored paths become recorded exclusions; the play's own run
artifacts land as the final chore group):
python3 scripts/execute_commits.py \
--analysis <working>/analysis.yaml \
--issue {issue} \
--out <working>/commits.yaml \
--run-state-dir <working>
- Prove or go again — evaluate the stop condition:
python3 scripts/check_stop_condition.py \
--manifest <play-dir>/stop-condition.yaml \
--base "{stm_base}{issue}/" \
--out "{stm_base}{issue}/status/stop-condition-commit-change.yaml"
Held (exit 0) → leave the loop. Unmet (exit 1) → the verdict's unmet clauses
are the next round's work list (fresh porcelain, plan the remainder); increment the
round counter and sync it to the status marker. Round 5 still unmet → halt HALTED,
exit_reason: loop_cap_exhausted, verdict recorded (F8/REC8 — human).
Loop evals (each round's exit is checked, the loop's exit is proven):
- SE-1 (F1/C2): each change group in
analysis.yaml holds files of a single concern.
- SE-2 (F2/C3): each commit message is conventional
type(scope): subject (#issue)
with NO doubled prefix and exactly one issue reference — the executor normalized any
pre-formatted subject.
- SE-3 (F3/C4):
commits.yaml reads leftover_count: 0 and tree_clean: true, or
the loop is still running / halted with the verdict naming the leftovers. Ignored paths
appear as recorded exclusions, never as add-failures.
- SE-4 (F4/C1): the commits were made on the feature branch, not
main.
- SE-5 (F5/C6): no file flagged BLOCKING by the scan is included; a blocking flag
halts the run. Prose/standards content matches are surfaced warnings, not blocks (#438).
- SE-6 (F6/C7): no push occurred —
commits.yaml reads pushed: false and the remote
tip is unchanged by this run.
- SE-7 (F7/C8): the dispatch matches the classification —
needs_judgment: false and
zero analyze-agent spawns, or true and exactly one (per round). A trivial-classified
analysis whose group spans unrelated file sets triggers REC7.
- SE-8 (F8/C9): the loop exited by a held stop condition, or halted at the cap with
the verdict recorded — the round counter never exceeds 5 and a cap-halt never closes
COMPLETED.
- SE-9 (F9/C9): every scanner warning the run produced is visible somewhere — at a
fired checkpoint (gate on) or in the delivery report's Run Summary Scanner Warnings
row (gate off); a run with warnings and neither surface fails this eval (REC9
re-renders the report).
Phase: Scenario Validation
Scenario Evals · Owner: play · Depends on: the loop
- SCE-1 (S1 — developer, multi-concern): each concern is a separate conventional
commit referencing the issue with no doubled prefix, the run artifacts are committed,
the tree is clean, and nothing was pushed.
- SCE-2 (S2 — developer, nothing to commit): a clean tree caused a graceful exit at
pre-flight with no commit created.
- SCE-3 (S3 — reviewer, log readability): every commit follows conventional format with
an issue reference, reviewable from the log alone.
- SCE-4 (S4 — developer, sensitive file): a sensitive file blocked the run
with no commit created.
- SCE-5 (S5 — developer, trivial changeset): a single-concern changeset ran entirely
through the bundled scripts — plan and commits script-emitted, zero analyze/commit agent
dispatches, same C2–C4 guarantees as the agent path.
- SCE-6 (S6 — developer, loop convergence): an unmet round fed its verdict to the next
round as the work list, and the run converged (verdict held) or halted with the unmet
clauses named and rounds ≤ 5.
Phase: Evidence & Close
Close · Owner: play · Depends on: Scenario Validation
Run the Standard Play Close. Evidence recording is play-only and config-gated per the D1
evidence rule (standards/rules/evidence-recording.md).
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-commit-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-commit-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 (or
loop_cap_exhausted when the loop capped) 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.commit-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 commit-change, run_id
commit-change-${ts}, issue, started_at/completed_at, status per C0, exit_reason;
artifacts produced: analysis.yaml, issue-mappings.yaml, commits.yaml, the
stop-condition verdict; step/scenario eval results; checkpoint decisions from the loop's
checkpoint rounds; commit reference = the created commit SHAs incl. the run-state commit;
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:
## commit-change Delivered — #${issue}, the Run Summary table (incl. rounds used, the
stop-condition verdict, and a Scanner Warnings row — every warning the scan produced,
carried here whenever the checkpoint did not fire or was skipped by config; none when
the scan was clean (F9/SE-9)), the Pipeline Steps table from the task DAG, the Artifacts Produced
table (the commits created), Next Steps (run propose-change to push and open the PR), and a
pointer to $evidence_dest. Always emitted; never gated.
Scenario Validation
| Scenario | Persona | Eval |
|---|
| S1 — multi-concern | developer | SCE-1 |
| S2 — nothing to commit | developer | SCE-2 |
| S3 — log readability | reviewer | SCE-3 |
| S4 — sensitive file | developer | SCE-4 |
| S5 — trivial changeset, scripts only | developer | SCE-5 |
| S6 — loop convergence | developer | SCE-6 |
Recovery
| For | Trigger | Direction | Handoff |
|---|
| F1 | a commit covers more than one concern | regroup so each commit holds a single concern; re-stage by group and recommit (a loop round) | autonomous |
| F2 | a commit subject is generic or missing the issue reference | rewrite the subject to describe the change and add the issue reference | autonomous |
| F3 | changed files remain uncommitted with no exclusion reason | the unmet verdict is the next round's work list — commit the remainder or record the exclusion | autonomous |
| F4 | the current branch is main | halt; move the work to a feature branch (start-change) before committing | human |
| F5 | a sensitive file is staged | unstage the sensitive file and confirm it is excluded before committing | human |
| F6 | the play attempted a push | this play commits only — drop the push; leave pushing to propose-change | autonomous |
| F7 | the script's triviality classification disagrees with the changeset | discard the fast-path analysis and re-run the scan through the analyze agent (or record a needless dispatch); the agent's grouping wins | autonomous |
| F8 | the loop hit its cap with the Done means unmet | halt HALTED (loop_cap_exhausted) with the verdict recorded; a human inspects the unmet clauses and fixes state or knowingly raises the cap | human |
| F9 | scan warnings exist but no checkpoint fired and the report carries no Scanner Warnings row | re-render the delivery report with the warnings carried into the Run Summary before closing | autonomous |
Pause and Resume
On entry, resolve the issue from the branch name, check the status marker, and re-enter
the loop at the recorded round count — the cap counts all rounds across resumes. A fresh
start with no marker begins at round 1 and creates the marker.
Compilation Metadata
| Field | Value |
|---|
| fingerprint | sha256:5e3b5afb2b9b097c99d610feda8d9b8bb749bf3de04b9c5583c3471995bfe95d (of reference/ice.md) |
| compiled_by | play-editor (#465, Level 3 pilot recompile per ADR 025; #467 Batch C) |
| pipeline_position | end |
| workflow_structure | loop (skeleton: pre-flight → loop ≤5 → validation → gated close) |
| stop_condition | stop-condition.yaml (D1–D4), gate live at Step C0 |
| iteration_cap | 5 |
| domain_agents | 0 |
| utility_agents | 2 (repo-orchestrator — judgment path only; project-orchestrator) |
| skills_reused | analyze-changes (judgment path), resolve-issues, manage-issue |
| scripts | 5 (preflight.py, analyze_changeset.py, execute_commits.py, check_stop_condition.py, session_stamp.py) |
| step_evals | 9 (SE-1…SE-9) |
| scenario_evals | 6 (SCE-1…SCE-6) |
| recovery_entries | 9 (one per failure condition; 6 autonomous / 3 human) |
Compiled note (#484, scripted chain — already compliant)
commit-change already embodies the #484 tool-first rule and needs no rewiring: C8 mandates
"mechanical work runs as bundled scripts, not model inference" — the changeset scan
(analyze_changeset.py), a trivial changeset's whole analysis, and commit execution
(execute_commits.py) all run as scripts, and an agent is dispatched ONLY when the script
classifies the changeset multi-concern, i.e. when grouping genuinely requires judgment. That
is exactly #484's principle (mechanical git/gh in scripts; agents only for judgment). Unlike
the four sibling chain plays, no operation here was an agent wrapper over a fixed git
sequence, so no ICE constraint changed and the fingerprint stands. This note records the
audit; the play is unchanged.
Compiled note (#467 Batch C)
The loop's conditional checkpoint went off by config for this project
(gates.plays.commit-change: off) with its replacement visibility checks in the same
change: scanner warnings are carried into the delivery report's Run Summary (F9, SE-9,
REC9 — never silently dropped); a low-confidence mapping proceeds recorded as
low-confidence, never silently upgraded; the sensitive-file BLOCK stays a hard halt at
the scan, untouched by the switch. The checkpoint machinery is KEPT — the switch
resolves per standards/rules/gate-config.md; flipping the config back to on restores
the wait; skips are always recorded in evidence. No new scripts. Owned by the compiled
play (ICE recompile, not a direct edit).
Compiled note (#438, absorbed on recompile)
analyze_changeset.py's secret scanner distinguishes artifact types: content-pattern
matches in prose/standards artifacts (pattern carriers by design) are non-blocking warnings
surfaced at the checkpoint; filename matches and content matches in runtime/config paths
hard-block. C6's guarantee is unchanged: actual secrets never commit. Formerly a direct-edit
deviation note; owned by the compiled play since the #465 recompile.