# Cleanup Feature

> Merge approved PR, migrate open tasks, archive OpenSpec proposal, and cleanup branches

- Skill: `jankneumann/cleanup-feature` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add jankneumann/cleanup-feature`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jankneumann/cleanup-feature/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: jankneumann (https://skillmd.com/u/jankneumann)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/jankneumann/cleanup-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`.
- `--defer-commit` - **Off by default.** Only meaningful with `--post-merge`. Merge-driven mode: do all the cleanup work in the sync-point checkout, `git add` the result, and return without committing or pushing — the main-context convergence sync point makes the single commit. See Step 1.6.

## 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

```bash
# From argument or current branch
CHANGE_ID=$ARGUMENTS
# Or: CHANGE_ID=$(git branch --show-current | sed 's/^openspec\///')

# Verify
openspec show $CHANGE_ID
```

**Launcher Invariant**: The shared checkout is read-only. Perform all cleanup operations in a worktree:

```bash
eval "$(python3 "<skill-base-dir>/../worktree/scripts/worktree.py" setup "$CHANGE_ID" --agent-id cleanup)"
cd "$WORKTREE_PATH"

# The cleanup worktree is on its OWN scratch branch (with the --cleanup suffix),
# so that cleanup operations don't collide with a still-running implementation
# worktree. We need two distinct branch variables:
#
#   CLEANUP_BRANCH — this worktree's own branch (openspec/<change-id>--cleanup
#                    by default, or <override>--cleanup when OPENSPEC_BRANCH_OVERRIDE
#                    is set). Used for teardown.
#   FEATURE_BRANCH — the PARENT feature branch being merged/deleted. This is
#                    the branch implement-feature pushed and opened a PR against.
#                    Used for gh pr merge, git branch -d, and lock cleanup.
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:

```bash
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.

**Which branch does post-merge cleanup land on?** `main`. The archive move, the
merged spec deltas, and the regenerated decision index describe state that is
already merged, so they belong on `main` rather than in a second review round.
Historically this instruction said "commit and push" without naming a branch
while Step 1 set up a scratch `--cleanup` worktree, which read as ambiguous. It is
not: the destination is `main`, and Step 1.6 is the mode in which the sync point
does the landing on this skill's behalf.

### 1.6. Merge-Driven Deferred-Commit Mode (`--defer-commit`)

`--defer-commit` is **off by default** and is only meaningful alongside
`--post-merge`. Without `--defer-commit`, `--post-merge` behaves exactly as it
always has: archive, validate, then commit and push that work itself. The default
never moves, because a flag you would have to pass in order to *keep* today's
behavior is a silent breaking change to every existing caller.

Pass `--defer-commit` only when this skill is invoked **by the main-context
convergence sync point** (`/merge-pull-requests`, Step 11.6 — Main Context
Convergence). There, this skill is one of several cleanups running back to back
over a single merged `main` state, and the sync point — not this skill — produces
the single convergence commit that carries every cleanup's output together with
the deterministic context-refresh output. Committing per change would produce
N+1 commits for N archived changes.

**What deferred-commit mode changes**

- **No cleanup worktree.** Skip the Step 1 `worktree.py setup ... --agent-id
  cleanup` block. Operate directly in the sync-point checkout, which is already on
  `main` at the merged revision. Writing in that checkout is legitimate only
  because the sync point holds the guards for it — active-agent check, coordinator
  lock, and a pre-push compare-and-swap against `origin/main`. This skill does not
  acquire them and must not assume them on its own.
- **Stage, do not commit.** After the steps below, `git add` every path the
  cleanup touched — the archived change directory, the merged
  `openspec/specs/`, `docs/decisions/`, the session log, and any `tasks.md`
  migration note. Then **return**. Commit nothing. Push nothing. Create no tag, no
  branch, and no PR.
- **Skip the local-remnant steps that assume a commit already landed.** Step 8.5
  (worktree removal) and Step 9 (final verification on a clean tree) do not run in
  this mode: the tree is deliberately dirty-then-staged when this skill returns,
  and the sync point owns what happens next.
- **Report what you staged.** Return the change-id, the archive destination path,
  and the list of staged paths, so the sync point can compose one commit message
  covering every change in the pass.

