Skip to content

Latest commit

 

History

History
1317 lines (1170 loc) · 78 KB

File metadata and controls

1317 lines (1170 loc) · 78 KB

Changelog

All notable changes to this project will be documented in this file.

v0.4.0

The first-impression release. Foreground prox up now opens the TUI by default and exits when the whole stack is dead (plan 026, plan 028); the daemon grew a warning channel that puts mkcert's own untrusted-CA note and unresolvable-hostname warnings in front of the user (plan 028, #97, #98) — with CA detection kept off the critical path: the trust probe plan 028 introduced was removed again after measurement, in favor of generation-time detection only (plan 029). Underneath: the integration suite was rebuilt for isolation and truthfulness (plan 027), and the docs moved to Zensical with the shared StrideLabs theme (plan 025).

Changed

  • A foreground prox up whose whole process stack has died now ends the session instead of sitting there supervising nothing (plan 028, #96). A typo in cmd: used to leave the terminal open indefinitely, showing nothing, until the user thought to press Ctrl-C — the trigger channel prox up waited on is closed by a signal, POST /shutdown, or a TUI quit, and by nothing a process does. It now notices for itself: once every process is dead and at least one of them ended in crashed or blocked (a partial crash with something still serving does not count, and an all-completed task config or a deliberate API-driven stop does not either), the session tears down and prints the same Crashed:/ Blocked: summary prox up -d, start and restart already print.

    This is a breaking exit-code change: a plain or piped foreground prox up previously always exited 0. It now exits non-zero whenever its whole stack ends up dead, printing the crash summary and a one-line hint before returning. A script that treated any foreground prox up return as "the session ended, nothing more to check" now needs to look at the exit code the way it already should for prox up -d.

    The interactive TUI does the opposite on purpose. A TUI session does not auto-exit — the user is right there reading, and pulling the screen away would take the crash output with it. Instead it shows a persistent full-width banner between the process panel and the viewport border: All processes have stopped — 2 crashed. Nothing is running. Press q to quit. Colour is emphasis only; the sentence survives ANSI stripping. This also applies in prox attach, since the banner is computed from the same process snapshots the TUI already streams.

    Not in the -d child. --detach short-circuits straight to plain mode internally, so without an explicit guard this watcher would have run inside the detached daemon too — and killed it moments after prox up -d told the user "The daemon is still running; stop it with prox down", taking the API and the crash logs with it. The daemon staying up on a crashed stack is the existing, deliberate contract; only the daemon child is exempted.

  • prox gained a channel for warning you about things it previously either swallowed or never checked (plan 028). Two long-deferred warnings (#97, #98) both needed a way to get from wherever prox actually observes a problem to wherever the person who typed the command is looking, across all three run modes — including the shared proxy daemon, whose own stdout/stderr are /dev/null, and a prox up -d child, whose stdout/stderr are .prox/prox.log. That channel now exists (domain.Warning{code, message, hint}), and it is additive on the wire in both directions: the daemon's register response and GET /status each gained a warnings field.

    Warnings print as Warning: <message> (with an indented <hint> line underneath, when there is one) in a plain prox up, in the TUI's pinned startup preamble (and therefore the system log and prox logs), and in prox status. A warning never changes any exit code — including prox status's, which already has its own exit contract for crashed/ blocked processes and a down proxy — because turning a script red over an untrusted CA it never asked about would be its own kind of false alarm.

    API change (additive): GET /status gains warnings (an array of {code, message, hint}, omitted when empty) and warnings_sealed (a bool, always present). The latter matters because a warning can still be in flight when a prox up -d parent takes its one status snapshot after the readiness + settle wait; the parent now polls warnings_sealed (bounded at 2.5s) rather than trusting a single race-prone fetch. See GET /status.

  • prox now surfaces mkcert's own untrusted-CA warning (plan 028, #97). Running brew install mkcert (or equivalent) without also running mkcert -install used to leave prox reporting every process healthy while every HTTPS request through the proxy failed in the browser — mkcert itself explains exactly why, but in shared mode that explanation used to go straight to the daemon's /dev/null. prox now captures mkcert's output and carries its line verbatim through the warning channel above, followed by the hint run 'mkcert -install' and restart prox. It still implements no trust-store detection of its own — that stays entirely mkcert's job, correctly, per OS/browser — and it withdraws the warning automatically once mkcert reports the CA as trusted again. Detection happens at generation time only: mkcert speaks when it generates a certificate, and that's the note prox carries. A machine whose certs already exist but whose CA later broke (OS reinstall, keychain reset) is not detected until something triggers a real generation — an accepted trade-off, since the probe that used to cover that gap cost ~217ms and two subprocesses inside the daemon's registration critical path (under lifecycleMu, delaying every other project's registration). The hint (run 'mkcert -install' and restart prox) still names the fix.

  • prox now warns when a registered hostname does not resolve (plan 028, #98). prox up prints Registered domains: app.sec.test, which looks fine right up until it's pasted into a browser and comes back NXDOMAIN, because a .test domain needs local DNS setup that a public wildcard like *.lvh.me does not. prox now resolves each registered hostname once at startup and warns, naming the actual configured domain and linking the local DNS guide, when one fails. The check only fires on a positive NXDOMAIN — a timeout, offline resolver, or any other lookup error is treated as "cannot tell", never as "unresolvable", so a developer on a plane or behind a network-sandboxed CI runner is not told their perfectly good setup is broken. Runs in both shared and standalone proxy modes and never delays startup past its own short budget.

  • The TUI process panel states, and the requests pane's empty state, no longer rely on colour or silence to say what happened (plan 028, #92). styles.Crashed/styles.Blocked and styles.Stopped/styles.Completed have always shared a style, so those pairs were only ever colour-distinguishable — nothing at all once ANSI is stripped (piped output, TERM=dumb, a screenshot) or to a colour-blind reader. Every process state but running now appends a word to the name in the panel: web (crashed), migrate (done), api (blocked on: db), worker (waiting on: redis), db (stopped), web (starting), web (stopping). running costs nothing extra, so the steady-state panel is unchanged.

    The requests pane's empty state used to read "No requests yet — traffic through the proxy appears here" unconditionally, telling a project with no proxy: block to wait for traffic that could never arrive. It now says why the pane is empty: "No proxy running — enable a proxy: block in prox.yaml to capture requests" when there is no proxy running this session, or "No requests yet — capture is off, so rows will show metadata only" when there is a proxy but proxy.capture.enabled: false. The request detail view gained the matching explanation for a record with no headers or body: "Capture is disabled (proxy.capture.enabled: false) — no headers or bodies were recorded". Also removed: the detail footer's [FOLLOW] N/M lines tag, which described a scrolling state the detail view never actually has.

  • prox up -d, prox start and prox restart now report the resulting state, not just that the request was accepted (plan 027, #94). They exit non-zero when a process is crashed or blocked within 500ms of starting, printing the same lines prox status does, so the two commands can no longer disagree. Previously prox up -d exited 0 while prox status reported crashed a second later.

    This changes exit codes. A non-zero prox up -d now means "the daemon is up, its processes are not" — it names the offending process and points at prox down, since the daemon is still running and still needs stopping. Scripts that treated a zero exit as "everything is running" were previously being misled; scripts that treat a non-zero exit as "nothing started" now need to distinguish the two cases.

    The guarantee is deliberately modest: no terminal failure observed within 500ms, not verified state. A process that dies at 501ms still exits 0. Only crashed and blocked count — starting, stopping and waiting are transient, and completed is task success. start and restart take ~500ms longer on success, because proving the absence of a failure means waiting the window out.

  • A process with no healthcheck configured now reports health - instead of unknown (plan 027, #100). unknown is reserved for a configured check that has not reported yet, so the two are finally distinguishable — including for a crashed process, which keeps unknown when it has a healthcheck.

    API change (additive): health on GET /processes and GET /processes/{name} gains the value "none", which prox status renders as -. prox status --json passes the raw value through, so a script matching "health":"unknown" to find un-healthchecked processes must now match "none". healthcheck.enabled also stops being hardcoded true and reports whether the check loop is actually running.

  • prox logs <unknown-process> now fails instead of printing nothing (plan 027, #95). It exits 1 and names the process, suggesting the closest match. Previously the name was silently used as a filter that matched no records, so a typo produced empty output and exit 0. A valid name that has logged nothing still prints nothing and exits 0 — silence is only an error when the name is wrong. Applies to the positional argument, --process, comma-separated lists, and --follow.

Fixed

  • A crashed process no longer keeps its health checker running against a dead pid (plan 028, #107). A process that crashed on its own used to keep re-executing its healthcheck: command — with whatever side effects that command has — until some later Stop happened to arrive, and healthcheck.enabled on GET /processes/{name} reported true the whole time (plan 027 made that field truthful, which is what made the bug visible). The health checker now stops the instant the process exits on its own, not only when it is explicitly stopped: a crashed (or otherwise terminally failed) process's healthcheck.enabled reads false immediately, and its check command stops executing right then rather than continuing indefinitely until some later stop request happened to arrive.

  • Errors no longer claim prox is down when it just answered (plan 027, #95). "Is prox running? Try 'prox up' first." was appended to every client error unconditionally, so prox stop <not-running> printed the daemon's own reply followed by advice to start the daemon that had produced it. The hint now appears only when the daemon is positively unreachable. PROCESS_NOT_FOUND also names the process and either suggests the closest match or lists the valid ones, sourced from the running daemon rather than the local config file.

  • prox up -d failure diagnostics no longer stack up across runs (plan 027, #99). .prox/prox.log is append-only and was tailed by a fixed line count, so each failed attempt buried the current error further under previous ones — worst exactly while iterating on a broken prox.yaml. Each daemon run now writes a marker identifying itself, and the tail is scoped to the current run. The log is still never truncated; history stays on disk. Reporting is bounded at both ends — only the end of the log is read, and a run that logged more than 200 lines is printed capped, and says so — so a huge log costs a huge print no more than it costs a huge read. A run marker that was cut short mid-write falls back to the unscoped tail rather than presenting the previous run's output as the current one's.

  • A foreground prox up now opens the interactive TUI (plan 026). Previously it streamed plain logs and the TUI was reachable only behind an explicit --tui, or via prox up -d + prox attach; the best view was the one nobody saw. This is a breaking UX change to foreground prox up.

    The TUI is preferred, never required: prox up falls back to plain log streaming, silently and without an error, whenever the terminal cannot host a full-screen UI — stdin or stdout is not a terminal (a pipe, a redirect, CI, an agent harness), TERM is unset or dumb, or the process is not in the terminal's foreground process group (a backgrounded prox up &, which would otherwise stop on SIGTTIN the moment the TUI read the keyboard). Scripted and CI invocations therefore behave exactly as before. If the TUI itself fails to start, the session says so and degrades to plain streaming rather than failing a command that never asked for a TUI. That fallback replays what the log buffer still holds — the most recent 1000 entries, which on a quiet startup is everything and on a noisy one is not; anything older has already been evicted from the ring and is gone.

    Escape hatches: prox up --no-tui for one run, or PROX_TUI=0 (also false, no, off) for a whole shell. --tui is retained as the explicit "require it" form — it now fails rather than falls back when the terminal cannot host a TUI, naming which condition failed — mirroring how --capture was kept after capture became default-on. A flag that asserts something — --tui, --tui=false, --no-tui — beats PROX_TUI, which is then not consulted at all; --no-tui=false asserts nothing (it is the flag's own default spelled out loud) and falls through to PROX_TUI and the default. --detach short-circuits the whole decision, so prox up -d is unaffected by any of it.

    q in the prox up TUI stops your processes, exactly as Ctrl-C does: the foreground prox up is their supervisor, so nothing outlives it. This is unchanged behavior, but it is now the default view rather than an opt-in one, so the owner-mode footer reads q stop (attach still reads q quit) and the help modal names prox up -d + prox attach as the way to keep processes running past quit.

  • A -d child's stdlib log diagnostics now reach .prox/prox.log (plan 026). daemon.SetupLogging reassigns the os.Stderr variable, but the stdlib logger captured the original at package init and the daemon child's real fd 2 is /dev/null — so every log.Printf in a detached session was written to nowhere. Those lines (shared-proxy connection loss and recovery, API/SSE errors, net/http TLS handshake failures and handler panics) now land in the daemon log file, which is a real increase in that file's volume.

  • Docs now use the shared StrideLabs theme (stridelabs-docs-theme v0.2.2), so this site and the other StrideLabs docs sites share one look without copying CSS between repos. The header pairs the StrideLabs owl with prox's own console icon; headings are Fraunces, body Inter, code JetBrains Mono. Page URLs, structure and heading anchors are unchanged.

    zensical.toml shrinks accordingly — the palette, fonts and feature toggles now come from the theme and were deleted here. The theme installs as a plain git dependency from a public repo, so there is no registry auth and forks can still build the docs.

    Fonts are self-hosted by the theme: the site no longer makes any request to fonts.googleapis.com or fonts.gstatic.com, so the Google Fonts CDN never sees a visitor IP. (This covers fonts specifically — the repo integration still calls api.github.com for star and fork counts, since repo_url is set.)

  • Documentation now builds with Zensical instead of MkDocs + Material for MkDocs (plan 025). Material for MkDocs entered maintenance mode in November 2025; Zensical is the successor from the same team. mkdocs.yml is replaced by a native zensical.toml, and the docs dependency group is a single zensical pin — mkdocs, mkdocs-material and pymdown-extensions are gone. Published URLs, page structure and heading anchors are unchanged, so existing links (including deep links) still resolve. The site adopts Zensical's "modern" theme variant, so it looks different; variant = "classic" restores the Material appearance.

  • Both docs workflows now build --strict. Previously only the PR build was strict and validation.links was warn-only, so a broken link could reach the published site. Zensical validates links and anchors natively and aborts under --strict. docs.yml also gained uv.lock in its paths filter, which it was missing, so a lockfile-only docs bump now redeploys.

  • pymdown-extensions is no longer a direct dependency; it comes in transitively via zensical. The config still declares eight pymdownx.* extensions, so if a future Zensical release drops that dependency the docs build will need a direct pin again.

  • Fixed the docs header lockup overlapping the site title in the mobile drawer. Behind the hamburger menu on a phone, the divider and project icon painted on top of the site name. Zensical sizes that drawer slot for a single glyph, so the wider lockup overflowed it. Fixed upstream in stridelabs-docs-theme v0.2.2; this bumps the pin. Desktop was never affected.

v0.3.0

The TUI release. prox up --tui and prox attach are now one redesigned TUI (plans 021–024, PR #102): a menu bar with View, Filter, and Theme dropdowns; six truecolor theme presets plus user TOML themes; a per-view filter query language with regex and status-range terms; styled log and request rendering with level detection across common dev log formats; grab-for-agent copy keys (request ID, curl, exact JSON); full mouse routing with pointer-shape feedback; a centered help modal; and persistent settings. Requests gained scroll-back paging to the full 5000 records, reconnect rebasing, and toggleable columns including request IDs. Beyond the TUI: prox commands now refuse to control a different project's instance (plan 020), and a crashing process's error is no longer swallowed at startup. As always the shared daemon requires an exact version match: restart the daemon and every project against the new binary after upgrading.

Added

  • TUI redesign (plan 021): menu bar with View, Filter, and Theme dropdowns; truecolor themes (six presets plus user TOML in ~/.prox/tui/themes/); filter query language on the s bar with a matching Filter menu; view toggles for process panel, timestamps, and soft-wrap; styled log/request/detail rendering; grab-for-agent copy keys (y / c / Y); and full mouse routing (wheel, row/chip clicks, double-click to open request detail). Settings persist in ~/.prox/tui/config.toml.
  • Dropdown height clamping with scroll indicators (plan 022). Long menus (for example the Theme list on short terminals) window to the available frame height with “… N more …” indicators; the mouse wheel scrolls an open dropdown and is consumed so the viewport underneath does not move.
  • Free-motion menu hover (plan 022). Hovering the menu bar slides an open menu across sibling cells; hovering dropdown rows moves the highlight (requires all-motion mouse reporting).
  • Broader log-level detection for level: filters and level tints. Beyond JSON level/lvl and logfmt level=, the classifier now reads JSON severity keys (Cloud Logging / stridelabs-python deployed format), pino/bunyan numeric levels ({"level":30}), critical as error, and a standalone UPPERCASE level token early in the line — covering python logging's dev format, tracing's text layer, pino-pretty, and uvicorn access lines, ANSI colors included. Previously these lines had no detected level, so level:info filtered them all out.
  • Requests scroll-back in the TUI (plan 018). Scrolling to the top of the requests list now loads the next older page of requests from the ring automatically (1000 at a time, up to the full 5000 the server retains), so you can walk back through traffic that the initial sync did not carry. The status bar reports the state: loading older… while a page is in flight, start of history once the oldest retained request is on screen, and ⚠ older: … if a page fetch fails — pressing up again retries. One page is ever in flight at a time, and paging is unaffected by an active filter or search: the filters apply to what you see, not to what is fetched.
  • A reconnect now rebases the requests list on the server's current window (plan 018). Requests carry no sequence numbers, so a client cannot prove that history it scrolled back to still joins up with a snapshot taken after a connection gap. Rather than showing a list with an invisible hole in it, a completed re-sync drops everything older than the snapshot it just fetched (and clears the list entirely when the server's ring is empty — a restarted or replaced daemon), then lets you re-page. This also fixes a wrong "stale?" marker: an in-flight request sitting deeper in the ring than the sync fetch reaches is no longer mistaken for one the server has lost.
  • The processes panel now shows a health dot (plan 018). A process with a healthcheck configured gets a small glyph after its name — a green while healthy, a red while unhealthy — styled independently of the process's own state color so it can't be swallowed or recolored. A process with no healthcheck (or one still reporting unknown) renders exactly as before.
  • Requests column toggles (plan 023). The View menu gains a Columns section in Requests view: Time, Host, Method, Status, Duration, and ID are individually toggleable (URL always on). Defaults are all on; choices persist under [requests] in ~/.prox/tui/config.toml. / search matches only visible columns; copy keys are unaffected.
  • OSC 22 pointer shapes (plan 023). On supporting terminals (kitty, WezTerm, Alacritty, Ghostty, …), the mouse pointer switches to a hand over menu-bar cells, activatable dropdown rows, and process chips.
  • re: regex filters for logs (plan 023). The logs s bar accepts re:<pattern> and -re:<pattern> terms (RE2, ≤256 bytes, compiled once at parse; multiple positive re: terms AND together).
  • Status-range filters for requests (plan 023). The requests s bar now accepts status:>=N, status:<=N, and inclusive status:N-M ranges (100–599) in addition to exact codes and 4xx/5xx classes.

Changed

  • ? help is now a centered modal over the live TUI (plan 022). Logs and requests keep updating behind it; the wheel scrolls the modal when content is taller than the box; Esc, ?, q, Enter, or a click outside the box closes it (replacing the old full-screen help view).
  • TUI unification (plan 018): prox up --tui and prox attach are now one TUI. The owner's TUI is the same API-client TUI attach has always run, so every feature, key binding, and fix lands in both at once.
  • prox up --tui runs the same API-client TUI as prox attach (plan 018). It is now a client of the API server prox up starts in its own process rather than a second, local-only implementation reading the supervisor and log manager directly. Everything attach mode gained — the reconnecting log/request/process streams, the stream-health status line, request detail and its live refresh, the cursor and filter behavior — is now what --tui shows, and the two can no longer drift. Quit semantics are unchanged: q still stops the supervisor and takes the processes down with it (attach mode still leaves the daemon running), and POST /shutdown plus SIGINT/SIGTERM still quit the TUI and run the normal shutdown sequence and exit contract. The status line carries no "Connected via API" text under --tui: it is the owner's own TUI, not a remote attach.
  • Proxy request retention raised to 5000 records (was 1000; plan 018). Every request ring — the standalone proxy, the shared daemon's per-project rings, and each project's local forwarded ring — now keeps the newest 5000 requests, so you can scroll much further back through traffic before records age out. The ?limit= ceiling on GET /api/v1/proxy/requests (and the daemon's equivalent) moves to 5000 with it: the ring size and the per-call limit are the same number by definition, because the shared-daemon forwarder has to be able to backfill a full ring in one fetch after a reconnect.
  • Captured bodies are now retained for the newest 1000 requests, not all 5000 (plan 018). Deeper retention would otherwise multiply memory by five, since a captured body can be up to 64KB inline. On a ring that owns capture (standalone, and the shared daemon's per-project ring), a request falling outside the newest-1000 window has its request/response bodies dropped — inline bytes released, spilled body files unlinked — while the record itself and its captured headers are kept, along with body metadata (size, content type, truncation). Opening such a request shows its full metadata and headers with the body reported as evicted, the same signal an over-budget spilled body already used. A slow in-flight request that ages out of the window before it completes keeps its completion status and headers, but its body is not stored. Each shared-mode project's local forwarded ring enforces this same 1000-record bound independently, ordered by request timestamp rather than ring position — the daemon's own eviction is silent, so without its own bound this ring would keep every live-forwarded body forever between registrations. It only ever drops inline bytes, since it owns no spilled files to unlink; a spilled body's file eviction is still the daemon's to report.
  • The TUI now syncs the newest 1000 requests rather than the whole ring (plan 018), so startup and reconnect cost stay flat as retention grows. Older records are fetched on demand as you scroll back (see scroll-back above); the TUI itself will hold up to the full 5000 once you have paged through them.
  • --tui on a non-interactive terminal is now an error (plan 018). prox up --tui with stdin or stdout redirected (a pipe, a CI runner, an agent harness) exits non-zero with --tui requires an interactive terminal before starting the supervisor, proxy, or API server, instead of starting everything and then drawing a full-screen TUI nobody can see or quit. Non-interactive callers want plain prox up (streams logs to stdout) or prox up -d.
  • Merged footer row (plan 023). Status text, view/follow/count badges, and key hints now share a single footer band with typed messages, precedence, and narrow-width hint degradation.
  • Bordered viewport panel, dropdowns, and help modal (plan 023). The main content viewport has a rounded titled border; dropdown menus use rounded borders with a right-aligned hint column; the help modal uses a focused border colour and a title spliced into the top edge.
  • Full-width selection band on FullFill themes (plan 023). The active cursor row (including every wrapped display row of a log entry) paints a full-viewport-width band; / search hits inside the band keep their highlight colour.
  • Logfmt level detection tightened (plan 023). level= / lvl= tokens must appear at line start or after whitespace so embedded tokens like xlevel= no longer match.
  • Settings persistence hardened (plan 023). ~/.prox/tui/config.toml saves use a flock-serialized read→merge→write→rename transaction with directory fsync so concurrent writers cannot clobber each other.

Fixed

  • TUI menu bar, dropdown, and process-chip mouse clicks (plan 022). Hit rectangles were recorded during render but discarded because View uses a value receiver — the live model never saw them, so menu and chip clicks were dead in the real app. Clicks now use a shared hit registry that survives the render copy.

  • A prox command can no longer control a different project's prox (plan 020). Commands discovered a running prox from .prox/prox.state, or failing that from api.port in the local prox.yaml, and then trusted whatever was listening there. With two projects on the same api.port — or one stale state file whose port had since been taken — prox status reported another project's processes as yours and prox down stopped another project outright. Every client command now verifies that the prox answering owns this project before acting, and refuses by name when it does not:

    Error: prox is not running for this project.
    A prox for /Users/you/projects/other is listening on 127.0.0.1:5552.
    Run commands from that directory, or target it deliberately with
      --addr http://127.0.0.1:5552
    

    Identity is the project directory, compared by device+inode, so symlinked roots, /tmp vs /private/tmp, a custom -c, and prox.yml vs prox.yaml all still resolve to "yes, that's yours". Git worktrees of one repo are correctly treated as separate projects — they are separate directories running potentially different code — with no VCS knowledge in prox. --addr bypasses the check entirely for deliberate cross-directory work, and now genuinely does so for prox attach, which previously failed before it ever consulted the flag.

  • The examples no longer teach a pinned api.port (plan 020). api.port is dynamic by default and that is the safe mode, but four shipped examples presented a pinned api: {port: 5555} block as the baseline shape of a prox.yaml — so copied configs, including agent-authored ones, collided on one port. This is the root cause of the cross-project bug above. This repo's own prox.yaml no longer pins the port either, so its dev API port is now dynamic; use prox status to see the actual port.

  • Foreground prox up no longer swallows the reason a process crashed (plan 020). Logs were subscribed after the supervisor had already started the processes, so a process that died instantly — a typo in cmd:, a missing binary — could emit its error before anything was listening, leaving the banner and then silence. The subscription is now established before any process starts. (Foreground prox up still does not exit on its own when every process is dead; that is tracked separately.)

  • The version-skew message's instructions now work when followed (plan 020). On a shared-daemon version mismatch, prox told you to run prox proxy stop --force and then prox up in each project — but prox up fails there ("already running"), bare prox restart requires a process name, and the still-running old projects resurrected an old daemon within ~15s, so the retry hit the same error. The instructions are now phased — stop every project, confirm the proxy has exited, then restart them — which also removes the need for --force and the resurrection race entirely.

  • ctrl+c now quits from every mode (plan 023), including the help modal and filter/search text entry, where it was previously swallowed.

  • Help modal width on narrow terminals (plan 023). Side padding and then the border degrade before content so the box no longer overflows very small frames.

  • Filter query serialization (plan 023). Bare terms round-trip verbatim (never re-quoted); unknown level: tokens are dropped on serialize.

  • Frame-fill on light themes (plan 023). FullFill presets no longer leave default-background holes in chrome rows; footer error flashes use readable styling on light footer backgrounds.

  • JSON log rendering performance (plan 023). JSON object lines are parsed once at ingest (level detection and path=value summary share one unmarshal); wrap-on display keeps summary plus compact raw consistently.

  • Plain log/request text now carries the theme background (plan 024). Level-less lines rendered on the terminal-default background — 62% of the frame in the light theme. Every TUI-formatted line now renders through the base (or level-tinted) style, and the frame-fill scanner no longer blanket-exempts the log region: only cells inside a line's own child ANSI spans stay exempt.

  • Requests header/data column alignment (plan 024). The header rendered Duration at 8 columns while data cells were 7 (ID and URL data sat one column left of their labels), and the Status header read Sta. Header and rows now share one column-spec table; Status is a full label with data padded to match.

  • Footer band flush and fill (plan 024). The key-hint group is padded flush to the right edge instead of floating mid-band, and every footer segment — join spaces, stream-health n/a, paging notices, truncation ellipses, and the filter text input itself — now renders on the footer background, eliminating default-background holes (including the 43-column hole while editing a filter).

  • Border and glyph details (plan 024). Panel and help border titles render ─ Label ─ (they rendered ─ Logs────); a clamped dropdown's bottom border now stops one row above the panel's bottom border instead of landing on it; a hovered closed menu cell is now visually distinct from an open one; and Theme dropdown rows no longer repeat the t hint.

  • Wrapped-row hanging indent (plan 024). Wrapped log continuation rows keep the timestamp/process gutter instead of dedenting to column 1, and wrapped help descriptions hang-indent to the description column.

  • Selective log-filter allocation regression (plan 024). The plan-023 filteredEntries preallocation reserved full capacity even with an active filter (50152 B/op); full preallocation now applies only when no filter is active, restoring selective filters to ~1000 B/op while keeping the no-filter fast path.

v0.2.4

Hardening release. Capture is now visible-by-default (redaction removed — the ngrok/DevTools model; #80, plan 015), prox.yaml parsing is fully strict (a typo'd or unknown key anywhere is a precise load-time error; #83, plan 016), and the two known integration-test flakes plus the missing PR-time docs build are fixed (#82, plan 014). Both behavior changes are Breaking — read those entries before upgrading. As always the shared daemon requires an exact version match: restart the daemon and every project against the new binary after upgrading.

Breaking

  • Capture-time redaction is removed entirely (#80, PR #85, plan 015). v0.2.3's on-by-default redaction replaced Authorization/Cookie-class header values and sensitive query params with [REDACTED]/REDACTED in capture records. It was a half-measure — bodies were always stored verbatim, so a token in a JSON payload landed on disk regardless — and it fought the point of a local inspector (ngrok's inspector, Chrome DevTools, Charles, Proxyman, and mitmproxy all show traffic verbatim). Captured URLs, headers, query params, and bodies (up to max_body_size) are now stored verbatim, in cleartext, in in-memory records with >64KB bodies spilling to .prox/capture / ~/.prox/capture (dirs 0700, spill files 0600 — now pinned by tests). The redact, redact_headers, and redact_query_params keys are gone; with this release's strict parsing a config still carrying them fails at load (proxy.capture: unknown field "redact") — delete the keys. Projects whose traffic must not be recorded should opt out entirely: proxy.capture.enabled: false or --no-capture.
  • Unknown keys anywhere in prox.yaml are load errors (#83, PR #86, plan 016). The lenient re-marshal parse path silently dropped typo'd fields under processes:/services: (and healthcheck:), unknown keys in the proxy:/proxy.capture:/api:/certs: blocks, and unknown top-level keys. Everything now parses strictly, matching the dependencies:/ tasks: precedent, with every structural error batched into one sorted report: invalid configuration: processes.web: unknown field "stop_timout"; .... Duplicate keys are rejected too (literal ones by the YAML parser with line numbers; alias-key duplicates and duplicates inside decoder-skipped regions by the new structural pass), a stray --- starting a second YAML document is an error, and self-referential alias cycles are rejected. YAML anchors and << merge keys remain fully supported — including merging into typed blocks with standard explicit-key-wins override semantics — but a merge can no longer smuggle an unknown key past a block's schema, and top-level anchor-container keys (x-defaults: style) are rejected: define anchors at their first natural occurrence (e.g. env: &common {...} on one process, env: *common on the next). String/int shorthand forms are unchanged.

Fixed

  • Two integration-test flakes (#82, PR #84, plan 014). The grandchild output-capture test polls the captured-output surface for the startup marker instead of sleeping a fixed 500ms (capture buffers are now goroutine-safe so polling is race-free); the detached-slow-dependency test replaces its timed 5s/4s window with a deterministic three-marker file barrier — the observation can no longer race convergence, and the subtest got ~4x faster. Both verified 20/20 under -race.

CI / Docs

  • mkdocs build --strict now gates every docs-touching PR (#82, PR #84). A build-only Docs PR Build workflow runs on pull_request for docs/**, mkdocs.yml, pyproject.toml, and uv.lock, with uv --locked so a stale lockfile fails instead of silently re-resolving, and an mkdocs validation: block promoting broken anchors/absolute links to build failures. (Path-filtered — deliberately not a required check.)
  • Configuration reference documents the unified strict-parsing rule, the supported anchor/merge idioms, and the cleartext capture posture.

v0.2.3

Capture-by-default (plan 012): a proxy-enabled project now records request/response headers and bodies through the proxy with no extra config, bounded by the disk-budget accounting from #69 and covered by capture-time header/query-param redaction. Bodies are captured verbatim, though — see the Breaking entry.

Dependencies, tasks, and process gating (plan 013, #76): prox can now wait on external resources, run one-shot setup commands, and gate a process's launch on either.

Added

  • dependencies:, tasks:, and processes.<name>.depends_on (#76, plan 013). dependencies: describes an external resource (a database, cache, or other service) with a readiness check (tcp/url/cmd, 30s total budget by default including any start: command's execution time, 1s poll interval) and an optional start: command that runs at most once — only when the initial check fails — to bring it up (daemonizing commands like docker compose up -d are the intended pattern; prox never tears down the external resource itself, though it does kill its own still-running start: helper on teardown). tasks: describes a run-to-completion command (a migration, a seed script) that runs once per prox up lifetime, gated on its own depends_on, with a 60s default run budget (0 = unlimited). A process's depends_on gates its launch on dependencies and/or tasks (never other processes): the process starts in a new waiting state while its targets resolve asynchronously in the background (prox up/prox up -d never block on this), then either launches normally or settles into a new terminal blocked state if a required target failed. prox restart on a running gated process/task re-resolves every target — against a fresh config reload — before touching the running instance (fail-before-stop). See the new Dependencies and Tasks guide and the configuration reference.
  • Status/exit-contract extension for dependencies and tasks (#76, plan 013). prox status gains a Dependencies: table (state, one-line check summary, last error) and decorates the STATUS column with waiting(x, y)/blocked(x) naming the unresolved/failed targets; a new Blocked: line mirrors the existing Crashed: line. The exit-1 contract extends to: any process left blocked, and any dependencies: entry in the terminal failed state (a warned dependency does not count — its dependents still run). Precedence for the primary stderr message is proxy-down > crashed > blocked > failed-dependency; all applicable lines still print regardless. GET /status gains a dependencies array; each process response gains kind ("task" when applicable), waiting_on, and blocked_on. The TUI colors waiting (amber), blocked (red, bold), and completed (gray), with an inline (waiting on: ...)/(blocked on: ...) annotation.

Changed

  • One-shot rc=0 caveat narrowed to plain processes (#76, plan 013). A bare processes: entry that exits 0 on its own is still marked crashed — that behavior (plan 011) is unchanged. A tasks: entry, by contrast, maps a natural exit 0 to the new dedicated completed terminal state, which does not trip prox status's exit-1 contract. Use tasks: rather than a plain process for anything meant to run once and exit cleanly.

Breaking

  • Capture is on by default whenever the proxy is enabled (plan 012). Previously a proxy-enabled project recorded only request/response metadata (method, URL, status, timing) unless --capture was passed; now it also captures headers and bodies by default, spilling large bodies to ~/.prox/capture under the shared disk-budget accounting (#69, 1GiB default across all projects on the daemon). To opt out, set proxy.capture.enabled: false in prox.yaml, or pass the new --no-capture flag for one run (--capture still exists for explicitness/compat but no longer does anything a default-on project doesn't already get; the two flags are mutually exclusive). Capture-time redaction (below) covers headers and URLs only — request/response bodies are captured verbatim, so a project whose bodies carry secrets (API keys, tokens, PII in JSON/form payloads) should opt out entirely rather than rely on redaction. Anything that assumes ~/.prox/capture stays empty (disk-space checks, backup exclusions, sandboxed CI with a tight tmpfs) should account for the new default. prox proxy status gained capture_available/capture_error (JSON) and a Capture: unavailable (<reason>) line (human output) for the case where the daemon's own capture manager failed to initialize — distinct from a project simply choosing capture off.

Added

  • Capture disk budget + record-group FIFO eviction (#69). proxy.capture.disk_budget (e.g. 512MB, 2GB) bounds the shared daemon's total spilled-body bytes across every registered project; unset defaults to 1GiB, and the daemon-wide effective bound is the minimum across every capture-enabled project's own budget (a project that leaves it unset contributes the default to that minimum). Once the bound is exceeded, the oldest record's spilled body files are evicted first (FIFO by record group).
  • O(1) ID index for the request ring (#71). RequestManager's Upsert/GetByID/cursor-anchoring lookups no longer linear-scan the ring, keeping prox requests and the TUI responsive as capture-by-default raises per-project request volume.
  • Capture-time redaction of sensitive headers and query params (plan 012 D4). On by default whenever a capture config exists (disable with redact: false); the built-in sets always redact the Authorization, Proxy-Authorization, Cookie, Set-Cookie, X-Api-Key, X-Auth-Token headers and the access_token, refresh_token, id_token, token, api_key, apikey, client_secret, code query params — including inside a Location/Referer redirect URL's query string — and are extendable per project via redact_headers/redact_query_params (these only add to the built-ins, never replace them). Limitation: redaction never touches request/response bodies — see the Breaking entry above.

v0.2.2

An exit-contract polish pass (plan 011, PR #77): prox status and prox stop/prox down exit codes now tell the whole truth (crashed children and wedged daemon teardowns exit 1), a crashed project's proxied routes converge in about one request instead of up to 30 seconds, and the repo gained a CLAUDE.md for agent sessions.

Upgrading: as with previous releases, the shared daemon requires an exact version match with its clients. After installing this release, stop the old daemon and restart every project: prox proxy stop --force, then prox up -d in each project — or run prox up in one project and let the idle-daemon auto-heal replace it. Scripts keying on prox status / prox stop exit codes: read the Breaking entries below.

Breaking

  • prox status exits 1 when any child process is crashed (#72). Previously prox status exited 0 while the table showed a crashed child, so scripts and coding agents keying on the exit code could miss a dead process entirely. It now exits non-zero whenever any process is in the crashed state: the table adds a Crashed: <name>[, ...] — check 'prox logs <name>'. line after the proxy line, and stderr carries Error: N process(es) crashed — unless the shared proxy is also down, in which case the proxy-down error takes stderr precedence while both signals still print (the JSON payload is unchanged — it already reports each process's status). The exit-0 contract is now exact: prox status exits 0 only when the supervisor query succeeded, no process is crashed, and any configured shared proxy is reachable — it does not assert every process is running or healthy (starting, stopping, stopped, and running-but-unhealthy all still exit 0). Note the supervisor marks any non-Stop-driven exit as crashed, including a one-shot child that exits 0 (a migration/seed step): such a project fails prox status until restarted, so one-shot helpers should live outside prox.yaml or expect a non-zero status. Scripts that need the old behavior must read prox status --json and inspect the per-process status they care about — not prox status || true, which would also mask discovery errors and an unreachable supervisor. This consolidates the status exit-code churn started in v0.2.1 (proxy-down → exit 1, #66) into one adoption window.
  • prox stop/prox down exit 1 when the daemon teardown wait times out (#73). Previously, once the process-stop verdict was clean, a prox stop (or prox down) that then timed out waiting (up to ~15s) for the daemon's own state/PID files to disappear still printed Stopped processes plus a Warning: the daemon is still finishing shutdown to stderr and exited 0. It now returns a shutdown incomplete: daemon still finishing after 15s error and exits 1, joining the same shutdown incomplete family as the existing survivors contract (v0.1.4, #36) — an unconfirmed daemon teardown is no longer treated as a clean stop. The existing stdout/stderr messages are unchanged, but stderr gains the CLI's standard Error: shutdown incomplete: … line. Scripts that assumed prox stop/prox down always exit 0 once processes are reported stopped must check the exit code (or parse stderr for the Warning: line) if they need to distinguish a fully torn down daemon from one still finishing up.

Lifecycle

  • Crashed projects' routes converge in about one request instead of up to 30s (#74). Previously, when a project crashed, the shared proxy kept serving its routes — returning 502 Backend unavailable — until the periodic stale-PID sweep noticed the dead owner, which could take up to 30 seconds. The daemon now also probes on demand: when a route's backend transport fails and the proxy returns a 502, it checks that route's owning prox up process off the request path and, if it is dead, reaps the registration right away, so subsequent requests get a clean 404 (or fall through to a shared-port neighbor) almost immediately. The probe is flap-safe — it keys on the owner's identity, not the backend's, so a merely restarting or flapping backend under a live prox up is never deregistered — and cheap under load: a single in-flight probe per project, rate-limited, with a trailing probe so a 502 that arrives right after the crash is never missed. The 30s sweep remains the backstop for cases with no live 502 to trigger on (backend-authored 502s, mid-stream aborts, and dead projects receiving no traffic).

Docs

  • Repo CLAUDE.md (#75): working conventions for agent-driven development — per-commit gate, plans convention, the port-15561 test-fixture pitfall, the daemon argv decoy, and the release convention.

v0.2.1

A hardening pass on lifecycle signals and the requests pipeline: prox up/prox status/prox stop now give trustworthy exit codes and error messages instead of silently degrading, the shared proxy daemon self-heals and isolates projects from each other, and captured request/response bodies decode more content types. Plus a refreshed agent skill and docs.

Upgrading: as with v0.2.0, the shared daemon requires an exact version match with its clients. After installing this release, stop the old daemon and restart every project: prox proxy stop --force, then prox up in each project — or just run prox up in one project and let the idle-daemon auto-heal (below) replace it for you.

Breaking

  • prox status exits 1 when the shared proxy is down (#66). Previously prox status only reflected the project supervisor's own process health, so a dead shared proxy daemon could go unnoticed behind a clean-looking status. It now probes the daemon and prints Proxy: DOWN — shared proxy daemon unreachable (proxied routes are dead). Check 'prox proxy status'., exiting non-zero, even when every child process is healthy. Scripts that treated prox status's exit code as pure process health must account for proxy health too.
  • Client commands no longer fall back to :5555 (#66). status, logs, stop, start, restart, down, attach, and requests previously dialed the compiled-in 127.0.0.1:5555 default when they couldn't discover a running instance from .prox/prox.state — silently talking to nothing, or the wrong daemon. They now error with remediation ("run from the project directory, or pass --addr") instead. Scripts invoking these commands from outside the project directory must pass --addr explicitly.
  • prox up -d exit code is now truthful (was always 0). The detach parent used to print prox started (pid N) and exit 0 immediately after forking, before the child had loaded its config, bound its ports, or registered its routes — so a child that died during startup still reported success. prox up -d now polls up to 15s for the child to become ready (state file + /health) before exiting 0; it exits 1 with a .prox/prox.log tail on early child death or a never-ready timeout (killing the child on timeout).
  • Orphan ledger format is a forward-incompatible envelope (#67). The supervisor's crash-recovery ledger (.prox/prox.children) is now {"boot_marker":"...","children":[...]} instead of a bare array, so a reboot can't let a PID/start-token collision reap an unrelated process group. A pre-upgrade prox binary cannot parse the new envelope — it fails to unmarshal and simply skips the reap (never unsafe, just less helpful) until every project has been restarted on this version. Downgrade from this version briefly loses crash-recovery reaping for the same reason.
  • New dependencies: github.com/klauspost/compress (zstd decoding) and github.com/andybalholm/brotli (brotli decoding), both pure Go.

Lifecycle

  • Version-skew hard fail with idle-daemon auto-heal. A shared proxy daemon running a different version than the connecting prox used to silently fall back to a proxy-less standalone start. Now a version mismatch is a typed error: if the daemon still has registered projects, prox up fails hard, naming both versions, every registered project directory, and the exact remediation; if the daemon is idle, prox up auto-replaces it with a fresh daemon of the current version and prints a one-line notice. Standalone proxy create/start failures are likewise now fatal when a proxy is configured (--no-proxy is the escape hatch) instead of warn-and-continue — no code path starts a project with a silently-disabled proxy anymore.
  • Registration recovers from a draining daemon. Registering during the shared daemon's brief shutdown grace used to be a fatal SHUTTING_DOWN error whose text said "retry" but nothing retried. prox up now polls for the old daemon to finish draining (up to 10s), starts a fresh daemon, and re-registers automatically; a second failure is still fatal.
  • Forwarder self-heal after daemon death (#66). When the shared daemon dies, every registered project used to reconnect silently forever with no path back. Projects now detect a prolonged (15s+) connection failure and re-register with a fresh or recovered daemon automatically (damped to at most once per 30s), worst case within ~45s — no prox proxy stop --force required. The daemon's re-registration path was also made idempotent for a live same-identity holder, closing a would-be REGISTRATION_CONFLICT loop. Healing is suppressed while a project is itself shutting down, and a version-mismatch failure surfaces as a status state rather than restarting a daemon that might still be in use.
  • Boot-marker guard for the orphan ledger (#67). See Breaking above — the ledger now discards itself across a reboot (and on Linux, when it predates this marker) instead of risking a PID/start-token collision.

Requests

  • prox status / GET /status surface shared-proxy health (#66). A new proxy block reports mode (shared/standalone/disabled), daemon_reachable, daemon_version, reconnect/drop/backfill counters, and heal_state. The CLI renders this as a Proxy: line (see Breaking).
  • Stale flag for stuck in-flight requests (#53). A request that has been in_flight for more than 5 minutes now also carries stale: true (stale? in the CLI/TUI) — the completion event may have been lost and the outcome is unknown. Computed at serve time; no protocol or storage change. Long-lived streams and large transfers can legitimately show stale? while still live.
  • Drop and degradation counters. The request stream's subscriber channels used to drop events silently on overflow. Dropped-event and backfill-failure counts are now tracked (atomics) and exposed through the proxy status block, so silent request loss becomes visible.
  • More captured-body content types decoded (#50). deflate (zlib with a raw-deflate fallback), zstd, and br (brotli) captured bodies now decode for display, joining gzip/x-gzip; all four are bounded by the same 10MB decode cap. Chained or unrecognized encodings, truncated captures, and corrupt streams still fall back to raw bytes.
  • Hex preview for binary bodies in the TUI (#50). The request detail view renders binary bodies as a bounded hexdump -C-style preview (first 256 bytes, offset + hex + ASCII gutter) instead of an inert [binary data] placeholder.
  • Cursor pagination for GET /proxy/requests (#50). A new before_id parameter pages strictly older than the given record, returning next_before_id (the oldest scanned record, so a fully-filtered page still advances) for the next page. An unknown, evicted, or out-of-scope cursor returns 410 with code CURSOR_GONE. prox requests (CLI) is unchanged — this is an API-only capability for now.
  • Per-project request rings and capture caps on the shared daemon (#49). The daemon's single global request ring and capture cap used to let one chatty project evict another project's records and bodies, and a per-project capture.max_body_size was silently ignored on the daemon. Each registered project now gets its own full-capacity ring and its max_body_size is enforced per request on the shared capture path — one project flooding traffic cannot affect another's history or capture quota.

Skill & Docs

  • Agent skill refreshed to post-hardening reality. skills/prox/references/api.md and SKILL.md now document 12-char request IDs, since/url_contains, in_flight/stale/captured_size/content_encoding/unavailable_reason, the before_id/next_before_id/CURSOR_GONE cursor, the proxy status block, the no-:5555-fallback rule, and the truthful prox up -d exit code / version-mismatch behavior.
  • README, architecture, and reference docs updated to match: the HTTP API table gains the /proxy/requests endpoints; docs/development/architecture.md documents the shared proxy daemon, the capture pipeline, and the forwarder bridge; docs/reference/api.md/cli.md/configuration.md document the proxy status block, stale, cursor pagination, the decoder list, and per-project max_body_size; docs/guides/shared-proxy.md documents self-healing and version-mismatch behavior.

v0.2.0

The requests/capture overhaul: the shared proxy daemon now captures request and response bodies per project, prox requests becomes an ngrok-style inspector, crashed generations self-heal, and the TUI gains request/log search. Plus a set of registration/lifecycle hardening fixes and supervisor orphan cleanup.

Upgrading: the daemon requires an exact version match with its clients, so after installing this release, stop the old daemon and restart every project: prox proxy stop --force, then prox up in each project (the version gate makes a mismatch loud).

Features

  • Body capture in the shared proxy daemon (#40, plan 005). Daemon mode now captures request/response bodies per project under ~/.prox/capture, gated by a per-project proxy.capture config (enabled, max_body_size). Records are scoped by project directory (not hostname), gzip/deflate bodies are decoded for display rather than corrupted, content_encoding/captured_size are recorded, binary bodies are detected integrity-first, and captured bodies are delivered to each project over the SSE bridge. Request IDs are now 12 hex chars.
  • prox requests as an inspector (plan 005). New agent- and human-friendly filters: --url <substr>, --since <5m|RFC3339>, --min-status/--max-status, --method, --subdomain, --json, and prox requests <id> --body to view captured bodies. Responses expose hostname, content_type, content_encoding, and unavailable_reason. Body titles show the content type and pretty-print JSON.
  • In-flight request visibility (#48, plan 006). Requests are recorded at response-header time with in_flight: true and completed via a defer, so a request is visible while it streams and an aborted stream still records instead of vanishing. Backed by a monotonic in-flight → final state machine (RequestManager.Upsert).
  • Snapshot backfill on (re)connect (#51, plan 006). A project's forwarder fetches the daemon's existing records concurrently with the live stream on every (re)connect, so reconnecting no longer loses history.
  • TUI requests view: explicit cursor + search (#47, plan 007). The requests view gains an ID-anchored cursor and /-search that jumps the cursor to matches with n/N navigation (wrapping), composing with the s filter rather than replacing it.
  • TUI detail live-refresh (#54, plan 007). An open request-detail view refreshes in place when the request completes, instead of going stale.
  • TUI logs view: search-match navigation (#56, plan 009). The logs view gains /-search that jumps the cursor to matching lines with n/N (wrapping) and match highlighting, mirroring the requests view. Note: logs / now navigates (non-matching lines stay visible) rather than filtering — use s for the live substring filter.

Fixes

  • Crash-restart registration self-heal (#55, plan 007). When a prox up crashes without deregistering, a restart of the same project now detects the dead registration and replaces it inline instead of failing with a 409, so a crashed generation recovers without prox proxy stop --force. Lifecycle transactions are serialized end-to-end, with a brief bind retry on every registration bind and an epoch-guarded graced shutdown check.
  • HTTPS certs for domains joining an existing listener (#58, plan 008). A project whose HTTPS port is already bound by another project now gets its domain's certificate generated, so the shared TLS listener's SNI callback can serve it (previously the joining domain's handshakes failed).
  • Forced-stop teardown serialized with lifecycle transactions (#60, plan 008). prox proxy stop --force now sets the shutdown flag before responding and drains any in-flight register/deregister under a barrier, so a concurrent registration can't interleave with physical teardown.
  • PID-reuse can no longer defeat liveness checks (#61, plans 008/009). Registrations carry an opaque per-host process start token; the stale-PID sweep and crash-restart self-heal key liveness on (pid, token) so a reused PID naming a different process reads as dead, and the sweep's removal guard is token-aware so it can't tear down a live restart that reused a crashed PID.
  • Supervisor: reap orphaned child groups after kill -9 (#59, plan 009). When a prox up is killed with kill -9, the backend process groups it supervised are orphaned and keep holding their ports. The supervisor now persists an ownership ledger and, on the next prox up, reaps any leftover group it can positively identify (strict start-token match) — so the restarted generation rebinds its ports instead of 502'ing on a wedged orphan.

Internal

  • Cert generation runs outside the cache lock (plan 009). EnsureDomain no longer holds the certificate cache lock across the mkcert subprocess and key load, so a joining domain's first-time generation no longer stalls TLS handshakes for other domains on the shared listener.
  • CI runs on macOS as well as Linux (plan 009). The test job now runs on both ubuntu-latest and macos-latest, exercising the darwin process start-token path on every change.

v0.1.4

Breaking

  • Foreground prox up now exits non-zero when a process group survives shutdown (#36). Previously foreground prox up (Ctrl-C or an API shutdown) always exited 0, even if a process group could not be reaped and still held its ports. It now exits 1 with a one-line shutdown incomplete: … summary (per-survivor detail is written to the log stream). Scripts that asserted a 0 exit from a foreground prox up regardless of outcome must be updated. A clean shutdown still exits 0.

Features

  • Full-stop failure contract (#36). prox stop (no arguments) and prox down now wait for the shutdown outcome instead of firing and forgetting. The daemon reports the process-stop verdict over a new POST /api/v1/shutdown?wait=true path, which responds HTTP 200 with {success, waited, failures[]} — 200 even when a group survived, so the structured survivor list (each carrying the stable PROCESS_GROUP_NOT_REAPED code) is not discarded. The CLI maps this to exit codes: 0 when everything stopped cleanly (after a brief bounded wait for the daemon's state/PID files to disappear), 1 when a process group survived (each printed as a process: error line), and 1 when the connection drops mid-wait and the outcome is unknown. An older daemon that predates the wait parameter is detected (the response omits waited) and the CLI falls back to the legacy Shutdown initiated message with exit 0. The daemon shutdown stages were reordered so the API server is stopped last (after the supervisor stage and the verdict publish), letting it deliver the waited response, with the launch gate closed first so a lifecycle request during the drain cannot orphan a process.

  • Concurrent stops return the same verdict (#32). Two Stop calls against the same process (or a daemon shutdown overlapping an in-flight per-process stop) now resolve to the same result: a secondary waiter joins the primary's stop episode and observes its authoritative verdict — including a PROCESS_GROUP_NOT_REAPED failure — instead of returning success early when the leader is reaped. A caller whose own context is canceled first still gets ctx.Err(). A reap failure now emits a process_crashed event uniformly from both the per-process stop and full-stop paths.

  • restart and start apply the current prox.yaml (#33). An API-driven (re)start — prox restart <name>, and prox start <name> after a prox stop <name> — now re-reads and validates the whole config file and runs the process with its current config, so the edit → restart → observe loop no longer needs a full prox stop + prox up. Applied on (re)start: cmd, healthcheck, stop_timeout (the new value governs the next stop; the restart's own stop half keeps the pre-edit budget), and environment inputs (inline env, per-process and global env_file, including changed file paths). Renames, added/removed processes, and services/proxy/port changes still require prox up. The reload is fail-closed: an invalid file (even an unrelated process or the proxy section), a missing referenced env file, or a removed target aborts the (re)start with the existing process left running unchanged, via two new error codes — CONFIG_RELOAD_FAILED (HTTP 422) and PROCESS_NOT_IN_CONFIG (HTTP 409). The config swap is applied atomically inside the start's locked critical section, so a start racing a restart never leaves the running process and stored config mismatched.

  • Configurable stop timeout (#35). Two new duration fields control the SIGTERM→SIGKILL escalation budget: global shutdown_timeout (top-level) and per-process stop_timeout (overrides the global). The effective budget for a process is its own stop_timeout, else the global shutdown_timeout, else the built-in 10s default. The value is the escalation window — a fixed 2s is reserved for the SIGKILL phase, so the graceful drain window is budget − 2s. Values must be greater than 2s and at most 10m; anything outside that range (including 0s/negatives) is rejected at load with a field-named error. The budget is honored end-to-end — prox stop, prox restart (the stop half), and full daemon shutdown — and the effective value is surfaced as stop_timeout in the GET /processes/{name} response.

Changed

  • Daemon shutdown now uses per-stage deadlines instead of one shared 10s budget (#35). Full prox stop / Ctrl-C previously wrapped proxy teardown, API-server shutdown, and all process stops in a single 10-second context, so slow proxy/API teardown silently ate into the time available to stop processes — truncating an otherwise-valid graceful drain. Teardown now runs in stages, each with its own deadline computed at shutdown time: the proxy and API server get short fixed deadlines, then every process is stopped concurrently, each on its own configured stop budget (read live, so a per-process budget raised at runtime is respected). With nothing configured, per-process escalation timing is unchanged (10s/2s); the daemon's outer window simply no longer truncates a stop.

Fixes

  • SSE streams are no longer cut at 30 seconds (#42). GET /api/v1/logs/stream and GET /api/v1/proxy/requests/stream sat in the router's default 30s request-timeout class, so prox logs -f, prox attach, and proxy-request streams silently terminated after ~30s. The two SSE routes are now exempt from the request timeout: streams are long-lived and end only on client disconnect or daemon shutdown. Shutdown now also closes the shared-proxy-daemon request forwarder's subscriber channels, so an attached request stream no longer pins the API server to its full teardown-stage deadline.
  • prox requests now discovers the daemon's API address (#43). requests was missing from the client-command discovery allowlist, so it always talked to the default :5555 address and failed against daemons on dynamic API ports (the default) — the same gap start had. The allowlist is now pinned by a test so a new client command can't silently miss discovery.
  • prox start <name> now discovers the daemon's API address from .prox/prox.state like the other client commands; previously it always used the default :5555 address and failed against daemons on dynamic API ports (found during #33 verification).
  • A second POST /shutdown no longer panics the daemon (#36). The shutdown trigger was a bare close(shutdownCh), so a duplicate or concurrent shutdown request (e.g. a rapid double prox stop) closed an already-closed channel and crashed the daemon. Shutdown is now latched through a sync.Once coordinator, so repeated triggers are safe no-ops.
  • POST /shutdown now works against a --tui daemon (#36). The TUI event loop never observed the shutdown channel, so an API shutdown (and therefore prox stop) was silently inert against prox up --tui — the request returned 200 but the daemon kept running. The trigger is now routed into the TUI so it quits and runs the normal shutdown sequence.
  • GET /api/v1/logs/stream now returns a clean JSON error (STREAMING_NOT_SUPPORTED) when the connection cannot stream, instead of writing SSE headers first (#40).
  • Healthcheck interval/timeout/retries/start_period are now honored (#31). Previously only healthcheck.cmd took effect; the timing/retry fields were silently dropped and replaced by the built-in defaults (10s/5s/3/ 30s), so a tuned healthcheck ran at the wrong cadence and a slow starter got no start_period grace. Configured values now reach the health checker. An invalid or negative duration fails prox up at load with a clear, process-named error (e.g. processes.api.healthcheck.interval: invalid duration "3x") instead of being silently ignored; 0/omitted still means "use the default".

v0.1.3

Bug-fix release for the prox restart/stop process lifecycle (#29): a restart now reloads the process's env_file, and neither stop nor restart leaves orphaned grandchild processes holding their ports.

Fixes

  • restart reloads env_file; stop/restart no longer orphan grandchildren (#29, #30). Previously prox restart could report success while running the replacement with a stale environment and leaving the old process's grandchildren alive — holding the listening port, so the replacement failed with EADDRINUSE — and prox stop could leave the same orphan behind. Now:
    • env_file (global + per-process) and inline env are re-read from disk on every start, so start/restart pick up edited values; a failed reload fails loudly instead of launching with a stale env.
    • stop/restart gate on the whole process group — SIGTERM, a time-based graceful wait, then SIGKILL of the group with reap verification — so a grandchild that ignores SIGTERM is still cleaned up and its port freed.
    • prox stop <name> / prox restart <name> now return a non-zero exit and a typed PROCESS_GROUP_NOT_REAPED error when a group can't be reaped, instead of always reporting success.
    • restart starts the replacement on the supervisor's context, so its health checker survives past the request that triggered it.

Documentation

  • Document the shared proxy daemon.

v0.1.2

Release-pipeline + plugin distribution release. Prox is now a distributable Claude Code plugin (this repo hosts the skill; previously it lived in charliek/cc-plugins), and the release pipeline is fully on the cc-plugins:release-workflows convention — no more per-pipeline PATs, single release-bot App identity for both the Homebrew tap push and the apt-charliek dispatch.

Features

  • Prox is now a distributable Claude Code plugin (#20). This repo is the canonical home for the prox skill. Removed from charliek/cc-plugins in a companion change.

Release process

  • Adopt cc-plugins:release-workflows convention (#21) — prox is the third consumer of the framework (after strix and roost). scripts/release/update-version.sh bumps .claude-plugin/plugin.json (delegating to the existing scripts/set-version.sh) with a grep-verify after the delegate so silent sed no-ops fail loudly; RELEASING.md documents the per-repo policy + break-glass recovery; multi-target sanity-check-app.yml verifies the App reach to homebrew-tap and apt-charliek; the previous CI-driven sync-version job is retired (plugin.json bump moves local).
  • Retire HOMEBREW_TAP_TOKEN and APT_DISPATCH_TOKEN PATs — both replaced by charliek-release-bot App tokens minted at workflow time (scoped to the target repo via actions/create-github-app-token's owner + repositories inputs). GoReleaser still reads HOMEBREW_TAP_TOKEN from env; the workflow now sets it from the App-minted token instead of from secrets. Legacy secrets deleted from the secret store.
  • Server-side Verify plugin.json matches tag safety net in release.yaml — catches mismatches between the released tag and what's actually in .claude-plugin/plugin.json before any artifacts ship. Replaces the deleted sync-version job's contract.
  • Branch protection ruleset on main with the release-bot App + admin role in bypass_actors.
  • update-version.sh is now grep-verifying — the wrapper around scripts/set-version.sh reads the file back and asserts the new version made it in. Silent sed no-ops on malformed manifests now fail loudly.

Docs

  • Document the Linux (apt) install path + a direct-.deb fallback (#18, #19) — apt install prox once the apt-charliek repo is added; apt install ./prox_X.Y.Z_amd64.deb for the no-apt-repo fallback (resolves dependencies, unlike dpkg -i).
  • RELEASING.md (new) — per-repo policy doc.

v0.1.1

Features

  • Publish .deb packages for amd64 and arm64 on every release via GoReleaser's nfpms: block. Install on Pop!_OS / Ubuntu 24.04+ via apt install prox once the apt-charliek repository is added.
  • Fire repository_dispatch at charliek/apt-charliek after a successful release so apt update picks up the new version automatically. Bounded retries on the dispatch call to ride out transient API blips.

Maintenance

  • Add release-snapshot CI job that runs goreleaser release --snapshot on every PR and validates both the amd64 and arm64 .deb artifacts (Package: prox, payload at /usr/local/bin/prox).

v0.1.0

Features

  • Add shared proxy daemon for multi-project port sharing
  • Add Homebrew tap automation via GoReleaser
  • Add Homebrew as recommended install method in README

Fixes

  • Fix WebSocket and SSE connections dying after 30s through proxy
  • Fix SSE/streaming support in reverse proxy
  • Make proxy port binding failures fatal with actionable errors

Maintenance

  • Upgrade deploy-pages to v5 for Node.js 24 support
  • Upgrade GitHub Actions to Node.js 24-compatible versions
  • Remove legacy plans and replaced watch-pr command
  • Remove release command, moved to cc-plugins

v0.0.3

Features

  • Add HTTP proxy support for dual-stack (HTTP + HTTPS) proxying
  • Add request/response body capture for proxy inspection

Improvements

  • Remove hosts and certs CLI commands, replaced with documentation
  • Upgrade golangci-lint to v2 with macOS support
  • Fix CI: upgrade golangci-lint-action to v7
  • Fix release workflow: upgrade golangci-lint-action to v7 for v2 support

Tests

  • Add comprehensive tests for request/response body capture

v0.0.2

Features

  • Add prox requests command to view and stream proxy HTTP requests
    • Filter by subdomain, HTTP method, and minimum status code
    • Stream in real-time with -f/--follow flag
    • JSON output support with --json flag
  • Add prox start <process> command to start stopped processes
  • Add prox stop <process> command to stop individual processes

Improvements

  • Add TTY detection to LogPrinter for clean output when piping
  • Add setcap to install target for privileged port binding
  • Case-insensitive HTTP method filtering (--method get works)

v0.0.1

Initial release of prox, a modern process manager for local development.

Features

  • Process supervision with automatic restarts and health checks
  • Real-time log aggregation with filtering and search
  • HTTPS reverse proxy with subdomain routing
  • Interactive TUI for monitoring processes and logs
  • Background daemon mode with --detach flag
  • CLI built with Cobra framework with shell completions
  • REST API for programmatic control