# Fin

> Finalize completed PR or local checkout work, or close a local review branch with nocheck. Use when explicitly invoked.

- Skill: `kevinslin-skills/fin` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add kevinslin-skills/fin`
- Raw SKILL.md: https://api.skillmd.com/api/skills/kevinslin-skills/fin/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: kevinslin (https://skillmd.com/u/kevinslin-skills)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/kevinslin-skills/fin

---


# fin

Use this skill at the end of a task before the final user-facing report.
Linear follow-up is best effort: attempt it when available, but never block
finalization or mark it partial solely because Linear work could not be completed.

## Context Selection

Run `fin [context] [target]`.

- `gh`: finalize from a GitHub PR context. Use this when the task should land by merging the current remote PR or when the matching PR already merged and only cleanup/final verification remains. The original `fin` workflow maps to this context.
- `local`: finalize from a local checkout. Use this when the task should land directly from local git state without depending on GitHub PR state.
- `nocheck`: close the task's local branch and linked worktree without checking or changing any remote state. Use explicitly after reviewing someone else's code when the local review checkout is no longer needed; no landing or merge proof is required.
- Route explicit `nocheck` directly to the `nocheck` workflow below, before detached-HEAD handling, PR-target inference, or context auto-detection. Return after that workflow; every subsequent landing workflow, guardrail, and done checklist applies only to `gh` / `local`.
- `[target]`: optional for `gh` only. Accept a PR number, PR URL, or branch name. Examples: `fin gh 85117`, `fin gh https://github.com/owner/repo/pull/85117`.
- If the current checkout is detached `HEAD`, treat that as a preflight issue, not a valid finalization state. Create a short-lived local branch from the current commit before auto-detecting context, checking mergeability, or attempting worktree cleanup.
- If the argument is `gh`, `local`, or `nocheck`, respect it throughout the flow. Do not silently switch later just because repo state would make the other path easier. Never auto-detect `nocheck`.
- If `gh` has an explicit `[target]`, lock that PR target before current-branch detection. Use the target PR as the source of truth for state, mergeability, comments, checks, spec matching, merge, and automation cleanup.
- If the user omits `[target]` but the immediately preceding active heartbeat or delayed-merge instruction names exactly one PR and the user asks to merge, finalize, ignore a waiting period, or ignore a proof gate, treat that PR as an explicit `gh` target after one live PR-state check. Report the target source as `heartbeat automation`.
- If the user omits `[target]` but the immediately preceding task in the same thread completed or repaired exactly one PR, such as after `trigger:fix-pr`, `trigger:fix-pr-conflict`, or a PR-specific babysit/CI run, treat that PR as an explicit `gh` target after one live PR-state check. Report the target source as `active task context`. If the current checkout points at another branch or PR, mention the mismatch and ignore the unrelated checkout for PR state, spec archival, merge, automation, and cleanup decisions unless it blocks local cleanup.
- If an explicit `gh` target does not match the current branch, do not silently fall back to the current branch's PR. Either run the remote-PR finalization path for that explicit target, or stop with a target mismatch before any spec archival, branch cleanup, or merge.
- If the argument is omitted, detect the context from the current branch before any archival or landing work:
  - Choose `gh` when the current branch has an open or already-merged PR that corresponds to the branch being finalized.
  - Choose `local` when the current branch has no matching PR and the work should land directly from local git state.
- Treat heartbeat-derived or active-task-derived PR targets as explicit `gh` targets, not as current-branch auto-detection.
- If the argument is present but not one of `gh` / `local` / `nocheck`, stop and ask the user which context to use.
- Report whether the finalization context was explicitly provided or auto-detected.
- Before reporting any PR status or blocker, print one target identity line: `Target: PR #<number>, branch <headRefName>, source=<current checkout|explicit user PR|heartbeat automation|active task context>`. When multiple PRs have been mentioned in the session, prefix every PR-specific state claim with the PR number.

## `nocheck` Context Workflow

An explicit `fin nocheck` authorizes deletion of the exact disposable local
branch even when its commits are not merged. It does not authorize discarding
uncommitted work or deleting a remote branch. A request to add or explain this
context does not itself invoke cleanup.

