# ADR-0027: Destructive CLI Commands Confirm by Default with `--force` and `--dry-run` Escape Hatches

## Status

✅ Accepted (2026-05-11)

## Context

omni-dev exposes a number of destructive Atlassian operations through both the
CLI and the MCP tool surface:

- `omni-dev atlassian jira delete`
- `omni-dev atlassian confluence delete`
- `omni-dev atlassian jira watcher remove`
- `omni-dev atlassian jira link remove` (added in [#711](https://github.com/rust-works/omni-dev/pull/711))
- `omni-dev atlassian confluence label remove`

Before this decision, the surface was inconsistent. `jira delete` and
`confluence delete` already prompted interactively and accepted `--force`. The
other three executed the API mutation immediately with no guard, which made an
accidental piped argument or shell-history recall destructive on first use. The
two existing prompt implementations were also duplicated copies of the same
~10-line `confirm_with_reader` helper — a small but real source of drift.

The MCP surface had a parallel inconsistency. The `jira_delete` and
`confluence_delete` tools required a mandatory `confirm: true` parameter and
refused to execute otherwise. `jira_watcher_remove`, `jira_link_remove`, and
`confluence_label_remove` had no such guard, so an assistant could remove a
watcher or label by name without any opt-in step.

[Issue #720](https://github.com/rust-works/omni-dev/issues/720) raised the
question. Four options were weighed:

- **Confirm + `--force` (no `--dry-run`).** Catches accidental human invocations
  but offers no preview path: a script author can only verify by running the
  full command. Adequate but leaves an obvious gap.
- **`--dry-run` only, no interactive prompt.** Preserves CLI/MCP parity
  (neither side prompts) but relies on the operator to remember `--dry-run`. A
  one-character typo on the command line is still destructive.
- **Status quo + a documented "operator is responsible" convention.** Lowest
  implementation cost. Discarded because the asymmetry between commands that
  already prompt and commands that do not is itself the surprise — a documented
  convention does not undo muscle memory.
- **Confirm by default + `--force` + `--dry-run`.** Selected. The interactive
  prompt catches accidents on the human path; `--force` keeps scripts working;
  `--dry-run` adds a server-free preview that composes with `--force` so the
  same scripted invocation can be sanity-checked.

## Decision

### CLI

Every destructive Atlassian subcommand listed above prompts on stdin by default
and accepts two flags:

- `--force` — skips the interactive prompt.
- `--dry-run` — prints `Would <verb> <target>.` and returns without calling the
  API.

The flag spelling is `--force` rather than `--yes` for consistency with the two
pre-existing destructive commands; an alias was rejected to keep the surface
minimal.

A new shared helper `src/cli/atlassian/confirm.rs` exposes
`guard_destructive(opts) -> GuardOutcome { Proceed | Cancelled | DryRun }`. It
is the single source of truth for prompt shape, dry-run preview shape, and
flag precedence. The two duplicated `confirm_with_reader` copies were removed.

### MCP

Every destructive MCP tool requires a mandatory `confirm: bool` parameter and
returns an explanatory error when called without `confirm: true`. The three
tools that previously lacked the guard (`jira_watcher_remove`,
`jira_link_remove`, `confluence_label_remove`) now match the existing
`jira_delete` / `confluence_delete` shape. There is no `dry_run` parameter on
the MCP surface — assistants can preview by reading the relevant resource
(`jira_read`, `confluence_read`, `jira_watcher_list`, `jira_link_list`, etc.)
before mutating, and adding a per-tool dry-run mode would diverge from the
established MCP convention.

### Flag precedence

`--dry-run` wins over `--force`. Setting both produces the dry-run preview and
makes no API call. This is intentional: it lets a script author add `--dry-run`
to an existing scripted `--force` invocation as a temporary safety check
without removing the force flag. The alternative — declaring the combination a
parse-time error via `clap`'s `conflicts_with` — was considered and rejected
for that reason.

### Dry-run for label removal

`confluence label remove --dry-run` does not GET the page's existing labels
before printing its preview. Labels supplied on the command line are echoed
back verbatim, even if they do not exist on the page. This keeps the "no API
call" contract strict; the small honesty gap is accepted as a documented
trade-off rather than incurring a read round-trip on every dry-run.

### Sprint operations

`omni-dev atlassian jira sprint *` is out of scope. Investigation found no
destructive sprint subcommands (the `update` subcommand mutates state but does
not delete). If a destructive sprint operation is added later, it inherits the
same pattern.

## Consequences

**Positive**

- Uniform destructive-action UX across the five subcommands and three MCP
  tools. Operators no longer have to remember which commands prompt.
- Single shared helper reduces drift and gives one place to extend (richer
  prompt formatting, NO_COLOR awareness, etc.) in future.
- `--dry-run` provides a server-free way to sanity-check scripted invocations
  before re-running with `--force`.
- MCP parity with the established `confirm: true` pattern closes a real gap in
  the assistant interface.

**Negative**

- Breaking change for non-interactive callers of `jira watcher remove`,
  `jira link remove`, and `confluence label remove`: scripts that previously
  ran these commands without input must add `--force` or be rewritten to feed
  `y\n` on stdin.
- Breaking change for MCP callers of the same three tools: the `confirm` field
  is mandatory; calls without it now error.
- The label-remove dry-run is correctness-loose: labels that do not exist on
  the page still appear in the preview. Documented above.

**Neutral**

- The `--force` early-skip of the issue/page lookup in `jira delete` and
  `confluence delete` is preserved when `--force` is set on its own. When
  `--dry-run` is also set, the lookup runs so the preview can name the target.
- Adding `--dry-run` to commands that currently have `--force` (the two
  delete commands) does not change their behaviour for any existing flag
  combination.