**What deferred-commit mode does NOT change.** Steps 5 (open-task migration), 5b
(session log), 6 (`openspec archive`, spec-delta merge, `make decisions`), and 7
(`openspec validate --strict`) run exactly as in normal post-merge mode.
`--defer-commit` changes **who commits**, not who archives: `cleanup-feature`
remains the sole owner of OpenSpec task migration, `openspec archive`, and the
spec-delta merge into `openspec/specs/`. The sync point never archives, never
merges a spec delta, and never migrates a task.

**Deferring the commit is NOT deferring the decision-index regeneration.** Run
`make decisions` and stage its output in this mode too:

```bash
make decisions
git add docs/decisions/
```

`docs/decisions/` is derived from `openspec/changes/archive/`, so the archive move
performed moments earlier stales the committed index *immediately*. If the regen
were left for the sync point to remember, the convergence commit could land an
archive move with a stale index and `main` would fail the `validate-decision-index`
CI job for every unrelated PR until someone bisected it — the exact 2026-05-12
incident recorded in Common Rationalizations. The commit is deferred by one step;
the regeneration is not deferred at all. Both land in the one convergence commit.

**Partial failure.** If this skill fails partway through a multi-change cleanup
pass, everything already staged by the changes that succeeded is committed by the
sync point as a cleanup-only convergence commit and is **never discarded**. Do not
`git reset`, `git checkout --`, or `git stash` on the way out of a failure. Report
the failing change-id and the error, leave the index as it stands, and let the
sync point stop the pass from a clean, pushed state.

### 2. Verify PR is Approved

```bash
# Check PR status
gh pr status

# Or check specific PR (use the resolved FEATURE_BRANCH, not a hardcoded prefix)
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:

```bash
python3 "<skill-base-dir>/../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:
1. Re-run the failing phases: `/validate-feature <change-id> --phase deploy,smoke,security,e2e`
2. Re-check the gate
3. Only if re-run fails AND the user explicitly requests override: add `--force`

```bash
# Explicit user override (must be requested by user, never autonomous)
python3 "<skill-base-dir>/../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:

```bash
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:
1. Return to `/iterate-on-implementation` to address holdout failures
2. Re-run `/validate-feature` to regenerate the rework report
3. 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:

```bash
# Via merge_pr.py with gate enforcement (preferred)
python3 "<skill-base-dir>/../merge-pull-requests/scripts/merge_pr.py" merge <pr_number> \
  --origin openspec \
  --validation-report openspec/changes/<change-id>/validation-report.md

# Direct gh merge (only if gate already passed in step 2.5a)
# Uses the resolved FEATURE_BRANCH, which honors OPENSPEC_BRANCH_OVERRIDE
gh pr merge "$FEATURE_BRANCH" --rebase --delete-branch
```

**Explicit user override** (only when user explicitly requests):
```bash
python3 "<skill-base-dir>/../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

```bash
# From the cleanup worktree, fetch the merged main
git fetch origin main
```

After merge, refresh project-global architecture artifacts with the **staged**
target:

```bash
make architecture-refresh
```

Use `make architecture-refresh`, never the bare `make architecture` generation
target. Provenance is written only by the staged path (`run_staged` in
`refresh-architecture/scripts/run_architecture.py`), and the artifacts this
repository tracks are worth nothing to a reader without it. The staged target
requires a committed HEAD, which holds here: this step runs after the merge has
landed.

This step is a courtesy to the next reader, not a gate. Architecture freshness is
a property of the checkout that last regenerated the artifacts, so it is
**informational** in `make context-drift-gate`: skipping this step cannot turn the
gate red, and running it cannot make anyone else's checkout fresh. What it does is
make the *committed-tier* artifacts on `main` describe `main`, so that the
`--ensure` call every consumer skill makes at its read boundary finds a fresh
check and writes nothing. Skip it and the artifacts are merely stale: the next
`explore-feature`, `plan-feature`, `validate-feature`, `tech-debt-analysis`,
`validate-flows` or `validate-packages` run regenerates them locally — correct
results, but a working tree dirtied with tracked-artifact churn the developer did
not ask for.

The baseline is local and tier-aware, not universally committed. Provenance shares
the version-control status of its committed-tier artifacts; a `local-cache`
artifact's absence from a clean checkout is not drift at all. A consumer
repository that records the whole analysis as `local-cache` has nothing to refresh
here and can drop this step entirely — its readers ensure on demand instead.

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

```bash
# Detect changed submodules and sync their main branches
python3 "<skill-base-dir>/scripts/sync_submodules.py" \
  --repo-dir "." \
  --feature-branch "$FEATURE_BRANCH"