1. Lock the local target from the current task's recorded review checkout and
   branch, or the current checkout when no separate task checkout exists. Read
   local Git state to confirm the repository, absolute worktree path, branch,
   and full HEAD OID. Report `Target: local branch <branch>, worktree <path>,
   context=nocheck (explicit)`. If the task's checkout differs from the shell's
   checkout, use the task's target and preserve the unrelated checkout. Stop on
   ambiguity, detached HEAD, or identity drift; do not create a temporary branch.
   For a branch checked out nowhere, inspect its local ref from a retained
   checkout and skip worktree-only checks. A missing path still registered as a
   worktree is a blocker; do not prune it to make cleanup pass.
2. Use local state only. Do not query GitHub, PRs, CI, reviews, downstream PRs,
   mergeability, or the remote default branch. Do not fetch, pull, push, merge,
   rebase, delete remote refs, or run `check_default_branch.py` or
   `$dev.worktrees cleanup-landed`. Set `GIT_NO_LAZY_FETCH=1` for Git commands so
   a partial clone cannot fetch missing objects implicitly; missing local data
   is a blocker. Do not run network-capable hooks or fall back to a landing flow.
3. Read repository preservation guidance and any matching `~/.fin.yaml` entry
   using the normalized primary checkout root. Apply local file-preservation
   instructions before removal; skip landing, deployment, and remote hooks.
   Stop when malformed or ambiguous config could hide preservation requirements.
   Inspect tracked, untracked, and ignored files with
   `git -C <target> status --short --untracked-files=all --ignored`. Preserve
   review notes, specs, credentials, and other wanted artifacts outside the
   target without overwriting existing files or exposing secrets, and verify
   the copies. A dirty tracked or untracked state blocks
   removal until separately resolved; do not stash, reset, clean, or force-remove
   it. Ignored files may be removed with the worktree only when known to be
   disposable generated output; preserve unknown files instead.
4. Protect the primary checkout directory, locked worktrees, unrelated branches,
   and local trunk branches (`main`, `master`, and any other default/trunk branch
   identified by local repository guidance or cached `refs/remotes/origin/HEAD`).
   Cached refs are local evidence only; do not refresh them or require an origin
   remote to exist. If the target might be a trunk branch, stop for clarification.
   Record a retained checkout outside the target before removing a linked
   worktree. If the target is in the primary checkout, keep that directory and
   switch to an existing, locally identified safe branch without force or remote
   guessing (`git switch --no-guess <retained-branch>`). Stop if a safe switch
   would require fetching or changing unrelated work.
5. Immediately recheck the target branch's full OID and, for a linked worktree,
   its branch, HEAD OID, registration, lock state, and file-preservation
   conditions. From the retained checkout,
   remove only the clean linked target using the ordinary removal path in
   `$dev.worktrees`: `git -C <retained> worktree remove <absolute-target-path>`.
   Never use `--force`, recursive path deletion, or global prune. Skip removal
   for a primary checkout or a branch already checked out nowhere.
6. Recheck the local branch OID and confirm no worktree has it checked out.
   Delete only that branch with `git -C <retained> branch -D -- <branch>`.
   This explicit `nocheck` authorization replaces the merged-branch requirement
   for this step only; do not fabricate a landed commit or weaken the `gh` /
   `local` cleanup gates. If the branch moved, stop and preserve it.
7. Verify the branch is absent and, for linked-worktree cleanup, both the target
   path and its worktree registration are absent. If only removal succeeded,
   report partial local cleanup and preserve the recorded branch/OID for a retry.
   Report the local cleanup result, retained checkout, and preserved artifact
   paths, followed by `Remote state was not checked or changed.` Do not claim a
   PR landed or a remote branch was deleted. Skip spec archival, issue completion,
   automation changes, retrospective publishing, and the landing checklist below.

## Shared Workflow (`gh` / `local` only)

