From a9cb0caf3980e0f191b774628ae9a573995d55ba Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Wed, 30 Sep 2026 11:15:19 +0100 Subject: [PATCH 1/3] docs: drop the /tmp pid fallback; document .hypatia-ignore consumers launcher-standard (deed + adoc, lock-step): the pid ladder becomes ${XDG_RUNTIME_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}}/launch-scaffolder/{app-name}/server.pid and logs move under launch-scaffolder/{app-name}/, matching the generator on launch-scaffolder main (#54/#58/#62). The old ${TMPDIR:-/tmp} last resort was the CWE-377 target the standard's own rationale names. No :standard-version bump, deliberately: launch-scaffolder already bakes THIS ladder at 0.4.0, so a bump here would desync the two copies and make scripts/check-launcher-standard-currency.sh fail every 0.4.0 claim. Shipped launchers on the old /tmp ladder are now non-conforming -- that is the point; the A8 re-mint sweep reaches them. QUICKSTART taught PID_FILE="/tmp/myapp-server.pid" in seven places, contradicting the standard in the same directory -- the likely seed of the ~17 shipped /tmp launchers. EXEMPTION-MECHANISMS said .hypatia-ignore is "never read by anything" and told reviewers to reject it. It has two live consumers (the Hypatia scanner: substring fragments; the governance gate: whole-line exact paths). New Layer 2a documents both and the emitted-module trap. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0136eszqrQ53Kj7aBH1D4rXK --- docs/EXEMPTION-MECHANISMS.adoc | 52 ++++++++++++++++++++---- docs/UX-standards/QUICKSTART.adoc | 18 ++++---- docs/UX-standards/launcher-standard.adoc | 23 ++++++----- launcher/launcher-standard_praxis.deed | 16 +++++--- 4 files changed, 77 insertions(+), 32 deletions(-) diff --git a/docs/EXEMPTION-MECHANISMS.adoc b/docs/EXEMPTION-MECHANISMS.adoc index 1449bb18..834a79f7 100644 --- a/docs/EXEMPTION-MECHANISMS.adoc +++ b/docs/EXEMPTION-MECHANISMS.adoc @@ -115,11 +115,43 @@ Suppresses matching findings from the gate, downgrades severity if * One-off PR-scoped suppression. Baseline edits are merged to main; they affect all subsequent PRs. See Layer 3. +=== 2a: `.hypatia-ignore` — scoped scanner suppression + +**File:** `.hypatia-ignore` at the calling repo's root. + +**Consumers — two, and they match differently:** + +1. **The Hypatia scanner** (`hypatia/lib/hypatia/scanner_suppression.ex`). + Each line is `/:`, + `/*:`, or a bare `` (any + rule). The fragment is a **substring** of the repo-relative path. +2. **The governance gate** (`governance-reusable.yml`, rules + `banned_language_file` and `banned_config_file`). It matches the + **whole line** `:` (`grep -qxF`). + A directory fragment or bare-path line quiets the scanner but not the + gate. That is deliberate: the gate is the stricter reader, and + listing each path in full means a newly added banned file still fails. + +`` is the string a finding is **emitted** under, which is not +always the module that defines the rule: content-pattern findings +(`hardcoded_tmp`, `http_in_docs`, …) are emitted as `content_patterns/…` +although defined in `CicdRules`. Copy the id from the alert, never from +the source file; a wrong module string matches nothing, silently. + +**Where it sits in the suppression ladder** (prefer the highest rung +that fits): inline directive at the call site → +`.hypatia-ignore` (a whole file or directory) → +`.hypatia-baseline.json` (acknowledged debt, Layer 2). + +**Don't use for:** hiding a new finding in freshly-authored code, or +wildcarding a directory of banned-language files — list each path, so the +ledger can only shrink. + == Layer 3: Per-PR exemptions (NOT YET IMPLEMENTED) -There is currently no supported per-PR exemption mechanism. PR authors -attempting one (e.g. by inventing a `.hypatia-ignore` file) will find -nothing reads it, and the gate will still fail. **Do not invent new +There is currently no supported per-PR exemption mechanism. `.hypatia-ignore` +(Layer 2a) is committed to the default branch like any other file, so it +is not per-PR either. **Do not invent new file conventions.** If you need per-PR exemption, file an issue against `hyperpolymath/standards` proposing a designed mechanism. @@ -151,11 +183,15 @@ A formal proposal should pick one and document it explicitly. These have been attempted and should be refused at review: -* `.hypatia-ignore` (any format) — never read by anything. Reject; point - the author at `.hypatia-baseline.json`. -* Adding `pragma: ignore-rule` comments in source. Hypatia scans by AST - and ignores comments; suppression has to be data-driven, not - source-comment driven. +* A `.hypatia-ignore` line naming a directory, a wildcard, or a rule + module the finding is not emitted under. It either silences more than + it says (scanner) or matches nothing (gate). Require one full path per + line — see Layer 2a. +* Invented comment pragmas (`pragma: ignore-rule`, `noqa`, …). Only the + documented directives are read: `hypatia: allow / -- reason` + (or `hypatia:ignore `) on the same or preceding line, by the + scanner; and `hypatia:ignore ` in a file's first 8 lines, by the + governance gate. Anything else is ignored silently. * Setting `continue-on-error: true` on the governance gate. This makes CI lie. Use a baseline entry with `severity_override: advisory` instead — keeps the finding visible while unblocking the gate. diff --git a/docs/UX-standards/QUICKSTART.adoc b/docs/UX-standards/QUICKSTART.adoc index 2ead9c15..2b488cfe 100644 --- a/docs/UX-standards/QUICKSTART.adoc +++ b/docs/UX-standards/QUICKSTART.adoc @@ -48,8 +48,9 @@ APP_NAME="MyApp" # Your application name REPO_DIR="/path/to/repo" # Repository directory COMMAND="cargo run" # Command to start your app URL="http://localhost:3000" # Web URL (if applicable) -PID_FILE="/tmp/myapp-server.pid" # Process ID file -LOG_FILE="/tmp/myapp-server.log" # Log file +PID_FILE="${XDG_RUNTIME_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}}/launch-scaffolder/myapp/server.pid" +LOG_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/launch-scaffolder/myapp/server.log" +# Never /tmp: see launcher-standard.adoc §Best Practices > Security. MODE="${1:---auto}" # Default mode ---- @@ -137,7 +138,7 @@ chmod +x /path/to/your/repo/scripts/warmup-*.sh ---- # Launcher starts server and opens browser start_server() { - nohup npm run dev > /tmp/app.log 2>&1 & + nohup npm run dev > "$LOG_FILE" 2>&1 & # LOG_FILE from Step 2 -- never /tmp wait_for_server 10 open_browser } @@ -173,12 +174,13 @@ Terminal=true # CLI tools need terminal [source,bash] ---- # Use nohup for background processes -nohup /path/to/service > /tmp/service.log 2>&1 & -echo $! > /tmp/service.pid +# PID_FILE / LOG_FILE as in Step 2 -- never /tmp (predictable name, shared dir) +nohup /path/to/service > "$LOG_FILE" 2>&1 & +echo $! > "$PID_FILE" # Check if running is_running() { - [ -f /tmp/service.pid ] && kill -0 $(cat /tmp/service.pid) 2>/dev/null + [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null } ---- @@ -196,10 +198,10 @@ sudo chmod 444 ~/.local/share/applications/.desktop ---- === "Server not starting" -1. Check logs: `tail -50 /tmp/.log` +1. Check logs: `tail -50 "${XDG_STATE_HOME:-$HOME/.local/state}/launch-scaffolder//server.log"` 2. Run diagnostics: `-dustfile.sh --diagnose` 3. Try repair: `-dustfile.sh --repair` -4. LLM assistance: `hypatia diagnose --app --log /tmp/.log` +4. LLM assistance: `hypatia diagnose --app --log "${XDG_STATE_HOME:-$HOME/.local/state}/launch-scaffolder//server.log"` === "Browser not opening" 1. Check if server is running: `curl -v http://localhost:PORT` diff --git a/docs/UX-standards/launcher-standard.adoc b/docs/UX-standards/launcher-standard.adoc index 886d80c5..76c547a4 100644 --- a/docs/UX-standards/launcher-standard.adoc +++ b/docs/UX-standards/launcher-standard.adoc @@ -24,12 +24,13 @@ 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" +# XDG_STATE_HOME, NEVER $TMPDIR or /tmp. See §Best Practices > Security. +PID_DIR="${XDG_RUNTIME_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}}/launch-scaffolder/${APP_NAME}" +PID_FILE="${PID_DIR}/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_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/launch-scaffolder/${APP_NAME}" LOG_FILE="${LOG_DIR}/server.log" -mkdir -p "$LOG_DIR" +mkdir -p -m 0700 "$PID_DIR" "$LOG_DIR" MODE="${1:---auto}" # Default mode ---- @@ -395,13 +396,13 @@ system". The `--disinteg` mode is its exact inverse. Everything `--integ` created, plus: -* The PID file (`$XDG_RUNTIME_DIR/-server.pid`, or the resolved equivalent — see §Best Practices > Security) +* The PID file (`$XDG_RUNTIME_DIR/launch-scaffolder//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//` — user preferences survive reinstall -* `$XDG_STATE_HOME//` (defaults to `$HOME/.local/state//`) — logs stay for post-mortem +* `$XDG_STATE_HOME/launch-scaffolder//` (defaults to `$HOME/.local/state/launch-scaffolder//`) — logs stay for post-mortem * The source repository at `REPO_DIR` The removal instructions for those are printed after `--disinteg` so the user @@ -614,10 +615,10 @@ Exec=launcher.sh --auto === Debugging Checklist -1. Check log file: `tail -f "$LOG_FILE"` (resolves to `$XDG_STATE_HOME//server.log`) +1. Check log file: `tail -f "$LOG_FILE"` (resolves to `$XDG_STATE_HOME/launch-scaffolder//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/-server.pid`) +4. Check PID file: `cat "$PID_FILE"` (resolves to `$XDG_RUNTIME_DIR/launch-scaffolder//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` @@ -665,7 +666,7 @@ APP_NAME="AppName" [ ] 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//server.log`, defaults to `$HOME/.local/state//server.log`); never use `/tmp/.log` +[ ] Log to XDG state dir (`$XDG_STATE_HOME/launch-scaffolder//server.log`, defaults to `$HOME/.local/state/launch-scaffolder//server.log`); never use `/tmp/.log` [ ] Handle browser launch failures gracefully [ ] Provide manual fallback instructions [ ] Implement `--start`, `--stop`, `--status` modes @@ -676,7 +677,7 @@ APP_NAME="AppName" == Best Practices === Logging -- Log to `$XDG_STATE_HOME//server.log` (defaults to `$HOME/.local/state//server.log` per XDG spec). Per-user, survives reboot, not world-writable. +- Log to `$XDG_STATE_HOME/launch-scaffolder//server.log` (defaults to `$HOME/.local/state/launch-scaffolder//server.log` per XDG spec). Per-user, survives reboot, not world-writable. - Never log to `/tmp/.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 @@ -696,7 +697,7 @@ APP_NAME="AppName" - 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/-server.pid` containing their own PID, after which the launcher's `is_running()` returns true and `stop_server()` will `kill ` — 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). +- **PID files MUST go in `$XDG_RUNTIME_DIR`**, falling back to `$XDG_STATE_HOME` (default `$HOME/.local/state`) — never `$TMPDIR` or `/tmp`. `$XDG_RUNTIME_DIR` is mode `0700` and user-scoped per the XDG Base Directory spec; the `launch-scaffolder//` subdirectory is created `0700` by the launcher. `/tmp` is world-writable: an attacker on a shared host can pre-create `/tmp/-server.pid` containing their own PID, after which the launcher's `is_running()` returns true and `stop_server()` will `kill ` — DoS or signal-handling abuse vector. The ladder is `${XDG_RUNTIME_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}}/launch-scaffolder//server.pid`. `mktemp` is not an alternative: a PID file must be re-findable by the next invocation, and an unpredictable name cannot be. - **Log files MUST go in `$XDG_STATE_HOME`** for the same reason — never `/tmp/.log`. - Use predictable but unique PID file *names within the chosen dir* (not in `/tmp`). - Clean up PID files on exit diff --git a/launcher/launcher-standard_praxis.deed b/launcher/launcher-standard_praxis.deed index c751c4ec..ba3421e3 100644 --- a/launcher/launcher-standard_praxis.deed +++ b/launcher/launcher-standard_praxis.deed @@ -121,12 +121,18 @@ ;; ------------------------------------------------------------------- runtime (runtime :background nohup - ;; PID files live in the user's runtime-state dir -- wiped on logout, - ;; user-scoped (mode 0700 per XDG), no symlink-attack target. - :pid-file-pattern "${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/{app-name}-server.pid" + ;; PID files live in the user's runtime dir -- wiped on logout, user-scoped + ;; (mode 0700 per XDG), no symlink-attack target. The fallback is + ;; XDG_STATE_HOME, NEVER TMPDIR or /tmp: a world-writable directory with a + ;; name predictable from {app-name} lets another user pre-create the file + ;; and choose which PID `stop` kills (CWE-377). mktemp is not an option + ;; either -- a pid file must be re-findable by the next invocation. + ;; The launch-scaffolder/{app-name} subdir is created 0700 by the launcher. + :pid-file-pattern "${XDG_RUNTIME_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}}/launch-scaffolder/{app-name}/server.pid" ;; Logs go to XDG_STATE_HOME. Per-user, survives reboot, not - ;; world-writable; the {app-name} subdir isolates each launcher's logs. - :log-file-pattern "${XDG_STATE_HOME:-$HOME/.local/state}/{app-name}/server.log" + ;; world-writable; the launch-scaffolder/{app-name} subdir isolates each + ;; launcher's logs and keeps them beside its pid fallback. + :log-file-pattern "${XDG_STATE_HOME:-$HOME/.local/state}/launch-scaffolder/{app-name}/server.log" ;; URL-readiness polling after start. All three timing values are env-var ;; overridable so operators can tune without re-minting the launcher. :wait-for-url-timeout-seconds 15 From e0c96f76fe7221a41029bfa52aba4fc9e0273a05 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Wed, 30 Sep 2026 11:22:59 +0100 Subject: [PATCH 2/3] docs(launcher): move copyable templates off /tmp pid/log QUICKSTART Step 1 copies comprehensive-launcher-template.sh and Step 2 edits its CONFIGURATION block, but the template still carried /tmp/-server.{pid,log} and created no directory. A reader following the doc verbatim would either keep the CWE-377 path or, after switching to the XDG ladder, fail on the first `nohup ... > "$LOG_FILE"`. Both templates (the dustfile reads the launcher's pid/log, so they must agree) now use the standard's ladder and `mkdir -p -m 0700` the leaf directories immediately before the first write. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0136eszqrQ53Kj7aBH1D4rXK --- docs/UX-standards/comprehensive-launcher-template.sh | 6 ++++-- docs/UX-standards/dustfile-template.sh | 8 +++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/UX-standards/comprehensive-launcher-template.sh b/docs/UX-standards/comprehensive-launcher-template.sh index c07dbd85..c155299b 100755 --- a/docs/UX-standards/comprehensive-launcher-template.sh +++ b/docs/UX-standards/comprehensive-launcher-template.sh @@ -85,8 +85,9 @@ REPO_DIR="/path/to/repo" # Repository directory COMMAND="command to run" # Command to execute URL="http://localhost:PORT" # URL if web app (empty if not) ICON_SOURCE="$REPO_DIR/assets/icon-256.png" # Source icon for --integ (optional) -PID_FILE="/tmp/${APP_NAME,,}-server.pid" # PID file -LOG_FILE="/tmp/${APP_NAME,,}-server.log" # Log file +# Never /tmp for pid/log (CWE-377: predictable shared path) — see launcher-standard.adoc +PID_FILE="${XDG_RUNTIME_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}}/launch-scaffolder/${APP_NAME,,}/server.pid" +LOG_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/launch-scaffolder/${APP_NAME,,}/server.log" MODE="${1:---auto}" # Default mode FORCE="false" # --force flag (used by --integ) [[ "${2:-}" == "--force" ]] && FORCE="true" @@ -178,6 +179,7 @@ start_server() { # Start in background with nohup to prevent process from being killed cd "$REPO_DIR" + mkdir -p -m 0700 "$(dirname "$PID_FILE")" "$(dirname "$LOG_FILE")" nohup $COMMAND >"$LOG_FILE" 2>&1 & echo $! > "$PID_FILE" diff --git a/docs/UX-standards/dustfile-template.sh b/docs/UX-standards/dustfile-template.sh index 48a81fbd..c49bd03c 100644 --- a/docs/UX-standards/dustfile-template.sh +++ b/docs/UX-standards/dustfile-template.sh @@ -15,8 +15,9 @@ set -euo pipefail # ============================================================================ APP_NAME="TemplateApp" APP_DIR="/path/to/app" -PID_FILE="/tmp/${APP_NAME,,}-server.pid" -LOG_FILE="/tmp/${APP_NAME,,}-server.log" +# Never /tmp for pid/log (CWE-377: predictable shared path) — see launcher-standard.adoc +PID_FILE="${XDG_RUNTIME_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}}/launch-scaffolder/${APP_NAME,,}/server.pid" +LOG_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/launch-scaffolder/${APP_NAME,,}/server.log" PORT=4000 COMMAND="command to run" @@ -139,6 +140,7 @@ repair_server_not_starting() { # Try to start with more verbose logging cd "$APP_DIR" + mkdir -p -m 0700 "$(dirname "$PID_FILE")" "$(dirname "$LOG_FILE")" nohup $COMMAND --verbose >"$LOG_FILE" 2>&1 & echo $! > "$PID_FILE" @@ -261,4 +263,4 @@ esac if command -v feedback-o-tron >/dev/null 2>&1; then feedback-o-tron --event "dustfile:used" \ --app "$APP_NAME" --mode "$MODE" 2>/dev/null || true -fi \ No newline at end of file +fi From a530b713bb80572b705ad69b1dbe40b55dc844f8 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Wed, 30 Sep 2026 11:23:25 +0100 Subject: [PATCH 3/3] docs(launcher): drop remaining /tmp teachings in UX docs README's checklist still recommended "/tmp/app-name.log", the e-grade template logged to /tmp, and soft-attach.sh's usage example passed a /tmp log. Only launcher-standard.adoc's explicit prohibitions of /tmp remain after this. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0136eszqrQ53Kj7aBH1D4rXK --- docs/UX-standards/README.adoc | 2 +- docs/UX-standards/e-grade-launcher-template.sh | 6 ++++-- launcher/soft-attach.sh | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/UX-standards/README.adoc b/docs/UX-standards/README.adoc index 3786874d..0bb04caa 100644 --- a/docs/UX-standards/README.adoc +++ b/docs/UX-standards/README.adoc @@ -108,7 +108,7 @@ Launchers must provide clear, actionable feedback: [ ] Implement `run_diagnostics()` function [ ] Add proper error handling for browser launching [ ] Provide clear user feedback at each step -[ ] Log to predictable locations (`/tmp/app-name.log`) +[ ] Log to the XDG state dir (`$XDG_STATE_HOME/launch-scaffolder//server.log`); never `/tmp` — see launcher-standard.adoc §Security [ ] Handle missing dependencies gracefully [ ] Provide manual fallback instructions diff --git a/docs/UX-standards/e-grade-launcher-template.sh b/docs/UX-standards/e-grade-launcher-template.sh index 91364531..20c8b781 100755 --- a/docs/UX-standards/e-grade-launcher-template.sh +++ b/docs/UX-standards/e-grade-launcher-template.sh @@ -12,7 +12,8 @@ APP_NAME="TemplateApp" APP_DIR="/path/to/app" APP_PORT=4000 APP_URL="http://localhost:$APP_PORT" -LOG_FILE="/tmp/${APP_NAME,,}-launcher.log" +# Never /tmp (CWE-377: predictable shared path) — see launcher-standard.adoc +LOG_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/launch-scaffolder/${APP_NAME,,}/launcher.log" # ============================================================================ # CORE FUNCTIONS - Standard E-grade functionality @@ -36,6 +37,7 @@ start_server() { # Use nohup for reliable background process management cd "$APP_DIR" + mkdir -p -m 0700 "$(dirname "$LOG_FILE")" nohup command_to_start_server >"$LOG_FILE" 2>&1 & # Wait for server to be ready @@ -151,4 +153,4 @@ case "$MODE" in start_server open_browser ;; -esac \ No newline at end of file +esac diff --git a/launcher/soft-attach.sh b/launcher/soft-attach.sh index 949a253b..7c634ac1 100755 --- a/launcher/soft-attach.sh +++ b/launcher/soft-attach.sh @@ -64,7 +64,7 @@ hp_soft_attach_event() { } # CLI mode (not sourced): provide a thin wrapper for ad-hoc invocation. -# ./soft-attach.sh run "hypatia diagnose --app foo --log /tmp/foo.log" +# ./soft-attach.sh run "hypatia diagnose --app foo --log ~/.local/state/launch-scaffolder/foo/server.log" # ./soft-attach.sh event feedback-o-tron launcher:start_failed # ./soft-attach.sh present hypatia if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then