```

This script:
1. Detects submodules whose SHA changed via `git diff --raw main@{1} main` (mode 160000 = gitlink)
2. 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)
3. 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:

```markdown
## 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

Construct a `PhaseRecord` for the `Cleanup` phase and call `write_both()`. This
writes `session-log.md` and the coordinator handoff from one structured record,
so the two cannot drift. If no `session-log.md` exists from prior phases, the
call creates it.

**Capture from this cleanup:**

- **Decisions** — Merge strategy (squash vs rebase) and why, open-task migration target (coordinator issues vs follow-up proposal), archive outcome.
- **Alternatives / Trade-offs** — Rejected merge or migration strategies, and what was accepted over what.
- **Open Questions** — Anything unresolved that the follow-up owner needs.
- **Completed Work** — Steps that ran (e.g. `["merge", "task-migration", "archive", "branch-cleanup"]`).
- **Next Steps** — Follow-up proposal id, or the next roadmap item.
- **Summary** — 2–3 sentences: merge strategy, task migration decisions, any cleanup issues encountered.

**Persist via `PhaseRecord.write_both()`:**

```bash
python3 - <<'EOF'
import sys
sys.path.insert(0, "<skill-base-dir>/../session-log/scripts")
from phase_record import PhaseRecord, Decision

record = PhaseRecord(
    change_id="<change-id>",
    phase_name="Cleanup",
    agent_type="<agent-type>",
    summary="<2-3 sentences: merge strategy, task migration, archive outcome>",
    decisions=[
        Decision(title="<title>", rationale="<rationale>"),
    ],
    completed_work=["merge", "task-migration", "archive"],
    next_steps=["<follow-up proposal id or next roadmap item>"],
)
result = record.write_both()
print(f"markdown_path={result.markdown_path}")
print(f"sanitized={result.sanitized}")
print(f"handoff_id={result.handoff_id or '(local fallback)'}")
print(f"handoff_local_path={result.handoff_local_path}")
for w in result.warnings:
    print(f"WARN: {w}", file=sys.stderr)
EOF
```

`write_both()` runs `sanitize_session_log.py` internally, so no separate
sanitize step is needed. Read the written file and verify: (1) all sections
present, (2) no incorrect `[REDACTED:*]` markers, (3) markdown intact. If
over-redacted, rewrite without secrets and re-run (one attempt max).

```bash
git add "openspec/changes/<change-id>/session-log.md"
```

If the session log write 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.

- [ ] CI: all required workflows green on the merge commit (no allowed-to-fail bypasses)
- [ ] Security scan: SAST + dependency audit clean (no new HIGH or CRITICAL findings vs baseline)
- [ ] OpenSpec validate: `openspec validate --strict` exits zero against the archived change
- [ ] Docs updated: any operator-facing change is reflected in `docs/` and the relevant runbook
- [ ] Smoke tests: pass against staging (or production canary in pre-rollout mode) using the merged artifact, not a stale build
- [ ] Rollback plan documented: feature flag name, kill-switch procedure, and on-call owner identified
- [ ] Observability: dashboards and alerts for the changed surface are loaded and have a known-good baseline window (≥24h) to compare against

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:

```bash
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
In this source repository, the **source-contribution-only** command is
`make decisions`; its implementation is the co-installed
`<skill-base-dir>/../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):

```bash
make decisions
git add docs/decisions/
```

Before committing the archive, confirm there is no leftover drift:

```bash
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

```bash
# Confirm specs updated
openspec list --specs

# Confirm change archived
ls openspec/changes/archive/<change-id>/