1. Confirm finalization preconditions
- Use this flow only when the requested scope is complete.
- If work is partial or blocked, do not archive specs and do not present the task as finished.
- Resolve the repository's origin main branch before any spec archival, landing, merge, or cleanup. In this skill, `main` means the default branch of the repository addressed by `origin`; do not assume the literal branch name is `main`.
  - For `gh`, query the target repository's authoritative default branch and the locked PR's `baseRefName`.
  - For `local`, resolve and verify the default branch associated with `origin`, using the remote provider when available or `refs/remotes/origin/HEAD` as a fallback. If the origin main branch cannot be determined unambiguously, stop.
  - Compare canonical short branch names. When a local fallback returns `refs/remotes/origin/<name>` or `origin/<name>`, retain the raw result as evidence and pass only `<name>` to the gate.
  - Pass the two resolved names through the mandatory machine gate before any spec archival, landing, merge, cleanup, or full-finalization wording:

    ```bash
    python3 ./scripts/check_default_branch.py \
      --context gh \
      --repository-default-branch "<resolved-origin-main>" \
      --target-base-ref "<locked-target-base-ref>"
    ```

    Resolve `./scripts/check_default_branch.py` relative to this `SKILL.md`, replace the quoted placeholders with the resolved values, and use `--context local` for local finalization. Do not replace this command with a manual comparison or a same-named repository script.
  - Continue only when the command exits `0` and its JSON reports `status: "pass"`, `matches: true`, and every `allow` field as `true`. Retain that JSON as finalization evidence. Treat a nonzero exit, malformed output, or any other result as a hard blocker.
  - Rerun the gate if the locked target, repository default branch, or target base ref changes. The latest passing record is the only valid authorization for the finishing actions.
  - A mismatch blocks intermediate, stacked, release, and feature branches. Use the script's retarget-or-create-PR message in the report.
  - If the PR already merged into a non-main branch, the gate still fails: stop before archival or cleanup, report that it did not land in origin's main branch, and do not describe it as fully finalized.
- Before archiving any spec or attempting to land the change, determine the current branch, whether it is attached to a linked worktree, and where the non-worktree `main` checkout lives.
- Read repository guidance for ignored-file preservation and verification
  boundaries even when no `~/.fin.yaml` exists. Treat checked-in product specs
  as deliverables, not disposable task artifacts, unless their own workflow
  explicitly requires archival.
- Check `~/.fin.yaml` for repo-specific finalization instructions before landing or cleanup. If the file exists, parse entries shaped as `workspace: [{path: ..., instructions: ...}]`.
- If `~/.fin.yaml` exists but cannot be parsed, do not treat the parse failure as "no hooks". Report the parser error separately and inspect the raw file. Continue only when the raw content is clearly non-executable, unambiguous context; if it might contain commands, hooks, destructive instructions, or ambiguous cleanup requirements, stop before spec archival, landing cleanup, or linked-worktree removal and report the malformed config as the blocker.
- Match `workspace[].path` against the normalized non-worktree checkout root for the branch's repository, not against a transient linked worktree path. Resolve symlinks and trailing slashes before comparing.
- If a matching `~/.fin.yaml` workspace entry exists, record its `instructions` and apply them after the PR merge / already-merged confirmation or local landing succeeds, but before removing any linked worktree. This preserves ignored or untracked files that may need to be copied out of the soon-to-be-removed worktree.
- Also treat matching `~/.fin.yaml` instructions as routing context for active spec discovery. If the instructions mention a `$specy`, `$mem`, or `.mem` artifact root, derive candidate docs roots from that instruction before falling back to the default docs root.
- If `~/.fin.yaml` is missing or no workspace entry matches the non-worktree checkout root, state that no repo-specific final hooks applied and continue.
- If the checkout is detached `HEAD`, create a temporary local branch from the current commit first. Prefer the repo's normal task-branch prefix when one exists, otherwise use a short `codex/` branch name derived from the task.
- After converting detached `HEAD` into a named branch, lock that branch identity for the rest of the run. Do not continue finalization from anonymous detached state.
- When the user omitted the context, lock the detected context once and use it for the rest of the run. Do not re-detect after archiving or mid-landing.
- When the user provided, or the heartbeat handoff or active task context implied, an explicit PR target, lock that PR number, head branch, and target source once. Do not replace it with the current checkout's PR because the current checkout is different or easier to operate from.
- In `gh` mode, apply the downstream pull request protection below to the locked repository and exact PR head branch before merging, enabling auto-merge, or deleting its remote branch.
- Do not silently switch contexts after selection. If the user requested `gh`, do not fall back to local-only landing. If the user requested `local`, do not silently land via PR merge just because a PR exists.
- For `gh` without an explicit target, identify the current PR and check its state before testing mergeability or attempting any merge command.
- For `gh` with an explicit target, identify that target PR directly with GitHub before consulting current-branch PR state. If the current checkout points at another PR, report the mismatch in the target identity line and ignore the other PR unless it blocks local cleanup.
- For `gh`, identify any active PR babysit/watch automation for the target PR or branch when automation state is visible. Record its id so it can be updated or deleted after the PR lands.
- For `gh`, entering `auto-merge pending` itself authorizes the monitoring needed to finish `$fin`. Do not require a second user request before creating a heartbeat or falling back to a foreground watch. Monitoring never waives the locked target identity, checks, reviews, conflicts, or any other blocker.
- If the task includes a requested external notification after green CI, such as Slack or another chat notice, track that notification as a separate finalization gate from PR mergeability and CI. Check notification prerequisites when feasible, such as required local credential files or configured CLIs, and record any missing prerequisite as a notification blocker, not as a CI failure.
- If the target PR is already merged into the verified origin main branch, treat the PR landing precondition as satisfied and skip mergeability repair. Continue with any matching spec archival, worktree cleanup, local `main` refresh, and retrospective.
- For `gh`, when the target PR is still open, confirm it is mergeable against the verified origin main branch.
  - If GraphQL or `gh pr view` reports `UNKNOWN`/indeterminate mergeability while checks and reviews otherwise look green, poll the REST pull-request endpoint once or twice for `mergeable` and `mergeable_state` before invoking conflict repair. Treat REST `mergeable: true` with `mergeable_state: clean` as the mergeability confirmation. If repeated REST results remain `null`/unknown, record the state as bounded-indeterminate and allow the merge attempt to continue when the exact PR head identity is known and there is no positive evidence of conflicts or unmergeability. Treat the repository merge operation as the authoritative final mergeability check.
