Skip to content
Back to skills

Merge Change

ASecurity

Merge an approved change where its branch lands — its plan''s branch for an issue in a plan, main otherwise (a plan''s own branch lands on main while the plan is in progress) — switch to that branch and pull the latest, and delete the change''s branch (local and remote). The final step of the end sequence in the ProductOS command model. Use when a reviewed, approved PR is ready to land.

  • 3 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 7, 2026
toolspythongobashgit

Works with

  • cli

Security analysis

A100/100

Pro scans all 11 files and shows the line behind each finding

Scanned November 7, 2026

npx -y skills add intent-driven-ai/garura --skill merge-change --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Merge Change?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Merge Change
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/intent-driven-ai-merge-change/badge)](https://www.skillsdirectory.com/skills/intent-driven-ai-merge-change)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: merge-change
position: end
description: 'Merge an approved change where its branch lands — its plan''s branch for an issue in a plan, main otherwise (a plan''s own branch lands on main while the plan is in progress) — switch to that branch and pull the latest, and delete the change''s branch (local and remote). The final step of the end sequence in the ProductOS command model. Use when a reviewed, approved PR is ready to land.'
user-invocable: true
---

# merge-change

Merge an approved change where its branch lands — its plan's branch for an issue in a plan,
main otherwise; a plan's own branch lands on main while the plan is still in progress. Merge
the PR, switch to that branch and pull the latest, and delete the change's branch (local and
remote). An issue that lands on its plan's branch is closed there.

**Pipeline position: end** (fourth step of the end sequence: commit-change →
propose-change → check-change → merge-change). The D2 pipeline-position rule injects this
sequence into any play declared `position: end`. Independent, invokable play; ordering is
enforced by pre-flight (the PR must be approved and mergeable). **Supersedes the old
merge-pr play.**

## Compiled From

