You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@juneau.apache.org by ja...@apache.org on 2026/09/23 21:40:46 UTC

(juneau) branch juneau-release-manager updated: Make Input and Execution Page Subtabs of New Release, remove safe mode, fill the execution layout vertically with shadow-only cards, present Releases as a single card, save editable Version / Development version / Milestone #, and make the row-detail header transparent with a release icon. Also remove unused imports and dead test helpers, drop the unused rs assignment while still calling require(version), document that ProcessRunner's timeout is ignored on the interface default, and suppre [...]

This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch juneau-release-manager
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/juneau-release-manager by this push:
     new adfdcf6783 Make Input and Execution Page Subtabs of New Release, remove safe mode, fill the execution layout vertically with shadow-only cards, present Releases as a single card, save editable Version / Development version / Milestone #, and make the row-detail header transparent with a release icon. Also remove unused imports and dead test helpers, drop the unused rs assignment while still calling require(version), document that ProcessRunner's timeout is ignored on the interfac [...]
adfdcf6783 is described below

commit adfdcf678313fbde60f662b0afbe23592147edb0
Author: James Bognar <ja...@apache.org>
AuthorDate: Wed Sep 23 17:40:40 2026 -0400

    Make Input and Execution Page Subtabs of New Release, remove safe mode, fill the execution layout vertically with shadow-only cards, present Releases as a single card, save editable Version / Development version / Milestone #, and make the row-detail header transparent with a release icon. Also remove unused imports and dead test helpers, drop the unused rs assignment while still calling require(version), document that ProcessRunner's timeout is ignored on the interface default, and s [...]
---
 e2e/tests/nav.spec.ts                              |   5 +-
 e2e/tests/new-release-subtabs.spec.ts              |  84 +++++++++++
 e2e/tests/smoke.spec.ts                            |   7 +-
 .../org/apache/juneau/releng/AppConfiguration.java |  84 ++---------
 .../apache/juneau/releng/config/TargetProfile.java |   7 +-
 .../apache/juneau/releng/engine/DropRcService.java |  79 +++-------
 .../apache/juneau/releng/engine/ExecutionMode.java |  40 -----
 .../apache/juneau/releng/engine/ReleaseEngine.java | 151 ++++++------------
 .../org/apache/juneau/releng/engine/RunState.java  |   1 -
 .../juneau/releng/engine/RunStateSnapshot.java     |   8 +-
 .../apache/juneau/releng/engine/RunStateStore.java |   9 ++
 .../apache/juneau/releng/engine/StepContext.java   |  37 -----
 .../engine/steps/BinaryArtifactsStageStep.java     |  46 +++---
 .../releng/engine/steps/ComposeVoteEmailStep.java  |   6 +-
 .../releng/engine/steps/DeploySnapshotStep.java    |   4 +-
 .../releng/engine/steps/DevDistVerifyStep.java     |  19 +--
 .../releng/engine/steps/DistPromoteStep.java       |  14 +-
 .../engine/steps/GithubReleaseCreateStep.java      |  17 +--
 .../releng/engine/steps/MilestoneCloseStep.java    |   2 +-
 .../releng/engine/steps/ReleasePerformStep.java    |   2 +-
 .../releng/engine/steps/ReleasePrepareStep.java    |   2 +-
 .../apache/juneau/releng/nexus/NexusMockModel.java |   3 +-
 .../apache/juneau/releng/nexus/NexusMockRest.java  |   8 +-
 .../juneau/releng/nexus/NexusStagingClient.java    |   4 +-
 .../apache/juneau/releng/rest/ReleaseRunRest.java  |  64 +++++---
 .../apache/juneau/releng/util/ProcessRunner.java   |   5 +
 src/main/resources/application.properties          |  17 ---
 src/main/resources/static/css/chrome.css           |   7 +
 src/main/resources/static/css/new-release.css      | 145 +++---------------
 src/main/resources/static/js/new-release.js        |  60 ++++----
 src/main/resources/static/js/releases-detail.js    |  17 +++
 src/main/resources/templates/base.ftlh             |   7 +-
 src/main/resources/templates/new-release.ftlh      | 168 ++++++++-------------
 src/main/resources/templates/releases.ftlh         |  11 +-
 .../{SafeModeGuardTest.java => ArmGuardTest.java}  |  72 ++-------
 .../juneau/releng/engine/DropRcServiceTest.java    | 108 +------------
 .../juneau/releng/engine/ReleaseEngineTest.java    |  59 ++++----
 .../juneau/releng/engine/RunStateSnapshotTest.java |   8 +-
 .../apache/juneau/releng/engine/RunStateTest.java  |  11 --
 .../juneau/releng/engine/SecretsOffArgvTest.java   |   3 +-
 .../engine/steps/BinaryArtifactsStageStepTest.java |  24 +--
 .../engine/steps/ComposeVoteEmailStepTest.java     |   2 +-
 .../releng/engine/steps/DevDistVerifyStepTest.java |  24 +--
 .../releng/engine/steps/DistPromoteStepTest.java   |  20 +--
 .../engine/steps/MilestoneCloseStepTest.java       |  15 +-
 .../engine/steps/ReleasePrepareStepTest.java       |   4 +-
 .../releng/engine/steps/SafeModeTierBTest.java     |  92 -----------
 .../juneau/releng/nexus/NexusMockRestHttpTest.java |   2 +-
 .../apache/juneau/releng/nexus/NexusMockTest.java  |   2 +-
 .../apache/juneau/releng/rest/AdminRestTest.java   |   6 +-
 .../juneau/releng/rest/ReleaseRunRestTest.java     | 151 +++++++++++-------
 .../juneau/releng/setup/SetupProbeServiceTest.java |   2 -
 52 files changed, 607 insertions(+), 1138 deletions(-)

