# macOS Production to Local Dev Session Runbook

Status: canonical operational procedure

Last live verification: 2026-07-14

## Purpose

Use this runbook whenever a local TailMux menu-bar build must be tested on a Mac
that already has the Production app installed. It is the source of truth for the
complete Production → Dev → Production handoff.

The workflow is deliberately sequential. Production and Dev reuse the same
TailMux routing configuration, license activation, profile state, control token,
and loopback ports, so they must never run at the same time.

Do not replace this procedure with an ad hoc `open`, Xcode Run action, local
package install, copied `.app`, direct CLI launch, or license-gate bypass.

## Non-Negotiable Contract

- Build Dev completely before stopping the currently running app.
- Run exactly one TailMux app and one `tailmux gui serve` process at a time.
- Never overwrite or modify `/Applications/TailMux.app` during local testing.
- Keep Production and Dev as separate LaunchServices identities.
- Use the existing machine activation. A local UI build must not consume another
  Polar device slot.
- Use `https://tailmux.app` by default for links and license validation.
  A separate Dev website hostname is not the Dev-channel identity.
- Do not bypass the license gate. A valid real entitlement is a launch gate.
- Preserve the shared PAC state: when Production owned the TailMux PAC before
  takeover, Dev must restore it before the handoff is accepted.
- A launched menu-bar app remains alive until an explicit Quit or controlled
  stop. Closing Settings is not an app termination event.
- If a guard fails, stop and diagnose it. Do not weaken or skip the guard.

## Sources of Truth

The operator entry points are the Make targets:

| Intent | Canonical command | Implementation |
|---|---|---|
| Build and enter Dev | `make run-macos-dev` | `scripts/run-macos-dev.sh` |
| Stop Dev and leave both stopped | `make stop-macos-dev` | `scripts/stop-macos-dev.sh` |
| Stop Dev and restore Production | `make restore-macos-production` | `scripts/restore-macos-production.sh` |

Shared process, port, health, license, PAC, and rollback checks live in
`scripts/lib/macos-dev-session.sh`. Build identity and the production website
default live in `Project.swift`.

When behavior changes, update the scripts, this runbook, `AGENTS.md`, and the
short links in the repository READMEs in the same change. The scripts define
runtime behavior; this document defines how operators and agents must use it.

## Runtime Topology

| Property | Production | Local Dev |
|---|---|---|
| App path | `/Applications/TailMux.app` | `.derivedData/dev-session/Build/Products/Debug/TailMux.app` |
| Executable | `/Applications/TailMux.app/Contents/MacOS/TailMux` | `.derivedData/dev-session/Build/Products/Debug/TailMux.app/Contents/MacOS/TailMux` |
| Bundle identifier | `com.cqfabrication.tailmux.menu` | `com.cqfabrication.tailmux.menu.dev` |
| Display name | `TailMux` | `TailMux Dev` |
| Go service binary | Bundled Production CLI | `.derivedData/dev-session/Build/Products/Debug/tailmux-dev` |
| Website/license URL | `https://tailmux.app` | `https://tailmux.app` unless explicitly overridden |
| Control API | `http://127.0.0.1:43200` | Same, sequentially |
| HTTP proxy | `127.0.0.1:43100` | Same, sequentially |
| PAC server | `127.0.0.1:43180` | Same, sequentially |

The following CLI/runtime data is intentionally shared because both channels run
under the same macOS user without config-path overrides:

- `~/.config/tailmux/config.yaml`
- `~/.config/tailmux/license.json`
- `~/Library/Application Support/TailMux/control.token`
- the profile state directories and tunnel definitions referenced by the config

The macOS app preferences are not shared. The distinct bundle identifiers give
Production and Dev separate `UserDefaults`, including the UI shortcut and app
preferences. Do not assume that changing a Dev UI preference changes Production.
Conversely, edits made to TailMux profiles, tunnels, routing config, or the
license are real shared-data edits and will still be present after Production is
restored.

## Preconditions

Before a runtime switch:

1. Obtain explicit user approval to stop or replace the currently running
   TailMux instance. Tests and compile-only builds do not grant runtime approval.
2. Work from the repository root:

   ```bash
   cd "/Users/claudioquaglia/CQ Fabrication/tailmux"
   ```

3. Confirm these tools are available:

   ```bash
   command -v go
   command -v tuist
   command -v xcodebuild
   ```

