REQ-O-048: High-Stakes Commands Default to Dry-Run Mode
Tier: Opt-In | Priority: P0
Source: §75 Safe-Default Execution Mode Absent
Addresses: Severity: Critical / Token Spend: Low / Time: Critical / Context: Low
Description
A command makes the dry-run path its zero-argument default in one of two ways: safe_default: true, where the framework injects --live, or confirm_flag: "<name>", where the command's own boolean flag (such as --yes) confirms execution. Either way, a call without the confirming flag previews and a call with it runs.
safe_default. Commands that declare safe_default: true MUST execute in dry-run mode when invoked without --live. The framework MUST inject a --live flag on all safe_default: true commands, route execution through the dry-run path when --live is absent, and include meta.dry_run: true in every response envelope for that path. A live invocation MUST include meta.dry_run: false and meta.confirmed: true. The safe_default field MUST be present in the manifest response so agents can detect and adjust their invocation strategy.
confirm_flag. A command with danger_level mutating or destructive MAY declare confirm_flag with the name, without --, of a boolean flag it accepts: one of its own flags or a root flag. The framework does not inject that flag; it MUST refuse the registration when the named flag is not a declared boolean flag of the command or root, when the command's danger_level is safe, when the same command declares safe_default: true (two confirmation mechanisms on one command), when the command is a passthrough command (REQ-C-031), or when the command implements no dry-run path. At run time:
- Without the confirm flag, the command runs the dry-run path under the dry-run contract (REQ-C-004): a
would_*effect, awould_affectobject,meta.dry_run: true, exit0, and no side effects. It does not prompt for the confirmation the flag stands for, with or without a TTY; the preview takes the prompt's place - With the confirm flag, the command executes and the envelope carries
meta.dry_run: falseandmeta.confirmed: true --dry-runwins: when the command accepts--dry-runand it is given, the command previews even when the confirm flag is given too. REQ-O-050'sexec --dry-run, which forwardsdry_run: trueto every dispatched mutating or destructive command, therefore previews aconfirm_flagcommand even when its dispatched line passes the confirm flag- When REQ-O-021 is enabled, a destructive command's
confirm_flagMUST beconfirm-destructive, so the command keeps one confirmation flag. Without it the command previews and exits0instead of exiting2withCONFIRMATION_REQUIRED - When the confirm flag is
yeson aninteractive: truecommand (REQ-C-005),--yesstill auto-confirms every other prompt;--non-interactivenever turns the missing confirmation into exit4, since the command previews instead of prompting
The manifest MUST expose confirm_flag on every command that declares it, so an agent knows before the first call that a bare call only previews and which flag runs it.
This requirement is distinct from REQ-C-004 (--dry-run availability) and REQ-O-021 (--confirm-destructive gate). Safe-default mode makes the dry-run path the zero-argument default (not a flag the caller must remember) and uses --live or the declared confirm flag as the explicit opt-in to execution.
Acceptance Criteria
- A
safe_default: truecommand invoked without--livereturns awould_*effect, exits 0, and causes no side effects - A
safe_default: truecommand invoked with--liveexecutes and returns an effect without awould_prefix - The response envelope always includes
meta.dry_run(boolean) forsafe_default: truecommands and for commands that declareconfirm_flag - The manifest exposes
safe_default: trueon commands that declare it - The framework raises a registration error if a
safe_default: truecommand does not implement a dry-run path - A command that declares
confirm_flagand is invoked without that flag returns awould_*effect, carriesmeta.dry_run: true, exits 0, and causes no side effects, in a TTY and outside one - A command that declares
confirm_flagand is invoked with that flag executes, returns an effect without awould_prefix, and carriesmeta.dry_run: falseandmeta.confirmed: true - A command that declares
confirm_flagand is invoked with both--dry-runand that flag previews, carriesmeta.dry_run: true, and causes no side effects tool exec --dry-runpreviews a dispatchedconfirm_flagcommand whose line passes the confirm flag- The framework raises a registration error for a
confirm_flagthat names no declared boolean flag of the command or root, on asafecommand, on a command that also declaressafe_default: true, on a passthrough command, and on a command without a dry-run path - With REQ-O-021 enabled, the framework raises a registration error for a destructive command whose
confirm_flagis notconfirm-destructive - The manifest exposes
confirm_flagon every command that declares it
Schema
Types: response-envelope.md · manifest-response.md · manifest-response.json
The meta object is extended with dry_run:
{
"meta": {
"dry_run": {
"type": "boolean",
"description": "True when the command ran in preview mode; false when --live or the confirm flag was passed and side effects occurred"
}
}
}
The manifest command entry is extended with safe_default and confirm_flag:
{
"safe_default": {
"type": "boolean",
"description": "True when the command defaults to dry-run mode; --live is required to execute for real"
},
"confirm_flag": {
"type": "string",
"description": "Boolean flag, without leading --, that confirms execution; without it the command previews"
}
}
The manifest schema rejects confirm_flag on a safe command, next to safe_default: true, and on a passthrough command. That the name refers to a declared boolean flag is checked at registration.
Wire Format
Without --live (dry-run by default):
$ trade execute --symbol BTC --amount 10000
{
"ok": true,
"data": {
"effect": "would_execute",
"would_affect": {
"order": { "symbol": "BTC", "side": "buy", "amount": 10000 },
"estimated_cost_usd": 847230.00,
"reversible": false
}
},
"error": null,
"warnings": [],
"meta": { "exit_code": 0, "dry_run": true, "duration_ms": 42 }
}
With --live:
$ trade execute --symbol BTC --amount 10000 --live
{
"ok": true,
"data": { "effect": "executed", "order_id": "ord_abc123" },
"error": null,
"warnings": [],
"meta": { "exit_code": 0, "dry_run": false, "confirmed": true, "duration_ms": 381 }
}
The --live flag appears in --schema:
{
"flags": {
"live": {
"type": "boolean",
"required": false,
"default": false,
"description": "Execute for real; omit to preview scope without side effects"
}
}
}
A mutating command that runs only with --yes declares it in the manifest:
{
"description": "Apply pending schema migrations",
"danger_level": "mutating",
"required_scopes": [],
"confirm_flag": "yes",
"flags": {
"yes": { "type": "boolean", "required": false, "default": false, "description": "Apply the migrations; omit to preview them" },
"dry-run": { "type": "boolean", "required": false, "default": false, "description": "Preview the migrations even when --yes is given" }
},
"exit_codes": {}
}
Without --yes, and with --dry-run --yes, the call previews:
$ db migrate
$ db migrate --dry-run --yes
{
"ok": true,
"data": {
"effect": "would_migrate",
"would_affect": { "migrations": ["0042_add_index", "0043_drop_legacy"], "reversible": false }
},
"error": null,
"warnings": [],
"meta": { "exit_code": 0, "dry_run": true, "duration_ms": 18 }
}
With --yes alone, it runs:
$ db migrate --yes
{
"ok": true,
"data": { "effect": "migrated", "applied": ["0042_add_index", "0043_drop_legacy"] },
"error": null,
"warnings": [],
"meta": { "exit_code": 0, "dry_run": false, "confirmed": true, "duration_ms": 912 }
}
Example
register command "execute":
danger_level: destructive
safe_default: true # framework injects --live; dry-run is the zero-arg default
execute(args, live=False):
order = build_order(args.symbol, args.amount)
cost = estimate_cost(order)
if not live:
return response(
effect="would_execute",
would_affect={"order": order, "estimated_cost_usd": cost, "reversible": False},
meta={"dry_run": True}
)
result = submit_order(order)
return response(
effect="executed",
order_id=result.id,
meta={"dry_run": False, "confirmed": True}
)
register command "migrate":
danger_level: mutating
flags: yes (boolean), dry-run (boolean)
confirm_flag: yes # framework checks --yes is a declared boolean flag
# framework: run = args.yes and not args.dry_run
migrate(args, run):
pending = pending_migrations()
if not run:
return response(effect="would_migrate", would_affect={"migrations": pending},
meta={"dry_run": True})
applied = apply(pending)
return response(effect="migrated", applied=applied,
meta={"dry_run": False, "confirmed": True})
register command "reset":
danger_level: safe
confirm_flag: yes
→ framework error: confirm_flag requires danger_level mutating or destructive
Related
| Requirement | Tier | Relationship |
|---|---|---|
| REQ-C-002 | C | Provides: danger_level: "destructive" is a prerequisite for safe_default: true, and mutating or destructive for confirm_flag |
| REQ-C-004 | C | Extends: safe-default mode is the next level above opt-in --dry-run availability; --dry-run wins over a confirm flag |
| REQ-C-005 | C | Composes: a confirm_flag of yes confirms execution as well as every prompt |
| REQ-C-031 | C | Composes: a passthrough command cannot preview, so it declares no confirm_flag |
| REQ-O-021 | O | Composes: --confirm-destructive and --live serve different patterns; safe-default replaces the gate with a natural preview → commit workflow, and a destructive confirm_flag names confirm-destructive when the gate is enabled |
| REQ-O-050 | O | Composes: exec --dry-run previews a confirm_flag command even when its line passes the confirm flag |
| REQ-O-041 | O | Exposes: safe_default and confirm_flag appear in the manifest |
| REQ-F-004 | F | Wraps: both dry-run and live responses use ResponseEnvelope |
| REQ-F-021 | F | Extends: meta.dry_run field added to standard envelope meta |