cad-land · git:20260830.4e6f2f3 · 2026-08-30 · sha256 4e487fee2b3795dd

cad-land git:20260830.4e6f2f3A

Immutable. This exact content is served forever at /api/v1/blob/4e487fee2b3795dd.

---
name: cad-land
description: "Land finished work - report git state, then ask the mechanism (push / MR or PR / tag / leave local). Never decides how you publish"
argument-hint: "[base branch | defaults to git.base_branch]"
allowed-tools:
  - Read
  - Bash
  - Task
  - AskUserQuestion
---

<objective>
Land the current branch's work. cad-land encodes "the git mechanism is the
user's call" by construction: it never has a preselected publish action and
never auto-pushes. It reports the state, asks how to publish, and executes
exactly that - nothing more.
</objective>

<execution_context>
@${CLAUDE_PLUGIN_ROOT}/cadence-core/references/git-guard.md
</execution_context>

<process>
Read every config key this run needs in ONE `config.mjs get` up front
(conventions.md Parallel work) - `git.base_branch git.protected_branches
git.auto_close git.on_land_cleanup git.create_tag` - and reuse the values
across the steps below rather than re-reading per step.

1. **Report git state.** Current branch; the base = `$ARGUMENTS`, else
   `git.base_branch`, else the first `git.protected_branches` entry that
   exists here (references/git-guard.md's fallback); commits ahead of base; unpushed commits; uncommitted/untracked
   changes; and the remote host detected from the origin URL (gitlab -> MR,
   github -> PR, any other host where `tea` holds a login -> Forgejo/Gitea PR
   via `tea`, else local only). Show this plainly before doing anything.

   **Then the tracker, in the same report.** Run it here, before any publish
   ask, on both step-3 arms:

```
node "${CLAUDE_PLUGIN_ROOT}/cadence-core/bin/issue-check.mjs" check --dir <root> --base <base>
```

   Branch on `action` alone. On `report`: say in ONE sentence which issues this
   branch's commits reference and which of them are still open ("your branch
   references #42 and #47; #42 is still open"), naming a `not-found` number as
   not found and an `unresolved` one unresolved, never closed or not found.
   Print the `open` list ONLY when
   `referenced` is empty - it is the fallback, never the headline, because a
   bare list is what a reader skims past. On `skip`: print `reason` verbatim as
   ONE line and carry on - never block, never retry, never ask, and never list
   an issue the seam did not read. On `off` - `git.issue_check: false`, decided
   before any forge CLI runs - say NOTHING about the tracker: not the reason,
   not that it was skipped. The off switch is off, so this paragraph produces
   no output at all.

   This report never writes: landing closes no issue, and closing one stays an
   explicit ask you make at publish time.

2. **Uncommitted changes.** If the tree is dirty, do NOT auto-commit. Ask
   (ask-user seam): commit them first (then continue), leave them out of this
   land, or stop. If HEAD is a protected branch, the protected-branch guard
   (references/git-guard.md) applies to any commit here.

3. **Publish - branch on `git.auto_close`.**

   **First the deferred queue, on BOTH arms and ahead of the branch below.** A
   gate resolved `deferred` ran its reviewer and let the run continue, so the
   finding stops the LAND. Ask what is still queued:

```
node "${CLAUDE_PLUGIN_ROOT}/cadence-core/bin/planning.mjs" deferred list
```

   On ANY entry in `members`, STOP the land here: no publish ask, no seam call,
   no merge, no tag, neither arm. Print each member's `trigger`,
   `discriminator` and finding count with the `path` its bodies are in, then
   point the reader at
   `${CLAUDE_PLUGIN_ROOT}/cadence-core/references/triage-gate.md`, whose
   `deferred` arm says how to clear one - triage the member's findings and
   record the rulings with `planning.mjs adjudication` for the same trigger,
   discriminator and round, which supersedes it. A non-empty `unreadable`
   arrives as `ok:false` and refuses exactly as a member does: the queue could
   not be PROVEN empty, and this gate never reports "nothing deferred" about
   input it could not read, the disposition `land-cleanup.mjs gate` already
   takes for a findings payload it could not parse.

   It is NOT `land-cleanup.mjs gate` and must not be folded into it: that gate
   halts only when `git.auto_close` is true and reads only `risk_surface`
   survivors, so a default-configured project would publish straight over a
   deferred `plan`, `diff` or `phase_diff` finding.

   **(a) `git.auto_close` false (default): ask the mechanism (ask-user seam, NO
   preselected default):**
   - **Direct push** - push the current branch to its remote.
   - **Open MR / PR** - the detected host's mechanism (`glab mr create` on
     GitLab, `gh pr create` on GitHub, `tea pr create --base <base> --head
     <branch>` on a Forgejo/Gitea remote - `tea` does not push the source
     branch itself, so push it first as part of this same chosen action). If
     no remote, or `tea` holds no login at all, this option is absent.
   - **Tag** - create an annotated tag (ask the name); ask separately
     whether to push it.
   - **Leave local** - do nothing further.

   **Read the publish rails before a publishing answer.** When the answer is
   direct push, open MR/PR, or a tag the user chose to push, Read
   `${CLAUDE_PLUGIN_ROOT}/cadence-core/references/git-publish.md` (one
   consult site - step 3a or 3b, never both) first: rail 3 and the
   `git.auto_close` policy govern all three, and this skill no longer preloads
   them.

   Then **execute exactly that, raw.** Run only the chosen action. Never push
   unless push (or push-tag) was chosen. No PR-body templating beyond a
   title/summary the user confirms. Report precisely what was done (branch
   pushed, MR/PR URL, tag created) and nothing implied.

   **(b) `git.auto_close` true: land the integration branch on base via
   `PR -> merge`, no prompts.** Skip the 3a ask entirely (this is the single
   opt-in that lets the close run unattended; it never installs a default into
   the 3a ask). The integration branch is local-only
   (references/git-publish.md rail 3 never
   auto-pushes).
   - **Gate the unattended merge on the RULINGS, not the raw findings.** Nobody
     is watching this arm, so a blocker/high finding still genuinely unfixed is
     a HARD halt before any merge rather than an ask. This skill fires no review
     of its own: what it reads is what this branch's `risk_surface` fires
     already ADJUDICATED. Read every `ADJUDICATION-risk_surface*.json` under
     BOTH record roots - `.planning/phases/*/` and `.planning/risk-carry/*/`,
     the copies `/cad-milestone` leaves when it prunes the phase dirs before
     chaining this command - and union every round's `entries[]`. Every round,
     never the highest alone: a re-arm is a second fire on the same
     discriminator and round 2 is not the record of round 1. Do NOT read the
     `findings` arrays out of the `REVIEW-risk_surface*.md` files - those are
     the reviewer's raw claims, before anything was fixed, refuted, downgraded
     or overridden, and halting on them halts on work already done.
     Then name on `unruled` every `REVIEW-risk_surface*.md` nothing ruled - from
     those two roots plus a legacy `.planning/REVIEW-risk_surface-*.md` an
     interrupted older close may have left behind. A review and its record never
     share a basename, so do not look for one:
     `REVIEW-<trigger>-<discriminator>[-rN].md` is ruled by
     `ADJUDICATION-<trigger>-<discriminator>[-rN].json` in the same directory -
     same trigger, same discriminator, same round, with `REVIEW-` swapped for
     `ADJUDICATION-` and `.md` for `.json`. `-r2` pairs with `-r2`, and a round-1
     record never rules a round-2 review. One asymmetry, and the corpus forces
     it: a LATER round's record does rule an earlier review, because a re-arm's
     adjudication settles the round it re-armed - v3.7.7's phase 2 shipped
     `REVIEW-risk_surface-plan-1.md` with only
     `ADJUDICATION-risk_surface-plan-1-r2.json` beside it. So a review is
     `unruled` only when its `<trigger>-<discriminator>` has no record at its own
     round and none at any later one. A fire nothing ruled says nothing about
     what survived, so it halts by name rather than being read past. Pipe
     `{"findings": [...], "unruled": [...]}` on stdin to
     `node "${CLAUDE_PLUGIN_ROOT}/cadence-core/bin/land-cleanup.mjs" gate`; on
     `action:"halt"` stop the chain and surface the `findings` instead of
     merging over them, and surface a non-empty `overridden` with them - those
     are halting survivors a person already cleared, so they never move
     `action`, but a close that showed neither is a close nobody can answer.
     When neither root holds a record or a review, pipe an explicit
     `{"findings":[]}` - that is the only spelling of "nothing survived", and
     `action:"halt"` also fires when the payload could not be read at all
     (empty stdin, malformed JSON, or a valid envelope carrying no findings
     list), since the gate never reports "no surviving finding" about input it
     never parsed.
   - **Read the publish rails first.** Read
     `${CLAUDE_PLUGIN_ROOT}/cadence-core/references/git-publish.md` (one
     consult site - step 3a or 3b, never both) before the
     first bullet below that publishes anything - the GitHub seam call and
     GitLab's `glab mr create`, which publishes the source branch itself, both
     count, so this read is NOT scoped to the GitHub arm. Rail 3 and the
     `git.auto_close` policy govern from here on and this skill no longer
     preloads them.
   - **Publish the branch (GitHub and Forgejo arms).** On GitHub, `gh pr
     create --head <branch>` will NOT push a remoteless branch
     non-interactively, and `tea pr create` never pushes at all, so on either
     host publish the branch first through the git-publish seam. Run it on its
     own physical line:
     `node "${CLAUDE_PLUGIN_ROOT}/cadence-core/bin/git-publish.mjs" publish --dir <root>`
     It does ONE sanctioned `git push` of the current non-protected branch as a
     subprocess (execFileSync argv) that git-guard's Bash push hook never sees,
     and refuses with `ok:false` unless repo `git.auto_close` is true and HEAD is
     a non-protected branch. On `ok:true` proceed to open the PR; on `ok:false`
     stop and surface the reason - do NOT fall back to a raw `git push`, which
     would hit the guard's unconditional ask. Relay the envelope's `warnings[]`
     to the user rather than dropping them: `reason:"config-parse-failed"` means
     a config layer that could carry `protected_branches` did not parse, so the
     branch was checked against the DEFAULT list - fix the file, never retry
     past it.
   - **Open (or reuse) the PR/MR.**
     On GitLab EVERY arm of this bullet mutates the remote: `glab mr create`
     pushes the source branch itself, and the reuse arm hands an already-open
     MR straight to the merge below with no create at all. So the GitLab arm
     asks BEFORE it probes, on its own physical line:
     `node "${CLAUDE_PLUGIN_ROOT}/cadence-core/bin/git-publish.mjs" authorized --dir <root>`
     On `ok:false` do not touch the remote at all - no view, no create, no merge -
     stop and surface the `detail`, which says which authorization was missing
     (a `git.auto_close` the user set globally does not authorize a repository
     that never set it in its own `.planning/config.json`). That is the same
     stop the GitHub/Forgejo arm makes on the publish seam's `ok:false`. ONE
     consult, ahead of the whole bullet and not beside the create: the reuse
     arm is what would otherwise reach `glab mr merge` unasked, so placed here
     no second check is needed there.
     Then reuse an existing open one when
     `gh pr view <branch>` / `glab mr view <branch>` / `tea pr list --state
     open` (filtered by head branch) finds it, else create: GitHub
     `gh pr create --base <base> --head <branch> --fill`,
     GitLab
     `glab mr create --source-branch <branch> --target-branch <base> --fill`,
     Forgejo `tea pr create --base <base> --head <branch>` (record the index
     it prints - tea addresses PRs by index, not branch).
   - **Merge on the platform.** GitHub `gh pr merge <branch> --merge
     --delete-branch` (an explicit merge strategy is required or gh
     errors/prompts; `--delete-branch` removes the remote+local source). GitLab
     `glab mr merge <branch> --yes --remove-source-branch --auto-merge=false`
     (`--yes` skips the confirm prompt; `--auto-merge=false` merges immediately
     rather than deferring behind a running pipeline). Forgejo
     `tea pr merge --style merge <index>` (tea deletes no local branch; the
     reap in step 4 owns that).
   - **Confirm it landed before any cleanup.** `gh pr view <branch> --json
     state,mergedAt` must show MERGED, `glab mr view <branch>` must show
     merged, or `tea pr <index>` must show state merged. A non-zero exit
     (protected-branch / not-mergeable) or a still-open
     PR/MR (auto-merge only enabled, CI pending) means the merge did NOT land:
     stop, surface the reason, and do NOT reap.

