Appearance
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.
Classification
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. |
One-way categories (must escalate)
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. |
Two-way categories (may auto-decide)
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
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)
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.
CLI
| 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."
Routing & recording (AC-02, AC-03)
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.
Decision-timeout policy (#875)
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).
CLI
| 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.
Decision budget (#876)
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, default10) is the cap.0disables it. It only bites at Level 4 — below L4 the engine never auto-decides, so the counter stays0and L2/L3 are unaffected.- Each auto-decision is accrued via the state machine and appended to the session digest (
state.decisions):bashWire this into the L4 loop right afternode ~/.claude/scripts/warp-drive/state-machine.js record-decision "$(pwd)" \ --description "<the decision>" --url "<the decision-issue URL>" [--category <cat>]decision-engine.js decidefiles thedecisionissue 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 inbudget-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:bashContinuing (
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 historybudget_continue) marks the reviewed decisions and resets the window, so the nextmax_auto_decisionsauto-decisions trigger the next digest. warp statusshowsDecisions: <count> / <cap>(yellow at the cap), andwarp configlistsmax_auto_decisions.
Dry-run pre-flight (#878)
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:
- pre-classifies each decision one-way/two-way,
- auto-resolves and records the two-way (reversible) doors — biased to the most reversible option — as
decisionissues, and - 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_complete → session_ending — the build never runs.
Testing
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.
See also
- Automation Behavior — the Decision Classification matrix (with the L4 column) and the irreversible-action floor.
- Warp-Drive State Reference — the loop the engine gates decisions within.