| name | cleanup-feature |
| description | Merge approved PR, migrate open tasks, archive OpenSpec proposal, and cleanup branches |
| category | Git Workflow |
| tags | ["openspec","archive","cleanup","merge","merge-queue"] |
| triggers | ["cleanup feature","merge feature","finish feature","archive feature","close feature","linear cleanup feature","parallel cleanup feature","parallel merge feature","parallel finish feature"] |
Cleanup Feature
Merge an approved PR, migrate any open tasks to coordinator issues or a follow-up proposal, archive the OpenSpec proposal, and cleanup branches.
Arguments
$ARGUMENTS - OpenSpec change-id (optional, will detect from current branch or open PR)
Optional flags:
--post-merge - PR was already merged by /merge-pull-requests; skip PR merge and pre-merge validation stages, then archive/spec-sync/cleanup local remnants.
--pr <number> - PR number to verify when using --post-merge.
Prerequisites
- PR has been approved
- All CI checks passing
- Run
/implement-feature first if PR doesn't exist
- Recommended: Run
/validate-feature first to verify live deployment
OpenSpec Execution Preference
Use OpenSpec-generated runtime assets first, then CLI fallback:
- Claude:
.claude/commands/opsx/*.md or .claude/skills/openspec-*/SKILL.md
- Codex:
.codex/skills/openspec-*/SKILL.md
- Gemini:
.gemini/commands/opsx/*.toml or .gemini/skills/openspec-*/SKILL.md
- Fallback: direct
openspec CLI commands
Coordinator Integration (Optional)
Use docs/coordination-detection-template.md as the shared detection preamble.
- Detect transport and capability flags at skill start
- Execute hooks only when the matching
CAN_* flag is true
- If coordinator is unavailable, continue with standalone behavior
Steps
0. Detect Coordinator and Read Handoff
At skill start, run the coordination detection preamble and set:
COORDINATOR_AVAILABLE
COORDINATION_TRANSPORT (mcp|http|none)
CAN_LOCK, CAN_QUEUE_WORK, CAN_HANDOFF, CAN_MEMORY, CAN_GUARDRAILS
If CAN_HANDOFF=true, read latest handoff context before merge/archive actions:
- MCP path:
read_handoff
- HTTP path:
"<skill-base-dir>/../coordination-bridge/scripts/coordination_bridge.py" try_handoff_read(...)
On handoff failure/unavailability, continue with standalone cleanup and log informationally.
1. Determine Change ID and Setup Cleanup Worktree
CHANGE_ID=$ARGUMENTS
openspec show $CHANGE_ID
Launcher Invariant: The shared checkout is read-only. Perform all cleanup operations in a worktree:
eval "$(python3 "<skill-base-dir>/../worktree/scripts/worktree.py" setup "$CHANGE_ID" --agent-id cleanup)"
cd "$WORKTREE_PATH"
CLEANUP_BRANCH="$WORKTREE_BRANCH"
eval "$(python3 "<skill-base-dir>/../worktree/scripts/worktree.py" resolve-branch "$CHANGE_ID" --parent)"
FEATURE_BRANCH="$BRANCH"
1.5. Post-Merge Mode
If --post-merge is present, this skill is being called by /merge-pull-requests after that skill already merged the PR and pulled latest main.
In post-merge mode:
- Require
--pr <number> unless the merged PR can be resolved unambiguously from the change-id.
- Verify the PR is merged before touching archives or local remnants:
gh pr view "$PR_NUMBER" --json number,state,mergedAt,headRefName,baseRefName
- Continue only when
state is MERGED.
- Skip Step 2, Step 2.5, Step 2.5a, Step 2.5b, Step 2.6, Step 3, and Step 3.5.
- Continue at Step 4 to fetch latest main, then archive, validate, commit, push, and remove local branches/worktrees.
- Treat local branch/worktree deletion as approval-scoped: remove only remnants for this
CHANGE_ID; do not force-delete dirty worktrees or unmerged local branches unless the operator explicitly approves that separate action.
2. Verify PR is Approved
gh pr status
gh pr view "$FEATURE_BRANCH"
Confirm PR is approved and CI is passing before proceeding.
2.5. Pre-Merge Validation Gate
Check whether Docker-dependent validation has been run. Cloud-created PRs pass environment-safe checks during implementation but may lack deployment-based validation.
If validation-report.md exists at openspec/changes/<change-id>/ with deploy/smoke/security/e2e phases completed, skip this step.
Otherwise, if Docker is available (docker info succeeds), run the missing phases:
/validate-feature <change-id> --phase deploy,smoke,security,e2e
This delegates to the canonical validation skill for service lifecycle, smoke tests, security scanning, and E2E. The resulting validation-report.md is committed to the PR branch.
If Docker is not available, warn the operator that deployment validation was skipped and let them decide whether to proceed.
If any phase fails, present findings and let the operator decide: fix, re-validate, or proceed anyway.
2.5a. Pre-Merge Validation Gate
Programmatic enforcement — run the gate check script before merge:
python3 skills/validate-feature/scripts/gate_logic.py \
openspec/changes/<change-id>/validation-report.md
This checks all required phases (smoke tests, security scan, E2E tests) in validation-report.md:
- Exit code 0 → all phases passed, proceed to merge
- Exit code 1 → one or more phases failed/missing/skipped → HALT
If the gate halts:
- Re-run the failing phases:
/validate-feature <change-id> --phase deploy,smoke,security,e2e
- Re-check the gate
- Only if re-run fails AND the user explicitly requests override: add
--force
python3 skills/validate-feature/scripts/gate_logic.py \
openspec/changes/<change-id>/validation-report.md --force
This is a hard gate — merge is blocked until all required phases pass or the user explicitly overrides.
2.5b. Holdout Gate Check (If Rework Report Exists)
If openspec/changes/<change-id>/rework-report.json exists, check whether holdout scenario failures block cleanup:
REWORK_REPORT="openspec/changes/$CHANGE_ID/rework-report.json"
if [[ -f "$REWORK_REPORT" ]]; then
python3 -c "
import json, sys
data = json.load(open('$REWORK_REPORT'))
summary = data.get('summary', {})
if summary.get('has_blocking_holdout'):
holdout_ids = [f['scenario_id'] for f in data.get('failures', [])
if f.get('visibility') == 'holdout' and f.get('recommended_action') == 'block-cleanup']
print(f'HALT: Holdout scenario failures block cleanup: {holdout_ids}')
sys.exit(1)
print('Holdout gate: clear')
"
fi
If the holdout gate halts:
- Return to
/iterate-on-implementation to address holdout failures
- Re-run
/validate-feature to regenerate the rework report
- Only proceed if the rework report no longer contains blocking holdout failures
The process-analysis.md artifact, if present, is consumed read-only at this stage for inclusion in the PR description and session log. It is NOT regenerated during cleanup.
2.6. Merge Queue Integration [coordinated only]
These steps run only when coordinator is available with CAN_MERGE_QUEUE and CAN_FEATURE_REGISTRY capabilities.
2.6a. Enqueue: enqueue_merge(feature_id="<change-id>", pr_url="<pr-url>")
2.6b. Pre-merge checks: run_pre_merge_checks(feature_id="<change-id>") -- verifies no new resource conflicts
2.6c. Check merge order: get_next_merge() -- inform user if another feature has higher priority
2.6d. Cross-feature rebase: If other features merged since branching, rebase on origin/main
3. Merge PR
The merge command integrates the pre-merge gate — it will refuse to merge unless the gate passes:
python3 skills/merge-pull-requests/scripts/merge_pr.py merge <pr_number> \
--origin openspec \
--validation-report openspec/changes/<change-id>/validation-report.md
gh pr merge "$FEATURE_BRANCH" --rebase --delete-branch
Explicit user override (only when user explicitly requests):
python3 skills/merge-pull-requests/scripts/merge_pr.py merge <pr_number> \
--origin openspec \
--validation-report openspec/changes/<change-id>/validation-report.md \
--force
Strategy rationale: OpenSpec PRs use rebase-merge by default because agent-authored commits follow conventional format and encode design intent (interface → implementation → tests). Preserving this history improves git blame and git bisect for future agents. Use squash only if the PR has noisy WIP commits.
3.5. Mark Merged in Registry [coordinated only]
If CAN_MERGE_QUEUE=true: mark_merged(feature_id="<change-id>") -- marks feature completed, frees resource claims, removes from merge queue.
4. Update Local Repository
git fetch origin main
After merge, refresh project-global architecture artifacts:
make architecture
4.5. Fast-Forward Submodule Main Branches
If the merged PR bumped any submodule gitlink SHAs (e.g., personas/ mount points), their own main branches must be fast-forwarded to avoid silent divergence.
python3 "<skill-base-dir>/scripts/sync_submodules.py" \
--repo-dir "." \
--feature-branch "$FEATURE_BRANCH"
This script:
- Detects submodules whose SHA changed via
git diff --raw main@{1} main (mode 160000 = gitlink)
- For each changed submodule:
- Fetches
origin main inside the submodule
- Switches to main and fast-forwards (
--ff-only) to the SHA the parent records
- Pushes main to the submodule's remote
- Deletes the submodule's feature branch (local + remote)
- If the fast-forward fails (non-FF changes on submodule main), logs a warning and stops for that submodule — does NOT force-merge
Credential handling: The same credentials may not work for the submodule's private remote. On auth/push failure, the script:
- Logs a clear diagnostic (submodule path, remote URL, error)
- Prints operator handoff commands to run manually
- Continues with the rest of cleanup (does not abort)
If no submodules changed, this step is a no-op.
5. Migrate Open Tasks
Before archiving, check for incomplete tasks in the proposal. Open tasks must not be silently dropped.
5a. Detect open tasks
Read openspec/changes/<change-id>/tasks.md and scan for unchecked items (- [ ]).
If all tasks are checked (- [x]), skip to Step 6.
If there are open tasks, collect them with their context:
- Task number and description (e.g.,
3.2 Add retry logic for failed requests)
- Parent task group heading (e.g.,
### 3. Error Handling)
- Dependencies from the group's
**Dependencies**: line
- File scope from the group's
**Files**: line
5b. Choose migration target
Ask the user which migration strategy to use:
Option A — Coordinator issues (if coordinator is available):
For each open task group that has unchecked items, use the coordinator's issue tracking MCP tools:
issue_create(
title="<task description>",
description="Followup from OpenSpec <change-id>. File scope: <files>",
issue_type="task",
priority=5,
labels=["followup", "openspec:<change-id>"]
)
# If tasks have dependencies on each other, create with depends_on
issue_create(
title="<dependent task>",
depends_on=["<parent-issue-id>"],
labels=["followup", "openspec:<change-id>"]
)
Include in each issue description:
- Original OpenSpec change-id for traceability
- The file scope from the task group
- Any relevant context from
proposal.md or design.md
Option B — Follow-up OpenSpec proposal (default if coordinator is not available):
Create a new proposal using runtime-native new/continue workflow (or CLI fallback) with:
- Change-id:
followup-<original-change-id> (e.g., followup-add-retry-logic)
- proposal.md: Reference the original change-id, explain these are remaining tasks
- tasks.md: Copy only the open (unchecked) tasks, preserving their numbering, dependencies, and file scope
- specs/: Copy any spec deltas that correspond to the open tasks (if the original proposal's spec changes included requirements that depend on unfinished work)
Let the user review and confirm the follow-up proposal before proceeding.
5c. Mark original tasks.md
After migration, annotate the original tasks.md to record where open tasks went:
## Migration Notes
Open tasks migrated to [coordinator issues labeled `openspec:<change-id>`] | [follow-up proposal `followup-<change-id>`] on YYYY-MM-DD.
This annotation is preserved in the archive for traceability.
5b. Append Session Log
Append a Cleanup phase entry to the session log, capturing merge strategy and task migration decisions. If no session-log.md exists from prior phases, create it and summarize the change from context.
Phase entry template:
---
## Phase: Cleanup (<YYYY-MM-DD>)
**Agent**: <agent-type> | **Session**: <session-id-or-N/A>
### Decisions
1. **<Decision title>** — <rationale>
### Alternatives Considered
- <Alternative>: rejected because <reason>
### Trade-offs
- Accepted <X> over <Y> because <reason>
### Open Questions
- [ ] <unresolved question>
### Context
<2-3 sentences: merge strategy, task migration decisions, archive outcome>
Focus on: Merge strategy (squash vs regular), open task migration decisions, any cleanup issues encountered.
Sanitize-then-verify:
python3 "<skill-base-dir>/../session-log/scripts/sanitize_session_log.py" \
"openspec/changes/<change-id>/session-log.md" \
"openspec/changes/<change-id>/session-log.md"
Read the sanitized output and verify: (1) all sections present, (2) no incorrect [REDACTED:*] markers, (3) markdown intact. If over-redacted, rewrite without secrets, re-sanitize (one attempt max). If sanitization exits non-zero, skip session log and proceed.
git add "openspec/changes/<change-id>/session-log.md"
If session log append or sanitization fails at any point, log a warning and proceed to archiving without the session log. This step is non-blocking.
5c. Pre-Launch Checklist
Before triggering staged rollout, every item below MUST pass. This is a gate, not a suggestion — if any item is red, halt and remediate before promoting traffic.
If any item fails, do NOT proceed to staged rollout. Open a follow-up issue, mark this change rollout-blocked, and return to the relevant phase (validate, iterate, or doc-update).
5d. Staged Rollout (post-merge)
Promote the change through fixed traffic stages with a monitoring window between each stage. The rollout sequence is:
| Stage | Traffic | Min monitoring window | Promotion criteria |
|---|
| 1 | 5% | 30 min (or 1 full traffic cycle) | All thresholds below green |
| 2 | 25% | 1 hour | All thresholds green AND no Stage 1 anomalies replayed |
| 3 | 50% | 2 hours | All thresholds green AND error budget intact |
| 4 | 100% | continuous | Steady-state, alerting active |
Reference: This skill assumes the change is shipped behind a feature flag (per the OpenSpec workflow). The flag, not a deploy, gates the traffic split. If the change is NOT flag-gated (e.g., infra-only), use blue/green or weighted DNS instead — the stages still apply.
Decision thresholds (promote to next stage only if ALL are true)
- Error rate < 2× baseline for the changed surface (compare to the 24h baseline window)
- p95 latency increase < 50% vs baseline
- Zero data integrity errors in the rollout window (FK violations, checksum mismatches, dropped writes)
- Zero security regressions (no new auth failures, no PII in logs, no expanded permission grants)
- Business KPI within ±10% of baseline (conversion, signup, etc., where applicable)
Rollback triggers (automatic — flip the flag back if ANY is true)
- Error rate ≥ 2× baseline sustained for >5 minutes
- p95 latency ≥ 50% above baseline sustained for >5 minutes
- ANY data integrity error (no threshold — one is enough)
- ANY security regression
- Pager-grade alert fired against the changed surface
- Operator intervention requested (manual rollback always wins)
On rollback: flip the flag to 0%, capture the rollout-state snapshot (metrics, logs, traces from the failure window), file an issue tagged rollback-postmortem, and DO NOT re-promote until root cause is identified.
6. Archive OpenSpec Proposal
Preferred path:
- Use runtime-native archive workflow (
opsx:archive equivalent for the active agent).
CLI fallback path:
openspec archive <change-id> --yes
openspec validate --strict
This archives the change, merges delta specs, and validates repository integrity.
Regenerate the decision index (REQUIRED — in the SAME commit as the archive).
docs/decisions/ is a derived artifact generated from openspec/changes/ via
make decisions (skills/explore-feature/scripts/archive_index.py --emit-decisions).
Archiving moves openspec/changes/<change-id>/ into archive/<date>-<change-id>/,
which is exactly what makes the committed index stale. Regenerate it now and stage
it so the refresh lands in the same commit as the archive move — never as a
follow-up. Skip this and main fails the validate-decision-index CI job for every
subsequent PR until someone notices (writer-drift: derived artifact + source mutation
must be bound together). Run it after EITHER archive path above (preferred or CLI):
make decisions
git add docs/decisions/
Before committing the archive, confirm there is no leftover drift:
make decisions && git diff --quiet -- docs/decisions/ \
&& echo "decision index clean" \
|| { echo "docs/decisions/ still drifting — stage it into the archive commit"; git add docs/decisions/; }
7. Verify Archive
openspec list --specs
ls openspec/changes/archive/<change-id>/
openspec validate --strict
8. Cleanup Local Branches
git branch -d "$FEATURE_BRANCH" 2>/dev/null || true
git fetch --prune
If CAN_LOCK=true, perform best-effort lock cleanup for files touched on the feature branch:
- Compare
main...$FEATURE_BRANCH changed files
- Attempt release for each lock owned by this agent/session
- Treat release failures as warnings (do not block cleanup)
8.5. Remove Worktrees
Remove all worktrees for this feature (including the cleanup worktree).
Submodule handling: worktree.py teardown automatically handles worktrees containing initialized submodules:
- Runs
git submodule deinit -f --all inside the worktree first
- Attempts normal
git worktree remove
- If removal still fails with "working trees containing submodules cannot be moved or removed", falls back to
git worktree remove --force (safe because the worktree's branch is already merged/pushed)
- Other removal errors (dirty tree, conflicts) are NOT force-overridden — they surface for operator attention
cd "$(git rev-parse --git-common-dir | sed 's|/.git$||')"
python3 "<skill-base-dir>/../worktree/scripts/worktree.py" teardown "${CHANGE_ID}" --agent-id cleanup
AGENT_FLAG=""
if [[ -n "${AGENT_ID:-}" ]]; then
AGENT_FLAG="--agent-id ${AGENT_ID}"
fi
python3 "<skill-base-dir>/../worktree/scripts/worktree.py" teardown "${CHANGE_ID}" ${AGENT_FLAG}
python3 "<skill-base-dir>/../worktree/scripts/worktree.py" gc
9. Final Verification
git status
pytest
9.5. Notify Dependent Features [coordinated only]
If CAN_FEATURE_REGISTRY=true, re-analyze conflicts for active features. Features that were PARTIAL or SEQUENTIAL may upgrade to FULL now that this features claims are freed.
10. Clear Session State
- Clear todo list
- Document any lessons learned in
CLAUDE.md if applicable
- If
CAN_HANDOFF=true, write a final handoff summary with merge status, migration notes, archive outcome, and follow-up references
Output
- PR merged to main
- Open tasks migrated to coordinator issues or follow-up OpenSpec proposal (if any)
- OpenSpec proposal archived
- Specs updated in
openspec/specs/
- Branches cleaned up
- Repository in clean state
Complete Workflow Reference
/plan-feature <description> # Create proposal → approval gate
/implement-feature <change-id> # Build + PR → review gate
/validate-feature <change-id> # Deploy + test → validation gate (optional)
/cleanup-feature <change-id> # Merge + archive → done
/cleanup-feature <change-id> --post-merge --pr <number> # Archive after /merge-pull-requests already merged
Common Rationalizations
| Rationalization | Why it's wrong |
|---|
| "CI is green, just send 100% — staged rollout is for big changes" | Staged rollout catches regressions CI cannot see (production traffic shape, real data, real concurrency). Skipping it pushes the failure mode onto users instead of synthetics. |
| "We don't have feature flags here, so we'll just merge and watch" | "Watching" without a flip-back path means the only rollback is a revert PR + redeploy, which is minutes-to-hours slow. A flag (or blue/green, or weighted DNS) is what makes rollback fast enough to matter. |
| "The pre-launch checklist is overkill for a small change" | Most rollback incidents come from "small" changes where the team waived a checklist item. The checklist's value is exactly that it doesn't bend for size. |
| "Latency went up 60%, but it's still within SLO — we'll keep going" | A 60% jump is a regression even when it's inside SLO. SLOs are the floor, not the rollback threshold. The rollback threshold is the threshold. |
| "We can skip archiving until tomorrow" | The archive step closes the loop on spec drift. Postponing it leaves openspec/changes/<id>/ and openspec/specs/ out of sync, which silently breaks the next agent's view of the world. |
"make decisions is a separate cleanup — I'll regenerate docs/decisions/ later" | Later never comes in the same commit. The archive move stales the index the instant it lands; a deferred regen means main fails validate-decision-index and blocks every unrelated PR until someone bisects it (this is exactly the 2026-05-12 incident). The regen costs one command — bind it to the archive commit. |
Red Flags
- The merge commit landed on main but no rollout-stage record exists in the session log (silent jump to 100%).
- A staged rollout was started but the monitoring window between stages was <30 minutes for Stage 1 (or skipped entirely).
- The pre-launch checklist appears in the session log as "checked" but no artifact (CI link, scan output, validation report) is cited.
- A rollback trigger fired but the flag was NOT flipped back — instead the team "kept watching".
- The OpenSpec change-id is archived but
openspec validate --strict was not re-run after the archive.
- The change was archived but
make decisions was not run (or docs/decisions/ was not staged into the archive commit) — main will fail validate-decision-index on the next PR.
- Open tasks were silently dropped (no migration to coordinator issues or follow-up proposal).
Verification
- The session log's Cleanup phase entry cites the rollout sequence (5%→25%→50%→100%) with timestamps for each stage promotion AND lists the metrics observed at each stage (error rate, p95, data-integrity count).
- Pre-launch checklist is recorded with a concrete artifact reference per item (CI run URL, security scan report path, validation-report.md, runbook link, dashboard link).
openspec validate --strict was re-run AFTER openspec archive and exited zero. The output is captured in the cleanup transcript.
3a. make decisions was run after the archive and docs/decisions/ was staged into the same commit as the archive move; git diff --quiet -- docs/decisions/ is clean before the commit (no pending regeneration).
- The feature flag (or equivalent traffic gate) is identified by name in the session log, with the kill-switch procedure documented.
- If a rollback trigger fired during rollout, an issue tagged
rollback-postmortem exists and the flag is currently at 0%.
- Open tasks from
tasks.md are accounted for: either all checked, or migrated to coordinator issues / follow-up proposal with a Migration Notes line in the original tasks.md.