4. **Terminal cleanup - return to base + pull + reap (`git.on_land_cleanup`,
   default on).** Run this ONLY when a merge actually landed on this machine
   (skip it after an open-PR-only or leave-local land; when that PR merges
   later outside this session, this cleanup - pull, tag, reap - is the piece
   to come back for). The auto_close merge
   lands on the platform, so the LOCAL base is stale - pull FIRST:
   `git checkout <base>` then `git pull`.

   **Release tag on the pulled base (tag-after-merge), before the reap.** When
   `git.create_tag` is true and the shipped version - the manifest's `version`
   (`.claude-plugin/plugin.json` or the project's own manifest), else the
   version PROJECT.md says just shipped - has no tag yet (`git tag`
   membership), cut it HERE: write the milestone label to a scratch file and run
   `git tag -a <version> -F <path>` on the now-current base - the label is a
   PROJECT.md milestone name, so it reaches git as a PATH (caller-derived text
   - references/conventions.md). Then ask separately whether to push it
   (references/git-publish.md rails; never auto-push a tag). This is
   deliberately NOT done at /cad-milestone: a tag cut at close names a
   pre-merge commit on the integration branch, and a non-fast-forward merge
   leaves that commit off base entirely. Skip silently when `git.create_tag`
   is false or the tag already exists.

   Then compute the reap decision against the now-current base:
   `node "${CLAUDE_PLUGIN_ROOT}/cadence-core/bin/land-cleanup.mjs" cleanup`.
   In the auto_close path append `--merged true` (step 3b confirmed the PR/MR
   MERGED) so the reap never hinges on local-base freshness; a manual land
   omits it and the seam falls back to `git branch --merged <base>`. Relay its
   `warnings[]`: a layer that did not parse means `on_land_cleanup` and `base`
   came from DEFAULTS, so `base` may name a branch this repo does not have. When the
   seam returns `reap:true`, reap through the git-publish seam - never a Bash
   git call, never a remote-tracking delete (that trips the push guard):
   `node "${CLAUDE_PLUGIN_ROOT}/cadence-core/bin/git-publish.mjs" reap --dir <root> --branch <decision.branch>`.
   It deletes by subprocess argv and refuses an unsafe, protected or
   checked-out branch. Relay its `warnings[]` too, and treat
   `reason:"config-parse-failed"` as a stop and not a retry: the seam refuses to
   delete anything while the protected list is unprovable. When either seam returns `action:"skip"` - the branch
   was already removed, or `git.on_land_cleanup` is off - leave HEAD and the
   branch in place. Report the final state: HEAD on `<base>`, pulled, branch
   reaped (or left).

   **Then clear the carried rulings.** Delete `.planning/risk-carry/` - the
   transient copies `/cad-milestone` made of this cycle's `risk_surface` reviews
   and records before it pruned the phase dirs. This step is the ONLY actor that
   clears them, and a confirmed merge is the only event proving the 3(b) halt
   they exist for was answered: a close that HALTED leaves them in place on
   purpose, so a retry still reads the blocker it stopped on. Do it whatever the
   reap seam answered - an `action:"skip"` is about the branch, not about the
   records. Untracked by construction, so this stages nothing.
</process>

<guardrails>
- No preselected publish default, ever. No auto-push. No auto-commit. The one
  exception is `git.auto_close` (default off), the explicit opt-in that runs the
  close unattended; its mechanic is stated once, at step 3(b), beside the
  code that runs it.
- With `git.auto_close` off, execute only the single chosen mechanism; do not
  chain (e.g. push AND tag) unless the user chose both.
- `/cad-land` fires no review of its own and commits no fix: it publishes what
  was already reviewed and already triaged upstream. The unattended arm acts on
  no survivor either - it reads them only to halt or proceed.
- It publishes what was already reviewed and TRIAGED, so an unadjudicated
  deferred finding is the one thing that stops it - on both arms, before either
  publishes anything.
</guardrails>