# Validate everything
openspec validate --strict
```

### 8. Cleanup Local Branches

```bash
# Delete local feature branch (if not already deleted)
# Uses the resolved FEATURE_BRANCH to honor OPENSPEC_BRANCH_OVERRIDE
git branch -d "$FEATURE_BRANCH" 2>/dev/null || true

# Prune remote tracking branches
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:
1. Runs `git submodule deinit -f --all` inside the worktree first
2. Attempts normal `git worktree remove`
3. 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)
4. Other removal errors (dirty tree, conflicts) are NOT force-overridden — they surface for operator attention

```bash
# Return to shared checkout first (cleanup worktree is about to be removed)
cd "$(git rev-parse --git-common-dir | sed 's|/.git$||')"

# Remove cleanup worktree
python3 "<skill-base-dir>/../worktree/scripts/worktree.py" teardown "${CHANGE_ID}" --agent-id cleanup

# Remove implementation worktree (if exists from linear-implement-feature)
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}

# Garbage-collect stale worktrees
python3 "<skill-base-dir>/../worktree/scripts/worktree.py" gc
```

### 9. Final Verification

```bash
# Confirm clean state
git status

# Run tests on main
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
/cleanup-feature <change-id> --post-merge --pr <number> --defer-commit  # Same, but stage only; the convergence sync point commits
```

## 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. |
| "I'm in `--defer-commit` mode, so the sync point will run `make decisions` for me" | `--defer-commit` defers the *commit* by one step, not the *regeneration*. The sync point commits whatever is staged; if the regen never ran, the convergence commit lands an archive move beside a stale index and reproduces the 2026-05-12 incident with an extra layer of indirection. Regenerate and `git add docs/decisions/` before returning. |
| "Committing my own archive inside `--defer-commit` is harmless — the sync point can just add another commit" | That is the N+1 commits the mode exists to prevent, and it breaks the sync point's compare-and-swap: it keyed its operation on a `main` revision that your commit has already moved. Stage and return. |
| "`make architecture` regenerated everything, so the architecture artifacts are fresh" | Freshness is decided by *committed provenance*, and provenance is written only by the staged `make architecture-refresh` path. The full generation target leaves provenance missing, which the producer routes to drift — every artifact current and the gate still red. |

## 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).
- A `--defer-commit` run created a commit, a push, a tag, or a branch of its own. That is N+1 commits for N archived changes, and it invalidates the sync point's pre-push compare-and-swap.
- A `--defer-commit` run returned with `git diff --cached --name-only` empty while an archive directory moved — the work happened but was never staged, so the convergence commit will silently omit it.
- A `--defer-commit` run staged an archive move without `docs/decisions/` alongside it — the regeneration was skipped or postponed, which is the deferred-regen failure in a new disguise.
- A `--defer-commit` run set up its own `--cleanup` worktree instead of operating in the sync-point checkout, so its staged output is invisible to the commit the sync point is about to make.
- A cleanup failed partway through a multi-change pass and the staged output of the changes that already succeeded was reset, stashed, or checked out — it must be preserved for the sync point.
- The post-merge path ran `make architecture` rather than `make architecture-refresh` — provenance is unwritten, so `make context-drift-gate` stays red even though every artifact was regenerated.

## Verification

1. 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).
2. 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).
3. `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).
4. The feature flag (or equivalent traffic gate) is identified by name in the session log, with the kill-switch procedure documented.
5. If a rollback trigger fired during rollout, an issue tagged `rollback-postmortem` exists and the flag is currently at 0%.
6. 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`.
7. The post-merge architecture step ran `make architecture-refresh` (the staged, provenance-writing target), not `make architecture`. Confirm the run wrote or updated the architecture provenance record.
8. In `--defer-commit` mode: `git diff --cached --name-only` lists the archive destination, the merged `openspec/specs/` paths, and `docs/decisions/` — and `git log -1` on `main` is unchanged from before the cleanup ran (this skill created no commit of its own).
9. In `--defer-commit` mode: `git status --short` shows no unstaged cleanup output left behind, and `make decisions && git diff --quiet -- docs/decisions/` is clean, proving the regeneration happened here rather than being left for the sync point.
10. In `--defer-commit` mode after a partial failure: the staged index from the changes that already succeeded is intact — no `git reset`, `git stash`, or `git checkout --` appears in the cleanup transcript.