- For `local`, confirm the current branch is mergeable into the verified local origin main branch.
- If the `gh` flow is blocked only by base-branch conflicts, run `trigger:fix-pr-conflict` against the locked target PR and let it try to restore a clean merge state.
- If the `gh` flow is blocked by broader PR issues, or conflict repair needs a fuller pass, run `trigger:fix-pr` against the locked target PR.
- If the `local` flow is blocked only by trunk drift, run `trigger:sync-branch` or otherwise rebase the current branch onto the merge target before retrying the check.
- Continue after mergeability is confirmed, the matching PR is already merged, or a `gh` target meets the bounded-indeterminate conditions above. If repair cannot clear positive evidence of conflicts or unmergeability, stop and report the blockage instead of archiving the spec or landing the change.

### Automatic Required-Review Bypass

- In `gh` context, automatically use a repository-supported administrator or maintainer bypass when missing required approvals (including code-owner or last-push approval) are the only remaining merge blocker. Invoking `fin` authorizes this narrow bypass; do not ask for additional confirmation.
- Before bypassing, verify the exact PR head, a passing default-branch gate, no merge conflicts, and successful required checks for that head. If required checks cannot be determined, are missing, pending, or failing, do not automatically bypass. Confirm that no unresolved review threads, changes-requested reviews, or other merge blockers remain; a generic `BLOCKED` status alone is insufficient evidence.
- Preserve all normal completion, spec, downstream-PR, permission, file-preservation, and cleanup gates. Honor explicit user or repository instructions prohibiting bypass, including any RIPP auto-merge-only path. Do not change repository rules, bypass lists, or permissions to make this operation succeed.
- Report the locked target and missing approvals being waived, then use `gh pr merge <number> --repo <owner/repo> --admin --match-head-commit <full-head-sha>` with the supported merge method. If GitHub rejects the bypass, report the exact remaining restriction; do not broaden the override. Verify the actual merged state before cleanup.
- Record the automatic required-review bypass, exact head, waived approvals, and merge method in the final report. Other blocker overrides still require the explicit authorization below.

### Explicit Blocker Override

