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 addthe 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-featurefirst if PR doesn't exist - Recommended: Run
/validate-featurefirst to verify live deployment
OpenSpec Execution Preference
Use OpenSpec-generated runtime assets first, then CLI fallback:
- Claude:
.claude/commands/opsx/*.mdor.claude/skills/openspec-*/SKILL.md - Codex:
.codex/skills/openspec-*/SKILL.md - Gemini:
.gemini/commands/opsx/*.tomlor.gemini/skills/openspec-*/SKILL.md - Fallback: direct
openspecCLI 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 istrue - 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_AVAILABLECOORDINATION_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
# 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:
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:
gh pr view "$PR_NUMBER" --json number,state,mergedAt,headRefName,baseRefName
- Continue only when
stateisMERGED. - 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 cleanupblock. Operate directly in the sync-point checkout, which is already onmainat 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 againstorigin/main. This skill does not acquire them and must not assume them on its own. - Stage, do not commit. After the steps below,
git addevery path the cleanup touched — the archived change directory, the mergedopenspec/specs/,docs/decisions/, the session log, and anytasks.mdmigration 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:
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
# 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:
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:
- 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
# 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:
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-implementationto address holdout failures - Re-run
/validate-featureto 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:
# 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):
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
# From the cleanup worktree, fetch the merged main
git fetch origin main
After merge, refresh project-global architecture artifacts with the staged target:
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.
# Detect changed submodules and sync their main branches
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 maininside 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)
- Fetches
- 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.mdordesign.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
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():
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).
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 --strictexits 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:archiveequivalent 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
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):
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
# Confirm specs updated
openspec list --specs
# Confirm change archived
ls openspec/changes/archive/<change-id>/
# Validate everything
openspec validate --strict
8. Cleanup Local Branches
# 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_BRANCHchanged 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 --allinside 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
# 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
# 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.mdif 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 --strictwas not re-run after the archive. - The change was archived but
make decisionswas not run (ordocs/decisions/was not staged into the archive commit) —mainwill failvalidate-decision-indexon the next PR. - Open tasks were silently dropped (no migration to coordinator issues or follow-up proposal).
- A
--defer-commitrun 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-commitrun returned withgit diff --cached --name-onlyempty while an archive directory moved — the work happened but was never staged, so the convergence commit will silently omit it. - A
--defer-commitrun staged an archive move withoutdocs/decisions/alongside it — the regeneration was skipped or postponed, which is the deferred-regen failure in a new disguise. - A
--defer-commitrun set up its own--cleanupworktree 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 architecturerather thanmake architecture-refresh— provenance is unwritten, somake context-drift-gatestays red even though every artifact was regenerated.
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 --strictwas re-run AFTERopenspec archiveand exited zero. The output is captured in the cleanup transcript. 3a.make decisionswas run after the archive anddocs/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-postmortemexists and the flag is currently at 0%. - Open tasks from
tasks.mdare accounted for: either all checked, or migrated to coordinator issues / follow-up proposal with a Migration Notes line in the originaltasks.md. - The post-merge architecture step ran
make architecture-refresh(the staged, provenance-writing target), notmake architecture. Confirm the run wrote or updated the architecture provenance record. - In
--defer-commitmode:git diff --cached --name-onlylists the archive destination, the mergedopenspec/specs/paths, anddocs/decisions/— andgit log -1onmainis unchanged from before the cleanup ran (this skill created no commit of its own). - In
--defer-commitmode:git status --shortshows no unstaged cleanup output left behind, andmake decisions && git diff --quiet -- docs/decisions/is clean, proving the regeneration happened here rather than being left for the sync point. - In
--defer-commitmode after a partial failure: the staged index from the changes that already succeeded is intact — nogit reset,git stash, orgit checkout --appears in the cleanup transcript.