4. Keep `/Applications/TailMux.app` installed if Production must be restored.
5. Do not manually quit Production before `make run-macos-dev`. The script needs
   to observe the initial runtime and PAC state to provide correct rollback.
6. Do not launch Dev from Xcode or Finder. The canonical script must inject the
   exact CLI path and site URL.
7. Commit, stash, or discard nothing merely to run this workflow. A dirty worktree
   is allowed; preserve unrelated user changes.

## Entering the Dev Session

Run one command:

```bash
make run-macos-dev
```

Do not run the underlying build and launch commands separately. The script uses
this ordered transaction:

1. **Classify the current process state.** Every process named `TailMux` must
   resolve to the exact installed Production executable or the exact Dev
   executable. The script refuses unknown processes and refuses concurrent
   Production and Dev instances.
2. **Record the initial state.** It remembers whether Production was running and
   whether Wi-Fi had the known TailMux PAC URL enabled.
3. **Build before takeover.** While the current runtime remains untouched, it:

   - runs `TUIST_SKIP_UPDATE_CHECK=1 tuist generate --no-open`;
   - builds the Debug app into `.derivedData/dev-session`;
   - overrides only the local app identity to
     `com.cqfabrication.tailmux.menu.dev` and `TailMux Dev`;
   - builds the pure-Go `tailmux-dev` CLI with `CGO_ENABLED=0`;
   - verifies both executables and the app's code signature.

4. **Begin takeover.** Only after the build succeeds does it mark the transaction
   as needing rollback on error.
5. **Quit the current channel gracefully.** It sends Quit to the exact bundle
   identifier and waits for the exact executable to exit. If graceful Quit does
   not complete within 20 seconds, it sends `TERM` only to the path-verified
   process. It never sends `KILL`.
6. **Wait for exclusive ownership.** Ports `43100`, `43180`, and `43200` must all
   be free before Dev can start.
7. **Launch the exact Dev artifact.** `open -n` starts the app from
   `.derivedData/dev-session` with:

   - `TAILMUX_CLI_PATH` set to the freshly built `tailmux-dev`;
   - `TAILMUX_SITE_URL` set to `https://tailmux.app` by default.

8. **Apply acceptance gates.** The handoff succeeds only when:

   - the app process runs from the exact Dev path;
   - `/api/health` becomes healthy;
   - `/api/license` reports `valid: true`;
   - process and health remain continuously valid for 25 seconds;
   - if Production previously owned the known TailMux PAC, Dev restores that PAC
     within the allowed window.

9. **Print the runtime report.** The script reports the process path, listeners,
   health response, PAC state, bundle identifier, site URL, and restore command.

Normal local testing uses no environment override. Only a deliberate website
integration test may change the site URL:

```bash
TAILMUX_SITE_URL=https://your-test-host.example make run-macos-dev
```

An alternate site must implement the same license API and is expected to affect
real entitlement validation. Never use an override merely to get around a failed
license check.

## Required Success Evidence

Do not declare the Dev handoff successful from `BUILD SUCCEEDED` alone. Confirm
the final script report and, for high-risk changes, run these read-only probes:

```bash
ps -axo pid=,ppid=,etime=,command= | \
  rg '/Applications/TailMux.app/Contents/MacOS/(TailMux|tailmux)|dev-session/Build/Products/Debug/(TailMux.app/Contents/MacOS/TailMux|tailmux-dev)'
```

Expected: the Dev app and `tailmux-dev gui serve` are present; no executable
under `/Applications/TailMux.app` is present.

```bash
lsof -nP \
  -iTCP:43100 -iTCP:43180 -iTCP:43200 \
  -sTCP:LISTEN
```

Expected: one `tailmux-dev` process owns all three loopback listeners.

```bash
curl -fsS --max-time 3 http://127.0.0.1:43200/api/health
curl -fsS --max-time 3 http://127.0.0.1:43200/api/license
networksetup -getautoproxyurl Wi-Fi
```

Expected:

- health is `{"status":"ok"}`;
- the license is activated and valid;
- `devices_used` has not increased merely because Dev was launched;
- when automatic PAC was active before takeover, the URL is
  `http://127.0.0.1:43180/proxy.pac` and `Enabled: Yes`.

The license endpoint returns only a masked key. Do not print or copy the raw
license file into logs, issues, commits, or chat.

## Manual Test Boundaries

