// SPDX-License-Identifier: CC-BY-SA-4.0
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath)
= UX Standards - Comprehensive Launcher Standard
:toc:
:toclevels: 4

== Overview

This document defines the **Comprehensive Launcher Standard** for the hyperpolymath ecosystem.
It extends the E-Grade principles with specific implementation patterns, code templates, and
troubleshooting guides for creating robust, reliable application launchers.

== Standard Launcher Template

The reference implementation is provided in link:comprehensive-launcher-template.sh[comprehensive-launcher-template.sh].

=== Key Features

[source,bash]
----
# Standardized structure for all launchers
APP_NAME="MyApp"              # Application name
REPO_DIR="/path/to/repo"       # Repository directory
COMMAND="command to run"      # Startup command
URL="http://localhost:PORT"     # Web URL (if applicable)
# PID file in XDG_RUNTIME_DIR (mode 0700, user-scoped) — falls back to
# $TMPDIR (macOS) then /tmp (last resort). See §Best Practices > Security.
PID_FILE="${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/${APP_NAME}-server.pid"
# Log file in XDG_STATE_HOME (defaults to $HOME/.local/state per spec).
LOG_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/${APP_NAME}"
LOG_FILE="${LOG_DIR}/server.log"
mkdir -p "$LOG_DIR"
MODE="${1:---auto}"            # Default mode
----

=== Required Functions

[source,bash]
----
log()          # User feedback (stdout)
err()          # Error messages (stderr)
is_running()   # Check if process is running
wait_for_server() # Active waiting with timeout
start_server() # Robust server startup
stop_server()  # Clean shutdown
open_browser() # Browser launching (if web app)
----

== Implementation Patterns

=== Process Management

[source,bash]
----
# Start process with nohup to prevent termination
nohup $COMMAND >"$LOG_FILE" 2>&1 &
echo $! > "$PID_FILE"

# Check if process is still running
is_running() {
  [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null
}

# Clean shutdown
stop_server() {
  if is_running; then
    kill "$(cat "$PID_FILE")" 2>/dev/null || true
    rm -f "$PID_FILE"
  fi
}
----

=== Server Readiness Checking

[source,bash]
----
wait_for_server() {
  # All three values are env-overridable per [runtime] in launcher-standard.a2ml.
  local max_wait="${1:-${WAIT_FOR_URL_TIMEOUT_SECONDS:-15}}"
  local poll_interval="${WAIT_FOR_URL_POLL_INTERVAL:-1}"
  local per_request_timeout=2   # curl --max-time; caps each probe so a
                                # hung server can't eat the whole budget
  local waited=0

  while [ "$waited" -lt "$max_wait" ]; do
    if curl -fsS --max-time "$per_request_timeout" "$URL" >/dev/null 2>&1; then
      return 0  # Success
    fi
    sleep "$poll_interval"
    waited=$((waited + poll_interval))
  done

  return 1  # Timeout
}

# Usage — pass nothing so the env-var/constant chain applies.
if ! wait_for_server; then
  err "Server did not start within ${WAIT_FOR_URL_TIMEOUT_SECONDS:-15}s"
  err "Check log: $LOG_FILE"
  err "Override with WAIT_FOR_URL_TIMEOUT_SECONDS=<N> if the host needs longer"
  return 1
fi
----

=== Browser Launching

Resolution order, per `[browser-launch]` in `launcher-standard.a2ml`:

1. `$BROWSER` env var (de-facto Unix convention; user/operator override)
2. Platform-specific ladder, dispatched via `uname -s`:
   - **macOS**: `open`
   - **Linux**: if WSL (detected via `/proc/version` containing
     "microsoft"), prefer `wslview` so the URL opens in the Windows-side
     default browser. Otherwise: `xdg-open` → `firefox` → `chromium`.
   - **Windows** (Git Bash / MSYS / Cygwin): `start` (cmd builtin)
3. Fallback: print `"Open manually: $URL"` so the user is never silently
   left without a URL.

[source,bash]
----
open_browser() {
  if ! is_running; then
    err "Server is not running"
    return 1
  fi

  # 1. $BROWSER override (canonical Unix convention)
  if [[ -n "${BROWSER:-}" ]]; then
    "$BROWSER" "$URL" &
    return $?
  fi

  # 2. Platform-specific ladder
  case "$(uname -s)" in
    Darwin*)
      open "$URL" &
      ;;
    Linux*)
      # WSL detection: /proc/version mentions "microsoft" under WSL1/2
      if grep -qi microsoft /proc/version 2>/dev/null && \
         command -v wslview >/dev/null 2>&1; then
        wslview "$URL" &
      elif command -v xdg-open >/dev/null 2>&1; then
        xdg-open "$URL" &
      elif command -v firefox >/dev/null 2>&1; then
        firefox "$URL" &
      elif command -v chromium >/dev/null 2>&1; then
        chromium "$URL" &
      else
        log "Open manually: $URL"
      fi
      ;;
    MINGW*|MSYS*|CYGWIN*)
      # Git Bash / MSYS / Cygwin all carry `start` from cmd
      start "$URL" &
      ;;
    *)
      log "Open manually: $URL"
      ;;
  esac
}
----

