ADR 0113: Destructive Workspace Operations — File Deletion in Flush

Status

Accepted (2026-08-18)

Context

Problem

ADR 0082 (Method-Level Edit and Save in the Live Workspace) shipped a full patch/create story — Behaviour compile:source: / tryCompile:source:, Workspace newClass:at:, Workspace flush — and explicitly deferred anything that deletes a file:

Class-level removal flush UXaClass removeFromSystem already exists (BT-785) for memory removal. What it should mean to flush a class removal — deleting a .bt file from disk — is irreversibly destructive and wants its own UX...

ADR 0112 (Method-Level Removal Language Primitive) then shipped Behaviour removeSelector: / removeSelector:ifAbsent: — the in-memory half of method removal — but drew its own scope boundary at the same line:

What Workspace flush does with a "remove-method" entry is explicitly out of scope for this ADR. Splicing dead text out of a live .bt file is a destructive disk operation... it deserves the same confirmation/tombstone/undo design ADR 0082 already deferred class-level removal-flush to BT-2192 for.

This ADR is that design, narrowed to the two flows that flush deletions. (BT-2192 originally also scoped in class/method rename; that turned out to carry real, unresolved design risk — an xref index gap for live-patched code, a selector-name-global-scope correctness problem in naive auto-rewrite, an undesigned in-memory cross-gen-server atomicity question — that has nothing to do with deletion. Rename is split out to its own follow-up, BT-3204, so this ADR can ship the small, low-risk half on its own. See Migration Path for how the split was decided.)

Two flows share one root cause — flush turning a durable in-memory change into an on-disk file mutation that cannot be undone by re-running the operation, the way a patch's byte-span replacement can:

  1. Method removal flushed to disk. Counter removeSelector: #increment already installs in memory and logs a kind: "remove-method" ChangeLog entry (ADR 0112, BT-3187). Workspace flush today explicitly excludes these entries from being spliced (see Current State) — they sit in Workspace changes forever, never applied, never reported as skipped-for-a-real-reason.
  2. Class removal flushed to disk. Counter removeFromSystem (BT-785) already removes the class from memory, purges every derived registry (BT-3105), and returns — but it does not append a ChangeLog entry at all today. There is no record that a removal happened, and no way for flush to know a .bt file should be deleted.

Current State