- Outside Automatic Required-Review Bypass, when finalization stops on named non-conflict blockers and the user explicitly says to merge or land while ignoring those blockers, treat that response as an auditable override limited to the blockers already reported for the locked target.
- Restate the target identity and the exact waived blockers before proceeding. Do not infer an override from a generic approval, an earlier broad permission, silence, or a request that does not clearly authorize landing.
- An override may waive failing or pending checks, review/proof/approval gates, waiting periods, and incomplete-spec landing gates. It does not waive a missing or failed default-branch gate, a non-main merge target, an unmergeable/conflicting target, a target mismatch, unknown commit identity, dirty-worktree preservation, malformed or failed final hooks, missing repository permission, or post-merge verification and cleanup.
- Keep incomplete specs and milestones active and unarchived. Do not mark them complete merely to satisfy the normal archival-before-landing order. Record the spec exception in the final report.
- In `gh` context, re-confirm the exact PR head identity and require no positive evidence of conflicts or unmergeability, then use a repository-supported administrator or maintainer override merge. If merge commits are disallowed, retry once with the supported squash method. An indeterminate mergeability value alone does not prohibit the override; never use the override to merge a conflicting or explicitly unmergeable head.
- Preserve the waived state in the final report: who authorized it, which blockers were ignored, whether an administrator merge was used, and which spec artifacts intentionally remained active.

### Downstream Pull Request Protection

- Before merging or enabling auto-merge, lock the target PR's exact repository, head branch, and base branch. Discover open PRs that depend on that head branch with `gh pr list --repo <owner/repo> --state open --base <head-branch> --limit 1000 --json number,url,headRefName,baseRefName`.
- Read the repository's automatic branch deletion policy with `gh api repos/<owner/repo> --jq '.delete_branch_on_merge'`. Treat a failed, incomplete, or ambiguous dependent-PR or deletion-policy lookup as a blocker; never assume a branch is safe to delete.
- If no open PR uses the locked head branch as its base, proceed with normal merge and remote branch cleanup.
- If dependent PRs exist and `delete_branch_on_merge` is `false`, merge without `--delete-branch`, preserve the remote head branch, and report the dependent PR numbers and retained branch. The parent's local branch or worktree may still be cleaned up after normal landing proof.
- If dependent PRs exist and `delete_branch_on_merge` is `true`, stop before merging or enabling auto-merge: repository policy would remove their base even without `--delete-branch`. Ask whether to retarget every dependent PR to the parent's verified base, or leave the parent unmerged.
- Retarget a downstream PR only with explicit user authorization. Run `gh pr edit <dependent-number> --repo <owner/repo> --base <parent-base>` for each authorized dependent, then repeat the exact-base discovery and require zero remaining open dependents before merging or enabling auto-merge.
- Immediately before any later explicit remote branch deletion, repeat the exact-base dependent-PR query. Delete only when it succeeds and returns zero open dependents; otherwise retain the remote branch and report the open PRs or lookup blocker.
- Do not change repository settings, close a downstream PR, or infer retargeting approval from a general request to merge, finalize, or clean up.

### PR Automation Lookup

- Resolve matching automation by exact automation id first, then by the locked repository, PR number, and head branch. Classify ordinary babysit/watch automation separately from a `fin: auto-merge ...` heartbeat; the latter is owned exclusively by the auto-merge lifecycle in step 4.
- Bound the automation service lookup to one request and at most 30 seconds of waiting. Do not let an unavailable automation service stall finalization indefinitely.
- If the service lookup errors or times out, search `${CODEX_HOME:-$HOME/.codex}/automations` for the exact automation id, PR number, or head branch. Treat this as read-only fallback evidence; never delete automation files directly.
- When the fallback finds a persisted ordinary babysit/watch automation, retry one service lookup/delete operation using its exact id. For a `fin: auto-merge ...` heartbeat, retry lookup or update only; never delete it through this generic cleanup path. If the allowed operation still fails, report automation cleanup as blocked with the id and error.
- When neither the bounded service lookup nor the exact registry search finds a match, report that no matching persisted automation was present and name the evidence used. Distinguish this from a service-only lookup failure.

### Linear Issue Lookup (Best Effort)