=== Error Handling

Stderr-only `err()` is the right default for TUI invocation, but when a
launcher is started from a desktop entry there is no terminal for the
user to see — every failure looks identical (a brief flash and nothing
else). The reference helper `launcher/gui-error.sh` surfaces errors via
the platform's dialog ladder AND stderr, so the failure is visible no
matter how the launcher was started.

[source,bash]
----
# Always provide actionable feedback
err() {
  echo "[$APP_NAME] ERROR: $1" >&2
  echo "[$APP_NAME] Try: check $LOG_FILE for details" >&2
}

# Source the shared GUI-error helper (reference impl of [error-visibility]
# from launcher-standard.a2ml). Falls back to err() if not available so
# every launcher works even without the shared helper on PATH.
if [ -r "$(hp_resolve_desktop_tools gui-error.sh 2>/dev/null)" ]; then
  # shellcheck disable=SC1090
  . "$(hp_resolve_desktop_tools gui-error.sh)"
else
  hp_gui_error() { err "$2"; }   # graceful degradation
fi

# Example usage — fails LOUDLY whether GUI or TUI
if ! start_server; then
  hp_gui_error "$APP_NAME failed to start" \
               "Check log: $LOG_FILE\n\nOverride wait timeout: WAIT_FOR_URL_TIMEOUT_SECONDS=<N>"
  exit 1
fi
----

NOTE: `hp_gui_error` writes to stderr unconditionally per
`[error-visibility].always-also-to-stderr = true`. Set `NO_GUI_ERROR=1`
to suppress the dialog attempt (useful in CI).

=== Soft-Attach (optional ecosystem integrations)

A "soft-attach" tool is one the launcher calls IF it is installed, and
silently skips otherwise. The estate ships three by default
(`feedback-o-tron`, `hypatia`, `panic-attack`) — see
`[soft-attach].tools` in `launcher-standard.a2ml` for the live list.

Downstream launchers SHOULD source `launcher/soft-attach.sh` rather
than re-implementing the if-installed-then-invoke pattern, so behaviour
stays consistent across the estate.

[source,bash]
----
# Source the shared soft-attach helper. Graceful degradation: if the
# helper is not on the resolution ladder, every soft-attach call
# becomes a silent no-op.
if [ -r "$(hp_resolve_desktop_tools soft-attach.sh 2>/dev/null)" ]; then
  # shellcheck disable=SC1090
  . "$(hp_resolve_desktop_tools soft-attach.sh)"
else
  hp_soft_attach_event() { :; }
  hp_soft_attach_run()   { :; }