This play was compiled from the merge-change ICE (`reference/ice.md`) by edit-play
(#466 Batch A, Level 3 rollout per ADR 025; #467 Batch C gate ruling; #484 scripted chain;
#673 plan targets). 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 edit-play.
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 merge work is
mechanical — a fixed command sequence with zero judgment — so you run it through **bundled
scripts** calling git/gh directly, not an agent dispatch (C8, #484). Reading the merge
state runs in `read_merge_state.py`; the merge + sync + cleanup runs in `merge_pr.py`; both
reach the code host through the bundled `platform_adapter.py`. No agent boots to do this.

**Forbidden:** running the mechanical git/gh work through an agent dispatch (the #484 bug);
ad-hoc `git`/`gh` reasoned out in play prose (it runs in the bundled scripts); merging
without first verifying the approval verdict and mergeability.

**Agents:** **none.** This play holds no judgment step — every operation is a bundled
script. Agent dispatch is reserved for genuine judgment, of which merge-change has none, so
it dispatches **zero agents** (domain or utility).

## Pre-flight

| Check | Constraint | Action on Failure |
|-------|-----------|-------------------|
| Resolve working base + config (`.garura/core/config.yaml`) | — | Hard halt |
| An open PR exists for the branch | — | Graceful exit |
| Platform CLI available (`gh`) | C2 | Hard halt |
| PR has an approval verdict from check-change | C1 | Hard halt |
| PR is mergeable (no conflicts, required checks not failing) | C4 | Hard halt |
| Already-merged check | C5 | Graceful no-op |

Resolve the deterministic pre-flight facts with the bundled resolver — config tokens,
`branch`, `issue` (from the branch name), `evidence_record`:

```
python3 scripts/preflight.py --play merge-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-merge-change.json" \
    --cwd "$(pwd)" --branch "$(git branch --show-current)"
```

The open-PR check, `gh` availability, the approval verdict, mergeability, and the
already-merged check are live host state resolved by the bundled `read_merge_state.py`
(Step 1, via `platform_adapter.py`) — no agent (C8). If the PR is already merged, exit
cleanly as a no-op (C5).

## 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] Verify Approval + Mergeable     blockedBy: []
[T2] Confirm Merge                   blockedBy: [T1]
[T3] Merge + Sync + Cleanup          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 — Verify Approval + Mergeable** · Owner: play (script) · Depends on: pre-flight
Run the bundled script — it reads the PR state (mergeable, already-merged, required-checks)
through `platform_adapter.py` and the approval from the review verdict, and writes the gate
(no agent, C8):

```
python3 scripts/read_merge_state.py --config .garura/core/config.yaml \
    --branch "$(git branch --show-current)" \
    --verdict "{stm_base}{issue}/review/decision.yaml" \
    --out "{stm_base}{issue}/review/merge-gate.json"
```

`merge-gate.json` records `{ approved: true|false, mergeable: true|false, already_merged,
checks_pass, pr_number, pr_merged, branch_deleted }` — the record ALWAYS ends with the two
Done-means booleans
(C6), on both paths including the no-op: `pr_merged` is true on a fresh merge AND on the
already-merged no-op path; `branch_deleted` is true when the feature branch is gone
locally and on origin (on the no-op path, true when it was already gone). On the no-op
path Step 1 finalizes both fields itself; otherwise Step 3 finalizes them after cleanup.
Then read where the change lands (C9), from the branch name, never by guessing:

```
python3 scripts/resolve_target.py --branch "$(git branch --show-current)" \
    --out "{stm_base}{issue}/review/target.json"
```

`target.json` records `{ kind, plan_id, issue, base }`. A plan's own branch lands on main
like any approved change, with issues of the plan still open; open work never holds it.
**SE-1 (F1/C1):** `merge-gate.json.approved == true` — an approval verdict exists before
any merge.
**SE-2 (F4/C4):** `merge-gate.json.mergeable == true` — the PR has no conflicts and no
failing required checks before any merge.

**Step 2 — Confirm Merge (class: one-way-door, pinned)** · Owner: play · Depends on: Step 1
Merging is outward-facing and irreversible on `main`, so this checkpoint is **pinned**
(C7): no config value, policy, or ledger turns it off — the land on `main` always keeps
its human beat (the #467 re-pin correction). *Skip as a no-op when `already_merged` is
true (C5).*
- **Machine wall first (verified before presenting).** Confirm the three preconditions from
  `merge-gate.json` (Step 1, written by `read_merge_state.py`): `approved: true`,
  `mergeable: true`, and `checks_pass: true` (no failing required checks). These three are
  the guard against breaking main, for a plan's branch too (C9). If any is false, HALT
  exactly as pre-flight does (REC7), naming the failed precondition — there is nothing to
  approve, so the human is not asked.
- **Then the human beat.** With all three true, present the PR, its approval, and
  mergeability, and wait for a typed approval. Approve → continue to Step 3; cancel →
  halt without merging. The merge proceeds solely on the typed approval; the three
  booleans are recorded as what the merge stood on.

**Step 3 — Merge + Sync + Cleanup** · Owner: play (script) · Depends on: Step 2
Run the bundled script — it refuses unless the PR's base equals `target.json.base`
(`on_target`), merges the PR (via `platform_adapter.py`), switches to that base (main, or the
plan's branch) and pulls, deletes the change's branch locally and on remote, closes the
issue when it landed on its plan's branch (and its capability once every issue under it is
closed — C9), sets that issue's item Done in the plan file with the installed plan tool
(`.garura/core/tools/plan/plan_status.py`; only that item, no roll-up — C9, #1007) and records
it as `plan_marked` (the plan file joins the records commit only when `plan_marked.changed` is
true; a `plan_marked.error` leaves the item open and the delivery report names it — never
silent), finalizes `merge-gate.json`'s
two Done-means (`pr_merged: true`, `branch_deleted: true`) on both paths incl. the no-op
(C6), **commits the run records on the branch the change landed on** (main, or the plan's
branch; #491) — `merge-gate.json` + the
review dir (+ the plan file when its item was set Done), message `chore(stm): record merge-change run records (#<issue>)` (ADR 012,
evidence self-commit) — and **pushes that records commit** (#493): the pinned Confirm
Merge approval (C7) already put the human on the outward act, so the bookkeeping push
rides on that authorization, no second ask. Its output carries `records_committed` and
`records_pushed`, so a leftover or an unpushed record is a visible state, never silent.
No agent (C8):

```
python3 scripts/merge_pr.py --config .garura/core/config.yaml \
    --branch "$(git branch --show-current)" --base "<target.json base>" \
    --gate "{stm_base}{issue}/review/merge-gate.json" \
    --issue <issue> --records-dir "{stm_base}{issue}/review/" \
    $( [ "<target.json kind>" = "issue-in-plan" ] && echo --close-issue <issue> \
        --plans-dir "<plans.base-path>" --plan-id "<target.json plan_id>" )
```

The tree is clean after the step and the branch the change landed on is level with origin; a
`records_committed: false` output is a failed step, and a `records_pushed: false` output
leaves the push owed and visibly reported (soft-fail — offline or a host guard), never
silent.
**SE-3 (F2/C2):** after the step, the current branch is `target.json.base` and is up to date
with its origin copy.
**SE-9 (F9/C9):** `target.json` was written by `resolve_target.py` before the merge;
`merge-gate.json.on_target` is true; for an issue in a plan, the issue is in
`merge-gate.json.issues_closed` and `merge-gate.json.plan_marked.to` reads Done (the item's
parent unchanged); a plan's branch with open issues was not held off main
for them — when the machine wall held and the human approved, it was merged.
**SE-4 (F3/C3):** the feature branch no longer exists locally or on remote.
**SE-5 (F5/C5):** when the PR was already merged at pre-flight, no second merge was
attempted and the run ended as a clean no-op.
**SE-6 (F6/C6):** `merge-gate.json` reads `pr_merged: true` + `branch_deleted: true`; the
records commit exists on the branch the change landed on (`target.json.base`); the stop-condition verdict reads held before any
COMPLETED close.
**SE-7 (F7/C7):** the merge proceeded only on the pinned human approval, AND the Step 2
record shows all three machine preconditions true — `approved: true`, `mergeable: true`,
and no failing required checks — verified before the human was presented; no merge
proceeded without both the wall and the human beat.
**SE-8 (F8/C8):** the merge state read and the merge+sync+cleanup ran as the bundled
scripts (`read_merge_state.py`, `merge_pr.py`) — no agent was dispatched for the mechanical
git/gh work; the play's agent count is zero.

### Phase: Scenario Validation

**Step 4 — Scenario Evals** · Owner: play · Depends on: Step 3
- **SCE-1 (S1 — developer, approved PR):** the PR is merged; the checkout is on updated
  main; the feature branch is gone locally and on remote; `merge-gate.json` reads
  `pr_merged: true` and `branch_deleted: true`; the records commit exists on main; the
  stop-condition verdict reads held.
- **SCE-2 (S2 — developer, not approved):** with no approval verdict, the run halted at
  pre-flight and the PR is not merged.
- **SCE-3 (S3 — developer, not mergeable):** with conflicts or failing checks, the run
  halted at pre-flight and the PR is not merged.
- **SCE-4 (S4 — developer, already merged):** a re-run on a merged PR was a clean no-op,
  no second merge attempted; `merge-gate.json` reads `pr_merged: true` and
  `branch_deleted: true`; the records commit exists on the branch the change landed on
  (`target.json.base`); the stop-condition verdict reads held.
- **SCE-5 (S5 — developer, issue in a plan):** the PR merged into `plan/<P>/base`;
  `merge-gate.json` reads `on_target: true`, lists the issue in `issues_closed` and reads
  `plan_marked.to: Done`; the plan file reads the item Done; the current branch is
  `plan/<P>/base`; the issue's branch is gone.
- **SCE-6 (S6 — developer, plan paused in progress):** `target.json` reads `kind: plan` and
  `base: main`; `merge-gate.json` reads `on_target: true`, `pr_merged: true` and
  `branch_deleted: true`; the checkout is on updated main; the plan's open issues are still
  open.

### 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`).

```bash
# --- 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/merge-change/"   ; 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-merge-change.json")
# Stop-condition gate (#464) — Step C0: this play carries a baked manifest, so the
# gate is LIVE. Evaluate the Done means 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-merge-change.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 — first fix the state per REC6 (delete a
surviving branch, rewrite the gate record) and re-evaluate; the close stays HALTED until
the verdict reads held (F6/C6). An unevaluable verdict is never a pass.

**Step C1 — Write evidence file.** Gated by the resolved `evidence.record` flag (global +
per-play `evidence.plays.merge-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 `merge-change`, run_id
`merge-change-${ts}`, issue, started_at/completed_at, status per C0, artifacts produced:
`merge-gate.json`, the merge commit SHA, the records commit SHA, the stop-condition
verdict; step/scenario eval results; checkpoint decision
from Step 2 (or its config-skip row with the three precondition booleans, C7);
commit reference = the merge commit SHA; and 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 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>.` (omitted when the mapped command is null), per `play-close.md`. Fill the `delivery-report.md` slots and output the
report: `## merge-change Delivered — #${issue}`, the Run Summary table, the Pipeline Steps
table from the task DAG, the Artifacts Produced table (incl. the merge commit), Next Steps
(the change has landed where its branch belongs: main, or its plan's branch), and a pointer to `$evidence_dest`. Always emitted; never
gated.

```bash
# --- end Standard Play Close ---
```

## Scenario Validation

| Scenario | Persona | Eval |
|----------|---------|------|
| S1 — approved PR | developer | SCE-1 |
| S2 — not approved | developer | SCE-2 |
| S3 — not mergeable | developer | SCE-3 |
| S4 — already merged | developer | SCE-4 |
| S5 — issue in a plan | developer | SCE-5 |
| S6 — plan paused in progress | developer | SCE-6 |

## Recovery

| For | Trigger | Direction | Handoff |
|-----|---------|-----------|---------|
| F1 | a merge was attempted without an approval verdict | halt and require check-change to approve the PR first | human |
| F2 | after merge the checkout isn't on the updated branch the change landed on | check out that branch (main or the plan's branch) and pull the latest | autonomous |
| F3 | the feature branch is still present after merge | delete the branch locally and on remote | autonomous |
| F4 | the PR is not mergeable | halt; conflicts must be resolved or failing checks fixed before retrying | human |
| F5 | a re-run on an already-merged PR | detect the merged state and no-op cleanly | autonomous |
| F6 | the run is about to close COMPLETED with the Done means unmet | fix the state — delete the surviving branch / rewrite the gate record — and re-evaluate; the close stays HALTED until the verdict reads held | autonomous |
| F7 | a merge would proceed without the pinned human approval, or a precondition (approved, mergeable, no-failing-required-checks) was not verified true before presenting | halt; on a failed precondition surface which one and do not present; if the human beat was skipped, present and wait | autonomous |
| F8 | a mechanical git/gh op ran through an agent instead of its bundled script | route it through the script (`read_merge_state.py` / `merge_pr.py`, via `platform_adapter.py`) and remove the agent dispatch | autonomous |
| F9 | the PR's base is not its target, an approved plan's branch was held off main for its open issues, a landed issue is still open, or its plan item is still open | on a wrong base, stop and retarget the PR (propose-change); on a plan held back for open work, drop the hold and land it when the machine wall holds; on a landed issue left open, close it; on a landed item left open in the plan file, set it Done with the plan tool | human |

## Pause and Resume

Steps run top to bottom. On entry, resolve the PR from the branch, check the status marker
and whether the PR is already merged, skip completed steps, reset any in-progress step to
pending, and continue. A re-run on an already-merged PR is a clean no-op (C5, F5).

## Compilation Metadata

| Field | Value |
|-------|-------|
| fingerprint | sha256:fa0f30167747872ebdc6f53252ec2387b2c536d637fbc81a1a8aecb6de8bf3a3 (of `reference/ice.md`) |
| compiled_by | edit-play (#466 Batch A; #467 Batch C; #484 scripted-chain; #673 plan targets; plans land on main while in progress; #1007 plan item follows its issue; #953 run records on the branch the change landed on) |
| pipeline_position | end |
| workflow_structure | B (fast execution flow, single-pass; stop-condition gated close) |
| stop_condition | stop-condition.yaml (D1–D4), gate live at Step C0 |
| domain_agents | 0 |
| utility_agents | 0 (#484 — mechanical git/gh moved to bundled scripts) |
| scripts | 7 (preflight.py, session_stamp.py, check_stop_condition.py, platform_adapter.py, read_merge_state.py, resolve_target.py, merge_pr.py) + test_merge_pr.py; tools: the plan tool's plan_status.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) |

Files in this skill

  • SKILL.md19.4 KB
  • reference/ice.md11.1 KB
  • scripts/check_stop_condition.py5 KB
  • scripts/merge_pr.py11.5 KB
  • scripts/platform_adapter.py19.9 KB
  • scripts/preflight.py7 KB
  • scripts/read_merge_state.py3.9 KB
  • scripts/resolve_target.py3.2 KB
  • scripts/session_stamp.py4.8 KB
  • scripts/test_merge_pr.py2.1 KB
  • stop-condition.yaml896 B

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.