- Read `~/.agents/profile` directly. Treat the profile as work only when it contains a trimmed, non-comment line exactly equal to `name=work`. For a non-work profile, skip Linear unless the user or current task context explicitly identifies a Linear issue to finish.
- If the profile or active thread id is unavailable, skip Linear with a brief reason and continue finalization.
- For a work profile, resolve the active Codex thread id from the current app/session context, using `dev.llm-session` when needed. Use the installed `linear:linear` connector skill and its connected Linear tools; do not use `linear-cli` or a browser fallback.
- Let `thread_link` be the exact deep link `codex://threads/<thread-id>`. Search with `list_issues(query=thread_link, assignee="me", includeArchived=true)` first. If that produces no exact match, run one unscoped `list_issues(query=thread_link, includeArchived=true)` search and follow its pagination until exhausted. If the connector cannot exhaust the candidate set, treat lookup as ambiguous instead of guessing.
- Treat search results only as candidates because Linear search is fuzzy. Keep bulk connector payloads out of the transcript: parse them in the tool-composition layer and emit only candidate ids needed for verification. For every candidate whose complete description is unavailable or marked truncated, call `get_issue` before comparison. Retain only issues whose complete description contains `thread_link` exactly, and expose only each exact match's id, team, status, status type, and URL.
- Accept only an exact `thread_link` match or an issue the user explicitly identified as the ticket for this task. Do not infer linkage from a search hit, similar title, branch name, PR number, assignee, or project.
- Treat statuses with type `backlog`, `unstarted`, or `started` as pending. Treat `completed` and `canceled` issues as terminal and leave them unchanged.
- When exactly one linked pending issue exists, lock its issue id and team for the rest of the run. When no linked pending issue exists, record that Linear completion is not applicable and continue.
- When more than one linked pending issue exists, do not update any of them. Record the matching issue ids as a non-blocking ambiguity note and continue finalization without guessing.
- If the connector is unavailable, unauthenticated, errors, times out, or returns an ambiguous result, skip the remaining Linear work and report the reason briefly. Do not wait for reauthentication, require the user to repair Linear, or retry through another client. Continue landing, cleanup, retrospective, and completion reporting.

2. Resolve the active spec
- Build an ordered list of candidate docs roots:
  - First, use `DOCS_ROOT` when it is configured.
  - Next, if the matching `~/.fin.yaml` instructions mention an absolute artifact root for `$specy`, `$mem`, or notes, use that configured root directly. Do not infer or append `/main`; aggregate `$mem` roots already name the complete managed root.
  - Finally, default to `./docs`.
- Only use candidate roots that exist or whose parent exists and is clearly the intended workspace artifact root. Report the selected spec root when a matching spec is found.
- Follow `specy`'s layout rule for each candidate root: active specs live directly under `$DOCS_ROOT/specs/`.
- Only treat files directly under a candidate `$DOCS_ROOT/specs/` as active specs. Ignore files already under `$DOCS_ROOT/specs/.archive/`.
- Also support folder specs when the workspace uses a folder schema such as `specs`: an active spec folder lives directly under a candidate `$DOCS_ROOT/specs/<spec-slug>/`, with optional sidecars such as `milestones/`, `flows/`, `reports/`, `cook/`, `checklist.md`, or `data/`. Treat the folder as the active spec unit.
- If the completed work is a milestone or sidecar inside an active folder spec,
  do not archive the parent folder unless the parent spec itself is complete. If
  sibling milestones remain, record that the milestone landed and leave the
  parent folder spec active.
- If multiple active specs exist across candidate roots, archive only the one that directly matches the completed task and leave unrelated active specs untouched.
- If no active spec exists for this task, state that explicitly and continue.

3. Mark the spec complete and archive it
- Update the active spec so its status clearly reflects completion before moving it. For folder specs, update the folder's `spec.md`.
- For a completed milestone or sidecar inside an otherwise active folder spec,
  mark or report only that milestone as complete when the file has a clear
  status field or checklist. Do not move the parent folder into `.archive/`
  until the whole folder spec is complete.
- Preserve the existing filename for single-file specs and the existing folder name for folder specs.
- Move the completed spec to `$DOCS_ROOT/specs/.archive/`, creating the directory if needed. For folder specs, move the whole folder so sidecars such as `checklist.md` and `data/` stay with the completed spec.
- Follow `specy`'s convention exactly: when a single-file spec is complete, move it to `$DOCS_ROOT/specs/.archive/` and keep the same filename.
- Do not delete completed specs.

### Deterministic Local Cleanup