fi

# Call sites: typically wired into start_server() on failure path
on_start_failed() {
  hp_soft_attach_event "feedback-o-tron" "launcher:start_failed" \
      --app "$APP_NAME" --log "$LOG_FILE"
  hp_soft_attach_run "hypatia diagnose --app $APP_NAME --log $LOG_FILE"
  hp_soft_attach_run "panic-attack assail $REPO_DIR"
}
----

Note that template substitution (`{app-name}`, `{log-file}`,
`{repo-dir}` in the a2ml) is the launcher's responsibility — interpolate
before passing the command line to `hp_soft_attach_run`.

== Standard Modes

=== Required Modes

[cols="1,3"]
|===
| Mode | Purpose

| `--start`
| Start the server/application without opening browser

| `--stop`
| Stop the running server/application

| `--status`
| Show current status (running/stopped, URL if applicable)

| `--auto` (default)
| Start server and open browser (for web apps)

| `--browser` / `--web`
| Alias for `--auto` (start and open browser)

| `--integ`
| **System integration**. Install the launcher as a first-class application:
  Start Menu / Applications folder entry, Desktop shortcut, icon, and the
  launcher itself copied to a stable location on `PATH`. Cross-platform
  (see §System Integration Modes below). Idempotent; `--integ --force`
  reinstalls without prompting.

| `--disinteg`
| **System dis-integration**. Undo everything `--integ` installed:
  Start Menu entry, Desktop shortcut, icon, and the installed launcher.
  Also stops the running server. Leaves user config in `~/.config/<app>/`
  and logs in place so reinstall is seamless. Idempotent.

| `--help` / `-h`
| Print usage text, including a description of every mode, the files the
  launcher reads/writes, and the detected platform.

| `--version` / `-V`
| Print the launcher's version on a single machine-greppable first line,
  then exit 0. Format: `<app-name> <version> (<build-sha-short>) [<platform>]`
  (e.g. `aerie-launcher 0.4.2 (a1b2c3d) [linux-x86_64]`). Additional lines
  (build date, runtime versions) MAY follow. Required so field bug reports
  can quote a specific build — a launcher without `--version` produces
  unactionable issues.
|===

NOTE: `--integ` and `--disinteg` replace the earlier pattern of separate
`install.sh` / `uninstall.sh` scripts from LM-LA-LIFECYCLE-STANDARD.
Having install, uninstall, and runtime in a single script means users have
one thing to run and one thing to remember, which is the whole point of the
E-Grade principle. The LM-LA-LIFECYCLE-STANDARD remains authoritative for
*what* must happen during install and uninstall; this standard specifies
*where* those operations are triggered from (the launcher itself).

=== Mode Implementation

[source,bash]
----
case "$MODE" in
  --start)
    start_server
    ;;
  
  --stop)
    stop_server
    ;;
  
  --status)
    if is_running; then
      log "Server running (PID: $(cat "$PID_FILE"))"
      [ -n "$URL" ] && log "URL: $URL"
    else
      log "Server not running"
    fi
    ;;
  
  --browser|--web|--auto|*)
    start_server && open_browser
    ;;
esac
----

== System Integration Modes (`--integ` / `--disinteg`)

The `--integ` mode installs the launcher as a first-class desktop application
on the host OS. It is the single entry point for "make this a real app on my
system". The `--disinteg` mode is its exact inverse.

=== Design principles

1. **Idempotent.** `--integ` run twice is fine. `--disinteg` on an unintegrated
   system is a friendly no-op, not an error.
2. **Cross-platform.** The same launcher script works on Linux, macOS, and
   Windows (via Git Bash or WSL). Platform detection is via `uname -s`.
3. **Detectable.** `--integ` detects existing installations and prompts
   before overwriting, unless `--force` is passed as a second argument.
4. **Reversible.** `--disinteg` removes everything `--integ` installed and
   leaves user config and logs alone so reinstall is seamless.
