Skip to content

Reversibility Decision Engine

The reversibility decision engine is what makes automation Level 4 different from Level 3: instead of escalating every genuine decision to the human, L4 classifies each decision one-way (irreversible) vs two-way (reversible) and acts on the classification. It is part of the Level 4 capability (#865) and realizes use case #863.

It formalizes an existing taxonomy — it does not invent one. The “Reversible action” row already lives in the Decision Classification matrix, and the irreversible categories are exactly the ones the full-auto floor (profiles/full-auto.json, #867) denies at the tool level. The engine is the semantic layer above that tool-level floor: the floor blocks the dangerous call; the engine decides whether a choice may be auto-made at all.

scripts/warp-drive/decision-engine.js (zero-dependency Node). A decision is a short description and/or a proposed command; the classifier returns a stable verdict { door, category, reversible, confidence, rationale }.

Door Meaning L4 routing
two-way Reversible — undoable by a revert, re-edit, or config flip. Auto-decide and record a decision issue; continue.
one-way Irreversible, or unrecognized (fails safe). Notify-and-wait, or stop; never auto-resolved.

Formalized from the full-auto floor’s irreversible denials:

Category Why it is a one-way door
money-spend Spending real money / provisioning a paid resource cannot be undone.
external-comms Email / SMS / social posts cannot be unsent.
destructive-data Dropping or truncating data is irrecoverable.
prod-deploy A production release past the ceiling is a one-way door.
history-loss Force-push / hard reset / force-delete destroys history irrecoverably.
provisioning Registering a domain / creating an external account needs a human owner.
subjective-signoff Brand / naming / legal / pricing calls are not the agent’s to make.
unknown An unrecognized decision — defaults to one-way so it escalates.

code-edit, branch-op, reversible-config, doc-change, dependency-add, requirement-tracking — all undoable, so safe to auto-decide and record.

Safety bias — never guess a one-way door

Section titled “Safety bias — never guess a one-way door”

One-way patterns are checked first, so a decision that matches both fails safe. An unrecognized decision defaults to one-way (low confidence): auto- deciding something that turns out irreversible is the dangerous failure mode, so the engine escalates rather than guesses (use case #863). Recognized reversible work still classifies two-way and auto-proceeds, so the common warp-drive decision runs unattended.

Biasing toward the most reversible option (AC-01)

Section titled “Biasing toward the most reversible option (AC-01)”

most-reversible --options '<json array>' grades a set of options and returns the most reversible one (ties break to the earliest). When no option is reversible it flags noReversibleOption so the caller escalates instead of auto-picking a one-way door.

Command Purpose Exit codes
classify --description "..." [--command "..."] [--json] Classify one decision. 0 two-way (may auto-decide) · 3 one-way (must escalate) · 2 usage
classify --from-json <file|-> [--json] Classify a decision object from JSON. as above
most-reversible --options '<json array>' [--json] Pick the most reversible option. 0 picked · 3 none reversible
decide --description "..." [--options '<json>'] [--rationale ...] [--chosen ...] [--issue NN] [--rdb] [--dry-run] [--json] Classify and act: a two-way door records a decision issue and returns success (the loop continues); a one-way door escalates and is never auto-resolved. 0 recorded / two-way · 3 escalated / one-way · 1 gh error · 2 usage

The exit code lets a shell caller gate without parsing JSON: exit 3 means “do not auto-decide — this is a one-way door.”

route(classification, { rdbEnabled }) turns a classification into an action — the semantic core of the L4 policy:

Door Action Records Continues?
two-way auto-decide a decision issue (decision,warp-drive) with the options considered + rationale yes — no human stop (AC-02)
one-way (RDB on) notify-and-wait escalates; never auto-resolved (AC-03)
one-way (RDB off) file-todo a todo (defer default) escalates; never auto-resolved (AC-03)

A one-way door is never turned into an auto-decision. The hard stop on a one-way door is a timeout consequence owned by the decision-timeout policy (#875, below), not this router — the router’s immediate action is always notify/defer.

decide wires this end-to-end: for a two-way door it builds the decision-issue body (via buildDecisionIssueBody, biasing the choice toward the most reversible option) and files it with gh; for a one-way door it prints the escalation routing and exits 3 without touching gh. --dry-run prints the body and files nothing — the safe way to preview or to test.

The router decides the immediate action for a one-way door (notify-and-wait, or file-todo when RDB is off). It deliberately does not decide how long to wait or what to do when the wait elapses — that is the decision-timeout policy, scripts/warp-drive/decision-timeout-policy.js (a separate pure module, mirroring budget-policy.js). The governing rule is use case #863: never guess a one-way door. A one-way decision the agent cannot get a human to make is never auto-walked — it is deferred (skip the blocked item, keep doing other work) or, when it blocks all remaining work, the session stops cleanly.

decision_timeout_minutes (default 10) governs the wait:

decision_timeout_minutes Door RDB Outcome
> 0 two-way any Auto-decided immediately — the timeout never applies to a reversible call.
> 0 one-way on Notify-and-wait up to N min. Reply → apply the human decision. No reply → defer the blocked item and continue, or stop if it blocks everything. Never a guess.
> 0 one-way off No interactive channel: “notify” degrades to a filed TODO with a defer default — record the decision for the human and continue (or stop if it blocks everything). Never a guess.
0 two-way any Auto-decided immediately (unchanged).
0 one-way any Never wait — “work around or stop”: defer-and-continue, or a clean stop when it blocks everything. Never a guess.

The one invariant on every path is guessedOneWay: false — the policy never resolves a one-way door by guessing. A negative timeout is nonsensical and clamps to 0 (never an unbounded wait).

Command Purpose Exit codes
outcome --door <one-way|two-way> [--timeout N] [--rdb] [--reply <yes|no|none>] [--blocks-everything] [--json] Decide the timeout consequence for one decision. 0 proceed (auto-decide / proceed-with-reply / defer-and-continue) · 3 stop cleanly · 4 notify-and-wait (the loop must wait waitMinutes, then re-invoke with --reply) · 2 usage

--reply is a tri-state: none (the initial call — decide whether to wait), yes (a human replied — apply the decision), no (the wait elapsed — apply the terminal defer/stop). The exit code lets a shell caller gate without parsing JSON.

A two-way door is auto-decided and recorded — but a long L4 run could quietly accumulate dozens of individually-reasonable auto-decisions that compound into an unreviewed architecture. The diff-based no-progress breaker can’t catch that (each decision makes real progress). The decision budget does: it caps the number of auto-decisions per session and, at the cap, forces a mandatory human digest checkpoint.

  • max_auto_decisions (_workflow, default 10) is the cap. 0 disables it. It only bites at Level 4 — below L4 the engine never auto-decides, so the counter stays 0 and L2/L3 are unaffected.
  • Each auto-decision is accrued via the state machine and appended to the session digest (state.decisions):
    Terminal window
    node ~/.claude/scripts/warp-drive/state-machine.js record-decision "$(pwd)" \
    --description "<the decision>" --url "<the decision-issue URL>" [--category <cat>]
    Wire this into the L4 loop right after decision-engine.js decide files the decision issue for a two-way door.
  • Reaching the cap makes the next transition halt at budget_exceeded — the same circuit breaker as the cost/stall limits, but this one is a mandatory checkpoint: it is never auto-continued past, even at L4 with budget remaining (it is listed in budget-policy.js’s always-halt set, so #873’s bounded auto-continue cannot walk past it).
  • The checkpoint presents the digest for review/reversal — each auto-decision is a two-way door, so any that looks wrong can be reverted:
    Terminal window
    node ~/.claude/scripts/warp-drive/state-machine.js decisions "$(pwd)" # unreviewed window
    node ~/.claude/scripts/warp-drive/state-machine.js decisions "$(pwd)" --all # full session history
    Continuing (budget_continue) marks the reviewed decisions and resets the window, so the next max_auto_decisions auto-decisions trigger the next digest.
  • warp status shows Decisions: <count> / <cap> (yellow at the cap), and warp config lists max_auto_decisions.

The dry-run pre-flight — “clear the runway” — is a standalone, read-mostly pass that runs before a full-auto build (exposed as /warp-drive --dry-run). scripts/warp-drive/preflight.js takes the work’s foreseeable decisions and, using the classifier above:

  1. pre-classifies each decision one-way/two-way,
  2. auto-resolves and records the two-way (reversible) doors — biased to the most reversible option — as decision issues, and
  3. emits every one-way (irreversible) door as a human-only TODO up front (paid accounts, API keys, domain registration, design sign-offs), conforming to the todo-format contract.

It executes no build and takes no irreversible action — the planner and the body builders are pure; the only side effects (filing decision + todo issues, both reversible) live in the emit layer. You wake up to a runway report: what was pre-decided, and what human-only blockers remain.

Command Purpose Exit codes
plan --decisions '<json array>' [--issue NN] [--json] Pure preview — classify + split + report. Files nothing. 0 runway cleared · 3 blockers remain
emit --decisions '<json array>' [--issue NN] [--json] File a decision per two-way door and a conformant todo per one-way door, then print the runway report. No code changes. 0 cleared · 3 blockers remain

A decision is a string or { description, command?, options?, issue? } — the same shape classify accepts, plus an optional issue (the requirement a blocker gates, used in the TODO’s Blocked by #NN). Exit 3 (“blockers remain”) lets a caller gate an unattended build on a cleared runway.

Within warp-drive, the planning phase branches here when session.kickoff.dry_run is set: it runs emit, posts the runway report, and transitions dry_run_completesession_ending — the build never runs.

make test-decision-engine runs tests/test-decision-engine.js — unit tests over the classifier plus CLI exit-code-contract tests. make test-decision-timeout-policy runs tests/test-decision-timeout-policy.js — unit tests over the timeout policy plus its CLI exit-code contract (0 proceed / 3 stop / 4 wait). make test-decision-budget runs tests/test-decision-budget.js — the cap → mandatory-checkpoint route, the digest, and reset-on-continue. make test-preflight runs tests/test-preflight.js — the dry-run split, conformant blocker TODOs (validated against the real todo-format contract), and the runway report. All are deterministic and dependency-free (no gh, no Claude, no sleeping), safe for CI.