- Use `$dev.worktrees cleanup-landed` as the only executor for destructive local worktree and local-branch cleanup during `fin gh` / `fin local`. The explicit `nocheck` workflow owns its separate ordinary removal path. Do not reproduce the landed executor's reset, clean, removal, orphan recovery, or branch deletion steps manually.
- Before invoking it:
  1. confirm the PR or local change actually landed;
  2. run every matching `~/.fin.yaml` final hook;
  3. refresh or verify the local base ref and prove it contains the landed commit;
  4. inventory ignored or untracked files required by repository guidance,
     including `.env` files and root or nested `node_modules`; preserve them in
     the retained checkout without exposing secrets, overwriting existing
     content, or installing dependencies; and
  5. record the exact target worktree path, branch or detached state, and expected pre-cleanup HEAD.
- When checking preserved dependency graphs, invoke installed tools directly if
  package-manager scripts might reconcile workspace state or install packages.
  Stop before cleanup if required preservation or safe verification fails.
- Pass the exact full commit OIDs, refreshed local base branch, target identity, and actual merge mode to `$dev.worktrees cleanup-landed`.
- Require its dry run to report `ready` or `noop`, then rerun the exact command in execute mode.
- Treat `blocked`, `partial`, a nonzero exit, identity drift, or failed postconditions as a finalization blocker. Preserve its journal and rerun the exact command after resolving the blocker.
- Require final status `complete` or `noop`, with the target path, worktree registration, and local branch absent and the base still containing the landed commit.

## `gh` Context Workflow

