name: task-land
description: Land the pull request for a work item — verify its checks, take it out of draft, then merge it. Refuses when there is no open PR or when checks are failing or still running; a rollup the credential cannot read warns loudly and proceeds. A provider that declines the merge (conflicts, unmet branch policy) surfaces as MERGE_FAILED rather than a false success.
steps:
  - ask:
      as: issueId
      type: text
      message: "Work item id to land?"
  # Declared as asks so both are DISCOVERABLE via baron_recipe_list; unanswered means
  # "let the provider decide" (its own default merge behaviour), not a hardcoded choice here.
  - ask:
      as: strategy
      type: text
      message: "Merge strategy — merge, squash, or rebase? Empty for the provider default."
      optional: true
  - ask:
      as: deleteSourceBranch
      type: text
      message: "Delete the source branch after merging? (yes/no)"
      optional: true
  - do: issue.get
    as: issue
    with:
      id: ${issueId}
  - require:
      truthy: "${issue.branchName}"
      message: "${issueId} has no branch recorded — run task-start first; there is nothing to land."
  # The branch is the join key: task-finish opened the PR from it, so the same lookup finds it back
  # without the caller having to carry a PR id between the two runs.
  - do: scm.pr.find
    as: pr
    with:
      sourceBranch: ${issue.branchName}
  - require:
      truthy: "${pr}"
      message: "No OPEN pull request on ${issue.branchName} for ${issueId}. Run task-finish to open one — or it may already be landed."
  # The verification gate, BEFORE the first mutation. This recipe once merged a PR whose checks were
  # failing and turned main red: nothing read them, and `mergeable` only means "no conflicts". The
  # whole argument for recipes over an agent behaving well is that the engine enforces the order, so
  # this is the one step that argument has to survive.
  - do: scm.pr.status
    as: status
    with:
      pullRequestId: ${pr.id}
  - require:
      notEquals: ["${status.checks.rollup}", "failed"]
      message: "${pr.url} has failing checks (${status.checks.failed} of ${status.checks.total}). Fix them and re-run task-land — landing red is what this guard exists to prevent."
  # Pending is 'not verified yet', which is the same category of risk as failed, so it stops here too
  # rather than racing the provider. The tool for "merge it once checks pass" is task-finish's
  # auto-complete, which hands the wait to the provider instead of guessing at it.
  - require:
      notEquals: ["${status.checks.rollup}", "pending"]
      message: "${pr.url} still has checks running (${status.checks.pending} pending). Wait and re-run task-land, or open it with auto-complete so the provider merges it when they pass."
  # An explicit rejection is the one review signal that means the same thing on every provider, so it
  # stops the land. `review_required` deliberately does NOT: it means no formal vote was cast, which a
  # repository where nobody votes reports on every single pull request — measured on a live Azure
  # project whose merged PRs come back with an empty `reviewers` array. Refusing on that would make
  # task-land unusable there, and a guard that fires on everything teaches nothing.
  - require:
      notEquals: ["${status.reviewDecision}", "changes_requested"]
      message: "${pr.url} has changes requested by a reviewer. Address them and re-run task-land — merging over an explicit rejection is what this guard exists to prevent."
  # 'unknown' does NOT stop the land. A fine-grained token cannot be granted the Checks permission at
  # all, so refusing on unknown would make task-land unusable for exactly the token `baron init`
  # recommends. It is loud instead, and says what it could not see — silence here would be the same
  # false assurance in a quieter voice.
  # The remedy comes from the provider, not from here: a permission a GitHub token needs is not one
  # an Azure token has, and each adapter knows which of its own reads failed. Azure used to make this
  # concrete by reporting 'unknown' on every pull request — it reads branch-policy evaluations now,
  # so an unknown there means a read genuinely failed rather than that nobody looked.
  - message: "WARNING — could not verify checks on ${pr.url}: the rollup is 'unknown' (unreadable: ${status.checks.unreadable}). Landing anyway; confirm CI yourself. ${status.checks.remedy}"
    when:
      equals: ["${status.checks.rollup}", "unknown"]
  - message: "Note — ${pr.url} reports no checks at all. Nothing verified this change; that is the repository's configuration, not a Baron failure."
    when:
      equals: ["${status.checks.rollup}", "none"]
  # Conditional: markPrReady on an already-ready PR is an error on some providers, so only undraft
  # what is actually a draft. task-finish opens drafts on purpose, which is why this step exists.
  - do: scm.pr.ready
    when:
      truthy: "${pr.draft}"
    with:
      pullRequestId: ${pr.id}
  - message: "Took ${pr.url} out of draft."
    when:
      truthy: "${pr.draft}"
  - do: scm.pr.merge
    as: merged
    with:
      pullRequestId: ${pr.id}
      strategy: ${strategy}
      deleteSourceBranch: ${deleteSourceBranch}
  # Still no issue.transition — a PR with a native closing link closes its item on merge on some
  # providers (GitHub) and not on others (Azure), so commanding a role would either be a no-op or
  # fight the provider. Reconcile is the other thing: it commands nothing and only clears the role
  # LABEL Baron itself wrote, once the provider's own state contradicts it. Without this the
  # in-progress label survives every landing and the board keeps showing finished work as in-flight.
  # Race note: if the provider has not closed the item yet, this no-ops and task-sync catches it.
  - do: issue.reconcile
    as: reconciled
    with:
      id: ${issueId}
  # Two messages rather than one hedged sentence. Reconcile reads the item moments after the merge,
  # and a provider that closes on merge does it asynchronously — so the unsettled reading is the
  # COMMON case on GitHub, not the exceptional one. The old single message asserted the role it had
  # just read and then prescribed task-move, which for an item that closed a second later is both a
  # stale fact and an instruction to hand-move something already done.
  - message: "Landed ${issue.key}: ${pr.url} merged (${merged.sha}). Item now reads '${reconciled.role}' (${reconciled.nativeState})."
    when:
      equals: ["${reconciled.role}", "done"]
  - message: "Landed ${issue.key}: ${pr.url} merged (${merged.sha}). The item still read '${reconciled.role}' (${reconciled.nativeState}) immediately after the merge. Providers that close on merge do so asynchronously, so this has very likely settled already — task-sync confirms it. Only if your provider does not close items on merge at all does ${issue.key} need task-move."
    when:
      notEquals: ["${reconciled.role}", "done"]