ConcernToday
Counter removeSelector: #selInstalls in memory, logs kind: "remove-method" with span, prev_source_ref, side, flushable (ADR 0112, BT-3187)
Workspace flush handling of "remove-method" entriesExplicitly excluded before splicing (beamtalk_workspace_flush:exclude_remove_method/1) — the entry survives shadowing (so a stale patch to the same selector doesn't wrongly resurrect it) but is never written to disk and never marked flushed. It stays in Workspace changes indefinitely with no path forward.
Counter removeFromSystemRemoves class from memory, purges xref/extensions/protocol/compiler-cache/class_sources (BT-3105). Does not append a ChangeLog entry. No record exists that the removal happened; Workspace changes shows nothing.
LSP workspace/applyEditRuntime emits one Change-shaped edit per touched file on flush (FlushEvent { files: Vec<String> }, crates/beamtalk-lsp/src/runtime.rs) — no DeleteFile operation is ever constructed.
Undo (Workspace changes revert:)Handles patch-modify, patch-add, and new-class (ADR 0082). Untested/undesigned for remove-method (though the schema already carries prev_source_ref for it), and has no case for remove-class.
Workspace flush confirmationSingle mode: writes every intent: durable AND flushable: true entry, no distinction by how destructive the write is.

Constraints

  1. ADR 0082's two-phase flush protocol (Phase A: validate + stage every write; Phase B: commit renames in sequence) is the existing atomicity mechanism for multi-file flush. Whatever this ADR adds must compose with it, not replace it — CLAUDE.md's no-duplicate-implementations rule leaves no other option once a working two-phase primitive exists.
  2. ADR 0082 Amendment 1 split the workspace into two audiences with different default postures: the LiveView cockpit is human, git-first, autoflush: true by default; MCP is agent, ChangeLog-first, autoflush: false always. Any confirmation UX this ADR designs has to work for both — a synchronous "are you sure?" dialog makes sense for a human clicking a button and is meaningless for a programmatic MCP call.
  3. ADR 0112's flushability precedent. compile:source: and removeSelector: both install unconditionally in memory and vary only flushable/not_flushable_reason for stdlib/dynamic/dependency classes ("flushability, not refusal"). removeFromSystem (BT-785) instead hard-refuses stdlib and subclassed classes before installing. This ADR doesn't revisit that choice for removeFromSystem — it only adds the missing ChangeLog logging to it.
  4. Reproducible-build guarantee (ADR 0082, ADR 0112): flush must never write into the stdlib source tree or a dependency's cache, exactly as it doesn't today for patches or method removal.
  5. Surface parity (docs/development/surface-parity.md, ADR 0082's "every tool op is a structured invocation of a Beamtalk expression"). Whatever this ADR adds must be reachable identically from REPL, MCP, LSP, and browser.

Decision

Fix the ChangeLog logging gap in the existing removeFromSystem, and extend Workspace flush with a second, explicit tier: ordinary Workspace flush applies every entry that only edits an existing file (patch, new-class, and now method removal, since excising a span leaves the file in place); a new unscoped Workspace flushIncludingDestructive selector (plus the keyword-argument flush: aClass confirmDestructive: true / flushKinds: aSet confirmDestructive: true variants, where the class/kind argument gives the second keyword a real partner) is required to apply an entry that deletes a file (class removal). This tiering — not autoflush, not a tombstone file — is the safety mechanism, and it applies uniformly regardless of the autoflush setting. No new language primitive is added — removeSelector: (ADR 0112) and removeFromSystem (BT-785) already exist; this ADR is entirely about what flush does with the ChangeLog entries they already produce (or, for removeFromSystem, should produce).

Why a confirmation tier at all

The Linear issue's two flows split into different risk classes once you ask "does flushing this entry destroy a file, or only edit its contents?":

FlowWhat flush doesFile survives?Tier
Method removalExcise the recorded byte span from the (still-existing) source fileYes1 — ordinary flush
Class removalDelete the .bt fileNo2 — confirmDestructive

Method removal reads, at first glance, like it belongs with class removal — both stem from a remove* primitive. But the actual risk a confirmation gate exists to catch is "flush is about to make a file disappear," and splicing dead text out of an existing file is mechanically identical to what a normal patch already does unconfirmed today (byte-span replacement — the empty-replacement special case). Gating it behind confirmDestructive would be inconsistent with compile:source: for no safety benefit, and would leave Workspace changes permanently non-empty for the common case of "I deleted one dead method," which is exactly the stuck state Current State describes today.

Fixing removeFromSystem's missing ChangeLog entry

Required fix, not new ground: Counter removeFromSystem must append a kind: "remove-class" ChangeLog entry on every successful removal of a flushable class, mirroring the audit-trail-is-unconditional rule ADR 0082 established for every other in-memory mutation ("every in-memory method mutation produces a ChangeEntry. Always."). Today it produces none, which means Workspace changes cannot answer "was this class removed, and is that removal reflected on disk?" — the same gap ADR 0082's audit trail exists to close everywhere else. removeFromSystem already refuses stdlib/dependency classes before acting (BT-785, unchanged by this ADR), so the entry it appends is always either flushable: true (ordinary project class) or flushable: false, not_flushable_reason: "dynamic" (ClassBuilder class, no file to delete) — never "stdlib" or "dependency:...", since those cases never reach the append point.

ChangeLog schema extension

Extending ADR 0082's open kind enum exactly where it said it would ("remove-method" already shipped via ADR 0112/BT-3187; "remove-class" is new):

%% remove-class — appended by removeFromSystem
{ts, seq, epoch, class, selector: null,
 kind: "remove-class",
 side: null,
 source_ref: null,
 prev_source_ref: "<seq>-prev.bt" | null,     % full class source, for revert:
 sourceFile: "<path>" | null,
 span: null,                                    % whole file, not a byte range
 intent: "durable",
 flushable: bool,
 not_flushable_reason: "dynamic" | null,        % never "stdlib"/"dependency" — refused earlier
 author, author_kind: "human" | "agent"}

Every other field on this entry matches ADR 0082's established shape exactly — no new concepts, just a new kind value and the whole-file (span: null) framing new-class already established for a non-byte-range target.

Workspace flush — the destructive tier

Workspace flush
=> flushed 2 methods across 1 file
   skipped: 1 destructive entry (Counter — remove-class) —
     use `Workspace flushIncludingDestructive` to include it

Workspace flushIncludingDestructive
=> flushed 2 methods + 1 removal across 2 files

Delete atomicity — extending ADR 0082's Phase A/B, not replacing it

ADR 0082's two-phase protocol (Phase A: validate every target, stage every write as <file>.tmp; Phase B: rename each .tmp into place, entries pruned only as each rename succeeds) already handles "write N files, all-or-nothing modulo a documented partial-failure mode." This ADR extends the staging step to cover delete, keeping the same two-phase shape:

OperationPhase A (stage)Phase B (commit)
Patch / new-class (existing)Write <file>.tmpRename <file>.tmp<file>
Method removal (Tier 1)Write <file>.tmp with the span excisedRename <file>.tmp<file> (identical to a patch — this is why Tier 1 needs no new atomicity work)
Class removal (Tier 2)Rename <file><file>.tmp-delete-<epoch>-<seq> (same-filesystem rename, POSIX-atomic, trivially reversible)unlink <file>.tmp-delete-<epoch>-<seq>

Because remove-class targets exactly one file, this ADR needs none of ADR 0082's multi-file sequencing complexity — a single staged rename, then a single unlink. A crash between the two leaves a recoverable .tmp-delete-* file on disk (nothing lost); a re-flush finishes the unlink. This staged-rename step is the closest thing this ADR has to a tombstone, and it is intentionally ephemeral, not persistent — see Steelman Analysis.

Disambiguating a missing <file> from a recoverable mid-delete crash. Phase A's stat <file> can fail for two different reasons that look identical from <file>'s own absence: (a) this entry's own prior flush attempt already completed the rename-to-.tmp-delete-* step and crashed before the unlink, or (b) something else deleted <file> externally, unrelated to this entry. These need different handling — (a) must finish the unlink to complete the recorded operation, (b) is the soft-success/prune path below. The tie-breaker: before concluding "externally deleted," Phase A checks for <file>.tmp-delete-<epoch>-<seq> using this entry's own epoch/seq (already part of the ChangeLog entry's identity, per ADR 0082's schema). If it exists, this is case (a) — finish the unlink and complete the entry normally, not as a soft success. Only when no matching .tmp-delete-* exists does Phase A fall through to the external-deletion path below. This keeps the "a re-flush finishes the unlink" recovery claim actually true, rather than racing against the external-edit conflict table pruning the entry first and orphaning the staged file.

Undo story

Workspace changes revert: (ADR 0082) extends to both kinds, symmetric with prev_source_ref's existing role:

Kindrevert: behaviour
remove-methodRe-installs prev_source_ref at the recorded selector/side — the recorded prior body is exactly what compile:source: needs, so revert is a patch back to the pre-removal method. Already implied by the schema (ADR 0112 recorded prev_source_ref on removal specifically for this); this ADR is the first thing that actually exercises it.
remove-classRecompiles and reinstalls the whole class from prev_source_ref (the full pre-removal source, captured at hook time by removeFromSystem's new logging step) via the same Workspace newClass:at:-shaped install path new-class revert already uses (ADR 0082, BT-2664) — reusing that path rather than inventing a second whole-class-install mechanism.

Once flushed, revert: degrades to "best-effort, pre-flush semantics only" for the same reason ADR 0082 already documents for ordinary flushed patches — the ChangeEntry is pruned on successful flush, so post-flush undo is git's job (for humans, per Amendment 1) or a fresh corrective operation (for agents, who can re-run newClass:at: from the same prev_source_ref snapshot if they kept it — the ChangeLog's own audit/archive retains it per ADR 0082's rotation policy even after pruning from the active view). This ADR does not add a third undo mechanism beyond "revert before flush" and "git/re-create after flush" — see Steelman Analysis, tombstone question.

External-edit conflicts

Reuses ADR 0082's (mtime, content-hash) snapshot-and-compare mechanism verbatim, extended to the one new failure shape deletion introduces beyond "content changed":

ConflictDetectionResolution
Target file's content changed since the entry was logged (patch, remove-method)Existing mechanism, unchangedExisting choices: flush:force, changes clear, changes diff:
Target file for a remove-class was already deleted externallystat fails at Phase A, and no <file>.tmp-delete-<epoch>-<seq> matching this entry's own epoch/seq exists (see Delete atomicity — that case is a mid-delete crash recovery, not an external deletion, and finishes the unlink instead)Surfaces as already gone — nothing to remove, a soft success: the entry is pruned, the outcome the user wanted already holds
Target file for a still-pending remove-class entry no longer matches its recorded snapshot before revert: (BT-3213, review follow-up)reinstall_reverted_class/3 reads the current on-disk sourceFile and compares it byte-for-byte against the entry's prev_source_ref snapshot before reinstalling (check_no_external_drift/3) — the same comparison the flush-path row above uses, applied to a whole file instead of a byte span, since a pending remove-class entry never touches disk itself (only an explicit flushIncludingDestructive does). Usually an out-of-band edit (another session, git, an editor) landing while the removal sat pending; can also be a class with an earlier durable-but-unflushed patch, which the check cannot distinguish from an external edit (both are a content mismatch with no further signal)Structured error, revert refused: the class stays removed, the on-disk content is untouched, and the original remove-class entry stays pending (not retired) rather than silently discarding whatever the disk/snapshot mismatch represents

Reproducible-build guarantee

Unaffected by construction: removeFromSystem refuses stdlib/dependency classes before any ChangeEntry exists, so a remove-class entry is never flushable against protected source. Flush still never writes into the stdlib tree or a dependency cache — the same guarantee ADR 0082/0112 already state, unmodified.

Surface

Per ADR 0082's principle, every surface constructs one of the Beamtalk expressions above and submits via the existing evaluate op — no new workspace-side dispatcher op. What differs by surface, per Amendment 1, is what "confirm" means:

SurfaceAudienceConfirmation gesture
REPLHuman (or scripted):remove-class <Class> prompts y/N at the terminal before constructing Counter removeFromSystem and then, if the class was flushable, a second prompt before running the follow-up :flush-destructive (a distinct meta-command, mirroring the :flush/:flush <Class> pair with a :flush-destructive/:flush-destructive <Class> pair rather than a flag on :flush — the REPL meta-command layer has no --flag precedent, only positional args) — or the human runs :flush (Tier 1 only) and later :flush-destructive explicitly. Two prompts, matching two genuinely separate decisions (remove from memory vs. delete from disk).
MCPAgentThe remove_class tool constructs the memory-mutating expression only — it does not implicitly flush. A distinct flush tool call with an explicit confirm_destructive: true argument is required to reach disk, mirroring try_methodsave_method's existing two-step promotion idiom (ADR 0082): the first call is exploratory/reversible-via-revert, the second is the one that actually commits. No interactive dialog exists for MCP, so the tool schema's required boolean argument is the confirmation — an agent cannot flush a destructive entry by accident because the parameter has no default.
LSPEditor (VSCode etc.)workspace/executeCommand: flush gains an optional confirmDestructive argument; the VSCode extension surfaces a native modal ("This will delete foo.bt — Continue?") before sending it, listing the affected path from Workspace changes (already queryable pre-flush).
BrowserHuman, cockpitA "Remove Class" action performs the memory-mutating call immediately (matching autoflush: true's existing "the memory step is not gated" behaviour for ordinary patches) but the resulting dirty indicator for that entry renders with a distinct "destructive — needs confirmation" affordance instead of silently participating in the autoflush write, requiring one explicit click ("Delete file") to actually call confirmDestructive: true. This is the browser's analogue of the REPL's second prompt — same two-decision shape, native-to-cockpit affordance instead of a terminal prompt.

docs/development/surface-parity.md gains one row (removeFromSystem's logging fix needs no new row — it is the same expression as today, just with an audit-trail side effect) and a note that confirmDestructive is a parity-preserving argument, not a surface-specific rule: every surface can express "flush without destructive entries" and "flush including them," they just gate the second one differently because a modal dialog and a required tool-schema argument are the same shape of gate (an explicit, un-defaultable extra step) expressed in each surface's native idiom.

Prior Art

Pharo / Squeak Smalltalk

Method/class removal in Pharo (Behavior>>#removeSelector:, SystemDictionary>>#removeClassNamed:) is blunt — no automatic confirmation gate, matching ADR 0112's own choice not to block removeSelector: on dangling senders. Pharo's .changes file is the canonical durable-log reference: every method edit appends a chunk to the changes file before the image even commits the change, which is what makes "save in place" tolerable elsewhere in Pharo's model.

Adopted: nothing new beyond what ADR 0082/0112 already adopted from Pharo's .changes model. Rejected: Pharo's removal bluntness for class removal specifically — this ADR gates remove-class's disk step behind confirmDestructive, where Pharo's removeClassNamed: just does it. The difference is disk-vs-memory: Pharo's removal is memory-only the same instant it happens (an image save is a separate, much coarser gesture); ours makes an irrecoverable filesystem change unless gated.

Git — rm as a distinct staged verb from add/commit

Git's index model treats a delete as a first-class staged operation (git rm) distinct from an ordinary content edit (git add after editing in place) — both still require a subsequent git commit to become durable, and both are trivially recoverable pre-commit (git checkout) and recoverable-with-effort post-commit (git revert, reflog). The "distinct verb, one commit step" shape is close to the design here: Tier 1 (edit) and Tier 2 (destroy) are different kinds of pending change, but both still funnel through one flush/commit gesture.

Adopted: distinguishing destructive changes as their own category within one staged-change model, rather than either (a) making every change equally "scary" (git doesn't require extra confirmation for git commit just because an rm is staged) or (b) making destructive changes an entirely separate workflow with their own commit step. confirmDestructive is closer to git commit --no-verify-style explicit intent than to a second commit command. Rejected: git's actual UX for the default case — git rm requires no extra confirmation flag at all; the safety net is entirely "it's just a commit, and commits are cheap to undo." This ADR does not rely on git-recoverability alone for the flush-time gate (see Steelman — "isn't git enough?") because MCP agents and REPL scripts don't necessarily commit between every flush, and because a destructive flush can happen against files that were never committed in the first place.

LSP — workspace/applyEdit with DeleteFile

The LSP spec's WorkspaceEdit.documentChanges array supports typed resource operations — CreateFile, RenameFile, DeleteFile — alongside ordinary TextDocumentEdits, specifically so a server-initiated change can tell the client "this isn't a content edit, this is a file-system operation" and let the client apply its own UX for that distinction. This is the mechanism ADR 0082 left unused — flush today emits one edit-shaped event per file regardless of what actually happened to it.

Adopted: flush must emit DeleteFile for remove-class. Workspace newClass:at: flush should also switch from the generic Change shape to CreateFile — a pre-existing gap this ADR's LSP work closes as a side effect, since the typed-operation machinery has to exist anyway for DeleteFile. Not adapted, nothing to depart from: the spec's shape maps directly for a single-file delete; there is no Beamtalk-specific wrinkle here beyond needing to build it (nothing existed before).

Erlang / Elixir

Erlang/Elixir modules are all-or-nothing load/purge (code:purge/1 unloads a whole module; there is no "delete part of a module"). This is expected: Beamtalk's method-level granularity is a language-level abstraction the class gen_server provides on top of whole-module BEAM loading, not something the BEAM itself offers. No new prior art to adopt or reject beyond what ADR 0082/0112 already recorded.

User Impact

Newcomer (from VSCode / Python / JS)

Smalltalk developer

Erlang/BEAM developer

Production operator

Tooling developer (LSP/MCP/browser)

Steelman Analysis

Confirm-by-default (confirmDestructive required) vs. unconfirmed (destructive entries flush like anything else)

Tombstone-first (leave a .bt.deleted marker on disk) vs. immediate-delete (git is the only durable record)

Separate command per destructive kind (e.g., Workspace flushRemovals) vs. one unified confirmDestructive mechanism

Tension points

Alternatives Considered

Alternative: do nothing — leave destructive-flush UX undesigned

There is no workaround for remove-class today at all — removeFromSystem has no flush story and none is emerging on its own. remove-method technically "does nothing" today by construction — Workspace flush silently excludes those entries — but that is not a stable resting state, it is an already-shipped bug surface (Current State): every removeSelector: call already produces a ChangeLog entry that can never be satisfied, growing without bound in a long-lived workspace. Rejected: ADR 0082 explicitly named this ADR's flows as deferred-not-abandoned work, BT-2192 exists specifically to make that deferral good, and "do nothing" does not avoid a design decision here — it leaves an already-committed-to gap unfilled and lets the remove-method bug compound.

Alternative: no confirmation tier — destructive entries flush like any other durable+flushable entry

See Steelman above. Rejected as a silent, breaking behaviour change for every existing unmodified Workspace flush caller once the remove-class kind starts appearing in a workspace's ChangeLog.

Alternative: persistent on-disk tombstone (<file>.bt.deleted) instead of (or in addition to) confirmDestructive

See Steelman above. Rejected as a third, redundant source of truth about what currently exists, echoing ADR 0082's Alternative F (Shadow-file overlay) rejection reasoning. The staged-rename-then-unlink step already gives equivalent crash-safety without the permanence.

Alternative: separate flushRemovals command instead of flush: confirmDestructive:

See Steelman above. Rejected in favour of extending the existing flushKinds: filter mechanism, avoiding a second, parallel vocabulary for "which entries to apply" — and leaving room for BT-3204 to extend the same confirmDestructive gate to rename kinds later without inventing a new mechanism.

Consequences

Positive

Negative

Neutral

DDD Model Impact

Implementation

(For downstream implementation work — this ADR does not implement any of the below.)

Affected components

LayerChange
runtime/apps/beamtalk_runtime/src/beamtalk_behaviour_intrinsics.erlclassRemoveFromSystemByName/1 gains a ChangeLog-append call at its existing success point (publish_class_removed/2 call site), capturing the class's full current source as prev_source_ref before the removal proceeds (mirrors the existing "read+parse before mutate" ordering compile:source:'s patch hook already uses).
runtime/apps/beamtalk_workspace/src/beamtalk_workspace_changelog.erlNew kind: 'remove-class' on the entry record, extending the existing open-schema pattern — no new fields beyond what new-class/remove-method already established.
runtime/apps/beamtalk_workspace/src/beamtalk_workspace_flush.erlRemove the current unconditional exclude_remove_method/1 filtering and replace with tier classification (entry_tier(Entry) -> tier1 | tier2); do_flush/1 gains a confirmDestructive parameter threaded through filter_entries/2; single-file staged-delete added per Delete atomicity above — everything else (grouping, shadow-duplicate detection, per-file status reporting) is reused unmodified.
stdlib/src/Workspace.btNew unscoped flushIncludingDestructive and two-keyword flush:confirmDestructive: variant on the existing flush/flush: facade methods; flushKinds:confirmDestructive: on ChangeLog.
crates/beamtalk-lsp/src/runtime.rsFlushEvent needs to carry per-file operation (Change/Create/Delete), not just a flat path list — a breaking change to the existing struct's shape, needed regardless of this ADR, since new-class flush already deserves CreateFile and doesn't get it today.
crates/beamtalk-lsp/src/server.rsworkspace/executeCommand: flush gains a confirmDestructive argument; emits DeleteFile/CreateFile resource operations per the extended FlushEvent.
crates/beamtalk-mcp/src/server.rsNew tool: remove_class (wraps removeFromSystem); flush tool gains a required-when-applicable confirm_destructive boolean.
crates/beamtalk-cli/src/commands/repl/mod.rsNew meta-command :remove-class, with the two-prompt shape from Surface above; new :flush-destructive / :flush-destructive <Class> pair alongside the existing :flush / :flush <Class> pair.
runtime/apps/beamtalk_workspace/priv/static/workspace.js"Remove Class" browser action with the distinct destructive-dirty-indicator affordance from Surface above.
docs/development/surface-parity.mdOne new expression-backed row; a note on confirmDestructive as a parity-preserving argument (see Surface).
docs/beamtalk-language-features.mdDocument Workspace flushIncludingDestructive alongside the existing removeFromSystem/removeSelector: sections.

Phased rollout

PhaseScopeEffortTests
1removeFromSystem ChangeLog-logging fix (kind: "remove-class") — the smallest, most independently-shippable piece, and the one closing an audit gap that exists today with zero new UX to design.SEUnit: entry appears with correct flushable/prev_source_ref; BUnit: Workspace changes shows it.
2Workspace flush tiering: classify remove-method/remove-class as Tier 1/Tier 2 respectively, implement flushIncludingDestructive + confirmDestructive: filter dimension, implement single-file class-removal staged-delete (rename-to-tmp, unlink). Unblocks the already-stuck remove-method entries as a side effect (Tier 1, no new gate needed for them).MEUnit: staged-delete crash-safety (kill between rename and unlink); BUnit: Workspace flush reports skipped: destructive, flushIncludingDestructive applies it.
3revert: extensions for both kinds (Undo story table above).SBUnit: revert of each kind, pre-flush and (where applicable) documented as unsupported post-flush.
4LSP DeleteFile/CreateFile typed resource operations (FlushEvent restructuring); MCP tool; REPL meta-commands; browser action. Surface-parity audit.MLSP command tests; MCP integration tests; browser e2e for the two-gesture destructive flow; surface-parity drift check passes.

Total: ~S-M across 4 phases — no phase carries the kind of unvalidated, load-bearing risk BT-3204's rename design does, because every mechanism here (single-file byte-span splice, single-file staged delete, prev_source_ref-backed revert) is a direct reuse of infrastructure ADR 0082/0112 already shipped and proved.

Migration Path

No user code changes required — every new surface (flushIncludingDestructive, flush:confirmDestructive:) is additive, and removeFromSystem's call signature is unchanged (it now logs, but the caller sees the same nil return).

One real behaviour change for already-shipped code, worth calling out explicitly rather than leaving implicit: ADR 0112/BT-3187 already ships removeSelector:, and any workspace that has been calling it has "remove-method" entries sitting in its ChangeLog today — permanently pending, since Workspace flush currently excludes them unconditionally (see Current State). Once Phase 2 of this ADR lands, the next ordinary Workspace flush call (Tier 1, no flushIncludingDestructive needed) will apply every such backlog entry, splicing the removed method's byte span out of its source file for real, for the first time. This is the intended fix, not a side effect to guard against — the entries were already durable, already flushable, and already represented a user's explicit removeSelector: call; they were simply stuck. But it does mean a long-running workspace upgraded to a build containing this ADR's Phase 2 can see disk writes on its next flush that a workspace administrator did not request at flush time — the request was made earlier, when removeSelector: was originally called. Operators with long-lived workspaces should be aware that upgrading and then running Workspace flush may write more than the immediately-preceding session's edits. No opt-out is provided (splicing dead text out of a still-existing file is Tier 1 by this ADR's own classification, see Why a confirmation tier at all) — an operator who wants to inspect the backlog first can do so pre-upgrade via Workspace changes select: [:e | e kind = #'remove-method'].

Scope split from the original bundled draft. This ADR originally also designed renameTo:/renameSelector:to: (class/method rename). During review, rename turned out to carry real, unresolved design risk unrelated to deletion — an xref index gap for live-patched code (beamtalk_xref:build_method_entry/5 hard-codes references => [] for any live-patched method), a correctness problem in naive sender-rewriting (sendersOf: is selector-name-keyed with no receiver-type narrowing, so blindly rewriting every "sender" would corrupt unrelated code sharing a selector name), and an undesigned in-memory cross-gen-server atomicity question. None of these affect deletion. Rather than hold the small, safe half hostage to the harder open questions, rename was split out to BT-3204 as its own follow-up ADR, and this ADR was narrowed to file deletion only. No functionality is lost by the split — nothing in the original draft's deletion design changed, only rename-specific content moved out.

For ADR 0046 (VSCode sidebar): no migration — it consumes workspace/applyEdit, which continues to fire per touched file; the DeleteFile/CreateFile typed-operation upgrade is additive precision, not a contract change existing consumers depend on differently.

References

Implementation Tracking

Epic: BT-3205 Issues: BT-3206 (Phase 1 — Foundation), BT-3207 (Phase 2 — Core mechanism), BT-3208 (Phase 3 — Undo), BT-3209 (Phase 4 — Surfaces), BT-3210 (Phase 4 — Surfaces), BT-3211 (Phase 4 — Validation) Status: Planned