4. Merge the PR
- Before running a merge command, check the target PR state with GitHub. If the PR is already `MERGED`, do not run `trigger:merge-pr`; record the merge commit or merged-at details when available and proceed as an already-landed PR.
- Before any merge command or auto-merge request for an open PR, complete Downstream Pull Request Protection using the locked repository and head branch. Pass `--delete-branch` only when its current exact-base query proves there are no open dependents.
- If the PR is not already merged and the target PR belongs to the current branch, run `trigger:merge-pr` immediately after the matching spec has been marked complete and archived.
- If the PR is not already merged and the explicit target PR does not belong to the current branch, use a target-aware remote merge for that PR, such as `gh pr merge <target>`, after the matching spec has been marked complete and archived. Do not use current-branch merge shortcuts for a different PR.
- Under an explicit blocker override, skip the archival prerequisite only for incomplete matching specs, leave them active, and use the target-aware override merge defined above.
- Treat the merge as part of finalization, not a follow-up option.
- If missing required approvals are the only remaining blocker, apply Automatic Required-Review Bypass before stopping for approval.
- If direct merge is rejected because repository policy requires auto-merge, and checks/reviews are otherwise green, enable repository-supported auto-merge for the locked target PR instead of treating the rejection as a terminal merge failure.
- After any successful auto-merge enablement, query the target PR for `autoMergeRequest`, `state`, `mergedAt`, `mergeCommit`, `mergeStateStatus`, and status checks.
- Treat `autoMergeRequest` present with the PR still `OPEN` as `auto-merge pending`, not as `blocked`, while checks remain green and no explicit cancellation or failing required check is present.
- Treat `auto-merge pending` as a nonterminal `$fin` state. Do not describe finalization as partial or complete, close or archive the task, send a terminal notification, run post-merge cleanup, or return a terminal final report while the locked PR remains in this state.
- As soon as the first live query confirms `auto-merge pending`, create or update exactly one dedicated thread heartbeat named `fin: auto-merge <owner>/<repo>#<number>` before beginning any bounded foreground polling. Reuse an existing exact match instead of creating duplicates. Use the first-class `automation_update` app tool: search the available tools for that exact capability when it is not already loaded. Do not infer that automation is unavailable merely because `${CODEX_HOME:-$HOME/.codex}/automations` is absent or has no matching entry, and do not substitute a standalone cron automation.
- Require the heartbeat to target the same `$fin` task so it can resume the deferred workflow. Store the locked repository, PR number and URL, base and head branches, exact head SHA, merge method, `autoMergeRequest.enabledAt`, the deferred `fin` stage, and instructions to query live state and resume at step 5 only after verified merge. Schedule its next wake promptly and keep it active while the same head remains safely pending.
- After heartbeat creation, the current wake may continue polling the locked PR for a bounded window. If the PR becomes `MERGED`, verify `mergedAt` and `mergeCommit` and continue with normal post-merge work. Otherwise report only a nonterminal monitoring handoff with the heartbeat id and next wake; keep `$fin` active and do not call the handoff partial finalization.
- On every heartbeat wake, query the locked PR live for `autoMergeRequest`, `state`, `mergedAt`, `mergeCommit`, `mergeStateStatus`, head SHA, and required checks. While the same head remains `OPEN` with auto-merge present and no terminal blocker, keep the heartbeat active, report `auto-merge pending`, and leave post-merge cleanup, Linear completion, and full-finalization wording deferred.
- When a heartbeat wake observes live `MERGED` state with non-empty `mergedAt` and `mergeCommit`, mark the heartbeat as post-merge/resuming and resume this workflow at step 5 in the same wake. Do not delete the heartbeat yet. Delete it only after steps 5-9 reach a terminal post-merge outcome, including exact reporting of any post-merge blocker, so a merge cannot strand unfinished refresh, cleanup, Linear, or retrospective work. If the auto-merge request is removed or cancelled before merge, the head changes unexpectedly, a required check fails, or conflicts appear, pause the heartbeat as blocked without cleanup and report the exact evidence.
- If the `automation_update` capability is genuinely absent after tool discovery, or heartbeat creation/update fails, continue automatically with a foreground watch instead of ending the run. No additional user request is required. Report the automation error as monitoring evidence, but do not treat heartbeat unavailability as a terminal or partial-finalization condition. Retain the locked repository, PR number, head branch, and exact head SHA; poll the same PR live with no more than 60 seconds between checks; and publish one concise progress update at least every 60 seconds.
- During a foreground watch, query `autoMergeRequest`, `state`, `mergedAt`, `mergeCommit`, `mergeStateStatus`, head SHA, and required checks on every poll. Continue until the locked head is `MERGED` with non-empty `mergedAt` and `mergeCommit`, then resume at step 5. Stop only for a terminal blocker: the auto-merge request is removed or cancelled, the head identity changes, a required check fails, conflicts or unmergeability appear, or live polling can no longer continue. Report the exact blocker and preserve deferred cleanup. Never infer or use an administrator, approval, or blocker override from a request to watch.
- Treat auto-merge as blocked only when GitHub reports the auto-merge request was removed/cancelled, a required check fails, conflicts appear, or the PR is otherwise no longer mergeable.
- If the merge command reports that the local branch cannot be deleted because it is still attached to a linked worktree, check whether the remote PR actually merged before treating the step as failed.
- If `gh pr merge --delete-branch` fails only in its implicit post-merge branch switch and pull, such as `error: cannot pull with rebase: You have unstaged changes` on a checkout with unrelated dirty files, do not treat that as a merge failure and do not stash or discard the dirty files. Confirm the PR is `MERGED` remotely, then complete the step-5 refresh with `git fetch <remote> <base>` followed by `git merge --ff-only <remote>/<base>`; a fast-forward is safe alongside dirty files the landed commits do not touch. The pre-refresh working tree may transiently look as if the landed feature was reverted; that is the stale local base, not lost work.
- When the PR merged remotely but local branch deletion failed only because of the linked worktree attachment, treat that as a successful merge followed by incomplete cleanup and continue with the deterministic cleanup script from a retained checkout.
- If there is no matching PR to merge, state that explicitly instead of claiming the task fully landed.
- For an explicit target PR that is not checked out locally, skip local branch/worktree cleanup unless the target branch is present in a local worktree or the user explicitly requested cleanup. It is acceptable to merge the remote PR, refresh local `main`, delete matching ordinary babysit/watch automation, and report that no local target checkout cleanup was applicable. Retain any `fin` auto-merge heartbeat until the terminal post-merge outcome.

5. Refresh and verify the local base ref
- Before destructive cleanup, run any matching repo-specific final hooks from `~/.fin.yaml`. If a hook fails, stop and preserve the worktree and branch.
- After the PR merges, or after confirming it was already merged, update the local base ref before cleanup. Run this refresh from a retained checkout, never from the soon-to-be-removed worktree.
- If the target PR was explicit, heartbeat-derived, or active-task-derived and the non-worktree checkout is on an unrelated active branch, prefer a non-switching base-ref refresh before checking out `main`: run `git fetch <remote> <base>:<base>` only when `<base>` is not checked out in any worktree, then confirm containment with `git merge-base --is-ancestor <mergeCommit> <base>`.
- If the non-switching base-ref refresh is rejected because `<base>` is checked out elsewhere or the ref can

…(truncated)