Safe UI checks include opening and closing the popover, opening Settings, using
the global shortcut, editing UI-only Dev preferences, and exercising the feature
under development.

Keep these shared-state effects in mind:

- Closing Settings must leave the menu-bar app running.
- **Quit TailMux** intentionally stops Dev and its managed service; it does not
  automatically start Production.
- Profile, tunnel, and router configuration changes persist into Production.
- Deactivating the license releases the real machine activation and must not be
  used as a casual UI test.
- Do not enable **Launch at Login** for the derived Dev app unless that feature is
  the explicit test subject. If tested, disable it again before removing or
  rebuilding the derived app.
- Do not run Production concurrently for comparison. Capture screenshots or
  restore Production between comparisons.

## Ending the Dev Session

### Normal path: restore Production

Run:

```bash
make restore-macos-production
```

The script:

1. verifies `/Applications/TailMux.app` exists;
2. classifies the running TailMux process state;
3. gracefully stops Dev by its Dev bundle identifier;
4. waits until the shared ports are free;
5. launches the exact installed app path;
6. verifies the exact Production process path;
7. waits for health and a valid local activation;
8. prints the Production runtime, listener, health, and PAC report.

If Production is already running and healthy, the command is idempotent and only
reports its state.

### Deliberate path: leave both channels stopped

Run:

```bash
make stop-macos-dev
```

This stops Dev and waits for the shared ports to be released. It never starts or
stops Production. If Production is already running, it reports that Dev is
already stopped.

### After using Quit in the Dev UI

The UI Quit action leaves both channels stopped. Restore Production explicitly:

```bash
make restore-macos-production
```

## Rollback Semantics

`make run-macos-dev` installs `ERR`, `INT`, and `TERM` traps before takeover.

| Failure point | Guaranteed behavior |
|---|---|
| Tuist, Xcode, Go, executable, or signature build failure | The current runtime has not been touched. |
| Unknown TailMux process or simultaneous known channels | The script refuses to continue. |
| Quit or port-release failure | Dev launch is aborted; rollback begins after takeover started. |
| Dev process, health, license, stability, or required PAC failure | Dev is stopped and the shared ports are released. |
| Production was running before a failed takeover | The installed Production app is reopened and health-checked. |
| Production was not running before a failed takeover | Production is not started; the prior stopped state is preserved. |

If automatic rollback prints `Automatic Production restore did not become
healthy`, do not launch another arbitrary binary. Run the canonical restore:

```bash
make restore-macos-production
```

Then use the read-only process, listener, health, license, and PAC probes above.
If an unknown TailMux process blocks the scripts, identify its full executable
path first. Never use `kill -9`, delete state directories, remove the license, or
disable an unrelated PAC as a recovery shortcut.

If both apps are intentionally stopped but the system still reports the exact
TailMux PAC URL as enabled, prefer restoring Production. Only when both channels
must remain stopped, first verify the URL is exactly TailMux-owned and then turn
off that PAC state:

```bash
networksetup -getautoproxyurl Wi-Fi
networksetup -setautoproxystate Wi-Fi off
```

Never disable a PAC URL that is not one of TailMux's known loopback URLs.

## Gatekeeper, Quarantine, and Polar

The local workflow does not install a package and does not replace the downloaded
Production app. It launches an ad-hoc-signed Debug artifact directly from the
workspace, so Production's notarization or quarantine metadata is not modified.
Do not remove quarantine from `/Applications/TailMux.app` to make a Dev build run.

Dev and Production use different bundle identifiers, but bundle identity is not
the license device identity. Both use the same local license record and machine
identity, and the Dev process validates them against `https://tailmux.app`.
Launching Dev must therefore reuse the existing activation rather than consuming
a second Polar slot.

## Maintaining This Workflow

Any change to `Project.swift`, app identity, executable names, shared ports,
control endpoints, license storage, PAC behavior, or the three session scripts
must review and update this runbook.

Minimum static validation after changing the workflow:

```bash
bash -n \
  scripts/lib/macos-dev-session.sh \
  scripts/run-macos-dev.sh \
  scripts/stop-macos-dev.sh \
  scripts/restore-macos-production.sh
git diff --check
```

Run relevant Go and Swift tests. A live takeover/restore verification is required
for launch-flow changes, but only after explicit user approval. Record the exact
process paths, health, license validity, listener ownership, PAC state, and
whether Production or Dev was intentionally left running.