5. **No elevated privileges.** All paths are user-level. No `sudo`, no
   system-wide directories. Reinstall per user, not per machine.
6. **Launcher copy, not symlink.** `--integ` copies the launcher script to a
   stable location on `PATH` rather than symlinking, so moving or renaming
   the source repo does not break the Start Menu entry.

=== What `--integ` creates

[cols="1,2,3"]
|===
| Platform | Path | Purpose

| Linux
| `~/.local/share/applications/<app>.desktop`
| Start Menu / app launcher entry

| Linux
| `~/Desktop/<app>.desktop`
| Desktop shortcut (marked trusted via `gio` if available, so KDE doesn't prompt)

| Linux
| `~/.local/share/icons/hicolor/256x256/apps/<app>.png`
| Icon used by the Start Menu entry

| Linux
| `~/.local/bin/<app>-launcher`
| Copy of the launcher script on `PATH`

| macOS
| `~/Applications/<App>.app/` (minimal bundle: `Info.plist`, `Contents/MacOS/<app>`)
| Launchpad + Spotlight entry. Not codesigned.

| macOS
| `~/Desktop/<App>.command`
| Double-clickable shortcut

| macOS
| `~/.local/bin/<app>-launcher`
| Copy of the launcher script on `PATH`

| Windows (Git Bash / WSL)
| `%APPDATA%\Microsoft\Windows\Start Menu\Programs\<App>.lnk`
| Start Menu entry. Created via PowerShell `WScript.Shell`. Falls back to `.bat` if PowerShell is not reachable.

| Windows (Git Bash / WSL)
| `~/Desktop/<App>.lnk`
| Desktop shortcut

| Windows (Git Bash / WSL)
| `~/.local/bin/<app>-launcher.sh`
| Copy of the launcher script
|===

=== What `--disinteg` removes

Everything `--integ` created, plus:

* The PID file (`$XDG_RUNTIME_DIR/<app>-server.pid`, or the resolved equivalent — see §Best Practices > Security)
* Any `.bat` fallback shortcuts written when PowerShell wasn't available

It deliberately **does not** remove:

* `~/.config/<app>/` — user preferences survive reinstall
* `$XDG_STATE_HOME/<app>/` (defaults to `$HOME/.local/state/<app>/`) — logs stay for post-mortem
* The source repository at `REPO_DIR`

The removal instructions for those are printed after `--disinteg` so the user
can run them manually if they really want a clean wipe.

=== Reference implementation

See link:comprehensive-launcher-template.sh[comprehensive-launcher-template.sh]
for the `do_integ()` / `do_disinteg()` functions and the platform-dispatch
pattern. Existing launchers should be updated to include these modes; the
pattern is drop-in because the integration paths are all globals set by
platform detection at the top of the script.

== Desktop File Standard

=== Required Format

Every `.desktop` file's primary `Exec=` line MUST go through `keepopen.sh`
(see §Fallback Ladder below). This ensures that a user who double-clicks
a desktop icon always lands somewhere useful — even when every upstream
hook is broken — instead of seeing a terminal flash on-and-off with no
feedback.

[source,ini]
----
[Desktop Entry]
Type=Application
Name=Application Name
# The Exec path MUST be the absolute path resolved at install time via
# the [resolution].desktop-tools-search ladder (see §Canonical location).
# The freedesktop spec does NOT expand environment variables in Exec=
# lines, so `--integ` is responsible for picking the host-correct path.
# The /var/mnt/eclipse/... value below is one possible resolution — on
# hosts using $HOME/developer/repos or $HOME/dev/repos the resolved path
# will differ.
Exec=/var/mnt/eclipse/repos/.desktop-tools/keepopen.sh "AppName" "/path/to/repo" "GUI_CMD" "TUI_CMD" "$HOME/.local/state/app/server.log"
Terminal=true   # keepopen needs a terminal for its loud banners and shell fallback
Icon=/path/to/icon.png
Categories=Category;
StartupNotify=true

# Actions for additional functionality — these can bypass keepopen because
# they are invoked from an already-open context menu; no fallback needed.
Actions=stop;status;

[Desktop Action stop]
Name=Stop Server
Exec=/path/to/launcher.sh --stop

[Desktop Action status]
Name=Server Status
Exec=/path/to/launcher.sh --status
----

NOTE: The old "pure-GUI apps use `Terminal=false`" advice is superseded by
`keepopen.sh`. A pure-GUI `Exec=` still flashes a terminal for ~1 second
when the GUI launch succeeds, but in exchange every failure mode becomes
*visible* instead of silent. The tradeoff favours debuggability.

== Fallback Ladder (`keepopen.sh`)

=== Why this exists

Desktop launchers fail in three predictable ways, and users must be able
to see which one happened:

1. The GUI binary is missing, broken, or the URL is unreachable.
2. The TUI/CLI fallback is also broken.
3. The repo itself is missing or unbuildable.

Without a visible fallback ladder, all three failures look identical to
the user: a terminal flashes for half a second, then the desktop is quiet.
`keepopen.sh` turns each failure into a LOUD, labelled banner and, when
all else fails, drops the user into an interactive shell at the repo
root so they can fix whatever is broken.

=== Canonical location

The single source of truth is:

    developer-ecosystem/standards/launcher/keepopen.sh

For desktop files (which need a stable absolute path) a symlink or copy
is deployed inside a `.desktop-tools/` directory whose location varies by
host. Consumers MUST resolve that location via the search ladder declared
in `launcher/launcher-standard.a2ml` `[resolution].desktop-tools-search`:

1. `$HP_DESKTOP_TOOLS`                            — explicit override
2. `$HP_ESTATE_ROOT/.desktop-tools`               — estate-root convention
3. `$XDG_DATA_HOME/hyperpolymath/.desktop-tools`  — XDG default
4. `/var/mnt/eclipse/repos/.desktop-tools`        — legacy eclipse-mount layout
5. `$HOME/developer/repos/.desktop-tools`         — alt: $HOME/developer/repos
6. `$HOME/dev/repos/.desktop-tools`               — alt: $HOME/dev/repos

First existing path wins. A reference shell implementation lives at
`launcher/resolve-desktop-tools.sh` (sourceable; provides
`hp_resolve_desktop_tools` and `hp_resolve_standard`). Downstream launchers
SHOULD use the reference impl rather than re-implementing the ladder.

`launch-scaffolder` copies the same script into its baked-in standards
so regenerated launchers stay in sync. The legacy single hard-coded path
(`/var/mnt/eclipse/repos/.desktop-tools/keepopen.sh`) remains in the
a2ml as `deployed-symlink` for compatibility with pre-resolution
consumers; new code MUST use the ladder.

=== Calling convention

[source,bash]
----
keepopen.sh APP_NAME REPO_DIR "GUI_CMD" "TUI_CMD" [LOG_FILE]
----

* `APP_NAME`: short label; used in banners and the `[keepopen:${APP_NAME}]` prefix.
* `REPO_DIR`: absolute path to the app's repository root — where the final shell fallback lands.
* `GUI_CMD`: shell command for the primary GUI path. Pass `""` to skip this stage.
* `TUI_CMD`: shell command for the TUI fallback. Pass `""` to skip this stage.
* `LOG_FILE` (optional): log file path, shown in banners so the user knows where to look.

Each `*_CMD` is executed via `bash -c`, so pipelines and shell quoting work.
For launchers that daemonise their payload and exit 0 immediately, chain
a `tail -f LOG` after the launcher so the terminal stays open:

[source,bash]
----
"aerie-launcher.sh --auto && tail -f $HOME/.local/state/aerie/server.log"
----

=== The three stages

[cols="1,3,3"]
|===
| Stage | On success | On failure

| **1. GUI**
| `keepopen` exits 0 silently.
| Yellow banner titled `FALLBACK 1/2 — GUI FAILED (exit N)` listing the
  GUI cmd, log file, and a human-readable hint. Proceeds to stage 2.

| **2. TUI**
| `keepopen` exits 0 silently.
| Red banner titled `FALLBACK 2/2 — TUI ALSO FAILED (exit N)` listing
  both commands and the repo path. Proceeds to stage 3.

| **3. Shell at repo root**
| `exec bash -l` inside `REPO_DIR` — user can investigate, run `just --list`, etc.
| If `REPO_DIR` does not exist, a red warning prints and the shell starts in `$PWD`.
|===

The banners are intentionally loud and ugly. Visibility beats aesthetics —
the point is that a broken launcher should *look* broken, not silently fail.

=== Forbidden patterns

* Do **not** use `read -rp 'Press Enter to close...'` as a terminal-keepalive.
  It loses the user's context. Use `keepopen.sh`'s shell fallback instead,
  which drops them into the repo root where they can actually debug.
* Do **not** wrap `Exec=` in `konsole --hold` — the `--hold` terminal has
  no working directory and no login-shell environment, so the user can't
  easily switch to investigating.
* Do **not** skip `keepopen.sh` for "simple" apps. Simple apps break too,
  and the symptom is identical to the flashy ones without the wrapper.

=== Forbidden (legacy) patterns

[source,ini]
----
# BAD: konsole wrapping (pre-keepopen era)
Exec=konsole -e /path/to/launcher.sh --auto
Terminal=true

# BAD: konsole --hold with no cwd / no login shell
Exec=konsole --background-mode --hold --qwindowtitle Title -e wrapper.sh launcher.sh

# BAD: launcher without keepopen — flashes a terminal on failure with no feedback
Exec=launcher.sh --auto
----

== Troubleshooting Guide

=== Common Issues and Solutions

[cols="1,2,2"]
|===
| Symptom | Likely Cause | Solution

| Black terminal window
| Terminal wrapping in desktop file
| Remove konsole wrapping, set Terminal=false

| "Cannot connect to server"
| Server not running or wrong URL
| Check `is_server_running()` function and URL

| Browser doesn't open
| Server not ready when browser launches
| Increase wait_for_server timeout

| Process dies immediately
| Missing `nohup` or process management
| Add `nohup` and proper PID tracking

| Multiple instances running
| No PID file checking
| Implement `is_running()` check before start

| Port already in use
| Previous instance not cleaned up
| Add `kill` before starting new instance
|===

=== Debugging Checklist

1. Check log file: `tail -f "$LOG_FILE"` (resolves to `$XDG_STATE_HOME/<app>/server.log`)
2. Verify process: `ps aux | grep app-name`
3. Test URL manually: `curl -v http://localhost:PORT`
4. Check PID file: `cat "$PID_FILE"` (resolves to `$XDG_RUNTIME_DIR/<app>-server.pid`)
5. Test browser opening: `xdg-open http://localhost:PORT`
6. Verify dependencies: `command -v required-command`
7. Check port availability: `ss -tlnp | grep PORT`

== Migration Guide

=== From Old Konsole-Wrapped Launchers

[source,bash]
----
# OLD (bad)
Exec=konsole --background-mode --hold --qwindowtitle Title -e wrapper.sh launcher.sh
Terminal=true

# NEW (good)
Exec=/path/to/launcher.sh --auto
Terminal=false
----

=== From Simple Pass-Through Scripts

[source,bash]
----
# OLD (minimal)
#!/bin/bash
exec upstream-launcher "$@"

# NEW (robust)
#!/bin/bash
# Standardized launcher with process management
APP_NAME="AppName"
# ... include standard template ...
----

== Compliance Checklist

[ ] Primary `Exec=` goes through `keepopen.sh` with non-empty GUI and/or TUI commands
[ ] `GUI_CMD` for daemon-style launchers chains `&& tail -f LOG` so the terminal stays open on success
[ ] `REPO_DIR` argument points to a real, existing repository root
[ ] `LOG_FILE` argument is passed when the launcher writes one, so banners can cite it
[ ] Remove all pre-keepopen konsole wrapping from desktop files
[ ] `Terminal=true` on the primary Exec (keepopen needs a terminal for banners + shell fallback)
[ ] Implement `nohup` for background processes
[ ] Add PID file tracking and cleanup
[ ] Implement `wait_for_server()` with reasonable timeout
[ ] Add proper error handling and user feedback
[ ] Provide clear success/failure messages
[ ] Log to XDG state dir (`$XDG_STATE_HOME/<app>/server.log`, defaults to `$HOME/.local/state/<app>/server.log`); never use `/tmp/<app>.log`
[ ] Handle browser launch failures gracefully
[ ] Provide manual fallback instructions
[ ] Implement `--start`, `--stop`, `--status` modes
[ ] Test desktop launching without terminal
[ ] Verify browser opens automatically
[ ] Test error conditions (port in use, missing deps)

== Best Practices

=== Logging
- Log to `$XDG_STATE_HOME/<app>/server.log` (defaults to `$HOME/.local/state/<app>/server.log` per XDG spec). Per-user, survives reboot, not world-writable.
- Never log to `/tmp/<app>.log`. Predictable names in a world-writable dir are a symlink-attack target on shared hosts — see §Best Practices > Security.
- Include timestamps for long-running processes
- Rotate logs if they grow large
- Provide log location in error messages

=== User Experience
- Provide immediate feedback when launching
- Show progress during server startup
- Clear success message when ready
- Actionable error messages with next steps
- Never leave user wondering what happened

=== Performance
- Use reasonable timeouts (10-20 seconds typical)
- Don't block indefinitely
- Provide feedback during waiting
- Optimize startup sequence

=== Security
- **PID files MUST go in `$XDG_RUNTIME_DIR`** (Linux) / `$TMPDIR` (macOS), not `/tmp`. `$XDG_RUNTIME_DIR` is mode `0700` and user-scoped per the XDG Base Directory spec; `$TMPDIR` on macOS is `/var/folders/.../T` (per-user). `/tmp` is world-writable: an attacker on a shared host can pre-create `/tmp/<app>-server.pid` containing their own PID, after which the launcher's `is_running()` returns true and `stop_server()` will `kill <attacker-pid>` — DoS or signal-handling abuse vector. The fallback ladder `${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}` exists only as a last resort for hosts that set neither (rare).
- **Log files MUST go in `$XDG_STATE_HOME`** for the same reason — never `/tmp/<app>.log`.
- Use predictable but unique PID file *names within the chosen dir* (not in `/tmp`).
- Clean up PID files on exit
- Don't log sensitive information
- Validate URLs before opening

== Reference Implementations

=== Burble Launcher
[source,bash]
----
include::../../../../.desktop-tools/burble-launcher.sh[]
----

=== IDApTIK Launcher
[source,bash]
----
include::../../../../.desktop-tools/idaptik-launcher.sh[]
----

=== PanLL Launcher
[source,bash]
----
include::../../../../.desktop-tools/panll-launcher.sh[]
----

== See Also

- link:e-grade-launcher-template.sh[E-Grade Launcher Template] - Original specification
- link:comprehensive-launcher-template.sh[Comprehensive Template] - Full reference implementation
- link:https://github.com/metadatastician/consent-aware-web[Consent-Aware Web] ⇗ - Privacy-compliant feedback
- link:../../groove-protocol/[Groove Protocol] - Soft-attach patterns
- link:../../hypatia-rules/[Hypatia Rules] - LLM integration guidelines