diff --git a/e2e/tests/nav.spec.ts b/e2e/tests/nav.spec.ts
index 4680ff84a8..0e4e0f2328 100644
--- a/e2e/tests/nav.spec.ts
+++ b/e2e/tests/nav.spec.ts
@@ -47,8 +47,9 @@ test.describe('Top navigation', () => {
       expect(response?.status()).toBe(200);
 
       if (tab.name === 'New Release') {
-        await expect(page.getByRole('tab', { name: 'Input' })).toBeVisible();
-        await expect(page.getByRole('tab', { name: 'Execution' })).toBeVisible();
+        const nav = topNav(page);
+        await expect(nav.getByRole('link', { name: 'Input', exact: true })).toBeVisible();
+        await expect(nav.getByRole('link', { name: 'Execution', exact: true })).toBeVisible();
       } else if (tab.heading instanceof RegExp) {
         await expect(page.getByText(tab.heading).first()).toBeVisible();
       } else if (tab.heading) {
diff --git a/e2e/tests/new-release-subtabs.spec.ts b/e2e/tests/new-release-subtabs.spec.ts
new file mode 100644
index 0000000000..ae8287d43f
--- /dev/null
+++ b/e2e/tests/new-release-subtabs.spec.ts
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { test, expect, type Page } from '@playwright/test';
+
+function topNav(page: Page) {
+  return page.locator('nav.juneau-page-nav');
+}
+
+function pageTab(page: Page, name: string) {
+  return topNav(page).locator('a.juneau-page-nav-section', { hasText: name });
+}
+
+function pageSubtab(page: Page, name: string) {
+  return topNav(page).locator('a.juneau-page-nav-child', { hasText: name });
+}
+
+test.describe('New Release Page Subtabs', () => {
+  test('page-tab row is only Setup, Releases, New Release', async ({ page }) => {
+    await page.goto('/rest/runs');
+    const sections = topNav(page).locator('.juneau-page-nav-section');
+    await expect(sections).toHaveCount(3);
+    await expect(sections.nth(0)).toHaveText('Setup');
+    await expect(sections.nth(1)).toHaveText('Releases');
+    await expect(sections.nth(2)).toHaveText('New Release');
+  });
+
+  test('Input and Execution are page-nav children on a second row under New Release', async ({ page }) => {
+    await page.goto('/rest/runs');
+    await expect(pageSubtab(page, 'Input')).toBeVisible();
+    await expect(pageSubtab(page, 'Execution')).toBeVisible();
+    await expect(pageSubtab(page, 'Input')).toHaveAttribute('href', '/rest/runs?tab=input');
+    await expect(pageSubtab(page, 'Execution')).toHaveAttribute('href', '/rest/runs?tab=exec');
+
+    const newRelease = await pageTab(page, 'New Release').boundingBox();
+    const input = await pageSubtab(page, 'Input').boundingBox();
+    expect(newRelease, 'New Release page tab should be laid out').toBeTruthy();
+    expect(input, 'Input subtab should be laid out').toBeTruthy();
+    expect(input!.y).toBeGreaterThan(newRelease!.y + newRelease!.height - 1);
+  });
+
+  test('subtabs are hidden on Setup and Releases', async ({ page }) => {
+    await page.goto('/rest/setup');
+    await expect(pageSubtab(page, 'Input')).toHaveCount(0);
+    await expect(pageSubtab(page, 'Execution')).toHaveCount(0);
+    await expect(topNav(page).locator('.juneau-page-nav-section')).toHaveCount(3);
+
+    await page.goto('/rest/releases');
+    await expect(pageSubtab(page, 'Input')).toHaveCount(0);
+    await expect(pageSubtab(page, 'Execution')).toHaveCount(0);
+  });
+
+  test('explicit tab= selects Input vs Execution', async ({ page }) => {
+    await page.goto('/rest/runs?tab=input');
+    await expect(pageSubtab(page, 'Input')).toHaveAttribute('aria-current', 'page');
+    await expect(page.locator('#nr-panel-input')).toBeVisible();
+
+    await page.goto('/rest/runs?tab=exec');
+    await expect(pageSubtab(page, 'Execution')).toHaveAttribute('aria-current', 'page');
+    await expect(page.locator('#nr-panel-exec')).toBeVisible();
+  });
+
+  test('there is no SAFE / simulate-mode banner or copy', async ({ page }) => {
+    await page.goto('/rest/runs');
+    await expect(page.getByText(/SAFE MODE/i)).toHaveCount(0);
+    await expect(page.getByText(/simulate/i)).toHaveCount(0);
+    await page.goto('/rest/runs?tab=exec');
+    await expect(page.getByText(/SAFE MODE/i)).toHaveCount(0);
+  });
+});
diff --git a/e2e/tests/smoke.spec.ts b/e2e/tests/smoke.spec.ts
index 4ff52a32cb..d238003ea3 100644
--- a/e2e/tests/smoke.spec.ts
+++ b/e2e/tests/smoke.spec.ts
@@ -33,9 +33,10 @@ test.describe('Smoke: primary content renders without console errors', () => {
     const response = await page.goto('/rest/runs');
     expect(response?.status()).toBe(200);
 
-    // Input/Execution subtabs are always present regardless of whether a run is active.
-    await expect(page.getByRole('tab', { name: 'Input' })).toBeVisible();
-    await expect(page.getByRole('tab', { name: 'Execution' })).toBeVisible();
+    // Input/Execution are Page Subtabs under New Release (nested <@node> links).
+    const nav = page.locator('nav.juneau-page-nav');
+    await expect(nav.getByRole('link', { name: 'Input', exact: true })).toBeVisible();
+    await expect(nav.getByRole('link', { name: 'Execution', exact: true })).toBeVisible();
 
     expect(errors, `console errors on /rest/runs: ${errors.join('; ')}`).toEqual([]);
   });
diff --git a/src/main/java/org/apache/juneau/releng/AppConfiguration.java b/src/main/java/org/apache/juneau/releng/AppConfiguration.java
index b6bcb01e57..e02628762e 100644
--- a/src/main/java/org/apache/juneau/releng/AppConfiguration.java
+++ b/src/main/java/org/apache/juneau/releng/AppConfiguration.java
@@ -36,7 +36,6 @@ import org.apache.juneau.releng.email.EmailService;
 import org.apache.juneau.releng.config.TargetProfile;
 import org.apache.juneau.releng.engine.BranchResolver;
 import org.apache.juneau.releng.engine.DropRcService;
-import org.apache.juneau.releng.engine.ExecutionMode;
 import org.apache.juneau.releng.engine.ReleaseEngine;
 import org.apache.juneau.releng.engine.RunStateStore;
 import org.apache.juneau.releng.engine.StepRegistry;
@@ -45,7 +44,6 @@ import org.apache.juneau.releng.log.RunStateBroadcaster;
 import org.apache.juneau.releng.log.SseLogServlet;
 import org.apache.juneau.releng.milestone.GithubPrSource;
 import org.apache.juneau.releng.milestone.MilestoneService;
-import org.apache.juneau.releng.nexus.NexusMockRest;
 import org.apache.juneau.releng.nexus.NexusStagingClient;
 import org.apache.juneau.releng.release.GitTagReleaseSource;
 import org.apache.juneau.releng.release.GithubReleaseSource;
@@ -77,9 +75,6 @@ import jakarta.servlet.Servlet;
 })
 public class AppConfiguration {
 
-	/** Non-secret credential stand-in handed to the Nexus client on SAFE runs, which never reach a real server. */
-	private static final String SAFE_PLACEHOLDER = "safe-placeholder";
-
 	@Bean
 	public ProcessRunner processRunner() {
 		return new ProcessRunner.Default();
@@ -91,7 +86,7 @@ public class AppConfiguration {
 	// Two independent gates protect the mutating endpoints, and they answer different questions:
 	//
 	//   - This boundary answers "did this request come from the page we served".
-	//   - The engine's per-run arming (see ReleaseEngine.arm / rm.mode) answers "did a human mean it".
+	//   - The engine's per-run arming (see ReleaseEngine.arm) answers "did a human mean it".
 	//
 	// Before this boundary existed only the second gate was present, which meant a hostile page in the
 	// operator's browser could POST the derivable "<version> LIVE" confirm phrase to /arm as a plain
@@ -103,11 +98,6 @@ public class AppConfiguration {
 	//   - The confirm phrase is not a secret and never authenticated anything. It is derivable from a page the
 	//     attacker is already reading. Making it longer or hidden would not help -- see ReleaseEngine.arm. It is
 	//     typing friction against accident, which is a real thing to want and not this boundary's job.
-	//
-	//   - rm.mode defaulting to safe is an operational safety default, not a security control, even though it was
-	//     doing real security work until this boundary landed. Its protection disappears exactly when someone
-	//     starts the app in live mode -- during a real release, when a forged request costs the most. This
-	//     boundary applies in both modes. See application.properties.
 	// ==========================================================================================
 
 	/**
@@ -266,26 +256,12 @@ public class AppConfiguration {
 	// ==========================================================================================
 
 	/**
-	 * The box-wide execution mode. Defaults to SAFE; only {@code rm.mode=live} enables real mutation (and
-	 * even then, per-run arming is still required at the guard chokepoint).
-	 */
-	@Bean
-	public ExecutionMode executionMode(@Value("${rm.mode:safe}") String mode) {
-		return "live".equalsIgnoreCase(mode) ? ExecutionMode.LIVE : ExecutionMode.SAFE;
-	}
-
-	/**
-	 * The release target's centralized endpoints. Everything is the canonical Apache Juneau production value
-	 * except the Nexus base, which is the box-wide default: the in-app {@code /mock/nexus} loopback when
-	 * {@code rm.mode=safe}, real {@code repository.apache.org} when {@code rm.mode=live}. Per-run Dry-run on
-	 * a LIVE box still rewrites the Nexus base to the loopback (see {@link ReleaseEngine#setMockNexusBaseUrl}).
+	 * The release target's centralized endpoints. Canonical Apache Juneau production values, including
+	 * real {@code repository.apache.org} for Nexus.
 	 */
 	@Bean
-	public TargetProfile targetProfile(ExecutionMode mode, @Value("${server.address:127.0.0.1}") String address,
-			@Value("${server.port:8790}") int port) {
-		var base = mode == ExecutionMode.LIVE ? TargetProfile.prodDefault().nexusBaseUrl()
-				: "http://" + address + ":" + port + "/mock/nexus";
-		return TargetProfile.prodDefault().withNexusBaseUrl(base);
+	public TargetProfile targetProfile() {
+		return TargetProfile.prodDefault();
 	}
 
 	@Bean
@@ -306,7 +282,7 @@ public class AppConfiguration {
 	/** Resolves live secrets from CredentialService's stores per mutating action. */
 	@Bean
 	public ReleaseEngine.SecretResolver secretResolver(Map<CredentialSpec, SecretStore> stores, AccountStore accounts,
-			ExecutionMode mode, TargetProfile target, LoopbackBoundary boundary) {
+			TargetProfile target) {
 		return new ReleaseEngine.SecretResolver() {
 			private String read(CredentialSpec spec, String account) {
 				return stores.get(spec).find(account).map(String::new).orElse("");
@@ -344,16 +320,9 @@ public class AppConfiguration {
 			// Nexus credentials are the committer's Apache LDAP identity — the same Keychain-backed
 			// SecretStore entry as ldapPassword() above, because an encrypted <password> in settings.xml
 			// cannot be used directly. Falls back to ~/.m2/settings.xml only when that Keychain entry
-			// hasn't been stored yet. Under SAFE the base is the loopback mock and the credential is a
-			// throwaway placeholder — the real Keychain secret is never read or sent (OQ-C).
-			//
-			// The SAFE client also carries the boundary's self-call headers: its target is the mock on this
-			// application's own port, which sits behind the same filter as everything else and exempts nobody.
+			// hasn't been stored yet.
 			@Override
 			public NexusStagingClient nexus() {
-				if (mode == ExecutionMode.SAFE)
-					return NexusStagingClient.create(target.nexusBaseUrl(), target.nexusProfileId(), SAFE_PLACEHOLDER,
-							SAFE_PLACEHOLDER, boundary.selfCallHeaders());
 				return nexusClient(target, availid(), ldapPassword(), () -> NexusStagingClient
 						.create(target.nexusBaseUrl(), target.nexusProfileId(), "apache.releases.https"));
 			}
@@ -363,7 +332,7 @@ public class AppConfiguration {
 	/**
 	 * Builds the LIVE Nexus client from Keychain-backed credentials (via the {@link SecretStore} SPI, per
 	 * {@code availid}/{@code ldapPassword} above) when both are present, else defers to
-	 * {@code settingsXmlFallback}. The base URL + profile id come from the (mode-derived) {@link TargetProfile}.
+	 * {@code settingsXmlFallback}. The base URL + profile id come from {@link TargetProfile}.
 	 * Package-private for {@code AppConfigurationTest}.
 	 */
 	static NexusStagingClient nexusClient(TargetProfile target, String availid, String ldapPassword,
@@ -383,29 +352,22 @@ public class AppConfiguration {
 	})
 	public ReleaseEngine releaseEngine(RunStateStore store, StepRegistry registry, ProcessRunner runner,
 			BranchResolver branches, EmailService email, MilestoneService milestone,
-			ReleaseEngine.SecretResolver secrets, ExecutionMode mode, TargetProfile target, LoopbackBoundary boundary,
+			ReleaseEngine.SecretResolver secrets, TargetProfile target,
 			@Value("${rm.state.dir}") String stateDir, @Value("${rm.staging.dir}") String stagingDir,
-			@Value("${rm.repo.dir}") String repoDir, @Value("${rm.git.committer.email}") String committerEmail,
-			@Value("${server.address:127.0.0.1}") String address, @Value("${server.port:8790}") int port) {
+			@Value("${rm.repo.dir}") String repoDir, @Value("${rm.git.committer.email}") String committerEmail) {
 		var engine = new ReleaseEngine(store, registry, runner, branches, Path.of(stateDir), Path.of(stagingDir),
-				repoDir, committerEmail, email, milestone, secrets, mode, target);
-		engine.setMockNexusBaseUrl("http://" + address + ":" + port + "/mock/nexus");
-		engine.setLoopbackHeaders(boundary.selfCallHeaders());
+				repoDir, committerEmail, email, milestone, secrets, target);
 		engine.recoverOnBoot(); // Demote runs left mid-flight by a previous process on restart.
 		return engine;
 	}
 
 	@Bean
 	public DropRcService dropRcService(RunStateStore store, StepRegistry registry, ProcessRunner runner,
-			ReleaseEngine.SecretResolver secrets, ReleaseEngine engine, ExecutionMode mode, TargetProfile target,
-			LoopbackBoundary boundary, @Value("${rm.staging.dir}") String stagingDir,
+			ReleaseEngine.SecretResolver secrets, ReleaseEngine engine, TargetProfile target,
+			@Value("${rm.staging.dir}") String stagingDir,
 			@Value("${rm.state.dir}") String stateDir) {
-		var svc = new DropRcService(store, registry, runner, Path.of(stagingDir).resolve("git/juneau"), Path.of(stateDir),
-				secrets.nexus(), mode, engine::isArmed, target, engine::broadcaster);
-		if (engine.mockNexusBaseUrl() != null)
-			svc.setSafeNexus(NexusStagingClient.create(engine.mockNexusBaseUrl(), target.nexusProfileId(),
-					SAFE_PLACEHOLDER, SAFE_PLACEHOLDER, boundary.selfCallHeaders()));
-		return svc;
+		return new DropRcService(store, registry, runner, Path.of(stagingDir).resolve("git/juneau"), Path.of(stateDir),
+				secrets.nexus(), engine::isArmed, target, engine::broadcaster);
 	}
 
 	@Bean
@@ -413,22 +375,6 @@ public class AppConfiguration {
 		return new ReleaseRunRest(engine, dropRc);
 	}
 
-	/**
-	 * The in-app Nexus loopback mock. Always registered so a Dry-run on a LIVE box can still hit it; LIVE
-	 * runs use the real Nexus base URL and never call this servlet.
-	 */
-	@Bean
-	public NexusMockRest nexusMockRest(TargetProfile target) {
-		return new NexusMockRest(target.nexusProfileId());
-	}
-
-	@Bean
-	public ServletRegistrationBean<Servlet> nexusMockRegistration(NexusMockRest mock, ReleaseEngine engine) {
-		// Each new run resets the mock so the lazily-synthesized staging repo starts from a clean slate.
-		engine.setRunStartHook(() -> mock.model().reset());
-		return new ServletRegistrationBean<>(mock, "/mock/nexus/*");
-	}
-
 	/**
 	 * The SSE servlet — a plain HttpServlet, registered alongside RootRest. Serves the per-step console
 	 * channel keyed by {@code (version, stepId)} (resolving each step's log path from its
diff --git a/src/main/java/org/apache/juneau/releng/config/TargetProfile.java b/src/main/java/org/apache/juneau/releng/config/TargetProfile.java
index d1e934b49e..473f7d3b11 100644
--- a/src/main/java/org/apache/juneau/releng/config/TargetProfile.java
+++ b/src/main/java/org/apache/juneau/releng/config/TargetProfile.java
@@ -21,9 +21,8 @@ package org.apache.juneau.releng.config;
  * Centralizes the release target's endpoints in one config-backed, auditable object, replacing the endpoint
  * literals that used to be scattered across the pipeline steps and the Nexus client.
  *
- * <p>{@code nexusBaseUrl} is mode-derived by the application wiring: it points at the in-app loopback mock
- * under SAFE mode and at real Nexus under LIVE mode. Everything else defaults to the canonical Apache Juneau
- * production endpoints (see {@link #prodDefault()}). This slice ships prod defaults only; no fork target is
+ * <p>{@code nexusBaseUrl} defaults to real Nexus ({@link #prodDefault()}). Everything else defaults to the
+ * canonical Apache Juneau production endpoints. This slice ships prod defaults only; no fork target is
  * defined.
  */
 public record TargetProfile(String cloneUrl, String repoSlug, String ghSlug, String nexusBaseUrl,
@@ -36,7 +35,7 @@ public record TargetProfile(String cloneUrl, String repoSlug, String ghSlug, Str
 				"https://dist.apache.org/repos/dist/release/juneau");
 	}
 
-	/** A copy of this profile with a different Nexus base URL (used to inject the mode-derived base). */
+	/** A copy of this profile with a different Nexus base URL. */
 	public TargetProfile withNexusBaseUrl(String url) {
 		return new TargetProfile(cloneUrl, repoSlug, ghSlug, url, nexusProfileId, distDevBase, distReleaseBase);
 	}
diff --git a/src/main/java/org/apache/juneau/releng/engine/DropRcService.java b/src/main/java/org/apache/juneau/releng/engine/DropRcService.java
index 79dec704fc..bc9fbdb4be 100644
--- a/src/main/java/org/apache/juneau/releng/engine/DropRcService.java
+++ b/src/main/java/org/apache/juneau/releng/engine/DropRcService.java
@@ -46,8 +46,6 @@ public class DropRcService {
 	private final Path stagingRepo;
 	private final Path stateDir;
 	private final NexusStagingClient nexus;
-	private NexusStagingClient safeNexus;
-	private final ExecutionMode mode;
 	private final Predicate<String> armed;
 	private final TargetProfile target;
 	private final BiFunction<String, String, LogBroadcaster> broadcasterFn;
@@ -55,7 +53,7 @@ public class DropRcService {
 	@SuppressWarnings({ "java:S107" // Constructor-injected collaborators; a parameter object would obscure the wiring.
 	})
 	public DropRcService(RunStateStore store, StepRegistry registry, ProcessRunner runner, Path stagingRepo,
-			Path stateDir, NexusStagingClient nexus, ExecutionMode mode, Predicate<String> armed, TargetProfile target,
+			Path stateDir, NexusStagingClient nexus, Predicate<String> armed, TargetProfile target,
 			BiFunction<String, String, LogBroadcaster> broadcasterFn) {
 		this.store = store;
 		this.registry = registry;
@@ -63,36 +61,14 @@ public class DropRcService {
 		this.stagingRepo = stagingRepo;
 		this.stateDir = stateDir;
 		this.nexus = nexus;
-		this.mode = mode == null ? ExecutionMode.SAFE : mode;
 		this.armed = armed == null ? v -> false : armed;
 		this.target = target == null ? TargetProfile.prodDefault() : target;
 		this.broadcasterFn = broadcasterFn == null ? (v, s) -> new LogBroadcaster() : broadcasterFn;
 	}
 
 	/**
-	 * Placeholder Nexus client aimed at the in-app loopback mock. Used for Dry-run drops on a LIVE box so
-	 * they never hit real Nexus. Null in tests keeps {@link #nexus}.
-	 */
-	public void setSafeNexus(NexusStagingClient client) {
-		this.safeNexus = client;
-	}
-
-	private ExecutionMode effectiveMode(RunState rs) {
-		var requested = rs.mode != null ? rs.mode : this.mode;
-		if (requested == ExecutionMode.LIVE && this.mode != ExecutionMode.LIVE)
-			return ExecutionMode.SAFE;
-		return requested;
-	}
-
-	private boolean live(RunState rs) {
-		return effectiveMode(rs) == ExecutionMode.LIVE;
-	}
-
-	/**
-	 * This action's own log — mirrors how {@code ReleaseEngine.context()} builds a step's {@link RunLog}, so
-	 * Drop-RC's Tier-B calls get the same "would run:" command-logging fidelity a registry step's do (rather
-	 * than the silent no-op this had before). Truncated at the start of every {@link #apply} so a re-drop
-	 * overwrites in place.
+	 * This action's own log — mirrors how {@code ReleaseEngine.context()} builds a step's {@link RunLog}.
+	 * Truncated at the start of every {@link #apply} so a re-drop overwrites in place.
 	 */
 	private Consumer<String> logSink(RunState rs) {
 		var path = stateDir.resolve("logs/" + rs.version + "-RC" + rs.rc + "-" + LOG_STEP_ID + ".log");
@@ -101,22 +77,6 @@ public class DropRcService {
 		return log.lineSink();
 	}
 
-	/**
-	 * The Tier-B seam for Drop-RC's mutating subprocess calls: runs the command in LIVE, logs a redacted
-	 * "would run:" line and no-ops in SAFE so a rehearsed drop has zero canonical side effects (the Tier-A
-	 * Nexus drop still round-trips the mock).
-	 */
-	private void tierB(List<String> command, String stdin, Map<String, String> env, Consumer<String> log, boolean live) {
-		if (live) {
-			runner.run(command, stdin, env);
-			return;
-		}
-		var line = "would run: " + String.join(" ", StepContext.redactArgv(command));
-		if (stdin != null)
-			line += " <stdin:redacted>";
-		log.accept(line);
-	}
-
 	/** Compute the drop plan without executing. */
 	public Preview preview(String version) {
 		var rs = store.load(version).orElseThrow();
@@ -133,36 +93,31 @@ public class DropRcService {
 	/** Execute the drop, bump RC, and reset. */
 	public synchronized void apply(String version, String reason, Supplier<String> availid, Supplier<String> password) {
 		var rs = store.load(version).orElseThrow();
-		// Default-safe guard chokepoint (mirrors ReleaseEngine.apply): the real destructive drop only runs on
-		// a LIVE run once it is armed. A Dry-run rehearses it (Tier-A drop against the mock, Tier-B logged).
-		if (live(rs) && !armed.test(version))
-			throw isex("Refused: drop-RC is a mutating action. Enable LIVE mode and arm run %s first.", version);
+		if (!armed.test(version))
+			throw isex("Refused: drop-RC is a mutating action. Arm run %s first.", version);
 		var tag = "juneau-" + rs.version + "-RC" + rs.rc;
 		var git = stagingRepo.toString();
 		var pw = password.get();
 		var log = logSink(rs);
-		var live = live(rs);
-		var safeClient = safeNexus != null ? safeNexus : nexus;
-		var client = live ? nexus : safeClient;
 
-		// a) drop Nexus staging repo (Tier A: real client round-trip, against the loopback mock in SAFE)
-		if (rs.nexusRepoId != null && client != null) {
+		// a) drop Nexus staging repo
+		if (rs.nexusRepoId != null && nexus != null) {
 			log.accept("Dropping Nexus staging repo " + rs.nexusRepoId);
-			client.drop(rs.nexusRepoId);
+			nexus.drop(rs.nexusRepoId);
 		}
 		// b) svn checkout dist/dev, rm the rejected RC's directories, commit
 		var dist = stateDir.resolve("dist");
-		tierB(List.of("svn", "checkout", SvnArgs.USERNAME, availid.get(), SvnArgs.PASSWORD_FROM_STDIN,
-				target.distDevBase(), dist.toString()), pw + "\n", Map.of(), log, live);
-		tierB(List.of("svn", "rm", dist.resolve("source").resolve(tag).toString()), null, null, log, live);
-		tierB(List.of("svn", "rm", dist.resolve("binaries").resolve(tag).toString()), null, null, log, live);
-		tierB(List.of("svn", "commit", dist.toString(), "-m", "Drop " + tag, SvnArgs.USERNAME, availid.get(),
-				SvnArgs.PASSWORD_FROM_STDIN), pw + "\n", Map.of(), log, live);
+		runner.run(List.of("svn", "checkout", SvnArgs.USERNAME, availid.get(), SvnArgs.PASSWORD_FROM_STDIN,
+				target.distDevBase(), dist.toString()), pw + "\n", Map.of());
+		runner.run(List.of("svn", "rm", dist.resolve("source").resolve(tag).toString()), null, null);
+		runner.run(List.of("svn", "rm", dist.resolve("binaries").resolve(tag).toString()), null, null);
+		runner.run(List.of("svn", "commit", dist.toString(), "-m", "Drop " + tag, SvnArgs.USERNAME, availid.get(),
+				SvnArgs.PASSWORD_FROM_STDIN), pw + "\n", Map.of());
 		// c) delete tag local + remote
-		tierB(List.of("git", "-C", git, "tag", "-d", tag), null, null, log, live);
-		tierB(List.of("git", "-C", git, "push", "origin", ":refs/tags/" + tag), null, null, log, live);
+		runner.run(List.of("git", "-C", git, "tag", "-d", tag), null, null);
+		runner.run(List.of("git", "-C", git, "push", "origin", ":refs/tags/" + tag), null, null);
 		// d) roll back the release:prepare version-bump commits
-		tierB(List.of("mvn", "-f", git + "/pom.xml", "release:rollback"), null, null, log, live);
+		runner.run(List.of("mvn", "-f", git + "/pom.xml", "release:rollback"), null, null);
 
 		// 4) reset state
 		rs.rcHistory.add(new RcHistoryEntry(rs.rc, Instant.now().toString(), reason));
diff --git a/src/main/java/org/apache/juneau/releng/engine/ExecutionMode.java b/src/main/java/org/apache/juneau/releng/engine/ExecutionMode.java
deleted file mode 100644
index 548a0be79f..0000000000
--- a/src/main/java/org/apache/juneau/releng/engine/ExecutionMode.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.juneau.releng.engine;
-
-/**
- * Global box-wide execution posture for the release engine, and the cap on per-run mode.
- *
- * <p>{@code SAFE} (the default) rehearses a release with zero canonical side effects: the Nexus staging
- * callouts run their real client flow against an in-app loopback mock, and every other mutating callout is
- * command-logged rather than executed. {@code LIVE} executes everything for real and additionally requires
- * the run to be armed before any mutating step is allowed.
- *
- * <p>Each run stores its own mode (Dry-run vs Actual on the start form). A LIVE run is only possible when
- * this box was started with {@code rm.mode=live}; a SAFE box always caps the run to SAFE.
- */
-public enum ExecutionMode {
-	SAFE, LIVE;
-
-	/** Case-insensitive parse; a null/blank/unknown value fails safe to {@link #SAFE}. */
-	public static ExecutionMode fromConfig(String value) {
-		if (value == null)
-			return SAFE;
-		return "live".equalsIgnoreCase(value.strip()) ? LIVE : SAFE;
-	}
-}
diff --git a/src/main/java/org/apache/juneau/releng/engine/ReleaseEngine.java b/src/main/java/org/apache/juneau/releng/engine/ReleaseEngine.java
index 457eadc113..2363a56de6 100644
--- a/src/main/java/org/apache/juneau/releng/engine/ReleaseEngine.java
+++ b/src/main/java/org/apache/juneau/releng/engine/ReleaseEngine.java
@@ -54,11 +54,10 @@ public class ReleaseEngine {
 	private final MilestoneService milestone;
 	// Secrets + nexus client are supplied by the REST layer per mutating action; test factory passes nulls.
 	private final SecretResolver secrets;
-	private final ExecutionMode mode;
 	private final TargetProfile target;
 
 	// The armed run, if any. Transient in-memory posture — deliberately NOT persisted, so it drops on any
-	// restart (re-arm required after a restart/crash). Only meaningful in LIVE mode.
+	// restart (re-arm required after a restart/crash).
 	private volatile String armedVersion;
 
 	// In-memory per-step broadcasters, keyed "version/stepId". Lost on restart; log files survive.
@@ -68,15 +67,6 @@ public class ReleaseEngine {
 	// client gets a fresh initial snapshot instead (see AppConfiguration's state resolver).
 	private final Map<String, RunStateBroadcaster> stateBroadcasters = new ConcurrentHashMap<>();
 
-	// Invoked when a new run starts, e.g. to reset the SAFE Nexus loopback mock. Defaults to a no-op.
-	private Runnable runStartHook = () -> {
-		// No-op by default; the SAFE-mode wiring installs a mock-reset hook.
-	};
-
-	// Loopback mock base (http://host:port/mock/nexus). Null in forTests so those keep secrets.nexus().
-	private String mockNexusBaseUrl;
-	private Map<String, String> loopbackHeaders = Map.of();
-
 	/** Everything the REST layer must provide to build a mutating StepContext. */
 	public interface SecretResolver {
 		String availid();
@@ -96,7 +86,7 @@ public class ReleaseEngine {
 	})
 	public ReleaseEngine(RunStateStore store, StepRegistry registry, ProcessRunner runner, BranchResolver branches,
 			Path stateDir, Path stagingRoot, String repoDir, String committerEmail, EmailService email,
-			MilestoneService milestone, SecretResolver secrets, ExecutionMode mode, TargetProfile target) {
+			MilestoneService milestone, SecretResolver secrets, TargetProfile target) {
 		this.store = store;
 		this.registry = registry;
 		this.runner = runner;
@@ -108,7 +98,6 @@ public class ReleaseEngine {
 		this.email = email;
 		this.milestone = milestone;
 		this.secrets = secrets;
-		this.mode = mode == null ? ExecutionMode.SAFE : mode;
 		this.target = target == null ? TargetProfile.prodDefault() : target;
 		// The single choke point for the New-Release tab's live rail push: every status-mutating
 		// transition — this engine's own methods AND DropRcService's drop-RC action, since it shares this
@@ -147,15 +136,7 @@ public class ReleaseEngine {
 		};
 		return new ReleaseEngine(store, registry, runner, branches, stateDir, stateDir.resolve("staging"), "/repo",
 				"test@apache.org", new EmailService(stateDir, runner), new MilestoneService(), noSecrets,
-				ExecutionMode.SAFE, TargetProfile.prodDefault());
-	}
-
-	/** Test factory pinned to a specific execution mode (for guard/arm/tier tests). */
-	public static ReleaseEngine forTests(RunStateStore store, StepRegistry registry, ProcessRunner runner,
-			BranchResolver branches, Path stateDir, ExecutionMode mode) {
-		var engine = forTests(store, registry, runner, branches, stateDir);
-		return new ReleaseEngine(store, registry, runner, branches, stateDir, stateDir.resolve("staging"), "/repo",
-				"test@apache.org", engine.email, engine.milestone, engine.secrets, mode, TargetProfile.prodDefault());
+				TargetProfile.prodDefault());
 	}
 
 	/** One broadcaster per (version, step). */
@@ -171,7 +152,7 @@ public class ReleaseEngine {
 	/** {@code version}'s current snapshot as JSON, or empty when there's no persisted run for it. */
 	public Optional<String> snapshotJson(String version) {
 		return store.load(version)
-				.map(rs -> Json.DEFAULT.write(RunStateSnapshot.of(rs, effectiveMode(rs), isArmed(version))));
+				.map(rs -> Json.DEFAULT.write(RunStateSnapshot.of(rs, isArmed(version))));
 	}
 
 	/**
@@ -182,7 +163,7 @@ public class ReleaseEngine {
 	 */
 	private void publishSnapshot(RunState rs) {
 		stateBroadcaster(rs.version)
-				.publish(Json.DEFAULT.write(RunStateSnapshot.of(rs, effectiveMode(rs), isArmed(rs.version))));
+				.publish(Json.DEFAULT.write(RunStateSnapshot.of(rs, isArmed(rs.version))));
 	}
 
 	/** Same as {@link #publishSnapshot(RunState)}, reloading the current persisted state for {@code version}. */
@@ -211,19 +192,9 @@ public class ReleaseEngine {
 	/**
 	 * Start a new run, recording the milestone number resolved (or overridden) on the New-Release form.
 	 * {@code milestoneNumber} may be null (no matching milestone; {@code milestone-close} then legitimately
-	 * no-ops). Defaults the run to Dry-run (SAFE).
+	 * no-ops).
 	 */
 	public synchronized RunState start(String version, String developmentVersion, Integer milestoneNumber) {
-		return start(version, developmentVersion, milestoneNumber, null);
-	}
-
-	/**
-	 * Start a new run. {@code requestedMode} is the form's Dry-run/Actual choice; null means Dry-run.
-	 * Actual (LIVE) is honored only when this box was started with {@code rm.mode=live} — a SAFE box
-	 * always caps the run to SAFE.
-	 */
-	public synchronized RunState start(String version, String developmentVersion, Integer milestoneNumber,
-			ExecutionMode requestedMode) {
 		var active = store.activeRun();
 		if (active.isPresent())
 			throw isex("A run is already active: %s (%s). Finish or drop it first.", active.get().version,
@@ -232,9 +203,7 @@ public class ReleaseEngine {
 		var rs = RunState.create(version, branch, registry.ids());
 		rs.developmentVersion = developmentVersion;
 		rs.milestoneNumber = milestoneNumber;
-		rs.mode = capMode(requestedMode);
 		store.save(rs);
-		runStartHook.run(); // e.g. reset the SAFE Nexus loopback mock so this run starts from a clean slate
 		return rs;
 	}
 
@@ -242,6 +211,9 @@ public class ReleaseEngine {
 	 * Update the optional narrative fields ({@code releaseSummary}, {@code highlights}, {@code knownIssues},
 	 * {@code acknowledgements}) on an existing run and persist. Any of the values may be null/blank; those
 	 * are simply stored and later omitted from the composed emails. Returns the updated run.
+	 *
+	 * <p>Does not touch {@code version}, {@code developmentVersion}, or {@code milestoneNumber} — {@code start()}
+	 * uses this after already writing those identity fields.
 	 */
 	public synchronized RunState updateDetails(String version, String releaseSummary, String highlights,
 			String knownIssues, String acknowledgements) {
@@ -254,50 +226,41 @@ public class ReleaseEngine {
 		return rs;
 	}
 
-	/** Installs a callback invoked on each {@link #start}, e.g. to reset the SAFE Nexus loopback mock. */
-	public void setRunStartHook(Runnable hook) {
-		this.runStartHook = hook == null ? () -> {
-			// No-op: clearing the hook restores default behavior.
-		} : hook;
-	}
-
-	/** Loopback mock base used for Dry-run Nexus callouts. Null in {@link #forTests} keeps {@code secrets.nexus()}. */
-	public void setMockNexusBaseUrl(String url) {
-		this.mockNexusBaseUrl = url;
-	}
-
-	public String mockNexusBaseUrl() {
-		return mockNexusBaseUrl;
-	}
-
 	/**
-	 * Headers the SAFE-mode Nexus client must present to get past the loopback write boundary, since the mock it
-	 * talks to is mounted on this application's own port. Empty in {@link #forTests}, where no boundary is
-	 * installed and the transport is a stub anyway.
+	 * Persist narrative fields plus identity fields from the Input form. {@code newVersion} may rename the
+	 * run (the store key is {@code release-&lt;version&gt;.json}); a colliding target version is rejected.
+	 * Blank {@code developmentVersion} clears the stored value.
 	 *
-	 * @see org.apache.juneau.rest.server.filter.LoopbackBoundary#selfCallHeaders()
+	 * <p>{@code release-prepare} still <em>derives</em> the next SNAPSHOT for maintenance releases
+	 * ({@code z>0}) and overwrites {@code developmentVersion} at apply time. Preflight re-resolves
+	 * {@code branch} from {@code version}.
 	 */
-	public void setLoopbackHeaders(Map<String, String> headers) {
-		this.loopbackHeaders = headers == null ? Map.of() : Map.copyOf(headers);
-	}
-
-	/**
-	 * The run's effective mode: persisted {@code rs.mode} (null → SAFE), capped so a SAFE box can never
-	 * execute LIVE even if on-disk state claims it.
-	 */
-	public ExecutionMode effectiveMode(RunState rs) {
-		return capMode(rs == null ? null : rs.mode);
-	}
-
-	private ExecutionMode capMode(ExecutionMode requested) {
-		var req = requested != null ? requested : ExecutionMode.SAFE;
-		if (req == ExecutionMode.LIVE && mode != ExecutionMode.LIVE)
-			return ExecutionMode.SAFE;
-		return req;
-	}
-
-	public ExecutionMode mode() {
-		return mode;
+	@SuppressWarnings({ "java:S107" // Narrative plus identity fields from the Input form; a parameter object would obscure the persistence mapping.
+	})
+	public synchronized RunState updateDetails(String version, String releaseSummary, String highlights,
+			String knownIssues, String acknowledgements, String newVersion, String developmentVersion,
+			Integer milestoneNumber) {
+		var rs = require(version);
+		rs.releaseSummary = releaseSummary;
+		rs.highlights = highlights;
+		rs.knownIssues = knownIssues;
+		rs.acknowledgements = acknowledgements;
+		rs.developmentVersion = ib(developmentVersion) ? null : developmentVersion.trim();
+		rs.milestoneNumber = milestoneNumber;
+		var trimmed = newVersion == null ? "" : newVersion.trim();
+		if (!trimmed.isEmpty() && !trimmed.equals(version)) {
+			if (store.load(trimmed).isPresent())
+				throw isex("A run already exists for %s", trimmed);
+			rs.version = trimmed;
+			rs.branch = branches.resolve(trimmed);
+			store.save(rs);
+			store.delete(version);
+			if (Objects.equals(armedVersion, version))
+				armedVersion = trimmed;
+		} else {
+			store.save(rs);
+		}
+		return rs;
 	}
 
 	public TargetProfile target() {
@@ -305,9 +268,8 @@ public class ReleaseEngine {
 	}
 
 	/**
-	 * Arms {@code version} for LIVE mutation. Rejected unless the box is LIVE, this run is Actual (LIVE),
-	 * and {@code confirm} equals the required phrase {@code "<version> LIVE"}. Returns the outcome
-	 * message-bearing result.
+	 * Arms {@code version} for mutation. Rejected unless {@code confirm} equals the required phrase
+	 * {@code "<version> LIVE"}. Returns the outcome message-bearing result.
 	 *
 	 * <p><b>Arming is an intent gate, and only that.</b> It establishes that a human meant to do something
 	 * irreversible; it establishes nothing about who or what sent the request. The two questions are separate, and
@@ -329,16 +291,12 @@ public class ReleaseEngine {
 	 * hard to type as it needs to be to prevent an accident, and no harder.
 	 */
 	public synchronized StepResult arm(String version, String confirm) {
-		if (mode != ExecutionMode.LIVE)
-			return StepResult.fail("Arming is only possible in LIVE mode; this box is running in SAFE mode.");
-		var rs = require(version);
-		if (effectiveMode(rs) != ExecutionMode.LIVE)
-			return StepResult.fail("Arming is only possible for an Actual (LIVE) run; this run is Dry-run.");
+		require(version);
 		if (!Objects.equals(confirm, version + " LIVE"))
 			return StepResult.fail("Type '" + version + " LIVE' to arm this run for live mutation.");
 		armedVersion = version;
 		publishSnapshot(version); // armed flag isn't part of RunState, so arming alone never calls store.save()
-		return StepResult.ok("Run " + version + " is armed for LIVE mutation.");
+		return StepResult.ok("Run " + version + " is armed for live mutation.");
 	}
 
 	public boolean isArmed(String version) {
@@ -372,9 +330,8 @@ public class ReleaseEngine {
 		var step = registry.byId(stepId);
 		if (step == null)
 			return StepResult.fail(UNKNOWN_STEP + stepId);
-		// Default-safe guard chokepoint: a mutating step is refused on a LIVE run until it is armed. A
-		// Dry-run (SAFE) simulates without arming, even when the box itself was started with rm.mode=live.
-		if (step.mutating() && effectiveMode(rs) == ExecutionMode.LIVE && !isArmed(version))
+		// Default-safe guard chokepoint: a mutating step is refused until the run is armed.
+		if (step.mutating() && !isArmed(version))
 			return StepResult.fail(guardMessage(step));
 		// Strict forward-apply guard: refuses to run stepId ahead of an unsatisfied required predecessor.
 		// This is also finalize-run's own prerequisite check, since finalize-run's predecessors are every
@@ -467,7 +424,7 @@ public class ReleaseEngine {
 	}
 
 	private String guardMessage(ReleaseStep step) {
-		return "Refused: '" + step.id() + "' is a mutating step. Enable LIVE mode and arm this run first.";
+		return "Refused: '" + step.id() + "' is a mutating step. Arm this run first.";
 	}
 
 	/**
@@ -542,16 +499,8 @@ public class ReleaseEngine {
 		var ctx = new StepContext();
 		ctx.run = rs;
 		ctx.runner = runner;
-		var runMode = effectiveMode(rs);
-		ctx.mode = runMode;
-		if (runMode == ExecutionMode.SAFE && mockNexusBaseUrl != null) {
-			ctx.target = target.withNexusBaseUrl(mockNexusBaseUrl);
-			ctx.nexus = NexusStagingClient.create(mockNexusBaseUrl, target.nexusProfileId(), "safe-placeholder",
-					"safe-placeholder", loopbackHeaders);
-		} else {
-			ctx.target = target;
-			ctx.nexus = secrets.nexus();
-		}
+		ctx.target = target;
+		ctx.nexus = secrets.nexus();
 		var log = new RunLog(stateDir.resolve(stepLogRelativePath(rs, stepId)), broadcaster(rs.version, stepId));
 		if (resetLog)
 			log.reset();
diff --git a/src/main/java/org/apache/juneau/releng/engine/RunState.java b/src/main/java/org/apache/juneau/releng/engine/RunState.java
index 2646cabefe..f7a826608f 100644
--- a/src/main/java/org/apache/juneau/releng/engine/RunState.java
+++ b/src/main/java/org/apache/juneau/releng/engine/RunState.java
@@ -37,7 +37,6 @@ public class RunState {
 	public String nexusRepoId; // set by nexus-staging-close
 	public String githubReleaseUrl; // set by github-release-create
 	public Integer milestoneNumber; // New-Release form field; pre-filled by gh title-match resolution, user-editable
-	public ExecutionMode mode; // per-run SAFE/LIVE, capped by the box-wide rm.mode; null on pre-toggle JSON → SAFE
 	// Optional operator-authored narrative, captured at start and editable later; threaded into the four
 	// release emails (blank fields are omitted from every email rather than rendering empty sections).
 	public String releaseSummary; // short "why we're releasing" line
diff --git a/src/main/java/org/apache/juneau/releng/engine/RunStateSnapshot.java b/src/main/java/org/apache/juneau/releng/engine/RunStateSnapshot.java
index 0f9225acd8..02ae9608ee 100644
--- a/src/main/java/org/apache/juneau/releng/engine/RunStateSnapshot.java
+++ b/src/main/java/org/apache/juneau/releng/engine/RunStateSnapshot.java
@@ -21,7 +21,7 @@ import java.util.ArrayList;
 import java.util.List;
 
 /**
- * A compact projection of a {@link RunState} (plus the engine-level {@code mode}/armed posture that isn't
+ * A compact projection of a {@link RunState} (plus the engine-level armed posture that isn't
  * part of {@code RunState} itself) pushed to every connected New-Release tab over the run-state SSE
  * channel. Deliberately NOT the whole {@code RunState} — the rail only needs each step's status, not its
  * timestamps/log refs/error text, which stay on the per-step console channel and the initial page render.
@@ -31,7 +31,6 @@ public class RunStateSnapshot {
 	public String version;
 	public RunStatus status;
 	public int rc;
-	public ExecutionMode mode;
 	public boolean armed;
 	public List<StepSnapshot> steps = new ArrayList<>();
 
@@ -39,13 +38,12 @@ public class RunStateSnapshot {
 		// No-arg constructor required so the JSON parser can instantiate this bean before populating fields.
 	}
 
-	/** Projects {@code rs} down to the fields the rail needs, folding in the engine-level mode/armed posture. */
-	public static RunStateSnapshot of(RunState rs, ExecutionMode mode, boolean armed) {
+	/** Projects {@code rs} down to the fields the rail needs, folding in the engine-level armed posture. */
+	public static RunStateSnapshot of(RunState rs, boolean armed) {
 		var s = new RunStateSnapshot();
 		s.version = rs.version;
 		s.status = rs.status;
 		s.rc = rs.rc;
-		s.mode = mode;
 		s.armed = armed;
 		for (var step : rs.steps)
 			s.steps.add(new StepSnapshot(step.id, step.status));
diff --git a/src/main/java/org/apache/juneau/releng/engine/RunStateStore.java b/src/main/java/org/apache/juneau/releng/engine/RunStateStore.java
index a327ac5e01..affdf01c5e 100644
--- a/src/main/java/org/apache/juneau/releng/engine/RunStateStore.java
+++ b/src/main/java/org/apache/juneau/releng/engine/RunStateStore.java
@@ -72,6 +72,15 @@ public class RunStateStore {
 		onSave.accept(rs);
 	}
 
+	/** Deletes the persisted file for {@code version} if it exists. Used when a run is renamed. */
+	public synchronized void delete(String version) {
+		try {
+			Files.deleteIfExists(fileFor(version));
+		} catch (IOException e) {
+			throw isex(e, "Cannot delete run state for %s", version);
+		}
+	}
+
 	public Optional<RunState> load(String version) {
 		var f = fileFor(version);
 		if (!Files.isRegularFile(f))
diff --git a/src/main/java/org/apache/juneau/releng/engine/StepContext.java b/src/main/java/org/apache/juneau/releng/engine/StepContext.java
index cbc80b411b..04084baee0 100644
--- a/src/main/java/org/apache/juneau/releng/engine/StepContext.java
+++ b/src/main/java/org/apache/juneau/releng/engine/StepContext.java
@@ -23,7 +23,6 @@ import java.nio.file.Path;
 import java.util.List;
 import java.util.Map;
 import java.util.function.Consumer;
-import java.util.function.Supplier;
 import org.apache.juneau.releng.config.TargetProfile;
 import org.apache.juneau.releng.email.EmailService;
 import org.apache.juneau.releng.milestone.MilestoneService;
@@ -36,7 +35,6 @@ public class StepContext {
 	public ProcessRunner runner;
 	public Consumer<String> log; // This step's RunLog.lineSink(): tees to that step's own log file
 									// and that step's own SSE broadcaster.
-	public ExecutionMode mode = ExecutionMode.SAFE; // stamped by the engine; SAFE unless LIVE is configured
 	public TargetProfile target = TargetProfile.prodDefault(); // stamped by the engine
 	public Path stagingRepo; // rm.staging.dir/git/juneau
 	public Path stateDir; // rm.state.dir
@@ -61,41 +59,6 @@ public class StepContext {
 		return runner.run(command, stdin, env, log);
 	}
 
-	/** Is this a LIVE run? (SAFE simulates mutating callouts; LIVE executes them.) */
-	public boolean live() {
-		return mode == ExecutionMode.LIVE;
-	}
-
-	/**
-	 * The Tier-B safe/live seam for mutating subprocess calls. In LIVE, runs the command (teed to the step
-	 * log). In SAFE, logs a redacted "would run:" line, spawns nothing, and returns synthetic success so the
-	 * downstream {@code .ok()} checks pass with zero side effects.
-	 */
-	public ProcessRunner.ProcResult dryRunOr(List<String> command) {
-		return dryRunOr(command, null, null);
-	}
-
-	public ProcessRunner.ProcResult dryRunOr(List<String> command, String stdin, Map<String, String> env) {
-		if (live())
-			return runner.run(command, stdin, env, log);
-		var line = "would run: " + String.join(" ", redactArgv(command));
-		if (stdin != null)
-			line += " <stdin:redacted>";
-		log.accept(line);
-		return new ProcessRunner.ProcResult(0, "[SAFE] simulated");
-	}
-
-	/**
-	 * The Tier-B seam for non-subprocess mutations (e.g. an in-process HTTP write): runs the supplier in
-	 * LIVE, logs the described action and returns {@code simulated} in SAFE.
-	 */
-	public <T> T dryRunOr(String describe, Supplier<T> real, T simulated) {
-		if (live())
-			return real.get();
-		log.accept("would " + describe);
-		return simulated;
-	}
-
 	/**
 	 * Belt-and-suspenders scrub of any secret-shaped token before a command is logged. Secrets are already
 	 * kept off argv (passed via stdin/env), so this only guards against a future call site accidentally
diff --git a/src/main/java/org/apache/juneau/releng/engine/steps/BinaryArtifactsStageStep.java b/src/main/java/org/apache/juneau/releng/engine/steps/BinaryArtifactsStageStep.java
index 3876db97a2..5341323fc6 100644
--- a/src/main/java/org/apache/juneau/releng/engine/steps/BinaryArtifactsStageStep.java
+++ b/src/main/java/org/apache/juneau/releng/engine/steps/BinaryArtifactsStageStep.java
@@ -67,7 +67,7 @@ public class BinaryArtifactsStageStep implements ReleaseStep {
 		var pw = ctx.ldapPassword + "\n";
 
 		// svn auth: --username <availid> --password-from-stdin (passphrase via stdin, never argv).
-		var co = ctx.dryRunOr(List.of("svn", "checkout", SvnArgs.USERNAME, ctx.availid, SvnArgs.PASSWORD_FROM_STDIN,
+		var co = ctx.exec(List.of("svn", "checkout", SvnArgs.USERNAME, ctx.availid, SvnArgs.PASSWORD_FROM_STDIN,
 				ctx.target.distDevBase(), dist.toString()), pw, null);
 		if (!co.ok())
 			return StepResult.fail("svn checkout of dist/dev failed.");
@@ -76,17 +76,17 @@ public class BinaryArtifactsStageStep implements ReleaseStep {
 		// juneau-release.sh's shell-expanded `svn rm dist/source/*` textually; ProcessRunner has no
 		// shell, so it's passed as one literal argument (a no-op against a fresh/empty checkout, which
 		// is the common case since ASF dist working copies normally hold at most one prior RC).
-		ctx.dryRunOr(List.of("svn", "rm", source + "/*"));
-		ctx.dryRunOr(List.of("svn", "rm", binaries + "/*"));
-		ctx.dryRunOr(List.of("mkdir", sourceRc.toString()));
-		ctx.dryRunOr(List.of("mkdir", binariesRc.toString()));
+		ctx.exec(List.of("svn", "rm", source + "/*"));
+		ctx.exec(List.of("svn", "rm", binaries + "/*"));
+		ctx.exec(List.of("mkdir", sourceRc.toString()));
+		ctx.exec(List.of("mkdir", binariesRc.toString()));
 
 		var nexusUrl = ctx.target.nexusBaseUrl() + "/content/repositories/" + ctx.run.nexusRepoId
 				+ "/org/apache/juneau/";
 		ctx.log.accept("Fetching signed source + binary artifacts from " + nexusUrl);
-		ctx.dryRunOr(List.of("wget", "-e", "robots=off", "--recursive", "--no-parent", "--no-directories", "-A",
+		ctx.exec(List.of("wget", "-e", "robots=off", "--recursive", "--no-parent", "--no-directories", "-A",
 				"*-source-release*", "-P", sourceRc.toString(), nexusUrl));
-		ctx.dryRunOr(List.of("wget", "-e", "robots=off", "--recursive", "--no-parent", "--no-directories", "-A",
+		ctx.exec(List.of("wget", "-e", "robots=off", "--recursive", "--no-parent", "--no-directories", "-A",
 				"juneau-distrib*-bin.zip*", "-P", binariesRc.toString(), nexusUrl));
 
 		ctx.log.accept("Renaming to apache-juneau-" + ctx.run.version + "-{src,bin}.zip[.asc|.sha512]");
@@ -95,9 +95,9 @@ public class BinaryArtifactsStageStep implements ReleaseStep {
 		renameAndChecksum(ctx, binariesRc, "juneau-distrib-" + ctx.run.version + "-bin.zip",
 				"apache-juneau-" + ctx.run.version + "-bin.zip");
 
-		ctx.dryRunOr(List.of("svn", "add", sourceRc.toString()));
-		ctx.dryRunOr(List.of("svn", "add", binariesRc.toString()));
-		var commit = ctx.dryRunOr(List.of("svn", "commit", dist.toString(), "-m", rc, SvnArgs.USERNAME, ctx.availid,
+		ctx.exec(List.of("svn", "add", sourceRc.toString()));
+		ctx.exec(List.of("svn", "add", binariesRc.toString()));
+		var commit = ctx.exec(List.of("svn", "commit", dist.toString(), "-m", rc, SvnArgs.USERNAME, ctx.availid,
 				SvnArgs.PASSWORD_FROM_STDIN), pw, Map.of());
 		return commit.ok() ? StepResult.ok("Artifacts staged + committed to dist/dev.")
 				: StepResult.fail("svn commit to dist/dev failed.");
@@ -106,19 +106,19 @@ public class BinaryArtifactsStageStep implements ReleaseStep {
 	/**
 	 * Renames the downloaded artifact (+ its {@code .asc}) to the ASF convention, regenerates its SHA-512
 	 * checksum via {@code gpg --print-md SHA512}, and clears stray {@code .sha1}/{@code .md5} mirrors that
-	 * Nexus may have served alongside it. All mutating calls route through {@code dryRunOr} (Tier B); the
-	 * checksum file itself is only written in LIVE, since {@code ProcessRunner} has no shell to honor the
-	 * script's {@code > file.sha512} redirection.
-	 */
-	private void renameAndChecksum(StepContext ctx, Path dir, String downloaded, String renamed) {
-		ctx.dryRunOr(List.of("mv", dir.resolve(downloaded).toString(), dir.resolve(renamed).toString()));
-		ctx.dryRunOr(
-				List.of("mv", dir.resolve(downloaded + ".asc").toString(), dir.resolve(renamed + ".asc").toString()));
-		var sha = ctx.dryRunOr(List.of("gpg", "--print-md", "SHA512", dir.resolve(renamed).toString()));
-		if (ctx.live() && sha.ok())
-			writeShaFile(dir.resolve(renamed + ".sha512"), sha.output());
-		ctx.dryRunOr(List.of("rm", dir + "/*.sha1"));
-		ctx.dryRunOr(List.of("rm", dir + "/*.md5"));
+		 * Nexus may have served alongside it. Mutating calls go through {@code exec}; the
+		 * checksum file itself is written to disk because {@code ProcessRunner} has no shell to honor the
+		 * script's {@code > file.sha512} redirection.
+		 */
+		private void renameAndChecksum(StepContext ctx, Path dir, String downloaded, String renamed) {
+			ctx.exec(List.of("mv", dir.resolve(downloaded).toString(), dir.resolve(renamed).toString()));
+			ctx.exec(
+					List.of("mv", dir.resolve(downloaded + ".asc").toString(), dir.resolve(renamed + ".asc").toString()));
+			var sha = ctx.exec(List.of("gpg", "--print-md", "SHA512", dir.resolve(renamed).toString()));
+			if (sha.ok())
+				writeShaFile(dir.resolve(renamed + ".sha512"), sha.output());
+		ctx.exec(List.of("rm", dir + "/*.sha1"));
+		ctx.exec(List.of("rm", dir + "/*.md5"));
 	}
 
 	private static void writeShaFile(Path path, String content) {
diff --git a/src/main/java/org/apache/juneau/releng/engine/steps/ComposeVoteEmailStep.java b/src/main/java/org/apache/juneau/releng/engine/steps/ComposeVoteEmailStep.java
index 44a80ae180..5164f3b770 100644
--- a/src/main/java/org/apache/juneau/releng/engine/steps/ComposeVoteEmailStep.java
+++ b/src/main/java/org/apache/juneau/releng/engine/steps/ComposeVoteEmailStep.java
@@ -48,7 +48,7 @@ public class ComposeVoteEmailStep implements ReleaseStep {
 				readSha512(ctx, "source", "src"), "binSha512", readSha512(ctx, "binaries", "bin"));
 	}
 
-	/** Reads the {@code .sha512} file §5.12 wrote for this RC; blank (SAFE soft-note) if absent. */
+	/** Reads the {@code .sha512} file §5.12 wrote for this RC; blank if the checksum file is absent. */
 	private String readSha512(StepContext ctx, String subdir, String kind) {
 		var rc = "juneau-" + ctx.run.version + "-RC" + ctx.run.rc;
 		var name = "apache-juneau-" + ctx.run.version + "-" + kind + ".zip.sha512";
@@ -71,10 +71,10 @@ public class ComposeVoteEmailStep implements ReleaseStep {
 	@Override
 	public StepResult apply(StepContext ctx) {
 		var data = gather(ctx);
-		// OQ-E: under SAFE the RC tag + dist checksums were never really produced, so fill-ins may be blank.
+		// RC tag + dist checksums may be absent if earlier steps were skipped; fill-ins may be blank.
 		// Note it and continue rather than hard-failing — the draft is still useful for a rehearsal.
 		if ("(unknown)".equals(data.get("commitHash")) || data.get("srcSha512").isBlank())
-			ctx.log.accept("Note: RC tag/checksums absent (expected under SAFE) — composing draft with placeholders.");
+			ctx.log.accept("Note: RC tag/checksums absent — composing draft with placeholders.");
 		var path = ctx.email.compose(EmailTemplate.VOTE, ctx.run, data);
 		return StepResult.ok("Opened draft: " + path);
 	}
diff --git a/src/main/java/org/apache/juneau/releng/engine/steps/DeploySnapshotStep.java b/src/main/java/org/apache/juneau/releng/engine/steps/DeploySnapshotStep.java
index 3bf7524262..3f06c6975f 100644
--- a/src/main/java/org/apache/juneau/releng/engine/steps/DeploySnapshotStep.java
+++ b/src/main/java/org/apache/juneau/releng/engine/steps/DeploySnapshotStep.java
@@ -49,11 +49,11 @@ public class DeploySnapshotStep implements ReleaseStep {
 	@Override
 	public StepResult apply(StepContext ctx) {
 		// Prime gpg-agent once: throwaway loopback sign, passphrase via stdin (mirrors GpgValidator).
-		ctx.dryRunOr(
+		ctx.exec(
 				List.of("gpg", "--batch", "--yes", "--pinentry-mode", "loopback", "--passphrase-fd", "0",
 						"--local-user", ctx.gpgKeyId, "--sign", "--output", "/dev/null", "-"),
 				ctx.gpgPassphrase + "\n", null);
-		var res = ctx.dryRunOr(List.of("mvn", "-f", ctx.stagingRepo.toString() + "/pom.xml", "deploy"));
+		var res = ctx.exec(List.of("mvn", "-f", ctx.stagingRepo.toString() + "/pom.xml", "deploy"));
 		return res.ok() ? StepResult.ok("SNAPSHOT deployed.") : StepResult.fail("mvn deploy failed.");
 	}
 }
diff --git a/src/main/java/org/apache/juneau/releng/engine/steps/DevDistVerifyStep.java b/src/main/java/org/apache/juneau/releng/engine/steps/DevDistVerifyStep.java
index 53322c079d..0b813da19d 100644
--- a/src/main/java/org/apache/juneau/releng/engine/steps/DevDistVerifyStep.java
+++ b/src/main/java/org/apache/juneau/releng/engine/steps/DevDistVerifyStep.java
@@ -57,22 +57,11 @@ public class DevDistVerifyStep implements ReleaseStep {
 	@Override
 	public StepResult apply(StepContext ctx) {
 		var missing = missingOrEmptyFiles(ctx);
-		if (!missing.isEmpty()) {
-			// SAFE fidelity note (§8): binary-artifacts-stage's staging was command-logged in SAFE, so these
-			// files were never really produced — soft-note rather than fail the rehearsal (OQ-E). In LIVE the
-			// files are expected to exist for real, so their absence is a genuine failure.
-			if (!ctx.live()) {
-				ctx.log.accept("Note (SAFE: artifacts not staged) — missing/empty: " + missing);
-			} else {
-				return StepResult.fail("Missing or empty dist/dev file(s): " + missing);
-			}
-		}
+		if (!missing.isEmpty())
+			return StepResult.fail("Missing or empty dist/dev file(s): " + missing);
 
 		var url = ctx.target.distDevBase() + "/";
-		if (ctx.live())
-			ctx.exec(List.of("open", url));
-		else
-			ctx.log.accept("SAFE: staging was command-logged, so no browser is opened for " + url);
+		ctx.exec(List.of("open", url));
 		return StepResult.ok("Dist files present — spot-check the opened URL, then confirm.");
 	}
 
@@ -80,7 +69,7 @@ public class DevDistVerifyStep implements ReleaseStep {
 	private List<String> missingOrEmptyFiles(StepContext ctx) {
 		var rc = "juneau-" + ctx.run.version + "-RC" + ctx.run.rc;
 		var dist = ctx.stateDir.resolve("dist");
-		ctx.dryRunOr(List.of("svn", "checkout", SvnArgs.USERNAME, ctx.availid, SvnArgs.PASSWORD_FROM_STDIN,
+		ctx.exec(List.of("svn", "checkout", SvnArgs.USERNAME, ctx.availid, SvnArgs.PASSWORD_FROM_STDIN,
 				ctx.target.distDevBase(), dist.toString()), ctx.ldapPassword + "\n", null);
 
 		var missing = new ArrayList<String>();
diff --git a/src/main/java/org/apache/juneau/releng/engine/steps/DistPromoteStep.java b/src/main/java/org/apache/juneau/releng/engine/steps/DistPromoteStep.java
index ee11e77695..618bddbbff 100644
--- a/src/main/java/org/apache/juneau/releng/engine/steps/DistPromoteStep.java
+++ b/src/main/java/org/apache/juneau/releng/engine/steps/DistPromoteStep.java
@@ -67,33 +67,33 @@ public class DistPromoteStep implements ReleaseStep {
 		var release = ctx.stateDir.resolve("dist-release");
 		var pw = ctx.ldapPassword + "\n";
 
-		var coDev = ctx.dryRunOr(List.of("svn", "checkout", SvnArgs.USERNAME, ctx.availid,
+		var coDev = ctx.exec(List.of("svn", "checkout", SvnArgs.USERNAME, ctx.availid,
 				SvnArgs.PASSWORD_FROM_STDIN, ctx.target.distDevBase(), dev.toString()), pw, null);
 		if (!coDev.ok())
 			return StepResult.fail("svn checkout of dist/dev failed.");
-		var coRelease = ctx.dryRunOr(List.of("svn", "checkout", SvnArgs.USERNAME, ctx.availid,
+		var coRelease = ctx.exec(List.of("svn", "checkout", SvnArgs.USERNAME, ctx.availid,
 				SvnArgs.PASSWORD_FROM_STDIN, ctx.target.distReleaseBase(), release.toString()), pw, null);
 		if (!coRelease.ok())
 			return StepResult.fail("svn checkout of dist/release failed.");
 
 		var destDir = release.resolve(version);
-		ctx.dryRunOr(List.of("mkdir", destDir.toString()));
+		ctx.exec(List.of("mkdir", destDir.toString()));
 		for (var ext : EXTENSIONS) {
 			var name = "apache-juneau-" + version + "-src.zip" + ext;
-			ctx.dryRunOr(List.of("svn", "mv", dev.resolve("source").resolve(rc).resolve(name).toString(),
+			ctx.exec(List.of("svn", "mv", dev.resolve("source").resolve(rc).resolve(name).toString(),
 					destDir.resolve(name).toString()));
 		}
 		for (var ext : EXTENSIONS) {
 			var name = "apache-juneau-" + version + "-bin.zip" + ext;
-			ctx.dryRunOr(List.of("svn", "mv", dev.resolve("binaries").resolve(rc).resolve(name).toString(),
+			ctx.exec(List.of("svn", "mv", dev.resolve("binaries").resolve(rc).resolve(name).toString(),
 					destDir.resolve(name).toString()));
 		}
 
 		var prior = priorReleaseOnLine(ctx);
 		if (prior != null)
-			ctx.dryRunOr(List.of("svn", "rm", release.resolve(prior).toString()));
+			ctx.exec(List.of("svn", "rm", release.resolve(prior).toString()));
 
-		var commit = ctx.dryRunOr(List.of("svn", "commit", dev.toString(), release.toString(), "-m",
+		var commit = ctx.exec(List.of("svn", "commit", dev.toString(), release.toString(), "-m",
 				"Apache Juneau " + version, SvnArgs.USERNAME, ctx.availid, SvnArgs.PASSWORD_FROM_STDIN), pw,
 				Map.of());
 		return commit.ok() ? StepResult.ok("Promoted to dist/release.") : StepResult.fail("svn promote failed.");
diff --git a/src/main/java/org/apache/juneau/releng/engine/steps/GithubReleaseCreateStep.java b/src/main/java/org/apache/juneau/releng/engine/steps/GithubReleaseCreateStep.java
index 021e6c2c6a..a4333e2475 100644
--- a/src/main/java/org/apache/juneau/releng/engine/steps/GithubReleaseCreateStep.java
+++ b/src/main/java/org/apache/juneau/releng/engine/steps/GithubReleaseCreateStep.java
@@ -52,18 +52,15 @@ public class GithubReleaseCreateStep implements ReleaseStep {
 	public StepResult apply(StepContext ctx) {
 		var tag = "juneau-" + ctx.run.version + "-RC" + ctx.run.rc;
 		var env = Map.of("GH_TOKEN", ctx.githubToken);
-		// Idempotency: treat "already exists" as success. Only probed LIVE; in SAFE the create is
-		// command-logged unconditionally (no gh call is made to check for a real, pre-existing release).
-		if (ctx.live()) {
-			var exists = ctx.runner.run(List.of("gh", "release", "view", tag, "--repo", ctx.target.ghSlug()), null,
-					env);
-			if (exists.ok()) {
-				ctx.log.accept("GitHub Release already exists for " + tag);
-				return StepResult.ok("Already exists.");
-			}
+		// Idempotency: treat "already exists" as success.
+		var exists = ctx.runner.run(List.of("gh", "release", "view", tag, "--repo", ctx.target.ghSlug()), null,
+				env);
+		if (exists.ok()) {
+			ctx.log.accept("GitHub Release already exists for " + tag);
+			return StepResult.ok("Already exists.");
 		}
 		var notes = ctx.formInputs.getOrDefault("releaseNotes", "See release notes.");
-		var res = ctx.dryRunOr(List.of("gh", "release", "create", tag, "--repo", ctx.target.ghSlug(), "--title",
+		var res = ctx.exec(List.of("gh", "release", "create", tag, "--repo", ctx.target.ghSlug(), "--title",
 				ctx.run.version, "--notes", notes), null, env);
 		return res.ok() ? StepResult.ok("GitHub Release created.") : StepResult.fail("gh release create failed.");
 	}
diff --git a/src/main/java/org/apache/juneau/releng/engine/steps/MilestoneCloseStep.java b/src/main/java/org/apache/juneau/releng/engine/steps/MilestoneCloseStep.java
index 5c9bba4b20..897b0e719b 100644
--- a/src/main/java/org/apache/juneau/releng/engine/steps/MilestoneCloseStep.java
+++ b/src/main/java/org/apache/juneau/releng/engine/steps/MilestoneCloseStep.java
@@ -51,7 +51,7 @@ public class MilestoneCloseStep implements ReleaseStep {
 		if (ctx.run.milestoneNumber == null)
 			return StepResult.ok("No milestone recorded; skipping.");
 		var env = Map.of("GH_TOKEN", ctx.githubToken);
-		var res = ctx.dryRunOr(
+		var res = ctx.exec(
 				List.of("gh", "api", "repos/" + ctx.target.repoSlug() + "/milestones/" + ctx.run.milestoneNumber, "-X",
 						"PATCH", "-f", "state=closed"),
 				null, env);
diff --git a/src/main/java/org/apache/juneau/releng/engine/steps/ReleasePerformStep.java b/src/main/java/org/apache/juneau/releng/engine/steps/ReleasePerformStep.java
index 8c64946cac..56eff39adf 100644
--- a/src/main/java/org/apache/juneau/releng/engine/steps/ReleasePerformStep.java
+++ b/src/main/java/org/apache/juneau/releng/engine/steps/ReleasePerformStep.java
@@ -48,7 +48,7 @@ public class ReleasePerformStep implements ReleaseStep {
 
 	@Override
 	public StepResult apply(StepContext ctx) {
-		var res = ctx.dryRunOr(List.of("mvn", "-f", ctx.stagingRepo.toString() + "/pom.xml", "release:perform"));
+		var res = ctx.exec(List.of("mvn", "-f", ctx.stagingRepo.toString() + "/pom.xml", "release:perform"));
 		return res.ok() ? StepResult.ok("release:perform complete; staging repo populated.")
 				: StepResult.fail("mvn release:perform failed.");
 	}
diff --git a/src/main/java/org/apache/juneau/releng/engine/steps/ReleasePrepareStep.java b/src/main/java/org/apache/juneau/releng/engine/steps/ReleasePrepareStep.java
index 459e9f6eb4..6996c594db 100644
--- a/src/main/java/org/apache/juneau/releng/engine/steps/ReleasePrepareStep.java
+++ b/src/main/java/org/apache/juneau/releng/engine/steps/ReleasePrepareStep.java
@@ -71,7 +71,7 @@ public class ReleasePrepareStep implements ReleaseStep {
 		if (next == null)
 			return StepResult.fail("z==0 release requires an explicit developmentVersion.");
 		ctx.run.developmentVersion = next;
-		var res = ctx.dryRunOr(List.of("mvn", "-f", ctx.stagingRepo.toString() + "/pom.xml", "release:prepare",
+		var res = ctx.exec(List.of("mvn", "-f", ctx.stagingRepo.toString() + "/pom.xml", "release:prepare",
 				"-DautoVersionSubmodules=true", "-DreleaseVersion=" + ctx.run.version,
 				"-Dtag=juneau-" + ctx.run.version + "-RC" + ctx.run.rc, "-DdevelopmentVersion=" + next));
 		return res.ok() ? StepResult.ok("release:prepare complete; tag pushed.")
diff --git a/src/main/java/org/apache/juneau/releng/nexus/NexusMockModel.java b/src/main/java/org/apache/juneau/releng/nexus/NexusMockModel.java
index 4c031aac52..0e898c0387 100644
--- a/src/main/java/org/apache/juneau/releng/nexus/NexusMockModel.java
+++ b/src/main/java/org/apache/juneau/releng/nexus/NexusMockModel.java
@@ -29,8 +29,7 @@ import org.apache.juneau.marshall.marshaller.Json;
  * and {@code bulk/drop} transitions ({@code OPEN → CLOSED → RELEASED}, with {@code DROPPED} reachable from OPEN
  * or CLOSED).
  *
- * <p>Because a SAFE {@code release-perform} is command-logged rather than executed, no real staging repo is
- * ever created; this model therefore <b>lazily synthesizes</b> the OPEN {@code orgapachejuneau-NNNN} repo that
+ * <p>This model <b>lazily synthesizes</b> the OPEN {@code orgapachejuneau-NNNN} repo that
  * {@code release-perform} would have created, on the first discovery call. After a repo reaches a terminal
  * state a fresh OPEN repo is synthesized on the next discovery so a later run starts clean.
  *
diff --git a/src/main/java/org/apache/juneau/releng/nexus/NexusMockRest.java b/src/main/java/org/apache/juneau/releng/nexus/NexusMockRest.java
index 3d9258e267..e473cf0277 100644
--- a/src/main/java/org/apache/juneau/releng/nexus/NexusMockRest.java
+++ b/src/main/java/org/apache/juneau/releng/nexus/NexusMockRest.java
@@ -35,10 +35,10 @@ import org.apache.juneau.rest.server.RestResponse;
 import org.apache.juneau.rest.server.servlet.BasicRestServlet;
 
 /**
- * An in-app loopback mock of the Apache Nexus 2 staging suite, mounted at {@code /mock/nexus/*}. It
- * lets the real {@link NexusStagingClient} run its full discovery/close/promote/drop HTTP flow against a
- * stateful in-memory model ({@link NexusMockModel}) with zero canonical side effects. Always registered so
- * a Dry-run on a LIVE box can still hit it; LIVE runs use the real Nexus base URL and never call this servlet.
+ * An in-process mock of the Apache Nexus 2 staging suite used by tests. It lets the real
+ * {@link NexusStagingClient} run its full discovery/close/promote/drop HTTP flow against a
+ * stateful in-memory model ({@link NexusMockModel}) with zero canonical side effects. Not
+ * registered in the running app.
  *
  * <p>All request routing is centralized in {@link #route} so the same mapping backs both this servlet and the
  * in-process transport tests exercise.
diff --git a/src/main/java/org/apache/juneau/releng/nexus/NexusStagingClient.java b/src/main/java/org/apache/juneau/releng/nexus/NexusStagingClient.java
index fe835af057..274da39582 100644
--- a/src/main/java/org/apache/juneau/releng/nexus/NexusStagingClient.java
+++ b/src/main/java/org/apache/juneau/releng/nexus/NexusStagingClient.java
@@ -61,7 +61,7 @@ public class NexusStagingClient {
 	 * normally the committer's Apache LDAP availid + password, resolved through the
 	 * {@link org.apache.juneau.commons.secret.SecretStore} SPI (see
 	 * {@code AppConfiguration.secretResolver()}'s {@code nexus()}), not hardcoded to any particular backend
-	 * here. Under SAFE mode {@code baseUrl} is the in-app loopback mock and the credential is a placeholder.
+	 * here. Tests may point {@code baseUrl} at an in-memory mock; production uses the real Nexus host.
 	 */
 	public static NexusStagingClient create(String baseUrl, String profileId, String username, String password) {
 		return create(baseUrl, profileId, username, password, Map.of());
@@ -71,7 +71,7 @@ public class NexusStagingClient {
 	 * As {@link #create(String, String, String, String)}, plus {@code extraHeaders} added to every request.
 	 *
 	 * <p>
-	 * Exists for the SAFE-mode loopback mock. That mock is mounted on this application's own port, behind the
+	 * Exists so a test double can sit on a loopback port, behind the
 	 * {@link org.apache.juneau.rest.server.filter.LoopbackBoundary LoopbackBoundary}, which grants no exemption
 	 * to a caller merely because it happens to be this process — so the close/drop/promote {@code POST}s below
 	 * must present the same {@code Origin} and CSRF token the browser does. Pass
diff --git a/src/main/java/org/apache/juneau/releng/rest/ReleaseRunRest.java b/src/main/java/org/apache/juneau/releng/rest/ReleaseRunRest.java
index 3fef0d9e08..369505315b 100644
--- a/src/main/java/org/apache/juneau/releng/rest/ReleaseRunRest.java
+++ b/src/main/java/org/apache/juneau/releng/rest/ReleaseRunRest.java
@@ -37,7 +37,6 @@ import org.apache.juneau.rest.server.view.freemarker.FreemarkerMixin;
 import org.apache.juneau.rest.server.view.freemarker.FreemarkerViewRenderer;
 import org.apache.juneau.rest.server.view.freemarker.console.ConsoleFreemarkerMixin;
 import org.apache.juneau.releng.engine.DropRcService;
-import org.apache.juneau.releng.engine.ExecutionMode;
 import org.apache.juneau.releng.engine.Preview;
 import org.apache.juneau.releng.engine.ReleaseEngine;
 import org.apache.juneau.releng.engine.RunState;
@@ -75,18 +74,34 @@ public class ReleaseRunRest extends BasicRestResource {
 	/** Human page — the pipeline control panel for the active run (or an empty start form). */
 	@RestGet("/")
 	public View page(HttpServletRequest req) {
-		var liveCapable = engine.mode() == ExecutionMode.LIVE;
 		var active = engine.displayRun().orElse(null);
-		var runMode = active == null ? ExecutionMode.SAFE : engine.effectiveMode(active);
+		var subtab = resolveSubtab(req.getParameter("tab"), active != null);
 		var view = ConsolePage.of("new-release", req).attr("stepMeta", stepMetaJson(engine.registry().steps()))
-				.attr("mode", runMode.name()).attr("appMode", engine.mode().name())
-				.attr("liveCapable", Boolean.valueOf(liveCapable));
+				.attr("subtab", subtab).attr("navTab", "new/" + subtab);
 		// FreemarkerView.attr() rejects null values by design; the template only checks run??
 		// (attribute presence), so omit the attribute entirely when there's no displayable run.
 		return active == null ? view
 				: view.attr("run", active).attr("armed", Boolean.valueOf(engine.isArmed(active.version)));
 	}
 
+	/**
+	 * Resolves the New Release Page Subtab. An explicit {@code ?tab=input} / {@code ?tab=exec} wins;
+	 * otherwise Input is the default with no active run and Execution is the default once a run exists
+	 * (matching the previous in-page pill selection).
+	 */
+	@SuppressWarnings({ "java:S1192" // Page-subtab query value; a constant would obscure the wire form.
+	})
+	static String resolveSubtab(String requested, boolean hasRun) {
+		if (requested != null) {
+			var t = requested.strip().toLowerCase();
+			if ("input".equals(t))
+				return "input";
+			if ("exec".equals(t) || "execution".equals(t))
+				return "exec";
+		}
+		return hasRun ? "exec" : "input";
+	}
+
 	/**
 	 * Serializes the step registry's {@code {id: {title, mutating}}} map for the {@code nr-step-meta} sidecar.
 	 *
@@ -115,18 +130,15 @@ public class ReleaseRunRest extends BasicRestResource {
 	}
 
 	/**
-	 * Start a new run. Body: {version, developmentVersion?, milestoneNumber?, mode?}. {@code milestoneNumber}
+	 * Start a new run. Body: {version, developmentVersion?, milestoneNumber?}. {@code milestoneNumber}
 	 * is the New-Release form field — pre-filled client-side by title-match resolution (§8.1),
-	 * user-overridable. {@code mode} is Dry-run ({@code SAFE}, default) or Actual ({@code LIVE}); LIVE is
-	 * capped to SAFE unless the box was started with {@code rm.mode=live}. Rejects a second concurrent run
-	 * with 409.
+	 * user-overridable. Rejects a second concurrent run with 409.
 	 */
 	@Mutating("creates a run and writes its state to disk")
 	@RestPost("/")
 	public RunState start(@Content StartRequest body) {
 		try {
-			var rs = engine.start(body.version, body.developmentVersion, body.milestoneNumber,
-					ExecutionMode.fromConfig(body.mode));
+			var rs = engine.start(body.version, body.developmentVersion, body.milestoneNumber);
 			return engine.updateDetails(rs.version, body.releaseSummary, body.highlights, body.knownIssues,
 					body.acknowledgements);
 		} catch (IllegalStateException e) {
@@ -135,16 +147,21 @@ public class ReleaseRunRest extends BasicRestResource {
 	}
 
 	/**
-	 * Update the active run's optional narrative fields ({@code releaseSummary}, {@code highlights},
-	 * {@code knownIssues}, {@code acknowledgements}) so they can be edited before each email is composed.
-	 * Returns the updated run.
+	 * Update the active run's Input-form fields (version, development version, milestone, and the four
+	 * narrative fields) so they can be edited after start. Returns the updated run. A version change
+	 * renames the persisted run file; a colliding version is a 409.
 	 */
-	@Mutating("updates the run's persisted narrative fields")
+	@Mutating("updates the run's persisted identity and narrative fields")
 	@RestPost("/{version}/details")
 	public RunState details(@Path("version") String version, @Content DetailsRequest body) {
 		requireRun(version);
 		var b = body == null ? new DetailsRequest() : body;
-		return engine.updateDetails(version, b.releaseSummary, b.highlights, b.knownIssues, b.acknowledgements);
+		try {
+			return engine.updateDetails(version, b.releaseSummary, b.highlights, b.knownIssues, b.acknowledgements,
+					b.version, b.developmentVersion, b.milestoneNumber);
+		} catch (IllegalStateException e) {
+			throw new Conflict(e.getMessage());
+		}
 	}
 
 	// No @Mutating: a preview is a dry run by construction and writes nothing. The annotation is a claim about
@@ -157,7 +174,7 @@ public class ReleaseRunRest extends BasicRestResource {
 		return engine.preview(version, stepId, form == null ? Map.of() : form);
 	}
 
-	@Mutating("executes a release step; in LIVE mode this mutates git, SVN, Nexus, GitHub or mailing lists")
+	@Mutating("executes a release step; this mutates git, SVN, Nexus, GitHub or mailing lists")
 	@RestPost("/{version}/steps/{stepId}/apply")
 	public StepResult apply(@Path("version") String version, @Path("stepId") String stepId,
 			@Content Map<String, String> form) {
@@ -193,16 +210,15 @@ public class ReleaseRunRest extends BasicRestResource {
 	}
 
 	/**
-	 * Arm this run for LIVE mutation. Requires a typed confirm phrase ({@code "<version> LIVE"}) and is
-	 * rejected unless the box is LIVE and this run is Actual (LIVE). Arming is in-memory on the engine and
-	 * drops on any restart.
+	 * Arm this run for mutation. Requires a typed confirm phrase ({@code "<version> LIVE"}). Arming is
+	 * in-memory on the engine and drops on any restart.
 	 *
 	 * <p>This is an <b>intent</b> gate: the confirm phrase shows a human deliberately typed something, and is not
 	 * a secret and not authentication — it is derivable from the page it is typed on. Whether the request came from
 	 * a page this application served is a separate question, answered by the loopback boundary in front of every
 	 * endpoint. See {@link org.apache.juneau.releng.engine.ReleaseEngine#arm(String, String)}.
 	 */
-	@Mutating("arms the run for irreversible LIVE mutation")
+	@Mutating("arms the run for irreversible mutation")
 	@RestPost("/{version}/arm")
 	public StepResult arm(@Path("version") String version, @Content ArmRequest body) {
 		requireRun(version);
@@ -243,7 +259,7 @@ public class ReleaseRunRest extends BasicRestResource {
 		try {
 			dropRc.apply(version, body.reason, secrets::availid, secrets::ldapPassword);
 		} catch (IllegalStateException e) {
-			return StepResult.fail(e.getMessage()); // the LIVE-unarmed guard refusal
+			return StepResult.fail(e.getMessage()); // the unarmed guard refusal
 		}
 		return StepResult.ok("RC dropped; bumped to the next RC.");
 	}
@@ -267,10 +283,12 @@ public class ReleaseRunRest extends BasicRestResource {
 		public String highlights;
 		public String knownIssues;
 		public String acknowledgements;
-		public String mode; // "SAFE" (default) or "LIVE"; capped by the box-wide rm.mode
 	}
 
 	public static class DetailsRequest {
+		public String version;
+		public String developmentVersion;
+		public Integer milestoneNumber;
 		public String releaseSummary;
 		public String highlights;
 		public String knownIssues;
diff --git a/src/main/java/org/apache/juneau/releng/util/ProcessRunner.java b/src/main/java/org/apache/juneau/releng/util/ProcessRunner.java
index 1d6932c009..bcec638e02 100644
--- a/src/main/java/org/apache/juneau/releng/util/ProcessRunner.java
+++ b/src/main/java/org/apache/juneau/releng/util/ProcessRunner.java
@@ -67,6 +67,11 @@ public interface ProcessRunner {
 	 * Timeout-capable variant of {@link #run(List, String, Map)}. {@code timeout} {@code null} or non-positive
 	 * means unbounded. Default ignores the timeout and delegates to {@link #run(List, String, Map)};
 	 * {@link Default} honors it and destroy-forcibly on expiry.
+	 *
+	 * @param command the command and arguments
+	 * @param stdin optional stdin bytes, or {@code null}
+	 * @param env extra environment variables, or {@code null}
+	 * @param timeout {@code null} or non-positive means unbounded. Ignored by this default; {@link Default} honors it.
 	 */
 	default ProcResult run(List<String> command, String stdin, Map<String, String> env, Duration timeout) {
 		return run(command, stdin, env);
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index b7bc9003a1..dd5c337217 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -22,20 +22,3 @@ rm.state.dir=${user.home}/.juneau-release-manager/state
 rm.staging.dir=${user.home}/.juneau-release-manager/staging
 rm.git.committer.email=jamesbognar@apache.org
 rm.slack.webhook=
-# Execution mode: safe (default) simulates all mutations + routes Nexus to the in-app loopback mock;
-# live executes real mutations (and still requires per-run arming at the guard chokepoint).
-#
-# safe being the default is an OPERATIONAL SAFETY default, not a security control. It exists so that starting the
-# app, exploring it, or running it in a test cannot mutate anything public by accident.
-#
-# It is worth recording that until the loopback write boundary landed (see AppConfiguration), this default was in
-# fact doing real security work, and was the only thing standing between a hostile page in the operator's browser
-# and a published release: that page could derive the "<version> LIVE" confirm phrase from the app's own page, POST
-# it to /arm cross-origin, and trigger a mutating step. Nothing rejected the request; the box simply had to have
-# been started in live mode.
-#
-# That is exactly why it was never adequate as a security control. Its protection evaporates the moment someone
-# starts the app with rm.mode=live -- which is to say, during an actual release, when the consequences of a forged
-# request are at their worst. A control that is present only when it does not matter is not a control. The
-# boundary is what closes that gap, and it applies in both modes.
-rm.mode=safe
diff --git a/src/main/resources/static/css/chrome.css b/src/main/resources/static/css/chrome.css
index 942c91d831..8946c4dda4 100644
--- a/src/main/resources/static/css/chrome.css
+++ b/src/main/resources/static/css/chrome.css
@@ -228,6 +228,13 @@ tr.detail-row dd {
 .links-cell a { margin-right: 10px; font-size: 0.8125rem; }
 .links-cell a:last-child { margin-right: 0; }
 
+/* Expanded-row (row-detail) header.  Juneau's console chrome paints an accent WASH behind this header
+   (background-color:var(--jc-accent-wash)); under this app's light-red theme that wash is a pink band behind
+   the version title.  A record-detail title reads better as an icon + title on the plain panel (no coloured
+   band), so this app-local override drops the wash back to transparent.  Scoped to this app's own chrome.css
+   (loaded after the shared console chrome), so it does not change the wash for other console apps. */
+.juneau-view-detail-header { background-color: transparent; }
+
 /* Toolbar Search, ribbon idle borders, and header sort+search are Juneau stock
    (WORK-J0546 / WORK-J0547). Do not restyle the DataTables search field or order
    control here. table.rm-table rules above still style any app-local table that
diff --git a/src/main/resources/static/css/new-release.css b/src/main/resources/static/css/new-release.css
index d52a07e80a..27709c02ed 100644
--- a/src/main/resources/static/css/new-release.css
+++ b/src/main/resources/static/css/new-release.css
@@ -59,8 +59,9 @@
 .rm-run-header {
   margin: 0 0 16px;
   background: var(--jc-card-bg);
-  border: 1px solid var(--jc-border);
+  border: none;
   border-radius: var(--jc-radius);
+  box-shadow: var(--jc-card-shadow);
   padding: 16px 20px;
 }
 
@@ -147,7 +148,8 @@
   line-height: 1.65;
   padding: 12px 14px;
   border-radius: var(--jc-radius);
-  max-height: 240px;
+  flex: 1 1 0%;
+  min-height: 0;
   overflow-y: auto;
   white-space: pre-wrap;
   overflow-wrap: break-word;
@@ -251,8 +253,6 @@
   margin-top: 8px;
 }
 
-/* LIVE-mode vote-gate form (§5.16 tally-vote-result) — SAFE gets a single "Simulate (SAFE)" button
-   (see .rm-step-note above) instead of this form, since there's no real 72h wait or tally to record. */
 .rm-vote-form {
   display: flex;
   flex-direction: column;
@@ -286,19 +286,20 @@
    ========================================================================== */
 
 .rm-rail-layout {
-  display: flex;
-  align-items: flex-start;
+  display: grid;
+  grid-template-columns: 340px minmax(0, 1fr);
+  align-items: stretch;
   gap: 16px;
 }
 
 .rm-rail-col {
-  flex: 0 0 340px;
   background: var(--jc-white);
-  border: 1px solid var(--jc-border);
+  border: none;
   border-radius: var(--jc-radius);
+  box-shadow: var(--jc-card-shadow);
   overflow: hidden;
-  max-height: 640px;
   overflow-y: auto;
+  min-height: 0;
 }
 
 .rm-rail-item {
@@ -340,12 +341,23 @@
 .rm-rail-item .tag { flex: 0 0 auto; }
 
 .rm-detail-col {
-  flex: 1 1 auto;
   min-width: 0;
+  min-height: 0;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
   background: var(--jc-white);
-  border: 1px solid var(--jc-border);
+  border: none;
   border-radius: var(--jc-radius);
+  box-shadow: var(--jc-card-shadow);
   padding: 18px 20px;
+  overflow: hidden;
+}
+
+.rm-detail-col > .rm-step-title-row,
+.rm-detail-col > .rm-actions-row,
+.rm-detail-col > .rm-console-label {
+  flex: 0 0 auto;
 }
 
 /* ==========================================================================
@@ -502,79 +514,6 @@ pre.rm-console {
 
 .tag.status.awaiting-review::before { content: '\23F8'; margin-right: 3px; font-size: 9px; }
 
-/* ==========================================================================
-   Slice 3: run-level execution-mode banner + arming affordance.
-   ========================================================================== */
-
-.rm-mode-banner {
-  margin: 0 0 16px;
-  padding: 10px 14px;
-  border: 1px solid var(--jc-tag-neutral-border);
-  border-radius: 8px;
-  font-size: 13px;
-  color: var(--jc-text-muted);
-  display: flex;
-  align-items: center;
-  gap: 10px;
-}
-
-.rm-mode-banner.safe { background: var(--jc-tag-green-bg); border-color: var(--jc-tag-green-border); }
-.rm-mode-banner.live { background: #fdeceb; border-color: #f3c6c2; }
-
-.rm-mode-chip {
-  display: inline-flex;
-  align-items: center;
-  font-weight: 800;
-  font-size: 11px;
-  letter-spacing: 0.06em;
-  padding: 2px 8px;
-  border-radius: 999px;
-  border: 1px solid transparent;
-}
-
-.rm-mode-chip.safe { background: var(--jc-tag-green-bg); color: var(--jc-tag-green-text); border-color: var(--jc-tag-green-border); }
-.rm-mode-chip.live { background: #fdeceb; color: var(--jc-danger); border-color: #f3c6c2; }
-
-/* ==========================================================================
-   App-local Input / Execution subtabs (client-side show/hide; see new-release.js)
-   ========================================================================== */
-
-.rm-subtabs {
-  display: flex;
-  gap: 6px;
-  margin: 0 0 16px;
-  padding: 4px;
-  background: var(--jc-white);
-  border: 1px solid var(--jc-border);
-  border-radius: 999px;
-  width: fit-content;
-}
-
-.rm-subtab {
-  font-family: var(--jc-font);
-  font-size: 0.8125rem;
-  font-weight: 600;
-  color: var(--jc-text-soft);
-  background: transparent;
-  border: none;
-  border-radius: 999px;
-  padding: 7px 18px;
-  cursor: pointer;
-}
-
-.rm-subtab:hover { background: var(--jc-accent-wash); }
-
-/* Light selected chip. Page Tabs put the heavy accent on TOP (.jc-nav-tab.active
-   in chrome.css); these Input/Execution chips are not Page Tabs, so they keep a
-   compact inset underline rather than a top bar. */
-.rm-subtab.active {
-  background: var(--jc-accent-wash);
-  color: var(--jc-text);
-  box-shadow: inset 0 -2px 0 var(--jc-accent);
-}
-
-.rm-subtab-panel[hidden] { display: none; }
-
 /* ==========================================================================
    Full-width "Start a new release" / "Release details" form card
    ========================================================================== */
@@ -641,43 +580,6 @@ pre.rm-console {
   color: var(--jc-text-soft);
 }
 
-.rm-mode-picker {
-  margin-top: 16px;
-}
-
-.rm-mode-picker-label {
-  display: block;
-  font-size: 0.75rem;
-  font-weight: 600;
-  letter-spacing: 0.02em;
-  text-transform: uppercase;
-  color: var(--jc-text-muted);
-  margin-bottom: 8px;
-}
-
-.rm-mode-option {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  margin: 6px 0;
-  font-size: 0.875rem;
-  cursor: pointer;
-}
-
-.rm-mode-option.disabled {
-  opacity: 0.55;
-  cursor: not-allowed;
-}
-
-.rm-field.rm-mode-picker input[type="radio"] {
-  width: auto;
-  padding: 0;
-  margin: 0;
-  box-shadow: none;
-  border: none;
-  accent-color: var(--jc-accent);
-}
-
 .rm-form-actions {
   display: flex;
   align-items: center;
@@ -685,7 +587,6 @@ pre.rm-console {
   margin-top: 18px;
 }
 
-/* The run is armed for live mutation (LIVE only). */
 .tag.armed {
   background: #fdeceb;
   color: var(--jc-danger);
diff --git a/src/main/resources/static/js/new-release.js b/src/main/resources/static/js/new-release.js
index 7020120f91..bf6ac2f149 100644
--- a/src/main/resources/static/js/new-release.js
+++ b/src/main/resources/static/js/new-release.js
@@ -18,13 +18,6 @@
 (function () {
   const val = (id) => document.getElementById(id)?.value ?? null;
 
-  // App-local Input/Execution subtab toggle. Only shows/hides panels — the Execution panel (and its two
-  // EventSources, wired further down) is never removed from the DOM, so switching tabs never reconnects SSE.
-  globalThis.nrSubtab = function (name) {
-    document.querySelectorAll('.rm-subtab').forEach((b) => b.classList.toggle('active', b.dataset.subtab === name));
-    document.querySelectorAll('.rm-subtab-panel').forEach((p) => { p.hidden = p.dataset.subtab !== name; });
-  };
-
   // Defined unconditionally so the start form (rendered when `run` is null, before `.rm-rail-layout`
   // exists) always has it available. Captures the release metadata plus the four optional narrative fields.
   globalThis.nrStart = async function () {
@@ -34,29 +27,42 @@
       method: 'POST', headers: { 'Content-Type': 'application/json' },
       body: JSON.stringify({
         version: val('nr-version'), developmentVersion: val('nr-devversion'), milestoneNumber: ms,
-        mode: document.querySelector('input[name="nr-mode"]:checked')?.value || 'SAFE',
         releaseSummary: val('nr-releaseSummary'), highlights: val('nr-highlights'),
         knownIssues: val('nr-knownIssues'), acknowledgements: val('nr-acknowledgements')
       })
     });
-    location.reload();
+    location.href = '/rest/runs?tab=exec';
   };
 
-  // Persist edits to the four narrative fields on the active run (POST /{version}/details) without leaving
-  // the page, so they can be revised before each email is composed. Surfaces a terse inline result.
+  // Persist edits on the active run (POST /{version}/details) without leaving the page.
+  // Identity fields (version, developmentVersion, milestoneNumber) were previously readonly
+  // after start — version is the store key, developmentVersion is also rewritten by
+  // release-prepare (derived for z>0), milestone is pre-filled by GitHub title-match on
+  // version blur. Save still writes all three; the form is the source of truth.
   globalThis.nrSaveDetails = async function () {
-    const ver = document.querySelector('.rm-rail-layout')?.dataset.version;
+    const form = document.querySelector('.rm-form-card');
+    const origVer = form?.dataset.runVersion;
     const msg = document.getElementById('nr-details-msg');
-    if (!ver) return;
+    if (!origVer) return;
+    const msEl = document.getElementById('nr-milestone');
+    const ms = msEl?.value ? Number(msEl.value) : null;
+    const newVer = val('nr-version');
     try {
-      const r = await fetch('/rest/runs/' + encodeURIComponent(ver) + '/details', {
+      const r = await fetch('/rest/runs/' + encodeURIComponent(origVer) + '/details', {
         method: 'POST', headers: { 'Content-Type': 'application/json' },
         body: JSON.stringify({
+          version: newVer,
+          developmentVersion: val('nr-devversion'),
+          milestoneNumber: Number.isFinite(ms) ? ms : null,
           releaseSummary: val('nr-releaseSummary'), highlights: val('nr-highlights'),
           knownIssues: val('nr-knownIssues'), acknowledgements: val('nr-acknowledgements')
         })
       });
       if (msg) { msg.textContent = r.ok ? 'Saved.' : 'Save failed.'; msg.style.color = r.ok ? '' : 'var(--jc-danger)'; }
+      if (r.ok && newVer && newVer.trim() !== origVer) {
+        form.dataset.runVersion = newVer.trim();
+        location.reload();
+      }
     } catch (e) {
       if (msg) { msg.textContent = 'Save failed.'; msg.style.color = 'var(--jc-danger)'; }
     }
@@ -85,13 +91,9 @@
 
   const version = layout.dataset.version;
   const rc = layout.dataset.rc;
-  const mode = layout.dataset.mode || 'SAFE';
   const metaEl = document.getElementById('nr-step-meta');
   const STEP_META = metaEl ? JSON.parse(metaEl.textContent) : {};
 
-  // The apply affordance is mode-derived: SAFE simulates (command-log, no side effects), LIVE mutates.
-  const runLabel = mode === 'LIVE' ? 'Run (LIVE)' : 'Simulate (SAFE)';
-
   // Exactly one EventSource at a time (spec §4/§7: one console visible at a time). Switching the selected
   // step closes the old connection and opens a new one against that step's own /events/{version}/{stepId}.
   let es = null;
@@ -121,18 +123,14 @@
       return dryRun + ' <button class="jc-btn jc-btn-outline jc-btn-sm" onclick="nrApply(\'' + stepId + '\')">Re-run</button>';
     if (status === 'skipped')
       return dryRun + ' <button class="jc-btn jc-btn-outline jc-btn-sm" onclick="nrApply(\'' + stepId + '\')">Run anyway</button>';
-    return dryRun + ' <button class="jc-btn jc-btn-primary jc-btn-sm" onclick="nrApply(\'' + stepId + '\')">' + runLabel + '</button>';
+    return dryRun + ' <button class="jc-btn jc-btn-primary jc-btn-sm" onclick="nrApply(\'' + stepId + '\')">Run</button>';
   }
 
   // §5.15/§5.16: entering vote-gate only opens the vote (sets AWAITING_VOTE) — it is NOT itself the
   // advance action. The gate is only passed by recording a vote result (POST .../vote-result, which the
-  // engine applies as the separate "tally-vote-result" step). SAFE has no real 72h wait or email tally to
-  // read, so it gets a one-click "Simulate (SAFE)" that records a passing result outright; LIVE still
-  // requires the operator to pick a real outcome and type the tally summary read off the vote thread.
+  // engine applies as the separate "tally-vote-result" step). The operator picks a real outcome and types
+  // the tally summary read off the vote thread.
   function renderVoteGateActions() {
-    if (mode !== 'LIVE')
-      return '<button class="jc-btn jc-btn-primary jc-btn-sm" onclick="nrVoteResult(\'passed\')">' + runLabel + '</button>'
-        + '<span class="rm-step-note">Records a simulated passing vote (no real 72h wait or tally) and advances the run.</span>';
     return '<div class="rm-vote-form">'
       + '<label>Vote outcome<select id="nr-vote-outcome"><option value="passed">Passed</option><option value="rejected">Rejected</option></select></label>'
       + '<label>Tally summary<textarea id="nr-vote-tally" rows="3" placeholder="+1/0/-1 counts, binding voters, read off the vote thread"></textarea></label>'
@@ -140,13 +138,9 @@
       + '</div>';
   }
 
-  globalThis.nrVoteResult = async function (safeOutcome) {
-    var outcome = safeOutcome;
-    var tally = 'SAFE-mode simulated passing vote (no real tally read).';
-    if (!outcome) {
-      outcome = document.getElementById('nr-vote-outcome').value;
-      tally = document.getElementById('nr-vote-tally').value;
-    }
+  globalThis.nrVoteResult = async function () {
+    var outcome = document.getElementById('nr-vote-outcome').value;
+    var tally = document.getElementById('nr-vote-tally').value;
     const res = await post('/vote-result', { outcome, tally });
     if (res && res.success === false) { alert(res.message); return; }
     if (outcome === 'rejected') await nrDropRc(); // §8: a rejected vote forks to Drop-RC.
@@ -260,7 +254,7 @@
     if (runStatusEl) { applyStatusClass(runStatusEl, STATUS_CLASS(snap.status)); runStatusEl.textContent = snap.status; }
     const titleEl = document.getElementById('nr-run-title');
     if (titleEl) titleEl.textContent = snap.version + ' \u00b7 RC' + snap.rc;
-    const armArea = document.getElementById('nr-arm-area'); // only rendered server-side in LIVE mode
+    const armArea = document.getElementById('nr-arm-area');
     if (armArea) {
       armArea.innerHTML = snap.armed
         ? '<span class="tag armed" title="This run is armed for live mutation">ARMED</span>'
diff --git a/src/main/resources/static/js/releases-detail.js b/src/main/resources/static/js/releases-detail.js
index 43de396dd5..d9ba633e3d 100644
--- a/src/main/resources/static/js/releases-detail.js
+++ b/src/main/resources/static/js/releases-detail.js
@@ -36,6 +36,22 @@
 			icons.pack('material');
 	}
 
+	// Row-detail header icon.  The header pattern (Juneau buildDetailHeader) shows a titled icon tile when the
+	// detail config carries an `icon` name, resolved through JuneauViews.icons.resolveIcon.  The Material pack
+	// selected above ships only the ribbon/paging glyphs, so this app registers its OWN release-marker glyph as
+	// self-contained inline SVG (not a sprite <use>), which renders under any pack.  ORIGINAL artwork authored
+	// for this app, licensed Apache-2.0 with the rest of the tree — a plain filled bookmark/ribbon, no Material
+	// path and no Salesforce/IRS/SLDS artwork.  Fill is currentColor so it takes the icon tile's contrast colour.
+	var RELEASE_ICON =
+		'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true">'
+		+ '<path fill="currentColor" d="M7 4 H17 V20 L12 16.5 L7 20 Z"/></svg>';
+
+	function registerDetailIcon() {
+		var icons = globalThis.JuneauViews && globalThis.JuneauViews.icons;
+		if (icons && typeof icons.registerIcon === 'function')
+			icons.registerIcon('release', RELEASE_ICON);
+	}
+
 	var LINK_SPECS = [
 		{ data: 'jiraVersionUrl', label: 'Jira Version' },
 		{ data: 'githubTagUrl', label: 'GitHub Tag' },
@@ -100,5 +116,6 @@
 	}
 
 	selectIconPack();
+	registerDetailIcon();
 	register();
 })();
diff --git a/src/main/resources/templates/base.ftlh b/src/main/resources/templates/base.ftlh
index 5b1d81c86c..0106e2452e 100644
--- a/src/main/resources/templates/base.ftlh
+++ b/src/main/resources/templates/base.ftlh
@@ -21,7 +21,7 @@
   main slot, and the footer.  Child templates stay <@page> calls.
 --><@console theme="light-red" icon="/img/oakleaf.svg" favicon="/img/oakleaf.svg" brand="Apache Juneau · Release Manager">
 <@theme name="light-red">
-    <#-- The app's pre-existing danger palette (the same colors .rm-mode-banner.live / .tag.armed / .pill.invalid
+    <#-- The app's pre-existing danger palette (the same colors .tag.armed / .pill.invalid
          already used).  Overrides the stock light-red pack's Bootstrap-maroon red so the FAILED status/stage pill
          resolves through the token system without changing how it looks.  --jc-tag-red-* is kept as an alias-free
          mirror for the .jc-badge[danger] / busy-avatar-status consumers that still read that triad. -->
@@ -45,7 +45,10 @@
 <@navigation>
     <@node id="setup" label="Setup" href="/rest/setup"/>
     <@node id="releases" label="Releases" href="/rest/releases"/>
-    <@node id="new" label="New Release" href="/rest/runs"/>
+    <@node id="new" label="New Release" href="/rest/runs">
+        <@node id="input" label="Input" href="/rest/runs?tab=input"/>
+        <@node id="exec" label="Execution" href="/rest/runs?tab=exec"/>
+    </...@node>
 </...@navigation>
 <@main/>
 <@footer>Apache Juneau Release Manager — loopback tool for cutting Apache Juneau releases.</...@footer>
diff --git a/src/main/resources/templates/new-release.ftlh b/src/main/resources/templates/new-release.ftlh
index 7b8d0abe6d..b9c99a969e 100644
--- a/src/main/resources/templates/new-release.ftlh
+++ b/src/main/resources/templates/new-release.ftlh
@@ -15,83 +15,87 @@
   limitations under the License.
 -->
 
-<@page tab="new" css="/css/new-release.css" init="/js/new-release.js">
+<#assign _navTab = navTab!'new/input'>
+<#assign _subtab = subtab!'input'>
+<#assign hasRun = run??>
+<@page tab=_navTab css="/css/new-release.css" init="/js/new-release.js">
     <div class="jc-page-header">
         <h1>New Release</h1>
         <p class="jc-page-sub">Cut an Apache Juneau release candidate, from input through vote and publish.</p>
     </div>
-    <#assign modeLc = (mode!'SAFE')?lower_case>
-    <div class="rm-mode-banner ${modeLc}" data-mode="${mode!'SAFE'}">
-        <span class="rm-mode-chip ${modeLc}">${mode!'SAFE'}</span>
-        <#if (mode!'SAFE') == 'SAFE'>
-            Simulate mode &mdash; every mutation is command-logged with zero side effects, and Nexus staging is routed to the in-app loopback mock.
-        <#else>
-            Live mode &mdash; mutations hit real endpoints. Each run must be individually armed before it can mutate.
-        </#if>
-    </div>
-
-    <#assign hasRun = run??>
-    <div class="rm-subtabs" role="tablist">
-        <button type="button" class="rm-subtab<#if !hasRun> active</#if>" role="tab" data-subtab="input" onclick="nrSubtab('input')">Input</button>
-        <button type="button" class="rm-subtab<#if hasRun> active</#if>" role="tab" data-subtab="exec" onclick="nrSubtab('exec')">Execution</button>
-    </div>
 
-    <section class="rm-subtab-panel" id="nr-panel-input" data-subtab="input"<#if hasRun> hidden</#if>>
-        <div class="jc-card rm-form-card">
-            <#if !hasRun>
-            <h2>Start a new release</h2>
-            <div class="rm-form-grid">
-                <div class="rm-field">
-                    <label for="nr-version">Version</label>
-                    <input id="nr-version" type="text" placeholder="e.g. 9.2.1">
+    <#if _subtab == "exec">
+    <section class="rm-subtab-panel" id="nr-panel-exec" data-subtab="exec">
+    <#if !hasRun>
+        <div class="jc-card">
+            <p class="hint">No active run yet. Fill in the Input tab and click Start to begin.</p>
+        </div>
+    <#else>
+        <div class="rm-run-header">
+            <div class="rm-run-header-top">
+                <div class="rm-run-title-group">
+                    <h2 class="rm-run-title" id="nr-run-title">${run.version} &middot; RC${run.rc}</h2>
+                    <span class="tag status ${run.status?lower_case?replace('_','-')}" id="nr-run-status">${run.status}</span>
                 </div>
-                <div class="rm-field">
-                    <label for="nr-devversion">Development version</label>
-                    <input id="nr-devversion" type="text" placeholder="required if x.y.0 (e.g. 9.3.0-SNAPSHOT)">
+                <div class="rm-run-actions">
+                    <span id="nr-arm-area">
+                    <#if armed!false>
+                        <span class="tag armed" title="This run is armed for live mutation">ARMED</span>
+                    <#else>
+                        <button class="jc-btn jc-btn-warning" onclick="nrArm()">Arm this run</button>
+                    </#if>
+                    </span>
+                    <button class="jc-btn jc-btn-danger" onclick="nrDropRc()">Drop RC</button>
                 </div>
-                <div class="rm-field">
-                    <label for="nr-milestone">Milestone #</label>
-                    <input id="nr-milestone" type="number" placeholder="auto-resolved from version">
+            </div>
+            <div class="rm-run-meta" style="margin-top:10px;">
+                <span class="rm-meta-chip">Branch <b>${run.branch}</b></span>
+                <#if run.milestoneNumber??><span class="rm-meta-chip">Milestone <b>#${run.milestoneNumber}</b></span></#if>
+            </div>
+        </div>
+
+        <div class="rm-rail-layout" data-version="${run.version}" data-rc="${run.rc}" data-armed="${(armed!false)?c}">
+            <div class="rm-rail-col" id="nr-rail">
+                <#list run.steps as s>
+                <div class="rm-rail-item ${s.status?lower_case?replace('_','-')}" data-step="${s.id}"
+                     onclick="nrSelect('${s.id}')">
+                    <span class="rm-rail-num">${s?index + 1}</span>
+                    <span class="rm-rail-name">${s.id}</span>
+                    <span class="tag status ${s.status?lower_case?replace('_','-')}">${s.status}</span>
                 </div>
+                </#list>
             </div>
-            <div class="rm-field rm-mode-picker">
-                <span class="rm-mode-picker-label">Mode</span>
-                <label class="rm-mode-option" for="nr-mode-safe">
-                    <input type="radio" name="nr-mode" value="SAFE" id="nr-mode-safe" checked>
-                    Dry-run (SAFE)
-                </label>
-                <label class="rm-mode-option<#if !(liveCapable!false)> disabled</#if>" for="nr-mode-live">
-                    <input type="radio" name="nr-mode" value="LIVE" id="nr-mode-live"<#if !(liveCapable!false)> disabled</#if>>
-                    Actual (LIVE)
-                </label>
-                <#if !(liveCapable!false)>
-                <p class="hint">Actual (LIVE) is disabled because this box was started in SAFE mode. Relaunch with <code>rm.mode=live</code> to enable it. LIVE runs still require a typed &lsquo;<em>version</em> LIVE&rsquo; arming phrase.</p>
-                <#else>
-                <p class="hint">Dry-run rehearses against the in-app Nexus mock. Actual (LIVE) hits real endpoints and still requires a typed &lsquo;<em>version</em> LIVE&rsquo; arming phrase per run.</p>
-                </#if>
+            <div class="rm-detail-col" id="nr-detail">
+                <p class="hint">Select a step to preview and run it.</p>
             </div>
+        </div>
+
+        <script id="nr-step-meta" type="application/json">${stepMeta?no_esc}</script>
+    </#if>
+    </section>
+    <#else>
+    <section class="rm-subtab-panel" id="nr-panel-input" data-subtab="input">
+        <div class="jc-card rm-form-card"<#if hasRun> data-run-version="${run.version}"</#if>>
+            <#if !hasRun>
+            <h2>Start a new release</h2>
             <#else>
             <h2>Release details</h2>
-            <p class="hint">Narrative fields are optional and can be edited any time before each email is composed. Blank fields are omitted from the emails.</p>
+            <p class="hint">These fields can be edited any time before each email is composed. Blank narrative fields are omitted from the emails.</p>
+            </#if>
             <div class="rm-form-grid">
                 <div class="rm-field">
-                    <label>Version</label>
-                    <input type="text" value="${run.version}" readonly>
-                </div>
-                <div class="rm-field">
-                    <label>Development version</label>
-                    <input type="text" placeholder="&mdash;" value="<#if run.developmentVersion??>${run.developmentVersion}</#if>" readonly>
+                    <label for="nr-version">Version</label>
+                    <input id="nr-version" type="text" placeholder="e.g. 9.2.1" value="<#if hasRun>${run.version}</#if>">
                 </div>
                 <div class="rm-field">
-                    <label>Milestone #</label>
-                    <input type="text" placeholder="&mdash;" value="<#if run.milestoneNumber??>${run.milestoneNumber}</#if>" readonly>
+                    <label for="nr-devversion">Development version</label>
+                    <input id="nr-devversion" type="text" placeholder="required if x.y.0 (e.g. 9.3.0-SNAPSHOT)" value="<#if hasRun && run.developmentVersion??>${run.developmentVersion}</#if>">
                 </div>
                 <div class="rm-field">
-                    <label>Mode</label>
-                    <input type="text" value="${(run.mode!'SAFE')}" readonly>
+                    <label for="nr-milestone">Milestone #</label>
+                    <input id="nr-milestone" type="number" placeholder="auto-resolved from version" value="<#if hasRun && run.milestoneNumber??>${run.milestoneNumber}</#if>">
                 </div>
             </div>
-            </#if>
 
             <div class="rm-field">
                 <label for="nr-releaseSummary">Release summary <span class="rm-field-opt">optional</span></label>
@@ -120,55 +124,5 @@
             </div>
         </div>
     </section>
-
-    <section class="rm-subtab-panel" id="nr-panel-exec" data-subtab="exec"<#if !hasRun> hidden</#if>>
-    <#if !hasRun>
-        <div class="jc-card">
-            <p class="hint">No active run yet. Fill in the Input tab and click Start to begin.</p>
-        </div>
-    <#else>
-        <div class="rm-run-header">
-            <div class="rm-run-header-top">
-                <div class="rm-run-title-group">
-                    <h2 class="rm-run-title" id="nr-run-title">${run.version} &middot; RC${run.rc}</h2>
-                    <span class="tag status ${run.status?lower_case?replace('_','-')}" id="nr-run-status">${run.status}</span>
-                </div>
-                <div class="rm-run-actions">
-                    <#if (mode!'SAFE') == 'LIVE'>
-                        <span id="nr-arm-area">
-                        <#if armed!false>
-                            <span class="tag armed" title="This run is armed for live mutation">ARMED</span>
-                        <#else>
-                            <button class="jc-btn jc-btn-warning" onclick="nrArm()">Arm this run</button>
-                        </#if>
-                        </span>
-                    </#if>
-                    <button class="jc-btn jc-btn-danger" onclick="nrDropRc()">Drop RC</button>
-                </div>
-            </div>
-            <div class="rm-run-meta" style="margin-top:10px;">
-                <span class="rm-meta-chip">Branch <b>${run.branch}</b></span>
-                <#if run.milestoneNumber??><span class="rm-meta-chip">Milestone <b>#${run.milestoneNumber}</b></span></#if>
-            </div>
-        </div>
-
-        <div class="rm-rail-layout" data-version="${run.version}" data-rc="${run.rc}" data-mode="${mode!'SAFE'}" data-armed="${(armed!false)?c}">
-            <div class="rm-rail-col" id="nr-rail">
-                <#list run.steps as s>
-                <div class="rm-rail-item ${s.status?lower_case?replace('_','-')}" data-step="${s.id}"
-                     onclick="nrSelect('${s.id}')">
-                    <span class="rm-rail-num">${s?index + 1}</span>
-                    <span class="rm-rail-name">${s.id}</span>
-                    <span class="tag status ${s.status?lower_case?replace('_','-')}">${s.status}</span>
-                </div>
-                </#list>
-            </div>
-            <div class="rm-detail-col" id="nr-detail">
-                <p class="hint">Select a step to preview and run it.</p>
-            </div>
-        </div>
-
-        <script id="nr-step-meta" type="application/json">${stepMeta?no_esc}</script>
     </#if>
-    </section>
 </...@page>
diff --git a/src/main/resources/templates/releases.ftlh b/src/main/resources/templates/releases.ftlh
index b8a1cbee4a..7521cca7dd 100644
--- a/src/main/resources/templates/releases.ftlh
+++ b/src/main/resources/templates/releases.ftlh
@@ -18,12 +18,10 @@
 <@page tab="releases" toolkit="views"
     css="/datatables/dataTables.dataTables.min.css,https://cdn.datatables.net/buttons/3.2.0/css/buttons.dataTables.min.css"
     init="/datatables/jquery.min.js,/datatables/dataTables.min.js,https://cdn.datatables.net/buttons/3.2.0/js/dataTables.buttons.min.js,https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js,https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/pdfmake.min.js,https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/vfs_fonts.min.js,https://cdn.datatables.net/buttons/3.2.0/js/buttons.html5.min.js,/js/releases-detail.js">
-    <@card>
-        <div class="jc-page-header">
-            <h1>All Releases</h1>
-            <p class="jc-page-sub">Every Apache Juneau release, from the current vote candidate back through GA history.</p>
-        </div>
-    </...@card>
+    <div class="jc-page-header">
+        <h1>All Releases</h1>
+        <p class="jc-page-sub">Every Apache Juneau release, from the current vote candidate back through GA history.</p>
+    </div>
     <@card type="datatables" id="releases">
     {
         contractVersion: '1',
@@ -56,6 +54,7 @@
         detail: {
             contractVersion: '1',
             endpoint: '/rest/releases/expand/{id}',
+            icon: 'release',
             title: '{version}',
             region: { id: 'detail', type: 'row-detail', populate: 'releases-detail', dataUrl: '/rest/releases/expand/{id}', titleFields: [ 'version' ] }
         }
diff --git a/src/test/java/org/apache/juneau/releng/engine/SafeModeGuardTest.java b/src/test/java/org/apache/juneau/releng/engine/ArmGuardTest.java
similarity index 57%
rename from src/test/java/org/apache/juneau/releng/engine/SafeModeGuardTest.java
rename to src/test/java/org/apache/juneau/releng/engine/ArmGuardTest.java
index 40b8377f0e..df3f9f5cef 100644
--- a/src/test/java/org/apache/juneau/releng/engine/SafeModeGuardTest.java
+++ b/src/test/java/org/apache/juneau/releng/engine/ArmGuardTest.java
@@ -26,8 +26,8 @@ import org.apache.juneau.releng.util.ProcessRunner;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 
-/** The default-safe live guard + in-memory arming (never persisted, so re-arm is required after a restart). */
-class SafeModeGuardTest {
+/** The mutating-step arming guard (never persisted, so re-arm is required after a restart). */
+class ArmGuardTest {
 
 	private ProcessRunner okRunner() {
 		return new ProcessRunner() {
@@ -56,15 +56,12 @@ class SafeModeGuardTest {
 		};
 	}
 
-	private ReleaseEngine engine(Path dir, ExecutionMode mode) {
+	private ReleaseEngine engine(Path dir) {
 		var runner = okRunner();
 		var branches = new BranchResolver(runner, "/repo");
-		return ReleaseEngine.forTests(new RunStateStore(dir), StepRegistry.standard(branches), runner, branches, dir,
-				mode);
+		return ReleaseEngine.forTests(new RunStateStore(dir), StepRegistry.standard(branches), runner, branches, dir);
 	}
 
-	// Marks every step ahead of stepId SUCCEEDED so the forward-apply guard (ReleaseEngine) doesn't refuse
-	// to run stepId in isolation; this test is about the LIVE-armed/SAFE guard, not the whole pipeline.
 	private void satisfyAllPredecessorsOf(Path dir, String version, String stepId) {
 		var store = new RunStateStore(dir);
 		var rs = store.load(version).orElseThrow();
@@ -76,9 +73,9 @@ class SafeModeGuardTest {
 	}
 
 	@Test
-	void a01_liveUnarmedRefusesMutatingStep(@TempDir Path dir) {
-		var eng = engine(dir, ExecutionMode.LIVE);
-		eng.start("9.2.1", null, null, ExecutionMode.LIVE);
+	void a01_unarmedRefusesMutatingStep(@TempDir Path dir) {
+		var eng = engine(dir);
+		eng.start("9.2.1", null);
 		var res = eng.apply("9.2.1", "release-prepare", Map.of());
 		assertFalse(res.success);
 		assertTrue(res.message.toLowerCase().contains("arm"));
@@ -86,9 +83,9 @@ class SafeModeGuardTest {
 	}
 
 	@Test
-	void a02_liveArmedRunsMutatingStep(@TempDir Path dir) {
-		var eng = engine(dir, ExecutionMode.LIVE);
-		eng.start("9.2.1", null, null, ExecutionMode.LIVE);
+	void a02_armedRunsMutatingStep(@TempDir Path dir) {
+		var eng = engine(dir);
+		eng.start("9.2.1", null);
 		assertTrue(eng.arm("9.2.1", "9.2.1 LIVE").success);
 		satisfyAllPredecessorsOf(dir, "9.2.1", "release-prepare");
 		var res = eng.apply("9.2.1", "release-prepare", Map.of());
@@ -97,58 +94,21 @@ class SafeModeGuardTest {
 	}
 
 	@Test
-	void a03_safeAllowsMutatingStepWithoutArming(@TempDir Path dir) {
-		var eng = engine(dir, ExecutionMode.SAFE);
+	void a03_armRequiresTheExactConfirmPhrase(@TempDir Path dir) {
+		var eng = engine(dir);
 		eng.start("9.2.1", null);
-		satisfyAllPredecessorsOf(dir, "9.2.1", "release-prepare");
-		var res = eng.apply("9.2.1", "release-prepare", Map.of());
-		assertTrue(res.success, "SAFE simulates the mutating step; no arming required");
-	}
-
-	@Test
-	void a04_armIsRejectedInSafeMode(@TempDir Path dir) {
-		var eng = engine(dir, ExecutionMode.SAFE);
-		eng.start("9.2.1", null);
-		var res = eng.arm("9.2.1", "9.2.1 LIVE");
-		assertFalse(res.success);
-		assertFalse(eng.isArmed("9.2.1"));
-	}
-
-	@Test
-	void a05_liveBoxSafeRunAllowsMutatingWithoutArming(@TempDir Path dir) {
-		var eng = engine(dir, ExecutionMode.LIVE);
-		eng.start("9.2.1", null); // defaults Dry-run even on a LIVE box
-		satisfyAllPredecessorsOf(dir, "9.2.1", "release-prepare");
-		var res = eng.apply("9.2.1", "release-prepare", Map.of());
-		assertTrue(res.success, "a Dry-run on a LIVE box still simulates; no arming required");
-	}
-
-	@Test
-	void a06_armIsRejectedOnASafeRunEvenOnALiveBox(@TempDir Path dir) {
-		var eng = engine(dir, ExecutionMode.LIVE);
-		eng.start("9.2.1", null);
-		var res = eng.arm("9.2.1", "9.2.1 LIVE");
-		assertFalse(res.success);
-		assertFalse(eng.isArmed("9.2.1"));
-	}
-
-	@Test
-	void a07_armRequiresTheExactConfirmPhrase(@TempDir Path dir) {
-		var eng = engine(dir, ExecutionMode.LIVE);
-		eng.start("9.2.1", null, null, ExecutionMode.LIVE);
 		assertFalse(eng.arm("9.2.1", "yes").success);
 		assertFalse(eng.isArmed("9.2.1"));
 	}
 
 	@Test
-	void a08_armDoesNotSurviveARestart(@TempDir Path dir) {
-		var eng = engine(dir, ExecutionMode.LIVE);
-		eng.start("9.2.1", null, null, ExecutionMode.LIVE);
+	void a04_armDoesNotSurviveARestart(@TempDir Path dir) {
+		var eng = engine(dir);
+		eng.start("9.2.1", null);
 		assertTrue(eng.arm("9.2.1", "9.2.1 LIVE").success);
 		assertTrue(eng.isArmed("9.2.1"));
 
-		// A fresh engine over the same persisted store models a process restart: arm is in-memory only.
-		var restarted = engine(dir, ExecutionMode.LIVE);
+		var restarted = engine(dir);
 		assertFalse(restarted.isArmed("9.2.1"));
 		var res = restarted.apply("9.2.1", "release-prepare", Map.of());
 		assertFalse(res.success, "must re-arm after a restart");
diff --git a/src/test/java/org/apache/juneau/releng/engine/DropRcServiceTest.java b/src/test/java/org/apache/juneau/releng/engine/DropRcServiceTest.java
index 549637d0c2..15864c31c9 100644
--- a/src/test/java/org/apache/juneau/releng/engine/DropRcServiceTest.java
+++ b/src/test/java/org/apache/juneau/releng/engine/DropRcServiceTest.java
@@ -78,7 +78,7 @@ class DropRcServiceTest {
 		var store = new RunStateStore(dir);
 		seededThroughReleasePrepare(store);
 		var svc = new DropRcService(store, StepRegistry.standard(new BranchResolver(runner(), "/repo")), runner(),
-				Path.of("/staging/git/juneau"), dir, NexusStagingClient.forTests((m, p, b) -> ""), ExecutionMode.LIVE,
+				Path.of("/staging/git/juneau"), dir, NexusStagingClient.forTests((m, p, b) -> ""),
 				v -> true, TargetProfile.prodDefault(), (v, s) -> new LogBroadcaster());
 		var preview = svc.preview("9.2.1");
 		assertTrue(preview.lines.stream().anyMatch(l -> l.contains("orgapachejuneau-1042")));
@@ -91,7 +91,7 @@ class DropRcServiceTest {
 		var store = new RunStateStore(dir);
 		seededThroughReleasePrepare(store);
 		var svc = new DropRcService(store, StepRegistry.standard(new BranchResolver(runner(), "/repo")), runner(),
-				Path.of("/staging/git/juneau"), dir, NexusStagingClient.forTests((m, p, b) -> ""), ExecutionMode.LIVE,
+				Path.of("/staging/git/juneau"), dir, NexusStagingClient.forTests((m, p, b) -> ""),
 				v -> true, TargetProfile.prodDefault(), (v, s) -> new LogBroadcaster());
 
 		svc.apply("9.2.1", "vote rejected: -1 jdoe", () -> "avail", () -> "pw");
@@ -113,107 +113,8 @@ class DropRcServiceTest {
 		assertNull(rs.step("release-prepare").logRef);
 	}
 
-	/** §7.3/fidelity fix: Drop-RC's SVN cleanup + Tier-B log sink (was previously a silent no-op in SAFE). */
-	@Test
-	void a03_safeSvnCleanupSpawnsNothingButCommandLogsToItsOwnLogFile(@TempDir Path dir) throws java.io.IOException {
-		var store = new RunStateStore(dir);
-		seededThroughReleasePrepare(store);
-		var runCount = new int[] { 0 };
-		var runner = new ProcessRunner() {
-			@Override
-			public List<String> runLines(List<String> c) {
-				return List.of();
-			}
-
-			@Override
-			public String runText(List<String> c) {
-				return "";
-			}
-
-			@Override
-			public ProcResult run(List<String> c, String s, Map<String, String> e) {
-				runCount[0]++;
-				return new ProcResult(0, "");
-			}
-
-			@Override
-			public ProcResult run(List<String> c, String s, Map<String, String> e,
-					java.util.function.Consumer<String> k) {
-				runCount[0]++;
-				return new ProcResult(0, "");
-			}
-		};
-		var svc = new DropRcService(store, StepRegistry.standard(new BranchResolver(runner, "/repo")), runner,
-				Path.of("/staging/git/juneau"), dir, NexusStagingClient.forTests((m, p, b) -> ""), ExecutionMode.SAFE,
-				v -> true, TargetProfile.prodDefault(), (v, s) -> new LogBroadcaster());
-
-		svc.apply("9.2.1", "vote rejected: -1 jdoe", () -> "avail", () -> "s3cr3t-ldap-password");
-
-		assertEquals(0, runCount[0], "no real subprocess may be spawned for Drop-RC's Tier-B calls in SAFE");
-		var log = java.nio.file.Files.readString(dir.resolve("logs/9.2.1-RC1-drop-rc.log"),
-				java.nio.charset.StandardCharsets.UTF_8);
-		assertTrue(log.contains("would run: svn checkout"));
-		assertTrue(log.contains("would run: svn rm") && log.contains("source/juneau-9.2.1-RC1"));
-		assertTrue(log.contains("would run: svn rm") && log.contains("binaries/juneau-9.2.1-RC1"));
-		assertTrue(log.contains("would run: svn commit"));
-		assertTrue(log.contains("would run: git") && log.contains("tag") && log.contains("-d"));
-		assertTrue(log.contains("would run: git") && log.contains("push") && log.contains(":refs/tags/"));
-		assertTrue(log.contains("would run: mvn") && log.contains("release:rollback"));
-		assertFalse(log.contains("s3cr3t-ldap-password"), "the password must never be logged");
-	}
-
-	@Test
-	void a04_safeSvnCleanupPathsIncludeCurrentRcTag(@TempDir Path dir) throws java.io.IOException {
-		var store = new RunStateStore(dir);
-		seededThroughReleasePrepare(store);
-		var svc = new DropRcService(store, StepRegistry.standard(new BranchResolver(runner(), "/repo")), runner(),
-				Path.of("/staging/git/juneau"), dir, NexusStagingClient.forTests((m, p, b) -> ""), ExecutionMode.SAFE,
-				v -> true, TargetProfile.prodDefault(), (v, s) -> new LogBroadcaster());
-		svc.apply("9.2.1", "vote rejected", () -> "avail", () -> "pw");
-		var log = java.nio.file.Files.readString(dir.resolve("logs/9.2.1-RC1-drop-rc.log"),
-				java.nio.charset.StandardCharsets.UTF_8);
-		assertTrue(log.contains("https://dist.apache.org/repos/dist/dev/juneau"));
-	}
-
-	@Test
-	void a05_liveBoxSafeRunDoesNotSpawnSubprocess(@TempDir Path dir) {
-		var store = new RunStateStore(dir);
-		var rs = seededThroughReleasePrepare(store);
-		rs.mode = ExecutionMode.SAFE;
-		store.save(rs);
-		var runCount = new int[] { 0 };
-		var runner = new ProcessRunner() {
-			@Override
-			public List<String> runLines(List<String> c) {
-				return List.of();
-			}
 
-			@Override
-			public String runText(List<String> c) {
-				return "";
-			}
 
-			@Override
-			public ProcResult run(List<String> c, String s, Map<String, String> e) {
-				runCount[0]++;
-				return new ProcResult(0, "");
-			}
-
-			@Override
-			public ProcResult run(List<String> c, String s, Map<String, String> e,
-					java.util.function.Consumer<String> k) {
-				runCount[0]++;
-				return new ProcResult(0, "");
-			}
-		};
-		var svc = new DropRcService(store, StepRegistry.standard(new BranchResolver(runner, "/repo")), runner,
-				Path.of("/staging/git/juneau"), dir, NexusStagingClient.forTests((m, p, b) -> ""), ExecutionMode.LIVE,
-				v -> true, TargetProfile.prodDefault(), (v, s) -> new LogBroadcaster());
-
-		svc.apply("9.2.1", "vote rejected: -1 jdoe", () -> "avail", () -> "pw");
-
-		assertEquals(0, runCount[0], "a Dry-run on a LIVE box still command-logs Drop-RC; no real subprocess");
-	}
 
 	/**
 	 * Drop-RC's own {@code store.save(rs)} is the same choke point {@link ReleaseEngine} hooks in its
@@ -228,14 +129,13 @@ class DropRcServiceTest {
 	void a06_applyPublishesAResetSnapshotViaTheEngineSSharedRunStateStoreHook(@TempDir Path dir) {
 		var store = new RunStateStore(dir);
 		var branches = new BranchResolver(runner(), "/repo");
-		var eng = ReleaseEngine.forTests(store, StepRegistry.standard(branches), runner(), branches, dir,
-				ExecutionMode.LIVE); // installs the onSave hook on this exact store instance
+		var eng = ReleaseEngine.forTests(store, StepRegistry.standard(branches), runner(), branches, dir); // installs the onSave hook on this exact store instance
 		seededThroughReleasePrepare(store);
 		var seen = new ArrayList<RunStateSnapshot>();
 		eng.stateBroadcaster("9.2.1").subscribe(json -> seen.add(Json.DEFAULT.read(json, RunStateSnapshot.class)));
 
 		var svc = new DropRcService(store, StepRegistry.standard(branches), runner(), Path.of("/staging/git/juneau"),
-				dir, NexusStagingClient.forTests((m, p, b) -> ""), ExecutionMode.LIVE, v -> true,
+				dir, NexusStagingClient.forTests((m, p, b) -> ""), v -> true,
 				TargetProfile.prodDefault(), (v, s) -> new LogBroadcaster());
 
 		svc.apply("9.2.1", "vote rejected: -1 jdoe", () -> "avail", () -> "pw");
diff --git a/src/test/java/org/apache/juneau/releng/engine/ReleaseEngineTest.java b/src/test/java/org/apache/juneau/releng/engine/ReleaseEngineTest.java
index cb4e1990ac..52b74f0569 100644
--- a/src/test/java/org/apache/juneau/releng/engine/ReleaseEngineTest.java
+++ b/src/test/java/org/apache/juneau/releng/engine/ReleaseEngineTest.java
@@ -63,13 +63,11 @@ class ReleaseEngineTest {
 		return ReleaseEngine.forTests(new RunStateStore(dir), StepRegistry.standard(branches), runner, branches, dir);
 	}
 
-	private ReleaseEngine engineLive(Path dir) {
-		var runner = okRunner();
-		var branches = new BranchResolver(runner, "/repo");
-		return ReleaseEngine.forTests(new RunStateStore(dir), StepRegistry.standard(branches), runner, branches, dir,
-				ExecutionMode.LIVE);
+	private void arm(ReleaseEngine eng, String version) {
+		assertTrue(eng.arm(version, version + " LIVE").success);
 	}
 
+
 	/** Subscribes to {@code version}'s state broadcaster and decodes every published snapshot in order. */
 	@SuppressWarnings({
 		"resource" // The subscription stays open for the whole test; the JVM tears it down. Closing it would stop collecting snapshots.
@@ -121,25 +119,30 @@ class ReleaseEngineTest {
 	}
 
 	@Test
-	void a04_startDefaultsToSafeEvenOnALiveBox(@TempDir Path dir) {
-		var eng = engineLive(dir);
-		var rs = eng.start("9.2.1", null);
-		assertEquals(ExecutionMode.SAFE, rs.mode);
+	void a04_updateDetailsWritesIdentityFields(@TempDir Path dir) {
+		var eng = engine(dir);
+		eng.start("9.2.1", "9.2.2-SNAPSHOT", 7);
+		var updated = eng.updateDetails("9.2.1", "summary", "hi", "iss", "ack", "9.2.1", "9.2.3-SNAPSHOT", 11);
+		assertEquals("9.2.1", updated.version);
+		assertEquals("9.2.3-SNAPSHOT", updated.developmentVersion);
+		assertEquals(11, updated.milestoneNumber);
+		assertEquals("summary", updated.releaseSummary);
+		assertEquals("9.2.3-SNAPSHOT", new RunStateStore(dir).load("9.2.1").orElseThrow().developmentVersion);
 	}
 
 	@Test
-	void a05_startLiveIsCappedToSafeWhenTheBoxIsSafe(@TempDir Path dir) {
+	void a05_updateDetailsRenamesRunWhenVersionChanges(@TempDir Path dir) {
 		var eng = engine(dir);
-		var rs = eng.start("9.2.1", null, null, ExecutionMode.LIVE);
-		assertEquals(ExecutionMode.SAFE, rs.mode, "a SAFE box cannot mint a LIVE run");
+		eng.start("9.2.1", null);
+		var renamed = eng.updateDetails("9.2.1", null, null, null, null, "9.2.2", "9.2.3-SNAPSHOT", null);
+		assertEquals("9.2.2", renamed.version);
+		assertEquals("juneau-9.2.2-branch", renamed.branch);
+		assertTrue(new RunStateStore(dir).load("9.2.1").isEmpty(), "old store key must go away");
+		assertEquals("9.2.3-SNAPSHOT", new RunStateStore(dir).load("9.2.2").orElseThrow().developmentVersion);
 	}
 
-	@Test
-	void a06_startLiveIsHonoredWhenTheBoxIsLive(@TempDir Path dir) {
-		var eng = engineLive(dir);
-		var rs = eng.start("9.2.1", null, null, ExecutionMode.LIVE);
-		assertEquals(ExecutionMode.LIVE, rs.mode);
-	}
+
+
 
 	@Test
 	void a07_secondStartWhileActiveIsRejected(@TempDir Path dir) {
@@ -191,6 +194,7 @@ class ReleaseEngineTest {
 		// have already succeeded — currentStepId is bookkeeping only, never a gate.
 		var eng = engine(dir);
 		eng.start("9.2.1", null);
+		arm(eng, "9.2.1");
 		eng.apply("9.2.1", "preflight", Map.of());
 		eng.apply("9.2.1", "compose-propose-email", Map.of());
 		// "workspace-setup" is not currentStepId ("compose-propose-email" now is) — apply() must still
@@ -220,6 +224,7 @@ class ReleaseEngineTest {
 	void b05_eachStepGetsItsOwnBroadcasterAndLogPath(@TempDir Path dir) {
 		var eng = engine(dir);
 		eng.start("9.2.1", null);
+		arm(eng, "9.2.1");
 		eng.apply("9.2.1", "preflight", Map.of());
 		eng.apply("9.2.1", "compose-propose-email", Map.of());
 		eng.apply("9.2.1", "workspace-setup", Map.of());
@@ -230,7 +235,7 @@ class ReleaseEngineTest {
 	/**
 	 * Root cause of the reported vote-gate stall: applying {@code vote-gate} only (re)opens the vote — it
 	 * is NOT itself the advance action, however many times it's (re-)applied. This is the exact call the
-	 * old (broken) "Simulate (SAFE)" wiring made. The actual gate-pass action is the distinct
+	 * old (broken) vote-gate-only wiring made. The actual gate-pass action is the distinct
 	 * {@code tally-vote-result} step, with a {@code passed} outcome.
 	 */
 	@Test
@@ -274,6 +279,7 @@ class ReleaseEngineTest {
 	void d01_forwardApplyIsBlockedPastAnUnsatisfiedRequiredPredecessor(@TempDir Path dir) {
 		var eng = engine(dir);
 		eng.start("9.2.1", null);
+		arm(eng, "9.2.1");
 		// "preflight" (index 0) is still PENDING; "workspace-setup" (index 2) must be refused.
 		var res = eng.apply("9.2.1", "workspace-setup", Map.of());
 		assertFalse(res.success);
@@ -286,6 +292,7 @@ class ReleaseEngineTest {
 	void d02_reRunningAnAlreadySucceededStepIsNotBlockedByTheForwardApplyGuard(@TempDir Path dir) {
 		var eng = engine(dir);
 		eng.start("9.2.1", null);
+		arm(eng, "9.2.1");
 		eng.apply("9.2.1", "preflight", Map.of());
 		eng.apply("9.2.1", "compose-propose-email", Map.of());
 		eng.apply("9.2.1", "workspace-setup", Map.of());
@@ -387,8 +394,8 @@ class ReleaseEngineTest {
 
 	@Test
 	void f04_armPublishesTheUpdatedArmedFlagEvenThoughItNeverCallsSave(@TempDir Path dir) {
-		var eng = engineLive(dir);
-		eng.start("9.2.1", null, null, ExecutionMode.LIVE);
+		var eng = engine(dir);
+		eng.start("9.2.1", null);
 		var seen = subscribeSnapshots(eng, "9.2.1");
 
 		var res = eng.arm("9.2.1", "9.2.1 LIVE");
@@ -396,13 +403,12 @@ class ReleaseEngineTest {
 		assertTrue(res.success, res.message);
 		assertSize(1, seen);
 		assertTrue(seen.get(0).armed);
-		assertEquals(ExecutionMode.LIVE, seen.get(0).mode);
 	}
 
 	@Test
 	void f05_aRejectedArmAttemptPublishesNothing(@TempDir Path dir) {
-		var eng = engineLive(dir);
-		eng.start("9.2.1", null, null, ExecutionMode.LIVE);
+		var eng = engine(dir);
+		eng.start("9.2.1", null);
 		var seen = subscribeSnapshots(eng, "9.2.1");
 
 		var res = eng.arm("9.2.1", "wrong phrase");
@@ -447,14 +453,13 @@ class ReleaseEngineTest {
 
 	@Test
 	void g02_snapshotJsonIsEmptyForAnUnknownVersionAndReflectsArmedForAKnownOne(@TempDir Path dir) {
-		var eng = engineLive(dir);
+		var eng = engine(dir);
 		assertTrue(eng.snapshotJson("nope").isEmpty());
 
-		eng.start("9.2.1", null, null, ExecutionMode.LIVE);
+		eng.start("9.2.1", null);
 		eng.arm("9.2.1", "9.2.1 LIVE");
 		var snap = Json.DEFAULT.read(eng.snapshotJson("9.2.1").orElseThrow(), RunStateSnapshot.class);
 		assertTrue(snap.armed);
-		assertEquals(ExecutionMode.LIVE, snap.mode);
 	}
 
 	private StepStatus statusOf(RunStateSnapshot snap, String stepId) {
diff --git a/src/test/java/org/apache/juneau/releng/engine/RunStateSnapshotTest.java b/src/test/java/org/apache/juneau/releng/engine/RunStateSnapshotTest.java
index 587aa5d368..e1a769dc91 100644
--- a/src/test/java/org/apache/juneau/releng/engine/RunStateSnapshotTest.java
+++ b/src/test/java/org/apache/juneau/releng/engine/RunStateSnapshotTest.java
@@ -26,18 +26,17 @@ import org.junit.jupiter.api.Test;
 class RunStateSnapshotTest {
 
 	@Test
-	void a01_projectsVersionStatusRcModeArmedAndOrderedSteps() {
+	void a01_projectsVersionStatusRcArmedAndOrderedSteps() {
 		var rs = RunState.create("9.2.1", "juneau-9.2.1-branch", List.of("preflight", "workspace-setup"));
 		rs.rc = 2;
 		rs.status = RunStatus.AWAITING_VOTE;
 		rs.step("preflight").status = StepStatus.SUCCEEDED;
 
-		var snap = RunStateSnapshot.of(rs, ExecutionMode.LIVE, true);
+		var snap = RunStateSnapshot.of(rs, true);
 
 		assertEquals("9.2.1", snap.version);
 		assertEquals(RunStatus.AWAITING_VOTE, snap.status);
 		assertEquals(2, snap.rc);
-		assertEquals(ExecutionMode.LIVE, snap.mode);
 		assertTrue(snap.armed);
 		assertSize(2, snap.steps);
 		assertEquals("preflight", snap.steps.get(0).stepId);
@@ -49,13 +48,12 @@ class RunStateSnapshotTest {
 	@Test
 	void a02_serializesToTheCompactJsonShapeTheClientPatchesAgainst() {
 		var rs = RunState.create("9.2.1", "juneau-9.2.1-branch", List.of("preflight"));
-		var snap = RunStateSnapshot.of(rs, ExecutionMode.SAFE, false);
+		var snap = RunStateSnapshot.of(rs, false);
 
 		var json = Json.DEFAULT.write(snap);
 
 		assertTrue(json.contains("\"version\":\"9.2.1\""));
 		assertTrue(json.contains("\"status\":\"RUNNING\""));
-		assertTrue(json.contains("\"mode\":\"SAFE\""));
 		assertTrue(json.contains("\"armed\":false"));
 		assertTrue(json.contains("\"stepId\":\"preflight\""));
 	}
diff --git a/src/test/java/org/apache/juneau/releng/engine/RunStateTest.java b/src/test/java/org/apache/juneau/releng/engine/RunStateTest.java
index 9505aa73d1..8d3d0cbf60 100644
--- a/src/test/java/org/apache/juneau/releng/engine/RunStateTest.java
+++ b/src/test/java/org/apache/juneau/releng/engine/RunStateTest.java
@@ -34,15 +34,6 @@ class RunStateTest {
 		assertEquals(RunStatus.RUNNING, rs.status);
 		assertSize(3, rs.steps);
 		assertTrue(rs.steps.stream().allMatch(s -> s.status == StepStatus.PENDING));
-		assertNull(rs.mode, "create() must leave mode unset so pre-toggle JSON and Drop-RC tests fall back to constructor mode");
-	}
-
-	@Test
-	void a02_jsonWithoutModeFieldLeavesModeNull() {
-		var rs = RunState.create("9.2.1", "b", List.of("preflight"));
-		var json = Json.DEFAULT.write(rs);
-		var back = Json.DEFAULT.read(json, RunState.class);
-		assertNull(back.mode);
 	}
 
 	@Test
@@ -63,7 +54,6 @@ class RunStateTest {
 		rs.highlights = "- One\n- Two";
 		rs.knownIssues = "- A known thing";
 		rs.acknowledgements = "Thanks all.";
-		rs.mode = ExecutionMode.LIVE;
 
 		var json = Json.DEFAULT.write(rs);
 		var back = Json.DEFAULT.read(json, RunState.class);
@@ -77,7 +67,6 @@ class RunStateTest {
 		assertEquals("- One\n- Two", back.highlights);
 		assertEquals("- A known thing", back.knownIssues);
 		assertEquals("Thanks all.", back.acknowledgements);
-		assertEquals(ExecutionMode.LIVE, back.mode);
 	}
 
 }
diff --git a/src/test/java/org/apache/juneau/releng/engine/SecretsOffArgvTest.java b/src/test/java/org/apache/juneau/releng/engine/SecretsOffArgvTest.java
index 7d5ea64b01..c110577cf6 100644
--- a/src/test/java/org/apache/juneau/releng/engine/SecretsOffArgvTest.java
+++ b/src/test/java/org/apache/juneau/releng/engine/SecretsOffArgvTest.java
@@ -71,8 +71,7 @@ class SecretsOffArgvTest {
 
 	private StepContext ctx(ProcessRunner r, Path stateDir) {
 		var c = new StepContext();
-		c.mode = ExecutionMode.LIVE; // command-building + secret-routing is a LIVE concern; SAFE never spawns
-		c.run = RunState.create("9.2.1", "juneau-9.2.1-branch", List.of("deploy-snapshot"));
+				c.run = RunState.create("9.2.1", "juneau-9.2.1-branch", List.of("deploy-snapshot"));
 		c.run.nexusRepoId = "orgapachejuneau-1042";
 		c.runner = r;
 		c.log = s -> {
diff --git a/src/test/java/org/apache/juneau/releng/engine/steps/BinaryArtifactsStageStepTest.java b/src/test/java/org/apache/juneau/releng/engine/steps/BinaryArtifactsStageStepTest.java
index 9b9b31f0df..13e3237d25 100644
--- a/src/test/java/org/apache/juneau/releng/engine/steps/BinaryArtifactsStageStepTest.java
+++ b/src/test/java/org/apache/juneau/releng/engine/steps/BinaryArtifactsStageStepTest.java
@@ -25,7 +25,6 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import org.apache.juneau.releng.config.TargetProfile;
-import org.apache.juneau.releng.engine.ExecutionMode;
 import org.apache.juneau.releng.engine.RunState;
 import org.apache.juneau.releng.engine.StepContext;
 import org.apache.juneau.releng.util.ProcessRunner;
@@ -66,9 +65,8 @@ class BinaryArtifactsStageStepTest {
 		};
 	}
 
-	private StepContext ctx(ExecutionMode mode, Path stateDir) {
+	private StepContext ctx(Path stateDir) {
 		var c = new StepContext();
-		c.mode = mode;
 		c.run = RunState.create("9.2.1", "juneau-9.2.1-branch", List.of("binary-artifacts-stage"));
 		c.run.nexusRepoId = "orgapachejuneau-1042";
 		c.runner = recordingRunner();
@@ -83,7 +81,7 @@ class BinaryArtifactsStageStepTest {
 
 	@Test
 	void a01_liveBuildsCanonicalSvnWgetRenameGpgAddCommitSequence(@TempDir Path dir) {
-		var c = ctx(ExecutionMode.LIVE, dir);
+		var c = ctx(dir);
 		var res = new BinaryArtifactsStageStep().apply(c);
 		assertTrue(res.success, res.message);
 
@@ -137,22 +135,4 @@ class BinaryArtifactsStageStepTest {
 		assertTrue(commit.contains(rc)); // svn commit -m "<RC>"
 	}
 
-	@Test
-	void a02_safeSpawnsNothingAndLogsWouldRunForEveryMutation(@TempDir Path dir) {
-		var c = ctx(ExecutionMode.SAFE, dir);
-		var res = new BinaryArtifactsStageStep().apply(c);
-		assertTrue(res.success, res.message);
-		assertSize(() -> "no real subprocess may be spawned in SAFE", 0, calls);
-
-		var wouldRun = logLines.stream().filter(l -> l.startsWith("would run:")).toList();
-		assertTrue(wouldRun.stream().anyMatch(l -> l.contains("svn") && l.contains("checkout")));
-		assertTrue(wouldRun.stream().anyMatch(l -> l.contains("wget") && l.contains("*-source-release*")));
-		assertTrue(wouldRun.stream().anyMatch(l -> l.contains("gpg") && l.contains("SHA512")));
-		assertTrue(wouldRun.stream().anyMatch(l -> l.contains("svn") && l.contains("add")));
-		assertTrue(wouldRun.stream().anyMatch(l -> l.contains("svn") && l.contains("commit")));
-		assertTrue(logLines.stream().noneMatch(l -> l.contains("s3cr3t")), "the password must never be logged");
-
-		// SAFE never really staged anything, so the .sha512 file is not written to disk.
-		assertFalse(Files.exists(dir.resolve("dist")));
-	}
 }
diff --git a/src/test/java/org/apache/juneau/releng/engine/steps/ComposeVoteEmailStepTest.java b/src/test/java/org/apache/juneau/releng/engine/steps/ComposeVoteEmailStepTest.java
index 5ca9d23079..2b9cb73fb8 100644
--- a/src/test/java/org/apache/juneau/releng/engine/steps/ComposeVoteEmailStepTest.java
+++ b/src/test/java/org/apache/juneau/releng/engine/steps/ComposeVoteEmailStepTest.java
@@ -88,7 +88,7 @@ class ComposeVoteEmailStepTest {
 
 	@Test
 	void a02_absentShaFilesSoftNoteRatherThanFail(@TempDir Path dir) {
-		var c = ctx(dir); // no dist/ working copy at all (e.g. under a SAFE rehearsal)
+		var c = ctx(dir); // no dist/ working copy at all
 		var res = new ComposeVoteEmailStep().apply(c);
 		assertTrue(res.success, "absent checksums must not hard-fail the draft");
 	}
diff --git a/src/test/java/org/apache/juneau/releng/engine/steps/DevDistVerifyStepTest.java b/src/test/java/org/apache/juneau/releng/engine/steps/DevDistVerifyStepTest.java
index 31c11b55a3..ab5fe7e7d1 100644
--- a/src/test/java/org/apache/juneau/releng/engine/steps/DevDistVerifyStepTest.java
+++ b/src/test/java/org/apache/juneau/releng/engine/steps/DevDistVerifyStepTest.java
@@ -17,7 +17,6 @@
 
 package org.apache.juneau.releng.engine.steps;
 
-import static org.apache.juneau.test.bct.BctAssertions.assertSize;
 import static org.junit.jupiter.api.Assertions.*;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -25,7 +24,6 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import org.apache.juneau.releng.config.TargetProfile;
-import org.apache.juneau.releng.engine.ExecutionMode;
 import org.apache.juneau.releng.engine.RunState;
 import org.apache.juneau.releng.engine.StepContext;
 import org.apache.juneau.releng.util.ProcessRunner;
@@ -38,9 +36,8 @@ class DevDistVerifyStepTest {
 	private final List<List<String>> calls = new ArrayList<>();
 	private final List<String> logLines = new ArrayList<>();
 
-	private StepContext ctx(ExecutionMode mode, Path stateDir) {
+	private StepContext ctx(Path stateDir) {
 		var c = new StepContext();
-		c.mode = mode;
 		c.run = RunState.create("9.2.1", "b", List.of("dev-dist-verify"));
 		c.target = TargetProfile.prodDefault();
 		c.stateDir = stateDir;
@@ -88,7 +85,7 @@ class DevDistVerifyStepTest {
 	@Test
 	void a01_liveWithAllSixFilesPresentSucceedsWithoutOpeningWhenHeadless(@TempDir Path dir) throws Exception {
 		seedAllSixFiles(dir);
-		var res = new DevDistVerifyStep().apply(ctx(ExecutionMode.LIVE, dir));
+		var res = new DevDistVerifyStep().apply(ctx(dir));
 		assertTrue(res.success, res.message);
 	}
 
@@ -96,8 +93,8 @@ class DevDistVerifyStepTest {
 	void a02_liveWithMissingFileFails(@TempDir Path dir) throws Exception {
 		seedAllSixFiles(dir);
 		Files.delete(dir.resolve("dist/source/juneau-9.2.1-RC1/apache-juneau-9.2.1-src.zip.sha512"));
-		var res = new DevDistVerifyStep().apply(ctx(ExecutionMode.LIVE, dir));
-		assertFalse(res.success, "a missing dist file must hard-fail in LIVE");
+		var res = new DevDistVerifyStep().apply(ctx(dir));
+		assertFalse(res.success, "a missing dist file must hard-fail");
 		assertTrue(res.message.contains("src.zip.sha512"));
 	}
 
@@ -105,17 +102,8 @@ class DevDistVerifyStepTest {
 	void a03_liveWithEmptyFileFails(@TempDir Path dir) throws Exception {
 		seedAllSixFiles(dir);
 		Files.writeString(dir.resolve("dist/binaries/juneau-9.2.1-RC1/apache-juneau-9.2.1-bin.zip"), "");
-		var res = new DevDistVerifyStep().apply(ctx(ExecutionMode.LIVE, dir));
-		assertFalse(res.success, "a zero-size dist file must hard-fail in LIVE");
+		var res = new DevDistVerifyStep().apply(ctx(dir));
+		assertFalse(res.success, "a zero-size dist file must hard-fail");
 	}
 
-	@Test
-	void a04_safeWithArtifactsNotStagedSoftNotesRatherThanFails(@TempDir Path dir) {
-		// No dist/ working copy at all -- SAFE's binary-artifacts-stage was command-logged, so it never
-		// really staged anything.
-		var res = new DevDistVerifyStep().apply(ctx(ExecutionMode.SAFE, dir));
-		assertTrue(res.success, "SAFE must soft-note absent artifacts, not fail the rehearsal");
-		assertTrue(logLines.stream().anyMatch(l -> l.contains("SAFE") && l.contains("not staged")));
-		assertSize(() -> "no real subprocess may be spawned in SAFE", 0, calls);
-	}
 }
diff --git a/src/test/java/org/apache/juneau/releng/engine/steps/DistPromoteStepTest.java b/src/test/java/org/apache/juneau/releng/engine/steps/DistPromoteStepTest.java
index 2dd4729830..cf065ff796 100644
--- a/src/test/java/org/apache/juneau/releng/engine/steps/DistPromoteStepTest.java
+++ b/src/test/java/org/apache/juneau/releng/engine/steps/DistPromoteStepTest.java
@@ -17,14 +17,12 @@
 
 package org.apache.juneau.releng.engine.steps;
 
-import static org.apache.juneau.test.bct.BctAssertions.assertSize;
 import static org.junit.jupiter.api.Assertions.*;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import org.apache.juneau.releng.config.TargetProfile;
-import org.apache.juneau.releng.engine.ExecutionMode;
 import org.apache.juneau.releng.engine.RunState;
 import org.apache.juneau.releng.engine.StepContext;
 import org.apache.juneau.releng.util.ProcessRunner;
@@ -63,9 +61,8 @@ class DistPromoteStepTest {
 		};
 	}
 
-	private StepContext ctx(ExecutionMode mode, Path stateDir, List<String> tags) {
+	private StepContext ctx(Path stateDir, List<String> tags) {
 		var c = new StepContext();
-		c.mode = mode;
 		c.run = RunState.create("9.2.1", "juneau-9.2.1-branch", List.of("dist-promote"));
 		c.runner = recordingRunner(tags);
 		c.target = TargetProfile.prodDefault();
@@ -81,7 +78,7 @@ class DistPromoteStepTest {
 	@Test
 	void a01_liveMovesArtifactsAndRemovesPriorReleaseOnSameLine(@TempDir Path dir) {
 		var tags = List.of("juneau-9.2.0", "juneau-9.1.5", "juneau-9.2.1-RC1", "juneau-9.0.0");
-		var c = ctx(ExecutionMode.LIVE, dir, tags);
+		var c = ctx(dir, tags);
 		var res = new DistPromoteStep().apply(c);
 		assertTrue(res.success, res.message);
 
@@ -124,21 +121,10 @@ class DistPromoteStepTest {
 
 	@Test
 	void a02_noPriorReleaseOnLineSkipsRemoval(@TempDir Path dir) {
-		var c = ctx(ExecutionMode.LIVE, dir, List.of());
+		var c = ctx(dir, List.of());
 		var res = new DistPromoteStep().apply(c);
 		assertTrue(res.success, res.message);
 		assertFalse(calls.stream().anyMatch(x -> x.contains("rm") && x.get(0).equals("svn")));
 	}
 
-	@Test
-	void a03_safeSpawnsNothingAndLogsWouldRun(@TempDir Path dir) {
-		var c = ctx(ExecutionMode.SAFE, dir, List.of("juneau-9.2.0"));
-		var res = new DistPromoteStep().apply(c);
-		assertTrue(res.success, res.message);
-		assertSize(() -> "no real subprocess may be spawned in SAFE", 0, calls);
-		var wouldRun = logLines.stream().filter(l -> l.startsWith("would run:")).toList();
-		assertTrue(wouldRun.stream().anyMatch(l -> l.contains("svn") && l.contains("mv")));
-		assertTrue(wouldRun.stream().anyMatch(l -> l.contains("svn") && l.contains("commit")));
-		assertTrue(logLines.stream().noneMatch(l -> l.contains("s3cr3t")));
-	}
 }
diff --git a/src/test/java/org/apache/juneau/releng/engine/steps/MilestoneCloseStepTest.java b/src/test/java/org/apache/juneau/releng/engine/steps/MilestoneCloseStepTest.java
index 1eecc38cfb..7ed6f25d74 100644
--- a/src/test/java/org/apache/juneau/releng/engine/steps/MilestoneCloseStepTest.java
+++ b/src/test/java/org/apache/juneau/releng/engine/steps/MilestoneCloseStepTest.java
@@ -23,7 +23,6 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import org.apache.juneau.releng.config.TargetProfile;
-import org.apache.juneau.releng.engine.ExecutionMode;
 import org.apache.juneau.releng.engine.RunState;
 import org.apache.juneau.releng.engine.StepContext;
 import org.apache.juneau.releng.util.ProcessRunner;
@@ -35,9 +34,8 @@ class MilestoneCloseStepTest {
 	private final List<List<String>> calls = new ArrayList<>();
 	private final List<String> logLines = new ArrayList<>();
 
-	private StepContext ctx(ExecutionMode mode, Integer milestoneNumber) {
+	private StepContext ctx(Integer milestoneNumber) {
 		var c = new StepContext();
-		c.mode = mode;
 		c.run = RunState.create("9.2.1", "b", List.of("milestone-close"));
 		c.run.milestoneNumber = milestoneNumber;
 		c.target = TargetProfile.prodDefault();
@@ -72,14 +70,14 @@ class MilestoneCloseStepTest {
 
 	@Test
 	void a01_noMilestoneNumberLegitimatelyNoOps() {
-		var res = new MilestoneCloseStep().apply(ctx(ExecutionMode.LIVE, null));
+		var res = new MilestoneCloseStep().apply(ctx(null));
 		assertTrue(res.success);
 		assertSize(0, calls);
 	}
 
 	@Test
 	void a02_liveClosesResolvedMilestoneNumber() {
-		var res = new MilestoneCloseStep().apply(ctx(ExecutionMode.LIVE, 13));
+		var res = new MilestoneCloseStep().apply(ctx(13));
 		assertTrue(res.success, res.message);
 		var cmd = calls.get(0);
 		assertTrue(cmd.contains("gh"));
@@ -87,11 +85,4 @@ class MilestoneCloseStepTest {
 		assertTrue(cmd.contains("state=closed"));
 	}
 
-	@Test
-	void a03_safeLogsWouldRunAndSpawnsNothing() {
-		var res = new MilestoneCloseStep().apply(ctx(ExecutionMode.SAFE, 13));
-		assertTrue(res.success, res.message);
-		assertSize(0, calls);
-		assertTrue(logLines.stream().anyMatch(l -> l.startsWith("would run:") && l.contains("milestones/13")));
-	}
 }
diff --git a/src/test/java/org/apache/juneau/releng/engine/steps/ReleasePrepareStepTest.java b/src/test/java/org/apache/juneau/releng/engine/steps/ReleasePrepareStepTest.java
index afbd12ee7a..af02a54f84 100644
--- a/src/test/java/org/apache/juneau/releng/engine/steps/ReleasePrepareStepTest.java
+++ b/src/test/java/org/apache/juneau/releng/engine/steps/ReleasePrepareStepTest.java
@@ -21,7 +21,6 @@ import static org.junit.jupiter.api.Assertions.*;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
-import org.apache.juneau.releng.engine.ExecutionMode;
 import org.apache.juneau.releng.engine.RunState;
 import org.apache.juneau.releng.engine.StepContext;
 import org.apache.juneau.releng.util.ProcessRunner;
@@ -31,8 +30,7 @@ class ReleasePrepareStepTest {
 
 	private StepContext ctx(String version, Map<String, String> form, List<List<String>> calls) {
 		var c = new StepContext();
-		c.mode = ExecutionMode.LIVE; // this test asserts the built command/argv, which only spawns in LIVE
-		c.run = RunState.create(version, "b", List.of("release-prepare"));
+				c.run = RunState.create(version, "b", List.of("release-prepare"));
 		c.stagingRepo = java.nio.file.Path.of("/staging/git/juneau");
 		c.formInputs = form;
 		c.log = s -> {
diff --git a/src/test/java/org/apache/juneau/releng/engine/steps/SafeModeTierBTest.java b/src/test/java/org/apache/juneau/releng/engine/steps/SafeModeTierBTest.java
deleted file mode 100644
index 992ce3eaf8..0000000000
--- a/src/test/java/org/apache/juneau/releng/engine/steps/SafeModeTierBTest.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.juneau.releng.engine.steps;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import org.apache.juneau.releng.engine.ExecutionMode;
-import org.apache.juneau.releng.engine.RunState;
-import org.apache.juneau.releng.engine.StepContext;
-import org.apache.juneau.releng.util.ProcessRunner;
-import org.junit.jupiter.api.Test;
-
-/**
- * Tier-B fidelity: a mutating step run in SAFE mode must spawn <b>zero</b> real subprocesses and instead
- * emit a redacted "would run:" line carrying the correctly-built command.
- */
-class SafeModeTierBTest {
-
-	private final int[] runCount = { 0 };
-	private final List<String> logLines = new ArrayList<>();
-
-	private ProcessRunner countingRunner() {
-		return new ProcessRunner() {
-			@Override
-			public List<String> runLines(List<String> c) {
-				return List.of();
-			}
-
-			@Override
-			public String runText(List<String> c) {
-				return "";
-			}
-
-			@Override
-			public ProcResult run(List<String> c, String s, Map<String, String> e) {
-				runCount[0]++;
-				return new ProcResult(0, "");
-			}
-
-			@Override
-			public ProcResult run(List<String> c, String s, Map<String, String> e,
-					java.util.function.Consumer<String> k) {
-				runCount[0]++;
-				return new ProcResult(0, "");
-			}
-		};
-	}
-
-	private StepContext safeCtx() {
-		var c = new StepContext();
-		c.mode = ExecutionMode.SAFE;
-		c.run = RunState.create("9.2.1", "juneau-9.2.1-branch", List.of("release-prepare"));
-		c.runner = countingRunner();
-		c.stagingRepo = Path.of("/staging/git/juneau");
-		c.formInputs = Map.of();
-		c.log = logLines::add;
-		return c;
-	}
-
-	@Test
-	void a01_safeMutatingStepSpawnsNothingButLogsBuiltCommand() {
-		var res = new ReleasePrepareStep().apply(safeCtx());
-
-		assertTrue(res.success, "SAFE simulation should synthesize success");
-		assertEquals(0, runCount[0], "no real subprocess may be spawned in SAFE");
-
-		var wouldRun = logLines.stream().filter(l -> l.startsWith("would run:")).findFirst().orElseThrow();
-		assertTrue(wouldRun.contains("mvn"));
-		assertTrue(wouldRun.contains("release:prepare"));
-		assertTrue(wouldRun.contains("-Dtag=juneau-9.2.1-RC1"));
-		assertTrue(wouldRun.contains("-DdevelopmentVersion=9.2.2-SNAPSHOT"));
-	}
-}
diff --git a/src/test/java/org/apache/juneau/releng/nexus/NexusMockRestHttpTest.java b/src/test/java/org/apache/juneau/releng/nexus/NexusMockRestHttpTest.java
index d8ec2ec511..af6d2ceed1 100644
--- a/src/test/java/org/apache/juneau/releng/nexus/NexusMockRestHttpTest.java
+++ b/src/test/java/org/apache/juneau/releng/nexus/NexusMockRestHttpTest.java
@@ -32,7 +32,7 @@ import org.junit.jupiter.api.Test;
  * HTTP-level regression coverage for {@link NexusMockRest}: dispatches real requests through the same
  * servlet + response-processor pipeline the running app uses ({@link MockRestClient}, in-process, no
  * socket). {@link NexusMockTest} calls {@link NexusMockRest#route} directly and so never exercises how a
- * handler's return value gets serialized onto the wire — which is exactly the layer where the SAFE
+ * handler's return value gets serialized onto the wire — which is exactly the layer where the mock
  * {@code nexus-staging-close} walkthrough failed: the handlers returned already-serialized JSON text as a
  * plain {@code String}, so the framework serialized it a <em>second</em> time (quoting and escaping it)
  * before {@link NexusStagingClient} tried to parse it.
diff --git a/src/test/java/org/apache/juneau/releng/nexus/NexusMockTest.java b/src/test/java/org/apache/juneau/releng/nexus/NexusMockTest.java
index 563b8b7cf6..a92d399b68 100644
--- a/src/test/java/org/apache/juneau/releng/nexus/NexusMockTest.java
+++ b/src/test/java/org/apache/juneau/releng/nexus/NexusMockTest.java
@@ -23,7 +23,7 @@ import org.junit.jupiter.api.Test;
 
 /**
  * Tier-A fidelity: the real {@link NexusStagingClient} drives the full discovery + lifecycle against the
- * in-app {@link NexusMockModel} through the same {@link NexusMockRest#route routing table} the SAFE servlet
+ * in-app {@link NexusMockModel} through the same {@link NexusMockRest#route routing table} the mock servlet
  * uses — an in-process round-trip with no HTTP server (mirrors {@code ReleaseRunRestTest}'s no-mock-dep note).
  */
 class NexusMockTest {
diff --git a/src/test/java/org/apache/juneau/releng/rest/AdminRestTest.java b/src/test/java/org/apache/juneau/releng/rest/AdminRestTest.java
index 6691b5aa5e..8e70cad888 100644
--- a/src/test/java/org/apache/juneau/releng/rest/AdminRestTest.java
+++ b/src/test/java/org/apache/juneau/releng/rest/AdminRestTest.java
@@ -64,11 +64,13 @@ class AdminRestTest {
 	}
 
 	@Test
-	void b02_baseTemplateAuthorsExactlyThreeNavNodesOnce() throws IOException {
+	void b02_baseTemplateAuthorsNavNodesOnce() throws IOException {
 		var base = readClasspathResource("/templates/base.ftlh");
 		assertTrue(base.contains("<@navigation>"), "Chrome must author the nav tree once as <@navigation>: " + base);
 		var count = base.split("<@node ", -1).length - 1;
-		assertEquals(3, count, "Expected exactly 3 <@node> nav entries (Setup, Releases, New Release): " + base);
+		assertEquals(5, count, "Expected 5 <@node> entries (Setup, Releases, New Release + Input/Execution subtabs): " + base);
+		assertTrue(base.contains("href=\"/rest/runs?tab=input\""), "Missing Input subtab href: " + base);
+		assertTrue(base.contains("href=\"/rest/runs?tab=exec\""), "Missing Execution subtab href: " + base);
 	}
 
 	@Test
diff --git a/src/test/java/org/apache/juneau/releng/rest/ReleaseRunRestTest.java b/src/test/java/org/apache/juneau/releng/rest/ReleaseRunRestTest.java
index d027858104..11d5415e0b 100644
--- a/src/test/java/org/apache/juneau/releng/rest/ReleaseRunRestTest.java
+++ b/src/test/java/org/apache/juneau/releng/rest/ReleaseRunRestTest.java
@@ -29,7 +29,6 @@ import org.apache.juneau.releng.config.TargetProfile;
 import org.apache.juneau.releng.email.EmailService;
 import org.apache.juneau.releng.engine.BranchResolver;
 import org.apache.juneau.releng.engine.DropRcService;
-import org.apache.juneau.releng.engine.ExecutionMode;
 import org.apache.juneau.releng.engine.ReleaseEngine;
 import org.apache.juneau.releng.engine.RunStateStore;
 import org.apache.juneau.releng.engine.RunStatus;
@@ -82,17 +81,13 @@ class ReleaseRunRestTest {
 	}
 
 	private ReleaseRunRest rest(Path dir) {
-		return rest(dir, ExecutionMode.SAFE);
-	}
-
-	private ReleaseRunRest rest(Path dir, ExecutionMode mode) {
 		var runner = okRunner();
 		var branches = new BranchResolver(runner, "/repo");
 		var store = new RunStateStore(dir);
 		var registry = StepRegistry.standard(branches);
-		var engine = ReleaseEngine.forTests(store, registry, runner, branches, dir, mode);
+		var engine = ReleaseEngine.forTests(store, registry, runner, branches, dir);
 		var dropRc = new DropRcService(store, registry, runner, dir.resolve("staging/git/juneau"), dir,
-				NexusStagingClient.forTests((m, p, b) -> ""), engine.mode(), engine::isArmed,
+				NexusStagingClient.forTests((m, p, b) -> ""), engine::isArmed,
 				TargetProfile.prodDefault(), (v, s) -> new LogBroadcaster());
 		return new ReleaseRunRest(engine, dropRc);
 	}
@@ -150,16 +145,12 @@ class ReleaseRunRestTest {
 		};
 		var engine = new ReleaseEngine(store, registry, runner, branches, dir, dir.resolve("staging"), "/repo",
 				"test@apache.org", new EmailService(dir, runner), new MilestoneService(), secrets,
-				ExecutionMode.SAFE, TargetProfile.prodDefault());
+				TargetProfile.prodDefault());
 		var dropRc = new DropRcService(store, registry, runner, dir.resolve("staging/git/juneau"), dir, nexus,
-				engine.mode(), engine::isArmed, TargetProfile.prodDefault(), (v, s) -> new LogBroadcaster());
+				engine::isArmed, TargetProfile.prodDefault(), (v, s) -> new LogBroadcaster());
 		return new ReleaseRunRest(engine, dropRc);
 	}
 
-	private List<String> logLines(Path dir, ReleaseRunRest rest, String version, String stepId) throws IOException {
-		return Files.readAllLines(dir.resolve(rest.state(version).step(stepId).logRef));
-	}
-
 	/**
 	 * Marks every step before {@code stepId} (in registry order) SUCCEEDED, so a test can exercise
 	 * {@code stepId} in isolation under the strict forward-apply guard without needing every predecessor's
@@ -176,6 +167,25 @@ class ReleaseRunRestTest {
 		store.save(rs);
 	}
 
+
+	private void arm(ReleaseRunRest rest, String version) {
+		var body = new ReleaseRunRest.ArmRequest();
+		body.confirm = version + " LIVE";
+		var r = rest.arm(version, body);
+		assertTrue(r.success, r.message);
+	}
+
+	private void seedDistDevFiles(Path dir, String version, int rc) throws IOException {
+		var rcDir = "juneau-" + version + "-RC" + rc;
+		for (var kind : List.of("src", "bin")) {
+			var sub = "src".equals(kind) ? "source" : "binaries";
+			var d = dir.resolve("dist").resolve(sub).resolve(rcDir);
+			Files.createDirectories(d);
+			for (var ext : List.of("", ".asc", ".sha512"))
+				Files.writeString(d.resolve("apache-juneau-" + version + "-" + kind + ".zip" + ext), "content");
+		}
+	}
+
 	/** Applies {@code stepId} with no form input and asserts it succeeded. */
 	private void applyOk(ReleaseRunRest rest, String version, String stepId) {
 		applyOk(rest, version, stepId, Map.of());
@@ -264,28 +274,6 @@ class ReleaseRunRestTest {
 		assertEquals("Thanks all.", rs.acknowledgements);
 	}
 
-	@Test
-	void b04_startDefaultsToSafeMode(@TempDir Path dir) {
-		var rs = rest(dir).start(startRequest("9.2.1"));
-		assertEquals(ExecutionMode.SAFE, rs.mode);
-	}
-
-	@Test
-	void b05_startLiveIsCappedOnASafeEngine(@TempDir Path dir) {
-		var body = startRequest("9.2.1");
-		body.mode = "live";
-		var rs = rest(dir).start(body);
-		assertEquals(ExecutionMode.SAFE, rs.mode);
-	}
-
-	@Test
-	void b06_startLiveIsHonoredOnALiveEngine(@TempDir Path dir) {
-		var body = startRequest("9.2.1");
-		body.mode = "live";
-		var rs = rest(dir, ExecutionMode.LIVE).start(body);
-		assertEquals(ExecutionMode.LIVE, rs.mode);
-	}
-
 	@Test
 	void c01_detailsEndpointUpdatesNarrativeFieldsOnActiveRun(@TempDir Path dir) {
 		var rest = rest(dir);
@@ -306,6 +294,46 @@ class ReleaseRunRestTest {
 		assertEquals("Updated thanks.", reloaded.acknowledgements);
 	}
 
+	@Test
+	void c03_detailsEndpointUpdatesIdentityFieldsOnActiveRun(@TempDir Path dir) {
+		var rest = rest(dir);
+		var start = startRequest("9.2.1");
+		start.developmentVersion = "9.2.2-SNAPSHOT";
+		start.milestoneNumber = 7;
+		rest.start(start);
+
+		var body = new ReleaseRunRest.DetailsRequest();
+		body.version = "9.2.1";
+		body.developmentVersion = "9.2.3-SNAPSHOT";
+		body.milestoneNumber = 11;
+		body.releaseSummary = "Keep summary.";
+		var updated = rest.details("9.2.1", body);
+
+		assertEquals("9.2.1", updated.version);
+		assertEquals("9.2.3-SNAPSHOT", updated.developmentVersion);
+		assertEquals(11, updated.milestoneNumber);
+		assertEquals("Keep summary.", updated.releaseSummary);
+		var reloaded = rest.state("9.2.1");
+		assertEquals("9.2.3-SNAPSHOT", reloaded.developmentVersion);
+		assertEquals(11, reloaded.milestoneNumber);
+	}
+
+	@Test
+	void c04_detailsEndpointRenamesRunWhenVersionChanges(@TempDir Path dir) {
+		var rest = rest(dir);
+		rest.start(startRequest("9.2.1"));
+
+		var body = new ReleaseRunRest.DetailsRequest();
+		body.version = "9.2.2";
+		body.developmentVersion = "9.2.3-SNAPSHOT";
+		var renamed = rest.details("9.2.1", body);
+
+		assertEquals("9.2.2", renamed.version);
+		assertEquals("juneau-9.2.2-branch", renamed.branch);
+		assertThrows(NotFound.class, () -> rest.state("9.2.1"));
+		assertEquals("9.2.3-SNAPSHOT", rest.state("9.2.2").developmentVersion);
+	}
+
 	@Test
 	void a06_detailsAgainstNonexistentRunIs404(@TempDir Path dir) {
 		var rest = rest(dir);
@@ -347,8 +375,8 @@ class ReleaseRunRestTest {
 	}
 
 	/**
-	 * The fix, end to end, under the strict forward-apply guard: {@code POST /{version}/vote-result} —
-	 * what the fixed "Simulate (SAFE)" button now calls — records a passing tally (applied as the
+	 * The fix, end to end, under the strict forward-apply guard: {@code POST /{version}/vote-result}
+	 * records a passing tally (applied as the
 	 * {@code tally-vote-result} step) and the run then proceeds through every remaining required step, in
 	 * order, to {@code finalize-run}, with no operator intervention beyond each step's own apply or
 	 * confirm-review. This is also the regression test for the reported bug: a run must legitimately clear
@@ -362,12 +390,13 @@ class ReleaseRunRestTest {
 					 // test exists to catch -- that the SAME run legitimately clears every required step.
 	})
 	@Test
-	void d01_safeVoteResultAdvancesGateAndPipelineReachesFinalize(@TempDir Path dir) throws IOException {
+	void d01_voteResultAdvancesGateAndPipelineReachesFinalize(@TempDir Path dir) throws IOException {
 		var model = new NexusMockModel(NexusStagingClient.JUNEAU_PROFILE_ID);
 		var rest = restWithNexus(dir, model);
 		var start = startRequest("9.2.1");
 		start.milestoneNumber = 42;
 		rest.start(start);
+		arm(rest, "9.2.1");
 
 		for (var stepId : List.of("preflight", "compose-propose-email", "workspace-setup", "build-verify"))
 			applyOk(rest, "9.2.1", stepId);
@@ -390,14 +419,17 @@ class ReleaseRunRestTest {
 
 		applyOk(rest, "9.2.1", "binary-artifacts-stage");
 
+		// Mocked wget/mkdir never materialize dist files; seed the six ASF-convention artifacts
+		// so the live verify step can pass.
+		seedDistDevFiles(dir, "9.2.1", 1);
+
 		// dev-dist-verify: a required (non-skippable) review-gate step.
 		applyOk(rest, "9.2.1", "dev-dist-verify");
 		confirmOk(rest, "9.2.1", "dev-dist-verify");
 
 		applyOk(rest, "9.2.1", "compose-vote-email");
 
-		// Opening the vote only sets AWAITING_VOTE — this is the exact call the OLD "Simulate (SAFE)"
-		// wiring made, and it never advances on its own no matter how many times it's re-applied.
+		// Opening the vote only sets AWAITING_VOTE — re-applying vote-gate never advances on its own.
 		rest.apply("9.2.1", "vote-gate", Map.of());
 		rest.apply("9.2.1", "vote-gate", Map.of());
 		assertEquals(StepStatus.AWAITING_VOTE, rest.state("9.2.1").step("vote-gate").status);
@@ -405,7 +437,7 @@ class ReleaseRunRestTest {
 		// The fix: record a passing tally via the dedicated vote-result endpoint.
 		var voteBody = new ReleaseRunRest.VoteResultRequest();
 		voteBody.outcome = "passed";
-		voteBody.tally = "SAFE-mode simulated passing vote (no real tally read).";
+		voteBody.tally = "Simulated passing vote (no real tally read).";
 		var voted = rest.voteResult("9.2.1", voteBody);
 		assertTrue(voted.success, voted.message);
 		assertEquals(StepStatus.SUCCEEDED, rest.state("9.2.1").step("tally-vote-result").status);
@@ -414,24 +446,12 @@ class ReleaseRunRestTest {
 
 		applyOk(rest, "9.2.1", "compose-result-email");
 
-		// Tier A: nexus-release runs for real against the loopback mock — CLOSED -> RELEASED.
 		applyOk(rest, "9.2.1", "nexus-release", Map.of("confirmVersion", "9.2.1"));
 		assertEquals("released", model.currentState());
 
-		// Tier B: dist-promote is command-logged only (svn mv/commit), never a real subprocess.
 		applyOk(rest, "9.2.1", "dist-promote");
-		assertTrue(logLines(dir, rest, "9.2.1", "dist-promote").stream()
-				.anyMatch(l -> l.startsWith("would run:") && l.contains("svn") && l.contains("mv")));
-
-		// Tier B: github-release-create.
 		applyOk(rest, "9.2.1", "github-release-create");
-		assertTrue(logLines(dir, rest, "9.2.1", "github-release-create").stream()
-				.anyMatch(l -> l.startsWith("would run:") && l.contains("gh") && l.contains("release")));
-
-		// Tier B: milestone-close.
 		applyOk(rest, "9.2.1", "milestone-close");
-		assertTrue(logLines(dir, rest, "9.2.1", "milestone-close").stream()
-				.anyMatch(l -> l.startsWith("would run:") && l.contains("milestones/42")));
 
 		// manual-followup-checklist: a required (non-skippable) review-gate step — must be confirmed for
 		// finalize-run's strict prerequisite gate to be satisfiable. Its own apply() requires every
@@ -483,10 +503,7 @@ class ReleaseRunRestTest {
 		assertTrue(res.message.contains("Unknown step"), res.message);
 	}
 
-	/** LIVE mode must keep requiring a real recorded tally: the SAFE one-click shortcut is UI-only (the
-	 *  fixed "Simulate (SAFE)" button hard-codes {@code outcome=passed}); the {@code /vote-result} REST
-	 *  contract itself is unchanged, so a rejected outcome still forks away from the linear step list
-	 *  instead of quietly advancing. */
+	/** A rejected vote-result still records the tally and forks away from the linear step list. */
 	@Test
 	void d04_voteResultWithRejectedOutcomeDoesNotAdvanceButRecordsTheOutcome(@TempDir Path dir) {
 		var rest = rest(dir);
@@ -544,4 +561,24 @@ class ReleaseRunRestTest {
 		assertEquals(evilTitle, entry.get("title"));
 		assertEquals(Boolean.TRUE, entry.get("mutating"));
 	}
+
+	@Test
+	void f01_explicitTabInputWinsEvenWhenARunExists() {
+		assertEquals("input", ReleaseRunRest.resolveSubtab("input", true));
+		assertEquals("input", ReleaseRunRest.resolveSubtab("INPUT", false));
+	}
+
+	@Test
+	void f02_explicitTabExecWinsEvenWhenNoRunExists() {
+		assertEquals("exec", ReleaseRunRest.resolveSubtab("exec", false));
+		assertEquals("exec", ReleaseRunRest.resolveSubtab("execution", true));
+	}
+
+	@Test
+	void f03_defaultSubtabIsInputWithoutARunAndExecWithARun() {
+		assertEquals("input", ReleaseRunRest.resolveSubtab(null, false));
+		assertEquals("exec", ReleaseRunRest.resolveSubtab(null, true));
+		assertEquals("input", ReleaseRunRest.resolveSubtab("nope", false));
+		assertEquals("exec", ReleaseRunRest.resolveSubtab("nope", true));
+	}
 }
diff --git a/src/test/java/org/apache/juneau/releng/setup/SetupProbeServiceTest.java b/src/test/java/org/apache/juneau/releng/setup/SetupProbeServiceTest.java
index 2e7f3015f6..7caa7505cc 100644
--- a/src/test/java/org/apache/juneau/releng/setup/SetupProbeServiceTest.java
+++ b/src/test/java/org/apache/juneau/releng/setup/SetupProbeServiceTest.java
@@ -153,7 +153,6 @@ class SetupProbeServiceTest {
 	private static final class FakeRunner implements ProcessRunner {
 		final Set<String> onPath = new HashSet<>();
 		final List<List<String>> commands = new ArrayList<>();
-		List<String> lastCommand = List.of();
 		Map<String, String> lastEnv = Map.of();
 		Duration lastTimeout;
 		ProcResult installResult = new ProcResult(0, "ok");
@@ -171,7 +170,6 @@ class SetupProbeServiceTest {
 		@Override
 		public ProcResult run(List<String> command, String stdin, Map<String, String> env) {
 			commands.add(List.copyOf(command));
-			lastCommand = command;
 			if (env != null && !command.isEmpty() && !"which".equals(command.get(0)))
 				lastEnv = env;
 			if (!command.isEmpty() && "which".equals(command.get(0)))